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/control/__init__.py +16 -0
- lib/python3.12/site-packages/sympy/physics/control/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/control/__pycache__/control_plots.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/control/control_plots.py +978 -0
- lib/python3.12/site-packages/sympy/physics/control/lti.py +0 -0
- lib/python3.12/site-packages/sympy/physics/control/tests/__init__.py +0 -0
- lib/python3.12/site-packages/sympy/physics/control/tests/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/control/tests/__pycache__/test_control_plots.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/control/tests/test_control_plots.py +299 -0
- lib/python3.12/site-packages/sympy/physics/control/tests/test_lti.py +1750 -0
- lib/python3.12/site-packages/sympy/physics/hep/__init__.py +0 -0
- lib/python3.12/site-packages/sympy/physics/hep/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/hep/__pycache__/gamma_matrices.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/hep/gamma_matrices.py +716 -0
- lib/python3.12/site-packages/sympy/physics/hep/tests/__init__.py +0 -0
- lib/python3.12/site-packages/sympy/physics/hep/tests/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/hep/tests/__pycache__/test_gamma_matrices.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/hep/tests/test_gamma_matrices.py +427 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/__init__.py +90 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/actuator.py +992 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/body.py +710 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/body_base.py +94 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/joint.py +2188 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/jointsmethod.py +318 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/kane.py +860 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/linearize.py +474 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/models.py +230 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/particle.py +209 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/rigidbody.py +314 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/system.py +1553 -0
- lib/python3.12/site-packages/sympy/physics/mechanics/wrapping_geometry.py +641 -0
- lib/python3.12/site-packages/sympy/physics/tests/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_clebsch_gordan.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_hydrogen.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_paulialgebra.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_physics_matrices.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_pring.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_qho_1d.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_secondquant.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_sho.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/tests/test_physics_matrices.py +84 -0
- lib/python3.12/site-packages/sympy/physics/tests/test_secondquant.py +1280 -0
- lib/python3.12/site-packages/sympy/physics/tests/test_sho.py +21 -0
- lib/python3.12/site-packages/sympy/physics/units/__init__.py +453 -0
- lib/python3.12/site-packages/sympy/physics/units/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/units/__pycache__/dimensions.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/units/__pycache__/prefixes.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/units/__pycache__/quantities.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/units/__pycache__/unitsystem.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sympy/physics/units/__pycache__/util.cpython-312.pyc +0 -0
lib/python3.12/site-packages/sympy/physics/control/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .lti import (TransferFunction, Series, MIMOSeries, Parallel, MIMOParallel,
|
| 2 |
+
Feedback, MIMOFeedback, TransferFunctionMatrix, StateSpace, gbt, bilinear, forward_diff,
|
| 3 |
+
backward_diff, phase_margin, gain_margin)
|
| 4 |
+
from .control_plots import (pole_zero_numerical_data, pole_zero_plot, step_response_numerical_data,
|
| 5 |
+
step_response_plot, impulse_response_numerical_data, impulse_response_plot, ramp_response_numerical_data,
|
| 6 |
+
ramp_response_plot, bode_magnitude_numerical_data, bode_phase_numerical_data, bode_magnitude_plot,
|
| 7 |
+
bode_phase_plot, bode_plot)
|
| 8 |
+
|
| 9 |
+
__all__ = ['TransferFunction', 'Series', 'MIMOSeries', 'Parallel',
|
| 10 |
+
'MIMOParallel', 'Feedback', 'MIMOFeedback', 'TransferFunctionMatrix', 'StateSpace',
|
| 11 |
+
'gbt', 'bilinear', 'forward_diff', 'backward_diff', 'phase_margin', 'gain_margin',
|
| 12 |
+
'pole_zero_numerical_data', 'pole_zero_plot', 'step_response_numerical_data',
|
| 13 |
+
'step_response_plot', 'impulse_response_numerical_data', 'impulse_response_plot',
|
| 14 |
+
'ramp_response_numerical_data', 'ramp_response_plot',
|
| 15 |
+
'bode_magnitude_numerical_data', 'bode_phase_numerical_data',
|
| 16 |
+
'bode_magnitude_plot', 'bode_phase_plot', 'bode_plot']
|
lib/python3.12/site-packages/sympy/physics/control/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (1.2 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/control/__pycache__/control_plots.cpython-312.pyc
ADDED
|
Binary file (36.9 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/control/control_plots.py
ADDED
|
@@ -0,0 +1,978 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy.core.numbers import I, pi
|
| 2 |
+
from sympy.functions.elementary.exponential import (exp, log)
|
| 3 |
+
from sympy.polys.partfrac import apart
|
| 4 |
+
from sympy.core.symbol import Dummy
|
| 5 |
+
from sympy.external import import_module
|
| 6 |
+
from sympy.functions import arg, Abs
|
| 7 |
+
from sympy.integrals.laplace import _fast_inverse_laplace
|
| 8 |
+
from sympy.physics.control.lti import SISOLinearTimeInvariant
|
| 9 |
+
from sympy.plotting.series import LineOver1DRangeSeries
|
| 10 |
+
from sympy.polys.polytools import Poly
|
| 11 |
+
from sympy.printing.latex import latex
|
| 12 |
+
|
| 13 |
+
__all__ = ['pole_zero_numerical_data', 'pole_zero_plot',
|
| 14 |
+
'step_response_numerical_data', 'step_response_plot',
|
| 15 |
+
'impulse_response_numerical_data', 'impulse_response_plot',
|
| 16 |
+
'ramp_response_numerical_data', 'ramp_response_plot',
|
| 17 |
+
'bode_magnitude_numerical_data', 'bode_phase_numerical_data',
|
| 18 |
+
'bode_magnitude_plot', 'bode_phase_plot', 'bode_plot']
|
| 19 |
+
|
| 20 |
+
matplotlib = import_module(
|
| 21 |
+
'matplotlib', import_kwargs={'fromlist': ['pyplot']},
|
| 22 |
+
catch=(RuntimeError,))
|
| 23 |
+
|
| 24 |
+
numpy = import_module('numpy')
|
| 25 |
+
|
| 26 |
+
if matplotlib:
|
| 27 |
+
plt = matplotlib.pyplot
|
| 28 |
+
|
| 29 |
+
if numpy:
|
| 30 |
+
np = numpy # Matplotlib already has numpy as a compulsory dependency. No need to install it separately.
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _check_system(system):
|
| 34 |
+
"""Function to check whether the dynamical system passed for plots is
|
| 35 |
+
compatible or not."""
|
| 36 |
+
if not isinstance(system, SISOLinearTimeInvariant):
|
| 37 |
+
raise NotImplementedError("Only SISO LTI systems are currently supported.")
|
| 38 |
+
sys = system.to_expr()
|
| 39 |
+
len_free_symbols = len(sys.free_symbols)
|
| 40 |
+
if len_free_symbols > 1:
|
| 41 |
+
raise ValueError("Extra degree of freedom found. Make sure"
|
| 42 |
+
" that there are no free symbols in the dynamical system other"
|
| 43 |
+
" than the variable of Laplace transform.")
|
| 44 |
+
if sys.has(exp):
|
| 45 |
+
# Should test that exp is not part of a constant, in which case
|
| 46 |
+
# no exception is required, compare exp(s) with s*exp(1)
|
| 47 |
+
raise NotImplementedError("Time delay terms are not supported.")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def pole_zero_numerical_data(system):
|
| 51 |
+
"""
|
| 52 |
+
Returns the numerical data of poles and zeros of the system.
|
| 53 |
+
It is internally used by ``pole_zero_plot`` to get the data
|
| 54 |
+
for plotting poles and zeros. Users can use this data to further
|
| 55 |
+
analyse the dynamics of the system or plot using a different
|
| 56 |
+
backend/plotting-module.
|
| 57 |
+
|
| 58 |
+
Parameters
|
| 59 |
+
==========
|
| 60 |
+
|
| 61 |
+
system : SISOLinearTimeInvariant
|
| 62 |
+
The system for which the pole-zero data is to be computed.
|
| 63 |
+
|
| 64 |
+
Returns
|
| 65 |
+
=======
|
| 66 |
+
|
| 67 |
+
tuple : (zeros, poles)
|
| 68 |
+
zeros = Zeros of the system. NumPy array of complex numbers.
|
| 69 |
+
poles = Poles of the system. NumPy array of complex numbers.
|
| 70 |
+
|
| 71 |
+
Raises
|
| 72 |
+
======
|
| 73 |
+
|
| 74 |
+
NotImplementedError
|
| 75 |
+
When a SISO LTI system is not passed.
|
| 76 |
+
|
| 77 |
+
When time delay terms are present in the system.
|
| 78 |
+
|
| 79 |
+
ValueError
|
| 80 |
+
When more than one free symbol is present in the system.
|
| 81 |
+
The only variable in the transfer function should be
|
| 82 |
+
the variable of the Laplace transform.
|
| 83 |
+
|
| 84 |
+
Examples
|
| 85 |
+
========
|
| 86 |
+
|
| 87 |
+
>>> from sympy.abc import s
|
| 88 |
+
>>> from sympy.physics.control.lti import TransferFunction
|
| 89 |
+
>>> from sympy.physics.control.control_plots import pole_zero_numerical_data
|
| 90 |
+
>>> tf1 = TransferFunction(s**2 + 1, s**4 + 4*s**3 + 6*s**2 + 5*s + 2, s)
|
| 91 |
+
>>> pole_zero_numerical_data(tf1) # doctest: +SKIP
|
| 92 |
+
([-0.+1.j 0.-1.j], [-2. +0.j -0.5+0.8660254j -0.5-0.8660254j -1. +0.j ])
|
| 93 |
+
|
| 94 |
+
See Also
|
| 95 |
+
========
|
| 96 |
+
|
| 97 |
+
pole_zero_plot
|
| 98 |
+
|
| 99 |
+
"""
|
| 100 |
+
_check_system(system)
|
| 101 |
+
system = system.doit() # Get the equivalent TransferFunction object.
|
| 102 |
+
|
| 103 |
+
num_poly = Poly(system.num, system.var).all_coeffs()
|
| 104 |
+
den_poly = Poly(system.den, system.var).all_coeffs()
|
| 105 |
+
|
| 106 |
+
num_poly = np.array(num_poly, dtype=np.complex128)
|
| 107 |
+
den_poly = np.array(den_poly, dtype=np.complex128)
|
| 108 |
+
|
| 109 |
+
zeros = np.roots(num_poly)
|
| 110 |
+
poles = np.roots(den_poly)
|
| 111 |
+
|
| 112 |
+
return zeros, poles
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def pole_zero_plot(system, pole_color='blue', pole_markersize=10,
|
| 116 |
+
zero_color='orange', zero_markersize=7, grid=True, show_axes=True,
|
| 117 |
+
show=True, **kwargs):
|
| 118 |
+
r"""
|
| 119 |
+
Returns the Pole-Zero plot (also known as PZ Plot or PZ Map) of a system.
|
| 120 |
+
|
| 121 |
+
A Pole-Zero plot is a graphical representation of a system's poles and
|
| 122 |
+
zeros. It is plotted on a complex plane, with circular markers representing
|
| 123 |
+
the system's zeros and 'x' shaped markers representing the system's poles.
|
| 124 |
+
|
| 125 |
+
Parameters
|
| 126 |
+
==========
|
| 127 |
+
|
| 128 |
+
system : SISOLinearTimeInvariant type systems
|
| 129 |
+
The system for which the pole-zero plot is to be computed.
|
| 130 |
+
pole_color : str, tuple, optional
|
| 131 |
+
The color of the pole points on the plot. Default color
|
| 132 |
+
is blue. The color can be provided as a matplotlib color string,
|
| 133 |
+
or a 3-tuple of floats each in the 0-1 range.
|
| 134 |
+
pole_markersize : Number, optional
|
| 135 |
+
The size of the markers used to mark the poles in the plot.
|
| 136 |
+
Default pole markersize is 10.
|
| 137 |
+
zero_color : str, tuple, optional
|
| 138 |
+
The color of the zero points on the plot. Default color
|
| 139 |
+
is orange. The color can be provided as a matplotlib color string,
|
| 140 |
+
or a 3-tuple of floats each in the 0-1 range.
|
| 141 |
+
zero_markersize : Number, optional
|
| 142 |
+
The size of the markers used to mark the zeros in the plot.
|
| 143 |
+
Default zero markersize is 7.
|
| 144 |
+
grid : boolean, optional
|
| 145 |
+
If ``True``, the plot will have a grid. Defaults to True.
|
| 146 |
+
show_axes : boolean, optional
|
| 147 |
+
If ``True``, the coordinate axes will be shown. Defaults to False.
|
| 148 |
+
show : boolean, optional
|
| 149 |
+
If ``True``, the plot will be displayed otherwise
|
| 150 |
+
the equivalent matplotlib ``plot`` object will be returned.
|
| 151 |
+
Defaults to True.
|
| 152 |
+
|
| 153 |
+
Examples
|
| 154 |
+
========
|
| 155 |
+
|
| 156 |
+
.. plot::
|
| 157 |
+
:context: close-figs
|
| 158 |
+
:format: doctest
|
| 159 |
+
:include-source: True
|
| 160 |
+
|
| 161 |
+
>>> from sympy.abc import s
|
| 162 |
+
>>> from sympy.physics.control.lti import TransferFunction
|
| 163 |
+
>>> from sympy.physics.control.control_plots import pole_zero_plot
|
| 164 |
+
>>> tf1 = TransferFunction(s**2 + 1, s**4 + 4*s**3 + 6*s**2 + 5*s + 2, s)
|
| 165 |
+
>>> pole_zero_plot(tf1) # doctest: +SKIP
|
| 166 |
+
|
| 167 |
+
See Also
|
| 168 |
+
========
|
| 169 |
+
|
| 170 |
+
pole_zero_numerical_data
|
| 171 |
+
|
| 172 |
+
References
|
| 173 |
+
==========
|
| 174 |
+
|
| 175 |
+
.. [1] https://en.wikipedia.org/wiki/Pole%E2%80%93zero_plot
|
| 176 |
+
|
| 177 |
+
"""
|
| 178 |
+
zeros, poles = pole_zero_numerical_data(system)
|
| 179 |
+
|
| 180 |
+
zero_real = np.real(zeros)
|
| 181 |
+
zero_imag = np.imag(zeros)
|
| 182 |
+
|
| 183 |
+
pole_real = np.real(poles)
|
| 184 |
+
pole_imag = np.imag(poles)
|
| 185 |
+
|
| 186 |
+
plt.plot(pole_real, pole_imag, 'x', mfc='none',
|
| 187 |
+
markersize=pole_markersize, color=pole_color)
|
| 188 |
+
plt.plot(zero_real, zero_imag, 'o', markersize=zero_markersize,
|
| 189 |
+
color=zero_color)
|
| 190 |
+
plt.xlabel('Real Axis')
|
| 191 |
+
plt.ylabel('Imaginary Axis')
|
| 192 |
+
plt.title(f'Poles and Zeros of ${latex(system)}$', pad=20)
|
| 193 |
+
|
| 194 |
+
if grid:
|
| 195 |
+
plt.grid()
|
| 196 |
+
if show_axes:
|
| 197 |
+
plt.axhline(0, color='black')
|
| 198 |
+
plt.axvline(0, color='black')
|
| 199 |
+
if show:
|
| 200 |
+
plt.show()
|
| 201 |
+
return
|
| 202 |
+
|
| 203 |
+
return plt
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def step_response_numerical_data(system, prec=8, lower_limit=0,
|
| 207 |
+
upper_limit=10, **kwargs):
|
| 208 |
+
"""
|
| 209 |
+
Returns the numerical values of the points in the step response plot
|
| 210 |
+
of a SISO continuous-time system. By default, adaptive sampling
|
| 211 |
+
is used. If the user wants to instead get an uniformly
|
| 212 |
+
sampled response, then ``adaptive`` kwarg should be passed ``False``
|
| 213 |
+
and ``n`` must be passed as additional kwargs.
|
| 214 |
+
Refer to the parameters of class :class:`sympy.plotting.series.LineOver1DRangeSeries`
|
| 215 |
+
for more details.
|
| 216 |
+
|
| 217 |
+
Parameters
|
| 218 |
+
==========
|
| 219 |
+
|
| 220 |
+
system : SISOLinearTimeInvariant
|
| 221 |
+
The system for which the unit step response data is to be computed.
|
| 222 |
+
prec : int, optional
|
| 223 |
+
The decimal point precision for the point coordinate values.
|
| 224 |
+
Defaults to 8.
|
| 225 |
+
lower_limit : Number, optional
|
| 226 |
+
The lower limit of the plot range. Defaults to 0.
|
| 227 |
+
upper_limit : Number, optional
|
| 228 |
+
The upper limit of the plot range. Defaults to 10.
|
| 229 |
+
kwargs :
|
| 230 |
+
Additional keyword arguments are passed to the underlying
|
| 231 |
+
:class:`sympy.plotting.series.LineOver1DRangeSeries` class.
|
| 232 |
+
|
| 233 |
+
Returns
|
| 234 |
+
=======
|
| 235 |
+
|
| 236 |
+
tuple : (x, y)
|
| 237 |
+
x = Time-axis values of the points in the step response. NumPy array.
|
| 238 |
+
y = Amplitude-axis values of the points in the step response. NumPy array.
|
| 239 |
+
|
| 240 |
+
Raises
|
| 241 |
+
======
|
| 242 |
+
|
| 243 |
+
NotImplementedError
|
| 244 |
+
When a SISO LTI system is not passed.
|
| 245 |
+
|
| 246 |
+
When time delay terms are present in the system.
|
| 247 |
+
|
| 248 |
+
ValueError
|
| 249 |
+
When more than one free symbol is present in the system.
|
| 250 |
+
The only variable in the transfer function should be
|
| 251 |
+
the variable of the Laplace transform.
|
| 252 |
+
|
| 253 |
+
When ``lower_limit`` parameter is less than 0.
|
| 254 |
+
|
| 255 |
+
Examples
|
| 256 |
+
========
|
| 257 |
+
|
| 258 |
+
>>> from sympy.abc import s
|
| 259 |
+
>>> from sympy.physics.control.lti import TransferFunction
|
| 260 |
+
>>> from sympy.physics.control.control_plots import step_response_numerical_data
|
| 261 |
+
>>> tf1 = TransferFunction(s, s**2 + 5*s + 8, s)
|
| 262 |
+
>>> step_response_numerical_data(tf1) # doctest: +SKIP
|
| 263 |
+
([0.0, 0.025413462339411542, 0.0484508722725343, ... , 9.670250533855183, 9.844291913708725, 10.0],
|
| 264 |
+
[0.0, 0.023844582399907256, 0.042894276802320226, ..., 6.828770759094287e-12, 6.456457160755703e-12])
|
| 265 |
+
|
| 266 |
+
See Also
|
| 267 |
+
========
|
| 268 |
+
|
| 269 |
+
step_response_plot
|
| 270 |
+
|
| 271 |
+
"""
|
| 272 |
+
if lower_limit < 0:
|
| 273 |
+
raise ValueError("Lower limit of time must be greater "
|
| 274 |
+
"than or equal to zero.")
|
| 275 |
+
_check_system(system)
|
| 276 |
+
_x = Dummy("x")
|
| 277 |
+
expr = system.to_expr()/(system.var)
|
| 278 |
+
expr = apart(expr, system.var, full=True)
|
| 279 |
+
_y = _fast_inverse_laplace(expr, system.var, _x).evalf(prec)
|
| 280 |
+
return LineOver1DRangeSeries(_y, (_x, lower_limit, upper_limit),
|
| 281 |
+
**kwargs).get_points()
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def step_response_plot(system, color='b', prec=8, lower_limit=0,
|
| 285 |
+
upper_limit=10, show_axes=False, grid=True, show=True, **kwargs):
|
| 286 |
+
r"""
|
| 287 |
+
Returns the unit step response of a continuous-time system. It is
|
| 288 |
+
the response of the system when the input signal is a step function.
|
| 289 |
+
|
| 290 |
+
Parameters
|
| 291 |
+
==========
|
| 292 |
+
|
| 293 |
+
system : SISOLinearTimeInvariant type
|
| 294 |
+
The LTI SISO system for which the Step Response is to be computed.
|
| 295 |
+
color : str, tuple, optional
|
| 296 |
+
The color of the line. Default is Blue.
|
| 297 |
+
show : boolean, optional
|
| 298 |
+
If ``True``, the plot will be displayed otherwise
|
| 299 |
+
the equivalent matplotlib ``plot`` object will be returned.
|
| 300 |
+
Defaults to True.
|
| 301 |
+
lower_limit : Number, optional
|
| 302 |
+
The lower limit of the plot range. Defaults to 0.
|
| 303 |
+
upper_limit : Number, optional
|
| 304 |
+
The upper limit of the plot range. Defaults to 10.
|
| 305 |
+
prec : int, optional
|
| 306 |
+
The decimal point precision for the point coordinate values.
|
| 307 |
+
Defaults to 8.
|
| 308 |
+
show_axes : boolean, optional
|
| 309 |
+
If ``True``, the coordinate axes will be shown. Defaults to False.
|
| 310 |
+
grid : boolean, optional
|
| 311 |
+
If ``True``, the plot will have a grid. Defaults to True.
|
| 312 |
+
|
| 313 |
+
Examples
|
| 314 |
+
========
|
| 315 |
+
|
| 316 |
+
.. plot::
|
| 317 |
+
:context: close-figs
|
| 318 |
+
:format: doctest
|
| 319 |
+
:include-source: True
|
| 320 |
+
|
| 321 |
+
>>> from sympy.abc import s
|
| 322 |
+
>>> from sympy.physics.control.lti import TransferFunction
|
| 323 |
+
>>> from sympy.physics.control.control_plots import step_response_plot
|
| 324 |
+
>>> tf1 = TransferFunction(8*s**2 + 18*s + 32, s**3 + 6*s**2 + 14*s + 24, s)
|
| 325 |
+
>>> step_response_plot(tf1) # doctest: +SKIP
|
| 326 |
+
|
| 327 |
+
See Also
|
| 328 |
+
========
|
| 329 |
+
|
| 330 |
+
impulse_response_plot, ramp_response_plot
|
| 331 |
+
|
| 332 |
+
References
|
| 333 |
+
==========
|
| 334 |
+
|
| 335 |
+
.. [1] https://www.mathworks.com/help/control/ref/lti.step.html
|
| 336 |
+
|
| 337 |
+
"""
|
| 338 |
+
x, y = step_response_numerical_data(system, prec=prec,
|
| 339 |
+
lower_limit=lower_limit, upper_limit=upper_limit, **kwargs)
|
| 340 |
+
plt.plot(x, y, color=color)
|
| 341 |
+
plt.xlabel('Time (s)')
|
| 342 |
+
plt.ylabel('Amplitude')
|
| 343 |
+
plt.title(f'Unit Step Response of ${latex(system)}$', pad=20)
|
| 344 |
+
|
| 345 |
+
if grid:
|
| 346 |
+
plt.grid()
|
| 347 |
+
if show_axes:
|
| 348 |
+
plt.axhline(0, color='black')
|
| 349 |
+
plt.axvline(0, color='black')
|
| 350 |
+
if show:
|
| 351 |
+
plt.show()
|
| 352 |
+
return
|
| 353 |
+
|
| 354 |
+
return plt
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def impulse_response_numerical_data(system, prec=8, lower_limit=0,
|
| 358 |
+
upper_limit=10, **kwargs):
|
| 359 |
+
"""
|
| 360 |
+
Returns the numerical values of the points in the impulse response plot
|
| 361 |
+
of a SISO continuous-time system. By default, adaptive sampling
|
| 362 |
+
is used. If the user wants to instead get an uniformly
|
| 363 |
+
sampled response, then ``adaptive`` kwarg should be passed ``False``
|
| 364 |
+
and ``n`` must be passed as additional kwargs.
|
| 365 |
+
Refer to the parameters of class :class:`sympy.plotting.series.LineOver1DRangeSeries`
|
| 366 |
+
for more details.
|
| 367 |
+
|
| 368 |
+
Parameters
|
| 369 |
+
==========
|
| 370 |
+
|
| 371 |
+
system : SISOLinearTimeInvariant
|
| 372 |
+
The system for which the impulse response data is to be computed.
|
| 373 |
+
prec : int, optional
|
| 374 |
+
The decimal point precision for the point coordinate values.
|
| 375 |
+
Defaults to 8.
|
| 376 |
+
lower_limit : Number, optional
|
| 377 |
+
The lower limit of the plot range. Defaults to 0.
|
| 378 |
+
upper_limit : Number, optional
|
| 379 |
+
The upper limit of the plot range. Defaults to 10.
|
| 380 |
+
kwargs :
|
| 381 |
+
Additional keyword arguments are passed to the underlying
|
| 382 |
+
:class:`sympy.plotting.series.LineOver1DRangeSeries` class.
|
| 383 |
+
|
| 384 |
+
Returns
|
| 385 |
+
=======
|
| 386 |
+
|
| 387 |
+
tuple : (x, y)
|
| 388 |
+
x = Time-axis values of the points in the impulse response. NumPy array.
|
| 389 |
+
y = Amplitude-axis values of the points in the impulse response. NumPy array.
|
| 390 |
+
|
| 391 |
+
Raises
|
| 392 |
+
======
|
| 393 |
+
|
| 394 |
+
NotImplementedError
|
| 395 |
+
When a SISO LTI system is not passed.
|
| 396 |
+
|
| 397 |
+
When time delay terms are present in the system.
|
| 398 |
+
|
| 399 |
+
ValueError
|
| 400 |
+
When more than one free symbol is present in the system.
|
| 401 |
+
The only variable in the transfer function should be
|
| 402 |
+
the variable of the Laplace transform.
|
| 403 |
+
|
| 404 |
+
When ``lower_limit`` parameter is less than 0.
|
| 405 |
+
|
| 406 |
+
Examples
|
| 407 |
+
========
|
| 408 |
+
|
| 409 |
+
>>> from sympy.abc import s
|
| 410 |
+
>>> from sympy.physics.control.lti import TransferFunction
|
| 411 |
+
>>> from sympy.physics.control.control_plots import impulse_response_numerical_data
|
| 412 |
+
>>> tf1 = TransferFunction(s, s**2 + 5*s + 8, s)
|
| 413 |
+
>>> impulse_response_numerical_data(tf1) # doctest: +SKIP
|
| 414 |
+
([0.0, 0.06616480200395854,... , 9.854500743565858, 10.0],
|
| 415 |
+
[0.9999999799999999, 0.7042848373025861,...,7.170748906965121e-13, -5.1901263495547205e-12])
|
| 416 |
+
|
| 417 |
+
See Also
|
| 418 |
+
========
|
| 419 |
+
|
| 420 |
+
impulse_response_plot
|
| 421 |
+
|
| 422 |
+
"""
|
| 423 |
+
if lower_limit < 0:
|
| 424 |
+
raise ValueError("Lower limit of time must be greater "
|
| 425 |
+
"than or equal to zero.")
|
| 426 |
+
_check_system(system)
|
| 427 |
+
_x = Dummy("x")
|
| 428 |
+
expr = system.to_expr()
|
| 429 |
+
expr = apart(expr, system.var, full=True)
|
| 430 |
+
_y = _fast_inverse_laplace(expr, system.var, _x).evalf(prec)
|
| 431 |
+
return LineOver1DRangeSeries(_y, (_x, lower_limit, upper_limit),
|
| 432 |
+
**kwargs).get_points()
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def impulse_response_plot(system, color='b', prec=8, lower_limit=0,
|
| 436 |
+
upper_limit=10, show_axes=False, grid=True, show=True, **kwargs):
|
| 437 |
+
r"""
|
| 438 |
+
Returns the unit impulse response (Input is the Dirac-Delta Function) of a
|
| 439 |
+
continuous-time system.
|
| 440 |
+
|
| 441 |
+
Parameters
|
| 442 |
+
==========
|
| 443 |
+
|
| 444 |
+
system : SISOLinearTimeInvariant type
|
| 445 |
+
The LTI SISO system for which the Impulse Response is to be computed.
|
| 446 |
+
color : str, tuple, optional
|
| 447 |
+
The color of the line. Default is Blue.
|
| 448 |
+
show : boolean, optional
|
| 449 |
+
If ``True``, the plot will be displayed otherwise
|
| 450 |
+
the equivalent matplotlib ``plot`` object will be returned.
|
| 451 |
+
Defaults to True.
|
| 452 |
+
lower_limit : Number, optional
|
| 453 |
+
The lower limit of the plot range. Defaults to 0.
|
| 454 |
+
upper_limit : Number, optional
|
| 455 |
+
The upper limit of the plot range. Defaults to 10.
|
| 456 |
+
prec : int, optional
|
| 457 |
+
The decimal point precision for the point coordinate values.
|
| 458 |
+
Defaults to 8.
|
| 459 |
+
show_axes : boolean, optional
|
| 460 |
+
If ``True``, the coordinate axes will be shown. Defaults to False.
|
| 461 |
+
grid : boolean, optional
|
| 462 |
+
If ``True``, the plot will have a grid. Defaults to True.
|
| 463 |
+
|
| 464 |
+
Examples
|
| 465 |
+
========
|
| 466 |
+
|
| 467 |
+
.. plot::
|
| 468 |
+
:context: close-figs
|
| 469 |
+
:format: doctest
|
| 470 |
+
:include-source: True
|
| 471 |
+
|
| 472 |
+
>>> from sympy.abc import s
|
| 473 |
+
>>> from sympy.physics.control.lti import TransferFunction
|
| 474 |
+
>>> from sympy.physics.control.control_plots import impulse_response_plot
|
| 475 |
+
>>> tf1 = TransferFunction(8*s**2 + 18*s + 32, s**3 + 6*s**2 + 14*s + 24, s)
|
| 476 |
+
>>> impulse_response_plot(tf1) # doctest: +SKIP
|
| 477 |
+
|
| 478 |
+
See Also
|
| 479 |
+
========
|
| 480 |
+
|
| 481 |
+
step_response_plot, ramp_response_plot
|
| 482 |
+
|
| 483 |
+
References
|
| 484 |
+
==========
|
| 485 |
+
|
| 486 |
+
.. [1] https://www.mathworks.com/help/control/ref/dynamicsystem.impulse.html
|
| 487 |
+
|
| 488 |
+
"""
|
| 489 |
+
x, y = impulse_response_numerical_data(system, prec=prec,
|
| 490 |
+
lower_limit=lower_limit, upper_limit=upper_limit, **kwargs)
|
| 491 |
+
plt.plot(x, y, color=color)
|
| 492 |
+
plt.xlabel('Time (s)')
|
| 493 |
+
plt.ylabel('Amplitude')
|
| 494 |
+
plt.title(f'Impulse Response of ${latex(system)}$', pad=20)
|
| 495 |
+
|
| 496 |
+
if grid:
|
| 497 |
+
plt.grid()
|
| 498 |
+
if show_axes:
|
| 499 |
+
plt.axhline(0, color='black')
|
| 500 |
+
plt.axvline(0, color='black')
|
| 501 |
+
if show:
|
| 502 |
+
plt.show()
|
| 503 |
+
return
|
| 504 |
+
|
| 505 |
+
return plt
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
def ramp_response_numerical_data(system, slope=1, prec=8,
|
| 509 |
+
lower_limit=0, upper_limit=10, **kwargs):
|
| 510 |
+
"""
|
| 511 |
+
Returns the numerical values of the points in the ramp response plot
|
| 512 |
+
of a SISO continuous-time system. By default, adaptive sampling
|
| 513 |
+
is used. If the user wants to instead get an uniformly
|
| 514 |
+
sampled response, then ``adaptive`` kwarg should be passed ``False``
|
| 515 |
+
and ``n`` must be passed as additional kwargs.
|
| 516 |
+
Refer to the parameters of class :class:`sympy.plotting.series.LineOver1DRangeSeries`
|
| 517 |
+
for more details.
|
| 518 |
+
|
| 519 |
+
Parameters
|
| 520 |
+
==========
|
| 521 |
+
|
| 522 |
+
system : SISOLinearTimeInvariant
|
| 523 |
+
The system for which the ramp response data is to be computed.
|
| 524 |
+
slope : Number, optional
|
| 525 |
+
The slope of the input ramp function. Defaults to 1.
|
| 526 |
+
prec : int, optional
|
| 527 |
+
The decimal point precision for the point coordinate values.
|
| 528 |
+
Defaults to 8.
|
| 529 |
+
lower_limit : Number, optional
|
| 530 |
+
The lower limit of the plot range. Defaults to 0.
|
| 531 |
+
upper_limit : Number, optional
|
| 532 |
+
The upper limit of the plot range. Defaults to 10.
|
| 533 |
+
kwargs :
|
| 534 |
+
Additional keyword arguments are passed to the underlying
|
| 535 |
+
:class:`sympy.plotting.series.LineOver1DRangeSeries` class.
|
| 536 |
+
|
| 537 |
+
Returns
|
| 538 |
+
=======
|
| 539 |
+
|
| 540 |
+
tuple : (x, y)
|
| 541 |
+
x = Time-axis values of the points in the ramp response plot. NumPy array.
|
| 542 |
+
y = Amplitude-axis values of the points in the ramp response plot. NumPy array.
|
| 543 |
+
|
| 544 |
+
Raises
|
| 545 |
+
======
|
| 546 |
+
|
| 547 |
+
NotImplementedError
|
| 548 |
+
When a SISO LTI system is not passed.
|
| 549 |
+
|
| 550 |
+
When time delay terms are present in the system.
|
| 551 |
+
|
| 552 |
+
ValueError
|
| 553 |
+
When more than one free symbol is present in the system.
|
| 554 |
+
The only variable in the transfer function should be
|
| 555 |
+
the variable of the Laplace transform.
|
| 556 |
+
|
| 557 |
+
When ``lower_limit`` parameter is less than 0.
|
| 558 |
+
|
| 559 |
+
When ``slope`` is negative.
|
| 560 |
+
|
| 561 |
+
Examples
|
| 562 |
+
========
|
| 563 |
+
|
| 564 |
+
>>> from sympy.abc import s
|
| 565 |
+
>>> from sympy.physics.control.lti import TransferFunction
|
| 566 |
+
>>> from sympy.physics.control.control_plots import ramp_response_numerical_data
|
| 567 |
+
>>> tf1 = TransferFunction(s, s**2 + 5*s + 8, s)
|
| 568 |
+
>>> ramp_response_numerical_data(tf1) # doctest: +SKIP
|
| 569 |
+
(([0.0, 0.12166980856813935,..., 9.861246379582118, 10.0],
|
| 570 |
+
[1.4504508011325967e-09, 0.006046440489058766,..., 0.12499999999568202, 0.12499999999661349]))
|
| 571 |
+
|
| 572 |
+
See Also
|
| 573 |
+
========
|
| 574 |
+
|
| 575 |
+
ramp_response_plot
|
| 576 |
+
|
| 577 |
+
"""
|
| 578 |
+
if slope < 0:
|
| 579 |
+
raise ValueError("Slope must be greater than or equal"
|
| 580 |
+
" to zero.")
|
| 581 |
+
if lower_limit < 0:
|
| 582 |
+
raise ValueError("Lower limit of time must be greater "
|
| 583 |
+
"than or equal to zero.")
|
| 584 |
+
_check_system(system)
|
| 585 |
+
_x = Dummy("x")
|
| 586 |
+
expr = (slope*system.to_expr())/((system.var)**2)
|
| 587 |
+
expr = apart(expr, system.var, full=True)
|
| 588 |
+
_y = _fast_inverse_laplace(expr, system.var, _x).evalf(prec)
|
| 589 |
+
return LineOver1DRangeSeries(_y, (_x, lower_limit, upper_limit),
|
| 590 |
+
**kwargs).get_points()
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
def ramp_response_plot(system, slope=1, color='b', prec=8, lower_limit=0,
|
| 594 |
+
upper_limit=10, show_axes=False, grid=True, show=True, **kwargs):
|
| 595 |
+
r"""
|
| 596 |
+
Returns the ramp response of a continuous-time system.
|
| 597 |
+
|
| 598 |
+
Ramp function is defined as the straight line
|
| 599 |
+
passing through origin ($f(x) = mx$). The slope of
|
| 600 |
+
the ramp function can be varied by the user and
|
| 601 |
+
the default value is 1.
|
| 602 |
+
|
| 603 |
+
Parameters
|
| 604 |
+
==========
|
| 605 |
+
|
| 606 |
+
system : SISOLinearTimeInvariant type
|
| 607 |
+
The LTI SISO system for which the Ramp Response is to be computed.
|
| 608 |
+
slope : Number, optional
|
| 609 |
+
The slope of the input ramp function. Defaults to 1.
|
| 610 |
+
color : str, tuple, optional
|
| 611 |
+
The color of the line. Default is Blue.
|
| 612 |
+
show : boolean, optional
|
| 613 |
+
If ``True``, the plot will be displayed otherwise
|
| 614 |
+
the equivalent matplotlib ``plot`` object will be returned.
|
| 615 |
+
Defaults to True.
|
| 616 |
+
lower_limit : Number, optional
|
| 617 |
+
The lower limit of the plot range. Defaults to 0.
|
| 618 |
+
upper_limit : Number, optional
|
| 619 |
+
The upper limit of the plot range. Defaults to 10.
|
| 620 |
+
prec : int, optional
|
| 621 |
+
The decimal point precision for the point coordinate values.
|
| 622 |
+
Defaults to 8.
|
| 623 |
+
show_axes : boolean, optional
|
| 624 |
+
If ``True``, the coordinate axes will be shown. Defaults to False.
|
| 625 |
+
grid : boolean, optional
|
| 626 |
+
If ``True``, the plot will have a grid. Defaults to True.
|
| 627 |
+
|
| 628 |
+
Examples
|
| 629 |
+
========
|
| 630 |
+
|
| 631 |
+
.. plot::
|
| 632 |
+
:context: close-figs
|
| 633 |
+
:format: doctest
|
| 634 |
+
:include-source: True
|
| 635 |
+
|
| 636 |
+
>>> from sympy.abc import s
|
| 637 |
+
>>> from sympy.physics.control.lti import TransferFunction
|
| 638 |
+
>>> from sympy.physics.control.control_plots import ramp_response_plot
|
| 639 |
+
>>> tf1 = TransferFunction(s, (s+4)*(s+8), s)
|
| 640 |
+
>>> ramp_response_plot(tf1, upper_limit=2) # doctest: +SKIP
|
| 641 |
+
|
| 642 |
+
See Also
|
| 643 |
+
========
|
| 644 |
+
|
| 645 |
+
step_response_plot, impulse_response_plot
|
| 646 |
+
|
| 647 |
+
References
|
| 648 |
+
==========
|
| 649 |
+
|
| 650 |
+
.. [1] https://en.wikipedia.org/wiki/Ramp_function
|
| 651 |
+
|
| 652 |
+
"""
|
| 653 |
+
x, y = ramp_response_numerical_data(system, slope=slope, prec=prec,
|
| 654 |
+
lower_limit=lower_limit, upper_limit=upper_limit, **kwargs)
|
| 655 |
+
plt.plot(x, y, color=color)
|
| 656 |
+
plt.xlabel('Time (s)')
|
| 657 |
+
plt.ylabel('Amplitude')
|
| 658 |
+
plt.title(f'Ramp Response of ${latex(system)}$ [Slope = {slope}]', pad=20)
|
| 659 |
+
|
| 660 |
+
if grid:
|
| 661 |
+
plt.grid()
|
| 662 |
+
if show_axes:
|
| 663 |
+
plt.axhline(0, color='black')
|
| 664 |
+
plt.axvline(0, color='black')
|
| 665 |
+
if show:
|
| 666 |
+
plt.show()
|
| 667 |
+
return
|
| 668 |
+
|
| 669 |
+
return plt
|
| 670 |
+
|
| 671 |
+
|
| 672 |
+
def bode_magnitude_numerical_data(system, initial_exp=-5, final_exp=5, freq_unit='rad/sec', **kwargs):
|
| 673 |
+
"""
|
| 674 |
+
Returns the numerical data of the Bode magnitude plot of the system.
|
| 675 |
+
It is internally used by ``bode_magnitude_plot`` to get the data
|
| 676 |
+
for plotting Bode magnitude plot. Users can use this data to further
|
| 677 |
+
analyse the dynamics of the system or plot using a different
|
| 678 |
+
backend/plotting-module.
|
| 679 |
+
|
| 680 |
+
Parameters
|
| 681 |
+
==========
|
| 682 |
+
|
| 683 |
+
system : SISOLinearTimeInvariant
|
| 684 |
+
The system for which the data is to be computed.
|
| 685 |
+
initial_exp : Number, optional
|
| 686 |
+
The initial exponent of 10 of the semilog plot. Defaults to -5.
|
| 687 |
+
final_exp : Number, optional
|
| 688 |
+
The final exponent of 10 of the semilog plot. Defaults to 5.
|
| 689 |
+
freq_unit : string, optional
|
| 690 |
+
User can choose between ``'rad/sec'`` (radians/second) and ``'Hz'`` (Hertz) as frequency units.
|
| 691 |
+
|
| 692 |
+
Returns
|
| 693 |
+
=======
|
| 694 |
+
|
| 695 |
+
tuple : (x, y)
|
| 696 |
+
x = x-axis values of the Bode magnitude plot.
|
| 697 |
+
y = y-axis values of the Bode magnitude plot.
|
| 698 |
+
|
| 699 |
+
Raises
|
| 700 |
+
======
|
| 701 |
+
|
| 702 |
+
NotImplementedError
|
| 703 |
+
When a SISO LTI system is not passed.
|
| 704 |
+
|
| 705 |
+
When time delay terms are present in the system.
|
| 706 |
+
|
| 707 |
+
ValueError
|
| 708 |
+
When more than one free symbol is present in the system.
|
| 709 |
+
The only variable in the transfer function should be
|
| 710 |
+
the variable of the Laplace transform.
|
| 711 |
+
|
| 712 |
+
When incorrect frequency units are given as input.
|
| 713 |
+
|
| 714 |
+
Examples
|
| 715 |
+
========
|
| 716 |
+
|
| 717 |
+
>>> from sympy.abc import s
|
| 718 |
+
>>> from sympy.physics.control.lti import TransferFunction
|
| 719 |
+
>>> from sympy.physics.control.control_plots import bode_magnitude_numerical_data
|
| 720 |
+
>>> tf1 = TransferFunction(s**2 + 1, s**4 + 4*s**3 + 6*s**2 + 5*s + 2, s)
|
| 721 |
+
>>> bode_magnitude_numerical_data(tf1) # doctest: +SKIP
|
| 722 |
+
([1e-05, 1.5148378120533502e-05,..., 68437.36188804005, 100000.0],
|
| 723 |
+
[-6.020599914256786, -6.0205999155219505,..., -193.4117304087953, -200.00000000260573])
|
| 724 |
+
|
| 725 |
+
See Also
|
| 726 |
+
========
|
| 727 |
+
|
| 728 |
+
bode_magnitude_plot, bode_phase_numerical_data
|
| 729 |
+
|
| 730 |
+
"""
|
| 731 |
+
_check_system(system)
|
| 732 |
+
expr = system.to_expr()
|
| 733 |
+
freq_units = ('rad/sec', 'Hz')
|
| 734 |
+
if freq_unit not in freq_units:
|
| 735 |
+
raise ValueError('Only "rad/sec" and "Hz" are accepted frequency units.')
|
| 736 |
+
|
| 737 |
+
_w = Dummy("w", real=True)
|
| 738 |
+
if freq_unit == 'Hz':
|
| 739 |
+
repl = I*_w*2*pi
|
| 740 |
+
else:
|
| 741 |
+
repl = I*_w
|
| 742 |
+
w_expr = expr.subs({system.var: repl})
|
| 743 |
+
|
| 744 |
+
mag = 20*log(Abs(w_expr), 10)
|
| 745 |
+
|
| 746 |
+
x, y = LineOver1DRangeSeries(mag,
|
| 747 |
+
(_w, 10**initial_exp, 10**final_exp), xscale='log', **kwargs).get_points()
|
| 748 |
+
|
| 749 |
+
return x, y
|
| 750 |
+
|
| 751 |
+
|
| 752 |
+
def bode_magnitude_plot(system, initial_exp=-5, final_exp=5,
|
| 753 |
+
color='b', show_axes=False, grid=True, show=True, freq_unit='rad/sec', **kwargs):
|
| 754 |
+
r"""
|
| 755 |
+
Returns the Bode magnitude plot of a continuous-time system.
|
| 756 |
+
|
| 757 |
+
See ``bode_plot`` for all the parameters.
|
| 758 |
+
"""
|
| 759 |
+
x, y = bode_magnitude_numerical_data(system, initial_exp=initial_exp,
|
| 760 |
+
final_exp=final_exp, freq_unit=freq_unit)
|
| 761 |
+
plt.plot(x, y, color=color, **kwargs)
|
| 762 |
+
plt.xscale('log')
|
| 763 |
+
|
| 764 |
+
|
| 765 |
+
plt.xlabel('Frequency (%s) [Log Scale]' % freq_unit)
|
| 766 |
+
plt.ylabel('Magnitude (dB)')
|
| 767 |
+
plt.title(f'Bode Plot (Magnitude) of ${latex(system)}$', pad=20)
|
| 768 |
+
|
| 769 |
+
if grid:
|
| 770 |
+
plt.grid(True)
|
| 771 |
+
if show_axes:
|
| 772 |
+
plt.axhline(0, color='black')
|
| 773 |
+
plt.axvline(0, color='black')
|
| 774 |
+
if show:
|
| 775 |
+
plt.show()
|
| 776 |
+
return
|
| 777 |
+
|
| 778 |
+
return plt
|
| 779 |
+
|
| 780 |
+
|
| 781 |
+
def bode_phase_numerical_data(system, initial_exp=-5, final_exp=5, freq_unit='rad/sec', phase_unit='rad', phase_unwrap = True, **kwargs):
|
| 782 |
+
"""
|
| 783 |
+
Returns the numerical data of the Bode phase plot of the system.
|
| 784 |
+
It is internally used by ``bode_phase_plot`` to get the data
|
| 785 |
+
for plotting Bode phase plot. Users can use this data to further
|
| 786 |
+
analyse the dynamics of the system or plot using a different
|
| 787 |
+
backend/plotting-module.
|
| 788 |
+
|
| 789 |
+
Parameters
|
| 790 |
+
==========
|
| 791 |
+
|
| 792 |
+
system : SISOLinearTimeInvariant
|
| 793 |
+
The system for which the Bode phase plot data is to be computed.
|
| 794 |
+
initial_exp : Number, optional
|
| 795 |
+
The initial exponent of 10 of the semilog plot. Defaults to -5.
|
| 796 |
+
final_exp : Number, optional
|
| 797 |
+
The final exponent of 10 of the semilog plot. Defaults to 5.
|
| 798 |
+
freq_unit : string, optional
|
| 799 |
+
User can choose between ``'rad/sec'`` (radians/second) and '``'Hz'`` (Hertz) as frequency units.
|
| 800 |
+
phase_unit : string, optional
|
| 801 |
+
User can choose between ``'rad'`` (radians) and ``'deg'`` (degree) as phase units.
|
| 802 |
+
phase_unwrap : bool, optional
|
| 803 |
+
Set to ``True`` by default.
|
| 804 |
+
|
| 805 |
+
Returns
|
| 806 |
+
=======
|
| 807 |
+
|
| 808 |
+
tuple : (x, y)
|
| 809 |
+
x = x-axis values of the Bode phase plot.
|
| 810 |
+
y = y-axis values of the Bode phase plot.
|
| 811 |
+
|
| 812 |
+
Raises
|
| 813 |
+
======
|
| 814 |
+
|
| 815 |
+
NotImplementedError
|
| 816 |
+
When a SISO LTI system is not passed.
|
| 817 |
+
|
| 818 |
+
When time delay terms are present in the system.
|
| 819 |
+
|
| 820 |
+
ValueError
|
| 821 |
+
When more than one free symbol is present in the system.
|
| 822 |
+
The only variable in the transfer function should be
|
| 823 |
+
the variable of the Laplace transform.
|
| 824 |
+
|
| 825 |
+
When incorrect frequency or phase units are given as input.
|
| 826 |
+
|
| 827 |
+
Examples
|
| 828 |
+
========
|
| 829 |
+
|
| 830 |
+
>>> from sympy.abc import s
|
| 831 |
+
>>> from sympy.physics.control.lti import TransferFunction
|
| 832 |
+
>>> from sympy.physics.control.control_plots import bode_phase_numerical_data
|
| 833 |
+
>>> tf1 = TransferFunction(s**2 + 1, s**4 + 4*s**3 + 6*s**2 + 5*s + 2, s)
|
| 834 |
+
>>> bode_phase_numerical_data(tf1) # doctest: +SKIP
|
| 835 |
+
([1e-05, 1.4472354033813751e-05, 2.035581932165858e-05,..., 47577.3248186011, 67884.09326036123, 100000.0],
|
| 836 |
+
[-2.5000000000291665e-05, -3.6180885085e-05, -5.08895483066e-05,...,-3.1415085799262523, -3.14155265358979])
|
| 837 |
+
|
| 838 |
+
See Also
|
| 839 |
+
========
|
| 840 |
+
|
| 841 |
+
bode_magnitude_plot, bode_phase_numerical_data
|
| 842 |
+
|
| 843 |
+
"""
|
| 844 |
+
_check_system(system)
|
| 845 |
+
expr = system.to_expr()
|
| 846 |
+
freq_units = ('rad/sec', 'Hz')
|
| 847 |
+
phase_units = ('rad', 'deg')
|
| 848 |
+
if freq_unit not in freq_units:
|
| 849 |
+
raise ValueError('Only "rad/sec" and "Hz" are accepted frequency units.')
|
| 850 |
+
if phase_unit not in phase_units:
|
| 851 |
+
raise ValueError('Only "rad" and "deg" are accepted phase units.')
|
| 852 |
+
|
| 853 |
+
_w = Dummy("w", real=True)
|
| 854 |
+
if freq_unit == 'Hz':
|
| 855 |
+
repl = I*_w*2*pi
|
| 856 |
+
else:
|
| 857 |
+
repl = I*_w
|
| 858 |
+
w_expr = expr.subs({system.var: repl})
|
| 859 |
+
|
| 860 |
+
if phase_unit == 'deg':
|
| 861 |
+
phase = arg(w_expr)*180/pi
|
| 862 |
+
else:
|
| 863 |
+
phase = arg(w_expr)
|
| 864 |
+
|
| 865 |
+
x, y = LineOver1DRangeSeries(phase,
|
| 866 |
+
(_w, 10**initial_exp, 10**final_exp), xscale='log', **kwargs).get_points()
|
| 867 |
+
|
| 868 |
+
half = None
|
| 869 |
+
if phase_unwrap:
|
| 870 |
+
if(phase_unit == 'rad'):
|
| 871 |
+
half = pi
|
| 872 |
+
elif(phase_unit == 'deg'):
|
| 873 |
+
half = 180
|
| 874 |
+
if half:
|
| 875 |
+
unit = 2*half
|
| 876 |
+
for i in range(1, len(y)):
|
| 877 |
+
diff = y[i] - y[i - 1]
|
| 878 |
+
if diff > half: # Jump from -half to half
|
| 879 |
+
y[i] = (y[i] - unit)
|
| 880 |
+
elif diff < -half: # Jump from half to -half
|
| 881 |
+
y[i] = (y[i] + unit)
|
| 882 |
+
|
| 883 |
+
return x, y
|
| 884 |
+
|
| 885 |
+
|
| 886 |
+
def bode_phase_plot(system, initial_exp=-5, final_exp=5,
|
| 887 |
+
color='b', show_axes=False, grid=True, show=True, freq_unit='rad/sec', phase_unit='rad', phase_unwrap=True, **kwargs):
|
| 888 |
+
r"""
|
| 889 |
+
Returns the Bode phase plot of a continuous-time system.
|
| 890 |
+
|
| 891 |
+
See ``bode_plot`` for all the parameters.
|
| 892 |
+
"""
|
| 893 |
+
x, y = bode_phase_numerical_data(system, initial_exp=initial_exp,
|
| 894 |
+
final_exp=final_exp, freq_unit=freq_unit, phase_unit=phase_unit, phase_unwrap=phase_unwrap)
|
| 895 |
+
plt.plot(x, y, color=color, **kwargs)
|
| 896 |
+
plt.xscale('log')
|
| 897 |
+
|
| 898 |
+
plt.xlabel('Frequency (%s) [Log Scale]' % freq_unit)
|
| 899 |
+
plt.ylabel('Phase (%s)' % phase_unit)
|
| 900 |
+
plt.title(f'Bode Plot (Phase) of ${latex(system)}$', pad=20)
|
| 901 |
+
|
| 902 |
+
if grid:
|
| 903 |
+
plt.grid(True)
|
| 904 |
+
if show_axes:
|
| 905 |
+
plt.axhline(0, color='black')
|
| 906 |
+
plt.axvline(0, color='black')
|
| 907 |
+
if show:
|
| 908 |
+
plt.show()
|
| 909 |
+
return
|
| 910 |
+
|
| 911 |
+
return plt
|
| 912 |
+
|
| 913 |
+
|
| 914 |
+
def bode_plot(system, initial_exp=-5, final_exp=5,
|
| 915 |
+
grid=True, show_axes=False, show=True, freq_unit='rad/sec', phase_unit='rad', phase_unwrap=True, **kwargs):
|
| 916 |
+
r"""
|
| 917 |
+
Returns the Bode phase and magnitude plots of a continuous-time system.
|
| 918 |
+
|
| 919 |
+
Parameters
|
| 920 |
+
==========
|
| 921 |
+
|
| 922 |
+
system : SISOLinearTimeInvariant type
|
| 923 |
+
The LTI SISO system for which the Bode Plot is to be computed.
|
| 924 |
+
initial_exp : Number, optional
|
| 925 |
+
The initial exponent of 10 of the semilog plot. Defaults to -5.
|
| 926 |
+
final_exp : Number, optional
|
| 927 |
+
The final exponent of 10 of the semilog plot. Defaults to 5.
|
| 928 |
+
show : boolean, optional
|
| 929 |
+
If ``True``, the plot will be displayed otherwise
|
| 930 |
+
the equivalent matplotlib ``plot`` object will be returned.
|
| 931 |
+
Defaults to True.
|
| 932 |
+
prec : int, optional
|
| 933 |
+
The decimal point precision for the point coordinate values.
|
| 934 |
+
Defaults to 8.
|
| 935 |
+
grid : boolean, optional
|
| 936 |
+
If ``True``, the plot will have a grid. Defaults to True.
|
| 937 |
+
show_axes : boolean, optional
|
| 938 |
+
If ``True``, the coordinate axes will be shown. Defaults to False.
|
| 939 |
+
freq_unit : string, optional
|
| 940 |
+
User can choose between ``'rad/sec'`` (radians/second) and ``'Hz'`` (Hertz) as frequency units.
|
| 941 |
+
phase_unit : string, optional
|
| 942 |
+
User can choose between ``'rad'`` (radians) and ``'deg'`` (degree) as phase units.
|
| 943 |
+
|
| 944 |
+
Examples
|
| 945 |
+
========
|
| 946 |
+
|
| 947 |
+
.. plot::
|
| 948 |
+
:context: close-figs
|
| 949 |
+
:format: doctest
|
| 950 |
+
:include-source: True
|
| 951 |
+
|
| 952 |
+
>>> from sympy.abc import s
|
| 953 |
+
>>> from sympy.physics.control.lti import TransferFunction
|
| 954 |
+
>>> from sympy.physics.control.control_plots import bode_plot
|
| 955 |
+
>>> tf1 = TransferFunction(1*s**2 + 0.1*s + 7.5, 1*s**4 + 0.12*s**3 + 9*s**2, s)
|
| 956 |
+
>>> bode_plot(tf1, initial_exp=0.2, final_exp=0.7) # doctest: +SKIP
|
| 957 |
+
|
| 958 |
+
See Also
|
| 959 |
+
========
|
| 960 |
+
|
| 961 |
+
bode_magnitude_plot, bode_phase_plot
|
| 962 |
+
|
| 963 |
+
"""
|
| 964 |
+
plt.subplot(211)
|
| 965 |
+
mag = bode_magnitude_plot(system, initial_exp=initial_exp, final_exp=final_exp,
|
| 966 |
+
show=False, grid=grid, show_axes=show_axes,
|
| 967 |
+
freq_unit=freq_unit, **kwargs)
|
| 968 |
+
mag.title(f'Bode Plot of ${latex(system)}$', pad=20)
|
| 969 |
+
mag.xlabel(None)
|
| 970 |
+
plt.subplot(212)
|
| 971 |
+
bode_phase_plot(system, initial_exp=initial_exp, final_exp=final_exp,
|
| 972 |
+
show=False, grid=grid, show_axes=show_axes, freq_unit=freq_unit, phase_unit=phase_unit, phase_unwrap=phase_unwrap, **kwargs).title(None)
|
| 973 |
+
|
| 974 |
+
if show:
|
| 975 |
+
plt.show()
|
| 976 |
+
return
|
| 977 |
+
|
| 978 |
+
return plt
|
lib/python3.12/site-packages/sympy/physics/control/lti.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
lib/python3.12/site-packages/sympy/physics/control/tests/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/sympy/physics/control/tests/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (206 Bytes). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/control/tests/__pycache__/test_control_plots.cpython-312.pyc
ADDED
|
Binary file (17.8 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/control/tests/test_control_plots.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from math import isclose
|
| 2 |
+
from sympy.core.numbers import I
|
| 3 |
+
from sympy.core.symbol import Dummy
|
| 4 |
+
from sympy.functions.elementary.complexes import (Abs, arg)
|
| 5 |
+
from sympy.functions.elementary.exponential import log
|
| 6 |
+
from sympy.abc import s, p, a
|
| 7 |
+
from sympy.external import import_module
|
| 8 |
+
from sympy.physics.control.control_plots import \
|
| 9 |
+
(pole_zero_numerical_data, pole_zero_plot, step_response_numerical_data,
|
| 10 |
+
step_response_plot, impulse_response_numerical_data,
|
| 11 |
+
impulse_response_plot, ramp_response_numerical_data,
|
| 12 |
+
ramp_response_plot, bode_magnitude_numerical_data,
|
| 13 |
+
bode_phase_numerical_data, bode_plot)
|
| 14 |
+
from sympy.physics.control.lti import (TransferFunction,
|
| 15 |
+
Series, Parallel, TransferFunctionMatrix)
|
| 16 |
+
from sympy.testing.pytest import raises, skip
|
| 17 |
+
|
| 18 |
+
matplotlib = import_module(
|
| 19 |
+
'matplotlib', import_kwargs={'fromlist': ['pyplot']},
|
| 20 |
+
catch=(RuntimeError,))
|
| 21 |
+
|
| 22 |
+
numpy = import_module('numpy')
|
| 23 |
+
|
| 24 |
+
tf1 = TransferFunction(1, p**2 + 0.5*p + 2, p)
|
| 25 |
+
tf2 = TransferFunction(p, 6*p**2 + 3*p + 1, p)
|
| 26 |
+
tf3 = TransferFunction(p, p**3 - 1, p)
|
| 27 |
+
tf4 = TransferFunction(10, p**3, p)
|
| 28 |
+
tf5 = TransferFunction(5, s**2 + 2*s + 10, s)
|
| 29 |
+
tf6 = TransferFunction(1, 1, s)
|
| 30 |
+
tf7 = TransferFunction(4*s*3 + 9*s**2 + 0.1*s + 11, 8*s**6 + 9*s**4 + 11, s)
|
| 31 |
+
tf8 = TransferFunction(5, s**2 + (2+I)*s + 10, s)
|
| 32 |
+
|
| 33 |
+
ser1 = Series(tf4, TransferFunction(1, p - 5, p))
|
| 34 |
+
ser2 = Series(tf3, TransferFunction(p, p + 2, p))
|
| 35 |
+
|
| 36 |
+
par1 = Parallel(tf1, tf2)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _to_tuple(a, b):
|
| 40 |
+
return tuple(a), tuple(b)
|
| 41 |
+
|
| 42 |
+
def _trim_tuple(a, b):
|
| 43 |
+
a, b = _to_tuple(a, b)
|
| 44 |
+
return tuple(a[0: 2] + a[len(a)//2 : len(a)//2 + 1] + a[-2:]), \
|
| 45 |
+
tuple(b[0: 2] + b[len(b)//2 : len(b)//2 + 1] + b[-2:])
|
| 46 |
+
|
| 47 |
+
def y_coordinate_equality(plot_data_func, evalf_func, system):
|
| 48 |
+
"""Checks whether the y-coordinate value of the plotted
|
| 49 |
+
data point is equal to the value of the function at a
|
| 50 |
+
particular x."""
|
| 51 |
+
x, y = plot_data_func(system)
|
| 52 |
+
x, y = _trim_tuple(x, y)
|
| 53 |
+
y_exp = tuple(evalf_func(system, x_i) for x_i in x)
|
| 54 |
+
return all(Abs(y_exp_i - y_i) < 1e-8 for y_exp_i, y_i in zip(y_exp, y))
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def test_errors():
|
| 58 |
+
if not matplotlib:
|
| 59 |
+
skip("Matplotlib not the default backend")
|
| 60 |
+
|
| 61 |
+
# Invalid `system` check
|
| 62 |
+
tfm = TransferFunctionMatrix([[tf6, tf5], [tf5, tf6]])
|
| 63 |
+
expr = 1/(s**2 - 1)
|
| 64 |
+
raises(NotImplementedError, lambda: pole_zero_plot(tfm))
|
| 65 |
+
raises(NotImplementedError, lambda: pole_zero_numerical_data(expr))
|
| 66 |
+
raises(NotImplementedError, lambda: impulse_response_plot(expr))
|
| 67 |
+
raises(NotImplementedError, lambda: impulse_response_numerical_data(tfm))
|
| 68 |
+
raises(NotImplementedError, lambda: step_response_plot(tfm))
|
| 69 |
+
raises(NotImplementedError, lambda: step_response_numerical_data(expr))
|
| 70 |
+
raises(NotImplementedError, lambda: ramp_response_plot(expr))
|
| 71 |
+
raises(NotImplementedError, lambda: ramp_response_numerical_data(tfm))
|
| 72 |
+
raises(NotImplementedError, lambda: bode_plot(tfm))
|
| 73 |
+
|
| 74 |
+
# More than 1 variables
|
| 75 |
+
tf_a = TransferFunction(a, s + 1, s)
|
| 76 |
+
raises(ValueError, lambda: pole_zero_plot(tf_a))
|
| 77 |
+
raises(ValueError, lambda: pole_zero_numerical_data(tf_a))
|
| 78 |
+
raises(ValueError, lambda: impulse_response_plot(tf_a))
|
| 79 |
+
raises(ValueError, lambda: impulse_response_numerical_data(tf_a))
|
| 80 |
+
raises(ValueError, lambda: step_response_plot(tf_a))
|
| 81 |
+
raises(ValueError, lambda: step_response_numerical_data(tf_a))
|
| 82 |
+
raises(ValueError, lambda: ramp_response_plot(tf_a))
|
| 83 |
+
raises(ValueError, lambda: ramp_response_numerical_data(tf_a))
|
| 84 |
+
raises(ValueError, lambda: bode_plot(tf_a))
|
| 85 |
+
|
| 86 |
+
# lower_limit > 0 for response plots
|
| 87 |
+
raises(ValueError, lambda: impulse_response_plot(tf1, lower_limit=-1))
|
| 88 |
+
raises(ValueError, lambda: step_response_plot(tf1, lower_limit=-0.1))
|
| 89 |
+
raises(ValueError, lambda: ramp_response_plot(tf1, lower_limit=-4/3))
|
| 90 |
+
|
| 91 |
+
# slope in ramp_response_plot() is negative
|
| 92 |
+
raises(ValueError, lambda: ramp_response_plot(tf1, slope=-0.1))
|
| 93 |
+
|
| 94 |
+
# incorrect frequency or phase unit
|
| 95 |
+
raises(ValueError, lambda: bode_plot(tf1,freq_unit = 'hz'))
|
| 96 |
+
raises(ValueError, lambda: bode_plot(tf1,phase_unit = 'degree'))
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def test_pole_zero():
|
| 100 |
+
if not numpy:
|
| 101 |
+
skip("NumPy is required for this test")
|
| 102 |
+
|
| 103 |
+
def pz_tester(sys, expected_value):
|
| 104 |
+
z, p = pole_zero_numerical_data(sys)
|
| 105 |
+
z_check = numpy.allclose(z, expected_value[0])
|
| 106 |
+
p_check = numpy.allclose(p, expected_value[1])
|
| 107 |
+
return p_check and z_check
|
| 108 |
+
|
| 109 |
+
exp1 = [[], [-0.24999999999999994+1.3919410907075054j, -0.24999999999999994-1.3919410907075054j]]
|
| 110 |
+
exp2 = [[0.0], [-0.25+0.3227486121839514j, -0.25-0.3227486121839514j]]
|
| 111 |
+
exp3 = [[0.0], [-0.5000000000000004+0.8660254037844395j,
|
| 112 |
+
-0.5000000000000004-0.8660254037844395j, 0.9999999999999998+0j]]
|
| 113 |
+
exp4 = [[], [5.0, 0.0, 0.0, 0.0]]
|
| 114 |
+
exp5 = [[-5.645751311064592, -0.5000000000000008, -0.3542486889354093],
|
| 115 |
+
[-0.24999999999999986+1.3919410907075052j,
|
| 116 |
+
-0.24999999999999986-1.3919410907075052j, -0.2499999999999998+0.32274861218395134j,
|
| 117 |
+
-0.2499999999999998-0.32274861218395134j]]
|
| 118 |
+
exp6 = [[], [-1.1641600331447917-3.545808351896439j,
|
| 119 |
+
-0.8358399668552097+2.5458083518964383j]]
|
| 120 |
+
|
| 121 |
+
assert pz_tester(tf1, exp1)
|
| 122 |
+
assert pz_tester(tf2, exp2)
|
| 123 |
+
assert pz_tester(tf3, exp3)
|
| 124 |
+
assert pz_tester(ser1, exp4)
|
| 125 |
+
assert pz_tester(par1, exp5)
|
| 126 |
+
assert pz_tester(tf8, exp6)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def test_bode():
|
| 130 |
+
if not numpy:
|
| 131 |
+
skip("NumPy is required for this test")
|
| 132 |
+
|
| 133 |
+
def bode_phase_evalf(system, point):
|
| 134 |
+
expr = system.to_expr()
|
| 135 |
+
_w = Dummy("w", real=True)
|
| 136 |
+
w_expr = expr.subs({system.var: I*_w})
|
| 137 |
+
return arg(w_expr).subs({_w: point}).evalf()
|
| 138 |
+
|
| 139 |
+
def bode_mag_evalf(system, point):
|
| 140 |
+
expr = system.to_expr()
|
| 141 |
+
_w = Dummy("w", real=True)
|
| 142 |
+
w_expr = expr.subs({system.var: I*_w})
|
| 143 |
+
return 20*log(Abs(w_expr), 10).subs({_w: point}).evalf()
|
| 144 |
+
|
| 145 |
+
def test_bode_data(sys):
|
| 146 |
+
return y_coordinate_equality(bode_magnitude_numerical_data, bode_mag_evalf, sys) \
|
| 147 |
+
and y_coordinate_equality(bode_phase_numerical_data, bode_phase_evalf, sys)
|
| 148 |
+
|
| 149 |
+
assert test_bode_data(tf1)
|
| 150 |
+
assert test_bode_data(tf2)
|
| 151 |
+
assert test_bode_data(tf3)
|
| 152 |
+
assert test_bode_data(tf4)
|
| 153 |
+
assert test_bode_data(tf5)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def check_point_accuracy(a, b):
|
| 157 |
+
return all(isclose(*_, rel_tol=1e-1, abs_tol=1e-6
|
| 158 |
+
) for _ in zip(a, b))
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def test_impulse_response():
|
| 162 |
+
if not numpy:
|
| 163 |
+
skip("NumPy is required for this test")
|
| 164 |
+
|
| 165 |
+
def impulse_res_tester(sys, expected_value):
|
| 166 |
+
x, y = _to_tuple(*impulse_response_numerical_data(sys,
|
| 167 |
+
adaptive=False, n=10))
|
| 168 |
+
x_check = check_point_accuracy(x, expected_value[0])
|
| 169 |
+
y_check = check_point_accuracy(y, expected_value[1])
|
| 170 |
+
return x_check and y_check
|
| 171 |
+
|
| 172 |
+
exp1 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
|
| 173 |
+
5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
|
| 174 |
+
(0.0, 0.544019738507865, 0.01993849743234938, -0.31140243360893216, -0.022852779906491996, 0.1778306498155759,
|
| 175 |
+
0.01962941084328499, -0.1013115194573652, -0.014975541213105696, 0.0575789724730714))
|
| 176 |
+
exp2 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555,
|
| 177 |
+
6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.1666666675, 0.08389223412935855,
|
| 178 |
+
0.02338051973475047, -0.014966807776379383, -0.034645954223054234, -0.040560075735512804,
|
| 179 |
+
-0.037658628907103885, -0.030149507719590022, -0.021162090730736834, -0.012721292737437523))
|
| 180 |
+
exp3 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555,
|
| 181 |
+
6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (4.369893391586999e-09, 1.1750333000630964,
|
| 182 |
+
3.2922404058312473, 9.432290008148343, 28.37098083007151, 86.18577464367974, 261.90356653762115,
|
| 183 |
+
795.6538758627842, 2416.9920942096983, 7342.159505206647))
|
| 184 |
+
exp4 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555,
|
| 185 |
+
6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.0, 6.17283950617284, 24.69135802469136,
|
| 186 |
+
55.555555555555564, 98.76543209876544, 154.320987654321, 222.22222222222226, 302.46913580246917,
|
| 187 |
+
395.0617283950618, 500.0))
|
| 188 |
+
exp5 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555,
|
| 189 |
+
6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.0, -0.10455606138085417,
|
| 190 |
+
0.06757671513476461, -0.03234567568833768, 0.013582514927757873, -0.005273419510705473,
|
| 191 |
+
0.0019364083003354075, -0.000680070134067832, 0.00022969845960406913, -7.476094359583917e-05))
|
| 192 |
+
exp6 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
|
| 193 |
+
5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
|
| 194 |
+
(-6.016699583000218e-09, 0.35039802056107394, 3.3728423827689884, 12.119846079276684,
|
| 195 |
+
25.86101014293389, 29.352480635282088, -30.49475907497664, -273.8717189554019, -863.2381702029659,
|
| 196 |
+
-1747.0262164682233))
|
| 197 |
+
exp7 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335,
|
| 198 |
+
4.444444444444445, 5.555555555555555, 6.666666666666667, 7.777777777777779,
|
| 199 |
+
8.88888888888889, 10.0), (0.0, 18.934638095560974, 5346.93244680907, 1384609.8718249386,
|
| 200 |
+
358161126.65801865, 92645770015.70108, 23964739753087.42, 6198974342083139.0, 1.603492601616059e+18,
|
| 201 |
+
4.147764422869658e+20))
|
| 202 |
+
|
| 203 |
+
assert impulse_res_tester(tf1, exp1)
|
| 204 |
+
assert impulse_res_tester(tf2, exp2)
|
| 205 |
+
assert impulse_res_tester(tf3, exp3)
|
| 206 |
+
assert impulse_res_tester(tf4, exp4)
|
| 207 |
+
assert impulse_res_tester(tf5, exp5)
|
| 208 |
+
assert impulse_res_tester(tf7, exp6)
|
| 209 |
+
assert impulse_res_tester(ser1, exp7)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def test_step_response():
|
| 213 |
+
if not numpy:
|
| 214 |
+
skip("NumPy is required for this test")
|
| 215 |
+
|
| 216 |
+
def step_res_tester(sys, expected_value):
|
| 217 |
+
x, y = _to_tuple(*step_response_numerical_data(sys,
|
| 218 |
+
adaptive=False, n=10))
|
| 219 |
+
x_check = check_point_accuracy(x, expected_value[0])
|
| 220 |
+
y_check = check_point_accuracy(y, expected_value[1])
|
| 221 |
+
return x_check and y_check
|
| 222 |
+
|
| 223 |
+
exp1 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
|
| 224 |
+
5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
|
| 225 |
+
(-1.9193285738516863e-08, 0.42283495488246126, 0.7840485977945262, 0.5546841805655717,
|
| 226 |
+
0.33903033806932087, 0.4627251747410237, 0.5909907598988051, 0.5247213989553071,
|
| 227 |
+
0.4486997874319281, 0.4839358435839171))
|
| 228 |
+
exp2 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
|
| 229 |
+
5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
|
| 230 |
+
(0.0, 0.13728409095645816, 0.19474559355325086, 0.1974909129243011, 0.16841657696573073,
|
| 231 |
+
0.12559777736159378, 0.08153828016664713, 0.04360471317348958, 0.015072994568868221,
|
| 232 |
+
-0.003636420058445484))
|
| 233 |
+
exp3 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
|
| 234 |
+
5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
|
| 235 |
+
(0.0, 0.6314542141914303, 2.9356520038101035, 9.37731009663807, 28.452300356688376,
|
| 236 |
+
86.25721933273988, 261.9236645044672, 795.6435410577224, 2416.9786984578764, 7342.154119725917))
|
| 237 |
+
exp4 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
|
| 238 |
+
5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
|
| 239 |
+
(0.0, 2.286236899862826, 18.28989519890261, 61.72839629629631, 146.31916159122088, 285.7796124828532,
|
| 240 |
+
493.8271703703705, 784.1792566529494, 1170.553292729767, 1666.6667))
|
| 241 |
+
exp5 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
|
| 242 |
+
5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
|
| 243 |
+
(-3.999999997894577e-09, 0.6720357068882895, 0.4429938256137113, 0.5182010838004518,
|
| 244 |
+
0.4944139147159695, 0.5016379853883338, 0.4995466896527733, 0.5001154784851325,
|
| 245 |
+
0.49997448824584123, 0.5000039745919259))
|
| 246 |
+
exp6 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
|
| 247 |
+
5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
|
| 248 |
+
(-1.5433688493882158e-09, 0.3428705539937336, 1.1253619102202777, 3.1849962651016517,
|
| 249 |
+
9.47532757182671, 28.727231099148135, 87.29426924860557, 265.2138681048606, 805.6636260007757,
|
| 250 |
+
2447.387582370878))
|
| 251 |
+
|
| 252 |
+
assert step_res_tester(tf1, exp1)
|
| 253 |
+
assert step_res_tester(tf2, exp2)
|
| 254 |
+
assert step_res_tester(tf3, exp3)
|
| 255 |
+
assert step_res_tester(tf4, exp4)
|
| 256 |
+
assert step_res_tester(tf5, exp5)
|
| 257 |
+
assert step_res_tester(ser2, exp6)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def test_ramp_response():
|
| 261 |
+
if not numpy:
|
| 262 |
+
skip("NumPy is required for this test")
|
| 263 |
+
|
| 264 |
+
def ramp_res_tester(sys, num_points, expected_value, slope=1):
|
| 265 |
+
x, y = _to_tuple(*ramp_response_numerical_data(sys,
|
| 266 |
+
slope=slope, adaptive=False, n=num_points))
|
| 267 |
+
x_check = check_point_accuracy(x, expected_value[0])
|
| 268 |
+
y_check = check_point_accuracy(y, expected_value[1])
|
| 269 |
+
return x_check and y_check
|
| 270 |
+
|
| 271 |
+
exp1 = ((0.0, 2.0, 4.0, 6.0, 8.0, 10.0), (0.0, 0.7324667795033895, 1.9909720978650398,
|
| 272 |
+
2.7956587704217783, 3.9224897567931514, 4.85022655284895))
|
| 273 |
+
exp2 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
|
| 274 |
+
5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
|
| 275 |
+
(2.4360213402019326e-08, 0.10175320182493253, 0.33057612497658406, 0.5967937263298935,
|
| 276 |
+
0.8431511866718248, 1.0398805391471613, 1.1776043125035738, 1.2600994825747305, 1.2981042689274653,
|
| 277 |
+
1.304684417610106))
|
| 278 |
+
exp3 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555,
|
| 279 |
+
6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (-3.9329040468771836e-08,
|
| 280 |
+
0.34686634635794555, 2.9998828170537903, 12.33303690737476, 40.993913948137795, 127.84145222317912,
|
| 281 |
+
391.41713691996, 1192.0006858708389, 3623.9808672503405, 11011.728034546572))
|
| 282 |
+
exp4 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555,
|
| 283 |
+
6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.0, 1.9051973784484078, 30.483158055174524,
|
| 284 |
+
154.32098765432104, 487.7305288827924, 1190.7483615302544, 2469.1358024691367, 4574.3789056546275,
|
| 285 |
+
7803.688462124678, 12500.0))
|
| 286 |
+
exp5 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555,
|
| 287 |
+
6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.0, 3.8844361856975635, 9.141792069209865,
|
| 288 |
+
14.096349157657231, 19.09783068994694, 24.10179770390321, 29.09907319114121, 34.10040420185154,
|
| 289 |
+
39.09983919254265, 44.10006013058409))
|
| 290 |
+
exp6 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555,
|
| 291 |
+
6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.0, 1.1111111111111112, 2.2222222222222223,
|
| 292 |
+
3.3333333333333335, 4.444444444444445, 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0))
|
| 293 |
+
|
| 294 |
+
assert ramp_res_tester(tf1, 6, exp1)
|
| 295 |
+
assert ramp_res_tester(tf2, 10, exp2, 1.2)
|
| 296 |
+
assert ramp_res_tester(tf3, 10, exp3, 1.5)
|
| 297 |
+
assert ramp_res_tester(tf4, 10, exp4, 3)
|
| 298 |
+
assert ramp_res_tester(tf5, 10, exp5, 9)
|
| 299 |
+
assert ramp_res_tester(tf6, 10, exp6)
|
lib/python3.12/site-packages/sympy/physics/control/tests/test_lti.py
ADDED
|
@@ -0,0 +1,1750 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy.core.add import Add
|
| 2 |
+
from sympy.core.function import Function
|
| 3 |
+
from sympy.core.mul import Mul
|
| 4 |
+
from sympy.core.numbers import (I, pi, Rational, oo)
|
| 5 |
+
from sympy.core.power import Pow
|
| 6 |
+
from sympy.core.singleton import S
|
| 7 |
+
from sympy.core.symbol import symbols
|
| 8 |
+
from sympy.functions.elementary.exponential import (exp, log)
|
| 9 |
+
from sympy.functions.elementary.miscellaneous import sqrt
|
| 10 |
+
from sympy.functions.elementary.trigonometric import atan
|
| 11 |
+
from sympy.matrices.dense import eye
|
| 12 |
+
from sympy.polys.polytools import factor
|
| 13 |
+
from sympy.polys.rootoftools import CRootOf
|
| 14 |
+
from sympy.simplify.simplify import simplify
|
| 15 |
+
from sympy.core.containers import Tuple
|
| 16 |
+
from sympy.matrices import ImmutableMatrix, Matrix, ShapeError
|
| 17 |
+
from sympy.physics.control import (TransferFunction, Series, Parallel,
|
| 18 |
+
Feedback, TransferFunctionMatrix, MIMOSeries, MIMOParallel, MIMOFeedback,
|
| 19 |
+
StateSpace, gbt, bilinear, forward_diff, backward_diff, phase_margin, gain_margin)
|
| 20 |
+
from sympy.testing.pytest import raises
|
| 21 |
+
|
| 22 |
+
a, x, b, c, s, g, d, p, k, tau, zeta, wn, T = symbols('a, x, b, c, s, g, d, p, k,\
|
| 23 |
+
tau, zeta, wn, T')
|
| 24 |
+
a0, a1, a2, a3, b0, b1, b2, b3, c0, c1, c2, c3, d0, d1, d2, d3 = symbols('a0:4,\
|
| 25 |
+
b0:4, c0:4, d0:4')
|
| 26 |
+
TF1 = TransferFunction(1, s**2 + 2*zeta*wn*s + wn**2, s)
|
| 27 |
+
TF2 = TransferFunction(k, 1, s)
|
| 28 |
+
TF3 = TransferFunction(a2*p - s, a2*s + p, s)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def test_TransferFunction_construction():
|
| 32 |
+
tf = TransferFunction(s + 1, s**2 + s + 1, s)
|
| 33 |
+
assert tf.num == (s + 1)
|
| 34 |
+
assert tf.den == (s**2 + s + 1)
|
| 35 |
+
assert tf.args == (s + 1, s**2 + s + 1, s)
|
| 36 |
+
|
| 37 |
+
tf1 = TransferFunction(s + 4, s - 5, s)
|
| 38 |
+
assert tf1.num == (s + 4)
|
| 39 |
+
assert tf1.den == (s - 5)
|
| 40 |
+
assert tf1.args == (s + 4, s - 5, s)
|
| 41 |
+
|
| 42 |
+
# using different polynomial variables.
|
| 43 |
+
tf2 = TransferFunction(p + 3, p**2 - 9, p)
|
| 44 |
+
assert tf2.num == (p + 3)
|
| 45 |
+
assert tf2.den == (p**2 - 9)
|
| 46 |
+
assert tf2.args == (p + 3, p**2 - 9, p)
|
| 47 |
+
|
| 48 |
+
tf3 = TransferFunction(p**3 + 5*p**2 + 4, p**4 + 3*p + 1, p)
|
| 49 |
+
assert tf3.args == (p**3 + 5*p**2 + 4, p**4 + 3*p + 1, p)
|
| 50 |
+
|
| 51 |
+
# no pole-zero cancellation on its own.
|
| 52 |
+
tf4 = TransferFunction((s + 3)*(s - 1), (s - 1)*(s + 5), s)
|
| 53 |
+
assert tf4.den == (s - 1)*(s + 5)
|
| 54 |
+
assert tf4.args == ((s + 3)*(s - 1), (s - 1)*(s + 5), s)
|
| 55 |
+
|
| 56 |
+
tf4_ = TransferFunction(p + 2, p + 2, p)
|
| 57 |
+
assert tf4_.args == (p + 2, p + 2, p)
|
| 58 |
+
|
| 59 |
+
tf5 = TransferFunction(s - 1, 4 - p, s)
|
| 60 |
+
assert tf5.args == (s - 1, 4 - p, s)
|
| 61 |
+
|
| 62 |
+
tf5_ = TransferFunction(s - 1, s - 1, s)
|
| 63 |
+
assert tf5_.args == (s - 1, s - 1, s)
|
| 64 |
+
|
| 65 |
+
tf6 = TransferFunction(5, 6, s)
|
| 66 |
+
assert tf6.num == 5
|
| 67 |
+
assert tf6.den == 6
|
| 68 |
+
assert tf6.args == (5, 6, s)
|
| 69 |
+
|
| 70 |
+
tf6_ = TransferFunction(1/2, 4, s)
|
| 71 |
+
assert tf6_.num == 0.5
|
| 72 |
+
assert tf6_.den == 4
|
| 73 |
+
assert tf6_.args == (0.500000000000000, 4, s)
|
| 74 |
+
|
| 75 |
+
tf7 = TransferFunction(3*s**2 + 2*p + 4*s, 8*p**2 + 7*s, s)
|
| 76 |
+
tf8 = TransferFunction(3*s**2 + 2*p + 4*s, 8*p**2 + 7*s, p)
|
| 77 |
+
assert not tf7 == tf8
|
| 78 |
+
|
| 79 |
+
tf7_ = TransferFunction(a0*s + a1*s**2 + a2*s**3, b0*p - b1*s, s)
|
| 80 |
+
tf8_ = TransferFunction(a0*s + a1*s**2 + a2*s**3, b0*p - b1*s, s)
|
| 81 |
+
assert tf7_ == tf8_
|
| 82 |
+
assert -(-tf7_) == tf7_ == -(-(-(-tf7_)))
|
| 83 |
+
|
| 84 |
+
tf9 = TransferFunction(a*s**3 + b*s**2 + g*s + d, d*p + g*p**2 + g*s, s)
|
| 85 |
+
assert tf9.args == (a*s**3 + b*s**2 + d + g*s, d*p + g*p**2 + g*s, s)
|
| 86 |
+
|
| 87 |
+
tf10 = TransferFunction(p**3 + d, g*s**2 + d*s + a, p)
|
| 88 |
+
tf10_ = TransferFunction(p**3 + d, g*s**2 + d*s + a, p)
|
| 89 |
+
assert tf10.args == (d + p**3, a + d*s + g*s**2, p)
|
| 90 |
+
assert tf10_ == tf10
|
| 91 |
+
|
| 92 |
+
tf11 = TransferFunction(a1*s + a0, b2*s**2 + b1*s + b0, s)
|
| 93 |
+
assert tf11.num == (a0 + a1*s)
|
| 94 |
+
assert tf11.den == (b0 + b1*s + b2*s**2)
|
| 95 |
+
assert tf11.args == (a0 + a1*s, b0 + b1*s + b2*s**2, s)
|
| 96 |
+
|
| 97 |
+
# when just the numerator is 0, leave the denominator alone.
|
| 98 |
+
tf12 = TransferFunction(0, p**2 - p + 1, p)
|
| 99 |
+
assert tf12.args == (0, p**2 - p + 1, p)
|
| 100 |
+
|
| 101 |
+
tf13 = TransferFunction(0, 1, s)
|
| 102 |
+
assert tf13.args == (0, 1, s)
|
| 103 |
+
|
| 104 |
+
# float exponents
|
| 105 |
+
tf14 = TransferFunction(a0*s**0.5 + a2*s**0.6 - a1, a1*p**(-8.7), s)
|
| 106 |
+
assert tf14.args == (a0*s**0.5 - a1 + a2*s**0.6, a1*p**(-8.7), s)
|
| 107 |
+
|
| 108 |
+
tf15 = TransferFunction(a2**2*p**(1/4) + a1*s**(-4/5), a0*s - p, p)
|
| 109 |
+
assert tf15.args == (a1*s**(-0.8) + a2**2*p**0.25, a0*s - p, p)
|
| 110 |
+
|
| 111 |
+
omega_o, k_p, k_o, k_i = symbols('omega_o, k_p, k_o, k_i')
|
| 112 |
+
tf18 = TransferFunction((k_p + k_o*s + k_i/s), s**2 + 2*omega_o*s + omega_o**2, s)
|
| 113 |
+
assert tf18.num == k_i/s + k_o*s + k_p
|
| 114 |
+
assert tf18.args == (k_i/s + k_o*s + k_p, omega_o**2 + 2*omega_o*s + s**2, s)
|
| 115 |
+
|
| 116 |
+
# ValueError when denominator is zero.
|
| 117 |
+
raises(ValueError, lambda: TransferFunction(4, 0, s))
|
| 118 |
+
raises(ValueError, lambda: TransferFunction(s, 0, s))
|
| 119 |
+
raises(ValueError, lambda: TransferFunction(0, 0, s))
|
| 120 |
+
|
| 121 |
+
raises(TypeError, lambda: TransferFunction(Matrix([1, 2, 3]), s, s))
|
| 122 |
+
|
| 123 |
+
raises(TypeError, lambda: TransferFunction(s**2 + 2*s - 1, s + 3, 3))
|
| 124 |
+
raises(TypeError, lambda: TransferFunction(p + 1, 5 - p, 4))
|
| 125 |
+
raises(TypeError, lambda: TransferFunction(3, 4, 8))
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def test_TransferFunction_functions():
|
| 129 |
+
# classmethod from_rational_expression
|
| 130 |
+
expr_1 = Mul(0, Pow(s, -1, evaluate=False), evaluate=False)
|
| 131 |
+
expr_2 = s/0
|
| 132 |
+
expr_3 = (p*s**2 + 5*s)/(s + 1)**3
|
| 133 |
+
expr_4 = 6
|
| 134 |
+
expr_5 = ((2 + 3*s)*(5 + 2*s))/((9 + 3*s)*(5 + 2*s**2))
|
| 135 |
+
expr_6 = (9*s**4 + 4*s**2 + 8)/((s + 1)*(s + 9))
|
| 136 |
+
tf = TransferFunction(s + 1, s**2 + 2, s)
|
| 137 |
+
delay = exp(-s/tau)
|
| 138 |
+
expr_7 = delay*tf.to_expr()
|
| 139 |
+
H1 = TransferFunction.from_rational_expression(expr_7, s)
|
| 140 |
+
H2 = TransferFunction(s + 1, (s**2 + 2)*exp(s/tau), s)
|
| 141 |
+
expr_8 = Add(2, 3*s/(s**2 + 1), evaluate=False)
|
| 142 |
+
|
| 143 |
+
assert TransferFunction.from_rational_expression(expr_1) == TransferFunction(0, s, s)
|
| 144 |
+
raises(ZeroDivisionError, lambda: TransferFunction.from_rational_expression(expr_2))
|
| 145 |
+
raises(ValueError, lambda: TransferFunction.from_rational_expression(expr_3))
|
| 146 |
+
assert TransferFunction.from_rational_expression(expr_3, s) == TransferFunction((p*s**2 + 5*s), (s + 1)**3, s)
|
| 147 |
+
assert TransferFunction.from_rational_expression(expr_3, p) == TransferFunction((p*s**2 + 5*s), (s + 1)**3, p)
|
| 148 |
+
raises(ValueError, lambda: TransferFunction.from_rational_expression(expr_4))
|
| 149 |
+
assert TransferFunction.from_rational_expression(expr_4, s) == TransferFunction(6, 1, s)
|
| 150 |
+
assert TransferFunction.from_rational_expression(expr_5, s) == \
|
| 151 |
+
TransferFunction((2 + 3*s)*(5 + 2*s), (9 + 3*s)*(5 + 2*s**2), s)
|
| 152 |
+
assert TransferFunction.from_rational_expression(expr_6, s) == \
|
| 153 |
+
TransferFunction((9*s**4 + 4*s**2 + 8), (s + 1)*(s + 9), s)
|
| 154 |
+
assert H1 == H2
|
| 155 |
+
assert TransferFunction.from_rational_expression(expr_8, s) == \
|
| 156 |
+
TransferFunction(2*s**2 + 3*s + 2, s**2 + 1, s)
|
| 157 |
+
|
| 158 |
+
# classmethod from_coeff_lists
|
| 159 |
+
tf1 = TransferFunction.from_coeff_lists([1, 2], [3, 4, 5], s)
|
| 160 |
+
num2 = [p**2, 2*p]
|
| 161 |
+
den2 = [p**3, p + 1, 4]
|
| 162 |
+
tf2 = TransferFunction.from_coeff_lists(num2, den2, s)
|
| 163 |
+
num3 = [1, 2, 3]
|
| 164 |
+
den3 = [0, 0]
|
| 165 |
+
|
| 166 |
+
assert tf1 == TransferFunction(s + 2, 3*s**2 + 4*s + 5, s)
|
| 167 |
+
assert tf2 == TransferFunction(p**2*s + 2*p, p**3*s**2 + s*(p + 1) + 4, s)
|
| 168 |
+
raises(ZeroDivisionError, lambda: TransferFunction.from_coeff_lists(num3, den3, s))
|
| 169 |
+
|
| 170 |
+
# classmethod from_zpk
|
| 171 |
+
zeros = [4]
|
| 172 |
+
poles = [-1+2j, -1-2j]
|
| 173 |
+
gain = 3
|
| 174 |
+
tf1 = TransferFunction.from_zpk(zeros, poles, gain, s)
|
| 175 |
+
|
| 176 |
+
assert tf1 == TransferFunction(3*s - 12, (s + 1.0 - 2.0*I)*(s + 1.0 + 2.0*I), s)
|
| 177 |
+
|
| 178 |
+
# explicitly cancel poles and zeros.
|
| 179 |
+
tf0 = TransferFunction(s**5 + s**3 + s, s - s**2, s)
|
| 180 |
+
a = TransferFunction(-(s**4 + s**2 + 1), s - 1, s)
|
| 181 |
+
assert tf0.simplify() == simplify(tf0) == a
|
| 182 |
+
|
| 183 |
+
tf1 = TransferFunction((p + 3)*(p - 1), (p - 1)*(p + 5), p)
|
| 184 |
+
b = TransferFunction(p + 3, p + 5, p)
|
| 185 |
+
assert tf1.simplify() == simplify(tf1) == b
|
| 186 |
+
|
| 187 |
+
# expand the numerator and the denominator.
|
| 188 |
+
G1 = TransferFunction((1 - s)**2, (s**2 + 1)**2, s)
|
| 189 |
+
G2 = TransferFunction(1, -3, p)
|
| 190 |
+
c = (a2*s**p + a1*s**s + a0*p**p)*(p**s + s**p)
|
| 191 |
+
d = (b0*s**s + b1*p**s)*(b2*s*p + p**p)
|
| 192 |
+
e = a0*p**p*p**s + a0*p**p*s**p + a1*p**s*s**s + a1*s**p*s**s + a2*p**s*s**p + a2*s**(2*p)
|
| 193 |
+
f = b0*b2*p*s*s**s + b0*p**p*s**s + b1*b2*p*p**s*s + b1*p**p*p**s
|
| 194 |
+
g = a1*a2*s*s**p + a1*p*s + a2*b1*p*s*s**p + b1*p**2*s
|
| 195 |
+
G3 = TransferFunction(c, d, s)
|
| 196 |
+
G4 = TransferFunction(a0*s**s - b0*p**p, (a1*s + b1*s*p)*(a2*s**p + p), p)
|
| 197 |
+
|
| 198 |
+
assert G1.expand() == TransferFunction(s**2 - 2*s + 1, s**4 + 2*s**2 + 1, s)
|
| 199 |
+
assert tf1.expand() == TransferFunction(p**2 + 2*p - 3, p**2 + 4*p - 5, p)
|
| 200 |
+
assert G2.expand() == G2
|
| 201 |
+
assert G3.expand() == TransferFunction(e, f, s)
|
| 202 |
+
assert G4.expand() == TransferFunction(a0*s**s - b0*p**p, g, p)
|
| 203 |
+
|
| 204 |
+
# purely symbolic polynomials.
|
| 205 |
+
p1 = a1*s + a0
|
| 206 |
+
p2 = b2*s**2 + b1*s + b0
|
| 207 |
+
SP1 = TransferFunction(p1, p2, s)
|
| 208 |
+
expect1 = TransferFunction(2.0*s + 1.0, 5.0*s**2 + 4.0*s + 3.0, s)
|
| 209 |
+
expect1_ = TransferFunction(2*s + 1, 5*s**2 + 4*s + 3, s)
|
| 210 |
+
assert SP1.subs({a0: 1, a1: 2, b0: 3, b1: 4, b2: 5}) == expect1_
|
| 211 |
+
assert SP1.subs({a0: 1, a1: 2, b0: 3, b1: 4, b2: 5}).evalf() == expect1
|
| 212 |
+
assert expect1_.evalf() == expect1
|
| 213 |
+
|
| 214 |
+
c1, d0, d1, d2 = symbols('c1, d0:3')
|
| 215 |
+
p3, p4 = c1*p, d2*p**3 + d1*p**2 - d0
|
| 216 |
+
SP2 = TransferFunction(p3, p4, p)
|
| 217 |
+
expect2 = TransferFunction(2.0*p, 5.0*p**3 + 2.0*p**2 - 3.0, p)
|
| 218 |
+
expect2_ = TransferFunction(2*p, 5*p**3 + 2*p**2 - 3, p)
|
| 219 |
+
assert SP2.subs({c1: 2, d0: 3, d1: 2, d2: 5}) == expect2_
|
| 220 |
+
assert SP2.subs({c1: 2, d0: 3, d1: 2, d2: 5}).evalf() == expect2
|
| 221 |
+
assert expect2_.evalf() == expect2
|
| 222 |
+
|
| 223 |
+
SP3 = TransferFunction(a0*p**3 + a1*s**2 - b0*s + b1, a1*s + p, s)
|
| 224 |
+
expect3 = TransferFunction(2.0*p**3 + 4.0*s**2 - s + 5.0, p + 4.0*s, s)
|
| 225 |
+
expect3_ = TransferFunction(2*p**3 + 4*s**2 - s + 5, p + 4*s, s)
|
| 226 |
+
assert SP3.subs({a0: 2, a1: 4, b0: 1, b1: 5}) == expect3_
|
| 227 |
+
assert SP3.subs({a0: 2, a1: 4, b0: 1, b1: 5}).evalf() == expect3
|
| 228 |
+
assert expect3_.evalf() == expect3
|
| 229 |
+
|
| 230 |
+
SP4 = TransferFunction(s - a1*p**3, a0*s + p, p)
|
| 231 |
+
expect4 = TransferFunction(7.0*p**3 + s, p - s, p)
|
| 232 |
+
expect4_ = TransferFunction(7*p**3 + s, p - s, p)
|
| 233 |
+
assert SP4.subs({a0: -1, a1: -7}) == expect4_
|
| 234 |
+
assert SP4.subs({a0: -1, a1: -7}).evalf() == expect4
|
| 235 |
+
assert expect4_.evalf() == expect4
|
| 236 |
+
|
| 237 |
+
# evaluate the transfer function at particular frequencies.
|
| 238 |
+
assert tf1.eval_frequency(wn) == wn**2/(wn**2 + 4*wn - 5) + 2*wn/(wn**2 + 4*wn - 5) - 3/(wn**2 + 4*wn - 5)
|
| 239 |
+
assert G1.eval_frequency(1 + I) == S(3)/25 + S(4)*I/25
|
| 240 |
+
assert G4.eval_frequency(S(5)/3) == \
|
| 241 |
+
a0*s**s/(a1*a2*s**(S(8)/3) + S(5)*a1*s/3 + 5*a2*b1*s**(S(8)/3)/3 + S(25)*b1*s/9) - 5*3**(S(1)/3)*5**(S(2)/3)*b0/(9*a1*a2*s**(S(8)/3) + 15*a1*s + 15*a2*b1*s**(S(8)/3) + 25*b1*s)
|
| 242 |
+
|
| 243 |
+
# Low-frequency (or DC) gain.
|
| 244 |
+
assert tf0.dc_gain() == 1
|
| 245 |
+
assert tf1.dc_gain() == Rational(3, 5)
|
| 246 |
+
assert SP2.dc_gain() == 0
|
| 247 |
+
assert expect4.dc_gain() == -1
|
| 248 |
+
assert expect2_.dc_gain() == 0
|
| 249 |
+
assert TransferFunction(1, s, s).dc_gain() == oo
|
| 250 |
+
|
| 251 |
+
# Poles of a transfer function.
|
| 252 |
+
tf_ = TransferFunction(x**3 - k, k, x)
|
| 253 |
+
_tf = TransferFunction(k, x**4 - k, x)
|
| 254 |
+
TF_ = TransferFunction(x**2, x**10 + x + x**2, x)
|
| 255 |
+
_TF = TransferFunction(x**10 + x + x**2, x**2, x)
|
| 256 |
+
assert G1.poles() == [I, I, -I, -I]
|
| 257 |
+
assert G2.poles() == []
|
| 258 |
+
assert tf1.poles() == [-5, 1]
|
| 259 |
+
assert expect4_.poles() == [s]
|
| 260 |
+
assert SP4.poles() == [-a0*s]
|
| 261 |
+
assert expect3.poles() == [-0.25*p]
|
| 262 |
+
assert str(expect2.poles()) == str([0.729001428685125, -0.564500714342563 - 0.710198984796332*I, -0.564500714342563 + 0.710198984796332*I])
|
| 263 |
+
assert str(expect1.poles()) == str([-0.4 - 0.66332495807108*I, -0.4 + 0.66332495807108*I])
|
| 264 |
+
assert _tf.poles() == [k**(Rational(1, 4)), -k**(Rational(1, 4)), I*k**(Rational(1, 4)), -I*k**(Rational(1, 4))]
|
| 265 |
+
assert TF_.poles() == [CRootOf(x**9 + x + 1, 0), 0, CRootOf(x**9 + x + 1, 1), CRootOf(x**9 + x + 1, 2),
|
| 266 |
+
CRootOf(x**9 + x + 1, 3), CRootOf(x**9 + x + 1, 4), CRootOf(x**9 + x + 1, 5), CRootOf(x**9 + x + 1, 6),
|
| 267 |
+
CRootOf(x**9 + x + 1, 7), CRootOf(x**9 + x + 1, 8)]
|
| 268 |
+
raises(NotImplementedError, lambda: TransferFunction(x**2, a0*x**10 + x + x**2, x).poles())
|
| 269 |
+
|
| 270 |
+
# Stability of a transfer function.
|
| 271 |
+
q, r = symbols('q, r', negative=True)
|
| 272 |
+
t = symbols('t', positive=True)
|
| 273 |
+
TF_ = TransferFunction(s**2 + a0 - a1*p, q*s - r, s)
|
| 274 |
+
stable_tf = TransferFunction(s**2 + a0 - a1*p, q*s - 1, s)
|
| 275 |
+
stable_tf_ = TransferFunction(s**2 + a0 - a1*p, q*s - t, s)
|
| 276 |
+
|
| 277 |
+
assert G1.is_stable() is False
|
| 278 |
+
assert G2.is_stable() is True
|
| 279 |
+
assert tf1.is_stable() is False # as one pole is +ve, and the other is -ve.
|
| 280 |
+
assert expect2.is_stable() is False
|
| 281 |
+
assert expect1.is_stable() is True
|
| 282 |
+
assert stable_tf.is_stable() is True
|
| 283 |
+
assert stable_tf_.is_stable() is True
|
| 284 |
+
assert TF_.is_stable() is False
|
| 285 |
+
assert expect4_.is_stable() is None # no assumption provided for the only pole 's'.
|
| 286 |
+
assert SP4.is_stable() is None
|
| 287 |
+
|
| 288 |
+
# Zeros of a transfer function.
|
| 289 |
+
assert G1.zeros() == [1, 1]
|
| 290 |
+
assert G2.zeros() == []
|
| 291 |
+
assert tf1.zeros() == [-3, 1]
|
| 292 |
+
assert expect4_.zeros() == [7**(Rational(2, 3))*(-s)**(Rational(1, 3))/7, -7**(Rational(2, 3))*(-s)**(Rational(1, 3))/14 -
|
| 293 |
+
sqrt(3)*7**(Rational(2, 3))*I*(-s)**(Rational(1, 3))/14, -7**(Rational(2, 3))*(-s)**(Rational(1, 3))/14 + sqrt(3)*7**(Rational(2, 3))*I*(-s)**(Rational(1, 3))/14]
|
| 294 |
+
assert SP4.zeros() == [(s/a1)**(Rational(1, 3)), -(s/a1)**(Rational(1, 3))/2 - sqrt(3)*I*(s/a1)**(Rational(1, 3))/2,
|
| 295 |
+
-(s/a1)**(Rational(1, 3))/2 + sqrt(3)*I*(s/a1)**(Rational(1, 3))/2]
|
| 296 |
+
assert str(expect3.zeros()) == str([0.125 - 1.11102430216445*sqrt(-0.405063291139241*p**3 - 1.0),
|
| 297 |
+
1.11102430216445*sqrt(-0.405063291139241*p**3 - 1.0) + 0.125])
|
| 298 |
+
assert tf_.zeros() == [k**(Rational(1, 3)), -k**(Rational(1, 3))/2 - sqrt(3)*I*k**(Rational(1, 3))/2,
|
| 299 |
+
-k**(Rational(1, 3))/2 + sqrt(3)*I*k**(Rational(1, 3))/2]
|
| 300 |
+
assert _TF.zeros() == [CRootOf(x**9 + x + 1, 0), 0, CRootOf(x**9 + x + 1, 1), CRootOf(x**9 + x + 1, 2),
|
| 301 |
+
CRootOf(x**9 + x + 1, 3), CRootOf(x**9 + x + 1, 4), CRootOf(x**9 + x + 1, 5), CRootOf(x**9 + x + 1, 6),
|
| 302 |
+
CRootOf(x**9 + x + 1, 7), CRootOf(x**9 + x + 1, 8)]
|
| 303 |
+
raises(NotImplementedError, lambda: TransferFunction(a0*x**10 + x + x**2, x**2, x).zeros())
|
| 304 |
+
|
| 305 |
+
# negation of TF.
|
| 306 |
+
tf2 = TransferFunction(s + 3, s**2 - s**3 + 9, s)
|
| 307 |
+
tf3 = TransferFunction(-3*p + 3, 1 - p, p)
|
| 308 |
+
assert -tf2 == TransferFunction(-s - 3, s**2 - s**3 + 9, s)
|
| 309 |
+
assert -tf3 == TransferFunction(3*p - 3, 1 - p, p)
|
| 310 |
+
|
| 311 |
+
# taking power of a TF.
|
| 312 |
+
tf4 = TransferFunction(p + 4, p - 3, p)
|
| 313 |
+
tf5 = TransferFunction(s**2 + 1, 1 - s, s)
|
| 314 |
+
expect2 = TransferFunction((s**2 + 1)**3, (1 - s)**3, s)
|
| 315 |
+
expect1 = TransferFunction((p + 4)**2, (p - 3)**2, p)
|
| 316 |
+
assert (tf4*tf4).doit() == tf4**2 == pow(tf4, 2) == expect1
|
| 317 |
+
assert (tf5*tf5*tf5).doit() == tf5**3 == pow(tf5, 3) == expect2
|
| 318 |
+
assert tf5**0 == pow(tf5, 0) == TransferFunction(1, 1, s)
|
| 319 |
+
assert Series(tf4).doit()**-1 == tf4**-1 == pow(tf4, -1) == TransferFunction(p - 3, p + 4, p)
|
| 320 |
+
assert (tf5*tf5).doit()**-1 == tf5**-2 == pow(tf5, -2) == TransferFunction((1 - s)**2, (s**2 + 1)**2, s)
|
| 321 |
+
|
| 322 |
+
raises(ValueError, lambda: tf4**(s**2 + s - 1))
|
| 323 |
+
raises(ValueError, lambda: tf5**s)
|
| 324 |
+
raises(ValueError, lambda: tf4**tf5)
|
| 325 |
+
|
| 326 |
+
# SymPy's own functions.
|
| 327 |
+
tf = TransferFunction(s - 1, s**2 - 2*s + 1, s)
|
| 328 |
+
tf6 = TransferFunction(s + p, p**2 - 5, s)
|
| 329 |
+
assert factor(tf) == TransferFunction(s - 1, (s - 1)**2, s)
|
| 330 |
+
assert tf.num.subs(s, 2) == tf.den.subs(s, 2) == 1
|
| 331 |
+
# subs & xreplace
|
| 332 |
+
assert tf.subs(s, 2) == TransferFunction(s - 1, s**2 - 2*s + 1, s)
|
| 333 |
+
assert tf6.subs(p, 3) == TransferFunction(s + 3, 4, s)
|
| 334 |
+
assert tf3.xreplace({p: s}) == TransferFunction(-3*s + 3, 1 - s, s)
|
| 335 |
+
raises(TypeError, lambda: tf3.xreplace({p: exp(2)}))
|
| 336 |
+
assert tf3.subs(p, exp(2)) == tf3
|
| 337 |
+
|
| 338 |
+
tf7 = TransferFunction(a0*s**p + a1*p**s, a2*p - s, s)
|
| 339 |
+
assert tf7.xreplace({s: k}) == TransferFunction(a0*k**p + a1*p**k, a2*p - k, k)
|
| 340 |
+
assert tf7.subs(s, k) == TransferFunction(a0*s**p + a1*p**s, a2*p - s, s)
|
| 341 |
+
|
| 342 |
+
# Conversion to Expr with to_expr()
|
| 343 |
+
tf8 = TransferFunction(a0*s**5 + 5*s**2 + 3, s**6 - 3, s)
|
| 344 |
+
tf9 = TransferFunction((5 + s), (5 + s)*(6 + s), s)
|
| 345 |
+
tf10 = TransferFunction(0, 1, s)
|
| 346 |
+
tf11 = TransferFunction(1, 1, s)
|
| 347 |
+
assert tf8.to_expr() == Mul((a0*s**5 + 5*s**2 + 3), Pow((s**6 - 3), -1, evaluate=False), evaluate=False)
|
| 348 |
+
assert tf9.to_expr() == Mul((s + 5), Pow((5 + s)*(6 + s), -1, evaluate=False), evaluate=False)
|
| 349 |
+
assert tf10.to_expr() == Mul(S(0), Pow(1, -1, evaluate=False), evaluate=False)
|
| 350 |
+
assert tf11.to_expr() == Pow(1, -1, evaluate=False)
|
| 351 |
+
|
| 352 |
+
def test_TransferFunction_addition_and_subtraction():
|
| 353 |
+
tf1 = TransferFunction(s + 6, s - 5, s)
|
| 354 |
+
tf2 = TransferFunction(s + 3, s + 1, s)
|
| 355 |
+
tf3 = TransferFunction(s + 1, s**2 + s + 1, s)
|
| 356 |
+
tf4 = TransferFunction(p, 2 - p, p)
|
| 357 |
+
|
| 358 |
+
# addition
|
| 359 |
+
assert tf1 + tf2 == Parallel(tf1, tf2)
|
| 360 |
+
assert tf3 + tf1 == Parallel(tf3, tf1)
|
| 361 |
+
assert -tf1 + tf2 + tf3 == Parallel(-tf1, tf2, tf3)
|
| 362 |
+
assert tf1 + (tf2 + tf3) == Parallel(tf1, tf2, tf3)
|
| 363 |
+
|
| 364 |
+
c = symbols("c", commutative=False)
|
| 365 |
+
raises(ValueError, lambda: tf1 + Matrix([1, 2, 3]))
|
| 366 |
+
raises(ValueError, lambda: tf2 + c)
|
| 367 |
+
raises(ValueError, lambda: tf3 + tf4)
|
| 368 |
+
raises(ValueError, lambda: tf1 + (s - 1))
|
| 369 |
+
raises(ValueError, lambda: tf1 + 8)
|
| 370 |
+
raises(ValueError, lambda: (1 - p**3) + tf1)
|
| 371 |
+
|
| 372 |
+
# subtraction
|
| 373 |
+
assert tf1 - tf2 == Parallel(tf1, -tf2)
|
| 374 |
+
assert tf3 - tf2 == Parallel(tf3, -tf2)
|
| 375 |
+
assert -tf1 - tf3 == Parallel(-tf1, -tf3)
|
| 376 |
+
assert tf1 - tf2 + tf3 == Parallel(tf1, -tf2, tf3)
|
| 377 |
+
|
| 378 |
+
raises(ValueError, lambda: tf1 - Matrix([1, 2, 3]))
|
| 379 |
+
raises(ValueError, lambda: tf3 - tf4)
|
| 380 |
+
raises(ValueError, lambda: tf1 - (s - 1))
|
| 381 |
+
raises(ValueError, lambda: tf1 - 8)
|
| 382 |
+
raises(ValueError, lambda: (s + 5) - tf2)
|
| 383 |
+
raises(ValueError, lambda: (1 + p**4) - tf1)
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def test_TransferFunction_multiplication_and_division():
|
| 387 |
+
G1 = TransferFunction(s + 3, -s**3 + 9, s)
|
| 388 |
+
G2 = TransferFunction(s + 1, s - 5, s)
|
| 389 |
+
G3 = TransferFunction(p, p**4 - 6, p)
|
| 390 |
+
G4 = TransferFunction(p + 4, p - 5, p)
|
| 391 |
+
G5 = TransferFunction(s + 6, s - 5, s)
|
| 392 |
+
G6 = TransferFunction(s + 3, s + 1, s)
|
| 393 |
+
G7 = TransferFunction(1, 1, s)
|
| 394 |
+
|
| 395 |
+
# multiplication
|
| 396 |
+
assert G1*G2 == Series(G1, G2)
|
| 397 |
+
assert -G1*G5 == Series(-G1, G5)
|
| 398 |
+
assert -G2*G5*-G6 == Series(-G2, G5, -G6)
|
| 399 |
+
assert -G1*-G2*-G5*-G6 == Series(-G1, -G2, -G5, -G6)
|
| 400 |
+
assert G3*G4 == Series(G3, G4)
|
| 401 |
+
assert (G1*G2)*-(G5*G6) == \
|
| 402 |
+
Series(G1, G2, TransferFunction(-1, 1, s), Series(G5, G6))
|
| 403 |
+
assert G1*G2*(G5 + G6) == Series(G1, G2, Parallel(G5, G6))
|
| 404 |
+
|
| 405 |
+
# division - See ``test_Feedback_functions()`` for division by Parallel objects.
|
| 406 |
+
assert G5/G6 == Series(G5, pow(G6, -1))
|
| 407 |
+
assert -G3/G4 == Series(-G3, pow(G4, -1))
|
| 408 |
+
assert (G5*G6)/G7 == Series(G5, G6, pow(G7, -1))
|
| 409 |
+
|
| 410 |
+
c = symbols("c", commutative=False)
|
| 411 |
+
raises(ValueError, lambda: G3 * Matrix([1, 2, 3]))
|
| 412 |
+
raises(ValueError, lambda: G1 * c)
|
| 413 |
+
raises(ValueError, lambda: G3 * G5)
|
| 414 |
+
raises(ValueError, lambda: G5 * (s - 1))
|
| 415 |
+
raises(ValueError, lambda: 9 * G5)
|
| 416 |
+
|
| 417 |
+
raises(ValueError, lambda: G3 / Matrix([1, 2, 3]))
|
| 418 |
+
raises(ValueError, lambda: G6 / 0)
|
| 419 |
+
raises(ValueError, lambda: G3 / G5)
|
| 420 |
+
raises(ValueError, lambda: G5 / 2)
|
| 421 |
+
raises(ValueError, lambda: G5 / s**2)
|
| 422 |
+
raises(ValueError, lambda: (s - 4*s**2) / G2)
|
| 423 |
+
raises(ValueError, lambda: 0 / G4)
|
| 424 |
+
raises(ValueError, lambda: G7 / (1 + G6))
|
| 425 |
+
raises(ValueError, lambda: G7 / (G5 * G6))
|
| 426 |
+
raises(ValueError, lambda: G7 / (G7 + (G5 + G6)))
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def test_TransferFunction_is_proper():
|
| 430 |
+
omega_o, zeta, tau = symbols('omega_o, zeta, tau')
|
| 431 |
+
G1 = TransferFunction(omega_o**2, s**2 + p*omega_o*zeta*s + omega_o**2, omega_o)
|
| 432 |
+
G2 = TransferFunction(tau - s**3, tau + p**4, tau)
|
| 433 |
+
G3 = TransferFunction(a*b*s**3 + s**2 - a*p + s, b - s*p**2, p)
|
| 434 |
+
G4 = TransferFunction(b*s**2 + p**2 - a*p + s, b - p**2, s)
|
| 435 |
+
assert G1.is_proper
|
| 436 |
+
assert G2.is_proper
|
| 437 |
+
assert G3.is_proper
|
| 438 |
+
assert not G4.is_proper
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
def test_TransferFunction_is_strictly_proper():
|
| 442 |
+
omega_o, zeta, tau = symbols('omega_o, zeta, tau')
|
| 443 |
+
tf1 = TransferFunction(omega_o**2, s**2 + p*omega_o*zeta*s + omega_o**2, omega_o)
|
| 444 |
+
tf2 = TransferFunction(tau - s**3, tau + p**4, tau)
|
| 445 |
+
tf3 = TransferFunction(a*b*s**3 + s**2 - a*p + s, b - s*p**2, p)
|
| 446 |
+
tf4 = TransferFunction(b*s**2 + p**2 - a*p + s, b - p**2, s)
|
| 447 |
+
assert not tf1.is_strictly_proper
|
| 448 |
+
assert not tf2.is_strictly_proper
|
| 449 |
+
assert tf3.is_strictly_proper
|
| 450 |
+
assert not tf4.is_strictly_proper
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
def test_TransferFunction_is_biproper():
|
| 454 |
+
tau, omega_o, zeta = symbols('tau, omega_o, zeta')
|
| 455 |
+
tf1 = TransferFunction(omega_o**2, s**2 + p*omega_o*zeta*s + omega_o**2, omega_o)
|
| 456 |
+
tf2 = TransferFunction(tau - s**3, tau + p**4, tau)
|
| 457 |
+
tf3 = TransferFunction(a*b*s**3 + s**2 - a*p + s, b - s*p**2, p)
|
| 458 |
+
tf4 = TransferFunction(b*s**2 + p**2 - a*p + s, b - p**2, s)
|
| 459 |
+
assert tf1.is_biproper
|
| 460 |
+
assert tf2.is_biproper
|
| 461 |
+
assert not tf3.is_biproper
|
| 462 |
+
assert not tf4.is_biproper
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
def test_Series_construction():
|
| 466 |
+
tf = TransferFunction(a0*s**3 + a1*s**2 - a2*s, b0*p**4 + b1*p**3 - b2*s*p, s)
|
| 467 |
+
tf2 = TransferFunction(a2*p - s, a2*s + p, s)
|
| 468 |
+
tf3 = TransferFunction(a0*p + p**a1 - s, p, p)
|
| 469 |
+
tf4 = TransferFunction(1, s**2 + 2*zeta*wn*s + wn**2, s)
|
| 470 |
+
inp = Function('X_d')(s)
|
| 471 |
+
out = Function('X')(s)
|
| 472 |
+
|
| 473 |
+
s0 = Series(tf, tf2)
|
| 474 |
+
assert s0.args == (tf, tf2)
|
| 475 |
+
assert s0.var == s
|
| 476 |
+
|
| 477 |
+
s1 = Series(Parallel(tf, -tf2), tf2)
|
| 478 |
+
assert s1.args == (Parallel(tf, -tf2), tf2)
|
| 479 |
+
assert s1.var == s
|
| 480 |
+
|
| 481 |
+
tf3_ = TransferFunction(inp, 1, s)
|
| 482 |
+
tf4_ = TransferFunction(-out, 1, s)
|
| 483 |
+
s2 = Series(tf, Parallel(tf3_, tf4_), tf2)
|
| 484 |
+
assert s2.args == (tf, Parallel(tf3_, tf4_), tf2)
|
| 485 |
+
|
| 486 |
+
s3 = Series(tf, tf2, tf4)
|
| 487 |
+
assert s3.args == (tf, tf2, tf4)
|
| 488 |
+
|
| 489 |
+
s4 = Series(tf3_, tf4_)
|
| 490 |
+
assert s4.args == (tf3_, tf4_)
|
| 491 |
+
assert s4.var == s
|
| 492 |
+
|
| 493 |
+
s6 = Series(tf2, tf4, Parallel(tf2, -tf), tf4)
|
| 494 |
+
assert s6.args == (tf2, tf4, Parallel(tf2, -tf), tf4)
|
| 495 |
+
|
| 496 |
+
s7 = Series(tf, tf2)
|
| 497 |
+
assert s0 == s7
|
| 498 |
+
assert not s0 == s2
|
| 499 |
+
|
| 500 |
+
raises(ValueError, lambda: Series(tf, tf3))
|
| 501 |
+
raises(ValueError, lambda: Series(tf, tf2, tf3, tf4))
|
| 502 |
+
raises(ValueError, lambda: Series(-tf3, tf2))
|
| 503 |
+
raises(TypeError, lambda: Series(2, tf, tf4))
|
| 504 |
+
raises(TypeError, lambda: Series(s**2 + p*s, tf3, tf2))
|
| 505 |
+
raises(TypeError, lambda: Series(tf3, Matrix([1, 2, 3, 4])))
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
def test_MIMOSeries_construction():
|
| 509 |
+
tf_1 = TransferFunction(a0*s**3 + a1*s**2 - a2*s, b0*p**4 + b1*p**3 - b2*s*p, s)
|
| 510 |
+
tf_2 = TransferFunction(a2*p - s, a2*s + p, s)
|
| 511 |
+
tf_3 = TransferFunction(1, s**2 + 2*zeta*wn*s + wn**2, s)
|
| 512 |
+
|
| 513 |
+
tfm_1 = TransferFunctionMatrix([[tf_1, tf_2, tf_3], [-tf_3, -tf_2, tf_1]])
|
| 514 |
+
tfm_2 = TransferFunctionMatrix([[-tf_2], [-tf_2], [-tf_3]])
|
| 515 |
+
tfm_3 = TransferFunctionMatrix([[-tf_3]])
|
| 516 |
+
tfm_4 = TransferFunctionMatrix([[TF3], [TF2], [-TF1]])
|
| 517 |
+
tfm_5 = TransferFunctionMatrix.from_Matrix(Matrix([1/p]), p)
|
| 518 |
+
|
| 519 |
+
s8 = MIMOSeries(tfm_2, tfm_1)
|
| 520 |
+
assert s8.args == (tfm_2, tfm_1)
|
| 521 |
+
assert s8.var == s
|
| 522 |
+
assert s8.shape == (s8.num_outputs, s8.num_inputs) == (2, 1)
|
| 523 |
+
|
| 524 |
+
s9 = MIMOSeries(tfm_3, tfm_2, tfm_1)
|
| 525 |
+
assert s9.args == (tfm_3, tfm_2, tfm_1)
|
| 526 |
+
assert s9.var == s
|
| 527 |
+
assert s9.shape == (s9.num_outputs, s9.num_inputs) == (2, 1)
|
| 528 |
+
|
| 529 |
+
s11 = MIMOSeries(tfm_3, MIMOParallel(-tfm_2, -tfm_4), tfm_1)
|
| 530 |
+
assert s11.args == (tfm_3, MIMOParallel(-tfm_2, -tfm_4), tfm_1)
|
| 531 |
+
assert s11.shape == (s11.num_outputs, s11.num_inputs) == (2, 1)
|
| 532 |
+
|
| 533 |
+
# arg cannot be empty tuple.
|
| 534 |
+
raises(ValueError, lambda: MIMOSeries())
|
| 535 |
+
|
| 536 |
+
# arg cannot contain SISO as well as MIMO systems.
|
| 537 |
+
raises(TypeError, lambda: MIMOSeries(tfm_1, tf_1))
|
| 538 |
+
|
| 539 |
+
# for all the adjacent transfer function matrices:
|
| 540 |
+
# no. of inputs of first TFM must be equal to the no. of outputs of the second TFM.
|
| 541 |
+
raises(ValueError, lambda: MIMOSeries(tfm_1, tfm_2, -tfm_1))
|
| 542 |
+
|
| 543 |
+
# all the TFMs must use the same complex variable.
|
| 544 |
+
raises(ValueError, lambda: MIMOSeries(tfm_3, tfm_5))
|
| 545 |
+
|
| 546 |
+
# Number or expression not allowed in the arguments.
|
| 547 |
+
raises(TypeError, lambda: MIMOSeries(2, tfm_2, tfm_3))
|
| 548 |
+
raises(TypeError, lambda: MIMOSeries(s**2 + p*s, -tfm_2, tfm_3))
|
| 549 |
+
raises(TypeError, lambda: MIMOSeries(Matrix([1/p]), tfm_3))
|
| 550 |
+
|
| 551 |
+
|
| 552 |
+
def test_Series_functions():
|
| 553 |
+
tf1 = TransferFunction(1, s**2 + 2*zeta*wn*s + wn**2, s)
|
| 554 |
+
tf2 = TransferFunction(k, 1, s)
|
| 555 |
+
tf3 = TransferFunction(a2*p - s, a2*s + p, s)
|
| 556 |
+
tf4 = TransferFunction(a0*p + p**a1 - s, p, p)
|
| 557 |
+
tf5 = TransferFunction(a1*s**2 + a2*s - a0, s + a0, s)
|
| 558 |
+
|
| 559 |
+
assert tf1*tf2*tf3 == Series(tf1, tf2, tf3) == Series(Series(tf1, tf2), tf3) \
|
| 560 |
+
== Series(tf1, Series(tf2, tf3))
|
| 561 |
+
assert tf1*(tf2 + tf3) == Series(tf1, Parallel(tf2, tf3))
|
| 562 |
+
assert tf1*tf2 + tf5 == Parallel(Series(tf1, tf2), tf5)
|
| 563 |
+
assert tf1*tf2 - tf5 == Parallel(Series(tf1, tf2), -tf5)
|
| 564 |
+
assert tf1*tf2 + tf3 + tf5 == Parallel(Series(tf1, tf2), tf3, tf5)
|
| 565 |
+
assert tf1*tf2 - tf3 - tf5 == Parallel(Series(tf1, tf2), -tf3, -tf5)
|
| 566 |
+
assert tf1*tf2 - tf3 + tf5 == Parallel(Series(tf1, tf2), -tf3, tf5)
|
| 567 |
+
assert tf1*tf2 + tf3*tf5 == Parallel(Series(tf1, tf2), Series(tf3, tf5))
|
| 568 |
+
assert tf1*tf2 - tf3*tf5 == Parallel(Series(tf1, tf2), Series(TransferFunction(-1, 1, s), Series(tf3, tf5)))
|
| 569 |
+
assert tf2*tf3*(tf2 - tf1)*tf3 == Series(tf2, tf3, Parallel(tf2, -tf1), tf3)
|
| 570 |
+
assert -tf1*tf2 == Series(-tf1, tf2)
|
| 571 |
+
assert -(tf1*tf2) == Series(TransferFunction(-1, 1, s), Series(tf1, tf2))
|
| 572 |
+
raises(ValueError, lambda: tf1*tf2*tf4)
|
| 573 |
+
raises(ValueError, lambda: tf1*(tf2 - tf4))
|
| 574 |
+
raises(ValueError, lambda: tf3*Matrix([1, 2, 3]))
|
| 575 |
+
|
| 576 |
+
# evaluate=True -> doit()
|
| 577 |
+
assert Series(tf1, tf2, evaluate=True) == Series(tf1, tf2).doit() == \
|
| 578 |
+
TransferFunction(k, s**2 + 2*s*wn*zeta + wn**2, s)
|
| 579 |
+
assert Series(tf1, tf2, Parallel(tf1, -tf3), evaluate=True) == Series(tf1, tf2, Parallel(tf1, -tf3)).doit() == \
|
| 580 |
+
TransferFunction(k*(a2*s + p + (-a2*p + s)*(s**2 + 2*s*wn*zeta + wn**2)), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2)**2, s)
|
| 581 |
+
assert Series(tf2, tf1, -tf3, evaluate=True) == Series(tf2, tf1, -tf3).doit() == \
|
| 582 |
+
TransferFunction(k*(-a2*p + s), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s)
|
| 583 |
+
assert not Series(tf1, -tf2, evaluate=False) == Series(tf1, -tf2).doit()
|
| 584 |
+
|
| 585 |
+
assert Series(Parallel(tf1, tf2), Parallel(tf2, -tf3)).doit() == \
|
| 586 |
+
TransferFunction((k*(s**2 + 2*s*wn*zeta + wn**2) + 1)*(-a2*p + k*(a2*s + p) + s), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s)
|
| 587 |
+
assert Series(-tf1, -tf2, -tf3).doit() == \
|
| 588 |
+
TransferFunction(k*(-a2*p + s), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s)
|
| 589 |
+
assert -Series(tf1, tf2, tf3).doit() == \
|
| 590 |
+
TransferFunction(-k*(a2*p - s), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s)
|
| 591 |
+
assert Series(tf2, tf3, Parallel(tf2, -tf1), tf3).doit() == \
|
| 592 |
+
TransferFunction(k*(a2*p - s)**2*(k*(s**2 + 2*s*wn*zeta + wn**2) - 1), (a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2), s)
|
| 593 |
+
|
| 594 |
+
assert Series(tf1, tf2).rewrite(TransferFunction) == TransferFunction(k, s**2 + 2*s*wn*zeta + wn**2, s)
|
| 595 |
+
assert Series(tf2, tf1, -tf3).rewrite(TransferFunction) == \
|
| 596 |
+
TransferFunction(k*(-a2*p + s), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s)
|
| 597 |
+
|
| 598 |
+
S1 = Series(Parallel(tf1, tf2), Parallel(tf2, -tf3))
|
| 599 |
+
assert S1.is_proper
|
| 600 |
+
assert not S1.is_strictly_proper
|
| 601 |
+
assert S1.is_biproper
|
| 602 |
+
|
| 603 |
+
S2 = Series(tf1, tf2, tf3)
|
| 604 |
+
assert S2.is_proper
|
| 605 |
+
assert S2.is_strictly_proper
|
| 606 |
+
assert not S2.is_biproper
|
| 607 |
+
|
| 608 |
+
S3 = Series(tf1, -tf2, Parallel(tf1, -tf3))
|
| 609 |
+
assert S3.is_proper
|
| 610 |
+
assert S3.is_strictly_proper
|
| 611 |
+
assert not S3.is_biproper
|
| 612 |
+
|
| 613 |
+
|
| 614 |
+
def test_MIMOSeries_functions():
|
| 615 |
+
tfm1 = TransferFunctionMatrix([[TF1, TF2, TF3], [-TF3, -TF2, TF1]])
|
| 616 |
+
tfm2 = TransferFunctionMatrix([[-TF1], [-TF2], [-TF3]])
|
| 617 |
+
tfm3 = TransferFunctionMatrix([[-TF1]])
|
| 618 |
+
tfm4 = TransferFunctionMatrix([[-TF2, -TF3], [-TF1, TF2]])
|
| 619 |
+
tfm5 = TransferFunctionMatrix([[TF2, -TF2], [-TF3, -TF2]])
|
| 620 |
+
tfm6 = TransferFunctionMatrix([[-TF3], [TF1]])
|
| 621 |
+
tfm7 = TransferFunctionMatrix([[TF1], [-TF2]])
|
| 622 |
+
|
| 623 |
+
assert tfm1*tfm2 + tfm6 == MIMOParallel(MIMOSeries(tfm2, tfm1), tfm6)
|
| 624 |
+
assert tfm1*tfm2 + tfm7 + tfm6 == MIMOParallel(MIMOSeries(tfm2, tfm1), tfm7, tfm6)
|
| 625 |
+
assert tfm1*tfm2 - tfm6 - tfm7 == MIMOParallel(MIMOSeries(tfm2, tfm1), -tfm6, -tfm7)
|
| 626 |
+
assert tfm4*tfm5 + (tfm4 - tfm5) == MIMOParallel(MIMOSeries(tfm5, tfm4), tfm4, -tfm5)
|
| 627 |
+
assert tfm4*-tfm6 + (-tfm4*tfm6) == MIMOParallel(MIMOSeries(-tfm6, tfm4), MIMOSeries(tfm6, -tfm4))
|
| 628 |
+
|
| 629 |
+
raises(ValueError, lambda: tfm1*tfm2 + TF1)
|
| 630 |
+
raises(TypeError, lambda: tfm1*tfm2 + a0)
|
| 631 |
+
raises(TypeError, lambda: tfm4*tfm6 - (s - 1))
|
| 632 |
+
raises(TypeError, lambda: tfm4*-tfm6 - 8)
|
| 633 |
+
raises(TypeError, lambda: (-1 + p**5) + tfm1*tfm2)
|
| 634 |
+
|
| 635 |
+
# Shape criteria.
|
| 636 |
+
|
| 637 |
+
raises(TypeError, lambda: -tfm1*tfm2 + tfm4)
|
| 638 |
+
raises(TypeError, lambda: tfm1*tfm2 - tfm4 + tfm5)
|
| 639 |
+
raises(TypeError, lambda: tfm1*tfm2 - tfm4*tfm5)
|
| 640 |
+
|
| 641 |
+
assert tfm1*tfm2*-tfm3 == MIMOSeries(-tfm3, tfm2, tfm1)
|
| 642 |
+
assert (tfm1*-tfm2)*tfm3 == MIMOSeries(tfm3, -tfm2, tfm1)
|
| 643 |
+
|
| 644 |
+
# Multiplication of a Series object with a SISO TF not allowed.
|
| 645 |
+
|
| 646 |
+
raises(ValueError, lambda: tfm4*tfm5*TF1)
|
| 647 |
+
raises(TypeError, lambda: tfm4*tfm5*a1)
|
| 648 |
+
raises(TypeError, lambda: tfm4*-tfm5*(s - 2))
|
| 649 |
+
raises(TypeError, lambda: tfm5*tfm4*9)
|
| 650 |
+
raises(TypeError, lambda: (-p**3 + 1)*tfm5*tfm4)
|
| 651 |
+
|
| 652 |
+
# Transfer function matrix in the arguments.
|
| 653 |
+
assert (MIMOSeries(tfm2, tfm1, evaluate=True) == MIMOSeries(tfm2, tfm1).doit()
|
| 654 |
+
== TransferFunctionMatrix(((TransferFunction(-k**2*(a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2)**2 + (-a2*p + s)*(a2*p - s)*(s**2 + 2*s*wn*zeta + wn**2)**2 - (a2*s + p)**2,
|
| 655 |
+
(a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2)**2, s),),
|
| 656 |
+
(TransferFunction(k**2*(a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2)**2 + (-a2*p + s)*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2) + (a2*p - s)*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2),
|
| 657 |
+
(a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2)**2, s),))))
|
| 658 |
+
|
| 659 |
+
# doit() should not cancel poles and zeros.
|
| 660 |
+
mat_1 = Matrix([[1/(1+s), (1+s)/(1+s**2+2*s)**3]])
|
| 661 |
+
mat_2 = Matrix([[(1+s)], [(1+s**2+2*s)**3/(1+s)]])
|
| 662 |
+
tm_1, tm_2 = TransferFunctionMatrix.from_Matrix(mat_1, s), TransferFunctionMatrix.from_Matrix(mat_2, s)
|
| 663 |
+
assert (MIMOSeries(tm_2, tm_1).doit()
|
| 664 |
+
== TransferFunctionMatrix(((TransferFunction(2*(s + 1)**2*(s**2 + 2*s + 1)**3, (s + 1)**2*(s**2 + 2*s + 1)**3, s),),)))
|
| 665 |
+
assert MIMOSeries(tm_2, tm_1).doit().simplify() == TransferFunctionMatrix(((TransferFunction(2, 1, s),),))
|
| 666 |
+
|
| 667 |
+
# calling doit() will expand the internal Series and Parallel objects.
|
| 668 |
+
assert (MIMOSeries(-tfm3, -tfm2, tfm1, evaluate=True)
|
| 669 |
+
== MIMOSeries(-tfm3, -tfm2, tfm1).doit()
|
| 670 |
+
== TransferFunctionMatrix(((TransferFunction(k**2*(a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2)**2 + (a2*p - s)**2*(s**2 + 2*s*wn*zeta + wn**2)**2 + (a2*s + p)**2,
|
| 671 |
+
(a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2)**3, s),),
|
| 672 |
+
(TransferFunction(-k**2*(a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2)**2 + (-a2*p + s)*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2) + (a2*p - s)*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2),
|
| 673 |
+
(a2*s + p)**2*(s**2 + 2*s*wn*zeta + wn**2)**3, s),))))
|
| 674 |
+
assert (MIMOSeries(MIMOParallel(tfm4, tfm5), tfm5, evaluate=True)
|
| 675 |
+
== MIMOSeries(MIMOParallel(tfm4, tfm5), tfm5).doit()
|
| 676 |
+
== TransferFunctionMatrix(((TransferFunction(-k*(-a2*s - p + (-a2*p + s)*(s**2 + 2*s*wn*zeta + wn**2)), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s), TransferFunction(k*(-a2*p - \
|
| 677 |
+
k*(a2*s + p) + s), a2*s + p, s)), (TransferFunction(-k*(-a2*s - p + (-a2*p + s)*(s**2 + 2*s*wn*zeta + wn**2)), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s), \
|
| 678 |
+
TransferFunction((-a2*p + s)*(-a2*p - k*(a2*s + p) + s), (a2*s + p)**2, s)))) == MIMOSeries(MIMOParallel(tfm4, tfm5), tfm5).rewrite(TransferFunctionMatrix))
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
def test_Parallel_construction():
|
| 682 |
+
tf = TransferFunction(a0*s**3 + a1*s**2 - a2*s, b0*p**4 + b1*p**3 - b2*s*p, s)
|
| 683 |
+
tf2 = TransferFunction(a2*p - s, a2*s + p, s)
|
| 684 |
+
tf3 = TransferFunction(a0*p + p**a1 - s, p, p)
|
| 685 |
+
tf4 = TransferFunction(1, s**2 + 2*zeta*wn*s + wn**2, s)
|
| 686 |
+
inp = Function('X_d')(s)
|
| 687 |
+
out = Function('X')(s)
|
| 688 |
+
|
| 689 |
+
p0 = Parallel(tf, tf2)
|
| 690 |
+
assert p0.args == (tf, tf2)
|
| 691 |
+
assert p0.var == s
|
| 692 |
+
|
| 693 |
+
p1 = Parallel(Series(tf, -tf2), tf2)
|
| 694 |
+
assert p1.args == (Series(tf, -tf2), tf2)
|
| 695 |
+
assert p1.var == s
|
| 696 |
+
|
| 697 |
+
tf3_ = TransferFunction(inp, 1, s)
|
| 698 |
+
tf4_ = TransferFunction(-out, 1, s)
|
| 699 |
+
p2 = Parallel(tf, Series(tf3_, -tf4_), tf2)
|
| 700 |
+
assert p2.args == (tf, Series(tf3_, -tf4_), tf2)
|
| 701 |
+
|
| 702 |
+
p3 = Parallel(tf, tf2, tf4)
|
| 703 |
+
assert p3.args == (tf, tf2, tf4)
|
| 704 |
+
|
| 705 |
+
p4 = Parallel(tf3_, tf4_)
|
| 706 |
+
assert p4.args == (tf3_, tf4_)
|
| 707 |
+
assert p4.var == s
|
| 708 |
+
|
| 709 |
+
p5 = Parallel(tf, tf2)
|
| 710 |
+
assert p0 == p5
|
| 711 |
+
assert not p0 == p1
|
| 712 |
+
|
| 713 |
+
p6 = Parallel(tf2, tf4, Series(tf2, -tf4))
|
| 714 |
+
assert p6.args == (tf2, tf4, Series(tf2, -tf4))
|
| 715 |
+
|
| 716 |
+
p7 = Parallel(tf2, tf4, Series(tf2, -tf), tf4)
|
| 717 |
+
assert p7.args == (tf2, tf4, Series(tf2, -tf), tf4)
|
| 718 |
+
|
| 719 |
+
raises(ValueError, lambda: Parallel(tf, tf3))
|
| 720 |
+
raises(ValueError, lambda: Parallel(tf, tf2, tf3, tf4))
|
| 721 |
+
raises(ValueError, lambda: Parallel(-tf3, tf4))
|
| 722 |
+
raises(TypeError, lambda: Parallel(2, tf, tf4))
|
| 723 |
+
raises(TypeError, lambda: Parallel(s**2 + p*s, tf3, tf2))
|
| 724 |
+
raises(TypeError, lambda: Parallel(tf3, Matrix([1, 2, 3, 4])))
|
| 725 |
+
|
| 726 |
+
|
| 727 |
+
def test_MIMOParallel_construction():
|
| 728 |
+
tfm1 = TransferFunctionMatrix([[TF1], [TF2], [TF3]])
|
| 729 |
+
tfm2 = TransferFunctionMatrix([[-TF3], [TF2], [TF1]])
|
| 730 |
+
tfm3 = TransferFunctionMatrix([[TF1]])
|
| 731 |
+
tfm4 = TransferFunctionMatrix([[TF2], [TF1], [TF3]])
|
| 732 |
+
tfm5 = TransferFunctionMatrix([[TF1, TF2], [TF2, TF1]])
|
| 733 |
+
tfm6 = TransferFunctionMatrix([[TF2, TF1], [TF1, TF2]])
|
| 734 |
+
tfm7 = TransferFunctionMatrix.from_Matrix(Matrix([[1/p]]), p)
|
| 735 |
+
|
| 736 |
+
p8 = MIMOParallel(tfm1, tfm2)
|
| 737 |
+
assert p8.args == (tfm1, tfm2)
|
| 738 |
+
assert p8.var == s
|
| 739 |
+
assert p8.shape == (p8.num_outputs, p8.num_inputs) == (3, 1)
|
| 740 |
+
|
| 741 |
+
p9 = MIMOParallel(MIMOSeries(tfm3, tfm1), tfm2)
|
| 742 |
+
assert p9.args == (MIMOSeries(tfm3, tfm1), tfm2)
|
| 743 |
+
assert p9.var == s
|
| 744 |
+
assert p9.shape == (p9.num_outputs, p9.num_inputs) == (3, 1)
|
| 745 |
+
|
| 746 |
+
p10 = MIMOParallel(tfm1, MIMOSeries(tfm3, tfm4), tfm2)
|
| 747 |
+
assert p10.args == (tfm1, MIMOSeries(tfm3, tfm4), tfm2)
|
| 748 |
+
assert p10.var == s
|
| 749 |
+
assert p10.shape == (p10.num_outputs, p10.num_inputs) == (3, 1)
|
| 750 |
+
|
| 751 |
+
p11 = MIMOParallel(tfm2, tfm1, tfm4)
|
| 752 |
+
assert p11.args == (tfm2, tfm1, tfm4)
|
| 753 |
+
assert p11.shape == (p11.num_outputs, p11.num_inputs) == (3, 1)
|
| 754 |
+
|
| 755 |
+
p12 = MIMOParallel(tfm6, tfm5)
|
| 756 |
+
assert p12.args == (tfm6, tfm5)
|
| 757 |
+
assert p12.shape == (p12.num_outputs, p12.num_inputs) == (2, 2)
|
| 758 |
+
|
| 759 |
+
p13 = MIMOParallel(tfm2, tfm4, MIMOSeries(-tfm3, tfm4), -tfm4)
|
| 760 |
+
assert p13.args == (tfm2, tfm4, MIMOSeries(-tfm3, tfm4), -tfm4)
|
| 761 |
+
assert p13.shape == (p13.num_outputs, p13.num_inputs) == (3, 1)
|
| 762 |
+
|
| 763 |
+
# arg cannot be empty tuple.
|
| 764 |
+
raises(TypeError, lambda: MIMOParallel(()))
|
| 765 |
+
|
| 766 |
+
# arg cannot contain SISO as well as MIMO systems.
|
| 767 |
+
raises(TypeError, lambda: MIMOParallel(tfm1, tfm2, TF1))
|
| 768 |
+
|
| 769 |
+
# all TFMs must have same shapes.
|
| 770 |
+
raises(TypeError, lambda: MIMOParallel(tfm1, tfm3, tfm4))
|
| 771 |
+
|
| 772 |
+
# all TFMs must be using the same complex variable.
|
| 773 |
+
raises(ValueError, lambda: MIMOParallel(tfm3, tfm7))
|
| 774 |
+
|
| 775 |
+
# Number or expression not allowed in the arguments.
|
| 776 |
+
raises(TypeError, lambda: MIMOParallel(2, tfm1, tfm4))
|
| 777 |
+
raises(TypeError, lambda: MIMOParallel(s**2 + p*s, -tfm4, tfm2))
|
| 778 |
+
|
| 779 |
+
|
| 780 |
+
def test_Parallel_functions():
|
| 781 |
+
tf1 = TransferFunction(1, s**2 + 2*zeta*wn*s + wn**2, s)
|
| 782 |
+
tf2 = TransferFunction(k, 1, s)
|
| 783 |
+
tf3 = TransferFunction(a2*p - s, a2*s + p, s)
|
| 784 |
+
tf4 = TransferFunction(a0*p + p**a1 - s, p, p)
|
| 785 |
+
tf5 = TransferFunction(a1*s**2 + a2*s - a0, s + a0, s)
|
| 786 |
+
|
| 787 |
+
assert tf1 + tf2 + tf3 == Parallel(tf1, tf2, tf3)
|
| 788 |
+
assert tf1 + tf2 + tf3 + tf5 == Parallel(tf1, tf2, tf3, tf5)
|
| 789 |
+
assert tf1 + tf2 - tf3 - tf5 == Parallel(tf1, tf2, -tf3, -tf5)
|
| 790 |
+
assert tf1 + tf2*tf3 == Parallel(tf1, Series(tf2, tf3))
|
| 791 |
+
assert tf1 - tf2*tf3 == Parallel(tf1, -Series(tf2,tf3))
|
| 792 |
+
assert -tf1 - tf2 == Parallel(-tf1, -tf2)
|
| 793 |
+
assert -(tf1 + tf2) == Series(TransferFunction(-1, 1, s), Parallel(tf1, tf2))
|
| 794 |
+
assert (tf2 + tf3)*tf1 == Series(Parallel(tf2, tf3), tf1)
|
| 795 |
+
assert (tf1 + tf2)*(tf3*tf5) == Series(Parallel(tf1, tf2), tf3, tf5)
|
| 796 |
+
assert -(tf2 + tf3)*-tf5 == Series(TransferFunction(-1, 1, s), Parallel(tf2, tf3), -tf5)
|
| 797 |
+
assert tf2 + tf3 + tf2*tf1 + tf5 == Parallel(tf2, tf3, Series(tf2, tf1), tf5)
|
| 798 |
+
assert tf2 + tf3 + tf2*tf1 - tf3 == Parallel(tf2, tf3, Series(tf2, tf1), -tf3)
|
| 799 |
+
assert (tf1 + tf2 + tf5)*(tf3 + tf5) == Series(Parallel(tf1, tf2, tf5), Parallel(tf3, tf5))
|
| 800 |
+
raises(ValueError, lambda: tf1 + tf2 + tf4)
|
| 801 |
+
raises(ValueError, lambda: tf1 - tf2*tf4)
|
| 802 |
+
raises(ValueError, lambda: tf3 + Matrix([1, 2, 3]))
|
| 803 |
+
|
| 804 |
+
# evaluate=True -> doit()
|
| 805 |
+
assert Parallel(tf1, tf2, evaluate=True) == Parallel(tf1, tf2).doit() == \
|
| 806 |
+
TransferFunction(k*(s**2 + 2*s*wn*zeta + wn**2) + 1, s**2 + 2*s*wn*zeta + wn**2, s)
|
| 807 |
+
assert Parallel(tf1, tf2, Series(-tf1, tf3), evaluate=True) == \
|
| 808 |
+
Parallel(tf1, tf2, Series(-tf1, tf3)).doit() == TransferFunction(k*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2)**2 + \
|
| 809 |
+
(-a2*p + s)*(s**2 + 2*s*wn*zeta + wn**2) + (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), (a2*s + p)*(s**2 + \
|
| 810 |
+
2*s*wn*zeta + wn**2)**2, s)
|
| 811 |
+
assert Parallel(tf2, tf1, -tf3, evaluate=True) == Parallel(tf2, tf1, -tf3).doit() == \
|
| 812 |
+
TransferFunction(a2*s + k*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2) + p + (-a2*p + s)*(s**2 + 2*s*wn*zeta + wn**2) \
|
| 813 |
+
, (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s)
|
| 814 |
+
assert not Parallel(tf1, -tf2, evaluate=False) == Parallel(tf1, -tf2).doit()
|
| 815 |
+
|
| 816 |
+
assert Parallel(Series(tf1, tf2), Series(tf2, tf3)).doit() == \
|
| 817 |
+
TransferFunction(k*(a2*p - s)*(s**2 + 2*s*wn*zeta + wn**2) + k*(a2*s + p), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s)
|
| 818 |
+
assert Parallel(-tf1, -tf2, -tf3).doit() == \
|
| 819 |
+
TransferFunction(-a2*s - k*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2) - p + (-a2*p + s)*(s**2 + 2*s*wn*zeta + wn**2), \
|
| 820 |
+
(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s)
|
| 821 |
+
assert -Parallel(tf1, tf2, tf3).doit() == \
|
| 822 |
+
TransferFunction(-a2*s - k*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2) - p - (a2*p - s)*(s**2 + 2*s*wn*zeta + wn**2), \
|
| 823 |
+
(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s)
|
| 824 |
+
assert Parallel(tf2, tf3, Series(tf2, -tf1), tf3).doit() == \
|
| 825 |
+
TransferFunction(k*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2) - k*(a2*s + p) + (2*a2*p - 2*s)*(s**2 + 2*s*wn*zeta \
|
| 826 |
+
+ wn**2), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s)
|
| 827 |
+
|
| 828 |
+
assert Parallel(tf1, tf2).rewrite(TransferFunction) == \
|
| 829 |
+
TransferFunction(k*(s**2 + 2*s*wn*zeta + wn**2) + 1, s**2 + 2*s*wn*zeta + wn**2, s)
|
| 830 |
+
assert Parallel(tf2, tf1, -tf3).rewrite(TransferFunction) == \
|
| 831 |
+
TransferFunction(a2*s + k*(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2) + p + (-a2*p + s)*(s**2 + 2*s*wn*zeta + \
|
| 832 |
+
wn**2), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s)
|
| 833 |
+
|
| 834 |
+
assert Parallel(tf1, Parallel(tf2, tf3)) == Parallel(tf1, tf2, tf3) == Parallel(Parallel(tf1, tf2), tf3)
|
| 835 |
+
|
| 836 |
+
P1 = Parallel(Series(tf1, tf2), Series(tf2, tf3))
|
| 837 |
+
assert P1.is_proper
|
| 838 |
+
assert not P1.is_strictly_proper
|
| 839 |
+
assert P1.is_biproper
|
| 840 |
+
|
| 841 |
+
P2 = Parallel(tf1, -tf2, -tf3)
|
| 842 |
+
assert P2.is_proper
|
| 843 |
+
assert not P2.is_strictly_proper
|
| 844 |
+
assert P2.is_biproper
|
| 845 |
+
|
| 846 |
+
P3 = Parallel(tf1, -tf2, Series(tf1, tf3))
|
| 847 |
+
assert P3.is_proper
|
| 848 |
+
assert not P3.is_strictly_proper
|
| 849 |
+
assert P3.is_biproper
|
| 850 |
+
|
| 851 |
+
|
| 852 |
+
def test_MIMOParallel_functions():
|
| 853 |
+
tf4 = TransferFunction(a0*p + p**a1 - s, p, p)
|
| 854 |
+
tf5 = TransferFunction(a1*s**2 + a2*s - a0, s + a0, s)
|
| 855 |
+
|
| 856 |
+
tfm1 = TransferFunctionMatrix([[TF1], [TF2], [TF3]])
|
| 857 |
+
tfm2 = TransferFunctionMatrix([[-TF2], [tf5], [-TF1]])
|
| 858 |
+
tfm3 = TransferFunctionMatrix([[tf5], [-tf5], [TF2]])
|
| 859 |
+
tfm4 = TransferFunctionMatrix([[TF2, -tf5], [TF1, tf5]])
|
| 860 |
+
tfm5 = TransferFunctionMatrix([[TF1, TF2], [TF3, -tf5]])
|
| 861 |
+
tfm6 = TransferFunctionMatrix([[-TF2]])
|
| 862 |
+
tfm7 = TransferFunctionMatrix([[tf4], [-tf4], [tf4]])
|
| 863 |
+
|
| 864 |
+
assert tfm1 + tfm2 + tfm3 == MIMOParallel(tfm1, tfm2, tfm3) == MIMOParallel(MIMOParallel(tfm1, tfm2), tfm3)
|
| 865 |
+
assert tfm2 - tfm1 - tfm3 == MIMOParallel(tfm2, -tfm1, -tfm3)
|
| 866 |
+
assert tfm2 - tfm3 + (-tfm1*tfm6*-tfm6) == MIMOParallel(tfm2, -tfm3, MIMOSeries(-tfm6, tfm6, -tfm1))
|
| 867 |
+
assert tfm1 + tfm1 - (-tfm1*tfm6) == MIMOParallel(tfm1, tfm1, -MIMOSeries(tfm6, -tfm1))
|
| 868 |
+
assert tfm2 - tfm3 - tfm1 + tfm2 == MIMOParallel(tfm2, -tfm3, -tfm1, tfm2)
|
| 869 |
+
assert tfm1 + tfm2 - tfm3 - tfm1 == MIMOParallel(tfm1, tfm2, -tfm3, -tfm1)
|
| 870 |
+
raises(ValueError, lambda: tfm1 + tfm2 + TF2)
|
| 871 |
+
raises(TypeError, lambda: tfm1 - tfm2 - a1)
|
| 872 |
+
raises(TypeError, lambda: tfm2 - tfm3 - (s - 1))
|
| 873 |
+
raises(TypeError, lambda: -tfm3 - tfm2 - 9)
|
| 874 |
+
raises(TypeError, lambda: (1 - p**3) - tfm3 - tfm2)
|
| 875 |
+
# All TFMs must use the same complex var. tfm7 uses 'p'.
|
| 876 |
+
raises(ValueError, lambda: tfm3 - tfm2 - tfm7)
|
| 877 |
+
raises(ValueError, lambda: tfm2 - tfm1 + tfm7)
|
| 878 |
+
# (tfm1 +/- tfm2) has (3, 1) shape while tfm4 has (2, 2) shape.
|
| 879 |
+
raises(TypeError, lambda: tfm1 + tfm2 + tfm4)
|
| 880 |
+
raises(TypeError, lambda: (tfm1 - tfm2) - tfm4)
|
| 881 |
+
|
| 882 |
+
assert (tfm1 + tfm2)*tfm6 == MIMOSeries(tfm6, MIMOParallel(tfm1, tfm2))
|
| 883 |
+
assert (tfm2 - tfm3)*tfm6*-tfm6 == MIMOSeries(-tfm6, tfm6, MIMOParallel(tfm2, -tfm3))
|
| 884 |
+
assert (tfm2 - tfm1 - tfm3)*(tfm6 + tfm6) == MIMOSeries(MIMOParallel(tfm6, tfm6), MIMOParallel(tfm2, -tfm1, -tfm3))
|
| 885 |
+
raises(ValueError, lambda: (tfm4 + tfm5)*TF1)
|
| 886 |
+
raises(TypeError, lambda: (tfm2 - tfm3)*a2)
|
| 887 |
+
raises(TypeError, lambda: (tfm3 + tfm2)*(s - 6))
|
| 888 |
+
raises(TypeError, lambda: (tfm1 + tfm2 + tfm3)*0)
|
| 889 |
+
raises(TypeError, lambda: (1 - p**3)*(tfm1 + tfm3))
|
| 890 |
+
|
| 891 |
+
# (tfm3 - tfm2) has (3, 1) shape while tfm4*tfm5 has (2, 2) shape.
|
| 892 |
+
raises(ValueError, lambda: (tfm3 - tfm2)*tfm4*tfm5)
|
| 893 |
+
# (tfm1 - tfm2) has (3, 1) shape while tfm5 has (2, 2) shape.
|
| 894 |
+
raises(ValueError, lambda: (tfm1 - tfm2)*tfm5)
|
| 895 |
+
|
| 896 |
+
# TFM in the arguments.
|
| 897 |
+
assert (MIMOParallel(tfm1, tfm2, evaluate=True) == MIMOParallel(tfm1, tfm2).doit()
|
| 898 |
+
== MIMOParallel(tfm1, tfm2).rewrite(TransferFunctionMatrix)
|
| 899 |
+
== TransferFunctionMatrix(((TransferFunction(-k*(s**2 + 2*s*wn*zeta + wn**2) + 1, s**2 + 2*s*wn*zeta + wn**2, s),), \
|
| 900 |
+
(TransferFunction(-a0 + a1*s**2 + a2*s + k*(a0 + s), a0 + s, s),), (TransferFunction(-a2*s - p + (a2*p - s)* \
|
| 901 |
+
(s**2 + 2*s*wn*zeta + wn**2), (a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s),))))
|
| 902 |
+
|
| 903 |
+
|
| 904 |
+
def test_Feedback_construction():
|
| 905 |
+
tf1 = TransferFunction(1, s**2 + 2*zeta*wn*s + wn**2, s)
|
| 906 |
+
tf2 = TransferFunction(k, 1, s)
|
| 907 |
+
tf3 = TransferFunction(a2*p - s, a2*s + p, s)
|
| 908 |
+
tf4 = TransferFunction(a0*p + p**a1 - s, p, p)
|
| 909 |
+
tf5 = TransferFunction(a1*s**2 + a2*s - a0, s + a0, s)
|
| 910 |
+
tf6 = TransferFunction(s - p, p + s, p)
|
| 911 |
+
|
| 912 |
+
f1 = Feedback(TransferFunction(1, 1, s), tf1*tf2*tf3)
|
| 913 |
+
assert f1.args == (TransferFunction(1, 1, s), Series(tf1, tf2, tf3), -1)
|
| 914 |
+
assert f1.sys1 == TransferFunction(1, 1, s)
|
| 915 |
+
assert f1.sys2 == Series(tf1, tf2, tf3)
|
| 916 |
+
assert f1.var == s
|
| 917 |
+
|
| 918 |
+
f2 = Feedback(tf1, tf2*tf3)
|
| 919 |
+
assert f2.args == (tf1, Series(tf2, tf3), -1)
|
| 920 |
+
assert f2.sys1 == tf1
|
| 921 |
+
assert f2.sys2 == Series(tf2, tf3)
|
| 922 |
+
assert f2.var == s
|
| 923 |
+
|
| 924 |
+
f3 = Feedback(tf1*tf2, tf5)
|
| 925 |
+
assert f3.args == (Series(tf1, tf2), tf5, -1)
|
| 926 |
+
assert f3.sys1 == Series(tf1, tf2)
|
| 927 |
+
|
| 928 |
+
f4 = Feedback(tf4, tf6)
|
| 929 |
+
assert f4.args == (tf4, tf6, -1)
|
| 930 |
+
assert f4.sys1 == tf4
|
| 931 |
+
assert f4.var == p
|
| 932 |
+
|
| 933 |
+
f5 = Feedback(tf5, TransferFunction(1, 1, s))
|
| 934 |
+
assert f5.args == (tf5, TransferFunction(1, 1, s), -1)
|
| 935 |
+
assert f5.var == s
|
| 936 |
+
assert f5 == Feedback(tf5) # When sys2 is not passed explicitly, it is assumed to be unit tf.
|
| 937 |
+
|
| 938 |
+
f6 = Feedback(TransferFunction(1, 1, p), tf4)
|
| 939 |
+
assert f6.args == (TransferFunction(1, 1, p), tf4, -1)
|
| 940 |
+
assert f6.var == p
|
| 941 |
+
|
| 942 |
+
f7 = -Feedback(tf4*tf6, TransferFunction(1, 1, p))
|
| 943 |
+
assert f7.args == (Series(TransferFunction(-1, 1, p), Series(tf4, tf6)), -TransferFunction(1, 1, p), -1)
|
| 944 |
+
assert f7.sys1 == Series(TransferFunction(-1, 1, p), Series(tf4, tf6))
|
| 945 |
+
|
| 946 |
+
# denominator can't be a Parallel instance
|
| 947 |
+
raises(TypeError, lambda: Feedback(tf1, tf2 + tf3))
|
| 948 |
+
raises(TypeError, lambda: Feedback(tf1, Matrix([1, 2, 3])))
|
| 949 |
+
raises(TypeError, lambda: Feedback(TransferFunction(1, 1, s), s - 1))
|
| 950 |
+
raises(TypeError, lambda: Feedback(1, 1))
|
| 951 |
+
# raises(ValueError, lambda: Feedback(TransferFunction(1, 1, s), TransferFunction(1, 1, s)))
|
| 952 |
+
raises(ValueError, lambda: Feedback(tf2, tf4*tf5))
|
| 953 |
+
raises(ValueError, lambda: Feedback(tf2, tf1, 1.5)) # `sign` can only be -1 or 1
|
| 954 |
+
raises(ValueError, lambda: Feedback(tf1, -tf1**-1)) # denominator can't be zero
|
| 955 |
+
raises(ValueError, lambda: Feedback(tf4, tf5)) # Both systems should use the same `var`
|
| 956 |
+
|
| 957 |
+
|
| 958 |
+
def test_Feedback_functions():
|
| 959 |
+
tf = TransferFunction(1, 1, s)
|
| 960 |
+
tf1 = TransferFunction(1, s**2 + 2*zeta*wn*s + wn**2, s)
|
| 961 |
+
tf2 = TransferFunction(k, 1, s)
|
| 962 |
+
tf3 = TransferFunction(a2*p - s, a2*s + p, s)
|
| 963 |
+
tf4 = TransferFunction(a0*p + p**a1 - s, p, p)
|
| 964 |
+
tf5 = TransferFunction(a1*s**2 + a2*s - a0, s + a0, s)
|
| 965 |
+
tf6 = TransferFunction(s - p, p + s, p)
|
| 966 |
+
|
| 967 |
+
assert (tf1*tf2*tf3 / tf3*tf5) == Series(tf1, tf2, tf3, pow(tf3, -1), tf5)
|
| 968 |
+
assert (tf1*tf2*tf3) / (tf3*tf5) == Series((tf1*tf2*tf3).doit(), pow((tf3*tf5).doit(),-1))
|
| 969 |
+
assert tf / (tf + tf1) == Feedback(tf, tf1)
|
| 970 |
+
assert tf / (tf + tf1*tf2*tf3) == Feedback(tf, tf1*tf2*tf3)
|
| 971 |
+
assert tf1 / (tf + tf1*tf2*tf3) == Feedback(tf1, tf2*tf3)
|
| 972 |
+
assert (tf1*tf2) / (tf + tf1*tf2) == Feedback(tf1*tf2, tf)
|
| 973 |
+
assert (tf1*tf2) / (tf + tf1*tf2*tf5) == Feedback(tf1*tf2, tf5)
|
| 974 |
+
assert (tf1*tf2) / (tf + tf1*tf2*tf5*tf3) in (Feedback(tf1*tf2, tf5*tf3), Feedback(tf1*tf2, tf3*tf5))
|
| 975 |
+
assert tf4 / (TransferFunction(1, 1, p) + tf4*tf6) == Feedback(tf4, tf6)
|
| 976 |
+
assert tf5 / (tf + tf5) == Feedback(tf5, tf)
|
| 977 |
+
|
| 978 |
+
raises(TypeError, lambda: tf1*tf2*tf3 / (1 + tf1*tf2*tf3))
|
| 979 |
+
raises(ValueError, lambda: tf2*tf3 / (tf + tf2*tf3*tf4))
|
| 980 |
+
|
| 981 |
+
assert Feedback(tf, tf1*tf2*tf3).doit() == \
|
| 982 |
+
TransferFunction((a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), k*(a2*p - s) + \
|
| 983 |
+
(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), s)
|
| 984 |
+
assert Feedback(tf, tf1*tf2*tf3).sensitivity == \
|
| 985 |
+
1/(k*(a2*p - s)/((a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2)) + 1)
|
| 986 |
+
assert Feedback(tf1, tf2*tf3).doit() == \
|
| 987 |
+
TransferFunction((a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2), (k*(a2*p - s) + \
|
| 988 |
+
(a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2))*(s**2 + 2*s*wn*zeta + wn**2), s)
|
| 989 |
+
assert Feedback(tf1, tf2*tf3).sensitivity == \
|
| 990 |
+
1/(k*(a2*p - s)/((a2*s + p)*(s**2 + 2*s*wn*zeta + wn**2)) + 1)
|
| 991 |
+
assert Feedback(tf1*tf2, tf5).doit() == \
|
| 992 |
+
TransferFunction(k*(a0 + s)*(s**2 + 2*s*wn*zeta + wn**2), (k*(-a0 + a1*s**2 + a2*s) + \
|
| 993 |
+
(a0 + s)*(s**2 + 2*s*wn*zeta + wn**2))*(s**2 + 2*s*wn*zeta + wn**2), s)
|
| 994 |
+
assert Feedback(tf1*tf2, tf5, 1).sensitivity == \
|
| 995 |
+
1/(-k*(-a0 + a1*s**2 + a2*s)/((a0 + s)*(s**2 + 2*s*wn*zeta + wn**2)) + 1)
|
| 996 |
+
assert Feedback(tf4, tf6).doit() == \
|
| 997 |
+
TransferFunction(p*(p + s)*(a0*p + p**a1 - s), p*(p*(p + s) + (-p + s)*(a0*p + p**a1 - s)), p)
|
| 998 |
+
assert -Feedback(tf4*tf6, TransferFunction(1, 1, p)).doit() == \
|
| 999 |
+
TransferFunction(-p*(-p + s)*(p + s)*(a0*p + p**a1 - s), p*(p + s)*(p*(p + s) + (-p + s)*(a0*p + p**a1 - s)), p)
|
| 1000 |
+
assert Feedback(tf, tf).doit() == TransferFunction(1, 2, s)
|
| 1001 |
+
|
| 1002 |
+
assert Feedback(tf1, tf2*tf5).rewrite(TransferFunction) == \
|
| 1003 |
+
TransferFunction((a0 + s)*(s**2 + 2*s*wn*zeta + wn**2), (k*(-a0 + a1*s**2 + a2*s) + \
|
| 1004 |
+
(a0 + s)*(s**2 + 2*s*wn*zeta + wn**2))*(s**2 + 2*s*wn*zeta + wn**2), s)
|
| 1005 |
+
assert Feedback(TransferFunction(1, 1, p), tf4).rewrite(TransferFunction) == \
|
| 1006 |
+
TransferFunction(p, a0*p + p + p**a1 - s, p)
|
| 1007 |
+
|
| 1008 |
+
|
| 1009 |
+
def test_Feedback_as_TransferFunction():
|
| 1010 |
+
# Solves issue https://github.com/sympy/sympy/issues/26161
|
| 1011 |
+
tf1 = TransferFunction(s+1, 1, s)
|
| 1012 |
+
tf2 = TransferFunction(s+2, 1, s)
|
| 1013 |
+
fd1 = Feedback(tf1, tf2, -1) # Negative Feedback system
|
| 1014 |
+
fd2 = Feedback(tf1, tf2, 1) # Positive Feedback system
|
| 1015 |
+
unit = TransferFunction(1, 1, s)
|
| 1016 |
+
|
| 1017 |
+
# Checking the type
|
| 1018 |
+
assert isinstance(fd1, TransferFunction)
|
| 1019 |
+
assert isinstance(fd1, Feedback)
|
| 1020 |
+
|
| 1021 |
+
# Testing the numerator and denominator
|
| 1022 |
+
assert fd1.num == tf1
|
| 1023 |
+
assert fd2.num == tf1
|
| 1024 |
+
assert fd1.den == Parallel(unit, Series(tf2, tf1))
|
| 1025 |
+
assert fd2.den == Parallel(unit, -Series(tf2, tf1))
|
| 1026 |
+
|
| 1027 |
+
# Testing the Series and Parallel Combination with Feedback and TransferFunction
|
| 1028 |
+
s1 = Series(tf1, fd1)
|
| 1029 |
+
p1 = Parallel(tf1, fd1)
|
| 1030 |
+
assert tf1 * fd1 == s1
|
| 1031 |
+
assert tf1 + fd1 == p1
|
| 1032 |
+
assert s1.doit() == TransferFunction((s + 1)**2, (s + 1)*(s + 2) + 1, s)
|
| 1033 |
+
assert p1.doit() == TransferFunction(s + (s + 1)*((s + 1)*(s + 2) + 1) + 1, (s + 1)*(s + 2) + 1, s)
|
| 1034 |
+
|
| 1035 |
+
# Testing the use of Feedback and TransferFunction with Feedback
|
| 1036 |
+
fd3 = Feedback(tf1*fd1, tf2, -1)
|
| 1037 |
+
assert fd3 == Feedback(Series(tf1, fd1), tf2)
|
| 1038 |
+
assert fd3.num == tf1 * fd1
|
| 1039 |
+
assert fd3.den == Parallel(unit, Series(tf2, Series(tf1, fd1)))
|
| 1040 |
+
|
| 1041 |
+
# Testing the use of Feedback and TransferFunction with TransferFunction
|
| 1042 |
+
tf3 = TransferFunction(tf1*fd1, tf2, s)
|
| 1043 |
+
assert tf3 == TransferFunction(Series(tf1, fd1), tf2, s)
|
| 1044 |
+
assert tf3.num == tf1*fd1
|
| 1045 |
+
|
| 1046 |
+
def test_issue_26161():
|
| 1047 |
+
# Issue https://github.com/sympy/sympy/issues/26161
|
| 1048 |
+
Ib, Is, m, h, l2, l1 = symbols('I_b, I_s, m, h, l2, l1',
|
| 1049 |
+
real=True, nonnegative=True)
|
| 1050 |
+
KD, KP, v = symbols('K_D, K_P, v', real=True)
|
| 1051 |
+
|
| 1052 |
+
tau1_sq = (Ib + m * h ** 2) / m / g / h
|
| 1053 |
+
tau2 = l2 / v
|
| 1054 |
+
tau3 = v / (l1 + l2)
|
| 1055 |
+
K = v ** 2 / g / (l1 + l2)
|
| 1056 |
+
|
| 1057 |
+
Gtheta = TransferFunction(-K * (tau2 * s + 1), tau1_sq * s ** 2 - 1, s)
|
| 1058 |
+
Gdelta = TransferFunction(1, Is * s ** 2 + c * s, s)
|
| 1059 |
+
Gpsi = TransferFunction(1, tau3 * s, s)
|
| 1060 |
+
Dcont = TransferFunction(KD * s, 1, s)
|
| 1061 |
+
PIcont = TransferFunction(KP, s, s)
|
| 1062 |
+
Gunity = TransferFunction(1, 1, s)
|
| 1063 |
+
|
| 1064 |
+
Ginner = Feedback(Dcont * Gdelta, Gtheta)
|
| 1065 |
+
Gouter = Feedback(PIcont * Ginner * Gpsi, Gunity)
|
| 1066 |
+
assert Gouter == Feedback(Series(PIcont, Series(Ginner, Gpsi)), Gunity)
|
| 1067 |
+
assert Gouter.num == Series(PIcont, Series(Ginner, Gpsi))
|
| 1068 |
+
assert Gouter.den == Parallel(Gunity, Series(Gunity, Series(PIcont, Series(Ginner, Gpsi))))
|
| 1069 |
+
expr = (KD*KP*g*s**3*v**2*(l1 + l2)*(Is*s**2 + c*s)**2*(-g*h*m + s**2*(Ib + h**2*m))*(-KD*g*h*m*s*v**2*(l2*s + v) + \
|
| 1070 |
+
g*v*(l1 + l2)*(Is*s**2 + c*s)*(-g*h*m + s**2*(Ib + h**2*m))))/((s**2*v*(Is*s**2 + c*s)*(-KD*g*h*m*s*v**2* \
|
| 1071 |
+
(l2*s + v) + g*v*(l1 + l2)*(Is*s**2 + c*s)*(-g*h*m + s**2*(Ib + h**2*m)))*(KD*KP*g*s*v*(l1 + l2)**2* \
|
| 1072 |
+
(Is*s**2 + c*s)*(-g*h*m + s**2*(Ib + h**2*m)) + s**2*v*(Is*s**2 + c*s)*(-KD*g*h*m*s*v**2*(l2*s + v) + \
|
| 1073 |
+
g*v*(l1 + l2)*(Is*s**2 + c*s)*(-g*h*m + s**2*(Ib + h**2*m))))/(l1 + l2)))
|
| 1074 |
+
|
| 1075 |
+
assert (Gouter.to_expr() - expr).simplify() == 0
|
| 1076 |
+
|
| 1077 |
+
|
| 1078 |
+
def test_MIMOFeedback_construction():
|
| 1079 |
+
tf1 = TransferFunction(1, s, s)
|
| 1080 |
+
tf2 = TransferFunction(s, s**3 - 1, s)
|
| 1081 |
+
tf3 = TransferFunction(s, s + 1, s)
|
| 1082 |
+
tf4 = TransferFunction(s, s**2 + 1, s)
|
| 1083 |
+
|
| 1084 |
+
tfm_1 = TransferFunctionMatrix([[tf1, tf2], [tf3, tf4]])
|
| 1085 |
+
tfm_2 = TransferFunctionMatrix([[tf2, tf3], [tf4, tf1]])
|
| 1086 |
+
tfm_3 = TransferFunctionMatrix([[tf3, tf4], [tf1, tf2]])
|
| 1087 |
+
|
| 1088 |
+
f1 = MIMOFeedback(tfm_1, tfm_2)
|
| 1089 |
+
assert f1.args == (tfm_1, tfm_2, -1)
|
| 1090 |
+
assert f1.sys1 == tfm_1
|
| 1091 |
+
assert f1.sys2 == tfm_2
|
| 1092 |
+
assert f1.var == s
|
| 1093 |
+
assert f1.sign == -1
|
| 1094 |
+
assert -(-f1) == f1
|
| 1095 |
+
|
| 1096 |
+
f2 = MIMOFeedback(tfm_2, tfm_1, 1)
|
| 1097 |
+
assert f2.args == (tfm_2, tfm_1, 1)
|
| 1098 |
+
assert f2.sys1 == tfm_2
|
| 1099 |
+
assert f2.sys2 == tfm_1
|
| 1100 |
+
assert f2.var == s
|
| 1101 |
+
assert f2.sign == 1
|
| 1102 |
+
|
| 1103 |
+
f3 = MIMOFeedback(tfm_1, MIMOSeries(tfm_3, tfm_2))
|
| 1104 |
+
assert f3.args == (tfm_1, MIMOSeries(tfm_3, tfm_2), -1)
|
| 1105 |
+
assert f3.sys1 == tfm_1
|
| 1106 |
+
assert f3.sys2 == MIMOSeries(tfm_3, tfm_2)
|
| 1107 |
+
assert f3.var == s
|
| 1108 |
+
assert f3.sign == -1
|
| 1109 |
+
|
| 1110 |
+
mat = Matrix([[1, 1/s], [0, 1]])
|
| 1111 |
+
sys1 = controller = TransferFunctionMatrix.from_Matrix(mat, s)
|
| 1112 |
+
f4 = MIMOFeedback(sys1, controller)
|
| 1113 |
+
assert f4.args == (sys1, controller, -1)
|
| 1114 |
+
assert f4.sys1 == f4.sys2 == sys1
|
| 1115 |
+
|
| 1116 |
+
|
| 1117 |
+
def test_MIMOFeedback_errors():
|
| 1118 |
+
tf1 = TransferFunction(1, s, s)
|
| 1119 |
+
tf2 = TransferFunction(s, s**3 - 1, s)
|
| 1120 |
+
tf3 = TransferFunction(s, s - 1, s)
|
| 1121 |
+
tf4 = TransferFunction(s, s**2 + 1, s)
|
| 1122 |
+
tf5 = TransferFunction(1, 1, s)
|
| 1123 |
+
tf6 = TransferFunction(-1, s - 1, s)
|
| 1124 |
+
|
| 1125 |
+
tfm_1 = TransferFunctionMatrix([[tf1, tf2], [tf3, tf4]])
|
| 1126 |
+
tfm_2 = TransferFunctionMatrix([[tf2, tf3], [tf4, tf1]])
|
| 1127 |
+
tfm_3 = TransferFunctionMatrix.from_Matrix(eye(2), var=s)
|
| 1128 |
+
tfm_4 = TransferFunctionMatrix([[tf1, tf5], [tf5, tf5]])
|
| 1129 |
+
tfm_5 = TransferFunctionMatrix([[-tf3, tf3], [tf3, tf6]])
|
| 1130 |
+
# tfm_4 is inverse of tfm_5. Therefore tfm_5*tfm_4 = I
|
| 1131 |
+
tfm_6 = TransferFunctionMatrix([[-tf3]])
|
| 1132 |
+
tfm_7 = TransferFunctionMatrix([[tf3, tf4]])
|
| 1133 |
+
|
| 1134 |
+
# Unsupported Types
|
| 1135 |
+
raises(TypeError, lambda: MIMOFeedback(tf1, tf2))
|
| 1136 |
+
raises(TypeError, lambda: MIMOFeedback(MIMOParallel(tfm_1, tfm_2), tfm_3))
|
| 1137 |
+
# Shape Errors
|
| 1138 |
+
raises(ValueError, lambda: MIMOFeedback(tfm_1, tfm_6, 1))
|
| 1139 |
+
raises(ValueError, lambda: MIMOFeedback(tfm_7, tfm_7))
|
| 1140 |
+
# sign not 1/-1
|
| 1141 |
+
raises(ValueError, lambda: MIMOFeedback(tfm_1, tfm_2, -2))
|
| 1142 |
+
# Non-Invertible Systems
|
| 1143 |
+
raises(ValueError, lambda: MIMOFeedback(tfm_5, tfm_4, 1))
|
| 1144 |
+
raises(ValueError, lambda: MIMOFeedback(tfm_4, -tfm_5))
|
| 1145 |
+
raises(ValueError, lambda: MIMOFeedback(tfm_3, tfm_3, 1))
|
| 1146 |
+
# Variable not same in both the systems
|
| 1147 |
+
tfm_8 = TransferFunctionMatrix.from_Matrix(eye(2), var=p)
|
| 1148 |
+
raises(ValueError, lambda: MIMOFeedback(tfm_1, tfm_8, 1))
|
| 1149 |
+
|
| 1150 |
+
|
| 1151 |
+
def test_MIMOFeedback_functions():
|
| 1152 |
+
tf1 = TransferFunction(1, s, s)
|
| 1153 |
+
tf2 = TransferFunction(s, s - 1, s)
|
| 1154 |
+
tf3 = TransferFunction(1, 1, s)
|
| 1155 |
+
tf4 = TransferFunction(-1, s - 1, s)
|
| 1156 |
+
|
| 1157 |
+
tfm_1 = TransferFunctionMatrix.from_Matrix(eye(2), var=s)
|
| 1158 |
+
tfm_2 = TransferFunctionMatrix([[tf1, tf3], [tf3, tf3]])
|
| 1159 |
+
tfm_3 = TransferFunctionMatrix([[-tf2, tf2], [tf2, tf4]])
|
| 1160 |
+
tfm_4 = TransferFunctionMatrix([[tf1, tf2], [-tf2, tf1]])
|
| 1161 |
+
|
| 1162 |
+
# sensitivity, doit(), rewrite()
|
| 1163 |
+
F_1 = MIMOFeedback(tfm_2, tfm_3)
|
| 1164 |
+
F_2 = MIMOFeedback(tfm_2, MIMOSeries(tfm_4, -tfm_1), 1)
|
| 1165 |
+
|
| 1166 |
+
assert F_1.sensitivity == Matrix([[S.Half, 0], [0, S.Half]])
|
| 1167 |
+
assert F_2.sensitivity == Matrix([[(-2*s**4 + s**2)/(s**2 - s + 1),
|
| 1168 |
+
(2*s**3 - s**2)/(s**2 - s + 1)], [-s**2, s]])
|
| 1169 |
+
|
| 1170 |
+
assert F_1.doit() == \
|
| 1171 |
+
TransferFunctionMatrix(((TransferFunction(1, 2*s, s),
|
| 1172 |
+
TransferFunction(1, 2, s)), (TransferFunction(1, 2, s),
|
| 1173 |
+
TransferFunction(1, 2, s)))) == F_1.rewrite(TransferFunctionMatrix)
|
| 1174 |
+
assert F_2.doit(cancel=False, expand=True) == \
|
| 1175 |
+
TransferFunctionMatrix(((TransferFunction(-s**5 + 2*s**4 - 2*s**3 + s**2, s**5 - 2*s**4 + 3*s**3 - 2*s**2 + s, s),
|
| 1176 |
+
TransferFunction(-2*s**4 + 2*s**3, s**2 - s + 1, s)), (TransferFunction(0, 1, s), TransferFunction(-s**2 + s, 1, s))))
|
| 1177 |
+
assert F_2.doit(cancel=False) == \
|
| 1178 |
+
TransferFunctionMatrix(((TransferFunction(s*(2*s**3 - s**2)*(s**2 - s + 1) + \
|
| 1179 |
+
(-2*s**4 + s**2)*(s**2 - s + 1), s*(s**2 - s + 1)**2, s), TransferFunction(-2*s**4 + 2*s**3, s**2 - s + 1, s)),
|
| 1180 |
+
(TransferFunction(0, 1, s), TransferFunction(-s**2 + s, 1, s))))
|
| 1181 |
+
assert F_2.doit() == \
|
| 1182 |
+
TransferFunctionMatrix(((TransferFunction(s*(-2*s**2 + s*(2*s - 1) + 1), s**2 - s + 1, s),
|
| 1183 |
+
TransferFunction(-2*s**3*(s - 1), s**2 - s + 1, s)), (TransferFunction(0, 1, s), TransferFunction(s*(1 - s), 1, s))))
|
| 1184 |
+
assert F_2.doit(expand=True) == \
|
| 1185 |
+
TransferFunctionMatrix(((TransferFunction(-s**2 + s, s**2 - s + 1, s), TransferFunction(-2*s**4 + 2*s**3, s**2 - s + 1, s)),
|
| 1186 |
+
(TransferFunction(0, 1, s), TransferFunction(-s**2 + s, 1, s))))
|
| 1187 |
+
|
| 1188 |
+
assert -(F_1.doit()) == (-F_1).doit() # First negating then calculating vs calculating then negating.
|
| 1189 |
+
|
| 1190 |
+
|
| 1191 |
+
def test_TransferFunctionMatrix_construction():
|
| 1192 |
+
tf5 = TransferFunction(a1*s**2 + a2*s - a0, s + a0, s)
|
| 1193 |
+
tf4 = TransferFunction(a0*p + p**a1 - s, p, p)
|
| 1194 |
+
|
| 1195 |
+
tfm3_ = TransferFunctionMatrix([[-TF3]])
|
| 1196 |
+
assert tfm3_.shape == (tfm3_.num_outputs, tfm3_.num_inputs) == (1, 1)
|
| 1197 |
+
assert tfm3_.args == Tuple(Tuple(Tuple(-TF3)))
|
| 1198 |
+
assert tfm3_.var == s
|
| 1199 |
+
|
| 1200 |
+
tfm5 = TransferFunctionMatrix([[TF1, -TF2], [TF3, tf5]])
|
| 1201 |
+
assert tfm5.shape == (tfm5.num_outputs, tfm5.num_inputs) == (2, 2)
|
| 1202 |
+
assert tfm5.args == Tuple(Tuple(Tuple(TF1, -TF2), Tuple(TF3, tf5)))
|
| 1203 |
+
assert tfm5.var == s
|
| 1204 |
+
|
| 1205 |
+
tfm7 = TransferFunctionMatrix([[TF1, TF2], [TF3, -tf5], [-tf5, TF2]])
|
| 1206 |
+
assert tfm7.shape == (tfm7.num_outputs, tfm7.num_inputs) == (3, 2)
|
| 1207 |
+
assert tfm7.args == Tuple(Tuple(Tuple(TF1, TF2), Tuple(TF3, -tf5), Tuple(-tf5, TF2)))
|
| 1208 |
+
assert tfm7.var == s
|
| 1209 |
+
|
| 1210 |
+
# all transfer functions will use the same complex variable. tf4 uses 'p'.
|
| 1211 |
+
raises(ValueError, lambda: TransferFunctionMatrix([[TF1], [TF2], [tf4]]))
|
| 1212 |
+
raises(ValueError, lambda: TransferFunctionMatrix([[TF1, tf4], [TF3, tf5]]))
|
| 1213 |
+
|
| 1214 |
+
# length of all the lists in the TFM should be equal.
|
| 1215 |
+
raises(ValueError, lambda: TransferFunctionMatrix([[TF1], [TF3, tf5]]))
|
| 1216 |
+
raises(ValueError, lambda: TransferFunctionMatrix([[TF1, TF3], [tf5]]))
|
| 1217 |
+
|
| 1218 |
+
# lists should only support transfer functions in them.
|
| 1219 |
+
raises(TypeError, lambda: TransferFunctionMatrix([[TF1, TF2], [TF3, Matrix([1, 2])]]))
|
| 1220 |
+
raises(TypeError, lambda: TransferFunctionMatrix([[TF1, Matrix([1, 2])], [TF3, TF2]]))
|
| 1221 |
+
|
| 1222 |
+
# `arg` should strictly be nested list of TransferFunction
|
| 1223 |
+
raises(ValueError, lambda: TransferFunctionMatrix([TF1, TF2, tf5]))
|
| 1224 |
+
raises(ValueError, lambda: TransferFunctionMatrix([TF1]))
|
| 1225 |
+
|
| 1226 |
+
def test_TransferFunctionMatrix_functions():
|
| 1227 |
+
tf5 = TransferFunction(a1*s**2 + a2*s - a0, s + a0, s)
|
| 1228 |
+
|
| 1229 |
+
# Classmethod (from_matrix)
|
| 1230 |
+
|
| 1231 |
+
mat_1 = ImmutableMatrix([
|
| 1232 |
+
[s*(s + 1)*(s - 3)/(s**4 + 1), 2],
|
| 1233 |
+
[p, p*(s + 1)/(s*(s**1 + 1))]
|
| 1234 |
+
])
|
| 1235 |
+
mat_2 = ImmutableMatrix([[(2*s + 1)/(s**2 - 9)]])
|
| 1236 |
+
mat_3 = ImmutableMatrix([[1, 2], [3, 4]])
|
| 1237 |
+
assert TransferFunctionMatrix.from_Matrix(mat_1, s) == \
|
| 1238 |
+
TransferFunctionMatrix([[TransferFunction(s*(s - 3)*(s + 1), s**4 + 1, s), TransferFunction(2, 1, s)],
|
| 1239 |
+
[TransferFunction(p, 1, s), TransferFunction(p, s, s)]])
|
| 1240 |
+
assert TransferFunctionMatrix.from_Matrix(mat_2, s) == \
|
| 1241 |
+
TransferFunctionMatrix([[TransferFunction(2*s + 1, s**2 - 9, s)]])
|
| 1242 |
+
assert TransferFunctionMatrix.from_Matrix(mat_3, p) == \
|
| 1243 |
+
TransferFunctionMatrix([[TransferFunction(1, 1, p), TransferFunction(2, 1, p)],
|
| 1244 |
+
[TransferFunction(3, 1, p), TransferFunction(4, 1, p)]])
|
| 1245 |
+
|
| 1246 |
+
# Negating a TFM
|
| 1247 |
+
|
| 1248 |
+
tfm1 = TransferFunctionMatrix([[TF1], [TF2]])
|
| 1249 |
+
assert -tfm1 == TransferFunctionMatrix([[-TF1], [-TF2]])
|
| 1250 |
+
|
| 1251 |
+
tfm2 = TransferFunctionMatrix([[TF1, TF2, TF3], [tf5, -TF1, -TF3]])
|
| 1252 |
+
assert -tfm2 == TransferFunctionMatrix([[-TF1, -TF2, -TF3], [-tf5, TF1, TF3]])
|
| 1253 |
+
|
| 1254 |
+
# subs()
|
| 1255 |
+
|
| 1256 |
+
H_1 = TransferFunctionMatrix.from_Matrix(mat_1, s)
|
| 1257 |
+
H_2 = TransferFunctionMatrix([[TransferFunction(a*p*s, k*s**2, s), TransferFunction(p*s, k*(s**2 - a), s)]])
|
| 1258 |
+
assert H_1.subs(p, 1) == TransferFunctionMatrix([[TransferFunction(s*(s - 3)*(s + 1), s**4 + 1, s), TransferFunction(2, 1, s)], [TransferFunction(1, 1, s), TransferFunction(1, s, s)]])
|
| 1259 |
+
assert H_1.subs({p: 1}) == TransferFunctionMatrix([[TransferFunction(s*(s - 3)*(s + 1), s**4 + 1, s), TransferFunction(2, 1, s)], [TransferFunction(1, 1, s), TransferFunction(1, s, s)]])
|
| 1260 |
+
assert H_1.subs({p: 1, s: 1}) == TransferFunctionMatrix([[TransferFunction(s*(s - 3)*(s + 1), s**4 + 1, s), TransferFunction(2, 1, s)], [TransferFunction(1, 1, s), TransferFunction(1, s, s)]]) # This should ignore `s` as it is `var`
|
| 1261 |
+
assert H_2.subs(p, 2) == TransferFunctionMatrix([[TransferFunction(2*a*s, k*s**2, s), TransferFunction(2*s, k*(-a + s**2), s)]])
|
| 1262 |
+
assert H_2.subs(k, 1) == TransferFunctionMatrix([[TransferFunction(a*p*s, s**2, s), TransferFunction(p*s, -a + s**2, s)]])
|
| 1263 |
+
assert H_2.subs(a, 0) == TransferFunctionMatrix([[TransferFunction(0, k*s**2, s), TransferFunction(p*s, k*s**2, s)]])
|
| 1264 |
+
assert H_2.subs({p: 1, k: 1, a: a0}) == TransferFunctionMatrix([[TransferFunction(a0*s, s**2, s), TransferFunction(s, -a0 + s**2, s)]])
|
| 1265 |
+
|
| 1266 |
+
# eval_frequency()
|
| 1267 |
+
assert H_2.eval_frequency(S(1)/2 + I) == Matrix([[2*a*p/(5*k) - 4*I*a*p/(5*k), I*p/(-a*k - 3*k/4 + I*k) + p/(-2*a*k - 3*k/2 + 2*I*k)]])
|
| 1268 |
+
|
| 1269 |
+
# transpose()
|
| 1270 |
+
|
| 1271 |
+
assert H_1.transpose() == TransferFunctionMatrix([[TransferFunction(s*(s - 3)*(s + 1), s**4 + 1, s), TransferFunction(p, 1, s)], [TransferFunction(2, 1, s), TransferFunction(p, s, s)]])
|
| 1272 |
+
assert H_2.transpose() == TransferFunctionMatrix([[TransferFunction(a*p*s, k*s**2, s)], [TransferFunction(p*s, k*(-a + s**2), s)]])
|
| 1273 |
+
assert H_1.transpose().transpose() == H_1
|
| 1274 |
+
assert H_2.transpose().transpose() == H_2
|
| 1275 |
+
|
| 1276 |
+
# elem_poles()
|
| 1277 |
+
|
| 1278 |
+
assert H_1.elem_poles() == [[[-sqrt(2)/2 - sqrt(2)*I/2, -sqrt(2)/2 + sqrt(2)*I/2, sqrt(2)/2 - sqrt(2)*I/2, sqrt(2)/2 + sqrt(2)*I/2], []],
|
| 1279 |
+
[[], [0]]]
|
| 1280 |
+
assert H_2.elem_poles() == [[[0, 0], [sqrt(a), -sqrt(a)]]]
|
| 1281 |
+
assert tfm2.elem_poles() == [[[wn*(-zeta + sqrt((zeta - 1)*(zeta + 1))), wn*(-zeta - sqrt((zeta - 1)*(zeta + 1)))], [], [-p/a2]],
|
| 1282 |
+
[[-a0], [wn*(-zeta + sqrt((zeta - 1)*(zeta + 1))), wn*(-zeta - sqrt((zeta - 1)*(zeta + 1)))], [-p/a2]]]
|
| 1283 |
+
|
| 1284 |
+
# elem_zeros()
|
| 1285 |
+
|
| 1286 |
+
assert H_1.elem_zeros() == [[[-1, 0, 3], []], [[], []]]
|
| 1287 |
+
assert H_2.elem_zeros() == [[[0], [0]]]
|
| 1288 |
+
assert tfm2.elem_zeros() == [[[], [], [a2*p]],
|
| 1289 |
+
[[-a2/(2*a1) - sqrt(4*a0*a1 + a2**2)/(2*a1), -a2/(2*a1) + sqrt(4*a0*a1 + a2**2)/(2*a1)], [], [a2*p]]]
|
| 1290 |
+
|
| 1291 |
+
# doit()
|
| 1292 |
+
|
| 1293 |
+
H_3 = TransferFunctionMatrix([[Series(TransferFunction(1, s**3 - 3, s), TransferFunction(s**2 - 2*s + 5, 1, s), TransferFunction(1, s, s))]])
|
| 1294 |
+
H_4 = TransferFunctionMatrix([[Parallel(TransferFunction(s**3 - 3, 4*s**4 - s**2 - 2*s + 5, s), TransferFunction(4 - s**3, 4*s**4 - s**2 - 2*s + 5, s))]])
|
| 1295 |
+
|
| 1296 |
+
assert H_3.doit() == TransferFunctionMatrix([[TransferFunction(s**2 - 2*s + 5, s*(s**3 - 3), s)]])
|
| 1297 |
+
assert H_4.doit() == TransferFunctionMatrix([[TransferFunction(1, 4*s**4 - s**2 - 2*s + 5, s)]])
|
| 1298 |
+
|
| 1299 |
+
# _flat()
|
| 1300 |
+
|
| 1301 |
+
assert H_1._flat() == [TransferFunction(s*(s - 3)*(s + 1), s**4 + 1, s), TransferFunction(2, 1, s), TransferFunction(p, 1, s), TransferFunction(p, s, s)]
|
| 1302 |
+
assert H_2._flat() == [TransferFunction(a*p*s, k*s**2, s), TransferFunction(p*s, k*(-a + s**2), s)]
|
| 1303 |
+
assert H_3._flat() == [Series(TransferFunction(1, s**3 - 3, s), TransferFunction(s**2 - 2*s + 5, 1, s), TransferFunction(1, s, s))]
|
| 1304 |
+
assert H_4._flat() == [Parallel(TransferFunction(s**3 - 3, 4*s**4 - s**2 - 2*s + 5, s), TransferFunction(4 - s**3, 4*s**4 - s**2 - 2*s + 5, s))]
|
| 1305 |
+
|
| 1306 |
+
# evalf()
|
| 1307 |
+
|
| 1308 |
+
assert H_1.evalf() == \
|
| 1309 |
+
TransferFunctionMatrix(((TransferFunction(s*(s - 3.0)*(s + 1.0), s**4 + 1.0, s), TransferFunction(2.0, 1, s)), (TransferFunction(1.0*p, 1, s), TransferFunction(p, s, s))))
|
| 1310 |
+
assert H_2.subs({a:3.141, p:2.88, k:2}).evalf() == \
|
| 1311 |
+
TransferFunctionMatrix(((TransferFunction(4.5230399999999999494093572138808667659759521484375, s, s),
|
| 1312 |
+
TransferFunction(2.87999999999999989341858963598497211933135986328125*s, 2.0*s**2 - 6.282000000000000028421709430404007434844970703125, s)),))
|
| 1313 |
+
|
| 1314 |
+
# simplify()
|
| 1315 |
+
|
| 1316 |
+
H_5 = TransferFunctionMatrix([[TransferFunction(s**5 + s**3 + s, s - s**2, s),
|
| 1317 |
+
TransferFunction((s + 3)*(s - 1), (s - 1)*(s + 5), s)]])
|
| 1318 |
+
|
| 1319 |
+
assert H_5.simplify() == simplify(H_5) == \
|
| 1320 |
+
TransferFunctionMatrix(((TransferFunction(-s**4 - s**2 - 1, s - 1, s), TransferFunction(s + 3, s + 5, s)),))
|
| 1321 |
+
|
| 1322 |
+
# expand()
|
| 1323 |
+
|
| 1324 |
+
assert (H_1.expand()
|
| 1325 |
+
== TransferFunctionMatrix(((TransferFunction(s**3 - 2*s**2 - 3*s, s**4 + 1, s), TransferFunction(2, 1, s)),
|
| 1326 |
+
(TransferFunction(p, 1, s), TransferFunction(p, s, s)))))
|
| 1327 |
+
assert H_5.expand() == \
|
| 1328 |
+
TransferFunctionMatrix(((TransferFunction(s**5 + s**3 + s, -s**2 + s, s), TransferFunction(s**2 + 2*s - 3, s**2 + 4*s - 5, s)),))
|
| 1329 |
+
|
| 1330 |
+
def test_TransferFunction_gbt():
|
| 1331 |
+
# simple transfer function, e.g. ohms law
|
| 1332 |
+
tf = TransferFunction(1, a*s+b, s)
|
| 1333 |
+
numZ, denZ = gbt(tf, T, 0.5)
|
| 1334 |
+
# discretized transfer function with coefs from tf.gbt()
|
| 1335 |
+
tf_test_bilinear = TransferFunction(s * numZ[0] + numZ[1], s * denZ[0] + denZ[1], s)
|
| 1336 |
+
# corresponding tf with manually calculated coefs
|
| 1337 |
+
tf_test_manual = TransferFunction(s * T/(2*(a + b*T/2)) + T/(2*(a + b*T/2)), s + (-a + b*T/2)/(a + b*T/2), s)
|
| 1338 |
+
|
| 1339 |
+
assert S.Zero == (tf_test_bilinear.simplify()-tf_test_manual.simplify()).simplify().num
|
| 1340 |
+
|
| 1341 |
+
tf = TransferFunction(1, a*s+b, s)
|
| 1342 |
+
numZ, denZ = gbt(tf, T, 0)
|
| 1343 |
+
# discretized transfer function with coefs from tf.gbt()
|
| 1344 |
+
tf_test_forward = TransferFunction(numZ[0], s*denZ[0]+denZ[1], s)
|
| 1345 |
+
# corresponding tf with manually calculated coefs
|
| 1346 |
+
tf_test_manual = TransferFunction(T/a, s + (-a + b*T)/a, s)
|
| 1347 |
+
|
| 1348 |
+
assert S.Zero == (tf_test_forward.simplify()-tf_test_manual.simplify()).simplify().num
|
| 1349 |
+
|
| 1350 |
+
tf = TransferFunction(1, a*s+b, s)
|
| 1351 |
+
numZ, denZ = gbt(tf, T, 1)
|
| 1352 |
+
# discretized transfer function with coefs from tf.gbt()
|
| 1353 |
+
tf_test_backward = TransferFunction(s*numZ[0], s*denZ[0]+denZ[1], s)
|
| 1354 |
+
# corresponding tf with manually calculated coefs
|
| 1355 |
+
tf_test_manual = TransferFunction(s * T/(a + b*T), s - a/(a + b*T), s)
|
| 1356 |
+
|
| 1357 |
+
assert S.Zero == (tf_test_backward.simplify()-tf_test_manual.simplify()).simplify().num
|
| 1358 |
+
|
| 1359 |
+
tf = TransferFunction(1, a*s+b, s)
|
| 1360 |
+
numZ, denZ = gbt(tf, T, 0.3)
|
| 1361 |
+
# discretized transfer function with coefs from tf.gbt()
|
| 1362 |
+
tf_test_gbt = TransferFunction(s*numZ[0]+numZ[1], s*denZ[0]+denZ[1], s)
|
| 1363 |
+
# corresponding tf with manually calculated coefs
|
| 1364 |
+
tf_test_manual = TransferFunction(s*3*T/(10*(a + 3*b*T/10)) + 7*T/(10*(a + 3*b*T/10)), s + (-a + 7*b*T/10)/(a + 3*b*T/10), s)
|
| 1365 |
+
|
| 1366 |
+
assert S.Zero == (tf_test_gbt.simplify()-tf_test_manual.simplify()).simplify().num
|
| 1367 |
+
|
| 1368 |
+
def test_TransferFunction_bilinear():
|
| 1369 |
+
# simple transfer function, e.g. ohms law
|
| 1370 |
+
tf = TransferFunction(1, a*s+b, s)
|
| 1371 |
+
numZ, denZ = bilinear(tf, T)
|
| 1372 |
+
# discretized transfer function with coefs from tf.bilinear()
|
| 1373 |
+
tf_test_bilinear = TransferFunction(s*numZ[0]+numZ[1], s*denZ[0]+denZ[1], s)
|
| 1374 |
+
# corresponding tf with manually calculated coefs
|
| 1375 |
+
tf_test_manual = TransferFunction(s * T/(2*(a + b*T/2)) + T/(2*(a + b*T/2)), s + (-a + b*T/2)/(a + b*T/2), s)
|
| 1376 |
+
|
| 1377 |
+
assert S.Zero == (tf_test_bilinear.simplify()-tf_test_manual.simplify()).simplify().num
|
| 1378 |
+
|
| 1379 |
+
def test_TransferFunction_forward_diff():
|
| 1380 |
+
# simple transfer function, e.g. ohms law
|
| 1381 |
+
tf = TransferFunction(1, a*s+b, s)
|
| 1382 |
+
numZ, denZ = forward_diff(tf, T)
|
| 1383 |
+
# discretized transfer function with coefs from tf.forward_diff()
|
| 1384 |
+
tf_test_forward = TransferFunction(numZ[0], s*denZ[0]+denZ[1], s)
|
| 1385 |
+
# corresponding tf with manually calculated coefs
|
| 1386 |
+
tf_test_manual = TransferFunction(T/a, s + (-a + b*T)/a, s)
|
| 1387 |
+
|
| 1388 |
+
assert S.Zero == (tf_test_forward.simplify()-tf_test_manual.simplify()).simplify().num
|
| 1389 |
+
|
| 1390 |
+
def test_TransferFunction_backward_diff():
|
| 1391 |
+
# simple transfer function, e.g. ohms law
|
| 1392 |
+
tf = TransferFunction(1, a*s+b, s)
|
| 1393 |
+
numZ, denZ = backward_diff(tf, T)
|
| 1394 |
+
# discretized transfer function with coefs from tf.backward_diff()
|
| 1395 |
+
tf_test_backward = TransferFunction(s*numZ[0]+numZ[1], s*denZ[0]+denZ[1], s)
|
| 1396 |
+
# corresponding tf with manually calculated coefs
|
| 1397 |
+
tf_test_manual = TransferFunction(s * T/(a + b*T), s - a/(a + b*T), s)
|
| 1398 |
+
|
| 1399 |
+
assert S.Zero == (tf_test_backward.simplify()-tf_test_manual.simplify()).simplify().num
|
| 1400 |
+
|
| 1401 |
+
def test_TransferFunction_phase_margin():
|
| 1402 |
+
# Test for phase margin
|
| 1403 |
+
tf1 = TransferFunction(10, p**3 + 1, p)
|
| 1404 |
+
tf2 = TransferFunction(s**2, 10, s)
|
| 1405 |
+
tf3 = TransferFunction(1, a*s+b, s)
|
| 1406 |
+
tf4 = TransferFunction((s + 1)*exp(s/tau), s**2 + 2, s)
|
| 1407 |
+
tf_m = TransferFunctionMatrix([[tf2],[tf3]])
|
| 1408 |
+
|
| 1409 |
+
assert phase_margin(tf1) == -180 + 180*atan(3*sqrt(11))/pi
|
| 1410 |
+
assert phase_margin(tf2) == 0
|
| 1411 |
+
|
| 1412 |
+
raises(NotImplementedError, lambda: phase_margin(tf4))
|
| 1413 |
+
raises(ValueError, lambda: phase_margin(tf3))
|
| 1414 |
+
raises(ValueError, lambda: phase_margin(MIMOSeries(tf_m)))
|
| 1415 |
+
|
| 1416 |
+
def test_TransferFunction_gain_margin():
|
| 1417 |
+
# Test for gain margin
|
| 1418 |
+
tf1 = TransferFunction(s**2, 5*(s+1)*(s-5)*(s-10), s)
|
| 1419 |
+
tf2 = TransferFunction(s**2 + 2*s + 1, 1, s)
|
| 1420 |
+
tf3 = TransferFunction(1, a*s+b, s)
|
| 1421 |
+
tf4 = TransferFunction((s + 1)*exp(s/tau), s**2 + 2, s)
|
| 1422 |
+
tf_m = TransferFunctionMatrix([[tf2],[tf3]])
|
| 1423 |
+
|
| 1424 |
+
assert gain_margin(tf1) == -20*log(S(7)/540)/log(10)
|
| 1425 |
+
assert gain_margin(tf2) == oo
|
| 1426 |
+
|
| 1427 |
+
raises(NotImplementedError, lambda: gain_margin(tf4))
|
| 1428 |
+
raises(ValueError, lambda: gain_margin(tf3))
|
| 1429 |
+
raises(ValueError, lambda: gain_margin(MIMOSeries(tf_m)))
|
| 1430 |
+
|
| 1431 |
+
|
| 1432 |
+
def test_StateSpace_construction():
|
| 1433 |
+
# using different numbers for a SISO system.
|
| 1434 |
+
A1 = Matrix([[0, 1], [1, 0]])
|
| 1435 |
+
B1 = Matrix([1, 0])
|
| 1436 |
+
C1 = Matrix([[0, 1]])
|
| 1437 |
+
D1 = Matrix([0])
|
| 1438 |
+
ss1 = StateSpace(A1, B1, C1, D1)
|
| 1439 |
+
|
| 1440 |
+
assert ss1.state_matrix == Matrix([[0, 1], [1, 0]])
|
| 1441 |
+
assert ss1.input_matrix == Matrix([1, 0])
|
| 1442 |
+
assert ss1.output_matrix == Matrix([[0, 1]])
|
| 1443 |
+
assert ss1.feedforward_matrix == Matrix([0])
|
| 1444 |
+
assert ss1.args == (Matrix([[0, 1], [1, 0]]), Matrix([[1], [0]]), Matrix([[0, 1]]), Matrix([[0]]))
|
| 1445 |
+
|
| 1446 |
+
# using different symbols for a SISO system.
|
| 1447 |
+
ss2 = StateSpace(Matrix([a0]), Matrix([a1]),
|
| 1448 |
+
Matrix([a2]), Matrix([a3]))
|
| 1449 |
+
|
| 1450 |
+
assert ss2.state_matrix == Matrix([[a0]])
|
| 1451 |
+
assert ss2.input_matrix == Matrix([[a1]])
|
| 1452 |
+
assert ss2.output_matrix == Matrix([[a2]])
|
| 1453 |
+
assert ss2.feedforward_matrix == Matrix([[a3]])
|
| 1454 |
+
assert ss2.args == (Matrix([[a0]]), Matrix([[a1]]), Matrix([[a2]]), Matrix([[a3]]))
|
| 1455 |
+
|
| 1456 |
+
# using different numbers for a MIMO system.
|
| 1457 |
+
ss3 = StateSpace(Matrix([[-1.5, -2], [1, 0]]),
|
| 1458 |
+
Matrix([[0.5, 0], [0, 1]]),
|
| 1459 |
+
Matrix([[0, 1], [0, 2]]),
|
| 1460 |
+
Matrix([[2, 2], [1, 1]]))
|
| 1461 |
+
|
| 1462 |
+
assert ss3.state_matrix == Matrix([[-1.5, -2], [1, 0]])
|
| 1463 |
+
assert ss3.input_matrix == Matrix([[0.5, 0], [0, 1]])
|
| 1464 |
+
assert ss3.output_matrix == Matrix([[0, 1], [0, 2]])
|
| 1465 |
+
assert ss3.feedforward_matrix == Matrix([[2, 2], [1, 1]])
|
| 1466 |
+
assert ss3.args == (Matrix([[-1.5, -2],
|
| 1467 |
+
[1, 0]]),
|
| 1468 |
+
Matrix([[0.5, 0],
|
| 1469 |
+
[0, 1]]),
|
| 1470 |
+
Matrix([[0, 1],
|
| 1471 |
+
[0, 2]]),
|
| 1472 |
+
Matrix([[2, 2],
|
| 1473 |
+
[1, 1]]))
|
| 1474 |
+
|
| 1475 |
+
# using different symbols for a MIMO system.
|
| 1476 |
+
A4 = Matrix([[a0, a1], [a2, a3]])
|
| 1477 |
+
B4 = Matrix([[b0, b1], [b2, b3]])
|
| 1478 |
+
C4 = Matrix([[c0, c1], [c2, c3]])
|
| 1479 |
+
D4 = Matrix([[d0, d1], [d2, d3]])
|
| 1480 |
+
ss4 = StateSpace(A4, B4, C4, D4)
|
| 1481 |
+
|
| 1482 |
+
assert ss4.state_matrix == Matrix([[a0, a1], [a2, a3]])
|
| 1483 |
+
assert ss4.input_matrix == Matrix([[b0, b1], [b2, b3]])
|
| 1484 |
+
assert ss4.output_matrix == Matrix([[c0, c1], [c2, c3]])
|
| 1485 |
+
assert ss4.feedforward_matrix == Matrix([[d0, d1], [d2, d3]])
|
| 1486 |
+
assert ss4.args == (Matrix([[a0, a1],
|
| 1487 |
+
[a2, a3]]),
|
| 1488 |
+
Matrix([[b0, b1],
|
| 1489 |
+
[b2, b3]]),
|
| 1490 |
+
Matrix([[c0, c1],
|
| 1491 |
+
[c2, c3]]),
|
| 1492 |
+
Matrix([[d0, d1],
|
| 1493 |
+
[d2, d3]]))
|
| 1494 |
+
|
| 1495 |
+
# using less matrices. Rest will be filled with a minimum of zeros.
|
| 1496 |
+
ss5 = StateSpace()
|
| 1497 |
+
assert ss5.args == (Matrix([[0]]), Matrix([[0]]), Matrix([[0]]), Matrix([[0]]))
|
| 1498 |
+
|
| 1499 |
+
A6 = Matrix([[0, 1], [1, 0]])
|
| 1500 |
+
B6 = Matrix([1, 1])
|
| 1501 |
+
ss6 = StateSpace(A6, B6)
|
| 1502 |
+
|
| 1503 |
+
assert ss6.state_matrix == Matrix([[0, 1], [1, 0]])
|
| 1504 |
+
assert ss6.input_matrix == Matrix([1, 1])
|
| 1505 |
+
assert ss6.output_matrix == Matrix([[0, 0]])
|
| 1506 |
+
assert ss6.feedforward_matrix == Matrix([[0]])
|
| 1507 |
+
assert ss6.args == (Matrix([[0, 1],
|
| 1508 |
+
[1, 0]]),
|
| 1509 |
+
Matrix([[1],
|
| 1510 |
+
[1]]),
|
| 1511 |
+
Matrix([[0, 0]]),
|
| 1512 |
+
Matrix([[0]]))
|
| 1513 |
+
|
| 1514 |
+
# Check if the system is SISO or MIMO.
|
| 1515 |
+
# If system is not SISO, then it is definitely MIMO.
|
| 1516 |
+
|
| 1517 |
+
assert ss1.is_SISO == True
|
| 1518 |
+
assert ss2.is_SISO == True
|
| 1519 |
+
assert ss3.is_SISO == False
|
| 1520 |
+
assert ss4.is_SISO == False
|
| 1521 |
+
assert ss5.is_SISO == True
|
| 1522 |
+
assert ss6.is_SISO == True
|
| 1523 |
+
|
| 1524 |
+
# ShapeError if matrices do not fit.
|
| 1525 |
+
raises(ShapeError, lambda: StateSpace(Matrix([s, (s+1)**2]), Matrix([s+1]),
|
| 1526 |
+
Matrix([s**2 - 1]), Matrix([2*s])))
|
| 1527 |
+
raises(ShapeError, lambda: StateSpace(Matrix([s]), Matrix([s+1, s**3 + 1]),
|
| 1528 |
+
Matrix([s**2 - 1]), Matrix([2*s])))
|
| 1529 |
+
raises(ShapeError, lambda: StateSpace(Matrix([s]), Matrix([s+1]),
|
| 1530 |
+
Matrix([[s**2 - 1], [s**2 + 2*s + 1]]), Matrix([2*s])))
|
| 1531 |
+
raises(ShapeError, lambda: StateSpace(Matrix([[-s, -s], [s, 0]]),
|
| 1532 |
+
Matrix([[s/2, 0], [0, s]]),
|
| 1533 |
+
Matrix([[0, s]]),
|
| 1534 |
+
Matrix([[2*s, 2*s], [s, s]])))
|
| 1535 |
+
|
| 1536 |
+
# TypeError if arguments are not sympy matrices.
|
| 1537 |
+
raises(TypeError, lambda: StateSpace(s**2, s+1, 2*s, 1))
|
| 1538 |
+
raises(TypeError, lambda: StateSpace(Matrix([2, 0.5]), Matrix([-1]),
|
| 1539 |
+
Matrix([1]), 0))
|
| 1540 |
+
def test_StateSpace_add():
|
| 1541 |
+
A1 = Matrix([[4, 1],[2, -3]])
|
| 1542 |
+
B1 = Matrix([[5, 2],[-3, -3]])
|
| 1543 |
+
C1 = Matrix([[2, -4],[0, 1]])
|
| 1544 |
+
D1 = Matrix([[3, 2],[1, -1]])
|
| 1545 |
+
ss1 = StateSpace(A1, B1, C1, D1)
|
| 1546 |
+
|
| 1547 |
+
A2 = Matrix([[-3, 4, 2],[-1, -3, 0],[2, 5, 3]])
|
| 1548 |
+
B2 = Matrix([[1, 4],[-3, -3],[-2, 1]])
|
| 1549 |
+
C2 = Matrix([[4, 2, -3],[1, 4, 3]])
|
| 1550 |
+
D2 = Matrix([[-2, 4],[0, 1]])
|
| 1551 |
+
ss2 = StateSpace(A2, B2, C2, D2)
|
| 1552 |
+
ss3 = StateSpace()
|
| 1553 |
+
ss4 = StateSpace(Matrix([1]), Matrix([2]), Matrix([3]), Matrix([4]))
|
| 1554 |
+
|
| 1555 |
+
expected_add = \
|
| 1556 |
+
StateSpace(
|
| 1557 |
+
Matrix([
|
| 1558 |
+
[4, 1, 0, 0, 0],
|
| 1559 |
+
[2, -3, 0, 0, 0],
|
| 1560 |
+
[0, 0, -3, 4, 2],
|
| 1561 |
+
[0, 0, -1, -3, 0],
|
| 1562 |
+
[0, 0, 2, 5, 3]]),
|
| 1563 |
+
Matrix([
|
| 1564 |
+
[ 5, 2],
|
| 1565 |
+
[-3, -3],
|
| 1566 |
+
[ 1, 4],
|
| 1567 |
+
[-3, -3],
|
| 1568 |
+
[-2, 1]]),
|
| 1569 |
+
Matrix([
|
| 1570 |
+
[2, -4, 4, 2, -3],
|
| 1571 |
+
[0, 1, 1, 4, 3]]),
|
| 1572 |
+
Matrix([
|
| 1573 |
+
[1, 6],
|
| 1574 |
+
[1, 0]]))
|
| 1575 |
+
|
| 1576 |
+
expected_mul = \
|
| 1577 |
+
StateSpace(
|
| 1578 |
+
Matrix([
|
| 1579 |
+
[ -3, 4, 2, 0, 0],
|
| 1580 |
+
[ -1, -3, 0, 0, 0],
|
| 1581 |
+
[ 2, 5, 3, 0, 0],
|
| 1582 |
+
[ 22, 18, -9, 4, 1],
|
| 1583 |
+
[-15, -18, 0, 2, -3]]),
|
| 1584 |
+
Matrix([
|
| 1585 |
+
[ 1, 4],
|
| 1586 |
+
[ -3, -3],
|
| 1587 |
+
[ -2, 1],
|
| 1588 |
+
[-10, 22],
|
| 1589 |
+
[ 6, -15]]),
|
| 1590 |
+
Matrix([
|
| 1591 |
+
[14, 14, -3, 2, -4],
|
| 1592 |
+
[ 3, -2, -6, 0, 1]]),
|
| 1593 |
+
Matrix([
|
| 1594 |
+
[-6, 14],
|
| 1595 |
+
[-2, 3]]))
|
| 1596 |
+
|
| 1597 |
+
assert ss1 + ss2 == expected_add
|
| 1598 |
+
assert ss1*ss2 == expected_mul
|
| 1599 |
+
assert ss3 + 1/2 == StateSpace(Matrix([[0]]), Matrix([[0]]), Matrix([[0]]), Matrix([[0.5]]))
|
| 1600 |
+
assert ss4*1.5 == StateSpace(Matrix([[1]]), Matrix([[2]]), Matrix([[4.5]]), Matrix([[6.0]]))
|
| 1601 |
+
assert 1.5*ss4 == StateSpace(Matrix([[1]]), Matrix([[3.0]]), Matrix([[3]]), Matrix([[6.0]]))
|
| 1602 |
+
raises(ShapeError, lambda: ss1 + ss3)
|
| 1603 |
+
raises(ShapeError, lambda: ss2*ss4)
|
| 1604 |
+
|
| 1605 |
+
def test_StateSpace_negation():
|
| 1606 |
+
A = Matrix([[a0, a1], [a2, a3]])
|
| 1607 |
+
B = Matrix([[b0, b1], [b2, b3]])
|
| 1608 |
+
C = Matrix([[c0, c1], [c1, c2], [c2, c3]])
|
| 1609 |
+
D = Matrix([[d0, d1], [d1, d2], [d2, d3]])
|
| 1610 |
+
SS = StateSpace(A, B, C, D)
|
| 1611 |
+
SS_neg = -SS
|
| 1612 |
+
|
| 1613 |
+
state_mat = Matrix([[-1, 1], [1, -1]])
|
| 1614 |
+
input_mat = Matrix([1, -1])
|
| 1615 |
+
output_mat = Matrix([[-1, 1]])
|
| 1616 |
+
feedforward_mat = Matrix([1])
|
| 1617 |
+
system = StateSpace(state_mat, input_mat, output_mat, feedforward_mat)
|
| 1618 |
+
|
| 1619 |
+
assert SS_neg == \
|
| 1620 |
+
StateSpace(Matrix([[a0, a1],
|
| 1621 |
+
[a2, a3]]),
|
| 1622 |
+
Matrix([[b0, b1],
|
| 1623 |
+
[b2, b3]]),
|
| 1624 |
+
Matrix([[-c0, -c1],
|
| 1625 |
+
[-c1, -c2],
|
| 1626 |
+
[-c2, -c3]]),
|
| 1627 |
+
Matrix([[-d0, -d1],
|
| 1628 |
+
[-d1, -d2],
|
| 1629 |
+
[-d2, -d3]]))
|
| 1630 |
+
assert -system == \
|
| 1631 |
+
StateSpace(Matrix([[-1, 1],
|
| 1632 |
+
[ 1, -1]]),
|
| 1633 |
+
Matrix([[ 1],[-1]]),
|
| 1634 |
+
Matrix([[1, -1]]),
|
| 1635 |
+
Matrix([[-1]]))
|
| 1636 |
+
assert -SS_neg == SS
|
| 1637 |
+
assert -(-(-(-system))) == system
|
| 1638 |
+
|
| 1639 |
+
def test_SymPy_substitution_functions():
|
| 1640 |
+
# subs
|
| 1641 |
+
ss1 = StateSpace(Matrix([s]), Matrix([(s + 1)**2]), Matrix([s**2 - 1]), Matrix([2*s]))
|
| 1642 |
+
ss2 = StateSpace(Matrix([s + p]), Matrix([(s + 1)*(p - 1)]), Matrix([p**3 - s**3]), Matrix([s - p]))
|
| 1643 |
+
|
| 1644 |
+
assert ss1.subs({s:5}) == StateSpace(Matrix([[5]]), Matrix([[36]]), Matrix([[24]]), Matrix([[10]]))
|
| 1645 |
+
assert ss2.subs({p:1}) == StateSpace(Matrix([[s + 1]]), Matrix([[0]]), Matrix([[1 - s**3]]), Matrix([[s - 1]]))
|
| 1646 |
+
|
| 1647 |
+
# xreplace
|
| 1648 |
+
assert ss1.xreplace({s:p}) == \
|
| 1649 |
+
StateSpace(Matrix([[p]]), Matrix([[(p + 1)**2]]), Matrix([[p**2 - 1]]), Matrix([[2*p]]))
|
| 1650 |
+
assert ss2.xreplace({s:a, p:b}) == \
|
| 1651 |
+
StateSpace(Matrix([[a + b]]), Matrix([[(a + 1)*(b - 1)]]), Matrix([[-a**3 + b**3]]), Matrix([[a - b]]))
|
| 1652 |
+
|
| 1653 |
+
# evalf
|
| 1654 |
+
p1 = a1*s + a0
|
| 1655 |
+
p2 = b2*s**2 + b1*s + b0
|
| 1656 |
+
G = StateSpace(Matrix([p1]), Matrix([p2]))
|
| 1657 |
+
expect = StateSpace(Matrix([[2*s + 1]]), Matrix([[5*s**2 + 4*s + 3]]), Matrix([[0]]), Matrix([[0]]))
|
| 1658 |
+
expect_ = StateSpace(Matrix([[2.0*s + 1.0]]), Matrix([[5.0*s**2 + 4.0*s + 3.0]]), Matrix([[0]]), Matrix([[0]]))
|
| 1659 |
+
assert G.subs({a0: 1, a1: 2, b0: 3, b1: 4, b2: 5}) == expect
|
| 1660 |
+
assert G.subs({a0: 1, a1: 2, b0: 3, b1: 4, b2: 5}).evalf() == expect_
|
| 1661 |
+
assert expect.evalf() == expect_
|
| 1662 |
+
|
| 1663 |
+
def test_conversion():
|
| 1664 |
+
# StateSpace to TransferFunction for SISO
|
| 1665 |
+
A1 = Matrix([[-5, -1], [3, -1]])
|
| 1666 |
+
B1 = Matrix([2, 5])
|
| 1667 |
+
C1 = Matrix([[1, 2]])
|
| 1668 |
+
D1 = Matrix([0])
|
| 1669 |
+
H1 = StateSpace(A1, B1, C1, D1)
|
| 1670 |
+
tm1 = H1.rewrite(TransferFunction)
|
| 1671 |
+
tm2 = (-H1).rewrite(TransferFunction)
|
| 1672 |
+
|
| 1673 |
+
tf1 = tm1[0][0]
|
| 1674 |
+
tf2 = tm2[0][0]
|
| 1675 |
+
|
| 1676 |
+
assert tf1 == TransferFunction(12*s + 59, s**2 + 6*s + 8, s)
|
| 1677 |
+
assert tf2.num == -tf1.num
|
| 1678 |
+
assert tf2.den == tf1.den
|
| 1679 |
+
|
| 1680 |
+
# StateSpace to TransferFunction for MIMO
|
| 1681 |
+
A2 = Matrix([[-1.5, -2, 3], [1, 0, 1], [2, 1, 1]])
|
| 1682 |
+
B2 = Matrix([[0.5, 0, 1], [0, 1, 2], [2, 2, 3]])
|
| 1683 |
+
C2 = Matrix([[0, 1, 0], [0, 2, 1], [1, 0, 2]])
|
| 1684 |
+
D2 = Matrix([[2, 2, 0], [1, 1, 1], [3, 2, 1]])
|
| 1685 |
+
H2 = StateSpace(A2, B2, C2, D2)
|
| 1686 |
+
tm3 = H2.rewrite(TransferFunction)
|
| 1687 |
+
|
| 1688 |
+
# outputs for input i obtained at Index i-1. Consider input 1
|
| 1689 |
+
assert tm3[0][0] == TransferFunction(2.0*s**3 + 1.0*s**2 - 10.5*s + 4.5, 1.0*s**3 + 0.5*s**2 - 6.5*s - 2.5, s)
|
| 1690 |
+
assert tm3[0][1] == TransferFunction(2.0*s**3 + 2.0*s**2 - 10.5*s - 3.5, 1.0*s**3 + 0.5*s**2 - 6.5*s - 2.5, s)
|
| 1691 |
+
assert tm3[0][2] == TransferFunction(2.0*s**2 + 5.0*s - 0.5, 1.0*s**3 + 0.5*s**2 - 6.5*s - 2.5, s)
|
| 1692 |
+
|
| 1693 |
+
# TransferFunction to StateSpace
|
| 1694 |
+
SS = TF1.rewrite(StateSpace)
|
| 1695 |
+
assert SS == \
|
| 1696 |
+
StateSpace(Matrix([[ 0, 1],
|
| 1697 |
+
[-wn**2, -2*wn*zeta]]),
|
| 1698 |
+
Matrix([[0],
|
| 1699 |
+
[1]]),
|
| 1700 |
+
Matrix([[1, 0]]),
|
| 1701 |
+
Matrix([[0]]))
|
| 1702 |
+
assert SS.rewrite(TransferFunction)[0][0] == TF1
|
| 1703 |
+
|
| 1704 |
+
# Transfer function has to be proper
|
| 1705 |
+
raises(ValueError, lambda: TransferFunction(b*s**2 + p**2 - a*p + s, b - p**2, s).rewrite(StateSpace))
|
| 1706 |
+
|
| 1707 |
+
|
| 1708 |
+
def test_StateSpace_functions():
|
| 1709 |
+
# https://in.mathworks.com/help/control/ref/statespacemodel.obsv.html
|
| 1710 |
+
|
| 1711 |
+
A_mat = Matrix([[-1.5, -2], [1, 0]])
|
| 1712 |
+
B_mat = Matrix([0.5, 0])
|
| 1713 |
+
C_mat = Matrix([[0, 1]])
|
| 1714 |
+
D_mat = Matrix([1])
|
| 1715 |
+
SS1 = StateSpace(A_mat, B_mat, C_mat, D_mat)
|
| 1716 |
+
SS2 = StateSpace(Matrix([[1, 1], [4, -2]]),Matrix([[0, 1], [0, 2]]),Matrix([[-1, 1], [1, -1]]))
|
| 1717 |
+
SS3 = StateSpace(Matrix([[1, 1], [4, -2]]),Matrix([[1, -1], [1, -1]]))
|
| 1718 |
+
|
| 1719 |
+
# Observability
|
| 1720 |
+
assert SS1.is_observable() == True
|
| 1721 |
+
assert SS2.is_observable() == False
|
| 1722 |
+
assert SS1.observability_matrix() == Matrix([[0, 1], [1, 0]])
|
| 1723 |
+
assert SS2.observability_matrix() == Matrix([[-1, 1], [ 1, -1], [ 3, -3], [-3, 3]])
|
| 1724 |
+
assert SS1.observable_subspace() == [Matrix([[0], [1]]), Matrix([[1], [0]])]
|
| 1725 |
+
assert SS2.observable_subspace() == [Matrix([[-1], [ 1], [ 3], [-3]])]
|
| 1726 |
+
|
| 1727 |
+
# Controllability
|
| 1728 |
+
assert SS1.is_controllable() == True
|
| 1729 |
+
assert SS3.is_controllable() == False
|
| 1730 |
+
assert SS1.controllability_matrix() == Matrix([[0.5, -0.75], [ 0, 0.5]])
|
| 1731 |
+
assert SS3.controllability_matrix() == Matrix([[1, -1, 2, -2], [1, -1, 2, -2]])
|
| 1732 |
+
assert SS1.controllable_subspace() == [Matrix([[0.5], [ 0]]), Matrix([[-0.75], [ 0.5]])]
|
| 1733 |
+
assert SS3.controllable_subspace() == [Matrix([[1], [1]])]
|
| 1734 |
+
|
| 1735 |
+
# Append
|
| 1736 |
+
A1 = Matrix([[0, 1], [1, 0]])
|
| 1737 |
+
B1 = Matrix([[0], [1]])
|
| 1738 |
+
C1 = Matrix([[0, 1]])
|
| 1739 |
+
D1 = Matrix([[0]])
|
| 1740 |
+
ss1 = StateSpace(A1, B1, C1, D1)
|
| 1741 |
+
ss2 = StateSpace(Matrix([[1, 0], [0, 1]]), Matrix([[1], [0]]), Matrix([[1, 0]]), Matrix([[1]]))
|
| 1742 |
+
ss3 = ss1.append(ss2)
|
| 1743 |
+
|
| 1744 |
+
assert ss3.num_states == ss1.num_states + ss2.num_states
|
| 1745 |
+
assert ss3.num_inputs == ss1.num_inputs + ss2.num_inputs
|
| 1746 |
+
assert ss3.num_outputs == ss1.num_outputs + ss2.num_outputs
|
| 1747 |
+
assert ss3.state_matrix == Matrix([[0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
|
| 1748 |
+
assert ss3.input_matrix == Matrix([[0, 0], [1, 0], [0, 1], [0, 0]])
|
| 1749 |
+
assert ss3.output_matrix == Matrix([[0, 1, 0, 0], [0, 0, 1, 0]])
|
| 1750 |
+
assert ss3.feedforward_matrix == Matrix([[0, 0], [0, 1]])
|
lib/python3.12/site-packages/sympy/physics/hep/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/sympy/physics/hep/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (196 Bytes). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/hep/__pycache__/gamma_matrices.cpython-312.pyc
ADDED
|
Binary file (20.1 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/hep/gamma_matrices.py
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Module to handle gamma matrices expressed as tensor objects.
|
| 3 |
+
|
| 4 |
+
Examples
|
| 5 |
+
========
|
| 6 |
+
|
| 7 |
+
>>> from sympy.physics.hep.gamma_matrices import GammaMatrix as G, LorentzIndex
|
| 8 |
+
>>> from sympy.tensor.tensor import tensor_indices
|
| 9 |
+
>>> i = tensor_indices('i', LorentzIndex)
|
| 10 |
+
>>> G(i)
|
| 11 |
+
GammaMatrix(i)
|
| 12 |
+
|
| 13 |
+
Note that there is already an instance of GammaMatrixHead in four dimensions:
|
| 14 |
+
GammaMatrix, which is simply declare as
|
| 15 |
+
|
| 16 |
+
>>> from sympy.physics.hep.gamma_matrices import GammaMatrix
|
| 17 |
+
>>> from sympy.tensor.tensor import tensor_indices
|
| 18 |
+
>>> i = tensor_indices('i', LorentzIndex)
|
| 19 |
+
>>> GammaMatrix(i)
|
| 20 |
+
GammaMatrix(i)
|
| 21 |
+
|
| 22 |
+
To access the metric tensor
|
| 23 |
+
|
| 24 |
+
>>> LorentzIndex.metric
|
| 25 |
+
metric(LorentzIndex,LorentzIndex)
|
| 26 |
+
|
| 27 |
+
"""
|
| 28 |
+
from sympy.core.mul import Mul
|
| 29 |
+
from sympy.core.singleton import S
|
| 30 |
+
from sympy.matrices.dense import eye
|
| 31 |
+
from sympy.matrices.expressions.trace import trace
|
| 32 |
+
from sympy.tensor.tensor import TensorIndexType, TensorIndex,\
|
| 33 |
+
TensMul, TensAdd, tensor_mul, Tensor, TensorHead, TensorSymmetry
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# DiracSpinorIndex = TensorIndexType('DiracSpinorIndex', dim=4, dummy_name="S")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
LorentzIndex = TensorIndexType('LorentzIndex', dim=4, dummy_name="L")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
GammaMatrix = TensorHead("GammaMatrix", [LorentzIndex],
|
| 43 |
+
TensorSymmetry.no_symmetry(1), comm=None)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def extract_type_tens(expression, component):
|
| 47 |
+
"""
|
| 48 |
+
Extract from a ``TensExpr`` all tensors with `component`.
|
| 49 |
+
|
| 50 |
+
Returns two tensor expressions:
|
| 51 |
+
|
| 52 |
+
* the first contains all ``Tensor`` of having `component`.
|
| 53 |
+
* the second contains all remaining.
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
"""
|
| 57 |
+
if isinstance(expression, Tensor):
|
| 58 |
+
sp = [expression]
|
| 59 |
+
elif isinstance(expression, TensMul):
|
| 60 |
+
sp = expression.args
|
| 61 |
+
else:
|
| 62 |
+
raise ValueError('wrong type')
|
| 63 |
+
|
| 64 |
+
# Collect all gamma matrices of the same dimension
|
| 65 |
+
new_expr = S.One
|
| 66 |
+
residual_expr = S.One
|
| 67 |
+
for i in sp:
|
| 68 |
+
if isinstance(i, Tensor) and i.component == component:
|
| 69 |
+
new_expr *= i
|
| 70 |
+
else:
|
| 71 |
+
residual_expr *= i
|
| 72 |
+
return new_expr, residual_expr
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def simplify_gamma_expression(expression):
|
| 76 |
+
extracted_expr, residual_expr = extract_type_tens(expression, GammaMatrix)
|
| 77 |
+
res_expr = _simplify_single_line(extracted_expr)
|
| 78 |
+
return res_expr * residual_expr
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def simplify_gpgp(ex, sort=True):
|
| 82 |
+
"""
|
| 83 |
+
simplify products ``G(i)*p(-i)*G(j)*p(-j) -> p(i)*p(-i)``
|
| 84 |
+
|
| 85 |
+
Examples
|
| 86 |
+
========
|
| 87 |
+
|
| 88 |
+
>>> from sympy.physics.hep.gamma_matrices import GammaMatrix as G, \
|
| 89 |
+
LorentzIndex, simplify_gpgp
|
| 90 |
+
>>> from sympy.tensor.tensor import tensor_indices, tensor_heads
|
| 91 |
+
>>> p, q = tensor_heads('p, q', [LorentzIndex])
|
| 92 |
+
>>> i0,i1,i2,i3,i4,i5 = tensor_indices('i0:6', LorentzIndex)
|
| 93 |
+
>>> ps = p(i0)*G(-i0)
|
| 94 |
+
>>> qs = q(i0)*G(-i0)
|
| 95 |
+
>>> simplify_gpgp(ps*qs*qs)
|
| 96 |
+
GammaMatrix(-L_0)*p(L_0)*q(L_1)*q(-L_1)
|
| 97 |
+
"""
|
| 98 |
+
def _simplify_gpgp(ex):
|
| 99 |
+
components = ex.components
|
| 100 |
+
a = []
|
| 101 |
+
comp_map = []
|
| 102 |
+
for i, comp in enumerate(components):
|
| 103 |
+
comp_map.extend([i]*comp.rank)
|
| 104 |
+
dum = [(i[0], i[1], comp_map[i[0]], comp_map[i[1]]) for i in ex.dum]
|
| 105 |
+
for i in range(len(components)):
|
| 106 |
+
if components[i] != GammaMatrix:
|
| 107 |
+
continue
|
| 108 |
+
for dx in dum:
|
| 109 |
+
if dx[2] == i:
|
| 110 |
+
p_pos1 = dx[3]
|
| 111 |
+
elif dx[3] == i:
|
| 112 |
+
p_pos1 = dx[2]
|
| 113 |
+
else:
|
| 114 |
+
continue
|
| 115 |
+
comp1 = components[p_pos1]
|
| 116 |
+
if comp1.comm == 0 and comp1.rank == 1:
|
| 117 |
+
a.append((i, p_pos1))
|
| 118 |
+
if not a:
|
| 119 |
+
return ex
|
| 120 |
+
elim = set()
|
| 121 |
+
tv = []
|
| 122 |
+
hit = True
|
| 123 |
+
coeff = S.One
|
| 124 |
+
ta = None
|
| 125 |
+
while hit:
|
| 126 |
+
hit = False
|
| 127 |
+
for i, ai in enumerate(a[:-1]):
|
| 128 |
+
if ai[0] in elim:
|
| 129 |
+
continue
|
| 130 |
+
if ai[0] != a[i + 1][0] - 1:
|
| 131 |
+
continue
|
| 132 |
+
if components[ai[1]] != components[a[i + 1][1]]:
|
| 133 |
+
continue
|
| 134 |
+
elim.add(ai[0])
|
| 135 |
+
elim.add(ai[1])
|
| 136 |
+
elim.add(a[i + 1][0])
|
| 137 |
+
elim.add(a[i + 1][1])
|
| 138 |
+
if not ta:
|
| 139 |
+
ta = ex.split()
|
| 140 |
+
mu = TensorIndex('mu', LorentzIndex)
|
| 141 |
+
hit = True
|
| 142 |
+
if i == 0:
|
| 143 |
+
coeff = ex.coeff
|
| 144 |
+
tx = components[ai[1]](mu)*components[ai[1]](-mu)
|
| 145 |
+
if len(a) == 2:
|
| 146 |
+
tx *= 4 # eye(4)
|
| 147 |
+
tv.append(tx)
|
| 148 |
+
break
|
| 149 |
+
|
| 150 |
+
if tv:
|
| 151 |
+
a = [x for j, x in enumerate(ta) if j not in elim]
|
| 152 |
+
a.extend(tv)
|
| 153 |
+
t = tensor_mul(*a)*coeff
|
| 154 |
+
# t = t.replace(lambda x: x.is_Matrix, lambda x: 1)
|
| 155 |
+
return t
|
| 156 |
+
else:
|
| 157 |
+
return ex
|
| 158 |
+
|
| 159 |
+
if sort:
|
| 160 |
+
ex = ex.sorted_components()
|
| 161 |
+
# this would be better off with pattern matching
|
| 162 |
+
while 1:
|
| 163 |
+
t = _simplify_gpgp(ex)
|
| 164 |
+
if t != ex:
|
| 165 |
+
ex = t
|
| 166 |
+
else:
|
| 167 |
+
return t
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def gamma_trace(t):
|
| 171 |
+
"""
|
| 172 |
+
trace of a single line of gamma matrices
|
| 173 |
+
|
| 174 |
+
Examples
|
| 175 |
+
========
|
| 176 |
+
|
| 177 |
+
>>> from sympy.physics.hep.gamma_matrices import GammaMatrix as G, \
|
| 178 |
+
gamma_trace, LorentzIndex
|
| 179 |
+
>>> from sympy.tensor.tensor import tensor_indices, tensor_heads
|
| 180 |
+
>>> p, q = tensor_heads('p, q', [LorentzIndex])
|
| 181 |
+
>>> i0,i1,i2,i3,i4,i5 = tensor_indices('i0:6', LorentzIndex)
|
| 182 |
+
>>> ps = p(i0)*G(-i0)
|
| 183 |
+
>>> qs = q(i0)*G(-i0)
|
| 184 |
+
>>> gamma_trace(G(i0)*G(i1))
|
| 185 |
+
4*metric(i0, i1)
|
| 186 |
+
>>> gamma_trace(ps*ps) - 4*p(i0)*p(-i0)
|
| 187 |
+
0
|
| 188 |
+
>>> gamma_trace(ps*qs + ps*ps) - 4*p(i0)*p(-i0) - 4*p(i0)*q(-i0)
|
| 189 |
+
0
|
| 190 |
+
|
| 191 |
+
"""
|
| 192 |
+
if isinstance(t, TensAdd):
|
| 193 |
+
res = TensAdd(*[gamma_trace(x) for x in t.args])
|
| 194 |
+
return res
|
| 195 |
+
t = _simplify_single_line(t)
|
| 196 |
+
res = _trace_single_line(t)
|
| 197 |
+
return res
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def _simplify_single_line(expression):
|
| 201 |
+
"""
|
| 202 |
+
Simplify single-line product of gamma matrices.
|
| 203 |
+
|
| 204 |
+
Examples
|
| 205 |
+
========
|
| 206 |
+
|
| 207 |
+
>>> from sympy.physics.hep.gamma_matrices import GammaMatrix as G, \
|
| 208 |
+
LorentzIndex, _simplify_single_line
|
| 209 |
+
>>> from sympy.tensor.tensor import tensor_indices, TensorHead
|
| 210 |
+
>>> p = TensorHead('p', [LorentzIndex])
|
| 211 |
+
>>> i0,i1 = tensor_indices('i0:2', LorentzIndex)
|
| 212 |
+
>>> _simplify_single_line(G(i0)*G(i1)*p(-i1)*G(-i0)) + 2*G(i0)*p(-i0)
|
| 213 |
+
0
|
| 214 |
+
|
| 215 |
+
"""
|
| 216 |
+
t1, t2 = extract_type_tens(expression, GammaMatrix)
|
| 217 |
+
if t1 != 1:
|
| 218 |
+
t1 = kahane_simplify(t1)
|
| 219 |
+
res = t1*t2
|
| 220 |
+
return res
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def _trace_single_line(t):
|
| 224 |
+
"""
|
| 225 |
+
Evaluate the trace of a single gamma matrix line inside a ``TensExpr``.
|
| 226 |
+
|
| 227 |
+
Notes
|
| 228 |
+
=====
|
| 229 |
+
|
| 230 |
+
If there are ``DiracSpinorIndex.auto_left`` and ``DiracSpinorIndex.auto_right``
|
| 231 |
+
indices trace over them; otherwise traces are not implied (explain)
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
Examples
|
| 235 |
+
========
|
| 236 |
+
|
| 237 |
+
>>> from sympy.physics.hep.gamma_matrices import GammaMatrix as G, \
|
| 238 |
+
LorentzIndex, _trace_single_line
|
| 239 |
+
>>> from sympy.tensor.tensor import tensor_indices, TensorHead
|
| 240 |
+
>>> p = TensorHead('p', [LorentzIndex])
|
| 241 |
+
>>> i0,i1,i2,i3,i4,i5 = tensor_indices('i0:6', LorentzIndex)
|
| 242 |
+
>>> _trace_single_line(G(i0)*G(i1))
|
| 243 |
+
4*metric(i0, i1)
|
| 244 |
+
>>> _trace_single_line(G(i0)*p(-i0)*G(i1)*p(-i1)) - 4*p(i0)*p(-i0)
|
| 245 |
+
0
|
| 246 |
+
|
| 247 |
+
"""
|
| 248 |
+
def _trace_single_line1(t):
|
| 249 |
+
t = t.sorted_components()
|
| 250 |
+
components = t.components
|
| 251 |
+
ncomps = len(components)
|
| 252 |
+
g = LorentzIndex.metric
|
| 253 |
+
# gamma matirices are in a[i:j]
|
| 254 |
+
hit = 0
|
| 255 |
+
for i in range(ncomps):
|
| 256 |
+
if components[i] == GammaMatrix:
|
| 257 |
+
hit = 1
|
| 258 |
+
break
|
| 259 |
+
|
| 260 |
+
for j in range(i + hit, ncomps):
|
| 261 |
+
if components[j] != GammaMatrix:
|
| 262 |
+
break
|
| 263 |
+
else:
|
| 264 |
+
j = ncomps
|
| 265 |
+
numG = j - i
|
| 266 |
+
if numG == 0:
|
| 267 |
+
tcoeff = t.coeff
|
| 268 |
+
return t.nocoeff if tcoeff else t
|
| 269 |
+
if numG % 2 == 1:
|
| 270 |
+
return TensMul.from_data(S.Zero, [], [], [])
|
| 271 |
+
elif numG > 4:
|
| 272 |
+
# find the open matrix indices and connect them:
|
| 273 |
+
a = t.split()
|
| 274 |
+
ind1 = a[i].get_indices()[0]
|
| 275 |
+
ind2 = a[i + 1].get_indices()[0]
|
| 276 |
+
aa = a[:i] + a[i + 2:]
|
| 277 |
+
t1 = tensor_mul(*aa)*g(ind1, ind2)
|
| 278 |
+
t1 = t1.contract_metric(g)
|
| 279 |
+
args = [t1]
|
| 280 |
+
sign = 1
|
| 281 |
+
for k in range(i + 2, j):
|
| 282 |
+
sign = -sign
|
| 283 |
+
ind2 = a[k].get_indices()[0]
|
| 284 |
+
aa = a[:i] + a[i + 1:k] + a[k + 1:]
|
| 285 |
+
t2 = sign*tensor_mul(*aa)*g(ind1, ind2)
|
| 286 |
+
t2 = t2.contract_metric(g)
|
| 287 |
+
t2 = simplify_gpgp(t2, False)
|
| 288 |
+
args.append(t2)
|
| 289 |
+
t3 = TensAdd(*args)
|
| 290 |
+
t3 = _trace_single_line(t3)
|
| 291 |
+
return t3
|
| 292 |
+
else:
|
| 293 |
+
a = t.split()
|
| 294 |
+
t1 = _gamma_trace1(*a[i:j])
|
| 295 |
+
a2 = a[:i] + a[j:]
|
| 296 |
+
t2 = tensor_mul(*a2)
|
| 297 |
+
t3 = t1*t2
|
| 298 |
+
if not t3:
|
| 299 |
+
return t3
|
| 300 |
+
t3 = t3.contract_metric(g)
|
| 301 |
+
return t3
|
| 302 |
+
|
| 303 |
+
t = t.expand()
|
| 304 |
+
if isinstance(t, TensAdd):
|
| 305 |
+
a = [_trace_single_line1(x)*x.coeff for x in t.args]
|
| 306 |
+
return TensAdd(*a)
|
| 307 |
+
elif isinstance(t, (Tensor, TensMul)):
|
| 308 |
+
r = t.coeff*_trace_single_line1(t)
|
| 309 |
+
return r
|
| 310 |
+
else:
|
| 311 |
+
return trace(t)
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def _gamma_trace1(*a):
|
| 315 |
+
gctr = 4 # FIXME specific for d=4
|
| 316 |
+
g = LorentzIndex.metric
|
| 317 |
+
if not a:
|
| 318 |
+
return gctr
|
| 319 |
+
n = len(a)
|
| 320 |
+
if n%2 == 1:
|
| 321 |
+
#return TensMul.from_data(S.Zero, [], [], [])
|
| 322 |
+
return S.Zero
|
| 323 |
+
if n == 2:
|
| 324 |
+
ind0 = a[0].get_indices()[0]
|
| 325 |
+
ind1 = a[1].get_indices()[0]
|
| 326 |
+
return gctr*g(ind0, ind1)
|
| 327 |
+
if n == 4:
|
| 328 |
+
ind0 = a[0].get_indices()[0]
|
| 329 |
+
ind1 = a[1].get_indices()[0]
|
| 330 |
+
ind2 = a[2].get_indices()[0]
|
| 331 |
+
ind3 = a[3].get_indices()[0]
|
| 332 |
+
|
| 333 |
+
return gctr*(g(ind0, ind1)*g(ind2, ind3) - \
|
| 334 |
+
g(ind0, ind2)*g(ind1, ind3) + g(ind0, ind3)*g(ind1, ind2))
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def kahane_simplify(expression):
|
| 338 |
+
r"""
|
| 339 |
+
This function cancels contracted elements in a product of four
|
| 340 |
+
dimensional gamma matrices, resulting in an expression equal to the given
|
| 341 |
+
one, without the contracted gamma matrices.
|
| 342 |
+
|
| 343 |
+
Parameters
|
| 344 |
+
==========
|
| 345 |
+
|
| 346 |
+
`expression` the tensor expression containing the gamma matrices to simplify.
|
| 347 |
+
|
| 348 |
+
Notes
|
| 349 |
+
=====
|
| 350 |
+
|
| 351 |
+
If spinor indices are given, the matrices must be given in
|
| 352 |
+
the order given in the product.
|
| 353 |
+
|
| 354 |
+
Algorithm
|
| 355 |
+
=========
|
| 356 |
+
|
| 357 |
+
The idea behind the algorithm is to use some well-known identities,
|
| 358 |
+
i.e., for contractions enclosing an even number of `\gamma` matrices
|
| 359 |
+
|
| 360 |
+
`\gamma^\mu \gamma_{a_1} \cdots \gamma_{a_{2N}} \gamma_\mu = 2 (\gamma_{a_{2N}} \gamma_{a_1} \cdots \gamma_{a_{2N-1}} + \gamma_{a_{2N-1}} \cdots \gamma_{a_1} \gamma_{a_{2N}} )`
|
| 361 |
+
|
| 362 |
+
for an odd number of `\gamma` matrices
|
| 363 |
+
|
| 364 |
+
`\gamma^\mu \gamma_{a_1} \cdots \gamma_{a_{2N+1}} \gamma_\mu = -2 \gamma_{a_{2N+1}} \gamma_{a_{2N}} \cdots \gamma_{a_{1}}`
|
| 365 |
+
|
| 366 |
+
Instead of repeatedly applying these identities to cancel out all contracted indices,
|
| 367 |
+
it is possible to recognize the links that would result from such an operation,
|
| 368 |
+
the problem is thus reduced to a simple rearrangement of free gamma matrices.
|
| 369 |
+
|
| 370 |
+
Examples
|
| 371 |
+
========
|
| 372 |
+
|
| 373 |
+
When using, always remember that the original expression coefficient
|
| 374 |
+
has to be handled separately
|
| 375 |
+
|
| 376 |
+
>>> from sympy.physics.hep.gamma_matrices import GammaMatrix as G, LorentzIndex
|
| 377 |
+
>>> from sympy.physics.hep.gamma_matrices import kahane_simplify
|
| 378 |
+
>>> from sympy.tensor.tensor import tensor_indices
|
| 379 |
+
>>> i0, i1, i2 = tensor_indices('i0:3', LorentzIndex)
|
| 380 |
+
>>> ta = G(i0)*G(-i0)
|
| 381 |
+
>>> kahane_simplify(ta)
|
| 382 |
+
Matrix([
|
| 383 |
+
[4, 0, 0, 0],
|
| 384 |
+
[0, 4, 0, 0],
|
| 385 |
+
[0, 0, 4, 0],
|
| 386 |
+
[0, 0, 0, 4]])
|
| 387 |
+
>>> tb = G(i0)*G(i1)*G(-i0)
|
| 388 |
+
>>> kahane_simplify(tb)
|
| 389 |
+
-2*GammaMatrix(i1)
|
| 390 |
+
>>> t = G(i0)*G(-i0)
|
| 391 |
+
>>> kahane_simplify(t)
|
| 392 |
+
Matrix([
|
| 393 |
+
[4, 0, 0, 0],
|
| 394 |
+
[0, 4, 0, 0],
|
| 395 |
+
[0, 0, 4, 0],
|
| 396 |
+
[0, 0, 0, 4]])
|
| 397 |
+
>>> t = G(i0)*G(-i0)
|
| 398 |
+
>>> kahane_simplify(t)
|
| 399 |
+
Matrix([
|
| 400 |
+
[4, 0, 0, 0],
|
| 401 |
+
[0, 4, 0, 0],
|
| 402 |
+
[0, 0, 4, 0],
|
| 403 |
+
[0, 0, 0, 4]])
|
| 404 |
+
|
| 405 |
+
If there are no contractions, the same expression is returned
|
| 406 |
+
|
| 407 |
+
>>> tc = G(i0)*G(i1)
|
| 408 |
+
>>> kahane_simplify(tc)
|
| 409 |
+
GammaMatrix(i0)*GammaMatrix(i1)
|
| 410 |
+
|
| 411 |
+
References
|
| 412 |
+
==========
|
| 413 |
+
|
| 414 |
+
[1] Algorithm for Reducing Contracted Products of gamma Matrices,
|
| 415 |
+
Joseph Kahane, Journal of Mathematical Physics, Vol. 9, No. 10, October 1968.
|
| 416 |
+
"""
|
| 417 |
+
|
| 418 |
+
if isinstance(expression, Mul):
|
| 419 |
+
return expression
|
| 420 |
+
if isinstance(expression, TensAdd):
|
| 421 |
+
return TensAdd(*[kahane_simplify(arg) for arg in expression.args])
|
| 422 |
+
|
| 423 |
+
if isinstance(expression, Tensor):
|
| 424 |
+
return expression
|
| 425 |
+
|
| 426 |
+
assert isinstance(expression, TensMul)
|
| 427 |
+
|
| 428 |
+
gammas = expression.args
|
| 429 |
+
|
| 430 |
+
for gamma in gammas:
|
| 431 |
+
assert gamma.component == GammaMatrix
|
| 432 |
+
|
| 433 |
+
free = expression.free
|
| 434 |
+
# spinor_free = [_ for _ in expression.free_in_args if _[1] != 0]
|
| 435 |
+
|
| 436 |
+
# if len(spinor_free) == 2:
|
| 437 |
+
# spinor_free.sort(key=lambda x: x[2])
|
| 438 |
+
# assert spinor_free[0][1] == 1 and spinor_free[-1][1] == 2
|
| 439 |
+
# assert spinor_free[0][2] == 0
|
| 440 |
+
# elif spinor_free:
|
| 441 |
+
# raise ValueError('spinor indices do not match')
|
| 442 |
+
|
| 443 |
+
dum = []
|
| 444 |
+
for dum_pair in expression.dum:
|
| 445 |
+
if expression.index_types[dum_pair[0]] == LorentzIndex:
|
| 446 |
+
dum.append((dum_pair[0], dum_pair[1]))
|
| 447 |
+
|
| 448 |
+
dum = sorted(dum)
|
| 449 |
+
|
| 450 |
+
if len(dum) == 0: # or GammaMatrixHead:
|
| 451 |
+
# no contractions in `expression`, just return it.
|
| 452 |
+
return expression
|
| 453 |
+
|
| 454 |
+
# find the `first_dum_pos`, i.e. the position of the first contracted
|
| 455 |
+
# gamma matrix, Kahane's algorithm as described in his paper requires the
|
| 456 |
+
# gamma matrix expression to start with a contracted gamma matrix, this is
|
| 457 |
+
# a workaround which ignores possible initial free indices, and re-adds
|
| 458 |
+
# them later.
|
| 459 |
+
|
| 460 |
+
first_dum_pos = min(map(min, dum))
|
| 461 |
+
|
| 462 |
+
# for p1, p2, a1, a2 in expression.dum_in_args:
|
| 463 |
+
# if p1 != 0 or p2 != 0:
|
| 464 |
+
# # only Lorentz indices, skip Dirac indices:
|
| 465 |
+
# continue
|
| 466 |
+
# first_dum_pos = min(p1, p2)
|
| 467 |
+
# break
|
| 468 |
+
|
| 469 |
+
total_number = len(free) + len(dum)*2
|
| 470 |
+
number_of_contractions = len(dum)
|
| 471 |
+
|
| 472 |
+
free_pos = [None]*total_number
|
| 473 |
+
for i in free:
|
| 474 |
+
free_pos[i[1]] = i[0]
|
| 475 |
+
|
| 476 |
+
# `index_is_free` is a list of booleans, to identify index position
|
| 477 |
+
# and whether that index is free or dummy.
|
| 478 |
+
index_is_free = [False]*total_number
|
| 479 |
+
|
| 480 |
+
for i, indx in enumerate(free):
|
| 481 |
+
index_is_free[indx[1]] = True
|
| 482 |
+
|
| 483 |
+
# `links` is a dictionary containing the graph described in Kahane's paper,
|
| 484 |
+
# to every key correspond one or two values, representing the linked indices.
|
| 485 |
+
# All values in `links` are integers, negative numbers are used in the case
|
| 486 |
+
# where it is necessary to insert gamma matrices between free indices, in
|
| 487 |
+
# order to make Kahane's algorithm work (see paper).
|
| 488 |
+
links = {i: [] for i in range(first_dum_pos, total_number)}
|
| 489 |
+
|
| 490 |
+
# `cum_sign` is a step variable to mark the sign of every index, see paper.
|
| 491 |
+
cum_sign = -1
|
| 492 |
+
# `cum_sign_list` keeps storage for all `cum_sign` (every index).
|
| 493 |
+
cum_sign_list = [None]*total_number
|
| 494 |
+
block_free_count = 0
|
| 495 |
+
|
| 496 |
+
# multiply `resulting_coeff` by the coefficient parameter, the rest
|
| 497 |
+
# of the algorithm ignores a scalar coefficient.
|
| 498 |
+
resulting_coeff = S.One
|
| 499 |
+
|
| 500 |
+
# initialize a list of lists of indices. The outer list will contain all
|
| 501 |
+
# additive tensor expressions, while the inner list will contain the
|
| 502 |
+
# free indices (rearranged according to the algorithm).
|
| 503 |
+
resulting_indices = [[]]
|
| 504 |
+
|
| 505 |
+
# start to count the `connected_components`, which together with the number
|
| 506 |
+
# of contractions, determines a -1 or +1 factor to be multiplied.
|
| 507 |
+
connected_components = 1
|
| 508 |
+
|
| 509 |
+
# First loop: here we fill `cum_sign_list`, and draw the links
|
| 510 |
+
# among consecutive indices (they are stored in `links`). Links among
|
| 511 |
+
# non-consecutive indices will be drawn later.
|
| 512 |
+
for i, is_free in enumerate(index_is_free):
|
| 513 |
+
# if `expression` starts with free indices, they are ignored here;
|
| 514 |
+
# they are later added as they are to the beginning of all
|
| 515 |
+
# `resulting_indices` list of lists of indices.
|
| 516 |
+
if i < first_dum_pos:
|
| 517 |
+
continue
|
| 518 |
+
|
| 519 |
+
if is_free:
|
| 520 |
+
block_free_count += 1
|
| 521 |
+
# if previous index was free as well, draw an arch in `links`.
|
| 522 |
+
if block_free_count > 1:
|
| 523 |
+
links[i - 1].append(i)
|
| 524 |
+
links[i].append(i - 1)
|
| 525 |
+
else:
|
| 526 |
+
# Change the sign of the index (`cum_sign`) if the number of free
|
| 527 |
+
# indices preceding it is even.
|
| 528 |
+
cum_sign *= 1 if (block_free_count % 2) else -1
|
| 529 |
+
if block_free_count == 0 and i != first_dum_pos:
|
| 530 |
+
# check if there are two consecutive dummy indices:
|
| 531 |
+
# in this case create virtual indices with negative position,
|
| 532 |
+
# these "virtual" indices represent the insertion of two
|
| 533 |
+
# gamma^0 matrices to separate consecutive dummy indices, as
|
| 534 |
+
# Kahane's algorithm requires dummy indices to be separated by
|
| 535 |
+
# free indices. The product of two gamma^0 matrices is unity,
|
| 536 |
+
# so the new expression being examined is the same as the
|
| 537 |
+
# original one.
|
| 538 |
+
if cum_sign == -1:
|
| 539 |
+
links[-1-i] = [-1-i+1]
|
| 540 |
+
links[-1-i+1] = [-1-i]
|
| 541 |
+
if (i - cum_sign) in links:
|
| 542 |
+
if i != first_dum_pos:
|
| 543 |
+
links[i].append(i - cum_sign)
|
| 544 |
+
if block_free_count != 0:
|
| 545 |
+
if i - cum_sign < len(index_is_free):
|
| 546 |
+
if index_is_free[i - cum_sign]:
|
| 547 |
+
links[i - cum_sign].append(i)
|
| 548 |
+
block_free_count = 0
|
| 549 |
+
|
| 550 |
+
cum_sign_list[i] = cum_sign
|
| 551 |
+
|
| 552 |
+
# The previous loop has only created links between consecutive free indices,
|
| 553 |
+
# it is necessary to properly create links among dummy (contracted) indices,
|
| 554 |
+
# according to the rules described in Kahane's paper. There is only one exception
|
| 555 |
+
# to Kahane's rules: the negative indices, which handle the case of some
|
| 556 |
+
# consecutive free indices (Kahane's paper just describes dummy indices
|
| 557 |
+
# separated by free indices, hinting that free indices can be added without
|
| 558 |
+
# altering the expression result).
|
| 559 |
+
for i in dum:
|
| 560 |
+
# get the positions of the two contracted indices:
|
| 561 |
+
pos1 = i[0]
|
| 562 |
+
pos2 = i[1]
|
| 563 |
+
|
| 564 |
+
# create Kahane's upper links, i.e. the upper arcs between dummy
|
| 565 |
+
# (i.e. contracted) indices:
|
| 566 |
+
links[pos1].append(pos2)
|
| 567 |
+
links[pos2].append(pos1)
|
| 568 |
+
|
| 569 |
+
# create Kahane's lower links, this corresponds to the arcs below
|
| 570 |
+
# the line described in the paper:
|
| 571 |
+
|
| 572 |
+
# first we move `pos1` and `pos2` according to the sign of the indices:
|
| 573 |
+
linkpos1 = pos1 + cum_sign_list[pos1]
|
| 574 |
+
linkpos2 = pos2 + cum_sign_list[pos2]
|
| 575 |
+
|
| 576 |
+
# otherwise, perform some checks before creating the lower arcs:
|
| 577 |
+
|
| 578 |
+
# make sure we are not exceeding the total number of indices:
|
| 579 |
+
if linkpos1 >= total_number:
|
| 580 |
+
continue
|
| 581 |
+
if linkpos2 >= total_number:
|
| 582 |
+
continue
|
| 583 |
+
|
| 584 |
+
# make sure we are not below the first dummy index in `expression`:
|
| 585 |
+
if linkpos1 < first_dum_pos:
|
| 586 |
+
continue
|
| 587 |
+
if linkpos2 < first_dum_pos:
|
| 588 |
+
continue
|
| 589 |
+
|
| 590 |
+
# check if the previous loop created "virtual" indices between dummy
|
| 591 |
+
# indices, in such a case relink `linkpos1` and `linkpos2`:
|
| 592 |
+
if (-1-linkpos1) in links:
|
| 593 |
+
linkpos1 = -1-linkpos1
|
| 594 |
+
if (-1-linkpos2) in links:
|
| 595 |
+
linkpos2 = -1-linkpos2
|
| 596 |
+
|
| 597 |
+
# move only if not next to free index:
|
| 598 |
+
if linkpos1 >= 0 and not index_is_free[linkpos1]:
|
| 599 |
+
linkpos1 = pos1
|
| 600 |
+
|
| 601 |
+
if linkpos2 >=0 and not index_is_free[linkpos2]:
|
| 602 |
+
linkpos2 = pos2
|
| 603 |
+
|
| 604 |
+
# create the lower arcs:
|
| 605 |
+
if linkpos2 not in links[linkpos1]:
|
| 606 |
+
links[linkpos1].append(linkpos2)
|
| 607 |
+
if linkpos1 not in links[linkpos2]:
|
| 608 |
+
links[linkpos2].append(linkpos1)
|
| 609 |
+
|
| 610 |
+
# This loop starts from the `first_dum_pos` index (first dummy index)
|
| 611 |
+
# walks through the graph deleting the visited indices from `links`,
|
| 612 |
+
# it adds a gamma matrix for every free index in encounters, while it
|
| 613 |
+
# completely ignores dummy indices and virtual indices.
|
| 614 |
+
pointer = first_dum_pos
|
| 615 |
+
previous_pointer = 0
|
| 616 |
+
while True:
|
| 617 |
+
if pointer in links:
|
| 618 |
+
next_ones = links.pop(pointer)
|
| 619 |
+
else:
|
| 620 |
+
break
|
| 621 |
+
|
| 622 |
+
if previous_pointer in next_ones:
|
| 623 |
+
next_ones.remove(previous_pointer)
|
| 624 |
+
|
| 625 |
+
previous_pointer = pointer
|
| 626 |
+
|
| 627 |
+
if next_ones:
|
| 628 |
+
pointer = next_ones[0]
|
| 629 |
+
else:
|
| 630 |
+
break
|
| 631 |
+
|
| 632 |
+
if pointer == previous_pointer:
|
| 633 |
+
break
|
| 634 |
+
if pointer >=0 and free_pos[pointer] is not None:
|
| 635 |
+
for ri in resulting_indices:
|
| 636 |
+
ri.append(free_pos[pointer])
|
| 637 |
+
|
| 638 |
+
# The following loop removes the remaining connected components in `links`.
|
| 639 |
+
# If there are free indices inside a connected component, it gives a
|
| 640 |
+
# contribution to the resulting expression given by the factor
|
| 641 |
+
# `gamma_a gamma_b ... gamma_z + gamma_z ... gamma_b gamma_a`, in Kahanes's
|
| 642 |
+
# paper represented as {gamma_a, gamma_b, ... , gamma_z},
|
| 643 |
+
# virtual indices are ignored. The variable `connected_components` is
|
| 644 |
+
# increased by one for every connected component this loop encounters.
|
| 645 |
+
|
| 646 |
+
# If the connected component has virtual and dummy indices only
|
| 647 |
+
# (no free indices), it contributes to `resulting_indices` by a factor of two.
|
| 648 |
+
# The multiplication by two is a result of the
|
| 649 |
+
# factor {gamma^0, gamma^0} = 2 I, as it appears in Kahane's paper.
|
| 650 |
+
# Note: curly brackets are meant as in the paper, as a generalized
|
| 651 |
+
# multi-element anticommutator!
|
| 652 |
+
|
| 653 |
+
while links:
|
| 654 |
+
connected_components += 1
|
| 655 |
+
pointer = min(links.keys())
|
| 656 |
+
previous_pointer = pointer
|
| 657 |
+
# the inner loop erases the visited indices from `links`, and it adds
|
| 658 |
+
# all free indices to `prepend_indices` list, virtual indices are
|
| 659 |
+
# ignored.
|
| 660 |
+
prepend_indices = []
|
| 661 |
+
while True:
|
| 662 |
+
if pointer in links:
|
| 663 |
+
next_ones = links.pop(pointer)
|
| 664 |
+
else:
|
| 665 |
+
break
|
| 666 |
+
|
| 667 |
+
if previous_pointer in next_ones:
|
| 668 |
+
if len(next_ones) > 1:
|
| 669 |
+
next_ones.remove(previous_pointer)
|
| 670 |
+
|
| 671 |
+
previous_pointer = pointer
|
| 672 |
+
|
| 673 |
+
if next_ones:
|
| 674 |
+
pointer = next_ones[0]
|
| 675 |
+
|
| 676 |
+
if pointer >= first_dum_pos and free_pos[pointer] is not None:
|
| 677 |
+
prepend_indices.insert(0, free_pos[pointer])
|
| 678 |
+
# if `prepend_indices` is void, it means there are no free indices
|
| 679 |
+
# in the loop (and it can be shown that there must be a virtual index),
|
| 680 |
+
# loops of virtual indices only contribute by a factor of two:
|
| 681 |
+
if len(prepend_indices) == 0:
|
| 682 |
+
resulting_coeff *= 2
|
| 683 |
+
# otherwise, add the free indices in `prepend_indices` to
|
| 684 |
+
# the `resulting_indices`:
|
| 685 |
+
else:
|
| 686 |
+
expr1 = prepend_indices
|
| 687 |
+
expr2 = list(reversed(prepend_indices))
|
| 688 |
+
resulting_indices = [expri + ri for ri in resulting_indices for expri in (expr1, expr2)]
|
| 689 |
+
|
| 690 |
+
# sign correction, as described in Kahane's paper:
|
| 691 |
+
resulting_coeff *= -1 if (number_of_contractions - connected_components + 1) % 2 else 1
|
| 692 |
+
# power of two factor, as described in Kahane's paper:
|
| 693 |
+
resulting_coeff *= 2**(number_of_contractions)
|
| 694 |
+
|
| 695 |
+
# If `first_dum_pos` is not zero, it means that there are trailing free gamma
|
| 696 |
+
# matrices in front of `expression`, so multiply by them:
|
| 697 |
+
resulting_indices = [ free_pos[0:first_dum_pos] + ri for ri in resulting_indices ]
|
| 698 |
+
|
| 699 |
+
resulting_expr = S.Zero
|
| 700 |
+
for i in resulting_indices:
|
| 701 |
+
temp_expr = S.One
|
| 702 |
+
for j in i:
|
| 703 |
+
temp_expr *= GammaMatrix(j)
|
| 704 |
+
resulting_expr += temp_expr
|
| 705 |
+
|
| 706 |
+
t = resulting_coeff * resulting_expr
|
| 707 |
+
t1 = None
|
| 708 |
+
if isinstance(t, TensAdd):
|
| 709 |
+
t1 = t.args[0]
|
| 710 |
+
elif isinstance(t, TensMul):
|
| 711 |
+
t1 = t
|
| 712 |
+
if t1:
|
| 713 |
+
pass
|
| 714 |
+
else:
|
| 715 |
+
t = eye(4)*t
|
| 716 |
+
return t
|
lib/python3.12/site-packages/sympy/physics/hep/tests/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/sympy/physics/hep/tests/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (202 Bytes). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/hep/tests/__pycache__/test_gamma_matrices.cpython-312.pyc
ADDED
|
Binary file (35.9 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/hep/tests/test_gamma_matrices.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy.matrices.dense import eye, Matrix
|
| 2 |
+
from sympy.tensor.tensor import tensor_indices, TensorHead, tensor_heads, \
|
| 3 |
+
TensExpr, canon_bp
|
| 4 |
+
from sympy.physics.hep.gamma_matrices import GammaMatrix as G, LorentzIndex, \
|
| 5 |
+
kahane_simplify, gamma_trace, _simplify_single_line, simplify_gamma_expression
|
| 6 |
+
from sympy import Symbol
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def _is_tensor_eq(arg1, arg2):
|
| 10 |
+
arg1 = canon_bp(arg1)
|
| 11 |
+
arg2 = canon_bp(arg2)
|
| 12 |
+
if isinstance(arg1, TensExpr):
|
| 13 |
+
return arg1.equals(arg2)
|
| 14 |
+
elif isinstance(arg2, TensExpr):
|
| 15 |
+
return arg2.equals(arg1)
|
| 16 |
+
return arg1 == arg2
|
| 17 |
+
|
| 18 |
+
def execute_gamma_simplify_tests_for_function(tfunc, D):
|
| 19 |
+
"""
|
| 20 |
+
Perform tests to check if sfunc is able to simplify gamma matrix expressions.
|
| 21 |
+
|
| 22 |
+
Parameters
|
| 23 |
+
==========
|
| 24 |
+
|
| 25 |
+
`sfunc` a function to simplify a `TIDS`, shall return the simplified `TIDS`.
|
| 26 |
+
`D` the number of dimension (in most cases `D=4`).
|
| 27 |
+
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
mu, nu, rho, sigma = tensor_indices("mu, nu, rho, sigma", LorentzIndex)
|
| 31 |
+
a1, a2, a3, a4, a5, a6 = tensor_indices("a1:7", LorentzIndex)
|
| 32 |
+
mu11, mu12, mu21, mu31, mu32, mu41, mu51, mu52 = tensor_indices("mu11, mu12, mu21, mu31, mu32, mu41, mu51, mu52", LorentzIndex)
|
| 33 |
+
mu61, mu71, mu72 = tensor_indices("mu61, mu71, mu72", LorentzIndex)
|
| 34 |
+
m0, m1, m2, m3, m4, m5, m6 = tensor_indices("m0:7", LorentzIndex)
|
| 35 |
+
|
| 36 |
+
def g(xx, yy):
|
| 37 |
+
return (G(xx)*G(yy) + G(yy)*G(xx))/2
|
| 38 |
+
|
| 39 |
+
# Some examples taken from Kahane's paper, 4 dim only:
|
| 40 |
+
if D == 4:
|
| 41 |
+
t = (G(a1)*G(mu11)*G(a2)*G(mu21)*G(-a1)*G(mu31)*G(-a2))
|
| 42 |
+
assert _is_tensor_eq(tfunc(t), -4*G(mu11)*G(mu31)*G(mu21) - 4*G(mu31)*G(mu11)*G(mu21))
|
| 43 |
+
|
| 44 |
+
t = (G(a1)*G(mu11)*G(mu12)*\
|
| 45 |
+
G(a2)*G(mu21)*\
|
| 46 |
+
G(a3)*G(mu31)*G(mu32)*\
|
| 47 |
+
G(a4)*G(mu41)*\
|
| 48 |
+
G(-a2)*G(mu51)*G(mu52)*\
|
| 49 |
+
G(-a1)*G(mu61)*\
|
| 50 |
+
G(-a3)*G(mu71)*G(mu72)*\
|
| 51 |
+
G(-a4))
|
| 52 |
+
assert _is_tensor_eq(tfunc(t), \
|
| 53 |
+
16*G(mu31)*G(mu32)*G(mu72)*G(mu71)*G(mu11)*G(mu52)*G(mu51)*G(mu12)*G(mu61)*G(mu21)*G(mu41) + 16*G(mu31)*G(mu32)*G(mu72)*G(mu71)*G(mu12)*G(mu51)*G(mu52)*G(mu11)*G(mu61)*G(mu21)*G(mu41) + 16*G(mu71)*G(mu72)*G(mu32)*G(mu31)*G(mu11)*G(mu52)*G(mu51)*G(mu12)*G(mu61)*G(mu21)*G(mu41) + 16*G(mu71)*G(mu72)*G(mu32)*G(mu31)*G(mu12)*G(mu51)*G(mu52)*G(mu11)*G(mu61)*G(mu21)*G(mu41))
|
| 54 |
+
|
| 55 |
+
# Fully Lorentz-contracted expressions, these return scalars:
|
| 56 |
+
|
| 57 |
+
def add_delta(ne):
|
| 58 |
+
return ne * eye(4) # DiracSpinorIndex.delta(DiracSpinorIndex.auto_left, -DiracSpinorIndex.auto_right)
|
| 59 |
+
|
| 60 |
+
t = (G(mu)*G(-mu))
|
| 61 |
+
ts = add_delta(D)
|
| 62 |
+
assert _is_tensor_eq(tfunc(t), ts)
|
| 63 |
+
|
| 64 |
+
t = (G(mu)*G(nu)*G(-mu)*G(-nu))
|
| 65 |
+
ts = add_delta(2*D - D**2) # -8
|
| 66 |
+
assert _is_tensor_eq(tfunc(t), ts)
|
| 67 |
+
|
| 68 |
+
t = (G(mu)*G(nu)*G(-nu)*G(-mu))
|
| 69 |
+
ts = add_delta(D**2) # 16
|
| 70 |
+
assert _is_tensor_eq(tfunc(t), ts)
|
| 71 |
+
|
| 72 |
+
t = (G(mu)*G(nu)*G(-rho)*G(-nu)*G(-mu)*G(rho))
|
| 73 |
+
ts = add_delta(4*D - 4*D**2 + D**3) # 16
|
| 74 |
+
assert _is_tensor_eq(tfunc(t), ts)
|
| 75 |
+
|
| 76 |
+
t = (G(mu)*G(nu)*G(rho)*G(-rho)*G(-nu)*G(-mu))
|
| 77 |
+
ts = add_delta(D**3) # 64
|
| 78 |
+
assert _is_tensor_eq(tfunc(t), ts)
|
| 79 |
+
|
| 80 |
+
t = (G(a1)*G(a2)*G(a3)*G(a4)*G(-a3)*G(-a1)*G(-a2)*G(-a4))
|
| 81 |
+
ts = add_delta(-8*D + 16*D**2 - 8*D**3 + D**4) # -32
|
| 82 |
+
assert _is_tensor_eq(tfunc(t), ts)
|
| 83 |
+
|
| 84 |
+
t = (G(-mu)*G(-nu)*G(-rho)*G(-sigma)*G(nu)*G(mu)*G(sigma)*G(rho))
|
| 85 |
+
ts = add_delta(-16*D + 24*D**2 - 8*D**3 + D**4) # 64
|
| 86 |
+
assert _is_tensor_eq(tfunc(t), ts)
|
| 87 |
+
|
| 88 |
+
t = (G(-mu)*G(nu)*G(-rho)*G(sigma)*G(rho)*G(-nu)*G(mu)*G(-sigma))
|
| 89 |
+
ts = add_delta(8*D - 12*D**2 + 6*D**3 - D**4) # -32
|
| 90 |
+
assert _is_tensor_eq(tfunc(t), ts)
|
| 91 |
+
|
| 92 |
+
t = (G(a1)*G(a2)*G(a3)*G(a4)*G(a5)*G(-a3)*G(-a2)*G(-a1)*G(-a5)*G(-a4))
|
| 93 |
+
ts = add_delta(64*D - 112*D**2 + 60*D**3 - 12*D**4 + D**5) # 256
|
| 94 |
+
assert _is_tensor_eq(tfunc(t), ts)
|
| 95 |
+
|
| 96 |
+
t = (G(a1)*G(a2)*G(a3)*G(a4)*G(a5)*G(-a3)*G(-a1)*G(-a2)*G(-a4)*G(-a5))
|
| 97 |
+
ts = add_delta(64*D - 120*D**2 + 72*D**3 - 16*D**4 + D**5) # -128
|
| 98 |
+
assert _is_tensor_eq(tfunc(t), ts)
|
| 99 |
+
|
| 100 |
+
t = (G(a1)*G(a2)*G(a3)*G(a4)*G(a5)*G(a6)*G(-a3)*G(-a2)*G(-a1)*G(-a6)*G(-a5)*G(-a4))
|
| 101 |
+
ts = add_delta(416*D - 816*D**2 + 528*D**3 - 144*D**4 + 18*D**5 - D**6) # -128
|
| 102 |
+
assert _is_tensor_eq(tfunc(t), ts)
|
| 103 |
+
|
| 104 |
+
t = (G(a1)*G(a2)*G(a3)*G(a4)*G(a5)*G(a6)*G(-a2)*G(-a3)*G(-a1)*G(-a6)*G(-a4)*G(-a5))
|
| 105 |
+
ts = add_delta(416*D - 848*D**2 + 584*D**3 - 172*D**4 + 22*D**5 - D**6) # -128
|
| 106 |
+
assert _is_tensor_eq(tfunc(t), ts)
|
| 107 |
+
|
| 108 |
+
# Expressions with free indices:
|
| 109 |
+
|
| 110 |
+
t = (G(mu)*G(nu)*G(rho)*G(sigma)*G(-mu))
|
| 111 |
+
assert _is_tensor_eq(tfunc(t), (-2*G(sigma)*G(rho)*G(nu) + (4-D)*G(nu)*G(rho)*G(sigma)))
|
| 112 |
+
|
| 113 |
+
t = (G(mu)*G(nu)*G(-mu))
|
| 114 |
+
assert _is_tensor_eq(tfunc(t), (2-D)*G(nu))
|
| 115 |
+
|
| 116 |
+
t = (G(mu)*G(nu)*G(rho)*G(-mu))
|
| 117 |
+
assert _is_tensor_eq(tfunc(t), 2*G(nu)*G(rho) + 2*G(rho)*G(nu) - (4-D)*G(nu)*G(rho))
|
| 118 |
+
|
| 119 |
+
t = 2*G(m2)*G(m0)*G(m1)*G(-m0)*G(-m1)
|
| 120 |
+
st = tfunc(t)
|
| 121 |
+
assert _is_tensor_eq(st, (D*(-2*D + 4))*G(m2))
|
| 122 |
+
|
| 123 |
+
t = G(m2)*G(m0)*G(m1)*G(-m0)*G(-m2)
|
| 124 |
+
st = tfunc(t)
|
| 125 |
+
assert _is_tensor_eq(st, ((-D + 2)**2)*G(m1))
|
| 126 |
+
|
| 127 |
+
t = G(m0)*G(m1)*G(m2)*G(m3)*G(-m1)
|
| 128 |
+
st = tfunc(t)
|
| 129 |
+
assert _is_tensor_eq(st, (D - 4)*G(m0)*G(m2)*G(m3) + 4*G(m0)*g(m2, m3))
|
| 130 |
+
|
| 131 |
+
t = G(m0)*G(m1)*G(m2)*G(m3)*G(-m1)*G(-m0)
|
| 132 |
+
st = tfunc(t)
|
| 133 |
+
assert _is_tensor_eq(st, ((D - 4)**2)*G(m2)*G(m3) + (8*D - 16)*g(m2, m3))
|
| 134 |
+
|
| 135 |
+
t = G(m2)*G(m0)*G(m1)*G(-m2)*G(-m0)
|
| 136 |
+
st = tfunc(t)
|
| 137 |
+
assert _is_tensor_eq(st, ((-D + 2)*(D - 4) + 4)*G(m1))
|
| 138 |
+
|
| 139 |
+
t = G(m3)*G(m1)*G(m0)*G(m2)*G(-m3)*G(-m0)*G(-m2)
|
| 140 |
+
st = tfunc(t)
|
| 141 |
+
assert _is_tensor_eq(st, (-4*D + (-D + 2)**2*(D - 4) + 8)*G(m1))
|
| 142 |
+
|
| 143 |
+
t = 2*G(m0)*G(m1)*G(m2)*G(m3)*G(-m0)
|
| 144 |
+
st = tfunc(t)
|
| 145 |
+
assert _is_tensor_eq(st, ((-2*D + 8)*G(m1)*G(m2)*G(m3) - 4*G(m3)*G(m2)*G(m1)))
|
| 146 |
+
|
| 147 |
+
t = G(m5)*G(m0)*G(m1)*G(m4)*G(m2)*G(-m4)*G(m3)*G(-m0)
|
| 148 |
+
st = tfunc(t)
|
| 149 |
+
assert _is_tensor_eq(st, (((-D + 2)*(-D + 4))*G(m5)*G(m1)*G(m2)*G(m3) + (2*D - 4)*G(m5)*G(m3)*G(m2)*G(m1)))
|
| 150 |
+
|
| 151 |
+
t = -G(m0)*G(m1)*G(m2)*G(m3)*G(-m0)*G(m4)
|
| 152 |
+
st = tfunc(t)
|
| 153 |
+
assert _is_tensor_eq(st, ((D - 4)*G(m1)*G(m2)*G(m3)*G(m4) + 2*G(m3)*G(m2)*G(m1)*G(m4)))
|
| 154 |
+
|
| 155 |
+
t = G(-m5)*G(m0)*G(m1)*G(m2)*G(m3)*G(m4)*G(-m0)*G(m5)
|
| 156 |
+
st = tfunc(t)
|
| 157 |
+
|
| 158 |
+
result1 = ((-D + 4)**2 + 4)*G(m1)*G(m2)*G(m3)*G(m4) +\
|
| 159 |
+
(4*D - 16)*G(m3)*G(m2)*G(m1)*G(m4) + (4*D - 16)*G(m4)*G(m1)*G(m2)*G(m3)\
|
| 160 |
+
+ 4*G(m2)*G(m1)*G(m4)*G(m3) + 4*G(m3)*G(m4)*G(m1)*G(m2) +\
|
| 161 |
+
4*G(m4)*G(m3)*G(m2)*G(m1)
|
| 162 |
+
|
| 163 |
+
# Kahane's algorithm yields this result, which is equivalent to `result1`
|
| 164 |
+
# in four dimensions, but is not automatically recognized as equal:
|
| 165 |
+
result2 = 8*G(m1)*G(m2)*G(m3)*G(m4) + 8*G(m4)*G(m3)*G(m2)*G(m1)
|
| 166 |
+
|
| 167 |
+
if D == 4:
|
| 168 |
+
assert _is_tensor_eq(st, (result1)) or _is_tensor_eq(st, (result2))
|
| 169 |
+
else:
|
| 170 |
+
assert _is_tensor_eq(st, (result1))
|
| 171 |
+
|
| 172 |
+
# and a few very simple cases, with no contracted indices:
|
| 173 |
+
|
| 174 |
+
t = G(m0)
|
| 175 |
+
st = tfunc(t)
|
| 176 |
+
assert _is_tensor_eq(st, t)
|
| 177 |
+
|
| 178 |
+
t = -7*G(m0)
|
| 179 |
+
st = tfunc(t)
|
| 180 |
+
assert _is_tensor_eq(st, t)
|
| 181 |
+
|
| 182 |
+
t = 224*G(m0)*G(m1)*G(-m2)*G(m3)
|
| 183 |
+
st = tfunc(t)
|
| 184 |
+
assert _is_tensor_eq(st, t)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def test_kahane_algorithm():
|
| 188 |
+
# Wrap this function to convert to and from TIDS:
|
| 189 |
+
|
| 190 |
+
def tfunc(e):
|
| 191 |
+
return _simplify_single_line(e)
|
| 192 |
+
|
| 193 |
+
execute_gamma_simplify_tests_for_function(tfunc, D=4)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def test_kahane_simplify1():
|
| 197 |
+
i0,i1,i2,i3,i4,i5,i6,i7,i8,i9,i10,i11,i12,i13,i14,i15 = tensor_indices('i0:16', LorentzIndex)
|
| 198 |
+
mu, nu, rho, sigma = tensor_indices("mu, nu, rho, sigma", LorentzIndex)
|
| 199 |
+
D = 4
|
| 200 |
+
t = G(i0)*G(i1)
|
| 201 |
+
r = kahane_simplify(t)
|
| 202 |
+
assert r.equals(t)
|
| 203 |
+
|
| 204 |
+
t = G(i0)*G(i1)*G(-i0)
|
| 205 |
+
r = kahane_simplify(t)
|
| 206 |
+
assert r.equals(-2*G(i1))
|
| 207 |
+
t = G(i0)*G(i1)*G(-i0)
|
| 208 |
+
r = kahane_simplify(t)
|
| 209 |
+
assert r.equals(-2*G(i1))
|
| 210 |
+
|
| 211 |
+
t = G(i0)*G(i1)
|
| 212 |
+
r = kahane_simplify(t)
|
| 213 |
+
assert r.equals(t)
|
| 214 |
+
t = G(i0)*G(i1)
|
| 215 |
+
r = kahane_simplify(t)
|
| 216 |
+
assert r.equals(t)
|
| 217 |
+
t = G(i0)*G(-i0)
|
| 218 |
+
r = kahane_simplify(t)
|
| 219 |
+
assert r.equals(4*eye(4))
|
| 220 |
+
t = G(i0)*G(-i0)
|
| 221 |
+
r = kahane_simplify(t)
|
| 222 |
+
assert r.equals(4*eye(4))
|
| 223 |
+
t = G(i0)*G(-i0)
|
| 224 |
+
r = kahane_simplify(t)
|
| 225 |
+
assert r.equals(4*eye(4))
|
| 226 |
+
t = G(i0)*G(i1)*G(-i0)
|
| 227 |
+
r = kahane_simplify(t)
|
| 228 |
+
assert r.equals(-2*G(i1))
|
| 229 |
+
t = G(i0)*G(i1)*G(-i0)*G(-i1)
|
| 230 |
+
r = kahane_simplify(t)
|
| 231 |
+
assert r.equals((2*D - D**2)*eye(4))
|
| 232 |
+
t = G(i0)*G(i1)*G(-i0)*G(-i1)
|
| 233 |
+
r = kahane_simplify(t)
|
| 234 |
+
assert r.equals((2*D - D**2)*eye(4))
|
| 235 |
+
t = G(i0)*G(-i0)*G(i1)*G(-i1)
|
| 236 |
+
r = kahane_simplify(t)
|
| 237 |
+
assert r.equals(16*eye(4))
|
| 238 |
+
t = (G(mu)*G(nu)*G(-nu)*G(-mu))
|
| 239 |
+
r = kahane_simplify(t)
|
| 240 |
+
assert r.equals(D**2*eye(4))
|
| 241 |
+
t = (G(mu)*G(nu)*G(-nu)*G(-mu))
|
| 242 |
+
r = kahane_simplify(t)
|
| 243 |
+
assert r.equals(D**2*eye(4))
|
| 244 |
+
t = (G(mu)*G(nu)*G(-nu)*G(-mu))
|
| 245 |
+
r = kahane_simplify(t)
|
| 246 |
+
assert r.equals(D**2*eye(4))
|
| 247 |
+
t = (G(mu)*G(nu)*G(-rho)*G(-nu)*G(-mu)*G(rho))
|
| 248 |
+
r = kahane_simplify(t)
|
| 249 |
+
assert r.equals((4*D - 4*D**2 + D**3)*eye(4))
|
| 250 |
+
t = (G(-mu)*G(-nu)*G(-rho)*G(-sigma)*G(nu)*G(mu)*G(sigma)*G(rho))
|
| 251 |
+
r = kahane_simplify(t)
|
| 252 |
+
assert r.equals((-16*D + 24*D**2 - 8*D**3 + D**4)*eye(4))
|
| 253 |
+
t = (G(-mu)*G(nu)*G(-rho)*G(sigma)*G(rho)*G(-nu)*G(mu)*G(-sigma))
|
| 254 |
+
r = kahane_simplify(t)
|
| 255 |
+
assert r.equals((8*D - 12*D**2 + 6*D**3 - D**4)*eye(4))
|
| 256 |
+
|
| 257 |
+
# Expressions with free indices:
|
| 258 |
+
t = (G(mu)*G(nu)*G(rho)*G(sigma)*G(-mu))
|
| 259 |
+
r = kahane_simplify(t)
|
| 260 |
+
assert r.equals(-2*G(sigma)*G(rho)*G(nu))
|
| 261 |
+
t = (G(mu)*G(-mu)*G(rho)*G(sigma))
|
| 262 |
+
r = kahane_simplify(t)
|
| 263 |
+
assert r.equals(4*G(rho)*G(sigma))
|
| 264 |
+
t = (G(rho)*G(sigma)*G(mu)*G(-mu))
|
| 265 |
+
r = kahane_simplify(t)
|
| 266 |
+
assert r.equals(4*G(rho)*G(sigma))
|
| 267 |
+
|
| 268 |
+
def test_gamma_matrix_class():
|
| 269 |
+
i, j, k = tensor_indices('i,j,k', LorentzIndex)
|
| 270 |
+
|
| 271 |
+
# define another type of TensorHead to see if exprs are correctly handled:
|
| 272 |
+
A = TensorHead('A', [LorentzIndex])
|
| 273 |
+
|
| 274 |
+
t = A(k)*G(i)*G(-i)
|
| 275 |
+
ts = simplify_gamma_expression(t)
|
| 276 |
+
assert _is_tensor_eq(ts, Matrix([
|
| 277 |
+
[4, 0, 0, 0],
|
| 278 |
+
[0, 4, 0, 0],
|
| 279 |
+
[0, 0, 4, 0],
|
| 280 |
+
[0, 0, 0, 4]])*A(k))
|
| 281 |
+
|
| 282 |
+
t = G(i)*A(k)*G(j)
|
| 283 |
+
ts = simplify_gamma_expression(t)
|
| 284 |
+
assert _is_tensor_eq(ts, A(k)*G(i)*G(j))
|
| 285 |
+
|
| 286 |
+
execute_gamma_simplify_tests_for_function(simplify_gamma_expression, D=4)
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def test_gamma_matrix_trace():
|
| 290 |
+
g = LorentzIndex.metric
|
| 291 |
+
|
| 292 |
+
m0, m1, m2, m3, m4, m5, m6 = tensor_indices('m0:7', LorentzIndex)
|
| 293 |
+
n0, n1, n2, n3, n4, n5 = tensor_indices('n0:6', LorentzIndex)
|
| 294 |
+
|
| 295 |
+
# working in D=4 dimensions
|
| 296 |
+
D = 4
|
| 297 |
+
|
| 298 |
+
# traces of odd number of gamma matrices are zero:
|
| 299 |
+
t = G(m0)
|
| 300 |
+
t1 = gamma_trace(t)
|
| 301 |
+
assert t1.equals(0)
|
| 302 |
+
|
| 303 |
+
t = G(m0)*G(m1)*G(m2)
|
| 304 |
+
t1 = gamma_trace(t)
|
| 305 |
+
assert t1.equals(0)
|
| 306 |
+
|
| 307 |
+
t = G(m0)*G(m1)*G(-m0)
|
| 308 |
+
t1 = gamma_trace(t)
|
| 309 |
+
assert t1.equals(0)
|
| 310 |
+
|
| 311 |
+
t = G(m0)*G(m1)*G(m2)*G(m3)*G(m4)
|
| 312 |
+
t1 = gamma_trace(t)
|
| 313 |
+
assert t1.equals(0)
|
| 314 |
+
|
| 315 |
+
# traces without internal contractions:
|
| 316 |
+
t = G(m0)*G(m1)
|
| 317 |
+
t1 = gamma_trace(t)
|
| 318 |
+
assert _is_tensor_eq(t1, 4*g(m0, m1))
|
| 319 |
+
|
| 320 |
+
t = G(m0)*G(m1)*G(m2)*G(m3)
|
| 321 |
+
t1 = gamma_trace(t)
|
| 322 |
+
t2 = -4*g(m0, m2)*g(m1, m3) + 4*g(m0, m1)*g(m2, m3) + 4*g(m0, m3)*g(m1, m2)
|
| 323 |
+
assert _is_tensor_eq(t1, t2)
|
| 324 |
+
|
| 325 |
+
t = G(m0)*G(m1)*G(m2)*G(m3)*G(m4)*G(m5)
|
| 326 |
+
t1 = gamma_trace(t)
|
| 327 |
+
t2 = t1*g(-m0, -m5)
|
| 328 |
+
t2 = t2.contract_metric(g)
|
| 329 |
+
assert _is_tensor_eq(t2, D*gamma_trace(G(m1)*G(m2)*G(m3)*G(m4)))
|
| 330 |
+
|
| 331 |
+
# traces of expressions with internal contractions:
|
| 332 |
+
t = G(m0)*G(-m0)
|
| 333 |
+
t1 = gamma_trace(t)
|
| 334 |
+
assert t1.equals(4*D)
|
| 335 |
+
|
| 336 |
+
t = G(m0)*G(m1)*G(-m0)*G(-m1)
|
| 337 |
+
t1 = gamma_trace(t)
|
| 338 |
+
assert t1.equals(8*D - 4*D**2)
|
| 339 |
+
|
| 340 |
+
t = G(m0)*G(m1)*G(m2)*G(m3)*G(m4)*G(-m0)
|
| 341 |
+
t1 = gamma_trace(t)
|
| 342 |
+
t2 = (-4*D)*g(m1, m3)*g(m2, m4) + (4*D)*g(m1, m2)*g(m3, m4) + \
|
| 343 |
+
(4*D)*g(m1, m4)*g(m2, m3)
|
| 344 |
+
assert _is_tensor_eq(t1, t2)
|
| 345 |
+
|
| 346 |
+
t = G(-m5)*G(m0)*G(m1)*G(m2)*G(m3)*G(m4)*G(-m0)*G(m5)
|
| 347 |
+
t1 = gamma_trace(t)
|
| 348 |
+
t2 = (32*D + 4*(-D + 4)**2 - 64)*(g(m1, m2)*g(m3, m4) - \
|
| 349 |
+
g(m1, m3)*g(m2, m4) + g(m1, m4)*g(m2, m3))
|
| 350 |
+
assert _is_tensor_eq(t1, t2)
|
| 351 |
+
|
| 352 |
+
t = G(m0)*G(m1)*G(-m0)*G(m3)
|
| 353 |
+
t1 = gamma_trace(t)
|
| 354 |
+
assert t1.equals((-4*D + 8)*g(m1, m3))
|
| 355 |
+
|
| 356 |
+
# p, q = S1('p,q')
|
| 357 |
+
# ps = p(m0)*G(-m0)
|
| 358 |
+
# qs = q(m0)*G(-m0)
|
| 359 |
+
# t = ps*qs*ps*qs
|
| 360 |
+
# t1 = gamma_trace(t)
|
| 361 |
+
# assert t1 == 8*p(m0)*q(-m0)*p(m1)*q(-m1) - 4*p(m0)*p(-m0)*q(m1)*q(-m1)
|
| 362 |
+
|
| 363 |
+
t = G(m0)*G(m1)*G(m2)*G(m3)*G(m4)*G(m5)*G(-m0)*G(-m1)*G(-m2)*G(-m3)*G(-m4)*G(-m5)
|
| 364 |
+
t1 = gamma_trace(t)
|
| 365 |
+
assert t1.equals(-4*D**6 + 120*D**5 - 1040*D**4 + 3360*D**3 - 4480*D**2 + 2048*D)
|
| 366 |
+
|
| 367 |
+
t = G(m0)*G(m1)*G(n1)*G(m2)*G(n2)*G(m3)*G(m4)*G(-n2)*G(-n1)*G(-m0)*G(-m1)*G(-m2)*G(-m3)*G(-m4)
|
| 368 |
+
t1 = gamma_trace(t)
|
| 369 |
+
tresu = -7168*D + 16768*D**2 - 14400*D**3 + 5920*D**4 - 1232*D**5 + 120*D**6 - 4*D**7
|
| 370 |
+
assert t1.equals(tresu)
|
| 371 |
+
|
| 372 |
+
# checked with Mathematica
|
| 373 |
+
# In[1]:= <<Tracer.m
|
| 374 |
+
# In[2]:= Spur[l];
|
| 375 |
+
# In[3]:= GammaTrace[l, {m0},{m1},{n1},{m2},{n2},{m3},{m4},{n3},{n4},{m0},{m1},{m2},{m3},{m4}]
|
| 376 |
+
t = G(m0)*G(m1)*G(n1)*G(m2)*G(n2)*G(m3)*G(m4)*G(n3)*G(n4)*G(-m0)*G(-m1)*G(-m2)*G(-m3)*G(-m4)
|
| 377 |
+
t1 = gamma_trace(t)
|
| 378 |
+
# t1 = t1.expand_coeff()
|
| 379 |
+
c1 = -4*D**5 + 120*D**4 - 1200*D**3 + 5280*D**2 - 10560*D + 7808
|
| 380 |
+
c2 = -4*D**5 + 88*D**4 - 560*D**3 + 1440*D**2 - 1600*D + 640
|
| 381 |
+
assert _is_tensor_eq(t1, c1*g(n1, n4)*g(n2, n3) + c2*g(n1, n2)*g(n3, n4) + \
|
| 382 |
+
(-c1)*g(n1, n3)*g(n2, n4))
|
| 383 |
+
|
| 384 |
+
p, q = tensor_heads('p,q', [LorentzIndex])
|
| 385 |
+
ps = p(m0)*G(-m0)
|
| 386 |
+
qs = q(m0)*G(-m0)
|
| 387 |
+
p2 = p(m0)*p(-m0)
|
| 388 |
+
q2 = q(m0)*q(-m0)
|
| 389 |
+
pq = p(m0)*q(-m0)
|
| 390 |
+
t = ps*qs*ps*qs
|
| 391 |
+
r = gamma_trace(t)
|
| 392 |
+
assert _is_tensor_eq(r, 8*pq*pq - 4*p2*q2)
|
| 393 |
+
t = ps*qs*ps*qs*ps*qs
|
| 394 |
+
r = gamma_trace(t)
|
| 395 |
+
assert _is_tensor_eq(r, -12*p2*pq*q2 + 16*pq*pq*pq)
|
| 396 |
+
t = ps*qs*ps*qs*ps*qs*ps*qs
|
| 397 |
+
r = gamma_trace(t)
|
| 398 |
+
assert _is_tensor_eq(r, -32*pq*pq*p2*q2 + 32*pq*pq*pq*pq + 4*p2*p2*q2*q2)
|
| 399 |
+
|
| 400 |
+
t = 4*p(m1)*p(m0)*p(-m0)*q(-m1)*q(m2)*q(-m2)
|
| 401 |
+
assert _is_tensor_eq(gamma_trace(t), t)
|
| 402 |
+
t = ps*ps*ps*ps*ps*ps*ps*ps
|
| 403 |
+
r = gamma_trace(t)
|
| 404 |
+
assert r.equals(4*p2*p2*p2*p2)
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
def test_bug_13636():
|
| 408 |
+
"""Test issue 13636 regarding handling traces of sums of products
|
| 409 |
+
of GammaMatrix mixed with other factors."""
|
| 410 |
+
pi, ki, pf = tensor_heads("pi, ki, pf", [LorentzIndex])
|
| 411 |
+
i0, i1, i2, i3, i4 = tensor_indices("i0:5", LorentzIndex)
|
| 412 |
+
x = Symbol("x")
|
| 413 |
+
pis = pi(i2) * G(-i2)
|
| 414 |
+
kis = ki(i3) * G(-i3)
|
| 415 |
+
pfs = pf(i4) * G(-i4)
|
| 416 |
+
|
| 417 |
+
a = pfs * G(i0) * kis * G(i1) * pis * G(-i1) * kis * G(-i0)
|
| 418 |
+
b = pfs * G(i0) * kis * G(i1) * pis * x * G(-i0) * pi(-i1)
|
| 419 |
+
ta = gamma_trace(a)
|
| 420 |
+
tb = gamma_trace(b)
|
| 421 |
+
t_a_plus_b = gamma_trace(a + b)
|
| 422 |
+
assert ta == 4 * (
|
| 423 |
+
-4 * ki(i0) * ki(-i0) * pf(i1) * pi(-i1)
|
| 424 |
+
+ 8 * ki(i0) * ki(i1) * pf(-i0) * pi(-i1)
|
| 425 |
+
)
|
| 426 |
+
assert tb == -8 * x * ki(i0) * pf(-i0) * pi(i1) * pi(-i1)
|
| 427 |
+
assert t_a_plus_b == ta + tb
|
lib/python3.12/site-packages/sympy/physics/mechanics/__init__.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__all__ = [
|
| 2 |
+
'vector',
|
| 3 |
+
|
| 4 |
+
'CoordinateSym', 'ReferenceFrame', 'Dyadic', 'Vector', 'Point', 'cross',
|
| 5 |
+
'dot', 'express', 'time_derivative', 'outer', 'kinematic_equations',
|
| 6 |
+
'get_motion_params', 'partial_velocity', 'dynamicsymbols', 'vprint',
|
| 7 |
+
'vsstrrepr', 'vsprint', 'vpprint', 'vlatex', 'init_vprinting', 'curl',
|
| 8 |
+
'divergence', 'gradient', 'is_conservative', 'is_solenoidal',
|
| 9 |
+
'scalar_potential', 'scalar_potential_difference',
|
| 10 |
+
|
| 11 |
+
'KanesMethod',
|
| 12 |
+
|
| 13 |
+
'RigidBody',
|
| 14 |
+
|
| 15 |
+
'linear_momentum', 'angular_momentum', 'kinetic_energy', 'potential_energy',
|
| 16 |
+
'Lagrangian', 'mechanics_printing', 'mprint', 'msprint', 'mpprint',
|
| 17 |
+
'mlatex', 'msubs', 'find_dynamicsymbols',
|
| 18 |
+
|
| 19 |
+
'inertia', 'inertia_of_point_mass', 'Inertia',
|
| 20 |
+
|
| 21 |
+
'Force', 'Torque',
|
| 22 |
+
|
| 23 |
+
'Particle',
|
| 24 |
+
|
| 25 |
+
'LagrangesMethod',
|
| 26 |
+
|
| 27 |
+
'Linearizer',
|
| 28 |
+
|
| 29 |
+
'Body',
|
| 30 |
+
|
| 31 |
+
'SymbolicSystem', 'System',
|
| 32 |
+
|
| 33 |
+
'PinJoint', 'PrismaticJoint', 'CylindricalJoint', 'PlanarJoint',
|
| 34 |
+
'SphericalJoint', 'WeldJoint',
|
| 35 |
+
|
| 36 |
+
'JointsMethod',
|
| 37 |
+
|
| 38 |
+
'WrappingCylinder', 'WrappingGeometryBase', 'WrappingSphere',
|
| 39 |
+
|
| 40 |
+
'PathwayBase', 'LinearPathway', 'ObstacleSetPathway', 'WrappingPathway',
|
| 41 |
+
|
| 42 |
+
'ActuatorBase', 'ForceActuator', 'LinearDamper', 'LinearSpring',
|
| 43 |
+
'TorqueActuator', 'DuffingSpring'
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
from sympy.physics import vector
|
| 47 |
+
|
| 48 |
+
from sympy.physics.vector import (CoordinateSym, ReferenceFrame, Dyadic, Vector, Point,
|
| 49 |
+
cross, dot, express, time_derivative, outer, kinematic_equations,
|
| 50 |
+
get_motion_params, partial_velocity, dynamicsymbols, vprint,
|
| 51 |
+
vsstrrepr, vsprint, vpprint, vlatex, init_vprinting, curl, divergence,
|
| 52 |
+
gradient, is_conservative, is_solenoidal, scalar_potential,
|
| 53 |
+
scalar_potential_difference)
|
| 54 |
+
|
| 55 |
+
from .kane import KanesMethod
|
| 56 |
+
|
| 57 |
+
from .rigidbody import RigidBody
|
| 58 |
+
|
| 59 |
+
from .functions import (linear_momentum, angular_momentum, kinetic_energy,
|
| 60 |
+
potential_energy, Lagrangian, mechanics_printing,
|
| 61 |
+
mprint, msprint, mpprint, mlatex, msubs,
|
| 62 |
+
find_dynamicsymbols)
|
| 63 |
+
|
| 64 |
+
from .inertia import inertia, inertia_of_point_mass, Inertia
|
| 65 |
+
|
| 66 |
+
from .loads import Force, Torque
|
| 67 |
+
|
| 68 |
+
from .particle import Particle
|
| 69 |
+
|
| 70 |
+
from .lagrange import LagrangesMethod
|
| 71 |
+
|
| 72 |
+
from .linearize import Linearizer
|
| 73 |
+
|
| 74 |
+
from .body import Body
|
| 75 |
+
|
| 76 |
+
from .system import SymbolicSystem, System
|
| 77 |
+
|
| 78 |
+
from .jointsmethod import JointsMethod
|
| 79 |
+
|
| 80 |
+
from .joint import (PinJoint, PrismaticJoint, CylindricalJoint, PlanarJoint,
|
| 81 |
+
SphericalJoint, WeldJoint)
|
| 82 |
+
|
| 83 |
+
from .wrapping_geometry import (WrappingCylinder, WrappingGeometryBase,
|
| 84 |
+
WrappingSphere)
|
| 85 |
+
|
| 86 |
+
from .pathway import (PathwayBase, LinearPathway, ObstacleSetPathway,
|
| 87 |
+
WrappingPathway)
|
| 88 |
+
|
| 89 |
+
from .actuator import (ActuatorBase, ForceActuator, LinearDamper, LinearSpring,
|
| 90 |
+
TorqueActuator, DuffingSpring)
|
lib/python3.12/site-packages/sympy/physics/mechanics/actuator.py
ADDED
|
@@ -0,0 +1,992 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Implementations of actuators for linked force and torque application."""
|
| 2 |
+
|
| 3 |
+
from abc import ABC, abstractmethod
|
| 4 |
+
|
| 5 |
+
from sympy import S, sympify
|
| 6 |
+
from sympy.physics.mechanics.joint import PinJoint
|
| 7 |
+
from sympy.physics.mechanics.loads import Torque
|
| 8 |
+
from sympy.physics.mechanics.pathway import PathwayBase
|
| 9 |
+
from sympy.physics.mechanics.rigidbody import RigidBody
|
| 10 |
+
from sympy.physics.vector import ReferenceFrame, Vector
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
__all__ = [
|
| 14 |
+
'ActuatorBase',
|
| 15 |
+
'ForceActuator',
|
| 16 |
+
'LinearDamper',
|
| 17 |
+
'LinearSpring',
|
| 18 |
+
'TorqueActuator',
|
| 19 |
+
'DuffingSpring'
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class ActuatorBase(ABC):
|
| 24 |
+
"""Abstract base class for all actuator classes to inherit from.
|
| 25 |
+
|
| 26 |
+
Notes
|
| 27 |
+
=====
|
| 28 |
+
|
| 29 |
+
Instances of this class cannot be directly instantiated by users. However,
|
| 30 |
+
it can be used to created custom actuator types through subclassing.
|
| 31 |
+
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
def __init__(self):
|
| 35 |
+
"""Initializer for ``ActuatorBase``."""
|
| 36 |
+
pass
|
| 37 |
+
|
| 38 |
+
@abstractmethod
|
| 39 |
+
def to_loads(self):
|
| 40 |
+
"""Loads required by the equations of motion method classes.
|
| 41 |
+
|
| 42 |
+
Explanation
|
| 43 |
+
===========
|
| 44 |
+
|
| 45 |
+
``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be
|
| 46 |
+
passed to the ``loads`` parameters of its ``kanes_equations`` method
|
| 47 |
+
when constructing the equations of motion. This method acts as a
|
| 48 |
+
utility to produce the correctly-structred pairs of points and vectors
|
| 49 |
+
required so that these can be easily concatenated with other items in
|
| 50 |
+
the list of loads and passed to ``KanesMethod.kanes_equations``. These
|
| 51 |
+
loads are also in the correct form to also be passed to the other
|
| 52 |
+
equations of motion method classes, e.g. ``LagrangesMethod``.
|
| 53 |
+
|
| 54 |
+
"""
|
| 55 |
+
pass
|
| 56 |
+
|
| 57 |
+
def __repr__(self):
|
| 58 |
+
"""Default representation of an actuator."""
|
| 59 |
+
return f'{self.__class__.__name__}()'
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class ForceActuator(ActuatorBase):
|
| 63 |
+
"""Force-producing actuator.
|
| 64 |
+
|
| 65 |
+
Explanation
|
| 66 |
+
===========
|
| 67 |
+
|
| 68 |
+
A ``ForceActuator`` is an actuator that produces a (expansile) force along
|
| 69 |
+
its length.
|
| 70 |
+
|
| 71 |
+
A force actuator uses a pathway instance to determine the direction and
|
| 72 |
+
number of forces that it applies to a system. Consider the simplest case
|
| 73 |
+
where a ``LinearPathway`` instance is used. This pathway is made up of two
|
| 74 |
+
points that can move relative to each other, and results in a pair of equal
|
| 75 |
+
and opposite forces acting on the endpoints. If the positive time-varying
|
| 76 |
+
Euclidean distance between the two points is defined, then the "extension
|
| 77 |
+
velocity" is the time derivative of this distance. The extension velocity
|
| 78 |
+
is positive when the two points are moving away from each other and
|
| 79 |
+
negative when moving closer to each other. The direction for the force
|
| 80 |
+
acting on either point is determined by constructing a unit vector directed
|
| 81 |
+
from the other point to this point. This establishes a sign convention such
|
| 82 |
+
that a positive force magnitude tends to push the points apart, this is the
|
| 83 |
+
meaning of "expansile" in this context. The following diagram shows the
|
| 84 |
+
positive force sense and the distance between the points::
|
| 85 |
+
|
| 86 |
+
P Q
|
| 87 |
+
o<--- F --->o
|
| 88 |
+
| |
|
| 89 |
+
|<--l(t)--->|
|
| 90 |
+
|
| 91 |
+
Examples
|
| 92 |
+
========
|
| 93 |
+
|
| 94 |
+
To construct an actuator, an expression (or symbol) must be supplied to
|
| 95 |
+
represent the force it can produce, alongside a pathway specifying its line
|
| 96 |
+
of action. Let's also create a global reference frame and spatially fix one
|
| 97 |
+
of the points in it while setting the other to be positioned such that it
|
| 98 |
+
can freely move in the frame's x direction specified by the coordinate
|
| 99 |
+
``q``.
|
| 100 |
+
|
| 101 |
+
>>> from sympy import symbols
|
| 102 |
+
>>> from sympy.physics.mechanics import (ForceActuator, LinearPathway,
|
| 103 |
+
... Point, ReferenceFrame)
|
| 104 |
+
>>> from sympy.physics.vector import dynamicsymbols
|
| 105 |
+
>>> N = ReferenceFrame('N')
|
| 106 |
+
>>> q = dynamicsymbols('q')
|
| 107 |
+
>>> force = symbols('F')
|
| 108 |
+
>>> pA, pB = Point('pA'), Point('pB')
|
| 109 |
+
>>> pA.set_vel(N, 0)
|
| 110 |
+
>>> pB.set_pos(pA, q*N.x)
|
| 111 |
+
>>> pB.pos_from(pA)
|
| 112 |
+
q(t)*N.x
|
| 113 |
+
>>> linear_pathway = LinearPathway(pA, pB)
|
| 114 |
+
>>> actuator = ForceActuator(force, linear_pathway)
|
| 115 |
+
>>> actuator
|
| 116 |
+
ForceActuator(F, LinearPathway(pA, pB))
|
| 117 |
+
|
| 118 |
+
Parameters
|
| 119 |
+
==========
|
| 120 |
+
|
| 121 |
+
force : Expr
|
| 122 |
+
The scalar expression defining the (expansile) force that the actuator
|
| 123 |
+
produces.
|
| 124 |
+
pathway : PathwayBase
|
| 125 |
+
The pathway that the actuator follows. This must be an instance of a
|
| 126 |
+
concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``.
|
| 127 |
+
|
| 128 |
+
"""
|
| 129 |
+
|
| 130 |
+
def __init__(self, force, pathway):
|
| 131 |
+
"""Initializer for ``ForceActuator``.
|
| 132 |
+
|
| 133 |
+
Parameters
|
| 134 |
+
==========
|
| 135 |
+
|
| 136 |
+
force : Expr
|
| 137 |
+
The scalar expression defining the (expansile) force that the
|
| 138 |
+
actuator produces.
|
| 139 |
+
pathway : PathwayBase
|
| 140 |
+
The pathway that the actuator follows. This must be an instance of
|
| 141 |
+
a concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``.
|
| 142 |
+
|
| 143 |
+
"""
|
| 144 |
+
self.force = force
|
| 145 |
+
self.pathway = pathway
|
| 146 |
+
|
| 147 |
+
@property
|
| 148 |
+
def force(self):
|
| 149 |
+
"""The magnitude of the force produced by the actuator."""
|
| 150 |
+
return self._force
|
| 151 |
+
|
| 152 |
+
@force.setter
|
| 153 |
+
def force(self, force):
|
| 154 |
+
if hasattr(self, '_force'):
|
| 155 |
+
msg = (
|
| 156 |
+
f'Can\'t set attribute `force` to {repr(force)} as it is '
|
| 157 |
+
f'immutable.'
|
| 158 |
+
)
|
| 159 |
+
raise AttributeError(msg)
|
| 160 |
+
self._force = sympify(force, strict=True)
|
| 161 |
+
|
| 162 |
+
@property
|
| 163 |
+
def pathway(self):
|
| 164 |
+
"""The ``Pathway`` defining the actuator's line of action."""
|
| 165 |
+
return self._pathway
|
| 166 |
+
|
| 167 |
+
@pathway.setter
|
| 168 |
+
def pathway(self, pathway):
|
| 169 |
+
if hasattr(self, '_pathway'):
|
| 170 |
+
msg = (
|
| 171 |
+
f'Can\'t set attribute `pathway` to {repr(pathway)} as it is '
|
| 172 |
+
f'immutable.'
|
| 173 |
+
)
|
| 174 |
+
raise AttributeError(msg)
|
| 175 |
+
if not isinstance(pathway, PathwayBase):
|
| 176 |
+
msg = (
|
| 177 |
+
f'Value {repr(pathway)} passed to `pathway` was of type '
|
| 178 |
+
f'{type(pathway)}, must be {PathwayBase}.'
|
| 179 |
+
)
|
| 180 |
+
raise TypeError(msg)
|
| 181 |
+
self._pathway = pathway
|
| 182 |
+
|
| 183 |
+
def to_loads(self):
|
| 184 |
+
"""Loads required by the equations of motion method classes.
|
| 185 |
+
|
| 186 |
+
Explanation
|
| 187 |
+
===========
|
| 188 |
+
|
| 189 |
+
``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be
|
| 190 |
+
passed to the ``loads`` parameters of its ``kanes_equations`` method
|
| 191 |
+
when constructing the equations of motion. This method acts as a
|
| 192 |
+
utility to produce the correctly-structred pairs of points and vectors
|
| 193 |
+
required so that these can be easily concatenated with other items in
|
| 194 |
+
the list of loads and passed to ``KanesMethod.kanes_equations``. These
|
| 195 |
+
loads are also in the correct form to also be passed to the other
|
| 196 |
+
equations of motion method classes, e.g. ``LagrangesMethod``.
|
| 197 |
+
|
| 198 |
+
Examples
|
| 199 |
+
========
|
| 200 |
+
|
| 201 |
+
The below example shows how to generate the loads produced by a force
|
| 202 |
+
actuator that follows a linear pathway. In this example we'll assume
|
| 203 |
+
that the force actuator is being used to model a simple linear spring.
|
| 204 |
+
First, create a linear pathway between two points separated by the
|
| 205 |
+
coordinate ``q`` in the ``x`` direction of the global frame ``N``.
|
| 206 |
+
|
| 207 |
+
>>> from sympy.physics.mechanics import (LinearPathway, Point,
|
| 208 |
+
... ReferenceFrame)
|
| 209 |
+
>>> from sympy.physics.vector import dynamicsymbols
|
| 210 |
+
>>> q = dynamicsymbols('q')
|
| 211 |
+
>>> N = ReferenceFrame('N')
|
| 212 |
+
>>> pA, pB = Point('pA'), Point('pB')
|
| 213 |
+
>>> pB.set_pos(pA, q*N.x)
|
| 214 |
+
>>> pathway = LinearPathway(pA, pB)
|
| 215 |
+
|
| 216 |
+
Now create a symbol ``k`` to describe the spring's stiffness and
|
| 217 |
+
instantiate a force actuator that produces a (contractile) force
|
| 218 |
+
proportional to both the spring's stiffness and the pathway's length.
|
| 219 |
+
Note that actuator classes use the sign convention that expansile
|
| 220 |
+
forces are positive, so for a spring to produce a contractile force the
|
| 221 |
+
spring force needs to be calculated as the negative for the stiffness
|
| 222 |
+
multiplied by the length.
|
| 223 |
+
|
| 224 |
+
>>> from sympy import symbols
|
| 225 |
+
>>> from sympy.physics.mechanics import ForceActuator
|
| 226 |
+
>>> stiffness = symbols('k')
|
| 227 |
+
>>> spring_force = -stiffness*pathway.length
|
| 228 |
+
>>> spring = ForceActuator(spring_force, pathway)
|
| 229 |
+
|
| 230 |
+
The forces produced by the spring can be generated in the list of loads
|
| 231 |
+
form that ``KanesMethod`` (and other equations of motion methods)
|
| 232 |
+
requires by calling the ``to_loads`` method.
|
| 233 |
+
|
| 234 |
+
>>> spring.to_loads()
|
| 235 |
+
[(pA, k*q(t)*N.x), (pB, - k*q(t)*N.x)]
|
| 236 |
+
|
| 237 |
+
A simple linear damper can be modeled in a similar way. Create another
|
| 238 |
+
symbol ``c`` to describe the dampers damping coefficient. This time
|
| 239 |
+
instantiate a force actuator that produces a force proportional to both
|
| 240 |
+
the damper's damping coefficient and the pathway's extension velocity.
|
| 241 |
+
Note that the damping force is negative as it acts in the opposite
|
| 242 |
+
direction to which the damper is changing in length.
|
| 243 |
+
|
| 244 |
+
>>> damping_coefficient = symbols('c')
|
| 245 |
+
>>> damping_force = -damping_coefficient*pathway.extension_velocity
|
| 246 |
+
>>> damper = ForceActuator(damping_force, pathway)
|
| 247 |
+
|
| 248 |
+
Again, the forces produces by the damper can be generated by calling
|
| 249 |
+
the ``to_loads`` method.
|
| 250 |
+
|
| 251 |
+
>>> damper.to_loads()
|
| 252 |
+
[(pA, c*Derivative(q(t), t)*N.x), (pB, - c*Derivative(q(t), t)*N.x)]
|
| 253 |
+
|
| 254 |
+
"""
|
| 255 |
+
return self.pathway.to_loads(self.force)
|
| 256 |
+
|
| 257 |
+
def __repr__(self):
|
| 258 |
+
"""Representation of a ``ForceActuator``."""
|
| 259 |
+
return f'{self.__class__.__name__}({self.force}, {self.pathway})'
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
class LinearSpring(ForceActuator):
|
| 263 |
+
"""A spring with its spring force as a linear function of its length.
|
| 264 |
+
|
| 265 |
+
Explanation
|
| 266 |
+
===========
|
| 267 |
+
|
| 268 |
+
Note that the "linear" in the name ``LinearSpring`` refers to the fact that
|
| 269 |
+
the spring force is a linear function of the springs length. I.e. for a
|
| 270 |
+
linear spring with stiffness ``k``, distance between its ends of ``x``, and
|
| 271 |
+
an equilibrium length of ``0``, the spring force will be ``-k*x``, which is
|
| 272 |
+
a linear function in ``x``. To create a spring that follows a linear, or
|
| 273 |
+
straight, pathway between its two ends, a ``LinearPathway`` instance needs
|
| 274 |
+
to be passed to the ``pathway`` parameter.
|
| 275 |
+
|
| 276 |
+
A ``LinearSpring`` is a subclass of ``ForceActuator`` and so follows the
|
| 277 |
+
same sign conventions for length, extension velocity, and the direction of
|
| 278 |
+
the forces it applies to its points of attachment on bodies. The sign
|
| 279 |
+
convention for the direction of forces is such that, for the case where a
|
| 280 |
+
linear spring is instantiated with a ``LinearPathway`` instance as its
|
| 281 |
+
pathway, they act to push the two ends of the spring away from one another.
|
| 282 |
+
Because springs produces a contractile force and acts to pull the two ends
|
| 283 |
+
together towards the equilibrium length when stretched, the scalar portion
|
| 284 |
+
of the forces on the endpoint are negative in order to flip the sign of the
|
| 285 |
+
forces on the endpoints when converted into vector quantities. The
|
| 286 |
+
following diagram shows the positive force sense and the distance between
|
| 287 |
+
the points::
|
| 288 |
+
|
| 289 |
+
P Q
|
| 290 |
+
o<--- F --->o
|
| 291 |
+
| |
|
| 292 |
+
|<--l(t)--->|
|
| 293 |
+
|
| 294 |
+
Examples
|
| 295 |
+
========
|
| 296 |
+
|
| 297 |
+
To construct a linear spring, an expression (or symbol) must be supplied to
|
| 298 |
+
represent the stiffness (spring constant) of the spring, alongside a
|
| 299 |
+
pathway specifying its line of action. Let's also create a global reference
|
| 300 |
+
frame and spatially fix one of the points in it while setting the other to
|
| 301 |
+
be positioned such that it can freely move in the frame's x direction
|
| 302 |
+
specified by the coordinate ``q``.
|
| 303 |
+
|
| 304 |
+
>>> from sympy import symbols
|
| 305 |
+
>>> from sympy.physics.mechanics import (LinearPathway, LinearSpring,
|
| 306 |
+
... Point, ReferenceFrame)
|
| 307 |
+
>>> from sympy.physics.vector import dynamicsymbols
|
| 308 |
+
>>> N = ReferenceFrame('N')
|
| 309 |
+
>>> q = dynamicsymbols('q')
|
| 310 |
+
>>> stiffness = symbols('k')
|
| 311 |
+
>>> pA, pB = Point('pA'), Point('pB')
|
| 312 |
+
>>> pA.set_vel(N, 0)
|
| 313 |
+
>>> pB.set_pos(pA, q*N.x)
|
| 314 |
+
>>> pB.pos_from(pA)
|
| 315 |
+
q(t)*N.x
|
| 316 |
+
>>> linear_pathway = LinearPathway(pA, pB)
|
| 317 |
+
>>> spring = LinearSpring(stiffness, linear_pathway)
|
| 318 |
+
>>> spring
|
| 319 |
+
LinearSpring(k, LinearPathway(pA, pB))
|
| 320 |
+
|
| 321 |
+
This spring will produce a force that is proportional to both its stiffness
|
| 322 |
+
and the pathway's length. Note that this force is negative as SymPy's sign
|
| 323 |
+
convention for actuators is that negative forces are contractile.
|
| 324 |
+
|
| 325 |
+
>>> spring.force
|
| 326 |
+
-k*sqrt(q(t)**2)
|
| 327 |
+
|
| 328 |
+
To create a linear spring with a non-zero equilibrium length, an expression
|
| 329 |
+
(or symbol) can be passed to the ``equilibrium_length`` parameter on
|
| 330 |
+
construction on a ``LinearSpring`` instance. Let's create a symbol ``l``
|
| 331 |
+
to denote a non-zero equilibrium length and create another linear spring.
|
| 332 |
+
|
| 333 |
+
>>> l = symbols('l')
|
| 334 |
+
>>> spring = LinearSpring(stiffness, linear_pathway, equilibrium_length=l)
|
| 335 |
+
>>> spring
|
| 336 |
+
LinearSpring(k, LinearPathway(pA, pB), equilibrium_length=l)
|
| 337 |
+
|
| 338 |
+
The spring force of this new spring is again proportional to both its
|
| 339 |
+
stiffness and the pathway's length. However, the spring will not produce
|
| 340 |
+
any force when ``q(t)`` equals ``l``. Note that the force will become
|
| 341 |
+
expansile when ``q(t)`` is less than ``l``, as expected.
|
| 342 |
+
|
| 343 |
+
>>> spring.force
|
| 344 |
+
-k*(-l + sqrt(q(t)**2))
|
| 345 |
+
|
| 346 |
+
Parameters
|
| 347 |
+
==========
|
| 348 |
+
|
| 349 |
+
stiffness : Expr
|
| 350 |
+
The spring constant.
|
| 351 |
+
pathway : PathwayBase
|
| 352 |
+
The pathway that the actuator follows. This must be an instance of a
|
| 353 |
+
concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``.
|
| 354 |
+
equilibrium_length : Expr, optional
|
| 355 |
+
The length at which the spring is in equilibrium, i.e. it produces no
|
| 356 |
+
force. The default value is 0, i.e. the spring force is a linear
|
| 357 |
+
function of the pathway's length with no constant offset.
|
| 358 |
+
|
| 359 |
+
See Also
|
| 360 |
+
========
|
| 361 |
+
|
| 362 |
+
ForceActuator: force-producing actuator (superclass of ``LinearSpring``).
|
| 363 |
+
LinearPathway: straight-line pathway between a pair of points.
|
| 364 |
+
|
| 365 |
+
"""
|
| 366 |
+
|
| 367 |
+
def __init__(self, stiffness, pathway, equilibrium_length=S.Zero):
|
| 368 |
+
"""Initializer for ``LinearSpring``.
|
| 369 |
+
|
| 370 |
+
Parameters
|
| 371 |
+
==========
|
| 372 |
+
|
| 373 |
+
stiffness : Expr
|
| 374 |
+
The spring constant.
|
| 375 |
+
pathway : PathwayBase
|
| 376 |
+
The pathway that the actuator follows. This must be an instance of
|
| 377 |
+
a concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``.
|
| 378 |
+
equilibrium_length : Expr, optional
|
| 379 |
+
The length at which the spring is in equilibrium, i.e. it produces
|
| 380 |
+
no force. The default value is 0, i.e. the spring force is a linear
|
| 381 |
+
function of the pathway's length with no constant offset.
|
| 382 |
+
|
| 383 |
+
"""
|
| 384 |
+
self.stiffness = stiffness
|
| 385 |
+
self.pathway = pathway
|
| 386 |
+
self.equilibrium_length = equilibrium_length
|
| 387 |
+
|
| 388 |
+
@property
|
| 389 |
+
def force(self):
|
| 390 |
+
"""The spring force produced by the linear spring."""
|
| 391 |
+
return -self.stiffness*(self.pathway.length - self.equilibrium_length)
|
| 392 |
+
|
| 393 |
+
@force.setter
|
| 394 |
+
def force(self, force):
|
| 395 |
+
raise AttributeError('Can\'t set computed attribute `force`.')
|
| 396 |
+
|
| 397 |
+
@property
|
| 398 |
+
def stiffness(self):
|
| 399 |
+
"""The spring constant for the linear spring."""
|
| 400 |
+
return self._stiffness
|
| 401 |
+
|
| 402 |
+
@stiffness.setter
|
| 403 |
+
def stiffness(self, stiffness):
|
| 404 |
+
if hasattr(self, '_stiffness'):
|
| 405 |
+
msg = (
|
| 406 |
+
f'Can\'t set attribute `stiffness` to {repr(stiffness)} as it '
|
| 407 |
+
f'is immutable.'
|
| 408 |
+
)
|
| 409 |
+
raise AttributeError(msg)
|
| 410 |
+
self._stiffness = sympify(stiffness, strict=True)
|
| 411 |
+
|
| 412 |
+
@property
|
| 413 |
+
def equilibrium_length(self):
|
| 414 |
+
"""The length of the spring at which it produces no force."""
|
| 415 |
+
return self._equilibrium_length
|
| 416 |
+
|
| 417 |
+
@equilibrium_length.setter
|
| 418 |
+
def equilibrium_length(self, equilibrium_length):
|
| 419 |
+
if hasattr(self, '_equilibrium_length'):
|
| 420 |
+
msg = (
|
| 421 |
+
f'Can\'t set attribute `equilibrium_length` to '
|
| 422 |
+
f'{repr(equilibrium_length)} as it is immutable.'
|
| 423 |
+
)
|
| 424 |
+
raise AttributeError(msg)
|
| 425 |
+
self._equilibrium_length = sympify(equilibrium_length, strict=True)
|
| 426 |
+
|
| 427 |
+
def __repr__(self):
|
| 428 |
+
"""Representation of a ``LinearSpring``."""
|
| 429 |
+
string = f'{self.__class__.__name__}({self.stiffness}, {self.pathway}'
|
| 430 |
+
if self.equilibrium_length == S.Zero:
|
| 431 |
+
string += ')'
|
| 432 |
+
else:
|
| 433 |
+
string += f', equilibrium_length={self.equilibrium_length})'
|
| 434 |
+
return string
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
class LinearDamper(ForceActuator):
|
| 438 |
+
"""A damper whose force is a linear function of its extension velocity.
|
| 439 |
+
|
| 440 |
+
Explanation
|
| 441 |
+
===========
|
| 442 |
+
|
| 443 |
+
Note that the "linear" in the name ``LinearDamper`` refers to the fact that
|
| 444 |
+
the damping force is a linear function of the damper's rate of change in
|
| 445 |
+
its length. I.e. for a linear damper with damping ``c`` and extension
|
| 446 |
+
velocity ``v``, the damping force will be ``-c*v``, which is a linear
|
| 447 |
+
function in ``v``. To create a damper that follows a linear, or straight,
|
| 448 |
+
pathway between its two ends, a ``LinearPathway`` instance needs to be
|
| 449 |
+
passed to the ``pathway`` parameter.
|
| 450 |
+
|
| 451 |
+
A ``LinearDamper`` is a subclass of ``ForceActuator`` and so follows the
|
| 452 |
+
same sign conventions for length, extension velocity, and the direction of
|
| 453 |
+
the forces it applies to its points of attachment on bodies. The sign
|
| 454 |
+
convention for the direction of forces is such that, for the case where a
|
| 455 |
+
linear damper is instantiated with a ``LinearPathway`` instance as its
|
| 456 |
+
pathway, they act to push the two ends of the damper away from one another.
|
| 457 |
+
Because dampers produce a force that opposes the direction of change in
|
| 458 |
+
length, when extension velocity is positive the scalar portions of the
|
| 459 |
+
forces applied at the two endpoints are negative in order to flip the sign
|
| 460 |
+
of the forces on the endpoints wen converted into vector quantities. When
|
| 461 |
+
extension velocity is negative (i.e. when the damper is shortening), the
|
| 462 |
+
scalar portions of the fofces applied are also negative so that the signs
|
| 463 |
+
cancel producing forces on the endpoints that are in the same direction as
|
| 464 |
+
the positive sign convention for the forces at the endpoints of the pathway
|
| 465 |
+
(i.e. they act to push the endpoints away from one another). The following
|
| 466 |
+
diagram shows the positive force sense and the distance between the
|
| 467 |
+
points::
|
| 468 |
+
|
| 469 |
+
P Q
|
| 470 |
+
o<--- F --->o
|
| 471 |
+
| |
|
| 472 |
+
|<--l(t)--->|
|
| 473 |
+
|
| 474 |
+
Examples
|
| 475 |
+
========
|
| 476 |
+
|
| 477 |
+
To construct a linear damper, an expression (or symbol) must be supplied to
|
| 478 |
+
represent the damping coefficient of the damper (we'll use the symbol
|
| 479 |
+
``c``), alongside a pathway specifying its line of action. Let's also
|
| 480 |
+
create a global reference frame and spatially fix one of the points in it
|
| 481 |
+
while setting the other to be positioned such that it can freely move in
|
| 482 |
+
the frame's x direction specified by the coordinate ``q``. The velocity
|
| 483 |
+
that the two points move away from one another can be specified by the
|
| 484 |
+
coordinate ``u`` where ``u`` is the first time derivative of ``q``
|
| 485 |
+
(i.e., ``u = Derivative(q(t), t)``).
|
| 486 |
+
|
| 487 |
+
>>> from sympy import symbols
|
| 488 |
+
>>> from sympy.physics.mechanics import (LinearDamper, LinearPathway,
|
| 489 |
+
... Point, ReferenceFrame)
|
| 490 |
+
>>> from sympy.physics.vector import dynamicsymbols
|
| 491 |
+
>>> N = ReferenceFrame('N')
|
| 492 |
+
>>> q = dynamicsymbols('q')
|
| 493 |
+
>>> damping = symbols('c')
|
| 494 |
+
>>> pA, pB = Point('pA'), Point('pB')
|
| 495 |
+
>>> pA.set_vel(N, 0)
|
| 496 |
+
>>> pB.set_pos(pA, q*N.x)
|
| 497 |
+
>>> pB.pos_from(pA)
|
| 498 |
+
q(t)*N.x
|
| 499 |
+
>>> pB.vel(N)
|
| 500 |
+
Derivative(q(t), t)*N.x
|
| 501 |
+
>>> linear_pathway = LinearPathway(pA, pB)
|
| 502 |
+
>>> damper = LinearDamper(damping, linear_pathway)
|
| 503 |
+
>>> damper
|
| 504 |
+
LinearDamper(c, LinearPathway(pA, pB))
|
| 505 |
+
|
| 506 |
+
This damper will produce a force that is proportional to both its damping
|
| 507 |
+
coefficient and the pathway's extension length. Note that this force is
|
| 508 |
+
negative as SymPy's sign convention for actuators is that negative forces
|
| 509 |
+
are contractile and the damping force of the damper will oppose the
|
| 510 |
+
direction of length change.
|
| 511 |
+
|
| 512 |
+
>>> damper.force
|
| 513 |
+
-c*sqrt(q(t)**2)*Derivative(q(t), t)/q(t)
|
| 514 |
+
|
| 515 |
+
Parameters
|
| 516 |
+
==========
|
| 517 |
+
|
| 518 |
+
damping : Expr
|
| 519 |
+
The damping constant.
|
| 520 |
+
pathway : PathwayBase
|
| 521 |
+
The pathway that the actuator follows. This must be an instance of a
|
| 522 |
+
concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``.
|
| 523 |
+
|
| 524 |
+
See Also
|
| 525 |
+
========
|
| 526 |
+
|
| 527 |
+
ForceActuator: force-producing actuator (superclass of ``LinearDamper``).
|
| 528 |
+
LinearPathway: straight-line pathway between a pair of points.
|
| 529 |
+
|
| 530 |
+
"""
|
| 531 |
+
|
| 532 |
+
def __init__(self, damping, pathway):
|
| 533 |
+
"""Initializer for ``LinearDamper``.
|
| 534 |
+
|
| 535 |
+
Parameters
|
| 536 |
+
==========
|
| 537 |
+
|
| 538 |
+
damping : Expr
|
| 539 |
+
The damping constant.
|
| 540 |
+
pathway : PathwayBase
|
| 541 |
+
The pathway that the actuator follows. This must be an instance of
|
| 542 |
+
a concrete subclass of ``PathwayBase``, e.g. ``LinearPathway``.
|
| 543 |
+
|
| 544 |
+
"""
|
| 545 |
+
self.damping = damping
|
| 546 |
+
self.pathway = pathway
|
| 547 |
+
|
| 548 |
+
@property
|
| 549 |
+
def force(self):
|
| 550 |
+
"""The damping force produced by the linear damper."""
|
| 551 |
+
return -self.damping*self.pathway.extension_velocity
|
| 552 |
+
|
| 553 |
+
@force.setter
|
| 554 |
+
def force(self, force):
|
| 555 |
+
raise AttributeError('Can\'t set computed attribute `force`.')
|
| 556 |
+
|
| 557 |
+
@property
|
| 558 |
+
def damping(self):
|
| 559 |
+
"""The damping constant for the linear damper."""
|
| 560 |
+
return self._damping
|
| 561 |
+
|
| 562 |
+
@damping.setter
|
| 563 |
+
def damping(self, damping):
|
| 564 |
+
if hasattr(self, '_damping'):
|
| 565 |
+
msg = (
|
| 566 |
+
f'Can\'t set attribute `damping` to {repr(damping)} as it is '
|
| 567 |
+
f'immutable.'
|
| 568 |
+
)
|
| 569 |
+
raise AttributeError(msg)
|
| 570 |
+
self._damping = sympify(damping, strict=True)
|
| 571 |
+
|
| 572 |
+
def __repr__(self):
|
| 573 |
+
"""Representation of a ``LinearDamper``."""
|
| 574 |
+
return f'{self.__class__.__name__}({self.damping}, {self.pathway})'
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
class TorqueActuator(ActuatorBase):
|
| 578 |
+
"""Torque-producing actuator.
|
| 579 |
+
|
| 580 |
+
Explanation
|
| 581 |
+
===========
|
| 582 |
+
|
| 583 |
+
A ``TorqueActuator`` is an actuator that produces a pair of equal and
|
| 584 |
+
opposite torques on a pair of bodies.
|
| 585 |
+
|
| 586 |
+
Examples
|
| 587 |
+
========
|
| 588 |
+
|
| 589 |
+
To construct a torque actuator, an expression (or symbol) must be supplied
|
| 590 |
+
to represent the torque it can produce, alongside a vector specifying the
|
| 591 |
+
axis about which the torque will act, and a pair of frames on which the
|
| 592 |
+
torque will act.
|
| 593 |
+
|
| 594 |
+
>>> from sympy import symbols
|
| 595 |
+
>>> from sympy.physics.mechanics import (ReferenceFrame, RigidBody,
|
| 596 |
+
... TorqueActuator)
|
| 597 |
+
>>> N = ReferenceFrame('N')
|
| 598 |
+
>>> A = ReferenceFrame('A')
|
| 599 |
+
>>> torque = symbols('T')
|
| 600 |
+
>>> axis = N.z
|
| 601 |
+
>>> parent = RigidBody('parent', frame=N)
|
| 602 |
+
>>> child = RigidBody('child', frame=A)
|
| 603 |
+
>>> bodies = (child, parent)
|
| 604 |
+
>>> actuator = TorqueActuator(torque, axis, *bodies)
|
| 605 |
+
>>> actuator
|
| 606 |
+
TorqueActuator(T, axis=N.z, target_frame=A, reaction_frame=N)
|
| 607 |
+
|
| 608 |
+
Note that because torques actually act on frames, not bodies,
|
| 609 |
+
``TorqueActuator`` will extract the frame associated with a ``RigidBody``
|
| 610 |
+
when one is passed instead of a ``ReferenceFrame``.
|
| 611 |
+
|
| 612 |
+
Parameters
|
| 613 |
+
==========
|
| 614 |
+
|
| 615 |
+
torque : Expr
|
| 616 |
+
The scalar expression defining the torque that the actuator produces.
|
| 617 |
+
axis : Vector
|
| 618 |
+
The axis about which the actuator applies torques.
|
| 619 |
+
target_frame : ReferenceFrame | RigidBody
|
| 620 |
+
The primary frame on which the actuator will apply the torque.
|
| 621 |
+
reaction_frame : ReferenceFrame | RigidBody | None
|
| 622 |
+
The secondary frame on which the actuator will apply the torque. Note
|
| 623 |
+
that the (equal and opposite) reaction torque is applied to this frame.
|
| 624 |
+
|
| 625 |
+
"""
|
| 626 |
+
|
| 627 |
+
def __init__(self, torque, axis, target_frame, reaction_frame=None):
|
| 628 |
+
"""Initializer for ``TorqueActuator``.
|
| 629 |
+
|
| 630 |
+
Parameters
|
| 631 |
+
==========
|
| 632 |
+
|
| 633 |
+
torque : Expr
|
| 634 |
+
The scalar expression defining the torque that the actuator
|
| 635 |
+
produces.
|
| 636 |
+
axis : Vector
|
| 637 |
+
The axis about which the actuator applies torques.
|
| 638 |
+
target_frame : ReferenceFrame | RigidBody
|
| 639 |
+
The primary frame on which the actuator will apply the torque.
|
| 640 |
+
reaction_frame : ReferenceFrame | RigidBody | None
|
| 641 |
+
The secondary frame on which the actuator will apply the torque.
|
| 642 |
+
Note that the (equal and opposite) reaction torque is applied to
|
| 643 |
+
this frame.
|
| 644 |
+
|
| 645 |
+
"""
|
| 646 |
+
self.torque = torque
|
| 647 |
+
self.axis = axis
|
| 648 |
+
self.target_frame = target_frame
|
| 649 |
+
self.reaction_frame = reaction_frame
|
| 650 |
+
|
| 651 |
+
@classmethod
|
| 652 |
+
def at_pin_joint(cls, torque, pin_joint):
|
| 653 |
+
"""Alternate construtor to instantiate from a ``PinJoint`` instance.
|
| 654 |
+
|
| 655 |
+
Examples
|
| 656 |
+
========
|
| 657 |
+
|
| 658 |
+
To create a pin joint the ``PinJoint`` class requires a name, parent
|
| 659 |
+
body, and child body to be passed to its constructor. It is also
|
| 660 |
+
possible to control the joint axis using the ``joint_axis`` keyword
|
| 661 |
+
argument. In this example let's use the parent body's reference frame's
|
| 662 |
+
z-axis as the joint axis.
|
| 663 |
+
|
| 664 |
+
>>> from sympy.physics.mechanics import (PinJoint, ReferenceFrame,
|
| 665 |
+
... RigidBody, TorqueActuator)
|
| 666 |
+
>>> N = ReferenceFrame('N')
|
| 667 |
+
>>> A = ReferenceFrame('A')
|
| 668 |
+
>>> parent = RigidBody('parent', frame=N)
|
| 669 |
+
>>> child = RigidBody('child', frame=A)
|
| 670 |
+
>>> pin_joint = PinJoint(
|
| 671 |
+
... 'pin',
|
| 672 |
+
... parent,
|
| 673 |
+
... child,
|
| 674 |
+
... joint_axis=N.z,
|
| 675 |
+
... )
|
| 676 |
+
|
| 677 |
+
Let's also create a symbol ``T`` that will represent the torque applied
|
| 678 |
+
by the torque actuator.
|
| 679 |
+
|
| 680 |
+
>>> from sympy import symbols
|
| 681 |
+
>>> torque = symbols('T')
|
| 682 |
+
|
| 683 |
+
To create the torque actuator from the ``torque`` and ``pin_joint``
|
| 684 |
+
variables previously instantiated, these can be passed to the alternate
|
| 685 |
+
constructor class method ``at_pin_joint`` of the ``TorqueActuator``
|
| 686 |
+
class. It should be noted that a positive torque will cause a positive
|
| 687 |
+
displacement of the joint coordinate or that the torque is applied on
|
| 688 |
+
the child body with a reaction torque on the parent.
|
| 689 |
+
|
| 690 |
+
>>> actuator = TorqueActuator.at_pin_joint(torque, pin_joint)
|
| 691 |
+
>>> actuator
|
| 692 |
+
TorqueActuator(T, axis=N.z, target_frame=A, reaction_frame=N)
|
| 693 |
+
|
| 694 |
+
Parameters
|
| 695 |
+
==========
|
| 696 |
+
|
| 697 |
+
torque : Expr
|
| 698 |
+
The scalar expression defining the torque that the actuator
|
| 699 |
+
produces.
|
| 700 |
+
pin_joint : PinJoint
|
| 701 |
+
The pin joint, and by association the parent and child bodies, on
|
| 702 |
+
which the torque actuator will act. The pair of bodies acted upon
|
| 703 |
+
by the torque actuator are the parent and child bodies of the pin
|
| 704 |
+
joint, with the child acting as the reaction body. The pin joint's
|
| 705 |
+
axis is used as the axis about which the torque actuator will apply
|
| 706 |
+
its torque.
|
| 707 |
+
|
| 708 |
+
"""
|
| 709 |
+
if not isinstance(pin_joint, PinJoint):
|
| 710 |
+
msg = (
|
| 711 |
+
f'Value {repr(pin_joint)} passed to `pin_joint` was of type '
|
| 712 |
+
f'{type(pin_joint)}, must be {PinJoint}.'
|
| 713 |
+
)
|
| 714 |
+
raise TypeError(msg)
|
| 715 |
+
return cls(
|
| 716 |
+
torque,
|
| 717 |
+
pin_joint.joint_axis,
|
| 718 |
+
pin_joint.child_interframe,
|
| 719 |
+
pin_joint.parent_interframe,
|
| 720 |
+
)
|
| 721 |
+
|
| 722 |
+
@property
|
| 723 |
+
def torque(self):
|
| 724 |
+
"""The magnitude of the torque produced by the actuator."""
|
| 725 |
+
return self._torque
|
| 726 |
+
|
| 727 |
+
@torque.setter
|
| 728 |
+
def torque(self, torque):
|
| 729 |
+
if hasattr(self, '_torque'):
|
| 730 |
+
msg = (
|
| 731 |
+
f'Can\'t set attribute `torque` to {repr(torque)} as it is '
|
| 732 |
+
f'immutable.'
|
| 733 |
+
)
|
| 734 |
+
raise AttributeError(msg)
|
| 735 |
+
self._torque = sympify(torque, strict=True)
|
| 736 |
+
|
| 737 |
+
@property
|
| 738 |
+
def axis(self):
|
| 739 |
+
"""The axis about which the torque acts."""
|
| 740 |
+
return self._axis
|
| 741 |
+
|
| 742 |
+
@axis.setter
|
| 743 |
+
def axis(self, axis):
|
| 744 |
+
if hasattr(self, '_axis'):
|
| 745 |
+
msg = (
|
| 746 |
+
f'Can\'t set attribute `axis` to {repr(axis)} as it is '
|
| 747 |
+
f'immutable.'
|
| 748 |
+
)
|
| 749 |
+
raise AttributeError(msg)
|
| 750 |
+
if not isinstance(axis, Vector):
|
| 751 |
+
msg = (
|
| 752 |
+
f'Value {repr(axis)} passed to `axis` was of type '
|
| 753 |
+
f'{type(axis)}, must be {Vector}.'
|
| 754 |
+
)
|
| 755 |
+
raise TypeError(msg)
|
| 756 |
+
self._axis = axis
|
| 757 |
+
|
| 758 |
+
@property
|
| 759 |
+
def target_frame(self):
|
| 760 |
+
"""The primary reference frames on which the torque will act."""
|
| 761 |
+
return self._target_frame
|
| 762 |
+
|
| 763 |
+
@target_frame.setter
|
| 764 |
+
def target_frame(self, target_frame):
|
| 765 |
+
if hasattr(self, '_target_frame'):
|
| 766 |
+
msg = (
|
| 767 |
+
f'Can\'t set attribute `target_frame` to {repr(target_frame)} '
|
| 768 |
+
f'as it is immutable.'
|
| 769 |
+
)
|
| 770 |
+
raise AttributeError(msg)
|
| 771 |
+
if isinstance(target_frame, RigidBody):
|
| 772 |
+
target_frame = target_frame.frame
|
| 773 |
+
elif not isinstance(target_frame, ReferenceFrame):
|
| 774 |
+
msg = (
|
| 775 |
+
f'Value {repr(target_frame)} passed to `target_frame` was of '
|
| 776 |
+
f'type {type(target_frame)}, must be {ReferenceFrame}.'
|
| 777 |
+
)
|
| 778 |
+
raise TypeError(msg)
|
| 779 |
+
self._target_frame = target_frame
|
| 780 |
+
|
| 781 |
+
@property
|
| 782 |
+
def reaction_frame(self):
|
| 783 |
+
"""The primary reference frames on which the torque will act."""
|
| 784 |
+
return self._reaction_frame
|
| 785 |
+
|
| 786 |
+
@reaction_frame.setter
|
| 787 |
+
def reaction_frame(self, reaction_frame):
|
| 788 |
+
if hasattr(self, '_reaction_frame'):
|
| 789 |
+
msg = (
|
| 790 |
+
f'Can\'t set attribute `reaction_frame` to '
|
| 791 |
+
f'{repr(reaction_frame)} as it is immutable.'
|
| 792 |
+
)
|
| 793 |
+
raise AttributeError(msg)
|
| 794 |
+
if isinstance(reaction_frame, RigidBody):
|
| 795 |
+
reaction_frame = reaction_frame.frame
|
| 796 |
+
elif (
|
| 797 |
+
not isinstance(reaction_frame, ReferenceFrame)
|
| 798 |
+
and reaction_frame is not None
|
| 799 |
+
):
|
| 800 |
+
msg = (
|
| 801 |
+
f'Value {repr(reaction_frame)} passed to `reaction_frame` was '
|
| 802 |
+
f'of type {type(reaction_frame)}, must be {ReferenceFrame}.'
|
| 803 |
+
)
|
| 804 |
+
raise TypeError(msg)
|
| 805 |
+
self._reaction_frame = reaction_frame
|
| 806 |
+
|
| 807 |
+
def to_loads(self):
|
| 808 |
+
"""Loads required by the equations of motion method classes.
|
| 809 |
+
|
| 810 |
+
Explanation
|
| 811 |
+
===========
|
| 812 |
+
|
| 813 |
+
``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be
|
| 814 |
+
passed to the ``loads`` parameters of its ``kanes_equations`` method
|
| 815 |
+
when constructing the equations of motion. This method acts as a
|
| 816 |
+
utility to produce the correctly-structred pairs of points and vectors
|
| 817 |
+
required so that these can be easily concatenated with other items in
|
| 818 |
+
the list of loads and passed to ``KanesMethod.kanes_equations``. These
|
| 819 |
+
loads are also in the correct form to also be passed to the other
|
| 820 |
+
equations of motion method classes, e.g. ``LagrangesMethod``.
|
| 821 |
+
|
| 822 |
+
Examples
|
| 823 |
+
========
|
| 824 |
+
|
| 825 |
+
The below example shows how to generate the loads produced by a torque
|
| 826 |
+
actuator that acts on a pair of bodies attached by a pin joint.
|
| 827 |
+
|
| 828 |
+
>>> from sympy import symbols
|
| 829 |
+
>>> from sympy.physics.mechanics import (PinJoint, ReferenceFrame,
|
| 830 |
+
... RigidBody, TorqueActuator)
|
| 831 |
+
>>> torque = symbols('T')
|
| 832 |
+
>>> N = ReferenceFrame('N')
|
| 833 |
+
>>> A = ReferenceFrame('A')
|
| 834 |
+
>>> parent = RigidBody('parent', frame=N)
|
| 835 |
+
>>> child = RigidBody('child', frame=A)
|
| 836 |
+
>>> pin_joint = PinJoint(
|
| 837 |
+
... 'pin',
|
| 838 |
+
... parent,
|
| 839 |
+
... child,
|
| 840 |
+
... joint_axis=N.z,
|
| 841 |
+
... )
|
| 842 |
+
>>> actuator = TorqueActuator.at_pin_joint(torque, pin_joint)
|
| 843 |
+
|
| 844 |
+
The forces produces by the damper can be generated by calling the
|
| 845 |
+
``to_loads`` method.
|
| 846 |
+
|
| 847 |
+
>>> actuator.to_loads()
|
| 848 |
+
[(A, T*N.z), (N, - T*N.z)]
|
| 849 |
+
|
| 850 |
+
Alternatively, if a torque actuator is created without a reaction frame
|
| 851 |
+
then the loads returned by the ``to_loads`` method will contain just
|
| 852 |
+
the single load acting on the target frame.
|
| 853 |
+
|
| 854 |
+
>>> actuator = TorqueActuator(torque, N.z, N)
|
| 855 |
+
>>> actuator.to_loads()
|
| 856 |
+
[(N, T*N.z)]
|
| 857 |
+
|
| 858 |
+
"""
|
| 859 |
+
loads = [
|
| 860 |
+
Torque(self.target_frame, self.torque*self.axis),
|
| 861 |
+
]
|
| 862 |
+
if self.reaction_frame is not None:
|
| 863 |
+
loads.append(Torque(self.reaction_frame, -self.torque*self.axis))
|
| 864 |
+
return loads
|
| 865 |
+
|
| 866 |
+
def __repr__(self):
|
| 867 |
+
"""Representation of a ``TorqueActuator``."""
|
| 868 |
+
string = (
|
| 869 |
+
f'{self.__class__.__name__}({self.torque}, axis={self.axis}, '
|
| 870 |
+
f'target_frame={self.target_frame}'
|
| 871 |
+
)
|
| 872 |
+
if self.reaction_frame is not None:
|
| 873 |
+
string += f', reaction_frame={self.reaction_frame})'
|
| 874 |
+
else:
|
| 875 |
+
string += ')'
|
| 876 |
+
return string
|
| 877 |
+
|
| 878 |
+
|
| 879 |
+
class DuffingSpring(ForceActuator):
|
| 880 |
+
"""A nonlinear spring based on the Duffing equation.
|
| 881 |
+
|
| 882 |
+
Explanation
|
| 883 |
+
===========
|
| 884 |
+
|
| 885 |
+
Here, ``DuffingSpring`` represents the force exerted by a nonlinear spring based on the Duffing equation:
|
| 886 |
+
F = -beta*x-alpha*x**3, where x is the displacement from the equilibrium position, beta is the linear spring constant,
|
| 887 |
+
and alpha is the coefficient for the nonlinear cubic term.
|
| 888 |
+
|
| 889 |
+
Parameters
|
| 890 |
+
==========
|
| 891 |
+
|
| 892 |
+
linear_stiffness : Expr
|
| 893 |
+
The linear stiffness coefficient (beta).
|
| 894 |
+
nonlinear_stiffness : Expr
|
| 895 |
+
The nonlinear stiffness coefficient (alpha).
|
| 896 |
+
pathway : PathwayBase
|
| 897 |
+
The pathway that the actuator follows.
|
| 898 |
+
equilibrium_length : Expr, optional
|
| 899 |
+
The length at which the spring is in equilibrium (x).
|
| 900 |
+
"""
|
| 901 |
+
|
| 902 |
+
def __init__(self, linear_stiffness, nonlinear_stiffness, pathway, equilibrium_length=S.Zero):
|
| 903 |
+
self.linear_stiffness = sympify(linear_stiffness, strict=True)
|
| 904 |
+
self.nonlinear_stiffness = sympify(nonlinear_stiffness, strict=True)
|
| 905 |
+
self.equilibrium_length = sympify(equilibrium_length, strict=True)
|
| 906 |
+
|
| 907 |
+
if not isinstance(pathway, PathwayBase):
|
| 908 |
+
raise TypeError("pathway must be an instance of PathwayBase.")
|
| 909 |
+
self._pathway = pathway
|
| 910 |
+
|
| 911 |
+
@property
|
| 912 |
+
def linear_stiffness(self):
|
| 913 |
+
return self._linear_stiffness
|
| 914 |
+
|
| 915 |
+
@linear_stiffness.setter
|
| 916 |
+
def linear_stiffness(self, linear_stiffness):
|
| 917 |
+
if hasattr(self, '_linear_stiffness'):
|
| 918 |
+
msg = (
|
| 919 |
+
f'Can\'t set attribute `linear_stiffness` to '
|
| 920 |
+
f'{repr(linear_stiffness)} as it is immutable.'
|
| 921 |
+
)
|
| 922 |
+
raise AttributeError(msg)
|
| 923 |
+
self._linear_stiffness = sympify(linear_stiffness, strict=True)
|
| 924 |
+
|
| 925 |
+
@property
|
| 926 |
+
def nonlinear_stiffness(self):
|
| 927 |
+
return self._nonlinear_stiffness
|
| 928 |
+
|
| 929 |
+
@nonlinear_stiffness.setter
|
| 930 |
+
def nonlinear_stiffness(self, nonlinear_stiffness):
|
| 931 |
+
if hasattr(self, '_nonlinear_stiffness'):
|
| 932 |
+
msg = (
|
| 933 |
+
f'Can\'t set attribute `nonlinear_stiffness` to '
|
| 934 |
+
f'{repr(nonlinear_stiffness)} as it is immutable.'
|
| 935 |
+
)
|
| 936 |
+
raise AttributeError(msg)
|
| 937 |
+
self._nonlinear_stiffness = sympify(nonlinear_stiffness, strict=True)
|
| 938 |
+
|
| 939 |
+
@property
|
| 940 |
+
def pathway(self):
|
| 941 |
+
return self._pathway
|
| 942 |
+
|
| 943 |
+
@pathway.setter
|
| 944 |
+
def pathway(self, pathway):
|
| 945 |
+
if hasattr(self, '_pathway'):
|
| 946 |
+
msg = (
|
| 947 |
+
f'Can\'t set attribute `pathway` to {repr(pathway)} as it is '
|
| 948 |
+
f'immutable.'
|
| 949 |
+
)
|
| 950 |
+
raise AttributeError(msg)
|
| 951 |
+
if not isinstance(pathway, PathwayBase):
|
| 952 |
+
msg = (
|
| 953 |
+
f'Value {repr(pathway)} passed to `pathway` was of type '
|
| 954 |
+
f'{type(pathway)}, must be {PathwayBase}.'
|
| 955 |
+
)
|
| 956 |
+
raise TypeError(msg)
|
| 957 |
+
self._pathway = pathway
|
| 958 |
+
|
| 959 |
+
@property
|
| 960 |
+
def equilibrium_length(self):
|
| 961 |
+
return self._equilibrium_length
|
| 962 |
+
|
| 963 |
+
@equilibrium_length.setter
|
| 964 |
+
def equilibrium_length(self, equilibrium_length):
|
| 965 |
+
if hasattr(self, '_equilibrium_length'):
|
| 966 |
+
msg = (
|
| 967 |
+
f'Can\'t set attribute `equilibrium_length` to '
|
| 968 |
+
f'{repr(equilibrium_length)} as it is immutable.'
|
| 969 |
+
)
|
| 970 |
+
raise AttributeError(msg)
|
| 971 |
+
self._equilibrium_length = sympify(equilibrium_length, strict=True)
|
| 972 |
+
|
| 973 |
+
@property
|
| 974 |
+
def force(self):
|
| 975 |
+
"""The force produced by the Duffing spring."""
|
| 976 |
+
displacement = self.pathway.length - self.equilibrium_length
|
| 977 |
+
return -self.linear_stiffness * displacement - self.nonlinear_stiffness * displacement**3
|
| 978 |
+
|
| 979 |
+
@force.setter
|
| 980 |
+
def force(self, force):
|
| 981 |
+
if hasattr(self, '_force'):
|
| 982 |
+
msg = (
|
| 983 |
+
f'Can\'t set attribute `force` to {repr(force)} as it is '
|
| 984 |
+
f'immutable.'
|
| 985 |
+
)
|
| 986 |
+
raise AttributeError(msg)
|
| 987 |
+
self._force = sympify(force, strict=True)
|
| 988 |
+
|
| 989 |
+
def __repr__(self):
|
| 990 |
+
return (f"{self.__class__.__name__}("
|
| 991 |
+
f"{self.linear_stiffness}, {self.nonlinear_stiffness}, {self.pathway}, "
|
| 992 |
+
f"equilibrium_length={self.equilibrium_length})")
|
lib/python3.12/site-packages/sympy/physics/mechanics/body.py
ADDED
|
@@ -0,0 +1,710 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import Symbol
|
| 2 |
+
from sympy.physics.vector import Point, Vector, ReferenceFrame, Dyadic
|
| 3 |
+
from sympy.physics.mechanics import RigidBody, Particle, Inertia
|
| 4 |
+
from sympy.physics.mechanics.body_base import BodyBase
|
| 5 |
+
from sympy.utilities.exceptions import sympy_deprecation_warning
|
| 6 |
+
|
| 7 |
+
__all__ = ['Body']
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# XXX: We use type:ignore because the classes RigidBody and Particle have
|
| 11 |
+
# inconsistent parallel axis methods that take different numbers of arguments.
|
| 12 |
+
class Body(RigidBody, Particle): # type: ignore
|
| 13 |
+
"""
|
| 14 |
+
Body is a common representation of either a RigidBody or a Particle SymPy
|
| 15 |
+
object depending on what is passed in during initialization. If a mass is
|
| 16 |
+
passed in and central_inertia is left as None, the Particle object is
|
| 17 |
+
created. Otherwise a RigidBody object will be created.
|
| 18 |
+
|
| 19 |
+
.. deprecated:: 1.13
|
| 20 |
+
The Body class is deprecated. Its functionality is captured by
|
| 21 |
+
:class:`~.RigidBody` and :class:`~.Particle`.
|
| 22 |
+
|
| 23 |
+
Explanation
|
| 24 |
+
===========
|
| 25 |
+
|
| 26 |
+
The attributes that Body possesses will be the same as a Particle instance
|
| 27 |
+
or a Rigid Body instance depending on which was created. Additional
|
| 28 |
+
attributes are listed below.
|
| 29 |
+
|
| 30 |
+
Attributes
|
| 31 |
+
==========
|
| 32 |
+
|
| 33 |
+
name : string
|
| 34 |
+
The body's name
|
| 35 |
+
masscenter : Point
|
| 36 |
+
The point which represents the center of mass of the rigid body
|
| 37 |
+
frame : ReferenceFrame
|
| 38 |
+
The reference frame which the body is fixed in
|
| 39 |
+
mass : Sympifyable
|
| 40 |
+
The body's mass
|
| 41 |
+
inertia : (Dyadic, Point)
|
| 42 |
+
The body's inertia around its center of mass. This attribute is specific
|
| 43 |
+
to the rigid body form of Body and is left undefined for the Particle
|
| 44 |
+
form
|
| 45 |
+
loads : iterable
|
| 46 |
+
This list contains information on the different loads acting on the
|
| 47 |
+
Body. Forces are listed as a (point, vector) tuple and torques are
|
| 48 |
+
listed as (reference frame, vector) tuples.
|
| 49 |
+
|
| 50 |
+
Parameters
|
| 51 |
+
==========
|
| 52 |
+
|
| 53 |
+
name : String
|
| 54 |
+
Defines the name of the body. It is used as the base for defining
|
| 55 |
+
body specific properties.
|
| 56 |
+
masscenter : Point, optional
|
| 57 |
+
A point that represents the center of mass of the body or particle.
|
| 58 |
+
If no point is given, a point is generated.
|
| 59 |
+
mass : Sympifyable, optional
|
| 60 |
+
A Sympifyable object which represents the mass of the body. If no
|
| 61 |
+
mass is passed, one is generated.
|
| 62 |
+
frame : ReferenceFrame, optional
|
| 63 |
+
The ReferenceFrame that represents the reference frame of the body.
|
| 64 |
+
If no frame is given, a frame is generated.
|
| 65 |
+
central_inertia : Dyadic, optional
|
| 66 |
+
Central inertia dyadic of the body. If none is passed while creating
|
| 67 |
+
RigidBody, a default inertia is generated.
|
| 68 |
+
|
| 69 |
+
Examples
|
| 70 |
+
========
|
| 71 |
+
|
| 72 |
+
As Body has been deprecated, the following examples are for illustrative
|
| 73 |
+
purposes only. The functionality of Body is fully captured by
|
| 74 |
+
:class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation
|
| 75 |
+
warning we can use the ignore_warnings context manager.
|
| 76 |
+
|
| 77 |
+
>>> from sympy.utilities.exceptions import ignore_warnings
|
| 78 |
+
|
| 79 |
+
Default behaviour. This results in the creation of a RigidBody object for
|
| 80 |
+
which the mass, mass center, frame and inertia attributes are given default
|
| 81 |
+
values. ::
|
| 82 |
+
|
| 83 |
+
>>> from sympy.physics.mechanics import Body
|
| 84 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 85 |
+
... body = Body('name_of_body')
|
| 86 |
+
|
| 87 |
+
This next example demonstrates the code required to specify all of the
|
| 88 |
+
values of the Body object. Note this will also create a RigidBody version of
|
| 89 |
+
the Body object. ::
|
| 90 |
+
|
| 91 |
+
>>> from sympy import Symbol
|
| 92 |
+
>>> from sympy.physics.mechanics import ReferenceFrame, Point, inertia
|
| 93 |
+
>>> from sympy.physics.mechanics import Body
|
| 94 |
+
>>> mass = Symbol('mass')
|
| 95 |
+
>>> masscenter = Point('masscenter')
|
| 96 |
+
>>> frame = ReferenceFrame('frame')
|
| 97 |
+
>>> ixx = Symbol('ixx')
|
| 98 |
+
>>> body_inertia = inertia(frame, ixx, 0, 0)
|
| 99 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 100 |
+
... body = Body('name_of_body', masscenter, mass, frame, body_inertia)
|
| 101 |
+
|
| 102 |
+
The minimal code required to create a Particle version of the Body object
|
| 103 |
+
involves simply passing in a name and a mass. ::
|
| 104 |
+
|
| 105 |
+
>>> from sympy import Symbol
|
| 106 |
+
>>> from sympy.physics.mechanics import Body
|
| 107 |
+
>>> mass = Symbol('mass')
|
| 108 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 109 |
+
... body = Body('name_of_body', mass=mass)
|
| 110 |
+
|
| 111 |
+
The Particle version of the Body object can also receive a masscenter point
|
| 112 |
+
and a reference frame, just not an inertia.
|
| 113 |
+
"""
|
| 114 |
+
|
| 115 |
+
def __init__(self, name, masscenter=None, mass=None, frame=None,
|
| 116 |
+
central_inertia=None):
|
| 117 |
+
sympy_deprecation_warning(
|
| 118 |
+
"""
|
| 119 |
+
Support for the Body class has been removed, as its functionality is
|
| 120 |
+
fully captured by RigidBody and Particle.
|
| 121 |
+
""",
|
| 122 |
+
deprecated_since_version="1.13",
|
| 123 |
+
active_deprecations_target="deprecated-mechanics-body-class"
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
self._loads = []
|
| 127 |
+
|
| 128 |
+
if frame is None:
|
| 129 |
+
frame = ReferenceFrame(name + '_frame')
|
| 130 |
+
|
| 131 |
+
if masscenter is None:
|
| 132 |
+
masscenter = Point(name + '_masscenter')
|
| 133 |
+
|
| 134 |
+
if central_inertia is None and mass is None:
|
| 135 |
+
ixx = Symbol(name + '_ixx')
|
| 136 |
+
iyy = Symbol(name + '_iyy')
|
| 137 |
+
izz = Symbol(name + '_izz')
|
| 138 |
+
izx = Symbol(name + '_izx')
|
| 139 |
+
ixy = Symbol(name + '_ixy')
|
| 140 |
+
iyz = Symbol(name + '_iyz')
|
| 141 |
+
_inertia = Inertia.from_inertia_scalars(masscenter, frame, ixx, iyy,
|
| 142 |
+
izz, ixy, iyz, izx)
|
| 143 |
+
else:
|
| 144 |
+
_inertia = (central_inertia, masscenter)
|
| 145 |
+
|
| 146 |
+
if mass is None:
|
| 147 |
+
_mass = Symbol(name + '_mass')
|
| 148 |
+
else:
|
| 149 |
+
_mass = mass
|
| 150 |
+
|
| 151 |
+
masscenter.set_vel(frame, 0)
|
| 152 |
+
|
| 153 |
+
# If user passes masscenter and mass then a particle is created
|
| 154 |
+
# otherwise a rigidbody. As a result a body may or may not have inertia.
|
| 155 |
+
# Note: BodyBase.__init__ is used to prevent problems with super() calls in
|
| 156 |
+
# Particle and RigidBody arising due to multiple inheritance.
|
| 157 |
+
if central_inertia is None and mass is not None:
|
| 158 |
+
BodyBase.__init__(self, name, masscenter, _mass)
|
| 159 |
+
self.frame = frame
|
| 160 |
+
self._central_inertia = Dyadic(0)
|
| 161 |
+
else:
|
| 162 |
+
BodyBase.__init__(self, name, masscenter, _mass)
|
| 163 |
+
self.frame = frame
|
| 164 |
+
self.inertia = _inertia
|
| 165 |
+
|
| 166 |
+
def __repr__(self):
|
| 167 |
+
if self.is_rigidbody:
|
| 168 |
+
return RigidBody.__repr__(self)
|
| 169 |
+
return Particle.__repr__(self)
|
| 170 |
+
|
| 171 |
+
@property
|
| 172 |
+
def loads(self):
|
| 173 |
+
return self._loads
|
| 174 |
+
|
| 175 |
+
@property
|
| 176 |
+
def x(self):
|
| 177 |
+
"""The basis Vector for the Body, in the x direction."""
|
| 178 |
+
return self.frame.x
|
| 179 |
+
|
| 180 |
+
@property
|
| 181 |
+
def y(self):
|
| 182 |
+
"""The basis Vector for the Body, in the y direction."""
|
| 183 |
+
return self.frame.y
|
| 184 |
+
|
| 185 |
+
@property
|
| 186 |
+
def z(self):
|
| 187 |
+
"""The basis Vector for the Body, in the z direction."""
|
| 188 |
+
return self.frame.z
|
| 189 |
+
|
| 190 |
+
@property
|
| 191 |
+
def inertia(self):
|
| 192 |
+
"""The body's inertia about a point; stored as (Dyadic, Point)."""
|
| 193 |
+
if self.is_rigidbody:
|
| 194 |
+
return RigidBody.inertia.fget(self)
|
| 195 |
+
return (self.central_inertia, self.masscenter)
|
| 196 |
+
|
| 197 |
+
@inertia.setter
|
| 198 |
+
def inertia(self, I):
|
| 199 |
+
RigidBody.inertia.fset(self, I)
|
| 200 |
+
|
| 201 |
+
@property
|
| 202 |
+
def is_rigidbody(self):
|
| 203 |
+
if hasattr(self, '_inertia'):
|
| 204 |
+
return True
|
| 205 |
+
return False
|
| 206 |
+
|
| 207 |
+
def kinetic_energy(self, frame):
|
| 208 |
+
"""Kinetic energy of the body.
|
| 209 |
+
|
| 210 |
+
Parameters
|
| 211 |
+
==========
|
| 212 |
+
|
| 213 |
+
frame : ReferenceFrame or Body
|
| 214 |
+
The Body's angular velocity and the velocity of it's mass
|
| 215 |
+
center are typically defined with respect to an inertial frame but
|
| 216 |
+
any relevant frame in which the velocities are known can be supplied.
|
| 217 |
+
|
| 218 |
+
Examples
|
| 219 |
+
========
|
| 220 |
+
|
| 221 |
+
As Body has been deprecated, the following examples are for illustrative
|
| 222 |
+
purposes only. The functionality of Body is fully captured by
|
| 223 |
+
:class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation
|
| 224 |
+
warning we can use the ignore_warnings context manager.
|
| 225 |
+
|
| 226 |
+
>>> from sympy.utilities.exceptions import ignore_warnings
|
| 227 |
+
>>> from sympy.physics.mechanics import Body, ReferenceFrame, Point
|
| 228 |
+
>>> from sympy import symbols
|
| 229 |
+
>>> m, v, r, omega = symbols('m v r omega')
|
| 230 |
+
>>> N = ReferenceFrame('N')
|
| 231 |
+
>>> O = Point('O')
|
| 232 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 233 |
+
... P = Body('P', masscenter=O, mass=m)
|
| 234 |
+
>>> P.masscenter.set_vel(N, v * N.y)
|
| 235 |
+
>>> P.kinetic_energy(N)
|
| 236 |
+
m*v**2/2
|
| 237 |
+
|
| 238 |
+
>>> N = ReferenceFrame('N')
|
| 239 |
+
>>> b = ReferenceFrame('b')
|
| 240 |
+
>>> b.set_ang_vel(N, omega * b.x)
|
| 241 |
+
>>> P = Point('P')
|
| 242 |
+
>>> P.set_vel(N, v * N.x)
|
| 243 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 244 |
+
... B = Body('B', masscenter=P, frame=b)
|
| 245 |
+
>>> B.kinetic_energy(N)
|
| 246 |
+
B_ixx*omega**2/2 + B_mass*v**2/2
|
| 247 |
+
|
| 248 |
+
See Also
|
| 249 |
+
========
|
| 250 |
+
|
| 251 |
+
sympy.physics.mechanics : Particle, RigidBody
|
| 252 |
+
|
| 253 |
+
"""
|
| 254 |
+
if isinstance(frame, Body):
|
| 255 |
+
frame = Body.frame
|
| 256 |
+
if self.is_rigidbody:
|
| 257 |
+
return RigidBody(self.name, self.masscenter, self.frame, self.mass,
|
| 258 |
+
(self.central_inertia, self.masscenter)).kinetic_energy(frame)
|
| 259 |
+
return Particle(self.name, self.masscenter, self.mass).kinetic_energy(frame)
|
| 260 |
+
|
| 261 |
+
def apply_force(self, force, point=None, reaction_body=None, reaction_point=None):
|
| 262 |
+
"""Add force to the body(s).
|
| 263 |
+
|
| 264 |
+
Explanation
|
| 265 |
+
===========
|
| 266 |
+
|
| 267 |
+
Applies the force on self or equal and opposite forces on
|
| 268 |
+
self and other body if both are given on the desired point on the bodies.
|
| 269 |
+
The force applied on other body is taken opposite of self, i.e, -force.
|
| 270 |
+
|
| 271 |
+
Parameters
|
| 272 |
+
==========
|
| 273 |
+
|
| 274 |
+
force: Vector
|
| 275 |
+
The force to be applied.
|
| 276 |
+
point: Point, optional
|
| 277 |
+
The point on self on which force is applied.
|
| 278 |
+
By default self's masscenter.
|
| 279 |
+
reaction_body: Body, optional
|
| 280 |
+
Second body on which equal and opposite force
|
| 281 |
+
is to be applied.
|
| 282 |
+
reaction_point : Point, optional
|
| 283 |
+
The point on other body on which equal and opposite
|
| 284 |
+
force is applied. By default masscenter of other body.
|
| 285 |
+
|
| 286 |
+
Example
|
| 287 |
+
=======
|
| 288 |
+
|
| 289 |
+
As Body has been deprecated, the following examples are for illustrative
|
| 290 |
+
purposes only. The functionality of Body is fully captured by
|
| 291 |
+
:class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation
|
| 292 |
+
warning we can use the ignore_warnings context manager.
|
| 293 |
+
|
| 294 |
+
>>> from sympy.utilities.exceptions import ignore_warnings
|
| 295 |
+
>>> from sympy import symbols
|
| 296 |
+
>>> from sympy.physics.mechanics import Body, Point, dynamicsymbols
|
| 297 |
+
>>> m, g = symbols('m g')
|
| 298 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 299 |
+
... B = Body('B')
|
| 300 |
+
>>> force1 = m*g*B.z
|
| 301 |
+
>>> B.apply_force(force1) #Applying force on B's masscenter
|
| 302 |
+
>>> B.loads
|
| 303 |
+
[(B_masscenter, g*m*B_frame.z)]
|
| 304 |
+
|
| 305 |
+
We can also remove some part of force from any point on the body by
|
| 306 |
+
adding the opposite force to the body on that point.
|
| 307 |
+
|
| 308 |
+
>>> f1, f2 = dynamicsymbols('f1 f2')
|
| 309 |
+
>>> P = Point('P') #Considering point P on body B
|
| 310 |
+
>>> B.apply_force(f1*B.x + f2*B.y, P)
|
| 311 |
+
>>> B.loads
|
| 312 |
+
[(B_masscenter, g*m*B_frame.z), (P, f1(t)*B_frame.x + f2(t)*B_frame.y)]
|
| 313 |
+
|
| 314 |
+
Let's remove f1 from point P on body B.
|
| 315 |
+
|
| 316 |
+
>>> B.apply_force(-f1*B.x, P)
|
| 317 |
+
>>> B.loads
|
| 318 |
+
[(B_masscenter, g*m*B_frame.z), (P, f2(t)*B_frame.y)]
|
| 319 |
+
|
| 320 |
+
To further demonstrate the use of ``apply_force`` attribute,
|
| 321 |
+
consider two bodies connected through a spring.
|
| 322 |
+
|
| 323 |
+
>>> from sympy.physics.mechanics import Body, dynamicsymbols
|
| 324 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 325 |
+
... N = Body('N') #Newtonion Frame
|
| 326 |
+
>>> x = dynamicsymbols('x')
|
| 327 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 328 |
+
... B1 = Body('B1')
|
| 329 |
+
... B2 = Body('B2')
|
| 330 |
+
>>> spring_force = x*N.x
|
| 331 |
+
|
| 332 |
+
Now let's apply equal and opposite spring force to the bodies.
|
| 333 |
+
|
| 334 |
+
>>> P1 = Point('P1')
|
| 335 |
+
>>> P2 = Point('P2')
|
| 336 |
+
>>> B1.apply_force(spring_force, point=P1, reaction_body=B2, reaction_point=P2)
|
| 337 |
+
|
| 338 |
+
We can check the loads(forces) applied to bodies now.
|
| 339 |
+
|
| 340 |
+
>>> B1.loads
|
| 341 |
+
[(P1, x(t)*N_frame.x)]
|
| 342 |
+
>>> B2.loads
|
| 343 |
+
[(P2, - x(t)*N_frame.x)]
|
| 344 |
+
|
| 345 |
+
Notes
|
| 346 |
+
=====
|
| 347 |
+
|
| 348 |
+
If a new force is applied to a body on a point which already has some
|
| 349 |
+
force applied on it, then the new force is added to the already applied
|
| 350 |
+
force on that point.
|
| 351 |
+
|
| 352 |
+
"""
|
| 353 |
+
|
| 354 |
+
if not isinstance(point, Point):
|
| 355 |
+
if point is None:
|
| 356 |
+
point = self.masscenter # masscenter
|
| 357 |
+
else:
|
| 358 |
+
raise TypeError("Force must be applied to a point on the body.")
|
| 359 |
+
if not isinstance(force, Vector):
|
| 360 |
+
raise TypeError("Force must be a vector.")
|
| 361 |
+
|
| 362 |
+
if reaction_body is not None:
|
| 363 |
+
reaction_body.apply_force(-force, point=reaction_point)
|
| 364 |
+
|
| 365 |
+
for load in self._loads:
|
| 366 |
+
if point in load:
|
| 367 |
+
force += load[1]
|
| 368 |
+
self._loads.remove(load)
|
| 369 |
+
break
|
| 370 |
+
|
| 371 |
+
self._loads.append((point, force))
|
| 372 |
+
|
| 373 |
+
def apply_torque(self, torque, reaction_body=None):
|
| 374 |
+
"""Add torque to the body(s).
|
| 375 |
+
|
| 376 |
+
Explanation
|
| 377 |
+
===========
|
| 378 |
+
|
| 379 |
+
Applies the torque on self or equal and opposite torques on
|
| 380 |
+
self and other body if both are given.
|
| 381 |
+
The torque applied on other body is taken opposite of self,
|
| 382 |
+
i.e, -torque.
|
| 383 |
+
|
| 384 |
+
Parameters
|
| 385 |
+
==========
|
| 386 |
+
|
| 387 |
+
torque: Vector
|
| 388 |
+
The torque to be applied.
|
| 389 |
+
reaction_body: Body, optional
|
| 390 |
+
Second body on which equal and opposite torque
|
| 391 |
+
is to be applied.
|
| 392 |
+
|
| 393 |
+
Example
|
| 394 |
+
=======
|
| 395 |
+
|
| 396 |
+
As Body has been deprecated, the following examples are for illustrative
|
| 397 |
+
purposes only. The functionality of Body is fully captured by
|
| 398 |
+
:class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation
|
| 399 |
+
warning we can use the ignore_warnings context manager.
|
| 400 |
+
|
| 401 |
+
>>> from sympy.utilities.exceptions import ignore_warnings
|
| 402 |
+
>>> from sympy import symbols
|
| 403 |
+
>>> from sympy.physics.mechanics import Body, dynamicsymbols
|
| 404 |
+
>>> t = symbols('t')
|
| 405 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 406 |
+
... B = Body('B')
|
| 407 |
+
>>> torque1 = t*B.z
|
| 408 |
+
>>> B.apply_torque(torque1)
|
| 409 |
+
>>> B.loads
|
| 410 |
+
[(B_frame, t*B_frame.z)]
|
| 411 |
+
|
| 412 |
+
We can also remove some part of torque from the body by
|
| 413 |
+
adding the opposite torque to the body.
|
| 414 |
+
|
| 415 |
+
>>> t1, t2 = dynamicsymbols('t1 t2')
|
| 416 |
+
>>> B.apply_torque(t1*B.x + t2*B.y)
|
| 417 |
+
>>> B.loads
|
| 418 |
+
[(B_frame, t1(t)*B_frame.x + t2(t)*B_frame.y + t*B_frame.z)]
|
| 419 |
+
|
| 420 |
+
Let's remove t1 from Body B.
|
| 421 |
+
|
| 422 |
+
>>> B.apply_torque(-t1*B.x)
|
| 423 |
+
>>> B.loads
|
| 424 |
+
[(B_frame, t2(t)*B_frame.y + t*B_frame.z)]
|
| 425 |
+
|
| 426 |
+
To further demonstrate the use, let us consider two bodies such that
|
| 427 |
+
a torque `T` is acting on one body, and `-T` on the other.
|
| 428 |
+
|
| 429 |
+
>>> from sympy.physics.mechanics import Body, dynamicsymbols
|
| 430 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 431 |
+
... N = Body('N') #Newtonion frame
|
| 432 |
+
... B1 = Body('B1')
|
| 433 |
+
... B2 = Body('B2')
|
| 434 |
+
>>> v = dynamicsymbols('v')
|
| 435 |
+
>>> T = v*N.y #Torque
|
| 436 |
+
|
| 437 |
+
Now let's apply equal and opposite torque to the bodies.
|
| 438 |
+
|
| 439 |
+
>>> B1.apply_torque(T, B2)
|
| 440 |
+
|
| 441 |
+
We can check the loads (torques) applied to bodies now.
|
| 442 |
+
|
| 443 |
+
>>> B1.loads
|
| 444 |
+
[(B1_frame, v(t)*N_frame.y)]
|
| 445 |
+
>>> B2.loads
|
| 446 |
+
[(B2_frame, - v(t)*N_frame.y)]
|
| 447 |
+
|
| 448 |
+
Notes
|
| 449 |
+
=====
|
| 450 |
+
|
| 451 |
+
If a new torque is applied on body which already has some torque applied on it,
|
| 452 |
+
then the new torque is added to the previous torque about the body's frame.
|
| 453 |
+
|
| 454 |
+
"""
|
| 455 |
+
|
| 456 |
+
if not isinstance(torque, Vector):
|
| 457 |
+
raise TypeError("A Vector must be supplied to add torque.")
|
| 458 |
+
|
| 459 |
+
if reaction_body is not None:
|
| 460 |
+
reaction_body.apply_torque(-torque)
|
| 461 |
+
|
| 462 |
+
for load in self._loads:
|
| 463 |
+
if self.frame in load:
|
| 464 |
+
torque += load[1]
|
| 465 |
+
self._loads.remove(load)
|
| 466 |
+
break
|
| 467 |
+
self._loads.append((self.frame, torque))
|
| 468 |
+
|
| 469 |
+
def clear_loads(self):
|
| 470 |
+
"""
|
| 471 |
+
Clears the Body's loads list.
|
| 472 |
+
|
| 473 |
+
Example
|
| 474 |
+
=======
|
| 475 |
+
|
| 476 |
+
As Body has been deprecated, the following examples are for illustrative
|
| 477 |
+
purposes only. The functionality of Body is fully captured by
|
| 478 |
+
:class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation
|
| 479 |
+
warning we can use the ignore_warnings context manager.
|
| 480 |
+
|
| 481 |
+
>>> from sympy.utilities.exceptions import ignore_warnings
|
| 482 |
+
>>> from sympy.physics.mechanics import Body
|
| 483 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 484 |
+
... B = Body('B')
|
| 485 |
+
>>> force = B.x + B.y
|
| 486 |
+
>>> B.apply_force(force)
|
| 487 |
+
>>> B.loads
|
| 488 |
+
[(B_masscenter, B_frame.x + B_frame.y)]
|
| 489 |
+
>>> B.clear_loads()
|
| 490 |
+
>>> B.loads
|
| 491 |
+
[]
|
| 492 |
+
|
| 493 |
+
"""
|
| 494 |
+
|
| 495 |
+
self._loads = []
|
| 496 |
+
|
| 497 |
+
def remove_load(self, about=None):
|
| 498 |
+
"""
|
| 499 |
+
Remove load about a point or frame.
|
| 500 |
+
|
| 501 |
+
Parameters
|
| 502 |
+
==========
|
| 503 |
+
|
| 504 |
+
about : Point or ReferenceFrame, optional
|
| 505 |
+
The point about which force is applied,
|
| 506 |
+
and is to be removed.
|
| 507 |
+
If about is None, then the torque about
|
| 508 |
+
self's frame is removed.
|
| 509 |
+
|
| 510 |
+
Example
|
| 511 |
+
=======
|
| 512 |
+
|
| 513 |
+
As Body has been deprecated, the following examples are for illustrative
|
| 514 |
+
purposes only. The functionality of Body is fully captured by
|
| 515 |
+
:class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation
|
| 516 |
+
warning we can use the ignore_warnings context manager.
|
| 517 |
+
|
| 518 |
+
>>> from sympy.utilities.exceptions import ignore_warnings
|
| 519 |
+
>>> from sympy.physics.mechanics import Body, Point
|
| 520 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 521 |
+
... B = Body('B')
|
| 522 |
+
>>> P = Point('P')
|
| 523 |
+
>>> f1 = B.x
|
| 524 |
+
>>> f2 = B.y
|
| 525 |
+
>>> B.apply_force(f1)
|
| 526 |
+
>>> B.apply_force(f2, P)
|
| 527 |
+
>>> B.loads
|
| 528 |
+
[(B_masscenter, B_frame.x), (P, B_frame.y)]
|
| 529 |
+
|
| 530 |
+
>>> B.remove_load(P)
|
| 531 |
+
>>> B.loads
|
| 532 |
+
[(B_masscenter, B_frame.x)]
|
| 533 |
+
|
| 534 |
+
"""
|
| 535 |
+
|
| 536 |
+
if about is not None:
|
| 537 |
+
if not isinstance(about, Point):
|
| 538 |
+
raise TypeError('Load is applied about Point or ReferenceFrame.')
|
| 539 |
+
else:
|
| 540 |
+
about = self.frame
|
| 541 |
+
|
| 542 |
+
for load in self._loads:
|
| 543 |
+
if about in load:
|
| 544 |
+
self._loads.remove(load)
|
| 545 |
+
break
|
| 546 |
+
|
| 547 |
+
def masscenter_vel(self, body):
|
| 548 |
+
"""
|
| 549 |
+
Returns the velocity of the mass center with respect to the provided
|
| 550 |
+
rigid body or reference frame.
|
| 551 |
+
|
| 552 |
+
Parameters
|
| 553 |
+
==========
|
| 554 |
+
|
| 555 |
+
body: Body or ReferenceFrame
|
| 556 |
+
The rigid body or reference frame to calculate the velocity in.
|
| 557 |
+
|
| 558 |
+
Example
|
| 559 |
+
=======
|
| 560 |
+
|
| 561 |
+
As Body has been deprecated, the following examples are for illustrative
|
| 562 |
+
purposes only. The functionality of Body is fully captured by
|
| 563 |
+
:class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation
|
| 564 |
+
warning we can use the ignore_warnings context manager.
|
| 565 |
+
|
| 566 |
+
>>> from sympy.utilities.exceptions import ignore_warnings
|
| 567 |
+
>>> from sympy.physics.mechanics import Body
|
| 568 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 569 |
+
... A = Body('A')
|
| 570 |
+
... B = Body('B')
|
| 571 |
+
>>> A.masscenter.set_vel(B.frame, 5*B.frame.x)
|
| 572 |
+
>>> A.masscenter_vel(B)
|
| 573 |
+
5*B_frame.x
|
| 574 |
+
>>> A.masscenter_vel(B.frame)
|
| 575 |
+
5*B_frame.x
|
| 576 |
+
|
| 577 |
+
"""
|
| 578 |
+
|
| 579 |
+
if isinstance(body, ReferenceFrame):
|
| 580 |
+
frame=body
|
| 581 |
+
elif isinstance(body, Body):
|
| 582 |
+
frame = body.frame
|
| 583 |
+
return self.masscenter.vel(frame)
|
| 584 |
+
|
| 585 |
+
def ang_vel_in(self, body):
|
| 586 |
+
"""
|
| 587 |
+
Returns this body's angular velocity with respect to the provided
|
| 588 |
+
rigid body or reference frame.
|
| 589 |
+
|
| 590 |
+
Parameters
|
| 591 |
+
==========
|
| 592 |
+
|
| 593 |
+
body: Body or ReferenceFrame
|
| 594 |
+
The rigid body or reference frame to calculate the angular velocity in.
|
| 595 |
+
|
| 596 |
+
Example
|
| 597 |
+
=======
|
| 598 |
+
|
| 599 |
+
As Body has been deprecated, the following examples are for illustrative
|
| 600 |
+
purposes only. The functionality of Body is fully captured by
|
| 601 |
+
:class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation
|
| 602 |
+
warning we can use the ignore_warnings context manager.
|
| 603 |
+
|
| 604 |
+
>>> from sympy.utilities.exceptions import ignore_warnings
|
| 605 |
+
>>> from sympy.physics.mechanics import Body, ReferenceFrame
|
| 606 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 607 |
+
... A = Body('A')
|
| 608 |
+
>>> N = ReferenceFrame('N')
|
| 609 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 610 |
+
... B = Body('B', frame=N)
|
| 611 |
+
>>> A.frame.set_ang_vel(N, 5*N.x)
|
| 612 |
+
>>> A.ang_vel_in(B)
|
| 613 |
+
5*N.x
|
| 614 |
+
>>> A.ang_vel_in(N)
|
| 615 |
+
5*N.x
|
| 616 |
+
|
| 617 |
+
"""
|
| 618 |
+
|
| 619 |
+
if isinstance(body, ReferenceFrame):
|
| 620 |
+
frame=body
|
| 621 |
+
elif isinstance(body, Body):
|
| 622 |
+
frame = body.frame
|
| 623 |
+
return self.frame.ang_vel_in(frame)
|
| 624 |
+
|
| 625 |
+
def dcm(self, body):
|
| 626 |
+
"""
|
| 627 |
+
Returns the direction cosine matrix of this body relative to the
|
| 628 |
+
provided rigid body or reference frame.
|
| 629 |
+
|
| 630 |
+
Parameters
|
| 631 |
+
==========
|
| 632 |
+
|
| 633 |
+
body: Body or ReferenceFrame
|
| 634 |
+
The rigid body or reference frame to calculate the dcm.
|
| 635 |
+
|
| 636 |
+
Example
|
| 637 |
+
=======
|
| 638 |
+
|
| 639 |
+
As Body has been deprecated, the following examples are for illustrative
|
| 640 |
+
purposes only. The functionality of Body is fully captured by
|
| 641 |
+
:class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation
|
| 642 |
+
warning we can use the ignore_warnings context manager.
|
| 643 |
+
|
| 644 |
+
>>> from sympy.utilities.exceptions import ignore_warnings
|
| 645 |
+
>>> from sympy.physics.mechanics import Body
|
| 646 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 647 |
+
... A = Body('A')
|
| 648 |
+
... B = Body('B')
|
| 649 |
+
>>> A.frame.orient_axis(B.frame, B.frame.x, 5)
|
| 650 |
+
>>> A.dcm(B)
|
| 651 |
+
Matrix([
|
| 652 |
+
[1, 0, 0],
|
| 653 |
+
[0, cos(5), sin(5)],
|
| 654 |
+
[0, -sin(5), cos(5)]])
|
| 655 |
+
>>> A.dcm(B.frame)
|
| 656 |
+
Matrix([
|
| 657 |
+
[1, 0, 0],
|
| 658 |
+
[0, cos(5), sin(5)],
|
| 659 |
+
[0, -sin(5), cos(5)]])
|
| 660 |
+
|
| 661 |
+
"""
|
| 662 |
+
|
| 663 |
+
if isinstance(body, ReferenceFrame):
|
| 664 |
+
frame=body
|
| 665 |
+
elif isinstance(body, Body):
|
| 666 |
+
frame = body.frame
|
| 667 |
+
return self.frame.dcm(frame)
|
| 668 |
+
|
| 669 |
+
def parallel_axis(self, point, frame=None):
|
| 670 |
+
"""Returns the inertia dyadic of the body with respect to another
|
| 671 |
+
point.
|
| 672 |
+
|
| 673 |
+
Parameters
|
| 674 |
+
==========
|
| 675 |
+
|
| 676 |
+
point : sympy.physics.vector.Point
|
| 677 |
+
The point to express the inertia dyadic about.
|
| 678 |
+
frame : sympy.physics.vector.ReferenceFrame
|
| 679 |
+
The reference frame used to construct the dyadic.
|
| 680 |
+
|
| 681 |
+
Returns
|
| 682 |
+
=======
|
| 683 |
+
|
| 684 |
+
inertia : sympy.physics.vector.Dyadic
|
| 685 |
+
The inertia dyadic of the rigid body expressed about the provided
|
| 686 |
+
point.
|
| 687 |
+
|
| 688 |
+
Example
|
| 689 |
+
=======
|
| 690 |
+
|
| 691 |
+
As Body has been deprecated, the following examples are for illustrative
|
| 692 |
+
purposes only. The functionality of Body is fully captured by
|
| 693 |
+
:class:`~.RigidBody` and :class:`~.Particle`. To ignore the deprecation
|
| 694 |
+
warning we can use the ignore_warnings context manager.
|
| 695 |
+
|
| 696 |
+
>>> from sympy.utilities.exceptions import ignore_warnings
|
| 697 |
+
>>> from sympy.physics.mechanics import Body
|
| 698 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 699 |
+
... A = Body('A')
|
| 700 |
+
>>> P = A.masscenter.locatenew('point', 3 * A.x + 5 * A.y)
|
| 701 |
+
>>> A.parallel_axis(P).to_matrix(A.frame)
|
| 702 |
+
Matrix([
|
| 703 |
+
[A_ixx + 25*A_mass, A_ixy - 15*A_mass, A_izx],
|
| 704 |
+
[A_ixy - 15*A_mass, A_iyy + 9*A_mass, A_iyz],
|
| 705 |
+
[ A_izx, A_iyz, A_izz + 34*A_mass]])
|
| 706 |
+
|
| 707 |
+
"""
|
| 708 |
+
if self.is_rigidbody:
|
| 709 |
+
return RigidBody.parallel_axis(self, point, frame)
|
| 710 |
+
return Particle.parallel_axis(self, point, frame)
|
lib/python3.12/site-packages/sympy/physics/mechanics/body_base.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from abc import ABC, abstractmethod
|
| 2 |
+
from sympy import Symbol, sympify
|
| 3 |
+
from sympy.physics.vector import Point
|
| 4 |
+
|
| 5 |
+
__all__ = ['BodyBase']
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class BodyBase(ABC):
|
| 9 |
+
"""Abstract class for body type objects."""
|
| 10 |
+
def __init__(self, name, masscenter=None, mass=None):
|
| 11 |
+
# Note: If frame=None, no auto-generated frame is created, because a
|
| 12 |
+
# Particle does not need to have a frame by default.
|
| 13 |
+
if not isinstance(name, str):
|
| 14 |
+
raise TypeError('Supply a valid name.')
|
| 15 |
+
self._name = name
|
| 16 |
+
if mass is None:
|
| 17 |
+
mass = Symbol(f'{name}_mass')
|
| 18 |
+
if masscenter is None:
|
| 19 |
+
masscenter = Point(f'{name}_masscenter')
|
| 20 |
+
self.mass = mass
|
| 21 |
+
self.masscenter = masscenter
|
| 22 |
+
self.potential_energy = 0
|
| 23 |
+
self.points = []
|
| 24 |
+
|
| 25 |
+
def __str__(self):
|
| 26 |
+
return self.name
|
| 27 |
+
|
| 28 |
+
def __repr__(self):
|
| 29 |
+
return (f'{self.__class__.__name__}({repr(self.name)}, masscenter='
|
| 30 |
+
f'{repr(self.masscenter)}, mass={repr(self.mass)})')
|
| 31 |
+
|
| 32 |
+
@property
|
| 33 |
+
def name(self):
|
| 34 |
+
"""The name of the body."""
|
| 35 |
+
return self._name
|
| 36 |
+
|
| 37 |
+
@property
|
| 38 |
+
def masscenter(self):
|
| 39 |
+
"""The body's center of mass."""
|
| 40 |
+
return self._masscenter
|
| 41 |
+
|
| 42 |
+
@masscenter.setter
|
| 43 |
+
def masscenter(self, point):
|
| 44 |
+
if not isinstance(point, Point):
|
| 45 |
+
raise TypeError("The body's center of mass must be a Point object.")
|
| 46 |
+
self._masscenter = point
|
| 47 |
+
|
| 48 |
+
@property
|
| 49 |
+
def mass(self):
|
| 50 |
+
"""The body's mass."""
|
| 51 |
+
return self._mass
|
| 52 |
+
|
| 53 |
+
@mass.setter
|
| 54 |
+
def mass(self, mass):
|
| 55 |
+
self._mass = sympify(mass)
|
| 56 |
+
|
| 57 |
+
@property
|
| 58 |
+
def potential_energy(self):
|
| 59 |
+
"""The potential energy of the body.
|
| 60 |
+
|
| 61 |
+
Examples
|
| 62 |
+
========
|
| 63 |
+
|
| 64 |
+
>>> from sympy.physics.mechanics import Particle, Point
|
| 65 |
+
>>> from sympy import symbols
|
| 66 |
+
>>> m, g, h = symbols('m g h')
|
| 67 |
+
>>> O = Point('O')
|
| 68 |
+
>>> P = Particle('P', O, m)
|
| 69 |
+
>>> P.potential_energy = m * g * h
|
| 70 |
+
>>> P.potential_energy
|
| 71 |
+
g*h*m
|
| 72 |
+
|
| 73 |
+
"""
|
| 74 |
+
return self._potential_energy
|
| 75 |
+
|
| 76 |
+
@potential_energy.setter
|
| 77 |
+
def potential_energy(self, scalar):
|
| 78 |
+
self._potential_energy = sympify(scalar)
|
| 79 |
+
|
| 80 |
+
@abstractmethod
|
| 81 |
+
def kinetic_energy(self, frame):
|
| 82 |
+
pass
|
| 83 |
+
|
| 84 |
+
@abstractmethod
|
| 85 |
+
def linear_momentum(self, frame):
|
| 86 |
+
pass
|
| 87 |
+
|
| 88 |
+
@abstractmethod
|
| 89 |
+
def angular_momentum(self, point, frame):
|
| 90 |
+
pass
|
| 91 |
+
|
| 92 |
+
@abstractmethod
|
| 93 |
+
def parallel_axis(self, point, frame):
|
| 94 |
+
pass
|
lib/python3.12/site-packages/sympy/physics/mechanics/joint.py
ADDED
|
@@ -0,0 +1,2188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
|
| 3 |
+
from abc import ABC, abstractmethod
|
| 4 |
+
|
| 5 |
+
from sympy import pi, Derivative, Matrix
|
| 6 |
+
from sympy.core.function import AppliedUndef
|
| 7 |
+
from sympy.physics.mechanics.body_base import BodyBase
|
| 8 |
+
from sympy.physics.mechanics.functions import _validate_coordinates
|
| 9 |
+
from sympy.physics.vector import (Vector, dynamicsymbols, cross, Point,
|
| 10 |
+
ReferenceFrame)
|
| 11 |
+
from sympy.utilities.iterables import iterable
|
| 12 |
+
from sympy.utilities.exceptions import sympy_deprecation_warning
|
| 13 |
+
|
| 14 |
+
__all__ = ['Joint', 'PinJoint', 'PrismaticJoint', 'CylindricalJoint',
|
| 15 |
+
'PlanarJoint', 'SphericalJoint', 'WeldJoint']
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class Joint(ABC):
|
| 19 |
+
"""Abstract base class for all specific joints.
|
| 20 |
+
|
| 21 |
+
Explanation
|
| 22 |
+
===========
|
| 23 |
+
|
| 24 |
+
A joint subtracts degrees of freedom from a body. This is the base class
|
| 25 |
+
for all specific joints and holds all common methods acting as an interface
|
| 26 |
+
for all joints. Custom joint can be created by inheriting Joint class and
|
| 27 |
+
defining all abstract functions.
|
| 28 |
+
|
| 29 |
+
The abstract methods are:
|
| 30 |
+
|
| 31 |
+
- ``_generate_coordinates``
|
| 32 |
+
- ``_generate_speeds``
|
| 33 |
+
- ``_orient_frames``
|
| 34 |
+
- ``_set_angular_velocity``
|
| 35 |
+
- ``_set_linear_velocity``
|
| 36 |
+
|
| 37 |
+
Parameters
|
| 38 |
+
==========
|
| 39 |
+
|
| 40 |
+
name : string
|
| 41 |
+
A unique name for the joint.
|
| 42 |
+
parent : Particle or RigidBody or Body
|
| 43 |
+
The parent body of joint.
|
| 44 |
+
child : Particle or RigidBody or Body
|
| 45 |
+
The child body of joint.
|
| 46 |
+
coordinates : iterable of dynamicsymbols, optional
|
| 47 |
+
Generalized coordinates of the joint.
|
| 48 |
+
speeds : iterable of dynamicsymbols, optional
|
| 49 |
+
Generalized speeds of joint.
|
| 50 |
+
parent_point : Point or Vector, optional
|
| 51 |
+
Attachment point where the joint is fixed to the parent body. If a
|
| 52 |
+
vector is provided, then the attachment point is computed by adding the
|
| 53 |
+
vector to the body's mass center. The default value is the parent's mass
|
| 54 |
+
center.
|
| 55 |
+
child_point : Point or Vector, optional
|
| 56 |
+
Attachment point where the joint is fixed to the child body. If a
|
| 57 |
+
vector is provided, then the attachment point is computed by adding the
|
| 58 |
+
vector to the body's mass center. The default value is the child's mass
|
| 59 |
+
center.
|
| 60 |
+
parent_axis : Vector, optional
|
| 61 |
+
.. deprecated:: 1.12
|
| 62 |
+
Axis fixed in the parent body which aligns with an axis fixed in the
|
| 63 |
+
child body. The default is the x axis of parent's reference frame.
|
| 64 |
+
For more information on this deprecation, see
|
| 65 |
+
:ref:`deprecated-mechanics-joint-axis`.
|
| 66 |
+
child_axis : Vector, optional
|
| 67 |
+
.. deprecated:: 1.12
|
| 68 |
+
Axis fixed in the child body which aligns with an axis fixed in the
|
| 69 |
+
parent body. The default is the x axis of child's reference frame.
|
| 70 |
+
For more information on this deprecation, see
|
| 71 |
+
:ref:`deprecated-mechanics-joint-axis`.
|
| 72 |
+
parent_interframe : ReferenceFrame, optional
|
| 73 |
+
Intermediate frame of the parent body with respect to which the joint
|
| 74 |
+
transformation is formulated. If a Vector is provided then an interframe
|
| 75 |
+
is created which aligns its X axis with the given vector. The default
|
| 76 |
+
value is the parent's own frame.
|
| 77 |
+
child_interframe : ReferenceFrame, optional
|
| 78 |
+
Intermediate frame of the child body with respect to which the joint
|
| 79 |
+
transformation is formulated. If a Vector is provided then an interframe
|
| 80 |
+
is created which aligns its X axis with the given vector. The default
|
| 81 |
+
value is the child's own frame.
|
| 82 |
+
parent_joint_pos : Point or Vector, optional
|
| 83 |
+
.. deprecated:: 1.12
|
| 84 |
+
This argument is replaced by parent_point and will be removed in a
|
| 85 |
+
future version.
|
| 86 |
+
See :ref:`deprecated-mechanics-joint-pos` for more information.
|
| 87 |
+
child_joint_pos : Point or Vector, optional
|
| 88 |
+
.. deprecated:: 1.12
|
| 89 |
+
This argument is replaced by child_point and will be removed in a
|
| 90 |
+
future version.
|
| 91 |
+
See :ref:`deprecated-mechanics-joint-pos` for more information.
|
| 92 |
+
|
| 93 |
+
Attributes
|
| 94 |
+
==========
|
| 95 |
+
|
| 96 |
+
name : string
|
| 97 |
+
The joint's name.
|
| 98 |
+
parent : Particle or RigidBody or Body
|
| 99 |
+
The joint's parent body.
|
| 100 |
+
child : Particle or RigidBody or Body
|
| 101 |
+
The joint's child body.
|
| 102 |
+
coordinates : Matrix
|
| 103 |
+
Matrix of the joint's generalized coordinates.
|
| 104 |
+
speeds : Matrix
|
| 105 |
+
Matrix of the joint's generalized speeds.
|
| 106 |
+
parent_point : Point
|
| 107 |
+
Attachment point where the joint is fixed to the parent body.
|
| 108 |
+
child_point : Point
|
| 109 |
+
Attachment point where the joint is fixed to the child body.
|
| 110 |
+
parent_axis : Vector
|
| 111 |
+
The axis fixed in the parent frame that represents the joint.
|
| 112 |
+
child_axis : Vector
|
| 113 |
+
The axis fixed in the child frame that represents the joint.
|
| 114 |
+
parent_interframe : ReferenceFrame
|
| 115 |
+
Intermediate frame of the parent body with respect to which the joint
|
| 116 |
+
transformation is formulated.
|
| 117 |
+
child_interframe : ReferenceFrame
|
| 118 |
+
Intermediate frame of the child body with respect to which the joint
|
| 119 |
+
transformation is formulated.
|
| 120 |
+
kdes : Matrix
|
| 121 |
+
Kinematical differential equations of the joint.
|
| 122 |
+
|
| 123 |
+
Notes
|
| 124 |
+
=====
|
| 125 |
+
|
| 126 |
+
When providing a vector as the intermediate frame, a new intermediate frame
|
| 127 |
+
is created which aligns its X axis with the provided vector. This is done
|
| 128 |
+
with a single fixed rotation about a rotation axis. This rotation axis is
|
| 129 |
+
determined by taking the cross product of the ``body.x`` axis with the
|
| 130 |
+
provided vector. In the case where the provided vector is in the ``-body.x``
|
| 131 |
+
direction, the rotation is done about the ``body.y`` axis.
|
| 132 |
+
|
| 133 |
+
"""
|
| 134 |
+
|
| 135 |
+
def __init__(self, name, parent, child, coordinates=None, speeds=None,
|
| 136 |
+
parent_point=None, child_point=None, parent_interframe=None,
|
| 137 |
+
child_interframe=None, parent_axis=None, child_axis=None,
|
| 138 |
+
parent_joint_pos=None, child_joint_pos=None):
|
| 139 |
+
|
| 140 |
+
if not isinstance(name, str):
|
| 141 |
+
raise TypeError('Supply a valid name.')
|
| 142 |
+
self._name = name
|
| 143 |
+
|
| 144 |
+
if not isinstance(parent, BodyBase):
|
| 145 |
+
raise TypeError('Parent must be a body.')
|
| 146 |
+
self._parent = parent
|
| 147 |
+
|
| 148 |
+
if not isinstance(child, BodyBase):
|
| 149 |
+
raise TypeError('Child must be a body.')
|
| 150 |
+
self._child = child
|
| 151 |
+
|
| 152 |
+
if parent_axis is not None or child_axis is not None:
|
| 153 |
+
sympy_deprecation_warning(
|
| 154 |
+
"""
|
| 155 |
+
The parent_axis and child_axis arguments for the Joint classes
|
| 156 |
+
are deprecated. Instead use parent_interframe, child_interframe.
|
| 157 |
+
""",
|
| 158 |
+
deprecated_since_version="1.12",
|
| 159 |
+
active_deprecations_target="deprecated-mechanics-joint-axis",
|
| 160 |
+
stacklevel=4
|
| 161 |
+
)
|
| 162 |
+
if parent_interframe is None:
|
| 163 |
+
parent_interframe = parent_axis
|
| 164 |
+
if child_interframe is None:
|
| 165 |
+
child_interframe = child_axis
|
| 166 |
+
|
| 167 |
+
# Set parent and child frame attributes
|
| 168 |
+
if hasattr(self._parent, 'frame'):
|
| 169 |
+
self._parent_frame = self._parent.frame
|
| 170 |
+
else:
|
| 171 |
+
if isinstance(parent_interframe, ReferenceFrame):
|
| 172 |
+
self._parent_frame = parent_interframe
|
| 173 |
+
else:
|
| 174 |
+
self._parent_frame = ReferenceFrame(
|
| 175 |
+
f'{self.name}_{self._parent.name}_frame')
|
| 176 |
+
if hasattr(self._child, 'frame'):
|
| 177 |
+
self._child_frame = self._child.frame
|
| 178 |
+
else:
|
| 179 |
+
if isinstance(child_interframe, ReferenceFrame):
|
| 180 |
+
self._child_frame = child_interframe
|
| 181 |
+
else:
|
| 182 |
+
self._child_frame = ReferenceFrame(
|
| 183 |
+
f'{self.name}_{self._child.name}_frame')
|
| 184 |
+
|
| 185 |
+
self._parent_interframe = self._locate_joint_frame(
|
| 186 |
+
self._parent, parent_interframe, self._parent_frame)
|
| 187 |
+
self._child_interframe = self._locate_joint_frame(
|
| 188 |
+
self._child, child_interframe, self._child_frame)
|
| 189 |
+
self._parent_axis = self._axis(parent_axis, self._parent_frame)
|
| 190 |
+
self._child_axis = self._axis(child_axis, self._child_frame)
|
| 191 |
+
|
| 192 |
+
if parent_joint_pos is not None or child_joint_pos is not None:
|
| 193 |
+
sympy_deprecation_warning(
|
| 194 |
+
"""
|
| 195 |
+
The parent_joint_pos and child_joint_pos arguments for the Joint
|
| 196 |
+
classes are deprecated. Instead use parent_point and child_point.
|
| 197 |
+
""",
|
| 198 |
+
deprecated_since_version="1.12",
|
| 199 |
+
active_deprecations_target="deprecated-mechanics-joint-pos",
|
| 200 |
+
stacklevel=4
|
| 201 |
+
)
|
| 202 |
+
if parent_point is None:
|
| 203 |
+
parent_point = parent_joint_pos
|
| 204 |
+
if child_point is None:
|
| 205 |
+
child_point = child_joint_pos
|
| 206 |
+
self._parent_point = self._locate_joint_pos(
|
| 207 |
+
self._parent, parent_point, self._parent_frame)
|
| 208 |
+
self._child_point = self._locate_joint_pos(
|
| 209 |
+
self._child, child_point, self._child_frame)
|
| 210 |
+
|
| 211 |
+
self._coordinates = self._generate_coordinates(coordinates)
|
| 212 |
+
self._speeds = self._generate_speeds(speeds)
|
| 213 |
+
_validate_coordinates(self.coordinates, self.speeds)
|
| 214 |
+
self._kdes = self._generate_kdes()
|
| 215 |
+
|
| 216 |
+
self._orient_frames()
|
| 217 |
+
self._set_angular_velocity()
|
| 218 |
+
self._set_linear_velocity()
|
| 219 |
+
|
| 220 |
+
def __str__(self):
|
| 221 |
+
return self.name
|
| 222 |
+
|
| 223 |
+
def __repr__(self):
|
| 224 |
+
return self.__str__()
|
| 225 |
+
|
| 226 |
+
@property
|
| 227 |
+
def name(self):
|
| 228 |
+
"""Name of the joint."""
|
| 229 |
+
return self._name
|
| 230 |
+
|
| 231 |
+
@property
|
| 232 |
+
def parent(self):
|
| 233 |
+
"""Parent body of Joint."""
|
| 234 |
+
return self._parent
|
| 235 |
+
|
| 236 |
+
@property
|
| 237 |
+
def child(self):
|
| 238 |
+
"""Child body of Joint."""
|
| 239 |
+
return self._child
|
| 240 |
+
|
| 241 |
+
@property
|
| 242 |
+
def coordinates(self):
|
| 243 |
+
"""Matrix of the joint's generalized coordinates."""
|
| 244 |
+
return self._coordinates
|
| 245 |
+
|
| 246 |
+
@property
|
| 247 |
+
def speeds(self):
|
| 248 |
+
"""Matrix of the joint's generalized speeds."""
|
| 249 |
+
return self._speeds
|
| 250 |
+
|
| 251 |
+
@property
|
| 252 |
+
def kdes(self):
|
| 253 |
+
"""Kinematical differential equations of the joint."""
|
| 254 |
+
return self._kdes
|
| 255 |
+
|
| 256 |
+
@property
|
| 257 |
+
def parent_axis(self):
|
| 258 |
+
"""The axis of parent frame."""
|
| 259 |
+
# Will be removed with `deprecated-mechanics-joint-axis`
|
| 260 |
+
return self._parent_axis
|
| 261 |
+
|
| 262 |
+
@property
|
| 263 |
+
def child_axis(self):
|
| 264 |
+
"""The axis of child frame."""
|
| 265 |
+
# Will be removed with `deprecated-mechanics-joint-axis`
|
| 266 |
+
return self._child_axis
|
| 267 |
+
|
| 268 |
+
@property
|
| 269 |
+
def parent_point(self):
|
| 270 |
+
"""Attachment point where the joint is fixed to the parent body."""
|
| 271 |
+
return self._parent_point
|
| 272 |
+
|
| 273 |
+
@property
|
| 274 |
+
def child_point(self):
|
| 275 |
+
"""Attachment point where the joint is fixed to the child body."""
|
| 276 |
+
return self._child_point
|
| 277 |
+
|
| 278 |
+
@property
|
| 279 |
+
def parent_interframe(self):
|
| 280 |
+
return self._parent_interframe
|
| 281 |
+
|
| 282 |
+
@property
|
| 283 |
+
def child_interframe(self):
|
| 284 |
+
return self._child_interframe
|
| 285 |
+
|
| 286 |
+
@abstractmethod
|
| 287 |
+
def _generate_coordinates(self, coordinates):
|
| 288 |
+
"""Generate Matrix of the joint's generalized coordinates."""
|
| 289 |
+
pass
|
| 290 |
+
|
| 291 |
+
@abstractmethod
|
| 292 |
+
def _generate_speeds(self, speeds):
|
| 293 |
+
"""Generate Matrix of the joint's generalized speeds."""
|
| 294 |
+
pass
|
| 295 |
+
|
| 296 |
+
@abstractmethod
|
| 297 |
+
def _orient_frames(self):
|
| 298 |
+
"""Orient frames as per the joint."""
|
| 299 |
+
pass
|
| 300 |
+
|
| 301 |
+
@abstractmethod
|
| 302 |
+
def _set_angular_velocity(self):
|
| 303 |
+
"""Set angular velocity of the joint related frames."""
|
| 304 |
+
pass
|
| 305 |
+
|
| 306 |
+
@abstractmethod
|
| 307 |
+
def _set_linear_velocity(self):
|
| 308 |
+
"""Set velocity of related points to the joint."""
|
| 309 |
+
pass
|
| 310 |
+
|
| 311 |
+
@staticmethod
|
| 312 |
+
def _to_vector(matrix, frame):
|
| 313 |
+
"""Converts a matrix to a vector in the given frame."""
|
| 314 |
+
return Vector([(matrix, frame)])
|
| 315 |
+
|
| 316 |
+
@staticmethod
|
| 317 |
+
def _axis(ax, *frames):
|
| 318 |
+
"""Check whether an axis is fixed in one of the frames."""
|
| 319 |
+
if ax is None:
|
| 320 |
+
ax = frames[0].x
|
| 321 |
+
return ax
|
| 322 |
+
if not isinstance(ax, Vector):
|
| 323 |
+
raise TypeError("Axis must be a Vector.")
|
| 324 |
+
ref_frame = None # Find a body in which the axis can be expressed
|
| 325 |
+
for frame in frames:
|
| 326 |
+
try:
|
| 327 |
+
ax.to_matrix(frame)
|
| 328 |
+
except ValueError:
|
| 329 |
+
pass
|
| 330 |
+
else:
|
| 331 |
+
ref_frame = frame
|
| 332 |
+
break
|
| 333 |
+
if ref_frame is None:
|
| 334 |
+
raise ValueError("Axis cannot be expressed in one of the body's "
|
| 335 |
+
"frames.")
|
| 336 |
+
if not ax.dt(ref_frame) == 0:
|
| 337 |
+
raise ValueError('Axis cannot be time-varying when viewed from the '
|
| 338 |
+
'associated body.')
|
| 339 |
+
return ax
|
| 340 |
+
|
| 341 |
+
@staticmethod
|
| 342 |
+
def _choose_rotation_axis(frame, axis):
|
| 343 |
+
components = axis.to_matrix(frame)
|
| 344 |
+
x, y, z = components[0], components[1], components[2]
|
| 345 |
+
|
| 346 |
+
if x != 0:
|
| 347 |
+
if y != 0:
|
| 348 |
+
if z != 0:
|
| 349 |
+
return cross(axis, frame.x)
|
| 350 |
+
if z != 0:
|
| 351 |
+
return frame.y
|
| 352 |
+
return frame.z
|
| 353 |
+
else:
|
| 354 |
+
if y != 0:
|
| 355 |
+
return frame.x
|
| 356 |
+
return frame.y
|
| 357 |
+
|
| 358 |
+
@staticmethod
|
| 359 |
+
def _create_aligned_interframe(frame, align_axis, frame_axis=None,
|
| 360 |
+
frame_name=None):
|
| 361 |
+
"""
|
| 362 |
+
Returns an intermediate frame, where the ``frame_axis`` defined in
|
| 363 |
+
``frame`` is aligned with ``axis``. By default this means that the X
|
| 364 |
+
axis will be aligned with ``axis``.
|
| 365 |
+
|
| 366 |
+
Parameters
|
| 367 |
+
==========
|
| 368 |
+
|
| 369 |
+
frame : BodyBase or ReferenceFrame
|
| 370 |
+
The body or reference frame with respect to which the intermediate
|
| 371 |
+
frame is oriented.
|
| 372 |
+
align_axis : Vector
|
| 373 |
+
The vector with respect to which the intermediate frame will be
|
| 374 |
+
aligned.
|
| 375 |
+
frame_axis : Vector
|
| 376 |
+
The vector of the frame which should get aligned with ``axis``. The
|
| 377 |
+
default is the X axis of the frame.
|
| 378 |
+
frame_name : string
|
| 379 |
+
Name of the to be created intermediate frame. The default adds
|
| 380 |
+
"_int_frame" to the name of ``frame``.
|
| 381 |
+
|
| 382 |
+
Example
|
| 383 |
+
=======
|
| 384 |
+
|
| 385 |
+
An intermediate frame, where the X axis of the parent becomes aligned
|
| 386 |
+
with ``parent.y + parent.z`` can be created as follows:
|
| 387 |
+
|
| 388 |
+
>>> from sympy.physics.mechanics.joint import Joint
|
| 389 |
+
>>> from sympy.physics.mechanics import RigidBody
|
| 390 |
+
>>> parent = RigidBody('parent')
|
| 391 |
+
>>> parent_interframe = Joint._create_aligned_interframe(
|
| 392 |
+
... parent, parent.y + parent.z)
|
| 393 |
+
>>> parent_interframe
|
| 394 |
+
parent_int_frame
|
| 395 |
+
>>> parent.frame.dcm(parent_interframe)
|
| 396 |
+
Matrix([
|
| 397 |
+
[ 0, -sqrt(2)/2, -sqrt(2)/2],
|
| 398 |
+
[sqrt(2)/2, 1/2, -1/2],
|
| 399 |
+
[sqrt(2)/2, -1/2, 1/2]])
|
| 400 |
+
>>> (parent.y + parent.z).express(parent_interframe)
|
| 401 |
+
sqrt(2)*parent_int_frame.x
|
| 402 |
+
|
| 403 |
+
Notes
|
| 404 |
+
=====
|
| 405 |
+
|
| 406 |
+
The direction cosine matrix between the given frame and intermediate
|
| 407 |
+
frame is formed using a simple rotation about an axis that is normal to
|
| 408 |
+
both ``align_axis`` and ``frame_axis``. In general, the normal axis is
|
| 409 |
+
formed by crossing the ``frame_axis`` with the ``align_axis``. The
|
| 410 |
+
exception is if the axes are parallel with opposite directions, in which
|
| 411 |
+
case the rotation vector is chosen using the rules in the following
|
| 412 |
+
table with the vectors expressed in the given frame:
|
| 413 |
+
|
| 414 |
+
.. list-table::
|
| 415 |
+
:header-rows: 1
|
| 416 |
+
|
| 417 |
+
* - ``align_axis``
|
| 418 |
+
- ``frame_axis``
|
| 419 |
+
- ``rotation_axis``
|
| 420 |
+
* - ``-x``
|
| 421 |
+
- ``x``
|
| 422 |
+
- ``z``
|
| 423 |
+
* - ``-y``
|
| 424 |
+
- ``y``
|
| 425 |
+
- ``x``
|
| 426 |
+
* - ``-z``
|
| 427 |
+
- ``z``
|
| 428 |
+
- ``y``
|
| 429 |
+
* - ``-x-y``
|
| 430 |
+
- ``x+y``
|
| 431 |
+
- ``z``
|
| 432 |
+
* - ``-y-z``
|
| 433 |
+
- ``y+z``
|
| 434 |
+
- ``x``
|
| 435 |
+
* - ``-x-z``
|
| 436 |
+
- ``x+z``
|
| 437 |
+
- ``y``
|
| 438 |
+
* - ``-x-y-z``
|
| 439 |
+
- ``x+y+z``
|
| 440 |
+
- ``(x+y+z) × x``
|
| 441 |
+
|
| 442 |
+
"""
|
| 443 |
+
if isinstance(frame, BodyBase):
|
| 444 |
+
frame = frame.frame
|
| 445 |
+
if frame_axis is None:
|
| 446 |
+
frame_axis = frame.x
|
| 447 |
+
if frame_name is None:
|
| 448 |
+
if frame.name[-6:] == '_frame':
|
| 449 |
+
frame_name = f'{frame.name[:-6]}_int_frame'
|
| 450 |
+
else:
|
| 451 |
+
frame_name = f'{frame.name}_int_frame'
|
| 452 |
+
angle = frame_axis.angle_between(align_axis)
|
| 453 |
+
rotation_axis = cross(frame_axis, align_axis)
|
| 454 |
+
if rotation_axis == Vector(0) and angle == 0:
|
| 455 |
+
return frame
|
| 456 |
+
if angle == pi:
|
| 457 |
+
rotation_axis = Joint._choose_rotation_axis(frame, align_axis)
|
| 458 |
+
|
| 459 |
+
int_frame = ReferenceFrame(frame_name)
|
| 460 |
+
int_frame.orient_axis(frame, rotation_axis, angle)
|
| 461 |
+
int_frame.set_ang_vel(frame, 0 * rotation_axis)
|
| 462 |
+
return int_frame
|
| 463 |
+
|
| 464 |
+
def _generate_kdes(self):
|
| 465 |
+
"""Generate kinematical differential equations."""
|
| 466 |
+
kdes = []
|
| 467 |
+
t = dynamicsymbols._t
|
| 468 |
+
for i in range(len(self.coordinates)):
|
| 469 |
+
kdes.append(-self.coordinates[i].diff(t) + self.speeds[i])
|
| 470 |
+
return Matrix(kdes)
|
| 471 |
+
|
| 472 |
+
def _locate_joint_pos(self, body, joint_pos, body_frame=None):
|
| 473 |
+
"""Returns the attachment point of a body."""
|
| 474 |
+
if body_frame is None:
|
| 475 |
+
body_frame = body.frame
|
| 476 |
+
if joint_pos is None:
|
| 477 |
+
return body.masscenter
|
| 478 |
+
if not isinstance(joint_pos, (Point, Vector)):
|
| 479 |
+
raise TypeError('Attachment point must be a Point or Vector.')
|
| 480 |
+
if isinstance(joint_pos, Vector):
|
| 481 |
+
point_name = f'{self.name}_{body.name}_joint'
|
| 482 |
+
joint_pos = body.masscenter.locatenew(point_name, joint_pos)
|
| 483 |
+
if not joint_pos.pos_from(body.masscenter).dt(body_frame) == 0:
|
| 484 |
+
raise ValueError('Attachment point must be fixed to the associated '
|
| 485 |
+
'body.')
|
| 486 |
+
return joint_pos
|
| 487 |
+
|
| 488 |
+
def _locate_joint_frame(self, body, interframe, body_frame=None):
|
| 489 |
+
"""Returns the attachment frame of a body."""
|
| 490 |
+
if body_frame is None:
|
| 491 |
+
body_frame = body.frame
|
| 492 |
+
if interframe is None:
|
| 493 |
+
return body_frame
|
| 494 |
+
if isinstance(interframe, Vector):
|
| 495 |
+
interframe = Joint._create_aligned_interframe(
|
| 496 |
+
body_frame, interframe,
|
| 497 |
+
frame_name=f'{self.name}_{body.name}_int_frame')
|
| 498 |
+
elif not isinstance(interframe, ReferenceFrame):
|
| 499 |
+
raise TypeError('Interframe must be a ReferenceFrame.')
|
| 500 |
+
if not interframe.ang_vel_in(body_frame) == 0:
|
| 501 |
+
raise ValueError(f'Interframe {interframe} is not fixed to body '
|
| 502 |
+
f'{body}.')
|
| 503 |
+
body.masscenter.set_vel(interframe, 0) # Fixate interframe to body
|
| 504 |
+
return interframe
|
| 505 |
+
|
| 506 |
+
def _fill_coordinate_list(self, coordinates, n_coords, label='q', offset=0,
|
| 507 |
+
number_single=False):
|
| 508 |
+
"""Helper method for _generate_coordinates and _generate_speeds.
|
| 509 |
+
|
| 510 |
+
Parameters
|
| 511 |
+
==========
|
| 512 |
+
|
| 513 |
+
coordinates : iterable
|
| 514 |
+
Iterable of coordinates or speeds that have been provided.
|
| 515 |
+
n_coords : Integer
|
| 516 |
+
Number of coordinates that should be returned.
|
| 517 |
+
label : String, optional
|
| 518 |
+
Coordinate type either 'q' (coordinates) or 'u' (speeds). The
|
| 519 |
+
Default is 'q'.
|
| 520 |
+
offset : Integer
|
| 521 |
+
Count offset when creating new dynamicsymbols. The default is 0.
|
| 522 |
+
number_single : Boolean
|
| 523 |
+
Boolean whether if n_coords == 1, number should still be used. The
|
| 524 |
+
default is False.
|
| 525 |
+
|
| 526 |
+
"""
|
| 527 |
+
|
| 528 |
+
def create_symbol(number):
|
| 529 |
+
if n_coords == 1 and not number_single:
|
| 530 |
+
return dynamicsymbols(f'{label}_{self.name}')
|
| 531 |
+
return dynamicsymbols(f'{label}{number}_{self.name}')
|
| 532 |
+
|
| 533 |
+
name = 'generalized coordinate' if label == 'q' else 'generalized speed'
|
| 534 |
+
generated_coordinates = []
|
| 535 |
+
if coordinates is None:
|
| 536 |
+
coordinates = []
|
| 537 |
+
elif not iterable(coordinates):
|
| 538 |
+
coordinates = [coordinates]
|
| 539 |
+
if not (len(coordinates) == 0 or len(coordinates) == n_coords):
|
| 540 |
+
raise ValueError(f'Expected {n_coords} {name}s, instead got '
|
| 541 |
+
f'{len(coordinates)} {name}s.')
|
| 542 |
+
# Supports more iterables, also Matrix
|
| 543 |
+
for i, coord in enumerate(coordinates):
|
| 544 |
+
if coord is None:
|
| 545 |
+
generated_coordinates.append(create_symbol(i + offset))
|
| 546 |
+
elif isinstance(coord, (AppliedUndef, Derivative)):
|
| 547 |
+
generated_coordinates.append(coord)
|
| 548 |
+
else:
|
| 549 |
+
raise TypeError(f'The {name} {coord} should have been a '
|
| 550 |
+
f'dynamicsymbol.')
|
| 551 |
+
for i in range(len(coordinates) + offset, n_coords + offset):
|
| 552 |
+
generated_coordinates.append(create_symbol(i))
|
| 553 |
+
return Matrix(generated_coordinates)
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
class PinJoint(Joint):
|
| 557 |
+
"""Pin (Revolute) Joint.
|
| 558 |
+
|
| 559 |
+
.. raw:: html
|
| 560 |
+
:file: ../../../doc/src/modules/physics/mechanics/api/PinJoint.svg
|
| 561 |
+
|
| 562 |
+
Explanation
|
| 563 |
+
===========
|
| 564 |
+
|
| 565 |
+
A pin joint is defined such that the joint rotation axis is fixed in both
|
| 566 |
+
the child and parent and the location of the joint is relative to the mass
|
| 567 |
+
center of each body. The child rotates an angle, θ, from the parent about
|
| 568 |
+
the rotation axis and has a simple angular speed, ω, relative to the
|
| 569 |
+
parent. The direction cosine matrix between the child interframe and
|
| 570 |
+
parent interframe is formed using a simple rotation about the joint axis.
|
| 571 |
+
The page on the joints framework gives a more detailed explanation of the
|
| 572 |
+
intermediate frames.
|
| 573 |
+
|
| 574 |
+
Parameters
|
| 575 |
+
==========
|
| 576 |
+
|
| 577 |
+
name : string
|
| 578 |
+
A unique name for the joint.
|
| 579 |
+
parent : Particle or RigidBody or Body
|
| 580 |
+
The parent body of joint.
|
| 581 |
+
child : Particle or RigidBody or Body
|
| 582 |
+
The child body of joint.
|
| 583 |
+
coordinates : dynamicsymbol, optional
|
| 584 |
+
Generalized coordinates of the joint.
|
| 585 |
+
speeds : dynamicsymbol, optional
|
| 586 |
+
Generalized speeds of joint.
|
| 587 |
+
parent_point : Point or Vector, optional
|
| 588 |
+
Attachment point where the joint is fixed to the parent body. If a
|
| 589 |
+
vector is provided, then the attachment point is computed by adding the
|
| 590 |
+
vector to the body's mass center. The default value is the parent's mass
|
| 591 |
+
center.
|
| 592 |
+
child_point : Point or Vector, optional
|
| 593 |
+
Attachment point where the joint is fixed to the child body. If a
|
| 594 |
+
vector is provided, then the attachment point is computed by adding the
|
| 595 |
+
vector to the body's mass center. The default value is the child's mass
|
| 596 |
+
center.
|
| 597 |
+
parent_axis : Vector, optional
|
| 598 |
+
.. deprecated:: 1.12
|
| 599 |
+
Axis fixed in the parent body which aligns with an axis fixed in the
|
| 600 |
+
child body. The default is the x axis of parent's reference frame.
|
| 601 |
+
For more information on this deprecation, see
|
| 602 |
+
:ref:`deprecated-mechanics-joint-axis`.
|
| 603 |
+
child_axis : Vector, optional
|
| 604 |
+
.. deprecated:: 1.12
|
| 605 |
+
Axis fixed in the child body which aligns with an axis fixed in the
|
| 606 |
+
parent body. The default is the x axis of child's reference frame.
|
| 607 |
+
For more information on this deprecation, see
|
| 608 |
+
:ref:`deprecated-mechanics-joint-axis`.
|
| 609 |
+
parent_interframe : ReferenceFrame, optional
|
| 610 |
+
Intermediate frame of the parent body with respect to which the joint
|
| 611 |
+
transformation is formulated. If a Vector is provided then an interframe
|
| 612 |
+
is created which aligns its X axis with the given vector. The default
|
| 613 |
+
value is the parent's own frame.
|
| 614 |
+
child_interframe : ReferenceFrame, optional
|
| 615 |
+
Intermediate frame of the child body with respect to which the joint
|
| 616 |
+
transformation is formulated. If a Vector is provided then an interframe
|
| 617 |
+
is created which aligns its X axis with the given vector. The default
|
| 618 |
+
value is the child's own frame.
|
| 619 |
+
joint_axis : Vector
|
| 620 |
+
The axis about which the rotation occurs. Note that the components
|
| 621 |
+
of this axis are the same in the parent_interframe and child_interframe.
|
| 622 |
+
parent_joint_pos : Point or Vector, optional
|
| 623 |
+
.. deprecated:: 1.12
|
| 624 |
+
This argument is replaced by parent_point and will be removed in a
|
| 625 |
+
future version.
|
| 626 |
+
See :ref:`deprecated-mechanics-joint-pos` for more information.
|
| 627 |
+
child_joint_pos : Point or Vector, optional
|
| 628 |
+
.. deprecated:: 1.12
|
| 629 |
+
This argument is replaced by child_point and will be removed in a
|
| 630 |
+
future version.
|
| 631 |
+
See :ref:`deprecated-mechanics-joint-pos` for more information.
|
| 632 |
+
|
| 633 |
+
Attributes
|
| 634 |
+
==========
|
| 635 |
+
|
| 636 |
+
name : string
|
| 637 |
+
The joint's name.
|
| 638 |
+
parent : Particle or RigidBody or Body
|
| 639 |
+
The joint's parent body.
|
| 640 |
+
child : Particle or RigidBody or Body
|
| 641 |
+
The joint's child body.
|
| 642 |
+
coordinates : Matrix
|
| 643 |
+
Matrix of the joint's generalized coordinates. The default value is
|
| 644 |
+
``dynamicsymbols(f'q_{joint.name}')``.
|
| 645 |
+
speeds : Matrix
|
| 646 |
+
Matrix of the joint's generalized speeds. The default value is
|
| 647 |
+
``dynamicsymbols(f'u_{joint.name}')``.
|
| 648 |
+
parent_point : Point
|
| 649 |
+
Attachment point where the joint is fixed to the parent body.
|
| 650 |
+
child_point : Point
|
| 651 |
+
Attachment point where the joint is fixed to the child body.
|
| 652 |
+
parent_axis : Vector
|
| 653 |
+
The axis fixed in the parent frame that represents the joint.
|
| 654 |
+
child_axis : Vector
|
| 655 |
+
The axis fixed in the child frame that represents the joint.
|
| 656 |
+
parent_interframe : ReferenceFrame
|
| 657 |
+
Intermediate frame of the parent body with respect to which the joint
|
| 658 |
+
transformation is formulated.
|
| 659 |
+
child_interframe : ReferenceFrame
|
| 660 |
+
Intermediate frame of the child body with respect to which the joint
|
| 661 |
+
transformation is formulated.
|
| 662 |
+
joint_axis : Vector
|
| 663 |
+
The axis about which the rotation occurs. Note that the components of
|
| 664 |
+
this axis are the same in the parent_interframe and child_interframe.
|
| 665 |
+
kdes : Matrix
|
| 666 |
+
Kinematical differential equations of the joint.
|
| 667 |
+
|
| 668 |
+
Examples
|
| 669 |
+
=========
|
| 670 |
+
|
| 671 |
+
A single pin joint is created from two bodies and has the following basic
|
| 672 |
+
attributes:
|
| 673 |
+
|
| 674 |
+
>>> from sympy.physics.mechanics import RigidBody, PinJoint
|
| 675 |
+
>>> parent = RigidBody('P')
|
| 676 |
+
>>> parent
|
| 677 |
+
P
|
| 678 |
+
>>> child = RigidBody('C')
|
| 679 |
+
>>> child
|
| 680 |
+
C
|
| 681 |
+
>>> joint = PinJoint('PC', parent, child)
|
| 682 |
+
>>> joint
|
| 683 |
+
PinJoint: PC parent: P child: C
|
| 684 |
+
>>> joint.name
|
| 685 |
+
'PC'
|
| 686 |
+
>>> joint.parent
|
| 687 |
+
P
|
| 688 |
+
>>> joint.child
|
| 689 |
+
C
|
| 690 |
+
>>> joint.parent_point
|
| 691 |
+
P_masscenter
|
| 692 |
+
>>> joint.child_point
|
| 693 |
+
C_masscenter
|
| 694 |
+
>>> joint.parent_axis
|
| 695 |
+
P_frame.x
|
| 696 |
+
>>> joint.child_axis
|
| 697 |
+
C_frame.x
|
| 698 |
+
>>> joint.coordinates
|
| 699 |
+
Matrix([[q_PC(t)]])
|
| 700 |
+
>>> joint.speeds
|
| 701 |
+
Matrix([[u_PC(t)]])
|
| 702 |
+
>>> child.frame.ang_vel_in(parent.frame)
|
| 703 |
+
u_PC(t)*P_frame.x
|
| 704 |
+
>>> child.frame.dcm(parent.frame)
|
| 705 |
+
Matrix([
|
| 706 |
+
[1, 0, 0],
|
| 707 |
+
[0, cos(q_PC(t)), sin(q_PC(t))],
|
| 708 |
+
[0, -sin(q_PC(t)), cos(q_PC(t))]])
|
| 709 |
+
>>> joint.child_point.pos_from(joint.parent_point)
|
| 710 |
+
0
|
| 711 |
+
|
| 712 |
+
To further demonstrate the use of the pin joint, the kinematics of simple
|
| 713 |
+
double pendulum that rotates about the Z axis of each connected body can be
|
| 714 |
+
created as follows.
|
| 715 |
+
|
| 716 |
+
>>> from sympy import symbols, trigsimp
|
| 717 |
+
>>> from sympy.physics.mechanics import RigidBody, PinJoint
|
| 718 |
+
>>> l1, l2 = symbols('l1 l2')
|
| 719 |
+
|
| 720 |
+
First create bodies to represent the fixed ceiling and one to represent
|
| 721 |
+
each pendulum bob.
|
| 722 |
+
|
| 723 |
+
>>> ceiling = RigidBody('C')
|
| 724 |
+
>>> upper_bob = RigidBody('U')
|
| 725 |
+
>>> lower_bob = RigidBody('L')
|
| 726 |
+
|
| 727 |
+
The first joint will connect the upper bob to the ceiling by a distance of
|
| 728 |
+
``l1`` and the joint axis will be about the Z axis for each body.
|
| 729 |
+
|
| 730 |
+
>>> ceiling_joint = PinJoint('P1', ceiling, upper_bob,
|
| 731 |
+
... child_point=-l1*upper_bob.frame.x,
|
| 732 |
+
... joint_axis=ceiling.frame.z)
|
| 733 |
+
|
| 734 |
+
The second joint will connect the lower bob to the upper bob by a distance
|
| 735 |
+
of ``l2`` and the joint axis will also be about the Z axis for each body.
|
| 736 |
+
|
| 737 |
+
>>> pendulum_joint = PinJoint('P2', upper_bob, lower_bob,
|
| 738 |
+
... child_point=-l2*lower_bob.frame.x,
|
| 739 |
+
... joint_axis=upper_bob.frame.z)
|
| 740 |
+
|
| 741 |
+
Once the joints are established the kinematics of the connected bodies can
|
| 742 |
+
be accessed. First the direction cosine matrices of pendulum link relative
|
| 743 |
+
to the ceiling are found:
|
| 744 |
+
|
| 745 |
+
>>> upper_bob.frame.dcm(ceiling.frame)
|
| 746 |
+
Matrix([
|
| 747 |
+
[ cos(q_P1(t)), sin(q_P1(t)), 0],
|
| 748 |
+
[-sin(q_P1(t)), cos(q_P1(t)), 0],
|
| 749 |
+
[ 0, 0, 1]])
|
| 750 |
+
>>> trigsimp(lower_bob.frame.dcm(ceiling.frame))
|
| 751 |
+
Matrix([
|
| 752 |
+
[ cos(q_P1(t) + q_P2(t)), sin(q_P1(t) + q_P2(t)), 0],
|
| 753 |
+
[-sin(q_P1(t) + q_P2(t)), cos(q_P1(t) + q_P2(t)), 0],
|
| 754 |
+
[ 0, 0, 1]])
|
| 755 |
+
|
| 756 |
+
The position of the lower bob's masscenter is found with:
|
| 757 |
+
|
| 758 |
+
>>> lower_bob.masscenter.pos_from(ceiling.masscenter)
|
| 759 |
+
l1*U_frame.x + l2*L_frame.x
|
| 760 |
+
|
| 761 |
+
The angular velocities of the two pendulum links can be computed with
|
| 762 |
+
respect to the ceiling.
|
| 763 |
+
|
| 764 |
+
>>> upper_bob.frame.ang_vel_in(ceiling.frame)
|
| 765 |
+
u_P1(t)*C_frame.z
|
| 766 |
+
>>> lower_bob.frame.ang_vel_in(ceiling.frame)
|
| 767 |
+
u_P1(t)*C_frame.z + u_P2(t)*U_frame.z
|
| 768 |
+
|
| 769 |
+
And finally, the linear velocities of the two pendulum bobs can be computed
|
| 770 |
+
with respect to the ceiling.
|
| 771 |
+
|
| 772 |
+
>>> upper_bob.masscenter.vel(ceiling.frame)
|
| 773 |
+
l1*u_P1(t)*U_frame.y
|
| 774 |
+
>>> lower_bob.masscenter.vel(ceiling.frame)
|
| 775 |
+
l1*u_P1(t)*U_frame.y + l2*(u_P1(t) + u_P2(t))*L_frame.y
|
| 776 |
+
|
| 777 |
+
"""
|
| 778 |
+
|
| 779 |
+
def __init__(self, name, parent, child, coordinates=None, speeds=None,
|
| 780 |
+
parent_point=None, child_point=None, parent_interframe=None,
|
| 781 |
+
child_interframe=None, parent_axis=None, child_axis=None,
|
| 782 |
+
joint_axis=None, parent_joint_pos=None, child_joint_pos=None):
|
| 783 |
+
|
| 784 |
+
self._joint_axis = joint_axis
|
| 785 |
+
super().__init__(name, parent, child, coordinates, speeds, parent_point,
|
| 786 |
+
child_point, parent_interframe, child_interframe,
|
| 787 |
+
parent_axis, child_axis, parent_joint_pos,
|
| 788 |
+
child_joint_pos)
|
| 789 |
+
|
| 790 |
+
def __str__(self):
|
| 791 |
+
return (f'PinJoint: {self.name} parent: {self.parent} '
|
| 792 |
+
f'child: {self.child}')
|
| 793 |
+
|
| 794 |
+
@property
|
| 795 |
+
def joint_axis(self):
|
| 796 |
+
"""Axis about which the child rotates with respect to the parent."""
|
| 797 |
+
return self._joint_axis
|
| 798 |
+
|
| 799 |
+
def _generate_coordinates(self, coordinate):
|
| 800 |
+
return self._fill_coordinate_list(coordinate, 1, 'q')
|
| 801 |
+
|
| 802 |
+
def _generate_speeds(self, speed):
|
| 803 |
+
return self._fill_coordinate_list(speed, 1, 'u')
|
| 804 |
+
|
| 805 |
+
def _orient_frames(self):
|
| 806 |
+
self._joint_axis = self._axis(self.joint_axis, self.parent_interframe)
|
| 807 |
+
self.child_interframe.orient_axis(
|
| 808 |
+
self.parent_interframe, self.joint_axis, self.coordinates[0])
|
| 809 |
+
|
| 810 |
+
def _set_angular_velocity(self):
|
| 811 |
+
self.child_interframe.set_ang_vel(self.parent_interframe, self.speeds[
|
| 812 |
+
0] * self.joint_axis.normalize())
|
| 813 |
+
|
| 814 |
+
def _set_linear_velocity(self):
|
| 815 |
+
self.child_point.set_pos(self.parent_point, 0)
|
| 816 |
+
self.parent_point.set_vel(self._parent_frame, 0)
|
| 817 |
+
self.child_point.set_vel(self._child_frame, 0)
|
| 818 |
+
self.child.masscenter.v2pt_theory(self.parent_point,
|
| 819 |
+
self._parent_frame, self._child_frame)
|
| 820 |
+
|
| 821 |
+
|
| 822 |
+
class PrismaticJoint(Joint):
|
| 823 |
+
"""Prismatic (Sliding) Joint.
|
| 824 |
+
|
| 825 |
+
.. image:: PrismaticJoint.svg
|
| 826 |
+
|
| 827 |
+
Explanation
|
| 828 |
+
===========
|
| 829 |
+
|
| 830 |
+
It is defined such that the child body translates with respect to the parent
|
| 831 |
+
body along the body-fixed joint axis. The location of the joint is defined
|
| 832 |
+
by two points, one in each body, which coincide when the generalized
|
| 833 |
+
coordinate is zero. The direction cosine matrix between the
|
| 834 |
+
parent_interframe and child_interframe is the identity matrix. Therefore,
|
| 835 |
+
the direction cosine matrix between the parent and child frames is fully
|
| 836 |
+
defined by the definition of the intermediate frames. The page on the joints
|
| 837 |
+
framework gives a more detailed explanation of the intermediate frames.
|
| 838 |
+
|
| 839 |
+
Parameters
|
| 840 |
+
==========
|
| 841 |
+
|
| 842 |
+
name : string
|
| 843 |
+
A unique name for the joint.
|
| 844 |
+
parent : Particle or RigidBody or Body
|
| 845 |
+
The parent body of joint.
|
| 846 |
+
child : Particle or RigidBody or Body
|
| 847 |
+
The child body of joint.
|
| 848 |
+
coordinates : dynamicsymbol, optional
|
| 849 |
+
Generalized coordinates of the joint. The default value is
|
| 850 |
+
``dynamicsymbols(f'q_{joint.name}')``.
|
| 851 |
+
speeds : dynamicsymbol, optional
|
| 852 |
+
Generalized speeds of joint. The default value is
|
| 853 |
+
``dynamicsymbols(f'u_{joint.name}')``.
|
| 854 |
+
parent_point : Point or Vector, optional
|
| 855 |
+
Attachment point where the joint is fixed to the parent body. If a
|
| 856 |
+
vector is provided, then the attachment point is computed by adding the
|
| 857 |
+
vector to the body's mass center. The default value is the parent's mass
|
| 858 |
+
center.
|
| 859 |
+
child_point : Point or Vector, optional
|
| 860 |
+
Attachment point where the joint is fixed to the child body. If a
|
| 861 |
+
vector is provided, then the attachment point is computed by adding the
|
| 862 |
+
vector to the body's mass center. The default value is the child's mass
|
| 863 |
+
center.
|
| 864 |
+
parent_axis : Vector, optional
|
| 865 |
+
.. deprecated:: 1.12
|
| 866 |
+
Axis fixed in the parent body which aligns with an axis fixed in the
|
| 867 |
+
child body. The default is the x axis of parent's reference frame.
|
| 868 |
+
For more information on this deprecation, see
|
| 869 |
+
:ref:`deprecated-mechanics-joint-axis`.
|
| 870 |
+
child_axis : Vector, optional
|
| 871 |
+
.. deprecated:: 1.12
|
| 872 |
+
Axis fixed in the child body which aligns with an axis fixed in the
|
| 873 |
+
parent body. The default is the x axis of child's reference frame.
|
| 874 |
+
For more information on this deprecation, see
|
| 875 |
+
:ref:`deprecated-mechanics-joint-axis`.
|
| 876 |
+
parent_interframe : ReferenceFrame, optional
|
| 877 |
+
Intermediate frame of the parent body with respect to which the joint
|
| 878 |
+
transformation is formulated. If a Vector is provided then an interframe
|
| 879 |
+
is created which aligns its X axis with the given vector. The default
|
| 880 |
+
value is the parent's own frame.
|
| 881 |
+
child_interframe : ReferenceFrame, optional
|
| 882 |
+
Intermediate frame of the child body with respect to which the joint
|
| 883 |
+
transformation is formulated. If a Vector is provided then an interframe
|
| 884 |
+
is created which aligns its X axis with the given vector. The default
|
| 885 |
+
value is the child's own frame.
|
| 886 |
+
joint_axis : Vector
|
| 887 |
+
The axis along which the translation occurs. Note that the components
|
| 888 |
+
of this axis are the same in the parent_interframe and child_interframe.
|
| 889 |
+
parent_joint_pos : Point or Vector, optional
|
| 890 |
+
.. deprecated:: 1.12
|
| 891 |
+
This argument is replaced by parent_point and will be removed in a
|
| 892 |
+
future version.
|
| 893 |
+
See :ref:`deprecated-mechanics-joint-pos` for more information.
|
| 894 |
+
child_joint_pos : Point or Vector, optional
|
| 895 |
+
.. deprecated:: 1.12
|
| 896 |
+
This argument is replaced by child_point and will be removed in a
|
| 897 |
+
future version.
|
| 898 |
+
See :ref:`deprecated-mechanics-joint-pos` for more information.
|
| 899 |
+
|
| 900 |
+
Attributes
|
| 901 |
+
==========
|
| 902 |
+
|
| 903 |
+
name : string
|
| 904 |
+
The joint's name.
|
| 905 |
+
parent : Particle or RigidBody or Body
|
| 906 |
+
The joint's parent body.
|
| 907 |
+
child : Particle or RigidBody or Body
|
| 908 |
+
The joint's child body.
|
| 909 |
+
coordinates : Matrix
|
| 910 |
+
Matrix of the joint's generalized coordinates.
|
| 911 |
+
speeds : Matrix
|
| 912 |
+
Matrix of the joint's generalized speeds.
|
| 913 |
+
parent_point : Point
|
| 914 |
+
Attachment point where the joint is fixed to the parent body.
|
| 915 |
+
child_point : Point
|
| 916 |
+
Attachment point where the joint is fixed to the child body.
|
| 917 |
+
parent_axis : Vector
|
| 918 |
+
The axis fixed in the parent frame that represents the joint.
|
| 919 |
+
child_axis : Vector
|
| 920 |
+
The axis fixed in the child frame that represents the joint.
|
| 921 |
+
parent_interframe : ReferenceFrame
|
| 922 |
+
Intermediate frame of the parent body with respect to which the joint
|
| 923 |
+
transformation is formulated.
|
| 924 |
+
child_interframe : ReferenceFrame
|
| 925 |
+
Intermediate frame of the child body with respect to which the joint
|
| 926 |
+
transformation is formulated.
|
| 927 |
+
kdes : Matrix
|
| 928 |
+
Kinematical differential equations of the joint.
|
| 929 |
+
|
| 930 |
+
Examples
|
| 931 |
+
=========
|
| 932 |
+
|
| 933 |
+
A single prismatic joint is created from two bodies and has the following
|
| 934 |
+
basic attributes:
|
| 935 |
+
|
| 936 |
+
>>> from sympy.physics.mechanics import RigidBody, PrismaticJoint
|
| 937 |
+
>>> parent = RigidBody('P')
|
| 938 |
+
>>> parent
|
| 939 |
+
P
|
| 940 |
+
>>> child = RigidBody('C')
|
| 941 |
+
>>> child
|
| 942 |
+
C
|
| 943 |
+
>>> joint = PrismaticJoint('PC', parent, child)
|
| 944 |
+
>>> joint
|
| 945 |
+
PrismaticJoint: PC parent: P child: C
|
| 946 |
+
>>> joint.name
|
| 947 |
+
'PC'
|
| 948 |
+
>>> joint.parent
|
| 949 |
+
P
|
| 950 |
+
>>> joint.child
|
| 951 |
+
C
|
| 952 |
+
>>> joint.parent_point
|
| 953 |
+
P_masscenter
|
| 954 |
+
>>> joint.child_point
|
| 955 |
+
C_masscenter
|
| 956 |
+
>>> joint.parent_axis
|
| 957 |
+
P_frame.x
|
| 958 |
+
>>> joint.child_axis
|
| 959 |
+
C_frame.x
|
| 960 |
+
>>> joint.coordinates
|
| 961 |
+
Matrix([[q_PC(t)]])
|
| 962 |
+
>>> joint.speeds
|
| 963 |
+
Matrix([[u_PC(t)]])
|
| 964 |
+
>>> child.frame.ang_vel_in(parent.frame)
|
| 965 |
+
0
|
| 966 |
+
>>> child.frame.dcm(parent.frame)
|
| 967 |
+
Matrix([
|
| 968 |
+
[1, 0, 0],
|
| 969 |
+
[0, 1, 0],
|
| 970 |
+
[0, 0, 1]])
|
| 971 |
+
>>> joint.child_point.pos_from(joint.parent_point)
|
| 972 |
+
q_PC(t)*P_frame.x
|
| 973 |
+
|
| 974 |
+
To further demonstrate the use of the prismatic joint, the kinematics of two
|
| 975 |
+
masses sliding, one moving relative to a fixed body and the other relative
|
| 976 |
+
to the moving body. about the X axis of each connected body can be created
|
| 977 |
+
as follows.
|
| 978 |
+
|
| 979 |
+
>>> from sympy.physics.mechanics import PrismaticJoint, RigidBody
|
| 980 |
+
|
| 981 |
+
First create bodies to represent the fixed ceiling and one to represent
|
| 982 |
+
a particle.
|
| 983 |
+
|
| 984 |
+
>>> wall = RigidBody('W')
|
| 985 |
+
>>> Part1 = RigidBody('P1')
|
| 986 |
+
>>> Part2 = RigidBody('P2')
|
| 987 |
+
|
| 988 |
+
The first joint will connect the particle to the ceiling and the
|
| 989 |
+
joint axis will be about the X axis for each body.
|
| 990 |
+
|
| 991 |
+
>>> J1 = PrismaticJoint('J1', wall, Part1)
|
| 992 |
+
|
| 993 |
+
The second joint will connect the second particle to the first particle
|
| 994 |
+
and the joint axis will also be about the X axis for each body.
|
| 995 |
+
|
| 996 |
+
>>> J2 = PrismaticJoint('J2', Part1, Part2)
|
| 997 |
+
|
| 998 |
+
Once the joint is established the kinematics of the connected bodies can
|
| 999 |
+
be accessed. First the direction cosine matrices of Part relative
|
| 1000 |
+
to the ceiling are found:
|
| 1001 |
+
|
| 1002 |
+
>>> Part1.frame.dcm(wall.frame)
|
| 1003 |
+
Matrix([
|
| 1004 |
+
[1, 0, 0],
|
| 1005 |
+
[0, 1, 0],
|
| 1006 |
+
[0, 0, 1]])
|
| 1007 |
+
|
| 1008 |
+
>>> Part2.frame.dcm(wall.frame)
|
| 1009 |
+
Matrix([
|
| 1010 |
+
[1, 0, 0],
|
| 1011 |
+
[0, 1, 0],
|
| 1012 |
+
[0, 0, 1]])
|
| 1013 |
+
|
| 1014 |
+
The position of the particles' masscenter is found with:
|
| 1015 |
+
|
| 1016 |
+
>>> Part1.masscenter.pos_from(wall.masscenter)
|
| 1017 |
+
q_J1(t)*W_frame.x
|
| 1018 |
+
|
| 1019 |
+
>>> Part2.masscenter.pos_from(wall.masscenter)
|
| 1020 |
+
q_J1(t)*W_frame.x + q_J2(t)*P1_frame.x
|
| 1021 |
+
|
| 1022 |
+
The angular velocities of the two particle links can be computed with
|
| 1023 |
+
respect to the ceiling.
|
| 1024 |
+
|
| 1025 |
+
>>> Part1.frame.ang_vel_in(wall.frame)
|
| 1026 |
+
0
|
| 1027 |
+
|
| 1028 |
+
>>> Part2.frame.ang_vel_in(wall.frame)
|
| 1029 |
+
0
|
| 1030 |
+
|
| 1031 |
+
And finally, the linear velocities of the two particles can be computed
|
| 1032 |
+
with respect to the ceiling.
|
| 1033 |
+
|
| 1034 |
+
>>> Part1.masscenter.vel(wall.frame)
|
| 1035 |
+
u_J1(t)*W_frame.x
|
| 1036 |
+
|
| 1037 |
+
>>> Part2.masscenter.vel(wall.frame)
|
| 1038 |
+
u_J1(t)*W_frame.x + Derivative(q_J2(t), t)*P1_frame.x
|
| 1039 |
+
|
| 1040 |
+
"""
|
| 1041 |
+
|
| 1042 |
+
def __init__(self, name, parent, child, coordinates=None, speeds=None,
|
| 1043 |
+
parent_point=None, child_point=None, parent_interframe=None,
|
| 1044 |
+
child_interframe=None, parent_axis=None, child_axis=None,
|
| 1045 |
+
joint_axis=None, parent_joint_pos=None, child_joint_pos=None):
|
| 1046 |
+
|
| 1047 |
+
self._joint_axis = joint_axis
|
| 1048 |
+
super().__init__(name, parent, child, coordinates, speeds, parent_point,
|
| 1049 |
+
child_point, parent_interframe, child_interframe,
|
| 1050 |
+
parent_axis, child_axis, parent_joint_pos,
|
| 1051 |
+
child_joint_pos)
|
| 1052 |
+
|
| 1053 |
+
def __str__(self):
|
| 1054 |
+
return (f'PrismaticJoint: {self.name} parent: {self.parent} '
|
| 1055 |
+
f'child: {self.child}')
|
| 1056 |
+
|
| 1057 |
+
@property
|
| 1058 |
+
def joint_axis(self):
|
| 1059 |
+
"""Axis along which the child translates with respect to the parent."""
|
| 1060 |
+
return self._joint_axis
|
| 1061 |
+
|
| 1062 |
+
def _generate_coordinates(self, coordinate):
|
| 1063 |
+
return self._fill_coordinate_list(coordinate, 1, 'q')
|
| 1064 |
+
|
| 1065 |
+
def _generate_speeds(self, speed):
|
| 1066 |
+
return self._fill_coordinate_list(speed, 1, 'u')
|
| 1067 |
+
|
| 1068 |
+
def _orient_frames(self):
|
| 1069 |
+
self._joint_axis = self._axis(self.joint_axis, self.parent_interframe)
|
| 1070 |
+
self.child_interframe.orient_axis(
|
| 1071 |
+
self.parent_interframe, self.joint_axis, 0)
|
| 1072 |
+
|
| 1073 |
+
def _set_angular_velocity(self):
|
| 1074 |
+
self.child_interframe.set_ang_vel(self.parent_interframe, 0)
|
| 1075 |
+
|
| 1076 |
+
def _set_linear_velocity(self):
|
| 1077 |
+
axis = self.joint_axis.normalize()
|
| 1078 |
+
self.child_point.set_pos(self.parent_point, self.coordinates[0] * axis)
|
| 1079 |
+
self.parent_point.set_vel(self._parent_frame, 0)
|
| 1080 |
+
self.child_point.set_vel(self._child_frame, 0)
|
| 1081 |
+
self.child_point.set_vel(self._parent_frame, self.speeds[0] * axis)
|
| 1082 |
+
self.child.masscenter.set_vel(self._parent_frame, self.speeds[0] * axis)
|
| 1083 |
+
|
| 1084 |
+
|
| 1085 |
+
class CylindricalJoint(Joint):
|
| 1086 |
+
"""Cylindrical Joint.
|
| 1087 |
+
|
| 1088 |
+
.. image:: CylindricalJoint.svg
|
| 1089 |
+
:align: center
|
| 1090 |
+
:width: 600
|
| 1091 |
+
|
| 1092 |
+
Explanation
|
| 1093 |
+
===========
|
| 1094 |
+
|
| 1095 |
+
A cylindrical joint is defined such that the child body both rotates about
|
| 1096 |
+
and translates along the body-fixed joint axis with respect to the parent
|
| 1097 |
+
body. The joint axis is both the rotation axis and translation axis. The
|
| 1098 |
+
location of the joint is defined by two points, one in each body, which
|
| 1099 |
+
coincide when the generalized coordinate corresponding to the translation is
|
| 1100 |
+
zero. The direction cosine matrix between the child interframe and parent
|
| 1101 |
+
interframe is formed using a simple rotation about the joint axis. The page
|
| 1102 |
+
on the joints framework gives a more detailed explanation of the
|
| 1103 |
+
intermediate frames.
|
| 1104 |
+
|
| 1105 |
+
Parameters
|
| 1106 |
+
==========
|
| 1107 |
+
|
| 1108 |
+
name : string
|
| 1109 |
+
A unique name for the joint.
|
| 1110 |
+
parent : Particle or RigidBody or Body
|
| 1111 |
+
The parent body of joint.
|
| 1112 |
+
child : Particle or RigidBody or Body
|
| 1113 |
+
The child body of joint.
|
| 1114 |
+
rotation_coordinate : dynamicsymbol, optional
|
| 1115 |
+
Generalized coordinate corresponding to the rotation angle. The default
|
| 1116 |
+
value is ``dynamicsymbols(f'q0_{joint.name}')``.
|
| 1117 |
+
translation_coordinate : dynamicsymbol, optional
|
| 1118 |
+
Generalized coordinate corresponding to the translation distance. The
|
| 1119 |
+
default value is ``dynamicsymbols(f'q1_{joint.name}')``.
|
| 1120 |
+
rotation_speed : dynamicsymbol, optional
|
| 1121 |
+
Generalized speed corresponding to the angular velocity. The default
|
| 1122 |
+
value is ``dynamicsymbols(f'u0_{joint.name}')``.
|
| 1123 |
+
translation_speed : dynamicsymbol, optional
|
| 1124 |
+
Generalized speed corresponding to the translation velocity. The default
|
| 1125 |
+
value is ``dynamicsymbols(f'u1_{joint.name}')``.
|
| 1126 |
+
parent_point : Point or Vector, optional
|
| 1127 |
+
Attachment point where the joint is fixed to the parent body. If a
|
| 1128 |
+
vector is provided, then the attachment point is computed by adding the
|
| 1129 |
+
vector to the body's mass center. The default value is the parent's mass
|
| 1130 |
+
center.
|
| 1131 |
+
child_point : Point or Vector, optional
|
| 1132 |
+
Attachment point where the joint is fixed to the child body. If a
|
| 1133 |
+
vector is provided, then the attachment point is computed by adding the
|
| 1134 |
+
vector to the body's mass center. The default value is the child's mass
|
| 1135 |
+
center.
|
| 1136 |
+
parent_interframe : ReferenceFrame, optional
|
| 1137 |
+
Intermediate frame of the parent body with respect to which the joint
|
| 1138 |
+
transformation is formulated. If a Vector is provided then an interframe
|
| 1139 |
+
is created which aligns its X axis with the given vector. The default
|
| 1140 |
+
value is the parent's own frame.
|
| 1141 |
+
child_interframe : ReferenceFrame, optional
|
| 1142 |
+
Intermediate frame of the child body with respect to which the joint
|
| 1143 |
+
transformation is formulated. If a Vector is provided then an interframe
|
| 1144 |
+
is created which aligns its X axis with the given vector. The default
|
| 1145 |
+
value is the child's own frame.
|
| 1146 |
+
joint_axis : Vector, optional
|
| 1147 |
+
The rotation as well as translation axis. Note that the components of
|
| 1148 |
+
this axis are the same in the parent_interframe and child_interframe.
|
| 1149 |
+
|
| 1150 |
+
Attributes
|
| 1151 |
+
==========
|
| 1152 |
+
|
| 1153 |
+
name : string
|
| 1154 |
+
The joint's name.
|
| 1155 |
+
parent : Particle or RigidBody or Body
|
| 1156 |
+
The joint's parent body.
|
| 1157 |
+
child : Particle or RigidBody or Body
|
| 1158 |
+
The joint's child body.
|
| 1159 |
+
rotation_coordinate : dynamicsymbol
|
| 1160 |
+
Generalized coordinate corresponding to the rotation angle.
|
| 1161 |
+
translation_coordinate : dynamicsymbol
|
| 1162 |
+
Generalized coordinate corresponding to the translation distance.
|
| 1163 |
+
rotation_speed : dynamicsymbol
|
| 1164 |
+
Generalized speed corresponding to the angular velocity.
|
| 1165 |
+
translation_speed : dynamicsymbol
|
| 1166 |
+
Generalized speed corresponding to the translation velocity.
|
| 1167 |
+
coordinates : Matrix
|
| 1168 |
+
Matrix of the joint's generalized coordinates.
|
| 1169 |
+
speeds : Matrix
|
| 1170 |
+
Matrix of the joint's generalized speeds.
|
| 1171 |
+
parent_point : Point
|
| 1172 |
+
Attachment point where the joint is fixed to the parent body.
|
| 1173 |
+
child_point : Point
|
| 1174 |
+
Attachment point where the joint is fixed to the child body.
|
| 1175 |
+
parent_interframe : ReferenceFrame
|
| 1176 |
+
Intermediate frame of the parent body with respect to which the joint
|
| 1177 |
+
transformation is formulated.
|
| 1178 |
+
child_interframe : ReferenceFrame
|
| 1179 |
+
Intermediate frame of the child body with respect to which the joint
|
| 1180 |
+
transformation is formulated.
|
| 1181 |
+
kdes : Matrix
|
| 1182 |
+
Kinematical differential equations of the joint.
|
| 1183 |
+
joint_axis : Vector
|
| 1184 |
+
The axis of rotation and translation.
|
| 1185 |
+
|
| 1186 |
+
Examples
|
| 1187 |
+
=========
|
| 1188 |
+
|
| 1189 |
+
A single cylindrical joint is created between two bodies and has the
|
| 1190 |
+
following basic attributes:
|
| 1191 |
+
|
| 1192 |
+
>>> from sympy.physics.mechanics import RigidBody, CylindricalJoint
|
| 1193 |
+
>>> parent = RigidBody('P')
|
| 1194 |
+
>>> parent
|
| 1195 |
+
P
|
| 1196 |
+
>>> child = RigidBody('C')
|
| 1197 |
+
>>> child
|
| 1198 |
+
C
|
| 1199 |
+
>>> joint = CylindricalJoint('PC', parent, child)
|
| 1200 |
+
>>> joint
|
| 1201 |
+
CylindricalJoint: PC parent: P child: C
|
| 1202 |
+
>>> joint.name
|
| 1203 |
+
'PC'
|
| 1204 |
+
>>> joint.parent
|
| 1205 |
+
P
|
| 1206 |
+
>>> joint.child
|
| 1207 |
+
C
|
| 1208 |
+
>>> joint.parent_point
|
| 1209 |
+
P_masscenter
|
| 1210 |
+
>>> joint.child_point
|
| 1211 |
+
C_masscenter
|
| 1212 |
+
>>> joint.parent_axis
|
| 1213 |
+
P_frame.x
|
| 1214 |
+
>>> joint.child_axis
|
| 1215 |
+
C_frame.x
|
| 1216 |
+
>>> joint.coordinates
|
| 1217 |
+
Matrix([
|
| 1218 |
+
[q0_PC(t)],
|
| 1219 |
+
[q1_PC(t)]])
|
| 1220 |
+
>>> joint.speeds
|
| 1221 |
+
Matrix([
|
| 1222 |
+
[u0_PC(t)],
|
| 1223 |
+
[u1_PC(t)]])
|
| 1224 |
+
>>> child.frame.ang_vel_in(parent.frame)
|
| 1225 |
+
u0_PC(t)*P_frame.x
|
| 1226 |
+
>>> child.frame.dcm(parent.frame)
|
| 1227 |
+
Matrix([
|
| 1228 |
+
[1, 0, 0],
|
| 1229 |
+
[0, cos(q0_PC(t)), sin(q0_PC(t))],
|
| 1230 |
+
[0, -sin(q0_PC(t)), cos(q0_PC(t))]])
|
| 1231 |
+
>>> joint.child_point.pos_from(joint.parent_point)
|
| 1232 |
+
q1_PC(t)*P_frame.x
|
| 1233 |
+
>>> child.masscenter.vel(parent.frame)
|
| 1234 |
+
u1_PC(t)*P_frame.x
|
| 1235 |
+
|
| 1236 |
+
To further demonstrate the use of the cylindrical joint, the kinematics of
|
| 1237 |
+
two cylindrical joints perpendicular to each other can be created as follows.
|
| 1238 |
+
|
| 1239 |
+
>>> from sympy import symbols
|
| 1240 |
+
>>> from sympy.physics.mechanics import RigidBody, CylindricalJoint
|
| 1241 |
+
>>> r, l, w = symbols('r l w')
|
| 1242 |
+
|
| 1243 |
+
First create bodies to represent the fixed floor with a fixed pole on it.
|
| 1244 |
+
The second body represents a freely moving tube around that pole. The third
|
| 1245 |
+
body represents a solid flag freely translating along and rotating around
|
| 1246 |
+
the Y axis of the tube.
|
| 1247 |
+
|
| 1248 |
+
>>> floor = RigidBody('floor')
|
| 1249 |
+
>>> tube = RigidBody('tube')
|
| 1250 |
+
>>> flag = RigidBody('flag')
|
| 1251 |
+
|
| 1252 |
+
The first joint will connect the first tube to the floor with it translating
|
| 1253 |
+
along and rotating around the Z axis of both bodies.
|
| 1254 |
+
|
| 1255 |
+
>>> floor_joint = CylindricalJoint('C1', floor, tube, joint_axis=floor.z)
|
| 1256 |
+
|
| 1257 |
+
The second joint will connect the tube perpendicular to the flag along the Y
|
| 1258 |
+
axis of both the tube and the flag, with the joint located at a distance
|
| 1259 |
+
``r`` from the tube's center of mass and a combination of the distances
|
| 1260 |
+
``l`` and ``w`` from the flag's center of mass.
|
| 1261 |
+
|
| 1262 |
+
>>> flag_joint = CylindricalJoint('C2', tube, flag,
|
| 1263 |
+
... parent_point=r * tube.y,
|
| 1264 |
+
... child_point=-w * flag.y + l * flag.z,
|
| 1265 |
+
... joint_axis=tube.y)
|
| 1266 |
+
|
| 1267 |
+
Once the joints are established the kinematics of the connected bodies can
|
| 1268 |
+
be accessed. First the direction cosine matrices of both the body and the
|
| 1269 |
+
flag relative to the floor are found:
|
| 1270 |
+
|
| 1271 |
+
>>> tube.frame.dcm(floor.frame)
|
| 1272 |
+
Matrix([
|
| 1273 |
+
[ cos(q0_C1(t)), sin(q0_C1(t)), 0],
|
| 1274 |
+
[-sin(q0_C1(t)), cos(q0_C1(t)), 0],
|
| 1275 |
+
[ 0, 0, 1]])
|
| 1276 |
+
>>> flag.frame.dcm(floor.frame)
|
| 1277 |
+
Matrix([
|
| 1278 |
+
[cos(q0_C1(t))*cos(q0_C2(t)), sin(q0_C1(t))*cos(q0_C2(t)), -sin(q0_C2(t))],
|
| 1279 |
+
[ -sin(q0_C1(t)), cos(q0_C1(t)), 0],
|
| 1280 |
+
[sin(q0_C2(t))*cos(q0_C1(t)), sin(q0_C1(t))*sin(q0_C2(t)), cos(q0_C2(t))]])
|
| 1281 |
+
|
| 1282 |
+
The position of the flag's center of mass is found with:
|
| 1283 |
+
|
| 1284 |
+
>>> flag.masscenter.pos_from(floor.masscenter)
|
| 1285 |
+
q1_C1(t)*floor_frame.z + (r + q1_C2(t))*tube_frame.y + w*flag_frame.y - l*flag_frame.z
|
| 1286 |
+
|
| 1287 |
+
The angular velocities of the two tubes can be computed with respect to the
|
| 1288 |
+
floor.
|
| 1289 |
+
|
| 1290 |
+
>>> tube.frame.ang_vel_in(floor.frame)
|
| 1291 |
+
u0_C1(t)*floor_frame.z
|
| 1292 |
+
>>> flag.frame.ang_vel_in(floor.frame)
|
| 1293 |
+
u0_C1(t)*floor_frame.z + u0_C2(t)*tube_frame.y
|
| 1294 |
+
|
| 1295 |
+
Finally, the linear velocities of the two tube centers of mass can be
|
| 1296 |
+
computed with respect to the floor, while expressed in the tube's frame.
|
| 1297 |
+
|
| 1298 |
+
>>> tube.masscenter.vel(floor.frame).to_matrix(tube.frame)
|
| 1299 |
+
Matrix([
|
| 1300 |
+
[ 0],
|
| 1301 |
+
[ 0],
|
| 1302 |
+
[u1_C1(t)]])
|
| 1303 |
+
>>> flag.masscenter.vel(floor.frame).to_matrix(tube.frame).simplify()
|
| 1304 |
+
Matrix([
|
| 1305 |
+
[-l*u0_C2(t)*cos(q0_C2(t)) - r*u0_C1(t) - w*u0_C1(t) - q1_C2(t)*u0_C1(t)],
|
| 1306 |
+
[ -l*u0_C1(t)*sin(q0_C2(t)) + Derivative(q1_C2(t), t)],
|
| 1307 |
+
[ l*u0_C2(t)*sin(q0_C2(t)) + u1_C1(t)]])
|
| 1308 |
+
|
| 1309 |
+
"""
|
| 1310 |
+
|
| 1311 |
+
def __init__(self, name, parent, child, rotation_coordinate=None,
|
| 1312 |
+
translation_coordinate=None, rotation_speed=None,
|
| 1313 |
+
translation_speed=None, parent_point=None, child_point=None,
|
| 1314 |
+
parent_interframe=None, child_interframe=None,
|
| 1315 |
+
joint_axis=None):
|
| 1316 |
+
self._joint_axis = joint_axis
|
| 1317 |
+
coordinates = (rotation_coordinate, translation_coordinate)
|
| 1318 |
+
speeds = (rotation_speed, translation_speed)
|
| 1319 |
+
super().__init__(name, parent, child, coordinates, speeds,
|
| 1320 |
+
parent_point, child_point,
|
| 1321 |
+
parent_interframe=parent_interframe,
|
| 1322 |
+
child_interframe=child_interframe)
|
| 1323 |
+
|
| 1324 |
+
def __str__(self):
|
| 1325 |
+
return (f'CylindricalJoint: {self.name} parent: {self.parent} '
|
| 1326 |
+
f'child: {self.child}')
|
| 1327 |
+
|
| 1328 |
+
@property
|
| 1329 |
+
def joint_axis(self):
|
| 1330 |
+
"""Axis about and along which the rotation and translation occurs."""
|
| 1331 |
+
return self._joint_axis
|
| 1332 |
+
|
| 1333 |
+
@property
|
| 1334 |
+
def rotation_coordinate(self):
|
| 1335 |
+
"""Generalized coordinate corresponding to the rotation angle."""
|
| 1336 |
+
return self.coordinates[0]
|
| 1337 |
+
|
| 1338 |
+
@property
|
| 1339 |
+
def translation_coordinate(self):
|
| 1340 |
+
"""Generalized coordinate corresponding to the translation distance."""
|
| 1341 |
+
return self.coordinates[1]
|
| 1342 |
+
|
| 1343 |
+
@property
|
| 1344 |
+
def rotation_speed(self):
|
| 1345 |
+
"""Generalized speed corresponding to the angular velocity."""
|
| 1346 |
+
return self.speeds[0]
|
| 1347 |
+
|
| 1348 |
+
@property
|
| 1349 |
+
def translation_speed(self):
|
| 1350 |
+
"""Generalized speed corresponding to the translation velocity."""
|
| 1351 |
+
return self.speeds[1]
|
| 1352 |
+
|
| 1353 |
+
def _generate_coordinates(self, coordinates):
|
| 1354 |
+
return self._fill_coordinate_list(coordinates, 2, 'q')
|
| 1355 |
+
|
| 1356 |
+
def _generate_speeds(self, speeds):
|
| 1357 |
+
return self._fill_coordinate_list(speeds, 2, 'u')
|
| 1358 |
+
|
| 1359 |
+
def _orient_frames(self):
|
| 1360 |
+
self._joint_axis = self._axis(self.joint_axis, self.parent_interframe)
|
| 1361 |
+
self.child_interframe.orient_axis(
|
| 1362 |
+
self.parent_interframe, self.joint_axis, self.rotation_coordinate)
|
| 1363 |
+
|
| 1364 |
+
def _set_angular_velocity(self):
|
| 1365 |
+
self.child_interframe.set_ang_vel(
|
| 1366 |
+
self.parent_interframe,
|
| 1367 |
+
self.rotation_speed * self.joint_axis.normalize())
|
| 1368 |
+
|
| 1369 |
+
def _set_linear_velocity(self):
|
| 1370 |
+
self.child_point.set_pos(
|
| 1371 |
+
self.parent_point,
|
| 1372 |
+
self.translation_coordinate * self.joint_axis.normalize())
|
| 1373 |
+
self.parent_point.set_vel(self._parent_frame, 0)
|
| 1374 |
+
self.child_point.set_vel(self._child_frame, 0)
|
| 1375 |
+
self.child_point.set_vel(
|
| 1376 |
+
self._parent_frame,
|
| 1377 |
+
self.translation_speed * self.joint_axis.normalize())
|
| 1378 |
+
self.child.masscenter.v2pt_theory(self.child_point, self._parent_frame,
|
| 1379 |
+
self.child_interframe)
|
| 1380 |
+
|
| 1381 |
+
|
| 1382 |
+
class PlanarJoint(Joint):
|
| 1383 |
+
"""Planar Joint.
|
| 1384 |
+
|
| 1385 |
+
.. raw:: html
|
| 1386 |
+
:file: ../../../doc/src/modules/physics/mechanics/api/PlanarJoint.svg
|
| 1387 |
+
|
| 1388 |
+
Explanation
|
| 1389 |
+
===========
|
| 1390 |
+
|
| 1391 |
+
A planar joint is defined such that the child body translates over a fixed
|
| 1392 |
+
plane of the parent body as well as rotate about the rotation axis, which
|
| 1393 |
+
is perpendicular to that plane. The origin of this plane is the
|
| 1394 |
+
``parent_point`` and the plane is spanned by two nonparallel planar vectors.
|
| 1395 |
+
The location of the ``child_point`` is based on the planar vectors
|
| 1396 |
+
($\\vec{v}_1$, $\\vec{v}_2$) and generalized coordinates ($q_1$, $q_2$),
|
| 1397 |
+
i.e. $\\vec{r} = q_1 \\hat{v}_1 + q_2 \\hat{v}_2$. The direction cosine
|
| 1398 |
+
matrix between the ``child_interframe`` and ``parent_interframe`` is formed
|
| 1399 |
+
using a simple rotation ($q_0$) about the rotation axis.
|
| 1400 |
+
|
| 1401 |
+
In order to simplify the definition of the ``PlanarJoint``, the
|
| 1402 |
+
``rotation_axis`` and ``planar_vectors`` are set to be the unit vectors of
|
| 1403 |
+
the ``parent_interframe`` according to the table below. This ensures that
|
| 1404 |
+
you can only define these vectors by creating a separate frame and supplying
|
| 1405 |
+
that as the interframe. If you however would only like to supply the normals
|
| 1406 |
+
of the plane with respect to the parent and child bodies, then you can also
|
| 1407 |
+
supply those to the ``parent_interframe`` and ``child_interframe``
|
| 1408 |
+
arguments. An example of both of these cases is in the examples section
|
| 1409 |
+
below and the page on the joints framework provides a more detailed
|
| 1410 |
+
explanation of the intermediate frames.
|
| 1411 |
+
|
| 1412 |
+
.. list-table::
|
| 1413 |
+
|
| 1414 |
+
* - ``rotation_axis``
|
| 1415 |
+
- ``parent_interframe.x``
|
| 1416 |
+
* - ``planar_vectors[0]``
|
| 1417 |
+
- ``parent_interframe.y``
|
| 1418 |
+
* - ``planar_vectors[1]``
|
| 1419 |
+
- ``parent_interframe.z``
|
| 1420 |
+
|
| 1421 |
+
Parameters
|
| 1422 |
+
==========
|
| 1423 |
+
|
| 1424 |
+
name : string
|
| 1425 |
+
A unique name for the joint.
|
| 1426 |
+
parent : Particle or RigidBody or Body
|
| 1427 |
+
The parent body of joint.
|
| 1428 |
+
child : Particle or RigidBody or Body
|
| 1429 |
+
The child body of joint.
|
| 1430 |
+
rotation_coordinate : dynamicsymbol, optional
|
| 1431 |
+
Generalized coordinate corresponding to the rotation angle. The default
|
| 1432 |
+
value is ``dynamicsymbols(f'q0_{joint.name}')``.
|
| 1433 |
+
planar_coordinates : iterable of dynamicsymbols, optional
|
| 1434 |
+
Two generalized coordinates used for the planar translation. The default
|
| 1435 |
+
value is ``dynamicsymbols(f'q1_{joint.name} q2_{joint.name}')``.
|
| 1436 |
+
rotation_speed : dynamicsymbol, optional
|
| 1437 |
+
Generalized speed corresponding to the angular velocity. The default
|
| 1438 |
+
value is ``dynamicsymbols(f'u0_{joint.name}')``.
|
| 1439 |
+
planar_speeds : dynamicsymbols, optional
|
| 1440 |
+
Two generalized speeds used for the planar translation velocity. The
|
| 1441 |
+
default value is ``dynamicsymbols(f'u1_{joint.name} u2_{joint.name}')``.
|
| 1442 |
+
parent_point : Point or Vector, optional
|
| 1443 |
+
Attachment point where the joint is fixed to the parent body. If a
|
| 1444 |
+
vector is provided, then the attachment point is computed by adding the
|
| 1445 |
+
vector to the body's mass center. The default value is the parent's mass
|
| 1446 |
+
center.
|
| 1447 |
+
child_point : Point or Vector, optional
|
| 1448 |
+
Attachment point where the joint is fixed to the child body. If a
|
| 1449 |
+
vector is provided, then the attachment point is computed by adding the
|
| 1450 |
+
vector to the body's mass center. The default value is the child's mass
|
| 1451 |
+
center.
|
| 1452 |
+
parent_interframe : ReferenceFrame, optional
|
| 1453 |
+
Intermediate frame of the parent body with respect to which the joint
|
| 1454 |
+
transformation is formulated. If a Vector is provided then an interframe
|
| 1455 |
+
is created which aligns its X axis with the given vector. The default
|
| 1456 |
+
value is the parent's own frame.
|
| 1457 |
+
child_interframe : ReferenceFrame, optional
|
| 1458 |
+
Intermediate frame of the child body with respect to which the joint
|
| 1459 |
+
transformation is formulated. If a Vector is provided then an interframe
|
| 1460 |
+
is created which aligns its X axis with the given vector. The default
|
| 1461 |
+
value is the child's own frame.
|
| 1462 |
+
|
| 1463 |
+
Attributes
|
| 1464 |
+
==========
|
| 1465 |
+
|
| 1466 |
+
name : string
|
| 1467 |
+
The joint's name.
|
| 1468 |
+
parent : Particle or RigidBody or Body
|
| 1469 |
+
The joint's parent body.
|
| 1470 |
+
child : Particle or RigidBody or Body
|
| 1471 |
+
The joint's child body.
|
| 1472 |
+
rotation_coordinate : dynamicsymbol
|
| 1473 |
+
Generalized coordinate corresponding to the rotation angle.
|
| 1474 |
+
planar_coordinates : Matrix
|
| 1475 |
+
Two generalized coordinates used for the planar translation.
|
| 1476 |
+
rotation_speed : dynamicsymbol
|
| 1477 |
+
Generalized speed corresponding to the angular velocity.
|
| 1478 |
+
planar_speeds : Matrix
|
| 1479 |
+
Two generalized speeds used for the planar translation velocity.
|
| 1480 |
+
coordinates : Matrix
|
| 1481 |
+
Matrix of the joint's generalized coordinates.
|
| 1482 |
+
speeds : Matrix
|
| 1483 |
+
Matrix of the joint's generalized speeds.
|
| 1484 |
+
parent_point : Point
|
| 1485 |
+
Attachment point where the joint is fixed to the parent body.
|
| 1486 |
+
child_point : Point
|
| 1487 |
+
Attachment point where the joint is fixed to the child body.
|
| 1488 |
+
parent_interframe : ReferenceFrame
|
| 1489 |
+
Intermediate frame of the parent body with respect to which the joint
|
| 1490 |
+
transformation is formulated.
|
| 1491 |
+
child_interframe : ReferenceFrame
|
| 1492 |
+
Intermediate frame of the child body with respect to which the joint
|
| 1493 |
+
transformation is formulated.
|
| 1494 |
+
kdes : Matrix
|
| 1495 |
+
Kinematical differential equations of the joint.
|
| 1496 |
+
rotation_axis : Vector
|
| 1497 |
+
The axis about which the rotation occurs.
|
| 1498 |
+
planar_vectors : list
|
| 1499 |
+
The vectors that describe the planar translation directions.
|
| 1500 |
+
|
| 1501 |
+
Examples
|
| 1502 |
+
=========
|
| 1503 |
+
|
| 1504 |
+
A single planar joint is created between two bodies and has the following
|
| 1505 |
+
basic attributes:
|
| 1506 |
+
|
| 1507 |
+
>>> from sympy.physics.mechanics import RigidBody, PlanarJoint
|
| 1508 |
+
>>> parent = RigidBody('P')
|
| 1509 |
+
>>> parent
|
| 1510 |
+
P
|
| 1511 |
+
>>> child = RigidBody('C')
|
| 1512 |
+
>>> child
|
| 1513 |
+
C
|
| 1514 |
+
>>> joint = PlanarJoint('PC', parent, child)
|
| 1515 |
+
>>> joint
|
| 1516 |
+
PlanarJoint: PC parent: P child: C
|
| 1517 |
+
>>> joint.name
|
| 1518 |
+
'PC'
|
| 1519 |
+
>>> joint.parent
|
| 1520 |
+
P
|
| 1521 |
+
>>> joint.child
|
| 1522 |
+
C
|
| 1523 |
+
>>> joint.parent_point
|
| 1524 |
+
P_masscenter
|
| 1525 |
+
>>> joint.child_point
|
| 1526 |
+
C_masscenter
|
| 1527 |
+
>>> joint.rotation_axis
|
| 1528 |
+
P_frame.x
|
| 1529 |
+
>>> joint.planar_vectors
|
| 1530 |
+
[P_frame.y, P_frame.z]
|
| 1531 |
+
>>> joint.rotation_coordinate
|
| 1532 |
+
q0_PC(t)
|
| 1533 |
+
>>> joint.planar_coordinates
|
| 1534 |
+
Matrix([
|
| 1535 |
+
[q1_PC(t)],
|
| 1536 |
+
[q2_PC(t)]])
|
| 1537 |
+
>>> joint.coordinates
|
| 1538 |
+
Matrix([
|
| 1539 |
+
[q0_PC(t)],
|
| 1540 |
+
[q1_PC(t)],
|
| 1541 |
+
[q2_PC(t)]])
|
| 1542 |
+
>>> joint.rotation_speed
|
| 1543 |
+
u0_PC(t)
|
| 1544 |
+
>>> joint.planar_speeds
|
| 1545 |
+
Matrix([
|
| 1546 |
+
[u1_PC(t)],
|
| 1547 |
+
[u2_PC(t)]])
|
| 1548 |
+
>>> joint.speeds
|
| 1549 |
+
Matrix([
|
| 1550 |
+
[u0_PC(t)],
|
| 1551 |
+
[u1_PC(t)],
|
| 1552 |
+
[u2_PC(t)]])
|
| 1553 |
+
>>> child.frame.ang_vel_in(parent.frame)
|
| 1554 |
+
u0_PC(t)*P_frame.x
|
| 1555 |
+
>>> child.frame.dcm(parent.frame)
|
| 1556 |
+
Matrix([
|
| 1557 |
+
[1, 0, 0],
|
| 1558 |
+
[0, cos(q0_PC(t)), sin(q0_PC(t))],
|
| 1559 |
+
[0, -sin(q0_PC(t)), cos(q0_PC(t))]])
|
| 1560 |
+
>>> joint.child_point.pos_from(joint.parent_point)
|
| 1561 |
+
q1_PC(t)*P_frame.y + q2_PC(t)*P_frame.z
|
| 1562 |
+
>>> child.masscenter.vel(parent.frame)
|
| 1563 |
+
u1_PC(t)*P_frame.y + u2_PC(t)*P_frame.z
|
| 1564 |
+
|
| 1565 |
+
To further demonstrate the use of the planar joint, the kinematics of a
|
| 1566 |
+
block sliding on a slope, can be created as follows.
|
| 1567 |
+
|
| 1568 |
+
>>> from sympy import symbols
|
| 1569 |
+
>>> from sympy.physics.mechanics import PlanarJoint, RigidBody, ReferenceFrame
|
| 1570 |
+
>>> a, d, h = symbols('a d h')
|
| 1571 |
+
|
| 1572 |
+
First create bodies to represent the slope and the block.
|
| 1573 |
+
|
| 1574 |
+
>>> ground = RigidBody('G')
|
| 1575 |
+
>>> block = RigidBody('B')
|
| 1576 |
+
|
| 1577 |
+
To define the slope you can either define the plane by specifying the
|
| 1578 |
+
``planar_vectors`` or/and the ``rotation_axis``. However it is advisable to
|
| 1579 |
+
create a rotated intermediate frame, so that the ``parent_vectors`` and
|
| 1580 |
+
``rotation_axis`` will be the unit vectors of this intermediate frame.
|
| 1581 |
+
|
| 1582 |
+
>>> slope = ReferenceFrame('A')
|
| 1583 |
+
>>> slope.orient_axis(ground.frame, ground.y, a)
|
| 1584 |
+
|
| 1585 |
+
The planar joint can be created using these bodies and intermediate frame.
|
| 1586 |
+
We can specify the origin of the slope to be ``d`` above the slope's center
|
| 1587 |
+
of mass and the block's center of mass to be a distance ``h`` above the
|
| 1588 |
+
slope's surface. Note that we can specify the normal of the plane using the
|
| 1589 |
+
rotation axis argument.
|
| 1590 |
+
|
| 1591 |
+
>>> joint = PlanarJoint('PC', ground, block, parent_point=d * ground.x,
|
| 1592 |
+
... child_point=-h * block.x, parent_interframe=slope)
|
| 1593 |
+
|
| 1594 |
+
Once the joint is established the kinematics of the bodies can be accessed.
|
| 1595 |
+
First the ``rotation_axis``, which is normal to the plane and the
|
| 1596 |
+
``plane_vectors``, can be found.
|
| 1597 |
+
|
| 1598 |
+
>>> joint.rotation_axis
|
| 1599 |
+
A.x
|
| 1600 |
+
>>> joint.planar_vectors
|
| 1601 |
+
[A.y, A.z]
|
| 1602 |
+
|
| 1603 |
+
The direction cosine matrix of the block with respect to the ground can be
|
| 1604 |
+
found with:
|
| 1605 |
+
|
| 1606 |
+
>>> block.frame.dcm(ground.frame)
|
| 1607 |
+
Matrix([
|
| 1608 |
+
[ cos(a), 0, -sin(a)],
|
| 1609 |
+
[sin(a)*sin(q0_PC(t)), cos(q0_PC(t)), sin(q0_PC(t))*cos(a)],
|
| 1610 |
+
[sin(a)*cos(q0_PC(t)), -sin(q0_PC(t)), cos(a)*cos(q0_PC(t))]])
|
| 1611 |
+
|
| 1612 |
+
The angular velocity of the block can be computed with respect to the
|
| 1613 |
+
ground.
|
| 1614 |
+
|
| 1615 |
+
>>> block.frame.ang_vel_in(ground.frame)
|
| 1616 |
+
u0_PC(t)*A.x
|
| 1617 |
+
|
| 1618 |
+
The position of the block's center of mass can be found with:
|
| 1619 |
+
|
| 1620 |
+
>>> block.masscenter.pos_from(ground.masscenter)
|
| 1621 |
+
d*G_frame.x + h*B_frame.x + q1_PC(t)*A.y + q2_PC(t)*A.z
|
| 1622 |
+
|
| 1623 |
+
Finally, the linear velocity of the block's center of mass can be
|
| 1624 |
+
computed with respect to the ground.
|
| 1625 |
+
|
| 1626 |
+
>>> block.masscenter.vel(ground.frame)
|
| 1627 |
+
u1_PC(t)*A.y + u2_PC(t)*A.z
|
| 1628 |
+
|
| 1629 |
+
In some cases it could be your preference to only define the normals of the
|
| 1630 |
+
plane with respect to both bodies. This can most easily be done by supplying
|
| 1631 |
+
vectors to the ``interframe`` arguments. What will happen in this case is
|
| 1632 |
+
that an interframe will be created with its ``x`` axis aligned with the
|
| 1633 |
+
provided vector. For a further explanation of how this is done see the notes
|
| 1634 |
+
of the ``Joint`` class. In the code below, the above example (with the block
|
| 1635 |
+
on the slope) is recreated by supplying vectors to the interframe arguments.
|
| 1636 |
+
Note that the previously described option is however more computationally
|
| 1637 |
+
efficient, because the algorithm now has to compute the rotation angle
|
| 1638 |
+
between the provided vector and the 'x' axis.
|
| 1639 |
+
|
| 1640 |
+
>>> from sympy import symbols, cos, sin
|
| 1641 |
+
>>> from sympy.physics.mechanics import PlanarJoint, RigidBody
|
| 1642 |
+
>>> a, d, h = symbols('a d h')
|
| 1643 |
+
>>> ground = RigidBody('G')
|
| 1644 |
+
>>> block = RigidBody('B')
|
| 1645 |
+
>>> joint = PlanarJoint(
|
| 1646 |
+
... 'PC', ground, block, parent_point=d * ground.x,
|
| 1647 |
+
... child_point=-h * block.x, child_interframe=block.x,
|
| 1648 |
+
... parent_interframe=cos(a) * ground.x + sin(a) * ground.z)
|
| 1649 |
+
>>> block.frame.dcm(ground.frame).simplify()
|
| 1650 |
+
Matrix([
|
| 1651 |
+
[ cos(a), 0, sin(a)],
|
| 1652 |
+
[-sin(a)*sin(q0_PC(t)), cos(q0_PC(t)), sin(q0_PC(t))*cos(a)],
|
| 1653 |
+
[-sin(a)*cos(q0_PC(t)), -sin(q0_PC(t)), cos(a)*cos(q0_PC(t))]])
|
| 1654 |
+
|
| 1655 |
+
"""
|
| 1656 |
+
|
| 1657 |
+
def __init__(self, name, parent, child, rotation_coordinate=None,
|
| 1658 |
+
planar_coordinates=None, rotation_speed=None,
|
| 1659 |
+
planar_speeds=None, parent_point=None, child_point=None,
|
| 1660 |
+
parent_interframe=None, child_interframe=None):
|
| 1661 |
+
# A ready to merge implementation of setting the planar_vectors and
|
| 1662 |
+
# rotation_axis was added and removed in PR #24046
|
| 1663 |
+
coordinates = (rotation_coordinate, planar_coordinates)
|
| 1664 |
+
speeds = (rotation_speed, planar_speeds)
|
| 1665 |
+
super().__init__(name, parent, child, coordinates, speeds,
|
| 1666 |
+
parent_point, child_point,
|
| 1667 |
+
parent_interframe=parent_interframe,
|
| 1668 |
+
child_interframe=child_interframe)
|
| 1669 |
+
|
| 1670 |
+
def __str__(self):
|
| 1671 |
+
return (f'PlanarJoint: {self.name} parent: {self.parent} '
|
| 1672 |
+
f'child: {self.child}')
|
| 1673 |
+
|
| 1674 |
+
@property
|
| 1675 |
+
def rotation_coordinate(self):
|
| 1676 |
+
"""Generalized coordinate corresponding to the rotation angle."""
|
| 1677 |
+
return self.coordinates[0]
|
| 1678 |
+
|
| 1679 |
+
@property
|
| 1680 |
+
def planar_coordinates(self):
|
| 1681 |
+
"""Two generalized coordinates used for the planar translation."""
|
| 1682 |
+
return self.coordinates[1:, 0]
|
| 1683 |
+
|
| 1684 |
+
@property
|
| 1685 |
+
def rotation_speed(self):
|
| 1686 |
+
"""Generalized speed corresponding to the angular velocity."""
|
| 1687 |
+
return self.speeds[0]
|
| 1688 |
+
|
| 1689 |
+
@property
|
| 1690 |
+
def planar_speeds(self):
|
| 1691 |
+
"""Two generalized speeds used for the planar translation velocity."""
|
| 1692 |
+
return self.speeds[1:, 0]
|
| 1693 |
+
|
| 1694 |
+
@property
|
| 1695 |
+
def rotation_axis(self):
|
| 1696 |
+
"""The axis about which the rotation occurs."""
|
| 1697 |
+
return self.parent_interframe.x
|
| 1698 |
+
|
| 1699 |
+
@property
|
| 1700 |
+
def planar_vectors(self):
|
| 1701 |
+
"""The vectors that describe the planar translation directions."""
|
| 1702 |
+
return [self.parent_interframe.y, self.parent_interframe.z]
|
| 1703 |
+
|
| 1704 |
+
def _generate_coordinates(self, coordinates):
|
| 1705 |
+
rotation_speed = self._fill_coordinate_list(coordinates[0], 1, 'q',
|
| 1706 |
+
number_single=True)
|
| 1707 |
+
planar_speeds = self._fill_coordinate_list(coordinates[1], 2, 'q', 1)
|
| 1708 |
+
return rotation_speed.col_join(planar_speeds)
|
| 1709 |
+
|
| 1710 |
+
def _generate_speeds(self, speeds):
|
| 1711 |
+
rotation_speed = self._fill_coordinate_list(speeds[0], 1, 'u',
|
| 1712 |
+
number_single=True)
|
| 1713 |
+
planar_speeds = self._fill_coordinate_list(speeds[1], 2, 'u', 1)
|
| 1714 |
+
return rotation_speed.col_join(planar_speeds)
|
| 1715 |
+
|
| 1716 |
+
def _orient_frames(self):
|
| 1717 |
+
self.child_interframe.orient_axis(
|
| 1718 |
+
self.parent_interframe, self.rotation_axis,
|
| 1719 |
+
self.rotation_coordinate)
|
| 1720 |
+
|
| 1721 |
+
def _set_angular_velocity(self):
|
| 1722 |
+
self.child_interframe.set_ang_vel(
|
| 1723 |
+
self.parent_interframe,
|
| 1724 |
+
self.rotation_speed * self.rotation_axis)
|
| 1725 |
+
|
| 1726 |
+
def _set_linear_velocity(self):
|
| 1727 |
+
self.child_point.set_pos(
|
| 1728 |
+
self.parent_point,
|
| 1729 |
+
self.planar_coordinates[0] * self.planar_vectors[0] +
|
| 1730 |
+
self.planar_coordinates[1] * self.planar_vectors[1])
|
| 1731 |
+
self.parent_point.set_vel(self.parent_interframe, 0)
|
| 1732 |
+
self.child_point.set_vel(self.child_interframe, 0)
|
| 1733 |
+
self.child_point.set_vel(
|
| 1734 |
+
self._parent_frame, self.planar_speeds[0] * self.planar_vectors[0] +
|
| 1735 |
+
self.planar_speeds[1] * self.planar_vectors[1])
|
| 1736 |
+
self.child.masscenter.v2pt_theory(self.child_point, self._parent_frame,
|
| 1737 |
+
self._child_frame)
|
| 1738 |
+
|
| 1739 |
+
|
| 1740 |
+
class SphericalJoint(Joint):
|
| 1741 |
+
"""Spherical (Ball-and-Socket) Joint.
|
| 1742 |
+
|
| 1743 |
+
.. image:: SphericalJoint.svg
|
| 1744 |
+
:align: center
|
| 1745 |
+
:width: 600
|
| 1746 |
+
|
| 1747 |
+
Explanation
|
| 1748 |
+
===========
|
| 1749 |
+
|
| 1750 |
+
A spherical joint is defined such that the child body is free to rotate in
|
| 1751 |
+
any direction, without allowing a translation of the ``child_point``. As can
|
| 1752 |
+
also be seen in the image, the ``parent_point`` and ``child_point`` are
|
| 1753 |
+
fixed on top of each other, i.e. the ``joint_point``. This rotation is
|
| 1754 |
+
defined using the :func:`parent_interframe.orient(child_interframe,
|
| 1755 |
+
rot_type, amounts, rot_order)
|
| 1756 |
+
<sympy.physics.vector.frame.ReferenceFrame.orient>` method. The default
|
| 1757 |
+
rotation consists of three relative rotations, i.e. body-fixed rotations.
|
| 1758 |
+
Based on the direction cosine matrix following from these rotations, the
|
| 1759 |
+
angular velocity is computed based on the generalized coordinates and
|
| 1760 |
+
generalized speeds.
|
| 1761 |
+
|
| 1762 |
+
Parameters
|
| 1763 |
+
==========
|
| 1764 |
+
|
| 1765 |
+
name : string
|
| 1766 |
+
A unique name for the joint.
|
| 1767 |
+
parent : Particle or RigidBody or Body
|
| 1768 |
+
The parent body of joint.
|
| 1769 |
+
child : Particle or RigidBody or Body
|
| 1770 |
+
The child body of joint.
|
| 1771 |
+
coordinates: iterable of dynamicsymbols, optional
|
| 1772 |
+
Generalized coordinates of the joint.
|
| 1773 |
+
speeds : iterable of dynamicsymbols, optional
|
| 1774 |
+
Generalized speeds of joint.
|
| 1775 |
+
parent_point : Point or Vector, optional
|
| 1776 |
+
Attachment point where the joint is fixed to the parent body. If a
|
| 1777 |
+
vector is provided, then the attachment point is computed by adding the
|
| 1778 |
+
vector to the body's mass center. The default value is the parent's mass
|
| 1779 |
+
center.
|
| 1780 |
+
child_point : Point or Vector, optional
|
| 1781 |
+
Attachment point where the joint is fixed to the child body. If a
|
| 1782 |
+
vector is provided, then the attachment point is computed by adding the
|
| 1783 |
+
vector to the body's mass center. The default value is the child's mass
|
| 1784 |
+
center.
|
| 1785 |
+
parent_interframe : ReferenceFrame, optional
|
| 1786 |
+
Intermediate frame of the parent body with respect to which the joint
|
| 1787 |
+
transformation is formulated. If a Vector is provided then an interframe
|
| 1788 |
+
is created which aligns its X axis with the given vector. The default
|
| 1789 |
+
value is the parent's own frame.
|
| 1790 |
+
child_interframe : ReferenceFrame, optional
|
| 1791 |
+
Intermediate frame of the child body with respect to which the joint
|
| 1792 |
+
transformation is formulated. If a Vector is provided then an interframe
|
| 1793 |
+
is created which aligns its X axis with the given vector. The default
|
| 1794 |
+
value is the child's own frame.
|
| 1795 |
+
rot_type : str, optional
|
| 1796 |
+
The method used to generate the direction cosine matrix. Supported
|
| 1797 |
+
methods are:
|
| 1798 |
+
|
| 1799 |
+
- ``'Body'``: three successive rotations about new intermediate axes,
|
| 1800 |
+
also called "Euler and Tait-Bryan angles"
|
| 1801 |
+
- ``'Space'``: three successive rotations about the parent frames' unit
|
| 1802 |
+
vectors
|
| 1803 |
+
|
| 1804 |
+
The default method is ``'Body'``.
|
| 1805 |
+
amounts :
|
| 1806 |
+
Expressions defining the rotation angles or direction cosine matrix.
|
| 1807 |
+
These must match the ``rot_type``. See examples below for details. The
|
| 1808 |
+
input types are:
|
| 1809 |
+
|
| 1810 |
+
- ``'Body'``: 3-tuple of expressions, symbols, or functions
|
| 1811 |
+
- ``'Space'``: 3-tuple of expressions, symbols, or functions
|
| 1812 |
+
|
| 1813 |
+
The default amounts are the given ``coordinates``.
|
| 1814 |
+
rot_order : str or int, optional
|
| 1815 |
+
If applicable, the order of the successive of rotations. The string
|
| 1816 |
+
``'123'`` and integer ``123`` are equivalent, for example. Required for
|
| 1817 |
+
``'Body'`` and ``'Space'``. The default value is ``123``.
|
| 1818 |
+
|
| 1819 |
+
Attributes
|
| 1820 |
+
==========
|
| 1821 |
+
|
| 1822 |
+
name : string
|
| 1823 |
+
The joint's name.
|
| 1824 |
+
parent : Particle or RigidBody or Body
|
| 1825 |
+
The joint's parent body.
|
| 1826 |
+
child : Particle or RigidBody or Body
|
| 1827 |
+
The joint's child body.
|
| 1828 |
+
coordinates : Matrix
|
| 1829 |
+
Matrix of the joint's generalized coordinates.
|
| 1830 |
+
speeds : Matrix
|
| 1831 |
+
Matrix of the joint's generalized speeds.
|
| 1832 |
+
parent_point : Point
|
| 1833 |
+
Attachment point where the joint is fixed to the parent body.
|
| 1834 |
+
child_point : Point
|
| 1835 |
+
Attachment point where the joint is fixed to the child body.
|
| 1836 |
+
parent_interframe : ReferenceFrame
|
| 1837 |
+
Intermediate frame of the parent body with respect to which the joint
|
| 1838 |
+
transformation is formulated.
|
| 1839 |
+
child_interframe : ReferenceFrame
|
| 1840 |
+
Intermediate frame of the child body with respect to which the joint
|
| 1841 |
+
transformation is formulated.
|
| 1842 |
+
kdes : Matrix
|
| 1843 |
+
Kinematical differential equations of the joint.
|
| 1844 |
+
|
| 1845 |
+
Examples
|
| 1846 |
+
=========
|
| 1847 |
+
|
| 1848 |
+
A single spherical joint is created from two bodies and has the following
|
| 1849 |
+
basic attributes:
|
| 1850 |
+
|
| 1851 |
+
>>> from sympy.physics.mechanics import RigidBody, SphericalJoint
|
| 1852 |
+
>>> parent = RigidBody('P')
|
| 1853 |
+
>>> parent
|
| 1854 |
+
P
|
| 1855 |
+
>>> child = RigidBody('C')
|
| 1856 |
+
>>> child
|
| 1857 |
+
C
|
| 1858 |
+
>>> joint = SphericalJoint('PC', parent, child)
|
| 1859 |
+
>>> joint
|
| 1860 |
+
SphericalJoint: PC parent: P child: C
|
| 1861 |
+
>>> joint.name
|
| 1862 |
+
'PC'
|
| 1863 |
+
>>> joint.parent
|
| 1864 |
+
P
|
| 1865 |
+
>>> joint.child
|
| 1866 |
+
C
|
| 1867 |
+
>>> joint.parent_point
|
| 1868 |
+
P_masscenter
|
| 1869 |
+
>>> joint.child_point
|
| 1870 |
+
C_masscenter
|
| 1871 |
+
>>> joint.parent_interframe
|
| 1872 |
+
P_frame
|
| 1873 |
+
>>> joint.child_interframe
|
| 1874 |
+
C_frame
|
| 1875 |
+
>>> joint.coordinates
|
| 1876 |
+
Matrix([
|
| 1877 |
+
[q0_PC(t)],
|
| 1878 |
+
[q1_PC(t)],
|
| 1879 |
+
[q2_PC(t)]])
|
| 1880 |
+
>>> joint.speeds
|
| 1881 |
+
Matrix([
|
| 1882 |
+
[u0_PC(t)],
|
| 1883 |
+
[u1_PC(t)],
|
| 1884 |
+
[u2_PC(t)]])
|
| 1885 |
+
>>> child.frame.ang_vel_in(parent.frame).to_matrix(child.frame)
|
| 1886 |
+
Matrix([
|
| 1887 |
+
[ u0_PC(t)*cos(q1_PC(t))*cos(q2_PC(t)) + u1_PC(t)*sin(q2_PC(t))],
|
| 1888 |
+
[-u0_PC(t)*sin(q2_PC(t))*cos(q1_PC(t)) + u1_PC(t)*cos(q2_PC(t))],
|
| 1889 |
+
[ u0_PC(t)*sin(q1_PC(t)) + u2_PC(t)]])
|
| 1890 |
+
>>> child.frame.x.to_matrix(parent.frame)
|
| 1891 |
+
Matrix([
|
| 1892 |
+
[ cos(q1_PC(t))*cos(q2_PC(t))],
|
| 1893 |
+
[sin(q0_PC(t))*sin(q1_PC(t))*cos(q2_PC(t)) + sin(q2_PC(t))*cos(q0_PC(t))],
|
| 1894 |
+
[sin(q0_PC(t))*sin(q2_PC(t)) - sin(q1_PC(t))*cos(q0_PC(t))*cos(q2_PC(t))]])
|
| 1895 |
+
>>> joint.child_point.pos_from(joint.parent_point)
|
| 1896 |
+
0
|
| 1897 |
+
|
| 1898 |
+
To further demonstrate the use of the spherical joint, the kinematics of a
|
| 1899 |
+
spherical joint with a ZXZ rotation can be created as follows.
|
| 1900 |
+
|
| 1901 |
+
>>> from sympy import symbols
|
| 1902 |
+
>>> from sympy.physics.mechanics import RigidBody, SphericalJoint
|
| 1903 |
+
>>> l1 = symbols('l1')
|
| 1904 |
+
|
| 1905 |
+
First create bodies to represent the fixed floor and a pendulum bob.
|
| 1906 |
+
|
| 1907 |
+
>>> floor = RigidBody('F')
|
| 1908 |
+
>>> bob = RigidBody('B')
|
| 1909 |
+
|
| 1910 |
+
The joint will connect the bob to the floor, with the joint located at a
|
| 1911 |
+
distance of ``l1`` from the child's center of mass and the rotation set to a
|
| 1912 |
+
body-fixed ZXZ rotation.
|
| 1913 |
+
|
| 1914 |
+
>>> joint = SphericalJoint('S', floor, bob, child_point=l1 * bob.y,
|
| 1915 |
+
... rot_type='body', rot_order='ZXZ')
|
| 1916 |
+
|
| 1917 |
+
Now that the joint is established, the kinematics of the connected body can
|
| 1918 |
+
be accessed.
|
| 1919 |
+
|
| 1920 |
+
The position of the bob's masscenter is found with:
|
| 1921 |
+
|
| 1922 |
+
>>> bob.masscenter.pos_from(floor.masscenter)
|
| 1923 |
+
- l1*B_frame.y
|
| 1924 |
+
|
| 1925 |
+
The angular velocities of the pendulum link can be computed with respect to
|
| 1926 |
+
the floor.
|
| 1927 |
+
|
| 1928 |
+
>>> bob.frame.ang_vel_in(floor.frame).to_matrix(
|
| 1929 |
+
... floor.frame).simplify()
|
| 1930 |
+
Matrix([
|
| 1931 |
+
[u1_S(t)*cos(q0_S(t)) + u2_S(t)*sin(q0_S(t))*sin(q1_S(t))],
|
| 1932 |
+
[u1_S(t)*sin(q0_S(t)) - u2_S(t)*sin(q1_S(t))*cos(q0_S(t))],
|
| 1933 |
+
[ u0_S(t) + u2_S(t)*cos(q1_S(t))]])
|
| 1934 |
+
|
| 1935 |
+
Finally, the linear velocity of the bob's center of mass can be computed.
|
| 1936 |
+
|
| 1937 |
+
>>> bob.masscenter.vel(floor.frame).to_matrix(bob.frame)
|
| 1938 |
+
Matrix([
|
| 1939 |
+
[ l1*(u0_S(t)*cos(q1_S(t)) + u2_S(t))],
|
| 1940 |
+
[ 0],
|
| 1941 |
+
[-l1*(u0_S(t)*sin(q1_S(t))*sin(q2_S(t)) + u1_S(t)*cos(q2_S(t)))]])
|
| 1942 |
+
|
| 1943 |
+
"""
|
| 1944 |
+
def __init__(self, name, parent, child, coordinates=None, speeds=None,
|
| 1945 |
+
parent_point=None, child_point=None, parent_interframe=None,
|
| 1946 |
+
child_interframe=None, rot_type='BODY', amounts=None,
|
| 1947 |
+
rot_order=123):
|
| 1948 |
+
self._rot_type = rot_type
|
| 1949 |
+
self._amounts = amounts
|
| 1950 |
+
self._rot_order = rot_order
|
| 1951 |
+
super().__init__(name, parent, child, coordinates, speeds,
|
| 1952 |
+
parent_point, child_point,
|
| 1953 |
+
parent_interframe=parent_interframe,
|
| 1954 |
+
child_interframe=child_interframe)
|
| 1955 |
+
|
| 1956 |
+
def __str__(self):
|
| 1957 |
+
return (f'SphericalJoint: {self.name} parent: {self.parent} '
|
| 1958 |
+
f'child: {self.child}')
|
| 1959 |
+
|
| 1960 |
+
def _generate_coordinates(self, coordinates):
|
| 1961 |
+
return self._fill_coordinate_list(coordinates, 3, 'q')
|
| 1962 |
+
|
| 1963 |
+
def _generate_speeds(self, speeds):
|
| 1964 |
+
return self._fill_coordinate_list(speeds, len(self.coordinates), 'u')
|
| 1965 |
+
|
| 1966 |
+
def _orient_frames(self):
|
| 1967 |
+
supported_rot_types = ('BODY', 'SPACE')
|
| 1968 |
+
if self._rot_type.upper() not in supported_rot_types:
|
| 1969 |
+
raise NotImplementedError(
|
| 1970 |
+
f'Rotation type "{self._rot_type}" is not implemented. '
|
| 1971 |
+
f'Implemented rotation types are: {supported_rot_types}')
|
| 1972 |
+
amounts = self.coordinates if self._amounts is None else self._amounts
|
| 1973 |
+
self.child_interframe.orient(self.parent_interframe, self._rot_type,
|
| 1974 |
+
amounts, self._rot_order)
|
| 1975 |
+
|
| 1976 |
+
def _set_angular_velocity(self):
|
| 1977 |
+
t = dynamicsymbols._t
|
| 1978 |
+
vel = self.child_interframe.ang_vel_in(self.parent_interframe).xreplace(
|
| 1979 |
+
{q.diff(t): u for q, u in zip(self.coordinates, self.speeds)}
|
| 1980 |
+
)
|
| 1981 |
+
self.child_interframe.set_ang_vel(self.parent_interframe, vel)
|
| 1982 |
+
|
| 1983 |
+
def _set_linear_velocity(self):
|
| 1984 |
+
self.child_point.set_pos(self.parent_point, 0)
|
| 1985 |
+
self.parent_point.set_vel(self._parent_frame, 0)
|
| 1986 |
+
self.child_point.set_vel(self._child_frame, 0)
|
| 1987 |
+
self.child.masscenter.v2pt_theory(self.parent_point, self._parent_frame,
|
| 1988 |
+
self._child_frame)
|
| 1989 |
+
|
| 1990 |
+
|
| 1991 |
+
class WeldJoint(Joint):
|
| 1992 |
+
"""Weld Joint.
|
| 1993 |
+
|
| 1994 |
+
.. raw:: html
|
| 1995 |
+
:file: ../../../doc/src/modules/physics/mechanics/api/WeldJoint.svg
|
| 1996 |
+
|
| 1997 |
+
Explanation
|
| 1998 |
+
===========
|
| 1999 |
+
|
| 2000 |
+
A weld joint is defined such that there is no relative motion between the
|
| 2001 |
+
child and parent bodies. The direction cosine matrix between the attachment
|
| 2002 |
+
frame (``parent_interframe`` and ``child_interframe``) is the identity
|
| 2003 |
+
matrix and the attachment points (``parent_point`` and ``child_point``) are
|
| 2004 |
+
coincident. The page on the joints framework gives a more detailed
|
| 2005 |
+
explanation of the intermediate frames.
|
| 2006 |
+
|
| 2007 |
+
Parameters
|
| 2008 |
+
==========
|
| 2009 |
+
|
| 2010 |
+
name : string
|
| 2011 |
+
A unique name for the joint.
|
| 2012 |
+
parent : Particle or RigidBody or Body
|
| 2013 |
+
The parent body of joint.
|
| 2014 |
+
child : Particle or RigidBody or Body
|
| 2015 |
+
The child body of joint.
|
| 2016 |
+
parent_point : Point or Vector, optional
|
| 2017 |
+
Attachment point where the joint is fixed to the parent body. If a
|
| 2018 |
+
vector is provided, then the attachment point is computed by adding the
|
| 2019 |
+
vector to the body's mass center. The default value is the parent's mass
|
| 2020 |
+
center.
|
| 2021 |
+
child_point : Point or Vector, optional
|
| 2022 |
+
Attachment point where the joint is fixed to the child body. If a
|
| 2023 |
+
vector is provided, then the attachment point is computed by adding the
|
| 2024 |
+
vector to the body's mass center. The default value is the child's mass
|
| 2025 |
+
center.
|
| 2026 |
+
parent_interframe : ReferenceFrame, optional
|
| 2027 |
+
Intermediate frame of the parent body with respect to which the joint
|
| 2028 |
+
transformation is formulated. If a Vector is provided then an interframe
|
| 2029 |
+
is created which aligns its X axis with the given vector. The default
|
| 2030 |
+
value is the parent's own frame.
|
| 2031 |
+
child_interframe : ReferenceFrame, optional
|
| 2032 |
+
Intermediate frame of the child body with respect to which the joint
|
| 2033 |
+
transformation is formulated. If a Vector is provided then an interframe
|
| 2034 |
+
is created which aligns its X axis with the given vector. The default
|
| 2035 |
+
value is the child's own frame.
|
| 2036 |
+
|
| 2037 |
+
Attributes
|
| 2038 |
+
==========
|
| 2039 |
+
|
| 2040 |
+
name : string
|
| 2041 |
+
The joint's name.
|
| 2042 |
+
parent : Particle or RigidBody or Body
|
| 2043 |
+
The joint's parent body.
|
| 2044 |
+
child : Particle or RigidBody or Body
|
| 2045 |
+
The joint's child body.
|
| 2046 |
+
coordinates : Matrix
|
| 2047 |
+
Matrix of the joint's generalized coordinates. The default value is
|
| 2048 |
+
``dynamicsymbols(f'q_{joint.name}')``.
|
| 2049 |
+
speeds : Matrix
|
| 2050 |
+
Matrix of the joint's generalized speeds. The default value is
|
| 2051 |
+
``dynamicsymbols(f'u_{joint.name}')``.
|
| 2052 |
+
parent_point : Point
|
| 2053 |
+
Attachment point where the joint is fixed to the parent body.
|
| 2054 |
+
child_point : Point
|
| 2055 |
+
Attachment point where the joint is fixed to the child body.
|
| 2056 |
+
parent_interframe : ReferenceFrame
|
| 2057 |
+
Intermediate frame of the parent body with respect to which the joint
|
| 2058 |
+
transformation is formulated.
|
| 2059 |
+
child_interframe : ReferenceFrame
|
| 2060 |
+
Intermediate frame of the child body with respect to which the joint
|
| 2061 |
+
transformation is formulated.
|
| 2062 |
+
kdes : Matrix
|
| 2063 |
+
Kinematical differential equations of the joint.
|
| 2064 |
+
|
| 2065 |
+
Examples
|
| 2066 |
+
=========
|
| 2067 |
+
|
| 2068 |
+
A single weld joint is created from two bodies and has the following basic
|
| 2069 |
+
attributes:
|
| 2070 |
+
|
| 2071 |
+
>>> from sympy.physics.mechanics import RigidBody, WeldJoint
|
| 2072 |
+
>>> parent = RigidBody('P')
|
| 2073 |
+
>>> parent
|
| 2074 |
+
P
|
| 2075 |
+
>>> child = RigidBody('C')
|
| 2076 |
+
>>> child
|
| 2077 |
+
C
|
| 2078 |
+
>>> joint = WeldJoint('PC', parent, child)
|
| 2079 |
+
>>> joint
|
| 2080 |
+
WeldJoint: PC parent: P child: C
|
| 2081 |
+
>>> joint.name
|
| 2082 |
+
'PC'
|
| 2083 |
+
>>> joint.parent
|
| 2084 |
+
P
|
| 2085 |
+
>>> joint.child
|
| 2086 |
+
C
|
| 2087 |
+
>>> joint.parent_point
|
| 2088 |
+
P_masscenter
|
| 2089 |
+
>>> joint.child_point
|
| 2090 |
+
C_masscenter
|
| 2091 |
+
>>> joint.coordinates
|
| 2092 |
+
Matrix(0, 0, [])
|
| 2093 |
+
>>> joint.speeds
|
| 2094 |
+
Matrix(0, 0, [])
|
| 2095 |
+
>>> child.frame.ang_vel_in(parent.frame)
|
| 2096 |
+
0
|
| 2097 |
+
>>> child.frame.dcm(parent.frame)
|
| 2098 |
+
Matrix([
|
| 2099 |
+
[1, 0, 0],
|
| 2100 |
+
[0, 1, 0],
|
| 2101 |
+
[0, 0, 1]])
|
| 2102 |
+
>>> joint.child_point.pos_from(joint.parent_point)
|
| 2103 |
+
0
|
| 2104 |
+
|
| 2105 |
+
To further demonstrate the use of the weld joint, two relatively-fixed
|
| 2106 |
+
bodies rotated by a quarter turn about the Y axis can be created as follows:
|
| 2107 |
+
|
| 2108 |
+
>>> from sympy import symbols, pi
|
| 2109 |
+
>>> from sympy.physics.mechanics import ReferenceFrame, RigidBody, WeldJoint
|
| 2110 |
+
>>> l1, l2 = symbols('l1 l2')
|
| 2111 |
+
|
| 2112 |
+
First create the bodies to represent the parent and rotated child body.
|
| 2113 |
+
|
| 2114 |
+
>>> parent = RigidBody('P')
|
| 2115 |
+
>>> child = RigidBody('C')
|
| 2116 |
+
|
| 2117 |
+
Next the intermediate frame specifying the fixed rotation with respect to
|
| 2118 |
+
the parent can be created.
|
| 2119 |
+
|
| 2120 |
+
>>> rotated_frame = ReferenceFrame('Pr')
|
| 2121 |
+
>>> rotated_frame.orient_axis(parent.frame, parent.y, pi / 2)
|
| 2122 |
+
|
| 2123 |
+
The weld between the parent body and child body is located at a distance
|
| 2124 |
+
``l1`` from the parent's center of mass in the X direction and ``l2`` from
|
| 2125 |
+
the child's center of mass in the child's negative X direction.
|
| 2126 |
+
|
| 2127 |
+
>>> weld = WeldJoint('weld', parent, child, parent_point=l1 * parent.x,
|
| 2128 |
+
... child_point=-l2 * child.x,
|
| 2129 |
+
... parent_interframe=rotated_frame)
|
| 2130 |
+
|
| 2131 |
+
Now that the joint has been established, the kinematics of the bodies can be
|
| 2132 |
+
accessed. The direction cosine matrix of the child body with respect to the
|
| 2133 |
+
parent can be found:
|
| 2134 |
+
|
| 2135 |
+
>>> child.frame.dcm(parent.frame)
|
| 2136 |
+
Matrix([
|
| 2137 |
+
[0, 0, -1],
|
| 2138 |
+
[0, 1, 0],
|
| 2139 |
+
[1, 0, 0]])
|
| 2140 |
+
|
| 2141 |
+
As can also been seen from the direction cosine matrix, the parent X axis is
|
| 2142 |
+
aligned with the child's Z axis:
|
| 2143 |
+
>>> parent.x == child.z
|
| 2144 |
+
True
|
| 2145 |
+
|
| 2146 |
+
The position of the child's center of mass with respect to the parent's
|
| 2147 |
+
center of mass can be found with:
|
| 2148 |
+
|
| 2149 |
+
>>> child.masscenter.pos_from(parent.masscenter)
|
| 2150 |
+
l1*P_frame.x + l2*C_frame.x
|
| 2151 |
+
|
| 2152 |
+
The angular velocity of the child with respect to the parent is 0 as one
|
| 2153 |
+
would expect.
|
| 2154 |
+
|
| 2155 |
+
>>> child.frame.ang_vel_in(parent.frame)
|
| 2156 |
+
0
|
| 2157 |
+
|
| 2158 |
+
"""
|
| 2159 |
+
|
| 2160 |
+
def __init__(self, name, parent, child, parent_point=None, child_point=None,
|
| 2161 |
+
parent_interframe=None, child_interframe=None):
|
| 2162 |
+
super().__init__(name, parent, child, [], [], parent_point,
|
| 2163 |
+
child_point, parent_interframe=parent_interframe,
|
| 2164 |
+
child_interframe=child_interframe)
|
| 2165 |
+
self._kdes = Matrix(1, 0, []).T # Removes stackability problems #10770
|
| 2166 |
+
|
| 2167 |
+
def __str__(self):
|
| 2168 |
+
return (f'WeldJoint: {self.name} parent: {self.parent} '
|
| 2169 |
+
f'child: {self.child}')
|
| 2170 |
+
|
| 2171 |
+
def _generate_coordinates(self, coordinate):
|
| 2172 |
+
return Matrix()
|
| 2173 |
+
|
| 2174 |
+
def _generate_speeds(self, speed):
|
| 2175 |
+
return Matrix()
|
| 2176 |
+
|
| 2177 |
+
def _orient_frames(self):
|
| 2178 |
+
self.child_interframe.orient_axis(self.parent_interframe,
|
| 2179 |
+
self.parent_interframe.x, 0)
|
| 2180 |
+
|
| 2181 |
+
def _set_angular_velocity(self):
|
| 2182 |
+
self.child_interframe.set_ang_vel(self.parent_interframe, 0)
|
| 2183 |
+
|
| 2184 |
+
def _set_linear_velocity(self):
|
| 2185 |
+
self.child_point.set_pos(self.parent_point, 0)
|
| 2186 |
+
self.parent_point.set_vel(self._parent_frame, 0)
|
| 2187 |
+
self.child_point.set_vel(self._child_frame, 0)
|
| 2188 |
+
self.child.masscenter.set_vel(self._parent_frame, 0)
|
lib/python3.12/site-packages/sympy/physics/mechanics/jointsmethod.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy.physics.mechanics import (Body, Lagrangian, KanesMethod, LagrangesMethod,
|
| 2 |
+
RigidBody, Particle)
|
| 3 |
+
from sympy.physics.mechanics.body_base import BodyBase
|
| 4 |
+
from sympy.physics.mechanics.method import _Methods
|
| 5 |
+
from sympy import Matrix
|
| 6 |
+
from sympy.utilities.exceptions import sympy_deprecation_warning
|
| 7 |
+
|
| 8 |
+
__all__ = ['JointsMethod']
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class JointsMethod(_Methods):
|
| 12 |
+
"""Method for formulating the equations of motion using a set of interconnected bodies with joints.
|
| 13 |
+
|
| 14 |
+
.. deprecated:: 1.13
|
| 15 |
+
The JointsMethod class is deprecated. Its functionality has been
|
| 16 |
+
replaced by the new :class:`~.System` class.
|
| 17 |
+
|
| 18 |
+
Parameters
|
| 19 |
+
==========
|
| 20 |
+
|
| 21 |
+
newtonion : Body or ReferenceFrame
|
| 22 |
+
The newtonion(inertial) frame.
|
| 23 |
+
*joints : Joint
|
| 24 |
+
The joints in the system
|
| 25 |
+
|
| 26 |
+
Attributes
|
| 27 |
+
==========
|
| 28 |
+
|
| 29 |
+
q, u : iterable
|
| 30 |
+
Iterable of the generalized coordinates and speeds
|
| 31 |
+
bodies : iterable
|
| 32 |
+
Iterable of Body objects in the system.
|
| 33 |
+
loads : iterable
|
| 34 |
+
Iterable of (Point, vector) or (ReferenceFrame, vector) tuples
|
| 35 |
+
describing the forces on the system.
|
| 36 |
+
mass_matrix : Matrix, shape(n, n)
|
| 37 |
+
The system's mass matrix
|
| 38 |
+
forcing : Matrix, shape(n, 1)
|
| 39 |
+
The system's forcing vector
|
| 40 |
+
mass_matrix_full : Matrix, shape(2*n, 2*n)
|
| 41 |
+
The "mass matrix" for the u's and q's
|
| 42 |
+
forcing_full : Matrix, shape(2*n, 1)
|
| 43 |
+
The "forcing vector" for the u's and q's
|
| 44 |
+
method : KanesMethod or Lagrange's method
|
| 45 |
+
Method's object.
|
| 46 |
+
kdes : iterable
|
| 47 |
+
Iterable of kde in they system.
|
| 48 |
+
|
| 49 |
+
Examples
|
| 50 |
+
========
|
| 51 |
+
|
| 52 |
+
As Body and JointsMethod have been deprecated, the following examples are
|
| 53 |
+
for illustrative purposes only. The functionality of Body is fully captured
|
| 54 |
+
by :class:`~.RigidBody` and :class:`~.Particle` and the functionality of
|
| 55 |
+
JointsMethod is fully captured by :class:`~.System`. To ignore the
|
| 56 |
+
deprecation warning we can use the ignore_warnings context manager.
|
| 57 |
+
|
| 58 |
+
>>> from sympy.utilities.exceptions import ignore_warnings
|
| 59 |
+
|
| 60 |
+
This is a simple example for a one degree of freedom translational
|
| 61 |
+
spring-mass-damper.
|
| 62 |
+
|
| 63 |
+
>>> from sympy import symbols
|
| 64 |
+
>>> from sympy.physics.mechanics import Body, JointsMethod, PrismaticJoint
|
| 65 |
+
>>> from sympy.physics.vector import dynamicsymbols
|
| 66 |
+
>>> c, k = symbols('c k')
|
| 67 |
+
>>> x, v = dynamicsymbols('x v')
|
| 68 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 69 |
+
... wall = Body('W')
|
| 70 |
+
... body = Body('B')
|
| 71 |
+
>>> J = PrismaticJoint('J', wall, body, coordinates=x, speeds=v)
|
| 72 |
+
>>> wall.apply_force(c*v*wall.x, reaction_body=body)
|
| 73 |
+
>>> wall.apply_force(k*x*wall.x, reaction_body=body)
|
| 74 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 75 |
+
... method = JointsMethod(wall, J)
|
| 76 |
+
>>> method.form_eoms()
|
| 77 |
+
Matrix([[-B_mass*Derivative(v(t), t) - c*v(t) - k*x(t)]])
|
| 78 |
+
>>> M = method.mass_matrix_full
|
| 79 |
+
>>> F = method.forcing_full
|
| 80 |
+
>>> rhs = M.LUsolve(F)
|
| 81 |
+
>>> rhs
|
| 82 |
+
Matrix([
|
| 83 |
+
[ v(t)],
|
| 84 |
+
[(-c*v(t) - k*x(t))/B_mass]])
|
| 85 |
+
|
| 86 |
+
Notes
|
| 87 |
+
=====
|
| 88 |
+
|
| 89 |
+
``JointsMethod`` currently only works with systems that do not have any
|
| 90 |
+
configuration or motion constraints.
|
| 91 |
+
|
| 92 |
+
"""
|
| 93 |
+
|
| 94 |
+
def __init__(self, newtonion, *joints):
|
| 95 |
+
sympy_deprecation_warning(
|
| 96 |
+
"""
|
| 97 |
+
The JointsMethod class is deprecated.
|
| 98 |
+
Its functionality has been replaced by the new System class.
|
| 99 |
+
""",
|
| 100 |
+
deprecated_since_version="1.13",
|
| 101 |
+
active_deprecations_target="deprecated-mechanics-jointsmethod"
|
| 102 |
+
)
|
| 103 |
+
if isinstance(newtonion, BodyBase):
|
| 104 |
+
self.frame = newtonion.frame
|
| 105 |
+
else:
|
| 106 |
+
self.frame = newtonion
|
| 107 |
+
|
| 108 |
+
self._joints = joints
|
| 109 |
+
self._bodies = self._generate_bodylist()
|
| 110 |
+
self._loads = self._generate_loadlist()
|
| 111 |
+
self._q = self._generate_q()
|
| 112 |
+
self._u = self._generate_u()
|
| 113 |
+
self._kdes = self._generate_kdes()
|
| 114 |
+
|
| 115 |
+
self._method = None
|
| 116 |
+
|
| 117 |
+
@property
|
| 118 |
+
def bodies(self):
|
| 119 |
+
"""List of bodies in they system."""
|
| 120 |
+
return self._bodies
|
| 121 |
+
|
| 122 |
+
@property
|
| 123 |
+
def loads(self):
|
| 124 |
+
"""List of loads on the system."""
|
| 125 |
+
return self._loads
|
| 126 |
+
|
| 127 |
+
@property
|
| 128 |
+
def q(self):
|
| 129 |
+
"""List of the generalized coordinates."""
|
| 130 |
+
return self._q
|
| 131 |
+
|
| 132 |
+
@property
|
| 133 |
+
def u(self):
|
| 134 |
+
"""List of the generalized speeds."""
|
| 135 |
+
return self._u
|
| 136 |
+
|
| 137 |
+
@property
|
| 138 |
+
def kdes(self):
|
| 139 |
+
"""List of the generalized coordinates."""
|
| 140 |
+
return self._kdes
|
| 141 |
+
|
| 142 |
+
@property
|
| 143 |
+
def forcing_full(self):
|
| 144 |
+
"""The "forcing vector" for the u's and q's."""
|
| 145 |
+
return self.method.forcing_full
|
| 146 |
+
|
| 147 |
+
@property
|
| 148 |
+
def mass_matrix_full(self):
|
| 149 |
+
"""The "mass matrix" for the u's and q's."""
|
| 150 |
+
return self.method.mass_matrix_full
|
| 151 |
+
|
| 152 |
+
@property
|
| 153 |
+
def mass_matrix(self):
|
| 154 |
+
"""The system's mass matrix."""
|
| 155 |
+
return self.method.mass_matrix
|
| 156 |
+
|
| 157 |
+
@property
|
| 158 |
+
def forcing(self):
|
| 159 |
+
"""The system's forcing vector."""
|
| 160 |
+
return self.method.forcing
|
| 161 |
+
|
| 162 |
+
@property
|
| 163 |
+
def method(self):
|
| 164 |
+
"""Object of method used to form equations of systems."""
|
| 165 |
+
return self._method
|
| 166 |
+
|
| 167 |
+
def _generate_bodylist(self):
|
| 168 |
+
bodies = []
|
| 169 |
+
for joint in self._joints:
|
| 170 |
+
if joint.child not in bodies:
|
| 171 |
+
bodies.append(joint.child)
|
| 172 |
+
if joint.parent not in bodies:
|
| 173 |
+
bodies.append(joint.parent)
|
| 174 |
+
return bodies
|
| 175 |
+
|
| 176 |
+
def _generate_loadlist(self):
|
| 177 |
+
load_list = []
|
| 178 |
+
for body in self.bodies:
|
| 179 |
+
if isinstance(body, Body):
|
| 180 |
+
load_list.extend(body.loads)
|
| 181 |
+
return load_list
|
| 182 |
+
|
| 183 |
+
def _generate_q(self):
|
| 184 |
+
q_ind = []
|
| 185 |
+
for joint in self._joints:
|
| 186 |
+
for coordinate in joint.coordinates:
|
| 187 |
+
if coordinate in q_ind:
|
| 188 |
+
raise ValueError('Coordinates of joints should be unique.')
|
| 189 |
+
q_ind.append(coordinate)
|
| 190 |
+
return Matrix(q_ind)
|
| 191 |
+
|
| 192 |
+
def _generate_u(self):
|
| 193 |
+
u_ind = []
|
| 194 |
+
for joint in self._joints:
|
| 195 |
+
for speed in joint.speeds:
|
| 196 |
+
if speed in u_ind:
|
| 197 |
+
raise ValueError('Speeds of joints should be unique.')
|
| 198 |
+
u_ind.append(speed)
|
| 199 |
+
return Matrix(u_ind)
|
| 200 |
+
|
| 201 |
+
def _generate_kdes(self):
|
| 202 |
+
kd_ind = Matrix(1, 0, []).T
|
| 203 |
+
for joint in self._joints:
|
| 204 |
+
kd_ind = kd_ind.col_join(joint.kdes)
|
| 205 |
+
return kd_ind
|
| 206 |
+
|
| 207 |
+
def _convert_bodies(self):
|
| 208 |
+
# Convert `Body` to `Particle` and `RigidBody`
|
| 209 |
+
bodylist = []
|
| 210 |
+
for body in self.bodies:
|
| 211 |
+
if not isinstance(body, Body):
|
| 212 |
+
bodylist.append(body)
|
| 213 |
+
continue
|
| 214 |
+
if body.is_rigidbody:
|
| 215 |
+
rb = RigidBody(body.name, body.masscenter, body.frame, body.mass,
|
| 216 |
+
(body.central_inertia, body.masscenter))
|
| 217 |
+
rb.potential_energy = body.potential_energy
|
| 218 |
+
bodylist.append(rb)
|
| 219 |
+
else:
|
| 220 |
+
part = Particle(body.name, body.masscenter, body.mass)
|
| 221 |
+
part.potential_energy = body.potential_energy
|
| 222 |
+
bodylist.append(part)
|
| 223 |
+
return bodylist
|
| 224 |
+
|
| 225 |
+
def form_eoms(self, method=KanesMethod):
|
| 226 |
+
"""Method to form system's equation of motions.
|
| 227 |
+
|
| 228 |
+
Parameters
|
| 229 |
+
==========
|
| 230 |
+
|
| 231 |
+
method : Class
|
| 232 |
+
Class name of method.
|
| 233 |
+
|
| 234 |
+
Returns
|
| 235 |
+
========
|
| 236 |
+
|
| 237 |
+
Matrix
|
| 238 |
+
Vector of equations of motions.
|
| 239 |
+
|
| 240 |
+
Examples
|
| 241 |
+
========
|
| 242 |
+
|
| 243 |
+
As Body and JointsMethod have been deprecated, the following examples
|
| 244 |
+
are for illustrative purposes only. The functionality of Body is fully
|
| 245 |
+
captured by :class:`~.RigidBody` and :class:`~.Particle` and the
|
| 246 |
+
functionality of JointsMethod is fully captured by :class:`~.System`. To
|
| 247 |
+
ignore the deprecation warning we can use the ignore_warnings context
|
| 248 |
+
manager.
|
| 249 |
+
|
| 250 |
+
>>> from sympy.utilities.exceptions import ignore_warnings
|
| 251 |
+
|
| 252 |
+
This is a simple example for a one degree of freedom translational
|
| 253 |
+
spring-mass-damper.
|
| 254 |
+
|
| 255 |
+
>>> from sympy import S, symbols
|
| 256 |
+
>>> from sympy.physics.mechanics import LagrangesMethod, dynamicsymbols, Body
|
| 257 |
+
>>> from sympy.physics.mechanics import PrismaticJoint, JointsMethod
|
| 258 |
+
>>> q = dynamicsymbols('q')
|
| 259 |
+
>>> qd = dynamicsymbols('q', 1)
|
| 260 |
+
>>> m, k, b = symbols('m k b')
|
| 261 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 262 |
+
... wall = Body('W')
|
| 263 |
+
... part = Body('P', mass=m)
|
| 264 |
+
>>> part.potential_energy = k * q**2 / S(2)
|
| 265 |
+
>>> J = PrismaticJoint('J', wall, part, coordinates=q, speeds=qd)
|
| 266 |
+
>>> wall.apply_force(b * qd * wall.x, reaction_body=part)
|
| 267 |
+
>>> with ignore_warnings(DeprecationWarning):
|
| 268 |
+
... method = JointsMethod(wall, J)
|
| 269 |
+
>>> method.form_eoms(LagrangesMethod)
|
| 270 |
+
Matrix([[b*Derivative(q(t), t) + k*q(t) + m*Derivative(q(t), (t, 2))]])
|
| 271 |
+
|
| 272 |
+
We can also solve for the states using the 'rhs' method.
|
| 273 |
+
|
| 274 |
+
>>> method.rhs()
|
| 275 |
+
Matrix([
|
| 276 |
+
[ Derivative(q(t), t)],
|
| 277 |
+
[(-b*Derivative(q(t), t) - k*q(t))/m]])
|
| 278 |
+
|
| 279 |
+
"""
|
| 280 |
+
|
| 281 |
+
bodylist = self._convert_bodies()
|
| 282 |
+
if issubclass(method, LagrangesMethod): #LagrangesMethod or similar
|
| 283 |
+
L = Lagrangian(self.frame, *bodylist)
|
| 284 |
+
self._method = method(L, self.q, self.loads, bodylist, self.frame)
|
| 285 |
+
else: #KanesMethod or similar
|
| 286 |
+
self._method = method(self.frame, q_ind=self.q, u_ind=self.u, kd_eqs=self.kdes,
|
| 287 |
+
forcelist=self.loads, bodies=bodylist)
|
| 288 |
+
soln = self.method._form_eoms()
|
| 289 |
+
return soln
|
| 290 |
+
|
| 291 |
+
def rhs(self, inv_method=None):
|
| 292 |
+
"""Returns equations that can be solved numerically.
|
| 293 |
+
|
| 294 |
+
Parameters
|
| 295 |
+
==========
|
| 296 |
+
|
| 297 |
+
inv_method : str
|
| 298 |
+
The specific sympy inverse matrix calculation method to use. For a
|
| 299 |
+
list of valid methods, see
|
| 300 |
+
:meth:`~sympy.matrices.matrixbase.MatrixBase.inv`
|
| 301 |
+
|
| 302 |
+
Returns
|
| 303 |
+
========
|
| 304 |
+
|
| 305 |
+
Matrix
|
| 306 |
+
Numerically solvable equations.
|
| 307 |
+
|
| 308 |
+
See Also
|
| 309 |
+
========
|
| 310 |
+
|
| 311 |
+
sympy.physics.mechanics.kane.KanesMethod.rhs:
|
| 312 |
+
KanesMethod's rhs function.
|
| 313 |
+
sympy.physics.mechanics.lagrange.LagrangesMethod.rhs:
|
| 314 |
+
LagrangesMethod's rhs function.
|
| 315 |
+
|
| 316 |
+
"""
|
| 317 |
+
|
| 318 |
+
return self.method.rhs(inv_method=inv_method)
|
lib/python3.12/site-packages/sympy/physics/mechanics/kane.py
ADDED
|
@@ -0,0 +1,860 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import zeros, Matrix, diff, eye
|
| 2 |
+
from sympy.core.sorting import default_sort_key
|
| 3 |
+
from sympy.physics.vector import (ReferenceFrame, dynamicsymbols,
|
| 4 |
+
partial_velocity)
|
| 5 |
+
from sympy.physics.mechanics.method import _Methods
|
| 6 |
+
from sympy.physics.mechanics.particle import Particle
|
| 7 |
+
from sympy.physics.mechanics.rigidbody import RigidBody
|
| 8 |
+
from sympy.physics.mechanics.functions import (msubs, find_dynamicsymbols,
|
| 9 |
+
_f_list_parser,
|
| 10 |
+
_validate_coordinates,
|
| 11 |
+
_parse_linear_solver)
|
| 12 |
+
from sympy.physics.mechanics.linearize import Linearizer
|
| 13 |
+
from sympy.utilities.iterables import iterable
|
| 14 |
+
|
| 15 |
+
__all__ = ['KanesMethod']
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class KanesMethod(_Methods):
|
| 19 |
+
r"""Kane's method object.
|
| 20 |
+
|
| 21 |
+
Explanation
|
| 22 |
+
===========
|
| 23 |
+
|
| 24 |
+
This object is used to do the "book-keeping" as you go through and form
|
| 25 |
+
equations of motion in the way Kane presents in:
|
| 26 |
+
Kane, T., Levinson, D. Dynamics Theory and Applications. 1985 McGraw-Hill
|
| 27 |
+
|
| 28 |
+
The attributes are for equations in the form [M] udot = forcing.
|
| 29 |
+
|
| 30 |
+
Attributes
|
| 31 |
+
==========
|
| 32 |
+
|
| 33 |
+
q, u : Matrix
|
| 34 |
+
Matrices of the generalized coordinates and speeds
|
| 35 |
+
bodies : iterable
|
| 36 |
+
Iterable of Particle and RigidBody objects in the system.
|
| 37 |
+
loads : iterable
|
| 38 |
+
Iterable of (Point, vector) or (ReferenceFrame, vector) tuples
|
| 39 |
+
describing the forces on the system.
|
| 40 |
+
auxiliary_eqs : Matrix
|
| 41 |
+
If applicable, the set of auxiliary Kane's
|
| 42 |
+
equations used to solve for non-contributing
|
| 43 |
+
forces.
|
| 44 |
+
mass_matrix : Matrix
|
| 45 |
+
The system's dynamics mass matrix: [k_d; k_dnh]
|
| 46 |
+
forcing : Matrix
|
| 47 |
+
The system's dynamics forcing vector: -[f_d; f_dnh]
|
| 48 |
+
mass_matrix_kin : Matrix
|
| 49 |
+
The "mass matrix" for kinematic differential equations: k_kqdot
|
| 50 |
+
forcing_kin : Matrix
|
| 51 |
+
The forcing vector for kinematic differential equations: -(k_ku*u + f_k)
|
| 52 |
+
mass_matrix_full : Matrix
|
| 53 |
+
The "mass matrix" for the u's and q's with dynamics and kinematics
|
| 54 |
+
forcing_full : Matrix
|
| 55 |
+
The "forcing vector" for the u's and q's with dynamics and kinematics
|
| 56 |
+
|
| 57 |
+
Parameters
|
| 58 |
+
==========
|
| 59 |
+
|
| 60 |
+
frame : ReferenceFrame
|
| 61 |
+
The inertial reference frame for the system.
|
| 62 |
+
q_ind : iterable of dynamicsymbols
|
| 63 |
+
Independent generalized coordinates.
|
| 64 |
+
u_ind : iterable of dynamicsymbols
|
| 65 |
+
Independent generalized speeds.
|
| 66 |
+
kd_eqs : iterable of Expr, optional
|
| 67 |
+
Kinematic differential equations, which linearly relate the generalized
|
| 68 |
+
speeds to the time-derivatives of the generalized coordinates.
|
| 69 |
+
q_dependent : iterable of dynamicsymbols, optional
|
| 70 |
+
Dependent generalized coordinates.
|
| 71 |
+
configuration_constraints : iterable of Expr, optional
|
| 72 |
+
Constraints on the system's configuration, i.e. holonomic constraints.
|
| 73 |
+
u_dependent : iterable of dynamicsymbols, optional
|
| 74 |
+
Dependent generalized speeds.
|
| 75 |
+
velocity_constraints : iterable of Expr, optional
|
| 76 |
+
Constraints on the system's velocity, i.e. the combination of the
|
| 77 |
+
nonholonomic constraints and the time-derivative of the holonomic
|
| 78 |
+
constraints.
|
| 79 |
+
acceleration_constraints : iterable of Expr, optional
|
| 80 |
+
Constraints on the system's acceleration, by default these are the
|
| 81 |
+
time-derivative of the velocity constraints.
|
| 82 |
+
u_auxiliary : iterable of dynamicsymbols, optional
|
| 83 |
+
Auxiliary generalized speeds.
|
| 84 |
+
bodies : iterable of Particle and/or RigidBody, optional
|
| 85 |
+
The particles and rigid bodies in the system.
|
| 86 |
+
forcelist : iterable of tuple[Point | ReferenceFrame, Vector], optional
|
| 87 |
+
Forces and torques applied on the system.
|
| 88 |
+
explicit_kinematics : bool
|
| 89 |
+
Boolean whether the mass matrices and forcing vectors should use the
|
| 90 |
+
explicit form (default) or implicit form for kinematics.
|
| 91 |
+
See the notes for more details.
|
| 92 |
+
kd_eqs_solver : str, callable
|
| 93 |
+
Method used to solve the kinematic differential equations. If a string
|
| 94 |
+
is supplied, it should be a valid method that can be used with the
|
| 95 |
+
:meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is
|
| 96 |
+
supplied, it should have the format ``f(A, rhs)``, where it solves the
|
| 97 |
+
equations and returns the solution. The default utilizes LU solve. See
|
| 98 |
+
the notes for more information.
|
| 99 |
+
constraint_solver : str, callable
|
| 100 |
+
Method used to solve the velocity constraints. If a string is
|
| 101 |
+
supplied, it should be a valid method that can be used with the
|
| 102 |
+
:meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is
|
| 103 |
+
supplied, it should have the format ``f(A, rhs)``, where it solves the
|
| 104 |
+
equations and returns the solution. The default utilizes LU solve. See
|
| 105 |
+
the notes for more information.
|
| 106 |
+
|
| 107 |
+
Notes
|
| 108 |
+
=====
|
| 109 |
+
|
| 110 |
+
The mass matrices and forcing vectors related to kinematic equations
|
| 111 |
+
are given in the explicit form by default. In other words, the kinematic
|
| 112 |
+
mass matrix is $\mathbf{k_{k\dot{q}}} = \mathbf{I}$.
|
| 113 |
+
In order to get the implicit form of those matrices/vectors, you can set the
|
| 114 |
+
``explicit_kinematics`` attribute to ``False``. So $\mathbf{k_{k\dot{q}}}$
|
| 115 |
+
is not necessarily an identity matrix. This can provide more compact
|
| 116 |
+
equations for non-simple kinematics.
|
| 117 |
+
|
| 118 |
+
Two linear solvers can be supplied to ``KanesMethod``: one for solving the
|
| 119 |
+
kinematic differential equations and one to solve the velocity constraints.
|
| 120 |
+
Both of these sets of equations can be expressed as a linear system ``Ax = rhs``,
|
| 121 |
+
which have to be solved in order to obtain the equations of motion.
|
| 122 |
+
|
| 123 |
+
The default solver ``'LU'``, which stands for LU solve, results relatively low
|
| 124 |
+
number of operations. The weakness of this method is that it can result in zero
|
| 125 |
+
division errors.
|
| 126 |
+
|
| 127 |
+
If zero divisions are encountered, a possible solver which may solve the problem
|
| 128 |
+
is ``"CRAMER"``. This method uses Cramer's rule to solve the system. This method
|
| 129 |
+
is slower and results in more operations than the default solver. However it only
|
| 130 |
+
uses a single division by default per entry of the solution.
|
| 131 |
+
|
| 132 |
+
While a valid list of solvers can be found at
|
| 133 |
+
:meth:`sympy.matrices.matrixbase.MatrixBase.solve`, it is also possible to supply a
|
| 134 |
+
`callable`. This way it is possible to use a different solver routine. If the
|
| 135 |
+
kinematic differential equations are not too complex it can be worth it to simplify
|
| 136 |
+
the solution by using ``lambda A, b: simplify(Matrix.LUsolve(A, b))``. Another
|
| 137 |
+
option solver one may use is :func:`sympy.solvers.solveset.linsolve`. This can be
|
| 138 |
+
done using `lambda A, b: tuple(linsolve((A, b)))[0]`, where we select the first
|
| 139 |
+
solution as our system should have only one unique solution.
|
| 140 |
+
|
| 141 |
+
Examples
|
| 142 |
+
========
|
| 143 |
+
|
| 144 |
+
This is a simple example for a one degree of freedom translational
|
| 145 |
+
spring-mass-damper.
|
| 146 |
+
|
| 147 |
+
In this example, we first need to do the kinematics.
|
| 148 |
+
This involves creating generalized speeds and coordinates and their
|
| 149 |
+
derivatives.
|
| 150 |
+
Then we create a point and set its velocity in a frame.
|
| 151 |
+
|
| 152 |
+
>>> from sympy import symbols
|
| 153 |
+
>>> from sympy.physics.mechanics import dynamicsymbols, ReferenceFrame
|
| 154 |
+
>>> from sympy.physics.mechanics import Point, Particle, KanesMethod
|
| 155 |
+
>>> q, u = dynamicsymbols('q u')
|
| 156 |
+
>>> qd, ud = dynamicsymbols('q u', 1)
|
| 157 |
+
>>> m, c, k = symbols('m c k')
|
| 158 |
+
>>> N = ReferenceFrame('N')
|
| 159 |
+
>>> P = Point('P')
|
| 160 |
+
>>> P.set_vel(N, u * N.x)
|
| 161 |
+
|
| 162 |
+
Next we need to arrange/store information in the way that KanesMethod
|
| 163 |
+
requires. The kinematic differential equations should be an iterable of
|
| 164 |
+
expressions. A list of forces/torques must be constructed, where each entry
|
| 165 |
+
in the list is a (Point, Vector) or (ReferenceFrame, Vector) tuple, where
|
| 166 |
+
the Vectors represent the Force or Torque.
|
| 167 |
+
Next a particle needs to be created, and it needs to have a point and mass
|
| 168 |
+
assigned to it.
|
| 169 |
+
Finally, a list of all bodies and particles needs to be created.
|
| 170 |
+
|
| 171 |
+
>>> kd = [qd - u]
|
| 172 |
+
>>> FL = [(P, (-k * q - c * u) * N.x)]
|
| 173 |
+
>>> pa = Particle('pa', P, m)
|
| 174 |
+
>>> BL = [pa]
|
| 175 |
+
|
| 176 |
+
Finally we can generate the equations of motion.
|
| 177 |
+
First we create the KanesMethod object and supply an inertial frame,
|
| 178 |
+
coordinates, generalized speeds, and the kinematic differential equations.
|
| 179 |
+
Additional quantities such as configuration and motion constraints,
|
| 180 |
+
dependent coordinates and speeds, and auxiliary speeds are also supplied
|
| 181 |
+
here (see the online documentation).
|
| 182 |
+
Next we form FR* and FR to complete: Fr + Fr* = 0.
|
| 183 |
+
We have the equations of motion at this point.
|
| 184 |
+
It makes sense to rearrange them though, so we calculate the mass matrix and
|
| 185 |
+
the forcing terms, for E.o.M. in the form: [MM] udot = forcing, where MM is
|
| 186 |
+
the mass matrix, udot is a vector of the time derivatives of the
|
| 187 |
+
generalized speeds, and forcing is a vector representing "forcing" terms.
|
| 188 |
+
|
| 189 |
+
>>> KM = KanesMethod(N, q_ind=[q], u_ind=[u], kd_eqs=kd)
|
| 190 |
+
>>> (fr, frstar) = KM.kanes_equations(BL, FL)
|
| 191 |
+
>>> MM = KM.mass_matrix
|
| 192 |
+
>>> forcing = KM.forcing
|
| 193 |
+
>>> rhs = MM.inv() * forcing
|
| 194 |
+
>>> rhs
|
| 195 |
+
Matrix([[(-c*u(t) - k*q(t))/m]])
|
| 196 |
+
>>> KM.linearize(A_and_B=True)[0]
|
| 197 |
+
Matrix([
|
| 198 |
+
[ 0, 1],
|
| 199 |
+
[-k/m, -c/m]])
|
| 200 |
+
|
| 201 |
+
Please look at the documentation pages for more information on how to
|
| 202 |
+
perform linearization and how to deal with dependent coordinates & speeds,
|
| 203 |
+
and how do deal with bringing non-contributing forces into evidence.
|
| 204 |
+
|
| 205 |
+
"""
|
| 206 |
+
|
| 207 |
+
def __init__(self, frame, q_ind, u_ind, kd_eqs=None, q_dependent=None,
|
| 208 |
+
configuration_constraints=None, u_dependent=None,
|
| 209 |
+
velocity_constraints=None, acceleration_constraints=None,
|
| 210 |
+
u_auxiliary=None, bodies=None, forcelist=None,
|
| 211 |
+
explicit_kinematics=True, kd_eqs_solver='LU',
|
| 212 |
+
constraint_solver='LU'):
|
| 213 |
+
|
| 214 |
+
"""Please read the online documentation. """
|
| 215 |
+
if not q_ind:
|
| 216 |
+
q_ind = [dynamicsymbols('dummy_q')]
|
| 217 |
+
kd_eqs = [dynamicsymbols('dummy_kd')]
|
| 218 |
+
|
| 219 |
+
if not isinstance(frame, ReferenceFrame):
|
| 220 |
+
raise TypeError('An inertial ReferenceFrame must be supplied')
|
| 221 |
+
self._inertial = frame
|
| 222 |
+
|
| 223 |
+
self._fr = None
|
| 224 |
+
self._frstar = None
|
| 225 |
+
|
| 226 |
+
self._forcelist = forcelist
|
| 227 |
+
self._bodylist = bodies
|
| 228 |
+
|
| 229 |
+
self.explicit_kinematics = explicit_kinematics
|
| 230 |
+
self._constraint_solver = constraint_solver
|
| 231 |
+
self._initialize_vectors(q_ind, q_dependent, u_ind, u_dependent,
|
| 232 |
+
u_auxiliary)
|
| 233 |
+
_validate_coordinates(self.q, self.u)
|
| 234 |
+
self._initialize_kindiffeq_matrices(kd_eqs, kd_eqs_solver)
|
| 235 |
+
self._initialize_constraint_matrices(
|
| 236 |
+
configuration_constraints, velocity_constraints,
|
| 237 |
+
acceleration_constraints, constraint_solver)
|
| 238 |
+
|
| 239 |
+
def _initialize_vectors(self, q_ind, q_dep, u_ind, u_dep, u_aux):
|
| 240 |
+
"""Initialize the coordinate and speed vectors."""
|
| 241 |
+
|
| 242 |
+
none_handler = lambda x: Matrix(x) if x else Matrix()
|
| 243 |
+
|
| 244 |
+
# Initialize generalized coordinates
|
| 245 |
+
q_dep = none_handler(q_dep)
|
| 246 |
+
if not iterable(q_ind):
|
| 247 |
+
raise TypeError('Generalized coordinates must be an iterable.')
|
| 248 |
+
if not iterable(q_dep):
|
| 249 |
+
raise TypeError('Dependent coordinates must be an iterable.')
|
| 250 |
+
q_ind = Matrix(q_ind)
|
| 251 |
+
self._qdep = q_dep
|
| 252 |
+
self._q = Matrix([q_ind, q_dep])
|
| 253 |
+
self._qdot = self.q.diff(dynamicsymbols._t)
|
| 254 |
+
|
| 255 |
+
# Initialize generalized speeds
|
| 256 |
+
u_dep = none_handler(u_dep)
|
| 257 |
+
if not iterable(u_ind):
|
| 258 |
+
raise TypeError('Generalized speeds must be an iterable.')
|
| 259 |
+
if not iterable(u_dep):
|
| 260 |
+
raise TypeError('Dependent speeds must be an iterable.')
|
| 261 |
+
u_ind = Matrix(u_ind)
|
| 262 |
+
self._udep = u_dep
|
| 263 |
+
self._u = Matrix([u_ind, u_dep])
|
| 264 |
+
self._udot = self.u.diff(dynamicsymbols._t)
|
| 265 |
+
self._uaux = none_handler(u_aux)
|
| 266 |
+
|
| 267 |
+
def _initialize_constraint_matrices(self, config, vel, acc, linear_solver='LU'):
|
| 268 |
+
"""Initializes constraint matrices."""
|
| 269 |
+
linear_solver = _parse_linear_solver(linear_solver)
|
| 270 |
+
# Define vector dimensions
|
| 271 |
+
o = len(self.u)
|
| 272 |
+
m = len(self._udep)
|
| 273 |
+
p = o - m
|
| 274 |
+
none_handler = lambda x: Matrix(x) if x else Matrix()
|
| 275 |
+
|
| 276 |
+
# Initialize configuration constraints
|
| 277 |
+
config = none_handler(config)
|
| 278 |
+
if len(self._qdep) != len(config):
|
| 279 |
+
raise ValueError('There must be an equal number of dependent '
|
| 280 |
+
'coordinates and configuration constraints.')
|
| 281 |
+
self._f_h = none_handler(config)
|
| 282 |
+
|
| 283 |
+
# Initialize velocity and acceleration constraints
|
| 284 |
+
vel = none_handler(vel)
|
| 285 |
+
acc = none_handler(acc)
|
| 286 |
+
if len(vel) != m:
|
| 287 |
+
raise ValueError('There must be an equal number of dependent '
|
| 288 |
+
'speeds and velocity constraints.')
|
| 289 |
+
if acc and (len(acc) != m):
|
| 290 |
+
raise ValueError('There must be an equal number of dependent '
|
| 291 |
+
'speeds and acceleration constraints.')
|
| 292 |
+
if vel:
|
| 293 |
+
u_zero = dict.fromkeys(self.u, 0)
|
| 294 |
+
udot_zero = dict.fromkeys(self._udot, 0)
|
| 295 |
+
|
| 296 |
+
# When calling kanes_equations, another class instance will be
|
| 297 |
+
# created if auxiliary u's are present. In this case, the
|
| 298 |
+
# computation of kinetic differential equation matrices will be
|
| 299 |
+
# skipped as this was computed during the original KanesMethod
|
| 300 |
+
# object, and the qd_u_map will not be available.
|
| 301 |
+
if self._qdot_u_map is not None:
|
| 302 |
+
vel = msubs(vel, self._qdot_u_map)
|
| 303 |
+
|
| 304 |
+
self._f_nh = msubs(vel, u_zero)
|
| 305 |
+
self._k_nh = (vel - self._f_nh).jacobian(self.u)
|
| 306 |
+
# If no acceleration constraints given, calculate them.
|
| 307 |
+
if not acc:
|
| 308 |
+
_f_dnh = (self._k_nh.diff(dynamicsymbols._t) * self.u +
|
| 309 |
+
self._f_nh.diff(dynamicsymbols._t))
|
| 310 |
+
if self._qdot_u_map is not None:
|
| 311 |
+
_f_dnh = msubs(_f_dnh, self._qdot_u_map)
|
| 312 |
+
self._f_dnh = _f_dnh
|
| 313 |
+
self._k_dnh = self._k_nh
|
| 314 |
+
else:
|
| 315 |
+
if self._qdot_u_map is not None:
|
| 316 |
+
acc = msubs(acc, self._qdot_u_map)
|
| 317 |
+
self._f_dnh = msubs(acc, udot_zero)
|
| 318 |
+
self._k_dnh = (acc - self._f_dnh).jacobian(self._udot)
|
| 319 |
+
|
| 320 |
+
# Form of non-holonomic constraints is B*u + C = 0.
|
| 321 |
+
# We partition B into independent and dependent columns:
|
| 322 |
+
# Ars is then -B_dep.inv() * B_ind, and it relates dependent speeds
|
| 323 |
+
# to independent speeds as: udep = Ars*uind, neglecting the C term.
|
| 324 |
+
B_ind = self._k_nh[:, :p]
|
| 325 |
+
B_dep = self._k_nh[:, p:o]
|
| 326 |
+
self._Ars = -linear_solver(B_dep, B_ind)
|
| 327 |
+
else:
|
| 328 |
+
self._f_nh = Matrix()
|
| 329 |
+
self._k_nh = Matrix()
|
| 330 |
+
self._f_dnh = Matrix()
|
| 331 |
+
self._k_dnh = Matrix()
|
| 332 |
+
self._Ars = Matrix()
|
| 333 |
+
|
| 334 |
+
def _initialize_kindiffeq_matrices(self, kdeqs, linear_solver='LU'):
|
| 335 |
+
"""Initialize the kinematic differential equation matrices.
|
| 336 |
+
|
| 337 |
+
Parameters
|
| 338 |
+
==========
|
| 339 |
+
kdeqs : sequence of sympy expressions
|
| 340 |
+
Kinematic differential equations in the form of f(u,q',q,t) where
|
| 341 |
+
f() = 0. The equations have to be linear in the generalized
|
| 342 |
+
coordinates and generalized speeds.
|
| 343 |
+
|
| 344 |
+
"""
|
| 345 |
+
linear_solver = _parse_linear_solver(linear_solver)
|
| 346 |
+
if kdeqs:
|
| 347 |
+
if len(self.q) != len(kdeqs):
|
| 348 |
+
raise ValueError('There must be an equal number of kinematic '
|
| 349 |
+
'differential equations and coordinates.')
|
| 350 |
+
|
| 351 |
+
u = self.u
|
| 352 |
+
qdot = self._qdot
|
| 353 |
+
|
| 354 |
+
kdeqs = Matrix(kdeqs)
|
| 355 |
+
|
| 356 |
+
u_zero = dict.fromkeys(u, 0)
|
| 357 |
+
uaux_zero = dict.fromkeys(self._uaux, 0)
|
| 358 |
+
qdot_zero = dict.fromkeys(qdot, 0)
|
| 359 |
+
|
| 360 |
+
# Extract the linear coefficient matrices as per the following
|
| 361 |
+
# equation:
|
| 362 |
+
#
|
| 363 |
+
# k_ku(q,t)*u(t) + k_kqdot(q,t)*q'(t) + f_k(q,t) = 0
|
| 364 |
+
#
|
| 365 |
+
k_ku = kdeqs.jacobian(u)
|
| 366 |
+
k_kqdot = kdeqs.jacobian(qdot)
|
| 367 |
+
f_k = kdeqs.xreplace(u_zero).xreplace(qdot_zero)
|
| 368 |
+
|
| 369 |
+
# The kinematic differential equations should be linear in both q'
|
| 370 |
+
# and u, so check for u and q' in the components.
|
| 371 |
+
dy_syms = find_dynamicsymbols(k_ku.row_join(k_kqdot).row_join(f_k))
|
| 372 |
+
nonlin_vars = [vari for vari in u[:] + qdot[:] if vari in dy_syms]
|
| 373 |
+
if nonlin_vars:
|
| 374 |
+
msg = ('The provided kinematic differential equations are '
|
| 375 |
+
'nonlinear in {}. They must be linear in the '
|
| 376 |
+
'generalized speeds and derivatives of the generalized '
|
| 377 |
+
'coordinates.')
|
| 378 |
+
raise ValueError(msg.format(nonlin_vars))
|
| 379 |
+
|
| 380 |
+
self._f_k_implicit = f_k.xreplace(uaux_zero)
|
| 381 |
+
self._k_ku_implicit = k_ku.xreplace(uaux_zero)
|
| 382 |
+
self._k_kqdot_implicit = k_kqdot
|
| 383 |
+
|
| 384 |
+
# Solve for q'(t) such that the coefficient matrices are now in
|
| 385 |
+
# this form:
|
| 386 |
+
#
|
| 387 |
+
# k_kqdot^-1*k_ku*u(t) + I*q'(t) + k_kqdot^-1*f_k = 0
|
| 388 |
+
#
|
| 389 |
+
# NOTE : Solving the kinematic differential equations here is not
|
| 390 |
+
# necessary and prevents the equations from being provided in fully
|
| 391 |
+
# implicit form.
|
| 392 |
+
f_k_explicit = linear_solver(k_kqdot, f_k)
|
| 393 |
+
k_ku_explicit = linear_solver(k_kqdot, k_ku)
|
| 394 |
+
self._qdot_u_map = dict(zip(qdot, -(k_ku_explicit*u + f_k_explicit)))
|
| 395 |
+
|
| 396 |
+
self._f_k = f_k_explicit.xreplace(uaux_zero)
|
| 397 |
+
self._k_ku = k_ku_explicit.xreplace(uaux_zero)
|
| 398 |
+
self._k_kqdot = eye(len(qdot))
|
| 399 |
+
|
| 400 |
+
else:
|
| 401 |
+
self._qdot_u_map = None
|
| 402 |
+
self._f_k_implicit = self._f_k = Matrix()
|
| 403 |
+
self._k_ku_implicit = self._k_ku = Matrix()
|
| 404 |
+
self._k_kqdot_implicit = self._k_kqdot = Matrix()
|
| 405 |
+
|
| 406 |
+
def _form_fr(self, fl):
|
| 407 |
+
"""Form the generalized active force."""
|
| 408 |
+
if fl is not None and (len(fl) == 0 or not iterable(fl)):
|
| 409 |
+
raise ValueError('Force pairs must be supplied in an '
|
| 410 |
+
'non-empty iterable or None.')
|
| 411 |
+
|
| 412 |
+
N = self._inertial
|
| 413 |
+
# pull out relevant velocities for constructing partial velocities
|
| 414 |
+
vel_list, f_list = _f_list_parser(fl, N)
|
| 415 |
+
vel_list = [msubs(i, self._qdot_u_map) for i in vel_list]
|
| 416 |
+
f_list = [msubs(i, self._qdot_u_map) for i in f_list]
|
| 417 |
+
|
| 418 |
+
# Fill Fr with dot product of partial velocities and forces
|
| 419 |
+
o = len(self.u)
|
| 420 |
+
b = len(f_list)
|
| 421 |
+
FR = zeros(o, 1)
|
| 422 |
+
partials = partial_velocity(vel_list, self.u, N)
|
| 423 |
+
for i in range(o):
|
| 424 |
+
FR[i] = sum(partials[j][i].dot(f_list[j]) for j in range(b))
|
| 425 |
+
|
| 426 |
+
# In case there are dependent speeds
|
| 427 |
+
if self._udep:
|
| 428 |
+
p = o - len(self._udep)
|
| 429 |
+
FRtilde = FR[:p, 0]
|
| 430 |
+
FRold = FR[p:o, 0]
|
| 431 |
+
FRtilde += self._Ars.T * FRold
|
| 432 |
+
FR = FRtilde
|
| 433 |
+
|
| 434 |
+
self._forcelist = fl
|
| 435 |
+
self._fr = FR
|
| 436 |
+
return FR
|
| 437 |
+
|
| 438 |
+
def _form_frstar(self, bl):
|
| 439 |
+
"""Form the generalized inertia force."""
|
| 440 |
+
|
| 441 |
+
if not iterable(bl):
|
| 442 |
+
raise TypeError('Bodies must be supplied in an iterable.')
|
| 443 |
+
|
| 444 |
+
t = dynamicsymbols._t
|
| 445 |
+
N = self._inertial
|
| 446 |
+
# Dicts setting things to zero
|
| 447 |
+
udot_zero = dict.fromkeys(self._udot, 0)
|
| 448 |
+
uaux_zero = dict.fromkeys(self._uaux, 0)
|
| 449 |
+
uauxdot = [diff(i, t) for i in self._uaux]
|
| 450 |
+
uauxdot_zero = dict.fromkeys(uauxdot, 0)
|
| 451 |
+
# Dictionary of q' and q'' to u and u'
|
| 452 |
+
q_ddot_u_map = {k.diff(t): v.diff(t).xreplace(
|
| 453 |
+
self._qdot_u_map) for (k, v) in self._qdot_u_map.items()}
|
| 454 |
+
q_ddot_u_map.update(self._qdot_u_map)
|
| 455 |
+
|
| 456 |
+
# Fill up the list of partials: format is a list with num elements
|
| 457 |
+
# equal to number of entries in body list. Each of these elements is a
|
| 458 |
+
# list - either of length 1 for the translational components of
|
| 459 |
+
# particles or of length 2 for the translational and rotational
|
| 460 |
+
# components of rigid bodies. The inner most list is the list of
|
| 461 |
+
# partial velocities.
|
| 462 |
+
def get_partial_velocity(body):
|
| 463 |
+
if isinstance(body, RigidBody):
|
| 464 |
+
vlist = [body.masscenter.vel(N), body.frame.ang_vel_in(N)]
|
| 465 |
+
elif isinstance(body, Particle):
|
| 466 |
+
vlist = [body.point.vel(N),]
|
| 467 |
+
else:
|
| 468 |
+
raise TypeError('The body list may only contain either '
|
| 469 |
+
'RigidBody or Particle as list elements.')
|
| 470 |
+
v = [msubs(vel, self._qdot_u_map) for vel in vlist]
|
| 471 |
+
return partial_velocity(v, self.u, N)
|
| 472 |
+
partials = [get_partial_velocity(body) for body in bl]
|
| 473 |
+
|
| 474 |
+
# Compute fr_star in two components:
|
| 475 |
+
# fr_star = -(MM*u' + nonMM)
|
| 476 |
+
o = len(self.u)
|
| 477 |
+
MM = zeros(o, o)
|
| 478 |
+
nonMM = zeros(o, 1)
|
| 479 |
+
zero_uaux = lambda expr: msubs(expr, uaux_zero)
|
| 480 |
+
zero_udot_uaux = lambda expr: msubs(msubs(expr, udot_zero), uaux_zero)
|
| 481 |
+
for i, body in enumerate(bl):
|
| 482 |
+
if isinstance(body, RigidBody):
|
| 483 |
+
M = zero_uaux(body.mass)
|
| 484 |
+
I = zero_uaux(body.central_inertia)
|
| 485 |
+
vel = zero_uaux(body.masscenter.vel(N))
|
| 486 |
+
omega = zero_uaux(body.frame.ang_vel_in(N))
|
| 487 |
+
acc = zero_udot_uaux(body.masscenter.acc(N))
|
| 488 |
+
inertial_force = (M.diff(t) * vel + M * acc)
|
| 489 |
+
inertial_torque = zero_uaux((I.dt(body.frame).dot(omega)) +
|
| 490 |
+
msubs(I.dot(body.frame.ang_acc_in(N)), udot_zero) +
|
| 491 |
+
(omega.cross(I.dot(omega))))
|
| 492 |
+
for j in range(o):
|
| 493 |
+
tmp_vel = zero_uaux(partials[i][0][j])
|
| 494 |
+
tmp_ang = zero_uaux(I.dot(partials[i][1][j]))
|
| 495 |
+
for k in range(o):
|
| 496 |
+
# translational
|
| 497 |
+
MM[j, k] += M*tmp_vel.dot(partials[i][0][k])
|
| 498 |
+
# rotational
|
| 499 |
+
MM[j, k] += tmp_ang.dot(partials[i][1][k])
|
| 500 |
+
nonMM[j] += inertial_force.dot(partials[i][0][j])
|
| 501 |
+
nonMM[j] += inertial_torque.dot(partials[i][1][j])
|
| 502 |
+
else:
|
| 503 |
+
M = zero_uaux(body.mass)
|
| 504 |
+
vel = zero_uaux(body.point.vel(N))
|
| 505 |
+
acc = zero_udot_uaux(body.point.acc(N))
|
| 506 |
+
inertial_force = (M.diff(t) * vel + M * acc)
|
| 507 |
+
for j in range(o):
|
| 508 |
+
temp = zero_uaux(partials[i][0][j])
|
| 509 |
+
for k in range(o):
|
| 510 |
+
MM[j, k] += M*temp.dot(partials[i][0][k])
|
| 511 |
+
nonMM[j] += inertial_force.dot(partials[i][0][j])
|
| 512 |
+
# Compose fr_star out of MM and nonMM
|
| 513 |
+
MM = zero_uaux(msubs(MM, q_ddot_u_map))
|
| 514 |
+
nonMM = msubs(msubs(nonMM, q_ddot_u_map),
|
| 515 |
+
udot_zero, uauxdot_zero, uaux_zero)
|
| 516 |
+
fr_star = -(MM * msubs(Matrix(self._udot), uauxdot_zero) + nonMM)
|
| 517 |
+
|
| 518 |
+
# If there are dependent speeds, we need to find fr_star_tilde
|
| 519 |
+
if self._udep:
|
| 520 |
+
p = o - len(self._udep)
|
| 521 |
+
fr_star_ind = fr_star[:p, 0]
|
| 522 |
+
fr_star_dep = fr_star[p:o, 0]
|
| 523 |
+
fr_star = fr_star_ind + (self._Ars.T * fr_star_dep)
|
| 524 |
+
# Apply the same to MM
|
| 525 |
+
MMi = MM[:p, :]
|
| 526 |
+
MMd = MM[p:o, :]
|
| 527 |
+
MM = MMi + (self._Ars.T * MMd)
|
| 528 |
+
# Apply the same to nonMM
|
| 529 |
+
nonMM = nonMM[:p, :] + (self._Ars.T * nonMM[p:o, :])
|
| 530 |
+
|
| 531 |
+
self._bodylist = bl
|
| 532 |
+
self._frstar = fr_star
|
| 533 |
+
self._k_d = MM
|
| 534 |
+
self._f_d = -(self._fr - nonMM)
|
| 535 |
+
return fr_star
|
| 536 |
+
|
| 537 |
+
def to_linearizer(self, linear_solver='LU'):
|
| 538 |
+
"""Returns an instance of the Linearizer class, initiated from the
|
| 539 |
+
data in the KanesMethod class. This may be more desirable than using
|
| 540 |
+
the linearize class method, as the Linearizer object will allow more
|
| 541 |
+
efficient recalculation (i.e. about varying operating points).
|
| 542 |
+
|
| 543 |
+
Parameters
|
| 544 |
+
==========
|
| 545 |
+
linear_solver : str, callable
|
| 546 |
+
Method used to solve the several symbolic linear systems of the
|
| 547 |
+
form ``A*x=b`` in the linearization process. If a string is
|
| 548 |
+
supplied, it should be a valid method that can be used with the
|
| 549 |
+
:meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is
|
| 550 |
+
supplied, it should have the format ``x = f(A, b)``, where it
|
| 551 |
+
solves the equations and returns the solution. The default is
|
| 552 |
+
``'LU'`` which corresponds to SymPy's ``A.LUsolve(b)``.
|
| 553 |
+
``LUsolve()`` is fast to compute but will often result in
|
| 554 |
+
divide-by-zero and thus ``nan`` results.
|
| 555 |
+
|
| 556 |
+
Returns
|
| 557 |
+
=======
|
| 558 |
+
Linearizer
|
| 559 |
+
An instantiated
|
| 560 |
+
:class:`sympy.physics.mechanics.linearize.Linearizer`.
|
| 561 |
+
|
| 562 |
+
"""
|
| 563 |
+
|
| 564 |
+
if (self._fr is None) or (self._frstar is None):
|
| 565 |
+
raise ValueError('Need to compute Fr, Fr* first.')
|
| 566 |
+
|
| 567 |
+
# Get required equation components. The Kane's method class breaks
|
| 568 |
+
# these into pieces. Need to reassemble
|
| 569 |
+
f_c = self._f_h
|
| 570 |
+
if self._f_nh and self._k_nh:
|
| 571 |
+
f_v = self._f_nh + self._k_nh*Matrix(self.u)
|
| 572 |
+
else:
|
| 573 |
+
f_v = Matrix()
|
| 574 |
+
if self._f_dnh and self._k_dnh:
|
| 575 |
+
f_a = self._f_dnh + self._k_dnh*Matrix(self._udot)
|
| 576 |
+
else:
|
| 577 |
+
f_a = Matrix()
|
| 578 |
+
# Dicts to sub to zero, for splitting up expressions
|
| 579 |
+
u_zero = dict.fromkeys(self.u, 0)
|
| 580 |
+
ud_zero = dict.fromkeys(self._udot, 0)
|
| 581 |
+
qd_zero = dict.fromkeys(self._qdot, 0)
|
| 582 |
+
qd_u_zero = dict.fromkeys(Matrix([self._qdot, self.u]), 0)
|
| 583 |
+
# Break the kinematic differential eqs apart into f_0 and f_1
|
| 584 |
+
f_0 = msubs(self._f_k, u_zero) + self._k_kqdot*Matrix(self._qdot)
|
| 585 |
+
f_1 = msubs(self._f_k, qd_zero) + self._k_ku*Matrix(self.u)
|
| 586 |
+
# Break the dynamic differential eqs into f_2 and f_3
|
| 587 |
+
f_2 = msubs(self._frstar, qd_u_zero)
|
| 588 |
+
f_3 = msubs(self._frstar, ud_zero) + self._fr
|
| 589 |
+
f_4 = zeros(len(f_2), 1)
|
| 590 |
+
|
| 591 |
+
# Get the required vector components
|
| 592 |
+
q = self.q
|
| 593 |
+
u = self.u
|
| 594 |
+
if self._qdep:
|
| 595 |
+
q_i = q[:-len(self._qdep)]
|
| 596 |
+
else:
|
| 597 |
+
q_i = q
|
| 598 |
+
q_d = self._qdep
|
| 599 |
+
if self._udep:
|
| 600 |
+
u_i = u[:-len(self._udep)]
|
| 601 |
+
else:
|
| 602 |
+
u_i = u
|
| 603 |
+
u_d = self._udep
|
| 604 |
+
|
| 605 |
+
# Form dictionary to set auxiliary speeds & their derivatives to 0.
|
| 606 |
+
uaux = self._uaux
|
| 607 |
+
uauxdot = uaux.diff(dynamicsymbols._t)
|
| 608 |
+
uaux_zero = dict.fromkeys(Matrix([uaux, uauxdot]), 0)
|
| 609 |
+
|
| 610 |
+
# Checking for dynamic symbols outside the dynamic differential
|
| 611 |
+
# equations; throws error if there is.
|
| 612 |
+
sym_list = set(Matrix([q, self._qdot, u, self._udot, uaux, uauxdot]))
|
| 613 |
+
if any(find_dynamicsymbols(i, sym_list) for i in [self._k_kqdot,
|
| 614 |
+
self._k_ku, self._f_k, self._k_dnh, self._f_dnh, self._k_d]):
|
| 615 |
+
raise ValueError('Cannot have dynamicsymbols outside dynamic \
|
| 616 |
+
forcing vector.')
|
| 617 |
+
|
| 618 |
+
# Find all other dynamic symbols, forming the forcing vector r.
|
| 619 |
+
# Sort r to make it canonical.
|
| 620 |
+
r = list(find_dynamicsymbols(msubs(self._f_d, uaux_zero), sym_list))
|
| 621 |
+
r.sort(key=default_sort_key)
|
| 622 |
+
|
| 623 |
+
# Check for any derivatives of variables in r that are also found in r.
|
| 624 |
+
for i in r:
|
| 625 |
+
if diff(i, dynamicsymbols._t) in r:
|
| 626 |
+
raise ValueError('Cannot have derivatives of specified \
|
| 627 |
+
quantities when linearizing forcing terms.')
|
| 628 |
+
return Linearizer(f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a, q, u, q_i,
|
| 629 |
+
q_d, u_i, u_d, r, linear_solver=linear_solver)
|
| 630 |
+
|
| 631 |
+
# TODO : Remove `new_method` after 1.1 has been released.
|
| 632 |
+
def linearize(self, *, new_method=None, linear_solver='LU', **kwargs):
|
| 633 |
+
""" Linearize the equations of motion about a symbolic operating point.
|
| 634 |
+
|
| 635 |
+
Parameters
|
| 636 |
+
==========
|
| 637 |
+
new_method
|
| 638 |
+
Deprecated, does nothing and will be removed.
|
| 639 |
+
linear_solver : str, callable
|
| 640 |
+
Method used to solve the several symbolic linear systems of the
|
| 641 |
+
form ``A*x=b`` in the linearization process. If a string is
|
| 642 |
+
supplied, it should be a valid method that can be used with the
|
| 643 |
+
:meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is
|
| 644 |
+
supplied, it should have the format ``x = f(A, b)``, where it
|
| 645 |
+
solves the equations and returns the solution. The default is
|
| 646 |
+
``'LU'`` which corresponds to SymPy's ``A.LUsolve(b)``.
|
| 647 |
+
``LUsolve()`` is fast to compute but will often result in
|
| 648 |
+
divide-by-zero and thus ``nan`` results.
|
| 649 |
+
**kwargs
|
| 650 |
+
Extra keyword arguments are passed to
|
| 651 |
+
:meth:`sympy.physics.mechanics.linearize.Linearizer.linearize`.
|
| 652 |
+
|
| 653 |
+
Explanation
|
| 654 |
+
===========
|
| 655 |
+
|
| 656 |
+
If kwarg A_and_B is False (default), returns M, A, B, r for the
|
| 657 |
+
linearized form, M*[q', u']^T = A*[q_ind, u_ind]^T + B*r.
|
| 658 |
+
|
| 659 |
+
If kwarg A_and_B is True, returns A, B, r for the linearized form
|
| 660 |
+
dx = A*x + B*r, where x = [q_ind, u_ind]^T. Note that this is
|
| 661 |
+
computationally intensive if there are many symbolic parameters. For
|
| 662 |
+
this reason, it may be more desirable to use the default A_and_B=False,
|
| 663 |
+
returning M, A, and B. Values may then be substituted in to these
|
| 664 |
+
matrices, and the state space form found as
|
| 665 |
+
A = P.T*M.inv()*A, B = P.T*M.inv()*B, where P = Linearizer.perm_mat.
|
| 666 |
+
|
| 667 |
+
In both cases, r is found as all dynamicsymbols in the equations of
|
| 668 |
+
motion that are not part of q, u, q', or u'. They are sorted in
|
| 669 |
+
canonical form.
|
| 670 |
+
|
| 671 |
+
The operating points may be also entered using the ``op_point`` kwarg.
|
| 672 |
+
This takes a dictionary of {symbol: value}, or a an iterable of such
|
| 673 |
+
dictionaries. The values may be numeric or symbolic. The more values
|
| 674 |
+
you can specify beforehand, the faster this computation will run.
|
| 675 |
+
|
| 676 |
+
For more documentation, please see the ``Linearizer`` class.
|
| 677 |
+
|
| 678 |
+
"""
|
| 679 |
+
|
| 680 |
+
linearizer = self.to_linearizer(linear_solver=linear_solver)
|
| 681 |
+
result = linearizer.linearize(**kwargs)
|
| 682 |
+
return result + (linearizer.r,)
|
| 683 |
+
|
| 684 |
+
def kanes_equations(self, bodies=None, loads=None):
|
| 685 |
+
""" Method to form Kane's equations, Fr + Fr* = 0.
|
| 686 |
+
|
| 687 |
+
Explanation
|
| 688 |
+
===========
|
| 689 |
+
|
| 690 |
+
Returns (Fr, Fr*). In the case where auxiliary generalized speeds are
|
| 691 |
+
present (say, s auxiliary speeds, o generalized speeds, and m motion
|
| 692 |
+
constraints) the length of the returned vectors will be o - m + s in
|
| 693 |
+
length. The first o - m equations will be the constrained Kane's
|
| 694 |
+
equations, then the s auxiliary Kane's equations. These auxiliary
|
| 695 |
+
equations can be accessed with the auxiliary_eqs property.
|
| 696 |
+
|
| 697 |
+
Parameters
|
| 698 |
+
==========
|
| 699 |
+
|
| 700 |
+
bodies : iterable
|
| 701 |
+
An iterable of all RigidBody's and Particle's in the system.
|
| 702 |
+
A system must have at least one body.
|
| 703 |
+
loads : iterable
|
| 704 |
+
Takes in an iterable of (Particle, Vector) or (ReferenceFrame, Vector)
|
| 705 |
+
tuples which represent the force at a point or torque on a frame.
|
| 706 |
+
Must be either a non-empty iterable of tuples or None which corresponds
|
| 707 |
+
to a system with no constraints.
|
| 708 |
+
"""
|
| 709 |
+
if bodies is None:
|
| 710 |
+
bodies = self.bodies
|
| 711 |
+
if loads is None and self._forcelist is not None:
|
| 712 |
+
loads = self._forcelist
|
| 713 |
+
if loads == []:
|
| 714 |
+
loads = None
|
| 715 |
+
if not self._k_kqdot:
|
| 716 |
+
raise AttributeError('Create an instance of KanesMethod with '
|
| 717 |
+
'kinematic differential equations to use this method.')
|
| 718 |
+
fr = self._form_fr(loads)
|
| 719 |
+
frstar = self._form_frstar(bodies)
|
| 720 |
+
if self._uaux:
|
| 721 |
+
if not self._udep:
|
| 722 |
+
km = KanesMethod(self._inertial, self.q, self._uaux,
|
| 723 |
+
u_auxiliary=self._uaux, constraint_solver=self._constraint_solver)
|
| 724 |
+
else:
|
| 725 |
+
km = KanesMethod(self._inertial, self.q, self._uaux,
|
| 726 |
+
u_auxiliary=self._uaux, u_dependent=self._udep,
|
| 727 |
+
velocity_constraints=(self._k_nh * self.u +
|
| 728 |
+
self._f_nh),
|
| 729 |
+
acceleration_constraints=(self._k_dnh * self._udot +
|
| 730 |
+
self._f_dnh),
|
| 731 |
+
constraint_solver=self._constraint_solver
|
| 732 |
+
)
|
| 733 |
+
km._qdot_u_map = self._qdot_u_map
|
| 734 |
+
self._km = km
|
| 735 |
+
fraux = km._form_fr(loads)
|
| 736 |
+
frstaraux = km._form_frstar(bodies)
|
| 737 |
+
self._aux_eq = fraux + frstaraux
|
| 738 |
+
self._fr = fr.col_join(fraux)
|
| 739 |
+
self._frstar = frstar.col_join(frstaraux)
|
| 740 |
+
return (self._fr, self._frstar)
|
| 741 |
+
|
| 742 |
+
def _form_eoms(self):
|
| 743 |
+
fr, frstar = self.kanes_equations(self.bodylist, self.forcelist)
|
| 744 |
+
return fr + frstar
|
| 745 |
+
|
| 746 |
+
def rhs(self, inv_method=None):
|
| 747 |
+
"""Returns the system's equations of motion in first order form. The
|
| 748 |
+
output is the right hand side of::
|
| 749 |
+
|
| 750 |
+
x' = |q'| =: f(q, u, r, p, t)
|
| 751 |
+
|u'|
|
| 752 |
+
|
| 753 |
+
The right hand side is what is needed by most numerical ODE
|
| 754 |
+
integrators.
|
| 755 |
+
|
| 756 |
+
Parameters
|
| 757 |
+
==========
|
| 758 |
+
|
| 759 |
+
inv_method : str
|
| 760 |
+
The specific sympy inverse matrix calculation method to use. For a
|
| 761 |
+
list of valid methods, see
|
| 762 |
+
:meth:`~sympy.matrices.matrixbase.MatrixBase.inv`
|
| 763 |
+
|
| 764 |
+
"""
|
| 765 |
+
rhs = zeros(len(self.q) + len(self.u), 1)
|
| 766 |
+
kdes = self.kindiffdict()
|
| 767 |
+
for i, q_i in enumerate(self.q):
|
| 768 |
+
rhs[i] = kdes[q_i.diff()]
|
| 769 |
+
|
| 770 |
+
if inv_method is None:
|
| 771 |
+
rhs[len(self.q):, 0] = self.mass_matrix.LUsolve(self.forcing)
|
| 772 |
+
else:
|
| 773 |
+
rhs[len(self.q):, 0] = (self.mass_matrix.inv(inv_method,
|
| 774 |
+
try_block_diag=True) *
|
| 775 |
+
self.forcing)
|
| 776 |
+
|
| 777 |
+
return rhs
|
| 778 |
+
|
| 779 |
+
def kindiffdict(self):
|
| 780 |
+
"""Returns a dictionary mapping q' to u."""
|
| 781 |
+
if not self._qdot_u_map:
|
| 782 |
+
raise AttributeError('Create an instance of KanesMethod with '
|
| 783 |
+
'kinematic differential equations to use this method.')
|
| 784 |
+
return self._qdot_u_map
|
| 785 |
+
|
| 786 |
+
@property
|
| 787 |
+
def auxiliary_eqs(self):
|
| 788 |
+
"""A matrix containing the auxiliary equations."""
|
| 789 |
+
if not self._fr or not self._frstar:
|
| 790 |
+
raise ValueError('Need to compute Fr, Fr* first.')
|
| 791 |
+
if not self._uaux:
|
| 792 |
+
raise ValueError('No auxiliary speeds have been declared.')
|
| 793 |
+
return self._aux_eq
|
| 794 |
+
|
| 795 |
+
@property
|
| 796 |
+
def mass_matrix_kin(self):
|
| 797 |
+
r"""The kinematic "mass matrix" $\mathbf{k_{k\dot{q}}}$ of the system."""
|
| 798 |
+
return self._k_kqdot if self.explicit_kinematics else self._k_kqdot_implicit
|
| 799 |
+
|
| 800 |
+
@property
|
| 801 |
+
def forcing_kin(self):
|
| 802 |
+
"""The kinematic "forcing vector" of the system."""
|
| 803 |
+
if self.explicit_kinematics:
|
| 804 |
+
return -(self._k_ku * Matrix(self.u) + self._f_k)
|
| 805 |
+
else:
|
| 806 |
+
return -(self._k_ku_implicit * Matrix(self.u) + self._f_k_implicit)
|
| 807 |
+
|
| 808 |
+
@property
|
| 809 |
+
def mass_matrix(self):
|
| 810 |
+
"""The mass matrix of the system."""
|
| 811 |
+
if not self._fr or not self._frstar:
|
| 812 |
+
raise ValueError('Need to compute Fr, Fr* first.')
|
| 813 |
+
return Matrix([self._k_d, self._k_dnh])
|
| 814 |
+
|
| 815 |
+
@property
|
| 816 |
+
def forcing(self):
|
| 817 |
+
"""The forcing vector of the system."""
|
| 818 |
+
if not self._fr or not self._frstar:
|
| 819 |
+
raise ValueError('Need to compute Fr, Fr* first.')
|
| 820 |
+
return -Matrix([self._f_d, self._f_dnh])
|
| 821 |
+
|
| 822 |
+
@property
|
| 823 |
+
def mass_matrix_full(self):
|
| 824 |
+
"""The mass matrix of the system, augmented by the kinematic
|
| 825 |
+
differential equations in explicit or implicit form."""
|
| 826 |
+
if not self._fr or not self._frstar:
|
| 827 |
+
raise ValueError('Need to compute Fr, Fr* first.')
|
| 828 |
+
o, n = len(self.u), len(self.q)
|
| 829 |
+
return (self.mass_matrix_kin.row_join(zeros(n, o))).col_join(
|
| 830 |
+
zeros(o, n).row_join(self.mass_matrix))
|
| 831 |
+
|
| 832 |
+
@property
|
| 833 |
+
def forcing_full(self):
|
| 834 |
+
"""The forcing vector of the system, augmented by the kinematic
|
| 835 |
+
differential equations in explicit or implicit form."""
|
| 836 |
+
return Matrix([self.forcing_kin, self.forcing])
|
| 837 |
+
|
| 838 |
+
@property
|
| 839 |
+
def q(self):
|
| 840 |
+
return self._q
|
| 841 |
+
|
| 842 |
+
@property
|
| 843 |
+
def u(self):
|
| 844 |
+
return self._u
|
| 845 |
+
|
| 846 |
+
@property
|
| 847 |
+
def bodylist(self):
|
| 848 |
+
return self._bodylist
|
| 849 |
+
|
| 850 |
+
@property
|
| 851 |
+
def forcelist(self):
|
| 852 |
+
return self._forcelist
|
| 853 |
+
|
| 854 |
+
@property
|
| 855 |
+
def bodies(self):
|
| 856 |
+
return self._bodylist
|
| 857 |
+
|
| 858 |
+
@property
|
| 859 |
+
def loads(self):
|
| 860 |
+
return self._forcelist
|
lib/python3.12/site-packages/sympy/physics/mechanics/linearize.py
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__all__ = ['Linearizer']
|
| 2 |
+
|
| 3 |
+
from sympy import Matrix, eye, zeros
|
| 4 |
+
from sympy.core.symbol import Dummy
|
| 5 |
+
from sympy.utilities.iterables import flatten
|
| 6 |
+
from sympy.physics.vector import dynamicsymbols
|
| 7 |
+
from sympy.physics.mechanics.functions import msubs, _parse_linear_solver
|
| 8 |
+
|
| 9 |
+
from collections import namedtuple
|
| 10 |
+
from collections.abc import Iterable
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class Linearizer:
|
| 14 |
+
"""This object holds the general model form for a dynamic system. This
|
| 15 |
+
model is used for computing the linearized form of the system, while
|
| 16 |
+
properly dealing with constraints leading to dependent coordinates and
|
| 17 |
+
speeds. The notation and method is described in [1]_.
|
| 18 |
+
|
| 19 |
+
Attributes
|
| 20 |
+
==========
|
| 21 |
+
|
| 22 |
+
f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a : Matrix
|
| 23 |
+
Matrices holding the general system form.
|
| 24 |
+
q, u, r : Matrix
|
| 25 |
+
Matrices holding the generalized coordinates, speeds, and
|
| 26 |
+
input vectors.
|
| 27 |
+
q_i, u_i : Matrix
|
| 28 |
+
Matrices of the independent generalized coordinates and speeds.
|
| 29 |
+
q_d, u_d : Matrix
|
| 30 |
+
Matrices of the dependent generalized coordinates and speeds.
|
| 31 |
+
perm_mat : Matrix
|
| 32 |
+
Permutation matrix such that [q_ind, u_ind]^T = perm_mat*[q, u]^T
|
| 33 |
+
|
| 34 |
+
References
|
| 35 |
+
==========
|
| 36 |
+
|
| 37 |
+
.. [1] D. L. Peterson, G. Gede, and M. Hubbard, "Symbolic linearization of
|
| 38 |
+
equations of motion of constrained multibody systems," Multibody
|
| 39 |
+
Syst Dyn, vol. 33, no. 2, pp. 143-161, Feb. 2015, doi:
|
| 40 |
+
10.1007/s11044-014-9436-5.
|
| 41 |
+
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
def __init__(self, f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a, q, u, q_i=None,
|
| 45 |
+
q_d=None, u_i=None, u_d=None, r=None, lams=None,
|
| 46 |
+
linear_solver='LU'):
|
| 47 |
+
"""
|
| 48 |
+
Parameters
|
| 49 |
+
==========
|
| 50 |
+
|
| 51 |
+
f_0, f_1, f_2, f_3, f_4, f_c, f_v, f_a : array_like
|
| 52 |
+
System of equations holding the general system form.
|
| 53 |
+
Supply empty array or Matrix if the parameter
|
| 54 |
+
does not exist.
|
| 55 |
+
q : array_like
|
| 56 |
+
The generalized coordinates.
|
| 57 |
+
u : array_like
|
| 58 |
+
The generalized speeds
|
| 59 |
+
q_i, u_i : array_like, optional
|
| 60 |
+
The independent generalized coordinates and speeds.
|
| 61 |
+
q_d, u_d : array_like, optional
|
| 62 |
+
The dependent generalized coordinates and speeds.
|
| 63 |
+
r : array_like, optional
|
| 64 |
+
The input variables.
|
| 65 |
+
lams : array_like, optional
|
| 66 |
+
The lagrange multipliers
|
| 67 |
+
linear_solver : str, callable
|
| 68 |
+
Method used to solve the several symbolic linear systems of the
|
| 69 |
+
form ``A*x=b`` in the linearization process. If a string is
|
| 70 |
+
supplied, it should be a valid method that can be used with the
|
| 71 |
+
:meth:`sympy.matrices.matrixbase.MatrixBase.solve`. If a callable is
|
| 72 |
+
supplied, it should have the format ``x = f(A, b)``, where it
|
| 73 |
+
solves the equations and returns the solution. The default is
|
| 74 |
+
``'LU'`` which corresponds to SymPy's ``A.LUsolve(b)``.
|
| 75 |
+
``LUsolve()`` is fast to compute but will often result in
|
| 76 |
+
divide-by-zero and thus ``nan`` results.
|
| 77 |
+
|
| 78 |
+
"""
|
| 79 |
+
self.linear_solver = _parse_linear_solver(linear_solver)
|
| 80 |
+
|
| 81 |
+
# Generalized equation form
|
| 82 |
+
self.f_0 = Matrix(f_0)
|
| 83 |
+
self.f_1 = Matrix(f_1)
|
| 84 |
+
self.f_2 = Matrix(f_2)
|
| 85 |
+
self.f_3 = Matrix(f_3)
|
| 86 |
+
self.f_4 = Matrix(f_4)
|
| 87 |
+
self.f_c = Matrix(f_c)
|
| 88 |
+
self.f_v = Matrix(f_v)
|
| 89 |
+
self.f_a = Matrix(f_a)
|
| 90 |
+
|
| 91 |
+
# Generalized equation variables
|
| 92 |
+
self.q = Matrix(q)
|
| 93 |
+
self.u = Matrix(u)
|
| 94 |
+
none_handler = lambda x: Matrix(x) if x else Matrix()
|
| 95 |
+
self.q_i = none_handler(q_i)
|
| 96 |
+
self.q_d = none_handler(q_d)
|
| 97 |
+
self.u_i = none_handler(u_i)
|
| 98 |
+
self.u_d = none_handler(u_d)
|
| 99 |
+
self.r = none_handler(r)
|
| 100 |
+
self.lams = none_handler(lams)
|
| 101 |
+
|
| 102 |
+
# Derivatives of generalized equation variables
|
| 103 |
+
self._qd = self.q.diff(dynamicsymbols._t)
|
| 104 |
+
self._ud = self.u.diff(dynamicsymbols._t)
|
| 105 |
+
# If the user doesn't actually use generalized variables, and the
|
| 106 |
+
# qd and u vectors have any intersecting variables, this can cause
|
| 107 |
+
# problems. We'll fix this with some hackery, and Dummy variables
|
| 108 |
+
dup_vars = set(self._qd).intersection(self.u)
|
| 109 |
+
self._qd_dup = Matrix([var if var not in dup_vars else Dummy() for var
|
| 110 |
+
in self._qd])
|
| 111 |
+
|
| 112 |
+
# Derive dimesion terms
|
| 113 |
+
l = len(self.f_c)
|
| 114 |
+
m = len(self.f_v)
|
| 115 |
+
n = len(self.q)
|
| 116 |
+
o = len(self.u)
|
| 117 |
+
s = len(self.r)
|
| 118 |
+
k = len(self.lams)
|
| 119 |
+
dims = namedtuple('dims', ['l', 'm', 'n', 'o', 's', 'k'])
|
| 120 |
+
self._dims = dims(l, m, n, o, s, k)
|
| 121 |
+
|
| 122 |
+
self._Pq = None
|
| 123 |
+
self._Pqi = None
|
| 124 |
+
self._Pqd = None
|
| 125 |
+
self._Pu = None
|
| 126 |
+
self._Pui = None
|
| 127 |
+
self._Pud = None
|
| 128 |
+
self._C_0 = None
|
| 129 |
+
self._C_1 = None
|
| 130 |
+
self._C_2 = None
|
| 131 |
+
self.perm_mat = None
|
| 132 |
+
|
| 133 |
+
self._setup_done = False
|
| 134 |
+
|
| 135 |
+
def _setup(self):
|
| 136 |
+
# Calculations here only need to be run once. They are moved out of
|
| 137 |
+
# the __init__ method to increase the speed of Linearizer creation.
|
| 138 |
+
self._form_permutation_matrices()
|
| 139 |
+
self._form_block_matrices()
|
| 140 |
+
self._form_coefficient_matrices()
|
| 141 |
+
self._setup_done = True
|
| 142 |
+
|
| 143 |
+
def _form_permutation_matrices(self):
|
| 144 |
+
"""Form the permutation matrices Pq and Pu."""
|
| 145 |
+
|
| 146 |
+
# Extract dimension variables
|
| 147 |
+
l, m, n, o, s, k = self._dims
|
| 148 |
+
# Compute permutation matrices
|
| 149 |
+
if n != 0:
|
| 150 |
+
self._Pq = permutation_matrix(self.q, Matrix([self.q_i, self.q_d]))
|
| 151 |
+
if l > 0:
|
| 152 |
+
self._Pqi = self._Pq[:, :-l]
|
| 153 |
+
self._Pqd = self._Pq[:, -l:]
|
| 154 |
+
else:
|
| 155 |
+
self._Pqi = self._Pq
|
| 156 |
+
self._Pqd = Matrix()
|
| 157 |
+
if o != 0:
|
| 158 |
+
self._Pu = permutation_matrix(self.u, Matrix([self.u_i, self.u_d]))
|
| 159 |
+
if m > 0:
|
| 160 |
+
self._Pui = self._Pu[:, :-m]
|
| 161 |
+
self._Pud = self._Pu[:, -m:]
|
| 162 |
+
else:
|
| 163 |
+
self._Pui = self._Pu
|
| 164 |
+
self._Pud = Matrix()
|
| 165 |
+
# Compute combination permutation matrix for computing A and B
|
| 166 |
+
P_col1 = Matrix([self._Pqi, zeros(o + k, n - l)])
|
| 167 |
+
P_col2 = Matrix([zeros(n, o - m), self._Pui, zeros(k, o - m)])
|
| 168 |
+
if P_col1:
|
| 169 |
+
if P_col2:
|
| 170 |
+
self.perm_mat = P_col1.row_join(P_col2)
|
| 171 |
+
else:
|
| 172 |
+
self.perm_mat = P_col1
|
| 173 |
+
else:
|
| 174 |
+
self.perm_mat = P_col2
|
| 175 |
+
|
| 176 |
+
def _form_coefficient_matrices(self):
|
| 177 |
+
"""Form the coefficient matrices C_0, C_1, and C_2."""
|
| 178 |
+
|
| 179 |
+
# Extract dimension variables
|
| 180 |
+
l, m, n, o, s, k = self._dims
|
| 181 |
+
# Build up the coefficient matrices C_0, C_1, and C_2
|
| 182 |
+
# If there are configuration constraints (l > 0), form C_0 as normal.
|
| 183 |
+
# If not, C_0 is I_(nxn). Note that this works even if n=0
|
| 184 |
+
if l > 0:
|
| 185 |
+
f_c_jac_q = self.f_c.jacobian(self.q)
|
| 186 |
+
self._C_0 = (eye(n) - self._Pqd *
|
| 187 |
+
self.linear_solver(f_c_jac_q*self._Pqd,
|
| 188 |
+
f_c_jac_q))*self._Pqi
|
| 189 |
+
else:
|
| 190 |
+
self._C_0 = eye(n)
|
| 191 |
+
# If there are motion constraints (m > 0), form C_1 and C_2 as normal.
|
| 192 |
+
# If not, C_1 is 0, and C_2 is I_(oxo). Note that this works even if
|
| 193 |
+
# o = 0.
|
| 194 |
+
if m > 0:
|
| 195 |
+
f_v_jac_u = self.f_v.jacobian(self.u)
|
| 196 |
+
temp = f_v_jac_u * self._Pud
|
| 197 |
+
if n != 0:
|
| 198 |
+
f_v_jac_q = self.f_v.jacobian(self.q)
|
| 199 |
+
self._C_1 = -self._Pud * self.linear_solver(temp, f_v_jac_q)
|
| 200 |
+
else:
|
| 201 |
+
self._C_1 = zeros(o, n)
|
| 202 |
+
self._C_2 = (eye(o) - self._Pud *
|
| 203 |
+
self.linear_solver(temp, f_v_jac_u))*self._Pui
|
| 204 |
+
else:
|
| 205 |
+
self._C_1 = zeros(o, n)
|
| 206 |
+
self._C_2 = eye(o)
|
| 207 |
+
|
| 208 |
+
def _form_block_matrices(self):
|
| 209 |
+
"""Form the block matrices for composing M, A, and B."""
|
| 210 |
+
|
| 211 |
+
# Extract dimension variables
|
| 212 |
+
l, m, n, o, s, k = self._dims
|
| 213 |
+
# Block Matrix Definitions. These are only defined if under certain
|
| 214 |
+
# conditions. If undefined, an empty matrix is used instead
|
| 215 |
+
if n != 0:
|
| 216 |
+
self._M_qq = self.f_0.jacobian(self._qd)
|
| 217 |
+
self._A_qq = -(self.f_0 + self.f_1).jacobian(self.q)
|
| 218 |
+
else:
|
| 219 |
+
self._M_qq = Matrix()
|
| 220 |
+
self._A_qq = Matrix()
|
| 221 |
+
if n != 0 and m != 0:
|
| 222 |
+
self._M_uqc = self.f_a.jacobian(self._qd_dup)
|
| 223 |
+
self._A_uqc = -self.f_a.jacobian(self.q)
|
| 224 |
+
else:
|
| 225 |
+
self._M_uqc = Matrix()
|
| 226 |
+
self._A_uqc = Matrix()
|
| 227 |
+
if n != 0 and o - m + k != 0:
|
| 228 |
+
self._M_uqd = self.f_3.jacobian(self._qd_dup)
|
| 229 |
+
self._A_uqd = -(self.f_2 + self.f_3 + self.f_4).jacobian(self.q)
|
| 230 |
+
else:
|
| 231 |
+
self._M_uqd = Matrix()
|
| 232 |
+
self._A_uqd = Matrix()
|
| 233 |
+
if o != 0 and m != 0:
|
| 234 |
+
self._M_uuc = self.f_a.jacobian(self._ud)
|
| 235 |
+
self._A_uuc = -self.f_a.jacobian(self.u)
|
| 236 |
+
else:
|
| 237 |
+
self._M_uuc = Matrix()
|
| 238 |
+
self._A_uuc = Matrix()
|
| 239 |
+
if o != 0 and o - m + k != 0:
|
| 240 |
+
self._M_uud = self.f_2.jacobian(self._ud)
|
| 241 |
+
self._A_uud = -(self.f_2 + self.f_3).jacobian(self.u)
|
| 242 |
+
else:
|
| 243 |
+
self._M_uud = Matrix()
|
| 244 |
+
self._A_uud = Matrix()
|
| 245 |
+
if o != 0 and n != 0:
|
| 246 |
+
self._A_qu = -self.f_1.jacobian(self.u)
|
| 247 |
+
else:
|
| 248 |
+
self._A_qu = Matrix()
|
| 249 |
+
if k != 0 and o - m + k != 0:
|
| 250 |
+
self._M_uld = self.f_4.jacobian(self.lams)
|
| 251 |
+
else:
|
| 252 |
+
self._M_uld = Matrix()
|
| 253 |
+
if s != 0 and o - m + k != 0:
|
| 254 |
+
self._B_u = -self.f_3.jacobian(self.r)
|
| 255 |
+
else:
|
| 256 |
+
self._B_u = Matrix()
|
| 257 |
+
|
| 258 |
+
def linearize(self, op_point=None, A_and_B=False, simplify=False):
|
| 259 |
+
"""Linearize the system about the operating point. Note that
|
| 260 |
+
q_op, u_op, qd_op, ud_op must satisfy the equations of motion.
|
| 261 |
+
These may be either symbolic or numeric.
|
| 262 |
+
|
| 263 |
+
Parameters
|
| 264 |
+
==========
|
| 265 |
+
op_point : dict or iterable of dicts, optional
|
| 266 |
+
Dictionary or iterable of dictionaries containing the operating
|
| 267 |
+
point conditions for all or a subset of the generalized
|
| 268 |
+
coordinates, generalized speeds, and time derivatives of the
|
| 269 |
+
generalized speeds. These will be substituted into the linearized
|
| 270 |
+
system before the linearization is complete. Leave set to ``None``
|
| 271 |
+
if you want the operating point to be an arbitrary set of symbols.
|
| 272 |
+
Note that any reduction in symbols (whether substituted for numbers
|
| 273 |
+
or expressions with a common parameter) will result in faster
|
| 274 |
+
runtime.
|
| 275 |
+
A_and_B : bool, optional
|
| 276 |
+
If A_and_B=False (default), (M, A, B) is returned and of
|
| 277 |
+
A_and_B=True, (A, B) is returned. See below.
|
| 278 |
+
simplify : bool, optional
|
| 279 |
+
Determines if returned values are simplified before return.
|
| 280 |
+
For large expressions this may be time consuming. Default is False.
|
| 281 |
+
|
| 282 |
+
Returns
|
| 283 |
+
=======
|
| 284 |
+
M, A, B : Matrices, ``A_and_B=False``
|
| 285 |
+
Matrices from the implicit form:
|
| 286 |
+
``[M]*[q', u']^T = [A]*[q_ind, u_ind]^T + [B]*r``
|
| 287 |
+
A, B : Matrices, ``A_and_B=True``
|
| 288 |
+
Matrices from the explicit form:
|
| 289 |
+
``[q_ind', u_ind']^T = [A]*[q_ind, u_ind]^T + [B]*r``
|
| 290 |
+
|
| 291 |
+
Notes
|
| 292 |
+
=====
|
| 293 |
+
|
| 294 |
+
Note that the process of solving with A_and_B=True is computationally
|
| 295 |
+
intensive if there are many symbolic parameters. For this reason, it
|
| 296 |
+
may be more desirable to use the default A_and_B=False, returning M, A,
|
| 297 |
+
and B. More values may then be substituted in to these matrices later
|
| 298 |
+
on. The state space form can then be found as A = P.T*M.LUsolve(A), B =
|
| 299 |
+
P.T*M.LUsolve(B), where P = Linearizer.perm_mat.
|
| 300 |
+
|
| 301 |
+
"""
|
| 302 |
+
|
| 303 |
+
# Run the setup if needed:
|
| 304 |
+
if not self._setup_done:
|
| 305 |
+
self._setup()
|
| 306 |
+
|
| 307 |
+
# Compose dict of operating conditions
|
| 308 |
+
if isinstance(op_point, dict):
|
| 309 |
+
op_point_dict = op_point
|
| 310 |
+
elif isinstance(op_point, Iterable):
|
| 311 |
+
op_point_dict = {}
|
| 312 |
+
for op in op_point:
|
| 313 |
+
op_point_dict.update(op)
|
| 314 |
+
else:
|
| 315 |
+
op_point_dict = {}
|
| 316 |
+
|
| 317 |
+
# Extract dimension variables
|
| 318 |
+
l, m, n, o, s, k = self._dims
|
| 319 |
+
|
| 320 |
+
# Rename terms to shorten expressions
|
| 321 |
+
M_qq = self._M_qq
|
| 322 |
+
M_uqc = self._M_uqc
|
| 323 |
+
M_uqd = self._M_uqd
|
| 324 |
+
M_uuc = self._M_uuc
|
| 325 |
+
M_uud = self._M_uud
|
| 326 |
+
M_uld = self._M_uld
|
| 327 |
+
A_qq = self._A_qq
|
| 328 |
+
A_uqc = self._A_uqc
|
| 329 |
+
A_uqd = self._A_uqd
|
| 330 |
+
A_qu = self._A_qu
|
| 331 |
+
A_uuc = self._A_uuc
|
| 332 |
+
A_uud = self._A_uud
|
| 333 |
+
B_u = self._B_u
|
| 334 |
+
C_0 = self._C_0
|
| 335 |
+
C_1 = self._C_1
|
| 336 |
+
C_2 = self._C_2
|
| 337 |
+
|
| 338 |
+
# Build up Mass Matrix
|
| 339 |
+
# |M_qq 0_nxo 0_nxk|
|
| 340 |
+
# M = |M_uqc M_uuc 0_mxk|
|
| 341 |
+
# |M_uqd M_uud M_uld|
|
| 342 |
+
if o != 0:
|
| 343 |
+
col2 = Matrix([zeros(n, o), M_uuc, M_uud])
|
| 344 |
+
if k != 0:
|
| 345 |
+
col3 = Matrix([zeros(n + m, k), M_uld])
|
| 346 |
+
if n != 0:
|
| 347 |
+
col1 = Matrix([M_qq, M_uqc, M_uqd])
|
| 348 |
+
if o != 0 and k != 0:
|
| 349 |
+
M = col1.row_join(col2).row_join(col3)
|
| 350 |
+
elif o != 0:
|
| 351 |
+
M = col1.row_join(col2)
|
| 352 |
+
else:
|
| 353 |
+
M = col1
|
| 354 |
+
elif k != 0:
|
| 355 |
+
M = col2.row_join(col3)
|
| 356 |
+
else:
|
| 357 |
+
M = col2
|
| 358 |
+
M_eq = msubs(M, op_point_dict)
|
| 359 |
+
|
| 360 |
+
# Build up state coefficient matrix A
|
| 361 |
+
# |(A_qq + A_qu*C_1)*C_0 A_qu*C_2|
|
| 362 |
+
# A = |(A_uqc + A_uuc*C_1)*C_0 A_uuc*C_2|
|
| 363 |
+
# |(A_uqd + A_uud*C_1)*C_0 A_uud*C_2|
|
| 364 |
+
# Col 1 is only defined if n != 0
|
| 365 |
+
if n != 0:
|
| 366 |
+
r1c1 = A_qq
|
| 367 |
+
if o != 0:
|
| 368 |
+
r1c1 += (A_qu * C_1)
|
| 369 |
+
r1c1 = r1c1 * C_0
|
| 370 |
+
if m != 0:
|
| 371 |
+
r2c1 = A_uqc
|
| 372 |
+
if o != 0:
|
| 373 |
+
r2c1 += (A_uuc * C_1)
|
| 374 |
+
r2c1 = r2c1 * C_0
|
| 375 |
+
else:
|
| 376 |
+
r2c1 = Matrix()
|
| 377 |
+
if o - m + k != 0:
|
| 378 |
+
r3c1 = A_uqd
|
| 379 |
+
if o != 0:
|
| 380 |
+
r3c1 += (A_uud * C_1)
|
| 381 |
+
r3c1 = r3c1 * C_0
|
| 382 |
+
else:
|
| 383 |
+
r3c1 = Matrix()
|
| 384 |
+
col1 = Matrix([r1c1, r2c1, r3c1])
|
| 385 |
+
else:
|
| 386 |
+
col1 = Matrix()
|
| 387 |
+
# Col 2 is only defined if o != 0
|
| 388 |
+
if o != 0:
|
| 389 |
+
if n != 0:
|
| 390 |
+
r1c2 = A_qu * C_2
|
| 391 |
+
else:
|
| 392 |
+
r1c2 = Matrix()
|
| 393 |
+
if m != 0:
|
| 394 |
+
r2c2 = A_uuc * C_2
|
| 395 |
+
else:
|
| 396 |
+
r2c2 = Matrix()
|
| 397 |
+
if o - m + k != 0:
|
| 398 |
+
r3c2 = A_uud * C_2
|
| 399 |
+
else:
|
| 400 |
+
r3c2 = Matrix()
|
| 401 |
+
col2 = Matrix([r1c2, r2c2, r3c2])
|
| 402 |
+
else:
|
| 403 |
+
col2 = Matrix()
|
| 404 |
+
if col1:
|
| 405 |
+
if col2:
|
| 406 |
+
Amat = col1.row_join(col2)
|
| 407 |
+
else:
|
| 408 |
+
Amat = col1
|
| 409 |
+
else:
|
| 410 |
+
Amat = col2
|
| 411 |
+
Amat_eq = msubs(Amat, op_point_dict)
|
| 412 |
+
|
| 413 |
+
# Build up the B matrix if there are forcing variables
|
| 414 |
+
# |0_(n + m)xs|
|
| 415 |
+
# B = |B_u |
|
| 416 |
+
if s != 0 and o - m + k != 0:
|
| 417 |
+
Bmat = zeros(n + m, s).col_join(B_u)
|
| 418 |
+
Bmat_eq = msubs(Bmat, op_point_dict)
|
| 419 |
+
else:
|
| 420 |
+
Bmat_eq = Matrix()
|
| 421 |
+
|
| 422 |
+
# kwarg A_and_B indicates to return A, B for forming the equation
|
| 423 |
+
# dx = [A]x + [B]r, where x = [q_indnd, u_indnd]^T,
|
| 424 |
+
if A_and_B:
|
| 425 |
+
A_cont = self.perm_mat.T * self.linear_solver(M_eq, Amat_eq)
|
| 426 |
+
if Bmat_eq:
|
| 427 |
+
B_cont = self.perm_mat.T * self.linear_solver(M_eq, Bmat_eq)
|
| 428 |
+
else:
|
| 429 |
+
# Bmat = Matrix([]), so no need to sub
|
| 430 |
+
B_cont = Bmat_eq
|
| 431 |
+
if simplify:
|
| 432 |
+
A_cont.simplify()
|
| 433 |
+
B_cont.simplify()
|
| 434 |
+
return A_cont, B_cont
|
| 435 |
+
# Otherwise return M, A, B for forming the equation
|
| 436 |
+
# [M]dx = [A]x + [B]r, where x = [q, u]^T
|
| 437 |
+
else:
|
| 438 |
+
if simplify:
|
| 439 |
+
M_eq.simplify()
|
| 440 |
+
Amat_eq.simplify()
|
| 441 |
+
Bmat_eq.simplify()
|
| 442 |
+
return M_eq, Amat_eq, Bmat_eq
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
def permutation_matrix(orig_vec, per_vec):
|
| 446 |
+
"""Compute the permutation matrix to change order of
|
| 447 |
+
orig_vec into order of per_vec.
|
| 448 |
+
|
| 449 |
+
Parameters
|
| 450 |
+
==========
|
| 451 |
+
|
| 452 |
+
orig_vec : array_like
|
| 453 |
+
Symbols in original ordering.
|
| 454 |
+
per_vec : array_like
|
| 455 |
+
Symbols in new ordering.
|
| 456 |
+
|
| 457 |
+
Returns
|
| 458 |
+
=======
|
| 459 |
+
|
| 460 |
+
p_matrix : Matrix
|
| 461 |
+
Permutation matrix such that orig_vec == (p_matrix * per_vec).
|
| 462 |
+
"""
|
| 463 |
+
if not isinstance(orig_vec, (list, tuple)):
|
| 464 |
+
orig_vec = flatten(orig_vec)
|
| 465 |
+
if not isinstance(per_vec, (list, tuple)):
|
| 466 |
+
per_vec = flatten(per_vec)
|
| 467 |
+
if set(orig_vec) != set(per_vec):
|
| 468 |
+
raise ValueError("orig_vec and per_vec must be the same length, "
|
| 469 |
+
"and contain the same symbols.")
|
| 470 |
+
ind_list = [orig_vec.index(i) for i in per_vec]
|
| 471 |
+
p_matrix = zeros(len(orig_vec))
|
| 472 |
+
for i, j in enumerate(ind_list):
|
| 473 |
+
p_matrix[i, j] = 1
|
| 474 |
+
return p_matrix
|
lib/python3.12/site-packages/sympy/physics/mechanics/models.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""This module contains some sample symbolic models used for testing and
|
| 3 |
+
examples."""
|
| 4 |
+
|
| 5 |
+
# Internal imports
|
| 6 |
+
from sympy.core import backend as sm
|
| 7 |
+
import sympy.physics.mechanics as me
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def multi_mass_spring_damper(n=1, apply_gravity=False,
|
| 11 |
+
apply_external_forces=False):
|
| 12 |
+
r"""Returns a system containing the symbolic equations of motion and
|
| 13 |
+
associated variables for a simple multi-degree of freedom point mass,
|
| 14 |
+
spring, damper system with optional gravitational and external
|
| 15 |
+
specified forces. For example, a two mass system under the influence of
|
| 16 |
+
gravity and external forces looks like:
|
| 17 |
+
|
| 18 |
+
::
|
| 19 |
+
|
| 20 |
+
----------------
|
| 21 |
+
| | | | g
|
| 22 |
+
\ | | | V
|
| 23 |
+
k0 / --- c0 |
|
| 24 |
+
| | | x0, v0
|
| 25 |
+
--------- V
|
| 26 |
+
| m0 | -----
|
| 27 |
+
--------- |
|
| 28 |
+
| | | |
|
| 29 |
+
\ v | | |
|
| 30 |
+
k1 / f0 --- c1 |
|
| 31 |
+
| | | x1, v1
|
| 32 |
+
--------- V
|
| 33 |
+
| m1 | -----
|
| 34 |
+
---------
|
| 35 |
+
| f1
|
| 36 |
+
V
|
| 37 |
+
|
| 38 |
+
Parameters
|
| 39 |
+
==========
|
| 40 |
+
|
| 41 |
+
n : integer
|
| 42 |
+
The number of masses in the serial chain.
|
| 43 |
+
apply_gravity : boolean
|
| 44 |
+
If true, gravity will be applied to each mass.
|
| 45 |
+
apply_external_forces : boolean
|
| 46 |
+
If true, a time varying external force will be applied to each mass.
|
| 47 |
+
|
| 48 |
+
Returns
|
| 49 |
+
=======
|
| 50 |
+
|
| 51 |
+
kane : sympy.physics.mechanics.kane.KanesMethod
|
| 52 |
+
A KanesMethod object.
|
| 53 |
+
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
mass = sm.symbols('m:{}'.format(n))
|
| 57 |
+
stiffness = sm.symbols('k:{}'.format(n))
|
| 58 |
+
damping = sm.symbols('c:{}'.format(n))
|
| 59 |
+
|
| 60 |
+
acceleration_due_to_gravity = sm.symbols('g')
|
| 61 |
+
|
| 62 |
+
coordinates = me.dynamicsymbols('x:{}'.format(n))
|
| 63 |
+
speeds = me.dynamicsymbols('v:{}'.format(n))
|
| 64 |
+
specifieds = me.dynamicsymbols('f:{}'.format(n))
|
| 65 |
+
|
| 66 |
+
ceiling = me.ReferenceFrame('N')
|
| 67 |
+
origin = me.Point('origin')
|
| 68 |
+
origin.set_vel(ceiling, 0)
|
| 69 |
+
|
| 70 |
+
points = [origin]
|
| 71 |
+
kinematic_equations = []
|
| 72 |
+
particles = []
|
| 73 |
+
forces = []
|
| 74 |
+
|
| 75 |
+
for i in range(n):
|
| 76 |
+
|
| 77 |
+
center = points[-1].locatenew('center{}'.format(i),
|
| 78 |
+
coordinates[i] * ceiling.x)
|
| 79 |
+
center.set_vel(ceiling, points[-1].vel(ceiling) +
|
| 80 |
+
speeds[i] * ceiling.x)
|
| 81 |
+
points.append(center)
|
| 82 |
+
|
| 83 |
+
block = me.Particle('block{}'.format(i), center, mass[i])
|
| 84 |
+
|
| 85 |
+
kinematic_equations.append(speeds[i] - coordinates[i].diff())
|
| 86 |
+
|
| 87 |
+
total_force = (-stiffness[i] * coordinates[i] -
|
| 88 |
+
damping[i] * speeds[i])
|
| 89 |
+
try:
|
| 90 |
+
total_force += (stiffness[i + 1] * coordinates[i + 1] +
|
| 91 |
+
damping[i + 1] * speeds[i + 1])
|
| 92 |
+
except IndexError: # no force from below on last mass
|
| 93 |
+
pass
|
| 94 |
+
|
| 95 |
+
if apply_gravity:
|
| 96 |
+
total_force += mass[i] * acceleration_due_to_gravity
|
| 97 |
+
|
| 98 |
+
if apply_external_forces:
|
| 99 |
+
total_force += specifieds[i]
|
| 100 |
+
|
| 101 |
+
forces.append((center, total_force * ceiling.x))
|
| 102 |
+
|
| 103 |
+
particles.append(block)
|
| 104 |
+
|
| 105 |
+
kane = me.KanesMethod(ceiling, q_ind=coordinates, u_ind=speeds,
|
| 106 |
+
kd_eqs=kinematic_equations)
|
| 107 |
+
kane.kanes_equations(particles, forces)
|
| 108 |
+
|
| 109 |
+
return kane
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def n_link_pendulum_on_cart(n=1, cart_force=True, joint_torques=False):
|
| 113 |
+
r"""Returns the system containing the symbolic first order equations of
|
| 114 |
+
motion for a 2D n-link pendulum on a sliding cart under the influence of
|
| 115 |
+
gravity.
|
| 116 |
+
|
| 117 |
+
::
|
| 118 |
+
|
| 119 |
+
|
|
| 120 |
+
o y v
|
| 121 |
+
\ 0 ^ g
|
| 122 |
+
\ |
|
| 123 |
+
--\-|----
|
| 124 |
+
| \| |
|
| 125 |
+
F-> | o --|---> x
|
| 126 |
+
| |
|
| 127 |
+
---------
|
| 128 |
+
o o
|
| 129 |
+
|
| 130 |
+
Parameters
|
| 131 |
+
==========
|
| 132 |
+
|
| 133 |
+
n : integer
|
| 134 |
+
The number of links in the pendulum.
|
| 135 |
+
cart_force : boolean, default=True
|
| 136 |
+
If true an external specified lateral force is applied to the cart.
|
| 137 |
+
joint_torques : boolean, default=False
|
| 138 |
+
If true joint torques will be added as specified inputs at each
|
| 139 |
+
joint.
|
| 140 |
+
|
| 141 |
+
Returns
|
| 142 |
+
=======
|
| 143 |
+
|
| 144 |
+
kane : sympy.physics.mechanics.kane.KanesMethod
|
| 145 |
+
A KanesMethod object.
|
| 146 |
+
|
| 147 |
+
Notes
|
| 148 |
+
=====
|
| 149 |
+
|
| 150 |
+
The degrees of freedom of the system are n + 1, i.e. one for each
|
| 151 |
+
pendulum link and one for the lateral motion of the cart.
|
| 152 |
+
|
| 153 |
+
M x' = F, where x = [u0, ..., un+1, q0, ..., qn+1]
|
| 154 |
+
|
| 155 |
+
The joint angles are all defined relative to the ground where the x axis
|
| 156 |
+
defines the ground line and the y axis points up. The joint torques are
|
| 157 |
+
applied between each adjacent link and the between the cart and the
|
| 158 |
+
lower link where a positive torque corresponds to positive angle.
|
| 159 |
+
|
| 160 |
+
"""
|
| 161 |
+
if n <= 0:
|
| 162 |
+
raise ValueError('The number of links must be a positive integer.')
|
| 163 |
+
|
| 164 |
+
q = me.dynamicsymbols('q:{}'.format(n + 1))
|
| 165 |
+
u = me.dynamicsymbols('u:{}'.format(n + 1))
|
| 166 |
+
|
| 167 |
+
if joint_torques is True:
|
| 168 |
+
T = me.dynamicsymbols('T1:{}'.format(n + 1))
|
| 169 |
+
|
| 170 |
+
m = sm.symbols('m:{}'.format(n + 1))
|
| 171 |
+
l = sm.symbols('l:{}'.format(n))
|
| 172 |
+
g, t = sm.symbols('g t')
|
| 173 |
+
|
| 174 |
+
I = me.ReferenceFrame('I')
|
| 175 |
+
O = me.Point('O')
|
| 176 |
+
O.set_vel(I, 0)
|
| 177 |
+
|
| 178 |
+
P0 = me.Point('P0')
|
| 179 |
+
P0.set_pos(O, q[0] * I.x)
|
| 180 |
+
P0.set_vel(I, u[0] * I.x)
|
| 181 |
+
Pa0 = me.Particle('Pa0', P0, m[0])
|
| 182 |
+
|
| 183 |
+
frames = [I]
|
| 184 |
+
points = [P0]
|
| 185 |
+
particles = [Pa0]
|
| 186 |
+
forces = [(P0, -m[0] * g * I.y)]
|
| 187 |
+
kindiffs = [q[0].diff(t) - u[0]]
|
| 188 |
+
|
| 189 |
+
if cart_force is True or joint_torques is True:
|
| 190 |
+
specified = []
|
| 191 |
+
else:
|
| 192 |
+
specified = None
|
| 193 |
+
|
| 194 |
+
for i in range(n):
|
| 195 |
+
Bi = I.orientnew('B{}'.format(i), 'Axis', [q[i + 1], I.z])
|
| 196 |
+
Bi.set_ang_vel(I, u[i + 1] * I.z)
|
| 197 |
+
frames.append(Bi)
|
| 198 |
+
|
| 199 |
+
Pi = points[-1].locatenew('P{}'.format(i + 1), l[i] * Bi.y)
|
| 200 |
+
Pi.v2pt_theory(points[-1], I, Bi)
|
| 201 |
+
points.append(Pi)
|
| 202 |
+
|
| 203 |
+
Pai = me.Particle('Pa' + str(i + 1), Pi, m[i + 1])
|
| 204 |
+
particles.append(Pai)
|
| 205 |
+
|
| 206 |
+
forces.append((Pi, -m[i + 1] * g * I.y))
|
| 207 |
+
|
| 208 |
+
if joint_torques is True:
|
| 209 |
+
|
| 210 |
+
specified.append(T[i])
|
| 211 |
+
|
| 212 |
+
if i == 0:
|
| 213 |
+
forces.append((I, -T[i] * I.z))
|
| 214 |
+
|
| 215 |
+
if i == n - 1:
|
| 216 |
+
forces.append((Bi, T[i] * I.z))
|
| 217 |
+
else:
|
| 218 |
+
forces.append((Bi, T[i] * I.z - T[i + 1] * I.z))
|
| 219 |
+
|
| 220 |
+
kindiffs.append(q[i + 1].diff(t) - u[i + 1])
|
| 221 |
+
|
| 222 |
+
if cart_force is True:
|
| 223 |
+
F = me.dynamicsymbols('F')
|
| 224 |
+
forces.append((P0, F * I.x))
|
| 225 |
+
specified.append(F)
|
| 226 |
+
|
| 227 |
+
kane = me.KanesMethod(I, q_ind=q, u_ind=u, kd_eqs=kindiffs)
|
| 228 |
+
kane.kanes_equations(particles, forces)
|
| 229 |
+
|
| 230 |
+
return kane
|
lib/python3.12/site-packages/sympy/physics/mechanics/particle.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import S
|
| 2 |
+
from sympy.physics.vector import cross, dot
|
| 3 |
+
from sympy.physics.mechanics.body_base import BodyBase
|
| 4 |
+
from sympy.physics.mechanics.inertia import inertia_of_point_mass
|
| 5 |
+
from sympy.utilities.exceptions import sympy_deprecation_warning
|
| 6 |
+
|
| 7 |
+
__all__ = ['Particle']
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Particle(BodyBase):
|
| 11 |
+
"""A particle.
|
| 12 |
+
|
| 13 |
+
Explanation
|
| 14 |
+
===========
|
| 15 |
+
|
| 16 |
+
Particles have a non-zero mass and lack spatial extension; they take up no
|
| 17 |
+
space.
|
| 18 |
+
|
| 19 |
+
Values need to be supplied on initialization, but can be changed later.
|
| 20 |
+
|
| 21 |
+
Parameters
|
| 22 |
+
==========
|
| 23 |
+
|
| 24 |
+
name : str
|
| 25 |
+
Name of particle
|
| 26 |
+
point : Point
|
| 27 |
+
A physics/mechanics Point which represents the position, velocity, and
|
| 28 |
+
acceleration of this Particle
|
| 29 |
+
mass : Sympifyable
|
| 30 |
+
A SymPy expression representing the Particle's mass
|
| 31 |
+
potential_energy : Sympifyable
|
| 32 |
+
The potential energy of the Particle.
|
| 33 |
+
|
| 34 |
+
Examples
|
| 35 |
+
========
|
| 36 |
+
|
| 37 |
+
>>> from sympy.physics.mechanics import Particle, Point
|
| 38 |
+
>>> from sympy import Symbol
|
| 39 |
+
>>> po = Point('po')
|
| 40 |
+
>>> m = Symbol('m')
|
| 41 |
+
>>> pa = Particle('pa', po, m)
|
| 42 |
+
>>> # Or you could change these later
|
| 43 |
+
>>> pa.mass = m
|
| 44 |
+
>>> pa.point = po
|
| 45 |
+
|
| 46 |
+
"""
|
| 47 |
+
point = BodyBase.masscenter
|
| 48 |
+
|
| 49 |
+
def __init__(self, name, point=None, mass=None):
|
| 50 |
+
super().__init__(name, point, mass)
|
| 51 |
+
|
| 52 |
+
def linear_momentum(self, frame):
|
| 53 |
+
"""Linear momentum of the particle.
|
| 54 |
+
|
| 55 |
+
Explanation
|
| 56 |
+
===========
|
| 57 |
+
|
| 58 |
+
The linear momentum L, of a particle P, with respect to frame N is
|
| 59 |
+
given by:
|
| 60 |
+
|
| 61 |
+
L = m * v
|
| 62 |
+
|
| 63 |
+
where m is the mass of the particle, and v is the velocity of the
|
| 64 |
+
particle in the frame N.
|
| 65 |
+
|
| 66 |
+
Parameters
|
| 67 |
+
==========
|
| 68 |
+
|
| 69 |
+
frame : ReferenceFrame
|
| 70 |
+
The frame in which linear momentum is desired.
|
| 71 |
+
|
| 72 |
+
Examples
|
| 73 |
+
========
|
| 74 |
+
|
| 75 |
+
>>> from sympy.physics.mechanics import Particle, Point, ReferenceFrame
|
| 76 |
+
>>> from sympy.physics.mechanics import dynamicsymbols
|
| 77 |
+
>>> from sympy.physics.vector import init_vprinting
|
| 78 |
+
>>> init_vprinting(pretty_print=False)
|
| 79 |
+
>>> m, v = dynamicsymbols('m v')
|
| 80 |
+
>>> N = ReferenceFrame('N')
|
| 81 |
+
>>> P = Point('P')
|
| 82 |
+
>>> A = Particle('A', P, m)
|
| 83 |
+
>>> P.set_vel(N, v * N.x)
|
| 84 |
+
>>> A.linear_momentum(N)
|
| 85 |
+
m*v*N.x
|
| 86 |
+
|
| 87 |
+
"""
|
| 88 |
+
|
| 89 |
+
return self.mass * self.point.vel(frame)
|
| 90 |
+
|
| 91 |
+
def angular_momentum(self, point, frame):
|
| 92 |
+
"""Angular momentum of the particle about the point.
|
| 93 |
+
|
| 94 |
+
Explanation
|
| 95 |
+
===========
|
| 96 |
+
|
| 97 |
+
The angular momentum H, about some point O of a particle, P, is given
|
| 98 |
+
by:
|
| 99 |
+
|
| 100 |
+
``H = cross(r, m * v)``
|
| 101 |
+
|
| 102 |
+
where r is the position vector from point O to the particle P, m is
|
| 103 |
+
the mass of the particle, and v is the velocity of the particle in
|
| 104 |
+
the inertial frame, N.
|
| 105 |
+
|
| 106 |
+
Parameters
|
| 107 |
+
==========
|
| 108 |
+
|
| 109 |
+
point : Point
|
| 110 |
+
The point about which angular momentum of the particle is desired.
|
| 111 |
+
|
| 112 |
+
frame : ReferenceFrame
|
| 113 |
+
The frame in which angular momentum is desired.
|
| 114 |
+
|
| 115 |
+
Examples
|
| 116 |
+
========
|
| 117 |
+
|
| 118 |
+
>>> from sympy.physics.mechanics import Particle, Point, ReferenceFrame
|
| 119 |
+
>>> from sympy.physics.mechanics import dynamicsymbols
|
| 120 |
+
>>> from sympy.physics.vector import init_vprinting
|
| 121 |
+
>>> init_vprinting(pretty_print=False)
|
| 122 |
+
>>> m, v, r = dynamicsymbols('m v r')
|
| 123 |
+
>>> N = ReferenceFrame('N')
|
| 124 |
+
>>> O = Point('O')
|
| 125 |
+
>>> A = O.locatenew('A', r * N.x)
|
| 126 |
+
>>> P = Particle('P', A, m)
|
| 127 |
+
>>> P.point.set_vel(N, v * N.y)
|
| 128 |
+
>>> P.angular_momentum(O, N)
|
| 129 |
+
m*r*v*N.z
|
| 130 |
+
|
| 131 |
+
"""
|
| 132 |
+
|
| 133 |
+
return cross(self.point.pos_from(point),
|
| 134 |
+
self.mass * self.point.vel(frame))
|
| 135 |
+
|
| 136 |
+
def kinetic_energy(self, frame):
|
| 137 |
+
"""Kinetic energy of the particle.
|
| 138 |
+
|
| 139 |
+
Explanation
|
| 140 |
+
===========
|
| 141 |
+
|
| 142 |
+
The kinetic energy, T, of a particle, P, is given by:
|
| 143 |
+
|
| 144 |
+
``T = 1/2 (dot(m * v, v))``
|
| 145 |
+
|
| 146 |
+
where m is the mass of particle P, and v is the velocity of the
|
| 147 |
+
particle in the supplied ReferenceFrame.
|
| 148 |
+
|
| 149 |
+
Parameters
|
| 150 |
+
==========
|
| 151 |
+
|
| 152 |
+
frame : ReferenceFrame
|
| 153 |
+
The Particle's velocity is typically defined with respect to
|
| 154 |
+
an inertial frame but any relevant frame in which the velocity is
|
| 155 |
+
known can be supplied.
|
| 156 |
+
|
| 157 |
+
Examples
|
| 158 |
+
========
|
| 159 |
+
|
| 160 |
+
>>> from sympy.physics.mechanics import Particle, Point, ReferenceFrame
|
| 161 |
+
>>> from sympy import symbols
|
| 162 |
+
>>> m, v, r = symbols('m v r')
|
| 163 |
+
>>> N = ReferenceFrame('N')
|
| 164 |
+
>>> O = Point('O')
|
| 165 |
+
>>> P = Particle('P', O, m)
|
| 166 |
+
>>> P.point.set_vel(N, v * N.y)
|
| 167 |
+
>>> P.kinetic_energy(N)
|
| 168 |
+
m*v**2/2
|
| 169 |
+
|
| 170 |
+
"""
|
| 171 |
+
|
| 172 |
+
return S.Half * self.mass * dot(self.point.vel(frame),
|
| 173 |
+
self.point.vel(frame))
|
| 174 |
+
|
| 175 |
+
def set_potential_energy(self, scalar):
|
| 176 |
+
sympy_deprecation_warning(
|
| 177 |
+
"""
|
| 178 |
+
The sympy.physics.mechanics.Particle.set_potential_energy()
|
| 179 |
+
method is deprecated. Instead use
|
| 180 |
+
|
| 181 |
+
P.potential_energy = scalar
|
| 182 |
+
""",
|
| 183 |
+
deprecated_since_version="1.5",
|
| 184 |
+
active_deprecations_target="deprecated-set-potential-energy",
|
| 185 |
+
)
|
| 186 |
+
self.potential_energy = scalar
|
| 187 |
+
|
| 188 |
+
def parallel_axis(self, point, frame):
|
| 189 |
+
"""Returns an inertia dyadic of the particle with respect to another
|
| 190 |
+
point and frame.
|
| 191 |
+
|
| 192 |
+
Parameters
|
| 193 |
+
==========
|
| 194 |
+
|
| 195 |
+
point : sympy.physics.vector.Point
|
| 196 |
+
The point to express the inertia dyadic about.
|
| 197 |
+
frame : sympy.physics.vector.ReferenceFrame
|
| 198 |
+
The reference frame used to construct the dyadic.
|
| 199 |
+
|
| 200 |
+
Returns
|
| 201 |
+
=======
|
| 202 |
+
|
| 203 |
+
inertia : sympy.physics.vector.Dyadic
|
| 204 |
+
The inertia dyadic of the particle expressed about the provided
|
| 205 |
+
point and frame.
|
| 206 |
+
|
| 207 |
+
"""
|
| 208 |
+
return inertia_of_point_mass(self.mass, self.point.pos_from(point),
|
| 209 |
+
frame)
|
lib/python3.12/site-packages/sympy/physics/mechanics/rigidbody.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy import Symbol, S
|
| 2 |
+
from sympy.physics.vector import ReferenceFrame, Dyadic, Point, dot
|
| 3 |
+
from sympy.physics.mechanics.body_base import BodyBase
|
| 4 |
+
from sympy.physics.mechanics.inertia import inertia_of_point_mass, Inertia
|
| 5 |
+
from sympy.utilities.exceptions import sympy_deprecation_warning
|
| 6 |
+
|
| 7 |
+
__all__ = ['RigidBody']
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class RigidBody(BodyBase):
|
| 11 |
+
"""An idealized rigid body.
|
| 12 |
+
|
| 13 |
+
Explanation
|
| 14 |
+
===========
|
| 15 |
+
|
| 16 |
+
This is essentially a container which holds the various components which
|
| 17 |
+
describe a rigid body: a name, mass, center of mass, reference frame, and
|
| 18 |
+
inertia.
|
| 19 |
+
|
| 20 |
+
All of these need to be supplied on creation, but can be changed
|
| 21 |
+
afterwards.
|
| 22 |
+
|
| 23 |
+
Attributes
|
| 24 |
+
==========
|
| 25 |
+
|
| 26 |
+
name : string
|
| 27 |
+
The body's name.
|
| 28 |
+
masscenter : Point
|
| 29 |
+
The point which represents the center of mass of the rigid body.
|
| 30 |
+
frame : ReferenceFrame
|
| 31 |
+
The ReferenceFrame which the rigid body is fixed in.
|
| 32 |
+
mass : Sympifyable
|
| 33 |
+
The body's mass.
|
| 34 |
+
inertia : (Dyadic, Point)
|
| 35 |
+
The body's inertia about a point; stored in a tuple as shown above.
|
| 36 |
+
potential_energy : Sympifyable
|
| 37 |
+
The potential energy of the RigidBody.
|
| 38 |
+
|
| 39 |
+
Examples
|
| 40 |
+
========
|
| 41 |
+
|
| 42 |
+
>>> from sympy import Symbol
|
| 43 |
+
>>> from sympy.physics.mechanics import ReferenceFrame, Point, RigidBody
|
| 44 |
+
>>> from sympy.physics.mechanics import outer
|
| 45 |
+
>>> m = Symbol('m')
|
| 46 |
+
>>> A = ReferenceFrame('A')
|
| 47 |
+
>>> P = Point('P')
|
| 48 |
+
>>> I = outer (A.x, A.x)
|
| 49 |
+
>>> inertia_tuple = (I, P)
|
| 50 |
+
>>> B = RigidBody('B', P, A, m, inertia_tuple)
|
| 51 |
+
>>> # Or you could change them afterwards
|
| 52 |
+
>>> m2 = Symbol('m2')
|
| 53 |
+
>>> B.mass = m2
|
| 54 |
+
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
def __init__(self, name, masscenter=None, frame=None, mass=None,
|
| 58 |
+
inertia=None):
|
| 59 |
+
super().__init__(name, masscenter, mass)
|
| 60 |
+
if frame is None:
|
| 61 |
+
frame = ReferenceFrame(f'{name}_frame')
|
| 62 |
+
self.frame = frame
|
| 63 |
+
if inertia is None:
|
| 64 |
+
ixx = Symbol(f'{name}_ixx')
|
| 65 |
+
iyy = Symbol(f'{name}_iyy')
|
| 66 |
+
izz = Symbol(f'{name}_izz')
|
| 67 |
+
izx = Symbol(f'{name}_izx')
|
| 68 |
+
ixy = Symbol(f'{name}_ixy')
|
| 69 |
+
iyz = Symbol(f'{name}_iyz')
|
| 70 |
+
inertia = Inertia.from_inertia_scalars(self.masscenter, self.frame,
|
| 71 |
+
ixx, iyy, izz, ixy, iyz, izx)
|
| 72 |
+
self.inertia = inertia
|
| 73 |
+
|
| 74 |
+
def __repr__(self):
|
| 75 |
+
return (f'{self.__class__.__name__}({repr(self.name)}, masscenter='
|
| 76 |
+
f'{repr(self.masscenter)}, frame={repr(self.frame)}, mass='
|
| 77 |
+
f'{repr(self.mass)}, inertia={repr(self.inertia)})')
|
| 78 |
+
|
| 79 |
+
@property
|
| 80 |
+
def frame(self):
|
| 81 |
+
"""The ReferenceFrame fixed to the body."""
|
| 82 |
+
return self._frame
|
| 83 |
+
|
| 84 |
+
@frame.setter
|
| 85 |
+
def frame(self, F):
|
| 86 |
+
if not isinstance(F, ReferenceFrame):
|
| 87 |
+
raise TypeError("RigidBody frame must be a ReferenceFrame object.")
|
| 88 |
+
self._frame = F
|
| 89 |
+
|
| 90 |
+
@property
|
| 91 |
+
def x(self):
|
| 92 |
+
"""The basis Vector for the body, in the x direction. """
|
| 93 |
+
return self.frame.x
|
| 94 |
+
|
| 95 |
+
@property
|
| 96 |
+
def y(self):
|
| 97 |
+
"""The basis Vector for the body, in the y direction. """
|
| 98 |
+
return self.frame.y
|
| 99 |
+
|
| 100 |
+
@property
|
| 101 |
+
def z(self):
|
| 102 |
+
"""The basis Vector for the body, in the z direction. """
|
| 103 |
+
return self.frame.z
|
| 104 |
+
|
| 105 |
+
@property
|
| 106 |
+
def inertia(self):
|
| 107 |
+
"""The body's inertia about a point; stored as (Dyadic, Point)."""
|
| 108 |
+
return self._inertia
|
| 109 |
+
|
| 110 |
+
@inertia.setter
|
| 111 |
+
def inertia(self, I):
|
| 112 |
+
# check if I is of the form (Dyadic, Point)
|
| 113 |
+
if len(I) != 2 or not isinstance(I[0], Dyadic) or not isinstance(I[1], Point):
|
| 114 |
+
raise TypeError("RigidBody inertia must be a tuple of the form (Dyadic, Point).")
|
| 115 |
+
|
| 116 |
+
self._inertia = Inertia(I[0], I[1])
|
| 117 |
+
# have I S/O, want I S/S*
|
| 118 |
+
# I S/O = I S/S* + I S*/O; I S/S* = I S/O - I S*/O
|
| 119 |
+
# I_S/S* = I_S/O - I_S*/O
|
| 120 |
+
I_Ss_O = inertia_of_point_mass(self.mass,
|
| 121 |
+
self.masscenter.pos_from(I[1]),
|
| 122 |
+
self.frame)
|
| 123 |
+
self._central_inertia = I[0] - I_Ss_O
|
| 124 |
+
|
| 125 |
+
@property
|
| 126 |
+
def central_inertia(self):
|
| 127 |
+
"""The body's central inertia dyadic."""
|
| 128 |
+
return self._central_inertia
|
| 129 |
+
|
| 130 |
+
@central_inertia.setter
|
| 131 |
+
def central_inertia(self, I):
|
| 132 |
+
if not isinstance(I, Dyadic):
|
| 133 |
+
raise TypeError("RigidBody inertia must be a Dyadic object.")
|
| 134 |
+
self.inertia = Inertia(I, self.masscenter)
|
| 135 |
+
|
| 136 |
+
def linear_momentum(self, frame):
|
| 137 |
+
""" Linear momentum of the rigid body.
|
| 138 |
+
|
| 139 |
+
Explanation
|
| 140 |
+
===========
|
| 141 |
+
|
| 142 |
+
The linear momentum L, of a rigid body B, with respect to frame N is
|
| 143 |
+
given by:
|
| 144 |
+
|
| 145 |
+
``L = m * v``
|
| 146 |
+
|
| 147 |
+
where m is the mass of the rigid body, and v is the velocity of the mass
|
| 148 |
+
center of B in the frame N.
|
| 149 |
+
|
| 150 |
+
Parameters
|
| 151 |
+
==========
|
| 152 |
+
|
| 153 |
+
frame : ReferenceFrame
|
| 154 |
+
The frame in which linear momentum is desired.
|
| 155 |
+
|
| 156 |
+
Examples
|
| 157 |
+
========
|
| 158 |
+
|
| 159 |
+
>>> from sympy.physics.mechanics import Point, ReferenceFrame, outer
|
| 160 |
+
>>> from sympy.physics.mechanics import RigidBody, dynamicsymbols
|
| 161 |
+
>>> from sympy.physics.vector import init_vprinting
|
| 162 |
+
>>> init_vprinting(pretty_print=False)
|
| 163 |
+
>>> m, v = dynamicsymbols('m v')
|
| 164 |
+
>>> N = ReferenceFrame('N')
|
| 165 |
+
>>> P = Point('P')
|
| 166 |
+
>>> P.set_vel(N, v * N.x)
|
| 167 |
+
>>> I = outer (N.x, N.x)
|
| 168 |
+
>>> Inertia_tuple = (I, P)
|
| 169 |
+
>>> B = RigidBody('B', P, N, m, Inertia_tuple)
|
| 170 |
+
>>> B.linear_momentum(N)
|
| 171 |
+
m*v*N.x
|
| 172 |
+
|
| 173 |
+
"""
|
| 174 |
+
|
| 175 |
+
return self.mass * self.masscenter.vel(frame)
|
| 176 |
+
|
| 177 |
+
def angular_momentum(self, point, frame):
|
| 178 |
+
"""Returns the angular momentum of the rigid body about a point in the
|
| 179 |
+
given frame.
|
| 180 |
+
|
| 181 |
+
Explanation
|
| 182 |
+
===========
|
| 183 |
+
|
| 184 |
+
The angular momentum H of a rigid body B about some point O in a frame N
|
| 185 |
+
is given by:
|
| 186 |
+
|
| 187 |
+
``H = dot(I, w) + cross(r, m * v)``
|
| 188 |
+
|
| 189 |
+
where I and m are the central inertia dyadic and mass of rigid body B, w
|
| 190 |
+
is the angular velocity of body B in the frame N, r is the position
|
| 191 |
+
vector from point O to the mass center of B, and v is the velocity of
|
| 192 |
+
the mass center in the frame N.
|
| 193 |
+
|
| 194 |
+
Parameters
|
| 195 |
+
==========
|
| 196 |
+
|
| 197 |
+
point : Point
|
| 198 |
+
The point about which angular momentum is desired.
|
| 199 |
+
frame : ReferenceFrame
|
| 200 |
+
The frame in which angular momentum is desired.
|
| 201 |
+
|
| 202 |
+
Examples
|
| 203 |
+
========
|
| 204 |
+
|
| 205 |
+
>>> from sympy.physics.mechanics import Point, ReferenceFrame, outer
|
| 206 |
+
>>> from sympy.physics.mechanics import RigidBody, dynamicsymbols
|
| 207 |
+
>>> from sympy.physics.vector import init_vprinting
|
| 208 |
+
>>> init_vprinting(pretty_print=False)
|
| 209 |
+
>>> m, v, r, omega = dynamicsymbols('m v r omega')
|
| 210 |
+
>>> N = ReferenceFrame('N')
|
| 211 |
+
>>> b = ReferenceFrame('b')
|
| 212 |
+
>>> b.set_ang_vel(N, omega * b.x)
|
| 213 |
+
>>> P = Point('P')
|
| 214 |
+
>>> P.set_vel(N, 1 * N.x)
|
| 215 |
+
>>> I = outer(b.x, b.x)
|
| 216 |
+
>>> B = RigidBody('B', P, b, m, (I, P))
|
| 217 |
+
>>> B.angular_momentum(P, N)
|
| 218 |
+
omega*b.x
|
| 219 |
+
|
| 220 |
+
"""
|
| 221 |
+
I = self.central_inertia
|
| 222 |
+
w = self.frame.ang_vel_in(frame)
|
| 223 |
+
m = self.mass
|
| 224 |
+
r = self.masscenter.pos_from(point)
|
| 225 |
+
v = self.masscenter.vel(frame)
|
| 226 |
+
|
| 227 |
+
return I.dot(w) + r.cross(m * v)
|
| 228 |
+
|
| 229 |
+
def kinetic_energy(self, frame):
|
| 230 |
+
"""Kinetic energy of the rigid body.
|
| 231 |
+
|
| 232 |
+
Explanation
|
| 233 |
+
===========
|
| 234 |
+
|
| 235 |
+
The kinetic energy, T, of a rigid body, B, is given by:
|
| 236 |
+
|
| 237 |
+
``T = 1/2 * (dot(dot(I, w), w) + dot(m * v, v))``
|
| 238 |
+
|
| 239 |
+
where I and m are the central inertia dyadic and mass of rigid body B
|
| 240 |
+
respectively, w is the body's angular velocity, and v is the velocity of
|
| 241 |
+
the body's mass center in the supplied ReferenceFrame.
|
| 242 |
+
|
| 243 |
+
Parameters
|
| 244 |
+
==========
|
| 245 |
+
|
| 246 |
+
frame : ReferenceFrame
|
| 247 |
+
The RigidBody's angular velocity and the velocity of it's mass
|
| 248 |
+
center are typically defined with respect to an inertial frame but
|
| 249 |
+
any relevant frame in which the velocities are known can be
|
| 250 |
+
supplied.
|
| 251 |
+
|
| 252 |
+
Examples
|
| 253 |
+
========
|
| 254 |
+
|
| 255 |
+
>>> from sympy.physics.mechanics import Point, ReferenceFrame, outer
|
| 256 |
+
>>> from sympy.physics.mechanics import RigidBody
|
| 257 |
+
>>> from sympy import symbols
|
| 258 |
+
>>> m, v, r, omega = symbols('m v r omega')
|
| 259 |
+
>>> N = ReferenceFrame('N')
|
| 260 |
+
>>> b = ReferenceFrame('b')
|
| 261 |
+
>>> b.set_ang_vel(N, omega * b.x)
|
| 262 |
+
>>> P = Point('P')
|
| 263 |
+
>>> P.set_vel(N, v * N.x)
|
| 264 |
+
>>> I = outer (b.x, b.x)
|
| 265 |
+
>>> inertia_tuple = (I, P)
|
| 266 |
+
>>> B = RigidBody('B', P, b, m, inertia_tuple)
|
| 267 |
+
>>> B.kinetic_energy(N)
|
| 268 |
+
m*v**2/2 + omega**2/2
|
| 269 |
+
|
| 270 |
+
"""
|
| 271 |
+
|
| 272 |
+
rotational_KE = S.Half * dot(
|
| 273 |
+
self.frame.ang_vel_in(frame),
|
| 274 |
+
dot(self.central_inertia, self.frame.ang_vel_in(frame)))
|
| 275 |
+
translational_KE = S.Half * self.mass * dot(self.masscenter.vel(frame),
|
| 276 |
+
self.masscenter.vel(frame))
|
| 277 |
+
return rotational_KE + translational_KE
|
| 278 |
+
|
| 279 |
+
def set_potential_energy(self, scalar):
|
| 280 |
+
sympy_deprecation_warning(
|
| 281 |
+
"""
|
| 282 |
+
The sympy.physics.mechanics.RigidBody.set_potential_energy()
|
| 283 |
+
method is deprecated. Instead use
|
| 284 |
+
|
| 285 |
+
B.potential_energy = scalar
|
| 286 |
+
""",
|
| 287 |
+
deprecated_since_version="1.5",
|
| 288 |
+
active_deprecations_target="deprecated-set-potential-energy",
|
| 289 |
+
)
|
| 290 |
+
self.potential_energy = scalar
|
| 291 |
+
|
| 292 |
+
def parallel_axis(self, point, frame=None):
|
| 293 |
+
"""Returns the inertia dyadic of the body with respect to another point.
|
| 294 |
+
|
| 295 |
+
Parameters
|
| 296 |
+
==========
|
| 297 |
+
|
| 298 |
+
point : sympy.physics.vector.Point
|
| 299 |
+
The point to express the inertia dyadic about.
|
| 300 |
+
frame : sympy.physics.vector.ReferenceFrame
|
| 301 |
+
The reference frame used to construct the dyadic.
|
| 302 |
+
|
| 303 |
+
Returns
|
| 304 |
+
=======
|
| 305 |
+
|
| 306 |
+
inertia : sympy.physics.vector.Dyadic
|
| 307 |
+
The inertia dyadic of the rigid body expressed about the provided
|
| 308 |
+
point.
|
| 309 |
+
|
| 310 |
+
"""
|
| 311 |
+
if frame is None:
|
| 312 |
+
frame = self.frame
|
| 313 |
+
return self.central_inertia + inertia_of_point_mass(
|
| 314 |
+
self.mass, self.masscenter.pos_from(point), frame)
|
lib/python3.12/site-packages/sympy/physics/mechanics/system.py
ADDED
|
@@ -0,0 +1,1553 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from functools import wraps
|
| 2 |
+
|
| 3 |
+
from sympy.core.basic import Basic
|
| 4 |
+
from sympy.matrices.immutable import ImmutableMatrix
|
| 5 |
+
from sympy.matrices.dense import Matrix, eye, zeros
|
| 6 |
+
from sympy.core.containers import OrderedSet
|
| 7 |
+
from sympy.physics.mechanics.actuator import ActuatorBase
|
| 8 |
+
from sympy.physics.mechanics.body_base import BodyBase
|
| 9 |
+
from sympy.physics.mechanics.functions import (
|
| 10 |
+
Lagrangian, _validate_coordinates, find_dynamicsymbols)
|
| 11 |
+
from sympy.physics.mechanics.joint import Joint
|
| 12 |
+
from sympy.physics.mechanics.kane import KanesMethod
|
| 13 |
+
from sympy.physics.mechanics.lagrange import LagrangesMethod
|
| 14 |
+
from sympy.physics.mechanics.loads import _parse_load, gravity
|
| 15 |
+
from sympy.physics.mechanics.method import _Methods
|
| 16 |
+
from sympy.physics.mechanics.particle import Particle
|
| 17 |
+
from sympy.physics.vector import Point, ReferenceFrame, dynamicsymbols
|
| 18 |
+
from sympy.utilities.iterables import iterable
|
| 19 |
+
from sympy.utilities.misc import filldedent
|
| 20 |
+
|
| 21 |
+
__all__ = ['SymbolicSystem', 'System']
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _reset_eom_method(method):
|
| 25 |
+
"""Decorator to reset the eom_method if a property is changed."""
|
| 26 |
+
|
| 27 |
+
@wraps(method)
|
| 28 |
+
def wrapper(self, *args, **kwargs):
|
| 29 |
+
self._eom_method = None
|
| 30 |
+
return method(self, *args, **kwargs)
|
| 31 |
+
|
| 32 |
+
return wrapper
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class System(_Methods):
|
| 36 |
+
"""Class to define a multibody system and form its equations of motion.
|
| 37 |
+
|
| 38 |
+
Explanation
|
| 39 |
+
===========
|
| 40 |
+
|
| 41 |
+
A ``System`` instance stores the different objects associated with a model,
|
| 42 |
+
including bodies, joints, constraints, and other relevant information. With
|
| 43 |
+
all the relationships between components defined, the ``System`` can be used
|
| 44 |
+
to form the equations of motion using a backend, such as ``KanesMethod``.
|
| 45 |
+
The ``System`` has been designed to be compatible with third-party
|
| 46 |
+
libraries for greater flexibility and integration with other tools.
|
| 47 |
+
|
| 48 |
+
Attributes
|
| 49 |
+
==========
|
| 50 |
+
|
| 51 |
+
frame : ReferenceFrame
|
| 52 |
+
Inertial reference frame of the system.
|
| 53 |
+
fixed_point : Point
|
| 54 |
+
A fixed point in the inertial reference frame.
|
| 55 |
+
x : Vector
|
| 56 |
+
Unit vector fixed in the inertial reference frame.
|
| 57 |
+
y : Vector
|
| 58 |
+
Unit vector fixed in the inertial reference frame.
|
| 59 |
+
z : Vector
|
| 60 |
+
Unit vector fixed in the inertial reference frame.
|
| 61 |
+
q : ImmutableMatrix
|
| 62 |
+
Matrix of all the generalized coordinates, i.e. the independent
|
| 63 |
+
generalized coordinates stacked upon the dependent.
|
| 64 |
+
u : ImmutableMatrix
|
| 65 |
+
Matrix of all the generalized speeds, i.e. the independent generealized
|
| 66 |
+
speeds stacked upon the dependent.
|
| 67 |
+
q_ind : ImmutableMatrix
|
| 68 |
+
Matrix of the independent generalized coordinates.
|
| 69 |
+
q_dep : ImmutableMatrix
|
| 70 |
+
Matrix of the dependent generalized coordinates.
|
| 71 |
+
u_ind : ImmutableMatrix
|
| 72 |
+
Matrix of the independent generalized speeds.
|
| 73 |
+
u_dep : ImmutableMatrix
|
| 74 |
+
Matrix of the dependent generalized speeds.
|
| 75 |
+
u_aux : ImmutableMatrix
|
| 76 |
+
Matrix of auxiliary generalized speeds.
|
| 77 |
+
kdes : ImmutableMatrix
|
| 78 |
+
Matrix of the kinematical differential equations as expressions equated
|
| 79 |
+
to the zero matrix.
|
| 80 |
+
bodies : tuple of BodyBase subclasses
|
| 81 |
+
Tuple of all bodies that make up the system.
|
| 82 |
+
joints : tuple of Joint
|
| 83 |
+
Tuple of all joints that connect bodies in the system.
|
| 84 |
+
loads : tuple of LoadBase subclasses
|
| 85 |
+
Tuple of all loads that have been applied to the system.
|
| 86 |
+
actuators : tuple of ActuatorBase subclasses
|
| 87 |
+
Tuple of all actuators present in the system.
|
| 88 |
+
holonomic_constraints : ImmutableMatrix
|
| 89 |
+
Matrix with the holonomic constraints as expressions equated to the zero
|
| 90 |
+
matrix.
|
| 91 |
+
nonholonomic_constraints : ImmutableMatrix
|
| 92 |
+
Matrix with the nonholonomic constraints as expressions equated to the
|
| 93 |
+
zero matrix.
|
| 94 |
+
velocity_constraints : ImmutableMatrix
|
| 95 |
+
Matrix with the velocity constraints as expressions equated to the zero
|
| 96 |
+
matrix. These are by default derived as the time derivatives of the
|
| 97 |
+
holonomic constraints extended with the nonholonomic constraints.
|
| 98 |
+
eom_method : subclass of KanesMethod or LagrangesMethod
|
| 99 |
+
Backend for forming the equations of motion.
|
| 100 |
+
|
| 101 |
+
Examples
|
| 102 |
+
========
|
| 103 |
+
|
| 104 |
+
In the example below a cart with a pendulum is created. The cart moves along
|
| 105 |
+
the x axis of the rail and the pendulum rotates about the z axis. The length
|
| 106 |
+
of the pendulum is ``l`` with the pendulum represented as a particle. To
|
| 107 |
+
move the cart a time dependent force ``F`` is applied to the cart.
|
| 108 |
+
|
| 109 |
+
We first need to import some functions and create some of our variables.
|
| 110 |
+
|
| 111 |
+
>>> from sympy import symbols, simplify
|
| 112 |
+
>>> from sympy.physics.mechanics import (
|
| 113 |
+
... mechanics_printing, dynamicsymbols, RigidBody, Particle,
|
| 114 |
+
... ReferenceFrame, PrismaticJoint, PinJoint, System)
|
| 115 |
+
>>> mechanics_printing(pretty_print=False)
|
| 116 |
+
>>> g, l = symbols('g l')
|
| 117 |
+
>>> F = dynamicsymbols('F')
|
| 118 |
+
|
| 119 |
+
The next step is to create bodies. It is also useful to create a frame for
|
| 120 |
+
locating the particle with respect to the pin joint later on, as a particle
|
| 121 |
+
does not have a body-fixed frame.
|
| 122 |
+
|
| 123 |
+
>>> rail = RigidBody('rail')
|
| 124 |
+
>>> cart = RigidBody('cart')
|
| 125 |
+
>>> bob = Particle('bob')
|
| 126 |
+
>>> bob_frame = ReferenceFrame('bob_frame')
|
| 127 |
+
|
| 128 |
+
Initialize the system, with the rail as the Newtonian reference. The body is
|
| 129 |
+
also automatically added to the system.
|
| 130 |
+
|
| 131 |
+
>>> system = System.from_newtonian(rail)
|
| 132 |
+
>>> print(system.bodies[0])
|
| 133 |
+
rail
|
| 134 |
+
|
| 135 |
+
Create the joints, while immediately also adding them to the system.
|
| 136 |
+
|
| 137 |
+
>>> system.add_joints(
|
| 138 |
+
... PrismaticJoint('slider', rail, cart, joint_axis=rail.x),
|
| 139 |
+
... PinJoint('pin', cart, bob, joint_axis=cart.z,
|
| 140 |
+
... child_interframe=bob_frame,
|
| 141 |
+
... child_point=l * bob_frame.y)
|
| 142 |
+
... )
|
| 143 |
+
>>> system.joints
|
| 144 |
+
(PrismaticJoint: slider parent: rail child: cart,
|
| 145 |
+
PinJoint: pin parent: cart child: bob)
|
| 146 |
+
|
| 147 |
+
While adding the joints, the associated generalized coordinates, generalized
|
| 148 |
+
speeds, kinematic differential equations and bodies are also added to the
|
| 149 |
+
system.
|
| 150 |
+
|
| 151 |
+
>>> system.q
|
| 152 |
+
Matrix([
|
| 153 |
+
[q_slider],
|
| 154 |
+
[ q_pin]])
|
| 155 |
+
>>> system.u
|
| 156 |
+
Matrix([
|
| 157 |
+
[u_slider],
|
| 158 |
+
[ u_pin]])
|
| 159 |
+
>>> system.kdes
|
| 160 |
+
Matrix([
|
| 161 |
+
[u_slider - q_slider'],
|
| 162 |
+
[ u_pin - q_pin']])
|
| 163 |
+
>>> [body.name for body in system.bodies]
|
| 164 |
+
['rail', 'cart', 'bob']
|
| 165 |
+
|
| 166 |
+
With the kinematics established, we can now apply gravity and the cart force
|
| 167 |
+
``F``.
|
| 168 |
+
|
| 169 |
+
>>> system.apply_uniform_gravity(-g * system.y)
|
| 170 |
+
>>> system.add_loads((cart.masscenter, F * rail.x))
|
| 171 |
+
>>> system.loads
|
| 172 |
+
((rail_masscenter, - g*rail_mass*rail_frame.y),
|
| 173 |
+
(cart_masscenter, - cart_mass*g*rail_frame.y),
|
| 174 |
+
(bob_masscenter, - bob_mass*g*rail_frame.y),
|
| 175 |
+
(cart_masscenter, F*rail_frame.x))
|
| 176 |
+
|
| 177 |
+
With the entire system defined, we can now form the equations of motion.
|
| 178 |
+
Before forming the equations of motion, one can also run some checks that
|
| 179 |
+
will try to identify some common errors.
|
| 180 |
+
|
| 181 |
+
>>> system.validate_system()
|
| 182 |
+
>>> system.form_eoms()
|
| 183 |
+
Matrix([
|
| 184 |
+
[bob_mass*l*u_pin**2*sin(q_pin) - bob_mass*l*cos(q_pin)*u_pin'
|
| 185 |
+
- (bob_mass + cart_mass)*u_slider' + F],
|
| 186 |
+
[ -bob_mass*g*l*sin(q_pin) - bob_mass*l**2*u_pin'
|
| 187 |
+
- bob_mass*l*cos(q_pin)*u_slider']])
|
| 188 |
+
>>> simplify(system.mass_matrix)
|
| 189 |
+
Matrix([
|
| 190 |
+
[ bob_mass + cart_mass, bob_mass*l*cos(q_pin)],
|
| 191 |
+
[bob_mass*l*cos(q_pin), bob_mass*l**2]])
|
| 192 |
+
>>> system.forcing
|
| 193 |
+
Matrix([
|
| 194 |
+
[bob_mass*l*u_pin**2*sin(q_pin) + F],
|
| 195 |
+
[ -bob_mass*g*l*sin(q_pin)]])
|
| 196 |
+
|
| 197 |
+
The complexity of the above example can be increased if we add a constraint
|
| 198 |
+
to prevent the particle from moving in the horizontal (x) direction. This
|
| 199 |
+
can be done by adding a holonomic constraint. After which we should also
|
| 200 |
+
redefine what our (in)dependent generalized coordinates and speeds are.
|
| 201 |
+
|
| 202 |
+
>>> system.add_holonomic_constraints(
|
| 203 |
+
... bob.masscenter.pos_from(rail.masscenter).dot(system.x)
|
| 204 |
+
... )
|
| 205 |
+
>>> system.q_ind = system.get_joint('pin').coordinates
|
| 206 |
+
>>> system.q_dep = system.get_joint('slider').coordinates
|
| 207 |
+
>>> system.u_ind = system.get_joint('pin').speeds
|
| 208 |
+
>>> system.u_dep = system.get_joint('slider').speeds
|
| 209 |
+
|
| 210 |
+
With the updated system the equations of motion can be formed again.
|
| 211 |
+
|
| 212 |
+
>>> system.validate_system()
|
| 213 |
+
>>> system.form_eoms()
|
| 214 |
+
Matrix([[-bob_mass*g*l*sin(q_pin)
|
| 215 |
+
- bob_mass*l**2*u_pin'
|
| 216 |
+
- bob_mass*l*cos(q_pin)*u_slider'
|
| 217 |
+
- l*(bob_mass*l*u_pin**2*sin(q_pin)
|
| 218 |
+
- bob_mass*l*cos(q_pin)*u_pin'
|
| 219 |
+
- (bob_mass + cart_mass)*u_slider')*cos(q_pin)
|
| 220 |
+
- l*F*cos(q_pin)]])
|
| 221 |
+
>>> simplify(system.mass_matrix)
|
| 222 |
+
Matrix([
|
| 223 |
+
[bob_mass*l**2*sin(q_pin)**2, -cart_mass*l*cos(q_pin)],
|
| 224 |
+
[ l*cos(q_pin), 1]])
|
| 225 |
+
>>> simplify(system.forcing)
|
| 226 |
+
Matrix([
|
| 227 |
+
[-l*(bob_mass*g*sin(q_pin) + bob_mass*l*u_pin**2*sin(2*q_pin)/2
|
| 228 |
+
+ F*cos(q_pin))],
|
| 229 |
+
[
|
| 230 |
+
l*u_pin**2*sin(q_pin)]])
|
| 231 |
+
|
| 232 |
+
"""
|
| 233 |
+
|
| 234 |
+
def __init__(self, frame=None, fixed_point=None):
|
| 235 |
+
"""Initialize the system.
|
| 236 |
+
|
| 237 |
+
Parameters
|
| 238 |
+
==========
|
| 239 |
+
|
| 240 |
+
frame : ReferenceFrame, optional
|
| 241 |
+
The inertial frame of the system. If none is supplied, a new frame
|
| 242 |
+
will be created.
|
| 243 |
+
fixed_point : Point, optional
|
| 244 |
+
A fixed point in the inertial reference frame. If none is supplied,
|
| 245 |
+
a new fixed_point will be created.
|
| 246 |
+
|
| 247 |
+
"""
|
| 248 |
+
if frame is None:
|
| 249 |
+
frame = ReferenceFrame('inertial_frame')
|
| 250 |
+
elif not isinstance(frame, ReferenceFrame):
|
| 251 |
+
raise TypeError('Frame must be an instance of ReferenceFrame.')
|
| 252 |
+
self._frame = frame
|
| 253 |
+
if fixed_point is None:
|
| 254 |
+
fixed_point = Point('inertial_point')
|
| 255 |
+
elif not isinstance(fixed_point, Point):
|
| 256 |
+
raise TypeError('Fixed point must be an instance of Point.')
|
| 257 |
+
self._fixed_point = fixed_point
|
| 258 |
+
self._fixed_point.set_vel(self._frame, 0)
|
| 259 |
+
self._q_ind = ImmutableMatrix(1, 0, []).T
|
| 260 |
+
self._q_dep = ImmutableMatrix(1, 0, []).T
|
| 261 |
+
self._u_ind = ImmutableMatrix(1, 0, []).T
|
| 262 |
+
self._u_dep = ImmutableMatrix(1, 0, []).T
|
| 263 |
+
self._u_aux = ImmutableMatrix(1, 0, []).T
|
| 264 |
+
self._kdes = ImmutableMatrix(1, 0, []).T
|
| 265 |
+
self._hol_coneqs = ImmutableMatrix(1, 0, []).T
|
| 266 |
+
self._nonhol_coneqs = ImmutableMatrix(1, 0, []).T
|
| 267 |
+
self._vel_constrs = None
|
| 268 |
+
self._bodies = []
|
| 269 |
+
self._joints = []
|
| 270 |
+
self._loads = []
|
| 271 |
+
self._actuators = []
|
| 272 |
+
self._eom_method = None
|
| 273 |
+
|
| 274 |
+
@classmethod
|
| 275 |
+
def from_newtonian(cls, newtonian):
|
| 276 |
+
"""Constructs the system with respect to a Newtonian body."""
|
| 277 |
+
if isinstance(newtonian, Particle):
|
| 278 |
+
raise TypeError('A Particle has no frame so cannot act as '
|
| 279 |
+
'the Newtonian.')
|
| 280 |
+
system = cls(frame=newtonian.frame, fixed_point=newtonian.masscenter)
|
| 281 |
+
system.add_bodies(newtonian)
|
| 282 |
+
return system
|
| 283 |
+
|
| 284 |
+
@property
|
| 285 |
+
def fixed_point(self):
|
| 286 |
+
"""Fixed point in the inertial reference frame."""
|
| 287 |
+
return self._fixed_point
|
| 288 |
+
|
| 289 |
+
@property
|
| 290 |
+
def frame(self):
|
| 291 |
+
"""Inertial reference frame of the system."""
|
| 292 |
+
return self._frame
|
| 293 |
+
|
| 294 |
+
@property
|
| 295 |
+
def x(self):
|
| 296 |
+
"""Unit vector fixed in the inertial reference frame."""
|
| 297 |
+
return self._frame.x
|
| 298 |
+
|
| 299 |
+
@property
|
| 300 |
+
def y(self):
|
| 301 |
+
"""Unit vector fixed in the inertial reference frame."""
|
| 302 |
+
return self._frame.y
|
| 303 |
+
|
| 304 |
+
@property
|
| 305 |
+
def z(self):
|
| 306 |
+
"""Unit vector fixed in the inertial reference frame."""
|
| 307 |
+
return self._frame.z
|
| 308 |
+
|
| 309 |
+
@property
|
| 310 |
+
def bodies(self):
|
| 311 |
+
"""Tuple of all bodies that have been added to the system."""
|
| 312 |
+
return tuple(self._bodies)
|
| 313 |
+
|
| 314 |
+
@bodies.setter
|
| 315 |
+
@_reset_eom_method
|
| 316 |
+
def bodies(self, bodies):
|
| 317 |
+
bodies = self._objects_to_list(bodies)
|
| 318 |
+
self._check_objects(bodies, [], BodyBase, 'Bodies', 'bodies')
|
| 319 |
+
self._bodies = bodies
|
| 320 |
+
|
| 321 |
+
@property
|
| 322 |
+
def joints(self):
|
| 323 |
+
"""Tuple of all joints that have been added to the system."""
|
| 324 |
+
return tuple(self._joints)
|
| 325 |
+
|
| 326 |
+
@joints.setter
|
| 327 |
+
@_reset_eom_method
|
| 328 |
+
def joints(self, joints):
|
| 329 |
+
joints = self._objects_to_list(joints)
|
| 330 |
+
self._check_objects(joints, [], Joint, 'Joints', 'joints')
|
| 331 |
+
self._joints = []
|
| 332 |
+
self.add_joints(*joints)
|
| 333 |
+
|
| 334 |
+
@property
|
| 335 |
+
def loads(self):
|
| 336 |
+
"""Tuple of loads that have been applied on the system."""
|
| 337 |
+
return tuple(self._loads)
|
| 338 |
+
|
| 339 |
+
@loads.setter
|
| 340 |
+
@_reset_eom_method
|
| 341 |
+
def loads(self, loads):
|
| 342 |
+
loads = self._objects_to_list(loads)
|
| 343 |
+
self._loads = [_parse_load(load) for load in loads]
|
| 344 |
+
|
| 345 |
+
@property
|
| 346 |
+
def actuators(self):
|
| 347 |
+
"""Tuple of actuators present in the system."""
|
| 348 |
+
return tuple(self._actuators)
|
| 349 |
+
|
| 350 |
+
@actuators.setter
|
| 351 |
+
@_reset_eom_method
|
| 352 |
+
def actuators(self, actuators):
|
| 353 |
+
actuators = self._objects_to_list(actuators)
|
| 354 |
+
self._check_objects(actuators, [], ActuatorBase, 'Actuators',
|
| 355 |
+
'actuators')
|
| 356 |
+
self._actuators = actuators
|
| 357 |
+
|
| 358 |
+
@property
|
| 359 |
+
def q(self):
|
| 360 |
+
"""Matrix of all the generalized coordinates with the independent
|
| 361 |
+
stacked upon the dependent."""
|
| 362 |
+
return self._q_ind.col_join(self._q_dep)
|
| 363 |
+
|
| 364 |
+
@property
|
| 365 |
+
def u(self):
|
| 366 |
+
"""Matrix of all the generalized speeds with the independent stacked
|
| 367 |
+
upon the dependent."""
|
| 368 |
+
return self._u_ind.col_join(self._u_dep)
|
| 369 |
+
|
| 370 |
+
@property
|
| 371 |
+
def q_ind(self):
|
| 372 |
+
"""Matrix of the independent generalized coordinates."""
|
| 373 |
+
return self._q_ind
|
| 374 |
+
|
| 375 |
+
@q_ind.setter
|
| 376 |
+
@_reset_eom_method
|
| 377 |
+
def q_ind(self, q_ind):
|
| 378 |
+
self._q_ind, self._q_dep = self._parse_coordinates(
|
| 379 |
+
self._objects_to_list(q_ind), True, [], self.q_dep, 'coordinates')
|
| 380 |
+
|
| 381 |
+
@property
|
| 382 |
+
def q_dep(self):
|
| 383 |
+
"""Matrix of the dependent generalized coordinates."""
|
| 384 |
+
return self._q_dep
|
| 385 |
+
|
| 386 |
+
@q_dep.setter
|
| 387 |
+
@_reset_eom_method
|
| 388 |
+
def q_dep(self, q_dep):
|
| 389 |
+
self._q_ind, self._q_dep = self._parse_coordinates(
|
| 390 |
+
self._objects_to_list(q_dep), False, self.q_ind, [], 'coordinates')
|
| 391 |
+
|
| 392 |
+
@property
|
| 393 |
+
def u_ind(self):
|
| 394 |
+
"""Matrix of the independent generalized speeds."""
|
| 395 |
+
return self._u_ind
|
| 396 |
+
|
| 397 |
+
@u_ind.setter
|
| 398 |
+
@_reset_eom_method
|
| 399 |
+
def u_ind(self, u_ind):
|
| 400 |
+
self._u_ind, self._u_dep = self._parse_coordinates(
|
| 401 |
+
self._objects_to_list(u_ind), True, [], self.u_dep, 'speeds')
|
| 402 |
+
|
| 403 |
+
@property
|
| 404 |
+
def u_dep(self):
|
| 405 |
+
"""Matrix of the dependent generalized speeds."""
|
| 406 |
+
return self._u_dep
|
| 407 |
+
|
| 408 |
+
@u_dep.setter
|
| 409 |
+
@_reset_eom_method
|
| 410 |
+
def u_dep(self, u_dep):
|
| 411 |
+
self._u_ind, self._u_dep = self._parse_coordinates(
|
| 412 |
+
self._objects_to_list(u_dep), False, self.u_ind, [], 'speeds')
|
| 413 |
+
|
| 414 |
+
@property
|
| 415 |
+
def u_aux(self):
|
| 416 |
+
"""Matrix of auxiliary generalized speeds."""
|
| 417 |
+
return self._u_aux
|
| 418 |
+
|
| 419 |
+
@u_aux.setter
|
| 420 |
+
@_reset_eom_method
|
| 421 |
+
def u_aux(self, u_aux):
|
| 422 |
+
self._u_aux = self._parse_coordinates(
|
| 423 |
+
self._objects_to_list(u_aux), True, [], [], 'u_auxiliary')[0]
|
| 424 |
+
|
| 425 |
+
@property
|
| 426 |
+
def kdes(self):
|
| 427 |
+
"""Kinematical differential equations as expressions equated to the zero
|
| 428 |
+
matrix. These equations describe the coupling between the generalized
|
| 429 |
+
coordinates and the generalized speeds."""
|
| 430 |
+
return self._kdes
|
| 431 |
+
|
| 432 |
+
@kdes.setter
|
| 433 |
+
@_reset_eom_method
|
| 434 |
+
def kdes(self, kdes):
|
| 435 |
+
kdes = self._objects_to_list(kdes)
|
| 436 |
+
self._kdes = self._parse_expressions(
|
| 437 |
+
kdes, [], 'kinematic differential equations')
|
| 438 |
+
|
| 439 |
+
@property
|
| 440 |
+
def holonomic_constraints(self):
|
| 441 |
+
"""Matrix with the holonomic constraints as expressions equated to the
|
| 442 |
+
zero matrix."""
|
| 443 |
+
return self._hol_coneqs
|
| 444 |
+
|
| 445 |
+
@holonomic_constraints.setter
|
| 446 |
+
@_reset_eom_method
|
| 447 |
+
def holonomic_constraints(self, constraints):
|
| 448 |
+
constraints = self._objects_to_list(constraints)
|
| 449 |
+
self._hol_coneqs = self._parse_expressions(
|
| 450 |
+
constraints, [], 'holonomic constraints')
|
| 451 |
+
|
| 452 |
+
@property
|
| 453 |
+
def nonholonomic_constraints(self):
|
| 454 |
+
"""Matrix with the nonholonomic constraints as expressions equated to
|
| 455 |
+
the zero matrix."""
|
| 456 |
+
return self._nonhol_coneqs
|
| 457 |
+
|
| 458 |
+
@nonholonomic_constraints.setter
|
| 459 |
+
@_reset_eom_method
|
| 460 |
+
def nonholonomic_constraints(self, constraints):
|
| 461 |
+
constraints = self._objects_to_list(constraints)
|
| 462 |
+
self._nonhol_coneqs = self._parse_expressions(
|
| 463 |
+
constraints, [], 'nonholonomic constraints')
|
| 464 |
+
|
| 465 |
+
@property
|
| 466 |
+
def velocity_constraints(self):
|
| 467 |
+
"""Matrix with the velocity constraints as expressions equated to the
|
| 468 |
+
zero matrix. The velocity constraints are by default derived from the
|
| 469 |
+
holonomic and nonholonomic constraints unless they are explicitly set.
|
| 470 |
+
"""
|
| 471 |
+
if self._vel_constrs is None:
|
| 472 |
+
return self.holonomic_constraints.diff(dynamicsymbols._t).col_join(
|
| 473 |
+
self.nonholonomic_constraints)
|
| 474 |
+
return self._vel_constrs
|
| 475 |
+
|
| 476 |
+
@velocity_constraints.setter
|
| 477 |
+
@_reset_eom_method
|
| 478 |
+
def velocity_constraints(self, constraints):
|
| 479 |
+
if constraints is None:
|
| 480 |
+
self._vel_constrs = None
|
| 481 |
+
return
|
| 482 |
+
constraints = self._objects_to_list(constraints)
|
| 483 |
+
self._vel_constrs = self._parse_expressions(
|
| 484 |
+
constraints, [], 'velocity constraints')
|
| 485 |
+
|
| 486 |
+
@property
|
| 487 |
+
def eom_method(self):
|
| 488 |
+
"""Backend for forming the equations of motion."""
|
| 489 |
+
return self._eom_method
|
| 490 |
+
|
| 491 |
+
@staticmethod
|
| 492 |
+
def _objects_to_list(lst):
|
| 493 |
+
"""Helper to convert passed objects to a list."""
|
| 494 |
+
if not iterable(lst): # Only one object
|
| 495 |
+
return [lst]
|
| 496 |
+
return list(lst[:]) # converts Matrix and tuple to flattened list
|
| 497 |
+
|
| 498 |
+
@staticmethod
|
| 499 |
+
def _check_objects(objects, obj_lst, expected_type, obj_name, type_name):
|
| 500 |
+
"""Helper to check the objects that are being added to the system.
|
| 501 |
+
|
| 502 |
+
Explanation
|
| 503 |
+
===========
|
| 504 |
+
This method checks that the objects that are being added to the system
|
| 505 |
+
are of the correct type and have not already been added. If any of the
|
| 506 |
+
objects are not of the correct type or have already been added, then
|
| 507 |
+
an error is raised.
|
| 508 |
+
|
| 509 |
+
Parameters
|
| 510 |
+
==========
|
| 511 |
+
objects : iterable
|
| 512 |
+
The objects that would be added to the system.
|
| 513 |
+
obj_lst : list
|
| 514 |
+
The list of objects that are already in the system.
|
| 515 |
+
expected_type : type
|
| 516 |
+
The type that the objects should be.
|
| 517 |
+
obj_name : str
|
| 518 |
+
The name of the category of objects. This string is used to
|
| 519 |
+
formulate the error message for the user.
|
| 520 |
+
type_name : str
|
| 521 |
+
The name of the type that the objects should be. This string is used
|
| 522 |
+
to formulate the error message for the user.
|
| 523 |
+
|
| 524 |
+
"""
|
| 525 |
+
seen = set(obj_lst)
|
| 526 |
+
duplicates = set()
|
| 527 |
+
wrong_types = set()
|
| 528 |
+
for obj in objects:
|
| 529 |
+
if not isinstance(obj, expected_type):
|
| 530 |
+
wrong_types.add(obj)
|
| 531 |
+
if obj in seen:
|
| 532 |
+
duplicates.add(obj)
|
| 533 |
+
else:
|
| 534 |
+
seen.add(obj)
|
| 535 |
+
if wrong_types:
|
| 536 |
+
raise TypeError(f'{obj_name} {wrong_types} are not {type_name}.')
|
| 537 |
+
if duplicates:
|
| 538 |
+
raise ValueError(f'{obj_name} {duplicates} have already been added '
|
| 539 |
+
f'to the system.')
|
| 540 |
+
|
| 541 |
+
def _parse_coordinates(self, new_coords, independent, old_coords_ind,
|
| 542 |
+
old_coords_dep, coord_type='coordinates'):
|
| 543 |
+
"""Helper to parse coordinates and speeds."""
|
| 544 |
+
# Construct lists of the independent and dependent coordinates
|
| 545 |
+
coords_ind, coords_dep = old_coords_ind[:], old_coords_dep[:]
|
| 546 |
+
if not iterable(independent):
|
| 547 |
+
independent = [independent] * len(new_coords)
|
| 548 |
+
for coord, indep in zip(new_coords, independent):
|
| 549 |
+
if indep:
|
| 550 |
+
coords_ind.append(coord)
|
| 551 |
+
else:
|
| 552 |
+
coords_dep.append(coord)
|
| 553 |
+
# Check types and duplicates
|
| 554 |
+
current = {'coordinates': self.q_ind[:] + self.q_dep[:],
|
| 555 |
+
'speeds': self.u_ind[:] + self.u_dep[:],
|
| 556 |
+
'u_auxiliary': self._u_aux[:],
|
| 557 |
+
coord_type: coords_ind + coords_dep}
|
| 558 |
+
_validate_coordinates(**current)
|
| 559 |
+
return (ImmutableMatrix(1, len(coords_ind), coords_ind).T,
|
| 560 |
+
ImmutableMatrix(1, len(coords_dep), coords_dep).T)
|
| 561 |
+
|
| 562 |
+
@staticmethod
|
| 563 |
+
def _parse_expressions(new_expressions, old_expressions, name,
|
| 564 |
+
check_negatives=False):
|
| 565 |
+
"""Helper to parse expressions like constraints."""
|
| 566 |
+
old_expressions = old_expressions[:]
|
| 567 |
+
new_expressions = list(new_expressions) # Converts a possible tuple
|
| 568 |
+
if check_negatives:
|
| 569 |
+
check_exprs = old_expressions + [-expr for expr in old_expressions]
|
| 570 |
+
else:
|
| 571 |
+
check_exprs = old_expressions
|
| 572 |
+
System._check_objects(new_expressions, check_exprs, Basic, name,
|
| 573 |
+
'expressions')
|
| 574 |
+
for expr in new_expressions:
|
| 575 |
+
if expr == 0:
|
| 576 |
+
raise ValueError(f'Parsed {name} are zero.')
|
| 577 |
+
return ImmutableMatrix(1, len(old_expressions) + len(new_expressions),
|
| 578 |
+
old_expressions + new_expressions).T
|
| 579 |
+
|
| 580 |
+
@_reset_eom_method
|
| 581 |
+
def add_coordinates(self, *coordinates, independent=True):
|
| 582 |
+
"""Add generalized coordinate(s) to the system.
|
| 583 |
+
|
| 584 |
+
Parameters
|
| 585 |
+
==========
|
| 586 |
+
|
| 587 |
+
*coordinates : dynamicsymbols
|
| 588 |
+
One or more generalized coordinates to be added to the system.
|
| 589 |
+
independent : bool or list of bool, optional
|
| 590 |
+
Boolean whether a coordinate is dependent or independent. The
|
| 591 |
+
default is True, so the coordinates are added as independent by
|
| 592 |
+
default.
|
| 593 |
+
|
| 594 |
+
"""
|
| 595 |
+
self._q_ind, self._q_dep = self._parse_coordinates(
|
| 596 |
+
coordinates, independent, self.q_ind, self.q_dep, 'coordinates')
|
| 597 |
+
|
| 598 |
+
@_reset_eom_method
|
| 599 |
+
def add_speeds(self, *speeds, independent=True):
|
| 600 |
+
"""Add generalized speed(s) to the system.
|
| 601 |
+
|
| 602 |
+
Parameters
|
| 603 |
+
==========
|
| 604 |
+
|
| 605 |
+
*speeds : dynamicsymbols
|
| 606 |
+
One or more generalized speeds to be added to the system.
|
| 607 |
+
independent : bool or list of bool, optional
|
| 608 |
+
Boolean whether a speed is dependent or independent. The default is
|
| 609 |
+
True, so the speeds are added as independent by default.
|
| 610 |
+
|
| 611 |
+
"""
|
| 612 |
+
self._u_ind, self._u_dep = self._parse_coordinates(
|
| 613 |
+
speeds, independent, self.u_ind, self.u_dep, 'speeds')
|
| 614 |
+
|
| 615 |
+
@_reset_eom_method
|
| 616 |
+
def add_auxiliary_speeds(self, *speeds):
|
| 617 |
+
"""Add auxiliary speed(s) to the system.
|
| 618 |
+
|
| 619 |
+
Parameters
|
| 620 |
+
==========
|
| 621 |
+
|
| 622 |
+
*speeds : dynamicsymbols
|
| 623 |
+
One or more auxiliary speeds to be added to the system.
|
| 624 |
+
|
| 625 |
+
"""
|
| 626 |
+
self._u_aux = self._parse_coordinates(
|
| 627 |
+
speeds, True, self._u_aux, [], 'u_auxiliary')[0]
|
| 628 |
+
|
| 629 |
+
@_reset_eom_method
|
| 630 |
+
def add_kdes(self, *kdes):
|
| 631 |
+
"""Add kinematic differential equation(s) to the system.
|
| 632 |
+
|
| 633 |
+
Parameters
|
| 634 |
+
==========
|
| 635 |
+
|
| 636 |
+
*kdes : Expr
|
| 637 |
+
One or more kinematic differential equations.
|
| 638 |
+
|
| 639 |
+
"""
|
| 640 |
+
self._kdes = self._parse_expressions(
|
| 641 |
+
kdes, self.kdes, 'kinematic differential equations',
|
| 642 |
+
check_negatives=True)
|
| 643 |
+
|
| 644 |
+
@_reset_eom_method
|
| 645 |
+
def add_holonomic_constraints(self, *constraints):
|
| 646 |
+
"""Add holonomic constraint(s) to the system.
|
| 647 |
+
|
| 648 |
+
Parameters
|
| 649 |
+
==========
|
| 650 |
+
|
| 651 |
+
*constraints : Expr
|
| 652 |
+
One or more holonomic constraints, which are expressions that should
|
| 653 |
+
be zero.
|
| 654 |
+
|
| 655 |
+
"""
|
| 656 |
+
self._hol_coneqs = self._parse_expressions(
|
| 657 |
+
constraints, self._hol_coneqs, 'holonomic constraints',
|
| 658 |
+
check_negatives=True)
|
| 659 |
+
|
| 660 |
+
@_reset_eom_method
|
| 661 |
+
def add_nonholonomic_constraints(self, *constraints):
|
| 662 |
+
"""Add nonholonomic constraint(s) to the system.
|
| 663 |
+
|
| 664 |
+
Parameters
|
| 665 |
+
==========
|
| 666 |
+
|
| 667 |
+
*constraints : Expr
|
| 668 |
+
One or more nonholonomic constraints, which are expressions that
|
| 669 |
+
should be zero.
|
| 670 |
+
|
| 671 |
+
"""
|
| 672 |
+
self._nonhol_coneqs = self._parse_expressions(
|
| 673 |
+
constraints, self._nonhol_coneqs, 'nonholonomic constraints',
|
| 674 |
+
check_negatives=True)
|
| 675 |
+
|
| 676 |
+
@_reset_eom_method
|
| 677 |
+
def add_bodies(self, *bodies):
|
| 678 |
+
"""Add body(ies) to the system.
|
| 679 |
+
|
| 680 |
+
Parameters
|
| 681 |
+
==========
|
| 682 |
+
|
| 683 |
+
bodies : Particle or RigidBody
|
| 684 |
+
One or more bodies.
|
| 685 |
+
|
| 686 |
+
"""
|
| 687 |
+
self._check_objects(bodies, self.bodies, BodyBase, 'Bodies', 'bodies')
|
| 688 |
+
self._bodies.extend(bodies)
|
| 689 |
+
|
| 690 |
+
@_reset_eom_method
|
| 691 |
+
def add_loads(self, *loads):
|
| 692 |
+
"""Add load(s) to the system.
|
| 693 |
+
|
| 694 |
+
Parameters
|
| 695 |
+
==========
|
| 696 |
+
|
| 697 |
+
*loads : Force or Torque
|
| 698 |
+
One or more loads.
|
| 699 |
+
|
| 700 |
+
"""
|
| 701 |
+
loads = [_parse_load(load) for load in loads] # Checks the loads
|
| 702 |
+
self._loads.extend(loads)
|
| 703 |
+
|
| 704 |
+
@_reset_eom_method
|
| 705 |
+
def apply_uniform_gravity(self, acceleration):
|
| 706 |
+
"""Apply uniform gravity to all bodies in the system by adding loads.
|
| 707 |
+
|
| 708 |
+
Parameters
|
| 709 |
+
==========
|
| 710 |
+
|
| 711 |
+
acceleration : Vector
|
| 712 |
+
The acceleration due to gravity.
|
| 713 |
+
|
| 714 |
+
"""
|
| 715 |
+
self.add_loads(*gravity(acceleration, *self.bodies))
|
| 716 |
+
|
| 717 |
+
@_reset_eom_method
|
| 718 |
+
def add_actuators(self, *actuators):
|
| 719 |
+
"""Add actuator(s) to the system.
|
| 720 |
+
|
| 721 |
+
Parameters
|
| 722 |
+
==========
|
| 723 |
+
|
| 724 |
+
*actuators : subclass of ActuatorBase
|
| 725 |
+
One or more actuators.
|
| 726 |
+
|
| 727 |
+
"""
|
| 728 |
+
self._check_objects(actuators, self.actuators, ActuatorBase,
|
| 729 |
+
'Actuators', 'actuators')
|
| 730 |
+
self._actuators.extend(actuators)
|
| 731 |
+
|
| 732 |
+
@_reset_eom_method
|
| 733 |
+
def add_joints(self, *joints):
|
| 734 |
+
"""Add joint(s) to the system.
|
| 735 |
+
|
| 736 |
+
Explanation
|
| 737 |
+
===========
|
| 738 |
+
|
| 739 |
+
This methods adds one or more joints to the system including its
|
| 740 |
+
associated objects, i.e. generalized coordinates, generalized speeds,
|
| 741 |
+
kinematic differential equations and the bodies.
|
| 742 |
+
|
| 743 |
+
Parameters
|
| 744 |
+
==========
|
| 745 |
+
|
| 746 |
+
*joints : subclass of Joint
|
| 747 |
+
One or more joints.
|
| 748 |
+
|
| 749 |
+
Notes
|
| 750 |
+
=====
|
| 751 |
+
|
| 752 |
+
For the generalized coordinates, generalized speeds and bodies it is
|
| 753 |
+
checked whether they are already known by the system instance. If they
|
| 754 |
+
are, then they are not added. The kinematic differential equations are
|
| 755 |
+
however always added to the system, so you should not also manually add
|
| 756 |
+
those on beforehand.
|
| 757 |
+
|
| 758 |
+
"""
|
| 759 |
+
self._check_objects(joints, self.joints, Joint, 'Joints', 'joints')
|
| 760 |
+
self._joints.extend(joints)
|
| 761 |
+
coordinates, speeds, kdes, bodies = (OrderedSet() for _ in range(4))
|
| 762 |
+
for joint in joints:
|
| 763 |
+
coordinates.update(joint.coordinates)
|
| 764 |
+
speeds.update(joint.speeds)
|
| 765 |
+
kdes.update(joint.kdes)
|
| 766 |
+
bodies.update((joint.parent, joint.child))
|
| 767 |
+
coordinates = coordinates.difference(self.q)
|
| 768 |
+
speeds = speeds.difference(self.u)
|
| 769 |
+
kdes = kdes.difference(self.kdes[:] + (-self.kdes)[:])
|
| 770 |
+
bodies = bodies.difference(self.bodies)
|
| 771 |
+
self.add_coordinates(*tuple(coordinates))
|
| 772 |
+
self.add_speeds(*tuple(speeds))
|
| 773 |
+
self.add_kdes(*(kde for kde in tuple(kdes) if not kde == 0))
|
| 774 |
+
self.add_bodies(*tuple(bodies))
|
| 775 |
+
|
| 776 |
+
def get_body(self, name):
|
| 777 |
+
"""Retrieve a body from the system by name.
|
| 778 |
+
|
| 779 |
+
Parameters
|
| 780 |
+
==========
|
| 781 |
+
|
| 782 |
+
name : str
|
| 783 |
+
The name of the body to retrieve.
|
| 784 |
+
|
| 785 |
+
Returns
|
| 786 |
+
=======
|
| 787 |
+
|
| 788 |
+
RigidBody or Particle
|
| 789 |
+
The body with the given name, or None if no such body exists.
|
| 790 |
+
|
| 791 |
+
"""
|
| 792 |
+
for body in self._bodies:
|
| 793 |
+
if body.name == name:
|
| 794 |
+
return body
|
| 795 |
+
|
| 796 |
+
def get_joint(self, name):
|
| 797 |
+
"""Retrieve a joint from the system by name.
|
| 798 |
+
|
| 799 |
+
Parameters
|
| 800 |
+
==========
|
| 801 |
+
|
| 802 |
+
name : str
|
| 803 |
+
The name of the joint to retrieve.
|
| 804 |
+
|
| 805 |
+
Returns
|
| 806 |
+
=======
|
| 807 |
+
|
| 808 |
+
subclass of Joint
|
| 809 |
+
The joint with the given name, or None if no such joint exists.
|
| 810 |
+
|
| 811 |
+
"""
|
| 812 |
+
for joint in self._joints:
|
| 813 |
+
if joint.name == name:
|
| 814 |
+
return joint
|
| 815 |
+
|
| 816 |
+
def _form_eoms(self):
|
| 817 |
+
return self.form_eoms()
|
| 818 |
+
|
| 819 |
+
def form_eoms(self, eom_method=KanesMethod, **kwargs):
|
| 820 |
+
"""Form the equations of motion of the system.
|
| 821 |
+
|
| 822 |
+
Parameters
|
| 823 |
+
==========
|
| 824 |
+
|
| 825 |
+
eom_method : subclass of KanesMethod or LagrangesMethod
|
| 826 |
+
Backend class to be used for forming the equations of motion. The
|
| 827 |
+
default is ``KanesMethod``.
|
| 828 |
+
|
| 829 |
+
Returns
|
| 830 |
+
========
|
| 831 |
+
|
| 832 |
+
ImmutableMatrix
|
| 833 |
+
Vector of equations of motions.
|
| 834 |
+
|
| 835 |
+
Examples
|
| 836 |
+
========
|
| 837 |
+
|
| 838 |
+
This is a simple example for a one degree of freedom translational
|
| 839 |
+
spring-mass-damper.
|
| 840 |
+
|
| 841 |
+
>>> from sympy import S, symbols
|
| 842 |
+
>>> from sympy.physics.mechanics import (
|
| 843 |
+
... LagrangesMethod, dynamicsymbols, PrismaticJoint, Particle,
|
| 844 |
+
... RigidBody, System)
|
| 845 |
+
>>> q = dynamicsymbols('q')
|
| 846 |
+
>>> qd = dynamicsymbols('q', 1)
|
| 847 |
+
>>> m, k, b = symbols('m k b')
|
| 848 |
+
>>> wall = RigidBody('W')
|
| 849 |
+
>>> system = System.from_newtonian(wall)
|
| 850 |
+
>>> bob = Particle('P', mass=m)
|
| 851 |
+
>>> bob.potential_energy = S.Half * k * q**2
|
| 852 |
+
>>> system.add_joints(PrismaticJoint('J', wall, bob, q, qd))
|
| 853 |
+
>>> system.add_loads((bob.masscenter, b * qd * system.x))
|
| 854 |
+
>>> system.form_eoms(LagrangesMethod)
|
| 855 |
+
Matrix([[-b*Derivative(q(t), t) + k*q(t) + m*Derivative(q(t), (t, 2))]])
|
| 856 |
+
|
| 857 |
+
We can also solve for the states using the 'rhs' method.
|
| 858 |
+
|
| 859 |
+
>>> system.rhs()
|
| 860 |
+
Matrix([
|
| 861 |
+
[ Derivative(q(t), t)],
|
| 862 |
+
[(b*Derivative(q(t), t) - k*q(t))/m]])
|
| 863 |
+
|
| 864 |
+
"""
|
| 865 |
+
# KanesMethod does not accept empty iterables
|
| 866 |
+
loads = self.loads + tuple(
|
| 867 |
+
load for act in self.actuators for load in act.to_loads())
|
| 868 |
+
loads = loads if loads else None
|
| 869 |
+
if issubclass(eom_method, KanesMethod):
|
| 870 |
+
disallowed_kwargs = {
|
| 871 |
+
"frame", "q_ind", "u_ind", "kd_eqs", "q_dependent",
|
| 872 |
+
"u_dependent", "u_auxiliary", "configuration_constraints",
|
| 873 |
+
"velocity_constraints", "forcelist", "bodies"}
|
| 874 |
+
wrong_kwargs = disallowed_kwargs.intersection(kwargs)
|
| 875 |
+
if wrong_kwargs:
|
| 876 |
+
raise ValueError(
|
| 877 |
+
f"The following keyword arguments are not allowed to be "
|
| 878 |
+
f"overwritten in {eom_method.__name__}: {wrong_kwargs}.")
|
| 879 |
+
kwargs = {"frame": self.frame, "q_ind": self.q_ind,
|
| 880 |
+
"u_ind": self.u_ind, "kd_eqs": self.kdes,
|
| 881 |
+
"q_dependent": self.q_dep, "u_dependent": self.u_dep,
|
| 882 |
+
"configuration_constraints": self.holonomic_constraints,
|
| 883 |
+
"velocity_constraints": self.velocity_constraints,
|
| 884 |
+
"u_auxiliary": self.u_aux,
|
| 885 |
+
"forcelist": loads, "bodies": self.bodies,
|
| 886 |
+
"explicit_kinematics": False, **kwargs}
|
| 887 |
+
self._eom_method = eom_method(**kwargs)
|
| 888 |
+
elif issubclass(eom_method, LagrangesMethod):
|
| 889 |
+
disallowed_kwargs = {
|
| 890 |
+
"frame", "qs", "forcelist", "bodies", "hol_coneqs",
|
| 891 |
+
"nonhol_coneqs", "Lagrangian"}
|
| 892 |
+
wrong_kwargs = disallowed_kwargs.intersection(kwargs)
|
| 893 |
+
if wrong_kwargs:
|
| 894 |
+
raise ValueError(
|
| 895 |
+
f"The following keyword arguments are not allowed to be "
|
| 896 |
+
f"overwritten in {eom_method.__name__}: {wrong_kwargs}.")
|
| 897 |
+
kwargs = {"frame": self.frame, "qs": self.q, "forcelist": loads,
|
| 898 |
+
"bodies": self.bodies,
|
| 899 |
+
"hol_coneqs": self.holonomic_constraints,
|
| 900 |
+
"nonhol_coneqs": self.nonholonomic_constraints, **kwargs}
|
| 901 |
+
if "Lagrangian" not in kwargs:
|
| 902 |
+
kwargs["Lagrangian"] = Lagrangian(kwargs["frame"],
|
| 903 |
+
*kwargs["bodies"])
|
| 904 |
+
self._eom_method = eom_method(**kwargs)
|
| 905 |
+
else:
|
| 906 |
+
raise NotImplementedError(f'{eom_method} has not been implemented.')
|
| 907 |
+
return self.eom_method._form_eoms()
|
| 908 |
+
|
| 909 |
+
def rhs(self, inv_method=None):
|
| 910 |
+
"""Compute the equations of motion in the explicit form.
|
| 911 |
+
|
| 912 |
+
Parameters
|
| 913 |
+
==========
|
| 914 |
+
|
| 915 |
+
inv_method : str
|
| 916 |
+
The specific sympy inverse matrix calculation method to use. For a
|
| 917 |
+
list of valid methods, see
|
| 918 |
+
:meth:`~sympy.matrices.matrixbase.MatrixBase.inv`
|
| 919 |
+
|
| 920 |
+
Returns
|
| 921 |
+
========
|
| 922 |
+
|
| 923 |
+
ImmutableMatrix
|
| 924 |
+
Equations of motion in the explicit form.
|
| 925 |
+
|
| 926 |
+
See Also
|
| 927 |
+
========
|
| 928 |
+
|
| 929 |
+
sympy.physics.mechanics.kane.KanesMethod.rhs:
|
| 930 |
+
KanesMethod's ``rhs`` function.
|
| 931 |
+
sympy.physics.mechanics.lagrange.LagrangesMethod.rhs:
|
| 932 |
+
LagrangesMethod's ``rhs`` function.
|
| 933 |
+
|
| 934 |
+
"""
|
| 935 |
+
return self.eom_method.rhs(inv_method=inv_method)
|
| 936 |
+
|
| 937 |
+
@property
|
| 938 |
+
def mass_matrix(self):
|
| 939 |
+
r"""The mass matrix of the system.
|
| 940 |
+
|
| 941 |
+
Explanation
|
| 942 |
+
===========
|
| 943 |
+
|
| 944 |
+
The mass matrix $M_d$ and the forcing vector $f_d$ of a system describe
|
| 945 |
+
the system's dynamics according to the following equations:
|
| 946 |
+
|
| 947 |
+
.. math::
|
| 948 |
+
M_d \dot{u} = f_d
|
| 949 |
+
|
| 950 |
+
where $\dot{u}$ is the time derivative of the generalized speeds.
|
| 951 |
+
|
| 952 |
+
"""
|
| 953 |
+
return self.eom_method.mass_matrix
|
| 954 |
+
|
| 955 |
+
@property
|
| 956 |
+
def mass_matrix_full(self):
|
| 957 |
+
r"""The mass matrix of the system, augmented by the kinematic
|
| 958 |
+
differential equations in explicit or implicit form.
|
| 959 |
+
|
| 960 |
+
Explanation
|
| 961 |
+
===========
|
| 962 |
+
|
| 963 |
+
The full mass matrix $M_m$ and the full forcing vector $f_m$ of a system
|
| 964 |
+
describe the dynamics and kinematics according to the following
|
| 965 |
+
equation:
|
| 966 |
+
|
| 967 |
+
.. math::
|
| 968 |
+
M_m \dot{x} = f_m
|
| 969 |
+
|
| 970 |
+
where $x$ is the state vector stacking $q$ and $u$.
|
| 971 |
+
|
| 972 |
+
"""
|
| 973 |
+
return self.eom_method.mass_matrix_full
|
| 974 |
+
|
| 975 |
+
@property
|
| 976 |
+
def forcing(self):
|
| 977 |
+
"""The forcing vector of the system."""
|
| 978 |
+
return self.eom_method.forcing
|
| 979 |
+
|
| 980 |
+
@property
|
| 981 |
+
def forcing_full(self):
|
| 982 |
+
"""The forcing vector of the system, augmented by the kinematic
|
| 983 |
+
differential equations in explicit or implicit form."""
|
| 984 |
+
return self.eom_method.forcing_full
|
| 985 |
+
|
| 986 |
+
def validate_system(self, eom_method=KanesMethod, check_duplicates=False):
|
| 987 |
+
"""Validates the system using some basic checks.
|
| 988 |
+
|
| 989 |
+
Explanation
|
| 990 |
+
===========
|
| 991 |
+
|
| 992 |
+
This method validates the system based on the following checks:
|
| 993 |
+
|
| 994 |
+
- The number of dependent generalized coordinates should equal the
|
| 995 |
+
number of holonomic constraints.
|
| 996 |
+
- All generalized coordinates defined by the joints should also be known
|
| 997 |
+
to the system.
|
| 998 |
+
- If ``KanesMethod`` is used as a ``eom_method``:
|
| 999 |
+
- All generalized speeds and kinematic differential equations
|
| 1000 |
+
defined by the joints should also be known to the system.
|
| 1001 |
+
- The number of dependent generalized speeds should equal the number
|
| 1002 |
+
of velocity constraints.
|
| 1003 |
+
- The number of generalized coordinates should be less than or equal
|
| 1004 |
+
to the number of generalized speeds.
|
| 1005 |
+
- The number of generalized coordinates should equal the number of
|
| 1006 |
+
kinematic differential equations.
|
| 1007 |
+
- If ``LagrangesMethod`` is used as ``eom_method``:
|
| 1008 |
+
- There should not be any generalized speeds that are not
|
| 1009 |
+
derivatives of the generalized coordinates (this includes the
|
| 1010 |
+
generalized speeds defined by the joints).
|
| 1011 |
+
|
| 1012 |
+
Parameters
|
| 1013 |
+
==========
|
| 1014 |
+
|
| 1015 |
+
eom_method : subclass of KanesMethod or LagrangesMethod
|
| 1016 |
+
Backend class that will be used for forming the equations of motion.
|
| 1017 |
+
There are different checks for the different backends. The default
|
| 1018 |
+
is ``KanesMethod``.
|
| 1019 |
+
check_duplicates : bool
|
| 1020 |
+
Boolean whether the system should be checked for duplicate
|
| 1021 |
+
definitions. The default is False, because duplicates are already
|
| 1022 |
+
checked when adding objects to the system.
|
| 1023 |
+
|
| 1024 |
+
Notes
|
| 1025 |
+
=====
|
| 1026 |
+
|
| 1027 |
+
This method is not guaranteed to be backwards compatible as it may
|
| 1028 |
+
improve over time. The method can become both more and less strict in
|
| 1029 |
+
certain areas. However a well-defined system should always pass all
|
| 1030 |
+
these tests.
|
| 1031 |
+
|
| 1032 |
+
"""
|
| 1033 |
+
msgs = []
|
| 1034 |
+
# Save some data in variables
|
| 1035 |
+
n_hc = self.holonomic_constraints.shape[0]
|
| 1036 |
+
n_vc = self.velocity_constraints.shape[0]
|
| 1037 |
+
n_q_dep, n_u_dep = self.q_dep.shape[0], self.u_dep.shape[0]
|
| 1038 |
+
q_set, u_set = set(self.q), set(self.u)
|
| 1039 |
+
n_q, n_u = len(q_set), len(u_set)
|
| 1040 |
+
# Check number of holonomic constraints
|
| 1041 |
+
if n_q_dep != n_hc:
|
| 1042 |
+
msgs.append(filldedent(f"""
|
| 1043 |
+
The number of dependent generalized coordinates {n_q_dep} should be
|
| 1044 |
+
equal to the number of holonomic constraints {n_hc}."""))
|
| 1045 |
+
# Check if all joint coordinates and speeds are present
|
| 1046 |
+
missing_q = set()
|
| 1047 |
+
for joint in self.joints:
|
| 1048 |
+
missing_q.update(set(joint.coordinates).difference(q_set))
|
| 1049 |
+
if missing_q:
|
| 1050 |
+
msgs.append(filldedent(f"""
|
| 1051 |
+
The generalized coordinates {missing_q} used in joints are not added
|
| 1052 |
+
to the system."""))
|
| 1053 |
+
# Method dependent checks
|
| 1054 |
+
if issubclass(eom_method, KanesMethod):
|
| 1055 |
+
n_kdes = len(self.kdes)
|
| 1056 |
+
missing_kdes, missing_u = set(), set()
|
| 1057 |
+
for joint in self.joints:
|
| 1058 |
+
missing_u.update(set(joint.speeds).difference(u_set))
|
| 1059 |
+
missing_kdes.update(set(joint.kdes).difference(
|
| 1060 |
+
self.kdes[:] + (-self.kdes)[:]))
|
| 1061 |
+
if missing_u:
|
| 1062 |
+
msgs.append(filldedent(f"""
|
| 1063 |
+
The generalized speeds {missing_u} used in joints are not added
|
| 1064 |
+
to the system."""))
|
| 1065 |
+
if missing_kdes:
|
| 1066 |
+
msgs.append(filldedent(f"""
|
| 1067 |
+
The kinematic differential equations {missing_kdes} used in
|
| 1068 |
+
joints are not added to the system."""))
|
| 1069 |
+
if n_u_dep != n_vc:
|
| 1070 |
+
msgs.append(filldedent(f"""
|
| 1071 |
+
The number of dependent generalized speeds {n_u_dep} should be
|
| 1072 |
+
equal to the number of velocity constraints {n_vc}."""))
|
| 1073 |
+
if n_q > n_u:
|
| 1074 |
+
msgs.append(filldedent(f"""
|
| 1075 |
+
The number of generalized coordinates {n_q} should be less than
|
| 1076 |
+
or equal to the number of generalized speeds {n_u}."""))
|
| 1077 |
+
if n_u != n_kdes:
|
| 1078 |
+
msgs.append(filldedent(f"""
|
| 1079 |
+
The number of generalized speeds {n_u} should be equal to the
|
| 1080 |
+
number of kinematic differential equations {n_kdes}."""))
|
| 1081 |
+
elif issubclass(eom_method, LagrangesMethod):
|
| 1082 |
+
not_qdots = set(self.u).difference(self.q.diff(dynamicsymbols._t))
|
| 1083 |
+
for joint in self.joints:
|
| 1084 |
+
not_qdots.update(set(
|
| 1085 |
+
joint.speeds).difference(self.q.diff(dynamicsymbols._t)))
|
| 1086 |
+
if not_qdots:
|
| 1087 |
+
msgs.append(filldedent(f"""
|
| 1088 |
+
The generalized speeds {not_qdots} are not supported by this
|
| 1089 |
+
method. Only derivatives of the generalized coordinates are
|
| 1090 |
+
supported. If these symbols are used in your expressions, then
|
| 1091 |
+
this will result in wrong equations of motion."""))
|
| 1092 |
+
if self.u_aux:
|
| 1093 |
+
msgs.append(filldedent(f"""
|
| 1094 |
+
This method does not support auxiliary speeds. If these symbols
|
| 1095 |
+
are used in your expressions, then this will result in wrong
|
| 1096 |
+
equations of motion. The auxiliary speeds are {self.u_aux}."""))
|
| 1097 |
+
else:
|
| 1098 |
+
raise NotImplementedError(f'{eom_method} has not been implemented.')
|
| 1099 |
+
if check_duplicates: # Should be redundant
|
| 1100 |
+
duplicates_to_check = [('generalized coordinates', self.q),
|
| 1101 |
+
('generalized speeds', self.u),
|
| 1102 |
+
('auxiliary speeds', self.u_aux),
|
| 1103 |
+
('bodies', self.bodies),
|
| 1104 |
+
('joints', self.joints)]
|
| 1105 |
+
for name, lst in duplicates_to_check:
|
| 1106 |
+
seen = set()
|
| 1107 |
+
duplicates = {x for x in lst if x in seen or seen.add(x)}
|
| 1108 |
+
if duplicates:
|
| 1109 |
+
msgs.append(filldedent(f"""
|
| 1110 |
+
The {name} {duplicates} exist multiple times within the
|
| 1111 |
+
system."""))
|
| 1112 |
+
if msgs:
|
| 1113 |
+
raise ValueError('\n'.join(msgs))
|
| 1114 |
+
|
| 1115 |
+
|
| 1116 |
+
class SymbolicSystem:
|
| 1117 |
+
"""SymbolicSystem is a class that contains all the information about a
|
| 1118 |
+
system in a symbolic format such as the equations of motions and the bodies
|
| 1119 |
+
and loads in the system.
|
| 1120 |
+
|
| 1121 |
+
There are three ways that the equations of motion can be described for
|
| 1122 |
+
Symbolic System:
|
| 1123 |
+
|
| 1124 |
+
|
| 1125 |
+
[1] Explicit form where the kinematics and dynamics are combined
|
| 1126 |
+
x' = F_1(x, t, r, p)
|
| 1127 |
+
|
| 1128 |
+
[2] Implicit form where the kinematics and dynamics are combined
|
| 1129 |
+
M_2(x, p) x' = F_2(x, t, r, p)
|
| 1130 |
+
|
| 1131 |
+
[3] Implicit form where the kinematics and dynamics are separate
|
| 1132 |
+
M_3(q, p) u' = F_3(q, u, t, r, p)
|
| 1133 |
+
q' = G(q, u, t, r, p)
|
| 1134 |
+
|
| 1135 |
+
where
|
| 1136 |
+
|
| 1137 |
+
x : states, e.g. [q, u]
|
| 1138 |
+
t : time
|
| 1139 |
+
r : specified (exogenous) inputs
|
| 1140 |
+
p : constants
|
| 1141 |
+
q : generalized coordinates
|
| 1142 |
+
u : generalized speeds
|
| 1143 |
+
F_1 : right hand side of the combined equations in explicit form
|
| 1144 |
+
F_2 : right hand side of the combined equations in implicit form
|
| 1145 |
+
F_3 : right hand side of the dynamical equations in implicit form
|
| 1146 |
+
M_2 : mass matrix of the combined equations in implicit form
|
| 1147 |
+
M_3 : mass matrix of the dynamical equations in implicit form
|
| 1148 |
+
G : right hand side of the kinematical differential equations
|
| 1149 |
+
|
| 1150 |
+
Parameters
|
| 1151 |
+
==========
|
| 1152 |
+
|
| 1153 |
+
coord_states : ordered iterable of functions of time
|
| 1154 |
+
This input will either be a collection of the coordinates or states
|
| 1155 |
+
of the system depending on whether or not the speeds are also
|
| 1156 |
+
given. If speeds are specified this input will be assumed to
|
| 1157 |
+
be the coordinates otherwise this input will be assumed to
|
| 1158 |
+
be the states.
|
| 1159 |
+
|
| 1160 |
+
right_hand_side : Matrix
|
| 1161 |
+
This variable is the right hand side of the equations of motion in
|
| 1162 |
+
any of the forms. The specific form will be assumed depending on
|
| 1163 |
+
whether a mass matrix or coordinate derivatives are given.
|
| 1164 |
+
|
| 1165 |
+
speeds : ordered iterable of functions of time, optional
|
| 1166 |
+
This is a collection of the generalized speeds of the system. If
|
| 1167 |
+
given it will be assumed that the first argument (coord_states)
|
| 1168 |
+
will represent the generalized coordinates of the system.
|
| 1169 |
+
|
| 1170 |
+
mass_matrix : Matrix, optional
|
| 1171 |
+
The matrix of the implicit forms of the equations of motion (forms
|
| 1172 |
+
[2] and [3]). The distinction between the forms is determined by
|
| 1173 |
+
whether or not the coordinate derivatives are passed in. If
|
| 1174 |
+
they are given form [3] will be assumed otherwise form [2] is
|
| 1175 |
+
assumed.
|
| 1176 |
+
|
| 1177 |
+
coordinate_derivatives : Matrix, optional
|
| 1178 |
+
The right hand side of the kinematical equations in explicit form.
|
| 1179 |
+
If given it will be assumed that the equations of motion are being
|
| 1180 |
+
entered in form [3].
|
| 1181 |
+
|
| 1182 |
+
alg_con : Iterable, optional
|
| 1183 |
+
The indexes of the rows in the equations of motion that contain
|
| 1184 |
+
algebraic constraints instead of differential equations. If the
|
| 1185 |
+
equations are input in form [3], it will be assumed the indexes are
|
| 1186 |
+
referencing the mass_matrix/right_hand_side combination and not the
|
| 1187 |
+
coordinate_derivatives.
|
| 1188 |
+
|
| 1189 |
+
output_eqns : Dictionary, optional
|
| 1190 |
+
Any output equations that are desired to be tracked are stored in a
|
| 1191 |
+
dictionary where the key corresponds to the name given for the
|
| 1192 |
+
specific equation and the value is the equation itself in symbolic
|
| 1193 |
+
form
|
| 1194 |
+
|
| 1195 |
+
coord_idxs : Iterable, optional
|
| 1196 |
+
If coord_states corresponds to the states rather than the
|
| 1197 |
+
coordinates this variable will tell SymbolicSystem which indexes of
|
| 1198 |
+
the states correspond to generalized coordinates.
|
| 1199 |
+
|
| 1200 |
+
speed_idxs : Iterable, optional
|
| 1201 |
+
If coord_states corresponds to the states rather than the
|
| 1202 |
+
coordinates this variable will tell SymbolicSystem which indexes of
|
| 1203 |
+
the states correspond to generalized speeds.
|
| 1204 |
+
|
| 1205 |
+
bodies : iterable of Body/Rigidbody objects, optional
|
| 1206 |
+
Iterable containing the bodies of the system
|
| 1207 |
+
|
| 1208 |
+
loads : iterable of load instances (described below), optional
|
| 1209 |
+
Iterable containing the loads of the system where forces are given
|
| 1210 |
+
by (point of application, force vector) and torques are given by
|
| 1211 |
+
(reference frame acting upon, torque vector). Ex [(point, force),
|
| 1212 |
+
(ref_frame, torque)]
|
| 1213 |
+
|
| 1214 |
+
Attributes
|
| 1215 |
+
==========
|
| 1216 |
+
|
| 1217 |
+
coordinates : Matrix, shape(n, 1)
|
| 1218 |
+
This is a matrix containing the generalized coordinates of the system
|
| 1219 |
+
|
| 1220 |
+
speeds : Matrix, shape(m, 1)
|
| 1221 |
+
This is a matrix containing the generalized speeds of the system
|
| 1222 |
+
|
| 1223 |
+
states : Matrix, shape(o, 1)
|
| 1224 |
+
This is a matrix containing the state variables of the system
|
| 1225 |
+
|
| 1226 |
+
alg_con : List
|
| 1227 |
+
This list contains the indices of the algebraic constraints in the
|
| 1228 |
+
combined equations of motion. The presence of these constraints
|
| 1229 |
+
requires that a DAE solver be used instead of an ODE solver.
|
| 1230 |
+
If the system is given in form [3] the alg_con variable will be
|
| 1231 |
+
adjusted such that it is a representation of the combined kinematics
|
| 1232 |
+
and dynamics thus make sure it always matches the mass matrix
|
| 1233 |
+
entered.
|
| 1234 |
+
|
| 1235 |
+
dyn_implicit_mat : Matrix, shape(m, m)
|
| 1236 |
+
This is the M matrix in form [3] of the equations of motion (the mass
|
| 1237 |
+
matrix or generalized inertia matrix of the dynamical equations of
|
| 1238 |
+
motion in implicit form).
|
| 1239 |
+
|
| 1240 |
+
dyn_implicit_rhs : Matrix, shape(m, 1)
|
| 1241 |
+
This is the F vector in form [3] of the equations of motion (the right
|
| 1242 |
+
hand side of the dynamical equations of motion in implicit form).
|
| 1243 |
+
|
| 1244 |
+
comb_implicit_mat : Matrix, shape(o, o)
|
| 1245 |
+
This is the M matrix in form [2] of the equations of motion.
|
| 1246 |
+
This matrix contains a block diagonal structure where the top
|
| 1247 |
+
left block (the first rows) represent the matrix in the
|
| 1248 |
+
implicit form of the kinematical equations and the bottom right
|
| 1249 |
+
block (the last rows) represent the matrix in the implicit form
|
| 1250 |
+
of the dynamical equations.
|
| 1251 |
+
|
| 1252 |
+
comb_implicit_rhs : Matrix, shape(o, 1)
|
| 1253 |
+
This is the F vector in form [2] of the equations of motion. The top
|
| 1254 |
+
part of the vector represents the right hand side of the implicit form
|
| 1255 |
+
of the kinemaical equations and the bottom of the vector represents the
|
| 1256 |
+
right hand side of the implicit form of the dynamical equations of
|
| 1257 |
+
motion.
|
| 1258 |
+
|
| 1259 |
+
comb_explicit_rhs : Matrix, shape(o, 1)
|
| 1260 |
+
This vector represents the right hand side of the combined equations of
|
| 1261 |
+
motion in explicit form (form [1] from above).
|
| 1262 |
+
|
| 1263 |
+
kin_explicit_rhs : Matrix, shape(m, 1)
|
| 1264 |
+
This is the right hand side of the explicit form of the kinematical
|
| 1265 |
+
equations of motion as can be seen in form [3] (the G matrix).
|
| 1266 |
+
|
| 1267 |
+
output_eqns : Dictionary
|
| 1268 |
+
If output equations were given they are stored in a dictionary where
|
| 1269 |
+
the key corresponds to the name given for the specific equation and
|
| 1270 |
+
the value is the equation itself in symbolic form
|
| 1271 |
+
|
| 1272 |
+
bodies : Tuple
|
| 1273 |
+
If the bodies in the system were given they are stored in a tuple for
|
| 1274 |
+
future access
|
| 1275 |
+
|
| 1276 |
+
loads : Tuple
|
| 1277 |
+
If the loads in the system were given they are stored in a tuple for
|
| 1278 |
+
future access. This includes forces and torques where forces are given
|
| 1279 |
+
by (point of application, force vector) and torques are given by
|
| 1280 |
+
(reference frame acted upon, torque vector).
|
| 1281 |
+
|
| 1282 |
+
Example
|
| 1283 |
+
=======
|
| 1284 |
+
|
| 1285 |
+
As a simple example, the dynamics of a simple pendulum will be input into a
|
| 1286 |
+
SymbolicSystem object manually. First some imports will be needed and then
|
| 1287 |
+
symbols will be set up for the length of the pendulum (l), mass at the end
|
| 1288 |
+
of the pendulum (m), and a constant for gravity (g). ::
|
| 1289 |
+
|
| 1290 |
+
>>> from sympy import Matrix, sin, symbols
|
| 1291 |
+
>>> from sympy.physics.mechanics import dynamicsymbols, SymbolicSystem
|
| 1292 |
+
>>> l, m, g = symbols('l m g')
|
| 1293 |
+
|
| 1294 |
+
The system will be defined by an angle of theta from the vertical and a
|
| 1295 |
+
generalized speed of omega will be used where omega = theta_dot. ::
|
| 1296 |
+
|
| 1297 |
+
>>> theta, omega = dynamicsymbols('theta omega')
|
| 1298 |
+
|
| 1299 |
+
Now the equations of motion are ready to be formed and passed to the
|
| 1300 |
+
SymbolicSystem object. ::
|
| 1301 |
+
|
| 1302 |
+
>>> kin_explicit_rhs = Matrix([omega])
|
| 1303 |
+
>>> dyn_implicit_mat = Matrix([l**2 * m])
|
| 1304 |
+
>>> dyn_implicit_rhs = Matrix([-g * l * m * sin(theta)])
|
| 1305 |
+
>>> symsystem = SymbolicSystem([theta], dyn_implicit_rhs, [omega],
|
| 1306 |
+
... dyn_implicit_mat)
|
| 1307 |
+
|
| 1308 |
+
Notes
|
| 1309 |
+
=====
|
| 1310 |
+
|
| 1311 |
+
m : number of generalized speeds
|
| 1312 |
+
n : number of generalized coordinates
|
| 1313 |
+
o : number of states
|
| 1314 |
+
|
| 1315 |
+
"""
|
| 1316 |
+
|
| 1317 |
+
def __init__(self, coord_states, right_hand_side, speeds=None,
|
| 1318 |
+
mass_matrix=None, coordinate_derivatives=None, alg_con=None,
|
| 1319 |
+
output_eqns={}, coord_idxs=None, speed_idxs=None, bodies=None,
|
| 1320 |
+
loads=None):
|
| 1321 |
+
"""Initializes a SymbolicSystem object"""
|
| 1322 |
+
|
| 1323 |
+
# Extract information on speeds, coordinates and states
|
| 1324 |
+
if speeds is None:
|
| 1325 |
+
self._states = Matrix(coord_states)
|
| 1326 |
+
|
| 1327 |
+
if coord_idxs is None:
|
| 1328 |
+
self._coordinates = None
|
| 1329 |
+
else:
|
| 1330 |
+
coords = [coord_states[i] for i in coord_idxs]
|
| 1331 |
+
self._coordinates = Matrix(coords)
|
| 1332 |
+
|
| 1333 |
+
if speed_idxs is None:
|
| 1334 |
+
self._speeds = None
|
| 1335 |
+
else:
|
| 1336 |
+
speeds_inter = [coord_states[i] for i in speed_idxs]
|
| 1337 |
+
self._speeds = Matrix(speeds_inter)
|
| 1338 |
+
else:
|
| 1339 |
+
self._coordinates = Matrix(coord_states)
|
| 1340 |
+
self._speeds = Matrix(speeds)
|
| 1341 |
+
self._states = self._coordinates.col_join(self._speeds)
|
| 1342 |
+
|
| 1343 |
+
# Extract equations of motion form
|
| 1344 |
+
if coordinate_derivatives is not None:
|
| 1345 |
+
self._kin_explicit_rhs = coordinate_derivatives
|
| 1346 |
+
self._dyn_implicit_rhs = right_hand_side
|
| 1347 |
+
self._dyn_implicit_mat = mass_matrix
|
| 1348 |
+
self._comb_implicit_rhs = None
|
| 1349 |
+
self._comb_implicit_mat = None
|
| 1350 |
+
self._comb_explicit_rhs = None
|
| 1351 |
+
elif mass_matrix is not None:
|
| 1352 |
+
self._kin_explicit_rhs = None
|
| 1353 |
+
self._dyn_implicit_rhs = None
|
| 1354 |
+
self._dyn_implicit_mat = None
|
| 1355 |
+
self._comb_implicit_rhs = right_hand_side
|
| 1356 |
+
self._comb_implicit_mat = mass_matrix
|
| 1357 |
+
self._comb_explicit_rhs = None
|
| 1358 |
+
else:
|
| 1359 |
+
self._kin_explicit_rhs = None
|
| 1360 |
+
self._dyn_implicit_rhs = None
|
| 1361 |
+
self._dyn_implicit_mat = None
|
| 1362 |
+
self._comb_implicit_rhs = None
|
| 1363 |
+
self._comb_implicit_mat = None
|
| 1364 |
+
self._comb_explicit_rhs = right_hand_side
|
| 1365 |
+
|
| 1366 |
+
# Set the remainder of the inputs as instance attributes
|
| 1367 |
+
if alg_con is not None and coordinate_derivatives is not None:
|
| 1368 |
+
alg_con = [i + len(coordinate_derivatives) for i in alg_con]
|
| 1369 |
+
self._alg_con = alg_con
|
| 1370 |
+
self.output_eqns = output_eqns
|
| 1371 |
+
|
| 1372 |
+
# Change the body and loads iterables to tuples if they are not tuples
|
| 1373 |
+
# already
|
| 1374 |
+
if not isinstance(bodies, tuple) and bodies is not None:
|
| 1375 |
+
bodies = tuple(bodies)
|
| 1376 |
+
if not isinstance(loads, tuple) and loads is not None:
|
| 1377 |
+
loads = tuple(loads)
|
| 1378 |
+
self._bodies = bodies
|
| 1379 |
+
self._loads = loads
|
| 1380 |
+
|
| 1381 |
+
@property
|
| 1382 |
+
def coordinates(self):
|
| 1383 |
+
"""Returns the column matrix of the generalized coordinates"""
|
| 1384 |
+
if self._coordinates is None:
|
| 1385 |
+
raise AttributeError("The coordinates were not specified.")
|
| 1386 |
+
else:
|
| 1387 |
+
return self._coordinates
|
| 1388 |
+
|
| 1389 |
+
@property
|
| 1390 |
+
def speeds(self):
|
| 1391 |
+
"""Returns the column matrix of generalized speeds"""
|
| 1392 |
+
if self._speeds is None:
|
| 1393 |
+
raise AttributeError("The speeds were not specified.")
|
| 1394 |
+
else:
|
| 1395 |
+
return self._speeds
|
| 1396 |
+
|
| 1397 |
+
@property
|
| 1398 |
+
def states(self):
|
| 1399 |
+
"""Returns the column matrix of the state variables"""
|
| 1400 |
+
return self._states
|
| 1401 |
+
|
| 1402 |
+
@property
|
| 1403 |
+
def alg_con(self):
|
| 1404 |
+
"""Returns a list with the indices of the rows containing algebraic
|
| 1405 |
+
constraints in the combined form of the equations of motion"""
|
| 1406 |
+
return self._alg_con
|
| 1407 |
+
|
| 1408 |
+
@property
|
| 1409 |
+
def dyn_implicit_mat(self):
|
| 1410 |
+
"""Returns the matrix, M, corresponding to the dynamic equations in
|
| 1411 |
+
implicit form, M x' = F, where the kinematical equations are not
|
| 1412 |
+
included"""
|
| 1413 |
+
if self._dyn_implicit_mat is None:
|
| 1414 |
+
raise AttributeError("dyn_implicit_mat is not specified for "
|
| 1415 |
+
"equations of motion form [1] or [2].")
|
| 1416 |
+
else:
|
| 1417 |
+
return self._dyn_implicit_mat
|
| 1418 |
+
|
| 1419 |
+
@property
|
| 1420 |
+
def dyn_implicit_rhs(self):
|
| 1421 |
+
"""Returns the column matrix, F, corresponding to the dynamic equations
|
| 1422 |
+
in implicit form, M x' = F, where the kinematical equations are not
|
| 1423 |
+
included"""
|
| 1424 |
+
if self._dyn_implicit_rhs is None:
|
| 1425 |
+
raise AttributeError("dyn_implicit_rhs is not specified for "
|
| 1426 |
+
"equations of motion form [1] or [2].")
|
| 1427 |
+
else:
|
| 1428 |
+
return self._dyn_implicit_rhs
|
| 1429 |
+
|
| 1430 |
+
@property
|
| 1431 |
+
def comb_implicit_mat(self):
|
| 1432 |
+
"""Returns the matrix, M, corresponding to the equations of motion in
|
| 1433 |
+
implicit form (form [2]), M x' = F, where the kinematical equations are
|
| 1434 |
+
included"""
|
| 1435 |
+
if self._comb_implicit_mat is None:
|
| 1436 |
+
if self._dyn_implicit_mat is not None:
|
| 1437 |
+
num_kin_eqns = len(self._kin_explicit_rhs)
|
| 1438 |
+
num_dyn_eqns = len(self._dyn_implicit_rhs)
|
| 1439 |
+
zeros1 = zeros(num_kin_eqns, num_dyn_eqns)
|
| 1440 |
+
zeros2 = zeros(num_dyn_eqns, num_kin_eqns)
|
| 1441 |
+
inter1 = eye(num_kin_eqns).row_join(zeros1)
|
| 1442 |
+
inter2 = zeros2.row_join(self._dyn_implicit_mat)
|
| 1443 |
+
self._comb_implicit_mat = inter1.col_join(inter2)
|
| 1444 |
+
return self._comb_implicit_mat
|
| 1445 |
+
else:
|
| 1446 |
+
raise AttributeError("comb_implicit_mat is not specified for "
|
| 1447 |
+
"equations of motion form [1].")
|
| 1448 |
+
else:
|
| 1449 |
+
return self._comb_implicit_mat
|
| 1450 |
+
|
| 1451 |
+
@property
|
| 1452 |
+
def comb_implicit_rhs(self):
|
| 1453 |
+
"""Returns the column matrix, F, corresponding to the equations of
|
| 1454 |
+
motion in implicit form (form [2]), M x' = F, where the kinematical
|
| 1455 |
+
equations are included"""
|
| 1456 |
+
if self._comb_implicit_rhs is None:
|
| 1457 |
+
if self._dyn_implicit_rhs is not None:
|
| 1458 |
+
kin_inter = self._kin_explicit_rhs
|
| 1459 |
+
dyn_inter = self._dyn_implicit_rhs
|
| 1460 |
+
self._comb_implicit_rhs = kin_inter.col_join(dyn_inter)
|
| 1461 |
+
return self._comb_implicit_rhs
|
| 1462 |
+
else:
|
| 1463 |
+
raise AttributeError("comb_implicit_mat is not specified for "
|
| 1464 |
+
"equations of motion in form [1].")
|
| 1465 |
+
else:
|
| 1466 |
+
return self._comb_implicit_rhs
|
| 1467 |
+
|
| 1468 |
+
def compute_explicit_form(self):
|
| 1469 |
+
"""If the explicit right hand side of the combined equations of motion
|
| 1470 |
+
is to provided upon initialization, this method will calculate it. This
|
| 1471 |
+
calculation can potentially take awhile to compute."""
|
| 1472 |
+
if self._comb_explicit_rhs is not None:
|
| 1473 |
+
raise AttributeError("comb_explicit_rhs is already formed.")
|
| 1474 |
+
|
| 1475 |
+
inter1 = getattr(self, 'kin_explicit_rhs', None)
|
| 1476 |
+
if inter1 is not None:
|
| 1477 |
+
inter2 = self._dyn_implicit_mat.LUsolve(self._dyn_implicit_rhs)
|
| 1478 |
+
out = inter1.col_join(inter2)
|
| 1479 |
+
else:
|
| 1480 |
+
out = self._comb_implicit_mat.LUsolve(self._comb_implicit_rhs)
|
| 1481 |
+
|
| 1482 |
+
self._comb_explicit_rhs = out
|
| 1483 |
+
|
| 1484 |
+
@property
|
| 1485 |
+
def comb_explicit_rhs(self):
|
| 1486 |
+
"""Returns the right hand side of the equations of motion in explicit
|
| 1487 |
+
form, x' = F, where the kinematical equations are included"""
|
| 1488 |
+
if self._comb_explicit_rhs is None:
|
| 1489 |
+
raise AttributeError("Please run .combute_explicit_form before "
|
| 1490 |
+
"attempting to access comb_explicit_rhs.")
|
| 1491 |
+
else:
|
| 1492 |
+
return self._comb_explicit_rhs
|
| 1493 |
+
|
| 1494 |
+
@property
|
| 1495 |
+
def kin_explicit_rhs(self):
|
| 1496 |
+
"""Returns the right hand side of the kinematical equations in explicit
|
| 1497 |
+
form, q' = G"""
|
| 1498 |
+
if self._kin_explicit_rhs is None:
|
| 1499 |
+
raise AttributeError("kin_explicit_rhs is not specified for "
|
| 1500 |
+
"equations of motion form [1] or [2].")
|
| 1501 |
+
else:
|
| 1502 |
+
return self._kin_explicit_rhs
|
| 1503 |
+
|
| 1504 |
+
def dynamic_symbols(self):
|
| 1505 |
+
"""Returns a column matrix containing all of the symbols in the system
|
| 1506 |
+
that depend on time"""
|
| 1507 |
+
# Create a list of all of the expressions in the equations of motion
|
| 1508 |
+
if self._comb_explicit_rhs is None:
|
| 1509 |
+
eom_expressions = (self.comb_implicit_mat[:] +
|
| 1510 |
+
self.comb_implicit_rhs[:])
|
| 1511 |
+
else:
|
| 1512 |
+
eom_expressions = (self._comb_explicit_rhs[:])
|
| 1513 |
+
|
| 1514 |
+
functions_of_time = set()
|
| 1515 |
+
for expr in eom_expressions:
|
| 1516 |
+
functions_of_time = functions_of_time.union(
|
| 1517 |
+
find_dynamicsymbols(expr))
|
| 1518 |
+
functions_of_time = functions_of_time.union(self._states)
|
| 1519 |
+
|
| 1520 |
+
return tuple(functions_of_time)
|
| 1521 |
+
|
| 1522 |
+
def constant_symbols(self):
|
| 1523 |
+
"""Returns a column matrix containing all of the symbols in the system
|
| 1524 |
+
that do not depend on time"""
|
| 1525 |
+
# Create a list of all of the expressions in the equations of motion
|
| 1526 |
+
if self._comb_explicit_rhs is None:
|
| 1527 |
+
eom_expressions = (self.comb_implicit_mat[:] +
|
| 1528 |
+
self.comb_implicit_rhs[:])
|
| 1529 |
+
else:
|
| 1530 |
+
eom_expressions = (self._comb_explicit_rhs[:])
|
| 1531 |
+
|
| 1532 |
+
constants = set()
|
| 1533 |
+
for expr in eom_expressions:
|
| 1534 |
+
constants = constants.union(expr.free_symbols)
|
| 1535 |
+
constants.remove(dynamicsymbols._t)
|
| 1536 |
+
|
| 1537 |
+
return tuple(constants)
|
| 1538 |
+
|
| 1539 |
+
@property
|
| 1540 |
+
def bodies(self):
|
| 1541 |
+
"""Returns the bodies in the system"""
|
| 1542 |
+
if self._bodies is None:
|
| 1543 |
+
raise AttributeError("bodies were not specified for the system.")
|
| 1544 |
+
else:
|
| 1545 |
+
return self._bodies
|
| 1546 |
+
|
| 1547 |
+
@property
|
| 1548 |
+
def loads(self):
|
| 1549 |
+
"""Returns the loads in the system"""
|
| 1550 |
+
if self._loads is None:
|
| 1551 |
+
raise AttributeError("loads were not specified for the system.")
|
| 1552 |
+
else:
|
| 1553 |
+
return self._loads
|
lib/python3.12/site-packages/sympy/physics/mechanics/wrapping_geometry.py
ADDED
|
@@ -0,0 +1,641 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Geometry objects for use by wrapping pathways."""
|
| 2 |
+
|
| 3 |
+
from abc import ABC, abstractmethod
|
| 4 |
+
|
| 5 |
+
from sympy import Integer, acos, pi, sqrt, sympify, tan
|
| 6 |
+
from sympy.core.relational import Eq
|
| 7 |
+
from sympy.functions.elementary.trigonometric import atan2
|
| 8 |
+
from sympy.polys.polytools import cancel
|
| 9 |
+
from sympy.physics.vector import Vector, dot
|
| 10 |
+
from sympy.simplify.simplify import trigsimp
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
__all__ = [
|
| 14 |
+
'WrappingGeometryBase',
|
| 15 |
+
'WrappingCylinder',
|
| 16 |
+
'WrappingSphere',
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class WrappingGeometryBase(ABC):
|
| 21 |
+
"""Abstract base class for all geometry classes to inherit from.
|
| 22 |
+
|
| 23 |
+
Notes
|
| 24 |
+
=====
|
| 25 |
+
|
| 26 |
+
Instances of this class cannot be directly instantiated by users. However,
|
| 27 |
+
it can be used to created custom geometry types through subclassing.
|
| 28 |
+
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
@property
|
| 32 |
+
@abstractmethod
|
| 33 |
+
def point(cls):
|
| 34 |
+
"""The point with which the geometry is associated."""
|
| 35 |
+
pass
|
| 36 |
+
|
| 37 |
+
@abstractmethod
|
| 38 |
+
def point_on_surface(self, point):
|
| 39 |
+
"""Returns ``True`` if a point is on the geometry's surface.
|
| 40 |
+
|
| 41 |
+
Parameters
|
| 42 |
+
==========
|
| 43 |
+
point : Point
|
| 44 |
+
The point for which it's to be ascertained if it's on the
|
| 45 |
+
geometry's surface or not.
|
| 46 |
+
|
| 47 |
+
"""
|
| 48 |
+
pass
|
| 49 |
+
|
| 50 |
+
@abstractmethod
|
| 51 |
+
def geodesic_length(self, point_1, point_2):
|
| 52 |
+
"""Returns the shortest distance between two points on a geometry's
|
| 53 |
+
surface.
|
| 54 |
+
|
| 55 |
+
Parameters
|
| 56 |
+
==========
|
| 57 |
+
|
| 58 |
+
point_1 : Point
|
| 59 |
+
The point from which the geodesic length should be calculated.
|
| 60 |
+
point_2 : Point
|
| 61 |
+
The point to which the geodesic length should be calculated.
|
| 62 |
+
|
| 63 |
+
"""
|
| 64 |
+
pass
|
| 65 |
+
|
| 66 |
+
@abstractmethod
|
| 67 |
+
def geodesic_end_vectors(self, point_1, point_2):
|
| 68 |
+
"""The vectors parallel to the geodesic at the two end points.
|
| 69 |
+
|
| 70 |
+
Parameters
|
| 71 |
+
==========
|
| 72 |
+
|
| 73 |
+
point_1 : Point
|
| 74 |
+
The point from which the geodesic originates.
|
| 75 |
+
point_2 : Point
|
| 76 |
+
The point at which the geodesic terminates.
|
| 77 |
+
|
| 78 |
+
"""
|
| 79 |
+
pass
|
| 80 |
+
|
| 81 |
+
def __repr__(self):
|
| 82 |
+
"""Default representation of a geometry object."""
|
| 83 |
+
return f'{self.__class__.__name__}()'
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class WrappingSphere(WrappingGeometryBase):
|
| 87 |
+
"""A solid spherical object.
|
| 88 |
+
|
| 89 |
+
Explanation
|
| 90 |
+
===========
|
| 91 |
+
|
| 92 |
+
A wrapping geometry that allows for circular arcs to be defined between
|
| 93 |
+
pairs of points. These paths are always geodetic (the shortest possible).
|
| 94 |
+
|
| 95 |
+
Examples
|
| 96 |
+
========
|
| 97 |
+
|
| 98 |
+
To create a ``WrappingSphere`` instance, a ``Symbol`` denoting its radius
|
| 99 |
+
and ``Point`` at which its center will be located are needed:
|
| 100 |
+
|
| 101 |
+
>>> from sympy import symbols
|
| 102 |
+
>>> from sympy.physics.mechanics import Point, WrappingSphere
|
| 103 |
+
>>> r = symbols('r')
|
| 104 |
+
>>> pO = Point('pO')
|
| 105 |
+
|
| 106 |
+
A sphere with radius ``r`` centered on ``pO`` can be instantiated with:
|
| 107 |
+
|
| 108 |
+
>>> WrappingSphere(r, pO)
|
| 109 |
+
WrappingSphere(radius=r, point=pO)
|
| 110 |
+
|
| 111 |
+
Parameters
|
| 112 |
+
==========
|
| 113 |
+
|
| 114 |
+
radius : Symbol
|
| 115 |
+
Radius of the sphere. This symbol must represent a value that is
|
| 116 |
+
positive and constant, i.e. it cannot be a dynamic symbol, nor can it
|
| 117 |
+
be an expression.
|
| 118 |
+
point : Point
|
| 119 |
+
A point at which the sphere is centered.
|
| 120 |
+
|
| 121 |
+
See Also
|
| 122 |
+
========
|
| 123 |
+
|
| 124 |
+
WrappingCylinder: Cylindrical geometry where the wrapping direction can be
|
| 125 |
+
defined.
|
| 126 |
+
|
| 127 |
+
"""
|
| 128 |
+
|
| 129 |
+
def __init__(self, radius, point):
|
| 130 |
+
"""Initializer for ``WrappingSphere``.
|
| 131 |
+
|
| 132 |
+
Parameters
|
| 133 |
+
==========
|
| 134 |
+
|
| 135 |
+
radius : Symbol
|
| 136 |
+
The radius of the sphere.
|
| 137 |
+
point : Point
|
| 138 |
+
A point on which the sphere is centered.
|
| 139 |
+
|
| 140 |
+
"""
|
| 141 |
+
self.radius = radius
|
| 142 |
+
self.point = point
|
| 143 |
+
|
| 144 |
+
@property
|
| 145 |
+
def radius(self):
|
| 146 |
+
"""Radius of the sphere."""
|
| 147 |
+
return self._radius
|
| 148 |
+
|
| 149 |
+
@radius.setter
|
| 150 |
+
def radius(self, radius):
|
| 151 |
+
self._radius = radius
|
| 152 |
+
|
| 153 |
+
@property
|
| 154 |
+
def point(self):
|
| 155 |
+
"""A point on which the sphere is centered."""
|
| 156 |
+
return self._point
|
| 157 |
+
|
| 158 |
+
@point.setter
|
| 159 |
+
def point(self, point):
|
| 160 |
+
self._point = point
|
| 161 |
+
|
| 162 |
+
def point_on_surface(self, point):
|
| 163 |
+
"""Returns ``True`` if a point is on the sphere's surface.
|
| 164 |
+
|
| 165 |
+
Parameters
|
| 166 |
+
==========
|
| 167 |
+
|
| 168 |
+
point : Point
|
| 169 |
+
The point for which it's to be ascertained if it's on the sphere's
|
| 170 |
+
surface or not. This point's position relative to the sphere's
|
| 171 |
+
center must be a simple expression involving the radius of the
|
| 172 |
+
sphere, otherwise this check will likely not work.
|
| 173 |
+
|
| 174 |
+
"""
|
| 175 |
+
point_vector = point.pos_from(self.point)
|
| 176 |
+
if isinstance(point_vector, Vector):
|
| 177 |
+
point_radius_squared = dot(point_vector, point_vector)
|
| 178 |
+
else:
|
| 179 |
+
point_radius_squared = point_vector**2
|
| 180 |
+
return Eq(point_radius_squared, self.radius**2) == True
|
| 181 |
+
|
| 182 |
+
def geodesic_length(self, point_1, point_2):
|
| 183 |
+
r"""Returns the shortest distance between two points on the sphere's
|
| 184 |
+
surface.
|
| 185 |
+
|
| 186 |
+
Explanation
|
| 187 |
+
===========
|
| 188 |
+
|
| 189 |
+
The geodesic length, i.e. the shortest arc along the surface of a
|
| 190 |
+
sphere, connecting two points can be calculated using the formula:
|
| 191 |
+
|
| 192 |
+
.. math::
|
| 193 |
+
|
| 194 |
+
l = \arccos\left(\mathbf{v}_1 \cdot \mathbf{v}_2\right)
|
| 195 |
+
|
| 196 |
+
where $\mathbf{v}_1$ and $\mathbf{v}_2$ are the unit vectors from the
|
| 197 |
+
sphere's center to the first and second points on the sphere's surface
|
| 198 |
+
respectively. Note that the actual path that the geodesic will take is
|
| 199 |
+
undefined when the two points are directly opposite one another.
|
| 200 |
+
|
| 201 |
+
Examples
|
| 202 |
+
========
|
| 203 |
+
|
| 204 |
+
A geodesic length can only be calculated between two points on the
|
| 205 |
+
sphere's surface. Firstly, a ``WrappingSphere`` instance must be
|
| 206 |
+
created along with two points that will lie on its surface:
|
| 207 |
+
|
| 208 |
+
>>> from sympy import symbols
|
| 209 |
+
>>> from sympy.physics.mechanics import (Point, ReferenceFrame,
|
| 210 |
+
... WrappingSphere)
|
| 211 |
+
>>> N = ReferenceFrame('N')
|
| 212 |
+
>>> r = symbols('r')
|
| 213 |
+
>>> pO = Point('pO')
|
| 214 |
+
>>> pO.set_vel(N, 0)
|
| 215 |
+
>>> sphere = WrappingSphere(r, pO)
|
| 216 |
+
>>> p1 = Point('p1')
|
| 217 |
+
>>> p2 = Point('p2')
|
| 218 |
+
|
| 219 |
+
Let's assume that ``p1`` lies at a distance of ``r`` in the ``N.x``
|
| 220 |
+
direction from ``pO`` and that ``p2`` is located on the sphere's
|
| 221 |
+
surface in the ``N.y + N.z`` direction from ``pO``. These positions can
|
| 222 |
+
be set with:
|
| 223 |
+
|
| 224 |
+
>>> p1.set_pos(pO, r*N.x)
|
| 225 |
+
>>> p1.pos_from(pO)
|
| 226 |
+
r*N.x
|
| 227 |
+
>>> p2.set_pos(pO, r*(N.y + N.z).normalize())
|
| 228 |
+
>>> p2.pos_from(pO)
|
| 229 |
+
sqrt(2)*r/2*N.y + sqrt(2)*r/2*N.z
|
| 230 |
+
|
| 231 |
+
The geodesic length, which is in this case is a quarter of the sphere's
|
| 232 |
+
circumference, can be calculated using the ``geodesic_length`` method:
|
| 233 |
+
|
| 234 |
+
>>> sphere.geodesic_length(p1, p2)
|
| 235 |
+
pi*r/2
|
| 236 |
+
|
| 237 |
+
If the ``geodesic_length`` method is passed an argument, the ``Point``
|
| 238 |
+
that doesn't lie on the sphere's surface then a ``ValueError`` is
|
| 239 |
+
raised because it's not possible to calculate a value in this case.
|
| 240 |
+
|
| 241 |
+
Parameters
|
| 242 |
+
==========
|
| 243 |
+
|
| 244 |
+
point_1 : Point
|
| 245 |
+
Point from which the geodesic length should be calculated.
|
| 246 |
+
point_2 : Point
|
| 247 |
+
Point to which the geodesic length should be calculated.
|
| 248 |
+
|
| 249 |
+
"""
|
| 250 |
+
for point in (point_1, point_2):
|
| 251 |
+
if not self.point_on_surface(point):
|
| 252 |
+
msg = (
|
| 253 |
+
f'Geodesic length cannot be calculated as point {point} '
|
| 254 |
+
f'with radius {point.pos_from(self.point).magnitude()} '
|
| 255 |
+
f'from the sphere\'s center {self.point} does not lie on '
|
| 256 |
+
f'the surface of {self} with radius {self.radius}.'
|
| 257 |
+
)
|
| 258 |
+
raise ValueError(msg)
|
| 259 |
+
point_1_vector = point_1.pos_from(self.point).normalize()
|
| 260 |
+
point_2_vector = point_2.pos_from(self.point).normalize()
|
| 261 |
+
central_angle = acos(point_2_vector.dot(point_1_vector))
|
| 262 |
+
geodesic_length = self.radius*central_angle
|
| 263 |
+
return geodesic_length
|
| 264 |
+
|
| 265 |
+
def geodesic_end_vectors(self, point_1, point_2):
|
| 266 |
+
"""The vectors parallel to the geodesic at the two end points.
|
| 267 |
+
|
| 268 |
+
Parameters
|
| 269 |
+
==========
|
| 270 |
+
|
| 271 |
+
point_1 : Point
|
| 272 |
+
The point from which the geodesic originates.
|
| 273 |
+
point_2 : Point
|
| 274 |
+
The point at which the geodesic terminates.
|
| 275 |
+
|
| 276 |
+
"""
|
| 277 |
+
pA, pB = point_1, point_2
|
| 278 |
+
pO = self.point
|
| 279 |
+
pA_vec = pA.pos_from(pO)
|
| 280 |
+
pB_vec = pB.pos_from(pO)
|
| 281 |
+
|
| 282 |
+
if pA_vec.cross(pB_vec) == 0:
|
| 283 |
+
msg = (
|
| 284 |
+
f'Can\'t compute geodesic end vectors for the pair of points '
|
| 285 |
+
f'{pA} and {pB} on a sphere {self} as they are diametrically '
|
| 286 |
+
f'opposed, thus the geodesic is not defined.'
|
| 287 |
+
)
|
| 288 |
+
raise ValueError(msg)
|
| 289 |
+
|
| 290 |
+
return (
|
| 291 |
+
pA_vec.cross(pB.pos_from(pA)).cross(pA_vec).normalize(),
|
| 292 |
+
pB_vec.cross(pA.pos_from(pB)).cross(pB_vec).normalize(),
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
def __repr__(self):
|
| 296 |
+
"""Representation of a ``WrappingSphere``."""
|
| 297 |
+
return (
|
| 298 |
+
f'{self.__class__.__name__}(radius={self.radius}, '
|
| 299 |
+
f'point={self.point})'
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
class WrappingCylinder(WrappingGeometryBase):
|
| 304 |
+
"""A solid (infinite) cylindrical object.
|
| 305 |
+
|
| 306 |
+
Explanation
|
| 307 |
+
===========
|
| 308 |
+
|
| 309 |
+
A wrapping geometry that allows for circular arcs to be defined between
|
| 310 |
+
pairs of points. These paths are always geodetic (the shortest possible) in
|
| 311 |
+
the sense that they will be a straight line on the unwrapped cylinder's
|
| 312 |
+
surface. However, it is also possible for a direction to be specified, i.e.
|
| 313 |
+
paths can be influenced such that they either wrap along the shortest side
|
| 314 |
+
or the longest side of the cylinder. To define these directions, rotations
|
| 315 |
+
are in the positive direction following the right-hand rule.
|
| 316 |
+
|
| 317 |
+
Examples
|
| 318 |
+
========
|
| 319 |
+
|
| 320 |
+
To create a ``WrappingCylinder`` instance, a ``Symbol`` denoting its
|
| 321 |
+
radius, a ``Vector`` defining its axis, and a ``Point`` through which its
|
| 322 |
+
axis passes are needed:
|
| 323 |
+
|
| 324 |
+
>>> from sympy import symbols
|
| 325 |
+
>>> from sympy.physics.mechanics import (Point, ReferenceFrame,
|
| 326 |
+
... WrappingCylinder)
|
| 327 |
+
>>> N = ReferenceFrame('N')
|
| 328 |
+
>>> r = symbols('r')
|
| 329 |
+
>>> pO = Point('pO')
|
| 330 |
+
>>> ax = N.x
|
| 331 |
+
|
| 332 |
+
A cylinder with radius ``r``, and axis parallel to ``N.x`` passing through
|
| 333 |
+
``pO`` can be instantiated with:
|
| 334 |
+
|
| 335 |
+
>>> WrappingCylinder(r, pO, ax)
|
| 336 |
+
WrappingCylinder(radius=r, point=pO, axis=N.x)
|
| 337 |
+
|
| 338 |
+
Parameters
|
| 339 |
+
==========
|
| 340 |
+
|
| 341 |
+
radius : Symbol
|
| 342 |
+
The radius of the cylinder.
|
| 343 |
+
point : Point
|
| 344 |
+
A point through which the cylinder's axis passes.
|
| 345 |
+
axis : Vector
|
| 346 |
+
The axis along which the cylinder is aligned.
|
| 347 |
+
|
| 348 |
+
See Also
|
| 349 |
+
========
|
| 350 |
+
|
| 351 |
+
WrappingSphere: Spherical geometry where the wrapping direction is always
|
| 352 |
+
geodetic.
|
| 353 |
+
|
| 354 |
+
"""
|
| 355 |
+
|
| 356 |
+
def __init__(self, radius, point, axis):
|
| 357 |
+
"""Initializer for ``WrappingCylinder``.
|
| 358 |
+
|
| 359 |
+
Parameters
|
| 360 |
+
==========
|
| 361 |
+
|
| 362 |
+
radius : Symbol
|
| 363 |
+
The radius of the cylinder. This symbol must represent a value that
|
| 364 |
+
is positive and constant, i.e. it cannot be a dynamic symbol.
|
| 365 |
+
point : Point
|
| 366 |
+
A point through which the cylinder's axis passes.
|
| 367 |
+
axis : Vector
|
| 368 |
+
The axis along which the cylinder is aligned.
|
| 369 |
+
|
| 370 |
+
"""
|
| 371 |
+
self.radius = radius
|
| 372 |
+
self.point = point
|
| 373 |
+
self.axis = axis
|
| 374 |
+
|
| 375 |
+
@property
|
| 376 |
+
def radius(self):
|
| 377 |
+
"""Radius of the cylinder."""
|
| 378 |
+
return self._radius
|
| 379 |
+
|
| 380 |
+
@radius.setter
|
| 381 |
+
def radius(self, radius):
|
| 382 |
+
self._radius = radius
|
| 383 |
+
|
| 384 |
+
@property
|
| 385 |
+
def point(self):
|
| 386 |
+
"""A point through which the cylinder's axis passes."""
|
| 387 |
+
return self._point
|
| 388 |
+
|
| 389 |
+
@point.setter
|
| 390 |
+
def point(self, point):
|
| 391 |
+
self._point = point
|
| 392 |
+
|
| 393 |
+
@property
|
| 394 |
+
def axis(self):
|
| 395 |
+
"""Axis along which the cylinder is aligned."""
|
| 396 |
+
return self._axis
|
| 397 |
+
|
| 398 |
+
@axis.setter
|
| 399 |
+
def axis(self, axis):
|
| 400 |
+
self._axis = axis.normalize()
|
| 401 |
+
|
| 402 |
+
def point_on_surface(self, point):
|
| 403 |
+
"""Returns ``True`` if a point is on the cylinder's surface.
|
| 404 |
+
|
| 405 |
+
Parameters
|
| 406 |
+
==========
|
| 407 |
+
|
| 408 |
+
point : Point
|
| 409 |
+
The point for which it's to be ascertained if it's on the
|
| 410 |
+
cylinder's surface or not. This point's position relative to the
|
| 411 |
+
cylinder's axis must be a simple expression involving the radius of
|
| 412 |
+
the sphere, otherwise this check will likely not work.
|
| 413 |
+
|
| 414 |
+
"""
|
| 415 |
+
relative_position = point.pos_from(self.point)
|
| 416 |
+
parallel = relative_position.dot(self.axis) * self.axis
|
| 417 |
+
point_vector = relative_position - parallel
|
| 418 |
+
if isinstance(point_vector, Vector):
|
| 419 |
+
point_radius_squared = dot(point_vector, point_vector)
|
| 420 |
+
else:
|
| 421 |
+
point_radius_squared = point_vector**2
|
| 422 |
+
return Eq(trigsimp(point_radius_squared), self.radius**2) == True
|
| 423 |
+
|
| 424 |
+
def geodesic_length(self, point_1, point_2):
|
| 425 |
+
"""The shortest distance between two points on a geometry's surface.
|
| 426 |
+
|
| 427 |
+
Explanation
|
| 428 |
+
===========
|
| 429 |
+
|
| 430 |
+
The geodesic length, i.e. the shortest arc along the surface of a
|
| 431 |
+
cylinder, connecting two points. It can be calculated using Pythagoras'
|
| 432 |
+
theorem. The first short side is the distance between the two points on
|
| 433 |
+
the cylinder's surface parallel to the cylinder's axis. The second
|
| 434 |
+
short side is the arc of a circle between the two points of the
|
| 435 |
+
cylinder's surface perpendicular to the cylinder's axis. The resulting
|
| 436 |
+
hypotenuse is the geodesic length.
|
| 437 |
+
|
| 438 |
+
Examples
|
| 439 |
+
========
|
| 440 |
+
|
| 441 |
+
A geodesic length can only be calculated between two points on the
|
| 442 |
+
cylinder's surface. Firstly, a ``WrappingCylinder`` instance must be
|
| 443 |
+
created along with two points that will lie on its surface:
|
| 444 |
+
|
| 445 |
+
>>> from sympy import symbols, cos, sin
|
| 446 |
+
>>> from sympy.physics.mechanics import (Point, ReferenceFrame,
|
| 447 |
+
... WrappingCylinder, dynamicsymbols)
|
| 448 |
+
>>> N = ReferenceFrame('N')
|
| 449 |
+
>>> r = symbols('r')
|
| 450 |
+
>>> pO = Point('pO')
|
| 451 |
+
>>> pO.set_vel(N, 0)
|
| 452 |
+
>>> cylinder = WrappingCylinder(r, pO, N.x)
|
| 453 |
+
>>> p1 = Point('p1')
|
| 454 |
+
>>> p2 = Point('p2')
|
| 455 |
+
|
| 456 |
+
Let's assume that ``p1`` is located at ``N.x + r*N.y`` relative to
|
| 457 |
+
``pO`` and that ``p2`` is located at ``r*(cos(q)*N.y + sin(q)*N.z)``
|
| 458 |
+
relative to ``pO``, where ``q(t)`` is a generalized coordinate
|
| 459 |
+
specifying the angle rotated around the ``N.x`` axis according to the
|
| 460 |
+
right-hand rule where ``N.y`` is zero. These positions can be set with:
|
| 461 |
+
|
| 462 |
+
>>> q = dynamicsymbols('q')
|
| 463 |
+
>>> p1.set_pos(pO, N.x + r*N.y)
|
| 464 |
+
>>> p1.pos_from(pO)
|
| 465 |
+
N.x + r*N.y
|
| 466 |
+
>>> p2.set_pos(pO, r*(cos(q)*N.y + sin(q)*N.z).normalize())
|
| 467 |
+
>>> p2.pos_from(pO).simplify()
|
| 468 |
+
r*cos(q(t))*N.y + r*sin(q(t))*N.z
|
| 469 |
+
|
| 470 |
+
The geodesic length, which is in this case a is the hypotenuse of a
|
| 471 |
+
right triangle where the other two side lengths are ``1`` (parallel to
|
| 472 |
+
the cylinder's axis) and ``r*q(t)`` (parallel to the cylinder's cross
|
| 473 |
+
section), can be calculated using the ``geodesic_length`` method:
|
| 474 |
+
|
| 475 |
+
>>> cylinder.geodesic_length(p1, p2).simplify()
|
| 476 |
+
sqrt(r**2*q(t)**2 + 1)
|
| 477 |
+
|
| 478 |
+
If the ``geodesic_length`` method is passed an argument ``Point`` that
|
| 479 |
+
doesn't lie on the sphere's surface then a ``ValueError`` is raised
|
| 480 |
+
because it's not possible to calculate a value in this case.
|
| 481 |
+
|
| 482 |
+
Parameters
|
| 483 |
+
==========
|
| 484 |
+
|
| 485 |
+
point_1 : Point
|
| 486 |
+
Point from which the geodesic length should be calculated.
|
| 487 |
+
point_2 : Point
|
| 488 |
+
Point to which the geodesic length should be calculated.
|
| 489 |
+
|
| 490 |
+
"""
|
| 491 |
+
for point in (point_1, point_2):
|
| 492 |
+
if not self.point_on_surface(point):
|
| 493 |
+
msg = (
|
| 494 |
+
f'Geodesic length cannot be calculated as point {point} '
|
| 495 |
+
f'with radius {point.pos_from(self.point).magnitude()} '
|
| 496 |
+
f'from the cylinder\'s center {self.point} does not lie on '
|
| 497 |
+
f'the surface of {self} with radius {self.radius} and axis '
|
| 498 |
+
f'{self.axis}.'
|
| 499 |
+
)
|
| 500 |
+
raise ValueError(msg)
|
| 501 |
+
|
| 502 |
+
relative_position = point_2.pos_from(point_1)
|
| 503 |
+
parallel_length = relative_position.dot(self.axis)
|
| 504 |
+
|
| 505 |
+
point_1_relative_position = point_1.pos_from(self.point)
|
| 506 |
+
point_1_perpendicular_vector = (
|
| 507 |
+
point_1_relative_position
|
| 508 |
+
- point_1_relative_position.dot(self.axis)*self.axis
|
| 509 |
+
).normalize()
|
| 510 |
+
|
| 511 |
+
point_2_relative_position = point_2.pos_from(self.point)
|
| 512 |
+
point_2_perpendicular_vector = (
|
| 513 |
+
point_2_relative_position
|
| 514 |
+
- point_2_relative_position.dot(self.axis)*self.axis
|
| 515 |
+
).normalize()
|
| 516 |
+
|
| 517 |
+
central_angle = _directional_atan(
|
| 518 |
+
cancel(point_1_perpendicular_vector
|
| 519 |
+
.cross(point_2_perpendicular_vector)
|
| 520 |
+
.dot(self.axis)),
|
| 521 |
+
cancel(point_1_perpendicular_vector.dot(point_2_perpendicular_vector)),
|
| 522 |
+
)
|
| 523 |
+
|
| 524 |
+
planar_arc_length = self.radius*central_angle
|
| 525 |
+
geodesic_length = sqrt(parallel_length**2 + planar_arc_length**2)
|
| 526 |
+
return geodesic_length
|
| 527 |
+
|
| 528 |
+
def geodesic_end_vectors(self, point_1, point_2):
|
| 529 |
+
"""The vectors parallel to the geodesic at the two end points.
|
| 530 |
+
|
| 531 |
+
Parameters
|
| 532 |
+
==========
|
| 533 |
+
|
| 534 |
+
point_1 : Point
|
| 535 |
+
The point from which the geodesic originates.
|
| 536 |
+
point_2 : Point
|
| 537 |
+
The point at which the geodesic terminates.
|
| 538 |
+
|
| 539 |
+
"""
|
| 540 |
+
point_1_from_origin_point = point_1.pos_from(self.point)
|
| 541 |
+
point_2_from_origin_point = point_2.pos_from(self.point)
|
| 542 |
+
|
| 543 |
+
if point_1_from_origin_point == point_2_from_origin_point:
|
| 544 |
+
msg = (
|
| 545 |
+
f'Cannot compute geodesic end vectors for coincident points '
|
| 546 |
+
f'{point_1} and {point_2} as no geodesic exists.'
|
| 547 |
+
)
|
| 548 |
+
raise ValueError(msg)
|
| 549 |
+
|
| 550 |
+
point_1_parallel = point_1_from_origin_point.dot(self.axis) * self.axis
|
| 551 |
+
point_2_parallel = point_2_from_origin_point.dot(self.axis) * self.axis
|
| 552 |
+
point_1_normal = (point_1_from_origin_point - point_1_parallel)
|
| 553 |
+
point_2_normal = (point_2_from_origin_point - point_2_parallel)
|
| 554 |
+
|
| 555 |
+
if point_1_normal == point_2_normal:
|
| 556 |
+
point_1_perpendicular = Vector(0)
|
| 557 |
+
point_2_perpendicular = Vector(0)
|
| 558 |
+
else:
|
| 559 |
+
point_1_perpendicular = self.axis.cross(point_1_normal).normalize()
|
| 560 |
+
point_2_perpendicular = -self.axis.cross(point_2_normal).normalize()
|
| 561 |
+
|
| 562 |
+
geodesic_length = self.geodesic_length(point_1, point_2)
|
| 563 |
+
relative_position = point_2.pos_from(point_1)
|
| 564 |
+
parallel_length = relative_position.dot(self.axis)
|
| 565 |
+
planar_arc_length = sqrt(geodesic_length**2 - parallel_length**2)
|
| 566 |
+
|
| 567 |
+
point_1_vector = (
|
| 568 |
+
planar_arc_length * point_1_perpendicular
|
| 569 |
+
+ parallel_length * self.axis
|
| 570 |
+
).normalize()
|
| 571 |
+
point_2_vector = (
|
| 572 |
+
planar_arc_length * point_2_perpendicular
|
| 573 |
+
- parallel_length * self.axis
|
| 574 |
+
).normalize()
|
| 575 |
+
|
| 576 |
+
return (point_1_vector, point_2_vector)
|
| 577 |
+
|
| 578 |
+
def __repr__(self):
|
| 579 |
+
"""Representation of a ``WrappingCylinder``."""
|
| 580 |
+
return (
|
| 581 |
+
f'{self.__class__.__name__}(radius={self.radius}, '
|
| 582 |
+
f'point={self.point}, axis={self.axis})'
|
| 583 |
+
)
|
| 584 |
+
|
| 585 |
+
|
| 586 |
+
def _directional_atan(numerator, denominator):
|
| 587 |
+
"""Compute atan in a directional sense as required for geodesics.
|
| 588 |
+
|
| 589 |
+
Explanation
|
| 590 |
+
===========
|
| 591 |
+
|
| 592 |
+
To be able to control the direction of the geodesic length along the
|
| 593 |
+
surface of a cylinder a dedicated arctangent function is needed that
|
| 594 |
+
properly handles the directionality of different case. This function
|
| 595 |
+
ensures that the central angle is always positive but shifting the case
|
| 596 |
+
where ``atan2`` would return a negative angle to be centered around
|
| 597 |
+
``2*pi``.
|
| 598 |
+
|
| 599 |
+
Notes
|
| 600 |
+
=====
|
| 601 |
+
|
| 602 |
+
This function only handles very specific cases, i.e. the ones that are
|
| 603 |
+
expected to be encountered when calculating symbolic geodesics on uniformly
|
| 604 |
+
curved surfaces. As such, ``NotImplemented`` errors can be raised in many
|
| 605 |
+
cases. This function is named with a leader underscore to indicate that it
|
| 606 |
+
only aims to provide very specific functionality within the private scope
|
| 607 |
+
of this module.
|
| 608 |
+
|
| 609 |
+
"""
|
| 610 |
+
|
| 611 |
+
if numerator.is_number and denominator.is_number:
|
| 612 |
+
angle = atan2(numerator, denominator)
|
| 613 |
+
if angle < 0:
|
| 614 |
+
angle += 2 * pi
|
| 615 |
+
elif numerator.is_number:
|
| 616 |
+
msg = (
|
| 617 |
+
f'Cannot compute a directional atan when the numerator {numerator} '
|
| 618 |
+
f'is numeric and the denominator {denominator} is symbolic.'
|
| 619 |
+
)
|
| 620 |
+
raise NotImplementedError(msg)
|
| 621 |
+
elif denominator.is_number:
|
| 622 |
+
msg = (
|
| 623 |
+
f'Cannot compute a directional atan when the numerator {numerator} '
|
| 624 |
+
f'is symbolic and the denominator {denominator} is numeric.'
|
| 625 |
+
)
|
| 626 |
+
raise NotImplementedError(msg)
|
| 627 |
+
else:
|
| 628 |
+
ratio = sympify(trigsimp(numerator / denominator))
|
| 629 |
+
if isinstance(ratio, tan):
|
| 630 |
+
angle = ratio.args[0]
|
| 631 |
+
elif (
|
| 632 |
+
ratio.is_Mul
|
| 633 |
+
and ratio.args[0] == Integer(-1)
|
| 634 |
+
and isinstance(ratio.args[1], tan)
|
| 635 |
+
):
|
| 636 |
+
angle = 2 * pi - ratio.args[1].args[0]
|
| 637 |
+
else:
|
| 638 |
+
msg = f'Cannot compute a directional atan for the value {ratio}.'
|
| 639 |
+
raise NotImplementedError(msg)
|
| 640 |
+
|
| 641 |
+
return angle
|
lib/python3.12/site-packages/sympy/physics/tests/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (198 Bytes). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_clebsch_gordan.cpython-312.pyc
ADDED
|
Binary file (17.9 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_hydrogen.cpython-312.pyc
ADDED
|
Binary file (10.5 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_paulialgebra.cpython-312.pyc
ADDED
|
Binary file (3.12 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_physics_matrices.cpython-312.pyc
ADDED
|
Binary file (4.79 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_pring.cpython-312.pyc
ADDED
|
Binary file (2.74 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_qho_1d.cpython-312.pyc
ADDED
|
Binary file (3.82 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_secondquant.cpython-312.pyc
ADDED
|
Binary file (86.8 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_sho.cpython-312.pyc
ADDED
|
Binary file (1.66 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/tests/test_physics_matrices.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy.physics.matrices import msigma, mgamma, minkowski_tensor, pat_matrix, mdft
|
| 2 |
+
from sympy.core.numbers import (I, Rational)
|
| 3 |
+
from sympy.core.singleton import S
|
| 4 |
+
from sympy.functions.elementary.miscellaneous import sqrt
|
| 5 |
+
from sympy.matrices.dense import (Matrix, eye, zeros)
|
| 6 |
+
from sympy.testing.pytest import warns_deprecated_sympy
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def test_parallel_axis_theorem():
|
| 10 |
+
# This tests the parallel axis theorem matrix by comparing to test
|
| 11 |
+
# matrices.
|
| 12 |
+
|
| 13 |
+
# First case, 1 in all directions.
|
| 14 |
+
mat1 = Matrix(((2, -1, -1), (-1, 2, -1), (-1, -1, 2)))
|
| 15 |
+
assert pat_matrix(1, 1, 1, 1) == mat1
|
| 16 |
+
assert pat_matrix(2, 1, 1, 1) == 2*mat1
|
| 17 |
+
|
| 18 |
+
# Second case, 1 in x, 0 in all others
|
| 19 |
+
mat2 = Matrix(((0, 0, 0), (0, 1, 0), (0, 0, 1)))
|
| 20 |
+
assert pat_matrix(1, 1, 0, 0) == mat2
|
| 21 |
+
assert pat_matrix(2, 1, 0, 0) == 2*mat2
|
| 22 |
+
|
| 23 |
+
# Third case, 1 in y, 0 in all others
|
| 24 |
+
mat3 = Matrix(((1, 0, 0), (0, 0, 0), (0, 0, 1)))
|
| 25 |
+
assert pat_matrix(1, 0, 1, 0) == mat3
|
| 26 |
+
assert pat_matrix(2, 0, 1, 0) == 2*mat3
|
| 27 |
+
|
| 28 |
+
# Fourth case, 1 in z, 0 in all others
|
| 29 |
+
mat4 = Matrix(((1, 0, 0), (0, 1, 0), (0, 0, 0)))
|
| 30 |
+
assert pat_matrix(1, 0, 0, 1) == mat4
|
| 31 |
+
assert pat_matrix(2, 0, 0, 1) == 2*mat4
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_Pauli():
|
| 35 |
+
#this and the following test are testing both Pauli and Dirac matrices
|
| 36 |
+
#and also that the general Matrix class works correctly in a real world
|
| 37 |
+
#situation
|
| 38 |
+
sigma1 = msigma(1)
|
| 39 |
+
sigma2 = msigma(2)
|
| 40 |
+
sigma3 = msigma(3)
|
| 41 |
+
|
| 42 |
+
assert sigma1 == sigma1
|
| 43 |
+
assert sigma1 != sigma2
|
| 44 |
+
|
| 45 |
+
# sigma*I -> I*sigma (see #354)
|
| 46 |
+
assert sigma1*sigma2 == sigma3*I
|
| 47 |
+
assert sigma3*sigma1 == sigma2*I
|
| 48 |
+
assert sigma2*sigma3 == sigma1*I
|
| 49 |
+
|
| 50 |
+
assert sigma1*sigma1 == eye(2)
|
| 51 |
+
assert sigma2*sigma2 == eye(2)
|
| 52 |
+
assert sigma3*sigma3 == eye(2)
|
| 53 |
+
|
| 54 |
+
assert sigma1*2*sigma1 == 2*eye(2)
|
| 55 |
+
assert sigma1*sigma3*sigma1 == -sigma3
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def test_Dirac():
|
| 59 |
+
gamma0 = mgamma(0)
|
| 60 |
+
gamma1 = mgamma(1)
|
| 61 |
+
gamma2 = mgamma(2)
|
| 62 |
+
gamma3 = mgamma(3)
|
| 63 |
+
gamma5 = mgamma(5)
|
| 64 |
+
|
| 65 |
+
# gamma*I -> I*gamma (see #354)
|
| 66 |
+
assert gamma5 == gamma0 * gamma1 * gamma2 * gamma3 * I
|
| 67 |
+
assert gamma1 * gamma2 + gamma2 * gamma1 == zeros(4)
|
| 68 |
+
assert gamma0 * gamma0 == eye(4) * minkowski_tensor[0, 0]
|
| 69 |
+
assert gamma2 * gamma2 != eye(4) * minkowski_tensor[0, 0]
|
| 70 |
+
assert gamma2 * gamma2 == eye(4) * minkowski_tensor[2, 2]
|
| 71 |
+
|
| 72 |
+
assert mgamma(5, True) == \
|
| 73 |
+
mgamma(0, True)*mgamma(1, True)*mgamma(2, True)*mgamma(3, True)*I
|
| 74 |
+
|
| 75 |
+
def test_mdft():
|
| 76 |
+
with warns_deprecated_sympy():
|
| 77 |
+
assert mdft(1) == Matrix([[1]])
|
| 78 |
+
with warns_deprecated_sympy():
|
| 79 |
+
assert mdft(2) == 1/sqrt(2)*Matrix([[1,1],[1,-1]])
|
| 80 |
+
with warns_deprecated_sympy():
|
| 81 |
+
assert mdft(4) == Matrix([[S.Half, S.Half, S.Half, S.Half],
|
| 82 |
+
[S.Half, -I/2, Rational(-1,2), I/2],
|
| 83 |
+
[S.Half, Rational(-1,2), S.Half, Rational(-1,2)],
|
| 84 |
+
[S.Half, I/2, Rational(-1,2), -I/2]])
|
lib/python3.12/site-packages/sympy/physics/tests/test_secondquant.py
ADDED
|
@@ -0,0 +1,1280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy.physics.secondquant import (
|
| 2 |
+
Dagger, Bd, VarBosonicBasis, BBra, B, BKet, FixedBosonicBasis,
|
| 3 |
+
matrix_rep, apply_operators, InnerProduct, Commutator, KroneckerDelta,
|
| 4 |
+
AnnihilateBoson, CreateBoson, BosonicOperator,
|
| 5 |
+
F, Fd, FKet, BosonState, CreateFermion, AnnihilateFermion,
|
| 6 |
+
evaluate_deltas, AntiSymmetricTensor, contraction, NO, wicks,
|
| 7 |
+
PermutationOperator, simplify_index_permutations,
|
| 8 |
+
_sort_anticommuting_fermions, _get_ordered_dummies,
|
| 9 |
+
substitute_dummies, FockStateBosonKet,
|
| 10 |
+
ContractionAppliesOnlyToFermions
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
from sympy.concrete.summations import Sum
|
| 14 |
+
from sympy.core.function import (Function, expand)
|
| 15 |
+
from sympy.core.numbers import (I, Rational)
|
| 16 |
+
from sympy.core.singleton import S
|
| 17 |
+
from sympy.core.symbol import (Dummy, Symbol, symbols)
|
| 18 |
+
from sympy.functions.elementary.miscellaneous import sqrt
|
| 19 |
+
from sympy.printing.repr import srepr
|
| 20 |
+
from sympy.simplify.simplify import simplify
|
| 21 |
+
|
| 22 |
+
from sympy.testing.pytest import slow, raises
|
| 23 |
+
from sympy.printing.latex import latex
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def test_PermutationOperator():
|
| 27 |
+
p, q, r, s = symbols('p,q,r,s')
|
| 28 |
+
f, g, h, i = map(Function, 'fghi')
|
| 29 |
+
P = PermutationOperator
|
| 30 |
+
assert P(p, q).get_permuted(f(p)*g(q)) == -f(q)*g(p)
|
| 31 |
+
assert P(p, q).get_permuted(f(p, q)) == -f(q, p)
|
| 32 |
+
assert P(p, q).get_permuted(f(p)) == f(p)
|
| 33 |
+
expr = (f(p)*g(q)*h(r)*i(s)
|
| 34 |
+
- f(q)*g(p)*h(r)*i(s)
|
| 35 |
+
- f(p)*g(q)*h(s)*i(r)
|
| 36 |
+
+ f(q)*g(p)*h(s)*i(r))
|
| 37 |
+
perms = [P(p, q), P(r, s)]
|
| 38 |
+
assert (simplify_index_permutations(expr, perms) ==
|
| 39 |
+
P(p, q)*P(r, s)*f(p)*g(q)*h(r)*i(s))
|
| 40 |
+
assert latex(P(p, q)) == 'P(pq)'
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def test_index_permutations_with_dummies():
|
| 44 |
+
a, b, c, d = symbols('a b c d')
|
| 45 |
+
p, q, r, s = symbols('p q r s', cls=Dummy)
|
| 46 |
+
f, g = map(Function, 'fg')
|
| 47 |
+
P = PermutationOperator
|
| 48 |
+
|
| 49 |
+
# No dummy substitution necessary
|
| 50 |
+
expr = f(a, b, p, q) - f(b, a, p, q)
|
| 51 |
+
assert simplify_index_permutations(
|
| 52 |
+
expr, [P(a, b)]) == P(a, b)*f(a, b, p, q)
|
| 53 |
+
|
| 54 |
+
# Cases where dummy substitution is needed
|
| 55 |
+
expected = P(a, b)*substitute_dummies(f(a, b, p, q))
|
| 56 |
+
|
| 57 |
+
expr = f(a, b, p, q) - f(b, a, q, p)
|
| 58 |
+
result = simplify_index_permutations(expr, [P(a, b)])
|
| 59 |
+
assert expected == substitute_dummies(result)
|
| 60 |
+
|
| 61 |
+
expr = f(a, b, q, p) - f(b, a, p, q)
|
| 62 |
+
result = simplify_index_permutations(expr, [P(a, b)])
|
| 63 |
+
assert expected == substitute_dummies(result)
|
| 64 |
+
|
| 65 |
+
# A case where nothing can be done
|
| 66 |
+
expr = f(a, b, q, p) - g(b, a, p, q)
|
| 67 |
+
result = simplify_index_permutations(expr, [P(a, b)])
|
| 68 |
+
assert expr == result
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_dagger():
|
| 72 |
+
i, j, n, m = symbols('i,j,n,m')
|
| 73 |
+
assert Dagger(1) == 1
|
| 74 |
+
assert Dagger(1.0) == 1.0
|
| 75 |
+
assert Dagger(2*I) == -2*I
|
| 76 |
+
assert Dagger(S.Half*I/3.0) == I*Rational(-1, 2)/3.0
|
| 77 |
+
assert Dagger(BKet([n])) == BBra([n])
|
| 78 |
+
assert Dagger(B(0)) == Bd(0)
|
| 79 |
+
assert Dagger(Bd(0)) == B(0)
|
| 80 |
+
assert Dagger(B(n)) == Bd(n)
|
| 81 |
+
assert Dagger(Bd(n)) == B(n)
|
| 82 |
+
assert Dagger(B(0) + B(1)) == Bd(0) + Bd(1)
|
| 83 |
+
assert Dagger(n*m) == Dagger(n)*Dagger(m) # n, m commute
|
| 84 |
+
assert Dagger(B(n)*B(m)) == Bd(m)*Bd(n)
|
| 85 |
+
assert Dagger(B(n)**10) == Dagger(B(n))**10
|
| 86 |
+
assert Dagger('a') == Dagger(Symbol('a'))
|
| 87 |
+
assert Dagger(Dagger('a')) == Symbol('a')
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def test_operator():
|
| 91 |
+
i, j = symbols('i,j')
|
| 92 |
+
o = BosonicOperator(i)
|
| 93 |
+
assert o.state == i
|
| 94 |
+
assert o.is_symbolic
|
| 95 |
+
o = BosonicOperator(1)
|
| 96 |
+
assert o.state == 1
|
| 97 |
+
assert not o.is_symbolic
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def test_create():
|
| 101 |
+
i, j, n, m = symbols('i,j,n,m')
|
| 102 |
+
o = Bd(i)
|
| 103 |
+
assert latex(o) == "{b^\\dagger_{i}}"
|
| 104 |
+
assert isinstance(o, CreateBoson)
|
| 105 |
+
o = o.subs(i, j)
|
| 106 |
+
assert o.atoms(Symbol) == {j}
|
| 107 |
+
o = Bd(0)
|
| 108 |
+
assert o.apply_operator(BKet([n])) == sqrt(n + 1)*BKet([n + 1])
|
| 109 |
+
o = Bd(n)
|
| 110 |
+
assert o.apply_operator(BKet([n])) == o*BKet([n])
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def test_annihilate():
|
| 114 |
+
i, j, n, m = symbols('i,j,n,m')
|
| 115 |
+
o = B(i)
|
| 116 |
+
assert latex(o) == "b_{i}"
|
| 117 |
+
assert isinstance(o, AnnihilateBoson)
|
| 118 |
+
o = o.subs(i, j)
|
| 119 |
+
assert o.atoms(Symbol) == {j}
|
| 120 |
+
o = B(0)
|
| 121 |
+
assert o.apply_operator(BKet([n])) == sqrt(n)*BKet([n - 1])
|
| 122 |
+
o = B(n)
|
| 123 |
+
assert o.apply_operator(BKet([n])) == o*BKet([n])
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def test_basic_state():
|
| 127 |
+
i, j, n, m = symbols('i,j,n,m')
|
| 128 |
+
s = BosonState([0, 1, 2, 3, 4])
|
| 129 |
+
assert len(s) == 5
|
| 130 |
+
assert s.args[0] == tuple(range(5))
|
| 131 |
+
assert s.up(0) == BosonState([1, 1, 2, 3, 4])
|
| 132 |
+
assert s.down(4) == BosonState([0, 1, 2, 3, 3])
|
| 133 |
+
for i in range(5):
|
| 134 |
+
assert s.up(i).down(i) == s
|
| 135 |
+
assert s.down(0) == 0
|
| 136 |
+
for i in range(5):
|
| 137 |
+
assert s[i] == i
|
| 138 |
+
s = BosonState([n, m])
|
| 139 |
+
assert s.down(0) == BosonState([n - 1, m])
|
| 140 |
+
assert s.up(0) == BosonState([n + 1, m])
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def test_basic_apply():
|
| 144 |
+
n = symbols("n")
|
| 145 |
+
e = B(0)*BKet([n])
|
| 146 |
+
assert apply_operators(e) == sqrt(n)*BKet([n - 1])
|
| 147 |
+
e = Bd(0)*BKet([n])
|
| 148 |
+
assert apply_operators(e) == sqrt(n + 1)*BKet([n + 1])
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def test_complex_apply():
|
| 152 |
+
n, m = symbols("n,m")
|
| 153 |
+
o = Bd(0)*B(0)*Bd(1)*B(0)
|
| 154 |
+
e = apply_operators(o*BKet([n, m]))
|
| 155 |
+
answer = sqrt(n)*sqrt(m + 1)*(-1 + n)*BKet([-1 + n, 1 + m])
|
| 156 |
+
assert expand(e) == expand(answer)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def test_number_operator():
|
| 160 |
+
n = symbols("n")
|
| 161 |
+
o = Bd(0)*B(0)
|
| 162 |
+
e = apply_operators(o*BKet([n]))
|
| 163 |
+
assert e == n*BKet([n])
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def test_inner_product():
|
| 167 |
+
i, j, k, l = symbols('i,j,k,l')
|
| 168 |
+
s1 = BBra([0])
|
| 169 |
+
s2 = BKet([1])
|
| 170 |
+
assert InnerProduct(s1, Dagger(s1)) == 1
|
| 171 |
+
assert InnerProduct(s1, s2) == 0
|
| 172 |
+
s1 = BBra([i, j])
|
| 173 |
+
s2 = BKet([k, l])
|
| 174 |
+
r = InnerProduct(s1, s2)
|
| 175 |
+
assert r == KroneckerDelta(i, k)*KroneckerDelta(j, l)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def test_symbolic_matrix_elements():
|
| 179 |
+
n, m = symbols('n,m')
|
| 180 |
+
s1 = BBra([n])
|
| 181 |
+
s2 = BKet([m])
|
| 182 |
+
o = B(0)
|
| 183 |
+
e = apply_operators(s1*o*s2)
|
| 184 |
+
assert e == sqrt(m)*KroneckerDelta(n, m - 1)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def test_matrix_elements():
|
| 188 |
+
b = VarBosonicBasis(5)
|
| 189 |
+
o = B(0)
|
| 190 |
+
m = matrix_rep(o, b)
|
| 191 |
+
for i in range(4):
|
| 192 |
+
assert m[i, i + 1] == sqrt(i + 1)
|
| 193 |
+
o = Bd(0)
|
| 194 |
+
m = matrix_rep(o, b)
|
| 195 |
+
for i in range(4):
|
| 196 |
+
assert m[i + 1, i] == sqrt(i + 1)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def test_fixed_bosonic_basis():
|
| 200 |
+
b = FixedBosonicBasis(2, 2)
|
| 201 |
+
# assert b == [FockState((2, 0)), FockState((1, 1)), FockState((0, 2))]
|
| 202 |
+
state = b.state(1)
|
| 203 |
+
assert state == FockStateBosonKet((1, 1))
|
| 204 |
+
assert b.index(state) == 1
|
| 205 |
+
assert b.state(1) == b[1]
|
| 206 |
+
assert len(b) == 3
|
| 207 |
+
assert str(b) == '[FockState((2, 0)), FockState((1, 1)), FockState((0, 2))]'
|
| 208 |
+
assert repr(b) == '[FockState((2, 0)), FockState((1, 1)), FockState((0, 2))]'
|
| 209 |
+
assert srepr(b) == '[FockState((2, 0)), FockState((1, 1)), FockState((0, 2))]'
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
@slow
|
| 213 |
+
def test_sho():
|
| 214 |
+
n, m = symbols('n,m')
|
| 215 |
+
h_n = Bd(n)*B(n)*(n + S.Half)
|
| 216 |
+
H = Sum(h_n, (n, 0, 5))
|
| 217 |
+
o = H.doit(deep=False)
|
| 218 |
+
b = FixedBosonicBasis(2, 6)
|
| 219 |
+
m = matrix_rep(o, b)
|
| 220 |
+
# We need to double check these energy values to make sure that they
|
| 221 |
+
# are correct and have the proper degeneracies!
|
| 222 |
+
diag = [1, 2, 3, 3, 4, 5, 4, 5, 6, 7, 5, 6, 7, 8, 9, 6, 7, 8, 9, 10, 11]
|
| 223 |
+
for i in range(len(diag)):
|
| 224 |
+
assert diag[i] == m[i, i]
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def test_commutation():
|
| 228 |
+
n, m = symbols("n,m", above_fermi=True)
|
| 229 |
+
c = Commutator(B(0), Bd(0))
|
| 230 |
+
assert c == 1
|
| 231 |
+
c = Commutator(Bd(0), B(0))
|
| 232 |
+
assert c == -1
|
| 233 |
+
c = Commutator(B(n), Bd(0))
|
| 234 |
+
assert c == KroneckerDelta(n, 0)
|
| 235 |
+
c = Commutator(B(0), B(0))
|
| 236 |
+
assert c == 0
|
| 237 |
+
c = Commutator(B(0), Bd(0))
|
| 238 |
+
e = simplify(apply_operators(c*BKet([n])))
|
| 239 |
+
assert e == BKet([n])
|
| 240 |
+
c = Commutator(B(0), B(1))
|
| 241 |
+
e = simplify(apply_operators(c*BKet([n, m])))
|
| 242 |
+
assert e == 0
|
| 243 |
+
|
| 244 |
+
c = Commutator(F(m), Fd(m))
|
| 245 |
+
assert c == +1 - 2*NO(Fd(m)*F(m))
|
| 246 |
+
c = Commutator(Fd(m), F(m))
|
| 247 |
+
assert c.expand() == -1 + 2*NO(Fd(m)*F(m))
|
| 248 |
+
|
| 249 |
+
C = Commutator
|
| 250 |
+
X, Y, Z = symbols('X,Y,Z', commutative=False)
|
| 251 |
+
assert C(C(X, Y), Z) != 0
|
| 252 |
+
assert C(C(X, Z), Y) != 0
|
| 253 |
+
assert C(Y, C(X, Z)) != 0
|
| 254 |
+
|
| 255 |
+
i, j, k, l = symbols('i,j,k,l', below_fermi=True)
|
| 256 |
+
a, b, c, d = symbols('a,b,c,d', above_fermi=True)
|
| 257 |
+
p, q, r, s = symbols('p,q,r,s')
|
| 258 |
+
D = KroneckerDelta
|
| 259 |
+
|
| 260 |
+
assert C(Fd(a), F(i)) == -2*NO(F(i)*Fd(a))
|
| 261 |
+
assert C(Fd(j), NO(Fd(a)*F(i))).doit(wicks=True) == -D(j, i)*Fd(a)
|
| 262 |
+
assert C(Fd(a)*F(i), Fd(b)*F(j)).doit(wicks=True) == 0
|
| 263 |
+
|
| 264 |
+
c1 = Commutator(F(a), Fd(a))
|
| 265 |
+
assert Commutator.eval(c1, c1) == 0
|
| 266 |
+
c = Commutator(Fd(a)*F(i),Fd(b)*F(j))
|
| 267 |
+
assert latex(c) == r'\left[{a^\dagger_{a}} a_{i},{a^\dagger_{b}} a_{j}\right]'
|
| 268 |
+
assert repr(c) == 'Commutator(CreateFermion(a)*AnnihilateFermion(i),CreateFermion(b)*AnnihilateFermion(j))'
|
| 269 |
+
assert str(c) == '[CreateFermion(a)*AnnihilateFermion(i),CreateFermion(b)*AnnihilateFermion(j)]'
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def test_create_f():
|
| 273 |
+
i, j, n, m = symbols('i,j,n,m')
|
| 274 |
+
o = Fd(i)
|
| 275 |
+
assert isinstance(o, CreateFermion)
|
| 276 |
+
o = o.subs(i, j)
|
| 277 |
+
assert o.atoms(Symbol) == {j}
|
| 278 |
+
o = Fd(1)
|
| 279 |
+
assert o.apply_operator(FKet([n])) == FKet([1, n])
|
| 280 |
+
assert o.apply_operator(FKet([n])) == -FKet([n, 1])
|
| 281 |
+
o = Fd(n)
|
| 282 |
+
assert o.apply_operator(FKet([])) == FKet([n])
|
| 283 |
+
|
| 284 |
+
vacuum = FKet([], fermi_level=4)
|
| 285 |
+
assert vacuum == FKet([], fermi_level=4)
|
| 286 |
+
|
| 287 |
+
i, j, k, l = symbols('i,j,k,l', below_fermi=True)
|
| 288 |
+
a, b, c, d = symbols('a,b,c,d', above_fermi=True)
|
| 289 |
+
p, q, r, s = symbols('p,q,r,s')
|
| 290 |
+
|
| 291 |
+
assert Fd(i).apply_operator(FKet([i, j, k], 4)) == FKet([j, k], 4)
|
| 292 |
+
assert Fd(a).apply_operator(FKet([i, b, k], 4)) == FKet([a, i, b, k], 4)
|
| 293 |
+
|
| 294 |
+
assert Dagger(B(p)).apply_operator(q) == q*CreateBoson(p)
|
| 295 |
+
assert repr(Fd(p)) == 'CreateFermion(p)'
|
| 296 |
+
assert srepr(Fd(p)) == "CreateFermion(Symbol('p'))"
|
| 297 |
+
assert latex(Fd(p)) == r'{a^\dagger_{p}}'
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def test_annihilate_f():
|
| 301 |
+
i, j, n, m = symbols('i,j,n,m')
|
| 302 |
+
o = F(i)
|
| 303 |
+
assert isinstance(o, AnnihilateFermion)
|
| 304 |
+
o = o.subs(i, j)
|
| 305 |
+
assert o.atoms(Symbol) == {j}
|
| 306 |
+
o = F(1)
|
| 307 |
+
assert o.apply_operator(FKet([1, n])) == FKet([n])
|
| 308 |
+
assert o.apply_operator(FKet([n, 1])) == -FKet([n])
|
| 309 |
+
o = F(n)
|
| 310 |
+
assert o.apply_operator(FKet([n])) == FKet([])
|
| 311 |
+
|
| 312 |
+
i, j, k, l = symbols('i,j,k,l', below_fermi=True)
|
| 313 |
+
a, b, c, d = symbols('a,b,c,d', above_fermi=True)
|
| 314 |
+
p, q, r, s = symbols('p,q,r,s')
|
| 315 |
+
assert F(i).apply_operator(FKet([i, j, k], 4)) == 0
|
| 316 |
+
assert F(a).apply_operator(FKet([i, b, k], 4)) == 0
|
| 317 |
+
assert F(l).apply_operator(FKet([i, j, k], 3)) == 0
|
| 318 |
+
assert F(l).apply_operator(FKet([i, j, k], 4)) == FKet([l, i, j, k], 4)
|
| 319 |
+
assert str(F(p)) == 'f(p)'
|
| 320 |
+
assert repr(F(p)) == 'AnnihilateFermion(p)'
|
| 321 |
+
assert srepr(F(p)) == "AnnihilateFermion(Symbol('p'))"
|
| 322 |
+
assert latex(F(p)) == 'a_{p}'
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def test_create_b():
|
| 326 |
+
i, j, n, m = symbols('i,j,n,m')
|
| 327 |
+
o = Bd(i)
|
| 328 |
+
assert isinstance(o, CreateBoson)
|
| 329 |
+
o = o.subs(i, j)
|
| 330 |
+
assert o.atoms(Symbol) == {j}
|
| 331 |
+
o = Bd(0)
|
| 332 |
+
assert o.apply_operator(BKet([n])) == sqrt(n + 1)*BKet([n + 1])
|
| 333 |
+
o = Bd(n)
|
| 334 |
+
assert o.apply_operator(BKet([n])) == o*BKet([n])
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def test_annihilate_b():
|
| 338 |
+
i, j, n, m = symbols('i,j,n,m')
|
| 339 |
+
o = B(i)
|
| 340 |
+
assert isinstance(o, AnnihilateBoson)
|
| 341 |
+
o = o.subs(i, j)
|
| 342 |
+
assert o.atoms(Symbol) == {j}
|
| 343 |
+
o = B(0)
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def test_wicks():
|
| 347 |
+
p, q, r, s = symbols('p,q,r,s', above_fermi=True)
|
| 348 |
+
|
| 349 |
+
# Testing for particles only
|
| 350 |
+
|
| 351 |
+
str = F(p)*Fd(q)
|
| 352 |
+
assert wicks(str) == NO(F(p)*Fd(q)) + KroneckerDelta(p, q)
|
| 353 |
+
str = Fd(p)*F(q)
|
| 354 |
+
assert wicks(str) == NO(Fd(p)*F(q))
|
| 355 |
+
|
| 356 |
+
str = F(p)*Fd(q)*F(r)*Fd(s)
|
| 357 |
+
nstr = wicks(str)
|
| 358 |
+
fasit = NO(
|
| 359 |
+
KroneckerDelta(p, q)*KroneckerDelta(r, s)
|
| 360 |
+
+ KroneckerDelta(p, q)*AnnihilateFermion(r)*CreateFermion(s)
|
| 361 |
+
+ KroneckerDelta(r, s)*AnnihilateFermion(p)*CreateFermion(q)
|
| 362 |
+
- KroneckerDelta(p, s)*AnnihilateFermion(r)*CreateFermion(q)
|
| 363 |
+
- AnnihilateFermion(p)*AnnihilateFermion(r)*CreateFermion(q)*CreateFermion(s))
|
| 364 |
+
assert nstr == fasit
|
| 365 |
+
|
| 366 |
+
assert (p*q*nstr).expand() == wicks(p*q*str)
|
| 367 |
+
assert (nstr*p*q*2).expand() == wicks(str*p*q*2)
|
| 368 |
+
|
| 369 |
+
# Testing CC equations particles and holes
|
| 370 |
+
i, j, k, l = symbols('i j k l', below_fermi=True, cls=Dummy)
|
| 371 |
+
a, b, c, d = symbols('a b c d', above_fermi=True, cls=Dummy)
|
| 372 |
+
p, q, r, s = symbols('p q r s', cls=Dummy)
|
| 373 |
+
|
| 374 |
+
assert (wicks(F(a)*NO(F(i)*F(j))*Fd(b)) ==
|
| 375 |
+
NO(F(a)*F(i)*F(j)*Fd(b)) +
|
| 376 |
+
KroneckerDelta(a, b)*NO(F(i)*F(j)))
|
| 377 |
+
assert (wicks(F(a)*NO(F(i)*F(j)*F(k))*Fd(b)) ==
|
| 378 |
+
NO(F(a)*F(i)*F(j)*F(k)*Fd(b)) -
|
| 379 |
+
KroneckerDelta(a, b)*NO(F(i)*F(j)*F(k)))
|
| 380 |
+
|
| 381 |
+
expr = wicks(Fd(i)*NO(Fd(j)*F(k))*F(l))
|
| 382 |
+
assert (expr ==
|
| 383 |
+
-KroneckerDelta(i, k)*NO(Fd(j)*F(l)) -
|
| 384 |
+
KroneckerDelta(j, l)*NO(Fd(i)*F(k)) -
|
| 385 |
+
KroneckerDelta(i, k)*KroneckerDelta(j, l) +
|
| 386 |
+
KroneckerDelta(i, l)*NO(Fd(j)*F(k)) +
|
| 387 |
+
NO(Fd(i)*Fd(j)*F(k)*F(l)))
|
| 388 |
+
expr = wicks(F(a)*NO(F(b)*Fd(c))*Fd(d))
|
| 389 |
+
assert (expr ==
|
| 390 |
+
-KroneckerDelta(a, c)*NO(F(b)*Fd(d)) -
|
| 391 |
+
KroneckerDelta(b, d)*NO(F(a)*Fd(c)) -
|
| 392 |
+
KroneckerDelta(a, c)*KroneckerDelta(b, d) +
|
| 393 |
+
KroneckerDelta(a, d)*NO(F(b)*Fd(c)) +
|
| 394 |
+
NO(F(a)*F(b)*Fd(c)*Fd(d)))
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
def test_NO():
|
| 398 |
+
i, j, k, l = symbols('i j k l', below_fermi=True)
|
| 399 |
+
a, b, c, d = symbols('a b c d', above_fermi=True)
|
| 400 |
+
p, q, r, s = symbols('p q r s', cls=Dummy)
|
| 401 |
+
|
| 402 |
+
assert (NO(Fd(p)*F(q) + Fd(a)*F(b)) ==
|
| 403 |
+
NO(Fd(p)*F(q)) + NO(Fd(a)*F(b)))
|
| 404 |
+
assert (NO(Fd(i)*NO(F(j)*Fd(a))) ==
|
| 405 |
+
NO(Fd(i)*F(j)*Fd(a)))
|
| 406 |
+
assert NO(1) == 1
|
| 407 |
+
assert NO(i) == i
|
| 408 |
+
assert (NO(Fd(a)*Fd(b)*(F(c) + F(d))) ==
|
| 409 |
+
NO(Fd(a)*Fd(b)*F(c)) +
|
| 410 |
+
NO(Fd(a)*Fd(b)*F(d)))
|
| 411 |
+
|
| 412 |
+
assert NO(Fd(a)*F(b))._remove_brackets() == Fd(a)*F(b)
|
| 413 |
+
assert NO(F(j)*Fd(i))._remove_brackets() == F(j)*Fd(i)
|
| 414 |
+
|
| 415 |
+
assert (NO(Fd(p)*F(q)).subs(Fd(p), Fd(a) + Fd(i)) ==
|
| 416 |
+
NO(Fd(a)*F(q)) + NO(Fd(i)*F(q)))
|
| 417 |
+
assert (NO(Fd(p)*F(q)).subs(F(q), F(a) + F(i)) ==
|
| 418 |
+
NO(Fd(p)*F(a)) + NO(Fd(p)*F(i)))
|
| 419 |
+
|
| 420 |
+
expr = NO(Fd(p)*F(q))._remove_brackets()
|
| 421 |
+
assert wicks(expr) == NO(expr)
|
| 422 |
+
|
| 423 |
+
assert NO(Fd(a)*F(b)) == - NO(F(b)*Fd(a))
|
| 424 |
+
|
| 425 |
+
no = NO(Fd(a)*F(i)*F(b)*Fd(j))
|
| 426 |
+
l1 = list(no.iter_q_creators())
|
| 427 |
+
assert l1 == [0, 1]
|
| 428 |
+
l2 = list(no.iter_q_annihilators())
|
| 429 |
+
assert l2 == [3, 2]
|
| 430 |
+
no = NO(Fd(a)*Fd(i))
|
| 431 |
+
assert no.has_q_creators == 1
|
| 432 |
+
assert no.has_q_annihilators == -1
|
| 433 |
+
assert str(no) == ':CreateFermion(a)*CreateFermion(i):'
|
| 434 |
+
assert repr(no) == 'NO(CreateFermion(a)*CreateFermion(i))'
|
| 435 |
+
assert latex(no) == r'\left\{{a^\dagger_{a}} {a^\dagger_{i}}\right\}'
|
| 436 |
+
raises(NotImplementedError, lambda: NO(Bd(p)*F(q)))
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
def test_sorting():
|
| 440 |
+
i, j = symbols('i,j', below_fermi=True)
|
| 441 |
+
a, b = symbols('a,b', above_fermi=True)
|
| 442 |
+
p, q = symbols('p,q')
|
| 443 |
+
|
| 444 |
+
# p, q
|
| 445 |
+
assert _sort_anticommuting_fermions([Fd(p), F(q)]) == ([Fd(p), F(q)], 0)
|
| 446 |
+
assert _sort_anticommuting_fermions([F(p), Fd(q)]) == ([Fd(q), F(p)], 1)
|
| 447 |
+
|
| 448 |
+
# i, p
|
| 449 |
+
assert _sort_anticommuting_fermions([F(p), Fd(i)]) == ([F(p), Fd(i)], 0)
|
| 450 |
+
assert _sort_anticommuting_fermions([Fd(i), F(p)]) == ([F(p), Fd(i)], 1)
|
| 451 |
+
assert _sort_anticommuting_fermions([Fd(p), Fd(i)]) == ([Fd(p), Fd(i)], 0)
|
| 452 |
+
assert _sort_anticommuting_fermions([Fd(i), Fd(p)]) == ([Fd(p), Fd(i)], 1)
|
| 453 |
+
assert _sort_anticommuting_fermions([F(p), F(i)]) == ([F(i), F(p)], 1)
|
| 454 |
+
assert _sort_anticommuting_fermions([F(i), F(p)]) == ([F(i), F(p)], 0)
|
| 455 |
+
assert _sort_anticommuting_fermions([Fd(p), F(i)]) == ([F(i), Fd(p)], 1)
|
| 456 |
+
assert _sort_anticommuting_fermions([F(i), Fd(p)]) == ([F(i), Fd(p)], 0)
|
| 457 |
+
|
| 458 |
+
# a, p
|
| 459 |
+
assert _sort_anticommuting_fermions([F(p), Fd(a)]) == ([Fd(a), F(p)], 1)
|
| 460 |
+
assert _sort_anticommuting_fermions([Fd(a), F(p)]) == ([Fd(a), F(p)], 0)
|
| 461 |
+
assert _sort_anticommuting_fermions([Fd(p), Fd(a)]) == ([Fd(a), Fd(p)], 1)
|
| 462 |
+
assert _sort_anticommuting_fermions([Fd(a), Fd(p)]) == ([Fd(a), Fd(p)], 0)
|
| 463 |
+
assert _sort_anticommuting_fermions([F(p), F(a)]) == ([F(p), F(a)], 0)
|
| 464 |
+
assert _sort_anticommuting_fermions([F(a), F(p)]) == ([F(p), F(a)], 1)
|
| 465 |
+
assert _sort_anticommuting_fermions([Fd(p), F(a)]) == ([Fd(p), F(a)], 0)
|
| 466 |
+
assert _sort_anticommuting_fermions([F(a), Fd(p)]) == ([Fd(p), F(a)], 1)
|
| 467 |
+
|
| 468 |
+
# i, a
|
| 469 |
+
assert _sort_anticommuting_fermions([F(i), Fd(j)]) == ([F(i), Fd(j)], 0)
|
| 470 |
+
assert _sort_anticommuting_fermions([Fd(j), F(i)]) == ([F(i), Fd(j)], 1)
|
| 471 |
+
assert _sort_anticommuting_fermions([Fd(a), Fd(i)]) == ([Fd(a), Fd(i)], 0)
|
| 472 |
+
assert _sort_anticommuting_fermions([Fd(i), Fd(a)]) == ([Fd(a), Fd(i)], 1)
|
| 473 |
+
assert _sort_anticommuting_fermions([F(a), F(i)]) == ([F(i), F(a)], 1)
|
| 474 |
+
assert _sort_anticommuting_fermions([F(i), F(a)]) == ([F(i), F(a)], 0)
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def test_contraction():
|
| 478 |
+
i, j, k, l = symbols('i,j,k,l', below_fermi=True)
|
| 479 |
+
a, b, c, d = symbols('a,b,c,d', above_fermi=True)
|
| 480 |
+
p, q, r, s = symbols('p,q,r,s')
|
| 481 |
+
assert contraction(Fd(i), F(j)) == KroneckerDelta(i, j)
|
| 482 |
+
assert contraction(F(a), Fd(b)) == KroneckerDelta(a, b)
|
| 483 |
+
assert contraction(F(a), Fd(i)) == 0
|
| 484 |
+
assert contraction(Fd(a), F(i)) == 0
|
| 485 |
+
assert contraction(F(i), Fd(a)) == 0
|
| 486 |
+
assert contraction(Fd(i), F(a)) == 0
|
| 487 |
+
assert contraction(Fd(i), F(p)) == KroneckerDelta(i, p)
|
| 488 |
+
restr = evaluate_deltas(contraction(Fd(p), F(q)))
|
| 489 |
+
assert restr.is_only_below_fermi
|
| 490 |
+
restr = evaluate_deltas(contraction(F(p), Fd(q)))
|
| 491 |
+
assert restr.is_only_above_fermi
|
| 492 |
+
raises(ContractionAppliesOnlyToFermions, lambda: contraction(B(a), Fd(b)))
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
def test_evaluate_deltas():
|
| 496 |
+
i, j, k = symbols('i,j,k')
|
| 497 |
+
|
| 498 |
+
r = KroneckerDelta(i, j) * KroneckerDelta(j, k)
|
| 499 |
+
assert evaluate_deltas(r) == KroneckerDelta(i, k)
|
| 500 |
+
|
| 501 |
+
r = KroneckerDelta(i, 0) * KroneckerDelta(j, k)
|
| 502 |
+
assert evaluate_deltas(r) == KroneckerDelta(i, 0) * KroneckerDelta(j, k)
|
| 503 |
+
|
| 504 |
+
r = KroneckerDelta(1, j) * KroneckerDelta(j, k)
|
| 505 |
+
assert evaluate_deltas(r) == KroneckerDelta(1, k)
|
| 506 |
+
|
| 507 |
+
r = KroneckerDelta(j, 2) * KroneckerDelta(k, j)
|
| 508 |
+
assert evaluate_deltas(r) == KroneckerDelta(2, k)
|
| 509 |
+
|
| 510 |
+
r = KroneckerDelta(i, 0) * KroneckerDelta(i, j) * KroneckerDelta(j, 1)
|
| 511 |
+
assert evaluate_deltas(r) == 0
|
| 512 |
+
|
| 513 |
+
r = (KroneckerDelta(0, i) * KroneckerDelta(0, j)
|
| 514 |
+
* KroneckerDelta(1, j) * KroneckerDelta(1, j))
|
| 515 |
+
assert evaluate_deltas(r) == 0
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
def test_Tensors():
|
| 519 |
+
i, j, k, l = symbols('i j k l', below_fermi=True, cls=Dummy)
|
| 520 |
+
a, b, c, d = symbols('a b c d', above_fermi=True, cls=Dummy)
|
| 521 |
+
p, q, r, s = symbols('p q r s')
|
| 522 |
+
|
| 523 |
+
AT = AntiSymmetricTensor
|
| 524 |
+
assert AT('t', (a, b), (i, j)) == -AT('t', (b, a), (i, j))
|
| 525 |
+
assert AT('t', (a, b), (i, j)) == AT('t', (b, a), (j, i))
|
| 526 |
+
assert AT('t', (a, b), (i, j)) == -AT('t', (a, b), (j, i))
|
| 527 |
+
assert AT('t', (a, a), (i, j)) == 0
|
| 528 |
+
assert AT('t', (a, b), (i, i)) == 0
|
| 529 |
+
assert AT('t', (a, b, c), (i, j)) == -AT('t', (b, a, c), (i, j))
|
| 530 |
+
assert AT('t', (a, b, c), (i, j, k)) == AT('t', (b, a, c), (i, k, j))
|
| 531 |
+
|
| 532 |
+
tabij = AT('t', (a, b), (i, j))
|
| 533 |
+
assert tabij.has(a)
|
| 534 |
+
assert tabij.has(b)
|
| 535 |
+
assert tabij.has(i)
|
| 536 |
+
assert tabij.has(j)
|
| 537 |
+
assert tabij.subs(b, c) == AT('t', (a, c), (i, j))
|
| 538 |
+
assert (2*tabij).subs(i, c) == 2*AT('t', (a, b), (c, j))
|
| 539 |
+
assert tabij.symbol == Symbol('t')
|
| 540 |
+
assert latex(tabij) == '{t^{ab}_{ij}}'
|
| 541 |
+
assert str(tabij) == 't((_a, _b),(_i, _j))'
|
| 542 |
+
|
| 543 |
+
assert AT('t', (a, a), (i, j)).subs(a, b) == AT('t', (b, b), (i, j))
|
| 544 |
+
assert AT('t', (a, i), (a, j)).subs(a, b) == AT('t', (b, i), (b, j))
|
| 545 |
+
|
| 546 |
+
|
| 547 |
+
def test_fully_contracted():
|
| 548 |
+
i, j, k, l = symbols('i j k l', below_fermi=True)
|
| 549 |
+
a, b, c, d = symbols('a b c d', above_fermi=True)
|
| 550 |
+
p, q, r, s = symbols('p q r s', cls=Dummy)
|
| 551 |
+
|
| 552 |
+
Fock = (AntiSymmetricTensor('f', (p,), (q,))*
|
| 553 |
+
NO(Fd(p)*F(q)))
|
| 554 |
+
V = (AntiSymmetricTensor('v', (p, q), (r, s))*
|
| 555 |
+
NO(Fd(p)*Fd(q)*F(s)*F(r)))/4
|
| 556 |
+
|
| 557 |
+
Fai = wicks(NO(Fd(i)*F(a))*Fock,
|
| 558 |
+
keep_only_fully_contracted=True,
|
| 559 |
+
simplify_kronecker_deltas=True)
|
| 560 |
+
assert Fai == AntiSymmetricTensor('f', (a,), (i,))
|
| 561 |
+
Vabij = wicks(NO(Fd(i)*Fd(j)*F(b)*F(a))*V,
|
| 562 |
+
keep_only_fully_contracted=True,
|
| 563 |
+
simplify_kronecker_deltas=True)
|
| 564 |
+
assert Vabij == AntiSymmetricTensor('v', (a, b), (i, j))
|
| 565 |
+
|
| 566 |
+
|
| 567 |
+
def test_substitute_dummies_without_dummies():
|
| 568 |
+
i, j = symbols('i,j')
|
| 569 |
+
assert substitute_dummies(att(i, j) + 2) == att(i, j) + 2
|
| 570 |
+
assert substitute_dummies(att(i, j) + 1) == att(i, j) + 1
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
def test_substitute_dummies_NO_operator():
|
| 574 |
+
i, j = symbols('i j', cls=Dummy)
|
| 575 |
+
assert substitute_dummies(att(i, j)*NO(Fd(i)*F(j))
|
| 576 |
+
- att(j, i)*NO(Fd(j)*F(i))) == 0
|
| 577 |
+
|
| 578 |
+
|
| 579 |
+
def test_substitute_dummies_SQ_operator():
|
| 580 |
+
i, j = symbols('i j', cls=Dummy)
|
| 581 |
+
assert substitute_dummies(att(i, j)*Fd(i)*F(j)
|
| 582 |
+
- att(j, i)*Fd(j)*F(i)) == 0
|
| 583 |
+
|
| 584 |
+
|
| 585 |
+
def test_substitute_dummies_new_indices():
|
| 586 |
+
i, j = symbols('i j', below_fermi=True, cls=Dummy)
|
| 587 |
+
a, b = symbols('a b', above_fermi=True, cls=Dummy)
|
| 588 |
+
p, q = symbols('p q', cls=Dummy)
|
| 589 |
+
f = Function('f')
|
| 590 |
+
assert substitute_dummies(f(i, a, p) - f(j, b, q), new_indices=True) == 0
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
def test_substitute_dummies_substitution_order():
|
| 594 |
+
i, j, k, l = symbols('i j k l', below_fermi=True, cls=Dummy)
|
| 595 |
+
f = Function('f')
|
| 596 |
+
from sympy.utilities.iterables import variations
|
| 597 |
+
for permut in variations([i, j, k, l], 4):
|
| 598 |
+
assert substitute_dummies(f(*permut) - f(i, j, k, l)) == 0
|
| 599 |
+
|
| 600 |
+
|
| 601 |
+
def test_dummy_order_inner_outer_lines_VT1T1T1():
|
| 602 |
+
ii = symbols('i', below_fermi=True)
|
| 603 |
+
aa = symbols('a', above_fermi=True)
|
| 604 |
+
k, l = symbols('k l', below_fermi=True, cls=Dummy)
|
| 605 |
+
c, d = symbols('c d', above_fermi=True, cls=Dummy)
|
| 606 |
+
|
| 607 |
+
v = Function('v')
|
| 608 |
+
t = Function('t')
|
| 609 |
+
dums = _get_ordered_dummies
|
| 610 |
+
|
| 611 |
+
# Coupled-Cluster T1 terms with V*T1*T1*T1
|
| 612 |
+
# t^{a}_{k} t^{c}_{i} t^{d}_{l} v^{lk}_{dc}
|
| 613 |
+
exprs = [
|
| 614 |
+
# permut v and t <=> swapping internal lines, equivalent
|
| 615 |
+
# irrespective of symmetries in v
|
| 616 |
+
v(k, l, c, d)*t(c, ii)*t(d, l)*t(aa, k),
|
| 617 |
+
v(l, k, c, d)*t(c, ii)*t(d, k)*t(aa, l),
|
| 618 |
+
v(k, l, d, c)*t(d, ii)*t(c, l)*t(aa, k),
|
| 619 |
+
v(l, k, d, c)*t(d, ii)*t(c, k)*t(aa, l),
|
| 620 |
+
]
|
| 621 |
+
for permut in exprs[1:]:
|
| 622 |
+
assert dums(exprs[0]) != dums(permut)
|
| 623 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 624 |
+
|
| 625 |
+
|
| 626 |
+
def test_dummy_order_inner_outer_lines_VT1T1T1T1():
|
| 627 |
+
ii, jj = symbols('i j', below_fermi=True)
|
| 628 |
+
aa, bb = symbols('a b', above_fermi=True)
|
| 629 |
+
k, l = symbols('k l', below_fermi=True, cls=Dummy)
|
| 630 |
+
c, d = symbols('c d', above_fermi=True, cls=Dummy)
|
| 631 |
+
|
| 632 |
+
v = Function('v')
|
| 633 |
+
t = Function('t')
|
| 634 |
+
dums = _get_ordered_dummies
|
| 635 |
+
|
| 636 |
+
# Coupled-Cluster T2 terms with V*T1*T1*T1*T1
|
| 637 |
+
exprs = [
|
| 638 |
+
# permut t <=> swapping external lines, not equivalent
|
| 639 |
+
# except if v has certain symmetries.
|
| 640 |
+
v(k, l, c, d)*t(c, ii)*t(d, jj)*t(aa, k)*t(bb, l),
|
| 641 |
+
v(k, l, c, d)*t(c, jj)*t(d, ii)*t(aa, k)*t(bb, l),
|
| 642 |
+
v(k, l, c, d)*t(c, ii)*t(d, jj)*t(bb, k)*t(aa, l),
|
| 643 |
+
v(k, l, c, d)*t(c, jj)*t(d, ii)*t(bb, k)*t(aa, l),
|
| 644 |
+
]
|
| 645 |
+
for permut in exprs[1:]:
|
| 646 |
+
assert dums(exprs[0]) != dums(permut)
|
| 647 |
+
assert substitute_dummies(exprs[0]) != substitute_dummies(permut)
|
| 648 |
+
exprs = [
|
| 649 |
+
# permut v <=> swapping external lines, not equivalent
|
| 650 |
+
# except if v has certain symmetries.
|
| 651 |
+
#
|
| 652 |
+
# Note that in contrast to above, these permutations have identical
|
| 653 |
+
# dummy order. That is because the proximity to external indices
|
| 654 |
+
# has higher influence on the canonical dummy ordering than the
|
| 655 |
+
# position of a dummy on the factors. In fact, the terms here are
|
| 656 |
+
# similar in structure as the result of the dummy substitutions above.
|
| 657 |
+
v(k, l, c, d)*t(c, ii)*t(d, jj)*t(aa, k)*t(bb, l),
|
| 658 |
+
v(l, k, c, d)*t(c, ii)*t(d, jj)*t(aa, k)*t(bb, l),
|
| 659 |
+
v(k, l, d, c)*t(c, ii)*t(d, jj)*t(aa, k)*t(bb, l),
|
| 660 |
+
v(l, k, d, c)*t(c, ii)*t(d, jj)*t(aa, k)*t(bb, l),
|
| 661 |
+
]
|
| 662 |
+
for permut in exprs[1:]:
|
| 663 |
+
assert dums(exprs[0]) == dums(permut)
|
| 664 |
+
assert substitute_dummies(exprs[0]) != substitute_dummies(permut)
|
| 665 |
+
exprs = [
|
| 666 |
+
# permut t and v <=> swapping internal lines, equivalent.
|
| 667 |
+
# Canonical dummy order is different, and a consistent
|
| 668 |
+
# substitution reveals the equivalence.
|
| 669 |
+
v(k, l, c, d)*t(c, ii)*t(d, jj)*t(aa, k)*t(bb, l),
|
| 670 |
+
v(k, l, d, c)*t(c, jj)*t(d, ii)*t(aa, k)*t(bb, l),
|
| 671 |
+
v(l, k, c, d)*t(c, ii)*t(d, jj)*t(bb, k)*t(aa, l),
|
| 672 |
+
v(l, k, d, c)*t(c, jj)*t(d, ii)*t(bb, k)*t(aa, l),
|
| 673 |
+
]
|
| 674 |
+
for permut in exprs[1:]:
|
| 675 |
+
assert dums(exprs[0]) != dums(permut)
|
| 676 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 677 |
+
|
| 678 |
+
|
| 679 |
+
def test_get_subNO():
|
| 680 |
+
p, q, r = symbols('p,q,r')
|
| 681 |
+
assert NO(F(p)*F(q)*F(r)).get_subNO(1) == NO(F(p)*F(r))
|
| 682 |
+
assert NO(F(p)*F(q)*F(r)).get_subNO(0) == NO(F(q)*F(r))
|
| 683 |
+
assert NO(F(p)*F(q)*F(r)).get_subNO(2) == NO(F(p)*F(q))
|
| 684 |
+
|
| 685 |
+
|
| 686 |
+
def test_equivalent_internal_lines_VT1T1():
|
| 687 |
+
i, j, k, l = symbols('i j k l', below_fermi=True, cls=Dummy)
|
| 688 |
+
a, b, c, d = symbols('a b c d', above_fermi=True, cls=Dummy)
|
| 689 |
+
|
| 690 |
+
v = Function('v')
|
| 691 |
+
t = Function('t')
|
| 692 |
+
dums = _get_ordered_dummies
|
| 693 |
+
|
| 694 |
+
exprs = [ # permute v. Different dummy order. Not equivalent.
|
| 695 |
+
v(i, j, a, b)*t(a, i)*t(b, j),
|
| 696 |
+
v(j, i, a, b)*t(a, i)*t(b, j),
|
| 697 |
+
v(i, j, b, a)*t(a, i)*t(b, j),
|
| 698 |
+
]
|
| 699 |
+
for permut in exprs[1:]:
|
| 700 |
+
assert dums(exprs[0]) != dums(permut)
|
| 701 |
+
assert substitute_dummies(exprs[0]) != substitute_dummies(permut)
|
| 702 |
+
|
| 703 |
+
exprs = [ # permute v. Different dummy order. Equivalent
|
| 704 |
+
v(i, j, a, b)*t(a, i)*t(b, j),
|
| 705 |
+
v(j, i, b, a)*t(a, i)*t(b, j),
|
| 706 |
+
]
|
| 707 |
+
for permut in exprs[1:]:
|
| 708 |
+
assert dums(exprs[0]) != dums(permut)
|
| 709 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 710 |
+
|
| 711 |
+
exprs = [ # permute t. Same dummy order, not equivalent.
|
| 712 |
+
v(i, j, a, b)*t(a, i)*t(b, j),
|
| 713 |
+
v(i, j, a, b)*t(b, i)*t(a, j),
|
| 714 |
+
]
|
| 715 |
+
for permut in exprs[1:]:
|
| 716 |
+
assert dums(exprs[0]) == dums(permut)
|
| 717 |
+
assert substitute_dummies(exprs[0]) != substitute_dummies(permut)
|
| 718 |
+
|
| 719 |
+
exprs = [ # permute v and t. Different dummy order, equivalent
|
| 720 |
+
v(i, j, a, b)*t(a, i)*t(b, j),
|
| 721 |
+
v(j, i, a, b)*t(a, j)*t(b, i),
|
| 722 |
+
v(i, j, b, a)*t(b, i)*t(a, j),
|
| 723 |
+
v(j, i, b, a)*t(b, j)*t(a, i),
|
| 724 |
+
]
|
| 725 |
+
for permut in exprs[1:]:
|
| 726 |
+
assert dums(exprs[0]) != dums(permut)
|
| 727 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 728 |
+
|
| 729 |
+
|
| 730 |
+
def test_equivalent_internal_lines_VT2conjT2():
|
| 731 |
+
# this diagram requires special handling in TCE
|
| 732 |
+
i, j, k, l, m, n = symbols('i j k l m n', below_fermi=True, cls=Dummy)
|
| 733 |
+
a, b, c, d, e, f = symbols('a b c d e f', above_fermi=True, cls=Dummy)
|
| 734 |
+
p1, p2, p3, p4 = symbols('p1 p2 p3 p4', above_fermi=True, cls=Dummy)
|
| 735 |
+
h1, h2, h3, h4 = symbols('h1 h2 h3 h4', below_fermi=True, cls=Dummy)
|
| 736 |
+
|
| 737 |
+
from sympy.utilities.iterables import variations
|
| 738 |
+
|
| 739 |
+
v = Function('v')
|
| 740 |
+
t = Function('t')
|
| 741 |
+
dums = _get_ordered_dummies
|
| 742 |
+
|
| 743 |
+
# v(abcd)t(abij)t(ijcd)
|
| 744 |
+
template = v(p1, p2, p3, p4)*t(p1, p2, i, j)*t(i, j, p3, p4)
|
| 745 |
+
permutator = variations([a, b, c, d], 4)
|
| 746 |
+
base = template.subs(zip([p1, p2, p3, p4], next(permutator)))
|
| 747 |
+
for permut in permutator:
|
| 748 |
+
subslist = zip([p1, p2, p3, p4], permut)
|
| 749 |
+
expr = template.subs(subslist)
|
| 750 |
+
assert dums(base) != dums(expr)
|
| 751 |
+
assert substitute_dummies(expr) == substitute_dummies(base)
|
| 752 |
+
template = v(p1, p2, p3, p4)*t(p1, p2, j, i)*t(j, i, p3, p4)
|
| 753 |
+
permutator = variations([a, b, c, d], 4)
|
| 754 |
+
base = template.subs(zip([p1, p2, p3, p4], next(permutator)))
|
| 755 |
+
for permut in permutator:
|
| 756 |
+
subslist = zip([p1, p2, p3, p4], permut)
|
| 757 |
+
expr = template.subs(subslist)
|
| 758 |
+
assert dums(base) != dums(expr)
|
| 759 |
+
assert substitute_dummies(expr) == substitute_dummies(base)
|
| 760 |
+
|
| 761 |
+
# v(abcd)t(abij)t(jicd)
|
| 762 |
+
template = v(p1, p2, p3, p4)*t(p1, p2, i, j)*t(j, i, p3, p4)
|
| 763 |
+
permutator = variations([a, b, c, d], 4)
|
| 764 |
+
base = template.subs(zip([p1, p2, p3, p4], next(permutator)))
|
| 765 |
+
for permut in permutator:
|
| 766 |
+
subslist = zip([p1, p2, p3, p4], permut)
|
| 767 |
+
expr = template.subs(subslist)
|
| 768 |
+
assert dums(base) != dums(expr)
|
| 769 |
+
assert substitute_dummies(expr) == substitute_dummies(base)
|
| 770 |
+
template = v(p1, p2, p3, p4)*t(p1, p2, j, i)*t(i, j, p3, p4)
|
| 771 |
+
permutator = variations([a, b, c, d], 4)
|
| 772 |
+
base = template.subs(zip([p1, p2, p3, p4], next(permutator)))
|
| 773 |
+
for permut in permutator:
|
| 774 |
+
subslist = zip([p1, p2, p3, p4], permut)
|
| 775 |
+
expr = template.subs(subslist)
|
| 776 |
+
assert dums(base) != dums(expr)
|
| 777 |
+
assert substitute_dummies(expr) == substitute_dummies(base)
|
| 778 |
+
|
| 779 |
+
|
| 780 |
+
def test_equivalent_internal_lines_VT2conjT2_ambiguous_order():
|
| 781 |
+
# These diagrams invokes _determine_ambiguous() because the
|
| 782 |
+
# dummies can not be ordered unambiguously by the key alone
|
| 783 |
+
i, j, k, l, m, n = symbols('i j k l m n', below_fermi=True, cls=Dummy)
|
| 784 |
+
a, b, c, d, e, f = symbols('a b c d e f', above_fermi=True, cls=Dummy)
|
| 785 |
+
p1, p2, p3, p4 = symbols('p1 p2 p3 p4', above_fermi=True, cls=Dummy)
|
| 786 |
+
h1, h2, h3, h4 = symbols('h1 h2 h3 h4', below_fermi=True, cls=Dummy)
|
| 787 |
+
|
| 788 |
+
from sympy.utilities.iterables import variations
|
| 789 |
+
|
| 790 |
+
v = Function('v')
|
| 791 |
+
t = Function('t')
|
| 792 |
+
dums = _get_ordered_dummies
|
| 793 |
+
|
| 794 |
+
# v(abcd)t(abij)t(cdij)
|
| 795 |
+
template = v(p1, p2, p3, p4)*t(p1, p2, i, j)*t(p3, p4, i, j)
|
| 796 |
+
permutator = variations([a, b, c, d], 4)
|
| 797 |
+
base = template.subs(zip([p1, p2, p3, p4], next(permutator)))
|
| 798 |
+
for permut in permutator:
|
| 799 |
+
subslist = zip([p1, p2, p3, p4], permut)
|
| 800 |
+
expr = template.subs(subslist)
|
| 801 |
+
assert dums(base) != dums(expr)
|
| 802 |
+
assert substitute_dummies(expr) == substitute_dummies(base)
|
| 803 |
+
template = v(p1, p2, p3, p4)*t(p1, p2, j, i)*t(p3, p4, i, j)
|
| 804 |
+
permutator = variations([a, b, c, d], 4)
|
| 805 |
+
base = template.subs(zip([p1, p2, p3, p4], next(permutator)))
|
| 806 |
+
for permut in permutator:
|
| 807 |
+
subslist = zip([p1, p2, p3, p4], permut)
|
| 808 |
+
expr = template.subs(subslist)
|
| 809 |
+
assert dums(base) != dums(expr)
|
| 810 |
+
assert substitute_dummies(expr) == substitute_dummies(base)
|
| 811 |
+
|
| 812 |
+
|
| 813 |
+
def test_equivalent_internal_lines_VT2():
|
| 814 |
+
i, j, k, l = symbols('i j k l', below_fermi=True, cls=Dummy)
|
| 815 |
+
a, b, c, d = symbols('a b c d', above_fermi=True, cls=Dummy)
|
| 816 |
+
|
| 817 |
+
v = Function('v')
|
| 818 |
+
t = Function('t')
|
| 819 |
+
dums = _get_ordered_dummies
|
| 820 |
+
exprs = [
|
| 821 |
+
# permute v. Same dummy order, not equivalent.
|
| 822 |
+
#
|
| 823 |
+
# This test show that the dummy order may not be sensitive to all
|
| 824 |
+
# index permutations. The following expressions have identical
|
| 825 |
+
# structure as the resulting terms from of the dummy substitutions
|
| 826 |
+
# in the test above. Here, all expressions have the same dummy
|
| 827 |
+
# order, so they cannot be simplified by means of dummy
|
| 828 |
+
# substitution. In order to simplify further, it is necessary to
|
| 829 |
+
# exploit symmetries in the objects, for instance if t or v is
|
| 830 |
+
# antisymmetric.
|
| 831 |
+
v(i, j, a, b)*t(a, b, i, j),
|
| 832 |
+
v(j, i, a, b)*t(a, b, i, j),
|
| 833 |
+
v(i, j, b, a)*t(a, b, i, j),
|
| 834 |
+
v(j, i, b, a)*t(a, b, i, j),
|
| 835 |
+
]
|
| 836 |
+
for permut in exprs[1:]:
|
| 837 |
+
assert dums(exprs[0]) == dums(permut)
|
| 838 |
+
assert substitute_dummies(exprs[0]) != substitute_dummies(permut)
|
| 839 |
+
|
| 840 |
+
exprs = [
|
| 841 |
+
# permute t.
|
| 842 |
+
v(i, j, a, b)*t(a, b, i, j),
|
| 843 |
+
v(i, j, a, b)*t(b, a, i, j),
|
| 844 |
+
v(i, j, a, b)*t(a, b, j, i),
|
| 845 |
+
v(i, j, a, b)*t(b, a, j, i),
|
| 846 |
+
]
|
| 847 |
+
for permut in exprs[1:]:
|
| 848 |
+
assert dums(exprs[0]) != dums(permut)
|
| 849 |
+
assert substitute_dummies(exprs[0]) != substitute_dummies(permut)
|
| 850 |
+
|
| 851 |
+
exprs = [ # permute v and t. Relabelling of dummies should be equivalent.
|
| 852 |
+
v(i, j, a, b)*t(a, b, i, j),
|
| 853 |
+
v(j, i, a, b)*t(a, b, j, i),
|
| 854 |
+
v(i, j, b, a)*t(b, a, i, j),
|
| 855 |
+
v(j, i, b, a)*t(b, a, j, i),
|
| 856 |
+
]
|
| 857 |
+
for permut in exprs[1:]:
|
| 858 |
+
assert dums(exprs[0]) != dums(permut)
|
| 859 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 860 |
+
|
| 861 |
+
|
| 862 |
+
def test_internal_external_VT2T2():
|
| 863 |
+
ii, jj = symbols('i j', below_fermi=True)
|
| 864 |
+
aa, bb = symbols('a b', above_fermi=True)
|
| 865 |
+
k, l = symbols('k l', below_fermi=True, cls=Dummy)
|
| 866 |
+
c, d = symbols('c d', above_fermi=True, cls=Dummy)
|
| 867 |
+
|
| 868 |
+
v = Function('v')
|
| 869 |
+
t = Function('t')
|
| 870 |
+
dums = _get_ordered_dummies
|
| 871 |
+
|
| 872 |
+
exprs = [
|
| 873 |
+
v(k, l, c, d)*t(aa, c, ii, k)*t(bb, d, jj, l),
|
| 874 |
+
v(l, k, c, d)*t(aa, c, ii, l)*t(bb, d, jj, k),
|
| 875 |
+
v(k, l, d, c)*t(aa, d, ii, k)*t(bb, c, jj, l),
|
| 876 |
+
v(l, k, d, c)*t(aa, d, ii, l)*t(bb, c, jj, k),
|
| 877 |
+
]
|
| 878 |
+
for permut in exprs[1:]:
|
| 879 |
+
assert dums(exprs[0]) != dums(permut)
|
| 880 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 881 |
+
exprs = [
|
| 882 |
+
v(k, l, c, d)*t(aa, c, ii, k)*t(d, bb, jj, l),
|
| 883 |
+
v(l, k, c, d)*t(aa, c, ii, l)*t(d, bb, jj, k),
|
| 884 |
+
v(k, l, d, c)*t(aa, d, ii, k)*t(c, bb, jj, l),
|
| 885 |
+
v(l, k, d, c)*t(aa, d, ii, l)*t(c, bb, jj, k),
|
| 886 |
+
]
|
| 887 |
+
for permut in exprs[1:]:
|
| 888 |
+
assert dums(exprs[0]) != dums(permut)
|
| 889 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 890 |
+
exprs = [
|
| 891 |
+
v(k, l, c, d)*t(c, aa, ii, k)*t(bb, d, jj, l),
|
| 892 |
+
v(l, k, c, d)*t(c, aa, ii, l)*t(bb, d, jj, k),
|
| 893 |
+
v(k, l, d, c)*t(d, aa, ii, k)*t(bb, c, jj, l),
|
| 894 |
+
v(l, k, d, c)*t(d, aa, ii, l)*t(bb, c, jj, k),
|
| 895 |
+
]
|
| 896 |
+
for permut in exprs[1:]:
|
| 897 |
+
assert dums(exprs[0]) != dums(permut)
|
| 898 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 899 |
+
|
| 900 |
+
|
| 901 |
+
def test_internal_external_pqrs():
|
| 902 |
+
ii, jj = symbols('i j')
|
| 903 |
+
aa, bb = symbols('a b')
|
| 904 |
+
k, l = symbols('k l', cls=Dummy)
|
| 905 |
+
c, d = symbols('c d', cls=Dummy)
|
| 906 |
+
|
| 907 |
+
v = Function('v')
|
| 908 |
+
t = Function('t')
|
| 909 |
+
dums = _get_ordered_dummies
|
| 910 |
+
|
| 911 |
+
exprs = [
|
| 912 |
+
v(k, l, c, d)*t(aa, c, ii, k)*t(bb, d, jj, l),
|
| 913 |
+
v(l, k, c, d)*t(aa, c, ii, l)*t(bb, d, jj, k),
|
| 914 |
+
v(k, l, d, c)*t(aa, d, ii, k)*t(bb, c, jj, l),
|
| 915 |
+
v(l, k, d, c)*t(aa, d, ii, l)*t(bb, c, jj, k),
|
| 916 |
+
]
|
| 917 |
+
for permut in exprs[1:]:
|
| 918 |
+
assert dums(exprs[0]) != dums(permut)
|
| 919 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 920 |
+
|
| 921 |
+
|
| 922 |
+
def test_dummy_order_well_defined():
|
| 923 |
+
aa, bb = symbols('a b', above_fermi=True)
|
| 924 |
+
k, l, m = symbols('k l m', below_fermi=True, cls=Dummy)
|
| 925 |
+
c, d = symbols('c d', above_fermi=True, cls=Dummy)
|
| 926 |
+
p, q = symbols('p q', cls=Dummy)
|
| 927 |
+
|
| 928 |
+
A = Function('A')
|
| 929 |
+
B = Function('B')
|
| 930 |
+
C = Function('C')
|
| 931 |
+
dums = _get_ordered_dummies
|
| 932 |
+
|
| 933 |
+
# We go through all key components in the order of increasing priority,
|
| 934 |
+
# and consider only fully orderable expressions. Non-orderable expressions
|
| 935 |
+
# are tested elsewhere.
|
| 936 |
+
|
| 937 |
+
# pos in first factor determines sort order
|
| 938 |
+
assert dums(A(k, l)*B(l, k)) == [k, l]
|
| 939 |
+
assert dums(A(l, k)*B(l, k)) == [l, k]
|
| 940 |
+
assert dums(A(k, l)*B(k, l)) == [k, l]
|
| 941 |
+
assert dums(A(l, k)*B(k, l)) == [l, k]
|
| 942 |
+
|
| 943 |
+
# factors involving the index
|
| 944 |
+
assert dums(A(k, l)*B(l, m)*C(k, m)) == [l, k, m]
|
| 945 |
+
assert dums(A(k, l)*B(l, m)*C(m, k)) == [l, k, m]
|
| 946 |
+
assert dums(A(l, k)*B(l, m)*C(k, m)) == [l, k, m]
|
| 947 |
+
assert dums(A(l, k)*B(l, m)*C(m, k)) == [l, k, m]
|
| 948 |
+
assert dums(A(k, l)*B(m, l)*C(k, m)) == [l, k, m]
|
| 949 |
+
assert dums(A(k, l)*B(m, l)*C(m, k)) == [l, k, m]
|
| 950 |
+
assert dums(A(l, k)*B(m, l)*C(k, m)) == [l, k, m]
|
| 951 |
+
assert dums(A(l, k)*B(m, l)*C(m, k)) == [l, k, m]
|
| 952 |
+
|
| 953 |
+
# same, but with factor order determined by non-dummies
|
| 954 |
+
assert dums(A(k, aa, l)*A(l, bb, m)*A(bb, k, m)) == [l, k, m]
|
| 955 |
+
assert dums(A(k, aa, l)*A(l, bb, m)*A(bb, m, k)) == [l, k, m]
|
| 956 |
+
assert dums(A(k, aa, l)*A(m, bb, l)*A(bb, k, m)) == [l, k, m]
|
| 957 |
+
assert dums(A(k, aa, l)*A(m, bb, l)*A(bb, m, k)) == [l, k, m]
|
| 958 |
+
assert dums(A(l, aa, k)*A(l, bb, m)*A(bb, k, m)) == [l, k, m]
|
| 959 |
+
assert dums(A(l, aa, k)*A(l, bb, m)*A(bb, m, k)) == [l, k, m]
|
| 960 |
+
assert dums(A(l, aa, k)*A(m, bb, l)*A(bb, k, m)) == [l, k, m]
|
| 961 |
+
assert dums(A(l, aa, k)*A(m, bb, l)*A(bb, m, k)) == [l, k, m]
|
| 962 |
+
|
| 963 |
+
# index range
|
| 964 |
+
assert dums(A(p, c, k)*B(p, c, k)) == [k, c, p]
|
| 965 |
+
assert dums(A(p, k, c)*B(p, c, k)) == [k, c, p]
|
| 966 |
+
assert dums(A(c, k, p)*B(p, c, k)) == [k, c, p]
|
| 967 |
+
assert dums(A(c, p, k)*B(p, c, k)) == [k, c, p]
|
| 968 |
+
assert dums(A(k, c, p)*B(p, c, k)) == [k, c, p]
|
| 969 |
+
assert dums(A(k, p, c)*B(p, c, k)) == [k, c, p]
|
| 970 |
+
assert dums(B(p, c, k)*A(p, c, k)) == [k, c, p]
|
| 971 |
+
assert dums(B(p, k, c)*A(p, c, k)) == [k, c, p]
|
| 972 |
+
assert dums(B(c, k, p)*A(p, c, k)) == [k, c, p]
|
| 973 |
+
assert dums(B(c, p, k)*A(p, c, k)) == [k, c, p]
|
| 974 |
+
assert dums(B(k, c, p)*A(p, c, k)) == [k, c, p]
|
| 975 |
+
assert dums(B(k, p, c)*A(p, c, k)) == [k, c, p]
|
| 976 |
+
|
| 977 |
+
|
| 978 |
+
def test_dummy_order_ambiguous():
|
| 979 |
+
aa, bb = symbols('a b', above_fermi=True)
|
| 980 |
+
i, j, k, l, m = symbols('i j k l m', below_fermi=True, cls=Dummy)
|
| 981 |
+
a, b, c, d, e = symbols('a b c d e', above_fermi=True, cls=Dummy)
|
| 982 |
+
p, q = symbols('p q', cls=Dummy)
|
| 983 |
+
p1, p2, p3, p4 = symbols('p1 p2 p3 p4', above_fermi=True, cls=Dummy)
|
| 984 |
+
p5, p6, p7, p8 = symbols('p5 p6 p7 p8', above_fermi=True, cls=Dummy)
|
| 985 |
+
h1, h2, h3, h4 = symbols('h1 h2 h3 h4', below_fermi=True, cls=Dummy)
|
| 986 |
+
h5, h6, h7, h8 = symbols('h5 h6 h7 h8', below_fermi=True, cls=Dummy)
|
| 987 |
+
|
| 988 |
+
A = Function('A')
|
| 989 |
+
B = Function('B')
|
| 990 |
+
|
| 991 |
+
from sympy.utilities.iterables import variations
|
| 992 |
+
|
| 993 |
+
# A*A*A*A*B -- ordering of p5 and p4 is used to figure out the rest
|
| 994 |
+
template = A(p1, p2)*A(p4, p1)*A(p2, p3)*A(p3, p5)*B(p5, p4)
|
| 995 |
+
permutator = variations([a, b, c, d, e], 5)
|
| 996 |
+
base = template.subs(zip([p1, p2, p3, p4, p5], next(permutator)))
|
| 997 |
+
for permut in permutator:
|
| 998 |
+
subslist = zip([p1, p2, p3, p4, p5], permut)
|
| 999 |
+
expr = template.subs(subslist)
|
| 1000 |
+
assert substitute_dummies(expr) == substitute_dummies(base)
|
| 1001 |
+
|
| 1002 |
+
# A*A*A*A*A -- an arbitrary index is assigned and the rest are figured out
|
| 1003 |
+
template = A(p1, p2)*A(p4, p1)*A(p2, p3)*A(p3, p5)*A(p5, p4)
|
| 1004 |
+
permutator = variations([a, b, c, d, e], 5)
|
| 1005 |
+
base = template.subs(zip([p1, p2, p3, p4, p5], next(permutator)))
|
| 1006 |
+
for permut in permutator:
|
| 1007 |
+
subslist = zip([p1, p2, p3, p4, p5], permut)
|
| 1008 |
+
expr = template.subs(subslist)
|
| 1009 |
+
assert substitute_dummies(expr) == substitute_dummies(base)
|
| 1010 |
+
|
| 1011 |
+
# A*A*A -- ordering of p5 and p4 is used to figure out the rest
|
| 1012 |
+
template = A(p1, p2, p4, p1)*A(p2, p3, p3, p5)*A(p5, p4)
|
| 1013 |
+
permutator = variations([a, b, c, d, e], 5)
|
| 1014 |
+
base = template.subs(zip([p1, p2, p3, p4, p5], next(permutator)))
|
| 1015 |
+
for permut in permutator:
|
| 1016 |
+
subslist = zip([p1, p2, p3, p4, p5], permut)
|
| 1017 |
+
expr = template.subs(subslist)
|
| 1018 |
+
assert substitute_dummies(expr) == substitute_dummies(base)
|
| 1019 |
+
|
| 1020 |
+
|
| 1021 |
+
def atv(*args):
|
| 1022 |
+
return AntiSymmetricTensor('v', args[:2], args[2:] )
|
| 1023 |
+
|
| 1024 |
+
|
| 1025 |
+
def att(*args):
|
| 1026 |
+
if len(args) == 4:
|
| 1027 |
+
return AntiSymmetricTensor('t', args[:2], args[2:] )
|
| 1028 |
+
elif len(args) == 2:
|
| 1029 |
+
return AntiSymmetricTensor('t', (args[0],), (args[1],))
|
| 1030 |
+
|
| 1031 |
+
|
| 1032 |
+
def test_dummy_order_inner_outer_lines_VT1T1T1_AT():
|
| 1033 |
+
ii = symbols('i', below_fermi=True)
|
| 1034 |
+
aa = symbols('a', above_fermi=True)
|
| 1035 |
+
k, l = symbols('k l', below_fermi=True, cls=Dummy)
|
| 1036 |
+
c, d = symbols('c d', above_fermi=True, cls=Dummy)
|
| 1037 |
+
|
| 1038 |
+
# Coupled-Cluster T1 terms with V*T1*T1*T1
|
| 1039 |
+
# t^{a}_{k} t^{c}_{i} t^{d}_{l} v^{lk}_{dc}
|
| 1040 |
+
exprs = [
|
| 1041 |
+
# permut v and t <=> swapping internal lines, equivalent
|
| 1042 |
+
# irrespective of symmetries in v
|
| 1043 |
+
atv(k, l, c, d)*att(c, ii)*att(d, l)*att(aa, k),
|
| 1044 |
+
atv(l, k, c, d)*att(c, ii)*att(d, k)*att(aa, l),
|
| 1045 |
+
atv(k, l, d, c)*att(d, ii)*att(c, l)*att(aa, k),
|
| 1046 |
+
atv(l, k, d, c)*att(d, ii)*att(c, k)*att(aa, l),
|
| 1047 |
+
]
|
| 1048 |
+
for permut in exprs[1:]:
|
| 1049 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 1050 |
+
|
| 1051 |
+
|
| 1052 |
+
def test_dummy_order_inner_outer_lines_VT1T1T1T1_AT():
|
| 1053 |
+
ii, jj = symbols('i j', below_fermi=True)
|
| 1054 |
+
aa, bb = symbols('a b', above_fermi=True)
|
| 1055 |
+
k, l = symbols('k l', below_fermi=True, cls=Dummy)
|
| 1056 |
+
c, d = symbols('c d', above_fermi=True, cls=Dummy)
|
| 1057 |
+
|
| 1058 |
+
# Coupled-Cluster T2 terms with V*T1*T1*T1*T1
|
| 1059 |
+
# non-equivalent substitutions (change of sign)
|
| 1060 |
+
exprs = [
|
| 1061 |
+
# permut t <=> swapping external lines
|
| 1062 |
+
atv(k, l, c, d)*att(c, ii)*att(d, jj)*att(aa, k)*att(bb, l),
|
| 1063 |
+
atv(k, l, c, d)*att(c, jj)*att(d, ii)*att(aa, k)*att(bb, l),
|
| 1064 |
+
atv(k, l, c, d)*att(c, ii)*att(d, jj)*att(bb, k)*att(aa, l),
|
| 1065 |
+
]
|
| 1066 |
+
for permut in exprs[1:]:
|
| 1067 |
+
assert substitute_dummies(exprs[0]) == -substitute_dummies(permut)
|
| 1068 |
+
|
| 1069 |
+
# equivalent substitutions
|
| 1070 |
+
exprs = [
|
| 1071 |
+
atv(k, l, c, d)*att(c, ii)*att(d, jj)*att(aa, k)*att(bb, l),
|
| 1072 |
+
# permut t <=> swapping external lines
|
| 1073 |
+
atv(k, l, c, d)*att(c, jj)*att(d, ii)*att(bb, k)*att(aa, l),
|
| 1074 |
+
]
|
| 1075 |
+
for permut in exprs[1:]:
|
| 1076 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 1077 |
+
|
| 1078 |
+
|
| 1079 |
+
def test_equivalent_internal_lines_VT1T1_AT():
|
| 1080 |
+
i, j, k, l = symbols('i j k l', below_fermi=True, cls=Dummy)
|
| 1081 |
+
a, b, c, d = symbols('a b c d', above_fermi=True, cls=Dummy)
|
| 1082 |
+
|
| 1083 |
+
exprs = [ # permute v. Different dummy order. Not equivalent.
|
| 1084 |
+
atv(i, j, a, b)*att(a, i)*att(b, j),
|
| 1085 |
+
atv(j, i, a, b)*att(a, i)*att(b, j),
|
| 1086 |
+
atv(i, j, b, a)*att(a, i)*att(b, j),
|
| 1087 |
+
]
|
| 1088 |
+
for permut in exprs[1:]:
|
| 1089 |
+
assert substitute_dummies(exprs[0]) != substitute_dummies(permut)
|
| 1090 |
+
|
| 1091 |
+
exprs = [ # permute v. Different dummy order. Equivalent
|
| 1092 |
+
atv(i, j, a, b)*att(a, i)*att(b, j),
|
| 1093 |
+
atv(j, i, b, a)*att(a, i)*att(b, j),
|
| 1094 |
+
]
|
| 1095 |
+
for permut in exprs[1:]:
|
| 1096 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 1097 |
+
|
| 1098 |
+
exprs = [ # permute t. Same dummy order, not equivalent.
|
| 1099 |
+
atv(i, j, a, b)*att(a, i)*att(b, j),
|
| 1100 |
+
atv(i, j, a, b)*att(b, i)*att(a, j),
|
| 1101 |
+
]
|
| 1102 |
+
for permut in exprs[1:]:
|
| 1103 |
+
assert substitute_dummies(exprs[0]) != substitute_dummies(permut)
|
| 1104 |
+
|
| 1105 |
+
exprs = [ # permute v and t. Different dummy order, equivalent
|
| 1106 |
+
atv(i, j, a, b)*att(a, i)*att(b, j),
|
| 1107 |
+
atv(j, i, a, b)*att(a, j)*att(b, i),
|
| 1108 |
+
atv(i, j, b, a)*att(b, i)*att(a, j),
|
| 1109 |
+
atv(j, i, b, a)*att(b, j)*att(a, i),
|
| 1110 |
+
]
|
| 1111 |
+
for permut in exprs[1:]:
|
| 1112 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 1113 |
+
|
| 1114 |
+
|
| 1115 |
+
def test_equivalent_internal_lines_VT2conjT2_AT():
|
| 1116 |
+
# this diagram requires special handling in TCE
|
| 1117 |
+
i, j, k, l, m, n = symbols('i j k l m n', below_fermi=True, cls=Dummy)
|
| 1118 |
+
a, b, c, d, e, f = symbols('a b c d e f', above_fermi=True, cls=Dummy)
|
| 1119 |
+
p1, p2, p3, p4 = symbols('p1 p2 p3 p4', above_fermi=True, cls=Dummy)
|
| 1120 |
+
h1, h2, h3, h4 = symbols('h1 h2 h3 h4', below_fermi=True, cls=Dummy)
|
| 1121 |
+
|
| 1122 |
+
from sympy.utilities.iterables import variations
|
| 1123 |
+
|
| 1124 |
+
# atv(abcd)att(abij)att(ijcd)
|
| 1125 |
+
template = atv(p1, p2, p3, p4)*att(p1, p2, i, j)*att(i, j, p3, p4)
|
| 1126 |
+
permutator = variations([a, b, c, d], 4)
|
| 1127 |
+
base = template.subs(zip([p1, p2, p3, p4], next(permutator)))
|
| 1128 |
+
for permut in permutator:
|
| 1129 |
+
subslist = zip([p1, p2, p3, p4], permut)
|
| 1130 |
+
expr = template.subs(subslist)
|
| 1131 |
+
assert substitute_dummies(expr) == substitute_dummies(base)
|
| 1132 |
+
template = atv(p1, p2, p3, p4)*att(p1, p2, j, i)*att(j, i, p3, p4)
|
| 1133 |
+
permutator = variations([a, b, c, d], 4)
|
| 1134 |
+
base = template.subs(zip([p1, p2, p3, p4], next(permutator)))
|
| 1135 |
+
for permut in permutator:
|
| 1136 |
+
subslist = zip([p1, p2, p3, p4], permut)
|
| 1137 |
+
expr = template.subs(subslist)
|
| 1138 |
+
assert substitute_dummies(expr) == substitute_dummies(base)
|
| 1139 |
+
|
| 1140 |
+
# atv(abcd)att(abij)att(jicd)
|
| 1141 |
+
template = atv(p1, p2, p3, p4)*att(p1, p2, i, j)*att(j, i, p3, p4)
|
| 1142 |
+
permutator = variations([a, b, c, d], 4)
|
| 1143 |
+
base = template.subs(zip([p1, p2, p3, p4], next(permutator)))
|
| 1144 |
+
for permut in permutator:
|
| 1145 |
+
subslist = zip([p1, p2, p3, p4], permut)
|
| 1146 |
+
expr = template.subs(subslist)
|
| 1147 |
+
assert substitute_dummies(expr) == substitute_dummies(base)
|
| 1148 |
+
template = atv(p1, p2, p3, p4)*att(p1, p2, j, i)*att(i, j, p3, p4)
|
| 1149 |
+
permutator = variations([a, b, c, d], 4)
|
| 1150 |
+
base = template.subs(zip([p1, p2, p3, p4], next(permutator)))
|
| 1151 |
+
for permut in permutator:
|
| 1152 |
+
subslist = zip([p1, p2, p3, p4], permut)
|
| 1153 |
+
expr = template.subs(subslist)
|
| 1154 |
+
assert substitute_dummies(expr) == substitute_dummies(base)
|
| 1155 |
+
|
| 1156 |
+
|
| 1157 |
+
def test_equivalent_internal_lines_VT2conjT2_ambiguous_order_AT():
|
| 1158 |
+
# These diagrams invokes _determine_ambiguous() because the
|
| 1159 |
+
# dummies can not be ordered unambiguously by the key alone
|
| 1160 |
+
i, j, k, l, m, n = symbols('i j k l m n', below_fermi=True, cls=Dummy)
|
| 1161 |
+
a, b, c, d, e, f = symbols('a b c d e f', above_fermi=True, cls=Dummy)
|
| 1162 |
+
p1, p2, p3, p4 = symbols('p1 p2 p3 p4', above_fermi=True, cls=Dummy)
|
| 1163 |
+
h1, h2, h3, h4 = symbols('h1 h2 h3 h4', below_fermi=True, cls=Dummy)
|
| 1164 |
+
|
| 1165 |
+
from sympy.utilities.iterables import variations
|
| 1166 |
+
|
| 1167 |
+
# atv(abcd)att(abij)att(cdij)
|
| 1168 |
+
template = atv(p1, p2, p3, p4)*att(p1, p2, i, j)*att(p3, p4, i, j)
|
| 1169 |
+
permutator = variations([a, b, c, d], 4)
|
| 1170 |
+
base = template.subs(zip([p1, p2, p3, p4], next(permutator)))
|
| 1171 |
+
for permut in permutator:
|
| 1172 |
+
subslist = zip([p1, p2, p3, p4], permut)
|
| 1173 |
+
expr = template.subs(subslist)
|
| 1174 |
+
assert substitute_dummies(expr) == substitute_dummies(base)
|
| 1175 |
+
template = atv(p1, p2, p3, p4)*att(p1, p2, j, i)*att(p3, p4, i, j)
|
| 1176 |
+
permutator = variations([a, b, c, d], 4)
|
| 1177 |
+
base = template.subs(zip([p1, p2, p3, p4], next(permutator)))
|
| 1178 |
+
for permut in permutator:
|
| 1179 |
+
subslist = zip([p1, p2, p3, p4], permut)
|
| 1180 |
+
expr = template.subs(subslist)
|
| 1181 |
+
assert substitute_dummies(expr) == substitute_dummies(base)
|
| 1182 |
+
|
| 1183 |
+
|
| 1184 |
+
def test_equivalent_internal_lines_VT2_AT():
|
| 1185 |
+
i, j, k, l = symbols('i j k l', below_fermi=True, cls=Dummy)
|
| 1186 |
+
a, b, c, d = symbols('a b c d', above_fermi=True, cls=Dummy)
|
| 1187 |
+
|
| 1188 |
+
exprs = [
|
| 1189 |
+
# permute v. Same dummy order, not equivalent.
|
| 1190 |
+
atv(i, j, a, b)*att(a, b, i, j),
|
| 1191 |
+
atv(j, i, a, b)*att(a, b, i, j),
|
| 1192 |
+
atv(i, j, b, a)*att(a, b, i, j),
|
| 1193 |
+
]
|
| 1194 |
+
for permut in exprs[1:]:
|
| 1195 |
+
assert substitute_dummies(exprs[0]) != substitute_dummies(permut)
|
| 1196 |
+
|
| 1197 |
+
exprs = [
|
| 1198 |
+
# permute t.
|
| 1199 |
+
atv(i, j, a, b)*att(a, b, i, j),
|
| 1200 |
+
atv(i, j, a, b)*att(b, a, i, j),
|
| 1201 |
+
atv(i, j, a, b)*att(a, b, j, i),
|
| 1202 |
+
]
|
| 1203 |
+
for permut in exprs[1:]:
|
| 1204 |
+
assert substitute_dummies(exprs[0]) != substitute_dummies(permut)
|
| 1205 |
+
|
| 1206 |
+
exprs = [ # permute v and t. Relabelling of dummies should be equivalent.
|
| 1207 |
+
atv(i, j, a, b)*att(a, b, i, j),
|
| 1208 |
+
atv(j, i, a, b)*att(a, b, j, i),
|
| 1209 |
+
atv(i, j, b, a)*att(b, a, i, j),
|
| 1210 |
+
atv(j, i, b, a)*att(b, a, j, i),
|
| 1211 |
+
]
|
| 1212 |
+
for permut in exprs[1:]:
|
| 1213 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 1214 |
+
|
| 1215 |
+
|
| 1216 |
+
def test_internal_external_VT2T2_AT():
|
| 1217 |
+
ii, jj = symbols('i j', below_fermi=True)
|
| 1218 |
+
aa, bb = symbols('a b', above_fermi=True)
|
| 1219 |
+
k, l = symbols('k l', below_fermi=True, cls=Dummy)
|
| 1220 |
+
c, d = symbols('c d', above_fermi=True, cls=Dummy)
|
| 1221 |
+
|
| 1222 |
+
exprs = [
|
| 1223 |
+
atv(k, l, c, d)*att(aa, c, ii, k)*att(bb, d, jj, l),
|
| 1224 |
+
atv(l, k, c, d)*att(aa, c, ii, l)*att(bb, d, jj, k),
|
| 1225 |
+
atv(k, l, d, c)*att(aa, d, ii, k)*att(bb, c, jj, l),
|
| 1226 |
+
atv(l, k, d, c)*att(aa, d, ii, l)*att(bb, c, jj, k),
|
| 1227 |
+
]
|
| 1228 |
+
for permut in exprs[1:]:
|
| 1229 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 1230 |
+
exprs = [
|
| 1231 |
+
atv(k, l, c, d)*att(aa, c, ii, k)*att(d, bb, jj, l),
|
| 1232 |
+
atv(l, k, c, d)*att(aa, c, ii, l)*att(d, bb, jj, k),
|
| 1233 |
+
atv(k, l, d, c)*att(aa, d, ii, k)*att(c, bb, jj, l),
|
| 1234 |
+
atv(l, k, d, c)*att(aa, d, ii, l)*att(c, bb, jj, k),
|
| 1235 |
+
]
|
| 1236 |
+
for permut in exprs[1:]:
|
| 1237 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 1238 |
+
exprs = [
|
| 1239 |
+
atv(k, l, c, d)*att(c, aa, ii, k)*att(bb, d, jj, l),
|
| 1240 |
+
atv(l, k, c, d)*att(c, aa, ii, l)*att(bb, d, jj, k),
|
| 1241 |
+
atv(k, l, d, c)*att(d, aa, ii, k)*att(bb, c, jj, l),
|
| 1242 |
+
atv(l, k, d, c)*att(d, aa, ii, l)*att(bb, c, jj, k),
|
| 1243 |
+
]
|
| 1244 |
+
for permut in exprs[1:]:
|
| 1245 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 1246 |
+
|
| 1247 |
+
|
| 1248 |
+
def test_internal_external_pqrs_AT():
|
| 1249 |
+
ii, jj = symbols('i j')
|
| 1250 |
+
aa, bb = symbols('a b')
|
| 1251 |
+
k, l = symbols('k l', cls=Dummy)
|
| 1252 |
+
c, d = symbols('c d', cls=Dummy)
|
| 1253 |
+
|
| 1254 |
+
exprs = [
|
| 1255 |
+
atv(k, l, c, d)*att(aa, c, ii, k)*att(bb, d, jj, l),
|
| 1256 |
+
atv(l, k, c, d)*att(aa, c, ii, l)*att(bb, d, jj, k),
|
| 1257 |
+
atv(k, l, d, c)*att(aa, d, ii, k)*att(bb, c, jj, l),
|
| 1258 |
+
atv(l, k, d, c)*att(aa, d, ii, l)*att(bb, c, jj, k),
|
| 1259 |
+
]
|
| 1260 |
+
for permut in exprs[1:]:
|
| 1261 |
+
assert substitute_dummies(exprs[0]) == substitute_dummies(permut)
|
| 1262 |
+
|
| 1263 |
+
|
| 1264 |
+
def test_issue_19661():
|
| 1265 |
+
a = Symbol('0')
|
| 1266 |
+
assert latex(Commutator(Bd(a)**2, B(a))
|
| 1267 |
+
) == '- \\left[b_{0},{b^\\dagger_{0}}^{2}\\right]'
|
| 1268 |
+
|
| 1269 |
+
|
| 1270 |
+
def test_canonical_ordering_AntiSymmetricTensor():
|
| 1271 |
+
v = symbols("v")
|
| 1272 |
+
|
| 1273 |
+
c, d = symbols(('c','d'), above_fermi=True,
|
| 1274 |
+
cls=Dummy)
|
| 1275 |
+
k, l = symbols(('k','l'), below_fermi=True,
|
| 1276 |
+
cls=Dummy)
|
| 1277 |
+
|
| 1278 |
+
# formerly, the left gave either the left or the right
|
| 1279 |
+
assert AntiSymmetricTensor(v, (k, l), (d, c)
|
| 1280 |
+
) == -AntiSymmetricTensor(v, (l, k), (d, c))
|
lib/python3.12/site-packages/sympy/physics/tests/test_sho.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sympy.core import symbols, Rational, Function, diff
|
| 2 |
+
from sympy.physics.sho import R_nl, E_nl
|
| 3 |
+
from sympy.simplify.simplify import simplify
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def test_sho_R_nl():
|
| 7 |
+
omega, r = symbols('omega r')
|
| 8 |
+
l = symbols('l', integer=True)
|
| 9 |
+
u = Function('u')
|
| 10 |
+
|
| 11 |
+
# check that it obeys the Schrodinger equation
|
| 12 |
+
for n in range(5):
|
| 13 |
+
schreq = ( -diff(u(r), r, 2)/2 + ((l*(l + 1))/(2*r**2)
|
| 14 |
+
+ omega**2*r**2/2 - E_nl(n, l, omega))*u(r) )
|
| 15 |
+
result = schreq.subs(u(r), r*R_nl(n, l, omega/2, r))
|
| 16 |
+
assert simplify(result.doit()) == 0
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def test_energy():
|
| 20 |
+
n, l, hw = symbols('n l hw')
|
| 21 |
+
assert simplify(E_nl(n, l, hw) - (2*n + l + Rational(3, 2))*hw) == 0
|
lib/python3.12/site-packages/sympy/physics/units/__init__.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# isort:skip_file
|
| 2 |
+
"""
|
| 3 |
+
Dimensional analysis and unit systems.
|
| 4 |
+
|
| 5 |
+
This module defines dimension/unit systems and physical quantities. It is
|
| 6 |
+
based on a group-theoretical construction where dimensions are represented as
|
| 7 |
+
vectors (coefficients being the exponents), and units are defined as a dimension
|
| 8 |
+
to which we added a scale.
|
| 9 |
+
|
| 10 |
+
Quantities are built from a factor and a unit, and are the basic objects that
|
| 11 |
+
one will use when doing computations.
|
| 12 |
+
|
| 13 |
+
All objects except systems and prefixes can be used in SymPy expressions.
|
| 14 |
+
Note that as part of a CAS, various objects do not combine automatically
|
| 15 |
+
under operations.
|
| 16 |
+
|
| 17 |
+
Details about the implementation can be found in the documentation, and we
|
| 18 |
+
will not repeat all the explanations we gave there concerning our approach.
|
| 19 |
+
Ideas about future developments can be found on the `Github wiki
|
| 20 |
+
<https://github.com/sympy/sympy/wiki/Unit-systems>`_, and you should consult
|
| 21 |
+
this page if you are willing to help.
|
| 22 |
+
|
| 23 |
+
Useful functions:
|
| 24 |
+
|
| 25 |
+
- ``find_unit``: easily lookup pre-defined units.
|
| 26 |
+
- ``convert_to(expr, newunit)``: converts an expression into the same
|
| 27 |
+
expression expressed in another unit.
|
| 28 |
+
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
from .dimensions import Dimension, DimensionSystem
|
| 32 |
+
from .unitsystem import UnitSystem
|
| 33 |
+
from .util import convert_to
|
| 34 |
+
from .quantities import Quantity
|
| 35 |
+
|
| 36 |
+
from .definitions.dimension_definitions import (
|
| 37 |
+
amount_of_substance, acceleration, action, area,
|
| 38 |
+
capacitance, charge, conductance, current, energy,
|
| 39 |
+
force, frequency, impedance, inductance, length,
|
| 40 |
+
luminous_intensity, magnetic_density,
|
| 41 |
+
magnetic_flux, mass, momentum, power, pressure, temperature, time,
|
| 42 |
+
velocity, voltage, volume
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
Unit = Quantity
|
| 46 |
+
|
| 47 |
+
speed = velocity
|
| 48 |
+
luminosity = luminous_intensity
|
| 49 |
+
magnetic_flux_density = magnetic_density
|
| 50 |
+
amount = amount_of_substance
|
| 51 |
+
|
| 52 |
+
from .prefixes import (
|
| 53 |
+
# 10-power based:
|
| 54 |
+
yotta,
|
| 55 |
+
zetta,
|
| 56 |
+
exa,
|
| 57 |
+
peta,
|
| 58 |
+
tera,
|
| 59 |
+
giga,
|
| 60 |
+
mega,
|
| 61 |
+
kilo,
|
| 62 |
+
hecto,
|
| 63 |
+
deca,
|
| 64 |
+
deci,
|
| 65 |
+
centi,
|
| 66 |
+
milli,
|
| 67 |
+
micro,
|
| 68 |
+
nano,
|
| 69 |
+
pico,
|
| 70 |
+
femto,
|
| 71 |
+
atto,
|
| 72 |
+
zepto,
|
| 73 |
+
yocto,
|
| 74 |
+
# 2-power based:
|
| 75 |
+
kibi,
|
| 76 |
+
mebi,
|
| 77 |
+
gibi,
|
| 78 |
+
tebi,
|
| 79 |
+
pebi,
|
| 80 |
+
exbi,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
from .definitions import (
|
| 84 |
+
percent, percents,
|
| 85 |
+
permille,
|
| 86 |
+
rad, radian, radians,
|
| 87 |
+
deg, degree, degrees,
|
| 88 |
+
sr, steradian, steradians,
|
| 89 |
+
mil, angular_mil, angular_mils,
|
| 90 |
+
m, meter, meters,
|
| 91 |
+
kg, kilogram, kilograms,
|
| 92 |
+
s, second, seconds,
|
| 93 |
+
A, ampere, amperes,
|
| 94 |
+
K, kelvin, kelvins,
|
| 95 |
+
mol, mole, moles,
|
| 96 |
+
cd, candela, candelas,
|
| 97 |
+
g, gram, grams,
|
| 98 |
+
mg, milligram, milligrams,
|
| 99 |
+
ug, microgram, micrograms,
|
| 100 |
+
t, tonne, metric_ton,
|
| 101 |
+
newton, newtons, N,
|
| 102 |
+
joule, joules, J,
|
| 103 |
+
watt, watts, W,
|
| 104 |
+
pascal, pascals, Pa, pa,
|
| 105 |
+
hertz, hz, Hz,
|
| 106 |
+
coulomb, coulombs, C,
|
| 107 |
+
volt, volts, v, V,
|
| 108 |
+
ohm, ohms,
|
| 109 |
+
siemens, S, mho, mhos,
|
| 110 |
+
farad, farads, F,
|
| 111 |
+
henry, henrys, H,
|
| 112 |
+
tesla, teslas, T,
|
| 113 |
+
weber, webers, Wb, wb,
|
| 114 |
+
optical_power, dioptre, D,
|
| 115 |
+
lux, lx,
|
| 116 |
+
katal, kat,
|
| 117 |
+
gray, Gy,
|
| 118 |
+
becquerel, Bq,
|
| 119 |
+
km, kilometer, kilometers,
|
| 120 |
+
dm, decimeter, decimeters,
|
| 121 |
+
cm, centimeter, centimeters,
|
| 122 |
+
mm, millimeter, millimeters,
|
| 123 |
+
um, micrometer, micrometers, micron, microns,
|
| 124 |
+
nm, nanometer, nanometers,
|
| 125 |
+
pm, picometer, picometers,
|
| 126 |
+
ft, foot, feet,
|
| 127 |
+
inch, inches,
|
| 128 |
+
yd, yard, yards,
|
| 129 |
+
mi, mile, miles,
|
| 130 |
+
nmi, nautical_mile, nautical_miles,
|
| 131 |
+
angstrom, angstroms,
|
| 132 |
+
ha, hectare,
|
| 133 |
+
l, L, liter, liters,
|
| 134 |
+
dl, dL, deciliter, deciliters,
|
| 135 |
+
cl, cL, centiliter, centiliters,
|
| 136 |
+
ml, mL, milliliter, milliliters,
|
| 137 |
+
ms, millisecond, milliseconds,
|
| 138 |
+
us, microsecond, microseconds,
|
| 139 |
+
ns, nanosecond, nanoseconds,
|
| 140 |
+
ps, picosecond, picoseconds,
|
| 141 |
+
minute, minutes,
|
| 142 |
+
h, hour, hours,
|
| 143 |
+
day, days,
|
| 144 |
+
anomalistic_year, anomalistic_years,
|
| 145 |
+
sidereal_year, sidereal_years,
|
| 146 |
+
tropical_year, tropical_years,
|
| 147 |
+
common_year, common_years,
|
| 148 |
+
julian_year, julian_years,
|
| 149 |
+
draconic_year, draconic_years,
|
| 150 |
+
gaussian_year, gaussian_years,
|
| 151 |
+
full_moon_cycle, full_moon_cycles,
|
| 152 |
+
year, years,
|
| 153 |
+
G, gravitational_constant,
|
| 154 |
+
c, speed_of_light,
|
| 155 |
+
elementary_charge,
|
| 156 |
+
hbar,
|
| 157 |
+
planck,
|
| 158 |
+
eV, electronvolt, electronvolts,
|
| 159 |
+
avogadro_number,
|
| 160 |
+
avogadro, avogadro_constant,
|
| 161 |
+
boltzmann, boltzmann_constant,
|
| 162 |
+
stefan, stefan_boltzmann_constant,
|
| 163 |
+
R, molar_gas_constant,
|
| 164 |
+
faraday_constant,
|
| 165 |
+
josephson_constant,
|
| 166 |
+
von_klitzing_constant,
|
| 167 |
+
Da, dalton, amu, amus, atomic_mass_unit, atomic_mass_constant,
|
| 168 |
+
me, electron_rest_mass,
|
| 169 |
+
gee, gees, acceleration_due_to_gravity,
|
| 170 |
+
u0, magnetic_constant, vacuum_permeability,
|
| 171 |
+
e0, electric_constant, vacuum_permittivity,
|
| 172 |
+
Z0, vacuum_impedance,
|
| 173 |
+
coulomb_constant, electric_force_constant,
|
| 174 |
+
atmosphere, atmospheres, atm,
|
| 175 |
+
kPa,
|
| 176 |
+
bar, bars,
|
| 177 |
+
pound, pounds,
|
| 178 |
+
psi,
|
| 179 |
+
dHg0,
|
| 180 |
+
mmHg, torr,
|
| 181 |
+
mmu, mmus, milli_mass_unit,
|
| 182 |
+
quart, quarts,
|
| 183 |
+
ly, lightyear, lightyears,
|
| 184 |
+
au, astronomical_unit, astronomical_units,
|
| 185 |
+
planck_mass,
|
| 186 |
+
planck_time,
|
| 187 |
+
planck_temperature,
|
| 188 |
+
planck_length,
|
| 189 |
+
planck_charge,
|
| 190 |
+
planck_area,
|
| 191 |
+
planck_volume,
|
| 192 |
+
planck_momentum,
|
| 193 |
+
planck_energy,
|
| 194 |
+
planck_force,
|
| 195 |
+
planck_power,
|
| 196 |
+
planck_density,
|
| 197 |
+
planck_energy_density,
|
| 198 |
+
planck_intensity,
|
| 199 |
+
planck_angular_frequency,
|
| 200 |
+
planck_pressure,
|
| 201 |
+
planck_current,
|
| 202 |
+
planck_voltage,
|
| 203 |
+
planck_impedance,
|
| 204 |
+
planck_acceleration,
|
| 205 |
+
bit, bits,
|
| 206 |
+
byte,
|
| 207 |
+
kibibyte, kibibytes,
|
| 208 |
+
mebibyte, mebibytes,
|
| 209 |
+
gibibyte, gibibytes,
|
| 210 |
+
tebibyte, tebibytes,
|
| 211 |
+
pebibyte, pebibytes,
|
| 212 |
+
exbibyte, exbibytes,
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
from .systems import (
|
| 216 |
+
mks, mksa, si
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def find_unit(quantity, unit_system="SI"):
|
| 221 |
+
"""
|
| 222 |
+
Return a list of matching units or dimension names.
|
| 223 |
+
|
| 224 |
+
- If ``quantity`` is a string -- units/dimensions containing the string
|
| 225 |
+
`quantity`.
|
| 226 |
+
- If ``quantity`` is a unit or dimension -- units having matching base
|
| 227 |
+
units or dimensions.
|
| 228 |
+
|
| 229 |
+
Examples
|
| 230 |
+
========
|
| 231 |
+
|
| 232 |
+
>>> from sympy.physics import units as u
|
| 233 |
+
>>> u.find_unit('charge')
|
| 234 |
+
['C', 'coulomb', 'coulombs', 'planck_charge', 'elementary_charge']
|
| 235 |
+
>>> u.find_unit(u.charge)
|
| 236 |
+
['C', 'coulomb', 'coulombs', 'planck_charge', 'elementary_charge']
|
| 237 |
+
>>> u.find_unit("ampere")
|
| 238 |
+
['ampere', 'amperes']
|
| 239 |
+
>>> u.find_unit('angstrom')
|
| 240 |
+
['angstrom', 'angstroms']
|
| 241 |
+
>>> u.find_unit('volt')
|
| 242 |
+
['volt', 'volts', 'electronvolt', 'electronvolts', 'planck_voltage']
|
| 243 |
+
>>> u.find_unit(u.inch**3)[:9]
|
| 244 |
+
['L', 'l', 'cL', 'cl', 'dL', 'dl', 'mL', 'ml', 'liter']
|
| 245 |
+
"""
|
| 246 |
+
unit_system = UnitSystem.get_unit_system(unit_system)
|
| 247 |
+
|
| 248 |
+
import sympy.physics.units as u
|
| 249 |
+
rv = []
|
| 250 |
+
if isinstance(quantity, str):
|
| 251 |
+
rv = [i for i in dir(u) if quantity in i and isinstance(getattr(u, i), Quantity)]
|
| 252 |
+
dim = getattr(u, quantity)
|
| 253 |
+
if isinstance(dim, Dimension):
|
| 254 |
+
rv.extend(find_unit(dim))
|
| 255 |
+
else:
|
| 256 |
+
for i in sorted(dir(u)):
|
| 257 |
+
other = getattr(u, i)
|
| 258 |
+
if not isinstance(other, Quantity):
|
| 259 |
+
continue
|
| 260 |
+
if isinstance(quantity, Quantity):
|
| 261 |
+
if quantity.dimension == other.dimension:
|
| 262 |
+
rv.append(str(i))
|
| 263 |
+
elif isinstance(quantity, Dimension):
|
| 264 |
+
if other.dimension == quantity:
|
| 265 |
+
rv.append(str(i))
|
| 266 |
+
elif other.dimension == Dimension(unit_system.get_dimensional_expr(quantity)):
|
| 267 |
+
rv.append(str(i))
|
| 268 |
+
return sorted(set(rv), key=lambda x: (len(x), x))
|
| 269 |
+
|
| 270 |
+
# NOTE: the old units module had additional variables:
|
| 271 |
+
# 'density', 'illuminance', 'resistance'.
|
| 272 |
+
# They were not dimensions, but units (old Unit class).
|
| 273 |
+
|
| 274 |
+
__all__ = [
|
| 275 |
+
'Dimension', 'DimensionSystem',
|
| 276 |
+
'UnitSystem',
|
| 277 |
+
'convert_to',
|
| 278 |
+
'Quantity',
|
| 279 |
+
|
| 280 |
+
'amount_of_substance', 'acceleration', 'action', 'area',
|
| 281 |
+
'capacitance', 'charge', 'conductance', 'current', 'energy',
|
| 282 |
+
'force', 'frequency', 'impedance', 'inductance', 'length',
|
| 283 |
+
'luminous_intensity', 'magnetic_density',
|
| 284 |
+
'magnetic_flux', 'mass', 'momentum', 'power', 'pressure', 'temperature', 'time',
|
| 285 |
+
'velocity', 'voltage', 'volume',
|
| 286 |
+
|
| 287 |
+
'Unit',
|
| 288 |
+
|
| 289 |
+
'speed',
|
| 290 |
+
'luminosity',
|
| 291 |
+
'magnetic_flux_density',
|
| 292 |
+
'amount',
|
| 293 |
+
|
| 294 |
+
'yotta',
|
| 295 |
+
'zetta',
|
| 296 |
+
'exa',
|
| 297 |
+
'peta',
|
| 298 |
+
'tera',
|
| 299 |
+
'giga',
|
| 300 |
+
'mega',
|
| 301 |
+
'kilo',
|
| 302 |
+
'hecto',
|
| 303 |
+
'deca',
|
| 304 |
+
'deci',
|
| 305 |
+
'centi',
|
| 306 |
+
'milli',
|
| 307 |
+
'micro',
|
| 308 |
+
'nano',
|
| 309 |
+
'pico',
|
| 310 |
+
'femto',
|
| 311 |
+
'atto',
|
| 312 |
+
'zepto',
|
| 313 |
+
'yocto',
|
| 314 |
+
|
| 315 |
+
'kibi',
|
| 316 |
+
'mebi',
|
| 317 |
+
'gibi',
|
| 318 |
+
'tebi',
|
| 319 |
+
'pebi',
|
| 320 |
+
'exbi',
|
| 321 |
+
|
| 322 |
+
'percent', 'percents',
|
| 323 |
+
'permille',
|
| 324 |
+
'rad', 'radian', 'radians',
|
| 325 |
+
'deg', 'degree', 'degrees',
|
| 326 |
+
'sr', 'steradian', 'steradians',
|
| 327 |
+
'mil', 'angular_mil', 'angular_mils',
|
| 328 |
+
'm', 'meter', 'meters',
|
| 329 |
+
'kg', 'kilogram', 'kilograms',
|
| 330 |
+
's', 'second', 'seconds',
|
| 331 |
+
'A', 'ampere', 'amperes',
|
| 332 |
+
'K', 'kelvin', 'kelvins',
|
| 333 |
+
'mol', 'mole', 'moles',
|
| 334 |
+
'cd', 'candela', 'candelas',
|
| 335 |
+
'g', 'gram', 'grams',
|
| 336 |
+
'mg', 'milligram', 'milligrams',
|
| 337 |
+
'ug', 'microgram', 'micrograms',
|
| 338 |
+
't', 'tonne', 'metric_ton',
|
| 339 |
+
'newton', 'newtons', 'N',
|
| 340 |
+
'joule', 'joules', 'J',
|
| 341 |
+
'watt', 'watts', 'W',
|
| 342 |
+
'pascal', 'pascals', 'Pa', 'pa',
|
| 343 |
+
'hertz', 'hz', 'Hz',
|
| 344 |
+
'coulomb', 'coulombs', 'C',
|
| 345 |
+
'volt', 'volts', 'v', 'V',
|
| 346 |
+
'ohm', 'ohms',
|
| 347 |
+
'siemens', 'S', 'mho', 'mhos',
|
| 348 |
+
'farad', 'farads', 'F',
|
| 349 |
+
'henry', 'henrys', 'H',
|
| 350 |
+
'tesla', 'teslas', 'T',
|
| 351 |
+
'weber', 'webers', 'Wb', 'wb',
|
| 352 |
+
'optical_power', 'dioptre', 'D',
|
| 353 |
+
'lux', 'lx',
|
| 354 |
+
'katal', 'kat',
|
| 355 |
+
'gray', 'Gy',
|
| 356 |
+
'becquerel', 'Bq',
|
| 357 |
+
'km', 'kilometer', 'kilometers',
|
| 358 |
+
'dm', 'decimeter', 'decimeters',
|
| 359 |
+
'cm', 'centimeter', 'centimeters',
|
| 360 |
+
'mm', 'millimeter', 'millimeters',
|
| 361 |
+
'um', 'micrometer', 'micrometers', 'micron', 'microns',
|
| 362 |
+
'nm', 'nanometer', 'nanometers',
|
| 363 |
+
'pm', 'picometer', 'picometers',
|
| 364 |
+
'ft', 'foot', 'feet',
|
| 365 |
+
'inch', 'inches',
|
| 366 |
+
'yd', 'yard', 'yards',
|
| 367 |
+
'mi', 'mile', 'miles',
|
| 368 |
+
'nmi', 'nautical_mile', 'nautical_miles',
|
| 369 |
+
'angstrom', 'angstroms',
|
| 370 |
+
'ha', 'hectare',
|
| 371 |
+
'l', 'L', 'liter', 'liters',
|
| 372 |
+
'dl', 'dL', 'deciliter', 'deciliters',
|
| 373 |
+
'cl', 'cL', 'centiliter', 'centiliters',
|
| 374 |
+
'ml', 'mL', 'milliliter', 'milliliters',
|
| 375 |
+
'ms', 'millisecond', 'milliseconds',
|
| 376 |
+
'us', 'microsecond', 'microseconds',
|
| 377 |
+
'ns', 'nanosecond', 'nanoseconds',
|
| 378 |
+
'ps', 'picosecond', 'picoseconds',
|
| 379 |
+
'minute', 'minutes',
|
| 380 |
+
'h', 'hour', 'hours',
|
| 381 |
+
'day', 'days',
|
| 382 |
+
'anomalistic_year', 'anomalistic_years',
|
| 383 |
+
'sidereal_year', 'sidereal_years',
|
| 384 |
+
'tropical_year', 'tropical_years',
|
| 385 |
+
'common_year', 'common_years',
|
| 386 |
+
'julian_year', 'julian_years',
|
| 387 |
+
'draconic_year', 'draconic_years',
|
| 388 |
+
'gaussian_year', 'gaussian_years',
|
| 389 |
+
'full_moon_cycle', 'full_moon_cycles',
|
| 390 |
+
'year', 'years',
|
| 391 |
+
'G', 'gravitational_constant',
|
| 392 |
+
'c', 'speed_of_light',
|
| 393 |
+
'elementary_charge',
|
| 394 |
+
'hbar',
|
| 395 |
+
'planck',
|
| 396 |
+
'eV', 'electronvolt', 'electronvolts',
|
| 397 |
+
'avogadro_number',
|
| 398 |
+
'avogadro', 'avogadro_constant',
|
| 399 |
+
'boltzmann', 'boltzmann_constant',
|
| 400 |
+
'stefan', 'stefan_boltzmann_constant',
|
| 401 |
+
'R', 'molar_gas_constant',
|
| 402 |
+
'faraday_constant',
|
| 403 |
+
'josephson_constant',
|
| 404 |
+
'von_klitzing_constant',
|
| 405 |
+
'Da', 'dalton', 'amu', 'amus', 'atomic_mass_unit', 'atomic_mass_constant',
|
| 406 |
+
'me', 'electron_rest_mass',
|
| 407 |
+
'gee', 'gees', 'acceleration_due_to_gravity',
|
| 408 |
+
'u0', 'magnetic_constant', 'vacuum_permeability',
|
| 409 |
+
'e0', 'electric_constant', 'vacuum_permittivity',
|
| 410 |
+
'Z0', 'vacuum_impedance',
|
| 411 |
+
'coulomb_constant', 'electric_force_constant',
|
| 412 |
+
'atmosphere', 'atmospheres', 'atm',
|
| 413 |
+
'kPa',
|
| 414 |
+
'bar', 'bars',
|
| 415 |
+
'pound', 'pounds',
|
| 416 |
+
'psi',
|
| 417 |
+
'dHg0',
|
| 418 |
+
'mmHg', 'torr',
|
| 419 |
+
'mmu', 'mmus', 'milli_mass_unit',
|
| 420 |
+
'quart', 'quarts',
|
| 421 |
+
'ly', 'lightyear', 'lightyears',
|
| 422 |
+
'au', 'astronomical_unit', 'astronomical_units',
|
| 423 |
+
'planck_mass',
|
| 424 |
+
'planck_time',
|
| 425 |
+
'planck_temperature',
|
| 426 |
+
'planck_length',
|
| 427 |
+
'planck_charge',
|
| 428 |
+
'planck_area',
|
| 429 |
+
'planck_volume',
|
| 430 |
+
'planck_momentum',
|
| 431 |
+
'planck_energy',
|
| 432 |
+
'planck_force',
|
| 433 |
+
'planck_power',
|
| 434 |
+
'planck_density',
|
| 435 |
+
'planck_energy_density',
|
| 436 |
+
'planck_intensity',
|
| 437 |
+
'planck_angular_frequency',
|
| 438 |
+
'planck_pressure',
|
| 439 |
+
'planck_current',
|
| 440 |
+
'planck_voltage',
|
| 441 |
+
'planck_impedance',
|
| 442 |
+
'planck_acceleration',
|
| 443 |
+
'bit', 'bits',
|
| 444 |
+
'byte',
|
| 445 |
+
'kibibyte', 'kibibytes',
|
| 446 |
+
'mebibyte', 'mebibytes',
|
| 447 |
+
'gibibyte', 'gibibytes',
|
| 448 |
+
'tebibyte', 'tebibytes',
|
| 449 |
+
'pebibyte', 'pebibytes',
|
| 450 |
+
'exbibyte', 'exbibytes',
|
| 451 |
+
|
| 452 |
+
'mks', 'mksa', 'si',
|
| 453 |
+
]
|
lib/python3.12/site-packages/sympy/physics/units/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (13.7 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/units/__pycache__/dimensions.cpython-312.pyc
ADDED
|
Binary file (29.4 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/units/__pycache__/prefixes.cpython-312.pyc
ADDED
|
Binary file (8.87 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/units/__pycache__/quantities.cpython-312.pyc
ADDED
|
Binary file (6.78 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/units/__pycache__/unitsystem.cpython-312.pyc
ADDED
|
Binary file (11.7 kB). View file
|
|
|
lib/python3.12/site-packages/sympy/physics/units/__pycache__/util.cpython-312.pyc
ADDED
|
Binary file (13.4 kB). View file
|
|
|