Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- lib/python3.12/site-packages/sympy/physics/mechanics/functions.py +735 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/inertia.py +197 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/lagrange.py +512 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/loads.py +177 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/method.py +39 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/pathway.py +688 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/__init__.py +0 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_inertia.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_kane5.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_method.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_rigidbody.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_system.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_actuator.py +851 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_body.py +340 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_functions.py +262 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_inertia.py +71 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_joint.py +1240 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_jointsmethod.py +249 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane.py +553 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane2.py +464 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane3.py +315 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane4.py +115 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane5.py +128 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_lagrange.py +247 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_lagrange2.py +46 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_linearize.py +372 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_loads.py +86 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_method.py +5 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_models.py +117 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_particle.py +78 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_pathway.py +691 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_rigidbody.py +184 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_system.py +245 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_system_class.py +831 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_wrapping_geometry.py +363 -0
- lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/anticommutator.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/boson.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/cartesian.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/cg.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/circuitplot.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/circuitutils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/commutator.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/constants.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/dagger.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/density.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/fermion.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/gate.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/grover.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/hilbert.cpython-312.pyc +0 -0
lib/python3.12/site-packages/sympy/physics/mechanics/functions.py
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy.utilities import dict_merge
|
| 2 |
+
from sympy.utilities.iterables import iterable
|
| 3 |
+
from sympy.physics.vector import (Dyadic, Vector, ReferenceFrame,
|
| 4 |
+
Point, dynamicsymbols)
|
| 5 |
+
from sympy.physics.vector.printing import (vprint, vsprint, vpprint, vlatex,
|
| 6 |
+
init_vprinting)
|
| 7 |
+
from sympy.physics.mechanics.particle import Particle
|
| 8 |
+
from sympy.physics.mechanics.rigidbody import RigidBody
|
| 9 |
+
from sympy.simplify.simplify import simplify
|
| 10 |
+
from sympy import Matrix, Mul, Derivative, sin, cos, tan, S
|
| 11 |
+
from sympy.core.function import AppliedUndef
|
| 12 |
+
from sympy.physics.mechanics.inertia import (inertia as _inertia,
|
| 13 |
+
inertia_of_point_mass as _inertia_of_point_mass)
|
| 14 |
+
from sympy.utilities.exceptions import sympy_deprecation_warning
|
| 15 |
+
|
| 16 |
+
__all__ = ['linear_momentum',
|
| 17 |
+
'angular_momentum',
|
| 18 |
+
'kinetic_energy',
|
| 19 |
+
'potential_energy',
|
| 20 |
+
'Lagrangian',
|
| 21 |
+
'mechanics_printing',
|
| 22 |
+
'mprint',
|
| 23 |
+
'msprint',
|
| 24 |
+
'mpprint',
|
| 25 |
+
'mlatex',
|
| 26 |
+
'msubs',
|
| 27 |
+
'find_dynamicsymbols']
|
| 28 |
+
|
| 29 |
+
# These are functions that we've moved and renamed during extracting the
|
| 30 |
+
# basic vector calculus code from the mechanics packages.
|
| 31 |
+
|
| 32 |
+
mprint = vprint
|
| 33 |
+
msprint = vsprint
|
| 34 |
+
mpprint = vpprint
|
| 35 |
+
mlatex = vlatex
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def mechanics_printing(**kwargs):
|
| 39 |
+
"""
|
| 40 |
+
Initializes time derivative printing for all SymPy objects in
|
| 41 |
+
mechanics module.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
init_vprinting(**kwargs)
|
| 45 |
+
|
| 46 |
+
mechanics_printing.__doc__ = init_vprinting.__doc__
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def inertia(frame, ixx, iyy, izz, ixy=0, iyz=0, izx=0):
|
| 50 |
+
sympy_deprecation_warning(
|
| 51 |
+
"""
|
| 52 |
+
The inertia function has been moved.
|
| 53 |
+
Import it from "sympy.physics.mechanics".
|
| 54 |
+
""",
|
| 55 |
+
deprecated_since_version="1.13",
|
| 56 |
+
active_deprecations_target="moved-mechanics-functions"
|
| 57 |
+
)
|
| 58 |
+
return _inertia(frame, ixx, iyy, izz, ixy, iyz, izx)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def inertia_of_point_mass(mass, pos_vec, frame):
|
| 62 |
+
sympy_deprecation_warning(
|
| 63 |
+
"""
|
| 64 |
+
The inertia_of_point_mass function has been moved.
|
| 65 |
+
Import it from "sympy.physics.mechanics".
|
| 66 |
+
""",
|
| 67 |
+
deprecated_since_version="1.13",
|
| 68 |
+
active_deprecations_target="moved-mechanics-functions"
|
| 69 |
+
)
|
| 70 |
+
return _inertia_of_point_mass(mass, pos_vec, frame)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def linear_momentum(frame, *body):
|
| 74 |
+
"""Linear momentum of the system.
|
| 75 |
+
|
| 76 |
+
Explanation
|
| 77 |
+
===========
|
| 78 |
+
|
| 79 |
+
This function returns the linear momentum of a system of Particle's and/or
|
| 80 |
+
RigidBody's. The linear momentum of a system is equal to the vector sum of
|
| 81 |
+
the linear momentum of its constituents. Consider a system, S, comprised of
|
| 82 |
+
a rigid body, A, and a particle, P. The linear momentum of the system, L,
|
| 83 |
+
is equal to the vector sum of the linear momentum of the particle, L1, and
|
| 84 |
+
the linear momentum of the rigid body, L2, i.e.
|
| 85 |
+
|
| 86 |
+
L = L1 + L2
|
| 87 |
+
|
| 88 |
+
Parameters
|
| 89 |
+
==========
|
| 90 |
+
|
| 91 |
+
frame : ReferenceFrame
|
| 92 |
+
The frame in which linear momentum is desired.
|
| 93 |
+
body1, body2, body3... : Particle and/or RigidBody
|
| 94 |
+
The body (or bodies) whose linear momentum is required.
|
| 95 |
+
|
| 96 |
+
Examples
|
| 97 |
+
========
|
| 98 |
+
|
| 99 |
+
>>> from sympy.physics.mechanics import Point, Particle, ReferenceFrame
|
| 100 |
+
>>> from sympy.physics.mechanics import RigidBody, outer, linear_momentum
|
| 101 |
+
>>> N = ReferenceFrame('N')
|
| 102 |
+
>>> P = Point('P')
|
| 103 |
+
>>> P.set_vel(N, 10 * N.x)
|
| 104 |
+
>>> Pa = Particle('Pa', P, 1)
|
| 105 |
+
>>> Ac = Point('Ac')
|
| 106 |
+
>>> Ac.set_vel(N, 25 * N.y)
|
| 107 |
+
>>> I = outer(N.x, N.x)
|
| 108 |
+
>>> A = RigidBody('A', Ac, N, 20, (I, Ac))
|
| 109 |
+
>>> linear_momentum(N, A, Pa)
|
| 110 |
+
10*N.x + 500*N.y
|
| 111 |
+
|
| 112 |
+
"""
|
| 113 |
+
|
| 114 |
+
if not isinstance(frame, ReferenceFrame):
|
| 115 |
+
raise TypeError('Please specify a valid ReferenceFrame')
|
| 116 |
+
else:
|
| 117 |
+
linear_momentum_sys = Vector(0)
|
| 118 |
+
for e in body:
|
| 119 |
+
if isinstance(e, (RigidBody, Particle)):
|
| 120 |
+
linear_momentum_sys += e.linear_momentum(frame)
|
| 121 |
+
else:
|
| 122 |
+
raise TypeError('*body must have only Particle or RigidBody')
|
| 123 |
+
return linear_momentum_sys
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def angular_momentum(point, frame, *body):
|
| 127 |
+
"""Angular momentum of a system.
|
| 128 |
+
|
| 129 |
+
Explanation
|
| 130 |
+
===========
|
| 131 |
+
|
| 132 |
+
This function returns the angular momentum of a system of Particle's and/or
|
| 133 |
+
RigidBody's. The angular momentum of such a system is equal to the vector
|
| 134 |
+
sum of the angular momentum of its constituents. Consider a system, S,
|
| 135 |
+
comprised of a rigid body, A, and a particle, P. The angular momentum of
|
| 136 |
+
the system, H, is equal to the vector sum of the angular momentum of the
|
| 137 |
+
particle, H1, and the angular momentum of the rigid body, H2, i.e.
|
| 138 |
+
|
| 139 |
+
H = H1 + H2
|
| 140 |
+
|
| 141 |
+
Parameters
|
| 142 |
+
==========
|
| 143 |
+
|
| 144 |
+
point : Point
|
| 145 |
+
The point about which angular momentum of the system is desired.
|
| 146 |
+
frame : ReferenceFrame
|
| 147 |
+
The frame in which angular momentum is desired.
|
| 148 |
+
body1, body2, body3... : Particle and/or RigidBody
|
| 149 |
+
The body (or bodies) whose angular momentum is required.
|
| 150 |
+
|
| 151 |
+
Examples
|
| 152 |
+
========
|
| 153 |
+
|
| 154 |
+
>>> from sympy.physics.mechanics import Point, Particle, ReferenceFrame
|
| 155 |
+
>>> from sympy.physics.mechanics import RigidBody, outer, angular_momentum
|
| 156 |
+
>>> N = ReferenceFrame('N')
|
| 157 |
+
>>> O = Point('O')
|
| 158 |
+
>>> O.set_vel(N, 0 * N.x)
|
| 159 |
+
>>> P = O.locatenew('P', 1 * N.x)
|
| 160 |
+
>>> P.set_vel(N, 10 * N.x)
|
| 161 |
+
>>> Pa = Particle('Pa', P, 1)
|
| 162 |
+
>>> Ac = O.locatenew('Ac', 2 * N.y)
|
| 163 |
+
>>> Ac.set_vel(N, 5 * N.y)
|
| 164 |
+
>>> a = ReferenceFrame('a')
|
| 165 |
+
>>> a.set_ang_vel(N, 10 * N.z)
|
| 166 |
+
>>> I = outer(N.z, N.z)
|
| 167 |
+
>>> A = RigidBody('A', Ac, a, 20, (I, Ac))
|
| 168 |
+
>>> angular_momentum(O, N, Pa, A)
|
| 169 |
+
10*N.z
|
| 170 |
+
|
| 171 |
+
"""
|
| 172 |
+
|
| 173 |
+
if not isinstance(frame, ReferenceFrame):
|
| 174 |
+
raise TypeError('Please enter a valid ReferenceFrame')
|
| 175 |
+
if not isinstance(point, Point):
|
| 176 |
+
raise TypeError('Please specify a valid Point')
|
| 177 |
+
else:
|
| 178 |
+
angular_momentum_sys = Vector(0)
|
| 179 |
+
for e in body:
|
| 180 |
+
if isinstance(e, (RigidBody, Particle)):
|
| 181 |
+
angular_momentum_sys += e.angular_momentum(point, frame)
|
| 182 |
+
else:
|
| 183 |
+
raise TypeError('*body must have only Particle or RigidBody')
|
| 184 |
+
return angular_momentum_sys
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def kinetic_energy(frame, *body):
|
| 188 |
+
"""Kinetic energy of a multibody system.
|
| 189 |
+
|
| 190 |
+
Explanation
|
| 191 |
+
===========
|
| 192 |
+
|
| 193 |
+
This function returns the kinetic energy of a system of Particle's and/or
|
| 194 |
+
RigidBody's. The kinetic energy of such a system is equal to the sum of
|
| 195 |
+
the kinetic energies of its constituents. Consider a system, S, comprising
|
| 196 |
+
a rigid body, A, and a particle, P. The kinetic energy of the system, T,
|
| 197 |
+
is equal to the vector sum of the kinetic energy of the particle, T1, and
|
| 198 |
+
the kinetic energy of the rigid body, T2, i.e.
|
| 199 |
+
|
| 200 |
+
T = T1 + T2
|
| 201 |
+
|
| 202 |
+
Kinetic energy is a scalar.
|
| 203 |
+
|
| 204 |
+
Parameters
|
| 205 |
+
==========
|
| 206 |
+
|
| 207 |
+
frame : ReferenceFrame
|
| 208 |
+
The frame in which the velocity or angular velocity of the body is
|
| 209 |
+
defined.
|
| 210 |
+
body1, body2, body3... : Particle and/or RigidBody
|
| 211 |
+
The body (or bodies) whose kinetic energy is required.
|
| 212 |
+
|
| 213 |
+
Examples
|
| 214 |
+
========
|
| 215 |
+
|
| 216 |
+
>>> from sympy.physics.mechanics import Point, Particle, ReferenceFrame
|
| 217 |
+
>>> from sympy.physics.mechanics import RigidBody, outer, kinetic_energy
|
| 218 |
+
>>> N = ReferenceFrame('N')
|
| 219 |
+
>>> O = Point('O')
|
| 220 |
+
>>> O.set_vel(N, 0 * N.x)
|
| 221 |
+
>>> P = O.locatenew('P', 1 * N.x)
|
| 222 |
+
>>> P.set_vel(N, 10 * N.x)
|
| 223 |
+
>>> Pa = Particle('Pa', P, 1)
|
| 224 |
+
>>> Ac = O.locatenew('Ac', 2 * N.y)
|
| 225 |
+
>>> Ac.set_vel(N, 5 * N.y)
|
| 226 |
+
>>> a = ReferenceFrame('a')
|
| 227 |
+
>>> a.set_ang_vel(N, 10 * N.z)
|
| 228 |
+
>>> I = outer(N.z, N.z)
|
| 229 |
+
>>> A = RigidBody('A', Ac, a, 20, (I, Ac))
|
| 230 |
+
>>> kinetic_energy(N, Pa, A)
|
| 231 |
+
350
|
| 232 |
+
|
| 233 |
+
"""
|
| 234 |
+
|
| 235 |
+
if not isinstance(frame, ReferenceFrame):
|
| 236 |
+
raise TypeError('Please enter a valid ReferenceFrame')
|
| 237 |
+
ke_sys = S.Zero
|
| 238 |
+
for e in body:
|
| 239 |
+
if isinstance(e, (RigidBody, Particle)):
|
| 240 |
+
ke_sys += e.kinetic_energy(frame)
|
| 241 |
+
else:
|
| 242 |
+
raise TypeError('*body must have only Particle or RigidBody')
|
| 243 |
+
return ke_sys
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def potential_energy(*body):
|
| 247 |
+
"""Potential energy of a multibody system.
|
| 248 |
+
|
| 249 |
+
Explanation
|
| 250 |
+
===========
|
| 251 |
+
|
| 252 |
+
This function returns the potential energy of a system of Particle's and/or
|
| 253 |
+
RigidBody's. The potential energy of such a system is equal to the sum of
|
| 254 |
+
the potential energy of its constituents. Consider a system, S, comprising
|
| 255 |
+
a rigid body, A, and a particle, P. The potential energy of the system, V,
|
| 256 |
+
is equal to the vector sum of the potential energy of the particle, V1, and
|
| 257 |
+
the potential energy of the rigid body, V2, i.e.
|
| 258 |
+
|
| 259 |
+
V = V1 + V2
|
| 260 |
+
|
| 261 |
+
Potential energy is a scalar.
|
| 262 |
+
|
| 263 |
+
Parameters
|
| 264 |
+
==========
|
| 265 |
+
|
| 266 |
+
body1, body2, body3... : Particle and/or RigidBody
|
| 267 |
+
The body (or bodies) whose potential energy is required.
|
| 268 |
+
|
| 269 |
+
Examples
|
| 270 |
+
========
|
| 271 |
+
|
| 272 |
+
>>> from sympy.physics.mechanics import Point, Particle, ReferenceFrame
|
| 273 |
+
>>> from sympy.physics.mechanics import RigidBody, outer, potential_energy
|
| 274 |
+
>>> from sympy import symbols
|
| 275 |
+
>>> M, m, g, h = symbols('M m g h')
|
| 276 |
+
>>> N = ReferenceFrame('N')
|
| 277 |
+
>>> O = Point('O')
|
| 278 |
+
>>> O.set_vel(N, 0 * N.x)
|
| 279 |
+
>>> P = O.locatenew('P', 1 * N.x)
|
| 280 |
+
>>> Pa = Particle('Pa', P, m)
|
| 281 |
+
>>> Ac = O.locatenew('Ac', 2 * N.y)
|
| 282 |
+
>>> a = ReferenceFrame('a')
|
| 283 |
+
>>> I = outer(N.z, N.z)
|
| 284 |
+
>>> A = RigidBody('A', Ac, a, M, (I, Ac))
|
| 285 |
+
>>> Pa.potential_energy = m * g * h
|
| 286 |
+
>>> A.potential_energy = M * g * h
|
| 287 |
+
>>> potential_energy(Pa, A)
|
| 288 |
+
M*g*h + g*h*m
|
| 289 |
+
|
| 290 |
+
"""
|
| 291 |
+
|
| 292 |
+
pe_sys = S.Zero
|
| 293 |
+
for e in body:
|
| 294 |
+
if isinstance(e, (RigidBody, Particle)):
|
| 295 |
+
pe_sys += e.potential_energy
|
| 296 |
+
else:
|
| 297 |
+
raise TypeError('*body must have only Particle or RigidBody')
|
| 298 |
+
return pe_sys
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def gravity(acceleration, *bodies):
|
| 302 |
+
from sympy.physics.mechanics.loads import gravity as _gravity
|
| 303 |
+
sympy_deprecation_warning(
|
| 304 |
+
"""
|
| 305 |
+
The gravity function has been moved.
|
| 306 |
+
Import it from "sympy.physics.mechanics.loads".
|
| 307 |
+
""",
|
| 308 |
+
deprecated_since_version="1.13",
|
| 309 |
+
active_deprecations_target="moved-mechanics-functions"
|
| 310 |
+
)
|
| 311 |
+
return _gravity(acceleration, *bodies)
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def center_of_mass(point, *bodies):
|
| 315 |
+
"""
|
| 316 |
+
Returns the position vector from the given point to the center of mass
|
| 317 |
+
of the given bodies(particles or rigidbodies).
|
| 318 |
+
|
| 319 |
+
Example
|
| 320 |
+
=======
|
| 321 |
+
|
| 322 |
+
>>> from sympy import symbols, S
|
| 323 |
+
>>> from sympy.physics.vector import Point
|
| 324 |
+
>>> from sympy.physics.mechanics import Particle, ReferenceFrame, RigidBody, outer
|
| 325 |
+
>>> from sympy.physics.mechanics.functions import center_of_mass
|
| 326 |
+
>>> a = ReferenceFrame('a')
|
| 327 |
+
>>> m = symbols('m', real=True)
|
| 328 |
+
>>> p1 = Particle('p1', Point('p1_pt'), S(1))
|
| 329 |
+
>>> p2 = Particle('p2', Point('p2_pt'), S(2))
|
| 330 |
+
>>> p3 = Particle('p3', Point('p3_pt'), S(3))
|
| 331 |
+
>>> p4 = Particle('p4', Point('p4_pt'), m)
|
| 332 |
+
>>> b_f = ReferenceFrame('b_f')
|
| 333 |
+
>>> b_cm = Point('b_cm')
|
| 334 |
+
>>> mb = symbols('mb')
|
| 335 |
+
>>> b = RigidBody('b', b_cm, b_f, mb, (outer(b_f.x, b_f.x), b_cm))
|
| 336 |
+
>>> p2.point.set_pos(p1.point, a.x)
|
| 337 |
+
>>> p3.point.set_pos(p1.point, a.x + a.y)
|
| 338 |
+
>>> p4.point.set_pos(p1.point, a.y)
|
| 339 |
+
>>> b.masscenter.set_pos(p1.point, a.y + a.z)
|
| 340 |
+
>>> point_o=Point('o')
|
| 341 |
+
>>> point_o.set_pos(p1.point, center_of_mass(p1.point, p1, p2, p3, p4, b))
|
| 342 |
+
>>> expr = 5/(m + mb + 6)*a.x + (m + mb + 3)/(m + mb + 6)*a.y + mb/(m + mb + 6)*a.z
|
| 343 |
+
>>> point_o.pos_from(p1.point)
|
| 344 |
+
5/(m + mb + 6)*a.x + (m + mb + 3)/(m + mb + 6)*a.y + mb/(m + mb + 6)*a.z
|
| 345 |
+
|
| 346 |
+
"""
|
| 347 |
+
if not bodies:
|
| 348 |
+
raise TypeError("No bodies(instances of Particle or Rigidbody) were passed.")
|
| 349 |
+
|
| 350 |
+
total_mass = 0
|
| 351 |
+
vec = Vector(0)
|
| 352 |
+
for i in bodies:
|
| 353 |
+
total_mass += i.mass
|
| 354 |
+
|
| 355 |
+
masscenter = getattr(i, 'masscenter', None)
|
| 356 |
+
if masscenter is None:
|
| 357 |
+
masscenter = i.point
|
| 358 |
+
vec += i.mass*masscenter.pos_from(point)
|
| 359 |
+
|
| 360 |
+
return vec/total_mass
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def Lagrangian(frame, *body):
|
| 364 |
+
"""Lagrangian of a multibody system.
|
| 365 |
+
|
| 366 |
+
Explanation
|
| 367 |
+
===========
|
| 368 |
+
|
| 369 |
+
This function returns the Lagrangian of a system of Particle's and/or
|
| 370 |
+
RigidBody's. The Lagrangian of such a system is equal to the difference
|
| 371 |
+
between the kinetic energies and potential energies of its constituents. If
|
| 372 |
+
T and V are the kinetic and potential energies of a system then it's
|
| 373 |
+
Lagrangian, L, is defined as
|
| 374 |
+
|
| 375 |
+
L = T - V
|
| 376 |
+
|
| 377 |
+
The Lagrangian is a scalar.
|
| 378 |
+
|
| 379 |
+
Parameters
|
| 380 |
+
==========
|
| 381 |
+
|
| 382 |
+
frame : ReferenceFrame
|
| 383 |
+
The frame in which the velocity or angular velocity of the body is
|
| 384 |
+
defined to determine the kinetic energy.
|
| 385 |
+
|
| 386 |
+
body1, body2, body3... : Particle and/or RigidBody
|
| 387 |
+
The body (or bodies) whose Lagrangian is required.
|
| 388 |
+
|
| 389 |
+
Examples
|
| 390 |
+
========
|
| 391 |
+
|
| 392 |
+
>>> from sympy.physics.mechanics import Point, Particle, ReferenceFrame
|
| 393 |
+
>>> from sympy.physics.mechanics import RigidBody, outer, Lagrangian
|
| 394 |
+
>>> from sympy import symbols
|
| 395 |
+
>>> M, m, g, h = symbols('M m g h')
|
| 396 |
+
>>> N = ReferenceFrame('N')
|
| 397 |
+
>>> O = Point('O')
|
| 398 |
+
>>> O.set_vel(N, 0 * N.x)
|
| 399 |
+
>>> P = O.locatenew('P', 1 * N.x)
|
| 400 |
+
>>> P.set_vel(N, 10 * N.x)
|
| 401 |
+
>>> Pa = Particle('Pa', P, 1)
|
| 402 |
+
>>> Ac = O.locatenew('Ac', 2 * N.y)
|
| 403 |
+
>>> Ac.set_vel(N, 5 * N.y)
|
| 404 |
+
>>> a = ReferenceFrame('a')
|
| 405 |
+
>>> a.set_ang_vel(N, 10 * N.z)
|
| 406 |
+
>>> I = outer(N.z, N.z)
|
| 407 |
+
>>> A = RigidBody('A', Ac, a, 20, (I, Ac))
|
| 408 |
+
>>> Pa.potential_energy = m * g * h
|
| 409 |
+
>>> A.potential_energy = M * g * h
|
| 410 |
+
>>> Lagrangian(N, Pa, A)
|
| 411 |
+
-M*g*h - g*h*m + 350
|
| 412 |
+
|
| 413 |
+
"""
|
| 414 |
+
|
| 415 |
+
if not isinstance(frame, ReferenceFrame):
|
| 416 |
+
raise TypeError('Please supply a valid ReferenceFrame')
|
| 417 |
+
for e in body:
|
| 418 |
+
if not isinstance(e, (RigidBody, Particle)):
|
| 419 |
+
raise TypeError('*body must have only Particle or RigidBody')
|
| 420 |
+
return kinetic_energy(frame, *body) - potential_energy(*body)
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def find_dynamicsymbols(expression, exclude=None, reference_frame=None):
|
| 424 |
+
"""Find all dynamicsymbols in expression.
|
| 425 |
+
|
| 426 |
+
Explanation
|
| 427 |
+
===========
|
| 428 |
+
|
| 429 |
+
If the optional ``exclude`` kwarg is used, only dynamicsymbols
|
| 430 |
+
not in the iterable ``exclude`` are returned.
|
| 431 |
+
If we intend to apply this function on a vector, the optional
|
| 432 |
+
``reference_frame`` is also used to inform about the corresponding frame
|
| 433 |
+
with respect to which the dynamic symbols of the given vector is to be
|
| 434 |
+
determined.
|
| 435 |
+
|
| 436 |
+
Parameters
|
| 437 |
+
==========
|
| 438 |
+
|
| 439 |
+
expression : SymPy expression
|
| 440 |
+
|
| 441 |
+
exclude : iterable of dynamicsymbols, optional
|
| 442 |
+
|
| 443 |
+
reference_frame : ReferenceFrame, optional
|
| 444 |
+
The frame with respect to which the dynamic symbols of the
|
| 445 |
+
given vector is to be determined.
|
| 446 |
+
|
| 447 |
+
Examples
|
| 448 |
+
========
|
| 449 |
+
|
| 450 |
+
>>> from sympy.physics.mechanics import dynamicsymbols, find_dynamicsymbols
|
| 451 |
+
>>> from sympy.physics.mechanics import ReferenceFrame
|
| 452 |
+
>>> x, y = dynamicsymbols('x, y')
|
| 453 |
+
>>> expr = x + x.diff()*y
|
| 454 |
+
>>> find_dynamicsymbols(expr)
|
| 455 |
+
{x(t), y(t), Derivative(x(t), t)}
|
| 456 |
+
>>> find_dynamicsymbols(expr, exclude=[x, y])
|
| 457 |
+
{Derivative(x(t), t)}
|
| 458 |
+
>>> a, b, c = dynamicsymbols('a, b, c')
|
| 459 |
+
>>> A = ReferenceFrame('A')
|
| 460 |
+
>>> v = a * A.x + b * A.y + c * A.z
|
| 461 |
+
>>> find_dynamicsymbols(v, reference_frame=A)
|
| 462 |
+
{a(t), b(t), c(t)}
|
| 463 |
+
|
| 464 |
+
"""
|
| 465 |
+
t_set = {dynamicsymbols._t}
|
| 466 |
+
if exclude:
|
| 467 |
+
if iterable(exclude):
|
| 468 |
+
exclude_set = set(exclude)
|
| 469 |
+
else:
|
| 470 |
+
raise TypeError("exclude kwarg must be iterable")
|
| 471 |
+
else:
|
| 472 |
+
exclude_set = set()
|
| 473 |
+
if isinstance(expression, Vector):
|
| 474 |
+
if reference_frame is None:
|
| 475 |
+
raise ValueError("You must provide reference_frame when passing a "
|
| 476 |
+
"vector expression, got %s." % reference_frame)
|
| 477 |
+
else:
|
| 478 |
+
expression = expression.to_matrix(reference_frame)
|
| 479 |
+
return {i for i in expression.atoms(AppliedUndef, Derivative) if
|
| 480 |
+
i.free_symbols == t_set} - exclude_set
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
def msubs(expr, *sub_dicts, smart=False, **kwargs):
|
| 484 |
+
"""A custom subs for use on expressions derived in physics.mechanics.
|
| 485 |
+
|
| 486 |
+
Traverses the expression tree once, performing the subs found in sub_dicts.
|
| 487 |
+
Terms inside ``Derivative`` expressions are ignored:
|
| 488 |
+
|
| 489 |
+
Examples
|
| 490 |
+
========
|
| 491 |
+
|
| 492 |
+
>>> from sympy.physics.mechanics import dynamicsymbols, msubs
|
| 493 |
+
>>> x = dynamicsymbols('x')
|
| 494 |
+
>>> msubs(x.diff() + x, {x: 1})
|
| 495 |
+
Derivative(x(t), t) + 1
|
| 496 |
+
|
| 497 |
+
Note that sub_dicts can be a single dictionary, or several dictionaries:
|
| 498 |
+
|
| 499 |
+
>>> x, y, z = dynamicsymbols('x, y, z')
|
| 500 |
+
>>> sub1 = {x: 1, y: 2}
|
| 501 |
+
>>> sub2 = {z: 3, x.diff(): 4}
|
| 502 |
+
>>> msubs(x.diff() + x + y + z, sub1, sub2)
|
| 503 |
+
10
|
| 504 |
+
|
| 505 |
+
If smart=True (default False), also checks for conditions that may result
|
| 506 |
+
in ``nan``, but if simplified would yield a valid expression. For example:
|
| 507 |
+
|
| 508 |
+
>>> from sympy import sin, tan
|
| 509 |
+
>>> (sin(x)/tan(x)).subs(x, 0)
|
| 510 |
+
nan
|
| 511 |
+
>>> msubs(sin(x)/tan(x), {x: 0}, smart=True)
|
| 512 |
+
1
|
| 513 |
+
|
| 514 |
+
It does this by first replacing all ``tan`` with ``sin/cos``. Then each
|
| 515 |
+
node is traversed. If the node is a fraction, subs is first evaluated on
|
| 516 |
+
the denominator. If this results in 0, simplification of the entire
|
| 517 |
+
fraction is attempted. Using this selective simplification, only
|
| 518 |
+
subexpressions that result in 1/0 are targeted, resulting in faster
|
| 519 |
+
performance.
|
| 520 |
+
|
| 521 |
+
"""
|
| 522 |
+
|
| 523 |
+
sub_dict = dict_merge(*sub_dicts)
|
| 524 |
+
if smart:
|
| 525 |
+
func = _smart_subs
|
| 526 |
+
elif hasattr(expr, 'msubs'):
|
| 527 |
+
return expr.msubs(sub_dict)
|
| 528 |
+
else:
|
| 529 |
+
func = lambda expr, sub_dict: _crawl(expr, _sub_func, sub_dict)
|
| 530 |
+
if isinstance(expr, (Matrix, Vector, Dyadic)):
|
| 531 |
+
return expr.applyfunc(lambda x: func(x, sub_dict))
|
| 532 |
+
else:
|
| 533 |
+
return func(expr, sub_dict)
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
def _crawl(expr, func, *args, **kwargs):
|
| 537 |
+
"""Crawl the expression tree, and apply func to every node."""
|
| 538 |
+
val = func(expr, *args, **kwargs)
|
| 539 |
+
if val is not None:
|
| 540 |
+
return val
|
| 541 |
+
new_args = (_crawl(arg, func, *args, **kwargs) for arg in expr.args)
|
| 542 |
+
return expr.func(*new_args)
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
def _sub_func(expr, sub_dict):
|
| 546 |
+
"""Perform direct matching substitution, ignoring derivatives."""
|
| 547 |
+
if expr in sub_dict:
|
| 548 |
+
return sub_dict[expr]
|
| 549 |
+
elif not expr.args or expr.is_Derivative:
|
| 550 |
+
return expr
|
| 551 |
+
|
| 552 |
+
|
| 553 |
+
def _tan_repl_func(expr):
|
| 554 |
+
"""Replace tan with sin/cos."""
|
| 555 |
+
if isinstance(expr, tan):
|
| 556 |
+
return sin(*expr.args) / cos(*expr.args)
|
| 557 |
+
elif not expr.args or expr.is_Derivative:
|
| 558 |
+
return expr
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
def _smart_subs(expr, sub_dict):
|
| 562 |
+
"""Performs subs, checking for conditions that may result in `nan` or
|
| 563 |
+
`oo`, and attempts to simplify them out.
|
| 564 |
+
|
| 565 |
+
The expression tree is traversed twice, and the following steps are
|
| 566 |
+
performed on each expression node:
|
| 567 |
+
- First traverse:
|
| 568 |
+
Replace all `tan` with `sin/cos`.
|
| 569 |
+
- Second traverse:
|
| 570 |
+
If node is a fraction, check if the denominator evaluates to 0.
|
| 571 |
+
If so, attempt to simplify it out. Then if node is in sub_dict,
|
| 572 |
+
sub in the corresponding value.
|
| 573 |
+
|
| 574 |
+
"""
|
| 575 |
+
expr = _crawl(expr, _tan_repl_func)
|
| 576 |
+
|
| 577 |
+
def _recurser(expr, sub_dict):
|
| 578 |
+
# Decompose the expression into num, den
|
| 579 |
+
num, den = _fraction_decomp(expr)
|
| 580 |
+
if den != 1:
|
| 581 |
+
# If there is a non trivial denominator, we need to handle it
|
| 582 |
+
denom_subbed = _recurser(den, sub_dict)
|
| 583 |
+
if denom_subbed.evalf() == 0:
|
| 584 |
+
# If denom is 0 after this, attempt to simplify the bad expr
|
| 585 |
+
expr = simplify(expr)
|
| 586 |
+
else:
|
| 587 |
+
# Expression won't result in nan, find numerator
|
| 588 |
+
num_subbed = _recurser(num, sub_dict)
|
| 589 |
+
return num_subbed / denom_subbed
|
| 590 |
+
# We have to crawl the tree manually, because `expr` may have been
|
| 591 |
+
# modified in the simplify step. First, perform subs as normal:
|
| 592 |
+
val = _sub_func(expr, sub_dict)
|
| 593 |
+
if val is not None:
|
| 594 |
+
return val
|
| 595 |
+
new_args = (_recurser(arg, sub_dict) for arg in expr.args)
|
| 596 |
+
return expr.func(*new_args)
|
| 597 |
+
return _recurser(expr, sub_dict)
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
def _fraction_decomp(expr):
|
| 601 |
+
"""Return num, den such that expr = num/den."""
|
| 602 |
+
if not isinstance(expr, Mul):
|
| 603 |
+
return expr, 1
|
| 604 |
+
num = []
|
| 605 |
+
den = []
|
| 606 |
+
for a in expr.args:
|
| 607 |
+
if a.is_Pow and a.args[1] < 0:
|
| 608 |
+
den.append(1 / a)
|
| 609 |
+
else:
|
| 610 |
+
num.append(a)
|
| 611 |
+
if not den:
|
| 612 |
+
return expr, 1
|
| 613 |
+
num = Mul(*num)
|
| 614 |
+
den = Mul(*den)
|
| 615 |
+
return num, den
|
| 616 |
+
|
| 617 |
+
|
| 618 |
+
def _f_list_parser(fl, ref_frame):
|
| 619 |
+
"""Parses the provided forcelist composed of items
|
| 620 |
+
of the form (obj, force).
|
| 621 |
+
Returns a tuple containing:
|
| 622 |
+
vel_list: The velocity (ang_vel for Frames, vel for Points) in
|
| 623 |
+
the provided reference frame.
|
| 624 |
+
f_list: The forces.
|
| 625 |
+
|
| 626 |
+
Used internally in the KanesMethod and LagrangesMethod classes.
|
| 627 |
+
|
| 628 |
+
"""
|
| 629 |
+
def flist_iter():
|
| 630 |
+
for pair in fl:
|
| 631 |
+
obj, force = pair
|
| 632 |
+
if isinstance(obj, ReferenceFrame):
|
| 633 |
+
yield obj.ang_vel_in(ref_frame), force
|
| 634 |
+
elif isinstance(obj, Point):
|
| 635 |
+
yield obj.vel(ref_frame), force
|
| 636 |
+
else:
|
| 637 |
+
raise TypeError('First entry in each forcelist pair must '
|
| 638 |
+
'be a point or frame.')
|
| 639 |
+
|
| 640 |
+
if not fl:
|
| 641 |
+
vel_list, f_list = (), ()
|
| 642 |
+
else:
|
| 643 |
+
unzip = lambda l: list(zip(*l)) if l[0] else [(), ()]
|
| 644 |
+
vel_list, f_list = unzip(list(flist_iter()))
|
| 645 |
+
return vel_list, f_list
|
| 646 |
+
|
| 647 |
+
|
| 648 |
+
def _validate_coordinates(coordinates=None, speeds=None, check_duplicates=True,
|
| 649 |
+
is_dynamicsymbols=True, u_auxiliary=None):
|
| 650 |
+
"""Validate the generalized coordinates and generalized speeds.
|
| 651 |
+
|
| 652 |
+
Parameters
|
| 653 |
+
==========
|
| 654 |
+
coordinates : iterable, optional
|
| 655 |
+
Generalized coordinates to be validated.
|
| 656 |
+
speeds : iterable, optional
|
| 657 |
+
Generalized speeds to be validated.
|
| 658 |
+
check_duplicates : bool, optional
|
| 659 |
+
Checks if there are duplicates in the generalized coordinates and
|
| 660 |
+
generalized speeds. If so it will raise a ValueError. The default is
|
| 661 |
+
True.
|
| 662 |
+
is_dynamicsymbols : iterable, optional
|
| 663 |
+
Checks if all the generalized coordinates and generalized speeds are
|
| 664 |
+
dynamicsymbols. If any is not a dynamicsymbol, a ValueError will be
|
| 665 |
+
raised. The default is True.
|
| 666 |
+
u_auxiliary : iterable, optional
|
| 667 |
+
Auxiliary generalized speeds to be validated.
|
| 668 |
+
|
| 669 |
+
"""
|
| 670 |
+
t_set = {dynamicsymbols._t}
|
| 671 |
+
# Convert input to iterables
|
| 672 |
+
if coordinates is None:
|
| 673 |
+
coordinates = []
|
| 674 |
+
elif not iterable(coordinates):
|
| 675 |
+
coordinates = [coordinates]
|
| 676 |
+
if speeds is None:
|
| 677 |
+
speeds = []
|
| 678 |
+
elif not iterable(speeds):
|
| 679 |
+
speeds = [speeds]
|
| 680 |
+
if u_auxiliary is None:
|
| 681 |
+
u_auxiliary = []
|
| 682 |
+
elif not iterable(u_auxiliary):
|
| 683 |
+
u_auxiliary = [u_auxiliary]
|
| 684 |
+
|
| 685 |
+
msgs = []
|
| 686 |
+
if check_duplicates: # Check for duplicates
|
| 687 |
+
seen = set()
|
| 688 |
+
coord_duplicates = {x for x in coordinates if x in seen or seen.add(x)}
|
| 689 |
+
seen = set()
|
| 690 |
+
speed_duplicates = {x for x in speeds if x in seen or seen.add(x)}
|
| 691 |
+
seen = set()
|
| 692 |
+
aux_duplicates = {x for x in u_auxiliary if x in seen or seen.add(x)}
|
| 693 |
+
overlap_coords = set(coordinates).intersection(speeds)
|
| 694 |
+
overlap_aux = set(coordinates).union(speeds).intersection(u_auxiliary)
|
| 695 |
+
if coord_duplicates:
|
| 696 |
+
msgs.append(f'The generalized coordinates {coord_duplicates} are '
|
| 697 |
+
f'duplicated, all generalized coordinates should be '
|
| 698 |
+
f'unique.')
|
| 699 |
+
if speed_duplicates:
|
| 700 |
+
msgs.append(f'The generalized speeds {speed_duplicates} are '
|
| 701 |
+
f'duplicated, all generalized speeds should be unique.')
|
| 702 |
+
if aux_duplicates:
|
| 703 |
+
msgs.append(f'The auxiliary speeds {aux_duplicates} are duplicated,'
|
| 704 |
+
f' all auxiliary speeds should be unique.')
|
| 705 |
+
if overlap_coords:
|
| 706 |
+
msgs.append(f'{overlap_coords} are defined as both generalized '
|
| 707 |
+
f'coordinates and generalized speeds.')
|
| 708 |
+
if overlap_aux:
|
| 709 |
+
msgs.append(f'The auxiliary speeds {overlap_aux} are also defined '
|
| 710 |
+
f'as generalized coordinates or generalized speeds.')
|
| 711 |
+
if is_dynamicsymbols: # Check whether all coordinates are dynamicsymbols
|
| 712 |
+
for coordinate in coordinates:
|
| 713 |
+
if not (isinstance(coordinate, (AppliedUndef, Derivative)) and
|
| 714 |
+
coordinate.free_symbols == t_set):
|
| 715 |
+
msgs.append(f'Generalized coordinate "{coordinate}" is not a '
|
| 716 |
+
f'dynamicsymbol.')
|
| 717 |
+
for speed in speeds:
|
| 718 |
+
if not (isinstance(speed, (AppliedUndef, Derivative)) and
|
| 719 |
+
speed.free_symbols == t_set):
|
| 720 |
+
msgs.append(
|
| 721 |
+
f'Generalized speed "{speed}" is not a dynamicsymbol.')
|
| 722 |
+
for aux in u_auxiliary:
|
| 723 |
+
if not (isinstance(aux, (AppliedUndef, Derivative)) and
|
| 724 |
+
aux.free_symbols == t_set):
|
| 725 |
+
msgs.append(
|
| 726 |
+
f'Auxiliary speed "{aux}" is not a dynamicsymbol.')
|
| 727 |
+
if msgs:
|
| 728 |
+
raise ValueError('\n'.join(msgs))
|
| 729 |
+
|
| 730 |
+
|
| 731 |
+
def _parse_linear_solver(linear_solver):
|
| 732 |
+
"""Helper function to retrieve a specified linear solver."""
|
| 733 |
+
if callable(linear_solver):
|
| 734 |
+
return linear_solver
|
| 735 |
+
return lambda A, b: Matrix.solve(A, b, method=linear_solver)
|
lib/python3.12/site-packages/sympy/physics/mechanics/inertia.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import sympify
|
| 2 |
+
from sympy.physics.vector import Point, Dyadic, ReferenceFrame, outer
|
| 3 |
+
from collections import namedtuple
|
| 4 |
+
|
| 5 |
+
__all__ = ['inertia', 'inertia_of_point_mass', 'Inertia']
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def inertia(frame, ixx, iyy, izz, ixy=0, iyz=0, izx=0):
|
| 9 |
+
"""Simple way to create inertia Dyadic object.
|
| 10 |
+
|
| 11 |
+
Explanation
|
| 12 |
+
===========
|
| 13 |
+
|
| 14 |
+
Creates an inertia Dyadic based on the given tensor values and a body-fixed
|
| 15 |
+
reference frame.
|
| 16 |
+
|
| 17 |
+
Parameters
|
| 18 |
+
==========
|
| 19 |
+
|
| 20 |
+
frame : ReferenceFrame
|
| 21 |
+
The frame the inertia is defined in.
|
| 22 |
+
ixx : Sympifyable
|
| 23 |
+
The xx element in the inertia dyadic.
|
| 24 |
+
iyy : Sympifyable
|
| 25 |
+
The yy element in the inertia dyadic.
|
| 26 |
+
izz : Sympifyable
|
| 27 |
+
The zz element in the inertia dyadic.
|
| 28 |
+
ixy : Sympifyable
|
| 29 |
+
The xy element in the inertia dyadic.
|
| 30 |
+
iyz : Sympifyable
|
| 31 |
+
The yz element in the inertia dyadic.
|
| 32 |
+
izx : Sympifyable
|
| 33 |
+
The zx element in the inertia dyadic.
|
| 34 |
+
|
| 35 |
+
Examples
|
| 36 |
+
========
|
| 37 |
+
|
| 38 |
+
>>> from sympy.physics.mechanics import ReferenceFrame, inertia
|
| 39 |
+
>>> N = ReferenceFrame('N')
|
| 40 |
+
>>> inertia(N, 1, 2, 3)
|
| 41 |
+
(N.x|N.x) + 2*(N.y|N.y) + 3*(N.z|N.z)
|
| 42 |
+
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
if not isinstance(frame, ReferenceFrame):
|
| 46 |
+
raise TypeError('Need to define the inertia in a frame')
|
| 47 |
+
ixx, iyy, izz = sympify(ixx), sympify(iyy), sympify(izz)
|
| 48 |
+
ixy, iyz, izx = sympify(ixy), sympify(iyz), sympify(izx)
|
| 49 |
+
return (ixx*outer(frame.x, frame.x) + ixy*outer(frame.x, frame.y) +
|
| 50 |
+
izx*outer(frame.x, frame.z) + ixy*outer(frame.y, frame.x) +
|
| 51 |
+
iyy*outer(frame.y, frame.y) + iyz*outer(frame.y, frame.z) +
|
| 52 |
+
izx*outer(frame.z, frame.x) + iyz*outer(frame.z, frame.y) +
|
| 53 |
+
izz*outer(frame.z, frame.z))
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def inertia_of_point_mass(mass, pos_vec, frame):
|
| 57 |
+
"""Inertia dyadic of a point mass relative to point O.
|
| 58 |
+
|
| 59 |
+
Parameters
|
| 60 |
+
==========
|
| 61 |
+
|
| 62 |
+
mass : Sympifyable
|
| 63 |
+
Mass of the point mass
|
| 64 |
+
pos_vec : Vector
|
| 65 |
+
Position from point O to point mass
|
| 66 |
+
frame : ReferenceFrame
|
| 67 |
+
Reference frame to express the dyadic in
|
| 68 |
+
|
| 69 |
+
Examples
|
| 70 |
+
========
|
| 71 |
+
|
| 72 |
+
>>> from sympy import symbols
|
| 73 |
+
>>> from sympy.physics.mechanics import ReferenceFrame, inertia_of_point_mass
|
| 74 |
+
>>> N = ReferenceFrame('N')
|
| 75 |
+
>>> r, m = symbols('r m')
|
| 76 |
+
>>> px = r * N.x
|
| 77 |
+
>>> inertia_of_point_mass(m, px, N)
|
| 78 |
+
m*r**2*(N.y|N.y) + m*r**2*(N.z|N.z)
|
| 79 |
+
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
return mass*(
|
| 83 |
+
(outer(frame.x, frame.x) +
|
| 84 |
+
outer(frame.y, frame.y) +
|
| 85 |
+
outer(frame.z, frame.z)) *
|
| 86 |
+
(pos_vec.dot(pos_vec)) - outer(pos_vec, pos_vec))
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class Inertia(namedtuple('Inertia', ['dyadic', 'point'])):
|
| 90 |
+
"""Inertia object consisting of a Dyadic and a Point of reference.
|
| 91 |
+
|
| 92 |
+
Explanation
|
| 93 |
+
===========
|
| 94 |
+
|
| 95 |
+
This is a simple class to store the Point and Dyadic, belonging to an
|
| 96 |
+
inertia.
|
| 97 |
+
|
| 98 |
+
Attributes
|
| 99 |
+
==========
|
| 100 |
+
|
| 101 |
+
dyadic : Dyadic
|
| 102 |
+
The dyadic of the inertia.
|
| 103 |
+
point : Point
|
| 104 |
+
The reference point of the inertia.
|
| 105 |
+
|
| 106 |
+
Examples
|
| 107 |
+
========
|
| 108 |
+
|
| 109 |
+
>>> from sympy.physics.mechanics import ReferenceFrame, Point, Inertia
|
| 110 |
+
>>> N = ReferenceFrame('N')
|
| 111 |
+
>>> Po = Point('Po')
|
| 112 |
+
>>> Inertia(N.x.outer(N.x) + N.y.outer(N.y) + N.z.outer(N.z), Po)
|
| 113 |
+
((N.x|N.x) + (N.y|N.y) + (N.z|N.z), Po)
|
| 114 |
+
|
| 115 |
+
In the example above the Dyadic was created manually, one can however also
|
| 116 |
+
use the ``inertia`` function for this or the class method ``from_tensor`` as
|
| 117 |
+
shown below.
|
| 118 |
+
|
| 119 |
+
>>> Inertia.from_inertia_scalars(Po, N, 1, 1, 1)
|
| 120 |
+
((N.x|N.x) + (N.y|N.y) + (N.z|N.z), Po)
|
| 121 |
+
|
| 122 |
+
"""
|
| 123 |
+
def __new__(cls, dyadic, point):
|
| 124 |
+
# Switch order if given in the wrong order
|
| 125 |
+
if isinstance(dyadic, Point) and isinstance(point, Dyadic):
|
| 126 |
+
point, dyadic = dyadic, point
|
| 127 |
+
if not isinstance(point, Point):
|
| 128 |
+
raise TypeError('Reference point should be of type Point')
|
| 129 |
+
if not isinstance(dyadic, Dyadic):
|
| 130 |
+
raise TypeError('Inertia value should be expressed as a Dyadic')
|
| 131 |
+
return super().__new__(cls, dyadic, point)
|
| 132 |
+
|
| 133 |
+
@classmethod
|
| 134 |
+
def from_inertia_scalars(cls, point, frame, ixx, iyy, izz, ixy=0, iyz=0,
|
| 135 |
+
izx=0):
|
| 136 |
+
"""Simple way to create an Inertia object based on the tensor values.
|
| 137 |
+
|
| 138 |
+
Explanation
|
| 139 |
+
===========
|
| 140 |
+
|
| 141 |
+
This class method uses the :func`~.inertia` to create the Dyadic based
|
| 142 |
+
on the tensor values.
|
| 143 |
+
|
| 144 |
+
Parameters
|
| 145 |
+
==========
|
| 146 |
+
|
| 147 |
+
point : Point
|
| 148 |
+
The reference point of the inertia.
|
| 149 |
+
frame : ReferenceFrame
|
| 150 |
+
The frame the inertia is defined in.
|
| 151 |
+
ixx : Sympifyable
|
| 152 |
+
The xx element in the inertia dyadic.
|
| 153 |
+
iyy : Sympifyable
|
| 154 |
+
The yy element in the inertia dyadic.
|
| 155 |
+
izz : Sympifyable
|
| 156 |
+
The zz element in the inertia dyadic.
|
| 157 |
+
ixy : Sympifyable
|
| 158 |
+
The xy element in the inertia dyadic.
|
| 159 |
+
iyz : Sympifyable
|
| 160 |
+
The yz element in the inertia dyadic.
|
| 161 |
+
izx : Sympifyable
|
| 162 |
+
The zx element in the inertia dyadic.
|
| 163 |
+
|
| 164 |
+
Examples
|
| 165 |
+
========
|
| 166 |
+
|
| 167 |
+
>>> from sympy import symbols
|
| 168 |
+
>>> from sympy.physics.mechanics import ReferenceFrame, Point, Inertia
|
| 169 |
+
>>> ixx, iyy, izz, ixy, iyz, izx = symbols('ixx iyy izz ixy iyz izx')
|
| 170 |
+
>>> N = ReferenceFrame('N')
|
| 171 |
+
>>> P = Point('P')
|
| 172 |
+
>>> I = Inertia.from_inertia_scalars(P, N, ixx, iyy, izz, ixy, iyz, izx)
|
| 173 |
+
|
| 174 |
+
The tensor values can easily be seen when converting the dyadic to a
|
| 175 |
+
matrix.
|
| 176 |
+
|
| 177 |
+
>>> I.dyadic.to_matrix(N)
|
| 178 |
+
Matrix([
|
| 179 |
+
[ixx, ixy, izx],
|
| 180 |
+
[ixy, iyy, iyz],
|
| 181 |
+
[izx, iyz, izz]])
|
| 182 |
+
|
| 183 |
+
"""
|
| 184 |
+
return cls(inertia(frame, ixx, iyy, izz, ixy, iyz, izx), point)
|
| 185 |
+
|
| 186 |
+
def __add__(self, other):
|
| 187 |
+
raise TypeError(f"unsupported operand type(s) for +: "
|
| 188 |
+
f"'{self.__class__.__name__}' and "
|
| 189 |
+
f"'{other.__class__.__name__}'")
|
| 190 |
+
|
| 191 |
+
def __mul__(self, other):
|
| 192 |
+
raise TypeError(f"unsupported operand type(s) for *: "
|
| 193 |
+
f"'{self.__class__.__name__}' and "
|
| 194 |
+
f"'{other.__class__.__name__}'")
|
| 195 |
+
|
| 196 |
+
__radd__ = __add__
|
| 197 |
+
__rmul__ = __mul__
|
lib/python3.12/site-packages/sympy/physics/mechanics/lagrange.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import diff, zeros, Matrix, eye, sympify
|
| 2 |
+
from sympy.core.sorting import default_sort_key
|
| 3 |
+
from sympy.physics.vector import dynamicsymbols, ReferenceFrame
|
| 4 |
+
from sympy.physics.mechanics.method import _Methods
|
| 5 |
+
from sympy.physics.mechanics.functions import (
|
| 6 |
+
find_dynamicsymbols, msubs, _f_list_parser, _validate_coordinates)
|
| 7 |
+
from sympy.physics.mechanics.linearize import Linearizer
|
| 8 |
+
from sympy.utilities.iterables import iterable
|
| 9 |
+
|
| 10 |
+
__all__ = ['LagrangesMethod']
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class LagrangesMethod(_Methods):
|
| 14 |
+
"""Lagrange's method object.
|
| 15 |
+
|
| 16 |
+
Explanation
|
| 17 |
+
===========
|
| 18 |
+
|
| 19 |
+
This object generates the equations of motion in a two step procedure. The
|
| 20 |
+
first step involves the initialization of LagrangesMethod by supplying the
|
| 21 |
+
Lagrangian and the generalized coordinates, at the bare minimum. If there
|
| 22 |
+
are any constraint equations, they can be supplied as keyword arguments.
|
| 23 |
+
The Lagrange multipliers are automatically generated and are equal in
|
| 24 |
+
number to the constraint equations. Similarly any non-conservative forces
|
| 25 |
+
can be supplied in an iterable (as described below and also shown in the
|
| 26 |
+
example) along with a ReferenceFrame. This is also discussed further in the
|
| 27 |
+
__init__ method.
|
| 28 |
+
|
| 29 |
+
Attributes
|
| 30 |
+
==========
|
| 31 |
+
|
| 32 |
+
q, u : Matrix
|
| 33 |
+
Matrices of the generalized coordinates and speeds
|
| 34 |
+
loads : iterable
|
| 35 |
+
Iterable of (Point, vector) or (ReferenceFrame, vector) tuples
|
| 36 |
+
describing the forces on the system.
|
| 37 |
+
bodies : iterable
|
| 38 |
+
Iterable containing the rigid bodies and particles of the system.
|
| 39 |
+
mass_matrix : Matrix
|
| 40 |
+
The system's mass matrix
|
| 41 |
+
forcing : Matrix
|
| 42 |
+
The system's forcing vector
|
| 43 |
+
mass_matrix_full : Matrix
|
| 44 |
+
The "mass matrix" for the qdot's, qdoubledot's, and the
|
| 45 |
+
lagrange multipliers (lam)
|
| 46 |
+
forcing_full : Matrix
|
| 47 |
+
The forcing vector for the qdot's, qdoubledot's and
|
| 48 |
+
lagrange multipliers (lam)
|
| 49 |
+
|
| 50 |
+
Examples
|
| 51 |
+
========
|
| 52 |
+
|
| 53 |
+
This is a simple example for a one degree of freedom translational
|
| 54 |
+
spring-mass-damper.
|
| 55 |
+
|
| 56 |
+
In this example, we first need to do the kinematics.
|
| 57 |
+
This involves creating generalized coordinates and their derivatives.
|
| 58 |
+
Then we create a point and set its velocity in a frame.
|
| 59 |
+
|
| 60 |
+
>>> from sympy.physics.mechanics import LagrangesMethod, Lagrangian
|
| 61 |
+
>>> from sympy.physics.mechanics import ReferenceFrame, Particle, Point
|
| 62 |
+
>>> from sympy.physics.mechanics import dynamicsymbols
|
| 63 |
+
>>> from sympy import symbols
|
| 64 |
+
>>> q = dynamicsymbols('q')
|
| 65 |
+
>>> qd = dynamicsymbols('q', 1)
|
| 66 |
+
>>> m, k, b = symbols('m k b')
|
| 67 |
+
>>> N = ReferenceFrame('N')
|
| 68 |
+
>>> P = Point('P')
|
| 69 |
+
>>> P.set_vel(N, qd * N.x)
|
| 70 |
+
|
| 71 |
+
We need to then prepare the information as required by LagrangesMethod to
|
| 72 |
+
generate equations of motion.
|
| 73 |
+
First we create the Particle, which has a point attached to it.
|
| 74 |
+
Following this the lagrangian is created from the kinetic and potential
|
| 75 |
+
energies.
|
| 76 |
+
Then, an iterable of nonconservative forces/torques must be constructed,
|
| 77 |
+
where each item is a (Point, Vector) or (ReferenceFrame, Vector) tuple,
|
| 78 |
+
with the Vectors representing the nonconservative forces or torques.
|
| 79 |
+
|
| 80 |
+
>>> Pa = Particle('Pa', P, m)
|
| 81 |
+
>>> Pa.potential_energy = k * q**2 / 2.0
|
| 82 |
+
>>> L = Lagrangian(N, Pa)
|
| 83 |
+
>>> fl = [(P, -b * qd * N.x)]
|
| 84 |
+
|
| 85 |
+
Finally we can generate the equations of motion.
|
| 86 |
+
First we create the LagrangesMethod object. To do this one must supply
|
| 87 |
+
the Lagrangian, and the generalized coordinates. The constraint equations,
|
| 88 |
+
the forcelist, and the inertial frame may also be provided, if relevant.
|
| 89 |
+
Next we generate Lagrange's equations of motion, such that:
|
| 90 |
+
Lagrange's equations of motion = 0.
|
| 91 |
+
We have the equations of motion at this point.
|
| 92 |
+
|
| 93 |
+
>>> l = LagrangesMethod(L, [q], forcelist = fl, frame = N)
|
| 94 |
+
>>> print(l.form_lagranges_equations())
|
| 95 |
+
Matrix([[b*Derivative(q(t), t) + 1.0*k*q(t) + m*Derivative(q(t), (t, 2))]])
|
| 96 |
+
|
| 97 |
+
We can also solve for the states using the 'rhs' method.
|
| 98 |
+
|
| 99 |
+
>>> print(l.rhs())
|
| 100 |
+
Matrix([[Derivative(q(t), t)], [(-b*Derivative(q(t), t) - 1.0*k*q(t))/m]])
|
| 101 |
+
|
| 102 |
+
Please refer to the docstrings on each method for more details.
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
def __init__(self, Lagrangian, qs, forcelist=None, bodies=None, frame=None,
|
| 106 |
+
hol_coneqs=None, nonhol_coneqs=None):
|
| 107 |
+
"""Supply the following for the initialization of LagrangesMethod.
|
| 108 |
+
|
| 109 |
+
Lagrangian : Sympifyable
|
| 110 |
+
|
| 111 |
+
qs : array_like
|
| 112 |
+
The generalized coordinates
|
| 113 |
+
|
| 114 |
+
hol_coneqs : array_like, optional
|
| 115 |
+
The holonomic constraint equations
|
| 116 |
+
|
| 117 |
+
nonhol_coneqs : array_like, optional
|
| 118 |
+
The nonholonomic constraint equations
|
| 119 |
+
|
| 120 |
+
forcelist : iterable, optional
|
| 121 |
+
Takes an iterable of (Point, Vector) or (ReferenceFrame, Vector)
|
| 122 |
+
tuples which represent the force at a point or torque on a frame.
|
| 123 |
+
This feature is primarily to account for the nonconservative forces
|
| 124 |
+
and/or moments.
|
| 125 |
+
|
| 126 |
+
bodies : iterable, optional
|
| 127 |
+
Takes an iterable containing the rigid bodies and particles of the
|
| 128 |
+
system.
|
| 129 |
+
|
| 130 |
+
frame : ReferenceFrame, optional
|
| 131 |
+
Supply the inertial frame. This is used to determine the
|
| 132 |
+
generalized forces due to non-conservative forces.
|
| 133 |
+
"""
|
| 134 |
+
|
| 135 |
+
self._L = Matrix([sympify(Lagrangian)])
|
| 136 |
+
self.eom = None
|
| 137 |
+
self._m_cd = Matrix() # Mass Matrix of differentiated coneqs
|
| 138 |
+
self._m_d = Matrix() # Mass Matrix of dynamic equations
|
| 139 |
+
self._f_cd = Matrix() # Forcing part of the diff coneqs
|
| 140 |
+
self._f_d = Matrix() # Forcing part of the dynamic equations
|
| 141 |
+
self.lam_coeffs = Matrix() # The coeffecients of the multipliers
|
| 142 |
+
|
| 143 |
+
forcelist = forcelist if forcelist else []
|
| 144 |
+
if not iterable(forcelist):
|
| 145 |
+
raise TypeError('Force pairs must be supplied in an iterable.')
|
| 146 |
+
self._forcelist = forcelist
|
| 147 |
+
if frame and not isinstance(frame, ReferenceFrame):
|
| 148 |
+
raise TypeError('frame must be a valid ReferenceFrame')
|
| 149 |
+
self._bodies = bodies
|
| 150 |
+
self.inertial = frame
|
| 151 |
+
|
| 152 |
+
self.lam_vec = Matrix()
|
| 153 |
+
|
| 154 |
+
self._term1 = Matrix()
|
| 155 |
+
self._term2 = Matrix()
|
| 156 |
+
self._term3 = Matrix()
|
| 157 |
+
self._term4 = Matrix()
|
| 158 |
+
|
| 159 |
+
# Creating the qs, qdots and qdoubledots
|
| 160 |
+
if not iterable(qs):
|
| 161 |
+
raise TypeError('Generalized coordinates must be an iterable')
|
| 162 |
+
self._q = Matrix(qs)
|
| 163 |
+
self._qdots = self.q.diff(dynamicsymbols._t)
|
| 164 |
+
self._qdoubledots = self._qdots.diff(dynamicsymbols._t)
|
| 165 |
+
_validate_coordinates(self.q)
|
| 166 |
+
|
| 167 |
+
mat_build = lambda x: Matrix(x) if x else Matrix()
|
| 168 |
+
hol_coneqs = mat_build(hol_coneqs)
|
| 169 |
+
nonhol_coneqs = mat_build(nonhol_coneqs)
|
| 170 |
+
self.coneqs = Matrix([hol_coneqs.diff(dynamicsymbols._t),
|
| 171 |
+
nonhol_coneqs])
|
| 172 |
+
self._hol_coneqs = hol_coneqs
|
| 173 |
+
|
| 174 |
+
def form_lagranges_equations(self):
|
| 175 |
+
"""Method to form Lagrange's equations of motion.
|
| 176 |
+
|
| 177 |
+
Returns a vector of equations of motion using Lagrange's equations of
|
| 178 |
+
the second kind.
|
| 179 |
+
"""
|
| 180 |
+
|
| 181 |
+
qds = self._qdots
|
| 182 |
+
qdd_zero = dict.fromkeys(self._qdoubledots, 0)
|
| 183 |
+
n = len(self.q)
|
| 184 |
+
|
| 185 |
+
# Internally we represent the EOM as four terms:
|
| 186 |
+
# EOM = term1 - term2 - term3 - term4 = 0
|
| 187 |
+
|
| 188 |
+
# First term
|
| 189 |
+
self._term1 = self._L.jacobian(qds)
|
| 190 |
+
self._term1 = self._term1.diff(dynamicsymbols._t).T
|
| 191 |
+
|
| 192 |
+
# Second term
|
| 193 |
+
self._term2 = self._L.jacobian(self.q).T
|
| 194 |
+
|
| 195 |
+
# Third term
|
| 196 |
+
if self.coneqs:
|
| 197 |
+
coneqs = self.coneqs
|
| 198 |
+
m = len(coneqs)
|
| 199 |
+
# Creating the multipliers
|
| 200 |
+
self.lam_vec = Matrix(dynamicsymbols('lam1:' + str(m + 1)))
|
| 201 |
+
self.lam_coeffs = -coneqs.jacobian(qds)
|
| 202 |
+
self._term3 = self.lam_coeffs.T * self.lam_vec
|
| 203 |
+
# Extracting the coeffecients of the qdds from the diff coneqs
|
| 204 |
+
diffconeqs = coneqs.diff(dynamicsymbols._t)
|
| 205 |
+
self._m_cd = diffconeqs.jacobian(self._qdoubledots)
|
| 206 |
+
# The remaining terms i.e. the 'forcing' terms in diff coneqs
|
| 207 |
+
self._f_cd = -diffconeqs.subs(qdd_zero)
|
| 208 |
+
else:
|
| 209 |
+
self._term3 = zeros(n, 1)
|
| 210 |
+
|
| 211 |
+
# Fourth term
|
| 212 |
+
if self.forcelist:
|
| 213 |
+
N = self.inertial
|
| 214 |
+
self._term4 = zeros(n, 1)
|
| 215 |
+
for i, qd in enumerate(qds):
|
| 216 |
+
flist = zip(*_f_list_parser(self.forcelist, N))
|
| 217 |
+
self._term4[i] = sum(v.diff(qd, N).dot(f) for (v, f) in flist)
|
| 218 |
+
else:
|
| 219 |
+
self._term4 = zeros(n, 1)
|
| 220 |
+
|
| 221 |
+
# Form the dynamic mass and forcing matrices
|
| 222 |
+
without_lam = self._term1 - self._term2 - self._term4
|
| 223 |
+
self._m_d = without_lam.jacobian(self._qdoubledots)
|
| 224 |
+
self._f_d = -without_lam.subs(qdd_zero)
|
| 225 |
+
|
| 226 |
+
# Form the EOM
|
| 227 |
+
self.eom = without_lam - self._term3
|
| 228 |
+
return self.eom
|
| 229 |
+
|
| 230 |
+
def _form_eoms(self):
|
| 231 |
+
return self.form_lagranges_equations()
|
| 232 |
+
|
| 233 |
+
@property
|
| 234 |
+
def mass_matrix(self):
|
| 235 |
+
"""Returns the mass matrix, which is augmented by the Lagrange
|
| 236 |
+
multipliers, if necessary.
|
| 237 |
+
|
| 238 |
+
Explanation
|
| 239 |
+
===========
|
| 240 |
+
|
| 241 |
+
If the system is described by 'n' generalized coordinates and there are
|
| 242 |
+
no constraint equations then an n X n matrix is returned.
|
| 243 |
+
|
| 244 |
+
If there are 'n' generalized coordinates and 'm' constraint equations
|
| 245 |
+
have been supplied during initialization then an n X (n+m) matrix is
|
| 246 |
+
returned. The (n + m - 1)th and (n + m)th columns contain the
|
| 247 |
+
coefficients of the Lagrange multipliers.
|
| 248 |
+
"""
|
| 249 |
+
|
| 250 |
+
if self.eom is None:
|
| 251 |
+
raise ValueError('Need to compute the equations of motion first')
|
| 252 |
+
if self.coneqs:
|
| 253 |
+
return (self._m_d).row_join(self.lam_coeffs.T)
|
| 254 |
+
else:
|
| 255 |
+
return self._m_d
|
| 256 |
+
|
| 257 |
+
@property
|
| 258 |
+
def mass_matrix_full(self):
|
| 259 |
+
"""Augments the coefficients of qdots to the mass_matrix."""
|
| 260 |
+
|
| 261 |
+
if self.eom is None:
|
| 262 |
+
raise ValueError('Need to compute the equations of motion first')
|
| 263 |
+
n = len(self.q)
|
| 264 |
+
m = len(self.coneqs)
|
| 265 |
+
row1 = eye(n).row_join(zeros(n, n + m))
|
| 266 |
+
row2 = zeros(n, n).row_join(self.mass_matrix)
|
| 267 |
+
if self.coneqs:
|
| 268 |
+
row3 = zeros(m, n).row_join(self._m_cd).row_join(zeros(m, m))
|
| 269 |
+
return row1.col_join(row2).col_join(row3)
|
| 270 |
+
else:
|
| 271 |
+
return row1.col_join(row2)
|
| 272 |
+
|
| 273 |
+
@property
|
| 274 |
+
def forcing(self):
|
| 275 |
+
"""Returns the forcing vector from 'lagranges_equations' method."""
|
| 276 |
+
|
| 277 |
+
if self.eom is None:
|
| 278 |
+
raise ValueError('Need to compute the equations of motion first')
|
| 279 |
+
return self._f_d
|
| 280 |
+
|
| 281 |
+
@property
|
| 282 |
+
def forcing_full(self):
|
| 283 |
+
"""Augments qdots to the forcing vector above."""
|
| 284 |
+
|
| 285 |
+
if self.eom is None:
|
| 286 |
+
raise ValueError('Need to compute the equations of motion first')
|
| 287 |
+
if self.coneqs:
|
| 288 |
+
return self._qdots.col_join(self.forcing).col_join(self._f_cd)
|
| 289 |
+
else:
|
| 290 |
+
return self._qdots.col_join(self.forcing)
|
| 291 |
+
|
| 292 |
+
def to_linearizer(self, q_ind=None, qd_ind=None, q_dep=None, qd_dep=None,
|
| 293 |
+
linear_solver='LU'):
|
| 294 |
+
"""Returns an instance of the Linearizer class, initiated from the data
|
| 295 |
+
in the LagrangesMethod class. This may be more desirable than using the
|
| 296 |
+
linearize class method, as the Linearizer object will allow more
|
| 297 |
+
efficient recalculation (i.e. about varying operating points).
|
| 298 |
+
|
| 299 |
+
Parameters
|
| 300 |
+
==========
|
| 301 |
+
|
| 302 |
+
q_ind, qd_ind : array_like, optional
|
| 303 |
+
The independent generalized coordinates and speeds.
|
| 304 |
+
q_dep, qd_dep : array_like, optional
|
| 305 |
+
The dependent generalized coordinates and speeds.
|
| 306 |
+
linear_solver : str, callable
|
| 307 |
+
Method used to solve the several symbolic linear systems of the
|
| 308 |
+
form ``A*x=b`` in the linearization process. If a string is
|
| 309 |
+
supplied, it should be a valid method that can be used with the
|
| 310 |
+
:meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is
|
| 311 |
+
supplied, it should have the format ``x = f(A, b)``, where it
|
| 312 |
+
solves the equations and returns the solution. The default is
|
| 313 |
+
``'LU'`` which corresponds to SymPy's ``A.LUsolve(b)``.
|
| 314 |
+
``LUsolve()`` is fast to compute but will often result in
|
| 315 |
+
divide-by-zero and thus ``nan`` results.
|
| 316 |
+
|
| 317 |
+
Returns
|
| 318 |
+
=======
|
| 319 |
+
Linearizer
|
| 320 |
+
An instantiated
|
| 321 |
+
:class:`sympy.physics.mechanics.linearize.Linearizer`.
|
| 322 |
+
|
| 323 |
+
"""
|
| 324 |
+
|
| 325 |
+
# Compose vectors
|
| 326 |
+
t = dynamicsymbols._t
|
| 327 |
+
q = self.q
|
| 328 |
+
u = self._qdots
|
| 329 |
+
ud = u.diff(t)
|
| 330 |
+
# Get vector of lagrange multipliers
|
| 331 |
+
lams = self.lam_vec
|
| 332 |
+
|
| 333 |
+
mat_build = lambda x: Matrix(x) if x else Matrix()
|
| 334 |
+
q_i = mat_build(q_ind)
|
| 335 |
+
q_d = mat_build(q_dep)
|
| 336 |
+
u_i = mat_build(qd_ind)
|
| 337 |
+
u_d = mat_build(qd_dep)
|
| 338 |
+
|
| 339 |
+
# Compose general form equations
|
| 340 |
+
f_c = self._hol_coneqs
|
| 341 |
+
f_v = self.coneqs
|
| 342 |
+
f_a = f_v.diff(t)
|
| 343 |
+
f_0 = u
|
| 344 |
+
f_1 = -u
|
| 345 |
+
f_2 = self._term1
|
| 346 |
+
f_3 = -(self._term2 + self._term4)
|
| 347 |
+
f_4 = -self._term3
|
| 348 |
+
|
| 349 |
+
# Check that there are an appropriate number of independent and
|
| 350 |
+
# dependent coordinates
|
| 351 |
+
if len(q_d) != len(f_c) or len(u_d) != len(f_v):
|
| 352 |
+
raise ValueError(("Must supply {:} dependent coordinates, and " +
|
| 353 |
+
"{:} dependent speeds").format(len(f_c), len(f_v)))
|
| 354 |
+
if set(Matrix([q_i, q_d])) != set(q):
|
| 355 |
+
raise ValueError("Must partition q into q_ind and q_dep, with " +
|
| 356 |
+
"no extra or missing symbols.")
|
| 357 |
+
if set(Matrix([u_i, u_d])) != set(u):
|
| 358 |
+
raise ValueError("Must partition qd into qd_ind and qd_dep, " +
|
| 359 |
+
"with no extra or missing symbols.")
|
| 360 |
+
|
| 361 |
+
# Find all other dynamic symbols, forming the forcing vector r.
|
| 362 |
+
# Sort r to make it canonical.
|
| 363 |
+
insyms = set(Matrix([q, u, ud, lams]))
|
| 364 |
+
r = list(find_dynamicsymbols(f_3, insyms))
|
| 365 |
+
r.sort(key=default_sort_key)
|
| 366 |
+
# Check for any derivatives of variables in r that are also found in r.
|
| 367 |
+
for i in r:
|
| 368 |
+
if diff(i, dynamicsymbols._t) in r:
|
| 369 |
+
raise ValueError('Cannot have derivatives of specified \
|
| 370 |
+
quantities when linearizing forcing terms.')
|
| 371 |
+
|
| 372 |
+
return Linearizer(f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a, q, u, q_i,
|
| 373 |
+
q_d, u_i, u_d, r, lams, linear_solver=linear_solver)
|
| 374 |
+
|
| 375 |
+
def linearize(self, q_ind=None, qd_ind=None, q_dep=None, qd_dep=None,
|
| 376 |
+
linear_solver='LU', **kwargs):
|
| 377 |
+
"""Linearize the equations of motion about a symbolic operating point.
|
| 378 |
+
|
| 379 |
+
Parameters
|
| 380 |
+
==========
|
| 381 |
+
linear_solver : str, callable
|
| 382 |
+
Method used to solve the several symbolic linear systems of the
|
| 383 |
+
form ``A*x=b`` in the linearization process. If a string is
|
| 384 |
+
supplied, it should be a valid method that can be used with the
|
| 385 |
+
:meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is
|
| 386 |
+
supplied, it should have the format ``x = f(A, b)``, where it
|
| 387 |
+
solves the equations and returns the solution. The default is
|
| 388 |
+
``'LU'`` which corresponds to SymPy's ``A.LUsolve(b)``.
|
| 389 |
+
``LUsolve()`` is fast to compute but will often result in
|
| 390 |
+
divide-by-zero and thus ``nan`` results.
|
| 391 |
+
**kwargs
|
| 392 |
+
Extra keyword arguments are passed to
|
| 393 |
+
:meth:`sympy.physics.mechanics.linearize.Linearizer.linearize`.
|
| 394 |
+
|
| 395 |
+
Explanation
|
| 396 |
+
===========
|
| 397 |
+
|
| 398 |
+
If kwarg A_and_B is False (default), returns M, A, B, r for the
|
| 399 |
+
linearized form, M*[q', u']^T = A*[q_ind, u_ind]^T + B*r.
|
| 400 |
+
|
| 401 |
+
If kwarg A_and_B is True, returns A, B, r for the linearized form
|
| 402 |
+
dx = A*x + B*r, where x = [q_ind, u_ind]^T. Note that this is
|
| 403 |
+
computationally intensive if there are many symbolic parameters. For
|
| 404 |
+
this reason, it may be more desirable to use the default A_and_B=False,
|
| 405 |
+
returning M, A, and B. Values may then be substituted in to these
|
| 406 |
+
matrices, and the state space form found as
|
| 407 |
+
A = P.T*M.inv()*A, B = P.T*M.inv()*B, where P = Linearizer.perm_mat.
|
| 408 |
+
|
| 409 |
+
In both cases, r is found as all dynamicsymbols in the equations of
|
| 410 |
+
motion that are not part of q, u, q', or u'. They are sorted in
|
| 411 |
+
canonical form.
|
| 412 |
+
|
| 413 |
+
The operating points may be also entered using the ``op_point`` kwarg.
|
| 414 |
+
This takes a dictionary of {symbol: value}, or a an iterable of such
|
| 415 |
+
dictionaries. The values may be numeric or symbolic. The more values
|
| 416 |
+
you can specify beforehand, the faster this computation will run.
|
| 417 |
+
|
| 418 |
+
For more documentation, please see the ``Linearizer`` class."""
|
| 419 |
+
|
| 420 |
+
linearizer = self.to_linearizer(q_ind, qd_ind, q_dep, qd_dep,
|
| 421 |
+
linear_solver=linear_solver)
|
| 422 |
+
result = linearizer.linearize(**kwargs)
|
| 423 |
+
return result + (linearizer.r,)
|
| 424 |
+
|
| 425 |
+
def solve_multipliers(self, op_point=None, sol_type='dict'):
|
| 426 |
+
"""Solves for the values of the lagrange multipliers symbolically at
|
| 427 |
+
the specified operating point.
|
| 428 |
+
|
| 429 |
+
Parameters
|
| 430 |
+
==========
|
| 431 |
+
|
| 432 |
+
op_point : dict or iterable of dicts, optional
|
| 433 |
+
Point at which to solve at. The operating point is specified as
|
| 434 |
+
a dictionary or iterable of dictionaries of {symbol: value}. The
|
| 435 |
+
value may be numeric or symbolic itself.
|
| 436 |
+
|
| 437 |
+
sol_type : str, optional
|
| 438 |
+
Solution return type. Valid options are:
|
| 439 |
+
- 'dict': A dict of {symbol : value} (default)
|
| 440 |
+
- 'Matrix': An ordered column matrix of the solution
|
| 441 |
+
"""
|
| 442 |
+
|
| 443 |
+
# Determine number of multipliers
|
| 444 |
+
k = len(self.lam_vec)
|
| 445 |
+
if k == 0:
|
| 446 |
+
raise ValueError("System has no lagrange multipliers to solve for.")
|
| 447 |
+
# Compose dict of operating conditions
|
| 448 |
+
if isinstance(op_point, dict):
|
| 449 |
+
op_point_dict = op_point
|
| 450 |
+
elif iterable(op_point):
|
| 451 |
+
op_point_dict = {}
|
| 452 |
+
for op in op_point:
|
| 453 |
+
op_point_dict.update(op)
|
| 454 |
+
elif op_point is None:
|
| 455 |
+
op_point_dict = {}
|
| 456 |
+
else:
|
| 457 |
+
raise TypeError("op_point must be either a dictionary or an "
|
| 458 |
+
"iterable of dictionaries.")
|
| 459 |
+
# Compose the system to be solved
|
| 460 |
+
mass_matrix = self.mass_matrix.col_join(-self.lam_coeffs.row_join(
|
| 461 |
+
zeros(k, k)))
|
| 462 |
+
force_matrix = self.forcing.col_join(self._f_cd)
|
| 463 |
+
# Sub in the operating point
|
| 464 |
+
mass_matrix = msubs(mass_matrix, op_point_dict)
|
| 465 |
+
force_matrix = msubs(force_matrix, op_point_dict)
|
| 466 |
+
# Solve for the multipliers
|
| 467 |
+
sol_list = mass_matrix.LUsolve(-force_matrix)[-k:]
|
| 468 |
+
if sol_type == 'dict':
|
| 469 |
+
return dict(zip(self.lam_vec, sol_list))
|
| 470 |
+
elif sol_type == 'Matrix':
|
| 471 |
+
return Matrix(sol_list)
|
| 472 |
+
else:
|
| 473 |
+
raise ValueError("Unknown sol_type {:}.".format(sol_type))
|
| 474 |
+
|
| 475 |
+
def rhs(self, inv_method=None, **kwargs):
|
| 476 |
+
"""Returns equations that can be solved numerically.
|
| 477 |
+
|
| 478 |
+
Parameters
|
| 479 |
+
==========
|
| 480 |
+
|
| 481 |
+
inv_method : str
|
| 482 |
+
The specific sympy inverse matrix calculation method to use. For a
|
| 483 |
+
list of valid methods, see
|
| 484 |
+
:meth:`~sympy.matrices.matrixbase.MatrixBase.inv`
|
| 485 |
+
"""
|
| 486 |
+
|
| 487 |
+
if inv_method is None:
|
| 488 |
+
self._rhs = self.mass_matrix_full.LUsolve(self.forcing_full)
|
| 489 |
+
else:
|
| 490 |
+
self._rhs = (self.mass_matrix_full.inv(inv_method,
|
| 491 |
+
try_block_diag=True) * self.forcing_full)
|
| 492 |
+
return self._rhs
|
| 493 |
+
|
| 494 |
+
@property
|
| 495 |
+
def q(self):
|
| 496 |
+
return self._q
|
| 497 |
+
|
| 498 |
+
@property
|
| 499 |
+
def u(self):
|
| 500 |
+
return self._qdots
|
| 501 |
+
|
| 502 |
+
@property
|
| 503 |
+
def bodies(self):
|
| 504 |
+
return self._bodies
|
| 505 |
+
|
| 506 |
+
@property
|
| 507 |
+
def forcelist(self):
|
| 508 |
+
return self._forcelist
|
| 509 |
+
|
| 510 |
+
@property
|
| 511 |
+
def loads(self):
|
| 512 |
+
return self._forcelist
|
lib/python3.12/site-packages/sympy/physics/mechanics/loads.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from abc import ABC
|
| 2 |
+
from collections import namedtuple
|
| 3 |
+
from sympy.physics.mechanics.body_base import BodyBase
|
| 4 |
+
from sympy.physics.vector import Vector, ReferenceFrame, Point
|
| 5 |
+
|
| 6 |
+
__all__ = ['LoadBase', 'Force', 'Torque']
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class LoadBase(ABC, namedtuple('LoadBase', ['location', 'vector'])):
|
| 10 |
+
"""Abstract base class for the various loading types."""
|
| 11 |
+
|
| 12 |
+
def __add__(self, other):
|
| 13 |
+
raise TypeError(f"unsupported operand type(s) for +: "
|
| 14 |
+
f"'{self.__class__.__name__}' and "
|
| 15 |
+
f"'{other.__class__.__name__}'")
|
| 16 |
+
|
| 17 |
+
def __mul__(self, other):
|
| 18 |
+
raise TypeError(f"unsupported operand type(s) for *: "
|
| 19 |
+
f"'{self.__class__.__name__}' and "
|
| 20 |
+
f"'{other.__class__.__name__}'")
|
| 21 |
+
|
| 22 |
+
__radd__ = __add__
|
| 23 |
+
__rmul__ = __mul__
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class Force(LoadBase):
|
| 27 |
+
"""Force acting upon a point.
|
| 28 |
+
|
| 29 |
+
Explanation
|
| 30 |
+
===========
|
| 31 |
+
|
| 32 |
+
A force is a vector that is bound to a line of action. This class stores
|
| 33 |
+
both a point, which lies on the line of action, and the vector. A tuple can
|
| 34 |
+
also be used, with the location as the first entry and the vector as second
|
| 35 |
+
entry.
|
| 36 |
+
|
| 37 |
+
Examples
|
| 38 |
+
========
|
| 39 |
+
|
| 40 |
+
A force of magnitude 2 along N.x acting on a point Po can be created as
|
| 41 |
+
follows:
|
| 42 |
+
|
| 43 |
+
>>> from sympy.physics.mechanics import Point, ReferenceFrame, Force
|
| 44 |
+
>>> N = ReferenceFrame('N')
|
| 45 |
+
>>> Po = Point('Po')
|
| 46 |
+
>>> Force(Po, 2 * N.x)
|
| 47 |
+
(Po, 2*N.x)
|
| 48 |
+
|
| 49 |
+
If a body is supplied, then the center of mass of that body is used.
|
| 50 |
+
|
| 51 |
+
>>> from sympy.physics.mechanics import Particle
|
| 52 |
+
>>> P = Particle('P', point=Po)
|
| 53 |
+
>>> Force(P, 2 * N.x)
|
| 54 |
+
(Po, 2*N.x)
|
| 55 |
+
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
def __new__(cls, point, force):
|
| 59 |
+
if isinstance(point, BodyBase):
|
| 60 |
+
point = point.masscenter
|
| 61 |
+
if not isinstance(point, Point):
|
| 62 |
+
raise TypeError('Force location should be a Point.')
|
| 63 |
+
if not isinstance(force, Vector):
|
| 64 |
+
raise TypeError('Force vector should be a Vector.')
|
| 65 |
+
return super().__new__(cls, point, force)
|
| 66 |
+
|
| 67 |
+
def __repr__(self):
|
| 68 |
+
return (f'{self.__class__.__name__}(point={self.point}, '
|
| 69 |
+
f'force={self.force})')
|
| 70 |
+
|
| 71 |
+
@property
|
| 72 |
+
def point(self):
|
| 73 |
+
return self.location
|
| 74 |
+
|
| 75 |
+
@property
|
| 76 |
+
def force(self):
|
| 77 |
+
return self.vector
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class Torque(LoadBase):
|
| 81 |
+
"""Torque acting upon a frame.
|
| 82 |
+
|
| 83 |
+
Explanation
|
| 84 |
+
===========
|
| 85 |
+
|
| 86 |
+
A torque is a free vector that is acting on a reference frame, which is
|
| 87 |
+
associated with a rigid body. This class stores both the frame and the
|
| 88 |
+
vector. A tuple can also be used, with the location as the first item and
|
| 89 |
+
the vector as second item.
|
| 90 |
+
|
| 91 |
+
Examples
|
| 92 |
+
========
|
| 93 |
+
|
| 94 |
+
A torque of magnitude 2 about N.x acting on a frame N can be created as
|
| 95 |
+
follows:
|
| 96 |
+
|
| 97 |
+
>>> from sympy.physics.mechanics import ReferenceFrame, Torque
|
| 98 |
+
>>> N = ReferenceFrame('N')
|
| 99 |
+
>>> Torque(N, 2 * N.x)
|
| 100 |
+
(N, 2*N.x)
|
| 101 |
+
|
| 102 |
+
If a body is supplied, then the frame fixed to that body is used.
|
| 103 |
+
|
| 104 |
+
>>> from sympy.physics.mechanics import RigidBody
|
| 105 |
+
>>> rb = RigidBody('rb', frame=N)
|
| 106 |
+
>>> Torque(rb, 2 * N.x)
|
| 107 |
+
(N, 2*N.x)
|
| 108 |
+
|
| 109 |
+
"""
|
| 110 |
+
|
| 111 |
+
def __new__(cls, frame, torque):
|
| 112 |
+
if isinstance(frame, BodyBase):
|
| 113 |
+
frame = frame.frame
|
| 114 |
+
if not isinstance(frame, ReferenceFrame):
|
| 115 |
+
raise TypeError('Torque location should be a ReferenceFrame.')
|
| 116 |
+
if not isinstance(torque, Vector):
|
| 117 |
+
raise TypeError('Torque vector should be a Vector.')
|
| 118 |
+
return super().__new__(cls, frame, torque)
|
| 119 |
+
|
| 120 |
+
def __repr__(self):
|
| 121 |
+
return (f'{self.__class__.__name__}(frame={self.frame}, '
|
| 122 |
+
f'torque={self.torque})')
|
| 123 |
+
|
| 124 |
+
@property
|
| 125 |
+
def frame(self):
|
| 126 |
+
return self.location
|
| 127 |
+
|
| 128 |
+
@property
|
| 129 |
+
def torque(self):
|
| 130 |
+
return self.vector
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def gravity(acceleration, *bodies):
|
| 134 |
+
"""
|
| 135 |
+
Returns a list of gravity forces given the acceleration
|
| 136 |
+
due to gravity and any number of particles or rigidbodies.
|
| 137 |
+
|
| 138 |
+
Example
|
| 139 |
+
=======
|
| 140 |
+
|
| 141 |
+
>>> from sympy.physics.mechanics import ReferenceFrame, Particle, RigidBody
|
| 142 |
+
>>> from sympy.physics.mechanics.loads import gravity
|
| 143 |
+
>>> from sympy import symbols
|
| 144 |
+
>>> N = ReferenceFrame('N')
|
| 145 |
+
>>> g = symbols('g')
|
| 146 |
+
>>> P = Particle('P')
|
| 147 |
+
>>> B = RigidBody('B')
|
| 148 |
+
>>> gravity(g*N.y, P, B)
|
| 149 |
+
[(P_masscenter, P_mass*g*N.y),
|
| 150 |
+
(B_masscenter, B_mass*g*N.y)]
|
| 151 |
+
|
| 152 |
+
"""
|
| 153 |
+
|
| 154 |
+
gravity_force = []
|
| 155 |
+
for body in bodies:
|
| 156 |
+
if not isinstance(body, BodyBase):
|
| 157 |
+
raise TypeError(f'{type(body)} is not a body type')
|
| 158 |
+
gravity_force.append(Force(body.masscenter, body.mass * acceleration))
|
| 159 |
+
return gravity_force
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _parse_load(load):
|
| 163 |
+
"""Helper function to parse loads and convert tuples to load objects."""
|
| 164 |
+
if isinstance(load, LoadBase):
|
| 165 |
+
return load
|
| 166 |
+
elif isinstance(load, tuple):
|
| 167 |
+
if len(load) != 2:
|
| 168 |
+
raise ValueError(f'Load {load} should have a length of 2.')
|
| 169 |
+
if isinstance(load[0], Point):
|
| 170 |
+
return Force(load[0], load[1])
|
| 171 |
+
elif isinstance(load[0], ReferenceFrame):
|
| 172 |
+
return Torque(load[0], load[1])
|
| 173 |
+
else:
|
| 174 |
+
raise ValueError(f'Load not recognized. The load location {load[0]}'
|
| 175 |
+
f' should either be a Point or a ReferenceFrame.')
|
| 176 |
+
raise TypeError(f'Load type {type(load)} not recognized as a load. It '
|
| 177 |
+
f'should be a Force, Torque or tuple.')
|
lib/python3.12/site-packages/sympy/physics/mechanics/method.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from abc import ABC, abstractmethod
|
| 2 |
+
|
| 3 |
+
class _Methods(ABC):
|
| 4 |
+
"""Abstract Base Class for all methods."""
|
| 5 |
+
|
| 6 |
+
@abstractmethod
|
| 7 |
+
def q(self):
|
| 8 |
+
pass
|
| 9 |
+
|
| 10 |
+
@abstractmethod
|
| 11 |
+
def u(self):
|
| 12 |
+
pass
|
| 13 |
+
|
| 14 |
+
@abstractmethod
|
| 15 |
+
def bodies(self):
|
| 16 |
+
pass
|
| 17 |
+
|
| 18 |
+
@abstractmethod
|
| 19 |
+
def loads(self):
|
| 20 |
+
pass
|
| 21 |
+
|
| 22 |
+
@abstractmethod
|
| 23 |
+
def mass_matrix(self):
|
| 24 |
+
pass
|
| 25 |
+
|
| 26 |
+
@abstractmethod
|
| 27 |
+
def forcing(self):
|
| 28 |
+
pass
|
| 29 |
+
|
| 30 |
+
@abstractmethod
|
| 31 |
+
def mass_matrix_full(self):
|
| 32 |
+
pass
|
| 33 |
+
|
| 34 |
+
@abstractmethod
|
| 35 |
+
def forcing_full(self):
|
| 36 |
+
pass
|
| 37 |
+
|
| 38 |
+
def _form_eoms(self):
|
| 39 |
+
raise NotImplementedError("Subclasses must implement this.")
|
lib/python3.12/site-packages/sympy/physics/mechanics/pathway.py
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Implementations of pathways for use by actuators."""
|
| 2 |
+
|
| 3 |
+
from abc import ABC, abstractmethod
|
| 4 |
+
|
| 5 |
+
from sympy.core.singleton import S
|
| 6 |
+
from sympy.physics.mechanics.loads import Force
|
| 7 |
+
from sympy.physics.mechanics.wrapping_geometry import WrappingGeometryBase
|
| 8 |
+
from sympy.physics.vector import Point, dynamicsymbols
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
__all__ = ['PathwayBase', 'LinearPathway', 'ObstacleSetPathway',
|
| 12 |
+
'WrappingPathway']
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class PathwayBase(ABC):
|
| 16 |
+
"""Abstract base class for all pathway classes to inherit from.
|
| 17 |
+
|
| 18 |
+
Notes
|
| 19 |
+
=====
|
| 20 |
+
|
| 21 |
+
Instances of this class cannot be directly instantiated by users. However,
|
| 22 |
+
it can be used to created custom pathway types through subclassing.
|
| 23 |
+
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(self, *attachments):
|
| 27 |
+
"""Initializer for ``PathwayBase``."""
|
| 28 |
+
self.attachments = attachments
|
| 29 |
+
|
| 30 |
+
@property
|
| 31 |
+
def attachments(self):
|
| 32 |
+
"""The pair of points defining a pathway's ends."""
|
| 33 |
+
return self._attachments
|
| 34 |
+
|
| 35 |
+
@attachments.setter
|
| 36 |
+
def attachments(self, attachments):
|
| 37 |
+
if hasattr(self, '_attachments'):
|
| 38 |
+
msg = (
|
| 39 |
+
f'Can\'t set attribute `attachments` to {repr(attachments)} '
|
| 40 |
+
f'as it is immutable.'
|
| 41 |
+
)
|
| 42 |
+
raise AttributeError(msg)
|
| 43 |
+
if len(attachments) != 2:
|
| 44 |
+
msg = (
|
| 45 |
+
f'Value {repr(attachments)} passed to `attachments` was an '
|
| 46 |
+
f'iterable of length {len(attachments)}, must be an iterable '
|
| 47 |
+
f'of length 2.'
|
| 48 |
+
)
|
| 49 |
+
raise ValueError(msg)
|
| 50 |
+
for i, point in enumerate(attachments):
|
| 51 |
+
if not isinstance(point, Point):
|
| 52 |
+
msg = (
|
| 53 |
+
f'Value {repr(point)} passed to `attachments` at index '
|
| 54 |
+
f'{i} was of type {type(point)}, must be {Point}.'
|
| 55 |
+
)
|
| 56 |
+
raise TypeError(msg)
|
| 57 |
+
self._attachments = tuple(attachments)
|
| 58 |
+
|
| 59 |
+
@property
|
| 60 |
+
@abstractmethod
|
| 61 |
+
def length(self):
|
| 62 |
+
"""An expression representing the pathway's length."""
|
| 63 |
+
pass
|
| 64 |
+
|
| 65 |
+
@property
|
| 66 |
+
@abstractmethod
|
| 67 |
+
def extension_velocity(self):
|
| 68 |
+
"""An expression representing the pathway's extension velocity."""
|
| 69 |
+
pass
|
| 70 |
+
|
| 71 |
+
@abstractmethod
|
| 72 |
+
def to_loads(self, force):
|
| 73 |
+
"""Loads required by the equations of motion method classes.
|
| 74 |
+
|
| 75 |
+
Explanation
|
| 76 |
+
===========
|
| 77 |
+
|
| 78 |
+
``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be
|
| 79 |
+
passed to the ``loads`` parameters of its ``kanes_equations`` method
|
| 80 |
+
when constructing the equations of motion. This method acts as a
|
| 81 |
+
utility to produce the correctly-structred pairs of points and vectors
|
| 82 |
+
required so that these can be easily concatenated with other items in
|
| 83 |
+
the list of loads and passed to ``KanesMethod.kanes_equations``. These
|
| 84 |
+
loads are also in the correct form to also be passed to the other
|
| 85 |
+
equations of motion method classes, e.g. ``LagrangesMethod``.
|
| 86 |
+
|
| 87 |
+
"""
|
| 88 |
+
pass
|
| 89 |
+
|
| 90 |
+
def __repr__(self):
|
| 91 |
+
"""Default representation of a pathway."""
|
| 92 |
+
attachments = ', '.join(str(a) for a in self.attachments)
|
| 93 |
+
return f'{self.__class__.__name__}({attachments})'
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class LinearPathway(PathwayBase):
|
| 97 |
+
"""Linear pathway between a pair of attachment points.
|
| 98 |
+
|
| 99 |
+
Explanation
|
| 100 |
+
===========
|
| 101 |
+
|
| 102 |
+
A linear pathway forms a straight-line segment between two points and is
|
| 103 |
+
the simplest pathway that can be formed. It will not interact with any
|
| 104 |
+
other objects in the system, i.e. a ``LinearPathway`` will intersect other
|
| 105 |
+
objects to ensure that the path between its two ends (its attachments) is
|
| 106 |
+
the shortest possible.
|
| 107 |
+
|
| 108 |
+
A linear pathway is made up of two points that can move relative to each
|
| 109 |
+
other, and a pair of equal and opposite forces acting on the points. If the
|
| 110 |
+
positive time-varying Euclidean distance between the two points is defined,
|
| 111 |
+
then the "extension velocity" is the time derivative of this distance. The
|
| 112 |
+
extension velocity is positive when the two points are moving away from
|
| 113 |
+
each other and negative when moving closer to each other. The direction for
|
| 114 |
+
the force acting on either point is determined by constructing a unit
|
| 115 |
+
vector directed from the other point to this point. This establishes a sign
|
| 116 |
+
convention such that a positive force magnitude tends to push the points
|
| 117 |
+
apart. The following diagram shows the positive force sense and the
|
| 118 |
+
distance between the points::
|
| 119 |
+
|
| 120 |
+
P Q
|
| 121 |
+
o<--- F --->o
|
| 122 |
+
| |
|
| 123 |
+
|<--l(t)--->|
|
| 124 |
+
|
| 125 |
+
Examples
|
| 126 |
+
========
|
| 127 |
+
|
| 128 |
+
>>> from sympy.physics.mechanics import LinearPathway
|
| 129 |
+
|
| 130 |
+
To construct a pathway, two points are required to be passed to the
|
| 131 |
+
``attachments`` parameter as a ``tuple``.
|
| 132 |
+
|
| 133 |
+
>>> from sympy.physics.mechanics import Point
|
| 134 |
+
>>> pA, pB = Point('pA'), Point('pB')
|
| 135 |
+
>>> linear_pathway = LinearPathway(pA, pB)
|
| 136 |
+
>>> linear_pathway
|
| 137 |
+
LinearPathway(pA, pB)
|
| 138 |
+
|
| 139 |
+
The pathway created above isn't very interesting without the positions and
|
| 140 |
+
velocities of its attachment points being described. Without this its not
|
| 141 |
+
possible to describe how the pathway moves, i.e. its length or its
|
| 142 |
+
extension velocity.
|
| 143 |
+
|
| 144 |
+
>>> from sympy.physics.mechanics import ReferenceFrame
|
| 145 |
+
>>> from sympy.physics.vector import dynamicsymbols
|
| 146 |
+
>>> N = ReferenceFrame('N')
|
| 147 |
+
>>> q = dynamicsymbols('q')
|
| 148 |
+
>>> pB.set_pos(pA, q*N.x)
|
| 149 |
+
>>> pB.pos_from(pA)
|
| 150 |
+
q(t)*N.x
|
| 151 |
+
|
| 152 |
+
A pathway's length can be accessed via its ``length`` attribute.
|
| 153 |
+
|
| 154 |
+
>>> linear_pathway.length
|
| 155 |
+
sqrt(q(t)**2)
|
| 156 |
+
|
| 157 |
+
Note how what appears to be an overly-complex expression is returned. This
|
| 158 |
+
is actually required as it ensures that a pathway's length is always
|
| 159 |
+
positive.
|
| 160 |
+
|
| 161 |
+
A pathway's extension velocity can be accessed similarly via its
|
| 162 |
+
``extension_velocity`` attribute.
|
| 163 |
+
|
| 164 |
+
>>> linear_pathway.extension_velocity
|
| 165 |
+
sqrt(q(t)**2)*Derivative(q(t), t)/q(t)
|
| 166 |
+
|
| 167 |
+
Parameters
|
| 168 |
+
==========
|
| 169 |
+
|
| 170 |
+
attachments : tuple[Point, Point]
|
| 171 |
+
Pair of ``Point`` objects between which the linear pathway spans.
|
| 172 |
+
Constructor expects two points to be passed, e.g.
|
| 173 |
+
``LinearPathway(Point('pA'), Point('pB'))``. More or fewer points will
|
| 174 |
+
cause an error to be thrown.
|
| 175 |
+
|
| 176 |
+
"""
|
| 177 |
+
|
| 178 |
+
def __init__(self, *attachments):
|
| 179 |
+
"""Initializer for ``LinearPathway``.
|
| 180 |
+
|
| 181 |
+
Parameters
|
| 182 |
+
==========
|
| 183 |
+
|
| 184 |
+
attachments : Point
|
| 185 |
+
Pair of ``Point`` objects between which the linear pathway spans.
|
| 186 |
+
Constructor expects two points to be passed, e.g.
|
| 187 |
+
``LinearPathway(Point('pA'), Point('pB'))``. More or fewer points
|
| 188 |
+
will cause an error to be thrown.
|
| 189 |
+
|
| 190 |
+
"""
|
| 191 |
+
super().__init__(*attachments)
|
| 192 |
+
|
| 193 |
+
@property
|
| 194 |
+
def length(self):
|
| 195 |
+
"""Exact analytical expression for the pathway's length."""
|
| 196 |
+
return _point_pair_length(*self.attachments)
|
| 197 |
+
|
| 198 |
+
@property
|
| 199 |
+
def extension_velocity(self):
|
| 200 |
+
"""Exact analytical expression for the pathway's extension velocity."""
|
| 201 |
+
return _point_pair_extension_velocity(*self.attachments)
|
| 202 |
+
|
| 203 |
+
def to_loads(self, force):
|
| 204 |
+
"""Loads required by the equations of motion method classes.
|
| 205 |
+
|
| 206 |
+
Explanation
|
| 207 |
+
===========
|
| 208 |
+
|
| 209 |
+
``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be
|
| 210 |
+
passed to the ``loads`` parameters of its ``kanes_equations`` method
|
| 211 |
+
when constructing the equations of motion. This method acts as a
|
| 212 |
+
utility to produce the correctly-structred pairs of points and vectors
|
| 213 |
+
required so that these can be easily concatenated with other items in
|
| 214 |
+
the list of loads and passed to ``KanesMethod.kanes_equations``. These
|
| 215 |
+
loads are also in the correct form to also be passed to the other
|
| 216 |
+
equations of motion method classes, e.g. ``LagrangesMethod``.
|
| 217 |
+
|
| 218 |
+
Examples
|
| 219 |
+
========
|
| 220 |
+
|
| 221 |
+
The below example shows how to generate the loads produced in a linear
|
| 222 |
+
actuator that produces an expansile force ``F``. First, create a linear
|
| 223 |
+
actuator between two points separated by the coordinate ``q`` in the
|
| 224 |
+
``x`` direction of the global frame ``N``.
|
| 225 |
+
|
| 226 |
+
>>> from sympy.physics.mechanics import (LinearPathway, Point,
|
| 227 |
+
... ReferenceFrame)
|
| 228 |
+
>>> from sympy.physics.vector import dynamicsymbols
|
| 229 |
+
>>> q = dynamicsymbols('q')
|
| 230 |
+
>>> N = ReferenceFrame('N')
|
| 231 |
+
>>> pA, pB = Point('pA'), Point('pB')
|
| 232 |
+
>>> pB.set_pos(pA, q*N.x)
|
| 233 |
+
>>> linear_pathway = LinearPathway(pA, pB)
|
| 234 |
+
|
| 235 |
+
Now create a symbol ``F`` to describe the magnitude of the (expansile)
|
| 236 |
+
force that will be produced along the pathway. The list of loads that
|
| 237 |
+
``KanesMethod`` requires can be produced by calling the pathway's
|
| 238 |
+
``to_loads`` method with ``F`` passed as the only argument.
|
| 239 |
+
|
| 240 |
+
>>> from sympy import symbols
|
| 241 |
+
>>> F = symbols('F')
|
| 242 |
+
>>> linear_pathway.to_loads(F)
|
| 243 |
+
[(pA, - F*q(t)/sqrt(q(t)**2)*N.x), (pB, F*q(t)/sqrt(q(t)**2)*N.x)]
|
| 244 |
+
|
| 245 |
+
Parameters
|
| 246 |
+
==========
|
| 247 |
+
|
| 248 |
+
force : Expr
|
| 249 |
+
Magnitude of the force acting along the length of the pathway. As
|
| 250 |
+
per the sign conventions for the pathway length, pathway extension
|
| 251 |
+
velocity, and pair of point forces, if this ``Expr`` is positive
|
| 252 |
+
then the force will act to push the pair of points away from one
|
| 253 |
+
another (it is expansile).
|
| 254 |
+
|
| 255 |
+
"""
|
| 256 |
+
relative_position = _point_pair_relative_position(*self.attachments)
|
| 257 |
+
loads = [
|
| 258 |
+
Force(self.attachments[0], -force*relative_position/self.length),
|
| 259 |
+
Force(self.attachments[-1], force*relative_position/self.length),
|
| 260 |
+
]
|
| 261 |
+
return loads
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
class ObstacleSetPathway(PathwayBase):
|
| 265 |
+
"""Obstacle-set pathway between a set of attachment points.
|
| 266 |
+
|
| 267 |
+
Explanation
|
| 268 |
+
===========
|
| 269 |
+
|
| 270 |
+
An obstacle-set pathway forms a series of straight-line segment between
|
| 271 |
+
pairs of consecutive points in a set of points. It is similiar to multiple
|
| 272 |
+
linear pathways joined end-to-end. It will not interact with any other
|
| 273 |
+
objects in the system, i.e. an ``ObstacleSetPathway`` will intersect other
|
| 274 |
+
objects to ensure that the path between its pairs of points (its
|
| 275 |
+
attachments) is the shortest possible.
|
| 276 |
+
|
| 277 |
+
Examples
|
| 278 |
+
========
|
| 279 |
+
|
| 280 |
+
To construct an obstacle-set pathway, three or more points are required to
|
| 281 |
+
be passed to the ``attachments`` parameter as a ``tuple``.
|
| 282 |
+
|
| 283 |
+
>>> from sympy.physics.mechanics import ObstacleSetPathway, Point
|
| 284 |
+
>>> pA, pB, pC, pD = Point('pA'), Point('pB'), Point('pC'), Point('pD')
|
| 285 |
+
>>> obstacle_set_pathway = ObstacleSetPathway(pA, pB, pC, pD)
|
| 286 |
+
>>> obstacle_set_pathway
|
| 287 |
+
ObstacleSetPathway(pA, pB, pC, pD)
|
| 288 |
+
|
| 289 |
+
The pathway created above isn't very interesting without the positions and
|
| 290 |
+
velocities of its attachment points being described. Without this its not
|
| 291 |
+
possible to describe how the pathway moves, i.e. its length or its
|
| 292 |
+
extension velocity.
|
| 293 |
+
|
| 294 |
+
>>> from sympy import cos, sin
|
| 295 |
+
>>> from sympy.physics.mechanics import ReferenceFrame
|
| 296 |
+
>>> from sympy.physics.vector import dynamicsymbols
|
| 297 |
+
>>> N = ReferenceFrame('N')
|
| 298 |
+
>>> q = dynamicsymbols('q')
|
| 299 |
+
>>> pO = Point('pO')
|
| 300 |
+
>>> pA.set_pos(pO, N.y)
|
| 301 |
+
>>> pB.set_pos(pO, -N.x)
|
| 302 |
+
>>> pC.set_pos(pA, cos(q) * N.x - (sin(q) + 1) * N.y)
|
| 303 |
+
>>> pD.set_pos(pA, sin(q) * N.x + (cos(q) - 1) * N.y)
|
| 304 |
+
>>> pB.pos_from(pA)
|
| 305 |
+
- N.x - N.y
|
| 306 |
+
>>> pC.pos_from(pA)
|
| 307 |
+
cos(q(t))*N.x + (-sin(q(t)) - 1)*N.y
|
| 308 |
+
>>> pD.pos_from(pA)
|
| 309 |
+
sin(q(t))*N.x + (cos(q(t)) - 1)*N.y
|
| 310 |
+
|
| 311 |
+
A pathway's length can be accessed via its ``length`` attribute.
|
| 312 |
+
|
| 313 |
+
>>> obstacle_set_pathway.length.simplify()
|
| 314 |
+
sqrt(2)*(sqrt(cos(q(t)) + 1) + 2)
|
| 315 |
+
|
| 316 |
+
A pathway's extension velocity can be accessed similarly via its
|
| 317 |
+
``extension_velocity`` attribute.
|
| 318 |
+
|
| 319 |
+
>>> obstacle_set_pathway.extension_velocity.simplify()
|
| 320 |
+
-sqrt(2)*sin(q(t))*Derivative(q(t), t)/(2*sqrt(cos(q(t)) + 1))
|
| 321 |
+
|
| 322 |
+
Parameters
|
| 323 |
+
==========
|
| 324 |
+
|
| 325 |
+
attachments : tuple[Point, Point]
|
| 326 |
+
The set of ``Point`` objects that define the segmented obstacle-set
|
| 327 |
+
pathway.
|
| 328 |
+
|
| 329 |
+
"""
|
| 330 |
+
|
| 331 |
+
def __init__(self, *attachments):
|
| 332 |
+
"""Initializer for ``ObstacleSetPathway``.
|
| 333 |
+
|
| 334 |
+
Parameters
|
| 335 |
+
==========
|
| 336 |
+
|
| 337 |
+
attachments : tuple[Point, ...]
|
| 338 |
+
The set of ``Point`` objects that define the segmented obstacle-set
|
| 339 |
+
pathway.
|
| 340 |
+
|
| 341 |
+
"""
|
| 342 |
+
super().__init__(*attachments)
|
| 343 |
+
|
| 344 |
+
@property
|
| 345 |
+
def attachments(self):
|
| 346 |
+
"""The set of points defining a pathway's segmented path."""
|
| 347 |
+
return self._attachments
|
| 348 |
+
|
| 349 |
+
@attachments.setter
|
| 350 |
+
def attachments(self, attachments):
|
| 351 |
+
if hasattr(self, '_attachments'):
|
| 352 |
+
msg = (
|
| 353 |
+
f'Can\'t set attribute `attachments` to {repr(attachments)} '
|
| 354 |
+
f'as it is immutable.'
|
| 355 |
+
)
|
| 356 |
+
raise AttributeError(msg)
|
| 357 |
+
if len(attachments) <= 2:
|
| 358 |
+
msg = (
|
| 359 |
+
f'Value {repr(attachments)} passed to `attachments` was an '
|
| 360 |
+
f'iterable of length {len(attachments)}, must be an iterable '
|
| 361 |
+
f'of length 3 or greater.'
|
| 362 |
+
)
|
| 363 |
+
raise ValueError(msg)
|
| 364 |
+
for i, point in enumerate(attachments):
|
| 365 |
+
if not isinstance(point, Point):
|
| 366 |
+
msg = (
|
| 367 |
+
f'Value {repr(point)} passed to `attachments` at index '
|
| 368 |
+
f'{i} was of type {type(point)}, must be {Point}.'
|
| 369 |
+
)
|
| 370 |
+
raise TypeError(msg)
|
| 371 |
+
self._attachments = tuple(attachments)
|
| 372 |
+
|
| 373 |
+
@property
|
| 374 |
+
def length(self):
|
| 375 |
+
"""Exact analytical expression for the pathway's length."""
|
| 376 |
+
length = S.Zero
|
| 377 |
+
attachment_pairs = zip(self.attachments[:-1], self.attachments[1:])
|
| 378 |
+
for attachment_pair in attachment_pairs:
|
| 379 |
+
length += _point_pair_length(*attachment_pair)
|
| 380 |
+
return length
|
| 381 |
+
|
| 382 |
+
@property
|
| 383 |
+
def extension_velocity(self):
|
| 384 |
+
"""Exact analytical expression for the pathway's extension velocity."""
|
| 385 |
+
extension_velocity = S.Zero
|
| 386 |
+
attachment_pairs = zip(self.attachments[:-1], self.attachments[1:])
|
| 387 |
+
for attachment_pair in attachment_pairs:
|
| 388 |
+
extension_velocity += _point_pair_extension_velocity(*attachment_pair)
|
| 389 |
+
return extension_velocity
|
| 390 |
+
|
| 391 |
+
def to_loads(self, force):
|
| 392 |
+
"""Loads required by the equations of motion method classes.
|
| 393 |
+
|
| 394 |
+
Explanation
|
| 395 |
+
===========
|
| 396 |
+
|
| 397 |
+
``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be
|
| 398 |
+
passed to the ``loads`` parameters of its ``kanes_equations`` method
|
| 399 |
+
when constructing the equations of motion. This method acts as a
|
| 400 |
+
utility to produce the correctly-structred pairs of points and vectors
|
| 401 |
+
required so that these can be easily concatenated with other items in
|
| 402 |
+
the list of loads and passed to ``KanesMethod.kanes_equations``. These
|
| 403 |
+
loads are also in the correct form to also be passed to the other
|
| 404 |
+
equations of motion method classes, e.g. ``LagrangesMethod``.
|
| 405 |
+
|
| 406 |
+
Examples
|
| 407 |
+
========
|
| 408 |
+
|
| 409 |
+
The below example shows how to generate the loads produced in an
|
| 410 |
+
actuator that follows an obstacle-set pathway between four points and
|
| 411 |
+
produces an expansile force ``F``. First, create a pair of reference
|
| 412 |
+
frames, ``A`` and ``B``, in which the four points ``pA``, ``pB``,
|
| 413 |
+
``pC``, and ``pD`` will be located. The first two points in frame ``A``
|
| 414 |
+
and the second two in frame ``B``. Frame ``B`` will also be oriented
|
| 415 |
+
such that it relates to ``A`` via a rotation of ``q`` about an axis
|
| 416 |
+
``N.z`` in a global frame (``N.z``, ``A.z``, and ``B.z`` are parallel).
|
| 417 |
+
|
| 418 |
+
>>> from sympy.physics.mechanics import (ObstacleSetPathway, Point,
|
| 419 |
+
... ReferenceFrame)
|
| 420 |
+
>>> from sympy.physics.vector import dynamicsymbols
|
| 421 |
+
>>> q = dynamicsymbols('q')
|
| 422 |
+
>>> N = ReferenceFrame('N')
|
| 423 |
+
>>> N = ReferenceFrame('N')
|
| 424 |
+
>>> A = N.orientnew('A', 'axis', (0, N.x))
|
| 425 |
+
>>> B = A.orientnew('B', 'axis', (q, N.z))
|
| 426 |
+
>>> pO = Point('pO')
|
| 427 |
+
>>> pA, pB, pC, pD = Point('pA'), Point('pB'), Point('pC'), Point('pD')
|
| 428 |
+
>>> pA.set_pos(pO, A.x)
|
| 429 |
+
>>> pB.set_pos(pO, -A.y)
|
| 430 |
+
>>> pC.set_pos(pO, B.y)
|
| 431 |
+
>>> pD.set_pos(pO, B.x)
|
| 432 |
+
>>> obstacle_set_pathway = ObstacleSetPathway(pA, pB, pC, pD)
|
| 433 |
+
|
| 434 |
+
Now create a symbol ``F`` to describe the magnitude of the (expansile)
|
| 435 |
+
force that will be produced along the pathway. The list of loads that
|
| 436 |
+
``KanesMethod`` requires can be produced by calling the pathway's
|
| 437 |
+
``to_loads`` method with ``F`` passed as the only argument.
|
| 438 |
+
|
| 439 |
+
>>> from sympy import Symbol
|
| 440 |
+
>>> F = Symbol('F')
|
| 441 |
+
>>> obstacle_set_pathway.to_loads(F)
|
| 442 |
+
[(pA, sqrt(2)*F/2*A.x + sqrt(2)*F/2*A.y),
|
| 443 |
+
(pB, - sqrt(2)*F/2*A.x - sqrt(2)*F/2*A.y),
|
| 444 |
+
(pB, - F/sqrt(2*cos(q(t)) + 2)*A.y - F/sqrt(2*cos(q(t)) + 2)*B.y),
|
| 445 |
+
(pC, F/sqrt(2*cos(q(t)) + 2)*A.y + F/sqrt(2*cos(q(t)) + 2)*B.y),
|
| 446 |
+
(pC, - sqrt(2)*F/2*B.x + sqrt(2)*F/2*B.y),
|
| 447 |
+
(pD, sqrt(2)*F/2*B.x - sqrt(2)*F/2*B.y)]
|
| 448 |
+
|
| 449 |
+
Parameters
|
| 450 |
+
==========
|
| 451 |
+
|
| 452 |
+
force : Expr
|
| 453 |
+
The force acting along the length of the pathway. It is assumed
|
| 454 |
+
that this ``Expr`` represents an expansile force.
|
| 455 |
+
|
| 456 |
+
"""
|
| 457 |
+
loads = []
|
| 458 |
+
attachment_pairs = zip(self.attachments[:-1], self.attachments[1:])
|
| 459 |
+
for attachment_pair in attachment_pairs:
|
| 460 |
+
relative_position = _point_pair_relative_position(*attachment_pair)
|
| 461 |
+
length = _point_pair_length(*attachment_pair)
|
| 462 |
+
loads.extend([
|
| 463 |
+
Force(attachment_pair[0], -force*relative_position/length),
|
| 464 |
+
Force(attachment_pair[1], force*relative_position/length),
|
| 465 |
+
])
|
| 466 |
+
return loads
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
class WrappingPathway(PathwayBase):
|
| 470 |
+
"""Pathway that wraps a geometry object.
|
| 471 |
+
|
| 472 |
+
Explanation
|
| 473 |
+
===========
|
| 474 |
+
|
| 475 |
+
A wrapping pathway interacts with a geometry object and forms a path that
|
| 476 |
+
wraps smoothly along its surface. The wrapping pathway along the geometry
|
| 477 |
+
object will be the geodesic that the geometry object defines based on the
|
| 478 |
+
two points. It will not interact with any other objects in the system, i.e.
|
| 479 |
+
a ``WrappingPathway`` will intersect other objects to ensure that the path
|
| 480 |
+
between its two ends (its attachments) is the shortest possible.
|
| 481 |
+
|
| 482 |
+
To explain the sign conventions used for pathway length, extension
|
| 483 |
+
velocity, and direction of applied forces, we can ignore the geometry with
|
| 484 |
+
which the wrapping pathway interacts. A wrapping pathway is made up of two
|
| 485 |
+
points that can move relative to each other, and a pair of equal and
|
| 486 |
+
opposite forces acting on the points. If the positive time-varying
|
| 487 |
+
Euclidean distance between the two points is defined, then the "extension
|
| 488 |
+
velocity" is the time derivative of this distance. The extension velocity
|
| 489 |
+
is positive when the two points are moving away from each other and
|
| 490 |
+
negative when moving closer to each other. The direction for the force
|
| 491 |
+
acting on either point is determined by constructing a unit vector directed
|
| 492 |
+
from the other point to this point. This establishes a sign convention such
|
| 493 |
+
that a positive force magnitude tends to push the points apart. The
|
| 494 |
+
following diagram shows the positive force sense and the distance between
|
| 495 |
+
the points::
|
| 496 |
+
|
| 497 |
+
P Q
|
| 498 |
+
o<--- F --->o
|
| 499 |
+
| |
|
| 500 |
+
|<--l(t)--->|
|
| 501 |
+
|
| 502 |
+
Examples
|
| 503 |
+
========
|
| 504 |
+
|
| 505 |
+
>>> from sympy.physics.mechanics import WrappingPathway
|
| 506 |
+
|
| 507 |
+
To construct a wrapping pathway, like other pathways, a pair of points must
|
| 508 |
+
be passed, followed by an instance of a wrapping geometry class as a
|
| 509 |
+
keyword argument. We'll use a cylinder with radius ``r`` and its axis
|
| 510 |
+
parallel to ``N.x`` passing through a point ``pO``.
|
| 511 |
+
|
| 512 |
+
>>> from sympy import symbols
|
| 513 |
+
>>> from sympy.physics.mechanics import Point, ReferenceFrame, WrappingCylinder
|
| 514 |
+
>>> r = symbols('r')
|
| 515 |
+
>>> N = ReferenceFrame('N')
|
| 516 |
+
>>> pA, pB, pO = Point('pA'), Point('pB'), Point('pO')
|
| 517 |
+
>>> cylinder = WrappingCylinder(r, pO, N.x)
|
| 518 |
+
>>> wrapping_pathway = WrappingPathway(pA, pB, cylinder)
|
| 519 |
+
>>> wrapping_pathway
|
| 520 |
+
WrappingPathway(pA, pB, geometry=WrappingCylinder(radius=r, point=pO,
|
| 521 |
+
axis=N.x))
|
| 522 |
+
|
| 523 |
+
Parameters
|
| 524 |
+
==========
|
| 525 |
+
|
| 526 |
+
attachment_1 : Point
|
| 527 |
+
First of the pair of ``Point`` objects between which the wrapping
|
| 528 |
+
pathway spans.
|
| 529 |
+
attachment_2 : Point
|
| 530 |
+
Second of the pair of ``Point`` objects between which the wrapping
|
| 531 |
+
pathway spans.
|
| 532 |
+
geometry : WrappingGeometryBase
|
| 533 |
+
Geometry about which the pathway wraps.
|
| 534 |
+
|
| 535 |
+
"""
|
| 536 |
+
|
| 537 |
+
def __init__(self, attachment_1, attachment_2, geometry):
|
| 538 |
+
"""Initializer for ``WrappingPathway``.
|
| 539 |
+
|
| 540 |
+
Parameters
|
| 541 |
+
==========
|
| 542 |
+
|
| 543 |
+
attachment_1 : Point
|
| 544 |
+
First of the pair of ``Point`` objects between which the wrapping
|
| 545 |
+
pathway spans.
|
| 546 |
+
attachment_2 : Point
|
| 547 |
+
Second of the pair of ``Point`` objects between which the wrapping
|
| 548 |
+
pathway spans.
|
| 549 |
+
geometry : WrappingGeometryBase
|
| 550 |
+
Geometry about which the pathway wraps.
|
| 551 |
+
The geometry about which the pathway wraps.
|
| 552 |
+
|
| 553 |
+
"""
|
| 554 |
+
super().__init__(attachment_1, attachment_2)
|
| 555 |
+
self.geometry = geometry
|
| 556 |
+
|
| 557 |
+
@property
|
| 558 |
+
def geometry(self):
|
| 559 |
+
"""Geometry around which the pathway wraps."""
|
| 560 |
+
return self._geometry
|
| 561 |
+
|
| 562 |
+
@geometry.setter
|
| 563 |
+
def geometry(self, geometry):
|
| 564 |
+
if hasattr(self, '_geometry'):
|
| 565 |
+
msg = (
|
| 566 |
+
f'Can\'t set attribute `geometry` to {repr(geometry)} as it '
|
| 567 |
+
f'is immutable.'
|
| 568 |
+
)
|
| 569 |
+
raise AttributeError(msg)
|
| 570 |
+
if not isinstance(geometry, WrappingGeometryBase):
|
| 571 |
+
msg = (
|
| 572 |
+
f'Value {repr(geometry)} passed to `geometry` was of type '
|
| 573 |
+
f'{type(geometry)}, must be {WrappingGeometryBase}.'
|
| 574 |
+
)
|
| 575 |
+
raise TypeError(msg)
|
| 576 |
+
self._geometry = geometry
|
| 577 |
+
|
| 578 |
+
@property
|
| 579 |
+
def length(self):
|
| 580 |
+
"""Exact analytical expression for the pathway's length."""
|
| 581 |
+
return self.geometry.geodesic_length(*self.attachments)
|
| 582 |
+
|
| 583 |
+
@property
|
| 584 |
+
def extension_velocity(self):
|
| 585 |
+
"""Exact analytical expression for the pathway's extension velocity."""
|
| 586 |
+
return self.length.diff(dynamicsymbols._t)
|
| 587 |
+
|
| 588 |
+
def to_loads(self, force):
|
| 589 |
+
"""Loads required by the equations of motion method classes.
|
| 590 |
+
|
| 591 |
+
Explanation
|
| 592 |
+
===========
|
| 593 |
+
|
| 594 |
+
``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be
|
| 595 |
+
passed to the ``loads`` parameters of its ``kanes_equations`` method
|
| 596 |
+
when constructing the equations of motion. This method acts as a
|
| 597 |
+
utility to produce the correctly-structred pairs of points and vectors
|
| 598 |
+
required so that these can be easily concatenated with other items in
|
| 599 |
+
the list of loads and passed to ``KanesMethod.kanes_equations``. These
|
| 600 |
+
loads are also in the correct form to also be passed to the other
|
| 601 |
+
equations of motion method classes, e.g. ``LagrangesMethod``.
|
| 602 |
+
|
| 603 |
+
Examples
|
| 604 |
+
========
|
| 605 |
+
|
| 606 |
+
The below example shows how to generate the loads produced in an
|
| 607 |
+
actuator that produces an expansile force ``F`` while wrapping around a
|
| 608 |
+
cylinder. First, create a cylinder with radius ``r`` and an axis
|
| 609 |
+
parallel to the ``N.z`` direction of the global frame ``N`` that also
|
| 610 |
+
passes through a point ``pO``.
|
| 611 |
+
|
| 612 |
+
>>> from sympy import symbols
|
| 613 |
+
>>> from sympy.physics.mechanics import (Point, ReferenceFrame,
|
| 614 |
+
... WrappingCylinder)
|
| 615 |
+
>>> N = ReferenceFrame('N')
|
| 616 |
+
>>> r = symbols('r', positive=True)
|
| 617 |
+
>>> pO = Point('pO')
|
| 618 |
+
>>> cylinder = WrappingCylinder(r, pO, N.z)
|
| 619 |
+
|
| 620 |
+
Create the pathway of the actuator using the ``WrappingPathway`` class,
|
| 621 |
+
defined to span between two points ``pA`` and ``pB``. Both points lie
|
| 622 |
+
on the surface of the cylinder and the location of ``pB`` is defined
|
| 623 |
+
relative to ``pA`` by the dynamics symbol ``q``.
|
| 624 |
+
|
| 625 |
+
>>> from sympy import cos, sin
|
| 626 |
+
>>> from sympy.physics.mechanics import WrappingPathway, dynamicsymbols
|
| 627 |
+
>>> q = dynamicsymbols('q')
|
| 628 |
+
>>> pA = Point('pA')
|
| 629 |
+
>>> pB = Point('pB')
|
| 630 |
+
>>> pA.set_pos(pO, r*N.x)
|
| 631 |
+
>>> pB.set_pos(pO, r*(cos(q)*N.x + sin(q)*N.y))
|
| 632 |
+
>>> pB.pos_from(pA)
|
| 633 |
+
(r*cos(q(t)) - r)*N.x + r*sin(q(t))*N.y
|
| 634 |
+
>>> pathway = WrappingPathway(pA, pB, cylinder)
|
| 635 |
+
|
| 636 |
+
Now create a symbol ``F`` to describe the magnitude of the (expansile)
|
| 637 |
+
force that will be produced along the pathway. The list of loads that
|
| 638 |
+
``KanesMethod`` requires can be produced by calling the pathway's
|
| 639 |
+
``to_loads`` method with ``F`` passed as the only argument.
|
| 640 |
+
|
| 641 |
+
>>> F = symbols('F')
|
| 642 |
+
>>> loads = pathway.to_loads(F)
|
| 643 |
+
>>> [load.__class__(load.location, load.vector.simplify()) for load in loads]
|
| 644 |
+
[(pA, F*N.y), (pB, F*sin(q(t))*N.x - F*cos(q(t))*N.y),
|
| 645 |
+
(pO, - F*sin(q(t))*N.x + F*(cos(q(t)) - 1)*N.y)]
|
| 646 |
+
|
| 647 |
+
Parameters
|
| 648 |
+
==========
|
| 649 |
+
|
| 650 |
+
force : Expr
|
| 651 |
+
Magnitude of the force acting along the length of the pathway. It
|
| 652 |
+
is assumed that this ``Expr`` represents an expansile force.
|
| 653 |
+
|
| 654 |
+
"""
|
| 655 |
+
pA, pB = self.attachments
|
| 656 |
+
pO = self.geometry.point
|
| 657 |
+
pA_force, pB_force = self.geometry.geodesic_end_vectors(pA, pB)
|
| 658 |
+
pO_force = -(pA_force + pB_force)
|
| 659 |
+
|
| 660 |
+
loads = [
|
| 661 |
+
Force(pA, force * pA_force),
|
| 662 |
+
Force(pB, force * pB_force),
|
| 663 |
+
Force(pO, force * pO_force),
|
| 664 |
+
]
|
| 665 |
+
return loads
|
| 666 |
+
|
| 667 |
+
def __repr__(self):
|
| 668 |
+
"""Representation of a ``WrappingPathway``."""
|
| 669 |
+
attachments = ', '.join(str(a) for a in self.attachments)
|
| 670 |
+
return (
|
| 671 |
+
f'{self.__class__.__name__}({attachments}, '
|
| 672 |
+
f'geometry={self.geometry})'
|
| 673 |
+
)
|
| 674 |
+
|
| 675 |
+
|
| 676 |
+
def _point_pair_relative_position(point_1, point_2):
|
| 677 |
+
"""The relative position between a pair of points."""
|
| 678 |
+
return point_2.pos_from(point_1)
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
def _point_pair_length(point_1, point_2):
|
| 682 |
+
"""The length of the direct linear path between two points."""
|
| 683 |
+
return _point_pair_relative_position(point_1, point_2).magnitude()
|
| 684 |
+
|
| 685 |
+
|
| 686 |
+
def _point_pair_extension_velocity(point_1, point_2):
|
| 687 |
+
"""The extension velocity of the direct linear path between two points."""
|
| 688 |
+
return _point_pair_length(point_1, point_2).diff(dynamicsymbols._t)
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_inertia.cpython-312.pyc
ADDED
|
Binary file (6.77 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_kane5.cpython-312.pyc
ADDED
|
Binary file (9.71 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_method.cpython-312.pyc
ADDED
|
Binary file (622 Bytes). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_rigidbody.cpython-312.pyc
ADDED
|
Binary file (12.6 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/__pycache__/test_system.cpython-312.pyc
ADDED
|
Binary file (13 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_actuator.py
ADDED
|
@@ -0,0 +1,851 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the ``sympy.physics.mechanics.actuator.py`` module."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
from sympy import (
|
| 6 |
+
S,
|
| 7 |
+
Matrix,
|
| 8 |
+
Symbol,
|
| 9 |
+
SympifyError,
|
| 10 |
+
sqrt,
|
| 11 |
+
Abs
|
| 12 |
+
)
|
| 13 |
+
from sympy.physics.mechanics import (
|
| 14 |
+
ActuatorBase,
|
| 15 |
+
Force,
|
| 16 |
+
ForceActuator,
|
| 17 |
+
KanesMethod,
|
| 18 |
+
LinearDamper,
|
| 19 |
+
LinearPathway,
|
| 20 |
+
LinearSpring,
|
| 21 |
+
Particle,
|
| 22 |
+
PinJoint,
|
| 23 |
+
Point,
|
| 24 |
+
ReferenceFrame,
|
| 25 |
+
RigidBody,
|
| 26 |
+
TorqueActuator,
|
| 27 |
+
Vector,
|
| 28 |
+
dynamicsymbols,
|
| 29 |
+
DuffingSpring,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
from sympy.core.expr import Expr as ExprType
|
| 33 |
+
|
| 34 |
+
target = RigidBody('target')
|
| 35 |
+
reaction = RigidBody('reaction')
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class TestForceActuator:
|
| 39 |
+
|
| 40 |
+
@pytest.fixture(autouse=True)
|
| 41 |
+
def _linear_pathway_fixture(self):
|
| 42 |
+
self.force = Symbol('F')
|
| 43 |
+
self.pA = Point('pA')
|
| 44 |
+
self.pB = Point('pB')
|
| 45 |
+
self.pathway = LinearPathway(self.pA, self.pB)
|
| 46 |
+
self.q1 = dynamicsymbols('q1')
|
| 47 |
+
self.q2 = dynamicsymbols('q2')
|
| 48 |
+
self.q3 = dynamicsymbols('q3')
|
| 49 |
+
self.q1d = dynamicsymbols('q1', 1)
|
| 50 |
+
self.q2d = dynamicsymbols('q2', 1)
|
| 51 |
+
self.q3d = dynamicsymbols('q3', 1)
|
| 52 |
+
self.N = ReferenceFrame('N')
|
| 53 |
+
|
| 54 |
+
def test_is_actuator_base_subclass(self):
|
| 55 |
+
assert issubclass(ForceActuator, ActuatorBase)
|
| 56 |
+
|
| 57 |
+
@pytest.mark.parametrize(
|
| 58 |
+
'force, expected_force',
|
| 59 |
+
[
|
| 60 |
+
(1, S.One),
|
| 61 |
+
(S.One, S.One),
|
| 62 |
+
(Symbol('F'), Symbol('F')),
|
| 63 |
+
(dynamicsymbols('F'), dynamicsymbols('F')),
|
| 64 |
+
(Symbol('F')**2 + Symbol('F'), Symbol('F')**2 + Symbol('F')),
|
| 65 |
+
]
|
| 66 |
+
)
|
| 67 |
+
def test_valid_constructor_force(self, force, expected_force):
|
| 68 |
+
instance = ForceActuator(force, self.pathway)
|
| 69 |
+
assert isinstance(instance, ForceActuator)
|
| 70 |
+
assert hasattr(instance, 'force')
|
| 71 |
+
assert isinstance(instance.force, ExprType)
|
| 72 |
+
assert instance.force == expected_force
|
| 73 |
+
|
| 74 |
+
@pytest.mark.parametrize('force', [None, 'F'])
|
| 75 |
+
def test_invalid_constructor_force_not_sympifyable(self, force):
|
| 76 |
+
with pytest.raises(SympifyError):
|
| 77 |
+
_ = ForceActuator(force, self.pathway)
|
| 78 |
+
|
| 79 |
+
@pytest.mark.parametrize(
|
| 80 |
+
'pathway',
|
| 81 |
+
[
|
| 82 |
+
LinearPathway(Point('pA'), Point('pB')),
|
| 83 |
+
]
|
| 84 |
+
)
|
| 85 |
+
def test_valid_constructor_pathway(self, pathway):
|
| 86 |
+
instance = ForceActuator(self.force, pathway)
|
| 87 |
+
assert isinstance(instance, ForceActuator)
|
| 88 |
+
assert hasattr(instance, 'pathway')
|
| 89 |
+
assert isinstance(instance.pathway, LinearPathway)
|
| 90 |
+
assert instance.pathway == pathway
|
| 91 |
+
|
| 92 |
+
def test_invalid_constructor_pathway_not_pathway_base(self):
|
| 93 |
+
with pytest.raises(TypeError):
|
| 94 |
+
_ = ForceActuator(self.force, None)
|
| 95 |
+
|
| 96 |
+
@pytest.mark.parametrize(
|
| 97 |
+
'property_name, fixture_attr_name',
|
| 98 |
+
[
|
| 99 |
+
('force', 'force'),
|
| 100 |
+
('pathway', 'pathway'),
|
| 101 |
+
]
|
| 102 |
+
)
|
| 103 |
+
def test_properties_are_immutable(self, property_name, fixture_attr_name):
|
| 104 |
+
instance = ForceActuator(self.force, self.pathway)
|
| 105 |
+
value = getattr(self, fixture_attr_name)
|
| 106 |
+
with pytest.raises(AttributeError):
|
| 107 |
+
setattr(instance, property_name, value)
|
| 108 |
+
|
| 109 |
+
def test_repr(self):
|
| 110 |
+
actuator = ForceActuator(self.force, self.pathway)
|
| 111 |
+
expected = "ForceActuator(F, LinearPathway(pA, pB))"
|
| 112 |
+
assert repr(actuator) == expected
|
| 113 |
+
|
| 114 |
+
def test_to_loads_static_pathway(self):
|
| 115 |
+
self.pB.set_pos(self.pA, 2*self.N.x)
|
| 116 |
+
actuator = ForceActuator(self.force, self.pathway)
|
| 117 |
+
expected = [
|
| 118 |
+
(self.pA, - self.force*self.N.x),
|
| 119 |
+
(self.pB, self.force*self.N.x),
|
| 120 |
+
]
|
| 121 |
+
assert actuator.to_loads() == expected
|
| 122 |
+
|
| 123 |
+
def test_to_loads_2D_pathway(self):
|
| 124 |
+
self.pB.set_pos(self.pA, 2*self.q1*self.N.x)
|
| 125 |
+
actuator = ForceActuator(self.force, self.pathway)
|
| 126 |
+
expected = [
|
| 127 |
+
(self.pA, - self.force*(self.q1/sqrt(self.q1**2))*self.N.x),
|
| 128 |
+
(self.pB, self.force*(self.q1/sqrt(self.q1**2))*self.N.x),
|
| 129 |
+
]
|
| 130 |
+
assert actuator.to_loads() == expected
|
| 131 |
+
|
| 132 |
+
def test_to_loads_3D_pathway(self):
|
| 133 |
+
self.pB.set_pos(
|
| 134 |
+
self.pA,
|
| 135 |
+
self.q1*self.N.x - self.q2*self.N.y + 2*self.q3*self.N.z,
|
| 136 |
+
)
|
| 137 |
+
actuator = ForceActuator(self.force, self.pathway)
|
| 138 |
+
length = sqrt(self.q1**2 + self.q2**2 + 4*self.q3**2)
|
| 139 |
+
pO_force = (
|
| 140 |
+
- self.force*self.q1*self.N.x/length
|
| 141 |
+
+ self.force*self.q2*self.N.y/length
|
| 142 |
+
- 2*self.force*self.q3*self.N.z/length
|
| 143 |
+
)
|
| 144 |
+
pI_force = (
|
| 145 |
+
self.force*self.q1*self.N.x/length
|
| 146 |
+
- self.force*self.q2*self.N.y/length
|
| 147 |
+
+ 2*self.force*self.q3*self.N.z/length
|
| 148 |
+
)
|
| 149 |
+
expected = [
|
| 150 |
+
(self.pA, pO_force),
|
| 151 |
+
(self.pB, pI_force),
|
| 152 |
+
]
|
| 153 |
+
assert actuator.to_loads() == expected
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class TestLinearSpring:
|
| 157 |
+
|
| 158 |
+
@pytest.fixture(autouse=True)
|
| 159 |
+
def _linear_spring_fixture(self):
|
| 160 |
+
self.stiffness = Symbol('k')
|
| 161 |
+
self.l = Symbol('l')
|
| 162 |
+
self.pA = Point('pA')
|
| 163 |
+
self.pB = Point('pB')
|
| 164 |
+
self.pathway = LinearPathway(self.pA, self.pB)
|
| 165 |
+
self.q = dynamicsymbols('q')
|
| 166 |
+
self.N = ReferenceFrame('N')
|
| 167 |
+
|
| 168 |
+
def test_is_force_actuator_subclass(self):
|
| 169 |
+
assert issubclass(LinearSpring, ForceActuator)
|
| 170 |
+
|
| 171 |
+
def test_is_actuator_base_subclass(self):
|
| 172 |
+
assert issubclass(LinearSpring, ActuatorBase)
|
| 173 |
+
|
| 174 |
+
@pytest.mark.parametrize(
|
| 175 |
+
(
|
| 176 |
+
'stiffness, '
|
| 177 |
+
'expected_stiffness, '
|
| 178 |
+
'equilibrium_length, '
|
| 179 |
+
'expected_equilibrium_length, '
|
| 180 |
+
'force'
|
| 181 |
+
),
|
| 182 |
+
[
|
| 183 |
+
(
|
| 184 |
+
1,
|
| 185 |
+
S.One,
|
| 186 |
+
0,
|
| 187 |
+
S.Zero,
|
| 188 |
+
-sqrt(dynamicsymbols('q')**2),
|
| 189 |
+
),
|
| 190 |
+
(
|
| 191 |
+
Symbol('k'),
|
| 192 |
+
Symbol('k'),
|
| 193 |
+
0,
|
| 194 |
+
S.Zero,
|
| 195 |
+
-Symbol('k')*sqrt(dynamicsymbols('q')**2),
|
| 196 |
+
),
|
| 197 |
+
(
|
| 198 |
+
Symbol('k'),
|
| 199 |
+
Symbol('k'),
|
| 200 |
+
S.Zero,
|
| 201 |
+
S.Zero,
|
| 202 |
+
-Symbol('k')*sqrt(dynamicsymbols('q')**2),
|
| 203 |
+
),
|
| 204 |
+
(
|
| 205 |
+
Symbol('k'),
|
| 206 |
+
Symbol('k'),
|
| 207 |
+
Symbol('l'),
|
| 208 |
+
Symbol('l'),
|
| 209 |
+
-Symbol('k')*(sqrt(dynamicsymbols('q')**2) - Symbol('l')),
|
| 210 |
+
),
|
| 211 |
+
]
|
| 212 |
+
)
|
| 213 |
+
def test_valid_constructor(
|
| 214 |
+
self,
|
| 215 |
+
stiffness,
|
| 216 |
+
expected_stiffness,
|
| 217 |
+
equilibrium_length,
|
| 218 |
+
expected_equilibrium_length,
|
| 219 |
+
force,
|
| 220 |
+
):
|
| 221 |
+
self.pB.set_pos(self.pA, self.q*self.N.x)
|
| 222 |
+
spring = LinearSpring(stiffness, self.pathway, equilibrium_length)
|
| 223 |
+
|
| 224 |
+
assert isinstance(spring, LinearSpring)
|
| 225 |
+
|
| 226 |
+
assert hasattr(spring, 'stiffness')
|
| 227 |
+
assert isinstance(spring.stiffness, ExprType)
|
| 228 |
+
assert spring.stiffness == expected_stiffness
|
| 229 |
+
|
| 230 |
+
assert hasattr(spring, 'pathway')
|
| 231 |
+
assert isinstance(spring.pathway, LinearPathway)
|
| 232 |
+
assert spring.pathway == self.pathway
|
| 233 |
+
|
| 234 |
+
assert hasattr(spring, 'equilibrium_length')
|
| 235 |
+
assert isinstance(spring.equilibrium_length, ExprType)
|
| 236 |
+
assert spring.equilibrium_length == expected_equilibrium_length
|
| 237 |
+
|
| 238 |
+
assert hasattr(spring, 'force')
|
| 239 |
+
assert isinstance(spring.force, ExprType)
|
| 240 |
+
assert spring.force == force
|
| 241 |
+
|
| 242 |
+
@pytest.mark.parametrize('stiffness', [None, 'k'])
|
| 243 |
+
def test_invalid_constructor_stiffness_not_sympifyable(self, stiffness):
|
| 244 |
+
with pytest.raises(SympifyError):
|
| 245 |
+
_ = LinearSpring(stiffness, self.pathway, self.l)
|
| 246 |
+
|
| 247 |
+
def test_invalid_constructor_pathway_not_pathway_base(self):
|
| 248 |
+
with pytest.raises(TypeError):
|
| 249 |
+
_ = LinearSpring(self.stiffness, None, self.l)
|
| 250 |
+
|
| 251 |
+
@pytest.mark.parametrize('equilibrium_length', [None, 'l'])
|
| 252 |
+
def test_invalid_constructor_equilibrium_length_not_sympifyable(
|
| 253 |
+
self,
|
| 254 |
+
equilibrium_length,
|
| 255 |
+
):
|
| 256 |
+
with pytest.raises(SympifyError):
|
| 257 |
+
_ = LinearSpring(self.stiffness, self.pathway, equilibrium_length)
|
| 258 |
+
|
| 259 |
+
@pytest.mark.parametrize(
|
| 260 |
+
'property_name, fixture_attr_name',
|
| 261 |
+
[
|
| 262 |
+
('stiffness', 'stiffness'),
|
| 263 |
+
('pathway', 'pathway'),
|
| 264 |
+
('equilibrium_length', 'l'),
|
| 265 |
+
]
|
| 266 |
+
)
|
| 267 |
+
def test_properties_are_immutable(self, property_name, fixture_attr_name):
|
| 268 |
+
spring = LinearSpring(self.stiffness, self.pathway, self.l)
|
| 269 |
+
value = getattr(self, fixture_attr_name)
|
| 270 |
+
with pytest.raises(AttributeError):
|
| 271 |
+
setattr(spring, property_name, value)
|
| 272 |
+
|
| 273 |
+
@pytest.mark.parametrize(
|
| 274 |
+
'equilibrium_length, expected',
|
| 275 |
+
[
|
| 276 |
+
(S.Zero, 'LinearSpring(k, LinearPathway(pA, pB))'),
|
| 277 |
+
(
|
| 278 |
+
Symbol('l'),
|
| 279 |
+
'LinearSpring(k, LinearPathway(pA, pB), equilibrium_length=l)',
|
| 280 |
+
),
|
| 281 |
+
]
|
| 282 |
+
)
|
| 283 |
+
def test_repr(self, equilibrium_length, expected):
|
| 284 |
+
self.pB.set_pos(self.pA, self.q*self.N.x)
|
| 285 |
+
spring = LinearSpring(self.stiffness, self.pathway, equilibrium_length)
|
| 286 |
+
assert repr(spring) == expected
|
| 287 |
+
|
| 288 |
+
def test_to_loads(self):
|
| 289 |
+
self.pB.set_pos(self.pA, self.q*self.N.x)
|
| 290 |
+
spring = LinearSpring(self.stiffness, self.pathway, self.l)
|
| 291 |
+
normal = self.q/sqrt(self.q**2)*self.N.x
|
| 292 |
+
pA_force = self.stiffness*(sqrt(self.q**2) - self.l)*normal
|
| 293 |
+
pB_force = -self.stiffness*(sqrt(self.q**2) - self.l)*normal
|
| 294 |
+
expected = [Force(self.pA, pA_force), Force(self.pB, pB_force)]
|
| 295 |
+
loads = spring.to_loads()
|
| 296 |
+
|
| 297 |
+
for load, (point, vector) in zip(loads, expected):
|
| 298 |
+
assert isinstance(load, Force)
|
| 299 |
+
assert load.point == point
|
| 300 |
+
assert (load.vector - vector).simplify() == 0
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
class TestLinearDamper:
|
| 304 |
+
|
| 305 |
+
@pytest.fixture(autouse=True)
|
| 306 |
+
def _linear_damper_fixture(self):
|
| 307 |
+
self.damping = Symbol('c')
|
| 308 |
+
self.l = Symbol('l')
|
| 309 |
+
self.pA = Point('pA')
|
| 310 |
+
self.pB = Point('pB')
|
| 311 |
+
self.pathway = LinearPathway(self.pA, self.pB)
|
| 312 |
+
self.q = dynamicsymbols('q')
|
| 313 |
+
self.dq = dynamicsymbols('q', 1)
|
| 314 |
+
self.u = dynamicsymbols('u')
|
| 315 |
+
self.N = ReferenceFrame('N')
|
| 316 |
+
|
| 317 |
+
def test_is_force_actuator_subclass(self):
|
| 318 |
+
assert issubclass(LinearDamper, ForceActuator)
|
| 319 |
+
|
| 320 |
+
def test_is_actuator_base_subclass(self):
|
| 321 |
+
assert issubclass(LinearDamper, ActuatorBase)
|
| 322 |
+
|
| 323 |
+
def test_valid_constructor(self):
|
| 324 |
+
self.pB.set_pos(self.pA, self.q*self.N.x)
|
| 325 |
+
damper = LinearDamper(self.damping, self.pathway)
|
| 326 |
+
|
| 327 |
+
assert isinstance(damper, LinearDamper)
|
| 328 |
+
|
| 329 |
+
assert hasattr(damper, 'damping')
|
| 330 |
+
assert isinstance(damper.damping, ExprType)
|
| 331 |
+
assert damper.damping == self.damping
|
| 332 |
+
|
| 333 |
+
assert hasattr(damper, 'pathway')
|
| 334 |
+
assert isinstance(damper.pathway, LinearPathway)
|
| 335 |
+
assert damper.pathway == self.pathway
|
| 336 |
+
|
| 337 |
+
def test_valid_constructor_force(self):
|
| 338 |
+
self.pB.set_pos(self.pA, self.q*self.N.x)
|
| 339 |
+
damper = LinearDamper(self.damping, self.pathway)
|
| 340 |
+
|
| 341 |
+
expected_force = -self.damping*sqrt(self.q**2)*self.dq/self.q
|
| 342 |
+
assert hasattr(damper, 'force')
|
| 343 |
+
assert isinstance(damper.force, ExprType)
|
| 344 |
+
assert damper.force == expected_force
|
| 345 |
+
|
| 346 |
+
@pytest.mark.parametrize('damping', [None, 'c'])
|
| 347 |
+
def test_invalid_constructor_damping_not_sympifyable(self, damping):
|
| 348 |
+
with pytest.raises(SympifyError):
|
| 349 |
+
_ = LinearDamper(damping, self.pathway)
|
| 350 |
+
|
| 351 |
+
def test_invalid_constructor_pathway_not_pathway_base(self):
|
| 352 |
+
with pytest.raises(TypeError):
|
| 353 |
+
_ = LinearDamper(self.damping, None)
|
| 354 |
+
|
| 355 |
+
@pytest.mark.parametrize(
|
| 356 |
+
'property_name, fixture_attr_name',
|
| 357 |
+
[
|
| 358 |
+
('damping', 'damping'),
|
| 359 |
+
('pathway', 'pathway'),
|
| 360 |
+
]
|
| 361 |
+
)
|
| 362 |
+
def test_properties_are_immutable(self, property_name, fixture_attr_name):
|
| 363 |
+
damper = LinearDamper(self.damping, self.pathway)
|
| 364 |
+
value = getattr(self, fixture_attr_name)
|
| 365 |
+
with pytest.raises(AttributeError):
|
| 366 |
+
setattr(damper, property_name, value)
|
| 367 |
+
|
| 368 |
+
def test_repr(self):
|
| 369 |
+
self.pB.set_pos(self.pA, self.q*self.N.x)
|
| 370 |
+
damper = LinearDamper(self.damping, self.pathway)
|
| 371 |
+
expected = 'LinearDamper(c, LinearPathway(pA, pB))'
|
| 372 |
+
assert repr(damper) == expected
|
| 373 |
+
|
| 374 |
+
def test_to_loads(self):
|
| 375 |
+
self.pB.set_pos(self.pA, self.q*self.N.x)
|
| 376 |
+
damper = LinearDamper(self.damping, self.pathway)
|
| 377 |
+
direction = self.q**2/self.q**2*self.N.x
|
| 378 |
+
pA_force = self.damping*self.dq*direction
|
| 379 |
+
pB_force = -self.damping*self.dq*direction
|
| 380 |
+
expected = [Force(self.pA, pA_force), Force(self.pB, pB_force)]
|
| 381 |
+
assert damper.to_loads() == expected
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
class TestForcedMassSpringDamperModel():
|
| 385 |
+
r"""A single degree of freedom translational forced mass-spring-damper.
|
| 386 |
+
|
| 387 |
+
Notes
|
| 388 |
+
=====
|
| 389 |
+
|
| 390 |
+
This system is well known to have the governing equation:
|
| 391 |
+
|
| 392 |
+
.. math::
|
| 393 |
+
m \ddot{x} = F - k x - c \dot{x}
|
| 394 |
+
|
| 395 |
+
where $F$ is an externally applied force, $m$ is the mass of the particle
|
| 396 |
+
to which the spring and damper are attached, $k$ is the spring's stiffness,
|
| 397 |
+
$c$ is the dampers damping coefficient, and $x$ is the generalized
|
| 398 |
+
coordinate representing the system's single (translational) degree of
|
| 399 |
+
freedom.
|
| 400 |
+
|
| 401 |
+
"""
|
| 402 |
+
|
| 403 |
+
@pytest.fixture(autouse=True)
|
| 404 |
+
def _force_mass_spring_damper_model_fixture(self):
|
| 405 |
+
self.m = Symbol('m')
|
| 406 |
+
self.k = Symbol('k')
|
| 407 |
+
self.c = Symbol('c')
|
| 408 |
+
self.F = Symbol('F')
|
| 409 |
+
|
| 410 |
+
self.q = dynamicsymbols('q')
|
| 411 |
+
self.dq = dynamicsymbols('q', 1)
|
| 412 |
+
self.u = dynamicsymbols('u')
|
| 413 |
+
|
| 414 |
+
self.frame = ReferenceFrame('N')
|
| 415 |
+
self.origin = Point('pO')
|
| 416 |
+
self.origin.set_vel(self.frame, 0)
|
| 417 |
+
|
| 418 |
+
self.attachment = Point('pA')
|
| 419 |
+
self.attachment.set_pos(self.origin, self.q*self.frame.x)
|
| 420 |
+
|
| 421 |
+
self.mass = Particle('mass', self.attachment, self.m)
|
| 422 |
+
self.pathway = LinearPathway(self.origin, self.attachment)
|
| 423 |
+
|
| 424 |
+
self.kanes_method = KanesMethod(
|
| 425 |
+
self.frame,
|
| 426 |
+
q_ind=[self.q],
|
| 427 |
+
u_ind=[self.u],
|
| 428 |
+
kd_eqs=[self.dq - self.u],
|
| 429 |
+
)
|
| 430 |
+
self.bodies = [self.mass]
|
| 431 |
+
|
| 432 |
+
self.mass_matrix = Matrix([[self.m]])
|
| 433 |
+
self.forcing = Matrix([[self.F - self.c*self.u - self.k*self.q]])
|
| 434 |
+
|
| 435 |
+
def test_force_acuator(self):
|
| 436 |
+
stiffness = -self.k*self.pathway.length
|
| 437 |
+
spring = ForceActuator(stiffness, self.pathway)
|
| 438 |
+
damping = -self.c*self.pathway.extension_velocity
|
| 439 |
+
damper = ForceActuator(damping, self.pathway)
|
| 440 |
+
|
| 441 |
+
loads = [
|
| 442 |
+
(self.attachment, self.F*self.frame.x),
|
| 443 |
+
*spring.to_loads(),
|
| 444 |
+
*damper.to_loads(),
|
| 445 |
+
]
|
| 446 |
+
self.kanes_method.kanes_equations(self.bodies, loads)
|
| 447 |
+
|
| 448 |
+
assert self.kanes_method.mass_matrix == self.mass_matrix
|
| 449 |
+
assert self.kanes_method.forcing == self.forcing
|
| 450 |
+
|
| 451 |
+
def test_linear_spring_linear_damper(self):
|
| 452 |
+
spring = LinearSpring(self.k, self.pathway)
|
| 453 |
+
damper = LinearDamper(self.c, self.pathway)
|
| 454 |
+
|
| 455 |
+
loads = [
|
| 456 |
+
(self.attachment, self.F*self.frame.x),
|
| 457 |
+
*spring.to_loads(),
|
| 458 |
+
*damper.to_loads(),
|
| 459 |
+
]
|
| 460 |
+
self.kanes_method.kanes_equations(self.bodies, loads)
|
| 461 |
+
|
| 462 |
+
assert self.kanes_method.mass_matrix == self.mass_matrix
|
| 463 |
+
assert self.kanes_method.forcing == self.forcing
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
class TestTorqueActuator:
|
| 467 |
+
|
| 468 |
+
@pytest.fixture(autouse=True)
|
| 469 |
+
def _torque_actuator_fixture(self):
|
| 470 |
+
self.torque = Symbol('T')
|
| 471 |
+
self.N = ReferenceFrame('N')
|
| 472 |
+
self.A = ReferenceFrame('A')
|
| 473 |
+
self.axis = self.N.z
|
| 474 |
+
self.target = RigidBody('target', frame=self.N)
|
| 475 |
+
self.reaction = RigidBody('reaction', frame=self.A)
|
| 476 |
+
|
| 477 |
+
def test_is_actuator_base_subclass(self):
|
| 478 |
+
assert issubclass(TorqueActuator, ActuatorBase)
|
| 479 |
+
|
| 480 |
+
@pytest.mark.parametrize(
|
| 481 |
+
'torque',
|
| 482 |
+
[
|
| 483 |
+
Symbol('T'),
|
| 484 |
+
dynamicsymbols('T'),
|
| 485 |
+
Symbol('T')**2 + Symbol('T'),
|
| 486 |
+
]
|
| 487 |
+
)
|
| 488 |
+
@pytest.mark.parametrize(
|
| 489 |
+
'target_frame, reaction_frame',
|
| 490 |
+
[
|
| 491 |
+
(target.frame, reaction.frame),
|
| 492 |
+
(target, reaction.frame),
|
| 493 |
+
(target.frame, reaction),
|
| 494 |
+
(target, reaction),
|
| 495 |
+
]
|
| 496 |
+
)
|
| 497 |
+
def test_valid_constructor_with_reaction(
|
| 498 |
+
self,
|
| 499 |
+
torque,
|
| 500 |
+
target_frame,
|
| 501 |
+
reaction_frame,
|
| 502 |
+
):
|
| 503 |
+
instance = TorqueActuator(
|
| 504 |
+
torque,
|
| 505 |
+
self.axis,
|
| 506 |
+
target_frame,
|
| 507 |
+
reaction_frame,
|
| 508 |
+
)
|
| 509 |
+
assert isinstance(instance, TorqueActuator)
|
| 510 |
+
|
| 511 |
+
assert hasattr(instance, 'torque')
|
| 512 |
+
assert isinstance(instance.torque, ExprType)
|
| 513 |
+
assert instance.torque == torque
|
| 514 |
+
|
| 515 |
+
assert hasattr(instance, 'axis')
|
| 516 |
+
assert isinstance(instance.axis, Vector)
|
| 517 |
+
assert instance.axis == self.axis
|
| 518 |
+
|
| 519 |
+
assert hasattr(instance, 'target_frame')
|
| 520 |
+
assert isinstance(instance.target_frame, ReferenceFrame)
|
| 521 |
+
assert instance.target_frame == target.frame
|
| 522 |
+
|
| 523 |
+
assert hasattr(instance, 'reaction_frame')
|
| 524 |
+
assert isinstance(instance.reaction_frame, ReferenceFrame)
|
| 525 |
+
assert instance.reaction_frame == reaction.frame
|
| 526 |
+
|
| 527 |
+
@pytest.mark.parametrize(
|
| 528 |
+
'torque',
|
| 529 |
+
[
|
| 530 |
+
Symbol('T'),
|
| 531 |
+
dynamicsymbols('T'),
|
| 532 |
+
Symbol('T')**2 + Symbol('T'),
|
| 533 |
+
]
|
| 534 |
+
)
|
| 535 |
+
@pytest.mark.parametrize('target_frame', [target.frame, target])
|
| 536 |
+
def test_valid_constructor_without_reaction(self, torque, target_frame):
|
| 537 |
+
instance = TorqueActuator(torque, self.axis, target_frame)
|
| 538 |
+
assert isinstance(instance, TorqueActuator)
|
| 539 |
+
|
| 540 |
+
assert hasattr(instance, 'torque')
|
| 541 |
+
assert isinstance(instance.torque, ExprType)
|
| 542 |
+
assert instance.torque == torque
|
| 543 |
+
|
| 544 |
+
assert hasattr(instance, 'axis')
|
| 545 |
+
assert isinstance(instance.axis, Vector)
|
| 546 |
+
assert instance.axis == self.axis
|
| 547 |
+
|
| 548 |
+
assert hasattr(instance, 'target_frame')
|
| 549 |
+
assert isinstance(instance.target_frame, ReferenceFrame)
|
| 550 |
+
assert instance.target_frame == target.frame
|
| 551 |
+
|
| 552 |
+
assert hasattr(instance, 'reaction_frame')
|
| 553 |
+
assert instance.reaction_frame is None
|
| 554 |
+
|
| 555 |
+
@pytest.mark.parametrize('torque', [None, 'T'])
|
| 556 |
+
def test_invalid_constructor_torque_not_sympifyable(self, torque):
|
| 557 |
+
with pytest.raises(SympifyError):
|
| 558 |
+
_ = TorqueActuator(torque, self.axis, self.target)
|
| 559 |
+
|
| 560 |
+
@pytest.mark.parametrize('axis', [Symbol('a'), dynamicsymbols('a')])
|
| 561 |
+
def test_invalid_constructor_axis_not_vector(self, axis):
|
| 562 |
+
with pytest.raises(TypeError):
|
| 563 |
+
_ = TorqueActuator(self.torque, axis, self.target, self.reaction)
|
| 564 |
+
|
| 565 |
+
@pytest.mark.parametrize(
|
| 566 |
+
'frames',
|
| 567 |
+
[
|
| 568 |
+
(None, ReferenceFrame('child')),
|
| 569 |
+
(ReferenceFrame('parent'), True),
|
| 570 |
+
(None, RigidBody('child')),
|
| 571 |
+
(RigidBody('parent'), True),
|
| 572 |
+
]
|
| 573 |
+
)
|
| 574 |
+
def test_invalid_constructor_frames_not_frame(self, frames):
|
| 575 |
+
with pytest.raises(TypeError):
|
| 576 |
+
_ = TorqueActuator(self.torque, self.axis, *frames)
|
| 577 |
+
|
| 578 |
+
@pytest.mark.parametrize(
|
| 579 |
+
'property_name, fixture_attr_name',
|
| 580 |
+
[
|
| 581 |
+
('torque', 'torque'),
|
| 582 |
+
('axis', 'axis'),
|
| 583 |
+
('target_frame', 'target'),
|
| 584 |
+
('reaction_frame', 'reaction'),
|
| 585 |
+
]
|
| 586 |
+
)
|
| 587 |
+
def test_properties_are_immutable(self, property_name, fixture_attr_name):
|
| 588 |
+
actuator = TorqueActuator(
|
| 589 |
+
self.torque,
|
| 590 |
+
self.axis,
|
| 591 |
+
self.target,
|
| 592 |
+
self.reaction,
|
| 593 |
+
)
|
| 594 |
+
value = getattr(self, fixture_attr_name)
|
| 595 |
+
with pytest.raises(AttributeError):
|
| 596 |
+
setattr(actuator, property_name, value)
|
| 597 |
+
|
| 598 |
+
def test_repr_without_reaction(self):
|
| 599 |
+
actuator = TorqueActuator(self.torque, self.axis, self.target)
|
| 600 |
+
expected = 'TorqueActuator(T, axis=N.z, target_frame=N)'
|
| 601 |
+
assert repr(actuator) == expected
|
| 602 |
+
|
| 603 |
+
def test_repr_with_reaction(self):
|
| 604 |
+
actuator = TorqueActuator(
|
| 605 |
+
self.torque,
|
| 606 |
+
self.axis,
|
| 607 |
+
self.target,
|
| 608 |
+
self.reaction,
|
| 609 |
+
)
|
| 610 |
+
expected = 'TorqueActuator(T, axis=N.z, target_frame=N, reaction_frame=A)'
|
| 611 |
+
assert repr(actuator) == expected
|
| 612 |
+
|
| 613 |
+
def test_at_pin_joint_constructor(self):
|
| 614 |
+
pin_joint = PinJoint(
|
| 615 |
+
'pin',
|
| 616 |
+
self.target,
|
| 617 |
+
self.reaction,
|
| 618 |
+
coordinates=dynamicsymbols('q'),
|
| 619 |
+
speeds=dynamicsymbols('u'),
|
| 620 |
+
parent_interframe=self.N,
|
| 621 |
+
joint_axis=self.axis,
|
| 622 |
+
)
|
| 623 |
+
instance = TorqueActuator.at_pin_joint(self.torque, pin_joint)
|
| 624 |
+
assert isinstance(instance, TorqueActuator)
|
| 625 |
+
|
| 626 |
+
assert hasattr(instance, 'torque')
|
| 627 |
+
assert isinstance(instance.torque, ExprType)
|
| 628 |
+
assert instance.torque == self.torque
|
| 629 |
+
|
| 630 |
+
assert hasattr(instance, 'axis')
|
| 631 |
+
assert isinstance(instance.axis, Vector)
|
| 632 |
+
assert instance.axis == self.axis
|
| 633 |
+
|
| 634 |
+
assert hasattr(instance, 'target_frame')
|
| 635 |
+
assert isinstance(instance.target_frame, ReferenceFrame)
|
| 636 |
+
assert instance.target_frame == self.A
|
| 637 |
+
|
| 638 |
+
assert hasattr(instance, 'reaction_frame')
|
| 639 |
+
assert isinstance(instance.reaction_frame, ReferenceFrame)
|
| 640 |
+
assert instance.reaction_frame == self.N
|
| 641 |
+
|
| 642 |
+
def test_at_pin_joint_pin_joint_not_pin_joint_invalid(self):
|
| 643 |
+
with pytest.raises(TypeError):
|
| 644 |
+
_ = TorqueActuator.at_pin_joint(self.torque, Symbol('pin'))
|
| 645 |
+
|
| 646 |
+
def test_to_loads_without_reaction(self):
|
| 647 |
+
actuator = TorqueActuator(self.torque, self.axis, self.target)
|
| 648 |
+
expected = [
|
| 649 |
+
(self.N, self.torque*self.axis),
|
| 650 |
+
]
|
| 651 |
+
assert actuator.to_loads() == expected
|
| 652 |
+
|
| 653 |
+
def test_to_loads_with_reaction(self):
|
| 654 |
+
actuator = TorqueActuator(
|
| 655 |
+
self.torque,
|
| 656 |
+
self.axis,
|
| 657 |
+
self.target,
|
| 658 |
+
self.reaction,
|
| 659 |
+
)
|
| 660 |
+
expected = [
|
| 661 |
+
(self.N, self.torque*self.axis),
|
| 662 |
+
(self.A, - self.torque*self.axis),
|
| 663 |
+
]
|
| 664 |
+
assert actuator.to_loads() == expected
|
| 665 |
+
|
| 666 |
+
|
| 667 |
+
class NonSympifyable:
|
| 668 |
+
pass
|
| 669 |
+
|
| 670 |
+
|
| 671 |
+
class TestDuffingSpring:
|
| 672 |
+
@pytest.fixture(autouse=True)
|
| 673 |
+
# Set up common vairables that will be used in multiple tests
|
| 674 |
+
def _duffing_spring_fixture(self):
|
| 675 |
+
self.linear_stiffness = Symbol('beta')
|
| 676 |
+
self.nonlinear_stiffness = Symbol('alpha')
|
| 677 |
+
self.equilibrium_length = Symbol('l')
|
| 678 |
+
self.pA = Point('pA')
|
| 679 |
+
self.pB = Point('pB')
|
| 680 |
+
self.pathway = LinearPathway(self.pA, self.pB)
|
| 681 |
+
self.q = dynamicsymbols('q')
|
| 682 |
+
self.N = ReferenceFrame('N')
|
| 683 |
+
|
| 684 |
+
# Simples tests to check that DuffingSpring is a subclass of ForceActuator and ActuatorBase
|
| 685 |
+
def test_is_force_actuator_subclass(self):
|
| 686 |
+
assert issubclass(DuffingSpring, ForceActuator)
|
| 687 |
+
|
| 688 |
+
def test_is_actuator_base_subclass(self):
|
| 689 |
+
assert issubclass(DuffingSpring, ActuatorBase)
|
| 690 |
+
|
| 691 |
+
@pytest.mark.parametrize(
|
| 692 |
+
# Create parametrized tests that allows running the same test function multiple times with different sets of arguments
|
| 693 |
+
(
|
| 694 |
+
'linear_stiffness, '
|
| 695 |
+
'expected_linear_stiffness, '
|
| 696 |
+
'nonlinear_stiffness, '
|
| 697 |
+
'expected_nonlinear_stiffness, '
|
| 698 |
+
'equilibrium_length, '
|
| 699 |
+
'expected_equilibrium_length, '
|
| 700 |
+
'force'
|
| 701 |
+
),
|
| 702 |
+
[
|
| 703 |
+
(
|
| 704 |
+
1,
|
| 705 |
+
S.One,
|
| 706 |
+
1,
|
| 707 |
+
S.One,
|
| 708 |
+
0,
|
| 709 |
+
S.Zero,
|
| 710 |
+
-sqrt(dynamicsymbols('q')**2)-(sqrt(dynamicsymbols('q')**2))**3,
|
| 711 |
+
),
|
| 712 |
+
(
|
| 713 |
+
Symbol('beta'),
|
| 714 |
+
Symbol('beta'),
|
| 715 |
+
Symbol('alpha'),
|
| 716 |
+
Symbol('alpha'),
|
| 717 |
+
0,
|
| 718 |
+
S.Zero,
|
| 719 |
+
-Symbol('beta')*sqrt(dynamicsymbols('q')**2)-Symbol('alpha')*(sqrt(dynamicsymbols('q')**2))**3,
|
| 720 |
+
),
|
| 721 |
+
(
|
| 722 |
+
Symbol('beta'),
|
| 723 |
+
Symbol('beta'),
|
| 724 |
+
Symbol('alpha'),
|
| 725 |
+
Symbol('alpha'),
|
| 726 |
+
S.Zero,
|
| 727 |
+
S.Zero,
|
| 728 |
+
-Symbol('beta')*sqrt(dynamicsymbols('q')**2)-Symbol('alpha')*(sqrt(dynamicsymbols('q')**2))**3,
|
| 729 |
+
),
|
| 730 |
+
(
|
| 731 |
+
Symbol('beta'),
|
| 732 |
+
Symbol('beta'),
|
| 733 |
+
Symbol('alpha'),
|
| 734 |
+
Symbol('alpha'),
|
| 735 |
+
Symbol('l'),
|
| 736 |
+
Symbol('l'),
|
| 737 |
+
-Symbol('beta') * (sqrt(dynamicsymbols('q')**2) - Symbol('l')) - Symbol('alpha') * (sqrt(dynamicsymbols('q')**2) - Symbol('l'))**3,
|
| 738 |
+
),
|
| 739 |
+
]
|
| 740 |
+
)
|
| 741 |
+
|
| 742 |
+
# Check if DuffingSpring correctly inializes its attributes
|
| 743 |
+
# It tests various combinations of linear & nonlinear stiffness, equilibriun length, and the resulting force expression
|
| 744 |
+
def test_valid_constructor(
|
| 745 |
+
self,
|
| 746 |
+
linear_stiffness,
|
| 747 |
+
expected_linear_stiffness,
|
| 748 |
+
nonlinear_stiffness,
|
| 749 |
+
expected_nonlinear_stiffness,
|
| 750 |
+
equilibrium_length,
|
| 751 |
+
expected_equilibrium_length,
|
| 752 |
+
force,
|
| 753 |
+
):
|
| 754 |
+
self.pB.set_pos(self.pA, self.q*self.N.x)
|
| 755 |
+
spring = DuffingSpring(linear_stiffness, nonlinear_stiffness, self.pathway, equilibrium_length)
|
| 756 |
+
|
| 757 |
+
assert isinstance(spring, DuffingSpring)
|
| 758 |
+
|
| 759 |
+
assert hasattr(spring, 'linear_stiffness')
|
| 760 |
+
assert isinstance(spring.linear_stiffness, ExprType)
|
| 761 |
+
assert spring.linear_stiffness == expected_linear_stiffness
|
| 762 |
+
|
| 763 |
+
assert hasattr(spring, 'nonlinear_stiffness')
|
| 764 |
+
assert isinstance(spring.nonlinear_stiffness, ExprType)
|
| 765 |
+
assert spring.nonlinear_stiffness == expected_nonlinear_stiffness
|
| 766 |
+
|
| 767 |
+
assert hasattr(spring, 'pathway')
|
| 768 |
+
assert isinstance(spring.pathway, LinearPathway)
|
| 769 |
+
assert spring.pathway == self.pathway
|
| 770 |
+
|
| 771 |
+
assert hasattr(spring, 'equilibrium_length')
|
| 772 |
+
assert isinstance(spring.equilibrium_length, ExprType)
|
| 773 |
+
assert spring.equilibrium_length == expected_equilibrium_length
|
| 774 |
+
|
| 775 |
+
assert hasattr(spring, 'force')
|
| 776 |
+
assert isinstance(spring.force, ExprType)
|
| 777 |
+
assert spring.force == force
|
| 778 |
+
|
| 779 |
+
@pytest.mark.parametrize('linear_stiffness', [None, NonSympifyable()])
|
| 780 |
+
def test_invalid_constructor_linear_stiffness_not_sympifyable(self, linear_stiffness):
|
| 781 |
+
with pytest.raises(SympifyError):
|
| 782 |
+
_ = DuffingSpring(linear_stiffness, self.nonlinear_stiffness, self.pathway, self.equilibrium_length)
|
| 783 |
+
|
| 784 |
+
@pytest.mark.parametrize('nonlinear_stiffness', [None, NonSympifyable()])
|
| 785 |
+
def test_invalid_constructor_nonlinear_stiffness_not_sympifyable(self, nonlinear_stiffness):
|
| 786 |
+
with pytest.raises(SympifyError):
|
| 787 |
+
_ = DuffingSpring(self.linear_stiffness, nonlinear_stiffness, self.pathway, self.equilibrium_length)
|
| 788 |
+
|
| 789 |
+
def test_invalid_constructor_pathway_not_pathway_base(self):
|
| 790 |
+
with pytest.raises(TypeError):
|
| 791 |
+
_ = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, NonSympifyable(), self.equilibrium_length)
|
| 792 |
+
|
| 793 |
+
@pytest.mark.parametrize('equilibrium_length', [None, NonSympifyable()])
|
| 794 |
+
def test_invalid_constructor_equilibrium_length_not_sympifyable(self, equilibrium_length):
|
| 795 |
+
with pytest.raises(SympifyError):
|
| 796 |
+
_ = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, self.pathway, equilibrium_length)
|
| 797 |
+
|
| 798 |
+
@pytest.mark.parametrize(
|
| 799 |
+
'property_name, fixture_attr_name',
|
| 800 |
+
[
|
| 801 |
+
('linear_stiffness', 'linear_stiffness'),
|
| 802 |
+
('nonlinear_stiffness', 'nonlinear_stiffness'),
|
| 803 |
+
('pathway', 'pathway'),
|
| 804 |
+
('equilibrium_length', 'equilibrium_length')
|
| 805 |
+
]
|
| 806 |
+
)
|
| 807 |
+
# Check if certain properties of DuffingSpring object are immutable after initialization
|
| 808 |
+
# Ensure that once DuffingSpring is created, its key properties cannot be changed
|
| 809 |
+
def test_properties_are_immutable(self, property_name, fixture_attr_name):
|
| 810 |
+
spring = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, self.pathway, self.equilibrium_length)
|
| 811 |
+
with pytest.raises(AttributeError):
|
| 812 |
+
setattr(spring, property_name, getattr(self, fixture_attr_name))
|
| 813 |
+
|
| 814 |
+
@pytest.mark.parametrize(
|
| 815 |
+
'equilibrium_length, expected',
|
| 816 |
+
[
|
| 817 |
+
(0, 'DuffingSpring(beta, alpha, LinearPathway(pA, pB), equilibrium_length=0)'),
|
| 818 |
+
(Symbol('l'), 'DuffingSpring(beta, alpha, LinearPathway(pA, pB), equilibrium_length=l)'),
|
| 819 |
+
]
|
| 820 |
+
)
|
| 821 |
+
# Check the __repr__ method of DuffingSpring class
|
| 822 |
+
# Check if the actual string representation of DuffingSpring instance matches the expected string for each provided parameter values
|
| 823 |
+
def test_repr(self, equilibrium_length, expected):
|
| 824 |
+
spring = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, self.pathway, equilibrium_length)
|
| 825 |
+
assert repr(spring) == expected
|
| 826 |
+
|
| 827 |
+
def test_to_loads(self):
|
| 828 |
+
self.pB.set_pos(self.pA, self.q*self.N.x)
|
| 829 |
+
spring = DuffingSpring(self.linear_stiffness, self.nonlinear_stiffness, self.pathway, self.equilibrium_length)
|
| 830 |
+
|
| 831 |
+
# Calculate the displacement from the equilibrium length
|
| 832 |
+
displacement = self.q - self.equilibrium_length
|
| 833 |
+
|
| 834 |
+
# Make sure this matches the computation in DuffingSpring class
|
| 835 |
+
force = -self.linear_stiffness * displacement - self.nonlinear_stiffness * displacement**3
|
| 836 |
+
|
| 837 |
+
# The expected loads on pA and pB due to the spring
|
| 838 |
+
expected_loads = [Force(self.pA, force * self.N.x), Force(self.pB, -force * self.N.x)]
|
| 839 |
+
|
| 840 |
+
# Compare expected loads to what is returned from DuffingSpring.to_loads()
|
| 841 |
+
calculated_loads = spring.to_loads()
|
| 842 |
+
for calculated, expected in zip(calculated_loads, expected_loads):
|
| 843 |
+
assert calculated.point == expected.point
|
| 844 |
+
for dim in self.N: # Assuming self.N is the reference frame
|
| 845 |
+
calculated_component = calculated.vector.dot(dim)
|
| 846 |
+
expected_component = expected.vector.dot(dim)
|
| 847 |
+
# Substitute all symbols with numeric values
|
| 848 |
+
substitutions = {self.q: 1, Symbol('l'): 1, Symbol('alpha'): 1, Symbol('beta'): 1} # Add other necessary symbols as needed
|
| 849 |
+
diff = (calculated_component - expected_component).subs(substitutions).evalf()
|
| 850 |
+
# Check if the absolute value of the difference is below a threshold
|
| 851 |
+
assert Abs(diff) < 1e-9, f"The forces do not match. Difference: {diff}"
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_body.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import (Symbol, symbols, sin, cos, Matrix, zeros,
|
| 2 |
+
simplify)
|
| 3 |
+
from sympy.physics.vector import Point, ReferenceFrame, dynamicsymbols, Dyadic
|
| 4 |
+
from sympy.physics.mechanics import inertia, Body
|
| 5 |
+
from sympy.testing.pytest import raises, warns_deprecated_sympy
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def test_default():
|
| 9 |
+
with warns_deprecated_sympy():
|
| 10 |
+
body = Body('body')
|
| 11 |
+
assert body.name == 'body'
|
| 12 |
+
assert body.loads == []
|
| 13 |
+
point = Point('body_masscenter')
|
| 14 |
+
point.set_vel(body.frame, 0)
|
| 15 |
+
com = body.masscenter
|
| 16 |
+
frame = body.frame
|
| 17 |
+
assert com.vel(frame) == point.vel(frame)
|
| 18 |
+
assert body.mass == Symbol('body_mass')
|
| 19 |
+
ixx, iyy, izz = symbols('body_ixx body_iyy body_izz')
|
| 20 |
+
ixy, iyz, izx = symbols('body_ixy body_iyz body_izx')
|
| 21 |
+
assert body.inertia == (inertia(body.frame, ixx, iyy, izz, ixy, iyz, izx),
|
| 22 |
+
body.masscenter)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def test_custom_rigid_body():
|
| 26 |
+
# Body with RigidBody.
|
| 27 |
+
rigidbody_masscenter = Point('rigidbody_masscenter')
|
| 28 |
+
rigidbody_mass = Symbol('rigidbody_mass')
|
| 29 |
+
rigidbody_frame = ReferenceFrame('rigidbody_frame')
|
| 30 |
+
body_inertia = inertia(rigidbody_frame, 1, 0, 0)
|
| 31 |
+
with warns_deprecated_sympy():
|
| 32 |
+
rigid_body = Body('rigidbody_body', rigidbody_masscenter,
|
| 33 |
+
rigidbody_mass, rigidbody_frame, body_inertia)
|
| 34 |
+
com = rigid_body.masscenter
|
| 35 |
+
frame = rigid_body.frame
|
| 36 |
+
rigidbody_masscenter.set_vel(rigidbody_frame, 0)
|
| 37 |
+
assert com.vel(frame) == rigidbody_masscenter.vel(frame)
|
| 38 |
+
assert com.pos_from(com) == rigidbody_masscenter.pos_from(com)
|
| 39 |
+
|
| 40 |
+
assert rigid_body.mass == rigidbody_mass
|
| 41 |
+
assert rigid_body.inertia == (body_inertia, rigidbody_masscenter)
|
| 42 |
+
|
| 43 |
+
assert rigid_body.is_rigidbody
|
| 44 |
+
|
| 45 |
+
assert hasattr(rigid_body, 'masscenter')
|
| 46 |
+
assert hasattr(rigid_body, 'mass')
|
| 47 |
+
assert hasattr(rigid_body, 'frame')
|
| 48 |
+
assert hasattr(rigid_body, 'inertia')
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def test_particle_body():
|
| 52 |
+
# Body with Particle
|
| 53 |
+
particle_masscenter = Point('particle_masscenter')
|
| 54 |
+
particle_mass = Symbol('particle_mass')
|
| 55 |
+
particle_frame = ReferenceFrame('particle_frame')
|
| 56 |
+
with warns_deprecated_sympy():
|
| 57 |
+
particle_body = Body('particle_body', particle_masscenter,
|
| 58 |
+
particle_mass, particle_frame)
|
| 59 |
+
com = particle_body.masscenter
|
| 60 |
+
frame = particle_body.frame
|
| 61 |
+
particle_masscenter.set_vel(particle_frame, 0)
|
| 62 |
+
assert com.vel(frame) == particle_masscenter.vel(frame)
|
| 63 |
+
assert com.pos_from(com) == particle_masscenter.pos_from(com)
|
| 64 |
+
|
| 65 |
+
assert particle_body.mass == particle_mass
|
| 66 |
+
assert not hasattr(particle_body, "_inertia")
|
| 67 |
+
assert hasattr(particle_body, 'frame')
|
| 68 |
+
assert hasattr(particle_body, 'masscenter')
|
| 69 |
+
assert hasattr(particle_body, 'mass')
|
| 70 |
+
assert particle_body.inertia == (Dyadic(0), particle_body.masscenter)
|
| 71 |
+
assert particle_body.central_inertia == Dyadic(0)
|
| 72 |
+
assert not particle_body.is_rigidbody
|
| 73 |
+
|
| 74 |
+
particle_body.central_inertia = inertia(particle_frame, 1, 1, 1)
|
| 75 |
+
assert particle_body.central_inertia == inertia(particle_frame, 1, 1, 1)
|
| 76 |
+
assert particle_body.is_rigidbody
|
| 77 |
+
|
| 78 |
+
with warns_deprecated_sympy():
|
| 79 |
+
particle_body = Body('particle_body', mass=particle_mass)
|
| 80 |
+
assert not particle_body.is_rigidbody
|
| 81 |
+
point = particle_body.masscenter.locatenew('point', particle_body.x)
|
| 82 |
+
point_inertia = particle_mass * inertia(particle_body.frame, 0, 1, 1)
|
| 83 |
+
particle_body.inertia = (point_inertia, point)
|
| 84 |
+
assert particle_body.inertia == (point_inertia, point)
|
| 85 |
+
assert particle_body.central_inertia == Dyadic(0)
|
| 86 |
+
assert particle_body.is_rigidbody
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def test_particle_body_add_force():
|
| 90 |
+
# Body with Particle
|
| 91 |
+
particle_masscenter = Point('particle_masscenter')
|
| 92 |
+
particle_mass = Symbol('particle_mass')
|
| 93 |
+
particle_frame = ReferenceFrame('particle_frame')
|
| 94 |
+
with warns_deprecated_sympy():
|
| 95 |
+
particle_body = Body('particle_body', particle_masscenter,
|
| 96 |
+
particle_mass, particle_frame)
|
| 97 |
+
|
| 98 |
+
a = Symbol('a')
|
| 99 |
+
force_vector = a * particle_body.frame.x
|
| 100 |
+
particle_body.apply_force(force_vector, particle_body.masscenter)
|
| 101 |
+
assert len(particle_body.loads) == 1
|
| 102 |
+
point = particle_body.masscenter.locatenew(
|
| 103 |
+
particle_body._name + '_point0', 0)
|
| 104 |
+
point.set_vel(particle_body.frame, 0)
|
| 105 |
+
force_point = particle_body.loads[0][0]
|
| 106 |
+
|
| 107 |
+
frame = particle_body.frame
|
| 108 |
+
assert force_point.vel(frame) == point.vel(frame)
|
| 109 |
+
assert force_point.pos_from(force_point) == point.pos_from(force_point)
|
| 110 |
+
|
| 111 |
+
assert particle_body.loads[0][1] == force_vector
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def test_body_add_force():
|
| 115 |
+
# Body with RigidBody.
|
| 116 |
+
rigidbody_masscenter = Point('rigidbody_masscenter')
|
| 117 |
+
rigidbody_mass = Symbol('rigidbody_mass')
|
| 118 |
+
rigidbody_frame = ReferenceFrame('rigidbody_frame')
|
| 119 |
+
body_inertia = inertia(rigidbody_frame, 1, 0, 0)
|
| 120 |
+
with warns_deprecated_sympy():
|
| 121 |
+
rigid_body = Body('rigidbody_body', rigidbody_masscenter,
|
| 122 |
+
rigidbody_mass, rigidbody_frame, body_inertia)
|
| 123 |
+
|
| 124 |
+
l = Symbol('l')
|
| 125 |
+
Fa = Symbol('Fa')
|
| 126 |
+
point = rigid_body.masscenter.locatenew(
|
| 127 |
+
'rigidbody_body_point0',
|
| 128 |
+
l * rigid_body.frame.x)
|
| 129 |
+
point.set_vel(rigid_body.frame, 0)
|
| 130 |
+
force_vector = Fa * rigid_body.frame.z
|
| 131 |
+
# apply_force with point
|
| 132 |
+
rigid_body.apply_force(force_vector, point)
|
| 133 |
+
assert len(rigid_body.loads) == 1
|
| 134 |
+
force_point = rigid_body.loads[0][0]
|
| 135 |
+
frame = rigid_body.frame
|
| 136 |
+
assert force_point.vel(frame) == point.vel(frame)
|
| 137 |
+
assert force_point.pos_from(force_point) == point.pos_from(force_point)
|
| 138 |
+
assert rigid_body.loads[0][1] == force_vector
|
| 139 |
+
# apply_force without point
|
| 140 |
+
rigid_body.apply_force(force_vector)
|
| 141 |
+
assert len(rigid_body.loads) == 2
|
| 142 |
+
assert rigid_body.loads[1][1] == force_vector
|
| 143 |
+
# passing something else than point
|
| 144 |
+
raises(TypeError, lambda: rigid_body.apply_force(force_vector, 0))
|
| 145 |
+
raises(TypeError, lambda: rigid_body.apply_force(0))
|
| 146 |
+
|
| 147 |
+
def test_body_add_torque():
|
| 148 |
+
with warns_deprecated_sympy():
|
| 149 |
+
body = Body('body')
|
| 150 |
+
torque_vector = body.frame.x
|
| 151 |
+
body.apply_torque(torque_vector)
|
| 152 |
+
|
| 153 |
+
assert len(body.loads) == 1
|
| 154 |
+
assert body.loads[0] == (body.frame, torque_vector)
|
| 155 |
+
raises(TypeError, lambda: body.apply_torque(0))
|
| 156 |
+
|
| 157 |
+
def test_body_masscenter_vel():
|
| 158 |
+
with warns_deprecated_sympy():
|
| 159 |
+
A = Body('A')
|
| 160 |
+
N = ReferenceFrame('N')
|
| 161 |
+
with warns_deprecated_sympy():
|
| 162 |
+
B = Body('B', frame=N)
|
| 163 |
+
A.masscenter.set_vel(N, N.z)
|
| 164 |
+
assert A.masscenter_vel(B) == N.z
|
| 165 |
+
assert A.masscenter_vel(N) == N.z
|
| 166 |
+
|
| 167 |
+
def test_body_ang_vel():
|
| 168 |
+
with warns_deprecated_sympy():
|
| 169 |
+
A = Body('A')
|
| 170 |
+
N = ReferenceFrame('N')
|
| 171 |
+
with warns_deprecated_sympy():
|
| 172 |
+
B = Body('B', frame=N)
|
| 173 |
+
A.frame.set_ang_vel(N, N.y)
|
| 174 |
+
assert A.ang_vel_in(B) == N.y
|
| 175 |
+
assert B.ang_vel_in(A) == -N.y
|
| 176 |
+
assert A.ang_vel_in(N) == N.y
|
| 177 |
+
|
| 178 |
+
def test_body_dcm():
|
| 179 |
+
with warns_deprecated_sympy():
|
| 180 |
+
A = Body('A')
|
| 181 |
+
B = Body('B')
|
| 182 |
+
A.frame.orient_axis(B.frame, B.frame.z, 10)
|
| 183 |
+
assert A.dcm(B) == Matrix([[cos(10), sin(10), 0], [-sin(10), cos(10), 0], [0, 0, 1]])
|
| 184 |
+
assert A.dcm(B.frame) == Matrix([[cos(10), sin(10), 0], [-sin(10), cos(10), 0], [0, 0, 1]])
|
| 185 |
+
|
| 186 |
+
def test_body_axis():
|
| 187 |
+
N = ReferenceFrame('N')
|
| 188 |
+
with warns_deprecated_sympy():
|
| 189 |
+
B = Body('B', frame=N)
|
| 190 |
+
assert B.x == N.x
|
| 191 |
+
assert B.y == N.y
|
| 192 |
+
assert B.z == N.z
|
| 193 |
+
|
| 194 |
+
def test_apply_force_multiple_one_point():
|
| 195 |
+
a, b = symbols('a b')
|
| 196 |
+
P = Point('P')
|
| 197 |
+
with warns_deprecated_sympy():
|
| 198 |
+
B = Body('B')
|
| 199 |
+
f1 = a*B.x
|
| 200 |
+
f2 = b*B.y
|
| 201 |
+
B.apply_force(f1, P)
|
| 202 |
+
assert B.loads == [(P, f1)]
|
| 203 |
+
B.apply_force(f2, P)
|
| 204 |
+
assert B.loads == [(P, f1+f2)]
|
| 205 |
+
|
| 206 |
+
def test_apply_force():
|
| 207 |
+
f, g = symbols('f g')
|
| 208 |
+
q, x, v1, v2 = dynamicsymbols('q x v1 v2')
|
| 209 |
+
P1 = Point('P1')
|
| 210 |
+
P2 = Point('P2')
|
| 211 |
+
with warns_deprecated_sympy():
|
| 212 |
+
B1 = Body('B1')
|
| 213 |
+
B2 = Body('B2')
|
| 214 |
+
N = ReferenceFrame('N')
|
| 215 |
+
|
| 216 |
+
P1.set_vel(B1.frame, v1*B1.x)
|
| 217 |
+
P2.set_vel(B2.frame, v2*B2.x)
|
| 218 |
+
force = f*q*N.z # time varying force
|
| 219 |
+
|
| 220 |
+
B1.apply_force(force, P1, B2, P2) #applying equal and opposite force on moving points
|
| 221 |
+
assert B1.loads == [(P1, force)]
|
| 222 |
+
assert B2.loads == [(P2, -force)]
|
| 223 |
+
|
| 224 |
+
g1 = B1.mass*g*N.y
|
| 225 |
+
g2 = B2.mass*g*N.y
|
| 226 |
+
|
| 227 |
+
B1.apply_force(g1) #applying gravity on B1 masscenter
|
| 228 |
+
B2.apply_force(g2) #applying gravity on B2 masscenter
|
| 229 |
+
|
| 230 |
+
assert B1.loads == [(P1,force), (B1.masscenter, g1)]
|
| 231 |
+
assert B2.loads == [(P2, -force), (B2.masscenter, g2)]
|
| 232 |
+
|
| 233 |
+
force2 = x*N.x
|
| 234 |
+
|
| 235 |
+
B1.apply_force(force2, reaction_body=B2) #Applying time varying force on masscenter
|
| 236 |
+
|
| 237 |
+
assert B1.loads == [(P1, force), (B1.masscenter, force2+g1)]
|
| 238 |
+
assert B2.loads == [(P2, -force), (B2.masscenter, -force2+g2)]
|
| 239 |
+
|
| 240 |
+
def test_apply_torque():
|
| 241 |
+
t = symbols('t')
|
| 242 |
+
q = dynamicsymbols('q')
|
| 243 |
+
with warns_deprecated_sympy():
|
| 244 |
+
B1 = Body('B1')
|
| 245 |
+
B2 = Body('B2')
|
| 246 |
+
N = ReferenceFrame('N')
|
| 247 |
+
torque = t*q*N.x
|
| 248 |
+
|
| 249 |
+
B1.apply_torque(torque, B2) #Applying equal and opposite torque
|
| 250 |
+
assert B1.loads == [(B1.frame, torque)]
|
| 251 |
+
assert B2.loads == [(B2.frame, -torque)]
|
| 252 |
+
|
| 253 |
+
torque2 = t*N.y
|
| 254 |
+
B1.apply_torque(torque2)
|
| 255 |
+
assert B1.loads == [(B1.frame, torque+torque2)]
|
| 256 |
+
|
| 257 |
+
def test_clear_load():
|
| 258 |
+
a = symbols('a')
|
| 259 |
+
P = Point('P')
|
| 260 |
+
with warns_deprecated_sympy():
|
| 261 |
+
B = Body('B')
|
| 262 |
+
force = a*B.z
|
| 263 |
+
B.apply_force(force, P)
|
| 264 |
+
assert B.loads == [(P, force)]
|
| 265 |
+
B.clear_loads()
|
| 266 |
+
assert B.loads == []
|
| 267 |
+
|
| 268 |
+
def test_remove_load():
|
| 269 |
+
P1 = Point('P1')
|
| 270 |
+
P2 = Point('P2')
|
| 271 |
+
with warns_deprecated_sympy():
|
| 272 |
+
B = Body('B')
|
| 273 |
+
f1 = B.x
|
| 274 |
+
f2 = B.y
|
| 275 |
+
B.apply_force(f1, P1)
|
| 276 |
+
B.apply_force(f2, P2)
|
| 277 |
+
assert B.loads == [(P1, f1), (P2, f2)]
|
| 278 |
+
B.remove_load(P2)
|
| 279 |
+
assert B.loads == [(P1, f1)]
|
| 280 |
+
B.apply_torque(f1.cross(f2))
|
| 281 |
+
assert B.loads == [(P1, f1), (B.frame, f1.cross(f2))]
|
| 282 |
+
B.remove_load()
|
| 283 |
+
assert B.loads == [(P1, f1)]
|
| 284 |
+
|
| 285 |
+
def test_apply_loads_on_multi_degree_freedom_holonomic_system():
|
| 286 |
+
"""Example based on: https://pydy.readthedocs.io/en/latest/examples/multidof-holonomic.html"""
|
| 287 |
+
with warns_deprecated_sympy():
|
| 288 |
+
W = Body('W') #Wall
|
| 289 |
+
B = Body('B') #Block
|
| 290 |
+
P = Body('P') #Pendulum
|
| 291 |
+
b = Body('b') #bob
|
| 292 |
+
q1, q2 = dynamicsymbols('q1 q2') #generalized coordinates
|
| 293 |
+
k, c, g, kT = symbols('k c g kT') #constants
|
| 294 |
+
F, T = dynamicsymbols('F T') #Specified forces
|
| 295 |
+
|
| 296 |
+
#Applying forces
|
| 297 |
+
B.apply_force(F*W.x)
|
| 298 |
+
W.apply_force(k*q1*W.x, reaction_body=B) #Spring force
|
| 299 |
+
W.apply_force(c*q1.diff()*W.x, reaction_body=B) #dampner
|
| 300 |
+
P.apply_force(P.mass*g*W.y)
|
| 301 |
+
b.apply_force(b.mass*g*W.y)
|
| 302 |
+
|
| 303 |
+
#Applying torques
|
| 304 |
+
P.apply_torque(kT*q2*W.z, reaction_body=b)
|
| 305 |
+
P.apply_torque(T*W.z)
|
| 306 |
+
|
| 307 |
+
assert B.loads == [(B.masscenter, (F - k*q1 - c*q1.diff())*W.x)]
|
| 308 |
+
assert P.loads == [(P.masscenter, P.mass*g*W.y), (P.frame, (T + kT*q2)*W.z)]
|
| 309 |
+
assert b.loads == [(b.masscenter, b.mass*g*W.y), (b.frame, -kT*q2*W.z)]
|
| 310 |
+
assert W.loads == [(W.masscenter, (c*q1.diff() + k*q1)*W.x)]
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def test_parallel_axis():
|
| 314 |
+
N = ReferenceFrame('N')
|
| 315 |
+
m, Ix, Iy, Iz, a, b = symbols('m, I_x, I_y, I_z, a, b')
|
| 316 |
+
Io = inertia(N, Ix, Iy, Iz)
|
| 317 |
+
# Test RigidBody
|
| 318 |
+
o = Point('o')
|
| 319 |
+
p = o.locatenew('p', a * N.x + b * N.y)
|
| 320 |
+
with warns_deprecated_sympy():
|
| 321 |
+
R = Body('R', masscenter=o, frame=N, mass=m, central_inertia=Io)
|
| 322 |
+
Ip = R.parallel_axis(p)
|
| 323 |
+
Ip_expected = inertia(N, Ix + m * b**2, Iy + m * a**2,
|
| 324 |
+
Iz + m * (a**2 + b**2), ixy=-m * a * b)
|
| 325 |
+
assert Ip == Ip_expected
|
| 326 |
+
# Reference frame from which the parallel axis is viewed should not matter
|
| 327 |
+
A = ReferenceFrame('A')
|
| 328 |
+
A.orient_axis(N, N.z, 1)
|
| 329 |
+
assert simplify(
|
| 330 |
+
(R.parallel_axis(p, A) - Ip_expected).to_matrix(A)) == zeros(3, 3)
|
| 331 |
+
# Test Particle
|
| 332 |
+
o = Point('o')
|
| 333 |
+
p = o.locatenew('p', a * N.x + b * N.y)
|
| 334 |
+
with warns_deprecated_sympy():
|
| 335 |
+
P = Body('P', masscenter=o, mass=m, frame=N)
|
| 336 |
+
Ip = P.parallel_axis(p, N)
|
| 337 |
+
Ip_expected = inertia(N, m * b ** 2, m * a ** 2, m * (a ** 2 + b ** 2),
|
| 338 |
+
ixy=-m * a * b)
|
| 339 |
+
assert not P.is_rigidbody
|
| 340 |
+
assert Ip == Ip_expected
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_functions.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import sin, cos, tan, pi, symbols, Matrix, S, Function
|
| 2 |
+
from sympy.physics.mechanics import (Particle, Point, ReferenceFrame,
|
| 3 |
+
RigidBody)
|
| 4 |
+
from sympy.physics.mechanics import (angular_momentum, dynamicsymbols,
|
| 5 |
+
kinetic_energy, linear_momentum,
|
| 6 |
+
outer, potential_energy, msubs,
|
| 7 |
+
find_dynamicsymbols, Lagrangian)
|
| 8 |
+
|
| 9 |
+
from sympy.physics.mechanics.functions import (
|
| 10 |
+
center_of_mass, _validate_coordinates, _parse_linear_solver)
|
| 11 |
+
from sympy.testing.pytest import raises, warns_deprecated_sympy
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
q1, q2, q3, q4, q5 = symbols('q1 q2 q3 q4 q5')
|
| 15 |
+
N = ReferenceFrame('N')
|
| 16 |
+
A = N.orientnew('A', 'Axis', [q1, N.z])
|
| 17 |
+
B = A.orientnew('B', 'Axis', [q2, A.x])
|
| 18 |
+
C = B.orientnew('C', 'Axis', [q3, B.y])
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_linear_momentum():
|
| 22 |
+
N = ReferenceFrame('N')
|
| 23 |
+
Ac = Point('Ac')
|
| 24 |
+
Ac.set_vel(N, 25 * N.y)
|
| 25 |
+
I = outer(N.x, N.x)
|
| 26 |
+
A = RigidBody('A', Ac, N, 20, (I, Ac))
|
| 27 |
+
P = Point('P')
|
| 28 |
+
Pa = Particle('Pa', P, 1)
|
| 29 |
+
Pa.point.set_vel(N, 10 * N.x)
|
| 30 |
+
raises(TypeError, lambda: linear_momentum(A, A, Pa))
|
| 31 |
+
raises(TypeError, lambda: linear_momentum(N, N, Pa))
|
| 32 |
+
assert linear_momentum(N, A, Pa) == 10 * N.x + 500 * N.y
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def test_angular_momentum_and_linear_momentum():
|
| 36 |
+
"""A rod with length 2l, centroidal inertia I, and mass M along with a
|
| 37 |
+
particle of mass m fixed to the end of the rod rotate with an angular rate
|
| 38 |
+
of omega about point O which is fixed to the non-particle end of the rod.
|
| 39 |
+
The rod's reference frame is A and the inertial frame is N."""
|
| 40 |
+
m, M, l, I = symbols('m, M, l, I')
|
| 41 |
+
omega = dynamicsymbols('omega')
|
| 42 |
+
N = ReferenceFrame('N')
|
| 43 |
+
a = ReferenceFrame('a')
|
| 44 |
+
O = Point('O')
|
| 45 |
+
Ac = O.locatenew('Ac', l * N.x)
|
| 46 |
+
P = Ac.locatenew('P', l * N.x)
|
| 47 |
+
O.set_vel(N, 0 * N.x)
|
| 48 |
+
a.set_ang_vel(N, omega * N.z)
|
| 49 |
+
Ac.v2pt_theory(O, N, a)
|
| 50 |
+
P.v2pt_theory(O, N, a)
|
| 51 |
+
Pa = Particle('Pa', P, m)
|
| 52 |
+
A = RigidBody('A', Ac, a, M, (I * outer(N.z, N.z), Ac))
|
| 53 |
+
expected = 2 * m * omega * l * N.y + M * l * omega * N.y
|
| 54 |
+
assert linear_momentum(N, A, Pa) == expected
|
| 55 |
+
raises(TypeError, lambda: angular_momentum(N, N, A, Pa))
|
| 56 |
+
raises(TypeError, lambda: angular_momentum(O, O, A, Pa))
|
| 57 |
+
raises(TypeError, lambda: angular_momentum(O, N, O, Pa))
|
| 58 |
+
expected = (I + M * l**2 + 4 * m * l**2) * omega * N.z
|
| 59 |
+
assert angular_momentum(O, N, A, Pa) == expected
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def test_kinetic_energy():
|
| 63 |
+
m, M, l1 = symbols('m M l1')
|
| 64 |
+
omega = dynamicsymbols('omega')
|
| 65 |
+
N = ReferenceFrame('N')
|
| 66 |
+
O = Point('O')
|
| 67 |
+
O.set_vel(N, 0 * N.x)
|
| 68 |
+
Ac = O.locatenew('Ac', l1 * N.x)
|
| 69 |
+
P = Ac.locatenew('P', l1 * N.x)
|
| 70 |
+
a = ReferenceFrame('a')
|
| 71 |
+
a.set_ang_vel(N, omega * N.z)
|
| 72 |
+
Ac.v2pt_theory(O, N, a)
|
| 73 |
+
P.v2pt_theory(O, N, a)
|
| 74 |
+
Pa = Particle('Pa', P, m)
|
| 75 |
+
I = outer(N.z, N.z)
|
| 76 |
+
A = RigidBody('A', Ac, a, M, (I, Ac))
|
| 77 |
+
raises(TypeError, lambda: kinetic_energy(Pa, Pa, A))
|
| 78 |
+
raises(TypeError, lambda: kinetic_energy(N, N, A))
|
| 79 |
+
assert 0 == (kinetic_energy(N, Pa, A) - (M*l1**2*omega**2/2
|
| 80 |
+
+ 2*l1**2*m*omega**2 + omega**2/2)).expand()
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def test_potential_energy():
|
| 84 |
+
m, M, l1, g, h, H = symbols('m M l1 g h H')
|
| 85 |
+
omega = dynamicsymbols('omega')
|
| 86 |
+
N = ReferenceFrame('N')
|
| 87 |
+
O = Point('O')
|
| 88 |
+
O.set_vel(N, 0 * N.x)
|
| 89 |
+
Ac = O.locatenew('Ac', l1 * N.x)
|
| 90 |
+
P = Ac.locatenew('P', l1 * N.x)
|
| 91 |
+
a = ReferenceFrame('a')
|
| 92 |
+
a.set_ang_vel(N, omega * N.z)
|
| 93 |
+
Ac.v2pt_theory(O, N, a)
|
| 94 |
+
P.v2pt_theory(O, N, a)
|
| 95 |
+
Pa = Particle('Pa', P, m)
|
| 96 |
+
I = outer(N.z, N.z)
|
| 97 |
+
A = RigidBody('A', Ac, a, M, (I, Ac))
|
| 98 |
+
Pa.potential_energy = m * g * h
|
| 99 |
+
A.potential_energy = M * g * H
|
| 100 |
+
assert potential_energy(A, Pa) == m * g * h + M * g * H
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def test_Lagrangian():
|
| 104 |
+
M, m, g, h = symbols('M m g h')
|
| 105 |
+
N = ReferenceFrame('N')
|
| 106 |
+
O = Point('O')
|
| 107 |
+
O.set_vel(N, 0 * N.x)
|
| 108 |
+
P = O.locatenew('P', 1 * N.x)
|
| 109 |
+
P.set_vel(N, 10 * N.x)
|
| 110 |
+
Pa = Particle('Pa', P, 1)
|
| 111 |
+
Ac = O.locatenew('Ac', 2 * N.y)
|
| 112 |
+
Ac.set_vel(N, 5 * N.y)
|
| 113 |
+
a = ReferenceFrame('a')
|
| 114 |
+
a.set_ang_vel(N, 10 * N.z)
|
| 115 |
+
I = outer(N.z, N.z)
|
| 116 |
+
A = RigidBody('A', Ac, a, 20, (I, Ac))
|
| 117 |
+
Pa.potential_energy = m * g * h
|
| 118 |
+
A.potential_energy = M * g * h
|
| 119 |
+
raises(TypeError, lambda: Lagrangian(A, A, Pa))
|
| 120 |
+
raises(TypeError, lambda: Lagrangian(N, N, Pa))
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def test_msubs():
|
| 124 |
+
a, b = symbols('a, b')
|
| 125 |
+
x, y, z = dynamicsymbols('x, y, z')
|
| 126 |
+
# Test simple substitution
|
| 127 |
+
expr = Matrix([[a*x + b, x*y.diff() + y],
|
| 128 |
+
[x.diff().diff(), z + sin(z.diff())]])
|
| 129 |
+
sol = Matrix([[a + b, y],
|
| 130 |
+
[x.diff().diff(), 1]])
|
| 131 |
+
sd = {x: 1, z: 1, z.diff(): 0, y.diff(): 0}
|
| 132 |
+
assert msubs(expr, sd) == sol
|
| 133 |
+
# Test smart substitution
|
| 134 |
+
expr = cos(x + y)*tan(x + y) + b*x.diff()
|
| 135 |
+
sd = {x: 0, y: pi/2, x.diff(): 1}
|
| 136 |
+
assert msubs(expr, sd, smart=True) == b + 1
|
| 137 |
+
N = ReferenceFrame('N')
|
| 138 |
+
v = x*N.x + y*N.y
|
| 139 |
+
d = x*(N.x|N.x) + y*(N.y|N.y)
|
| 140 |
+
v_sol = 1*N.y
|
| 141 |
+
d_sol = 1*(N.y|N.y)
|
| 142 |
+
sd = {x: 0, y: 1}
|
| 143 |
+
assert msubs(v, sd) == v_sol
|
| 144 |
+
assert msubs(d, sd) == d_sol
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def test_find_dynamicsymbols():
|
| 148 |
+
a, b = symbols('a, b')
|
| 149 |
+
x, y, z = dynamicsymbols('x, y, z')
|
| 150 |
+
expr = Matrix([[a*x + b, x*y.diff() + y],
|
| 151 |
+
[x.diff().diff(), z + sin(z.diff())]])
|
| 152 |
+
# Test finding all dynamicsymbols
|
| 153 |
+
sol = {x, y.diff(), y, x.diff().diff(), z, z.diff()}
|
| 154 |
+
assert find_dynamicsymbols(expr) == sol
|
| 155 |
+
# Test finding all but those in sym_list
|
| 156 |
+
exclude_list = [x, y, z]
|
| 157 |
+
sol = {y.diff(), x.diff().diff(), z.diff()}
|
| 158 |
+
assert find_dynamicsymbols(expr, exclude=exclude_list) == sol
|
| 159 |
+
# Test finding all dynamicsymbols in a vector with a given reference frame
|
| 160 |
+
d, e, f = dynamicsymbols('d, e, f')
|
| 161 |
+
A = ReferenceFrame('A')
|
| 162 |
+
v = d * A.x + e * A.y + f * A.z
|
| 163 |
+
sol = {d, e, f}
|
| 164 |
+
assert find_dynamicsymbols(v, reference_frame=A) == sol
|
| 165 |
+
# Test if a ValueError is raised on supplying only a vector as input
|
| 166 |
+
raises(ValueError, lambda: find_dynamicsymbols(v))
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
# This function tests the center_of_mass() function
|
| 170 |
+
# that was added in PR #14758 to compute the center of
|
| 171 |
+
# mass of a system of bodies.
|
| 172 |
+
def test_center_of_mass():
|
| 173 |
+
a = ReferenceFrame('a')
|
| 174 |
+
m = symbols('m', real=True)
|
| 175 |
+
p1 = Particle('p1', Point('p1_pt'), S.One)
|
| 176 |
+
p2 = Particle('p2', Point('p2_pt'), S(2))
|
| 177 |
+
p3 = Particle('p3', Point('p3_pt'), S(3))
|
| 178 |
+
p4 = Particle('p4', Point('p4_pt'), m)
|
| 179 |
+
b_f = ReferenceFrame('b_f')
|
| 180 |
+
b_cm = Point('b_cm')
|
| 181 |
+
mb = symbols('mb')
|
| 182 |
+
b = RigidBody('b', b_cm, b_f, mb, (outer(b_f.x, b_f.x), b_cm))
|
| 183 |
+
p2.point.set_pos(p1.point, a.x)
|
| 184 |
+
p3.point.set_pos(p1.point, a.x + a.y)
|
| 185 |
+
p4.point.set_pos(p1.point, a.y)
|
| 186 |
+
b.masscenter.set_pos(p1.point, a.y + a.z)
|
| 187 |
+
point_o=Point('o')
|
| 188 |
+
point_o.set_pos(p1.point, center_of_mass(p1.point, p1, p2, p3, p4, b))
|
| 189 |
+
expr = 5/(m + mb + 6)*a.x + (m + mb + 3)/(m + mb + 6)*a.y + mb/(m + mb + 6)*a.z
|
| 190 |
+
assert point_o.pos_from(p1.point)-expr == 0
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def test_validate_coordinates():
|
| 194 |
+
q1, q2, q3, u1, u2, u3, ua1, ua2, ua3 = dynamicsymbols('q1:4 u1:4 ua1:4')
|
| 195 |
+
s1, s2, s3 = symbols('s1:4')
|
| 196 |
+
# Test normal
|
| 197 |
+
_validate_coordinates([q1, q2, q3], [u1, u2, u3],
|
| 198 |
+
u_auxiliary=[ua1, ua2, ua3])
|
| 199 |
+
# Test not equal number of coordinates and speeds
|
| 200 |
+
_validate_coordinates([q1, q2])
|
| 201 |
+
_validate_coordinates([q1, q2], [u1])
|
| 202 |
+
_validate_coordinates(speeds=[u1, u2])
|
| 203 |
+
# Test duplicate
|
| 204 |
+
_validate_coordinates([q1, q2, q2], [u1, u2, u3], check_duplicates=False)
|
| 205 |
+
raises(ValueError, lambda: _validate_coordinates(
|
| 206 |
+
[q1, q2, q2], [u1, u2, u3]))
|
| 207 |
+
_validate_coordinates([q1, q2, q3], [u1, u2, u2], check_duplicates=False)
|
| 208 |
+
raises(ValueError, lambda: _validate_coordinates(
|
| 209 |
+
[q1, q2, q3], [u1, u2, u2], check_duplicates=True))
|
| 210 |
+
raises(ValueError, lambda: _validate_coordinates(
|
| 211 |
+
[q1, q2, q3], [q1, u2, u3], check_duplicates=True))
|
| 212 |
+
_validate_coordinates([q1, q2, q3], [u1, u2, u3], check_duplicates=False,
|
| 213 |
+
u_auxiliary=[u1, ua2, ua2])
|
| 214 |
+
raises(ValueError, lambda: _validate_coordinates(
|
| 215 |
+
[q1, q2, q3], [u1, u2, u3], u_auxiliary=[u1, ua2, ua3]))
|
| 216 |
+
raises(ValueError, lambda: _validate_coordinates(
|
| 217 |
+
[q1, q2, q3], [u1, u2, u3], u_auxiliary=[q1, ua2, ua3]))
|
| 218 |
+
raises(ValueError, lambda: _validate_coordinates(
|
| 219 |
+
[q1, q2, q3], [u1, u2, u3], u_auxiliary=[ua1, ua2, ua2]))
|
| 220 |
+
# Test is_dynamicsymbols
|
| 221 |
+
_validate_coordinates([q1 + q2, q3], is_dynamicsymbols=False)
|
| 222 |
+
raises(ValueError, lambda: _validate_coordinates([q1 + q2, q3]))
|
| 223 |
+
_validate_coordinates([s1, q1, q2], [0, u1, u2], is_dynamicsymbols=False)
|
| 224 |
+
raises(ValueError, lambda: _validate_coordinates(
|
| 225 |
+
[s1, q1, q2], [0, u1, u2], is_dynamicsymbols=True))
|
| 226 |
+
_validate_coordinates([s1 + s2 + s3, q1], [0, u1], is_dynamicsymbols=False)
|
| 227 |
+
raises(ValueError, lambda: _validate_coordinates(
|
| 228 |
+
[s1 + s2 + s3, q1], [0, u1], is_dynamicsymbols=True))
|
| 229 |
+
_validate_coordinates(u_auxiliary=[s1, ua1], is_dynamicsymbols=False)
|
| 230 |
+
raises(ValueError, lambda: _validate_coordinates(u_auxiliary=[s1, ua1]))
|
| 231 |
+
# Test normal function
|
| 232 |
+
t = dynamicsymbols._t
|
| 233 |
+
a = symbols('a')
|
| 234 |
+
f1, f2 = symbols('f1:3', cls=Function)
|
| 235 |
+
_validate_coordinates([f1(a), f2(a)], is_dynamicsymbols=False)
|
| 236 |
+
raises(ValueError, lambda: _validate_coordinates([f1(a), f2(a)]))
|
| 237 |
+
raises(ValueError, lambda: _validate_coordinates(speeds=[f1(a), f2(a)]))
|
| 238 |
+
dynamicsymbols._t = a
|
| 239 |
+
_validate_coordinates([f1(a), f2(a)])
|
| 240 |
+
raises(ValueError, lambda: _validate_coordinates([f1(t), f2(t)]))
|
| 241 |
+
dynamicsymbols._t = t
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def test_parse_linear_solver():
|
| 245 |
+
A, b = Matrix(3, 3, symbols('a:9')), Matrix(3, 2, symbols('b:6'))
|
| 246 |
+
assert _parse_linear_solver(Matrix.LUsolve) == Matrix.LUsolve # Test callable
|
| 247 |
+
assert _parse_linear_solver('LU')(A, b) == Matrix.LUsolve(A, b)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def test_deprecated_moved_functions():
|
| 251 |
+
from sympy.physics.mechanics.functions import (
|
| 252 |
+
inertia, inertia_of_point_mass, gravity)
|
| 253 |
+
N = ReferenceFrame('N')
|
| 254 |
+
with warns_deprecated_sympy():
|
| 255 |
+
assert inertia(N, 0, 1, 0, 1) == (N.x | N.y) + (N.y | N.x) + (N.y | N.y)
|
| 256 |
+
with warns_deprecated_sympy():
|
| 257 |
+
assert inertia_of_point_mass(1, N.x + N.y, N) == (
|
| 258 |
+
(N.x | N.x) + (N.y | N.y) + 2 * (N.z | N.z) -
|
| 259 |
+
(N.x | N.y) - (N.y | N.x))
|
| 260 |
+
p = Particle('P')
|
| 261 |
+
with warns_deprecated_sympy():
|
| 262 |
+
assert gravity(-2 * N.z, p) == [(p.masscenter, -2 * p.mass * N.z)]
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_inertia.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import symbols
|
| 2 |
+
from sympy.testing.pytest import raises
|
| 3 |
+
from sympy.physics.mechanics import (inertia, inertia_of_point_mass,
|
| 4 |
+
Inertia, ReferenceFrame, Point)
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def test_inertia_dyadic():
|
| 8 |
+
N = ReferenceFrame('N')
|
| 9 |
+
ixx, iyy, izz = symbols('ixx iyy izz')
|
| 10 |
+
ixy, iyz, izx = symbols('ixy iyz izx')
|
| 11 |
+
assert inertia(N, ixx, iyy, izz) == (ixx * (N.x | N.x) + iyy *
|
| 12 |
+
(N.y | N.y) + izz * (N.z | N.z))
|
| 13 |
+
assert inertia(N, 0, 0, 0) == 0 * (N.x | N.x)
|
| 14 |
+
raises(TypeError, lambda: inertia(0, 0, 0, 0))
|
| 15 |
+
assert inertia(N, ixx, iyy, izz, ixy, iyz, izx) == (ixx * (N.x | N.x) +
|
| 16 |
+
ixy * (N.x | N.y) + izx * (N.x | N.z) + ixy * (N.y | N.x) + iyy *
|
| 17 |
+
(N.y | N.y) + iyz * (N.y | N.z) + izx * (N.z | N.x) + iyz * (N.z |
|
| 18 |
+
N.y) + izz * (N.z | N.z))
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_inertia_of_point_mass():
|
| 22 |
+
r, s, t, m = symbols('r s t m')
|
| 23 |
+
N = ReferenceFrame('N')
|
| 24 |
+
|
| 25 |
+
px = r * N.x
|
| 26 |
+
I = inertia_of_point_mass(m, px, N)
|
| 27 |
+
assert I == m * r**2 * (N.y | N.y) + m * r**2 * (N.z | N.z)
|
| 28 |
+
|
| 29 |
+
py = s * N.y
|
| 30 |
+
I = inertia_of_point_mass(m, py, N)
|
| 31 |
+
assert I == m * s**2 * (N.x | N.x) + m * s**2 * (N.z | N.z)
|
| 32 |
+
|
| 33 |
+
pz = t * N.z
|
| 34 |
+
I = inertia_of_point_mass(m, pz, N)
|
| 35 |
+
assert I == m * t**2 * (N.x | N.x) + m * t**2 * (N.y | N.y)
|
| 36 |
+
|
| 37 |
+
p = px + py + pz
|
| 38 |
+
I = inertia_of_point_mass(m, p, N)
|
| 39 |
+
assert I == (m * (s**2 + t**2) * (N.x | N.x) -
|
| 40 |
+
m * r * s * (N.x | N.y) -
|
| 41 |
+
m * r * t * (N.x | N.z) -
|
| 42 |
+
m * r * s * (N.y | N.x) +
|
| 43 |
+
m * (r**2 + t**2) * (N.y | N.y) -
|
| 44 |
+
m * s * t * (N.y | N.z) -
|
| 45 |
+
m * r * t * (N.z | N.x) -
|
| 46 |
+
m * s * t * (N.z | N.y) +
|
| 47 |
+
m * (r**2 + s**2) * (N.z | N.z))
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def test_inertia_object():
|
| 51 |
+
N = ReferenceFrame('N')
|
| 52 |
+
O = Point('O')
|
| 53 |
+
ixx, iyy, izz = symbols('ixx iyy izz')
|
| 54 |
+
I_dyadic = ixx * (N.x | N.x) + iyy * (N.y | N.y) + izz * (N.z | N.z)
|
| 55 |
+
I = Inertia(inertia(N, ixx, iyy, izz), O)
|
| 56 |
+
assert isinstance(I, tuple)
|
| 57 |
+
assert I.__repr__() == ('Inertia(dyadic=ixx*(N.x|N.x) + iyy*(N.y|N.y) + '
|
| 58 |
+
'izz*(N.z|N.z), point=O)')
|
| 59 |
+
assert I.dyadic == I_dyadic
|
| 60 |
+
assert I.point == O
|
| 61 |
+
assert I[0] == I_dyadic
|
| 62 |
+
assert I[1] == O
|
| 63 |
+
assert I == (I_dyadic, O) # Test tuple equal
|
| 64 |
+
raises(TypeError, lambda: I != (O, I_dyadic)) # Incorrect tuple order
|
| 65 |
+
assert I == Inertia(O, I_dyadic) # Parse changed argument order
|
| 66 |
+
assert I == Inertia.from_inertia_scalars(O, N, ixx, iyy, izz)
|
| 67 |
+
# Test invalid tuple operations
|
| 68 |
+
raises(TypeError, lambda: I + (1, 2))
|
| 69 |
+
raises(TypeError, lambda: (1, 2) + I)
|
| 70 |
+
raises(TypeError, lambda: I * 2)
|
| 71 |
+
raises(TypeError, lambda: 2 * I)
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_joint.py
ADDED
|
@@ -0,0 +1,1240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy.core.function import expand_mul
|
| 2 |
+
from sympy.core.numbers import pi
|
| 3 |
+
from sympy.core.singleton import S
|
| 4 |
+
from sympy.functions.elementary.miscellaneous import sqrt
|
| 5 |
+
from sympy.functions.elementary.trigonometric import (cos, sin)
|
| 6 |
+
from sympy import Matrix, simplify, eye, zeros
|
| 7 |
+
from sympy.core.symbol import symbols
|
| 8 |
+
from sympy.physics.mechanics import (
|
| 9 |
+
dynamicsymbols, RigidBody, Particle, JointsMethod, PinJoint, PrismaticJoint,
|
| 10 |
+
CylindricalJoint, PlanarJoint, SphericalJoint, WeldJoint, Body)
|
| 11 |
+
from sympy.physics.mechanics.joint import Joint
|
| 12 |
+
from sympy.physics.vector import Vector, ReferenceFrame, Point
|
| 13 |
+
from sympy.testing.pytest import raises, warns_deprecated_sympy
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
t = dynamicsymbols._t # type: ignore
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _generate_body(interframe=False):
|
| 20 |
+
N = ReferenceFrame('N')
|
| 21 |
+
A = ReferenceFrame('A')
|
| 22 |
+
P = RigidBody('P', frame=N)
|
| 23 |
+
C = RigidBody('C', frame=A)
|
| 24 |
+
if interframe:
|
| 25 |
+
Pint, Cint = ReferenceFrame('P_int'), ReferenceFrame('C_int')
|
| 26 |
+
Pint.orient_axis(N, N.x, pi)
|
| 27 |
+
Cint.orient_axis(A, A.y, -pi / 2)
|
| 28 |
+
return N, A, P, C, Pint, Cint
|
| 29 |
+
return N, A, P, C
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_Joint():
|
| 33 |
+
parent = RigidBody('parent')
|
| 34 |
+
child = RigidBody('child')
|
| 35 |
+
raises(TypeError, lambda: Joint('J', parent, child))
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_coordinate_generation():
|
| 39 |
+
q, u, qj, uj = dynamicsymbols('q u q_J u_J')
|
| 40 |
+
q0j, q1j, q2j, q3j, u0j, u1j, u2j, u3j = dynamicsymbols('q0:4_J u0:4_J')
|
| 41 |
+
q0, q1, q2, q3, u0, u1, u2, u3 = dynamicsymbols('q0:4 u0:4')
|
| 42 |
+
_, _, P, C = _generate_body()
|
| 43 |
+
# Using PinJoint to access Joint's coordinate generation method
|
| 44 |
+
J = PinJoint('J', P, C)
|
| 45 |
+
# Test single given
|
| 46 |
+
assert J._fill_coordinate_list(q, 1) == Matrix([q])
|
| 47 |
+
assert J._fill_coordinate_list([u], 1) == Matrix([u])
|
| 48 |
+
assert J._fill_coordinate_list([u], 1, offset=2) == Matrix([u])
|
| 49 |
+
# Test None
|
| 50 |
+
assert J._fill_coordinate_list(None, 1) == Matrix([qj])
|
| 51 |
+
assert J._fill_coordinate_list([None], 1) == Matrix([qj])
|
| 52 |
+
assert J._fill_coordinate_list([q0, None, None], 3) == Matrix(
|
| 53 |
+
[q0, q1j, q2j])
|
| 54 |
+
# Test autofill
|
| 55 |
+
assert J._fill_coordinate_list(None, 3) == Matrix([q0j, q1j, q2j])
|
| 56 |
+
assert J._fill_coordinate_list([], 3) == Matrix([q0j, q1j, q2j])
|
| 57 |
+
# Test offset
|
| 58 |
+
assert J._fill_coordinate_list([], 3, offset=1) == Matrix([q1j, q2j, q3j])
|
| 59 |
+
assert J._fill_coordinate_list([q1, None, q3], 3, offset=1) == Matrix(
|
| 60 |
+
[q1, q2j, q3])
|
| 61 |
+
assert J._fill_coordinate_list(None, 2, offset=2) == Matrix([q2j, q3j])
|
| 62 |
+
# Test label
|
| 63 |
+
assert J._fill_coordinate_list(None, 1, 'u') == Matrix([uj])
|
| 64 |
+
assert J._fill_coordinate_list([], 3, 'u') == Matrix([u0j, u1j, u2j])
|
| 65 |
+
# Test single numbering
|
| 66 |
+
assert J._fill_coordinate_list(None, 1, number_single=True) == Matrix([q0j])
|
| 67 |
+
assert J._fill_coordinate_list([], 1, 'u', 2, True) == Matrix([u2j])
|
| 68 |
+
assert J._fill_coordinate_list([], 3, 'q') == Matrix([q0j, q1j, q2j])
|
| 69 |
+
# Test invalid number of coordinates supplied
|
| 70 |
+
raises(ValueError, lambda: J._fill_coordinate_list([q0, q1], 1))
|
| 71 |
+
raises(ValueError, lambda: J._fill_coordinate_list([u0, u1, None], 2, 'u'))
|
| 72 |
+
raises(ValueError, lambda: J._fill_coordinate_list([q0, q1], 3))
|
| 73 |
+
# Test incorrect coordinate type
|
| 74 |
+
raises(TypeError, lambda: J._fill_coordinate_list([q0, symbols('q1')], 2))
|
| 75 |
+
raises(TypeError, lambda: J._fill_coordinate_list([q0 + q1, q1], 2))
|
| 76 |
+
# Test if derivative as generalized speed is allowed
|
| 77 |
+
_, _, P, C = _generate_body()
|
| 78 |
+
PinJoint('J', P, C, q1, q1.diff(t))
|
| 79 |
+
# Test duplicate coordinates
|
| 80 |
+
_, _, P, C = _generate_body()
|
| 81 |
+
raises(ValueError, lambda: SphericalJoint('J', P, C, [q1j, None, None]))
|
| 82 |
+
raises(ValueError, lambda: SphericalJoint('J', P, C, speeds=[u0, u0, u1]))
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def test_pin_joint():
|
| 86 |
+
P = RigidBody('P')
|
| 87 |
+
C = RigidBody('C')
|
| 88 |
+
l, m = symbols('l m')
|
| 89 |
+
q, u = dynamicsymbols('q_J, u_J')
|
| 90 |
+
Pj = PinJoint('J', P, C)
|
| 91 |
+
assert Pj.name == 'J'
|
| 92 |
+
assert Pj.parent == P
|
| 93 |
+
assert Pj.child == C
|
| 94 |
+
assert Pj.coordinates == Matrix([q])
|
| 95 |
+
assert Pj.speeds == Matrix([u])
|
| 96 |
+
assert Pj.kdes == Matrix([u - q.diff(t)])
|
| 97 |
+
assert Pj.joint_axis == P.frame.x
|
| 98 |
+
assert Pj.child_point.pos_from(C.masscenter) == Vector(0)
|
| 99 |
+
assert Pj.parent_point.pos_from(P.masscenter) == Vector(0)
|
| 100 |
+
assert Pj.parent_point.pos_from(Pj._child_point) == Vector(0)
|
| 101 |
+
assert C.masscenter.pos_from(P.masscenter) == Vector(0)
|
| 102 |
+
assert Pj.parent_interframe == P.frame
|
| 103 |
+
assert Pj.child_interframe == C.frame
|
| 104 |
+
assert Pj.__str__() == 'PinJoint: J parent: P child: C'
|
| 105 |
+
|
| 106 |
+
P1 = RigidBody('P1')
|
| 107 |
+
C1 = RigidBody('C1')
|
| 108 |
+
Pint = ReferenceFrame('P_int')
|
| 109 |
+
Pint.orient_axis(P1.frame, P1.y, pi / 2)
|
| 110 |
+
J1 = PinJoint('J1', P1, C1, parent_point=l*P1.frame.x,
|
| 111 |
+
child_point=m*C1.frame.y, joint_axis=P1.frame.z,
|
| 112 |
+
parent_interframe=Pint)
|
| 113 |
+
assert J1._joint_axis == P1.frame.z
|
| 114 |
+
assert J1._child_point.pos_from(C1.masscenter) == m * C1.frame.y
|
| 115 |
+
assert J1._parent_point.pos_from(P1.masscenter) == l * P1.frame.x
|
| 116 |
+
assert J1._parent_point.pos_from(J1._child_point) == Vector(0)
|
| 117 |
+
assert (P1.masscenter.pos_from(C1.masscenter) ==
|
| 118 |
+
-l*P1.frame.x + m*C1.frame.y)
|
| 119 |
+
assert J1.parent_interframe == Pint
|
| 120 |
+
assert J1.child_interframe == C1.frame
|
| 121 |
+
|
| 122 |
+
q, u = dynamicsymbols('q, u')
|
| 123 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 124 |
+
parent_point = P.masscenter.locatenew('parent_point', N.x + N.y)
|
| 125 |
+
child_point = C.masscenter.locatenew('child_point', C.y + C.z)
|
| 126 |
+
J = PinJoint('J', P, C, q, u, parent_point=parent_point,
|
| 127 |
+
child_point=child_point, parent_interframe=Pint,
|
| 128 |
+
child_interframe=Cint, joint_axis=N.z)
|
| 129 |
+
assert J.joint_axis == N.z
|
| 130 |
+
assert J.parent_point.vel(N) == 0
|
| 131 |
+
assert J.parent_point == parent_point
|
| 132 |
+
assert J.child_point == child_point
|
| 133 |
+
assert J.child_point.pos_from(P.masscenter) == N.x + N.y
|
| 134 |
+
assert J.parent_point.pos_from(C.masscenter) == C.y + C.z
|
| 135 |
+
assert C.masscenter.pos_from(P.masscenter) == N.x + N.y - C.y - C.z
|
| 136 |
+
assert C.masscenter.vel(N).express(N) == (u * sin(q) - u * cos(q)) * N.x + (
|
| 137 |
+
-u * sin(q) - u * cos(q)) * N.y
|
| 138 |
+
assert J.parent_interframe == Pint
|
| 139 |
+
assert J.child_interframe == Cint
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def test_particle_compatibility():
|
| 143 |
+
m, l = symbols('m l')
|
| 144 |
+
C_frame = ReferenceFrame('C')
|
| 145 |
+
P = Particle('P')
|
| 146 |
+
C = Particle('C', mass=m)
|
| 147 |
+
q, u = dynamicsymbols('q, u')
|
| 148 |
+
J = PinJoint('J', P, C, q, u, child_interframe=C_frame,
|
| 149 |
+
child_point=l * C_frame.y)
|
| 150 |
+
assert J.child_interframe == C_frame
|
| 151 |
+
assert J.parent_interframe.name == 'J_P_frame'
|
| 152 |
+
assert C.masscenter.pos_from(P.masscenter) == -l * C_frame.y
|
| 153 |
+
assert C_frame.dcm(J.parent_interframe) == Matrix([[1, 0, 0],
|
| 154 |
+
[0, cos(q), sin(q)],
|
| 155 |
+
[0, -sin(q), cos(q)]])
|
| 156 |
+
assert C.masscenter.vel(J.parent_interframe) == -l * u * C_frame.z
|
| 157 |
+
# Test with specified joint axis
|
| 158 |
+
P_frame = ReferenceFrame('P')
|
| 159 |
+
C_frame = ReferenceFrame('C')
|
| 160 |
+
P = Particle('P')
|
| 161 |
+
C = Particle('C', mass=m)
|
| 162 |
+
q, u = dynamicsymbols('q, u')
|
| 163 |
+
J = PinJoint('J', P, C, q, u, parent_interframe=P_frame,
|
| 164 |
+
child_interframe=C_frame, child_point=l * C_frame.y,
|
| 165 |
+
joint_axis=P_frame.z)
|
| 166 |
+
assert J.joint_axis == J.parent_interframe.z
|
| 167 |
+
assert C_frame.dcm(J.parent_interframe) == Matrix([[cos(q), sin(q), 0],
|
| 168 |
+
[-sin(q), cos(q), 0],
|
| 169 |
+
[0, 0, 1]])
|
| 170 |
+
assert P.masscenter.vel(J.parent_interframe) == 0
|
| 171 |
+
assert C.masscenter.vel(J.parent_interframe) == l * u * C_frame.x
|
| 172 |
+
q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1:4 u1:4')
|
| 173 |
+
qdot_to_u = {qi.diff(t): ui for qi, ui in ((q1, u1), (q2, u2), (q3, u3))}
|
| 174 |
+
# Test compatibility for prismatic joint
|
| 175 |
+
P, C = Particle('P'), Particle('C')
|
| 176 |
+
J = PrismaticJoint('J', P, C, q, u)
|
| 177 |
+
assert J.parent_interframe.dcm(J.child_interframe) == eye(3)
|
| 178 |
+
assert C.masscenter.pos_from(P.masscenter) == q * J.parent_interframe.x
|
| 179 |
+
assert P.masscenter.vel(J.parent_interframe) == 0
|
| 180 |
+
assert C.masscenter.vel(J.parent_interframe) == u * J.parent_interframe.x
|
| 181 |
+
# Test compatibility for cylindrical joint
|
| 182 |
+
P, C = Particle('P'), Particle('C')
|
| 183 |
+
P_frame = ReferenceFrame('P_frame')
|
| 184 |
+
J = CylindricalJoint('J', P, C, q1, q2, u1, u2, parent_interframe=P_frame,
|
| 185 |
+
parent_point=l * P_frame.x, joint_axis=P_frame.y)
|
| 186 |
+
assert J.parent_interframe.dcm(J.child_interframe) == Matrix([
|
| 187 |
+
[cos(q1), 0, sin(q1)], [0, 1, 0], [-sin(q1), 0, cos(q1)]])
|
| 188 |
+
assert C.masscenter.pos_from(P.masscenter) == l * P_frame.x + q2 * P_frame.y
|
| 189 |
+
assert C.masscenter.vel(J.parent_interframe) == u2 * P_frame.y
|
| 190 |
+
assert P.masscenter.vel(J.child_interframe).xreplace(qdot_to_u) == (
|
| 191 |
+
-u2 * P_frame.y - l * u1 * P_frame.z)
|
| 192 |
+
# Test compatibility for planar joint
|
| 193 |
+
P, C = Particle('P'), Particle('C')
|
| 194 |
+
C_frame = ReferenceFrame('C_frame')
|
| 195 |
+
J = PlanarJoint('J', P, C, q1, [q2, q3], u1, [u2, u3],
|
| 196 |
+
child_interframe=C_frame, child_point=l * C_frame.z)
|
| 197 |
+
P_frame = J.parent_interframe
|
| 198 |
+
assert J.parent_interframe.dcm(J.child_interframe) == Matrix([
|
| 199 |
+
[1, 0, 0], [0, cos(q1), -sin(q1)], [0, sin(q1), cos(q1)]])
|
| 200 |
+
assert C.masscenter.pos_from(P.masscenter) == (
|
| 201 |
+
-l * C_frame.z + q2 * P_frame.y + q3 * P_frame.z)
|
| 202 |
+
assert C.masscenter.vel(J.parent_interframe) == (
|
| 203 |
+
l * u1 * C_frame.y + u2 * P_frame.y + u3 * P_frame.z)
|
| 204 |
+
# Test compatibility for weld joint
|
| 205 |
+
P, C = Particle('P'), Particle('C')
|
| 206 |
+
C_frame, P_frame = ReferenceFrame('C_frame'), ReferenceFrame('P_frame')
|
| 207 |
+
J = WeldJoint('J', P, C, parent_interframe=P_frame,
|
| 208 |
+
child_interframe=C_frame, parent_point=l * P_frame.x,
|
| 209 |
+
child_point=l * C_frame.y)
|
| 210 |
+
assert P_frame.dcm(C_frame) == eye(3)
|
| 211 |
+
assert C.masscenter.pos_from(P.masscenter) == l * P_frame.x - l * C_frame.y
|
| 212 |
+
assert C.masscenter.vel(J.parent_interframe) == 0
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def test_body_compatibility():
|
| 216 |
+
m, l = symbols('m l')
|
| 217 |
+
C_frame = ReferenceFrame('C')
|
| 218 |
+
with warns_deprecated_sympy():
|
| 219 |
+
P = Body('P')
|
| 220 |
+
C = Body('C', mass=m, frame=C_frame)
|
| 221 |
+
q, u = dynamicsymbols('q, u')
|
| 222 |
+
PinJoint('J', P, C, q, u, child_point=l * C_frame.y)
|
| 223 |
+
assert C.frame == C_frame
|
| 224 |
+
assert P.frame.name == 'P_frame'
|
| 225 |
+
assert C.masscenter.pos_from(P.masscenter) == -l * C.y
|
| 226 |
+
assert C.frame.dcm(P.frame) == Matrix([[1, 0, 0],
|
| 227 |
+
[0, cos(q), sin(q)],
|
| 228 |
+
[0, -sin(q), cos(q)]])
|
| 229 |
+
assert C.masscenter.vel(P.frame) == -l * u * C.z
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def test_pin_joint_double_pendulum():
|
| 233 |
+
q1, q2 = dynamicsymbols('q1 q2')
|
| 234 |
+
u1, u2 = dynamicsymbols('u1 u2')
|
| 235 |
+
m, l = symbols('m l')
|
| 236 |
+
N = ReferenceFrame('N')
|
| 237 |
+
A = ReferenceFrame('A')
|
| 238 |
+
B = ReferenceFrame('B')
|
| 239 |
+
C = RigidBody('C', frame=N) # ceiling
|
| 240 |
+
PartP = RigidBody('P', frame=A, mass=m)
|
| 241 |
+
PartR = RigidBody('R', frame=B, mass=m)
|
| 242 |
+
|
| 243 |
+
J1 = PinJoint('J1', C, PartP, speeds=u1, coordinates=q1,
|
| 244 |
+
child_point=-l*A.x, joint_axis=C.frame.z)
|
| 245 |
+
J2 = PinJoint('J2', PartP, PartR, speeds=u2, coordinates=q2,
|
| 246 |
+
child_point=-l*B.x, joint_axis=PartP.frame.z)
|
| 247 |
+
|
| 248 |
+
# Check orientation
|
| 249 |
+
assert N.dcm(A) == Matrix([[cos(q1), -sin(q1), 0],
|
| 250 |
+
[sin(q1), cos(q1), 0], [0, 0, 1]])
|
| 251 |
+
assert A.dcm(B) == Matrix([[cos(q2), -sin(q2), 0],
|
| 252 |
+
[sin(q2), cos(q2), 0], [0, 0, 1]])
|
| 253 |
+
assert simplify(N.dcm(B)) == Matrix([[cos(q1 + q2), -sin(q1 + q2), 0],
|
| 254 |
+
[sin(q1 + q2), cos(q1 + q2), 0],
|
| 255 |
+
[0, 0, 1]])
|
| 256 |
+
|
| 257 |
+
# Check Angular Velocity
|
| 258 |
+
assert A.ang_vel_in(N) == u1 * N.z
|
| 259 |
+
assert B.ang_vel_in(A) == u2 * A.z
|
| 260 |
+
assert B.ang_vel_in(N) == u1 * N.z + u2 * A.z
|
| 261 |
+
|
| 262 |
+
# Check kde
|
| 263 |
+
assert J1.kdes == Matrix([u1 - q1.diff(t)])
|
| 264 |
+
assert J2.kdes == Matrix([u2 - q2.diff(t)])
|
| 265 |
+
|
| 266 |
+
# Check Linear Velocity
|
| 267 |
+
assert PartP.masscenter.vel(N) == l*u1*A.y
|
| 268 |
+
assert PartR.masscenter.vel(A) == l*u2*B.y
|
| 269 |
+
assert PartR.masscenter.vel(N) == l*u1*A.y + l*(u1 + u2)*B.y
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def test_pin_joint_chaos_pendulum():
|
| 273 |
+
mA, mB, lA, lB, h = symbols('mA, mB, lA, lB, h')
|
| 274 |
+
theta, phi, omega, alpha = dynamicsymbols('theta phi omega alpha')
|
| 275 |
+
N = ReferenceFrame('N')
|
| 276 |
+
A = ReferenceFrame('A')
|
| 277 |
+
B = ReferenceFrame('B')
|
| 278 |
+
lA = (lB - h / 2) / 2
|
| 279 |
+
lC = (lB/2 + h/4)
|
| 280 |
+
rod = RigidBody('rod', frame=A, mass=mA)
|
| 281 |
+
plate = RigidBody('plate', mass=mB, frame=B)
|
| 282 |
+
C = RigidBody('C', frame=N)
|
| 283 |
+
J1 = PinJoint('J1', C, rod, coordinates=theta, speeds=omega,
|
| 284 |
+
child_point=lA*A.z, joint_axis=N.y)
|
| 285 |
+
J2 = PinJoint('J2', rod, plate, coordinates=phi, speeds=alpha,
|
| 286 |
+
parent_point=lC*A.z, joint_axis=A.z)
|
| 287 |
+
|
| 288 |
+
# Check orientation
|
| 289 |
+
assert A.dcm(N) == Matrix([[cos(theta), 0, -sin(theta)],
|
| 290 |
+
[0, 1, 0],
|
| 291 |
+
[sin(theta), 0, cos(theta)]])
|
| 292 |
+
assert A.dcm(B) == Matrix([[cos(phi), -sin(phi), 0],
|
| 293 |
+
[sin(phi), cos(phi), 0],
|
| 294 |
+
[0, 0, 1]])
|
| 295 |
+
assert B.dcm(N) == Matrix([
|
| 296 |
+
[cos(phi)*cos(theta), sin(phi), -sin(theta)*cos(phi)],
|
| 297 |
+
[-sin(phi)*cos(theta), cos(phi), sin(phi)*sin(theta)],
|
| 298 |
+
[sin(theta), 0, cos(theta)]])
|
| 299 |
+
|
| 300 |
+
# Check Angular Velocity
|
| 301 |
+
assert A.ang_vel_in(N) == omega*N.y
|
| 302 |
+
assert A.ang_vel_in(B) == -alpha*A.z
|
| 303 |
+
assert N.ang_vel_in(B) == -omega*N.y - alpha*A.z
|
| 304 |
+
|
| 305 |
+
# Check kde
|
| 306 |
+
assert J1.kdes == Matrix([omega - theta.diff(t)])
|
| 307 |
+
assert J2.kdes == Matrix([alpha - phi.diff(t)])
|
| 308 |
+
|
| 309 |
+
# Check pos of masscenters
|
| 310 |
+
assert C.masscenter.pos_from(rod.masscenter) == lA*A.z
|
| 311 |
+
assert rod.masscenter.pos_from(plate.masscenter) == - lC * A.z
|
| 312 |
+
|
| 313 |
+
# Check Linear Velocities
|
| 314 |
+
assert rod.masscenter.vel(N) == (h/4 - lB/2)*omega*A.x
|
| 315 |
+
assert plate.masscenter.vel(N) == ((h/4 - lB/2)*omega +
|
| 316 |
+
(h/4 + lB/2)*omega)*A.x
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
def test_pin_joint_interframe():
|
| 320 |
+
q, u = dynamicsymbols('q, u')
|
| 321 |
+
# Check not connected
|
| 322 |
+
N, A, P, C = _generate_body()
|
| 323 |
+
Pint, Cint = ReferenceFrame('Pint'), ReferenceFrame('Cint')
|
| 324 |
+
raises(ValueError, lambda: PinJoint('J', P, C, parent_interframe=Pint))
|
| 325 |
+
raises(ValueError, lambda: PinJoint('J', P, C, child_interframe=Cint))
|
| 326 |
+
# Check not fixed interframe
|
| 327 |
+
Pint.orient_axis(N, N.z, q)
|
| 328 |
+
Cint.orient_axis(A, A.z, q)
|
| 329 |
+
raises(ValueError, lambda: PinJoint('J', P, C, parent_interframe=Pint))
|
| 330 |
+
raises(ValueError, lambda: PinJoint('J', P, C, child_interframe=Cint))
|
| 331 |
+
# Check only parent_interframe
|
| 332 |
+
N, A, P, C = _generate_body()
|
| 333 |
+
Pint = ReferenceFrame('Pint')
|
| 334 |
+
Pint.orient_body_fixed(N, (pi / 4, pi, pi / 3), 'xyz')
|
| 335 |
+
PinJoint('J', P, C, q, u, parent_point=N.x, child_point=-C.y,
|
| 336 |
+
parent_interframe=Pint, joint_axis=Pint.x)
|
| 337 |
+
assert simplify(N.dcm(A)) - Matrix([
|
| 338 |
+
[-1 / 2, sqrt(3) * cos(q) / 2, -sqrt(3) * sin(q) / 2],
|
| 339 |
+
[sqrt(6) / 4, sqrt(2) * (2 * sin(q) + cos(q)) / 4,
|
| 340 |
+
sqrt(2) * (-sin(q) + 2 * cos(q)) / 4],
|
| 341 |
+
[sqrt(6) / 4, sqrt(2) * (-2 * sin(q) + cos(q)) / 4,
|
| 342 |
+
-sqrt(2) * (sin(q) + 2 * cos(q)) / 4]]) == zeros(3)
|
| 343 |
+
assert A.ang_vel_in(N) == u * Pint.x
|
| 344 |
+
assert C.masscenter.pos_from(P.masscenter) == N.x + A.y
|
| 345 |
+
assert C.masscenter.vel(N) == u * A.z
|
| 346 |
+
assert P.masscenter.vel(Pint) == Vector(0)
|
| 347 |
+
assert C.masscenter.vel(Pint) == u * A.z
|
| 348 |
+
# Check only child_interframe
|
| 349 |
+
N, A, P, C = _generate_body()
|
| 350 |
+
Cint = ReferenceFrame('Cint')
|
| 351 |
+
Cint.orient_body_fixed(A, (2 * pi / 3, -pi, pi / 2), 'xyz')
|
| 352 |
+
PinJoint('J', P, C, q, u, parent_point=-N.z, child_point=C.x,
|
| 353 |
+
child_interframe=Cint, joint_axis=P.x + P.z)
|
| 354 |
+
assert simplify(N.dcm(A)) == Matrix([
|
| 355 |
+
[-sqrt(2) * sin(q) / 2,
|
| 356 |
+
-sqrt(3) * (cos(q) - 1) / 4 - cos(q) / 4 - S(1) / 4,
|
| 357 |
+
sqrt(3) * (cos(q) + 1) / 4 - cos(q) / 4 + S(1) / 4],
|
| 358 |
+
[cos(q), (sqrt(2) + sqrt(6)) * -sin(q) / 4,
|
| 359 |
+
(-sqrt(2) + sqrt(6)) * sin(q) / 4],
|
| 360 |
+
[sqrt(2) * sin(q) / 2,
|
| 361 |
+
sqrt(3) * (cos(q) + 1) / 4 + cos(q) / 4 - S(1) / 4,
|
| 362 |
+
sqrt(3) * (1 - cos(q)) / 4 + cos(q) / 4 + S(1) / 4]])
|
| 363 |
+
assert A.ang_vel_in(N) == sqrt(2) * u / 2 * N.x + sqrt(2) * u / 2 * N.z
|
| 364 |
+
assert C.masscenter.pos_from(P.masscenter) == - N.z - A.x
|
| 365 |
+
assert C.masscenter.vel(N).simplify() == (
|
| 366 |
+
-sqrt(6) - sqrt(2)) * u / 4 * A.y + (
|
| 367 |
+
-sqrt(2) + sqrt(6)) * u / 4 * A.z
|
| 368 |
+
assert C.masscenter.vel(Cint) == Vector(0)
|
| 369 |
+
# Check combination
|
| 370 |
+
N, A, P, C = _generate_body()
|
| 371 |
+
Pint, Cint = ReferenceFrame('Pint'), ReferenceFrame('Cint')
|
| 372 |
+
Pint.orient_body_fixed(N, (-pi / 2, pi, pi / 2), 'xyz')
|
| 373 |
+
Cint.orient_body_fixed(A, (2 * pi / 3, -pi, pi / 2), 'xyz')
|
| 374 |
+
PinJoint('J', P, C, q, u, parent_point=N.x - N.y, child_point=-C.z,
|
| 375 |
+
parent_interframe=Pint, child_interframe=Cint,
|
| 376 |
+
joint_axis=Pint.x + Pint.z)
|
| 377 |
+
assert simplify(N.dcm(A)) == Matrix([
|
| 378 |
+
[cos(q), (sqrt(2) + sqrt(6)) * -sin(q) / 4,
|
| 379 |
+
(-sqrt(2) + sqrt(6)) * sin(q) / 4],
|
| 380 |
+
[-sqrt(2) * sin(q) / 2,
|
| 381 |
+
-sqrt(3) * (cos(q) + 1) / 4 - cos(q) / 4 + S(1) / 4,
|
| 382 |
+
sqrt(3) * (cos(q) - 1) / 4 - cos(q) / 4 - S(1) / 4],
|
| 383 |
+
[sqrt(2) * sin(q) / 2,
|
| 384 |
+
sqrt(3) * (cos(q) - 1) / 4 + cos(q) / 4 + S(1) / 4,
|
| 385 |
+
-sqrt(3) * (cos(q) + 1) / 4 + cos(q) / 4 - S(1) / 4]])
|
| 386 |
+
assert A.ang_vel_in(N) == sqrt(2) * u / 2 * Pint.x + sqrt(
|
| 387 |
+
2) * u / 2 * Pint.z
|
| 388 |
+
assert C.masscenter.pos_from(P.masscenter) == N.x - N.y + A.z
|
| 389 |
+
N_v_C = (-sqrt(2) + sqrt(6)) * u / 4 * A.x
|
| 390 |
+
assert C.masscenter.vel(N).simplify() == N_v_C
|
| 391 |
+
assert C.masscenter.vel(Pint).simplify() == N_v_C
|
| 392 |
+
assert C.masscenter.vel(Cint) == Vector(0)
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
def test_pin_joint_joint_axis():
|
| 396 |
+
q, u = dynamicsymbols('q, u')
|
| 397 |
+
# Check parent as reference
|
| 398 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 399 |
+
pin = PinJoint('J', P, C, q, u, parent_interframe=Pint,
|
| 400 |
+
child_interframe=Cint, joint_axis=P.y)
|
| 401 |
+
assert pin.joint_axis == P.y
|
| 402 |
+
assert N.dcm(A) == Matrix([[sin(q), 0, cos(q)], [0, -1, 0],
|
| 403 |
+
[cos(q), 0, -sin(q)]])
|
| 404 |
+
# Check parent_interframe as reference
|
| 405 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 406 |
+
pin = PinJoint('J', P, C, q, u, parent_interframe=Pint,
|
| 407 |
+
child_interframe=Cint, joint_axis=Pint.y)
|
| 408 |
+
assert pin.joint_axis == Pint.y
|
| 409 |
+
assert N.dcm(A) == Matrix([[-sin(q), 0, cos(q)], [0, -1, 0],
|
| 410 |
+
[cos(q), 0, sin(q)]])
|
| 411 |
+
# Check combination of joint_axis with interframes supplied as vectors (2x)
|
| 412 |
+
N, A, P, C = _generate_body()
|
| 413 |
+
pin = PinJoint('J', P, C, q, u, parent_interframe=N.z,
|
| 414 |
+
child_interframe=-C.z, joint_axis=N.z)
|
| 415 |
+
assert pin.joint_axis == N.z
|
| 416 |
+
assert N.dcm(A) == Matrix([[-cos(q), -sin(q), 0], [-sin(q), cos(q), 0],
|
| 417 |
+
[0, 0, -1]])
|
| 418 |
+
N, A, P, C = _generate_body()
|
| 419 |
+
pin = PinJoint('J', P, C, q, u, parent_interframe=N.z,
|
| 420 |
+
child_interframe=-C.z, joint_axis=N.x)
|
| 421 |
+
assert pin.joint_axis == N.x
|
| 422 |
+
assert N.dcm(A) == Matrix([[-1, 0, 0], [0, cos(q), sin(q)],
|
| 423 |
+
[0, sin(q), -cos(q)]])
|
| 424 |
+
# Check time varying axis
|
| 425 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 426 |
+
raises(ValueError, lambda: PinJoint('J', P, C,
|
| 427 |
+
joint_axis=cos(q) * N.x + sin(q) * N.y))
|
| 428 |
+
# Check joint_axis provided in child frame
|
| 429 |
+
raises(ValueError, lambda: PinJoint('J', P, C, joint_axis=C.x))
|
| 430 |
+
# Check some invalid combinations
|
| 431 |
+
raises(ValueError, lambda: PinJoint('J', P, C, joint_axis=P.x + C.y))
|
| 432 |
+
raises(ValueError, lambda: PinJoint(
|
| 433 |
+
'J', P, C, parent_interframe=Pint, child_interframe=Cint,
|
| 434 |
+
joint_axis=Pint.x + C.y))
|
| 435 |
+
raises(ValueError, lambda: PinJoint(
|
| 436 |
+
'J', P, C, parent_interframe=Pint, child_interframe=Cint,
|
| 437 |
+
joint_axis=P.x + Cint.y))
|
| 438 |
+
# Check valid special combination
|
| 439 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 440 |
+
PinJoint('J', P, C, parent_interframe=Pint, child_interframe=Cint,
|
| 441 |
+
joint_axis=Pint.x + P.y)
|
| 442 |
+
# Check invalid zero vector
|
| 443 |
+
raises(Exception, lambda: PinJoint(
|
| 444 |
+
'J', P, C, parent_interframe=Pint, child_interframe=Cint,
|
| 445 |
+
joint_axis=Vector(0)))
|
| 446 |
+
raises(Exception, lambda: PinJoint(
|
| 447 |
+
'J', P, C, parent_interframe=Pint, child_interframe=Cint,
|
| 448 |
+
joint_axis=P.y + Pint.y))
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
def test_pin_joint_arbitrary_axis():
|
| 452 |
+
q, u = dynamicsymbols('q_J, u_J')
|
| 453 |
+
|
| 454 |
+
# When the bodies are attached though masscenters but axes are opposite.
|
| 455 |
+
N, A, P, C = _generate_body()
|
| 456 |
+
PinJoint('J', P, C, child_interframe=-A.x)
|
| 457 |
+
|
| 458 |
+
assert (-A.x).angle_between(N.x) == 0
|
| 459 |
+
assert -A.x.express(N) == N.x
|
| 460 |
+
assert A.dcm(N) == Matrix([[-1, 0, 0],
|
| 461 |
+
[0, -cos(q), -sin(q)],
|
| 462 |
+
[0, -sin(q), cos(q)]])
|
| 463 |
+
assert A.ang_vel_in(N) == u*N.x
|
| 464 |
+
assert A.ang_vel_in(N).magnitude() == sqrt(u**2)
|
| 465 |
+
assert C.masscenter.pos_from(P.masscenter) == 0
|
| 466 |
+
assert C.masscenter.pos_from(P.masscenter).express(N).simplify() == 0
|
| 467 |
+
assert C.masscenter.vel(N) == 0
|
| 468 |
+
|
| 469 |
+
# When axes are different and parent joint is at masscenter but child joint
|
| 470 |
+
# is at a unit vector from child masscenter.
|
| 471 |
+
N, A, P, C = _generate_body()
|
| 472 |
+
PinJoint('J', P, C, child_interframe=A.y, child_point=A.x)
|
| 473 |
+
|
| 474 |
+
assert A.y.angle_between(N.x) == 0 # Axis are aligned
|
| 475 |
+
assert A.y.express(N) == N.x
|
| 476 |
+
assert A.dcm(N) == Matrix([[0, -cos(q), -sin(q)],
|
| 477 |
+
[1, 0, 0],
|
| 478 |
+
[0, -sin(q), cos(q)]])
|
| 479 |
+
assert A.ang_vel_in(N) == u*N.x
|
| 480 |
+
assert A.ang_vel_in(N).express(A) == u * A.y
|
| 481 |
+
assert A.ang_vel_in(N).magnitude() == sqrt(u**2)
|
| 482 |
+
assert A.ang_vel_in(N).cross(A.y) == 0
|
| 483 |
+
assert C.masscenter.vel(N) == u*A.z
|
| 484 |
+
assert C.masscenter.pos_from(P.masscenter) == -A.x
|
| 485 |
+
assert (C.masscenter.pos_from(P.masscenter).express(N).simplify() ==
|
| 486 |
+
cos(q)*N.y + sin(q)*N.z)
|
| 487 |
+
assert C.masscenter.vel(N).angle_between(A.x) == pi/2
|
| 488 |
+
|
| 489 |
+
# Similar to previous case but wrt parent body
|
| 490 |
+
N, A, P, C = _generate_body()
|
| 491 |
+
PinJoint('J', P, C, parent_interframe=N.y, parent_point=N.x)
|
| 492 |
+
|
| 493 |
+
assert N.y.angle_between(A.x) == 0 # Axis are aligned
|
| 494 |
+
assert N.y.express(A) == A.x
|
| 495 |
+
assert A.dcm(N) == Matrix([[0, 1, 0],
|
| 496 |
+
[-cos(q), 0, sin(q)],
|
| 497 |
+
[sin(q), 0, cos(q)]])
|
| 498 |
+
assert A.ang_vel_in(N) == u*N.y
|
| 499 |
+
assert A.ang_vel_in(N).express(A) == u*A.x
|
| 500 |
+
assert A.ang_vel_in(N).magnitude() == sqrt(u**2)
|
| 501 |
+
angle = A.ang_vel_in(N).angle_between(A.x)
|
| 502 |
+
assert angle.xreplace({u: 1}) == 0
|
| 503 |
+
assert C.masscenter.vel(N) == 0
|
| 504 |
+
assert C.masscenter.pos_from(P.masscenter) == N.x
|
| 505 |
+
|
| 506 |
+
# Both joint pos id defined but different axes
|
| 507 |
+
N, A, P, C = _generate_body()
|
| 508 |
+
PinJoint('J', P, C, parent_point=N.x, child_point=A.x,
|
| 509 |
+
child_interframe=A.x + A.y)
|
| 510 |
+
assert expand_mul(N.x.angle_between(A.x + A.y)) == 0 # Axis are aligned
|
| 511 |
+
assert (A.x + A.y).express(N).simplify() == sqrt(2)*N.x
|
| 512 |
+
assert simplify(A.dcm(N)) == Matrix([
|
| 513 |
+
[sqrt(2)/2, -sqrt(2)*cos(q)/2, -sqrt(2)*sin(q)/2],
|
| 514 |
+
[sqrt(2)/2, sqrt(2)*cos(q)/2, sqrt(2)*sin(q)/2],
|
| 515 |
+
[0, -sin(q), cos(q)]])
|
| 516 |
+
assert A.ang_vel_in(N) == u*N.x
|
| 517 |
+
assert (A.ang_vel_in(N).express(A).simplify() ==
|
| 518 |
+
(u*A.x + u*A.y)/sqrt(2))
|
| 519 |
+
assert A.ang_vel_in(N).magnitude() == sqrt(u**2)
|
| 520 |
+
angle = A.ang_vel_in(N).angle_between(A.x + A.y)
|
| 521 |
+
assert angle.xreplace({u: 1}) == 0
|
| 522 |
+
assert C.masscenter.vel(N).simplify() == (u * A.z)/sqrt(2)
|
| 523 |
+
assert C.masscenter.pos_from(P.masscenter) == N.x - A.x
|
| 524 |
+
assert (C.masscenter.pos_from(P.masscenter).express(N).simplify() ==
|
| 525 |
+
(1 - sqrt(2)/2)*N.x + sqrt(2)*cos(q)/2*N.y +
|
| 526 |
+
sqrt(2)*sin(q)/2*N.z)
|
| 527 |
+
assert (C.masscenter.vel(N).express(N).simplify() ==
|
| 528 |
+
-sqrt(2)*u*sin(q)/2*N.y + sqrt(2)*u*cos(q)/2*N.z)
|
| 529 |
+
assert C.masscenter.vel(N).angle_between(A.x) == pi/2
|
| 530 |
+
|
| 531 |
+
N, A, P, C = _generate_body()
|
| 532 |
+
PinJoint('J', P, C, parent_point=N.x, child_point=A.x,
|
| 533 |
+
child_interframe=A.x + A.y - A.z)
|
| 534 |
+
assert expand_mul(N.x.angle_between(A.x + A.y - A.z)) == 0 # Axis aligned
|
| 535 |
+
assert (A.x + A.y - A.z).express(N).simplify() == sqrt(3)*N.x
|
| 536 |
+
assert simplify(A.dcm(N)) == Matrix([
|
| 537 |
+
[sqrt(3)/3, -sqrt(6)*sin(q + pi/4)/3,
|
| 538 |
+
sqrt(6)*cos(q + pi/4)/3],
|
| 539 |
+
[sqrt(3)/3, sqrt(6)*cos(q + pi/12)/3,
|
| 540 |
+
sqrt(6)*sin(q + pi/12)/3],
|
| 541 |
+
[-sqrt(3)/3, sqrt(6)*cos(q + 5*pi/12)/3,
|
| 542 |
+
sqrt(6)*sin(q + 5*pi/12)/3]])
|
| 543 |
+
assert A.ang_vel_in(N) == u*N.x
|
| 544 |
+
assert A.ang_vel_in(N).express(A).simplify() == (u*A.x + u*A.y -
|
| 545 |
+
u*A.z)/sqrt(3)
|
| 546 |
+
assert A.ang_vel_in(N).magnitude() == sqrt(u**2)
|
| 547 |
+
angle = A.ang_vel_in(N).angle_between(A.x + A.y-A.z)
|
| 548 |
+
assert angle.xreplace({u: 1}).simplify() == 0
|
| 549 |
+
assert C.masscenter.vel(N).simplify() == (u*A.y + u*A.z)/sqrt(3)
|
| 550 |
+
assert C.masscenter.pos_from(P.masscenter) == N.x - A.x
|
| 551 |
+
assert (C.masscenter.pos_from(P.masscenter).express(N).simplify() ==
|
| 552 |
+
(1 - sqrt(3)/3)*N.x + sqrt(6)*sin(q + pi/4)/3*N.y -
|
| 553 |
+
sqrt(6)*cos(q + pi/4)/3*N.z)
|
| 554 |
+
assert (C.masscenter.vel(N).express(N).simplify() ==
|
| 555 |
+
sqrt(6)*u*cos(q + pi/4)/3*N.y +
|
| 556 |
+
sqrt(6)*u*sin(q + pi/4)/3*N.z)
|
| 557 |
+
assert C.masscenter.vel(N).angle_between(A.x) == pi/2
|
| 558 |
+
|
| 559 |
+
N, A, P, C = _generate_body()
|
| 560 |
+
m, n = symbols('m n')
|
| 561 |
+
PinJoint('J', P, C, parent_point=m * N.x, child_point=n * A.x,
|
| 562 |
+
child_interframe=A.x + A.y - A.z,
|
| 563 |
+
parent_interframe=N.x - N.y + N.z)
|
| 564 |
+
angle = (N.x - N.y + N.z).angle_between(A.x + A.y - A.z)
|
| 565 |
+
assert expand_mul(angle) == 0 # Axis are aligned
|
| 566 |
+
assert ((A.x-A.y+A.z).express(N).simplify() ==
|
| 567 |
+
(-4*cos(q)/3 - S(1)/3)*N.x + (S(1)/3 - 4*sin(q + pi/6)/3)*N.y +
|
| 568 |
+
(4*cos(q + pi/3)/3 - S(1)/3)*N.z)
|
| 569 |
+
assert simplify(A.dcm(N)) == Matrix([
|
| 570 |
+
[S(1)/3 - 2*cos(q)/3, -2*sin(q + pi/6)/3 - S(1)/3,
|
| 571 |
+
2*cos(q + pi/3)/3 + S(1)/3],
|
| 572 |
+
[2*cos(q + pi/3)/3 + S(1)/3, 2*cos(q)/3 - S(1)/3,
|
| 573 |
+
2*sin(q + pi/6)/3 + S(1)/3],
|
| 574 |
+
[-2*sin(q + pi/6)/3 - S(1)/3, 2*cos(q + pi/3)/3 + S(1)/3,
|
| 575 |
+
2*cos(q)/3 - S(1)/3]])
|
| 576 |
+
assert (A.ang_vel_in(N) - (u*N.x - u*N.y + u*N.z)/sqrt(3)).simplify()
|
| 577 |
+
assert A.ang_vel_in(N).express(A).simplify() == (u*A.x + u*A.y -
|
| 578 |
+
u*A.z)/sqrt(3)
|
| 579 |
+
assert A.ang_vel_in(N).magnitude() == sqrt(u**2)
|
| 580 |
+
angle = A.ang_vel_in(N).angle_between(A.x+A.y-A.z)
|
| 581 |
+
assert angle.xreplace({u: 1}).simplify() == 0
|
| 582 |
+
assert (C.masscenter.vel(N).simplify() ==
|
| 583 |
+
sqrt(3)*n*u/3*A.y + sqrt(3)*n*u/3*A.z)
|
| 584 |
+
assert C.masscenter.pos_from(P.masscenter) == m*N.x - n*A.x
|
| 585 |
+
assert (C.masscenter.pos_from(P.masscenter).express(N).simplify() ==
|
| 586 |
+
(m + n*(2*cos(q) - 1)/3)*N.x + n*(2*sin(q + pi/6) +
|
| 587 |
+
1)/3*N.y - n*(2*cos(q + pi/3) + 1)/3*N.z)
|
| 588 |
+
assert (C.masscenter.vel(N).express(N).simplify() ==
|
| 589 |
+
- 2*n*u*sin(q)/3*N.x + 2*n*u*cos(q + pi/6)/3*N.y +
|
| 590 |
+
2*n*u*sin(q + pi/3)/3*N.z)
|
| 591 |
+
assert C.masscenter.vel(N).dot(N.x - N.y + N.z).simplify() == 0
|
| 592 |
+
|
| 593 |
+
|
| 594 |
+
def test_create_aligned_frame_pi():
|
| 595 |
+
N, A, P, C = _generate_body()
|
| 596 |
+
f = Joint._create_aligned_interframe(P, -P.x, P.x)
|
| 597 |
+
assert f.z == P.z
|
| 598 |
+
f = Joint._create_aligned_interframe(P, -P.y, P.y)
|
| 599 |
+
assert f.x == P.x
|
| 600 |
+
f = Joint._create_aligned_interframe(P, -P.z, P.z)
|
| 601 |
+
assert f.y == P.y
|
| 602 |
+
f = Joint._create_aligned_interframe(P, -P.x - P.y, P.x + P.y)
|
| 603 |
+
assert f.z == P.z
|
| 604 |
+
f = Joint._create_aligned_interframe(P, -P.y - P.z, P.y + P.z)
|
| 605 |
+
assert f.x == P.x
|
| 606 |
+
f = Joint._create_aligned_interframe(P, -P.x - P.z, P.x + P.z)
|
| 607 |
+
assert f.y == P.y
|
| 608 |
+
f = Joint._create_aligned_interframe(P, -P.x - P.y - P.z, P.x + P.y + P.z)
|
| 609 |
+
assert f.y - f.z == P.y - P.z
|
| 610 |
+
|
| 611 |
+
|
| 612 |
+
def test_pin_joint_axis():
|
| 613 |
+
q, u = dynamicsymbols('q u')
|
| 614 |
+
# Test default joint axis
|
| 615 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 616 |
+
J = PinJoint('J', P, C, q, u, parent_interframe=Pint, child_interframe=Cint)
|
| 617 |
+
assert J.joint_axis == Pint.x
|
| 618 |
+
# Test for the same joint axis expressed in different frames
|
| 619 |
+
N_R_A = Matrix([[0, sin(q), cos(q)],
|
| 620 |
+
[0, -cos(q), sin(q)],
|
| 621 |
+
[1, 0, 0]])
|
| 622 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 623 |
+
PinJoint('J', P, C, q, u, parent_interframe=Pint, child_interframe=Cint,
|
| 624 |
+
joint_axis=N.z)
|
| 625 |
+
assert N.dcm(A) == N_R_A
|
| 626 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 627 |
+
PinJoint('J', P, C, q, u, parent_interframe=Pint, child_interframe=Cint,
|
| 628 |
+
joint_axis=-Pint.z)
|
| 629 |
+
assert N.dcm(A) == N_R_A
|
| 630 |
+
# Test time varying joint axis
|
| 631 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 632 |
+
raises(ValueError, lambda: PinJoint('J', P, C, joint_axis=q * N.z))
|
| 633 |
+
|
| 634 |
+
|
| 635 |
+
def test_locate_joint_pos():
|
| 636 |
+
# Test Vector and default
|
| 637 |
+
N, A, P, C = _generate_body()
|
| 638 |
+
joint = PinJoint('J', P, C, parent_point=N.y + N.z)
|
| 639 |
+
assert joint.parent_point.name == 'J_P_joint'
|
| 640 |
+
assert joint.parent_point.pos_from(P.masscenter) == N.y + N.z
|
| 641 |
+
assert joint.child_point == C.masscenter
|
| 642 |
+
# Test Point objects
|
| 643 |
+
N, A, P, C = _generate_body()
|
| 644 |
+
parent_point = P.masscenter.locatenew('p', N.y + N.z)
|
| 645 |
+
joint = PinJoint('J', P, C, parent_point=parent_point,
|
| 646 |
+
child_point=C.masscenter)
|
| 647 |
+
assert joint.parent_point == parent_point
|
| 648 |
+
assert joint.child_point == C.masscenter
|
| 649 |
+
# Check invalid type
|
| 650 |
+
N, A, P, C = _generate_body()
|
| 651 |
+
raises(TypeError,
|
| 652 |
+
lambda: PinJoint('J', P, C, parent_point=N.x.to_matrix(N)))
|
| 653 |
+
# Test time varying positions
|
| 654 |
+
q = dynamicsymbols('q')
|
| 655 |
+
N, A, P, C = _generate_body()
|
| 656 |
+
raises(ValueError, lambda: PinJoint('J', P, C, parent_point=q * N.x))
|
| 657 |
+
N, A, P, C = _generate_body()
|
| 658 |
+
child_point = C.masscenter.locatenew('p', q * A.y)
|
| 659 |
+
raises(ValueError, lambda: PinJoint('J', P, C, child_point=child_point))
|
| 660 |
+
# Test undefined position
|
| 661 |
+
child_point = Point('p')
|
| 662 |
+
raises(ValueError, lambda: PinJoint('J', P, C, child_point=child_point))
|
| 663 |
+
|
| 664 |
+
|
| 665 |
+
def test_locate_joint_frame():
|
| 666 |
+
# Test rotated frame and default
|
| 667 |
+
N, A, P, C = _generate_body()
|
| 668 |
+
parent_interframe = ReferenceFrame('int_frame')
|
| 669 |
+
parent_interframe.orient_axis(N, N.z, 1)
|
| 670 |
+
joint = PinJoint('J', P, C, parent_interframe=parent_interframe)
|
| 671 |
+
assert joint.parent_interframe == parent_interframe
|
| 672 |
+
assert joint.parent_interframe.ang_vel_in(N) == 0
|
| 673 |
+
assert joint.child_interframe == A
|
| 674 |
+
# Test time varying orientations
|
| 675 |
+
q = dynamicsymbols('q')
|
| 676 |
+
N, A, P, C = _generate_body()
|
| 677 |
+
parent_interframe = ReferenceFrame('int_frame')
|
| 678 |
+
parent_interframe.orient_axis(N, N.z, q)
|
| 679 |
+
raises(ValueError,
|
| 680 |
+
lambda: PinJoint('J', P, C, parent_interframe=parent_interframe))
|
| 681 |
+
# Test undefined frame
|
| 682 |
+
N, A, P, C = _generate_body()
|
| 683 |
+
child_interframe = ReferenceFrame('int_frame')
|
| 684 |
+
child_interframe.orient_axis(N, N.z, 1) # Defined with respect to parent
|
| 685 |
+
raises(ValueError,
|
| 686 |
+
lambda: PinJoint('J', P, C, child_interframe=child_interframe))
|
| 687 |
+
|
| 688 |
+
|
| 689 |
+
def test_prismatic_joint():
|
| 690 |
+
_, _, P, C = _generate_body()
|
| 691 |
+
q, u = dynamicsymbols('q_S, u_S')
|
| 692 |
+
S = PrismaticJoint('S', P, C)
|
| 693 |
+
assert S.name == 'S'
|
| 694 |
+
assert S.parent == P
|
| 695 |
+
assert S.child == C
|
| 696 |
+
assert S.coordinates == Matrix([q])
|
| 697 |
+
assert S.speeds == Matrix([u])
|
| 698 |
+
assert S.kdes == Matrix([u - q.diff(t)])
|
| 699 |
+
assert S.joint_axis == P.frame.x
|
| 700 |
+
assert S.child_point.pos_from(C.masscenter) == Vector(0)
|
| 701 |
+
assert S.parent_point.pos_from(P.masscenter) == Vector(0)
|
| 702 |
+
assert S.parent_point.pos_from(S.child_point) == - q * P.frame.x
|
| 703 |
+
assert P.masscenter.pos_from(C.masscenter) == - q * P.frame.x
|
| 704 |
+
assert C.masscenter.vel(P.frame) == u * P.frame.x
|
| 705 |
+
assert P.frame.ang_vel_in(C.frame) == 0
|
| 706 |
+
assert C.frame.ang_vel_in(P.frame) == 0
|
| 707 |
+
assert S.__str__() == 'PrismaticJoint: S parent: P child: C'
|
| 708 |
+
|
| 709 |
+
N, A, P, C = _generate_body()
|
| 710 |
+
l, m = symbols('l m')
|
| 711 |
+
Pint = ReferenceFrame('P_int')
|
| 712 |
+
Pint.orient_axis(P.frame, P.y, pi / 2)
|
| 713 |
+
S = PrismaticJoint('S', P, C, parent_point=l * P.frame.x,
|
| 714 |
+
child_point=m * C.frame.y, joint_axis=P.frame.z,
|
| 715 |
+
parent_interframe=Pint)
|
| 716 |
+
|
| 717 |
+
assert S.joint_axis == P.frame.z
|
| 718 |
+
assert S.child_point.pos_from(C.masscenter) == m * C.frame.y
|
| 719 |
+
assert S.parent_point.pos_from(P.masscenter) == l * P.frame.x
|
| 720 |
+
assert S.parent_point.pos_from(S.child_point) == - q * P.frame.z
|
| 721 |
+
assert P.masscenter.pos_from(C.masscenter) == - l * N.x - q * N.z + m * A.y
|
| 722 |
+
assert C.masscenter.vel(P.frame) == u * P.frame.z
|
| 723 |
+
assert P.masscenter.vel(Pint) == Vector(0)
|
| 724 |
+
assert C.frame.ang_vel_in(P.frame) == 0
|
| 725 |
+
assert P.frame.ang_vel_in(C.frame) == 0
|
| 726 |
+
|
| 727 |
+
_, _, P, C = _generate_body()
|
| 728 |
+
Pint = ReferenceFrame('P_int')
|
| 729 |
+
Pint.orient_axis(P.frame, P.y, pi / 2)
|
| 730 |
+
S = PrismaticJoint('S', P, C, parent_point=l * P.frame.z,
|
| 731 |
+
child_point=m * C.frame.x, joint_axis=P.frame.z,
|
| 732 |
+
parent_interframe=Pint)
|
| 733 |
+
assert S.joint_axis == P.frame.z
|
| 734 |
+
assert S.child_point.pos_from(C.masscenter) == m * C.frame.x
|
| 735 |
+
assert S.parent_point.pos_from(P.masscenter) == l * P.frame.z
|
| 736 |
+
assert S.parent_point.pos_from(S.child_point) == - q * P.frame.z
|
| 737 |
+
assert P.masscenter.pos_from(C.masscenter) == (-l - q)*P.frame.z + m*C.frame.x
|
| 738 |
+
assert C.masscenter.vel(P.frame) == u * P.frame.z
|
| 739 |
+
assert C.frame.ang_vel_in(P.frame) == 0
|
| 740 |
+
assert P.frame.ang_vel_in(C.frame) == 0
|
| 741 |
+
|
| 742 |
+
|
| 743 |
+
def test_prismatic_joint_arbitrary_axis():
|
| 744 |
+
q, u = dynamicsymbols('q_S, u_S')
|
| 745 |
+
|
| 746 |
+
N, A, P, C = _generate_body()
|
| 747 |
+
PrismaticJoint('S', P, C, child_interframe=-A.x)
|
| 748 |
+
|
| 749 |
+
assert (-A.x).angle_between(N.x) == 0
|
| 750 |
+
assert -A.x.express(N) == N.x
|
| 751 |
+
assert A.dcm(N) == Matrix([[-1, 0, 0], [0, -1, 0], [0, 0, 1]])
|
| 752 |
+
assert C.masscenter.pos_from(P.masscenter) == q * N.x
|
| 753 |
+
assert C.masscenter.pos_from(P.masscenter).express(A).simplify() == -q * A.x
|
| 754 |
+
assert C.masscenter.vel(N) == u * N.x
|
| 755 |
+
assert C.masscenter.vel(N).express(A) == -u * A.x
|
| 756 |
+
assert A.ang_vel_in(N) == 0
|
| 757 |
+
assert N.ang_vel_in(A) == 0
|
| 758 |
+
|
| 759 |
+
#When axes are different and parent joint is at masscenter but child joint is at a unit vector from
|
| 760 |
+
#child masscenter.
|
| 761 |
+
N, A, P, C = _generate_body()
|
| 762 |
+
PrismaticJoint('S', P, C, child_interframe=A.y, child_point=A.x)
|
| 763 |
+
|
| 764 |
+
assert A.y.angle_between(N.x) == 0 #Axis are aligned
|
| 765 |
+
assert A.y.express(N) == N.x
|
| 766 |
+
assert A.dcm(N) == Matrix([[0, -1, 0], [1, 0, 0], [0, 0, 1]])
|
| 767 |
+
assert C.masscenter.vel(N) == u * N.x
|
| 768 |
+
assert C.masscenter.vel(N).express(A) == u * A.y
|
| 769 |
+
assert C.masscenter.pos_from(P.masscenter) == q*N.x - A.x
|
| 770 |
+
assert C.masscenter.pos_from(P.masscenter).express(N).simplify() == q*N.x + N.y
|
| 771 |
+
assert A.ang_vel_in(N) == 0
|
| 772 |
+
assert N.ang_vel_in(A) == 0
|
| 773 |
+
|
| 774 |
+
#Similar to previous case but wrt parent body
|
| 775 |
+
N, A, P, C = _generate_body()
|
| 776 |
+
PrismaticJoint('S', P, C, parent_interframe=N.y, parent_point=N.x)
|
| 777 |
+
|
| 778 |
+
assert N.y.angle_between(A.x) == 0 #Axis are aligned
|
| 779 |
+
assert N.y.express(A) == A.x
|
| 780 |
+
assert A.dcm(N) == Matrix([[0, 1, 0], [-1, 0, 0], [0, 0, 1]])
|
| 781 |
+
assert C.masscenter.vel(N) == u * N.y
|
| 782 |
+
assert C.masscenter.vel(N).express(A) == u * A.x
|
| 783 |
+
assert C.masscenter.pos_from(P.masscenter) == N.x + q*N.y
|
| 784 |
+
assert A.ang_vel_in(N) == 0
|
| 785 |
+
assert N.ang_vel_in(A) == 0
|
| 786 |
+
|
| 787 |
+
#Both joint pos is defined but different axes
|
| 788 |
+
N, A, P, C = _generate_body()
|
| 789 |
+
PrismaticJoint('S', P, C, parent_point=N.x, child_point=A.x,
|
| 790 |
+
child_interframe=A.x + A.y)
|
| 791 |
+
assert N.x.angle_between(A.x + A.y) == 0 #Axis are aligned
|
| 792 |
+
assert (A.x + A.y).express(N) == sqrt(2)*N.x
|
| 793 |
+
assert A.dcm(N) == Matrix([[sqrt(2)/2, -sqrt(2)/2, 0], [sqrt(2)/2, sqrt(2)/2, 0], [0, 0, 1]])
|
| 794 |
+
assert C.masscenter.pos_from(P.masscenter) == (q + 1)*N.x - A.x
|
| 795 |
+
assert C.masscenter.pos_from(P.masscenter).express(N) == (q - sqrt(2)/2 + 1)*N.x + sqrt(2)/2*N.y
|
| 796 |
+
assert C.masscenter.vel(N).express(A) == u * (A.x + A.y)/sqrt(2)
|
| 797 |
+
assert C.masscenter.vel(N) == u*N.x
|
| 798 |
+
assert A.ang_vel_in(N) == 0
|
| 799 |
+
assert N.ang_vel_in(A) == 0
|
| 800 |
+
|
| 801 |
+
N, A, P, C = _generate_body()
|
| 802 |
+
PrismaticJoint('S', P, C, parent_point=N.x, child_point=A.x,
|
| 803 |
+
child_interframe=A.x + A.y - A.z)
|
| 804 |
+
assert N.x.angle_between(A.x + A.y - A.z).simplify() == 0 #Axis are aligned
|
| 805 |
+
assert ((A.x + A.y - A.z).express(N) - sqrt(3)*N.x).simplify() == 0
|
| 806 |
+
assert simplify(A.dcm(N)) == Matrix([[sqrt(3)/3, -sqrt(3)/3, sqrt(3)/3],
|
| 807 |
+
[sqrt(3)/3, sqrt(3)/6 + S(1)/2, S(1)/2 - sqrt(3)/6],
|
| 808 |
+
[-sqrt(3)/3, S(1)/2 - sqrt(3)/6, sqrt(3)/6 + S(1)/2]])
|
| 809 |
+
assert C.masscenter.pos_from(P.masscenter) == (q + 1)*N.x - A.x
|
| 810 |
+
assert (C.masscenter.pos_from(P.masscenter).express(N) -
|
| 811 |
+
((q - sqrt(3)/3 + 1)*N.x + sqrt(3)/3*N.y - sqrt(3)/3*N.z)).simplify() == 0
|
| 812 |
+
assert C.masscenter.vel(N) == u*N.x
|
| 813 |
+
assert (C.masscenter.vel(N).express(A) - (
|
| 814 |
+
sqrt(3)*u/3*A.x + sqrt(3)*u/3*A.y - sqrt(3)*u/3*A.z)).simplify()
|
| 815 |
+
assert A.ang_vel_in(N) == 0
|
| 816 |
+
assert N.ang_vel_in(A) == 0
|
| 817 |
+
|
| 818 |
+
N, A, P, C = _generate_body()
|
| 819 |
+
m, n = symbols('m n')
|
| 820 |
+
PrismaticJoint('S', P, C, parent_point=m*N.x, child_point=n*A.x,
|
| 821 |
+
child_interframe=A.x + A.y - A.z,
|
| 822 |
+
parent_interframe=N.x - N.y + N.z)
|
| 823 |
+
# 0 angle means that the axis are aligned
|
| 824 |
+
assert (N.x-N.y+N.z).angle_between(A.x+A.y-A.z).simplify() == 0
|
| 825 |
+
assert ((A.x+A.y-A.z).express(N) - (N.x - N.y + N.z)).simplify() == 0
|
| 826 |
+
assert simplify(A.dcm(N)) == Matrix([[-S(1)/3, -S(2)/3, S(2)/3],
|
| 827 |
+
[S(2)/3, S(1)/3, S(2)/3],
|
| 828 |
+
[-S(2)/3, S(2)/3, S(1)/3]])
|
| 829 |
+
assert (C.masscenter.pos_from(P.masscenter) - (
|
| 830 |
+
(m + sqrt(3)*q/3)*N.x - sqrt(3)*q/3*N.y + sqrt(3)*q/3*N.z - n*A.x)
|
| 831 |
+
).express(N).simplify() == 0
|
| 832 |
+
assert (C.masscenter.pos_from(P.masscenter).express(N) - (
|
| 833 |
+
(m + n/3 + sqrt(3)*q/3)*N.x + (2*n/3 - sqrt(3)*q/3)*N.y +
|
| 834 |
+
(-2*n/3 + sqrt(3)*q/3)*N.z)).simplify() == 0
|
| 835 |
+
assert (C.masscenter.vel(N).express(N) - (
|
| 836 |
+
sqrt(3)*u/3*N.x - sqrt(3)*u/3*N.y + sqrt(3)*u/3*N.z)).simplify() == 0
|
| 837 |
+
assert (C.masscenter.vel(N).express(A) -
|
| 838 |
+
(sqrt(3)*u/3*A.x + sqrt(3)*u/3*A.y - sqrt(3)*u/3*A.z)).simplify() == 0
|
| 839 |
+
assert A.ang_vel_in(N) == 0
|
| 840 |
+
assert N.ang_vel_in(A) == 0
|
| 841 |
+
|
| 842 |
+
|
| 843 |
+
def test_cylindrical_joint():
|
| 844 |
+
N, A, P, C = _generate_body()
|
| 845 |
+
q0_def, q1_def, u0_def, u1_def = dynamicsymbols('q0:2_J, u0:2_J')
|
| 846 |
+
Cj = CylindricalJoint('J', P, C)
|
| 847 |
+
assert Cj.name == 'J'
|
| 848 |
+
assert Cj.parent == P
|
| 849 |
+
assert Cj.child == C
|
| 850 |
+
assert Cj.coordinates == Matrix([q0_def, q1_def])
|
| 851 |
+
assert Cj.speeds == Matrix([u0_def, u1_def])
|
| 852 |
+
assert Cj.rotation_coordinate == q0_def
|
| 853 |
+
assert Cj.translation_coordinate == q1_def
|
| 854 |
+
assert Cj.rotation_speed == u0_def
|
| 855 |
+
assert Cj.translation_speed == u1_def
|
| 856 |
+
assert Cj.kdes == Matrix([u0_def - q0_def.diff(t), u1_def - q1_def.diff(t)])
|
| 857 |
+
assert Cj.joint_axis == N.x
|
| 858 |
+
assert Cj.child_point.pos_from(C.masscenter) == Vector(0)
|
| 859 |
+
assert Cj.parent_point.pos_from(P.masscenter) == Vector(0)
|
| 860 |
+
assert Cj.parent_point.pos_from(Cj._child_point) == -q1_def * N.x
|
| 861 |
+
assert C.masscenter.pos_from(P.masscenter) == q1_def * N.x
|
| 862 |
+
assert Cj.child_point.vel(N) == u1_def * N.x
|
| 863 |
+
assert A.ang_vel_in(N) == u0_def * N.x
|
| 864 |
+
assert Cj.parent_interframe == N
|
| 865 |
+
assert Cj.child_interframe == A
|
| 866 |
+
assert Cj.__str__() == 'CylindricalJoint: J parent: P child: C'
|
| 867 |
+
|
| 868 |
+
q0, q1, u0, u1 = dynamicsymbols('q0:2, u0:2')
|
| 869 |
+
l, m = symbols('l, m')
|
| 870 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 871 |
+
Cj = CylindricalJoint('J', P, C, rotation_coordinate=q0, rotation_speed=u0,
|
| 872 |
+
translation_speed=u1, parent_point=m * N.x,
|
| 873 |
+
child_point=l * A.y, parent_interframe=Pint,
|
| 874 |
+
child_interframe=Cint, joint_axis=2 * N.z)
|
| 875 |
+
assert Cj.coordinates == Matrix([q0, q1_def])
|
| 876 |
+
assert Cj.speeds == Matrix([u0, u1])
|
| 877 |
+
assert Cj.rotation_coordinate == q0
|
| 878 |
+
assert Cj.translation_coordinate == q1_def
|
| 879 |
+
assert Cj.rotation_speed == u0
|
| 880 |
+
assert Cj.translation_speed == u1
|
| 881 |
+
assert Cj.kdes == Matrix([u0 - q0.diff(t), u1 - q1_def.diff(t)])
|
| 882 |
+
assert Cj.joint_axis == 2 * N.z
|
| 883 |
+
assert Cj.child_point.pos_from(C.masscenter) == l * A.y
|
| 884 |
+
assert Cj.parent_point.pos_from(P.masscenter) == m * N.x
|
| 885 |
+
assert Cj.parent_point.pos_from(Cj._child_point) == -q1_def * N.z
|
| 886 |
+
assert C.masscenter.pos_from(
|
| 887 |
+
P.masscenter) == m * N.x + q1_def * N.z - l * A.y
|
| 888 |
+
assert C.masscenter.vel(N) == u1 * N.z - u0 * l * A.z
|
| 889 |
+
assert A.ang_vel_in(N) == u0 * N.z
|
| 890 |
+
|
| 891 |
+
|
| 892 |
+
def test_planar_joint():
|
| 893 |
+
N, A, P, C = _generate_body()
|
| 894 |
+
q0_def, q1_def, q2_def = dynamicsymbols('q0:3_J')
|
| 895 |
+
u0_def, u1_def, u2_def = dynamicsymbols('u0:3_J')
|
| 896 |
+
Cj = PlanarJoint('J', P, C)
|
| 897 |
+
assert Cj.name == 'J'
|
| 898 |
+
assert Cj.parent == P
|
| 899 |
+
assert Cj.child == C
|
| 900 |
+
assert Cj.coordinates == Matrix([q0_def, q1_def, q2_def])
|
| 901 |
+
assert Cj.speeds == Matrix([u0_def, u1_def, u2_def])
|
| 902 |
+
assert Cj.rotation_coordinate == q0_def
|
| 903 |
+
assert Cj.planar_coordinates == Matrix([q1_def, q2_def])
|
| 904 |
+
assert Cj.rotation_speed == u0_def
|
| 905 |
+
assert Cj.planar_speeds == Matrix([u1_def, u2_def])
|
| 906 |
+
assert Cj.kdes == Matrix([u0_def - q0_def.diff(t), u1_def - q1_def.diff(t),
|
| 907 |
+
u2_def - q2_def.diff(t)])
|
| 908 |
+
assert Cj.rotation_axis == N.x
|
| 909 |
+
assert Cj.planar_vectors == [N.y, N.z]
|
| 910 |
+
assert Cj.child_point.pos_from(C.masscenter) == Vector(0)
|
| 911 |
+
assert Cj.parent_point.pos_from(P.masscenter) == Vector(0)
|
| 912 |
+
r_P_C = q1_def * N.y + q2_def * N.z
|
| 913 |
+
assert Cj.parent_point.pos_from(Cj.child_point) == -r_P_C
|
| 914 |
+
assert C.masscenter.pos_from(P.masscenter) == r_P_C
|
| 915 |
+
assert Cj.child_point.vel(N) == u1_def * N.y + u2_def * N.z
|
| 916 |
+
assert A.ang_vel_in(N) == u0_def * N.x
|
| 917 |
+
assert Cj.parent_interframe == N
|
| 918 |
+
assert Cj.child_interframe == A
|
| 919 |
+
assert Cj.__str__() == 'PlanarJoint: J parent: P child: C'
|
| 920 |
+
|
| 921 |
+
q0, q1, q2, u0, u1, u2 = dynamicsymbols('q0:3, u0:3')
|
| 922 |
+
l, m = symbols('l, m')
|
| 923 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 924 |
+
Cj = PlanarJoint('J', P, C, rotation_coordinate=q0,
|
| 925 |
+
planar_coordinates=[q1, q2], planar_speeds=[u1, u2],
|
| 926 |
+
parent_point=m * N.x, child_point=l * A.y,
|
| 927 |
+
parent_interframe=Pint, child_interframe=Cint)
|
| 928 |
+
assert Cj.coordinates == Matrix([q0, q1, q2])
|
| 929 |
+
assert Cj.speeds == Matrix([u0_def, u1, u2])
|
| 930 |
+
assert Cj.rotation_coordinate == q0
|
| 931 |
+
assert Cj.planar_coordinates == Matrix([q1, q2])
|
| 932 |
+
assert Cj.rotation_speed == u0_def
|
| 933 |
+
assert Cj.planar_speeds == Matrix([u1, u2])
|
| 934 |
+
assert Cj.kdes == Matrix([u0_def - q0.diff(t), u1 - q1.diff(t),
|
| 935 |
+
u2 - q2.diff(t)])
|
| 936 |
+
assert Cj.rotation_axis == Pint.x
|
| 937 |
+
assert Cj.planar_vectors == [Pint.y, Pint.z]
|
| 938 |
+
assert Cj.child_point.pos_from(C.masscenter) == l * A.y
|
| 939 |
+
assert Cj.parent_point.pos_from(P.masscenter) == m * N.x
|
| 940 |
+
assert Cj.parent_point.pos_from(Cj.child_point) == q1 * N.y + q2 * N.z
|
| 941 |
+
assert C.masscenter.pos_from(
|
| 942 |
+
P.masscenter) == m * N.x - q1 * N.y - q2 * N.z - l * A.y
|
| 943 |
+
assert C.masscenter.vel(N) == -u1 * N.y - u2 * N.z + u0_def * l * A.x
|
| 944 |
+
assert A.ang_vel_in(N) == u0_def * N.x
|
| 945 |
+
|
| 946 |
+
|
| 947 |
+
def test_planar_joint_advanced():
|
| 948 |
+
# Tests whether someone is able to just specify two normals, which will form
|
| 949 |
+
# the rotation axis seen from the parent and child body.
|
| 950 |
+
# This specific example is a block on a slope, which has that same slope of
|
| 951 |
+
# 30 degrees, so in the zero configuration the frames of the parent and
|
| 952 |
+
# child are actually aligned.
|
| 953 |
+
q0, q1, q2, u0, u1, u2 = dynamicsymbols('q0:3, u0:3')
|
| 954 |
+
l1, l2 = symbols('l1:3')
|
| 955 |
+
N, A, P, C = _generate_body()
|
| 956 |
+
J = PlanarJoint('J', P, C, q0, [q1, q2], u0, [u1, u2],
|
| 957 |
+
parent_point=l1 * N.z,
|
| 958 |
+
child_point=-l2 * C.z,
|
| 959 |
+
parent_interframe=N.z + N.y / sqrt(3),
|
| 960 |
+
child_interframe=A.z + A.y / sqrt(3))
|
| 961 |
+
assert J.rotation_axis.express(N) == (N.z + N.y / sqrt(3)).normalize()
|
| 962 |
+
assert J.rotation_axis.express(A) == (A.z + A.y / sqrt(3)).normalize()
|
| 963 |
+
assert J.rotation_axis.angle_between(N.z) == pi / 6
|
| 964 |
+
assert N.dcm(A).xreplace({q0: 0, q1: 0, q2: 0}) == eye(3)
|
| 965 |
+
N_R_A = Matrix([
|
| 966 |
+
[cos(q0), -sqrt(3) * sin(q0) / 2, sin(q0) / 2],
|
| 967 |
+
[sqrt(3) * sin(q0) / 2, 3 * cos(q0) / 4 + 1 / 4,
|
| 968 |
+
sqrt(3) * (1 - cos(q0)) / 4],
|
| 969 |
+
[-sin(q0) / 2, sqrt(3) * (1 - cos(q0)) / 4, cos(q0) / 4 + 3 / 4]])
|
| 970 |
+
# N.dcm(A) == N_R_A did not work
|
| 971 |
+
assert simplify(N.dcm(A) - N_R_A) == zeros(3)
|
| 972 |
+
|
| 973 |
+
|
| 974 |
+
def test_spherical_joint():
|
| 975 |
+
N, A, P, C = _generate_body()
|
| 976 |
+
q0, q1, q2, u0, u1, u2 = dynamicsymbols('q0:3_S, u0:3_S')
|
| 977 |
+
S = SphericalJoint('S', P, C)
|
| 978 |
+
assert S.name == 'S'
|
| 979 |
+
assert S.parent == P
|
| 980 |
+
assert S.child == C
|
| 981 |
+
assert S.coordinates == Matrix([q0, q1, q2])
|
| 982 |
+
assert S.speeds == Matrix([u0, u1, u2])
|
| 983 |
+
assert S.kdes == Matrix([u0 - q0.diff(t), u1 - q1.diff(t), u2 - q2.diff(t)])
|
| 984 |
+
assert S.child_point.pos_from(C.masscenter) == Vector(0)
|
| 985 |
+
assert S.parent_point.pos_from(P.masscenter) == Vector(0)
|
| 986 |
+
assert S.parent_point.pos_from(S.child_point) == Vector(0)
|
| 987 |
+
assert P.masscenter.pos_from(C.masscenter) == Vector(0)
|
| 988 |
+
assert C.masscenter.vel(N) == Vector(0)
|
| 989 |
+
assert N.ang_vel_in(A) == (-u0 * cos(q1) * cos(q2) - u1 * sin(q2)) * A.x + (
|
| 990 |
+
u0 * sin(q2) * cos(q1) - u1 * cos(q2)) * A.y + (
|
| 991 |
+
-u0 * sin(q1) - u2) * A.z
|
| 992 |
+
assert A.ang_vel_in(N) == (u0 * cos(q1) * cos(q2) + u1 * sin(q2)) * A.x + (
|
| 993 |
+
-u0 * sin(q2) * cos(q1) + u1 * cos(q2)) * A.y + (
|
| 994 |
+
u0 * sin(q1) + u2) * A.z
|
| 995 |
+
assert S.__str__() == 'SphericalJoint: S parent: P child: C'
|
| 996 |
+
assert S._rot_type == 'BODY'
|
| 997 |
+
assert S._rot_order == 123
|
| 998 |
+
assert S._amounts is None
|
| 999 |
+
|
| 1000 |
+
|
| 1001 |
+
def test_spherical_joint_speeds_as_derivative_terms():
|
| 1002 |
+
# This tests checks whether the system remains valid if the user chooses to
|
| 1003 |
+
# pass the derivative of the generalized coordinates as generalized speeds
|
| 1004 |
+
q0, q1, q2 = dynamicsymbols('q0:3')
|
| 1005 |
+
u0, u1, u2 = dynamicsymbols('q0:3', 1)
|
| 1006 |
+
N, A, P, C = _generate_body()
|
| 1007 |
+
S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2])
|
| 1008 |
+
assert S.coordinates == Matrix([q0, q1, q2])
|
| 1009 |
+
assert S.speeds == Matrix([u0, u1, u2])
|
| 1010 |
+
assert S.kdes == Matrix([0, 0, 0])
|
| 1011 |
+
assert N.ang_vel_in(A) == (-u0 * cos(q1) * cos(q2) - u1 * sin(q2)) * A.x + (
|
| 1012 |
+
u0 * sin(q2) * cos(q1) - u1 * cos(q2)) * A.y + (
|
| 1013 |
+
-u0 * sin(q1) - u2) * A.z
|
| 1014 |
+
|
| 1015 |
+
|
| 1016 |
+
def test_spherical_joint_coords():
|
| 1017 |
+
q0s, q1s, q2s, u0s, u1s, u2s = dynamicsymbols('q0:3_S, u0:3_S')
|
| 1018 |
+
q0, q1, q2, q3, u0, u1, u2, u4 = dynamicsymbols('q0:4, u0:4')
|
| 1019 |
+
# Test coordinates as list
|
| 1020 |
+
N, A, P, C = _generate_body()
|
| 1021 |
+
S = SphericalJoint('S', P, C, [q0, q1, q2], [u0, u1, u2])
|
| 1022 |
+
assert S.coordinates == Matrix([q0, q1, q2])
|
| 1023 |
+
assert S.speeds == Matrix([u0, u1, u2])
|
| 1024 |
+
# Test coordinates as Matrix
|
| 1025 |
+
N, A, P, C = _generate_body()
|
| 1026 |
+
S = SphericalJoint('S', P, C, Matrix([q0, q1, q2]),
|
| 1027 |
+
Matrix([u0, u1, u2]))
|
| 1028 |
+
assert S.coordinates == Matrix([q0, q1, q2])
|
| 1029 |
+
assert S.speeds == Matrix([u0, u1, u2])
|
| 1030 |
+
# Test too few generalized coordinates
|
| 1031 |
+
N, A, P, C = _generate_body()
|
| 1032 |
+
raises(ValueError,
|
| 1033 |
+
lambda: SphericalJoint('S', P, C, Matrix([q0, q1]), Matrix([u0])))
|
| 1034 |
+
# Test too many generalized coordinates
|
| 1035 |
+
raises(ValueError, lambda: SphericalJoint(
|
| 1036 |
+
'S', P, C, Matrix([q0, q1, q2, q3]), Matrix([u0, u1, u2])))
|
| 1037 |
+
raises(ValueError, lambda: SphericalJoint(
|
| 1038 |
+
'S', P, C, Matrix([q0, q1, q2]), Matrix([u0, u1, u2, u4])))
|
| 1039 |
+
|
| 1040 |
+
|
| 1041 |
+
def test_spherical_joint_orient_body():
|
| 1042 |
+
q0, q1, q2, u0, u1, u2 = dynamicsymbols('q0:3, u0:3')
|
| 1043 |
+
N_R_A = Matrix([
|
| 1044 |
+
[-sin(q1), -sin(q2) * cos(q1), cos(q1) * cos(q2)],
|
| 1045 |
+
[-sin(q0) * cos(q1), sin(q0) * sin(q1) * sin(q2) - cos(q0) * cos(q2),
|
| 1046 |
+
-sin(q0) * sin(q1) * cos(q2) - sin(q2) * cos(q0)],
|
| 1047 |
+
[cos(q0) * cos(q1), -sin(q0) * cos(q2) - sin(q1) * sin(q2) * cos(q0),
|
| 1048 |
+
-sin(q0) * sin(q2) + sin(q1) * cos(q0) * cos(q2)]])
|
| 1049 |
+
N_w_A = Matrix([[-u0 * sin(q1) - u2],
|
| 1050 |
+
[-u0 * sin(q2) * cos(q1) + u1 * cos(q2)],
|
| 1051 |
+
[u0 * cos(q1) * cos(q2) + u1 * sin(q2)]])
|
| 1052 |
+
N_v_Co = Matrix([
|
| 1053 |
+
[-sqrt(2) * (u0 * cos(q2 + pi / 4) * cos(q1) + u1 * sin(q2 + pi / 4))],
|
| 1054 |
+
[-u0 * sin(q1) - u2], [-u0 * sin(q1) - u2]])
|
| 1055 |
+
# Test default rot_type='BODY', rot_order=123
|
| 1056 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 1057 |
+
S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2],
|
| 1058 |
+
parent_point=N.x + N.y, child_point=-A.y + A.z,
|
| 1059 |
+
parent_interframe=Pint, child_interframe=Cint,
|
| 1060 |
+
rot_type='body', rot_order=123)
|
| 1061 |
+
assert S._rot_type.upper() == 'BODY'
|
| 1062 |
+
assert S._rot_order == 123
|
| 1063 |
+
assert simplify(N.dcm(A) - N_R_A) == zeros(3)
|
| 1064 |
+
assert simplify(A.ang_vel_in(N).to_matrix(A) - N_w_A) == zeros(3, 1)
|
| 1065 |
+
assert simplify(C.masscenter.vel(N).to_matrix(A)) == N_v_Co
|
| 1066 |
+
# Test change of amounts
|
| 1067 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 1068 |
+
S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2],
|
| 1069 |
+
parent_point=N.x + N.y, child_point=-A.y + A.z,
|
| 1070 |
+
parent_interframe=Pint, child_interframe=Cint,
|
| 1071 |
+
rot_type='BODY', amounts=(q1, q0, q2), rot_order=123)
|
| 1072 |
+
switch_order = lambda expr: expr.xreplace(
|
| 1073 |
+
{q0: q1, q1: q0, q2: q2, u0: u1, u1: u0, u2: u2})
|
| 1074 |
+
assert S._rot_type.upper() == 'BODY'
|
| 1075 |
+
assert S._rot_order == 123
|
| 1076 |
+
assert simplify(N.dcm(A) - switch_order(N_R_A)) == zeros(3)
|
| 1077 |
+
assert simplify(A.ang_vel_in(N).to_matrix(A) - switch_order(N_w_A)
|
| 1078 |
+
) == zeros(3, 1)
|
| 1079 |
+
assert simplify(C.masscenter.vel(N).to_matrix(A)) == switch_order(N_v_Co)
|
| 1080 |
+
# Test different rot_order
|
| 1081 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 1082 |
+
S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2],
|
| 1083 |
+
parent_point=N.x + N.y, child_point=-A.y + A.z,
|
| 1084 |
+
parent_interframe=Pint, child_interframe=Cint,
|
| 1085 |
+
rot_type='BodY', rot_order='yxz')
|
| 1086 |
+
assert S._rot_type.upper() == 'BODY'
|
| 1087 |
+
assert S._rot_order == 'yxz'
|
| 1088 |
+
assert simplify(N.dcm(A) - Matrix([
|
| 1089 |
+
[-sin(q0) * cos(q1), sin(q0) * sin(q1) * cos(q2) - sin(q2) * cos(q0),
|
| 1090 |
+
sin(q0) * sin(q1) * sin(q2) + cos(q0) * cos(q2)],
|
| 1091 |
+
[-sin(q1), -cos(q1) * cos(q2), -sin(q2) * cos(q1)],
|
| 1092 |
+
[cos(q0) * cos(q1), -sin(q0) * sin(q2) - sin(q1) * cos(q0) * cos(q2),
|
| 1093 |
+
sin(q0) * cos(q2) - sin(q1) * sin(q2) * cos(q0)]])) == zeros(3)
|
| 1094 |
+
assert simplify(A.ang_vel_in(N).to_matrix(A) - Matrix([
|
| 1095 |
+
[u0 * sin(q1) - u2], [u0 * cos(q1) * cos(q2) - u1 * sin(q2)],
|
| 1096 |
+
[u0 * sin(q2) * cos(q1) + u1 * cos(q2)]])) == zeros(3, 1)
|
| 1097 |
+
assert simplify(C.masscenter.vel(N).to_matrix(A)) == Matrix([
|
| 1098 |
+
[-sqrt(2) * (u0 * sin(q2 + pi / 4) * cos(q1) + u1 * cos(q2 + pi / 4))],
|
| 1099 |
+
[u0 * sin(q1) - u2], [u0 * sin(q1) - u2]])
|
| 1100 |
+
|
| 1101 |
+
|
| 1102 |
+
def test_spherical_joint_orient_space():
|
| 1103 |
+
q0, q1, q2, u0, u1, u2 = dynamicsymbols('q0:3, u0:3')
|
| 1104 |
+
N_R_A = Matrix([
|
| 1105 |
+
[-sin(q0) * sin(q2) - sin(q1) * cos(q0) * cos(q2),
|
| 1106 |
+
sin(q0) * sin(q1) * cos(q2) - sin(q2) * cos(q0), cos(q1) * cos(q2)],
|
| 1107 |
+
[-sin(q0) * cos(q2) + sin(q1) * sin(q2) * cos(q0),
|
| 1108 |
+
-sin(q0) * sin(q1) * sin(q2) - cos(q0) * cos(q2), -sin(q2) * cos(q1)],
|
| 1109 |
+
[cos(q0) * cos(q1), -sin(q0) * cos(q1), sin(q1)]])
|
| 1110 |
+
N_w_A = Matrix([
|
| 1111 |
+
[u1 * sin(q0) - u2 * cos(q0) * cos(q1)],
|
| 1112 |
+
[u1 * cos(q0) + u2 * sin(q0) * cos(q1)], [u0 - u2 * sin(q1)]])
|
| 1113 |
+
N_v_Co = Matrix([
|
| 1114 |
+
[u0 - u2 * sin(q1)], [u0 - u2 * sin(q1)],
|
| 1115 |
+
[sqrt(2) * (-u1 * sin(q0 + pi / 4) + u2 * cos(q0 + pi / 4) * cos(q1))]])
|
| 1116 |
+
# Test default rot_type='BODY', rot_order=123
|
| 1117 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 1118 |
+
S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2],
|
| 1119 |
+
parent_point=N.x + N.z, child_point=-A.x + A.y,
|
| 1120 |
+
parent_interframe=Pint, child_interframe=Cint,
|
| 1121 |
+
rot_type='space', rot_order=123)
|
| 1122 |
+
assert S._rot_type.upper() == 'SPACE'
|
| 1123 |
+
assert S._rot_order == 123
|
| 1124 |
+
assert simplify(N.dcm(A) - N_R_A) == zeros(3)
|
| 1125 |
+
assert simplify(A.ang_vel_in(N).to_matrix(A)) == N_w_A
|
| 1126 |
+
assert simplify(C.masscenter.vel(N).to_matrix(A)) == N_v_Co
|
| 1127 |
+
# Test change of amounts
|
| 1128 |
+
switch_order = lambda expr: expr.xreplace(
|
| 1129 |
+
{q0: q1, q1: q0, q2: q2, u0: u1, u1: u0, u2: u2})
|
| 1130 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 1131 |
+
S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2],
|
| 1132 |
+
parent_point=N.x + N.z, child_point=-A.x + A.y,
|
| 1133 |
+
parent_interframe=Pint, child_interframe=Cint,
|
| 1134 |
+
rot_type='SPACE', amounts=(q1, q0, q2), rot_order=123)
|
| 1135 |
+
assert S._rot_type.upper() == 'SPACE'
|
| 1136 |
+
assert S._rot_order == 123
|
| 1137 |
+
assert simplify(N.dcm(A) - switch_order(N_R_A)) == zeros(3)
|
| 1138 |
+
assert simplify(A.ang_vel_in(N).to_matrix(A)) == switch_order(N_w_A)
|
| 1139 |
+
assert simplify(C.masscenter.vel(N).to_matrix(A)) == switch_order(N_v_Co)
|
| 1140 |
+
# Test different rot_order
|
| 1141 |
+
N, A, P, C, Pint, Cint = _generate_body(True)
|
| 1142 |
+
S = SphericalJoint('S', P, C, coordinates=[q0, q1, q2], speeds=[u0, u1, u2],
|
| 1143 |
+
parent_point=N.x + N.z, child_point=-A.x + A.y,
|
| 1144 |
+
parent_interframe=Pint, child_interframe=Cint,
|
| 1145 |
+
rot_type='SPaCe', rot_order='zxy')
|
| 1146 |
+
assert S._rot_type.upper() == 'SPACE'
|
| 1147 |
+
assert S._rot_order == 'zxy'
|
| 1148 |
+
assert simplify(N.dcm(A) - Matrix([
|
| 1149 |
+
[-sin(q2) * cos(q1), -sin(q0) * cos(q2) + sin(q1) * sin(q2) * cos(q0),
|
| 1150 |
+
sin(q0) * sin(q1) * sin(q2) + cos(q0) * cos(q2)],
|
| 1151 |
+
[-sin(q1), -cos(q0) * cos(q1), -sin(q0) * cos(q1)],
|
| 1152 |
+
[cos(q1) * cos(q2), -sin(q0) * sin(q2) - sin(q1) * cos(q0) * cos(q2),
|
| 1153 |
+
-sin(q0) * sin(q1) * cos(q2) + sin(q2) * cos(q0)]]))
|
| 1154 |
+
assert simplify(A.ang_vel_in(N).to_matrix(A) - Matrix([
|
| 1155 |
+
[-u0 + u2 * sin(q1)], [-u1 * sin(q0) + u2 * cos(q0) * cos(q1)],
|
| 1156 |
+
[u1 * cos(q0) + u2 * sin(q0) * cos(q1)]])) == zeros(3, 1)
|
| 1157 |
+
assert simplify(C.masscenter.vel(N).to_matrix(A) - Matrix([
|
| 1158 |
+
[u1 * cos(q0) + u2 * sin(q0) * cos(q1)],
|
| 1159 |
+
[u1 * cos(q0) + u2 * sin(q0) * cos(q1)],
|
| 1160 |
+
[u0 + u1 * sin(q0) - u2 * sin(q1) -
|
| 1161 |
+
u2 * cos(q0) * cos(q1)]])) == zeros(3, 1)
|
| 1162 |
+
|
| 1163 |
+
|
| 1164 |
+
def test_weld_joint():
|
| 1165 |
+
_, _, P, C = _generate_body()
|
| 1166 |
+
W = WeldJoint('W', P, C)
|
| 1167 |
+
assert W.name == 'W'
|
| 1168 |
+
assert W.parent == P
|
| 1169 |
+
assert W.child == C
|
| 1170 |
+
assert W.coordinates == Matrix()
|
| 1171 |
+
assert W.speeds == Matrix()
|
| 1172 |
+
assert W.kdes == Matrix(1, 0, []).T
|
| 1173 |
+
assert P.frame.dcm(C.frame) == eye(3)
|
| 1174 |
+
assert W.child_point.pos_from(C.masscenter) == Vector(0)
|
| 1175 |
+
assert W.parent_point.pos_from(P.masscenter) == Vector(0)
|
| 1176 |
+
assert W.parent_point.pos_from(W.child_point) == Vector(0)
|
| 1177 |
+
assert P.masscenter.pos_from(C.masscenter) == Vector(0)
|
| 1178 |
+
assert C.masscenter.vel(P.frame) == Vector(0)
|
| 1179 |
+
assert P.frame.ang_vel_in(C.frame) == 0
|
| 1180 |
+
assert C.frame.ang_vel_in(P.frame) == 0
|
| 1181 |
+
assert W.__str__() == 'WeldJoint: W parent: P child: C'
|
| 1182 |
+
|
| 1183 |
+
N, A, P, C = _generate_body()
|
| 1184 |
+
l, m = symbols('l m')
|
| 1185 |
+
Pint = ReferenceFrame('P_int')
|
| 1186 |
+
Pint.orient_axis(P.frame, P.y, pi / 2)
|
| 1187 |
+
W = WeldJoint('W', P, C, parent_point=l * P.frame.x,
|
| 1188 |
+
child_point=m * C.frame.y, parent_interframe=Pint)
|
| 1189 |
+
|
| 1190 |
+
assert W.child_point.pos_from(C.masscenter) == m * C.frame.y
|
| 1191 |
+
assert W.parent_point.pos_from(P.masscenter) == l * P.frame.x
|
| 1192 |
+
assert W.parent_point.pos_from(W.child_point) == Vector(0)
|
| 1193 |
+
assert P.masscenter.pos_from(C.masscenter) == - l * N.x + m * A.y
|
| 1194 |
+
assert C.masscenter.vel(P.frame) == Vector(0)
|
| 1195 |
+
assert P.masscenter.vel(Pint) == Vector(0)
|
| 1196 |
+
assert C.frame.ang_vel_in(P.frame) == 0
|
| 1197 |
+
assert P.frame.ang_vel_in(C.frame) == 0
|
| 1198 |
+
assert P.x == A.z
|
| 1199 |
+
|
| 1200 |
+
with warns_deprecated_sympy():
|
| 1201 |
+
JointsMethod(P, W) # Tests #10770
|
| 1202 |
+
|
| 1203 |
+
|
| 1204 |
+
def test_deprecated_parent_child_axis():
|
| 1205 |
+
q, u = dynamicsymbols('q_J, u_J')
|
| 1206 |
+
N, A, P, C = _generate_body()
|
| 1207 |
+
with warns_deprecated_sympy():
|
| 1208 |
+
PinJoint('J', P, C, child_axis=-A.x)
|
| 1209 |
+
assert (-A.x).angle_between(N.x) == 0
|
| 1210 |
+
assert -A.x.express(N) == N.x
|
| 1211 |
+
assert A.dcm(N) == Matrix([[-1, 0, 0],
|
| 1212 |
+
[0, -cos(q), -sin(q)],
|
| 1213 |
+
[0, -sin(q), cos(q)]])
|
| 1214 |
+
assert A.ang_vel_in(N) == u * N.x
|
| 1215 |
+
assert A.ang_vel_in(N).magnitude() == sqrt(u ** 2)
|
| 1216 |
+
|
| 1217 |
+
N, A, P, C = _generate_body()
|
| 1218 |
+
with warns_deprecated_sympy():
|
| 1219 |
+
PrismaticJoint('J', P, C, parent_axis=P.x + P.y)
|
| 1220 |
+
assert (A.x).angle_between(N.x + N.y) == 0
|
| 1221 |
+
assert A.x.express(N) == (N.x + N.y) / sqrt(2)
|
| 1222 |
+
assert A.dcm(N) == Matrix([[sqrt(2) / 2, sqrt(2) / 2, 0],
|
| 1223 |
+
[-sqrt(2) / 2, sqrt(2) / 2, 0], [0, 0, 1]])
|
| 1224 |
+
assert A.ang_vel_in(N) == Vector(0)
|
| 1225 |
+
|
| 1226 |
+
|
| 1227 |
+
def test_deprecated_joint_pos():
|
| 1228 |
+
N, A, P, C = _generate_body()
|
| 1229 |
+
with warns_deprecated_sympy():
|
| 1230 |
+
pin = PinJoint('J', P, C, parent_joint_pos=N.x + N.y,
|
| 1231 |
+
child_joint_pos=C.y - C.z)
|
| 1232 |
+
assert pin.parent_point.pos_from(P.masscenter) == N.x + N.y
|
| 1233 |
+
assert pin.child_point.pos_from(C.masscenter) == C.y - C.z
|
| 1234 |
+
|
| 1235 |
+
N, A, P, C = _generate_body()
|
| 1236 |
+
with warns_deprecated_sympy():
|
| 1237 |
+
slider = PrismaticJoint('J', P, C, parent_joint_pos=N.z + N.y,
|
| 1238 |
+
child_joint_pos=C.y - C.x)
|
| 1239 |
+
assert slider.parent_point.pos_from(P.masscenter) == N.z + N.y
|
| 1240 |
+
assert slider.child_point.pos_from(C.masscenter) == C.y - C.x
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_jointsmethod.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy.core.function import expand
|
| 2 |
+
from sympy.core.symbol import symbols
|
| 3 |
+
from sympy.functions.elementary.trigonometric import (cos, sin)
|
| 4 |
+
from sympy.matrices.dense import Matrix
|
| 5 |
+
from sympy.simplify.trigsimp import trigsimp
|
| 6 |
+
from sympy.physics.mechanics import (
|
| 7 |
+
PinJoint, JointsMethod, RigidBody, Particle, Body, KanesMethod,
|
| 8 |
+
PrismaticJoint, LagrangesMethod, inertia)
|
| 9 |
+
from sympy.physics.vector import dynamicsymbols, ReferenceFrame
|
| 10 |
+
from sympy.testing.pytest import raises, warns_deprecated_sympy
|
| 11 |
+
from sympy import zeros
|
| 12 |
+
from sympy.utilities.lambdify import lambdify
|
| 13 |
+
from sympy.solvers.solvers import solve
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
t = dynamicsymbols._t # type: ignore
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def test_jointsmethod():
|
| 20 |
+
with warns_deprecated_sympy():
|
| 21 |
+
P = Body('P')
|
| 22 |
+
C = Body('C')
|
| 23 |
+
Pin = PinJoint('P1', P, C)
|
| 24 |
+
C_ixx, g = symbols('C_ixx g')
|
| 25 |
+
q, u = dynamicsymbols('q_P1, u_P1')
|
| 26 |
+
P.apply_force(g*P.y)
|
| 27 |
+
with warns_deprecated_sympy():
|
| 28 |
+
method = JointsMethod(P, Pin)
|
| 29 |
+
assert method.frame == P.frame
|
| 30 |
+
assert method.bodies == [C, P]
|
| 31 |
+
assert method.loads == [(P.masscenter, g*P.frame.y)]
|
| 32 |
+
assert method.q == Matrix([q])
|
| 33 |
+
assert method.u == Matrix([u])
|
| 34 |
+
assert method.kdes == Matrix([u - q.diff()])
|
| 35 |
+
soln = method.form_eoms()
|
| 36 |
+
assert soln == Matrix([[-C_ixx*u.diff()]])
|
| 37 |
+
assert method.forcing_full == Matrix([[u], [0]])
|
| 38 |
+
assert method.mass_matrix_full == Matrix([[1, 0], [0, C_ixx]])
|
| 39 |
+
assert isinstance(method.method, KanesMethod)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def test_rigid_body_particle_compatibility():
|
| 43 |
+
l, m, g = symbols('l m g')
|
| 44 |
+
C = RigidBody('C')
|
| 45 |
+
b = Particle('b', mass=m)
|
| 46 |
+
b_frame = ReferenceFrame('b_frame')
|
| 47 |
+
q, u = dynamicsymbols('q u')
|
| 48 |
+
P = PinJoint('P', C, b, coordinates=q, speeds=u, child_interframe=b_frame,
|
| 49 |
+
child_point=-l * b_frame.x, joint_axis=C.z)
|
| 50 |
+
with warns_deprecated_sympy():
|
| 51 |
+
method = JointsMethod(C, P)
|
| 52 |
+
method.loads.append((b.masscenter, m * g * C.x))
|
| 53 |
+
method.form_eoms()
|
| 54 |
+
rhs = method.rhs()
|
| 55 |
+
assert rhs[1] == -g*sin(q)/l
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def test_jointmethod_duplicate_coordinates_speeds():
|
| 59 |
+
with warns_deprecated_sympy():
|
| 60 |
+
P = Body('P')
|
| 61 |
+
C = Body('C')
|
| 62 |
+
T = Body('T')
|
| 63 |
+
q, u = dynamicsymbols('q u')
|
| 64 |
+
P1 = PinJoint('P1', P, C, q)
|
| 65 |
+
P2 = PrismaticJoint('P2', C, T, q)
|
| 66 |
+
with warns_deprecated_sympy():
|
| 67 |
+
raises(ValueError, lambda: JointsMethod(P, P1, P2))
|
| 68 |
+
|
| 69 |
+
P1 = PinJoint('P1', P, C, speeds=u)
|
| 70 |
+
P2 = PrismaticJoint('P2', C, T, speeds=u)
|
| 71 |
+
with warns_deprecated_sympy():
|
| 72 |
+
raises(ValueError, lambda: JointsMethod(P, P1, P2))
|
| 73 |
+
|
| 74 |
+
P1 = PinJoint('P1', P, C, q, u)
|
| 75 |
+
P2 = PrismaticJoint('P2', C, T, q, u)
|
| 76 |
+
with warns_deprecated_sympy():
|
| 77 |
+
raises(ValueError, lambda: JointsMethod(P, P1, P2))
|
| 78 |
+
|
| 79 |
+
def test_complete_simple_double_pendulum():
|
| 80 |
+
q1, q2 = dynamicsymbols('q1 q2')
|
| 81 |
+
u1, u2 = dynamicsymbols('u1 u2')
|
| 82 |
+
m, l, g = symbols('m l g')
|
| 83 |
+
with warns_deprecated_sympy():
|
| 84 |
+
C = Body('C') # ceiling
|
| 85 |
+
PartP = Body('P', mass=m)
|
| 86 |
+
PartR = Body('R', mass=m)
|
| 87 |
+
J1 = PinJoint('J1', C, PartP, speeds=u1, coordinates=q1,
|
| 88 |
+
child_point=-l*PartP.x, joint_axis=C.z)
|
| 89 |
+
J2 = PinJoint('J2', PartP, PartR, speeds=u2, coordinates=q2,
|
| 90 |
+
child_point=-l*PartR.x, joint_axis=PartP.z)
|
| 91 |
+
|
| 92 |
+
PartP.apply_force(m*g*C.x)
|
| 93 |
+
PartR.apply_force(m*g*C.x)
|
| 94 |
+
|
| 95 |
+
with warns_deprecated_sympy():
|
| 96 |
+
method = JointsMethod(C, J1, J2)
|
| 97 |
+
method.form_eoms()
|
| 98 |
+
|
| 99 |
+
assert expand(method.mass_matrix_full) == Matrix([[1, 0, 0, 0],
|
| 100 |
+
[0, 1, 0, 0],
|
| 101 |
+
[0, 0, 2*l**2*m*cos(q2) + 3*l**2*m, l**2*m*cos(q2) + l**2*m],
|
| 102 |
+
[0, 0, l**2*m*cos(q2) + l**2*m, l**2*m]])
|
| 103 |
+
assert trigsimp(method.forcing_full) == trigsimp(Matrix([[u1], [u2], [-g*l*m*(sin(q1 + q2) + sin(q1)) -
|
| 104 |
+
g*l*m*sin(q1) + l**2*m*(2*u1 + u2)*u2*sin(q2)],
|
| 105 |
+
[-g*l*m*sin(q1 + q2) - l**2*m*u1**2*sin(q2)]]))
|
| 106 |
+
|
| 107 |
+
def test_two_dof_joints():
|
| 108 |
+
q1, q2, u1, u2 = dynamicsymbols('q1 q2 u1 u2')
|
| 109 |
+
m, c1, c2, k1, k2 = symbols('m c1 c2 k1 k2')
|
| 110 |
+
with warns_deprecated_sympy():
|
| 111 |
+
W = Body('W')
|
| 112 |
+
B1 = Body('B1', mass=m)
|
| 113 |
+
B2 = Body('B2', mass=m)
|
| 114 |
+
J1 = PrismaticJoint('J1', W, B1, coordinates=q1, speeds=u1)
|
| 115 |
+
J2 = PrismaticJoint('J2', B1, B2, coordinates=q2, speeds=u2)
|
| 116 |
+
W.apply_force(k1*q1*W.x, reaction_body=B1)
|
| 117 |
+
W.apply_force(c1*u1*W.x, reaction_body=B1)
|
| 118 |
+
B1.apply_force(k2*q2*W.x, reaction_body=B2)
|
| 119 |
+
B1.apply_force(c2*u2*W.x, reaction_body=B2)
|
| 120 |
+
with warns_deprecated_sympy():
|
| 121 |
+
method = JointsMethod(W, J1, J2)
|
| 122 |
+
method.form_eoms()
|
| 123 |
+
MM = method.mass_matrix
|
| 124 |
+
forcing = method.forcing
|
| 125 |
+
rhs = MM.LUsolve(forcing)
|
| 126 |
+
assert expand(rhs[0]) == expand((-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2)/m)
|
| 127 |
+
assert expand(rhs[1]) == expand((k1 * q1 + c1 * u1 - 2 * k2 * q2 - 2 *
|
| 128 |
+
c2 * u2) / m)
|
| 129 |
+
|
| 130 |
+
def test_simple_pedulum():
|
| 131 |
+
l, m, g = symbols('l m g')
|
| 132 |
+
with warns_deprecated_sympy():
|
| 133 |
+
C = Body('C')
|
| 134 |
+
b = Body('b', mass=m)
|
| 135 |
+
q = dynamicsymbols('q')
|
| 136 |
+
P = PinJoint('P', C, b, speeds=q.diff(t), coordinates=q,
|
| 137 |
+
child_point=-l * b.x, joint_axis=C.z)
|
| 138 |
+
b.potential_energy = - m * g * l * cos(q)
|
| 139 |
+
with warns_deprecated_sympy():
|
| 140 |
+
method = JointsMethod(C, P)
|
| 141 |
+
method.form_eoms(LagrangesMethod)
|
| 142 |
+
rhs = method.rhs()
|
| 143 |
+
assert rhs[1] == -g*sin(q)/l
|
| 144 |
+
|
| 145 |
+
def test_chaos_pendulum():
|
| 146 |
+
#https://www.pydy.org/examples/chaos_pendulum.html
|
| 147 |
+
mA, mB, lA, lB, IAxx, IBxx, IByy, IBzz, g = symbols('mA, mB, lA, lB, IAxx, IBxx, IByy, IBzz, g')
|
| 148 |
+
theta, phi, omega, alpha = dynamicsymbols('theta phi omega alpha')
|
| 149 |
+
|
| 150 |
+
A = ReferenceFrame('A')
|
| 151 |
+
B = ReferenceFrame('B')
|
| 152 |
+
|
| 153 |
+
with warns_deprecated_sympy():
|
| 154 |
+
rod = Body('rod', mass=mA, frame=A,
|
| 155 |
+
central_inertia=inertia(A, IAxx, IAxx, 0))
|
| 156 |
+
plate = Body('plate', mass=mB, frame=B,
|
| 157 |
+
central_inertia=inertia(B, IBxx, IByy, IBzz))
|
| 158 |
+
C = Body('C')
|
| 159 |
+
J1 = PinJoint('J1', C, rod, coordinates=theta, speeds=omega,
|
| 160 |
+
child_point=-lA * rod.z, joint_axis=C.y)
|
| 161 |
+
J2 = PinJoint('J2', rod, plate, coordinates=phi, speeds=alpha,
|
| 162 |
+
parent_point=(lB - lA) * rod.z, joint_axis=rod.z)
|
| 163 |
+
|
| 164 |
+
rod.apply_force(mA*g*C.z)
|
| 165 |
+
plate.apply_force(mB*g*C.z)
|
| 166 |
+
|
| 167 |
+
with warns_deprecated_sympy():
|
| 168 |
+
method = JointsMethod(C, J1, J2)
|
| 169 |
+
method.form_eoms()
|
| 170 |
+
|
| 171 |
+
MM = method.mass_matrix
|
| 172 |
+
forcing = method.forcing
|
| 173 |
+
rhs = MM.LUsolve(forcing)
|
| 174 |
+
xd = (-2 * IBxx * alpha * omega * sin(phi) * cos(phi) + 2 * IByy * alpha * omega * sin(phi) *
|
| 175 |
+
cos(phi) - g * lA * mA * sin(theta) - g * lB * mB * sin(theta)) / (IAxx + IBxx *
|
| 176 |
+
sin(phi)**2 + IByy * cos(phi)**2 + lA**2 * mA + lB**2 * mB)
|
| 177 |
+
assert (rhs[0] - xd).simplify() == 0
|
| 178 |
+
xd = (IBxx - IByy) * omega**2 * sin(phi) * cos(phi) / IBzz
|
| 179 |
+
assert (rhs[1] - xd).simplify() == 0
|
| 180 |
+
|
| 181 |
+
def test_four_bar_linkage_with_manual_constraints():
|
| 182 |
+
q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1:4, u1:4')
|
| 183 |
+
l1, l2, l3, l4, rho = symbols('l1:5, rho')
|
| 184 |
+
|
| 185 |
+
N = ReferenceFrame('N')
|
| 186 |
+
inertias = [inertia(N, 0, 0, rho * l ** 3 / 12) for l in (l1, l2, l3, l4)]
|
| 187 |
+
with warns_deprecated_sympy():
|
| 188 |
+
link1 = Body('Link1', frame=N, mass=rho * l1,
|
| 189 |
+
central_inertia=inertias[0])
|
| 190 |
+
link2 = Body('Link2', mass=rho * l2, central_inertia=inertias[1])
|
| 191 |
+
link3 = Body('Link3', mass=rho * l3, central_inertia=inertias[2])
|
| 192 |
+
link4 = Body('Link4', mass=rho * l4, central_inertia=inertias[3])
|
| 193 |
+
|
| 194 |
+
joint1 = PinJoint(
|
| 195 |
+
'J1', link1, link2, coordinates=q1, speeds=u1, joint_axis=link1.z,
|
| 196 |
+
parent_point=l1 / 2 * link1.x, child_point=-l2 / 2 * link2.x)
|
| 197 |
+
joint2 = PinJoint(
|
| 198 |
+
'J2', link2, link3, coordinates=q2, speeds=u2, joint_axis=link2.z,
|
| 199 |
+
parent_point=l2 / 2 * link2.x, child_point=-l3 / 2 * link3.x)
|
| 200 |
+
joint3 = PinJoint(
|
| 201 |
+
'J3', link3, link4, coordinates=q3, speeds=u3, joint_axis=link3.z,
|
| 202 |
+
parent_point=l3 / 2 * link3.x, child_point=-l4 / 2 * link4.x)
|
| 203 |
+
|
| 204 |
+
loop = link4.masscenter.pos_from(link1.masscenter) \
|
| 205 |
+
+ l1 / 2 * link1.x + l4 / 2 * link4.x
|
| 206 |
+
|
| 207 |
+
fh = Matrix([loop.dot(link1.x), loop.dot(link1.y)])
|
| 208 |
+
|
| 209 |
+
with warns_deprecated_sympy():
|
| 210 |
+
method = JointsMethod(link1, joint1, joint2, joint3)
|
| 211 |
+
|
| 212 |
+
t = dynamicsymbols._t
|
| 213 |
+
qdots = solve(method.kdes, [q1.diff(t), q2.diff(t), q3.diff(t)])
|
| 214 |
+
fhd = fh.diff(t).subs(qdots)
|
| 215 |
+
|
| 216 |
+
kane = KanesMethod(method.frame, q_ind=[q1], u_ind=[u1],
|
| 217 |
+
q_dependent=[q2, q3], u_dependent=[u2, u3],
|
| 218 |
+
kd_eqs=method.kdes, configuration_constraints=fh,
|
| 219 |
+
velocity_constraints=fhd, forcelist=method.loads,
|
| 220 |
+
bodies=method.bodies)
|
| 221 |
+
fr, frs = kane.kanes_equations()
|
| 222 |
+
assert fr == zeros(1)
|
| 223 |
+
|
| 224 |
+
# Numerically check the mass- and forcing-matrix
|
| 225 |
+
p = Matrix([l1, l2, l3, l4, rho])
|
| 226 |
+
q = Matrix([q1, q2, q3])
|
| 227 |
+
u = Matrix([u1, u2, u3])
|
| 228 |
+
eval_m = lambdify((q, p), kane.mass_matrix)
|
| 229 |
+
eval_f = lambdify((q, u, p), kane.forcing)
|
| 230 |
+
eval_fhd = lambdify((q, u, p), fhd)
|
| 231 |
+
|
| 232 |
+
p_vals = [0.13, 0.24, 0.21, 0.34, 997]
|
| 233 |
+
q_vals = [2.1, 0.6655470375077588, 2.527408138024188] # Satisfies fh
|
| 234 |
+
u_vals = [0.2, -0.17963733938852067, 0.1309060540601612] # Satisfies fhd
|
| 235 |
+
mass_check = Matrix([[3.452709815256506e+01, 7.003948798374735e+00,
|
| 236 |
+
-4.939690970641498e+00],
|
| 237 |
+
[-2.203792703880936e-14, 2.071702479957077e-01,
|
| 238 |
+
2.842917573033711e-01],
|
| 239 |
+
[-1.300000000000123e-01, -8.836934896046506e-03,
|
| 240 |
+
1.864891330060847e-01]])
|
| 241 |
+
forcing_check = Matrix([[-0.031211821321648],
|
| 242 |
+
[-0.00066022608181],
|
| 243 |
+
[0.001813559741243]])
|
| 244 |
+
eps = 1e-10
|
| 245 |
+
assert all(abs(x) < eps for x in eval_fhd(q_vals, u_vals, p_vals))
|
| 246 |
+
assert all(abs(x) < eps for x in
|
| 247 |
+
(Matrix(eval_m(q_vals, p_vals)) - mass_check))
|
| 248 |
+
assert all(abs(x) < eps for x in
|
| 249 |
+
(Matrix(eval_f(q_vals, u_vals, p_vals)) - forcing_check))
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane.py
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import solve
|
| 2 |
+
from sympy import (cos, expand, Matrix, sin, symbols, tan, sqrt, S,
|
| 3 |
+
zeros, eye)
|
| 4 |
+
from sympy.simplify.simplify import simplify
|
| 5 |
+
from sympy.physics.mechanics import (dynamicsymbols, ReferenceFrame, Point,
|
| 6 |
+
RigidBody, KanesMethod, inertia, Particle,
|
| 7 |
+
dot, find_dynamicsymbols)
|
| 8 |
+
from sympy.testing.pytest import raises
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_invalid_coordinates():
|
| 12 |
+
# Simple pendulum, but use symbols instead of dynamicsymbols
|
| 13 |
+
l, m, g = symbols('l m g')
|
| 14 |
+
q, u = symbols('q u') # Generalized coordinate
|
| 15 |
+
kd = [q.diff(dynamicsymbols._t) - u]
|
| 16 |
+
N, O = ReferenceFrame('N'), Point('O')
|
| 17 |
+
O.set_vel(N, 0)
|
| 18 |
+
P = Particle('P', Point('P'), m)
|
| 19 |
+
P.point.set_pos(O, l * (sin(q) * N.x - cos(q) * N.y))
|
| 20 |
+
F = (P.point, -m * g * N.y)
|
| 21 |
+
raises(ValueError, lambda: KanesMethod(N, [q], [u], kd, bodies=[P],
|
| 22 |
+
forcelist=[F]))
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def test_one_dof():
|
| 26 |
+
# This is for a 1 dof spring-mass-damper case.
|
| 27 |
+
# It is described in more detail in the KanesMethod docstring.
|
| 28 |
+
q, u = dynamicsymbols('q u')
|
| 29 |
+
qd, ud = dynamicsymbols('q u', 1)
|
| 30 |
+
m, c, k = symbols('m c k')
|
| 31 |
+
N = ReferenceFrame('N')
|
| 32 |
+
P = Point('P')
|
| 33 |
+
P.set_vel(N, u * N.x)
|
| 34 |
+
|
| 35 |
+
kd = [qd - u]
|
| 36 |
+
FL = [(P, (-k * q - c * u) * N.x)]
|
| 37 |
+
pa = Particle('pa', P, m)
|
| 38 |
+
BL = [pa]
|
| 39 |
+
|
| 40 |
+
KM = KanesMethod(N, [q], [u], kd)
|
| 41 |
+
KM.kanes_equations(BL, FL)
|
| 42 |
+
|
| 43 |
+
assert KM.bodies == BL
|
| 44 |
+
assert KM.loads == FL
|
| 45 |
+
|
| 46 |
+
MM = KM.mass_matrix
|
| 47 |
+
forcing = KM.forcing
|
| 48 |
+
rhs = MM.inv() * forcing
|
| 49 |
+
assert expand(rhs[0]) == expand(-(q * k + u * c) / m)
|
| 50 |
+
|
| 51 |
+
assert simplify(KM.rhs() -
|
| 52 |
+
KM.mass_matrix_full.LUsolve(KM.forcing_full)) == zeros(2, 1)
|
| 53 |
+
|
| 54 |
+
assert (KM.linearize(A_and_B=True, )[0] == Matrix([[0, 1], [-k/m, -c/m]]))
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def test_two_dof():
|
| 58 |
+
# This is for a 2 d.o.f., 2 particle spring-mass-damper.
|
| 59 |
+
# The first coordinate is the displacement of the first particle, and the
|
| 60 |
+
# second is the relative displacement between the first and second
|
| 61 |
+
# particles. Speeds are defined as the time derivatives of the particles.
|
| 62 |
+
q1, q2, u1, u2 = dynamicsymbols('q1 q2 u1 u2')
|
| 63 |
+
q1d, q2d, u1d, u2d = dynamicsymbols('q1 q2 u1 u2', 1)
|
| 64 |
+
m, c1, c2, k1, k2 = symbols('m c1 c2 k1 k2')
|
| 65 |
+
N = ReferenceFrame('N')
|
| 66 |
+
P1 = Point('P1')
|
| 67 |
+
P2 = Point('P2')
|
| 68 |
+
P1.set_vel(N, u1 * N.x)
|
| 69 |
+
P2.set_vel(N, (u1 + u2) * N.x)
|
| 70 |
+
# Note we multiply the kinematic equation by an arbitrary factor
|
| 71 |
+
# to test the implicit vs explicit kinematics attribute
|
| 72 |
+
kd = [q1d/2 - u1/2, 2*q2d - 2*u2]
|
| 73 |
+
|
| 74 |
+
# Now we create the list of forces, then assign properties to each
|
| 75 |
+
# particle, then create a list of all particles.
|
| 76 |
+
FL = [(P1, (-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2) * N.x), (P2, (-k2 *
|
| 77 |
+
q2 - c2 * u2) * N.x)]
|
| 78 |
+
pa1 = Particle('pa1', P1, m)
|
| 79 |
+
pa2 = Particle('pa2', P2, m)
|
| 80 |
+
BL = [pa1, pa2]
|
| 81 |
+
|
| 82 |
+
# Finally we create the KanesMethod object, specify the inertial frame,
|
| 83 |
+
# pass relevant information, and form Fr & Fr*. Then we calculate the mass
|
| 84 |
+
# matrix and forcing terms, and finally solve for the udots.
|
| 85 |
+
KM = KanesMethod(N, q_ind=[q1, q2], u_ind=[u1, u2], kd_eqs=kd)
|
| 86 |
+
KM.kanes_equations(BL, FL)
|
| 87 |
+
MM = KM.mass_matrix
|
| 88 |
+
forcing = KM.forcing
|
| 89 |
+
rhs = MM.inv() * forcing
|
| 90 |
+
assert expand(rhs[0]) == expand((-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2)/m)
|
| 91 |
+
assert expand(rhs[1]) == expand((k1 * q1 + c1 * u1 - 2 * k2 * q2 - 2 *
|
| 92 |
+
c2 * u2) / m)
|
| 93 |
+
|
| 94 |
+
# Check that the explicit form is the default and kinematic mass matrix is identity
|
| 95 |
+
assert KM.explicit_kinematics
|
| 96 |
+
assert KM.mass_matrix_kin == eye(2)
|
| 97 |
+
|
| 98 |
+
# Check that for the implicit form the mass matrix is not identity
|
| 99 |
+
KM.explicit_kinematics = False
|
| 100 |
+
assert KM.mass_matrix_kin == Matrix([[S(1)/2, 0], [0, 2]])
|
| 101 |
+
|
| 102 |
+
# Check that whether using implicit or explicit kinematics the RHS
|
| 103 |
+
# equations are consistent with the matrix form
|
| 104 |
+
for explicit_kinematics in [False, True]:
|
| 105 |
+
KM.explicit_kinematics = explicit_kinematics
|
| 106 |
+
assert simplify(KM.rhs() -
|
| 107 |
+
KM.mass_matrix_full.LUsolve(KM.forcing_full)) == zeros(4, 1)
|
| 108 |
+
|
| 109 |
+
# Make sure an error is raised if nonlinear kinematic differential
|
| 110 |
+
# equations are supplied.
|
| 111 |
+
kd = [q1d - u1**2, sin(q2d) - cos(u2)]
|
| 112 |
+
raises(ValueError, lambda: KanesMethod(N, q_ind=[q1, q2],
|
| 113 |
+
u_ind=[u1, u2], kd_eqs=kd))
|
| 114 |
+
|
| 115 |
+
def test_pend():
|
| 116 |
+
q, u = dynamicsymbols('q u')
|
| 117 |
+
qd, ud = dynamicsymbols('q u', 1)
|
| 118 |
+
m, l, g = symbols('m l g')
|
| 119 |
+
N = ReferenceFrame('N')
|
| 120 |
+
P = Point('P')
|
| 121 |
+
P.set_vel(N, -l * u * sin(q) * N.x + l * u * cos(q) * N.y)
|
| 122 |
+
kd = [qd - u]
|
| 123 |
+
|
| 124 |
+
FL = [(P, m * g * N.x)]
|
| 125 |
+
pa = Particle('pa', P, m)
|
| 126 |
+
BL = [pa]
|
| 127 |
+
|
| 128 |
+
KM = KanesMethod(N, [q], [u], kd)
|
| 129 |
+
KM.kanes_equations(BL, FL)
|
| 130 |
+
MM = KM.mass_matrix
|
| 131 |
+
forcing = KM.forcing
|
| 132 |
+
rhs = MM.inv() * forcing
|
| 133 |
+
rhs.simplify()
|
| 134 |
+
assert expand(rhs[0]) == expand(-g / l * sin(q))
|
| 135 |
+
assert simplify(KM.rhs() -
|
| 136 |
+
KM.mass_matrix_full.LUsolve(KM.forcing_full)) == zeros(2, 1)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def test_rolling_disc():
|
| 140 |
+
# Rolling Disc Example
|
| 141 |
+
# Here the rolling disc is formed from the contact point up, removing the
|
| 142 |
+
# need to introduce generalized speeds. Only 3 configuration and three
|
| 143 |
+
# speed variables are need to describe this system, along with the disc's
|
| 144 |
+
# mass and radius, and the local gravity (note that mass will drop out).
|
| 145 |
+
q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1 q2 q3 u1 u2 u3')
|
| 146 |
+
q1d, q2d, q3d, u1d, u2d, u3d = dynamicsymbols('q1 q2 q3 u1 u2 u3', 1)
|
| 147 |
+
r, m, g = symbols('r m g')
|
| 148 |
+
|
| 149 |
+
# The kinematics are formed by a series of simple rotations. Each simple
|
| 150 |
+
# rotation creates a new frame, and the next rotation is defined by the new
|
| 151 |
+
# frame's basis vectors. This example uses a 3-1-2 series of rotations, or
|
| 152 |
+
# Z, X, Y series of rotations. Angular velocity for this is defined using
|
| 153 |
+
# the second frame's basis (the lean frame).
|
| 154 |
+
N = ReferenceFrame('N')
|
| 155 |
+
Y = N.orientnew('Y', 'Axis', [q1, N.z])
|
| 156 |
+
L = Y.orientnew('L', 'Axis', [q2, Y.x])
|
| 157 |
+
R = L.orientnew('R', 'Axis', [q3, L.y])
|
| 158 |
+
w_R_N_qd = R.ang_vel_in(N)
|
| 159 |
+
R.set_ang_vel(N, u1 * L.x + u2 * L.y + u3 * L.z)
|
| 160 |
+
|
| 161 |
+
# This is the translational kinematics. We create a point with no velocity
|
| 162 |
+
# in N; this is the contact point between the disc and ground. Next we form
|
| 163 |
+
# the position vector from the contact point to the disc's center of mass.
|
| 164 |
+
# Finally we form the velocity and acceleration of the disc.
|
| 165 |
+
C = Point('C')
|
| 166 |
+
C.set_vel(N, 0)
|
| 167 |
+
Dmc = C.locatenew('Dmc', r * L.z)
|
| 168 |
+
Dmc.v2pt_theory(C, N, R)
|
| 169 |
+
|
| 170 |
+
# This is a simple way to form the inertia dyadic.
|
| 171 |
+
I = inertia(L, m / 4 * r**2, m / 2 * r**2, m / 4 * r**2)
|
| 172 |
+
|
| 173 |
+
# Kinematic differential equations; how the generalized coordinate time
|
| 174 |
+
# derivatives relate to generalized speeds.
|
| 175 |
+
kd = [dot(R.ang_vel_in(N) - w_R_N_qd, uv) for uv in L]
|
| 176 |
+
|
| 177 |
+
# Creation of the force list; it is the gravitational force at the mass
|
| 178 |
+
# center of the disc. Then we create the disc by assigning a Point to the
|
| 179 |
+
# center of mass attribute, a ReferenceFrame to the frame attribute, and mass
|
| 180 |
+
# and inertia. Then we form the body list.
|
| 181 |
+
ForceList = [(Dmc, - m * g * Y.z)]
|
| 182 |
+
BodyD = RigidBody('BodyD', Dmc, R, m, (I, Dmc))
|
| 183 |
+
BodyList = [BodyD]
|
| 184 |
+
|
| 185 |
+
# Finally we form the equations of motion, using the same steps we did
|
| 186 |
+
# before. Specify inertial frame, supply generalized speeds, supply
|
| 187 |
+
# kinematic differential equation dictionary, compute Fr from the force
|
| 188 |
+
# list and Fr* from the body list, compute the mass matrix and forcing
|
| 189 |
+
# terms, then solve for the u dots (time derivatives of the generalized
|
| 190 |
+
# speeds).
|
| 191 |
+
KM = KanesMethod(N, q_ind=[q1, q2, q3], u_ind=[u1, u2, u3], kd_eqs=kd)
|
| 192 |
+
KM.kanes_equations(BodyList, ForceList)
|
| 193 |
+
MM = KM.mass_matrix
|
| 194 |
+
forcing = KM.forcing
|
| 195 |
+
rhs = MM.inv() * forcing
|
| 196 |
+
kdd = KM.kindiffdict()
|
| 197 |
+
rhs = rhs.subs(kdd)
|
| 198 |
+
rhs.simplify()
|
| 199 |
+
assert rhs.expand() == Matrix([(6*u2*u3*r - u3**2*r*tan(q2) +
|
| 200 |
+
4*g*sin(q2))/(5*r), -2*u1*u3/3, u1*(-2*u2 + u3*tan(q2))]).expand()
|
| 201 |
+
assert simplify(KM.rhs() -
|
| 202 |
+
KM.mass_matrix_full.LUsolve(KM.forcing_full)) == zeros(6, 1)
|
| 203 |
+
|
| 204 |
+
# This code tests our output vs. benchmark values. When r=g=m=1, the
|
| 205 |
+
# critical speed (where all eigenvalues of the linearized equations are 0)
|
| 206 |
+
# is 1 / sqrt(3) for the upright case.
|
| 207 |
+
A = KM.linearize(A_and_B=True)[0]
|
| 208 |
+
A_upright = A.subs({r: 1, g: 1, m: 1}).subs({q1: 0, q2: 0, q3: 0, u1: 0, u3: 0})
|
| 209 |
+
import sympy
|
| 210 |
+
assert sympy.sympify(A_upright.subs({u2: 1 / sqrt(3)})).eigenvals() == {S.Zero: 6}
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def test_aux():
|
| 214 |
+
# Same as above, except we have 2 auxiliary speeds for the ground contact
|
| 215 |
+
# point, which is known to be zero. In one case, we go through then
|
| 216 |
+
# substitute the aux. speeds in at the end (they are zero, as well as their
|
| 217 |
+
# derivative), in the other case, we use the built-in auxiliary speed part
|
| 218 |
+
# of KanesMethod. The equations from each should be the same.
|
| 219 |
+
q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1 q2 q3 u1 u2 u3')
|
| 220 |
+
q1d, q2d, q3d, u1d, u2d, u3d = dynamicsymbols('q1 q2 q3 u1 u2 u3', 1)
|
| 221 |
+
u4, u5, f1, f2 = dynamicsymbols('u4, u5, f1, f2')
|
| 222 |
+
u4d, u5d = dynamicsymbols('u4, u5', 1)
|
| 223 |
+
r, m, g = symbols('r m g')
|
| 224 |
+
|
| 225 |
+
N = ReferenceFrame('N')
|
| 226 |
+
Y = N.orientnew('Y', 'Axis', [q1, N.z])
|
| 227 |
+
L = Y.orientnew('L', 'Axis', [q2, Y.x])
|
| 228 |
+
R = L.orientnew('R', 'Axis', [q3, L.y])
|
| 229 |
+
w_R_N_qd = R.ang_vel_in(N)
|
| 230 |
+
R.set_ang_vel(N, u1 * L.x + u2 * L.y + u3 * L.z)
|
| 231 |
+
|
| 232 |
+
C = Point('C')
|
| 233 |
+
C.set_vel(N, u4 * L.x + u5 * (Y.z ^ L.x))
|
| 234 |
+
Dmc = C.locatenew('Dmc', r * L.z)
|
| 235 |
+
Dmc.v2pt_theory(C, N, R)
|
| 236 |
+
Dmc.a2pt_theory(C, N, R)
|
| 237 |
+
|
| 238 |
+
I = inertia(L, m / 4 * r**2, m / 2 * r**2, m / 4 * r**2)
|
| 239 |
+
|
| 240 |
+
kd = [dot(R.ang_vel_in(N) - w_R_N_qd, uv) for uv in L]
|
| 241 |
+
|
| 242 |
+
ForceList = [(Dmc, - m * g * Y.z), (C, f1 * L.x + f2 * (Y.z ^ L.x))]
|
| 243 |
+
BodyD = RigidBody('BodyD', Dmc, R, m, (I, Dmc))
|
| 244 |
+
BodyList = [BodyD]
|
| 245 |
+
|
| 246 |
+
KM = KanesMethod(N, q_ind=[q1, q2, q3], u_ind=[u1, u2, u3, u4, u5],
|
| 247 |
+
kd_eqs=kd)
|
| 248 |
+
(fr, frstar) = KM.kanes_equations(BodyList, ForceList)
|
| 249 |
+
fr = fr.subs({u4d: 0, u5d: 0}).subs({u4: 0, u5: 0})
|
| 250 |
+
frstar = frstar.subs({u4d: 0, u5d: 0}).subs({u4: 0, u5: 0})
|
| 251 |
+
|
| 252 |
+
KM2 = KanesMethod(N, q_ind=[q1, q2, q3], u_ind=[u1, u2, u3], kd_eqs=kd,
|
| 253 |
+
u_auxiliary=[u4, u5])
|
| 254 |
+
(fr2, frstar2) = KM2.kanes_equations(BodyList, ForceList)
|
| 255 |
+
fr2 = fr2.subs({u4d: 0, u5d: 0}).subs({u4: 0, u5: 0})
|
| 256 |
+
frstar2 = frstar2.subs({u4d: 0, u5d: 0}).subs({u4: 0, u5: 0})
|
| 257 |
+
|
| 258 |
+
frstar.simplify()
|
| 259 |
+
frstar2.simplify()
|
| 260 |
+
|
| 261 |
+
assert (fr - fr2).expand() == Matrix([0, 0, 0, 0, 0])
|
| 262 |
+
assert (frstar - frstar2).expand() == Matrix([0, 0, 0, 0, 0])
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def test_parallel_axis():
|
| 266 |
+
# This is for a 2 dof inverted pendulum on a cart.
|
| 267 |
+
# This tests the parallel axis code in KanesMethod. The inertia of the
|
| 268 |
+
# pendulum is defined about the hinge, not about the center of mass.
|
| 269 |
+
|
| 270 |
+
# Defining the constants and knowns of the system
|
| 271 |
+
gravity = symbols('g')
|
| 272 |
+
k, ls = symbols('k ls')
|
| 273 |
+
a, mA, mC = symbols('a mA mC')
|
| 274 |
+
F = dynamicsymbols('F')
|
| 275 |
+
Ix, Iy, Iz = symbols('Ix Iy Iz')
|
| 276 |
+
|
| 277 |
+
# Declaring the Generalized coordinates and speeds
|
| 278 |
+
q1, q2 = dynamicsymbols('q1 q2')
|
| 279 |
+
q1d, q2d = dynamicsymbols('q1 q2', 1)
|
| 280 |
+
u1, u2 = dynamicsymbols('u1 u2')
|
| 281 |
+
u1d, u2d = dynamicsymbols('u1 u2', 1)
|
| 282 |
+
|
| 283 |
+
# Creating reference frames
|
| 284 |
+
N = ReferenceFrame('N')
|
| 285 |
+
A = ReferenceFrame('A')
|
| 286 |
+
|
| 287 |
+
A.orient(N, 'Axis', [-q2, N.z])
|
| 288 |
+
A.set_ang_vel(N, -u2 * N.z)
|
| 289 |
+
|
| 290 |
+
# Origin of Newtonian reference frame
|
| 291 |
+
O = Point('O')
|
| 292 |
+
|
| 293 |
+
# Creating and Locating the positions of the cart, C, and the
|
| 294 |
+
# center of mass of the pendulum, A
|
| 295 |
+
C = O.locatenew('C', q1 * N.x)
|
| 296 |
+
Ao = C.locatenew('Ao', a * A.y)
|
| 297 |
+
|
| 298 |
+
# Defining velocities of the points
|
| 299 |
+
O.set_vel(N, 0)
|
| 300 |
+
C.set_vel(N, u1 * N.x)
|
| 301 |
+
Ao.v2pt_theory(C, N, A)
|
| 302 |
+
Cart = Particle('Cart', C, mC)
|
| 303 |
+
Pendulum = RigidBody('Pendulum', Ao, A, mA, (inertia(A, Ix, Iy, Iz), C))
|
| 304 |
+
|
| 305 |
+
# kinematical differential equations
|
| 306 |
+
|
| 307 |
+
kindiffs = [q1d - u1, q2d - u2]
|
| 308 |
+
|
| 309 |
+
bodyList = [Cart, Pendulum]
|
| 310 |
+
|
| 311 |
+
forceList = [(Ao, -N.y * gravity * mA),
|
| 312 |
+
(C, -N.y * gravity * mC),
|
| 313 |
+
(C, -N.x * k * (q1 - ls)),
|
| 314 |
+
(C, N.x * F)]
|
| 315 |
+
|
| 316 |
+
km = KanesMethod(N, [q1, q2], [u1, u2], kindiffs)
|
| 317 |
+
(fr, frstar) = km.kanes_equations(bodyList, forceList)
|
| 318 |
+
mm = km.mass_matrix_full
|
| 319 |
+
assert mm[3, 3] == Iz
|
| 320 |
+
|
| 321 |
+
def test_input_format():
|
| 322 |
+
# 1 dof problem from test_one_dof
|
| 323 |
+
q, u = dynamicsymbols('q u')
|
| 324 |
+
qd, ud = dynamicsymbols('q u', 1)
|
| 325 |
+
m, c, k = symbols('m c k')
|
| 326 |
+
N = ReferenceFrame('N')
|
| 327 |
+
P = Point('P')
|
| 328 |
+
P.set_vel(N, u * N.x)
|
| 329 |
+
|
| 330 |
+
kd = [qd - u]
|
| 331 |
+
FL = [(P, (-k * q - c * u) * N.x)]
|
| 332 |
+
pa = Particle('pa', P, m)
|
| 333 |
+
BL = [pa]
|
| 334 |
+
|
| 335 |
+
KM = KanesMethod(N, [q], [u], kd)
|
| 336 |
+
# test for input format kane.kanes_equations((body1, body2, particle1))
|
| 337 |
+
assert KM.kanes_equations(BL)[0] == Matrix([0])
|
| 338 |
+
# test for input format kane.kanes_equations(bodies=(body1, body 2), loads=(load1,load2))
|
| 339 |
+
assert KM.kanes_equations(bodies=BL, loads=None)[0] == Matrix([0])
|
| 340 |
+
# test for input format kane.kanes_equations(bodies=(body1, body 2), loads=None)
|
| 341 |
+
assert KM.kanes_equations(BL, loads=None)[0] == Matrix([0])
|
| 342 |
+
# test for input format kane.kanes_equations(bodies=(body1, body 2))
|
| 343 |
+
assert KM.kanes_equations(BL)[0] == Matrix([0])
|
| 344 |
+
# test for input format kane.kanes_equations(bodies=(body1, body2), loads=[])
|
| 345 |
+
assert KM.kanes_equations(BL, [])[0] == Matrix([0])
|
| 346 |
+
# test for error raised when a wrong force list (in this case a string) is provided
|
| 347 |
+
raises(ValueError, lambda: KM._form_fr('bad input'))
|
| 348 |
+
|
| 349 |
+
# 1 dof problem from test_one_dof with FL & BL in instance
|
| 350 |
+
KM = KanesMethod(N, [q], [u], kd, bodies=BL, forcelist=FL)
|
| 351 |
+
assert KM.kanes_equations()[0] == Matrix([-c*u - k*q])
|
| 352 |
+
|
| 353 |
+
# 2 dof problem from test_two_dof
|
| 354 |
+
q1, q2, u1, u2 = dynamicsymbols('q1 q2 u1 u2')
|
| 355 |
+
q1d, q2d, u1d, u2d = dynamicsymbols('q1 q2 u1 u2', 1)
|
| 356 |
+
m, c1, c2, k1, k2 = symbols('m c1 c2 k1 k2')
|
| 357 |
+
N = ReferenceFrame('N')
|
| 358 |
+
P1 = Point('P1')
|
| 359 |
+
P2 = Point('P2')
|
| 360 |
+
P1.set_vel(N, u1 * N.x)
|
| 361 |
+
P2.set_vel(N, (u1 + u2) * N.x)
|
| 362 |
+
kd = [q1d - u1, q2d - u2]
|
| 363 |
+
|
| 364 |
+
FL = ((P1, (-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2) * N.x), (P2, (-k2 *
|
| 365 |
+
q2 - c2 * u2) * N.x))
|
| 366 |
+
pa1 = Particle('pa1', P1, m)
|
| 367 |
+
pa2 = Particle('pa2', P2, m)
|
| 368 |
+
BL = (pa1, pa2)
|
| 369 |
+
|
| 370 |
+
KM = KanesMethod(N, q_ind=[q1, q2], u_ind=[u1, u2], kd_eqs=kd)
|
| 371 |
+
# test for input format
|
| 372 |
+
# kane.kanes_equations((body1, body2), (load1, load2))
|
| 373 |
+
KM.kanes_equations(BL, FL)
|
| 374 |
+
MM = KM.mass_matrix
|
| 375 |
+
forcing = KM.forcing
|
| 376 |
+
rhs = MM.inv() * forcing
|
| 377 |
+
assert expand(rhs[0]) == expand((-k1 * q1 - c1 * u1 + k2 * q2 + c2 * u2)/m)
|
| 378 |
+
assert expand(rhs[1]) == expand((k1 * q1 + c1 * u1 - 2 * k2 * q2 - 2 *
|
| 379 |
+
c2 * u2) / m)
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
def test_implicit_kinematics():
|
| 383 |
+
# Test that implicit kinematics can handle complicated
|
| 384 |
+
# equations that explicit form struggles with
|
| 385 |
+
# See https://github.com/sympy/sympy/issues/22626
|
| 386 |
+
|
| 387 |
+
# Inertial frame
|
| 388 |
+
NED = ReferenceFrame('NED')
|
| 389 |
+
NED_o = Point('NED_o')
|
| 390 |
+
NED_o.set_vel(NED, 0)
|
| 391 |
+
|
| 392 |
+
# body frame
|
| 393 |
+
q_att = dynamicsymbols('lambda_0:4', real=True)
|
| 394 |
+
B = NED.orientnew('B', 'Quaternion', q_att)
|
| 395 |
+
|
| 396 |
+
# Generalized coordinates
|
| 397 |
+
q_pos = dynamicsymbols('B_x:z')
|
| 398 |
+
B_cm = NED_o.locatenew('B_cm', q_pos[0]*B.x + q_pos[1]*B.y + q_pos[2]*B.z)
|
| 399 |
+
|
| 400 |
+
q_ind = q_att[1:] + q_pos
|
| 401 |
+
q_dep = [q_att[0]]
|
| 402 |
+
|
| 403 |
+
kinematic_eqs = []
|
| 404 |
+
|
| 405 |
+
# Generalized velocities
|
| 406 |
+
B_ang_vel = B.ang_vel_in(NED)
|
| 407 |
+
P, Q, R = dynamicsymbols('P Q R')
|
| 408 |
+
B.set_ang_vel(NED, P*B.x + Q*B.y + R*B.z)
|
| 409 |
+
|
| 410 |
+
B_ang_vel_kd = (B.ang_vel_in(NED) - B_ang_vel).simplify()
|
| 411 |
+
|
| 412 |
+
# Equating the two gives us the kinematic equation
|
| 413 |
+
kinematic_eqs += [
|
| 414 |
+
B_ang_vel_kd & B.x,
|
| 415 |
+
B_ang_vel_kd & B.y,
|
| 416 |
+
B_ang_vel_kd & B.z
|
| 417 |
+
]
|
| 418 |
+
|
| 419 |
+
B_cm_vel = B_cm.vel(NED)
|
| 420 |
+
U, V, W = dynamicsymbols('U V W')
|
| 421 |
+
B_cm.set_vel(NED, U*B.x + V*B.y + W*B.z)
|
| 422 |
+
|
| 423 |
+
# Compute the velocity of the point using the two methods
|
| 424 |
+
B_ref_vel_kd = (B_cm.vel(NED) - B_cm_vel)
|
| 425 |
+
|
| 426 |
+
# taking dot product with unit vectors to get kinematic equations
|
| 427 |
+
# relating body coordinates and velocities
|
| 428 |
+
|
| 429 |
+
# Note, there is a choice to dot with NED.xyz here. That makes
|
| 430 |
+
# the implicit form have some bigger terms but is still fine, the
|
| 431 |
+
# explicit form still struggles though
|
| 432 |
+
kinematic_eqs += [
|
| 433 |
+
B_ref_vel_kd & B.x,
|
| 434 |
+
B_ref_vel_kd & B.y,
|
| 435 |
+
B_ref_vel_kd & B.z,
|
| 436 |
+
]
|
| 437 |
+
|
| 438 |
+
u_ind = [U, V, W, P, Q, R]
|
| 439 |
+
|
| 440 |
+
# constraints
|
| 441 |
+
q_att_vec = Matrix(q_att)
|
| 442 |
+
config_cons = [(q_att_vec.T*q_att_vec)[0] - 1] #unit norm
|
| 443 |
+
kinematic_eqs = kinematic_eqs + [(q_att_vec.T * q_att_vec.diff())[0]]
|
| 444 |
+
|
| 445 |
+
try:
|
| 446 |
+
KM = KanesMethod(NED, q_ind, u_ind,
|
| 447 |
+
q_dependent= q_dep,
|
| 448 |
+
kd_eqs = kinematic_eqs,
|
| 449 |
+
configuration_constraints = config_cons,
|
| 450 |
+
velocity_constraints= [],
|
| 451 |
+
u_dependent= [], #no dependent speeds
|
| 452 |
+
u_auxiliary = [], # No auxiliary speeds
|
| 453 |
+
explicit_kinematics = False # implicit kinematics
|
| 454 |
+
)
|
| 455 |
+
except Exception as e:
|
| 456 |
+
raise e
|
| 457 |
+
|
| 458 |
+
# mass and inertia dyadic relative to CM
|
| 459 |
+
M_B = symbols('M_B')
|
| 460 |
+
J_B = inertia(B, *[S(f'J_B_{ax}')*(1 if ax[0] == ax[1] else -1)
|
| 461 |
+
for ax in ['xx', 'yy', 'zz', 'xy', 'yz', 'xz']])
|
| 462 |
+
J_B = J_B.subs({S('J_B_xy'): 0, S('J_B_yz'): 0})
|
| 463 |
+
RB = RigidBody('RB', B_cm, B, M_B, (J_B, B_cm))
|
| 464 |
+
|
| 465 |
+
rigid_bodies = [RB]
|
| 466 |
+
# Forces
|
| 467 |
+
force_list = [
|
| 468 |
+
#gravity pointing down
|
| 469 |
+
(RB.masscenter, RB.mass*S('g')*NED.z),
|
| 470 |
+
#generic forces and torques in body frame(inputs)
|
| 471 |
+
(RB.frame, dynamicsymbols('T_z')*B.z),
|
| 472 |
+
(RB.masscenter, dynamicsymbols('F_z')*B.z)
|
| 473 |
+
]
|
| 474 |
+
|
| 475 |
+
KM.kanes_equations(rigid_bodies, force_list)
|
| 476 |
+
|
| 477 |
+
# Expecting implicit form to be less than 5% of the flops
|
| 478 |
+
n_ops_implicit = sum(
|
| 479 |
+
[x.count_ops() for x in KM.forcing_full] +
|
| 480 |
+
[x.count_ops() for x in KM.mass_matrix_full]
|
| 481 |
+
)
|
| 482 |
+
# Save implicit kinematic matrices to use later
|
| 483 |
+
mass_matrix_kin_implicit = KM.mass_matrix_kin
|
| 484 |
+
forcing_kin_implicit = KM.forcing_kin
|
| 485 |
+
|
| 486 |
+
KM.explicit_kinematics = True
|
| 487 |
+
n_ops_explicit = sum(
|
| 488 |
+
[x.count_ops() for x in KM.forcing_full] +
|
| 489 |
+
[x.count_ops() for x in KM.mass_matrix_full]
|
| 490 |
+
)
|
| 491 |
+
forcing_kin_explicit = KM.forcing_kin
|
| 492 |
+
|
| 493 |
+
assert n_ops_implicit / n_ops_explicit < .05
|
| 494 |
+
|
| 495 |
+
# Ideally we would check that implicit and explicit equations give the same result as done in test_one_dof
|
| 496 |
+
# But the whole raison-d'etre of the implicit equations is to deal with problems such
|
| 497 |
+
# as this one where the explicit form is too complicated to handle, especially the angular part
|
| 498 |
+
# (i.e. tests would be too slow)
|
| 499 |
+
# Instead, we check that the kinematic equations are correct using more fundamental tests:
|
| 500 |
+
#
|
| 501 |
+
# (1) that we recover the kinematic equations we have provided
|
| 502 |
+
assert (mass_matrix_kin_implicit * KM.q.diff() - forcing_kin_implicit) == Matrix(kinematic_eqs)
|
| 503 |
+
|
| 504 |
+
# (2) that rate of quaternions matches what 'textbook' solutions give
|
| 505 |
+
# Note that we just use the explicit kinematics for the linear velocities
|
| 506 |
+
# as they are not as complicated as the angular ones
|
| 507 |
+
qdot_candidate = forcing_kin_explicit
|
| 508 |
+
|
| 509 |
+
quat_dot_textbook = Matrix([
|
| 510 |
+
[0, -P, -Q, -R],
|
| 511 |
+
[P, 0, R, -Q],
|
| 512 |
+
[Q, -R, 0, P],
|
| 513 |
+
[R, Q, -P, 0],
|
| 514 |
+
]) * q_att_vec / 2
|
| 515 |
+
|
| 516 |
+
# Again, if we don't use this "textbook" solution
|
| 517 |
+
# sympy will struggle to deal with the terms related to quaternion rates
|
| 518 |
+
# due to the number of operations involved
|
| 519 |
+
qdot_candidate[-1] = quat_dot_textbook[0] # lambda_0, note the [-1] as sympy's Kane puts the dependent coordinate last
|
| 520 |
+
qdot_candidate[0] = quat_dot_textbook[1] # lambda_1
|
| 521 |
+
qdot_candidate[1] = quat_dot_textbook[2] # lambda_2
|
| 522 |
+
qdot_candidate[2] = quat_dot_textbook[3] # lambda_3
|
| 523 |
+
|
| 524 |
+
# sub the config constraint in the candidate solution and compare to the implicit rhs
|
| 525 |
+
lambda_0_sol = solve(config_cons[0], q_att_vec[0])[1]
|
| 526 |
+
lhs_candidate = simplify(mass_matrix_kin_implicit * qdot_candidate).subs({q_att_vec[0]: lambda_0_sol})
|
| 527 |
+
assert lhs_candidate == forcing_kin_implicit
|
| 528 |
+
|
| 529 |
+
def test_issue_24887():
|
| 530 |
+
# Spherical pendulum
|
| 531 |
+
g, l, m, c = symbols('g l m c')
|
| 532 |
+
q1, q2, q3, u1, u2, u3 = dynamicsymbols('q1:4 u1:4')
|
| 533 |
+
N = ReferenceFrame('N')
|
| 534 |
+
A = ReferenceFrame('A')
|
| 535 |
+
A.orient_body_fixed(N, (q1, q2, q3), 'zxy')
|
| 536 |
+
N_w_A = A.ang_vel_in(N)
|
| 537 |
+
# A.set_ang_vel(N, u1 * A.x + u2 * A.y + u3 * A.z)
|
| 538 |
+
kdes = [N_w_A.dot(A.x) - u1, N_w_A.dot(A.y) - u2, N_w_A.dot(A.z) - u3]
|
| 539 |
+
O = Point('O')
|
| 540 |
+
O.set_vel(N, 0)
|
| 541 |
+
Po = O.locatenew('Po', -l * A.y)
|
| 542 |
+
Po.set_vel(A, 0)
|
| 543 |
+
P = Particle('P', Po, m)
|
| 544 |
+
kane = KanesMethod(N, [q1, q2, q3], [u1, u2, u3], kdes, bodies=[P],
|
| 545 |
+
forcelist=[(Po, -m * g * N.y)])
|
| 546 |
+
kane.kanes_equations()
|
| 547 |
+
expected_md = m * l ** 2 * Matrix([[1, 0, 0], [0, 0, 0], [0, 0, 1]])
|
| 548 |
+
expected_fd = Matrix([
|
| 549 |
+
[l*m*(g*(sin(q1)*sin(q3) - sin(q2)*cos(q1)*cos(q3)) - l*u2*u3)],
|
| 550 |
+
[0], [l*m*(-g*(sin(q1)*cos(q3) + sin(q2)*sin(q3)*cos(q1)) + l*u1*u2)]])
|
| 551 |
+
assert find_dynamicsymbols(kane.forcing).issubset({q1, q2, q3, u1, u2, u3})
|
| 552 |
+
assert simplify(kane.mass_matrix - expected_md) == zeros(3, 3)
|
| 553 |
+
assert simplify(kane.forcing - expected_fd) == zeros(3, 1)
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane2.py
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import cos, Matrix, sin, zeros, tan, pi, symbols
|
| 2 |
+
from sympy.simplify.simplify import simplify
|
| 3 |
+
from sympy.simplify.trigsimp import trigsimp
|
| 4 |
+
from sympy.solvers.solvers import solve
|
| 5 |
+
from sympy.physics.mechanics import (cross, dot, dynamicsymbols,
|
| 6 |
+
find_dynamicsymbols, KanesMethod, inertia,
|
| 7 |
+
inertia_of_point_mass, Point,
|
| 8 |
+
ReferenceFrame, RigidBody)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_aux_dep():
|
| 12 |
+
# This test is about rolling disc dynamics, comparing the results found
|
| 13 |
+
# with KanesMethod to those found when deriving the equations "manually"
|
| 14 |
+
# with SymPy.
|
| 15 |
+
# The terms Fr, Fr*, and Fr*_steady are all compared between the two
|
| 16 |
+
# methods. Here, Fr*_steady refers to the generalized inertia forces for an
|
| 17 |
+
# equilibrium configuration.
|
| 18 |
+
# Note: comparing to the test of test_rolling_disc() in test_kane.py, this
|
| 19 |
+
# test also tests auxiliary speeds and configuration and motion constraints
|
| 20 |
+
#, seen in the generalized dependent coordinates q[3], and depend speeds
|
| 21 |
+
# u[3], u[4] and u[5].
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# First, manual derivation of Fr, Fr_star, Fr_star_steady.
|
| 25 |
+
|
| 26 |
+
# Symbols for time and constant parameters.
|
| 27 |
+
# Symbols for contact forces: Fx, Fy, Fz.
|
| 28 |
+
t, r, m, g, I, J = symbols('t r m g I J')
|
| 29 |
+
Fx, Fy, Fz = symbols('Fx Fy Fz')
|
| 30 |
+
|
| 31 |
+
# Configuration variables and their time derivatives:
|
| 32 |
+
# q[0] -- yaw
|
| 33 |
+
# q[1] -- lean
|
| 34 |
+
# q[2] -- spin
|
| 35 |
+
# q[3] -- dot(-r*B.z, A.z) -- distance from ground plane to disc center in
|
| 36 |
+
# A.z direction
|
| 37 |
+
# Generalized speeds and their time derivatives:
|
| 38 |
+
# u[0] -- disc angular velocity component, disc fixed x direction
|
| 39 |
+
# u[1] -- disc angular velocity component, disc fixed y direction
|
| 40 |
+
# u[2] -- disc angular velocity component, disc fixed z direction
|
| 41 |
+
# u[3] -- disc velocity component, A.x direction
|
| 42 |
+
# u[4] -- disc velocity component, A.y direction
|
| 43 |
+
# u[5] -- disc velocity component, A.z direction
|
| 44 |
+
# Auxiliary generalized speeds:
|
| 45 |
+
# ua[0] -- contact point auxiliary generalized speed, A.x direction
|
| 46 |
+
# ua[1] -- contact point auxiliary generalized speed, A.y direction
|
| 47 |
+
# ua[2] -- contact point auxiliary generalized speed, A.z direction
|
| 48 |
+
q = dynamicsymbols('q:4')
|
| 49 |
+
qd = [qi.diff(t) for qi in q]
|
| 50 |
+
u = dynamicsymbols('u:6')
|
| 51 |
+
ud = [ui.diff(t) for ui in u]
|
| 52 |
+
ud_zero = dict(zip(ud, [0.]*len(ud)))
|
| 53 |
+
ua = dynamicsymbols('ua:3')
|
| 54 |
+
ua_zero = dict(zip(ua, [0.]*len(ua))) # noqa:F841
|
| 55 |
+
|
| 56 |
+
# Reference frames:
|
| 57 |
+
# Yaw intermediate frame: A.
|
| 58 |
+
# Lean intermediate frame: B.
|
| 59 |
+
# Disc fixed frame: C.
|
| 60 |
+
N = ReferenceFrame('N')
|
| 61 |
+
A = N.orientnew('A', 'Axis', [q[0], N.z])
|
| 62 |
+
B = A.orientnew('B', 'Axis', [q[1], A.x])
|
| 63 |
+
C = B.orientnew('C', 'Axis', [q[2], B.y])
|
| 64 |
+
|
| 65 |
+
# Angular velocity and angular acceleration of disc fixed frame
|
| 66 |
+
# u[0], u[1] and u[2] are generalized independent speeds.
|
| 67 |
+
C.set_ang_vel(N, u[0]*B.x + u[1]*B.y + u[2]*B.z)
|
| 68 |
+
C.set_ang_acc(N, C.ang_vel_in(N).diff(t, B)
|
| 69 |
+
+ cross(B.ang_vel_in(N), C.ang_vel_in(N)))
|
| 70 |
+
|
| 71 |
+
# Velocity and acceleration of points:
|
| 72 |
+
# Disc-ground contact point: P.
|
| 73 |
+
# Center of disc: O, defined from point P with depend coordinate: q[3]
|
| 74 |
+
# u[3], u[4] and u[5] are generalized dependent speeds.
|
| 75 |
+
P = Point('P')
|
| 76 |
+
P.set_vel(N, ua[0]*A.x + ua[1]*A.y + ua[2]*A.z)
|
| 77 |
+
O = P.locatenew('O', q[3]*A.z + r*sin(q[1])*A.y)
|
| 78 |
+
O.set_vel(N, u[3]*A.x + u[4]*A.y + u[5]*A.z)
|
| 79 |
+
O.set_acc(N, O.vel(N).diff(t, A) + cross(A.ang_vel_in(N), O.vel(N)))
|
| 80 |
+
|
| 81 |
+
# Kinematic differential equations:
|
| 82 |
+
# Two equalities: one is w_c_n_qd = C.ang_vel_in(N) in three coordinates
|
| 83 |
+
# directions of B, for qd0, qd1 and qd2.
|
| 84 |
+
# the other is v_o_n_qd = O.vel(N) in A.z direction for qd3.
|
| 85 |
+
# Then, solve for dq/dt's in terms of u's: qd_kd.
|
| 86 |
+
w_c_n_qd = qd[0]*A.z + qd[1]*B.x + qd[2]*B.y
|
| 87 |
+
v_o_n_qd = O.pos_from(P).diff(t, A) + cross(A.ang_vel_in(N), O.pos_from(P))
|
| 88 |
+
kindiffs = Matrix([dot(w_c_n_qd - C.ang_vel_in(N), uv) for uv in B] +
|
| 89 |
+
[dot(v_o_n_qd - O.vel(N), A.z)])
|
| 90 |
+
qd_kd = solve(kindiffs, qd) # noqa:F841
|
| 91 |
+
|
| 92 |
+
# Values of generalized speeds during a steady turn for later substitution
|
| 93 |
+
# into the Fr_star_steady.
|
| 94 |
+
steady_conditions = solve(kindiffs.subs({qd[1] : 0, qd[3] : 0}), u)
|
| 95 |
+
steady_conditions.update({qd[1] : 0, qd[3] : 0})
|
| 96 |
+
|
| 97 |
+
# Partial angular velocities and velocities.
|
| 98 |
+
partial_w_C = [C.ang_vel_in(N).diff(ui, N) for ui in u + ua]
|
| 99 |
+
partial_v_O = [O.vel(N).diff(ui, N) for ui in u + ua]
|
| 100 |
+
partial_v_P = [P.vel(N).diff(ui, N) for ui in u + ua]
|
| 101 |
+
|
| 102 |
+
# Configuration constraint: f_c, the projection of radius r in A.z direction
|
| 103 |
+
# is q[3].
|
| 104 |
+
# Velocity constraints: f_v, for u3, u4 and u5.
|
| 105 |
+
# Acceleration constraints: f_a.
|
| 106 |
+
f_c = Matrix([dot(-r*B.z, A.z) - q[3]])
|
| 107 |
+
f_v = Matrix([dot(O.vel(N) - (P.vel(N) + cross(C.ang_vel_in(N),
|
| 108 |
+
O.pos_from(P))), ai).expand() for ai in A])
|
| 109 |
+
v_o_n = cross(C.ang_vel_in(N), O.pos_from(P))
|
| 110 |
+
a_o_n = v_o_n.diff(t, A) + cross(A.ang_vel_in(N), v_o_n)
|
| 111 |
+
f_a = Matrix([dot(O.acc(N) - a_o_n, ai) for ai in A]) # noqa:F841
|
| 112 |
+
|
| 113 |
+
# Solve for constraint equations in the form of
|
| 114 |
+
# u_dependent = A_rs * [u_i; u_aux].
|
| 115 |
+
# First, obtain constraint coefficient matrix: M_v * [u; ua] = 0;
|
| 116 |
+
# Second, taking u[0], u[1], u[2] as independent,
|
| 117 |
+
# taking u[3], u[4], u[5] as dependent,
|
| 118 |
+
# rearranging the matrix of M_v to be A_rs for u_dependent.
|
| 119 |
+
# Third, u_aux ==0 for u_dep, and resulting dictionary of u_dep_dict.
|
| 120 |
+
M_v = zeros(3, 9)
|
| 121 |
+
for i in range(3):
|
| 122 |
+
for j, ui in enumerate(u + ua):
|
| 123 |
+
M_v[i, j] = f_v[i].diff(ui)
|
| 124 |
+
|
| 125 |
+
M_v_i = M_v[:, :3]
|
| 126 |
+
M_v_d = M_v[:, 3:6]
|
| 127 |
+
M_v_aux = M_v[:, 6:]
|
| 128 |
+
M_v_i_aux = M_v_i.row_join(M_v_aux)
|
| 129 |
+
A_rs = - M_v_d.inv() * M_v_i_aux
|
| 130 |
+
|
| 131 |
+
u_dep = A_rs[:, :3] * Matrix(u[:3])
|
| 132 |
+
u_dep_dict = dict(zip(u[3:], u_dep))
|
| 133 |
+
|
| 134 |
+
# Active forces: F_O acting on point O; F_P acting on point P.
|
| 135 |
+
# Generalized active forces (unconstrained): Fr_u = F_point * pv_point.
|
| 136 |
+
F_O = m*g*A.z
|
| 137 |
+
F_P = Fx * A.x + Fy * A.y + Fz * A.z
|
| 138 |
+
Fr_u = Matrix([dot(F_O, pv_o) + dot(F_P, pv_p) for pv_o, pv_p in
|
| 139 |
+
zip(partial_v_O, partial_v_P)])
|
| 140 |
+
|
| 141 |
+
# Inertia force: R_star_O.
|
| 142 |
+
# Inertia of disc: I_C_O, where J is a inertia component about principal axis.
|
| 143 |
+
# Inertia torque: T_star_C.
|
| 144 |
+
# Generalized inertia forces (unconstrained): Fr_star_u.
|
| 145 |
+
R_star_O = -m*O.acc(N)
|
| 146 |
+
I_C_O = inertia(B, I, J, I)
|
| 147 |
+
T_star_C = -(dot(I_C_O, C.ang_acc_in(N)) \
|
| 148 |
+
+ cross(C.ang_vel_in(N), dot(I_C_O, C.ang_vel_in(N))))
|
| 149 |
+
Fr_star_u = Matrix([dot(R_star_O, pv) + dot(T_star_C, pav) for pv, pav in
|
| 150 |
+
zip(partial_v_O, partial_w_C)])
|
| 151 |
+
|
| 152 |
+
# Form nonholonomic Fr: Fr_c, and nonholonomic Fr_star: Fr_star_c.
|
| 153 |
+
# Also, nonholonomic Fr_star in steady turning condition: Fr_star_steady.
|
| 154 |
+
Fr_c = Fr_u[:3, :].col_join(Fr_u[6:, :]) + A_rs.T * Fr_u[3:6, :]
|
| 155 |
+
Fr_star_c = Fr_star_u[:3, :].col_join(Fr_star_u[6:, :])\
|
| 156 |
+
+ A_rs.T * Fr_star_u[3:6, :]
|
| 157 |
+
Fr_star_steady = Fr_star_c.subs(ud_zero).subs(u_dep_dict)\
|
| 158 |
+
.subs(steady_conditions).subs({q[3]: -r*cos(q[1])}).expand()
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
# Second, using KaneMethod in mechanics for fr, frstar and frstar_steady.
|
| 162 |
+
|
| 163 |
+
# Rigid Bodies: disc, with inertia I_C_O.
|
| 164 |
+
iner_tuple = (I_C_O, O)
|
| 165 |
+
disc = RigidBody('disc', O, C, m, iner_tuple)
|
| 166 |
+
bodyList = [disc]
|
| 167 |
+
|
| 168 |
+
# Generalized forces: Gravity: F_o; Auxiliary forces: F_p.
|
| 169 |
+
F_o = (O, F_O)
|
| 170 |
+
F_p = (P, F_P)
|
| 171 |
+
forceList = [F_o, F_p]
|
| 172 |
+
|
| 173 |
+
# KanesMethod.
|
| 174 |
+
kane = KanesMethod(
|
| 175 |
+
N, q_ind= q[:3], u_ind= u[:3], kd_eqs=kindiffs,
|
| 176 |
+
q_dependent=q[3:], configuration_constraints = f_c,
|
| 177 |
+
u_dependent=u[3:], velocity_constraints= f_v,
|
| 178 |
+
u_auxiliary=ua
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
# fr, frstar, frstar_steady and kdd(kinematic differential equations).
|
| 182 |
+
(fr, frstar)= kane.kanes_equations(bodyList, forceList)
|
| 183 |
+
frstar_steady = frstar.subs(ud_zero).subs(u_dep_dict).subs(steady_conditions)\
|
| 184 |
+
.subs({q[3]: -r*cos(q[1])}).expand()
|
| 185 |
+
kdd = kane.kindiffdict()
|
| 186 |
+
|
| 187 |
+
assert Matrix(Fr_c).expand() == fr.expand()
|
| 188 |
+
assert Matrix(Fr_star_c.subs(kdd)).expand() == frstar.expand()
|
| 189 |
+
# These Matrices have some Integer(0) and some Float(0). Running under
|
| 190 |
+
# SymEngine gives different types of zero.
|
| 191 |
+
assert (simplify(Matrix(Fr_star_steady).expand()).xreplace({0:0.0}) ==
|
| 192 |
+
simplify(frstar_steady.expand()).xreplace({0:0.0}))
|
| 193 |
+
|
| 194 |
+
syms_in_forcing = find_dynamicsymbols(kane.forcing)
|
| 195 |
+
for qdi in qd:
|
| 196 |
+
assert qdi not in syms_in_forcing
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def test_non_central_inertia():
|
| 200 |
+
# This tests that the calculation of Fr* does not depend the point
|
| 201 |
+
# about which the inertia of a rigid body is defined. This test solves
|
| 202 |
+
# exercises 8.12, 8.17 from Kane 1985.
|
| 203 |
+
|
| 204 |
+
# Declare symbols
|
| 205 |
+
q1, q2, q3 = dynamicsymbols('q1:4')
|
| 206 |
+
q1d, q2d, q3d = dynamicsymbols('q1:4', level=1)
|
| 207 |
+
u1, u2, u3, u4, u5 = dynamicsymbols('u1:6')
|
| 208 |
+
u_prime, R, M, g, e, f, theta = symbols('u\' R, M, g, e, f, theta')
|
| 209 |
+
a, b, mA, mB, IA, J, K, t = symbols('a b mA mB IA J K t')
|
| 210 |
+
Q1, Q2, Q3 = symbols('Q1, Q2 Q3')
|
| 211 |
+
IA22, IA23, IA33 = symbols('IA22 IA23 IA33')
|
| 212 |
+
|
| 213 |
+
# Reference Frames
|
| 214 |
+
F = ReferenceFrame('F')
|
| 215 |
+
P = F.orientnew('P', 'axis', [-theta, F.y])
|
| 216 |
+
A = P.orientnew('A', 'axis', [q1, P.x])
|
| 217 |
+
A.set_ang_vel(F, u1*A.x + u3*A.z)
|
| 218 |
+
# define frames for wheels
|
| 219 |
+
B = A.orientnew('B', 'axis', [q2, A.z])
|
| 220 |
+
C = A.orientnew('C', 'axis', [q3, A.z])
|
| 221 |
+
B.set_ang_vel(A, u4 * A.z)
|
| 222 |
+
C.set_ang_vel(A, u5 * A.z)
|
| 223 |
+
|
| 224 |
+
# define points D, S*, Q on frame A and their velocities
|
| 225 |
+
pD = Point('D')
|
| 226 |
+
pD.set_vel(A, 0)
|
| 227 |
+
# u3 will not change v_D_F since wheels are still assumed to roll without slip.
|
| 228 |
+
pD.set_vel(F, u2 * A.y)
|
| 229 |
+
|
| 230 |
+
pS_star = pD.locatenew('S*', e*A.y)
|
| 231 |
+
pQ = pD.locatenew('Q', f*A.y - R*A.x)
|
| 232 |
+
for p in [pS_star, pQ]:
|
| 233 |
+
p.v2pt_theory(pD, F, A)
|
| 234 |
+
|
| 235 |
+
# masscenters of bodies A, B, C
|
| 236 |
+
pA_star = pD.locatenew('A*', a*A.y)
|
| 237 |
+
pB_star = pD.locatenew('B*', b*A.z)
|
| 238 |
+
pC_star = pD.locatenew('C*', -b*A.z)
|
| 239 |
+
for p in [pA_star, pB_star, pC_star]:
|
| 240 |
+
p.v2pt_theory(pD, F, A)
|
| 241 |
+
|
| 242 |
+
# points of B, C touching the plane P
|
| 243 |
+
pB_hat = pB_star.locatenew('B^', -R*A.x)
|
| 244 |
+
pC_hat = pC_star.locatenew('C^', -R*A.x)
|
| 245 |
+
pB_hat.v2pt_theory(pB_star, F, B)
|
| 246 |
+
pC_hat.v2pt_theory(pC_star, F, C)
|
| 247 |
+
|
| 248 |
+
# the velocities of B^, C^ are zero since B, C are assumed to roll without slip
|
| 249 |
+
kde = [q1d - u1, q2d - u4, q3d - u5]
|
| 250 |
+
vc = [dot(p.vel(F), A.y) for p in [pB_hat, pC_hat]]
|
| 251 |
+
|
| 252 |
+
# inertias of bodies A, B, C
|
| 253 |
+
# IA22, IA23, IA33 are not specified in the problem statement, but are
|
| 254 |
+
# necessary to define an inertia object. Although the values of
|
| 255 |
+
# IA22, IA23, IA33 are not known in terms of the variables given in the
|
| 256 |
+
# problem statement, they do not appear in the general inertia terms.
|
| 257 |
+
inertia_A = inertia(A, IA, IA22, IA33, 0, IA23, 0)
|
| 258 |
+
inertia_B = inertia(B, K, K, J)
|
| 259 |
+
inertia_C = inertia(C, K, K, J)
|
| 260 |
+
|
| 261 |
+
# define the rigid bodies A, B, C
|
| 262 |
+
rbA = RigidBody('rbA', pA_star, A, mA, (inertia_A, pA_star))
|
| 263 |
+
rbB = RigidBody('rbB', pB_star, B, mB, (inertia_B, pB_star))
|
| 264 |
+
rbC = RigidBody('rbC', pC_star, C, mB, (inertia_C, pC_star))
|
| 265 |
+
|
| 266 |
+
km = KanesMethod(F, q_ind=[q1, q2, q3], u_ind=[u1, u2], kd_eqs=kde,
|
| 267 |
+
u_dependent=[u4, u5], velocity_constraints=vc,
|
| 268 |
+
u_auxiliary=[u3])
|
| 269 |
+
|
| 270 |
+
forces = [(pS_star, -M*g*F.x), (pQ, Q1*A.x + Q2*A.y + Q3*A.z)]
|
| 271 |
+
bodies = [rbA, rbB, rbC]
|
| 272 |
+
fr, fr_star = km.kanes_equations(bodies, forces)
|
| 273 |
+
vc_map = solve(vc, [u4, u5])
|
| 274 |
+
|
| 275 |
+
# KanesMethod returns the negative of Fr, Fr* as defined in Kane1985.
|
| 276 |
+
fr_star_expected = Matrix([
|
| 277 |
+
-(IA + 2*J*b**2/R**2 + 2*K +
|
| 278 |
+
mA*a**2 + 2*mB*b**2) * u1.diff(t) - mA*a*u1*u2,
|
| 279 |
+
-(mA + 2*mB +2*J/R**2) * u2.diff(t) + mA*a*u1**2,
|
| 280 |
+
0])
|
| 281 |
+
t = trigsimp(fr_star.subs(vc_map).subs({u3: 0})).doit().expand()
|
| 282 |
+
assert ((fr_star_expected - t).expand() == zeros(3, 1))
|
| 283 |
+
|
| 284 |
+
# define inertias of rigid bodies A, B, C about point D
|
| 285 |
+
# I_S/O = I_S/S* + I_S*/O
|
| 286 |
+
bodies2 = []
|
| 287 |
+
for rb, I_star in zip([rbA, rbB, rbC], [inertia_A, inertia_B, inertia_C]):
|
| 288 |
+
I = I_star + inertia_of_point_mass(rb.mass,
|
| 289 |
+
rb.masscenter.pos_from(pD),
|
| 290 |
+
rb.frame)
|
| 291 |
+
bodies2.append(RigidBody('', rb.masscenter, rb.frame, rb.mass,
|
| 292 |
+
(I, pD)))
|
| 293 |
+
fr2, fr_star2 = km.kanes_equations(bodies2, forces)
|
| 294 |
+
|
| 295 |
+
t = trigsimp(fr_star2.subs(vc_map).subs({u3: 0})).doit()
|
| 296 |
+
assert (fr_star_expected - t).expand() == zeros(3, 1)
|
| 297 |
+
|
| 298 |
+
def test_sub_qdot():
|
| 299 |
+
# This test solves exercises 8.12, 8.17 from Kane 1985 and defines
|
| 300 |
+
# some velocities in terms of q, qdot.
|
| 301 |
+
|
| 302 |
+
## --- Declare symbols ---
|
| 303 |
+
q1, q2, q3 = dynamicsymbols('q1:4')
|
| 304 |
+
q1d, q2d, q3d = dynamicsymbols('q1:4', level=1)
|
| 305 |
+
u1, u2, u3 = dynamicsymbols('u1:4')
|
| 306 |
+
u_prime, R, M, g, e, f, theta = symbols('u\' R, M, g, e, f, theta')
|
| 307 |
+
a, b, mA, mB, IA, J, K, t = symbols('a b mA mB IA J K t')
|
| 308 |
+
IA22, IA23, IA33 = symbols('IA22 IA23 IA33')
|
| 309 |
+
Q1, Q2, Q3 = symbols('Q1 Q2 Q3')
|
| 310 |
+
|
| 311 |
+
# --- Reference Frames ---
|
| 312 |
+
F = ReferenceFrame('F')
|
| 313 |
+
P = F.orientnew('P', 'axis', [-theta, F.y])
|
| 314 |
+
A = P.orientnew('A', 'axis', [q1, P.x])
|
| 315 |
+
A.set_ang_vel(F, u1*A.x + u3*A.z)
|
| 316 |
+
# define frames for wheels
|
| 317 |
+
B = A.orientnew('B', 'axis', [q2, A.z])
|
| 318 |
+
C = A.orientnew('C', 'axis', [q3, A.z])
|
| 319 |
+
|
| 320 |
+
## --- define points D, S*, Q on frame A and their velocities ---
|
| 321 |
+
pD = Point('D')
|
| 322 |
+
pD.set_vel(A, 0)
|
| 323 |
+
# u3 will not change v_D_F since wheels are still assumed to roll w/o slip
|
| 324 |
+
pD.set_vel(F, u2 * A.y)
|
| 325 |
+
|
| 326 |
+
pS_star = pD.locatenew('S*', e*A.y)
|
| 327 |
+
pQ = pD.locatenew('Q', f*A.y - R*A.x)
|
| 328 |
+
# masscenters of bodies A, B, C
|
| 329 |
+
pA_star = pD.locatenew('A*', a*A.y)
|
| 330 |
+
pB_star = pD.locatenew('B*', b*A.z)
|
| 331 |
+
pC_star = pD.locatenew('C*', -b*A.z)
|
| 332 |
+
for p in [pS_star, pQ, pA_star, pB_star, pC_star]:
|
| 333 |
+
p.v2pt_theory(pD, F, A)
|
| 334 |
+
|
| 335 |
+
# points of B, C touching the plane P
|
| 336 |
+
pB_hat = pB_star.locatenew('B^', -R*A.x)
|
| 337 |
+
pC_hat = pC_star.locatenew('C^', -R*A.x)
|
| 338 |
+
pB_hat.v2pt_theory(pB_star, F, B)
|
| 339 |
+
pC_hat.v2pt_theory(pC_star, F, C)
|
| 340 |
+
|
| 341 |
+
# --- relate qdot, u ---
|
| 342 |
+
# the velocities of B^, C^ are zero since B, C are assumed to roll w/o slip
|
| 343 |
+
kde = [dot(p.vel(F), A.y) for p in [pB_hat, pC_hat]]
|
| 344 |
+
kde += [u1 - q1d]
|
| 345 |
+
kde_map = solve(kde, [q1d, q2d, q3d])
|
| 346 |
+
for k, v in list(kde_map.items()):
|
| 347 |
+
kde_map[k.diff(t)] = v.diff(t)
|
| 348 |
+
|
| 349 |
+
# inertias of bodies A, B, C
|
| 350 |
+
# IA22, IA23, IA33 are not specified in the problem statement, but are
|
| 351 |
+
# necessary to define an inertia object. Although the values of
|
| 352 |
+
# IA22, IA23, IA33 are not known in terms of the variables given in the
|
| 353 |
+
# problem statement, they do not appear in the general inertia terms.
|
| 354 |
+
inertia_A = inertia(A, IA, IA22, IA33, 0, IA23, 0)
|
| 355 |
+
inertia_B = inertia(B, K, K, J)
|
| 356 |
+
inertia_C = inertia(C, K, K, J)
|
| 357 |
+
|
| 358 |
+
# define the rigid bodies A, B, C
|
| 359 |
+
rbA = RigidBody('rbA', pA_star, A, mA, (inertia_A, pA_star))
|
| 360 |
+
rbB = RigidBody('rbB', pB_star, B, mB, (inertia_B, pB_star))
|
| 361 |
+
rbC = RigidBody('rbC', pC_star, C, mB, (inertia_C, pC_star))
|
| 362 |
+
|
| 363 |
+
## --- use kanes method ---
|
| 364 |
+
km = KanesMethod(F, [q1, q2, q3], [u1, u2], kd_eqs=kde, u_auxiliary=[u3])
|
| 365 |
+
|
| 366 |
+
forces = [(pS_star, -M*g*F.x), (pQ, Q1*A.x + Q2*A.y + Q3*A.z)]
|
| 367 |
+
bodies = [rbA, rbB, rbC]
|
| 368 |
+
|
| 369 |
+
# Q2 = -u_prime * u2 * Q1 / sqrt(u2**2 + f**2 * u1**2)
|
| 370 |
+
# -u_prime * R * u2 / sqrt(u2**2 + f**2 * u1**2) = R / Q1 * Q2
|
| 371 |
+
fr_expected = Matrix([
|
| 372 |
+
f*Q3 + M*g*e*sin(theta)*cos(q1),
|
| 373 |
+
Q2 + M*g*sin(theta)*sin(q1),
|
| 374 |
+
e*M*g*cos(theta) - Q1*f - Q2*R])
|
| 375 |
+
#Q1 * (f - u_prime * R * u2 / sqrt(u2**2 + f**2 * u1**2)))])
|
| 376 |
+
fr_star_expected = Matrix([
|
| 377 |
+
-(IA + 2*J*b**2/R**2 + 2*K +
|
| 378 |
+
mA*a**2 + 2*mB*b**2) * u1.diff(t) - mA*a*u1*u2,
|
| 379 |
+
-(mA + 2*mB +2*J/R**2) * u2.diff(t) + mA*a*u1**2,
|
| 380 |
+
0])
|
| 381 |
+
|
| 382 |
+
fr, fr_star = km.kanes_equations(bodies, forces)
|
| 383 |
+
assert (fr.expand() == fr_expected.expand())
|
| 384 |
+
assert ((fr_star_expected - trigsimp(fr_star)).expand() == zeros(3, 1))
|
| 385 |
+
|
| 386 |
+
def test_sub_qdot2():
|
| 387 |
+
# This test solves exercises 8.3 from Kane 1985 and defines
|
| 388 |
+
# all velocities in terms of q, qdot. We check that the generalized active
|
| 389 |
+
# forces are correctly computed if u terms are only defined in the
|
| 390 |
+
# kinematic differential equations.
|
| 391 |
+
#
|
| 392 |
+
# This functionality was added in PR 8948. Without qdot/u substitution, the
|
| 393 |
+
# KanesMethod constructor will fail during the constraint initialization as
|
| 394 |
+
# the B matrix will be poorly formed and inversion of the dependent part
|
| 395 |
+
# will fail.
|
| 396 |
+
|
| 397 |
+
g, m, Px, Py, Pz, R, t = symbols('g m Px Py Pz R t')
|
| 398 |
+
q = dynamicsymbols('q:5')
|
| 399 |
+
qd = dynamicsymbols('q:5', level=1)
|
| 400 |
+
u = dynamicsymbols('u:5')
|
| 401 |
+
|
| 402 |
+
## Define inertial, intermediate, and rigid body reference frames
|
| 403 |
+
A = ReferenceFrame('A')
|
| 404 |
+
B_prime = A.orientnew('B_prime', 'Axis', [q[0], A.z])
|
| 405 |
+
B = B_prime.orientnew('B', 'Axis', [pi/2 - q[1], B_prime.x])
|
| 406 |
+
C = B.orientnew('C', 'Axis', [q[2], B.z])
|
| 407 |
+
|
| 408 |
+
## Define points of interest and their velocities
|
| 409 |
+
pO = Point('O')
|
| 410 |
+
pO.set_vel(A, 0)
|
| 411 |
+
|
| 412 |
+
# R is the point in plane H that comes into contact with disk C.
|
| 413 |
+
pR = pO.locatenew('R', q[3]*A.x + q[4]*A.y)
|
| 414 |
+
pR.set_vel(A, pR.pos_from(pO).diff(t, A))
|
| 415 |
+
pR.set_vel(B, 0)
|
| 416 |
+
|
| 417 |
+
# C^ is the point in disk C that comes into contact with plane H.
|
| 418 |
+
pC_hat = pR.locatenew('C^', 0)
|
| 419 |
+
pC_hat.set_vel(C, 0)
|
| 420 |
+
|
| 421 |
+
# C* is the point at the center of disk C.
|
| 422 |
+
pCs = pC_hat.locatenew('C*', R*B.y)
|
| 423 |
+
pCs.set_vel(C, 0)
|
| 424 |
+
pCs.set_vel(B, 0)
|
| 425 |
+
|
| 426 |
+
# calculate velocites of points C* and C^ in frame A
|
| 427 |
+
pCs.v2pt_theory(pR, A, B) # points C* and R are fixed in frame B
|
| 428 |
+
pC_hat.v2pt_theory(pCs, A, C) # points C* and C^ are fixed in frame C
|
| 429 |
+
|
| 430 |
+
## Define forces on each point of the system
|
| 431 |
+
R_C_hat = Px*A.x + Py*A.y + Pz*A.z
|
| 432 |
+
R_Cs = -m*g*A.z
|
| 433 |
+
forces = [(pC_hat, R_C_hat), (pCs, R_Cs)]
|
| 434 |
+
|
| 435 |
+
## Define kinematic differential equations
|
| 436 |
+
# let ui = omega_C_A & bi (i = 1, 2, 3)
|
| 437 |
+
# u4 = qd4, u5 = qd5
|
| 438 |
+
u_expr = [C.ang_vel_in(A) & uv for uv in B]
|
| 439 |
+
u_expr += qd[3:]
|
| 440 |
+
kde = [ui - e for ui, e in zip(u, u_expr)]
|
| 441 |
+
km1 = KanesMethod(A, q, u, kde)
|
| 442 |
+
fr1, _ = km1.kanes_equations([], forces)
|
| 443 |
+
|
| 444 |
+
## Calculate generalized active forces if we impose the condition that the
|
| 445 |
+
# disk C is rolling without slipping
|
| 446 |
+
u_indep = u[:3]
|
| 447 |
+
u_dep = list(set(u) - set(u_indep))
|
| 448 |
+
vc = [pC_hat.vel(A) & uv for uv in [A.x, A.y]]
|
| 449 |
+
km2 = KanesMethod(A, q, u_indep, kde,
|
| 450 |
+
u_dependent=u_dep, velocity_constraints=vc)
|
| 451 |
+
fr2, _ = km2.kanes_equations([], forces)
|
| 452 |
+
|
| 453 |
+
fr1_expected = Matrix([
|
| 454 |
+
-R*g*m*sin(q[1]),
|
| 455 |
+
-R*(Px*cos(q[0]) + Py*sin(q[0]))*tan(q[1]),
|
| 456 |
+
R*(Px*cos(q[0]) + Py*sin(q[0])),
|
| 457 |
+
Px,
|
| 458 |
+
Py])
|
| 459 |
+
fr2_expected = Matrix([
|
| 460 |
+
-R*g*m*sin(q[1]),
|
| 461 |
+
0,
|
| 462 |
+
0])
|
| 463 |
+
assert (trigsimp(fr1.expand()) == trigsimp(fr1_expected.expand()))
|
| 464 |
+
assert (trigsimp(fr2.expand()) == trigsimp(fr2_expected.expand()))
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane3.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy.core.numbers import pi
|
| 2 |
+
from sympy.core.symbol import symbols
|
| 3 |
+
from sympy.functions.elementary.miscellaneous import sqrt
|
| 4 |
+
from sympy.functions.elementary.trigonometric import acos, sin, cos
|
| 5 |
+
from sympy.matrices.dense import Matrix
|
| 6 |
+
from sympy.physics.mechanics import (ReferenceFrame, dynamicsymbols,
|
| 7 |
+
KanesMethod, inertia, Point, RigidBody,
|
| 8 |
+
dot)
|
| 9 |
+
from sympy.testing.pytest import slow
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@slow
|
| 13 |
+
def test_bicycle():
|
| 14 |
+
# Code to get equations of motion for a bicycle modeled as in:
|
| 15 |
+
# J.P Meijaard, Jim M Papadopoulos, Andy Ruina and A.L Schwab. Linearized
|
| 16 |
+
# dynamics equations for the balance and steer of a bicycle: a benchmark
|
| 17 |
+
# and review. Proceedings of The Royal Society (2007) 463, 1955-1982
|
| 18 |
+
# doi: 10.1098/rspa.2007.1857
|
| 19 |
+
|
| 20 |
+
# Note that this code has been crudely ported from Autolev, which is the
|
| 21 |
+
# reason for some of the unusual naming conventions. It was purposefully as
|
| 22 |
+
# similar as possible in order to aide debugging.
|
| 23 |
+
|
| 24 |
+
# Declare Coordinates & Speeds
|
| 25 |
+
# Simple definitions for qdots - qd = u
|
| 26 |
+
# Speeds are:
|
| 27 |
+
# - u1: yaw frame ang. rate
|
| 28 |
+
# - u2: roll frame ang. rate
|
| 29 |
+
# - u3: rear wheel frame ang. rate (spinning motion)
|
| 30 |
+
# - u4: frame ang. rate (pitching motion)
|
| 31 |
+
# - u5: steering frame ang. rate
|
| 32 |
+
# - u6: front wheel ang. rate (spinning motion)
|
| 33 |
+
# Wheel positions are ignorable coordinates, so they are not introduced.
|
| 34 |
+
q1, q2, q4, q5 = dynamicsymbols('q1 q2 q4 q5')
|
| 35 |
+
q1d, q2d, q4d, q5d = dynamicsymbols('q1 q2 q4 q5', 1)
|
| 36 |
+
u1, u2, u3, u4, u5, u6 = dynamicsymbols('u1 u2 u3 u4 u5 u6')
|
| 37 |
+
u1d, u2d, u3d, u4d, u5d, u6d = dynamicsymbols('u1 u2 u3 u4 u5 u6', 1)
|
| 38 |
+
|
| 39 |
+
# Declare System's Parameters
|
| 40 |
+
WFrad, WRrad, htangle, forkoffset = symbols('WFrad WRrad htangle forkoffset')
|
| 41 |
+
forklength, framelength, forkcg1 = symbols('forklength framelength forkcg1')
|
| 42 |
+
forkcg3, framecg1, framecg3, Iwr11 = symbols('forkcg3 framecg1 framecg3 Iwr11')
|
| 43 |
+
Iwr22, Iwf11, Iwf22, Iframe11 = symbols('Iwr22 Iwf11 Iwf22 Iframe11')
|
| 44 |
+
Iframe22, Iframe33, Iframe31, Ifork11 = symbols('Iframe22 Iframe33 Iframe31 Ifork11')
|
| 45 |
+
Ifork22, Ifork33, Ifork31, g = symbols('Ifork22 Ifork33 Ifork31 g')
|
| 46 |
+
mframe, mfork, mwf, mwr = symbols('mframe mfork mwf mwr')
|
| 47 |
+
|
| 48 |
+
# Set up reference frames for the system
|
| 49 |
+
# N - inertial
|
| 50 |
+
# Y - yaw
|
| 51 |
+
# R - roll
|
| 52 |
+
# WR - rear wheel, rotation angle is ignorable coordinate so not oriented
|
| 53 |
+
# Frame - bicycle frame
|
| 54 |
+
# TempFrame - statically rotated frame for easier reference inertia definition
|
| 55 |
+
# Fork - bicycle fork
|
| 56 |
+
# TempFork - statically rotated frame for easier reference inertia definition
|
| 57 |
+
# WF - front wheel, again posses a ignorable coordinate
|
| 58 |
+
N = ReferenceFrame('N')
|
| 59 |
+
Y = N.orientnew('Y', 'Axis', [q1, N.z])
|
| 60 |
+
R = Y.orientnew('R', 'Axis', [q2, Y.x])
|
| 61 |
+
Frame = R.orientnew('Frame', 'Axis', [q4 + htangle, R.y])
|
| 62 |
+
WR = ReferenceFrame('WR')
|
| 63 |
+
TempFrame = Frame.orientnew('TempFrame', 'Axis', [-htangle, Frame.y])
|
| 64 |
+
Fork = Frame.orientnew('Fork', 'Axis', [q5, Frame.x])
|
| 65 |
+
TempFork = Fork.orientnew('TempFork', 'Axis', [-htangle, Fork.y])
|
| 66 |
+
WF = ReferenceFrame('WF')
|
| 67 |
+
|
| 68 |
+
# Kinematics of the Bicycle First block of code is forming the positions of
|
| 69 |
+
# the relevant points
|
| 70 |
+
# rear wheel contact -> rear wheel mass center -> frame mass center +
|
| 71 |
+
# frame/fork connection -> fork mass center + front wheel mass center ->
|
| 72 |
+
# front wheel contact point
|
| 73 |
+
WR_cont = Point('WR_cont')
|
| 74 |
+
WR_mc = WR_cont.locatenew('WR_mc', WRrad * R.z)
|
| 75 |
+
Steer = WR_mc.locatenew('Steer', framelength * Frame.z)
|
| 76 |
+
Frame_mc = WR_mc.locatenew('Frame_mc', - framecg1 * Frame.x
|
| 77 |
+
+ framecg3 * Frame.z)
|
| 78 |
+
Fork_mc = Steer.locatenew('Fork_mc', - forkcg1 * Fork.x
|
| 79 |
+
+ forkcg3 * Fork.z)
|
| 80 |
+
WF_mc = Steer.locatenew('WF_mc', forklength * Fork.x + forkoffset * Fork.z)
|
| 81 |
+
WF_cont = WF_mc.locatenew('WF_cont', WFrad * (dot(Fork.y, Y.z) * Fork.y -
|
| 82 |
+
Y.z).normalize())
|
| 83 |
+
|
| 84 |
+
# Set the angular velocity of each frame.
|
| 85 |
+
# Angular accelerations end up being calculated automatically by
|
| 86 |
+
# differentiating the angular velocities when first needed.
|
| 87 |
+
# u1 is yaw rate
|
| 88 |
+
# u2 is roll rate
|
| 89 |
+
# u3 is rear wheel rate
|
| 90 |
+
# u4 is frame pitch rate
|
| 91 |
+
# u5 is fork steer rate
|
| 92 |
+
# u6 is front wheel rate
|
| 93 |
+
Y.set_ang_vel(N, u1 * Y.z)
|
| 94 |
+
R.set_ang_vel(Y, u2 * R.x)
|
| 95 |
+
WR.set_ang_vel(Frame, u3 * Frame.y)
|
| 96 |
+
Frame.set_ang_vel(R, u4 * Frame.y)
|
| 97 |
+
Fork.set_ang_vel(Frame, u5 * Fork.x)
|
| 98 |
+
WF.set_ang_vel(Fork, u6 * Fork.y)
|
| 99 |
+
|
| 100 |
+
# Form the velocities of the previously defined points, using the 2 - point
|
| 101 |
+
# theorem (written out by hand here). Accelerations again are calculated
|
| 102 |
+
# automatically when first needed.
|
| 103 |
+
WR_cont.set_vel(N, 0)
|
| 104 |
+
WR_mc.v2pt_theory(WR_cont, N, WR)
|
| 105 |
+
Steer.v2pt_theory(WR_mc, N, Frame)
|
| 106 |
+
Frame_mc.v2pt_theory(WR_mc, N, Frame)
|
| 107 |
+
Fork_mc.v2pt_theory(Steer, N, Fork)
|
| 108 |
+
WF_mc.v2pt_theory(Steer, N, Fork)
|
| 109 |
+
WF_cont.v2pt_theory(WF_mc, N, WF)
|
| 110 |
+
|
| 111 |
+
# Sets the inertias of each body. Uses the inertia frame to construct the
|
| 112 |
+
# inertia dyadics. Wheel inertias are only defined by principle moments of
|
| 113 |
+
# inertia, and are in fact constant in the frame and fork reference frames;
|
| 114 |
+
# it is for this reason that the orientations of the wheels does not need
|
| 115 |
+
# to be defined. The frame and fork inertias are defined in the 'Temp'
|
| 116 |
+
# frames which are fixed to the appropriate body frames; this is to allow
|
| 117 |
+
# easier input of the reference values of the benchmark paper. Note that
|
| 118 |
+
# due to slightly different orientations, the products of inertia need to
|
| 119 |
+
# have their signs flipped; this is done later when entering the numerical
|
| 120 |
+
# value.
|
| 121 |
+
|
| 122 |
+
Frame_I = (inertia(TempFrame, Iframe11, Iframe22, Iframe33, 0, 0, Iframe31), Frame_mc)
|
| 123 |
+
Fork_I = (inertia(TempFork, Ifork11, Ifork22, Ifork33, 0, 0, Ifork31), Fork_mc)
|
| 124 |
+
WR_I = (inertia(Frame, Iwr11, Iwr22, Iwr11), WR_mc)
|
| 125 |
+
WF_I = (inertia(Fork, Iwf11, Iwf22, Iwf11), WF_mc)
|
| 126 |
+
|
| 127 |
+
# Declaration of the RigidBody containers. ::
|
| 128 |
+
|
| 129 |
+
BodyFrame = RigidBody('BodyFrame', Frame_mc, Frame, mframe, Frame_I)
|
| 130 |
+
BodyFork = RigidBody('BodyFork', Fork_mc, Fork, mfork, Fork_I)
|
| 131 |
+
BodyWR = RigidBody('BodyWR', WR_mc, WR, mwr, WR_I)
|
| 132 |
+
BodyWF = RigidBody('BodyWF', WF_mc, WF, mwf, WF_I)
|
| 133 |
+
|
| 134 |
+
# The kinematic differential equations; they are defined quite simply. Each
|
| 135 |
+
# entry in this list is equal to zero.
|
| 136 |
+
kd = [q1d - u1, q2d - u2, q4d - u4, q5d - u5]
|
| 137 |
+
|
| 138 |
+
# The nonholonomic constraints are the velocity of the front wheel contact
|
| 139 |
+
# point dotted into the X, Y, and Z directions; the yaw frame is used as it
|
| 140 |
+
# is "closer" to the front wheel (1 less DCM connecting them). These
|
| 141 |
+
# constraints force the velocity of the front wheel contact point to be 0
|
| 142 |
+
# in the inertial frame; the X and Y direction constraints enforce a
|
| 143 |
+
# "no-slip" condition, and the Z direction constraint forces the front
|
| 144 |
+
# wheel contact point to not move away from the ground frame, essentially
|
| 145 |
+
# replicating the holonomic constraint which does not allow the frame pitch
|
| 146 |
+
# to change in an invalid fashion.
|
| 147 |
+
|
| 148 |
+
conlist_speed = [WF_cont.vel(N) & Y.x, WF_cont.vel(N) & Y.y, WF_cont.vel(N) & Y.z]
|
| 149 |
+
|
| 150 |
+
# The holonomic constraint is that the position from the rear wheel contact
|
| 151 |
+
# point to the front wheel contact point when dotted into the
|
| 152 |
+
# normal-to-ground plane direction must be zero; effectively that the front
|
| 153 |
+
# and rear wheel contact points are always touching the ground plane. This
|
| 154 |
+
# is actually not part of the dynamic equations, but instead is necessary
|
| 155 |
+
# for the lineraization process.
|
| 156 |
+
|
| 157 |
+
conlist_coord = [WF_cont.pos_from(WR_cont) & Y.z]
|
| 158 |
+
|
| 159 |
+
# The force list; each body has the appropriate gravitational force applied
|
| 160 |
+
# at its mass center.
|
| 161 |
+
FL = [(Frame_mc, -mframe * g * Y.z),
|
| 162 |
+
(Fork_mc, -mfork * g * Y.z),
|
| 163 |
+
(WF_mc, -mwf * g * Y.z),
|
| 164 |
+
(WR_mc, -mwr * g * Y.z)]
|
| 165 |
+
BL = [BodyFrame, BodyFork, BodyWR, BodyWF]
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
# The N frame is the inertial frame, coordinates are supplied in the order
|
| 169 |
+
# of independent, dependent coordinates, as are the speeds. The kinematic
|
| 170 |
+
# differential equation are also entered here. Here the dependent speeds
|
| 171 |
+
# are specified, in the same order they were provided in earlier, along
|
| 172 |
+
# with the non-holonomic constraints. The dependent coordinate is also
|
| 173 |
+
# provided, with the holonomic constraint. Again, this is only provided
|
| 174 |
+
# for the linearization process.
|
| 175 |
+
|
| 176 |
+
KM = KanesMethod(N, q_ind=[q1, q2, q5],
|
| 177 |
+
q_dependent=[q4], configuration_constraints=conlist_coord,
|
| 178 |
+
u_ind=[u2, u3, u5],
|
| 179 |
+
u_dependent=[u1, u4, u6], velocity_constraints=conlist_speed,
|
| 180 |
+
kd_eqs=kd,
|
| 181 |
+
constraint_solver="CRAMER")
|
| 182 |
+
(fr, frstar) = KM.kanes_equations(BL, FL)
|
| 183 |
+
|
| 184 |
+
# This is the start of entering in the numerical values from the benchmark
|
| 185 |
+
# paper to validate the eigen values of the linearized equations from this
|
| 186 |
+
# model to the reference eigen values. Look at the aforementioned paper for
|
| 187 |
+
# more information. Some of these are intermediate values, used to
|
| 188 |
+
# transform values from the paper into the coordinate systems used in this
|
| 189 |
+
# model.
|
| 190 |
+
PaperRadRear = 0.3
|
| 191 |
+
PaperRadFront = 0.35
|
| 192 |
+
HTA = (pi / 2 - pi / 10).evalf()
|
| 193 |
+
TrailPaper = 0.08
|
| 194 |
+
rake = (-(TrailPaper*sin(HTA)-(PaperRadFront*cos(HTA)))).evalf()
|
| 195 |
+
PaperWb = 1.02
|
| 196 |
+
PaperFrameCgX = 0.3
|
| 197 |
+
PaperFrameCgZ = 0.9
|
| 198 |
+
PaperForkCgX = 0.9
|
| 199 |
+
PaperForkCgZ = 0.7
|
| 200 |
+
FrameLength = (PaperWb*sin(HTA)-(rake-(PaperRadFront-PaperRadRear)*cos(HTA))).evalf()
|
| 201 |
+
FrameCGNorm = ((PaperFrameCgZ - PaperRadRear-(PaperFrameCgX/sin(HTA))*cos(HTA))*sin(HTA)).evalf()
|
| 202 |
+
FrameCGPar = (PaperFrameCgX / sin(HTA) + (PaperFrameCgZ - PaperRadRear - PaperFrameCgX / sin(HTA) * cos(HTA)) * cos(HTA)).evalf()
|
| 203 |
+
tempa = (PaperForkCgZ - PaperRadFront)
|
| 204 |
+
tempb = (PaperWb-PaperForkCgX)
|
| 205 |
+
tempc = (sqrt(tempa**2+tempb**2)).evalf()
|
| 206 |
+
PaperForkL = (PaperWb*cos(HTA)-(PaperRadFront-PaperRadRear)*sin(HTA)).evalf()
|
| 207 |
+
ForkCGNorm = (rake+(tempc * sin(pi/2-HTA-acos(tempa/tempc)))).evalf()
|
| 208 |
+
ForkCGPar = (tempc * cos((pi/2-HTA)-acos(tempa/tempc))-PaperForkL).evalf()
|
| 209 |
+
|
| 210 |
+
# Here is the final assembly of the numerical values. The symbol 'v' is the
|
| 211 |
+
# forward speed of the bicycle (a concept which only makes sense in the
|
| 212 |
+
# upright, static equilibrium case?). These are in a dictionary which will
|
| 213 |
+
# later be substituted in. Again the sign on the *product* of inertia
|
| 214 |
+
# values is flipped here, due to different orientations of coordinate
|
| 215 |
+
# systems.
|
| 216 |
+
v = symbols('v')
|
| 217 |
+
val_dict = {WFrad: PaperRadFront,
|
| 218 |
+
WRrad: PaperRadRear,
|
| 219 |
+
htangle: HTA,
|
| 220 |
+
forkoffset: rake,
|
| 221 |
+
forklength: PaperForkL,
|
| 222 |
+
framelength: FrameLength,
|
| 223 |
+
forkcg1: ForkCGPar,
|
| 224 |
+
forkcg3: ForkCGNorm,
|
| 225 |
+
framecg1: FrameCGNorm,
|
| 226 |
+
framecg3: FrameCGPar,
|
| 227 |
+
Iwr11: 0.0603,
|
| 228 |
+
Iwr22: 0.12,
|
| 229 |
+
Iwf11: 0.1405,
|
| 230 |
+
Iwf22: 0.28,
|
| 231 |
+
Ifork11: 0.05892,
|
| 232 |
+
Ifork22: 0.06,
|
| 233 |
+
Ifork33: 0.00708,
|
| 234 |
+
Ifork31: 0.00756,
|
| 235 |
+
Iframe11: 9.2,
|
| 236 |
+
Iframe22: 11,
|
| 237 |
+
Iframe33: 2.8,
|
| 238 |
+
Iframe31: -2.4,
|
| 239 |
+
mfork: 4,
|
| 240 |
+
mframe: 85,
|
| 241 |
+
mwf: 3,
|
| 242 |
+
mwr: 2,
|
| 243 |
+
g: 9.81,
|
| 244 |
+
q1: 0,
|
| 245 |
+
q2: 0,
|
| 246 |
+
q4: 0,
|
| 247 |
+
q5: 0,
|
| 248 |
+
u1: 0,
|
| 249 |
+
u2: 0,
|
| 250 |
+
u3: v / PaperRadRear,
|
| 251 |
+
u4: 0,
|
| 252 |
+
u5: 0,
|
| 253 |
+
u6: v / PaperRadFront}
|
| 254 |
+
|
| 255 |
+
# Linearizes the forcing vector; the equations are set up as MM udot =
|
| 256 |
+
# forcing, where MM is the mass matrix, udot is the vector representing the
|
| 257 |
+
# time derivatives of the generalized speeds, and forcing is a vector which
|
| 258 |
+
# contains both external forcing terms and internal forcing terms, such as
|
| 259 |
+
# centripital or coriolis forces. This actually returns a matrix with as
|
| 260 |
+
# many rows as *total* coordinates and speeds, but only as many columns as
|
| 261 |
+
# independent coordinates and speeds.
|
| 262 |
+
|
| 263 |
+
A, B, _ = KM.linearize(
|
| 264 |
+
A_and_B=True,
|
| 265 |
+
op_point={
|
| 266 |
+
# Operating points for the accelerations are required for the
|
| 267 |
+
# linearizer to eliminate u' terms showing up in the coefficient
|
| 268 |
+
# matrices.
|
| 269 |
+
u1.diff(): 0,
|
| 270 |
+
u2.diff(): 0,
|
| 271 |
+
u3.diff(): 0,
|
| 272 |
+
u4.diff(): 0,
|
| 273 |
+
u5.diff(): 0,
|
| 274 |
+
u6.diff(): 0,
|
| 275 |
+
u1: 0,
|
| 276 |
+
u2: 0,
|
| 277 |
+
u3: v / PaperRadRear,
|
| 278 |
+
u4: 0,
|
| 279 |
+
u5: 0,
|
| 280 |
+
u6: v / PaperRadFront,
|
| 281 |
+
q1: 0,
|
| 282 |
+
q2: 0,
|
| 283 |
+
q4: 0,
|
| 284 |
+
q5: 0,
|
| 285 |
+
},
|
| 286 |
+
linear_solver="CRAMER",
|
| 287 |
+
)
|
| 288 |
+
# As mentioned above, the size of the linearized forcing terms is expanded
|
| 289 |
+
# to include both q's and u's, so the mass matrix must have this done as
|
| 290 |
+
# well. This will likely be changed to be part of the linearized process,
|
| 291 |
+
# for future reference.
|
| 292 |
+
A_s = A.xreplace(val_dict)
|
| 293 |
+
B_s = B.xreplace(val_dict)
|
| 294 |
+
|
| 295 |
+
A_s = A_s.evalf()
|
| 296 |
+
B_s = B_s.evalf()
|
| 297 |
+
|
| 298 |
+
# Finally, we construct an "A" matrix for the form xdot = A x (x being the
|
| 299 |
+
# state vector, although in this case, the sizes are a little off). The
|
| 300 |
+
# following line extracts only the minimum entries required for eigenvalue
|
| 301 |
+
# analysis, which correspond to rows and columns for lean, steer, lean
|
| 302 |
+
# rate, and steer rate.
|
| 303 |
+
A = A_s.extract([1, 2, 3, 5], [1, 2, 3, 5])
|
| 304 |
+
|
| 305 |
+
# Precomputed for comparison
|
| 306 |
+
Res = Matrix([[ 0, 0, 1.0, 0],
|
| 307 |
+
[ 0, 0, 0, 1.0],
|
| 308 |
+
[9.48977444677355, -0.891197738059089*v**2 - 0.571523173729245, -0.105522449805691*v, -0.330515398992311*v],
|
| 309 |
+
[11.7194768719633, -1.97171508499972*v**2 + 30.9087533932407, 3.67680523332152*v, -3.08486552743311*v]])
|
| 310 |
+
|
| 311 |
+
# Actual eigenvalue comparison
|
| 312 |
+
eps = 1.e-12
|
| 313 |
+
for i in range(6):
|
| 314 |
+
error = Res.subs(v, i) - A.subs(v, i)
|
| 315 |
+
assert all(abs(x) < eps for x in error)
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane4.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import (cos, sin, Matrix, symbols)
|
| 2 |
+
from sympy.physics.mechanics import (dynamicsymbols, ReferenceFrame, Point,
|
| 3 |
+
KanesMethod, Particle)
|
| 4 |
+
|
| 5 |
+
def test_replace_qdots_in_force():
|
| 6 |
+
# Test PR 16700 "Replaces qdots with us in force-list in kanes.py"
|
| 7 |
+
# The new functionality allows one to specify forces in qdots which will
|
| 8 |
+
# automatically be replaced with u:s which are defined by the kde supplied
|
| 9 |
+
# to KanesMethod. The test case is the double pendulum with interacting
|
| 10 |
+
# forces in the example of chapter 4.7 "CONTRIBUTING INTERACTION FORCES"
|
| 11 |
+
# in Ref. [1]. Reference list at end test function.
|
| 12 |
+
|
| 13 |
+
q1, q2 = dynamicsymbols('q1, q2')
|
| 14 |
+
qd1, qd2 = dynamicsymbols('q1, q2', level=1)
|
| 15 |
+
u1, u2 = dynamicsymbols('u1, u2')
|
| 16 |
+
|
| 17 |
+
l, m = symbols('l, m')
|
| 18 |
+
|
| 19 |
+
N = ReferenceFrame('N') # Inertial frame
|
| 20 |
+
A = N.orientnew('A', 'Axis', (q1, N.z)) # Rod A frame
|
| 21 |
+
B = A.orientnew('B', 'Axis', (q2, N.z)) # Rod B frame
|
| 22 |
+
|
| 23 |
+
O = Point('O') # Origo
|
| 24 |
+
O.set_vel(N, 0)
|
| 25 |
+
|
| 26 |
+
P = O.locatenew('P', ( l * A.x )) # Point @ end of rod A
|
| 27 |
+
P.v2pt_theory(O, N, A)
|
| 28 |
+
|
| 29 |
+
Q = P.locatenew('Q', ( l * B.x )) # Point @ end of rod B
|
| 30 |
+
Q.v2pt_theory(P, N, B)
|
| 31 |
+
|
| 32 |
+
Ap = Particle('Ap', P, m)
|
| 33 |
+
Bp = Particle('Bp', Q, m)
|
| 34 |
+
|
| 35 |
+
# The forces are specified below. sigma is the torsional spring stiffness
|
| 36 |
+
# and delta is the viscous damping coefficient acting between the two
|
| 37 |
+
# bodies. Here, we specify the viscous damper as function of qdots prior
|
| 38 |
+
# forming the kde. In more complex systems it not might be obvious which
|
| 39 |
+
# kde is most efficient, why it is convenient to specify viscous forces in
|
| 40 |
+
# qdots independently of the kde.
|
| 41 |
+
sig, delta = symbols('sigma, delta')
|
| 42 |
+
Ta = (sig * q2 + delta * qd2) * N.z
|
| 43 |
+
forces = [(A, Ta), (B, -Ta)]
|
| 44 |
+
|
| 45 |
+
# Try different kdes.
|
| 46 |
+
kde1 = [u1 - qd1, u2 - qd2]
|
| 47 |
+
kde2 = [u1 - qd1, u2 - (qd1 + qd2)]
|
| 48 |
+
|
| 49 |
+
KM1 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde1)
|
| 50 |
+
fr1, fstar1 = KM1.kanes_equations([Ap, Bp], forces)
|
| 51 |
+
|
| 52 |
+
KM2 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde2)
|
| 53 |
+
fr2, fstar2 = KM2.kanes_equations([Ap, Bp], forces)
|
| 54 |
+
|
| 55 |
+
# Check EOM for KM2:
|
| 56 |
+
# Mass and force matrix from p.6 in Ref. [2] with added forces from
|
| 57 |
+
# example of chapter 4.7 in [1] and without gravity.
|
| 58 |
+
forcing_matrix_expected = Matrix( [ [ m * l**2 * sin(q2) * u2**2 + sig * q2
|
| 59 |
+
+ delta * (u2 - u1)],
|
| 60 |
+
[ m * l**2 * sin(q2) * -u1**2 - sig * q2
|
| 61 |
+
- delta * (u2 - u1)] ] )
|
| 62 |
+
mass_matrix_expected = Matrix( [ [ 2 * m * l**2, m * l**2 * cos(q2) ],
|
| 63 |
+
[ m * l**2 * cos(q2), m * l**2 ] ] )
|
| 64 |
+
|
| 65 |
+
assert (KM2.mass_matrix.expand() == mass_matrix_expected.expand())
|
| 66 |
+
assert (KM2.forcing.expand() == forcing_matrix_expected.expand())
|
| 67 |
+
|
| 68 |
+
# Check fr1 with reference fr_expected from [1] with u:s instead of qdots.
|
| 69 |
+
fr1_expected = Matrix([ 0, -(sig*q2 + delta * u2) ])
|
| 70 |
+
assert fr1.expand() == fr1_expected.expand()
|
| 71 |
+
|
| 72 |
+
# Check fr2
|
| 73 |
+
fr2_expected = Matrix([sig * q2 + delta * (u2 - u1),
|
| 74 |
+
- sig * q2 - delta * (u2 - u1)])
|
| 75 |
+
assert fr2.expand() == fr2_expected.expand()
|
| 76 |
+
|
| 77 |
+
# Specifying forces in u:s should stay the same:
|
| 78 |
+
Ta = (sig * q2 + delta * u2) * N.z
|
| 79 |
+
forces = [(A, Ta), (B, -Ta)]
|
| 80 |
+
KM1 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde1)
|
| 81 |
+
fr1, fstar1 = KM1.kanes_equations([Ap, Bp], forces)
|
| 82 |
+
|
| 83 |
+
assert fr1.expand() == fr1_expected.expand()
|
| 84 |
+
|
| 85 |
+
Ta = (sig * q2 + delta * (u2-u1)) * N.z
|
| 86 |
+
forces = [(A, Ta), (B, -Ta)]
|
| 87 |
+
KM2 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde2)
|
| 88 |
+
fr2, fstar2 = KM2.kanes_equations([Ap, Bp], forces)
|
| 89 |
+
|
| 90 |
+
assert fr2.expand() == fr2_expected.expand()
|
| 91 |
+
|
| 92 |
+
# Test if we have a qubic qdot force:
|
| 93 |
+
Ta = (sig * q2 + delta * qd2**3) * N.z
|
| 94 |
+
forces = [(A, Ta), (B, -Ta)]
|
| 95 |
+
|
| 96 |
+
KM1 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde1)
|
| 97 |
+
fr1, fstar1 = KM1.kanes_equations([Ap, Bp], forces)
|
| 98 |
+
|
| 99 |
+
fr1_cubic_expected = Matrix([ 0, -(sig*q2 + delta * u2**3) ])
|
| 100 |
+
|
| 101 |
+
assert fr1.expand() == fr1_cubic_expected.expand()
|
| 102 |
+
|
| 103 |
+
KM2 = KanesMethod(N, [q1, q2], [u1, u2], kd_eqs=kde2)
|
| 104 |
+
fr2, fstar2 = KM2.kanes_equations([Ap, Bp], forces)
|
| 105 |
+
|
| 106 |
+
fr2_cubic_expected = Matrix([sig * q2 + delta * (u2 - u1)**3,
|
| 107 |
+
- sig * q2 - delta * (u2 - u1)**3])
|
| 108 |
+
|
| 109 |
+
assert fr2.expand() == fr2_cubic_expected.expand()
|
| 110 |
+
|
| 111 |
+
# References:
|
| 112 |
+
# [1] T.R. Kane, D. a Levinson, Dynamics Theory and Applications, 2005.
|
| 113 |
+
# [2] Arun K Banerjee, Flexible Multibody Dynamics:Efficient Formulations
|
| 114 |
+
# and Applications, John Wiley and Sons, Ltd, 2016.
|
| 115 |
+
# doi:http://dx.doi.org/10.1002/9781119015635.
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_kane5.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import (zeros, Matrix, symbols, lambdify, sqrt, pi,
|
| 2 |
+
simplify)
|
| 3 |
+
from sympy.physics.mechanics import (dynamicsymbols, cross, inertia, RigidBody,
|
| 4 |
+
ReferenceFrame, KanesMethod)
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def _create_rolling_disc():
|
| 8 |
+
# Define symbols and coordinates
|
| 9 |
+
t = dynamicsymbols._t
|
| 10 |
+
q1, q2, q3, q4, q5, u1, u2, u3, u4, u5 = dynamicsymbols('q1:6 u1:6')
|
| 11 |
+
g, r, m = symbols('g r m')
|
| 12 |
+
# Define bodies and frames
|
| 13 |
+
ground = RigidBody('ground')
|
| 14 |
+
disc = RigidBody('disk', mass=m)
|
| 15 |
+
disc.inertia = (m * r ** 2 / 4 * inertia(disc.frame, 1, 2, 1),
|
| 16 |
+
disc.masscenter)
|
| 17 |
+
ground.masscenter.set_vel(ground.frame, 0)
|
| 18 |
+
disc.masscenter.set_vel(disc.frame, 0)
|
| 19 |
+
int_frame = ReferenceFrame('int_frame')
|
| 20 |
+
# Orient frames
|
| 21 |
+
int_frame.orient_body_fixed(ground.frame, (q1, q2, 0), 'zxy')
|
| 22 |
+
disc.frame.orient_axis(int_frame, int_frame.y, q3)
|
| 23 |
+
g_w_d = disc.frame.ang_vel_in(ground.frame)
|
| 24 |
+
disc.frame.set_ang_vel(ground.frame,
|
| 25 |
+
u1 * disc.x + u2 * disc.y + u3 * disc.z)
|
| 26 |
+
# Define points
|
| 27 |
+
cp = ground.masscenter.locatenew('contact_point',
|
| 28 |
+
q4 * ground.x + q5 * ground.y)
|
| 29 |
+
cp.set_vel(ground.frame, u4 * ground.x + u5 * ground.y)
|
| 30 |
+
disc.masscenter.set_pos(cp, r * int_frame.z)
|
| 31 |
+
disc.masscenter.set_vel(ground.frame, cross(
|
| 32 |
+
disc.frame.ang_vel_in(ground.frame), disc.masscenter.pos_from(cp)))
|
| 33 |
+
# Define kinematic differential equations
|
| 34 |
+
kdes = [g_w_d.dot(disc.x) - u1, g_w_d.dot(disc.y) - u2,
|
| 35 |
+
g_w_d.dot(disc.z) - u3, q4.diff(t) - u4, q5.diff(t) - u5]
|
| 36 |
+
# Define nonholonomic constraints
|
| 37 |
+
v0 = cp.vel(ground.frame) + cross(
|
| 38 |
+
disc.frame.ang_vel_in(int_frame), cp.pos_from(disc.masscenter))
|
| 39 |
+
fnh = [v0.dot(ground.x), v0.dot(ground.y)]
|
| 40 |
+
# Define loads
|
| 41 |
+
loads = [(disc.masscenter, -disc.mass * g * ground.z)]
|
| 42 |
+
bodies = [disc]
|
| 43 |
+
return {
|
| 44 |
+
'frame': ground.frame,
|
| 45 |
+
'q_ind': [q1, q2, q3, q4, q5],
|
| 46 |
+
'u_ind': [u1, u2, u3],
|
| 47 |
+
'u_dep': [u4, u5],
|
| 48 |
+
'kdes': kdes,
|
| 49 |
+
'fnh': fnh,
|
| 50 |
+
'bodies': bodies,
|
| 51 |
+
'loads': loads
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _verify_rolling_disc_numerically(kane, all_zero=False):
|
| 56 |
+
q, u, p = dynamicsymbols('q1:6'), dynamicsymbols('u1:6'), symbols('g r m')
|
| 57 |
+
eval_sys = lambdify((q, u, p), (kane.mass_matrix_full, kane.forcing_full),
|
| 58 |
+
cse=True)
|
| 59 |
+
solve_sys = lambda q, u, p: Matrix.LUsolve(
|
| 60 |
+
*(Matrix(mat) for mat in eval_sys(q, u, p)))
|
| 61 |
+
solve_u_dep = lambdify((q, u[:3], p), kane._Ars * Matrix(u[:3]), cse=True)
|
| 62 |
+
eps = 1e-10
|
| 63 |
+
p_vals = (9.81, 0.26, 3.43)
|
| 64 |
+
# First numeric test
|
| 65 |
+
q_vals = (0.3, 0.1, 1.97, -0.35, 2.27)
|
| 66 |
+
u_vals = [-0.2, 1.3, 0.15]
|
| 67 |
+
u_vals.extend(solve_u_dep(q_vals, u_vals, p_vals)[:2, 0])
|
| 68 |
+
expected = Matrix([
|
| 69 |
+
0.126603940595934, 0.215942571601660, 1.28736069604936,
|
| 70 |
+
0.319764288376543, 0.0989146857254898, -0.925848952664489,
|
| 71 |
+
-0.0181350656532944, 2.91695398184589, -0.00992793421754526,
|
| 72 |
+
0.0412861634829171])
|
| 73 |
+
assert all(abs(x) < eps for x in
|
| 74 |
+
(solve_sys(q_vals, u_vals, p_vals) - expected))
|
| 75 |
+
# Second numeric test
|
| 76 |
+
q_vals = (3.97, -0.28, 8.2, -0.35, 2.27)
|
| 77 |
+
u_vals = [-0.25, -2.2, 0.62]
|
| 78 |
+
u_vals.extend(solve_u_dep(q_vals, u_vals, p_vals)[:2, 0])
|
| 79 |
+
expected = Matrix([
|
| 80 |
+
0.0259159090798597, 0.668041660387416, -2.19283799213811,
|
| 81 |
+
0.385441810852219, 0.420109283790573, 1.45030568179066,
|
| 82 |
+
-0.0110924422400793, -8.35617840186040, -0.154098542632173,
|
| 83 |
+
-0.146102664410010])
|
| 84 |
+
assert all(abs(x) < eps for x in
|
| 85 |
+
(solve_sys(q_vals, u_vals, p_vals) - expected))
|
| 86 |
+
if all_zero:
|
| 87 |
+
q_vals = (0, 0, 0, 0, 0)
|
| 88 |
+
u_vals = (0, 0, 0, 0, 0)
|
| 89 |
+
assert solve_sys(q_vals, u_vals, p_vals) == zeros(10, 1)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def test_kane_rolling_disc_lu():
|
| 93 |
+
props = _create_rolling_disc()
|
| 94 |
+
kane = KanesMethod(props['frame'], props['q_ind'], props['u_ind'],
|
| 95 |
+
props['kdes'], u_dependent=props['u_dep'],
|
| 96 |
+
velocity_constraints=props['fnh'],
|
| 97 |
+
bodies=props['bodies'], forcelist=props['loads'],
|
| 98 |
+
explicit_kinematics=False, constraint_solver='LU')
|
| 99 |
+
kane.kanes_equations()
|
| 100 |
+
_verify_rolling_disc_numerically(kane)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def test_kane_rolling_disc_kdes_callable():
|
| 104 |
+
props = _create_rolling_disc()
|
| 105 |
+
kane = KanesMethod(
|
| 106 |
+
props['frame'], props['q_ind'], props['u_ind'], props['kdes'],
|
| 107 |
+
u_dependent=props['u_dep'], velocity_constraints=props['fnh'],
|
| 108 |
+
bodies=props['bodies'], forcelist=props['loads'],
|
| 109 |
+
explicit_kinematics=False,
|
| 110 |
+
kd_eqs_solver=lambda A, b: simplify(A.LUsolve(b)))
|
| 111 |
+
q, u, p = dynamicsymbols('q1:6'), dynamicsymbols('u1:6'), symbols('g r m')
|
| 112 |
+
qd = dynamicsymbols('q1:6', 1)
|
| 113 |
+
eval_kdes = lambdify((q, qd, u, p), tuple(kane.kindiffdict().items()))
|
| 114 |
+
eps = 1e-10
|
| 115 |
+
# Test with only zeros. If 'LU' would be used this would result in nan.
|
| 116 |
+
p_vals = (9.81, 0.25, 3.5)
|
| 117 |
+
zero_vals = (0, 0, 0, 0, 0)
|
| 118 |
+
assert all(abs(qdi - fui) < eps for qdi, fui in
|
| 119 |
+
eval_kdes(zero_vals, zero_vals, zero_vals, p_vals))
|
| 120 |
+
# Test with some arbitrary values
|
| 121 |
+
q_vals = tuple(map(float, (pi / 6, pi / 3, pi / 2, 0.42, 0.62)))
|
| 122 |
+
qd_vals = tuple(map(float, (4, 1 / 3, 4 - 2 * sqrt(3),
|
| 123 |
+
0.25 * (2 * sqrt(3) - 3),
|
| 124 |
+
0.25 * (2 - sqrt(3)))))
|
| 125 |
+
u_vals = tuple(map(float, (-2, 4, 1 / 3, 0.25 * (-3 + 2 * sqrt(3)),
|
| 126 |
+
0.25 * (-sqrt(3) + 2))))
|
| 127 |
+
assert all(abs(qdi - fui) < eps for qdi, fui in
|
| 128 |
+
eval_kdes(q_vals, qd_vals, u_vals, p_vals))
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_lagrange.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy.physics.mechanics import (dynamicsymbols, ReferenceFrame, Point,
|
| 2 |
+
RigidBody, LagrangesMethod, Particle,
|
| 3 |
+
inertia, Lagrangian)
|
| 4 |
+
from sympy.core.function import (Derivative, Function)
|
| 5 |
+
from sympy.core.numbers import pi
|
| 6 |
+
from sympy.core.symbol import symbols
|
| 7 |
+
from sympy.functions.elementary.trigonometric import (cos, sin, tan)
|
| 8 |
+
from sympy.matrices.dense import Matrix
|
| 9 |
+
from sympy.simplify.simplify import simplify
|
| 10 |
+
from sympy.testing.pytest import raises
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_invalid_coordinates():
|
| 14 |
+
# Simple pendulum, but use symbol instead of dynamicsymbol
|
| 15 |
+
l, m, g = symbols('l m g')
|
| 16 |
+
q = symbols('q') # Generalized coordinate
|
| 17 |
+
N, O = ReferenceFrame('N'), Point('O')
|
| 18 |
+
O.set_vel(N, 0)
|
| 19 |
+
P = Particle('P', Point('P'), m)
|
| 20 |
+
P.point.set_pos(O, l * (sin(q) * N.x - cos(q) * N.y))
|
| 21 |
+
P.potential_energy = m * g * P.point.pos_from(O).dot(N.y)
|
| 22 |
+
L = Lagrangian(N, P)
|
| 23 |
+
raises(ValueError, lambda: LagrangesMethod(L, [q], bodies=P))
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def test_disc_on_an_incline_plane():
|
| 27 |
+
# Disc rolling on an inclined plane
|
| 28 |
+
# First the generalized coordinates are created. The mass center of the
|
| 29 |
+
# disc is located from top vertex of the inclined plane by the generalized
|
| 30 |
+
# coordinate 'y'. The orientation of the disc is defined by the angle
|
| 31 |
+
# 'theta'. The mass of the disc is 'm' and its radius is 'R'. The length of
|
| 32 |
+
# the inclined path is 'l', the angle of inclination is 'alpha'. 'g' is the
|
| 33 |
+
# gravitational constant.
|
| 34 |
+
y, theta = dynamicsymbols('y theta')
|
| 35 |
+
yd, thetad = dynamicsymbols('y theta', 1)
|
| 36 |
+
m, g, R, l, alpha = symbols('m g R l alpha')
|
| 37 |
+
|
| 38 |
+
# Next, we create the inertial reference frame 'N'. A reference frame 'A'
|
| 39 |
+
# is attached to the inclined plane. Finally a frame is created which is attached to the disk.
|
| 40 |
+
N = ReferenceFrame('N')
|
| 41 |
+
A = N.orientnew('A', 'Axis', [pi/2 - alpha, N.z])
|
| 42 |
+
B = A.orientnew('B', 'Axis', [-theta, A.z])
|
| 43 |
+
|
| 44 |
+
# Creating the disc 'D'; we create the point that represents the mass
|
| 45 |
+
# center of the disc and set its velocity. The inertia dyadic of the disc
|
| 46 |
+
# is created. Finally, we create the disc.
|
| 47 |
+
Do = Point('Do')
|
| 48 |
+
Do.set_vel(N, yd * A.x)
|
| 49 |
+
I = m * R**2/2 * B.z | B.z
|
| 50 |
+
D = RigidBody('D', Do, B, m, (I, Do))
|
| 51 |
+
|
| 52 |
+
# To construct the Lagrangian, 'L', of the disc, we determine its kinetic
|
| 53 |
+
# and potential energies, T and U, respectively. L is defined as the
|
| 54 |
+
# difference between T and U.
|
| 55 |
+
D.potential_energy = m * g * (l - y) * sin(alpha)
|
| 56 |
+
L = Lagrangian(N, D)
|
| 57 |
+
|
| 58 |
+
# We then create the list of generalized coordinates and constraint
|
| 59 |
+
# equations. The constraint arises due to the disc rolling without slip on
|
| 60 |
+
# on the inclined path. We then invoke the 'LagrangesMethod' class and
|
| 61 |
+
# supply it the necessary arguments and generate the equations of motion.
|
| 62 |
+
# The'rhs' method solves for the q_double_dots (i.e. the second derivative
|
| 63 |
+
# with respect to time of the generalized coordinates and the lagrange
|
| 64 |
+
# multipliers.
|
| 65 |
+
q = [y, theta]
|
| 66 |
+
hol_coneqs = [y - R * theta]
|
| 67 |
+
m = LagrangesMethod(L, q, hol_coneqs=hol_coneqs)
|
| 68 |
+
m.form_lagranges_equations()
|
| 69 |
+
rhs = m.rhs()
|
| 70 |
+
rhs.simplify()
|
| 71 |
+
assert rhs[2] == 2*g*sin(alpha)/3
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def test_simp_pen():
|
| 75 |
+
# This tests that the equations generated by LagrangesMethod are identical
|
| 76 |
+
# to those obtained by hand calculations. The system under consideration is
|
| 77 |
+
# the simple pendulum.
|
| 78 |
+
# We begin by creating the generalized coordinates as per the requirements
|
| 79 |
+
# of LagrangesMethod. Also we created the associate symbols
|
| 80 |
+
# that characterize the system: 'm' is the mass of the bob, l is the length
|
| 81 |
+
# of the massless rigid rod connecting the bob to a point O fixed in the
|
| 82 |
+
# inertial frame.
|
| 83 |
+
q, u = dynamicsymbols('q u')
|
| 84 |
+
qd, ud = dynamicsymbols('q u ', 1)
|
| 85 |
+
l, m, g = symbols('l m g')
|
| 86 |
+
|
| 87 |
+
# We then create the inertial frame and a frame attached to the massless
|
| 88 |
+
# string following which we define the inertial angular velocity of the
|
| 89 |
+
# string.
|
| 90 |
+
N = ReferenceFrame('N')
|
| 91 |
+
A = N.orientnew('A', 'Axis', [q, N.z])
|
| 92 |
+
A.set_ang_vel(N, qd * N.z)
|
| 93 |
+
|
| 94 |
+
# Next, we create the point O and fix it in the inertial frame. We then
|
| 95 |
+
# locate the point P to which the bob is attached. Its corresponding
|
| 96 |
+
# velocity is then determined by the 'two point formula'.
|
| 97 |
+
O = Point('O')
|
| 98 |
+
O.set_vel(N, 0)
|
| 99 |
+
P = O.locatenew('P', l * A.x)
|
| 100 |
+
P.v2pt_theory(O, N, A)
|
| 101 |
+
|
| 102 |
+
# The 'Particle' which represents the bob is then created and its
|
| 103 |
+
# Lagrangian generated.
|
| 104 |
+
Pa = Particle('Pa', P, m)
|
| 105 |
+
Pa.potential_energy = - m * g * l * cos(q)
|
| 106 |
+
L = Lagrangian(N, Pa)
|
| 107 |
+
|
| 108 |
+
# The 'LagrangesMethod' class is invoked to obtain equations of motion.
|
| 109 |
+
lm = LagrangesMethod(L, [q])
|
| 110 |
+
lm.form_lagranges_equations()
|
| 111 |
+
RHS = lm.rhs()
|
| 112 |
+
assert RHS[1] == -g*sin(q)/l
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def test_nonminimal_pendulum():
|
| 116 |
+
q1, q2 = dynamicsymbols('q1:3')
|
| 117 |
+
q1d, q2d = dynamicsymbols('q1:3', level=1)
|
| 118 |
+
L, m, t = symbols('L, m, t')
|
| 119 |
+
g = 9.8
|
| 120 |
+
# Compose World Frame
|
| 121 |
+
N = ReferenceFrame('N')
|
| 122 |
+
pN = Point('N*')
|
| 123 |
+
pN.set_vel(N, 0)
|
| 124 |
+
# Create point P, the pendulum mass
|
| 125 |
+
P = pN.locatenew('P1', q1*N.x + q2*N.y)
|
| 126 |
+
P.set_vel(N, P.pos_from(pN).dt(N))
|
| 127 |
+
pP = Particle('pP', P, m)
|
| 128 |
+
# Constraint Equations
|
| 129 |
+
f_c = Matrix([q1**2 + q2**2 - L**2])
|
| 130 |
+
# Calculate the lagrangian, and form the equations of motion
|
| 131 |
+
Lag = Lagrangian(N, pP)
|
| 132 |
+
LM = LagrangesMethod(Lag, [q1, q2], hol_coneqs=f_c,
|
| 133 |
+
forcelist=[(P, m*g*N.x)], frame=N)
|
| 134 |
+
LM.form_lagranges_equations()
|
| 135 |
+
# Check solution
|
| 136 |
+
lam1 = LM.lam_vec[0, 0]
|
| 137 |
+
eom_sol = Matrix([[m*Derivative(q1, t, t) - 9.8*m + 2*lam1*q1],
|
| 138 |
+
[m*Derivative(q2, t, t) + 2*lam1*q2]])
|
| 139 |
+
assert LM.eom == eom_sol
|
| 140 |
+
# Check multiplier solution
|
| 141 |
+
lam_sol = Matrix([(19.6*q1 + 2*q1d**2 + 2*q2d**2)/(4*q1**2/m + 4*q2**2/m)])
|
| 142 |
+
assert simplify(LM.solve_multipliers(sol_type='Matrix')) == simplify(lam_sol)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def test_dub_pen():
|
| 146 |
+
|
| 147 |
+
# The system considered is the double pendulum. Like in the
|
| 148 |
+
# test of the simple pendulum above, we begin by creating the generalized
|
| 149 |
+
# coordinates and the simple generalized speeds and accelerations which
|
| 150 |
+
# will be used later. Following this we create frames and points necessary
|
| 151 |
+
# for the kinematics. The procedure isn't explicitly explained as this is
|
| 152 |
+
# similar to the simple pendulum. Also this is documented on the pydy.org
|
| 153 |
+
# website.
|
| 154 |
+
q1, q2 = dynamicsymbols('q1 q2')
|
| 155 |
+
q1d, q2d = dynamicsymbols('q1 q2', 1)
|
| 156 |
+
q1dd, q2dd = dynamicsymbols('q1 q2', 2)
|
| 157 |
+
u1, u2 = dynamicsymbols('u1 u2')
|
| 158 |
+
u1d, u2d = dynamicsymbols('u1 u2', 1)
|
| 159 |
+
l, m, g = symbols('l m g')
|
| 160 |
+
|
| 161 |
+
N = ReferenceFrame('N')
|
| 162 |
+
A = N.orientnew('A', 'Axis', [q1, N.z])
|
| 163 |
+
B = N.orientnew('B', 'Axis', [q2, N.z])
|
| 164 |
+
|
| 165 |
+
A.set_ang_vel(N, q1d * A.z)
|
| 166 |
+
B.set_ang_vel(N, q2d * A.z)
|
| 167 |
+
|
| 168 |
+
O = Point('O')
|
| 169 |
+
P = O.locatenew('P', l * A.x)
|
| 170 |
+
R = P.locatenew('R', l * B.x)
|
| 171 |
+
|
| 172 |
+
O.set_vel(N, 0)
|
| 173 |
+
P.v2pt_theory(O, N, A)
|
| 174 |
+
R.v2pt_theory(P, N, B)
|
| 175 |
+
|
| 176 |
+
ParP = Particle('ParP', P, m)
|
| 177 |
+
ParR = Particle('ParR', R, m)
|
| 178 |
+
|
| 179 |
+
ParP.potential_energy = - m * g * l * cos(q1)
|
| 180 |
+
ParR.potential_energy = - m * g * l * cos(q1) - m * g * l * cos(q2)
|
| 181 |
+
L = Lagrangian(N, ParP, ParR)
|
| 182 |
+
lm = LagrangesMethod(L, [q1, q2], bodies=[ParP, ParR])
|
| 183 |
+
lm.form_lagranges_equations()
|
| 184 |
+
|
| 185 |
+
assert simplify(l*m*(2*g*sin(q1) + l*sin(q1)*sin(q2)*q2dd
|
| 186 |
+
+ l*sin(q1)*cos(q2)*q2d**2 - l*sin(q2)*cos(q1)*q2d**2
|
| 187 |
+
+ l*cos(q1)*cos(q2)*q2dd + 2*l*q1dd) - lm.eom[0]) == 0
|
| 188 |
+
assert simplify(l*m*(g*sin(q2) + l*sin(q1)*sin(q2)*q1dd
|
| 189 |
+
- l*sin(q1)*cos(q2)*q1d**2 + l*sin(q2)*cos(q1)*q1d**2
|
| 190 |
+
+ l*cos(q1)*cos(q2)*q1dd + l*q2dd) - lm.eom[1]) == 0
|
| 191 |
+
assert lm.bodies == [ParP, ParR]
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def test_rolling_disc():
|
| 195 |
+
# Rolling Disc Example
|
| 196 |
+
# Here the rolling disc is formed from the contact point up, removing the
|
| 197 |
+
# need to introduce generalized speeds. Only 3 configuration and 3
|
| 198 |
+
# speed variables are need to describe this system, along with the
|
| 199 |
+
# disc's mass and radius, and the local gravity.
|
| 200 |
+
q1, q2, q3 = dynamicsymbols('q1 q2 q3')
|
| 201 |
+
q1d, q2d, q3d = dynamicsymbols('q1 q2 q3', 1)
|
| 202 |
+
r, m, g = symbols('r m g')
|
| 203 |
+
|
| 204 |
+
# The kinematics are formed by a series of simple rotations. Each simple
|
| 205 |
+
# rotation creates a new frame, and the next rotation is defined by the new
|
| 206 |
+
# frame's basis vectors. This example uses a 3-1-2 series of rotations, or
|
| 207 |
+
# Z, X, Y series of rotations. Angular velocity for this is defined using
|
| 208 |
+
# the second frame's basis (the lean frame).
|
| 209 |
+
N = ReferenceFrame('N')
|
| 210 |
+
Y = N.orientnew('Y', 'Axis', [q1, N.z])
|
| 211 |
+
L = Y.orientnew('L', 'Axis', [q2, Y.x])
|
| 212 |
+
R = L.orientnew('R', 'Axis', [q3, L.y])
|
| 213 |
+
|
| 214 |
+
# This is the translational kinematics. We create a point with no velocity
|
| 215 |
+
# in N; this is the contact point between the disc and ground. Next we form
|
| 216 |
+
# the position vector from the contact point to the disc's center of mass.
|
| 217 |
+
# Finally we form the velocity and acceleration of the disc.
|
| 218 |
+
C = Point('C')
|
| 219 |
+
C.set_vel(N, 0)
|
| 220 |
+
Dmc = C.locatenew('Dmc', r * L.z)
|
| 221 |
+
Dmc.v2pt_theory(C, N, R)
|
| 222 |
+
|
| 223 |
+
# Forming the inertia dyadic.
|
| 224 |
+
I = inertia(L, m/4 * r**2, m/2 * r**2, m/4 * r**2)
|
| 225 |
+
BodyD = RigidBody('BodyD', Dmc, R, m, (I, Dmc))
|
| 226 |
+
|
| 227 |
+
# Finally we form the equations of motion, using the same steps we did
|
| 228 |
+
# before. Supply the Lagrangian, the generalized speeds.
|
| 229 |
+
BodyD.potential_energy = - m * g * r * cos(q2)
|
| 230 |
+
Lag = Lagrangian(N, BodyD)
|
| 231 |
+
q = [q1, q2, q3]
|
| 232 |
+
q1 = Function('q1')
|
| 233 |
+
q2 = Function('q2')
|
| 234 |
+
q3 = Function('q3')
|
| 235 |
+
l = LagrangesMethod(Lag, q)
|
| 236 |
+
l.form_lagranges_equations()
|
| 237 |
+
RHS = l.rhs()
|
| 238 |
+
RHS.simplify()
|
| 239 |
+
t = symbols('t')
|
| 240 |
+
|
| 241 |
+
assert (l.mass_matrix[3:6] == [0, 5*m*r**2/4, 0])
|
| 242 |
+
assert RHS[4].simplify() == (
|
| 243 |
+
(-8*g*sin(q2(t)) + r*(5*sin(2*q2(t))*Derivative(q1(t), t) +
|
| 244 |
+
12*cos(q2(t))*Derivative(q3(t), t))*Derivative(q1(t), t))/(10*r))
|
| 245 |
+
assert RHS[5] == (-5*cos(q2(t))*Derivative(q1(t), t) + 6*tan(q2(t)
|
| 246 |
+
)*Derivative(q3(t), t) + 4*Derivative(q1(t), t)/cos(q2(t))
|
| 247 |
+
)*Derivative(q2(t), t)
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_lagrange2.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import symbols
|
| 2 |
+
from sympy.physics.mechanics import dynamicsymbols
|
| 3 |
+
from sympy.physics.mechanics import ReferenceFrame, Point, Particle
|
| 4 |
+
from sympy.physics.mechanics import LagrangesMethod, Lagrangian
|
| 5 |
+
|
| 6 |
+
### This test asserts that a system with more than one external forces
|
| 7 |
+
### is acurately formed with Lagrange method (see issue #8626)
|
| 8 |
+
|
| 9 |
+
def test_lagrange_2forces():
|
| 10 |
+
### Equations for two damped springs in serie with two forces
|
| 11 |
+
|
| 12 |
+
### generalized coordinates
|
| 13 |
+
q1, q2 = dynamicsymbols('q1, q2')
|
| 14 |
+
### generalized speeds
|
| 15 |
+
q1d, q2d = dynamicsymbols('q1, q2', 1)
|
| 16 |
+
|
| 17 |
+
### Mass, spring strength, friction coefficient
|
| 18 |
+
m, k, nu = symbols('m, k, nu')
|
| 19 |
+
|
| 20 |
+
N = ReferenceFrame('N')
|
| 21 |
+
O = Point('O')
|
| 22 |
+
|
| 23 |
+
### Two points
|
| 24 |
+
P1 = O.locatenew('P1', q1 * N.x)
|
| 25 |
+
P1.set_vel(N, q1d * N.x)
|
| 26 |
+
P2 = O.locatenew('P1', q2 * N.x)
|
| 27 |
+
P2.set_vel(N, q2d * N.x)
|
| 28 |
+
|
| 29 |
+
pP1 = Particle('pP1', P1, m)
|
| 30 |
+
pP1.potential_energy = k * q1**2 / 2
|
| 31 |
+
|
| 32 |
+
pP2 = Particle('pP2', P2, m)
|
| 33 |
+
pP2.potential_energy = k * (q1 - q2)**2 / 2
|
| 34 |
+
|
| 35 |
+
#### Friction forces
|
| 36 |
+
forcelist = [(P1, - nu * q1d * N.x),
|
| 37 |
+
(P2, - nu * q2d * N.x)]
|
| 38 |
+
lag = Lagrangian(N, pP1, pP2)
|
| 39 |
+
|
| 40 |
+
l_method = LagrangesMethod(lag, (q1, q2), forcelist=forcelist, frame=N)
|
| 41 |
+
l_method.form_lagranges_equations()
|
| 42 |
+
|
| 43 |
+
eq1 = l_method.eom[0]
|
| 44 |
+
assert eq1.diff(q1d) == nu
|
| 45 |
+
eq2 = l_method.eom[1]
|
| 46 |
+
assert eq2.diff(q2d) == nu
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_linearize.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import symbols, Matrix, cos, sin, atan, sqrt, Rational
|
| 2 |
+
from sympy.core.sympify import sympify
|
| 3 |
+
from sympy.simplify.simplify import simplify
|
| 4 |
+
from sympy.solvers.solvers import solve
|
| 5 |
+
from sympy.physics.mechanics import dynamicsymbols, ReferenceFrame, Point,\
|
| 6 |
+
dot, cross, inertia, KanesMethod, Particle, RigidBody, Lagrangian,\
|
| 7 |
+
LagrangesMethod
|
| 8 |
+
from sympy.testing.pytest import slow
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@slow
|
| 12 |
+
def test_linearize_rolling_disc_kane():
|
| 13 |
+
# Symbols for time and constant parameters
|
| 14 |
+
t, r, m, g, v = symbols('t r m g v')
|
| 15 |
+
|
| 16 |
+
# Configuration variables and their time derivatives
|
| 17 |
+
q1, q2, q3, q4, q5, q6 = q = dynamicsymbols('q1:7')
|
| 18 |
+
q1d, q2d, q3d, q4d, q5d, q6d = qd = [qi.diff(t) for qi in q]
|
| 19 |
+
|
| 20 |
+
# Generalized speeds and their time derivatives
|
| 21 |
+
u = dynamicsymbols('u:6')
|
| 22 |
+
u1, u2, u3, u4, u5, u6 = u = dynamicsymbols('u1:7')
|
| 23 |
+
u1d, u2d, u3d, u4d, u5d, u6d = [ui.diff(t) for ui in u]
|
| 24 |
+
|
| 25 |
+
# Reference frames
|
| 26 |
+
N = ReferenceFrame('N') # Inertial frame
|
| 27 |
+
NO = Point('NO') # Inertial origin
|
| 28 |
+
A = N.orientnew('A', 'Axis', [q1, N.z]) # Yaw intermediate frame
|
| 29 |
+
B = A.orientnew('B', 'Axis', [q2, A.x]) # Lean intermediate frame
|
| 30 |
+
C = B.orientnew('C', 'Axis', [q3, B.y]) # Disc fixed frame
|
| 31 |
+
CO = NO.locatenew('CO', q4*N.x + q5*N.y + q6*N.z) # Disc center
|
| 32 |
+
|
| 33 |
+
# Disc angular velocity in N expressed using time derivatives of coordinates
|
| 34 |
+
w_c_n_qd = C.ang_vel_in(N)
|
| 35 |
+
w_b_n_qd = B.ang_vel_in(N)
|
| 36 |
+
|
| 37 |
+
# Inertial angular velocity and angular acceleration of disc fixed frame
|
| 38 |
+
C.set_ang_vel(N, u1*B.x + u2*B.y + u3*B.z)
|
| 39 |
+
|
| 40 |
+
# Disc center velocity in N expressed using time derivatives of coordinates
|
| 41 |
+
v_co_n_qd = CO.pos_from(NO).dt(N)
|
| 42 |
+
|
| 43 |
+
# Disc center velocity in N expressed using generalized speeds
|
| 44 |
+
CO.set_vel(N, u4*C.x + u5*C.y + u6*C.z)
|
| 45 |
+
|
| 46 |
+
# Disc Ground Contact Point
|
| 47 |
+
P = CO.locatenew('P', r*B.z)
|
| 48 |
+
P.v2pt_theory(CO, N, C)
|
| 49 |
+
|
| 50 |
+
# Configuration constraint
|
| 51 |
+
f_c = Matrix([q6 - dot(CO.pos_from(P), N.z)])
|
| 52 |
+
|
| 53 |
+
# Velocity level constraints
|
| 54 |
+
f_v = Matrix([dot(P.vel(N), uv) for uv in C])
|
| 55 |
+
|
| 56 |
+
# Kinematic differential equations
|
| 57 |
+
kindiffs = Matrix([dot(w_c_n_qd - C.ang_vel_in(N), uv) for uv in B] +
|
| 58 |
+
[dot(v_co_n_qd - CO.vel(N), uv) for uv in N])
|
| 59 |
+
qdots = solve(kindiffs, qd)
|
| 60 |
+
|
| 61 |
+
# Set angular velocity of remaining frames
|
| 62 |
+
B.set_ang_vel(N, w_b_n_qd.subs(qdots))
|
| 63 |
+
C.set_ang_acc(N, C.ang_vel_in(N).dt(B) + cross(B.ang_vel_in(N), C.ang_vel_in(N)))
|
| 64 |
+
|
| 65 |
+
# Active forces
|
| 66 |
+
F_CO = m*g*A.z
|
| 67 |
+
|
| 68 |
+
# Create inertia dyadic of disc C about point CO
|
| 69 |
+
I = (m * r**2) / 4
|
| 70 |
+
J = (m * r**2) / 2
|
| 71 |
+
I_C_CO = inertia(C, I, J, I)
|
| 72 |
+
|
| 73 |
+
Disc = RigidBody('Disc', CO, C, m, (I_C_CO, CO))
|
| 74 |
+
BL = [Disc]
|
| 75 |
+
FL = [(CO, F_CO)]
|
| 76 |
+
KM = KanesMethod(N, [q1, q2, q3, q4, q5], [u1, u2, u3], kd_eqs=kindiffs,
|
| 77 |
+
q_dependent=[q6], configuration_constraints=f_c,
|
| 78 |
+
u_dependent=[u4, u5, u6], velocity_constraints=f_v)
|
| 79 |
+
(fr, fr_star) = KM.kanes_equations(BL, FL)
|
| 80 |
+
|
| 81 |
+
# Test generalized form equations
|
| 82 |
+
linearizer = KM.to_linearizer()
|
| 83 |
+
assert linearizer.f_c == f_c
|
| 84 |
+
assert linearizer.f_v == f_v
|
| 85 |
+
assert linearizer.f_a == f_v.diff(t).subs(KM.kindiffdict())
|
| 86 |
+
sol = solve(linearizer.f_0 + linearizer.f_1, qd)
|
| 87 |
+
for qi in qdots.keys():
|
| 88 |
+
assert sol[qi] == qdots[qi]
|
| 89 |
+
assert simplify(linearizer.f_2 + linearizer.f_3 - fr - fr_star) == Matrix([0, 0, 0])
|
| 90 |
+
|
| 91 |
+
# Perform the linearization
|
| 92 |
+
# Precomputed operating point
|
| 93 |
+
q_op = {q6: -r*cos(q2)}
|
| 94 |
+
u_op = {u1: 0,
|
| 95 |
+
u2: sin(q2)*q1d + q3d,
|
| 96 |
+
u3: cos(q2)*q1d,
|
| 97 |
+
u4: -r*(sin(q2)*q1d + q3d)*cos(q3),
|
| 98 |
+
u5: 0,
|
| 99 |
+
u6: -r*(sin(q2)*q1d + q3d)*sin(q3)}
|
| 100 |
+
qd_op = {q2d: 0,
|
| 101 |
+
q4d: -r*(sin(q2)*q1d + q3d)*cos(q1),
|
| 102 |
+
q5d: -r*(sin(q2)*q1d + q3d)*sin(q1),
|
| 103 |
+
q6d: 0}
|
| 104 |
+
ud_op = {u1d: 4*g*sin(q2)/(5*r) + sin(2*q2)*q1d**2/2 + 6*cos(q2)*q1d*q3d/5,
|
| 105 |
+
u2d: 0,
|
| 106 |
+
u3d: 0,
|
| 107 |
+
u4d: r*(sin(q2)*sin(q3)*q1d*q3d + sin(q3)*q3d**2),
|
| 108 |
+
u5d: r*(4*g*sin(q2)/(5*r) + sin(2*q2)*q1d**2/2 + 6*cos(q2)*q1d*q3d/5),
|
| 109 |
+
u6d: -r*(sin(q2)*cos(q3)*q1d*q3d + cos(q3)*q3d**2)}
|
| 110 |
+
|
| 111 |
+
A, B = linearizer.linearize(op_point=[q_op, u_op, qd_op, ud_op], A_and_B=True, simplify=True)
|
| 112 |
+
|
| 113 |
+
upright_nominal = {q1d: 0, q2: 0, m: 1, r: 1, g: 1}
|
| 114 |
+
|
| 115 |
+
# Precomputed solution
|
| 116 |
+
A_sol = Matrix([[0, 0, 0, 0, 0, 0, 0, 1],
|
| 117 |
+
[0, 0, 0, 0, 0, 1, 0, 0],
|
| 118 |
+
[0, 0, 0, 0, 0, 0, 1, 0],
|
| 119 |
+
[sin(q1)*q3d, 0, 0, 0, 0, -sin(q1), -cos(q1), 0],
|
| 120 |
+
[-cos(q1)*q3d, 0, 0, 0, 0, cos(q1), -sin(q1), 0],
|
| 121 |
+
[0, Rational(4, 5), 0, 0, 0, 0, 0, 6*q3d/5],
|
| 122 |
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
| 123 |
+
[0, 0, 0, 0, 0, -2*q3d, 0, 0]])
|
| 124 |
+
B_sol = Matrix([])
|
| 125 |
+
|
| 126 |
+
# Check that linearization is correct
|
| 127 |
+
assert A.subs(upright_nominal) == A_sol
|
| 128 |
+
assert B.subs(upright_nominal) == B_sol
|
| 129 |
+
|
| 130 |
+
# Check eigenvalues at critical speed are all zero:
|
| 131 |
+
assert sympify(A.subs(upright_nominal).subs(q3d, 1/sqrt(3))).eigenvals() == {0: 8}
|
| 132 |
+
|
| 133 |
+
# Check whether alternative solvers work
|
| 134 |
+
# symengine doesn't support method='GJ'
|
| 135 |
+
linearizer = KM.to_linearizer(linear_solver='GJ')
|
| 136 |
+
A, B = linearizer.linearize(op_point=[q_op, u_op, qd_op, ud_op],
|
| 137 |
+
A_and_B=True, simplify=True)
|
| 138 |
+
assert A.subs(upright_nominal) == A_sol
|
| 139 |
+
assert B.subs(upright_nominal) == B_sol
|
| 140 |
+
|
| 141 |
+
def test_linearize_pendulum_kane_minimal():
|
| 142 |
+
q1 = dynamicsymbols('q1') # angle of pendulum
|
| 143 |
+
u1 = dynamicsymbols('u1') # Angular velocity
|
| 144 |
+
q1d = dynamicsymbols('q1', 1) # Angular velocity
|
| 145 |
+
L, m, t = symbols('L, m, t')
|
| 146 |
+
g = 9.8
|
| 147 |
+
|
| 148 |
+
# Compose world frame
|
| 149 |
+
N = ReferenceFrame('N')
|
| 150 |
+
pN = Point('N*')
|
| 151 |
+
pN.set_vel(N, 0)
|
| 152 |
+
|
| 153 |
+
# A.x is along the pendulum
|
| 154 |
+
A = N.orientnew('A', 'axis', [q1, N.z])
|
| 155 |
+
A.set_ang_vel(N, u1*N.z)
|
| 156 |
+
|
| 157 |
+
# Locate point P relative to the origin N*
|
| 158 |
+
P = pN.locatenew('P', L*A.x)
|
| 159 |
+
P.v2pt_theory(pN, N, A)
|
| 160 |
+
pP = Particle('pP', P, m)
|
| 161 |
+
|
| 162 |
+
# Create Kinematic Differential Equations
|
| 163 |
+
kde = Matrix([q1d - u1])
|
| 164 |
+
|
| 165 |
+
# Input the force resultant at P
|
| 166 |
+
R = m*g*N.x
|
| 167 |
+
|
| 168 |
+
# Solve for eom with kanes method
|
| 169 |
+
KM = KanesMethod(N, q_ind=[q1], u_ind=[u1], kd_eqs=kde)
|
| 170 |
+
(fr, frstar) = KM.kanes_equations([pP], [(P, R)])
|
| 171 |
+
|
| 172 |
+
# Linearize
|
| 173 |
+
A, B, inp_vec = KM.linearize(A_and_B=True, simplify=True)
|
| 174 |
+
|
| 175 |
+
assert A == Matrix([[0, 1], [-9.8*cos(q1)/L, 0]])
|
| 176 |
+
assert B == Matrix([])
|
| 177 |
+
|
| 178 |
+
def test_linearize_pendulum_kane_nonminimal():
|
| 179 |
+
# Create generalized coordinates and speeds for this non-minimal realization
|
| 180 |
+
# q1, q2 = N.x and N.y coordinates of pendulum
|
| 181 |
+
# u1, u2 = N.x and N.y velocities of pendulum
|
| 182 |
+
q1, q2 = dynamicsymbols('q1:3')
|
| 183 |
+
q1d, q2d = dynamicsymbols('q1:3', level=1)
|
| 184 |
+
u1, u2 = dynamicsymbols('u1:3')
|
| 185 |
+
u1d, u2d = dynamicsymbols('u1:3', level=1)
|
| 186 |
+
L, m, t = symbols('L, m, t')
|
| 187 |
+
g = 9.8
|
| 188 |
+
|
| 189 |
+
# Compose world frame
|
| 190 |
+
N = ReferenceFrame('N')
|
| 191 |
+
pN = Point('N*')
|
| 192 |
+
pN.set_vel(N, 0)
|
| 193 |
+
|
| 194 |
+
# A.x is along the pendulum
|
| 195 |
+
theta1 = atan(q2/q1)
|
| 196 |
+
A = N.orientnew('A', 'axis', [theta1, N.z])
|
| 197 |
+
|
| 198 |
+
# Locate the pendulum mass
|
| 199 |
+
P = pN.locatenew('P1', q1*N.x + q2*N.y)
|
| 200 |
+
pP = Particle('pP', P, m)
|
| 201 |
+
|
| 202 |
+
# Calculate the kinematic differential equations
|
| 203 |
+
kde = Matrix([q1d - u1,
|
| 204 |
+
q2d - u2])
|
| 205 |
+
dq_dict = solve(kde, [q1d, q2d])
|
| 206 |
+
|
| 207 |
+
# Set velocity of point P
|
| 208 |
+
P.set_vel(N, P.pos_from(pN).dt(N).subs(dq_dict))
|
| 209 |
+
|
| 210 |
+
# Configuration constraint is length of pendulum
|
| 211 |
+
f_c = Matrix([P.pos_from(pN).magnitude() - L])
|
| 212 |
+
|
| 213 |
+
# Velocity constraint is that the velocity in the A.x direction is
|
| 214 |
+
# always zero (the pendulum is never getting longer).
|
| 215 |
+
f_v = Matrix([P.vel(N).express(A).dot(A.x)])
|
| 216 |
+
f_v.simplify()
|
| 217 |
+
|
| 218 |
+
# Acceleration constraints is the time derivative of the velocity constraint
|
| 219 |
+
f_a = f_v.diff(t)
|
| 220 |
+
f_a.simplify()
|
| 221 |
+
|
| 222 |
+
# Input the force resultant at P
|
| 223 |
+
R = m*g*N.x
|
| 224 |
+
|
| 225 |
+
# Derive the equations of motion using the KanesMethod class.
|
| 226 |
+
KM = KanesMethod(N, q_ind=[q2], u_ind=[u2], q_dependent=[q1],
|
| 227 |
+
u_dependent=[u1], configuration_constraints=f_c,
|
| 228 |
+
velocity_constraints=f_v, acceleration_constraints=f_a, kd_eqs=kde)
|
| 229 |
+
(fr, frstar) = KM.kanes_equations([pP], [(P, R)])
|
| 230 |
+
|
| 231 |
+
# Set the operating point to be straight down, and non-moving
|
| 232 |
+
q_op = {q1: L, q2: 0}
|
| 233 |
+
u_op = {u1: 0, u2: 0}
|
| 234 |
+
ud_op = {u1d: 0, u2d: 0}
|
| 235 |
+
|
| 236 |
+
A, B, inp_vec = KM.linearize(op_point=[q_op, u_op, ud_op], A_and_B=True,
|
| 237 |
+
simplify=True)
|
| 238 |
+
|
| 239 |
+
assert A.expand() == Matrix([[0, 1], [-9.8/L, 0]])
|
| 240 |
+
assert B == Matrix([])
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
# symengine doesn't support method='GJ'
|
| 244 |
+
A, B, inp_vec = KM.linearize(op_point=[q_op, u_op, ud_op], A_and_B=True,
|
| 245 |
+
simplify=True, linear_solver='GJ')
|
| 246 |
+
|
| 247 |
+
assert A.expand() == Matrix([[0, 1], [-9.8/L, 0]])
|
| 248 |
+
assert B == Matrix([])
|
| 249 |
+
|
| 250 |
+
A, B, inp_vec = KM.linearize(op_point=[q_op, u_op, ud_op],
|
| 251 |
+
A_and_B=True,
|
| 252 |
+
simplify=True,
|
| 253 |
+
linear_solver=lambda A, b: A.LUsolve(b))
|
| 254 |
+
|
| 255 |
+
assert A.expand() == Matrix([[0, 1], [-9.8/L, 0]])
|
| 256 |
+
assert B == Matrix([])
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def test_linearize_pendulum_lagrange_minimal():
|
| 260 |
+
q1 = dynamicsymbols('q1') # angle of pendulum
|
| 261 |
+
q1d = dynamicsymbols('q1', 1) # Angular velocity
|
| 262 |
+
L, m, t = symbols('L, m, t')
|
| 263 |
+
g = 9.8
|
| 264 |
+
|
| 265 |
+
# Compose world frame
|
| 266 |
+
N = ReferenceFrame('N')
|
| 267 |
+
pN = Point('N*')
|
| 268 |
+
pN.set_vel(N, 0)
|
| 269 |
+
|
| 270 |
+
# A.x is along the pendulum
|
| 271 |
+
A = N.orientnew('A', 'axis', [q1, N.z])
|
| 272 |
+
A.set_ang_vel(N, q1d*N.z)
|
| 273 |
+
|
| 274 |
+
# Locate point P relative to the origin N*
|
| 275 |
+
P = pN.locatenew('P', L*A.x)
|
| 276 |
+
P.v2pt_theory(pN, N, A)
|
| 277 |
+
pP = Particle('pP', P, m)
|
| 278 |
+
|
| 279 |
+
# Solve for eom with Lagranges method
|
| 280 |
+
Lag = Lagrangian(N, pP)
|
| 281 |
+
LM = LagrangesMethod(Lag, [q1], forcelist=[(P, m*g*N.x)], frame=N)
|
| 282 |
+
LM.form_lagranges_equations()
|
| 283 |
+
|
| 284 |
+
# Linearize
|
| 285 |
+
A, B, inp_vec = LM.linearize([q1], [q1d], A_and_B=True)
|
| 286 |
+
|
| 287 |
+
assert simplify(A) == Matrix([[0, 1], [-9.8*cos(q1)/L, 0]])
|
| 288 |
+
assert B == Matrix([])
|
| 289 |
+
|
| 290 |
+
# Check an alternative solver
|
| 291 |
+
A, B, inp_vec = LM.linearize([q1], [q1d], A_and_B=True, linear_solver='GJ')
|
| 292 |
+
|
| 293 |
+
assert simplify(A) == Matrix([[0, 1], [-9.8*cos(q1)/L, 0]])
|
| 294 |
+
assert B == Matrix([])
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
def test_linearize_pendulum_lagrange_nonminimal():
|
| 298 |
+
q1, q2 = dynamicsymbols('q1:3')
|
| 299 |
+
q1d, q2d = dynamicsymbols('q1:3', level=1)
|
| 300 |
+
L, m, t = symbols('L, m, t')
|
| 301 |
+
g = 9.8
|
| 302 |
+
# Compose World Frame
|
| 303 |
+
N = ReferenceFrame('N')
|
| 304 |
+
pN = Point('N*')
|
| 305 |
+
pN.set_vel(N, 0)
|
| 306 |
+
# A.x is along the pendulum
|
| 307 |
+
theta1 = atan(q2/q1)
|
| 308 |
+
A = N.orientnew('A', 'axis', [theta1, N.z])
|
| 309 |
+
# Create point P, the pendulum mass
|
| 310 |
+
P = pN.locatenew('P1', q1*N.x + q2*N.y)
|
| 311 |
+
P.set_vel(N, P.pos_from(pN).dt(N))
|
| 312 |
+
pP = Particle('pP', P, m)
|
| 313 |
+
# Constraint Equations
|
| 314 |
+
f_c = Matrix([q1**2 + q2**2 - L**2])
|
| 315 |
+
# Calculate the lagrangian, and form the equations of motion
|
| 316 |
+
Lag = Lagrangian(N, pP)
|
| 317 |
+
LM = LagrangesMethod(Lag, [q1, q2], hol_coneqs=f_c, forcelist=[(P, m*g*N.x)], frame=N)
|
| 318 |
+
LM.form_lagranges_equations()
|
| 319 |
+
# Compose operating point
|
| 320 |
+
op_point = {q1: L, q2: 0, q1d: 0, q2d: 0, q1d.diff(t): 0, q2d.diff(t): 0}
|
| 321 |
+
# Solve for multiplier operating point
|
| 322 |
+
lam_op = LM.solve_multipliers(op_point=op_point)
|
| 323 |
+
op_point.update(lam_op)
|
| 324 |
+
# Perform the Linearization
|
| 325 |
+
A, B, inp_vec = LM.linearize([q2], [q2d], [q1], [q1d],
|
| 326 |
+
op_point=op_point, A_and_B=True)
|
| 327 |
+
assert simplify(A) == Matrix([[0, 1], [-9.8/L, 0]])
|
| 328 |
+
assert B == Matrix([])
|
| 329 |
+
|
| 330 |
+
# Check if passing a function to linear_solver works
|
| 331 |
+
A, B, inp_vec = LM.linearize([q2], [q2d], [q1], [q1d], op_point=op_point,
|
| 332 |
+
A_and_B=True, linear_solver=lambda A, b:
|
| 333 |
+
A.LUsolve(b))
|
| 334 |
+
assert simplify(A) == Matrix([[0, 1], [-9.8/L, 0]])
|
| 335 |
+
assert B == Matrix([])
|
| 336 |
+
|
| 337 |
+
def test_linearize_rolling_disc_lagrange():
|
| 338 |
+
q1, q2, q3 = q = dynamicsymbols('q1 q2 q3')
|
| 339 |
+
q1d, q2d, q3d = qd = dynamicsymbols('q1 q2 q3', 1)
|
| 340 |
+
r, m, g = symbols('r m g')
|
| 341 |
+
|
| 342 |
+
N = ReferenceFrame('N')
|
| 343 |
+
Y = N.orientnew('Y', 'Axis', [q1, N.z])
|
| 344 |
+
L = Y.orientnew('L', 'Axis', [q2, Y.x])
|
| 345 |
+
R = L.orientnew('R', 'Axis', [q3, L.y])
|
| 346 |
+
|
| 347 |
+
C = Point('C')
|
| 348 |
+
C.set_vel(N, 0)
|
| 349 |
+
Dmc = C.locatenew('Dmc', r * L.z)
|
| 350 |
+
Dmc.v2pt_theory(C, N, R)
|
| 351 |
+
|
| 352 |
+
I = inertia(L, m / 4 * r**2, m / 2 * r**2, m / 4 * r**2)
|
| 353 |
+
BodyD = RigidBody('BodyD', Dmc, R, m, (I, Dmc))
|
| 354 |
+
BodyD.potential_energy = - m * g * r * cos(q2)
|
| 355 |
+
|
| 356 |
+
Lag = Lagrangian(N, BodyD)
|
| 357 |
+
l = LagrangesMethod(Lag, q)
|
| 358 |
+
l.form_lagranges_equations()
|
| 359 |
+
|
| 360 |
+
# Linearize about steady-state upright rolling
|
| 361 |
+
op_point = {q1: 0, q2: 0, q3: 0,
|
| 362 |
+
q1d: 0, q2d: 0,
|
| 363 |
+
q1d.diff(): 0, q2d.diff(): 0, q3d.diff(): 0}
|
| 364 |
+
A = l.linearize(q_ind=q, qd_ind=qd, op_point=op_point, A_and_B=True)[0]
|
| 365 |
+
sol = Matrix([[0, 0, 0, 1, 0, 0],
|
| 366 |
+
[0, 0, 0, 0, 1, 0],
|
| 367 |
+
[0, 0, 0, 0, 0, 1],
|
| 368 |
+
[0, 0, 0, 0, -6*q3d, 0],
|
| 369 |
+
[0, -4*g/(5*r), 0, 6*q3d/5, 0, 0],
|
| 370 |
+
[0, 0, 0, 0, 0, 0]])
|
| 371 |
+
|
| 372 |
+
assert A == sol
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_loads.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pytest import raises
|
| 2 |
+
|
| 3 |
+
from sympy import symbols
|
| 4 |
+
from sympy.physics.mechanics import (RigidBody, Particle, ReferenceFrame, Point,
|
| 5 |
+
outer, dynamicsymbols, Force, Torque)
|
| 6 |
+
from sympy.physics.mechanics.loads import gravity, _parse_load
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def test_force_default():
|
| 10 |
+
N = ReferenceFrame('N')
|
| 11 |
+
Po = Point('Po')
|
| 12 |
+
f1 = Force(Po, N.x)
|
| 13 |
+
assert f1.point == Po
|
| 14 |
+
assert f1.force == N.x
|
| 15 |
+
assert f1.__repr__() == 'Force(point=Po, force=N.x)'
|
| 16 |
+
# Test tuple behaviour
|
| 17 |
+
assert isinstance(f1, tuple)
|
| 18 |
+
assert f1[0] == Po
|
| 19 |
+
assert f1[1] == N.x
|
| 20 |
+
assert f1 == (Po, N.x)
|
| 21 |
+
assert f1 != (N.x, Po)
|
| 22 |
+
assert f1 != (Po, N.x + N.y)
|
| 23 |
+
assert f1 != (Point('Co'), N.x)
|
| 24 |
+
# Test body as input
|
| 25 |
+
P = Particle('P', Po)
|
| 26 |
+
f2 = Force(P, N.x)
|
| 27 |
+
assert f1 == f2
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def test_torque_default():
|
| 31 |
+
N = ReferenceFrame('N')
|
| 32 |
+
f1 = Torque(N, N.x)
|
| 33 |
+
assert f1.frame == N
|
| 34 |
+
assert f1.torque == N.x
|
| 35 |
+
assert f1.__repr__() == 'Torque(frame=N, torque=N.x)'
|
| 36 |
+
# Test tuple behaviour
|
| 37 |
+
assert isinstance(f1, tuple)
|
| 38 |
+
assert f1[0] == N
|
| 39 |
+
assert f1[1] == N.x
|
| 40 |
+
assert f1 == (N, N.x)
|
| 41 |
+
assert f1 != (N.x, N)
|
| 42 |
+
assert f1 != (N, N.x + N.y)
|
| 43 |
+
assert f1 != (ReferenceFrame('A'), N.x)
|
| 44 |
+
# Test body as input
|
| 45 |
+
rb = RigidBody('P', frame=N)
|
| 46 |
+
f2 = Torque(rb, N.x)
|
| 47 |
+
assert f1 == f2
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def test_gravity():
|
| 51 |
+
N = ReferenceFrame('N')
|
| 52 |
+
m, M, g = symbols('m M g')
|
| 53 |
+
F1, F2 = dynamicsymbols('F1 F2')
|
| 54 |
+
po = Point('po')
|
| 55 |
+
pa = Particle('pa', po, m)
|
| 56 |
+
A = ReferenceFrame('A')
|
| 57 |
+
P = Point('P')
|
| 58 |
+
I = outer(A.x, A.x)
|
| 59 |
+
B = RigidBody('B', P, A, M, (I, P))
|
| 60 |
+
forceList = [(po, F1), (P, F2)]
|
| 61 |
+
forceList.extend(gravity(g * N.y, pa, B))
|
| 62 |
+
l = [(po, F1), (P, F2), (po, g * m * N.y), (P, g * M * N.y)]
|
| 63 |
+
|
| 64 |
+
for i in range(len(l)):
|
| 65 |
+
for j in range(len(l[i])):
|
| 66 |
+
assert forceList[i][j] == l[i][j]
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def test_parse_loads():
|
| 70 |
+
N = ReferenceFrame('N')
|
| 71 |
+
po = Point('po')
|
| 72 |
+
assert _parse_load(Force(po, N.z)) == (po, N.z)
|
| 73 |
+
assert _parse_load(Torque(N, N.x)) == (N, N.x)
|
| 74 |
+
f1 = _parse_load((po, N.x)) # Test whether a force is recognized
|
| 75 |
+
assert isinstance(f1, Force)
|
| 76 |
+
assert f1 == Force(po, N.x)
|
| 77 |
+
t1 = _parse_load((N, N.y)) # Test whether a torque is recognized
|
| 78 |
+
assert isinstance(t1, Torque)
|
| 79 |
+
assert t1 == Torque(N, N.y)
|
| 80 |
+
# Bodies should be undetermined (even in case of a Particle)
|
| 81 |
+
raises(ValueError, lambda: _parse_load((Particle('pa', po), N.x)))
|
| 82 |
+
raises(ValueError, lambda: _parse_load((RigidBody('pa', po, N), N.x)))
|
| 83 |
+
# Invalid tuple length
|
| 84 |
+
raises(ValueError, lambda: _parse_load((po, N.x, po, N.x)))
|
| 85 |
+
# Invalid type
|
| 86 |
+
raises(TypeError, lambda: _parse_load([po, N.x]))
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_method.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy.physics.mechanics.method import _Methods
|
| 2 |
+
from sympy.testing.pytest import raises
|
| 3 |
+
|
| 4 |
+
def test_method():
|
| 5 |
+
raises(TypeError, lambda: _Methods())
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_models.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sympy.physics.mechanics.models as models
|
| 2 |
+
from sympy import (cos, sin, Matrix, symbols, zeros)
|
| 3 |
+
from sympy.simplify.simplify import simplify
|
| 4 |
+
from sympy.physics.mechanics import (dynamicsymbols)
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def test_multi_mass_spring_damper_inputs():
|
| 8 |
+
|
| 9 |
+
c0, k0, m0 = symbols("c0 k0 m0")
|
| 10 |
+
g = symbols("g")
|
| 11 |
+
v0, x0, f0 = dynamicsymbols("v0 x0 f0")
|
| 12 |
+
|
| 13 |
+
kane1 = models.multi_mass_spring_damper(1)
|
| 14 |
+
massmatrix1 = Matrix([[m0]])
|
| 15 |
+
forcing1 = Matrix([[-c0*v0 - k0*x0]])
|
| 16 |
+
assert simplify(massmatrix1 - kane1.mass_matrix) == Matrix([0])
|
| 17 |
+
assert simplify(forcing1 - kane1.forcing) == Matrix([0])
|
| 18 |
+
|
| 19 |
+
kane2 = models.multi_mass_spring_damper(1, True)
|
| 20 |
+
massmatrix2 = Matrix([[m0]])
|
| 21 |
+
forcing2 = Matrix([[-c0*v0 + g*m0 - k0*x0]])
|
| 22 |
+
assert simplify(massmatrix2 - kane2.mass_matrix) == Matrix([0])
|
| 23 |
+
assert simplify(forcing2 - kane2.forcing) == Matrix([0])
|
| 24 |
+
|
| 25 |
+
kane3 = models.multi_mass_spring_damper(1, True, True)
|
| 26 |
+
massmatrix3 = Matrix([[m0]])
|
| 27 |
+
forcing3 = Matrix([[-c0*v0 + g*m0 - k0*x0 + f0]])
|
| 28 |
+
assert simplify(massmatrix3 - kane3.mass_matrix) == Matrix([0])
|
| 29 |
+
assert simplify(forcing3 - kane3.forcing) == Matrix([0])
|
| 30 |
+
|
| 31 |
+
kane4 = models.multi_mass_spring_damper(1, False, True)
|
| 32 |
+
massmatrix4 = Matrix([[m0]])
|
| 33 |
+
forcing4 = Matrix([[-c0*v0 - k0*x0 + f0]])
|
| 34 |
+
assert simplify(massmatrix4 - kane4.mass_matrix) == Matrix([0])
|
| 35 |
+
assert simplify(forcing4 - kane4.forcing) == Matrix([0])
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_multi_mass_spring_damper_higher_order():
|
| 39 |
+
c0, k0, m0 = symbols("c0 k0 m0")
|
| 40 |
+
c1, k1, m1 = symbols("c1 k1 m1")
|
| 41 |
+
c2, k2, m2 = symbols("c2 k2 m2")
|
| 42 |
+
v0, x0 = dynamicsymbols("v0 x0")
|
| 43 |
+
v1, x1 = dynamicsymbols("v1 x1")
|
| 44 |
+
v2, x2 = dynamicsymbols("v2 x2")
|
| 45 |
+
|
| 46 |
+
kane1 = models.multi_mass_spring_damper(3)
|
| 47 |
+
massmatrix1 = Matrix([[m0 + m1 + m2, m1 + m2, m2],
|
| 48 |
+
[m1 + m2, m1 + m2, m2],
|
| 49 |
+
[m2, m2, m2]])
|
| 50 |
+
forcing1 = Matrix([[-c0*v0 - k0*x0],
|
| 51 |
+
[-c1*v1 - k1*x1],
|
| 52 |
+
[-c2*v2 - k2*x2]])
|
| 53 |
+
assert simplify(massmatrix1 - kane1.mass_matrix) == zeros(3)
|
| 54 |
+
assert simplify(forcing1 - kane1.forcing) == Matrix([0, 0, 0])
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def test_n_link_pendulum_on_cart_inputs():
|
| 58 |
+
l0, m0 = symbols("l0 m0")
|
| 59 |
+
m1 = symbols("m1")
|
| 60 |
+
g = symbols("g")
|
| 61 |
+
q0, q1, F, T1 = dynamicsymbols("q0 q1 F T1")
|
| 62 |
+
u0, u1 = dynamicsymbols("u0 u1")
|
| 63 |
+
|
| 64 |
+
kane1 = models.n_link_pendulum_on_cart(1)
|
| 65 |
+
massmatrix1 = Matrix([[m0 + m1, -l0*m1*cos(q1)],
|
| 66 |
+
[-l0*m1*cos(q1), l0**2*m1]])
|
| 67 |
+
forcing1 = Matrix([[-l0*m1*u1**2*sin(q1) + F], [g*l0*m1*sin(q1)]])
|
| 68 |
+
assert simplify(massmatrix1 - kane1.mass_matrix) == zeros(2)
|
| 69 |
+
assert simplify(forcing1 - kane1.forcing) == Matrix([0, 0])
|
| 70 |
+
|
| 71 |
+
kane2 = models.n_link_pendulum_on_cart(1, False)
|
| 72 |
+
massmatrix2 = Matrix([[m0 + m1, -l0*m1*cos(q1)],
|
| 73 |
+
[-l0*m1*cos(q1), l0**2*m1]])
|
| 74 |
+
forcing2 = Matrix([[-l0*m1*u1**2*sin(q1)], [g*l0*m1*sin(q1)]])
|
| 75 |
+
assert simplify(massmatrix2 - kane2.mass_matrix) == zeros(2)
|
| 76 |
+
assert simplify(forcing2 - kane2.forcing) == Matrix([0, 0])
|
| 77 |
+
|
| 78 |
+
kane3 = models.n_link_pendulum_on_cart(1, False, True)
|
| 79 |
+
massmatrix3 = Matrix([[m0 + m1, -l0*m1*cos(q1)],
|
| 80 |
+
[-l0*m1*cos(q1), l0**2*m1]])
|
| 81 |
+
forcing3 = Matrix([[-l0*m1*u1**2*sin(q1)], [g*l0*m1*sin(q1) + T1]])
|
| 82 |
+
assert simplify(massmatrix3 - kane3.mass_matrix) == zeros(2)
|
| 83 |
+
assert simplify(forcing3 - kane3.forcing) == Matrix([0, 0])
|
| 84 |
+
|
| 85 |
+
kane4 = models.n_link_pendulum_on_cart(1, True, False)
|
| 86 |
+
massmatrix4 = Matrix([[m0 + m1, -l0*m1*cos(q1)],
|
| 87 |
+
[-l0*m1*cos(q1), l0**2*m1]])
|
| 88 |
+
forcing4 = Matrix([[-l0*m1*u1**2*sin(q1) + F], [g*l0*m1*sin(q1)]])
|
| 89 |
+
assert simplify(massmatrix4 - kane4.mass_matrix) == zeros(2)
|
| 90 |
+
assert simplify(forcing4 - kane4.forcing) == Matrix([0, 0])
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def test_n_link_pendulum_on_cart_higher_order():
|
| 94 |
+
l0, m0 = symbols("l0 m0")
|
| 95 |
+
l1, m1 = symbols("l1 m1")
|
| 96 |
+
m2 = symbols("m2")
|
| 97 |
+
g = symbols("g")
|
| 98 |
+
q0, q1, q2 = dynamicsymbols("q0 q1 q2")
|
| 99 |
+
u0, u1, u2 = dynamicsymbols("u0 u1 u2")
|
| 100 |
+
F, T1 = dynamicsymbols("F T1")
|
| 101 |
+
|
| 102 |
+
kane1 = models.n_link_pendulum_on_cart(2)
|
| 103 |
+
massmatrix1 = Matrix([[m0 + m1 + m2, -l0*m1*cos(q1) - l0*m2*cos(q1),
|
| 104 |
+
-l1*m2*cos(q2)],
|
| 105 |
+
[-l0*m1*cos(q1) - l0*m2*cos(q1), l0**2*m1 + l0**2*m2,
|
| 106 |
+
l0*l1*m2*(sin(q1)*sin(q2) + cos(q1)*cos(q2))],
|
| 107 |
+
[-l1*m2*cos(q2),
|
| 108 |
+
l0*l1*m2*(sin(q1)*sin(q2) + cos(q1)*cos(q2)),
|
| 109 |
+
l1**2*m2]])
|
| 110 |
+
forcing1 = Matrix([[-l0*m1*u1**2*sin(q1) - l0*m2*u1**2*sin(q1) -
|
| 111 |
+
l1*m2*u2**2*sin(q2) + F],
|
| 112 |
+
[g*l0*m1*sin(q1) + g*l0*m2*sin(q1) -
|
| 113 |
+
l0*l1*m2*(sin(q1)*cos(q2) - sin(q2)*cos(q1))*u2**2],
|
| 114 |
+
[g*l1*m2*sin(q2) - l0*l1*m2*(-sin(q1)*cos(q2) +
|
| 115 |
+
sin(q2)*cos(q1))*u1**2]])
|
| 116 |
+
assert simplify(massmatrix1 - kane1.mass_matrix) == zeros(3)
|
| 117 |
+
assert simplify(forcing1 - kane1.forcing) == Matrix([0, 0, 0])
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_particle.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import symbols
|
| 2 |
+
from sympy.physics.mechanics import Point, Particle, ReferenceFrame, inertia
|
| 3 |
+
from sympy.physics.mechanics.body_base import BodyBase
|
| 4 |
+
from sympy.testing.pytest import raises, warns_deprecated_sympy
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def test_particle_default():
|
| 8 |
+
# Test default
|
| 9 |
+
p = Particle('P')
|
| 10 |
+
assert p.name == 'P'
|
| 11 |
+
assert p.mass == symbols('P_mass')
|
| 12 |
+
assert p.masscenter.name == 'P_masscenter'
|
| 13 |
+
assert p.potential_energy == 0
|
| 14 |
+
assert p.__str__() == 'P'
|
| 15 |
+
assert p.__repr__() == ("Particle('P', masscenter=P_masscenter, "
|
| 16 |
+
"mass=P_mass)")
|
| 17 |
+
raises(AttributeError, lambda: p.frame)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def test_particle():
|
| 21 |
+
# Test initializing with parameters
|
| 22 |
+
m, m2, v1, v2, v3, r, g, h = symbols('m m2 v1 v2 v3 r g h')
|
| 23 |
+
P = Point('P')
|
| 24 |
+
P2 = Point('P2')
|
| 25 |
+
p = Particle('pa', P, m)
|
| 26 |
+
assert isinstance(p, BodyBase)
|
| 27 |
+
assert p.mass == m
|
| 28 |
+
assert p.point == P
|
| 29 |
+
# Test the mass setter
|
| 30 |
+
p.mass = m2
|
| 31 |
+
assert p.mass == m2
|
| 32 |
+
# Test the point setter
|
| 33 |
+
p.point = P2
|
| 34 |
+
assert p.point == P2
|
| 35 |
+
# Test the linear momentum function
|
| 36 |
+
N = ReferenceFrame('N')
|
| 37 |
+
O = Point('O')
|
| 38 |
+
P2.set_pos(O, r * N.y)
|
| 39 |
+
P2.set_vel(N, v1 * N.x)
|
| 40 |
+
raises(TypeError, lambda: Particle(P, P, m))
|
| 41 |
+
raises(TypeError, lambda: Particle('pa', m, m))
|
| 42 |
+
assert p.linear_momentum(N) == m2 * v1 * N.x
|
| 43 |
+
assert p.angular_momentum(O, N) == -m2 * r * v1 * N.z
|
| 44 |
+
P2.set_vel(N, v2 * N.y)
|
| 45 |
+
assert p.linear_momentum(N) == m2 * v2 * N.y
|
| 46 |
+
assert p.angular_momentum(O, N) == 0
|
| 47 |
+
P2.set_vel(N, v3 * N.z)
|
| 48 |
+
assert p.linear_momentum(N) == m2 * v3 * N.z
|
| 49 |
+
assert p.angular_momentum(O, N) == m2 * r * v3 * N.x
|
| 50 |
+
P2.set_vel(N, v1 * N.x + v2 * N.y + v3 * N.z)
|
| 51 |
+
assert p.linear_momentum(N) == m2 * (v1 * N.x + v2 * N.y + v3 * N.z)
|
| 52 |
+
assert p.angular_momentum(O, N) == m2 * r * (v3 * N.x - v1 * N.z)
|
| 53 |
+
p.potential_energy = m * g * h
|
| 54 |
+
assert p.potential_energy == m * g * h
|
| 55 |
+
# TODO make the result not be system-dependent
|
| 56 |
+
assert p.kinetic_energy(
|
| 57 |
+
N) in [m2 * (v1 ** 2 + v2 ** 2 + v3 ** 2) / 2,
|
| 58 |
+
m2 * v1 ** 2 / 2 + m2 * v2 ** 2 / 2 + m2 * v3 ** 2 / 2]
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def test_parallel_axis():
|
| 62 |
+
N = ReferenceFrame('N')
|
| 63 |
+
m, a, b = symbols('m, a, b')
|
| 64 |
+
o = Point('o')
|
| 65 |
+
p = o.locatenew('p', a * N.x + b * N.y)
|
| 66 |
+
P = Particle('P', o, m)
|
| 67 |
+
Ip = P.parallel_axis(p, N)
|
| 68 |
+
Ip_expected = inertia(N, m * b ** 2, m * a ** 2, m * (a ** 2 + b ** 2),
|
| 69 |
+
ixy=-m * a * b)
|
| 70 |
+
assert Ip == Ip_expected
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def test_deprecated_set_potential_energy():
|
| 74 |
+
m, g, h = symbols('m g h')
|
| 75 |
+
P = Point('P')
|
| 76 |
+
p = Particle('pa', P, m)
|
| 77 |
+
with warns_deprecated_sympy():
|
| 78 |
+
p.set_potential_energy(m * g * h)
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_pathway.py
ADDED
|
@@ -0,0 +1,691 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the ``sympy.physics.mechanics.pathway.py`` module."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
from sympy import (
|
| 6 |
+
Rational,
|
| 7 |
+
Symbol,
|
| 8 |
+
cos,
|
| 9 |
+
pi,
|
| 10 |
+
sin,
|
| 11 |
+
sqrt,
|
| 12 |
+
)
|
| 13 |
+
from sympy.physics.mechanics import (
|
| 14 |
+
Force,
|
| 15 |
+
LinearPathway,
|
| 16 |
+
ObstacleSetPathway,
|
| 17 |
+
PathwayBase,
|
| 18 |
+
Point,
|
| 19 |
+
ReferenceFrame,
|
| 20 |
+
WrappingCylinder,
|
| 21 |
+
WrappingGeometryBase,
|
| 22 |
+
WrappingPathway,
|
| 23 |
+
WrappingSphere,
|
| 24 |
+
dynamicsymbols,
|
| 25 |
+
)
|
| 26 |
+
from sympy.simplify.simplify import simplify
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _simplify_loads(loads):
|
| 30 |
+
return [
|
| 31 |
+
load.__class__(load.location, load.vector.simplify())
|
| 32 |
+
for load in loads
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class TestLinearPathway:
|
| 37 |
+
|
| 38 |
+
def test_is_pathway_base_subclass(self):
|
| 39 |
+
assert issubclass(LinearPathway, PathwayBase)
|
| 40 |
+
|
| 41 |
+
@staticmethod
|
| 42 |
+
@pytest.mark.parametrize(
|
| 43 |
+
'args, kwargs',
|
| 44 |
+
[
|
| 45 |
+
((Point('pA'), Point('pB')), {}),
|
| 46 |
+
]
|
| 47 |
+
)
|
| 48 |
+
def test_valid_constructor(args, kwargs):
|
| 49 |
+
pointA, pointB = args
|
| 50 |
+
instance = LinearPathway(*args, **kwargs)
|
| 51 |
+
assert isinstance(instance, LinearPathway)
|
| 52 |
+
assert hasattr(instance, 'attachments')
|
| 53 |
+
assert len(instance.attachments) == 2
|
| 54 |
+
assert instance.attachments[0] is pointA
|
| 55 |
+
assert instance.attachments[1] is pointB
|
| 56 |
+
assert isinstance(instance.attachments[0], Point)
|
| 57 |
+
assert instance.attachments[0].name == 'pA'
|
| 58 |
+
assert isinstance(instance.attachments[1], Point)
|
| 59 |
+
assert instance.attachments[1].name == 'pB'
|
| 60 |
+
|
| 61 |
+
@staticmethod
|
| 62 |
+
@pytest.mark.parametrize(
|
| 63 |
+
'attachments',
|
| 64 |
+
[
|
| 65 |
+
(Point('pA'), ),
|
| 66 |
+
(Point('pA'), Point('pB'), Point('pZ')),
|
| 67 |
+
]
|
| 68 |
+
)
|
| 69 |
+
def test_invalid_attachments_incorrect_number(attachments):
|
| 70 |
+
with pytest.raises(ValueError):
|
| 71 |
+
_ = LinearPathway(*attachments)
|
| 72 |
+
|
| 73 |
+
@staticmethod
|
| 74 |
+
@pytest.mark.parametrize(
|
| 75 |
+
'attachments',
|
| 76 |
+
[
|
| 77 |
+
(None, Point('pB')),
|
| 78 |
+
(Point('pA'), None),
|
| 79 |
+
]
|
| 80 |
+
)
|
| 81 |
+
def test_invalid_attachments_not_point(attachments):
|
| 82 |
+
with pytest.raises(TypeError):
|
| 83 |
+
_ = LinearPathway(*attachments)
|
| 84 |
+
|
| 85 |
+
@pytest.fixture(autouse=True)
|
| 86 |
+
def _linear_pathway_fixture(self):
|
| 87 |
+
self.N = ReferenceFrame('N')
|
| 88 |
+
self.pA = Point('pA')
|
| 89 |
+
self.pB = Point('pB')
|
| 90 |
+
self.pathway = LinearPathway(self.pA, self.pB)
|
| 91 |
+
self.q1 = dynamicsymbols('q1')
|
| 92 |
+
self.q2 = dynamicsymbols('q2')
|
| 93 |
+
self.q3 = dynamicsymbols('q3')
|
| 94 |
+
self.q1d = dynamicsymbols('q1', 1)
|
| 95 |
+
self.q2d = dynamicsymbols('q2', 1)
|
| 96 |
+
self.q3d = dynamicsymbols('q3', 1)
|
| 97 |
+
self.F = Symbol('F')
|
| 98 |
+
|
| 99 |
+
def test_properties_are_immutable(self):
|
| 100 |
+
instance = LinearPathway(self.pA, self.pB)
|
| 101 |
+
with pytest.raises(AttributeError):
|
| 102 |
+
instance.attachments = None
|
| 103 |
+
with pytest.raises(TypeError):
|
| 104 |
+
instance.attachments[0] = None
|
| 105 |
+
with pytest.raises(TypeError):
|
| 106 |
+
instance.attachments[1] = None
|
| 107 |
+
|
| 108 |
+
def test_repr(self):
|
| 109 |
+
pathway = LinearPathway(self.pA, self.pB)
|
| 110 |
+
expected = 'LinearPathway(pA, pB)'
|
| 111 |
+
assert repr(pathway) == expected
|
| 112 |
+
|
| 113 |
+
def test_static_pathway_length(self):
|
| 114 |
+
self.pB.set_pos(self.pA, 2*self.N.x)
|
| 115 |
+
assert self.pathway.length == 2
|
| 116 |
+
|
| 117 |
+
def test_static_pathway_extension_velocity(self):
|
| 118 |
+
self.pB.set_pos(self.pA, 2*self.N.x)
|
| 119 |
+
assert self.pathway.extension_velocity == 0
|
| 120 |
+
|
| 121 |
+
def test_static_pathway_to_loads(self):
|
| 122 |
+
self.pB.set_pos(self.pA, 2*self.N.x)
|
| 123 |
+
expected = [
|
| 124 |
+
(self.pA, - self.F*self.N.x),
|
| 125 |
+
(self.pB, self.F*self.N.x),
|
| 126 |
+
]
|
| 127 |
+
assert self.pathway.to_loads(self.F) == expected
|
| 128 |
+
|
| 129 |
+
def test_2D_pathway_length(self):
|
| 130 |
+
self.pB.set_pos(self.pA, 2*self.q1*self.N.x)
|
| 131 |
+
expected = 2*sqrt(self.q1**2)
|
| 132 |
+
assert self.pathway.length == expected
|
| 133 |
+
|
| 134 |
+
def test_2D_pathway_extension_velocity(self):
|
| 135 |
+
self.pB.set_pos(self.pA, 2*self.q1*self.N.x)
|
| 136 |
+
expected = 2*sqrt(self.q1**2)*self.q1d/self.q1
|
| 137 |
+
assert self.pathway.extension_velocity == expected
|
| 138 |
+
|
| 139 |
+
def test_2D_pathway_to_loads(self):
|
| 140 |
+
self.pB.set_pos(self.pA, 2*self.q1*self.N.x)
|
| 141 |
+
expected = [
|
| 142 |
+
(self.pA, - self.F*(self.q1 / sqrt(self.q1**2))*self.N.x),
|
| 143 |
+
(self.pB, self.F*(self.q1 / sqrt(self.q1**2))*self.N.x),
|
| 144 |
+
]
|
| 145 |
+
assert self.pathway.to_loads(self.F) == expected
|
| 146 |
+
|
| 147 |
+
def test_3D_pathway_length(self):
|
| 148 |
+
self.pB.set_pos(
|
| 149 |
+
self.pA,
|
| 150 |
+
self.q1*self.N.x - self.q2*self.N.y + 2*self.q3*self.N.z,
|
| 151 |
+
)
|
| 152 |
+
expected = sqrt(self.q1**2 + self.q2**2 + 4*self.q3**2)
|
| 153 |
+
assert simplify(self.pathway.length - expected) == 0
|
| 154 |
+
|
| 155 |
+
def test_3D_pathway_extension_velocity(self):
|
| 156 |
+
self.pB.set_pos(
|
| 157 |
+
self.pA,
|
| 158 |
+
self.q1*self.N.x - self.q2*self.N.y + 2*self.q3*self.N.z,
|
| 159 |
+
)
|
| 160 |
+
length = sqrt(self.q1**2 + self.q2**2 + 4*self.q3**2)
|
| 161 |
+
expected = (
|
| 162 |
+
self.q1*self.q1d/length
|
| 163 |
+
+ self.q2*self.q2d/length
|
| 164 |
+
+ 4*self.q3*self.q3d/length
|
| 165 |
+
)
|
| 166 |
+
assert simplify(self.pathway.extension_velocity - expected) == 0
|
| 167 |
+
|
| 168 |
+
def test_3D_pathway_to_loads(self):
|
| 169 |
+
self.pB.set_pos(
|
| 170 |
+
self.pA,
|
| 171 |
+
self.q1*self.N.x - self.q2*self.N.y + 2*self.q3*self.N.z,
|
| 172 |
+
)
|
| 173 |
+
length = sqrt(self.q1**2 + self.q2**2 + 4*self.q3**2)
|
| 174 |
+
pO_force = (
|
| 175 |
+
- self.F*self.q1*self.N.x/length
|
| 176 |
+
+ self.F*self.q2*self.N.y/length
|
| 177 |
+
- 2*self.F*self.q3*self.N.z/length
|
| 178 |
+
)
|
| 179 |
+
pI_force = (
|
| 180 |
+
self.F*self.q1*self.N.x/length
|
| 181 |
+
- self.F*self.q2*self.N.y/length
|
| 182 |
+
+ 2*self.F*self.q3*self.N.z/length
|
| 183 |
+
)
|
| 184 |
+
expected = [
|
| 185 |
+
(self.pA, pO_force),
|
| 186 |
+
(self.pB, pI_force),
|
| 187 |
+
]
|
| 188 |
+
assert self.pathway.to_loads(self.F) == expected
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
class TestObstacleSetPathway:
|
| 192 |
+
|
| 193 |
+
def test_is_pathway_base_subclass(self):
|
| 194 |
+
assert issubclass(ObstacleSetPathway, PathwayBase)
|
| 195 |
+
|
| 196 |
+
@staticmethod
|
| 197 |
+
@pytest.mark.parametrize(
|
| 198 |
+
'num_attachments, attachments',
|
| 199 |
+
[
|
| 200 |
+
(3, [Point(name) for name in ('pO', 'pA', 'pI')]),
|
| 201 |
+
(4, [Point(name) for name in ('pO', 'pA', 'pB', 'pI')]),
|
| 202 |
+
(5, [Point(name) for name in ('pO', 'pA', 'pB', 'pC', 'pI')]),
|
| 203 |
+
(6, [Point(name) for name in ('pO', 'pA', 'pB', 'pC', 'pD', 'pI')]),
|
| 204 |
+
]
|
| 205 |
+
)
|
| 206 |
+
def test_valid_constructor(num_attachments, attachments):
|
| 207 |
+
instance = ObstacleSetPathway(*attachments)
|
| 208 |
+
assert isinstance(instance, ObstacleSetPathway)
|
| 209 |
+
assert hasattr(instance, 'attachments')
|
| 210 |
+
assert len(instance.attachments) == num_attachments
|
| 211 |
+
for attachment in instance.attachments:
|
| 212 |
+
assert isinstance(attachment, Point)
|
| 213 |
+
|
| 214 |
+
@staticmethod
|
| 215 |
+
@pytest.mark.parametrize(
|
| 216 |
+
'attachments',
|
| 217 |
+
[[Point('pO')], [Point('pO'), Point('pI')]],
|
| 218 |
+
)
|
| 219 |
+
def test_invalid_constructor_attachments_incorrect_number(attachments):
|
| 220 |
+
with pytest.raises(ValueError):
|
| 221 |
+
_ = ObstacleSetPathway(*attachments)
|
| 222 |
+
|
| 223 |
+
@staticmethod
|
| 224 |
+
@pytest.mark.parametrize(
|
| 225 |
+
'attachments',
|
| 226 |
+
[
|
| 227 |
+
(None, Point('pA'), Point('pI')),
|
| 228 |
+
(Point('pO'), None, Point('pI')),
|
| 229 |
+
(Point('pO'), Point('pA'), None),
|
| 230 |
+
]
|
| 231 |
+
)
|
| 232 |
+
def test_invalid_constructor_attachments_not_point(attachments):
|
| 233 |
+
with pytest.raises(TypeError):
|
| 234 |
+
_ = WrappingPathway(*attachments) # type: ignore
|
| 235 |
+
|
| 236 |
+
def test_properties_are_immutable(self):
|
| 237 |
+
pathway = ObstacleSetPathway(Point('pO'), Point('pA'), Point('pI'))
|
| 238 |
+
with pytest.raises(AttributeError):
|
| 239 |
+
pathway.attachments = None # type: ignore
|
| 240 |
+
with pytest.raises(TypeError):
|
| 241 |
+
pathway.attachments[0] = None # type: ignore
|
| 242 |
+
with pytest.raises(TypeError):
|
| 243 |
+
pathway.attachments[1] = None # type: ignore
|
| 244 |
+
with pytest.raises(TypeError):
|
| 245 |
+
pathway.attachments[-1] = None # type: ignore
|
| 246 |
+
|
| 247 |
+
@staticmethod
|
| 248 |
+
@pytest.mark.parametrize(
|
| 249 |
+
'attachments, expected',
|
| 250 |
+
[
|
| 251 |
+
(
|
| 252 |
+
[Point(name) for name in ('pO', 'pA', 'pI')],
|
| 253 |
+
'ObstacleSetPathway(pO, pA, pI)'
|
| 254 |
+
),
|
| 255 |
+
(
|
| 256 |
+
[Point(name) for name in ('pO', 'pA', 'pB', 'pI')],
|
| 257 |
+
'ObstacleSetPathway(pO, pA, pB, pI)'
|
| 258 |
+
),
|
| 259 |
+
(
|
| 260 |
+
[Point(name) for name in ('pO', 'pA', 'pB', 'pC', 'pI')],
|
| 261 |
+
'ObstacleSetPathway(pO, pA, pB, pC, pI)'
|
| 262 |
+
),
|
| 263 |
+
]
|
| 264 |
+
)
|
| 265 |
+
def test_repr(attachments, expected):
|
| 266 |
+
pathway = ObstacleSetPathway(*attachments)
|
| 267 |
+
assert repr(pathway) == expected
|
| 268 |
+
|
| 269 |
+
@pytest.fixture(autouse=True)
|
| 270 |
+
def _obstacle_set_pathway_fixture(self):
|
| 271 |
+
self.N = ReferenceFrame('N')
|
| 272 |
+
self.pO = Point('pO')
|
| 273 |
+
self.pI = Point('pI')
|
| 274 |
+
self.pA = Point('pA')
|
| 275 |
+
self.pB = Point('pB')
|
| 276 |
+
self.q = dynamicsymbols('q')
|
| 277 |
+
self.qd = dynamicsymbols('q', 1)
|
| 278 |
+
self.F = Symbol('F')
|
| 279 |
+
|
| 280 |
+
def test_static_pathway_length(self):
|
| 281 |
+
self.pA.set_pos(self.pO, self.N.x)
|
| 282 |
+
self.pB.set_pos(self.pO, self.N.y)
|
| 283 |
+
self.pI.set_pos(self.pO, self.N.z)
|
| 284 |
+
pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI)
|
| 285 |
+
assert pathway.length == 1 + 2 * sqrt(2)
|
| 286 |
+
|
| 287 |
+
def test_static_pathway_extension_velocity(self):
|
| 288 |
+
self.pA.set_pos(self.pO, self.N.x)
|
| 289 |
+
self.pB.set_pos(self.pO, self.N.y)
|
| 290 |
+
self.pI.set_pos(self.pO, self.N.z)
|
| 291 |
+
pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI)
|
| 292 |
+
assert pathway.extension_velocity == 0
|
| 293 |
+
|
| 294 |
+
def test_static_pathway_to_loads(self):
|
| 295 |
+
self.pA.set_pos(self.pO, self.N.x)
|
| 296 |
+
self.pB.set_pos(self.pO, self.N.y)
|
| 297 |
+
self.pI.set_pos(self.pO, self.N.z)
|
| 298 |
+
pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI)
|
| 299 |
+
expected = [
|
| 300 |
+
Force(self.pO, -self.F * self.N.x),
|
| 301 |
+
Force(self.pA, self.F * self.N.x),
|
| 302 |
+
Force(self.pA, self.F * sqrt(2) / 2 * (self.N.x - self.N.y)),
|
| 303 |
+
Force(self.pB, self.F * sqrt(2) / 2 * (self.N.y - self.N.x)),
|
| 304 |
+
Force(self.pB, self.F * sqrt(2) / 2 * (self.N.y - self.N.z)),
|
| 305 |
+
Force(self.pI, self.F * sqrt(2) / 2 * (self.N.z - self.N.y)),
|
| 306 |
+
]
|
| 307 |
+
assert pathway.to_loads(self.F) == expected
|
| 308 |
+
|
| 309 |
+
def test_2D_pathway_length(self):
|
| 310 |
+
self.pA.set_pos(self.pO, -(self.N.x + self.N.y))
|
| 311 |
+
self.pB.set_pos(
|
| 312 |
+
self.pO, cos(self.q) * self.N.x - (sin(self.q) + 1) * self.N.y
|
| 313 |
+
)
|
| 314 |
+
self.pI.set_pos(
|
| 315 |
+
self.pO, sin(self.q) * self.N.x + (cos(self.q) - 1) * self.N.y
|
| 316 |
+
)
|
| 317 |
+
pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI)
|
| 318 |
+
expected = 2 * sqrt(2) + sqrt(2 + 2*cos(self.q))
|
| 319 |
+
assert (pathway.length - expected).simplify() == 0
|
| 320 |
+
|
| 321 |
+
def test_2D_pathway_extension_velocity(self):
|
| 322 |
+
self.pA.set_pos(self.pO, -(self.N.x + self.N.y))
|
| 323 |
+
self.pB.set_pos(
|
| 324 |
+
self.pO, cos(self.q) * self.N.x - (sin(self.q) + 1) * self.N.y
|
| 325 |
+
)
|
| 326 |
+
self.pI.set_pos(
|
| 327 |
+
self.pO, sin(self.q) * self.N.x + (cos(self.q) - 1) * self.N.y
|
| 328 |
+
)
|
| 329 |
+
pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI)
|
| 330 |
+
expected = - (sqrt(2) * sin(self.q) * self.qd) / (2 * sqrt(cos(self.q) + 1))
|
| 331 |
+
assert (pathway.extension_velocity - expected).simplify() == 0
|
| 332 |
+
|
| 333 |
+
def test_2D_pathway_to_loads(self):
|
| 334 |
+
self.pA.set_pos(self.pO, -(self.N.x + self.N.y))
|
| 335 |
+
self.pB.set_pos(
|
| 336 |
+
self.pO, cos(self.q) * self.N.x - (sin(self.q) + 1) * self.N.y
|
| 337 |
+
)
|
| 338 |
+
self.pI.set_pos(
|
| 339 |
+
self.pO, sin(self.q) * self.N.x + (cos(self.q) - 1) * self.N.y
|
| 340 |
+
)
|
| 341 |
+
pathway = ObstacleSetPathway(self.pO, self.pA, self.pB, self.pI)
|
| 342 |
+
pO_pA_force_vec = sqrt(2) / 2 * (self.N.x + self.N.y)
|
| 343 |
+
pA_pB_force_vec = (
|
| 344 |
+
- sqrt(2 * cos(self.q) + 2) / 2 * self.N.x
|
| 345 |
+
+ sqrt(2) * sin(self.q) / (2 * sqrt(cos(self.q) + 1)) * self.N.y
|
| 346 |
+
)
|
| 347 |
+
pB_pI_force_vec = cos(self.q + pi/4) * self.N.x - sin(self.q + pi/4) * self.N.y
|
| 348 |
+
expected = [
|
| 349 |
+
Force(self.pO, self.F * pO_pA_force_vec),
|
| 350 |
+
Force(self.pA, -self.F * pO_pA_force_vec),
|
| 351 |
+
Force(self.pA, self.F * pA_pB_force_vec),
|
| 352 |
+
Force(self.pB, -self.F * pA_pB_force_vec),
|
| 353 |
+
Force(self.pB, self.F * pB_pI_force_vec),
|
| 354 |
+
Force(self.pI, -self.F * pB_pI_force_vec),
|
| 355 |
+
]
|
| 356 |
+
assert _simplify_loads(pathway.to_loads(self.F)) == expected
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
class TestWrappingPathway:
|
| 360 |
+
|
| 361 |
+
def test_is_pathway_base_subclass(self):
|
| 362 |
+
assert issubclass(WrappingPathway, PathwayBase)
|
| 363 |
+
|
| 364 |
+
@pytest.fixture(autouse=True)
|
| 365 |
+
def _wrapping_pathway_fixture(self):
|
| 366 |
+
self.pA = Point('pA')
|
| 367 |
+
self.pB = Point('pB')
|
| 368 |
+
self.r = Symbol('r', positive=True)
|
| 369 |
+
self.pO = Point('pO')
|
| 370 |
+
self.N = ReferenceFrame('N')
|
| 371 |
+
self.ax = self.N.z
|
| 372 |
+
self.sphere = WrappingSphere(self.r, self.pO)
|
| 373 |
+
self.cylinder = WrappingCylinder(self.r, self.pO, self.ax)
|
| 374 |
+
self.pathway = WrappingPathway(self.pA, self.pB, self.cylinder)
|
| 375 |
+
self.F = Symbol('F')
|
| 376 |
+
|
| 377 |
+
def test_valid_constructor(self):
|
| 378 |
+
instance = WrappingPathway(self.pA, self.pB, self.cylinder)
|
| 379 |
+
assert isinstance(instance, WrappingPathway)
|
| 380 |
+
assert hasattr(instance, 'attachments')
|
| 381 |
+
assert len(instance.attachments) == 2
|
| 382 |
+
assert isinstance(instance.attachments[0], Point)
|
| 383 |
+
assert instance.attachments[0] == self.pA
|
| 384 |
+
assert isinstance(instance.attachments[1], Point)
|
| 385 |
+
assert instance.attachments[1] == self.pB
|
| 386 |
+
assert hasattr(instance, 'geometry')
|
| 387 |
+
assert isinstance(instance.geometry, WrappingGeometryBase)
|
| 388 |
+
assert instance.geometry == self.cylinder
|
| 389 |
+
|
| 390 |
+
@pytest.mark.parametrize(
|
| 391 |
+
'attachments',
|
| 392 |
+
[
|
| 393 |
+
(Point('pA'), ),
|
| 394 |
+
(Point('pA'), Point('pB'), Point('pZ')),
|
| 395 |
+
]
|
| 396 |
+
)
|
| 397 |
+
def test_invalid_constructor_attachments_incorrect_number(self, attachments):
|
| 398 |
+
with pytest.raises(TypeError):
|
| 399 |
+
_ = WrappingPathway(*attachments, self.cylinder)
|
| 400 |
+
|
| 401 |
+
@staticmethod
|
| 402 |
+
@pytest.mark.parametrize(
|
| 403 |
+
'attachments',
|
| 404 |
+
[
|
| 405 |
+
(None, Point('pB')),
|
| 406 |
+
(Point('pA'), None),
|
| 407 |
+
]
|
| 408 |
+
)
|
| 409 |
+
def test_invalid_constructor_attachments_not_point(attachments):
|
| 410 |
+
with pytest.raises(TypeError):
|
| 411 |
+
_ = WrappingPathway(*attachments)
|
| 412 |
+
|
| 413 |
+
def test_invalid_constructor_geometry_is_not_supplied(self):
|
| 414 |
+
with pytest.raises(TypeError):
|
| 415 |
+
_ = WrappingPathway(self.pA, self.pB)
|
| 416 |
+
|
| 417 |
+
@pytest.mark.parametrize(
|
| 418 |
+
'geometry',
|
| 419 |
+
[
|
| 420 |
+
Symbol('r'),
|
| 421 |
+
dynamicsymbols('q'),
|
| 422 |
+
ReferenceFrame('N'),
|
| 423 |
+
ReferenceFrame('N').x,
|
| 424 |
+
]
|
| 425 |
+
)
|
| 426 |
+
def test_invalid_geometry_not_geometry(self, geometry):
|
| 427 |
+
with pytest.raises(TypeError):
|
| 428 |
+
_ = WrappingPathway(self.pA, self.pB, geometry)
|
| 429 |
+
|
| 430 |
+
def test_attachments_property_is_immutable(self):
|
| 431 |
+
with pytest.raises(TypeError):
|
| 432 |
+
self.pathway.attachments[0] = self.pB
|
| 433 |
+
with pytest.raises(TypeError):
|
| 434 |
+
self.pathway.attachments[1] = self.pA
|
| 435 |
+
|
| 436 |
+
def test_geometry_property_is_immutable(self):
|
| 437 |
+
with pytest.raises(AttributeError):
|
| 438 |
+
self.pathway.geometry = None
|
| 439 |
+
|
| 440 |
+
def test_repr(self):
|
| 441 |
+
expected = (
|
| 442 |
+
f'WrappingPathway(pA, pB, '
|
| 443 |
+
f'geometry={self.cylinder!r})'
|
| 444 |
+
)
|
| 445 |
+
assert repr(self.pathway) == expected
|
| 446 |
+
|
| 447 |
+
@staticmethod
|
| 448 |
+
def _expand_pos_to_vec(pos, frame):
|
| 449 |
+
return sum(mag*unit for (mag, unit) in zip(pos, frame))
|
| 450 |
+
|
| 451 |
+
@pytest.mark.parametrize(
|
| 452 |
+
'pA_vec, pB_vec, factor',
|
| 453 |
+
[
|
| 454 |
+
((1, 0, 0), (0, 1, 0), pi/2),
|
| 455 |
+
((0, 1, 0), (sqrt(2)/2, -sqrt(2)/2, 0), 3*pi/4),
|
| 456 |
+
((1, 0, 0), (Rational(1, 2), sqrt(3)/2, 0), pi/3),
|
| 457 |
+
]
|
| 458 |
+
)
|
| 459 |
+
def test_static_pathway_on_sphere_length(self, pA_vec, pB_vec, factor):
|
| 460 |
+
pA_vec = self._expand_pos_to_vec(pA_vec, self.N)
|
| 461 |
+
pB_vec = self._expand_pos_to_vec(pB_vec, self.N)
|
| 462 |
+
self.pA.set_pos(self.pO, self.r*pA_vec)
|
| 463 |
+
self.pB.set_pos(self.pO, self.r*pB_vec)
|
| 464 |
+
pathway = WrappingPathway(self.pA, self.pB, self.sphere)
|
| 465 |
+
expected = factor*self.r
|
| 466 |
+
assert simplify(pathway.length - expected) == 0
|
| 467 |
+
|
| 468 |
+
@pytest.mark.parametrize(
|
| 469 |
+
'pA_vec, pB_vec, factor',
|
| 470 |
+
[
|
| 471 |
+
((1, 0, 0), (0, 1, 0), Rational(1, 2)*pi),
|
| 472 |
+
((1, 0, 0), (-1, 0, 0), pi),
|
| 473 |
+
((-1, 0, 0), (1, 0, 0), pi),
|
| 474 |
+
((0, 1, 0), (sqrt(2)/2, -sqrt(2)/2, 0), 5*pi/4),
|
| 475 |
+
((1, 0, 0), (Rational(1, 2), sqrt(3)/2, 0), pi/3),
|
| 476 |
+
(
|
| 477 |
+
(0, 1, 0),
|
| 478 |
+
(sqrt(2)*Rational(1, 2), -sqrt(2)*Rational(1, 2), 1),
|
| 479 |
+
sqrt(1 + (Rational(5, 4)*pi)**2),
|
| 480 |
+
),
|
| 481 |
+
(
|
| 482 |
+
(1, 0, 0),
|
| 483 |
+
(Rational(1, 2), sqrt(3)*Rational(1, 2), 1),
|
| 484 |
+
sqrt(1 + (Rational(1, 3)*pi)**2),
|
| 485 |
+
),
|
| 486 |
+
]
|
| 487 |
+
)
|
| 488 |
+
def test_static_pathway_on_cylinder_length(self, pA_vec, pB_vec, factor):
|
| 489 |
+
pA_vec = self._expand_pos_to_vec(pA_vec, self.N)
|
| 490 |
+
pB_vec = self._expand_pos_to_vec(pB_vec, self.N)
|
| 491 |
+
self.pA.set_pos(self.pO, self.r*pA_vec)
|
| 492 |
+
self.pB.set_pos(self.pO, self.r*pB_vec)
|
| 493 |
+
pathway = WrappingPathway(self.pA, self.pB, self.cylinder)
|
| 494 |
+
expected = factor*sqrt(self.r**2)
|
| 495 |
+
assert simplify(pathway.length - expected) == 0
|
| 496 |
+
|
| 497 |
+
@pytest.mark.parametrize(
|
| 498 |
+
'pA_vec, pB_vec',
|
| 499 |
+
[
|
| 500 |
+
((1, 0, 0), (0, 1, 0)),
|
| 501 |
+
((0, 1, 0), (sqrt(2)*Rational(1, 2), -sqrt(2)*Rational(1, 2), 0)),
|
| 502 |
+
((1, 0, 0), (Rational(1, 2), sqrt(3)*Rational(1, 2), 0)),
|
| 503 |
+
]
|
| 504 |
+
)
|
| 505 |
+
def test_static_pathway_on_sphere_extension_velocity(self, pA_vec, pB_vec):
|
| 506 |
+
pA_vec = self._expand_pos_to_vec(pA_vec, self.N)
|
| 507 |
+
pB_vec = self._expand_pos_to_vec(pB_vec, self.N)
|
| 508 |
+
self.pA.set_pos(self.pO, self.r*pA_vec)
|
| 509 |
+
self.pB.set_pos(self.pO, self.r*pB_vec)
|
| 510 |
+
pathway = WrappingPathway(self.pA, self.pB, self.sphere)
|
| 511 |
+
assert pathway.extension_velocity == 0
|
| 512 |
+
|
| 513 |
+
@pytest.mark.parametrize(
|
| 514 |
+
'pA_vec, pB_vec',
|
| 515 |
+
[
|
| 516 |
+
((1, 0, 0), (0, 1, 0)),
|
| 517 |
+
((1, 0, 0), (-1, 0, 0)),
|
| 518 |
+
((-1, 0, 0), (1, 0, 0)),
|
| 519 |
+
((0, 1, 0), (sqrt(2)/2, -sqrt(2)/2, 0)),
|
| 520 |
+
((1, 0, 0), (Rational(1, 2), sqrt(3)/2, 0)),
|
| 521 |
+
((0, 1, 0), (sqrt(2)*Rational(1, 2), -sqrt(2)/2, 1)),
|
| 522 |
+
((1, 0, 0), (Rational(1, 2), sqrt(3)/2, 1)),
|
| 523 |
+
]
|
| 524 |
+
)
|
| 525 |
+
def test_static_pathway_on_cylinder_extension_velocity(self, pA_vec, pB_vec):
|
| 526 |
+
pA_vec = self._expand_pos_to_vec(pA_vec, self.N)
|
| 527 |
+
pB_vec = self._expand_pos_to_vec(pB_vec, self.N)
|
| 528 |
+
self.pA.set_pos(self.pO, self.r*pA_vec)
|
| 529 |
+
self.pB.set_pos(self.pO, self.r*pB_vec)
|
| 530 |
+
pathway = WrappingPathway(self.pA, self.pB, self.cylinder)
|
| 531 |
+
assert pathway.extension_velocity == 0
|
| 532 |
+
|
| 533 |
+
@pytest.mark.parametrize(
|
| 534 |
+
'pA_vec, pB_vec, pA_vec_expected, pB_vec_expected, pO_vec_expected',
|
| 535 |
+
(
|
| 536 |
+
((1, 0, 0), (0, 1, 0), (0, 1, 0), (1, 0, 0), (-1, -1, 0)),
|
| 537 |
+
(
|
| 538 |
+
(0, 1, 0),
|
| 539 |
+
(sqrt(2)/2, -sqrt(2)/2, 0),
|
| 540 |
+
(1, 0, 0),
|
| 541 |
+
(sqrt(2)/2, sqrt(2)/2, 0),
|
| 542 |
+
(-1 - sqrt(2)/2, -sqrt(2)/2, 0)
|
| 543 |
+
),
|
| 544 |
+
(
|
| 545 |
+
(1, 0, 0),
|
| 546 |
+
(Rational(1, 2), sqrt(3)/2, 0),
|
| 547 |
+
(0, 1, 0),
|
| 548 |
+
(sqrt(3)/2, -Rational(1, 2), 0),
|
| 549 |
+
(-sqrt(3)/2, Rational(1, 2) - 1, 0),
|
| 550 |
+
),
|
| 551 |
+
)
|
| 552 |
+
)
|
| 553 |
+
def test_static_pathway_on_sphere_to_loads(
|
| 554 |
+
self,
|
| 555 |
+
pA_vec,
|
| 556 |
+
pB_vec,
|
| 557 |
+
pA_vec_expected,
|
| 558 |
+
pB_vec_expected,
|
| 559 |
+
pO_vec_expected,
|
| 560 |
+
):
|
| 561 |
+
pA_vec = self._expand_pos_to_vec(pA_vec, self.N)
|
| 562 |
+
pB_vec = self._expand_pos_to_vec(pB_vec, self.N)
|
| 563 |
+
self.pA.set_pos(self.pO, self.r*pA_vec)
|
| 564 |
+
self.pB.set_pos(self.pO, self.r*pB_vec)
|
| 565 |
+
pathway = WrappingPathway(self.pA, self.pB, self.sphere)
|
| 566 |
+
|
| 567 |
+
pA_vec_expected = sum(
|
| 568 |
+
mag*unit for (mag, unit) in zip(pA_vec_expected, self.N)
|
| 569 |
+
)
|
| 570 |
+
pB_vec_expected = sum(
|
| 571 |
+
mag*unit for (mag, unit) in zip(pB_vec_expected, self.N)
|
| 572 |
+
)
|
| 573 |
+
pO_vec_expected = sum(
|
| 574 |
+
mag*unit for (mag, unit) in zip(pO_vec_expected, self.N)
|
| 575 |
+
)
|
| 576 |
+
expected = [
|
| 577 |
+
Force(self.pA, self.F*(self.r**3/sqrt(self.r**6))*pA_vec_expected),
|
| 578 |
+
Force(self.pB, self.F*(self.r**3/sqrt(self.r**6))*pB_vec_expected),
|
| 579 |
+
Force(self.pO, self.F*(self.r**3/sqrt(self.r**6))*pO_vec_expected),
|
| 580 |
+
]
|
| 581 |
+
assert pathway.to_loads(self.F) == expected
|
| 582 |
+
|
| 583 |
+
@pytest.mark.parametrize(
|
| 584 |
+
'pA_vec, pB_vec, pA_vec_expected, pB_vec_expected, pO_vec_expected',
|
| 585 |
+
(
|
| 586 |
+
((1, 0, 0), (0, 1, 0), (0, 1, 0), (1, 0, 0), (-1, -1, 0)),
|
| 587 |
+
((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, 1, 0), (0, -2, 0)),
|
| 588 |
+
((-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, -1, 0), (0, 2, 0)),
|
| 589 |
+
(
|
| 590 |
+
(0, 1, 0),
|
| 591 |
+
(sqrt(2)/2, -sqrt(2)/2, 0),
|
| 592 |
+
(-1, 0, 0),
|
| 593 |
+
(-sqrt(2)/2, -sqrt(2)/2, 0),
|
| 594 |
+
(1 + sqrt(2)/2, sqrt(2)/2, 0)
|
| 595 |
+
),
|
| 596 |
+
(
|
| 597 |
+
(1, 0, 0),
|
| 598 |
+
(Rational(1, 2), sqrt(3)/2, 0),
|
| 599 |
+
(0, 1, 0),
|
| 600 |
+
(sqrt(3)/2, -Rational(1, 2), 0),
|
| 601 |
+
(-sqrt(3)/2, Rational(1, 2) - 1, 0),
|
| 602 |
+
),
|
| 603 |
+
(
|
| 604 |
+
(1, 0, 0),
|
| 605 |
+
(sqrt(2)/2, sqrt(2)/2, 0),
|
| 606 |
+
(0, 1, 0),
|
| 607 |
+
(sqrt(2)/2, -sqrt(2)/2, 0),
|
| 608 |
+
(-sqrt(2)/2, sqrt(2)/2 - 1, 0),
|
| 609 |
+
),
|
| 610 |
+
((0, 1, 0), (0, 1, 1), (0, 0, 1), (0, 0, -1), (0, 0, 0)),
|
| 611 |
+
(
|
| 612 |
+
(0, 1, 0),
|
| 613 |
+
(sqrt(2)/2, -sqrt(2)/2, 1),
|
| 614 |
+
(-5*pi/sqrt(16 + 25*pi**2), 0, 4/sqrt(16 + 25*pi**2)),
|
| 615 |
+
(
|
| 616 |
+
-5*sqrt(2)*pi/(2*sqrt(16 + 25*pi**2)),
|
| 617 |
+
-5*sqrt(2)*pi/(2*sqrt(16 + 25*pi**2)),
|
| 618 |
+
-4/sqrt(16 + 25*pi**2),
|
| 619 |
+
),
|
| 620 |
+
(
|
| 621 |
+
5*(sqrt(2) + 2)*pi/(2*sqrt(16 + 25*pi**2)),
|
| 622 |
+
5*sqrt(2)*pi/(2*sqrt(16 + 25*pi**2)),
|
| 623 |
+
0,
|
| 624 |
+
),
|
| 625 |
+
),
|
| 626 |
+
)
|
| 627 |
+
)
|
| 628 |
+
def test_static_pathway_on_cylinder_to_loads(
|
| 629 |
+
self,
|
| 630 |
+
pA_vec,
|
| 631 |
+
pB_vec,
|
| 632 |
+
pA_vec_expected,
|
| 633 |
+
pB_vec_expected,
|
| 634 |
+
pO_vec_expected,
|
| 635 |
+
):
|
| 636 |
+
pA_vec = self._expand_pos_to_vec(pA_vec, self.N)
|
| 637 |
+
pB_vec = self._expand_pos_to_vec(pB_vec, self.N)
|
| 638 |
+
self.pA.set_pos(self.pO, self.r*pA_vec)
|
| 639 |
+
self.pB.set_pos(self.pO, self.r*pB_vec)
|
| 640 |
+
pathway = WrappingPathway(self.pA, self.pB, self.cylinder)
|
| 641 |
+
|
| 642 |
+
pA_force_expected = self.F*self._expand_pos_to_vec(pA_vec_expected,
|
| 643 |
+
self.N)
|
| 644 |
+
pB_force_expected = self.F*self._expand_pos_to_vec(pB_vec_expected,
|
| 645 |
+
self.N)
|
| 646 |
+
pO_force_expected = self.F*self._expand_pos_to_vec(pO_vec_expected,
|
| 647 |
+
self.N)
|
| 648 |
+
expected = [
|
| 649 |
+
Force(self.pA, pA_force_expected),
|
| 650 |
+
Force(self.pB, pB_force_expected),
|
| 651 |
+
Force(self.pO, pO_force_expected),
|
| 652 |
+
]
|
| 653 |
+
assert _simplify_loads(pathway.to_loads(self.F)) == expected
|
| 654 |
+
|
| 655 |
+
def test_2D_pathway_on_cylinder_length(self):
|
| 656 |
+
q = dynamicsymbols('q')
|
| 657 |
+
pA_pos = self.r*self.N.x
|
| 658 |
+
pB_pos = self.r*(cos(q)*self.N.x + sin(q)*self.N.y)
|
| 659 |
+
self.pA.set_pos(self.pO, pA_pos)
|
| 660 |
+
self.pB.set_pos(self.pO, pB_pos)
|
| 661 |
+
expected = self.r*sqrt(q**2)
|
| 662 |
+
assert simplify(self.pathway.length - expected) == 0
|
| 663 |
+
|
| 664 |
+
def test_2D_pathway_on_cylinder_extension_velocity(self):
|
| 665 |
+
q = dynamicsymbols('q')
|
| 666 |
+
qd = dynamicsymbols('q', 1)
|
| 667 |
+
pA_pos = self.r*self.N.x
|
| 668 |
+
pB_pos = self.r*(cos(q)*self.N.x + sin(q)*self.N.y)
|
| 669 |
+
self.pA.set_pos(self.pO, pA_pos)
|
| 670 |
+
self.pB.set_pos(self.pO, pB_pos)
|
| 671 |
+
expected = self.r*(sqrt(q**2)/q)*qd
|
| 672 |
+
assert simplify(self.pathway.extension_velocity - expected) == 0
|
| 673 |
+
|
| 674 |
+
def test_2D_pathway_on_cylinder_to_loads(self):
|
| 675 |
+
q = dynamicsymbols('q')
|
| 676 |
+
pA_pos = self.r*self.N.x
|
| 677 |
+
pB_pos = self.r*(cos(q)*self.N.x + sin(q)*self.N.y)
|
| 678 |
+
self.pA.set_pos(self.pO, pA_pos)
|
| 679 |
+
self.pB.set_pos(self.pO, pB_pos)
|
| 680 |
+
|
| 681 |
+
pA_force = self.F*self.N.y
|
| 682 |
+
pB_force = self.F*(sin(q)*self.N.x - cos(q)*self.N.y)
|
| 683 |
+
pO_force = self.F*(-sin(q)*self.N.x + (cos(q) - 1)*self.N.y)
|
| 684 |
+
expected = [
|
| 685 |
+
Force(self.pA, pA_force),
|
| 686 |
+
Force(self.pB, pB_force),
|
| 687 |
+
Force(self.pO, pO_force),
|
| 688 |
+
]
|
| 689 |
+
|
| 690 |
+
loads = _simplify_loads(self.pathway.to_loads(self.F))
|
| 691 |
+
assert loads == expected
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_rigidbody.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy.physics.mechanics import Point, ReferenceFrame, Dyadic, RigidBody
|
| 2 |
+
from sympy.physics.mechanics import dynamicsymbols, outer, inertia, Inertia
|
| 3 |
+
from sympy.physics.mechanics import inertia_of_point_mass
|
| 4 |
+
from sympy import expand, zeros, simplify, symbols
|
| 5 |
+
from sympy.testing.pytest import raises, warns_deprecated_sympy
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def test_rigidbody_default():
|
| 9 |
+
# Test default
|
| 10 |
+
b = RigidBody('B')
|
| 11 |
+
I = inertia(b.frame, *symbols('B_ixx B_iyy B_izz B_ixy B_iyz B_izx'))
|
| 12 |
+
assert b.name == 'B'
|
| 13 |
+
assert b.mass == symbols('B_mass')
|
| 14 |
+
assert b.masscenter.name == 'B_masscenter'
|
| 15 |
+
assert b.inertia == (I, b.masscenter)
|
| 16 |
+
assert b.central_inertia == I
|
| 17 |
+
assert b.frame.name == 'B_frame'
|
| 18 |
+
assert b.__str__() == 'B'
|
| 19 |
+
assert b.__repr__() == (
|
| 20 |
+
"RigidBody('B', masscenter=B_masscenter, frame=B_frame, mass=B_mass, "
|
| 21 |
+
"inertia=Inertia(dyadic=B_ixx*(B_frame.x|B_frame.x) + "
|
| 22 |
+
"B_ixy*(B_frame.x|B_frame.y) + B_izx*(B_frame.x|B_frame.z) + "
|
| 23 |
+
"B_ixy*(B_frame.y|B_frame.x) + B_iyy*(B_frame.y|B_frame.y) + "
|
| 24 |
+
"B_iyz*(B_frame.y|B_frame.z) + B_izx*(B_frame.z|B_frame.x) + "
|
| 25 |
+
"B_iyz*(B_frame.z|B_frame.y) + B_izz*(B_frame.z|B_frame.z), "
|
| 26 |
+
"point=B_masscenter))")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_rigidbody():
|
| 30 |
+
m, m2, v1, v2, v3, omega = symbols('m m2 v1 v2 v3 omega')
|
| 31 |
+
A = ReferenceFrame('A')
|
| 32 |
+
A2 = ReferenceFrame('A2')
|
| 33 |
+
P = Point('P')
|
| 34 |
+
P2 = Point('P2')
|
| 35 |
+
I = Dyadic(0)
|
| 36 |
+
I2 = Dyadic(0)
|
| 37 |
+
B = RigidBody('B', P, A, m, (I, P))
|
| 38 |
+
assert B.mass == m
|
| 39 |
+
assert B.frame == A
|
| 40 |
+
assert B.masscenter == P
|
| 41 |
+
assert B.inertia == (I, B.masscenter)
|
| 42 |
+
|
| 43 |
+
B.mass = m2
|
| 44 |
+
B.frame = A2
|
| 45 |
+
B.masscenter = P2
|
| 46 |
+
B.inertia = (I2, B.masscenter)
|
| 47 |
+
raises(TypeError, lambda: RigidBody(P, P, A, m, (I, P)))
|
| 48 |
+
raises(TypeError, lambda: RigidBody('B', P, P, m, (I, P)))
|
| 49 |
+
raises(TypeError, lambda: RigidBody('B', P, A, m, (P, P)))
|
| 50 |
+
raises(TypeError, lambda: RigidBody('B', P, A, m, (I, I)))
|
| 51 |
+
assert B.__str__() == 'B'
|
| 52 |
+
assert B.mass == m2
|
| 53 |
+
assert B.frame == A2
|
| 54 |
+
assert B.masscenter == P2
|
| 55 |
+
assert B.inertia == (I2, B.masscenter)
|
| 56 |
+
assert isinstance(B.inertia, Inertia)
|
| 57 |
+
|
| 58 |
+
# Testing linear momentum function assuming A2 is the inertial frame
|
| 59 |
+
N = ReferenceFrame('N')
|
| 60 |
+
P2.set_vel(N, v1 * N.x + v2 * N.y + v3 * N.z)
|
| 61 |
+
assert B.linear_momentum(N) == m2 * (v1 * N.x + v2 * N.y + v3 * N.z)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def test_rigidbody2():
|
| 65 |
+
M, v, r, omega, g, h = dynamicsymbols('M v r omega g h')
|
| 66 |
+
N = ReferenceFrame('N')
|
| 67 |
+
b = ReferenceFrame('b')
|
| 68 |
+
b.set_ang_vel(N, omega * b.x)
|
| 69 |
+
P = Point('P')
|
| 70 |
+
I = outer(b.x, b.x)
|
| 71 |
+
Inertia_tuple = (I, P)
|
| 72 |
+
B = RigidBody('B', P, b, M, Inertia_tuple)
|
| 73 |
+
P.set_vel(N, v * b.x)
|
| 74 |
+
assert B.angular_momentum(P, N) == omega * b.x
|
| 75 |
+
O = Point('O')
|
| 76 |
+
O.set_vel(N, v * b.x)
|
| 77 |
+
P.set_pos(O, r * b.y)
|
| 78 |
+
assert B.angular_momentum(O, N) == omega * b.x - M*v*r*b.z
|
| 79 |
+
B.potential_energy = M * g * h
|
| 80 |
+
assert B.potential_energy == M * g * h
|
| 81 |
+
assert expand(2 * B.kinetic_energy(N)) == omega**2 + M * v**2
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def test_rigidbody3():
|
| 85 |
+
q1, q2, q3, q4 = dynamicsymbols('q1:5')
|
| 86 |
+
p1, p2, p3 = symbols('p1:4')
|
| 87 |
+
m = symbols('m')
|
| 88 |
+
|
| 89 |
+
A = ReferenceFrame('A')
|
| 90 |
+
B = A.orientnew('B', 'axis', [q1, A.x])
|
| 91 |
+
O = Point('O')
|
| 92 |
+
O.set_vel(A, q2*A.x + q3*A.y + q4*A.z)
|
| 93 |
+
P = O.locatenew('P', p1*B.x + p2*B.y + p3*B.z)
|
| 94 |
+
P.v2pt_theory(O, A, B)
|
| 95 |
+
I = outer(B.x, B.x)
|
| 96 |
+
|
| 97 |
+
rb1 = RigidBody('rb1', P, B, m, (I, P))
|
| 98 |
+
# I_S/O = I_S/S* + I_S*/O
|
| 99 |
+
rb2 = RigidBody('rb2', P, B, m,
|
| 100 |
+
(I + inertia_of_point_mass(m, P.pos_from(O), B), O))
|
| 101 |
+
|
| 102 |
+
assert rb1.central_inertia == rb2.central_inertia
|
| 103 |
+
assert rb1.angular_momentum(O, A) == rb2.angular_momentum(O, A)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def test_pendulum_angular_momentum():
|
| 107 |
+
"""Consider a pendulum of length OA = 2a, of mass m as a rigid body of
|
| 108 |
+
center of mass G (OG = a) which turn around (O,z). The angle between the
|
| 109 |
+
reference frame R and the rod is q. The inertia of the body is I =
|
| 110 |
+
(G,0,ma^2/3,ma^2/3). """
|
| 111 |
+
|
| 112 |
+
m, a = symbols('m, a')
|
| 113 |
+
q = dynamicsymbols('q')
|
| 114 |
+
|
| 115 |
+
R = ReferenceFrame('R')
|
| 116 |
+
R1 = R.orientnew('R1', 'Axis', [q, R.z])
|
| 117 |
+
R1.set_ang_vel(R, q.diff() * R.z)
|
| 118 |
+
|
| 119 |
+
I = inertia(R1, 0, m * a**2 / 3, m * a**2 / 3)
|
| 120 |
+
|
| 121 |
+
O = Point('O')
|
| 122 |
+
|
| 123 |
+
A = O.locatenew('A', 2*a * R1.x)
|
| 124 |
+
G = O.locatenew('G', a * R1.x)
|
| 125 |
+
|
| 126 |
+
S = RigidBody('S', G, R1, m, (I, G))
|
| 127 |
+
|
| 128 |
+
O.set_vel(R, 0)
|
| 129 |
+
A.v2pt_theory(O, R, R1)
|
| 130 |
+
G.v2pt_theory(O, R, R1)
|
| 131 |
+
|
| 132 |
+
assert (4 * m * a**2 / 3 * q.diff() * R.z -
|
| 133 |
+
S.angular_momentum(O, R).express(R)) == 0
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def test_rigidbody_inertia():
|
| 137 |
+
N = ReferenceFrame('N')
|
| 138 |
+
m, Ix, Iy, Iz, a, b = symbols('m, I_x, I_y, I_z, a, b')
|
| 139 |
+
Io = inertia(N, Ix, Iy, Iz)
|
| 140 |
+
o = Point('o')
|
| 141 |
+
p = o.locatenew('p', a * N.x + b * N.y)
|
| 142 |
+
R = RigidBody('R', o, N, m, (Io, p))
|
| 143 |
+
I_check = inertia(N, Ix - b ** 2 * m, Iy - a ** 2 * m,
|
| 144 |
+
Iz - m * (a ** 2 + b ** 2), m * a * b)
|
| 145 |
+
assert isinstance(R.inertia, Inertia)
|
| 146 |
+
assert R.inertia == (Io, p)
|
| 147 |
+
assert R.central_inertia == I_check
|
| 148 |
+
R.central_inertia = Io
|
| 149 |
+
assert R.inertia == (Io, o)
|
| 150 |
+
assert R.central_inertia == Io
|
| 151 |
+
R.inertia = (Io, p)
|
| 152 |
+
assert R.inertia == (Io, p)
|
| 153 |
+
assert R.central_inertia == I_check
|
| 154 |
+
# parse Inertia object
|
| 155 |
+
R.inertia = Inertia(Io, o)
|
| 156 |
+
assert R.inertia == (Io, o)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def test_parallel_axis():
|
| 160 |
+
N = ReferenceFrame('N')
|
| 161 |
+
m, Ix, Iy, Iz, a, b = symbols('m, I_x, I_y, I_z, a, b')
|
| 162 |
+
Io = inertia(N, Ix, Iy, Iz)
|
| 163 |
+
o = Point('o')
|
| 164 |
+
p = o.locatenew('p', a * N.x + b * N.y)
|
| 165 |
+
R = RigidBody('R', o, N, m, (Io, o))
|
| 166 |
+
Ip = R.parallel_axis(p)
|
| 167 |
+
Ip_expected = inertia(N, Ix + m * b**2, Iy + m * a**2,
|
| 168 |
+
Iz + m * (a**2 + b**2), ixy=-m * a * b)
|
| 169 |
+
assert Ip == Ip_expected
|
| 170 |
+
# Reference frame from which the parallel axis is viewed should not matter
|
| 171 |
+
A = ReferenceFrame('A')
|
| 172 |
+
A.orient_axis(N, N.z, 1)
|
| 173 |
+
assert simplify(
|
| 174 |
+
(R.parallel_axis(p, A) - Ip_expected).to_matrix(A)) == zeros(3, 3)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def test_deprecated_set_potential_energy():
|
| 178 |
+
m, g, h = symbols('m g h')
|
| 179 |
+
A = ReferenceFrame('A')
|
| 180 |
+
P = Point('P')
|
| 181 |
+
I = Dyadic(0)
|
| 182 |
+
B = RigidBody('B', P, A, m, (I, P))
|
| 183 |
+
with warns_deprecated_sympy():
|
| 184 |
+
B.set_potential_energy(m*g*h)
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_system.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import symbols, Matrix, atan, zeros
|
| 2 |
+
from sympy.simplify.simplify import simplify
|
| 3 |
+
from sympy.physics.mechanics import (dynamicsymbols, Particle, Point,
|
| 4 |
+
ReferenceFrame, SymbolicSystem)
|
| 5 |
+
from sympy.testing.pytest import raises
|
| 6 |
+
|
| 7 |
+
# This class is going to be tested using a simple pendulum set up in x and y
|
| 8 |
+
# coordinates
|
| 9 |
+
x, y, u, v, lam = dynamicsymbols('x y u v lambda')
|
| 10 |
+
m, l, g = symbols('m l g')
|
| 11 |
+
|
| 12 |
+
# Set up the different forms the equations can take
|
| 13 |
+
# [1] Explicit form where the kinematics and dynamics are combined
|
| 14 |
+
# x' = F(x, t, r, p)
|
| 15 |
+
#
|
| 16 |
+
# [2] Implicit form where the kinematics and dynamics are combined
|
| 17 |
+
# M(x, p) x' = F(x, t, r, p)
|
| 18 |
+
#
|
| 19 |
+
# [3] Implicit form where the kinematics and dynamics are separate
|
| 20 |
+
# M(q, p) u' = F(q, u, t, r, p)
|
| 21 |
+
# q' = G(q, u, t, r, p)
|
| 22 |
+
dyn_implicit_mat = Matrix([[1, 0, -x/m],
|
| 23 |
+
[0, 1, -y/m],
|
| 24 |
+
[0, 0, l**2/m]])
|
| 25 |
+
|
| 26 |
+
dyn_implicit_rhs = Matrix([0, 0, u**2 + v**2 - g*y])
|
| 27 |
+
|
| 28 |
+
comb_implicit_mat = Matrix([[1, 0, 0, 0, 0],
|
| 29 |
+
[0, 1, 0, 0, 0],
|
| 30 |
+
[0, 0, 1, 0, -x/m],
|
| 31 |
+
[0, 0, 0, 1, -y/m],
|
| 32 |
+
[0, 0, 0, 0, l**2/m]])
|
| 33 |
+
|
| 34 |
+
comb_implicit_rhs = Matrix([u, v, 0, 0, u**2 + v**2 - g*y])
|
| 35 |
+
|
| 36 |
+
kin_explicit_rhs = Matrix([u, v])
|
| 37 |
+
|
| 38 |
+
comb_explicit_rhs = comb_implicit_mat.LUsolve(comb_implicit_rhs)
|
| 39 |
+
|
| 40 |
+
# Set up a body and load to pass into the system
|
| 41 |
+
theta = atan(x/y)
|
| 42 |
+
N = ReferenceFrame('N')
|
| 43 |
+
A = N.orientnew('A', 'Axis', [theta, N.z])
|
| 44 |
+
O = Point('O')
|
| 45 |
+
P = O.locatenew('P', l * A.x)
|
| 46 |
+
|
| 47 |
+
Pa = Particle('Pa', P, m)
|
| 48 |
+
|
| 49 |
+
bodies = [Pa]
|
| 50 |
+
loads = [(P, g * m * N.x)]
|
| 51 |
+
|
| 52 |
+
# Set up some output equations to be given to SymbolicSystem
|
| 53 |
+
# Change to make these fit the pendulum
|
| 54 |
+
PE = symbols("PE")
|
| 55 |
+
out_eqns = {PE: m*g*(l+y)}
|
| 56 |
+
|
| 57 |
+
# Set up remaining arguments that can be passed to SymbolicSystem
|
| 58 |
+
alg_con = [2]
|
| 59 |
+
alg_con_full = [4]
|
| 60 |
+
coordinates = (x, y, lam)
|
| 61 |
+
speeds = (u, v)
|
| 62 |
+
states = (x, y, u, v, lam)
|
| 63 |
+
coord_idxs = (0, 1)
|
| 64 |
+
speed_idxs = (2, 3)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def test_form_1():
|
| 68 |
+
symsystem1 = SymbolicSystem(states, comb_explicit_rhs,
|
| 69 |
+
alg_con=alg_con_full, output_eqns=out_eqns,
|
| 70 |
+
coord_idxs=coord_idxs, speed_idxs=speed_idxs,
|
| 71 |
+
bodies=bodies, loads=loads)
|
| 72 |
+
|
| 73 |
+
assert symsystem1.coordinates == Matrix([x, y])
|
| 74 |
+
assert symsystem1.speeds == Matrix([u, v])
|
| 75 |
+
assert symsystem1.states == Matrix([x, y, u, v, lam])
|
| 76 |
+
|
| 77 |
+
assert symsystem1.alg_con == [4]
|
| 78 |
+
|
| 79 |
+
inter = comb_explicit_rhs
|
| 80 |
+
assert simplify(symsystem1.comb_explicit_rhs - inter) == zeros(5, 1)
|
| 81 |
+
|
| 82 |
+
assert set(symsystem1.dynamic_symbols()) == {y, v, lam, u, x}
|
| 83 |
+
assert type(symsystem1.dynamic_symbols()) == tuple
|
| 84 |
+
assert set(symsystem1.constant_symbols()) == {l, g, m}
|
| 85 |
+
assert type(symsystem1.constant_symbols()) == tuple
|
| 86 |
+
|
| 87 |
+
assert symsystem1.output_eqns == out_eqns
|
| 88 |
+
|
| 89 |
+
assert symsystem1.bodies == (Pa,)
|
| 90 |
+
assert symsystem1.loads == ((P, g * m * N.x),)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def test_form_2():
|
| 94 |
+
symsystem2 = SymbolicSystem(coordinates, comb_implicit_rhs, speeds=speeds,
|
| 95 |
+
mass_matrix=comb_implicit_mat,
|
| 96 |
+
alg_con=alg_con_full, output_eqns=out_eqns,
|
| 97 |
+
bodies=bodies, loads=loads)
|
| 98 |
+
|
| 99 |
+
assert symsystem2.coordinates == Matrix([x, y, lam])
|
| 100 |
+
assert symsystem2.speeds == Matrix([u, v])
|
| 101 |
+
assert symsystem2.states == Matrix([x, y, lam, u, v])
|
| 102 |
+
|
| 103 |
+
assert symsystem2.alg_con == [4]
|
| 104 |
+
|
| 105 |
+
inter = comb_implicit_rhs
|
| 106 |
+
assert simplify(symsystem2.comb_implicit_rhs - inter) == zeros(5, 1)
|
| 107 |
+
assert simplify(symsystem2.comb_implicit_mat-comb_implicit_mat) == zeros(5)
|
| 108 |
+
|
| 109 |
+
assert set(symsystem2.dynamic_symbols()) == {y, v, lam, u, x}
|
| 110 |
+
assert type(symsystem2.dynamic_symbols()) == tuple
|
| 111 |
+
assert set(symsystem2.constant_symbols()) == {l, g, m}
|
| 112 |
+
assert type(symsystem2.constant_symbols()) == tuple
|
| 113 |
+
|
| 114 |
+
inter = comb_explicit_rhs
|
| 115 |
+
symsystem2.compute_explicit_form()
|
| 116 |
+
assert simplify(symsystem2.comb_explicit_rhs - inter) == zeros(5, 1)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
assert symsystem2.output_eqns == out_eqns
|
| 120 |
+
|
| 121 |
+
assert symsystem2.bodies == (Pa,)
|
| 122 |
+
assert symsystem2.loads == ((P, g * m * N.x),)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def test_form_3():
|
| 126 |
+
symsystem3 = SymbolicSystem(states, dyn_implicit_rhs,
|
| 127 |
+
mass_matrix=dyn_implicit_mat,
|
| 128 |
+
coordinate_derivatives=kin_explicit_rhs,
|
| 129 |
+
alg_con=alg_con, coord_idxs=coord_idxs,
|
| 130 |
+
speed_idxs=speed_idxs, bodies=bodies,
|
| 131 |
+
loads=loads)
|
| 132 |
+
|
| 133 |
+
assert symsystem3.coordinates == Matrix([x, y])
|
| 134 |
+
assert symsystem3.speeds == Matrix([u, v])
|
| 135 |
+
assert symsystem3.states == Matrix([x, y, u, v, lam])
|
| 136 |
+
|
| 137 |
+
assert symsystem3.alg_con == [4]
|
| 138 |
+
|
| 139 |
+
inter1 = kin_explicit_rhs
|
| 140 |
+
inter2 = dyn_implicit_rhs
|
| 141 |
+
assert simplify(symsystem3.kin_explicit_rhs - inter1) == zeros(2, 1)
|
| 142 |
+
assert simplify(symsystem3.dyn_implicit_mat - dyn_implicit_mat) == zeros(3)
|
| 143 |
+
assert simplify(symsystem3.dyn_implicit_rhs - inter2) == zeros(3, 1)
|
| 144 |
+
|
| 145 |
+
inter = comb_implicit_rhs
|
| 146 |
+
assert simplify(symsystem3.comb_implicit_rhs - inter) == zeros(5, 1)
|
| 147 |
+
assert simplify(symsystem3.comb_implicit_mat-comb_implicit_mat) == zeros(5)
|
| 148 |
+
|
| 149 |
+
inter = comb_explicit_rhs
|
| 150 |
+
symsystem3.compute_explicit_form()
|
| 151 |
+
assert simplify(symsystem3.comb_explicit_rhs - inter) == zeros(5, 1)
|
| 152 |
+
|
| 153 |
+
assert set(symsystem3.dynamic_symbols()) == {y, v, lam, u, x}
|
| 154 |
+
assert type(symsystem3.dynamic_symbols()) == tuple
|
| 155 |
+
assert set(symsystem3.constant_symbols()) == {l, g, m}
|
| 156 |
+
assert type(symsystem3.constant_symbols()) == tuple
|
| 157 |
+
|
| 158 |
+
assert symsystem3.output_eqns == {}
|
| 159 |
+
|
| 160 |
+
assert symsystem3.bodies == (Pa,)
|
| 161 |
+
assert symsystem3.loads == ((P, g * m * N.x),)
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def test_property_attributes():
|
| 165 |
+
symsystem = SymbolicSystem(states, comb_explicit_rhs,
|
| 166 |
+
alg_con=alg_con_full, output_eqns=out_eqns,
|
| 167 |
+
coord_idxs=coord_idxs, speed_idxs=speed_idxs,
|
| 168 |
+
bodies=bodies, loads=loads)
|
| 169 |
+
|
| 170 |
+
with raises(AttributeError):
|
| 171 |
+
symsystem.bodies = 42
|
| 172 |
+
with raises(AttributeError):
|
| 173 |
+
symsystem.coordinates = 42
|
| 174 |
+
with raises(AttributeError):
|
| 175 |
+
symsystem.dyn_implicit_rhs = 42
|
| 176 |
+
with raises(AttributeError):
|
| 177 |
+
symsystem.comb_implicit_rhs = 42
|
| 178 |
+
with raises(AttributeError):
|
| 179 |
+
symsystem.loads = 42
|
| 180 |
+
with raises(AttributeError):
|
| 181 |
+
symsystem.dyn_implicit_mat = 42
|
| 182 |
+
with raises(AttributeError):
|
| 183 |
+
symsystem.comb_implicit_mat = 42
|
| 184 |
+
with raises(AttributeError):
|
| 185 |
+
symsystem.kin_explicit_rhs = 42
|
| 186 |
+
with raises(AttributeError):
|
| 187 |
+
symsystem.comb_explicit_rhs = 42
|
| 188 |
+
with raises(AttributeError):
|
| 189 |
+
symsystem.speeds = 42
|
| 190 |
+
with raises(AttributeError):
|
| 191 |
+
symsystem.states = 42
|
| 192 |
+
with raises(AttributeError):
|
| 193 |
+
symsystem.alg_con = 42
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def test_not_specified_errors():
|
| 197 |
+
"""This test will cover errors that arise from trying to access attributes
|
| 198 |
+
that were not specified upon object creation or were specified on creation
|
| 199 |
+
and the user tries to recalculate them."""
|
| 200 |
+
# Trying to access form 2 when form 1 given
|
| 201 |
+
# Trying to access form 3 when form 2 given
|
| 202 |
+
|
| 203 |
+
symsystem1 = SymbolicSystem(states, comb_explicit_rhs)
|
| 204 |
+
|
| 205 |
+
with raises(AttributeError):
|
| 206 |
+
symsystem1.comb_implicit_mat
|
| 207 |
+
with raises(AttributeError):
|
| 208 |
+
symsystem1.comb_implicit_rhs
|
| 209 |
+
with raises(AttributeError):
|
| 210 |
+
symsystem1.dyn_implicit_mat
|
| 211 |
+
with raises(AttributeError):
|
| 212 |
+
symsystem1.dyn_implicit_rhs
|
| 213 |
+
with raises(AttributeError):
|
| 214 |
+
symsystem1.kin_explicit_rhs
|
| 215 |
+
with raises(AttributeError):
|
| 216 |
+
symsystem1.compute_explicit_form()
|
| 217 |
+
|
| 218 |
+
symsystem2 = SymbolicSystem(coordinates, comb_implicit_rhs, speeds=speeds,
|
| 219 |
+
mass_matrix=comb_implicit_mat)
|
| 220 |
+
|
| 221 |
+
with raises(AttributeError):
|
| 222 |
+
symsystem2.dyn_implicit_mat
|
| 223 |
+
with raises(AttributeError):
|
| 224 |
+
symsystem2.dyn_implicit_rhs
|
| 225 |
+
with raises(AttributeError):
|
| 226 |
+
symsystem2.kin_explicit_rhs
|
| 227 |
+
|
| 228 |
+
# Attribute error when trying to access coordinates and speeds when only the
|
| 229 |
+
# states were given.
|
| 230 |
+
with raises(AttributeError):
|
| 231 |
+
symsystem1.coordinates
|
| 232 |
+
with raises(AttributeError):
|
| 233 |
+
symsystem1.speeds
|
| 234 |
+
|
| 235 |
+
# Attribute error when trying to access bodies and loads when they are not
|
| 236 |
+
# given
|
| 237 |
+
with raises(AttributeError):
|
| 238 |
+
symsystem1.bodies
|
| 239 |
+
with raises(AttributeError):
|
| 240 |
+
symsystem1.loads
|
| 241 |
+
|
| 242 |
+
# Attribute error when trying to access comb_explicit_rhs before it was
|
| 243 |
+
# calculated
|
| 244 |
+
with raises(AttributeError):
|
| 245 |
+
symsystem2.comb_explicit_rhs
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_system_class.py
ADDED
|
@@ -0,0 +1,831 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
from sympy.core.symbol import symbols
|
| 4 |
+
from sympy.core.sympify import sympify
|
| 5 |
+
from sympy.functions.elementary.trigonometric import cos, sin
|
| 6 |
+
from sympy.matrices.dense import eye, zeros
|
| 7 |
+
from sympy.matrices.immutable import ImmutableMatrix
|
| 8 |
+
from sympy.physics.mechanics import (
|
| 9 |
+
Force, KanesMethod, LagrangesMethod, Particle, PinJoint, Point,
|
| 10 |
+
PrismaticJoint, ReferenceFrame, RigidBody, Torque, TorqueActuator, System,
|
| 11 |
+
dynamicsymbols)
|
| 12 |
+
from sympy.simplify.simplify import simplify
|
| 13 |
+
from sympy.solvers.solvers import solve
|
| 14 |
+
|
| 15 |
+
t = dynamicsymbols._t # type: ignore
|
| 16 |
+
q = dynamicsymbols('q:6') # type: ignore
|
| 17 |
+
qd = dynamicsymbols('q:6', 1) # type: ignore
|
| 18 |
+
u = dynamicsymbols('u:6') # type: ignore
|
| 19 |
+
ua = dynamicsymbols('ua:3') # type: ignore
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class TestSystemBase:
|
| 23 |
+
@pytest.fixture()
|
| 24 |
+
def _empty_system_setup(self):
|
| 25 |
+
self.system = System(ReferenceFrame('frame'), Point('fixed_point'))
|
| 26 |
+
|
| 27 |
+
def _empty_system_check(self, exclude=()):
|
| 28 |
+
matrices = ('q_ind', 'q_dep', 'q', 'u_ind', 'u_dep', 'u', 'u_aux',
|
| 29 |
+
'kdes', 'holonomic_constraints', 'nonholonomic_constraints')
|
| 30 |
+
tuples = ('loads', 'bodies', 'joints', 'actuators')
|
| 31 |
+
for attr in matrices:
|
| 32 |
+
if attr not in exclude:
|
| 33 |
+
assert getattr(self.system, attr)[:] == []
|
| 34 |
+
for attr in tuples:
|
| 35 |
+
if attr not in exclude:
|
| 36 |
+
assert getattr(self.system, attr) == ()
|
| 37 |
+
if 'eom_method' not in exclude:
|
| 38 |
+
assert self.system.eom_method is None
|
| 39 |
+
|
| 40 |
+
def _create_filled_system(self, with_speeds=True):
|
| 41 |
+
self.system = System(ReferenceFrame('frame'), Point('fixed_point'))
|
| 42 |
+
u = dynamicsymbols('u:6') if with_speeds else qd
|
| 43 |
+
self.bodies = symbols('rb1:5', cls=RigidBody)
|
| 44 |
+
self.joints = (
|
| 45 |
+
PinJoint('J1', self.bodies[0], self.bodies[1], q[0], u[0]),
|
| 46 |
+
PrismaticJoint('J2', self.bodies[1], self.bodies[2], q[1], u[1]),
|
| 47 |
+
PinJoint('J3', self.bodies[2], self.bodies[3], q[2], u[2])
|
| 48 |
+
)
|
| 49 |
+
self.system.add_joints(*self.joints)
|
| 50 |
+
self.system.add_coordinates(q[3], independent=[False])
|
| 51 |
+
self.system.add_speeds(u[3], independent=False)
|
| 52 |
+
if with_speeds:
|
| 53 |
+
self.system.add_kdes(u[3] - qd[3])
|
| 54 |
+
self.system.add_auxiliary_speeds(ua[0], ua[1])
|
| 55 |
+
self.system.add_holonomic_constraints(q[2] - q[0] + q[1])
|
| 56 |
+
self.system.add_nonholonomic_constraints(u[3] - qd[1] + u[2])
|
| 57 |
+
self.system.u_ind = u[:2]
|
| 58 |
+
self.system.u_dep = u[2:4]
|
| 59 |
+
self.q_ind, self.q_dep = self.system.q_ind[:], self.system.q_dep[:]
|
| 60 |
+
self.u_ind, self.u_dep = self.system.u_ind[:], self.system.u_dep[:]
|
| 61 |
+
self.kdes = self.system.kdes[:]
|
| 62 |
+
self.hc = self.system.holonomic_constraints[:]
|
| 63 |
+
self.vc = self.system.velocity_constraints[:]
|
| 64 |
+
self.nhc = self.system.nonholonomic_constraints[:]
|
| 65 |
+
|
| 66 |
+
@pytest.fixture()
|
| 67 |
+
def _filled_system_setup(self):
|
| 68 |
+
self._create_filled_system(with_speeds=True)
|
| 69 |
+
|
| 70 |
+
@pytest.fixture()
|
| 71 |
+
def _filled_system_setup_no_speeds(self):
|
| 72 |
+
self._create_filled_system(with_speeds=False)
|
| 73 |
+
|
| 74 |
+
def _filled_system_check(self, exclude=()):
|
| 75 |
+
assert 'q_ind' in exclude or self.system.q_ind[:] == q[:3]
|
| 76 |
+
assert 'q_dep' in exclude or self.system.q_dep[:] == [q[3]]
|
| 77 |
+
assert 'q' in exclude or self.system.q[:] == q[:4]
|
| 78 |
+
assert 'u_ind' in exclude or self.system.u_ind[:] == u[:2]
|
| 79 |
+
assert 'u_dep' in exclude or self.system.u_dep[:] == u[2:4]
|
| 80 |
+
assert 'u' in exclude or self.system.u[:] == u[:4]
|
| 81 |
+
assert 'u_aux' in exclude or self.system.u_aux[:] == ua[:2]
|
| 82 |
+
assert 'kdes' in exclude or self.system.kdes[:] == [
|
| 83 |
+
ui - qdi for ui, qdi in zip(u[:4], qd[:4])]
|
| 84 |
+
assert ('holonomic_constraints' in exclude or
|
| 85 |
+
self.system.holonomic_constraints[:] == [q[2] - q[0] + q[1]])
|
| 86 |
+
assert ('nonholonomic_constraints' in exclude or
|
| 87 |
+
self.system.nonholonomic_constraints[:] == [u[3] - qd[1] + u[2]]
|
| 88 |
+
)
|
| 89 |
+
assert ('velocity_constraints' in exclude or
|
| 90 |
+
self.system.velocity_constraints[:] == [
|
| 91 |
+
qd[2] - qd[0] + qd[1], u[3] - qd[1] + u[2]])
|
| 92 |
+
assert ('bodies' in exclude or
|
| 93 |
+
self.system.bodies == tuple(self.bodies))
|
| 94 |
+
assert ('joints' in exclude or
|
| 95 |
+
self.system.joints == tuple(self.joints))
|
| 96 |
+
|
| 97 |
+
@pytest.fixture()
|
| 98 |
+
def _moving_point_mass(self, _empty_system_setup):
|
| 99 |
+
self.system.q_ind = q[0]
|
| 100 |
+
self.system.u_ind = u[0]
|
| 101 |
+
self.system.kdes = u[0] - q[0].diff(t)
|
| 102 |
+
p = Particle('p', mass=symbols('m'))
|
| 103 |
+
self.system.add_bodies(p)
|
| 104 |
+
p.masscenter.set_pos(self.system.fixed_point, q[0] * self.system.x)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class TestSystem(TestSystemBase):
|
| 108 |
+
def test_empty_system(self, _empty_system_setup):
|
| 109 |
+
self._empty_system_check()
|
| 110 |
+
self.system.validate_system()
|
| 111 |
+
|
| 112 |
+
def test_filled_system(self, _filled_system_setup):
|
| 113 |
+
self._filled_system_check()
|
| 114 |
+
self.system.validate_system()
|
| 115 |
+
|
| 116 |
+
@pytest.mark.parametrize('frame', [None, ReferenceFrame('frame')])
|
| 117 |
+
@pytest.mark.parametrize('fixed_point', [None, Point('fixed_point')])
|
| 118 |
+
def test_init(self, frame, fixed_point):
|
| 119 |
+
if fixed_point is None and frame is None:
|
| 120 |
+
self.system = System()
|
| 121 |
+
else:
|
| 122 |
+
self.system = System(frame, fixed_point)
|
| 123 |
+
if fixed_point is None:
|
| 124 |
+
assert self.system.fixed_point.name == 'inertial_point'
|
| 125 |
+
else:
|
| 126 |
+
assert self.system.fixed_point == fixed_point
|
| 127 |
+
if frame is None:
|
| 128 |
+
assert self.system.frame.name == 'inertial_frame'
|
| 129 |
+
else:
|
| 130 |
+
assert self.system.frame == frame
|
| 131 |
+
self._empty_system_check()
|
| 132 |
+
assert isinstance(self.system.q_ind, ImmutableMatrix)
|
| 133 |
+
assert isinstance(self.system.q_dep, ImmutableMatrix)
|
| 134 |
+
assert isinstance(self.system.q, ImmutableMatrix)
|
| 135 |
+
assert isinstance(self.system.u_ind, ImmutableMatrix)
|
| 136 |
+
assert isinstance(self.system.u_dep, ImmutableMatrix)
|
| 137 |
+
assert isinstance(self.system.u, ImmutableMatrix)
|
| 138 |
+
assert isinstance(self.system.kdes, ImmutableMatrix)
|
| 139 |
+
assert isinstance(self.system.holonomic_constraints, ImmutableMatrix)
|
| 140 |
+
assert isinstance(self.system.nonholonomic_constraints, ImmutableMatrix)
|
| 141 |
+
|
| 142 |
+
def test_from_newtonian_rigid_body(self):
|
| 143 |
+
rb = RigidBody('body')
|
| 144 |
+
self.system = System.from_newtonian(rb)
|
| 145 |
+
assert self.system.fixed_point == rb.masscenter
|
| 146 |
+
assert self.system.frame == rb.frame
|
| 147 |
+
self._empty_system_check(exclude=('bodies',))
|
| 148 |
+
self.system.bodies = (rb,)
|
| 149 |
+
|
| 150 |
+
def test_from_newtonian_particle(self):
|
| 151 |
+
pt = Particle('particle')
|
| 152 |
+
with pytest.raises(TypeError):
|
| 153 |
+
System.from_newtonian(pt)
|
| 154 |
+
|
| 155 |
+
@pytest.mark.parametrize('args, kwargs, exp_q_ind, exp_q_dep, exp_q', [
|
| 156 |
+
(q[:3], {}, q[:3], [], q[:3]),
|
| 157 |
+
(q[:3], {'independent': True}, q[:3], [], q[:3]),
|
| 158 |
+
(q[:3], {'independent': False}, [], q[:3], q[:3]),
|
| 159 |
+
(q[:3], {'independent': [True, False, True]}, [q[0], q[2]], [q[1]],
|
| 160 |
+
[q[0], q[2], q[1]]),
|
| 161 |
+
])
|
| 162 |
+
def test_coordinates(self, _empty_system_setup, args, kwargs,
|
| 163 |
+
exp_q_ind, exp_q_dep, exp_q):
|
| 164 |
+
# Test add_coordinates
|
| 165 |
+
self.system.add_coordinates(*args, **kwargs)
|
| 166 |
+
assert self.system.q_ind[:] == exp_q_ind
|
| 167 |
+
assert self.system.q_dep[:] == exp_q_dep
|
| 168 |
+
assert self.system.q[:] == exp_q
|
| 169 |
+
self._empty_system_check(exclude=('q_ind', 'q_dep', 'q'))
|
| 170 |
+
# Test setter for q_ind and q_dep
|
| 171 |
+
self.system.q_ind = exp_q_ind
|
| 172 |
+
self.system.q_dep = exp_q_dep
|
| 173 |
+
assert self.system.q_ind[:] == exp_q_ind
|
| 174 |
+
assert self.system.q_dep[:] == exp_q_dep
|
| 175 |
+
assert self.system.q[:] == exp_q
|
| 176 |
+
self._empty_system_check(exclude=('q_ind', 'q_dep', 'q'))
|
| 177 |
+
|
| 178 |
+
@pytest.mark.parametrize('func', ['add_coordinates', 'add_speeds'])
|
| 179 |
+
@pytest.mark.parametrize('args, kwargs', [
|
| 180 |
+
((q[0], q[5]), {}),
|
| 181 |
+
((u[0], u[5]), {}),
|
| 182 |
+
((q[0],), {'independent': False}),
|
| 183 |
+
((u[0],), {'independent': False}),
|
| 184 |
+
((u[0], q[5]), {}),
|
| 185 |
+
((symbols('a'), q[5]), {}),
|
| 186 |
+
])
|
| 187 |
+
def test_coordinates_speeds_invalid(self, _filled_system_setup, func, args,
|
| 188 |
+
kwargs):
|
| 189 |
+
with pytest.raises(ValueError):
|
| 190 |
+
getattr(self.system, func)(*args, **kwargs)
|
| 191 |
+
self._filled_system_check()
|
| 192 |
+
|
| 193 |
+
@pytest.mark.parametrize('args, kwargs, exp_u_ind, exp_u_dep, exp_u', [
|
| 194 |
+
(u[:3], {}, u[:3], [], u[:3]),
|
| 195 |
+
(u[:3], {'independent': True}, u[:3], [], u[:3]),
|
| 196 |
+
(u[:3], {'independent': False}, [], u[:3], u[:3]),
|
| 197 |
+
(u[:3], {'independent': [True, False, True]}, [u[0], u[2]], [u[1]],
|
| 198 |
+
[u[0], u[2], u[1]]),
|
| 199 |
+
])
|
| 200 |
+
def test_speeds(self, _empty_system_setup, args, kwargs, exp_u_ind,
|
| 201 |
+
exp_u_dep, exp_u):
|
| 202 |
+
# Test add_speeds
|
| 203 |
+
self.system.add_speeds(*args, **kwargs)
|
| 204 |
+
assert self.system.u_ind[:] == exp_u_ind
|
| 205 |
+
assert self.system.u_dep[:] == exp_u_dep
|
| 206 |
+
assert self.system.u[:] == exp_u
|
| 207 |
+
self._empty_system_check(exclude=('u_ind', 'u_dep', 'u'))
|
| 208 |
+
# Test setter for u_ind and u_dep
|
| 209 |
+
self.system.u_ind = exp_u_ind
|
| 210 |
+
self.system.u_dep = exp_u_dep
|
| 211 |
+
assert self.system.u_ind[:] == exp_u_ind
|
| 212 |
+
assert self.system.u_dep[:] == exp_u_dep
|
| 213 |
+
assert self.system.u[:] == exp_u
|
| 214 |
+
self._empty_system_check(exclude=('u_ind', 'u_dep', 'u'))
|
| 215 |
+
|
| 216 |
+
@pytest.mark.parametrize('args, kwargs, exp_u_aux', [
|
| 217 |
+
(ua[:3], {}, ua[:3]),
|
| 218 |
+
])
|
| 219 |
+
def test_auxiliary_speeds(self, _empty_system_setup, args, kwargs,
|
| 220 |
+
exp_u_aux):
|
| 221 |
+
# Test add_speeds
|
| 222 |
+
self.system.add_auxiliary_speeds(*args, **kwargs)
|
| 223 |
+
assert self.system.u_aux[:] == exp_u_aux
|
| 224 |
+
self._empty_system_check(exclude=('u_aux',))
|
| 225 |
+
# Test setter for u_ind and u_dep
|
| 226 |
+
self.system.u_aux = exp_u_aux
|
| 227 |
+
assert self.system.u_aux[:] == exp_u_aux
|
| 228 |
+
self._empty_system_check(exclude=('u_aux',))
|
| 229 |
+
|
| 230 |
+
@pytest.mark.parametrize('args, kwargs', [
|
| 231 |
+
((ua[2], q[0]), {}),
|
| 232 |
+
((ua[2], u[1]), {}),
|
| 233 |
+
((ua[0], ua[2]), {}),
|
| 234 |
+
((symbols('a'), ua[2]), {}),
|
| 235 |
+
])
|
| 236 |
+
def test_auxiliary_invalid(self, _filled_system_setup, args, kwargs):
|
| 237 |
+
with pytest.raises(ValueError):
|
| 238 |
+
self.system.add_auxiliary_speeds(*args, **kwargs)
|
| 239 |
+
self._filled_system_check()
|
| 240 |
+
|
| 241 |
+
@pytest.mark.parametrize('prop, add_func, args, kwargs', [
|
| 242 |
+
('q_ind', 'add_coordinates', (q[0],), {}),
|
| 243 |
+
('q_dep', 'add_coordinates', (q[3],), {'independent': False}),
|
| 244 |
+
('u_ind', 'add_speeds', (u[0],), {}),
|
| 245 |
+
('u_dep', 'add_speeds', (u[3],), {'independent': False}),
|
| 246 |
+
('u_aux', 'add_auxiliary_speeds', (ua[2],), {}),
|
| 247 |
+
('kdes', 'add_kdes', (qd[0] - u[0],), {}),
|
| 248 |
+
('holonomic_constraints', 'add_holonomic_constraints',
|
| 249 |
+
(q[0] - q[1],), {}),
|
| 250 |
+
('nonholonomic_constraints', 'add_nonholonomic_constraints',
|
| 251 |
+
(u[0] - u[1],), {}),
|
| 252 |
+
('bodies', 'add_bodies', (RigidBody('body'),), {}),
|
| 253 |
+
('loads', 'add_loads', (Force(Point('P'), ReferenceFrame('N').x),), {}),
|
| 254 |
+
('actuators', 'add_actuators', (TorqueActuator(
|
| 255 |
+
symbols('T'), ReferenceFrame('N').x, ReferenceFrame('A')),), {}),
|
| 256 |
+
])
|
| 257 |
+
def test_add_after_reset(self, _filled_system_setup, prop, add_func, args,
|
| 258 |
+
kwargs):
|
| 259 |
+
setattr(self.system, prop, ())
|
| 260 |
+
exclude = (prop, 'q', 'u')
|
| 261 |
+
if prop in ('holonomic_constraints', 'nonholonomic_constraints'):
|
| 262 |
+
exclude += ('velocity_constraints',)
|
| 263 |
+
self._filled_system_check(exclude=exclude)
|
| 264 |
+
assert list(getattr(self.system, prop)[:]) == []
|
| 265 |
+
getattr(self.system, add_func)(*args, **kwargs)
|
| 266 |
+
assert list(getattr(self.system, prop)[:]) == list(args)
|
| 267 |
+
|
| 268 |
+
@pytest.mark.parametrize('prop, add_func, value, error', [
|
| 269 |
+
('q_ind', 'add_coordinates', symbols('a'), ValueError),
|
| 270 |
+
('q_dep', 'add_coordinates', symbols('a'), ValueError),
|
| 271 |
+
('u_ind', 'add_speeds', symbols('a'), ValueError),
|
| 272 |
+
('u_dep', 'add_speeds', symbols('a'), ValueError),
|
| 273 |
+
('u_aux', 'add_auxiliary_speeds', symbols('a'), ValueError),
|
| 274 |
+
('kdes', 'add_kdes', 7, TypeError),
|
| 275 |
+
('holonomic_constraints', 'add_holonomic_constraints', 7, TypeError),
|
| 276 |
+
('nonholonomic_constraints', 'add_nonholonomic_constraints', 7,
|
| 277 |
+
TypeError),
|
| 278 |
+
('bodies', 'add_bodies', symbols('a'), TypeError),
|
| 279 |
+
('loads', 'add_loads', symbols('a'), TypeError),
|
| 280 |
+
('actuators', 'add_actuators', symbols('a'), TypeError),
|
| 281 |
+
])
|
| 282 |
+
def test_type_error(self, _filled_system_setup, prop, add_func, value,
|
| 283 |
+
error):
|
| 284 |
+
with pytest.raises(error):
|
| 285 |
+
getattr(self.system, add_func)(value)
|
| 286 |
+
with pytest.raises(error):
|
| 287 |
+
setattr(self.system, prop, value)
|
| 288 |
+
self._filled_system_check()
|
| 289 |
+
|
| 290 |
+
@pytest.mark.parametrize('args, kwargs, exp_kdes', [
|
| 291 |
+
((), {}, [ui - qdi for ui, qdi in zip(u[:4], qd[:4])]),
|
| 292 |
+
((u[4] - qd[4], u[5] - qd[5]), {},
|
| 293 |
+
[ui - qdi for ui, qdi in zip(u[:6], qd[:6])]),
|
| 294 |
+
])
|
| 295 |
+
def test_kdes(self, _filled_system_setup, args, kwargs, exp_kdes):
|
| 296 |
+
# Test add_speeds
|
| 297 |
+
self.system.add_kdes(*args, **kwargs)
|
| 298 |
+
self._filled_system_check(exclude=('kdes',))
|
| 299 |
+
assert self.system.kdes[:] == exp_kdes
|
| 300 |
+
# Test setter for kdes
|
| 301 |
+
self.system.kdes = exp_kdes
|
| 302 |
+
self._filled_system_check(exclude=('kdes',))
|
| 303 |
+
assert self.system.kdes[:] == exp_kdes
|
| 304 |
+
|
| 305 |
+
@pytest.mark.parametrize('args, kwargs', [
|
| 306 |
+
((u[0] - qd[0], u[4] - qd[4]), {}),
|
| 307 |
+
((-(u[0] - qd[0]), u[4] - qd[4]), {}),
|
| 308 |
+
(([u[0] - u[0], u[4] - qd[4]]), {}),
|
| 309 |
+
])
|
| 310 |
+
def test_kdes_invalid(self, _filled_system_setup, args, kwargs):
|
| 311 |
+
with pytest.raises(ValueError):
|
| 312 |
+
self.system.add_kdes(*args, **kwargs)
|
| 313 |
+
self._filled_system_check()
|
| 314 |
+
|
| 315 |
+
@pytest.mark.parametrize('args, kwargs, exp_con', [
|
| 316 |
+
((), {}, [q[2] - q[0] + q[1]]),
|
| 317 |
+
((q[4] - q[5], q[5] + q[3]), {},
|
| 318 |
+
[q[2] - q[0] + q[1], q[4] - q[5], q[5] + q[3]]),
|
| 319 |
+
])
|
| 320 |
+
def test_holonomic_constraints(self, _filled_system_setup, args, kwargs,
|
| 321 |
+
exp_con):
|
| 322 |
+
exclude = ('holonomic_constraints', 'velocity_constraints')
|
| 323 |
+
exp_vel_con = [c.diff(t) for c in exp_con] + self.nhc
|
| 324 |
+
# Test add_holonomic_constraints
|
| 325 |
+
self.system.add_holonomic_constraints(*args, **kwargs)
|
| 326 |
+
self._filled_system_check(exclude=exclude)
|
| 327 |
+
assert self.system.holonomic_constraints[:] == exp_con
|
| 328 |
+
assert self.system.velocity_constraints[:] == exp_vel_con
|
| 329 |
+
# Test setter for holonomic_constraints
|
| 330 |
+
self.system.holonomic_constraints = exp_con
|
| 331 |
+
self._filled_system_check(exclude=exclude)
|
| 332 |
+
assert self.system.holonomic_constraints[:] == exp_con
|
| 333 |
+
assert self.system.velocity_constraints[:] == exp_vel_con
|
| 334 |
+
|
| 335 |
+
@pytest.mark.parametrize('args, kwargs', [
|
| 336 |
+
((q[2] - q[0] + q[1], q[4] - q[3]), {}),
|
| 337 |
+
((-(q[2] - q[0] + q[1]), q[4] - q[3]), {}),
|
| 338 |
+
((q[0] - q[0], q[4] - q[3]), {}),
|
| 339 |
+
])
|
| 340 |
+
def test_holonomic_constraints_invalid(self, _filled_system_setup, args,
|
| 341 |
+
kwargs):
|
| 342 |
+
with pytest.raises(ValueError):
|
| 343 |
+
self.system.add_holonomic_constraints(*args, **kwargs)
|
| 344 |
+
self._filled_system_check()
|
| 345 |
+
|
| 346 |
+
@pytest.mark.parametrize('args, kwargs, exp_con', [
|
| 347 |
+
((), {}, [u[3] - qd[1] + u[2]]),
|
| 348 |
+
((u[4] - u[5], u[5] + u[3]), {},
|
| 349 |
+
[u[3] - qd[1] + u[2], u[4] - u[5], u[5] + u[3]]),
|
| 350 |
+
])
|
| 351 |
+
def test_nonholonomic_constraints(self, _filled_system_setup, args, kwargs,
|
| 352 |
+
exp_con):
|
| 353 |
+
exclude = ('nonholonomic_constraints', 'velocity_constraints')
|
| 354 |
+
exp_vel_con = self.vc[:len(self.hc)] + exp_con
|
| 355 |
+
# Test add_nonholonomic_constraints
|
| 356 |
+
self.system.add_nonholonomic_constraints(*args, **kwargs)
|
| 357 |
+
self._filled_system_check(exclude=exclude)
|
| 358 |
+
assert self.system.nonholonomic_constraints[:] == exp_con
|
| 359 |
+
assert self.system.velocity_constraints[:] == exp_vel_con
|
| 360 |
+
# Test setter for nonholonomic_constraints
|
| 361 |
+
self.system.nonholonomic_constraints = exp_con
|
| 362 |
+
self._filled_system_check(exclude=exclude)
|
| 363 |
+
assert self.system.nonholonomic_constraints[:] == exp_con
|
| 364 |
+
assert self.system.velocity_constraints[:] == exp_vel_con
|
| 365 |
+
|
| 366 |
+
@pytest.mark.parametrize('args, kwargs', [
|
| 367 |
+
((u[3] - qd[1] + u[2], u[4] - u[3]), {}),
|
| 368 |
+
((-(u[3] - qd[1] + u[2]), u[4] - u[3]), {}),
|
| 369 |
+
((u[0] - u[0], u[4] - u[3]), {}),
|
| 370 |
+
(([u[0] - u[0], u[4] - u[3]]), {}),
|
| 371 |
+
])
|
| 372 |
+
def test_nonholonomic_constraints_invalid(self, _filled_system_setup, args,
|
| 373 |
+
kwargs):
|
| 374 |
+
with pytest.raises(ValueError):
|
| 375 |
+
self.system.add_nonholonomic_constraints(*args, **kwargs)
|
| 376 |
+
self._filled_system_check()
|
| 377 |
+
|
| 378 |
+
@pytest.mark.parametrize('constraints, expected', [
|
| 379 |
+
([], []),
|
| 380 |
+
(qd[2] - qd[0] + qd[1], [qd[2] - qd[0] + qd[1]]),
|
| 381 |
+
([qd[2] + qd[1], u[2] - u[1]], [qd[2] + qd[1], u[2] - u[1]]),
|
| 382 |
+
])
|
| 383 |
+
def test_velocity_constraints_overwrite(self, _filled_system_setup,
|
| 384 |
+
constraints, expected):
|
| 385 |
+
self.system.velocity_constraints = constraints
|
| 386 |
+
self._filled_system_check(exclude=('velocity_constraints',))
|
| 387 |
+
assert self.system.velocity_constraints[:] == expected
|
| 388 |
+
|
| 389 |
+
def test_velocity_constraints_back_to_auto(self, _filled_system_setup):
|
| 390 |
+
self.system.velocity_constraints = qd[3] - qd[2]
|
| 391 |
+
self._filled_system_check(exclude=('velocity_constraints',))
|
| 392 |
+
assert self.system.velocity_constraints[:] == [qd[3] - qd[2]]
|
| 393 |
+
self.system.velocity_constraints = None
|
| 394 |
+
self._filled_system_check()
|
| 395 |
+
|
| 396 |
+
def test_bodies(self, _filled_system_setup):
|
| 397 |
+
rb1, rb2 = RigidBody('rb1'), RigidBody('rb2')
|
| 398 |
+
p1, p2 = Particle('p1'), Particle('p2')
|
| 399 |
+
self.system.add_bodies(rb1, p1)
|
| 400 |
+
assert self.system.bodies == (*self.bodies, rb1, p1)
|
| 401 |
+
self.system.add_bodies(p2)
|
| 402 |
+
assert self.system.bodies == (*self.bodies, rb1, p1, p2)
|
| 403 |
+
self.system.bodies = []
|
| 404 |
+
assert self.system.bodies == ()
|
| 405 |
+
self.system.bodies = p2
|
| 406 |
+
assert self.system.bodies == (p2,)
|
| 407 |
+
symb = symbols('symb')
|
| 408 |
+
pytest.raises(TypeError, lambda: self.system.add_bodies(symb))
|
| 409 |
+
pytest.raises(ValueError, lambda: self.system.add_bodies(p2))
|
| 410 |
+
with pytest.raises(TypeError):
|
| 411 |
+
self.system.bodies = (rb1, rb2, p1, p2, symb)
|
| 412 |
+
assert self.system.bodies == (p2,)
|
| 413 |
+
|
| 414 |
+
def test_add_loads(self):
|
| 415 |
+
system = System()
|
| 416 |
+
N, A = ReferenceFrame('N'), ReferenceFrame('A')
|
| 417 |
+
rb1 = RigidBody('rb1', frame=N)
|
| 418 |
+
mc1 = Point('mc1')
|
| 419 |
+
p1 = Particle('p1', mc1)
|
| 420 |
+
system.add_loads(Torque(rb1, N.x), (mc1, A.x), Force(p1, A.x))
|
| 421 |
+
assert system.loads == ((N, N.x), (mc1, A.x), (mc1, A.x))
|
| 422 |
+
system.loads = [(A, A.x)]
|
| 423 |
+
assert system.loads == ((A, A.x),)
|
| 424 |
+
pytest.raises(ValueError, lambda: system.add_loads((N, N.x, N.y)))
|
| 425 |
+
with pytest.raises(TypeError):
|
| 426 |
+
system.loads = (N, N.x)
|
| 427 |
+
assert system.loads == ((A, A.x),)
|
| 428 |
+
|
| 429 |
+
def test_add_actuators(self):
|
| 430 |
+
system = System()
|
| 431 |
+
N, A = ReferenceFrame('N'), ReferenceFrame('A')
|
| 432 |
+
act1 = TorqueActuator(symbols('T1'), N.x, N)
|
| 433 |
+
act2 = TorqueActuator(symbols('T2'), N.y, N, A)
|
| 434 |
+
system.add_actuators(act1)
|
| 435 |
+
assert system.actuators == (act1,)
|
| 436 |
+
assert system.loads == ()
|
| 437 |
+
system.actuators = (act2,)
|
| 438 |
+
assert system.actuators == (act2,)
|
| 439 |
+
|
| 440 |
+
def test_add_joints(self):
|
| 441 |
+
q1, q2, q3, q4, u1, u2, u3 = dynamicsymbols('q1:5 u1:4')
|
| 442 |
+
rb1, rb2, rb3, rb4, rb5 = symbols('rb1:6', cls=RigidBody)
|
| 443 |
+
J1 = PinJoint('J1', rb1, rb2, q1, u1)
|
| 444 |
+
J2 = PrismaticJoint('J2', rb2, rb3, q2, u2)
|
| 445 |
+
J3 = PinJoint('J3', rb3, rb4, q3, u3)
|
| 446 |
+
J_lag = PinJoint('J_lag', rb4, rb5, q4, q4.diff(t))
|
| 447 |
+
system = System()
|
| 448 |
+
system.add_joints(J1)
|
| 449 |
+
assert system.joints == (J1,)
|
| 450 |
+
assert system.bodies == (rb1, rb2)
|
| 451 |
+
assert system.q_ind == ImmutableMatrix([q1])
|
| 452 |
+
assert system.u_ind == ImmutableMatrix([u1])
|
| 453 |
+
assert system.kdes == ImmutableMatrix([u1 - q1.diff(t)])
|
| 454 |
+
system.add_bodies(rb4)
|
| 455 |
+
system.add_coordinates(q3)
|
| 456 |
+
system.add_kdes(u3 - q3.diff(t))
|
| 457 |
+
system.add_joints(J3)
|
| 458 |
+
assert system.joints == (J1, J3)
|
| 459 |
+
assert system.bodies == (rb1, rb2, rb4, rb3)
|
| 460 |
+
assert system.q_ind == ImmutableMatrix([q1, q3])
|
| 461 |
+
assert system.u_ind == ImmutableMatrix([u1, u3])
|
| 462 |
+
assert system.kdes == ImmutableMatrix(
|
| 463 |
+
[u1 - q1.diff(t), u3 - q3.diff(t)])
|
| 464 |
+
system.add_kdes(-(u2 - q2.diff(t)))
|
| 465 |
+
system.add_joints(J2)
|
| 466 |
+
assert system.joints == (J1, J3, J2)
|
| 467 |
+
assert system.bodies == (rb1, rb2, rb4, rb3)
|
| 468 |
+
assert system.q_ind == ImmutableMatrix([q1, q3, q2])
|
| 469 |
+
assert system.u_ind == ImmutableMatrix([u1, u3, u2])
|
| 470 |
+
assert system.kdes == ImmutableMatrix([u1 - q1.diff(t), u3 - q3.diff(t),
|
| 471 |
+
-(u2 - q2.diff(t))])
|
| 472 |
+
system.add_joints(J_lag)
|
| 473 |
+
assert system.joints == (J1, J3, J2, J_lag)
|
| 474 |
+
assert system.bodies == (rb1, rb2, rb4, rb3, rb5)
|
| 475 |
+
assert system.q_ind == ImmutableMatrix([q1, q3, q2, q4])
|
| 476 |
+
assert system.u_ind == ImmutableMatrix([u1, u3, u2, q4.diff(t)])
|
| 477 |
+
assert system.kdes == ImmutableMatrix([u1 - q1.diff(t), u3 - q3.diff(t),
|
| 478 |
+
-(u2 - q2.diff(t))])
|
| 479 |
+
assert system.q_dep[:] == []
|
| 480 |
+
assert system.u_dep[:] == []
|
| 481 |
+
pytest.raises(ValueError, lambda: system.add_joints(J2))
|
| 482 |
+
pytest.raises(TypeError, lambda: system.add_joints(rb1))
|
| 483 |
+
|
| 484 |
+
def test_joints_setter(self, _filled_system_setup):
|
| 485 |
+
self.system.joints = self.joints[1:]
|
| 486 |
+
assert self.system.joints == self.joints[1:]
|
| 487 |
+
self._filled_system_check(exclude=('joints',))
|
| 488 |
+
self.system.q_ind = ()
|
| 489 |
+
self.system.u_ind = ()
|
| 490 |
+
self.system.joints = self.joints
|
| 491 |
+
self._filled_system_check()
|
| 492 |
+
|
| 493 |
+
@pytest.mark.parametrize('name, joint_index', [
|
| 494 |
+
('J1', 0),
|
| 495 |
+
('J2', 1),
|
| 496 |
+
('not_existing', None),
|
| 497 |
+
])
|
| 498 |
+
def test_get_joint(self, _filled_system_setup, name, joint_index):
|
| 499 |
+
joint = self.system.get_joint(name)
|
| 500 |
+
if joint_index is None:
|
| 501 |
+
assert joint is None
|
| 502 |
+
else:
|
| 503 |
+
assert joint == self.joints[joint_index]
|
| 504 |
+
|
| 505 |
+
@pytest.mark.parametrize('name, body_index', [
|
| 506 |
+
('rb1', 0),
|
| 507 |
+
('rb3', 2),
|
| 508 |
+
('not_existing', None),
|
| 509 |
+
])
|
| 510 |
+
def test_get_body(self, _filled_system_setup, name, body_index):
|
| 511 |
+
body = self.system.get_body(name)
|
| 512 |
+
if body_index is None:
|
| 513 |
+
assert body is None
|
| 514 |
+
else:
|
| 515 |
+
assert body == self.bodies[body_index]
|
| 516 |
+
|
| 517 |
+
@pytest.mark.parametrize('eom_method', [KanesMethod, LagrangesMethod])
|
| 518 |
+
def test_form_eoms_calls_subclass(self, _moving_point_mass, eom_method):
|
| 519 |
+
class MyMethod(eom_method):
|
| 520 |
+
pass
|
| 521 |
+
|
| 522 |
+
self.system.form_eoms(eom_method=MyMethod)
|
| 523 |
+
assert isinstance(self.system.eom_method, MyMethod)
|
| 524 |
+
|
| 525 |
+
@pytest.mark.parametrize('kwargs, expected', [
|
| 526 |
+
({}, ImmutableMatrix([[-1, 0], [0, symbols('m')]])),
|
| 527 |
+
({'explicit_kinematics': True}, ImmutableMatrix([[1, 0],
|
| 528 |
+
[0, symbols('m')]])),
|
| 529 |
+
])
|
| 530 |
+
def test_system_kane_form_eoms_kwargs(self, _moving_point_mass, kwargs,
|
| 531 |
+
expected):
|
| 532 |
+
self.system.form_eoms(**kwargs)
|
| 533 |
+
assert self.system.mass_matrix_full == expected
|
| 534 |
+
|
| 535 |
+
@pytest.mark.parametrize('kwargs, mm, gm', [
|
| 536 |
+
({}, ImmutableMatrix([[1, 0], [0, symbols('m')]]),
|
| 537 |
+
ImmutableMatrix([q[0].diff(t), 0])),
|
| 538 |
+
])
|
| 539 |
+
def test_system_lagrange_form_eoms_kwargs(self, _moving_point_mass, kwargs,
|
| 540 |
+
mm, gm):
|
| 541 |
+
self.system.form_eoms(eom_method=LagrangesMethod, **kwargs)
|
| 542 |
+
assert self.system.mass_matrix_full == mm
|
| 543 |
+
assert self.system.forcing_full == gm
|
| 544 |
+
|
| 545 |
+
@pytest.mark.parametrize('eom_method, kwargs, error', [
|
| 546 |
+
(KanesMethod, {'non_existing_kwarg': 1}, TypeError),
|
| 547 |
+
(LagrangesMethod, {'non_existing_kwarg': 1}, TypeError),
|
| 548 |
+
(KanesMethod, {'bodies': []}, ValueError),
|
| 549 |
+
(KanesMethod, {'kd_eqs': []}, ValueError),
|
| 550 |
+
(LagrangesMethod, {'bodies': []}, ValueError),
|
| 551 |
+
(LagrangesMethod, {'Lagrangian': 1}, ValueError),
|
| 552 |
+
])
|
| 553 |
+
def test_form_eoms_kwargs_errors(self, _empty_system_setup, eom_method,
|
| 554 |
+
kwargs, error):
|
| 555 |
+
self.system.q_ind = q[0]
|
| 556 |
+
p = Particle('p', mass=symbols('m'))
|
| 557 |
+
self.system.add_bodies(p)
|
| 558 |
+
p.masscenter.set_pos(self.system.fixed_point, q[0] * self.system.x)
|
| 559 |
+
with pytest.raises(error):
|
| 560 |
+
self.system.form_eoms(eom_method=eom_method, **kwargs)
|
| 561 |
+
|
| 562 |
+
|
| 563 |
+
class TestValidateSystem(TestSystemBase):
|
| 564 |
+
@pytest.mark.parametrize('valid_method, invalid_method, with_speeds', [
|
| 565 |
+
(KanesMethod, LagrangesMethod, True),
|
| 566 |
+
(LagrangesMethod, KanesMethod, False)
|
| 567 |
+
])
|
| 568 |
+
def test_only_valid(self, valid_method, invalid_method, with_speeds):
|
| 569 |
+
self._create_filled_system(with_speeds=with_speeds)
|
| 570 |
+
self.system.validate_system(valid_method)
|
| 571 |
+
# Test Lagrange should fail due to the usage of generalized speeds
|
| 572 |
+
with pytest.raises(ValueError):
|
| 573 |
+
self.system.validate_system(invalid_method)
|
| 574 |
+
|
| 575 |
+
@pytest.mark.parametrize('method, with_speeds', [
|
| 576 |
+
(KanesMethod, True), (LagrangesMethod, False)])
|
| 577 |
+
def test_missing_joint_coordinate(self, method, with_speeds):
|
| 578 |
+
self._create_filled_system(with_speeds=with_speeds)
|
| 579 |
+
self.system.q_ind = self.q_ind[1:]
|
| 580 |
+
self.system.u_ind = self.u_ind[:-1]
|
| 581 |
+
self.system.kdes = self.kdes[:-1]
|
| 582 |
+
pytest.raises(ValueError, lambda: self.system.validate_system(method))
|
| 583 |
+
|
| 584 |
+
def test_missing_joint_speed(self, _filled_system_setup):
|
| 585 |
+
self.system.q_ind = self.q_ind[:-1]
|
| 586 |
+
self.system.u_ind = self.u_ind[1:]
|
| 587 |
+
self.system.kdes = self.kdes[:-1]
|
| 588 |
+
pytest.raises(ValueError, lambda: self.system.validate_system())
|
| 589 |
+
|
| 590 |
+
def test_missing_joint_kdes(self, _filled_system_setup):
|
| 591 |
+
self.system.kdes = self.kdes[1:]
|
| 592 |
+
pytest.raises(ValueError, lambda: self.system.validate_system())
|
| 593 |
+
|
| 594 |
+
def test_negative_joint_kdes(self, _filled_system_setup):
|
| 595 |
+
self.system.kdes = [-self.kdes[0]] + self.kdes[1:]
|
| 596 |
+
self.system.validate_system()
|
| 597 |
+
|
| 598 |
+
@pytest.mark.parametrize('method, with_speeds', [
|
| 599 |
+
(KanesMethod, True), (LagrangesMethod, False)])
|
| 600 |
+
def test_missing_holonomic_constraint(self, method, with_speeds):
|
| 601 |
+
self._create_filled_system(with_speeds=with_speeds)
|
| 602 |
+
self.system.holonomic_constraints = []
|
| 603 |
+
self.system.nonholonomic_constraints = self.nhc + [
|
| 604 |
+
self.u_ind[1] - self.u_dep[0] + self.u_ind[0]]
|
| 605 |
+
pytest.raises(ValueError, lambda: self.system.validate_system(method))
|
| 606 |
+
self.system.q_dep = []
|
| 607 |
+
self.system.q_ind = self.q_ind + self.q_dep
|
| 608 |
+
self.system.validate_system(method)
|
| 609 |
+
|
| 610 |
+
def test_missing_nonholonomic_constraint(self, _filled_system_setup):
|
| 611 |
+
self.system.nonholonomic_constraints = []
|
| 612 |
+
pytest.raises(ValueError, lambda: self.system.validate_system())
|
| 613 |
+
self.system.u_dep = self.u_dep[1]
|
| 614 |
+
self.system.u_ind = self.u_ind + [self.u_dep[0]]
|
| 615 |
+
self.system.validate_system()
|
| 616 |
+
|
| 617 |
+
def test_number_of_coordinates_speeds(self, _filled_system_setup):
|
| 618 |
+
# Test more speeds than coordinates
|
| 619 |
+
self.system.u_ind = self.u_ind + [u[5]]
|
| 620 |
+
self.system.kdes = self.kdes + [u[5] - qd[5]]
|
| 621 |
+
self.system.validate_system()
|
| 622 |
+
# Test more coordinates than speeds
|
| 623 |
+
self.system.q_ind = self.q_ind
|
| 624 |
+
self.system.u_ind = self.u_ind[:-1]
|
| 625 |
+
self.system.kdes = self.kdes[:-1]
|
| 626 |
+
pytest.raises(ValueError, lambda: self.system.validate_system())
|
| 627 |
+
|
| 628 |
+
def test_number_of_kdes(self, _filled_system_setup):
|
| 629 |
+
# Test wrong number of kdes
|
| 630 |
+
self.system.kdes = self.kdes[:-1]
|
| 631 |
+
pytest.raises(ValueError, lambda: self.system.validate_system())
|
| 632 |
+
self.system.kdes = self.kdes + [u[2] + u[1] - qd[2]]
|
| 633 |
+
pytest.raises(ValueError, lambda: self.system.validate_system())
|
| 634 |
+
|
| 635 |
+
def test_duplicates(self, _filled_system_setup):
|
| 636 |
+
# This is basically a redundant feature, which should never fail
|
| 637 |
+
self.system.validate_system(check_duplicates=True)
|
| 638 |
+
|
| 639 |
+
def test_speeds_in_lagrange(self, _filled_system_setup_no_speeds):
|
| 640 |
+
self.system.u_ind = u[:len(self.u_ind)]
|
| 641 |
+
with pytest.raises(ValueError):
|
| 642 |
+
self.system.validate_system(LagrangesMethod)
|
| 643 |
+
self.system.u_ind = []
|
| 644 |
+
self.system.validate_system(LagrangesMethod)
|
| 645 |
+
self.system.u_aux = ua
|
| 646 |
+
with pytest.raises(ValueError):
|
| 647 |
+
self.system.validate_system(LagrangesMethod)
|
| 648 |
+
self.system.u_aux = []
|
| 649 |
+
self.system.validate_system(LagrangesMethod)
|
| 650 |
+
self.system.add_joints(
|
| 651 |
+
PinJoint('Ju', RigidBody('rbu1'), RigidBody('rbu2')))
|
| 652 |
+
self.system.u_ind = []
|
| 653 |
+
with pytest.raises(ValueError):
|
| 654 |
+
self.system.validate_system(LagrangesMethod)
|
| 655 |
+
|
| 656 |
+
|
| 657 |
+
class TestSystemExamples:
|
| 658 |
+
def test_cart_pendulum_kanes(self):
|
| 659 |
+
# This example is the same as in the top documentation of System
|
| 660 |
+
# Added a spring to the cart
|
| 661 |
+
g, l, mc, mp, k = symbols('g l mc mp k')
|
| 662 |
+
F, qp, qc, up, uc = dynamicsymbols('F qp qc up uc')
|
| 663 |
+
rail = RigidBody('rail')
|
| 664 |
+
cart = RigidBody('cart', mass=mc)
|
| 665 |
+
bob = Particle('bob', mass=mp)
|
| 666 |
+
bob_frame = ReferenceFrame('bob_frame')
|
| 667 |
+
system = System.from_newtonian(rail)
|
| 668 |
+
assert system.bodies == (rail,)
|
| 669 |
+
assert system.frame == rail.frame
|
| 670 |
+
assert system.fixed_point == rail.masscenter
|
| 671 |
+
slider = PrismaticJoint('slider', rail, cart, qc, uc, joint_axis=rail.x)
|
| 672 |
+
pin = PinJoint('pin', cart, bob, qp, up, joint_axis=cart.z,
|
| 673 |
+
child_interframe=bob_frame, child_point=l * bob_frame.y)
|
| 674 |
+
system.add_joints(slider, pin)
|
| 675 |
+
assert system.joints == (slider, pin)
|
| 676 |
+
assert system.get_joint('slider') == slider
|
| 677 |
+
assert system.get_body('bob') == bob
|
| 678 |
+
system.apply_uniform_gravity(-g * system.y)
|
| 679 |
+
system.add_loads((cart.masscenter, F * rail.x))
|
| 680 |
+
system.add_actuators(TorqueActuator(k * qp, cart.z, bob_frame, cart))
|
| 681 |
+
system.validate_system()
|
| 682 |
+
system.form_eoms()
|
| 683 |
+
assert isinstance(system.eom_method, KanesMethod)
|
| 684 |
+
assert (simplify(system.mass_matrix - ImmutableMatrix(
|
| 685 |
+
[[mp + mc, mp * l * cos(qp)], [mp * l * cos(qp), mp * l ** 2]]))
|
| 686 |
+
== zeros(2, 2))
|
| 687 |
+
assert (simplify(system.forcing - ImmutableMatrix([
|
| 688 |
+
[mp * l * up ** 2 * sin(qp) + F],
|
| 689 |
+
[-mp * g * l * sin(qp) + k * qp]])) == zeros(2, 1))
|
| 690 |
+
|
| 691 |
+
system.add_holonomic_constraints(
|
| 692 |
+
sympify(bob.masscenter.pos_from(rail.masscenter).dot(system.x)))
|
| 693 |
+
assert system.eom_method is None
|
| 694 |
+
system.q_ind, system.q_dep = qp, qc
|
| 695 |
+
system.u_ind, system.u_dep = up, uc
|
| 696 |
+
system.validate_system()
|
| 697 |
+
|
| 698 |
+
# Computed solution based on manually solving the constraints
|
| 699 |
+
subs = {qc: -l * sin(qp),
|
| 700 |
+
uc: -l * cos(qp) * up,
|
| 701 |
+
uc.diff(t): l * (up ** 2 * sin(qp) - up.diff(t) * cos(qp))}
|
| 702 |
+
upd_expected = (
|
| 703 |
+
(-g * mp * sin(qp) + k * qp / l + l * mc * sin(2 * qp) * up ** 2 / 2
|
| 704 |
+
- l * mp * sin(2 * qp) * up ** 2 / 2 - F * cos(qp)) /
|
| 705 |
+
(l * (mc * cos(qp) ** 2 + mp * sin(qp) ** 2)))
|
| 706 |
+
upd_sol = tuple(solve(system.form_eoms().xreplace(subs),
|
| 707 |
+
up.diff(t)).values())[0]
|
| 708 |
+
assert simplify(upd_sol - upd_expected) == 0
|
| 709 |
+
assert isinstance(system.eom_method, KanesMethod)
|
| 710 |
+
|
| 711 |
+
# Test other output
|
| 712 |
+
Mk = -ImmutableMatrix([[0, 1], [1, 0]])
|
| 713 |
+
gk = -ImmutableMatrix([uc, up])
|
| 714 |
+
Md = ImmutableMatrix([[-l ** 2 * mp * cos(qp) ** 2 + l ** 2 * mp,
|
| 715 |
+
l * mp * cos(qp) - l * (mc + mp) * cos(qp)],
|
| 716 |
+
[l * cos(qp), 1]])
|
| 717 |
+
gd = ImmutableMatrix(
|
| 718 |
+
[[-g * l * mp * sin(qp) + k * qp - l ** 2 * mp * up ** 2 * sin(qp) *
|
| 719 |
+
cos(qp) - l * F * cos(qp)], [l * up ** 2 * sin(qp)]])
|
| 720 |
+
Mm = (Mk.row_join(zeros(2, 2))).col_join(zeros(2, 2).row_join(Md))
|
| 721 |
+
gm = gk.col_join(gd)
|
| 722 |
+
assert simplify(system.mass_matrix - Md) == zeros(2, 2)
|
| 723 |
+
assert simplify(system.forcing - gd) == zeros(2, 1)
|
| 724 |
+
assert simplify(system.mass_matrix_full - Mm) == zeros(4, 4)
|
| 725 |
+
assert simplify(system.forcing_full - gm) == zeros(4, 1)
|
| 726 |
+
|
| 727 |
+
def test_cart_pendulum_lagrange(self):
|
| 728 |
+
# Lagrange version of test_cart_pendulus_kanes
|
| 729 |
+
# Added a spring to the cart
|
| 730 |
+
g, l, mc, mp, k = symbols('g l mc mp k')
|
| 731 |
+
F, qp, qc = dynamicsymbols('F qp qc')
|
| 732 |
+
qpd, qcd = dynamicsymbols('qp qc', 1)
|
| 733 |
+
rail = RigidBody('rail')
|
| 734 |
+
cart = RigidBody('cart', mass=mc)
|
| 735 |
+
bob = Particle('bob', mass=mp)
|
| 736 |
+
bob_frame = ReferenceFrame('bob_frame')
|
| 737 |
+
system = System.from_newtonian(rail)
|
| 738 |
+
assert system.bodies == (rail,)
|
| 739 |
+
assert system.frame == rail.frame
|
| 740 |
+
assert system.fixed_point == rail.masscenter
|
| 741 |
+
slider = PrismaticJoint('slider', rail, cart, qc, qcd,
|
| 742 |
+
joint_axis=rail.x)
|
| 743 |
+
pin = PinJoint('pin', cart, bob, qp, qpd, joint_axis=cart.z,
|
| 744 |
+
child_interframe=bob_frame, child_point=l * bob_frame.y)
|
| 745 |
+
system.add_joints(slider, pin)
|
| 746 |
+
assert system.joints == (slider, pin)
|
| 747 |
+
assert system.get_joint('slider') == slider
|
| 748 |
+
assert system.get_body('bob') == bob
|
| 749 |
+
for body in system.bodies:
|
| 750 |
+
body.potential_energy = body.mass * g * body.masscenter.pos_from(
|
| 751 |
+
system.fixed_point).dot(system.y)
|
| 752 |
+
system.add_loads((cart.masscenter, F * rail.x))
|
| 753 |
+
system.add_actuators(TorqueActuator(k * qp, cart.z, bob_frame, cart))
|
| 754 |
+
system.validate_system(LagrangesMethod)
|
| 755 |
+
system.form_eoms(LagrangesMethod)
|
| 756 |
+
assert (simplify(system.mass_matrix - ImmutableMatrix(
|
| 757 |
+
[[mp + mc, mp * l * cos(qp)], [mp * l * cos(qp), mp * l ** 2]]))
|
| 758 |
+
== zeros(2, 2))
|
| 759 |
+
assert (simplify(system.forcing - ImmutableMatrix([
|
| 760 |
+
[mp * l * qpd ** 2 * sin(qp) + F], [-mp * g * l * sin(qp) + k * qp]]
|
| 761 |
+
)) == zeros(2, 1))
|
| 762 |
+
|
| 763 |
+
system.add_holonomic_constraints(
|
| 764 |
+
sympify(bob.masscenter.pos_from(rail.masscenter).dot(system.x)))
|
| 765 |
+
assert system.eom_method is None
|
| 766 |
+
system.q_ind, system.q_dep = qp, qc
|
| 767 |
+
|
| 768 |
+
# Computed solution based on manually solving the constraints
|
| 769 |
+
subs = {qc: -l * sin(qp),
|
| 770 |
+
qcd: -l * cos(qp) * qpd,
|
| 771 |
+
qcd.diff(t): l * (qpd ** 2 * sin(qp) - qpd.diff(t) * cos(qp))}
|
| 772 |
+
qpdd_expected = (
|
| 773 |
+
(-g * mp * sin(qp) + k * qp / l + l * mc * sin(2 * qp) * qpd ** 2 /
|
| 774 |
+
2 - l * mp * sin(2 * qp) * qpd ** 2 / 2 - F * cos(qp)) /
|
| 775 |
+
(l * (mc * cos(qp) ** 2 + mp * sin(qp) ** 2)))
|
| 776 |
+
eoms = system.form_eoms(LagrangesMethod)
|
| 777 |
+
lam1 = system.eom_method.lam_vec[0]
|
| 778 |
+
lam1_sol = system.eom_method.solve_multipliers()[lam1]
|
| 779 |
+
qpdd_sol = solve(eoms[0].xreplace({lam1: lam1_sol}).xreplace(subs),
|
| 780 |
+
qpd.diff(t))[0]
|
| 781 |
+
assert simplify(qpdd_sol - qpdd_expected) == 0
|
| 782 |
+
assert isinstance(system.eom_method, LagrangesMethod)
|
| 783 |
+
|
| 784 |
+
# Test other output
|
| 785 |
+
Md = ImmutableMatrix([[l ** 2 * mp, l * mp * cos(qp), -l * cos(qp)],
|
| 786 |
+
[l * mp * cos(qp), mc + mp, -1]])
|
| 787 |
+
gd = ImmutableMatrix(
|
| 788 |
+
[[-g * l * mp * sin(qp) + k * qp],
|
| 789 |
+
[l * mp * sin(qp) * qpd ** 2 + F]])
|
| 790 |
+
Mm = (eye(2).row_join(zeros(2, 3))).col_join(zeros(3, 2).row_join(
|
| 791 |
+
Md.col_join(ImmutableMatrix([l * cos(qp), 1, 0]).T)))
|
| 792 |
+
gm = ImmutableMatrix([qpd, qcd] + gd[:] + [l * sin(qp) * qpd ** 2])
|
| 793 |
+
assert simplify(system.mass_matrix - Md) == zeros(2, 3)
|
| 794 |
+
assert simplify(system.forcing - gd) == zeros(2, 1)
|
| 795 |
+
assert simplify(system.mass_matrix_full - Mm) == zeros(5, 5)
|
| 796 |
+
assert simplify(system.forcing_full - gm) == zeros(5, 1)
|
| 797 |
+
|
| 798 |
+
def test_box_on_ground(self):
|
| 799 |
+
# Particle sliding on ground with friction. The applied force is assumed
|
| 800 |
+
# to be positive and to be higher than the friction force.
|
| 801 |
+
g, m, mu = symbols('g m mu')
|
| 802 |
+
q, u, ua = dynamicsymbols('q u ua')
|
| 803 |
+
N, F = dynamicsymbols('N F', positive=True)
|
| 804 |
+
P = Particle("P", mass=m)
|
| 805 |
+
system = System()
|
| 806 |
+
system.add_bodies(P)
|
| 807 |
+
P.masscenter.set_pos(system.fixed_point, q * system.x)
|
| 808 |
+
P.masscenter.set_vel(system.frame, u * system.x + ua * system.y)
|
| 809 |
+
system.q_ind, system.u_ind, system.u_aux = [q], [u], [ua]
|
| 810 |
+
system.kdes = [q.diff(t) - u]
|
| 811 |
+
system.apply_uniform_gravity(-g * system.y)
|
| 812 |
+
system.add_loads(
|
| 813 |
+
Force(P, N * system.y),
|
| 814 |
+
Force(P, F * system.x - mu * N * system.x))
|
| 815 |
+
system.validate_system()
|
| 816 |
+
system.form_eoms()
|
| 817 |
+
|
| 818 |
+
# Test other output
|
| 819 |
+
Mk = ImmutableMatrix([1])
|
| 820 |
+
gk = ImmutableMatrix([u])
|
| 821 |
+
Md = ImmutableMatrix([m])
|
| 822 |
+
gd = ImmutableMatrix([F - mu * N])
|
| 823 |
+
Mm = (Mk.row_join(zeros(1, 1))).col_join(zeros(1, 1).row_join(Md))
|
| 824 |
+
gm = gk.col_join(gd)
|
| 825 |
+
aux_eqs = ImmutableMatrix([N - m * g])
|
| 826 |
+
assert simplify(system.mass_matrix - Md) == zeros(1, 1)
|
| 827 |
+
assert simplify(system.forcing - gd) == zeros(1, 1)
|
| 828 |
+
assert simplify(system.mass_matrix_full - Mm) == zeros(2, 2)
|
| 829 |
+
assert simplify(system.forcing_full - gm) == zeros(2, 1)
|
| 830 |
+
assert simplify(system.eom_method.auxiliary_eqs - aux_eqs
|
| 831 |
+
) == zeros(1, 1)
|
lib/python3.12/site-packages/sympy/physics/mechanics/tests/test_wrapping_geometry.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the ``sympy.physics.mechanics.wrapping_geometry.py`` module."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
from sympy import (
|
| 6 |
+
Integer,
|
| 7 |
+
Rational,
|
| 8 |
+
S,
|
| 9 |
+
Symbol,
|
| 10 |
+
acos,
|
| 11 |
+
cos,
|
| 12 |
+
pi,
|
| 13 |
+
sin,
|
| 14 |
+
sqrt,
|
| 15 |
+
)
|
| 16 |
+
from sympy.core.relational import Eq
|
| 17 |
+
from sympy.physics.mechanics import (
|
| 18 |
+
Point,
|
| 19 |
+
ReferenceFrame,
|
| 20 |
+
WrappingCylinder,
|
| 21 |
+
WrappingSphere,
|
| 22 |
+
dynamicsymbols,
|
| 23 |
+
)
|
| 24 |
+
from sympy.simplify.simplify import simplify
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
r = Symbol('r', positive=True)
|
| 28 |
+
x = Symbol('x')
|
| 29 |
+
q = dynamicsymbols('q')
|
| 30 |
+
N = ReferenceFrame('N')
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class TestWrappingSphere:
|
| 34 |
+
|
| 35 |
+
@staticmethod
|
| 36 |
+
def test_valid_constructor():
|
| 37 |
+
r = Symbol('r', positive=True)
|
| 38 |
+
pO = Point('pO')
|
| 39 |
+
sphere = WrappingSphere(r, pO)
|
| 40 |
+
assert isinstance(sphere, WrappingSphere)
|
| 41 |
+
assert hasattr(sphere, 'radius')
|
| 42 |
+
assert sphere.radius == r
|
| 43 |
+
assert hasattr(sphere, 'point')
|
| 44 |
+
assert sphere.point == pO
|
| 45 |
+
|
| 46 |
+
@staticmethod
|
| 47 |
+
@pytest.mark.parametrize('position', [S.Zero, Integer(2)*r*N.x])
|
| 48 |
+
def test_geodesic_length_point_not_on_surface_invalid(position):
|
| 49 |
+
r = Symbol('r', positive=True)
|
| 50 |
+
pO = Point('pO')
|
| 51 |
+
sphere = WrappingSphere(r, pO)
|
| 52 |
+
|
| 53 |
+
p1 = Point('p1')
|
| 54 |
+
p1.set_pos(pO, position)
|
| 55 |
+
p2 = Point('p2')
|
| 56 |
+
p2.set_pos(pO, position)
|
| 57 |
+
|
| 58 |
+
error_msg = r'point .* does not lie on the surface of'
|
| 59 |
+
with pytest.raises(ValueError, match=error_msg):
|
| 60 |
+
sphere.geodesic_length(p1, p2)
|
| 61 |
+
|
| 62 |
+
@staticmethod
|
| 63 |
+
@pytest.mark.parametrize(
|
| 64 |
+
'position_1, position_2, expected',
|
| 65 |
+
[
|
| 66 |
+
(r*N.x, r*N.x, S.Zero),
|
| 67 |
+
(r*N.x, r*N.y, S.Half*pi*r),
|
| 68 |
+
(r*N.x, r*-N.x, pi*r),
|
| 69 |
+
(r*-N.x, r*N.x, pi*r),
|
| 70 |
+
(r*N.x, r*sqrt(2)*S.Half*(N.x + N.y), Rational(1, 4)*pi*r),
|
| 71 |
+
(
|
| 72 |
+
r*sqrt(2)*S.Half*(N.x + N.y),
|
| 73 |
+
r*sqrt(3)*Rational(1, 3)*(N.x + N.y + N.z),
|
| 74 |
+
r*acos(sqrt(6)*Rational(1, 3)),
|
| 75 |
+
),
|
| 76 |
+
]
|
| 77 |
+
)
|
| 78 |
+
def test_geodesic_length(position_1, position_2, expected):
|
| 79 |
+
r = Symbol('r', positive=True)
|
| 80 |
+
pO = Point('pO')
|
| 81 |
+
sphere = WrappingSphere(r, pO)
|
| 82 |
+
|
| 83 |
+
p1 = Point('p1')
|
| 84 |
+
p1.set_pos(pO, position_1)
|
| 85 |
+
p2 = Point('p2')
|
| 86 |
+
p2.set_pos(pO, position_2)
|
| 87 |
+
|
| 88 |
+
assert simplify(Eq(sphere.geodesic_length(p1, p2), expected))
|
| 89 |
+
|
| 90 |
+
@staticmethod
|
| 91 |
+
@pytest.mark.parametrize(
|
| 92 |
+
'position_1, position_2, vector_1, vector_2',
|
| 93 |
+
[
|
| 94 |
+
(r * N.x, r * N.y, N.y, N.x),
|
| 95 |
+
(r * N.x, -r * N.y, -N.y, N.x),
|
| 96 |
+
(
|
| 97 |
+
r * N.y,
|
| 98 |
+
sqrt(2)/2 * r * N.x - sqrt(2)/2 * r * N.y,
|
| 99 |
+
N.x,
|
| 100 |
+
sqrt(2)/2 * N.x + sqrt(2)/2 * N.y,
|
| 101 |
+
),
|
| 102 |
+
(
|
| 103 |
+
r * N.x,
|
| 104 |
+
r / 2 * N.x + sqrt(3)/2 * r * N.y,
|
| 105 |
+
N.y,
|
| 106 |
+
sqrt(3)/2 * N.x - 1/2 * N.y,
|
| 107 |
+
),
|
| 108 |
+
(
|
| 109 |
+
r * N.x,
|
| 110 |
+
sqrt(2)/2 * r * N.x + sqrt(2)/2 * r * N.y,
|
| 111 |
+
N.y,
|
| 112 |
+
sqrt(2)/2 * N.x - sqrt(2)/2 * N.y,
|
| 113 |
+
),
|
| 114 |
+
]
|
| 115 |
+
)
|
| 116 |
+
def test_geodesic_end_vectors(position_1, position_2, vector_1, vector_2):
|
| 117 |
+
r = Symbol('r', positive=True)
|
| 118 |
+
pO = Point('pO')
|
| 119 |
+
sphere = WrappingSphere(r, pO)
|
| 120 |
+
|
| 121 |
+
p1 = Point('p1')
|
| 122 |
+
p1.set_pos(pO, position_1)
|
| 123 |
+
p2 = Point('p2')
|
| 124 |
+
p2.set_pos(pO, position_2)
|
| 125 |
+
|
| 126 |
+
expected = (vector_1, vector_2)
|
| 127 |
+
|
| 128 |
+
assert sphere.geodesic_end_vectors(p1, p2) == expected
|
| 129 |
+
|
| 130 |
+
@staticmethod
|
| 131 |
+
@pytest.mark.parametrize(
|
| 132 |
+
'position',
|
| 133 |
+
[r * N.x, r * cos(q) * N.x + r * sin(q) * N.y]
|
| 134 |
+
)
|
| 135 |
+
def test_geodesic_end_vectors_invalid_coincident(position):
|
| 136 |
+
r = Symbol('r', positive=True)
|
| 137 |
+
pO = Point('pO')
|
| 138 |
+
sphere = WrappingSphere(r, pO)
|
| 139 |
+
|
| 140 |
+
p1 = Point('p1')
|
| 141 |
+
p1.set_pos(pO, position)
|
| 142 |
+
p2 = Point('p2')
|
| 143 |
+
p2.set_pos(pO, position)
|
| 144 |
+
|
| 145 |
+
with pytest.raises(ValueError):
|
| 146 |
+
_ = sphere.geodesic_end_vectors(p1, p2)
|
| 147 |
+
|
| 148 |
+
@staticmethod
|
| 149 |
+
@pytest.mark.parametrize(
|
| 150 |
+
'position_1, position_2',
|
| 151 |
+
[
|
| 152 |
+
(r * N.x, -r * N.x),
|
| 153 |
+
(-r * N.y, r * N.y),
|
| 154 |
+
(
|
| 155 |
+
r * cos(q) * N.x + r * sin(q) * N.y,
|
| 156 |
+
-r * cos(q) * N.x - r * sin(q) * N.y,
|
| 157 |
+
)
|
| 158 |
+
]
|
| 159 |
+
)
|
| 160 |
+
def test_geodesic_end_vectors_invalid_diametrically_opposite(
|
| 161 |
+
position_1,
|
| 162 |
+
position_2,
|
| 163 |
+
):
|
| 164 |
+
r = Symbol('r', positive=True)
|
| 165 |
+
pO = Point('pO')
|
| 166 |
+
sphere = WrappingSphere(r, pO)
|
| 167 |
+
|
| 168 |
+
p1 = Point('p1')
|
| 169 |
+
p1.set_pos(pO, position_1)
|
| 170 |
+
p2 = Point('p2')
|
| 171 |
+
p2.set_pos(pO, position_2)
|
| 172 |
+
|
| 173 |
+
with pytest.raises(ValueError):
|
| 174 |
+
_ = sphere.geodesic_end_vectors(p1, p2)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
class TestWrappingCylinder:
|
| 178 |
+
|
| 179 |
+
@staticmethod
|
| 180 |
+
def test_valid_constructor():
|
| 181 |
+
N = ReferenceFrame('N')
|
| 182 |
+
r = Symbol('r', positive=True)
|
| 183 |
+
pO = Point('pO')
|
| 184 |
+
cylinder = WrappingCylinder(r, pO, N.x)
|
| 185 |
+
assert isinstance(cylinder, WrappingCylinder)
|
| 186 |
+
assert hasattr(cylinder, 'radius')
|
| 187 |
+
assert cylinder.radius == r
|
| 188 |
+
assert hasattr(cylinder, 'point')
|
| 189 |
+
assert cylinder.point == pO
|
| 190 |
+
assert hasattr(cylinder, 'axis')
|
| 191 |
+
assert cylinder.axis == N.x
|
| 192 |
+
|
| 193 |
+
@staticmethod
|
| 194 |
+
@pytest.mark.parametrize(
|
| 195 |
+
'position, expected',
|
| 196 |
+
[
|
| 197 |
+
(S.Zero, False),
|
| 198 |
+
(r*N.y, True),
|
| 199 |
+
(r*N.z, True),
|
| 200 |
+
(r*(N.y + N.z).normalize(), True),
|
| 201 |
+
(Integer(2)*r*N.y, False),
|
| 202 |
+
(r*(N.x + N.y), True),
|
| 203 |
+
(r*(Integer(2)*N.x + N.y), True),
|
| 204 |
+
(Integer(2)*N.x + r*(Integer(2)*N.y + N.z).normalize(), True),
|
| 205 |
+
(r*(cos(q)*N.y + sin(q)*N.z), True)
|
| 206 |
+
]
|
| 207 |
+
)
|
| 208 |
+
def test_point_is_on_surface(position, expected):
|
| 209 |
+
r = Symbol('r', positive=True)
|
| 210 |
+
pO = Point('pO')
|
| 211 |
+
cylinder = WrappingCylinder(r, pO, N.x)
|
| 212 |
+
|
| 213 |
+
p1 = Point('p1')
|
| 214 |
+
p1.set_pos(pO, position)
|
| 215 |
+
|
| 216 |
+
assert cylinder.point_on_surface(p1) is expected
|
| 217 |
+
|
| 218 |
+
@staticmethod
|
| 219 |
+
@pytest.mark.parametrize('position', [S.Zero, Integer(2)*r*N.y])
|
| 220 |
+
def test_geodesic_length_point_not_on_surface_invalid(position):
|
| 221 |
+
r = Symbol('r', positive=True)
|
| 222 |
+
pO = Point('pO')
|
| 223 |
+
cylinder = WrappingCylinder(r, pO, N.x)
|
| 224 |
+
|
| 225 |
+
p1 = Point('p1')
|
| 226 |
+
p1.set_pos(pO, position)
|
| 227 |
+
p2 = Point('p2')
|
| 228 |
+
p2.set_pos(pO, position)
|
| 229 |
+
|
| 230 |
+
error_msg = r'point .* does not lie on the surface of'
|
| 231 |
+
with pytest.raises(ValueError, match=error_msg):
|
| 232 |
+
cylinder.geodesic_length(p1, p2)
|
| 233 |
+
|
| 234 |
+
@staticmethod
|
| 235 |
+
@pytest.mark.parametrize(
|
| 236 |
+
'axis, position_1, position_2, expected',
|
| 237 |
+
[
|
| 238 |
+
(N.x, r*N.y, r*N.y, S.Zero),
|
| 239 |
+
(N.x, r*N.y, N.x + r*N.y, S.One),
|
| 240 |
+
(N.x, r*N.y, -x*N.x + r*N.y, sqrt(x**2)),
|
| 241 |
+
(-N.x, r*N.y, x*N.x + r*N.y, sqrt(x**2)),
|
| 242 |
+
(N.x, r*N.y, r*N.z, S.Half*pi*sqrt(r**2)),
|
| 243 |
+
(-N.x, r*N.y, r*N.z, Integer(3)*S.Half*pi*sqrt(r**2)),
|
| 244 |
+
(N.x, r*N.z, r*N.y, Integer(3)*S.Half*pi*sqrt(r**2)),
|
| 245 |
+
(-N.x, r*N.z, r*N.y, S.Half*pi*sqrt(r**2)),
|
| 246 |
+
(N.x, r*N.y, r*(cos(q)*N.y + sin(q)*N.z), sqrt(r**2*q**2)),
|
| 247 |
+
(
|
| 248 |
+
-N.x, r*N.y,
|
| 249 |
+
r*(cos(q)*N.y + sin(q)*N.z),
|
| 250 |
+
sqrt(r**2*(Integer(2)*pi - q)**2),
|
| 251 |
+
),
|
| 252 |
+
]
|
| 253 |
+
)
|
| 254 |
+
def test_geodesic_length(axis, position_1, position_2, expected):
|
| 255 |
+
r = Symbol('r', positive=True)
|
| 256 |
+
pO = Point('pO')
|
| 257 |
+
cylinder = WrappingCylinder(r, pO, axis)
|
| 258 |
+
|
| 259 |
+
p1 = Point('p1')
|
| 260 |
+
p1.set_pos(pO, position_1)
|
| 261 |
+
p2 = Point('p2')
|
| 262 |
+
p2.set_pos(pO, position_2)
|
| 263 |
+
|
| 264 |
+
assert simplify(Eq(cylinder.geodesic_length(p1, p2), expected))
|
| 265 |
+
|
| 266 |
+
@staticmethod
|
| 267 |
+
@pytest.mark.parametrize(
|
| 268 |
+
'axis, position_1, position_2, vector_1, vector_2',
|
| 269 |
+
[
|
| 270 |
+
(N.z, r * N.x, r * N.y, N.y, N.x),
|
| 271 |
+
(N.z, r * N.x, -r * N.x, N.y, N.y),
|
| 272 |
+
(N.z, -r * N.x, r * N.x, -N.y, -N.y),
|
| 273 |
+
(-N.z, r * N.x, -r * N.x, -N.y, -N.y),
|
| 274 |
+
(-N.z, -r * N.x, r * N.x, N.y, N.y),
|
| 275 |
+
(N.z, r * N.x, -r * N.y, N.y, -N.x),
|
| 276 |
+
(
|
| 277 |
+
N.z,
|
| 278 |
+
r * N.y,
|
| 279 |
+
sqrt(2)/2 * r * N.x - sqrt(2)/2 * r * N.y,
|
| 280 |
+
- N.x,
|
| 281 |
+
- sqrt(2)/2 * N.x - sqrt(2)/2 * N.y,
|
| 282 |
+
),
|
| 283 |
+
(
|
| 284 |
+
N.z,
|
| 285 |
+
r * N.x,
|
| 286 |
+
r / 2 * N.x + sqrt(3)/2 * r * N.y,
|
| 287 |
+
N.y,
|
| 288 |
+
sqrt(3)/2 * N.x - 1/2 * N.y,
|
| 289 |
+
),
|
| 290 |
+
(
|
| 291 |
+
N.z,
|
| 292 |
+
r * N.x,
|
| 293 |
+
sqrt(2)/2 * r * N.x + sqrt(2)/2 * r * N.y,
|
| 294 |
+
N.y,
|
| 295 |
+
sqrt(2)/2 * N.x - sqrt(2)/2 * N.y,
|
| 296 |
+
),
|
| 297 |
+
(
|
| 298 |
+
N.z,
|
| 299 |
+
r * N.x,
|
| 300 |
+
r * N.x + N.z,
|
| 301 |
+
N.z,
|
| 302 |
+
-N.z,
|
| 303 |
+
),
|
| 304 |
+
(
|
| 305 |
+
N.z,
|
| 306 |
+
r * N.x,
|
| 307 |
+
r * N.y + pi/2 * r * N.z,
|
| 308 |
+
sqrt(2)/2 * N.y + sqrt(2)/2 * N.z,
|
| 309 |
+
sqrt(2)/2 * N.x - sqrt(2)/2 * N.z,
|
| 310 |
+
),
|
| 311 |
+
(
|
| 312 |
+
N.z,
|
| 313 |
+
r * N.x,
|
| 314 |
+
r * cos(q) * N.x + r * sin(q) * N.y,
|
| 315 |
+
N.y,
|
| 316 |
+
sin(q) * N.x - cos(q) * N.y,
|
| 317 |
+
),
|
| 318 |
+
]
|
| 319 |
+
)
|
| 320 |
+
def test_geodesic_end_vectors(
|
| 321 |
+
axis,
|
| 322 |
+
position_1,
|
| 323 |
+
position_2,
|
| 324 |
+
vector_1,
|
| 325 |
+
vector_2,
|
| 326 |
+
):
|
| 327 |
+
r = Symbol('r', positive=True)
|
| 328 |
+
pO = Point('pO')
|
| 329 |
+
cylinder = WrappingCylinder(r, pO, axis)
|
| 330 |
+
|
| 331 |
+
p1 = Point('p1')
|
| 332 |
+
p1.set_pos(pO, position_1)
|
| 333 |
+
p2 = Point('p2')
|
| 334 |
+
p2.set_pos(pO, position_2)
|
| 335 |
+
|
| 336 |
+
expected = (vector_1, vector_2)
|
| 337 |
+
end_vectors = tuple(
|
| 338 |
+
end_vector.simplify()
|
| 339 |
+
for end_vector in cylinder.geodesic_end_vectors(p1, p2)
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
assert end_vectors == expected
|
| 343 |
+
|
| 344 |
+
@staticmethod
|
| 345 |
+
@pytest.mark.parametrize(
|
| 346 |
+
'axis, position',
|
| 347 |
+
[
|
| 348 |
+
(N.z, r * N.x),
|
| 349 |
+
(N.z, r * cos(q) * N.x + r * sin(q) * N.y + N.z),
|
| 350 |
+
]
|
| 351 |
+
)
|
| 352 |
+
def test_geodesic_end_vectors_invalid_coincident(axis, position):
|
| 353 |
+
r = Symbol('r', positive=True)
|
| 354 |
+
pO = Point('pO')
|
| 355 |
+
cylinder = WrappingCylinder(r, pO, axis)
|
| 356 |
+
|
| 357 |
+
p1 = Point('p1')
|
| 358 |
+
p1.set_pos(pO, position)
|
| 359 |
+
p2 = Point('p2')
|
| 360 |
+
p2.set_pos(pO, position)
|
| 361 |
+
|
| 362 |
+
with pytest.raises(ValueError):
|
| 363 |
+
_ = cylinder.geodesic_end_vectors(p1, p2)
|
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (1.69 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/anticommutator.cpython-312.pyc
ADDED
|
Binary file (6.55 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/boson.cpython-312.pyc
ADDED
|
Binary file (11.7 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/cartesian.cpython-312.pyc
ADDED
|
Binary file (15.5 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/cg.cpython-312.pyc
ADDED
|
Binary file (36.3 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/circuitplot.cpython-312.pyc
ADDED
|
Binary file (16.2 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/circuitutils.cpython-312.pyc
ADDED
|
Binary file (14 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/commutator.cpython-312.pyc
ADDED
|
Binary file (9.93 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/constants.cpython-312.pyc
ADDED
|
Binary file (2.01 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/dagger.cpython-312.pyc
ADDED
|
Binary file (3.42 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/density.cpython-312.pyc
ADDED
|
Binary file (12.7 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/fermion.cpython-312.pyc
ADDED
|
Binary file (8.3 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/gate.cpython-312.pyc
ADDED
|
Binary file (50.3 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/grover.cpython-312.pyc
ADDED
|
Binary file (11.9 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/quantum/__pycache__/hilbert.cpython-312.pyc
ADDED
|
Binary file (27.2 kB). View file
|
|
|