ZhengyangZhang commited on
Commit
44c9281
·
verified ·
1 Parent(s): 731c7bb

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. lib/python3.12/site-packages/sympy/physics/control/__init__.py +16 -0
  2. lib/python3.12/site-packages/sympy/physics/control/__pycache__/__init__.cpython-312.pyc +0 -0
  3. lib/python3.12/site-packages/sympy/physics/control/__pycache__/control_plots.cpython-312.pyc +0 -0
  4. lib/python3.12/site-packages/sympy/physics/control/control_plots.py +978 -0
  5. lib/python3.12/site-packages/sympy/physics/control/lti.py +0 -0
  6. lib/python3.12/site-packages/sympy/physics/control/tests/__init__.py +0 -0
  7. lib/python3.12/site-packages/sympy/physics/control/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  8. lib/python3.12/site-packages/sympy/physics/control/tests/__pycache__/test_control_plots.cpython-312.pyc +0 -0
  9. lib/python3.12/site-packages/sympy/physics/control/tests/test_control_plots.py +299 -0
  10. lib/python3.12/site-packages/sympy/physics/control/tests/test_lti.py +1750 -0
  11. lib/python3.12/site-packages/sympy/physics/hep/__init__.py +0 -0
  12. lib/python3.12/site-packages/sympy/physics/hep/__pycache__/__init__.cpython-312.pyc +0 -0
  13. lib/python3.12/site-packages/sympy/physics/hep/__pycache__/gamma_matrices.cpython-312.pyc +0 -0
  14. lib/python3.12/site-packages/sympy/physics/hep/gamma_matrices.py +716 -0
  15. lib/python3.12/site-packages/sympy/physics/hep/tests/__init__.py +0 -0
  16. lib/python3.12/site-packages/sympy/physics/hep/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  17. lib/python3.12/site-packages/sympy/physics/hep/tests/__pycache__/test_gamma_matrices.cpython-312.pyc +0 -0
  18. lib/python3.12/site-packages/sympy/physics/hep/tests/test_gamma_matrices.py +427 -0
  19. lib/python3.12/site-packages/sympy/physics/mechanics/__init__.py +90 -0
  20. lib/python3.12/site-packages/sympy/physics/mechanics/actuator.py +992 -0
  21. lib/python3.12/site-packages/sympy/physics/mechanics/body.py +710 -0
  22. lib/python3.12/site-packages/sympy/physics/mechanics/body_base.py +94 -0
  23. lib/python3.12/site-packages/sympy/physics/mechanics/joint.py +2188 -0
  24. lib/python3.12/site-packages/sympy/physics/mechanics/jointsmethod.py +318 -0
  25. lib/python3.12/site-packages/sympy/physics/mechanics/kane.py +860 -0
  26. lib/python3.12/site-packages/sympy/physics/mechanics/linearize.py +474 -0
  27. lib/python3.12/site-packages/sympy/physics/mechanics/models.py +230 -0
  28. lib/python3.12/site-packages/sympy/physics/mechanics/particle.py +209 -0
  29. lib/python3.12/site-packages/sympy/physics/mechanics/rigidbody.py +314 -0
  30. lib/python3.12/site-packages/sympy/physics/mechanics/system.py +1553 -0
  31. lib/python3.12/site-packages/sympy/physics/mechanics/wrapping_geometry.py +641 -0
  32. lib/python3.12/site-packages/sympy/physics/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  33. lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_clebsch_gordan.cpython-312.pyc +0 -0
  34. lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_hydrogen.cpython-312.pyc +0 -0
  35. lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_paulialgebra.cpython-312.pyc +0 -0
  36. lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_physics_matrices.cpython-312.pyc +0 -0
  37. lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_pring.cpython-312.pyc +0 -0
  38. lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_qho_1d.cpython-312.pyc +0 -0
  39. lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_secondquant.cpython-312.pyc +0 -0
  40. lib/python3.12/site-packages/sympy/physics/tests/__pycache__/test_sho.cpython-312.pyc +0 -0
  41. lib/python3.12/site-packages/sympy/physics/tests/test_physics_matrices.py +84 -0
  42. lib/python3.12/site-packages/sympy/physics/tests/test_secondquant.py +1280 -0
  43. lib/python3.12/site-packages/sympy/physics/tests/test_sho.py +21 -0
  44. lib/python3.12/site-packages/sympy/physics/units/__init__.py +453 -0
  45. lib/python3.12/site-packages/sympy/physics/units/__pycache__/__init__.cpython-312.pyc +0 -0
  46. lib/python3.12/site-packages/sympy/physics/units/__pycache__/dimensions.cpython-312.pyc +0 -0
  47. lib/python3.12/site-packages/sympy/physics/units/__pycache__/prefixes.cpython-312.pyc +0 -0
  48. lib/python3.12/site-packages/sympy/physics/units/__pycache__/quantities.cpython-312.pyc +0 -0
  49. lib/python3.12/site-packages/sympy/physics/units/__pycache__/unitsystem.cpython-312.pyc +0 -0
  50. 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