geomech 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. geomech-0.2.0/LICENSE +29 -0
  2. geomech-0.2.0/PKG-INFO +353 -0
  3. geomech-0.2.0/README.md +311 -0
  4. geomech-0.2.0/geomech/__init__.py +128 -0
  5. geomech-0.2.0/geomech/codegen/__init__.py +29 -0
  6. geomech-0.2.0/geomech/codegen/casadi_target.py +171 -0
  7. geomech-0.2.0/geomech/codegen/emitter.py +298 -0
  8. geomech-0.2.0/geomech/codegen/energy.py +111 -0
  9. geomech-0.2.0/geomech/codegen/layout.py +553 -0
  10. geomech-0.2.0/geomech/codegen/module.py +403 -0
  11. geomech-0.2.0/geomech/codegen/numpy_target.py +186 -0
  12. geomech-0.2.0/geomech/codegen/source.py +159 -0
  13. geomech-0.2.0/geomech/core/__init__.py +0 -0
  14. geomech-0.2.0/geomech/core/base/__init__.py +27 -0
  15. geomech-0.2.0/geomech/core/base/constants.py +58 -0
  16. geomech-0.2.0/geomech/core/base/expressions.py +814 -0
  17. geomech-0.2.0/geomech/core/base/types.py +96 -0
  18. geomech-0.2.0/geomech/core/math/__init__.py +16 -0
  19. geomech-0.2.0/geomech/core/math/collecting.py +274 -0
  20. geomech-0.2.0/geomech/core/math/extract.py +721 -0
  21. geomech-0.2.0/geomech/core/math/ibp.py +221 -0
  22. geomech-0.2.0/geomech/core/operations/__init__.py +24 -0
  23. geomech-0.2.0/geomech/core/operations/addition.py +124 -0
  24. geomech-0.2.0/geomech/core/operations/calculus.py +115 -0
  25. geomech-0.2.0/geomech/core/operations/geometry.py +257 -0
  26. geomech-0.2.0/geomech/core/operations/mixins.py +201 -0
  27. geomech-0.2.0/geomech/core/operations/multiplication.py +274 -0
  28. geomech-0.2.0/geomech/core/transformations/__init__.py +29 -0
  29. geomech-0.2.0/geomech/core/transformations/expansion.py +238 -0
  30. geomech-0.2.0/geomech/core/transformations/manifold_rules.py +118 -0
  31. geomech-0.2.0/geomech/core/transformations/pulling.py +237 -0
  32. geomech-0.2.0/geomech/core/transformations/simplification.py +732 -0
  33. geomech-0.2.0/geomech/core/transformations/utils.py +118 -0
  34. geomech-0.2.0/geomech/core/transformations/vector_identities.py +207 -0
  35. geomech-0.2.0/geomech/dynamics/__init__.py +14 -0
  36. geomech-0.2.0/geomech/dynamics/eom.py +1062 -0
  37. geomech-0.2.0/geomech/dynamics/standard_form.py +251 -0
  38. geomech-0.2.0/geomech/dynamics/variables.py +41 -0
  39. geomech-0.2.0/geomech/dynamics/vbl.py +654 -0
  40. geomech-0.2.0/geomech/numeric.py +893 -0
  41. geomech-0.2.0/geomech/simulation/__init__.py +24 -0
  42. geomech-0.2.0/geomech/simulation/control.py +358 -0
  43. geomech-0.2.0/geomech/simulation/integrators.py +469 -0
  44. geomech-0.2.0/geomech/simulation/model.py +270 -0
  45. geomech-0.2.0/geomech/systems/__init__.py +98 -0
  46. geomech-0.2.0/geomech/systems/_base.py +392 -0
  47. geomech-0.2.0/geomech/systems/aerial.py +512 -0
  48. geomech-0.2.0/geomech/systems/rigid.py +408 -0
  49. geomech-0.2.0/geomech/systems/spheres.py +381 -0
  50. geomech-0.2.0/geomech/utils/__init__.py +18 -0
  51. geomech-0.2.0/geomech/utils/errors.py +107 -0
  52. geomech-0.2.0/geomech/utils/invariants.py +267 -0
  53. geomech-0.2.0/geomech/utils/printing.py +998 -0
  54. geomech-0.2.0/geomech/utils/trace.py +417 -0
  55. geomech-0.2.0/geomech/viz/__init__.py +41 -0
  56. geomech-0.2.0/geomech/viz/latex.py +168 -0
  57. geomech-0.2.0/geomech/viz/report.py +750 -0
  58. geomech-0.2.0/geomech/viz/scene.py +691 -0
  59. geomech-0.2.0/geomech/viz/template.html +688 -0
  60. geomech-0.2.0/geomech.egg-info/PKG-INFO +353 -0
  61. geomech-0.2.0/geomech.egg-info/SOURCES.txt +151 -0
  62. geomech-0.2.0/geomech.egg-info/dependency_links.txt +1 -0
  63. geomech-0.2.0/geomech.egg-info/requires.txt +20 -0
  64. geomech-0.2.0/geomech.egg-info/top_level.txt +1 -0
  65. geomech-0.2.0/pyproject.toml +96 -0
  66. geomech-0.2.0/setup.cfg +4 -0
  67. geomech-0.2.0/tests/test_addition.py +523 -0
  68. geomech-0.2.0/tests/test_calculus.py +372 -0
  69. geomech-0.2.0/tests/test_codegen_adversarial.py +643 -0
  70. geomech-0.2.0/tests/test_codegen_arguments.py +331 -0
  71. geomech-0.2.0/tests/test_codegen_casadi.py +79 -0
  72. geomech-0.2.0/tests/test_codegen_energy.py +149 -0
  73. geomech-0.2.0/tests/test_codegen_generated.py +152 -0
  74. geomech-0.2.0/tests/test_codegen_layout.py +232 -0
  75. geomech-0.2.0/tests/test_codegen_numeric.py +133 -0
  76. geomech-0.2.0/tests/test_codegen_simulation_integration.py +384 -0
  77. geomech-0.2.0/tests/test_codegen_translation.py +148 -0
  78. geomech-0.2.0/tests/test_collect.py +264 -0
  79. geomech-0.2.0/tests/test_collect_fix.py +222 -0
  80. geomech-0.2.0/tests/test_collect_termination.py +66 -0
  81. geomech-0.2.0/tests/test_eom.py +188 -0
  82. geomech-0.2.0/tests/test_eom_oracle.py +278 -0
  83. geomech-0.2.0/tests/test_error_context.py +263 -0
  84. geomech-0.2.0/tests/test_error_messages.py +277 -0
  85. geomech-0.2.0/tests/test_expand.py +428 -0
  86. geomech-0.2.0/tests/test_expand_vvmul.py +147 -0
  87. geomech-0.2.0/tests/test_extract.py +1180 -0
  88. geomech-0.2.0/tests/test_extract_fix.py +197 -0
  89. geomech-0.2.0/tests/test_geometry.py +627 -0
  90. geomech-0.2.0/tests/test_ibp.py +168 -0
  91. geomech-0.2.0/tests/test_ibp_prefactor.py +126 -0
  92. geomech-0.2.0/tests/test_input_name_clash.py +153 -0
  93. geomech-0.2.0/tests/test_input_validation_shared.py +269 -0
  94. geomech-0.2.0/tests/test_inputs_adversarial.py +305 -0
  95. geomech-0.2.0/tests/test_invariants.py +278 -0
  96. geomech-0.2.0/tests/test_latex_precedence.py +179 -0
  97. geomech-0.2.0/tests/test_logging.py +126 -0
  98. geomech-0.2.0/tests/test_manifold_pipelines.py +846 -0
  99. geomech-0.2.0/tests/test_manifold_rules.py +338 -0
  100. geomech-0.2.0/tests/test_manifolds.py +98 -0
  101. geomech-0.2.0/tests/test_matrices.py +225 -0
  102. geomech-0.2.0/tests/test_mixins.py +362 -0
  103. geomech-0.2.0/tests/test_multiplication.py +804 -0
  104. geomech-0.2.0/tests/test_numeric.py +299 -0
  105. geomech-0.2.0/tests/test_operations.py +209 -0
  106. geomech-0.2.0/tests/test_operators.py +351 -0
  107. geomech-0.2.0/tests/test_phase1_adversarial.py +669 -0
  108. geomech-0.2.0/tests/test_phase2_adversarial.py +720 -0
  109. geomech-0.2.0/tests/test_phase3_adversarial.py +369 -0
  110. geomech-0.2.0/tests/test_phase3_fixes.py +111 -0
  111. geomech-0.2.0/tests/test_phase4_adversarial.py +1023 -0
  112. geomech-0.2.0/tests/test_phase5a_adversarial.py +701 -0
  113. geomech-0.2.0/tests/test_phase5a_simplifier.py +276 -0
  114. geomech-0.2.0/tests/test_phase5b_adversarial.py +412 -0
  115. geomech-0.2.0/tests/test_phase5b_structure.py +389 -0
  116. geomech-0.2.0/tests/test_printing.py +105 -0
  117. geomech-0.2.0/tests/test_pull.py +329 -0
  118. geomech-0.2.0/tests/test_repr_debug.py +153 -0
  119. geomech-0.2.0/tests/test_row_forms.py +133 -0
  120. geomech-0.2.0/tests/test_rule_coverage.py +110 -0
  121. geomech-0.2.0/tests/test_rule_names.py +216 -0
  122. geomech-0.2.0/tests/test_s2_variation_fix.py +239 -0
  123. geomech-0.2.0/tests/test_scalar_adversarial.py +1002 -0
  124. geomech-0.2.0/tests/test_scalars.py +164 -0
  125. geomech-0.2.0/tests/test_simplification.py +86 -0
  126. geomech-0.2.0/tests/test_simplify.py +786 -0
  127. geomech-0.2.0/tests/test_simplify_numbers.py +97 -0
  128. geomech-0.2.0/tests/test_simulation.py +406 -0
  129. geomech-0.2.0/tests/test_simulation_adversarial.py +639 -0
  130. geomech-0.2.0/tests/test_simulation_conservation.py +168 -0
  131. geomech-0.2.0/tests/test_simulation_lqr.py +196 -0
  132. geomech-0.2.0/tests/test_standard_form.py +435 -0
  133. geomech-0.2.0/tests/test_systems.py +313 -0
  134. geomech-0.2.0/tests/test_systems_book.py +265 -0
  135. geomech-0.2.0/tests/test_systems_golden.py +115 -0
  136. geomech-0.2.0/tests/test_systems_physics.py +358 -0
  137. geomech-0.2.0/tests/test_systems_pipeline.py +138 -0
  138. geomech-0.2.0/tests/test_trace.py +207 -0
  139. geomech-0.2.0/tests/test_transpose.py +52 -0
  140. geomech-0.2.0/tests/test_troubleshooting_doc.py +107 -0
  141. geomech-0.2.0/tests/test_utils.py +64 -0
  142. geomech-0.2.0/tests/test_validation.py +652 -0
  143. geomech-0.2.0/tests/test_vbl.py +897 -0
  144. geomech-0.2.0/tests/test_vbl_adversarial.py +748 -0
  145. geomech-0.2.0/tests/test_vbl_validation.py +138 -0
  146. geomech-0.2.0/tests/test_vector_rules.py +225 -0
  147. geomech-0.2.0/tests/test_vectors.py +221 -0
  148. geomech-0.2.0/tests/test_version.py +26 -0
  149. geomech-0.2.0/tests/test_viz.py +503 -0
  150. geomech-0.2.0/tests/test_viz_adversarial.py +856 -0
  151. geomech-0.2.0/tests/test_viz_final.py +121 -0
  152. geomech-0.2.0/tests/test_viz_fixes.py +618 -0
  153. geomech-0.2.0/tests/test_viz_skill.py +80 -0
geomech-0.2.0/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2021, Prasanth Kotaru
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
geomech-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,353 @@
1
+ Metadata-Version: 2.4
2
+ Name: geomech
3
+ Version: 0.2.0
4
+ Summary: Symbolic computation of dynamics on smooth manifolds
5
+ Author: Prasanth Kotaru
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/vkotaru/geomech
8
+ Project-URL: Documentation, https://geomech.readthedocs.io
9
+ Project-URL: Repository, https://github.com/vkotaru/geomech
10
+ Project-URL: Changelog, https://github.com/vkotaru/geomech/blob/main/CHANGELOG.md
11
+ Keywords: geometric-mechanics,equations-of-motion,symbolic-computation,lagrangian-mechanics,robotics,SO3,S2,linearization,code-generation
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
20
+ Classifier: Topic :: Scientific/Engineering :: Physics
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: numpy
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest; extra == "dev"
28
+ Requires-Dist: ruff; extra == "dev"
29
+ Requires-Dist: mypy==2.4.1; extra == "dev"
30
+ Requires-Dist: jupyterlab; extra == "dev"
31
+ Requires-Dist: scipy; extra == "dev"
32
+ Provides-Extra: casadi
33
+ Requires-Dist: casadi<4,>=3.6; extra == "casadi"
34
+ Provides-Extra: sim
35
+ Requires-Dist: scipy; extra == "sim"
36
+ Provides-Extra: docs
37
+ Requires-Dist: sphinx>=7.2; extra == "docs"
38
+ Requires-Dist: furo>=2024.1.29; extra == "docs"
39
+ Requires-Dist: myst-parser>=3.0; extra == "docs"
40
+ Requires-Dist: sphinx-copybutton>=0.5.2; extra == "docs"
41
+ Dynamic: license-file
42
+
43
+ # geomech
44
+
45
+ A Python library for the symbolic derivation of equations of motion and their variation-based linearization on smooth manifolds: $R$, $R^3$, $S^2$, $SO(3)$ and their products (e.g. $SE(3)$ as $R^3 \times SO(3)$).
46
+
47
+ This work builds on the methods and math described in
48
+ 1. [Global Formulations of Lagrangian and Hamiltonian Dynamics on Manifolds](https://link.springer.com/book/10.1007/978-3-319-56953-6)
49
+ 2. [Symbolic Computation of Dynamics on Smooth Manifolds](https://link.springer.com/chapter/10.1007/978-3-030-43089-4_22) and its [Scala implementation](https://github.com/HybridRobotics/dynamics_on_manifolds).
50
+
51
+ Documentation: https://geomech.readthedocs.io
52
+
53
+ ## What it does
54
+
55
+ Given a Lagrangian and virtual work, `geomech` automatically derives
56
+ globally valid, singularity-free equations of motion on smooth manifolds
57
+ (SO(3), S2) using Hamilton's principle and variational calculus --
58
+ no Euler angles or quaternions required.
59
+
60
+ The pipeline:
61
+
62
+ 1. Define symbolic scalars, vectors, and matrices
63
+ 2. Build kinetic/potential energy and virtual work expressions
64
+ 3. Call `compute_eom()` -- the library takes the variation of the action, applies the
65
+ manifold kinematics, simplifies, integrates by parts and extracts the equations
66
+ ([pipeline, stage by stage](https://geomech.readthedocs.io/en/latest/pipeline.html))
67
+ 4. Optionally call `to_standard_form()` for `M(q) a + f + G u = 0`, or
68
+ `linearize()` for the variation-based linearization `M δẋ = A δx + B δu`
69
+ along a trajectory
70
+ 5. Optionally generate NumPy (or CasADi) code for the result with `geomech.codegen`
71
+ (`generate_module`, `lambdify`, `energy_function`; [code generation](https://geomech.readthedocs.io/en/latest/codegen.html));
72
+ `examples/generated/` holds the modules of nine systems
73
+ 6. Optionally simulate a generated model on its manifold with `geomech.simulation`
74
+ (`simulate` with a Lie-group RK4 integrator or `solve_ivp`, energy checks, and `lqr`
75
+ from the linearization; [simulation](https://geomech.readthedocs.io/en/latest/simulation.html),
76
+ `examples/inverted_rigid_pendulum_lqr.py`)
77
+ 7. Optionally draw the simulated system in 3-D and write one self-contained HTML report
78
+ (animation, equations, standard form, verification numbers) with `geomech.viz`
79
+ ([visualization](https://geomech.readthedocs.io/en/latest/visualization.html), `examples/report_*.py`); an agent skill that
80
+ follows this whole procedure is in `skills/geomech/`
81
+
82
+ ## How it works
83
+
84
+ Expressions are **symbolic trees**. Python operators (`*`, `+`, etc.) build
85
+ the tree; transformations rewrite it using `match`/`case` pattern matching.
86
+
87
+ **Node arity:**
88
+
89
+ | Arity | Nodes | Example |
90
+ | ------ | ----- | ------- |
91
+ | N-ary | `Add`, `VAdd`, `MAdd` | `a + b + c` → one node, three children |
92
+ | Binary | `Mul`, `Dot`, `Cross`, `SVMul`, `MVMul`, ... | `Dot(x, y)` → two children |
93
+ | Unary | `Hat`, `Vee`, `Transpose`, `TimeDerivative`, `Variation` | `Hat(x)` → one child |
94
+
95
+ **Sample trees:**
96
+
97
+ `M * (x + y + Cross(m, n))` -- matrix-vector multiply with N-ary addition:
98
+
99
+ ```
100
+ MVMul ← Binary (matrix × vector → vector)
101
+ ╱ ╲
102
+ M VAdd ← N-ary (3 children)
103
+ ╱ | ╲
104
+ x y Cross ← Binary (vector × vector)
105
+ ╱ ╲
106
+ m n
107
+ ```
108
+
109
+ `KE = m * Dot(ẋ, ẋ) * 0.5` -- kinetic energy:
110
+
111
+ ```
112
+ Mul ← Binary (scalar × scalar)
113
+ ╱ ╲
114
+ Mul 0.5
115
+ ╱ ╲
116
+ m Dot ← Binary (vector · vector → scalar)
117
+ ╱ ╲
118
+ d/dt d/dt ← Unary (time derivative)
119
+ | |
120
+ x x ← Vector leaves
121
+ ```
122
+
123
+ **Tree rewrites** -- one function per node class, picked by the class of the node
124
+ (`match`/`case` inside a handler where a rule depends on the operands' classes):
125
+
126
+ ```python
127
+ def _elim_dot(expr): # binary
128
+ l, r = _eliminate(expr.left), _eliminate(expr.right)
129
+ if _is_zero(l) or _is_zero(r):
130
+ return rule("simplify.dot_zero", expr, Zero)
131
+ return Dot(l, r)
132
+
133
+
134
+ def _elim_hat(expr): # unary
135
+ inner = _eliminate(expr.expr)
136
+ if isinstance(inner, Vee) and _is_skew_symmetric(inner.expr):
137
+ return rule("simplify.hat_vee_skew", expr, inner.expr) # Hat(Vee(X)) = X
138
+ return Hat(inner)
139
+
140
+
141
+ _ELIMINATE = {Dot: _elim_dot, Hat: _elim_hat, Add: _elim_add, ...} # a leaf has none
142
+
143
+
144
+ def _eliminate(expr):
145
+ handler = _ELIMINATE.get(type(expr))
146
+ return _canonical_literal(expr) if handler is None else handler(expr)
147
+ ```
148
+
149
+ Nodes are frozen: a rewrite always builds new nodes, and a subtree can be shared by
150
+ several trees.
151
+
152
+ Every rewrite is logged through `rule(name, before, after)`; the names, with the hypothesis
153
+ each rule needs, are listed in the module docstrings, and `GEOMECH_TRACE=1` logs every
154
+ firing.
155
+
156
+ The full pipeline (`compute_eom`), each stage a recursive tree rewrite:
157
+
158
+ δS = δL + δW → manifold rules (δω, δΩ) → `full_simplify` → integration by parts
159
+ (`expand`, `full_simplify`, then per variation vector: `collect` → IBP → `full_simplify`) →
160
+ `expand` → extract the coefficient of each variation vector → `full_simplify`.
161
+
162
+ [Deriving equations](https://geomech.readthedocs.io/en/latest/pipeline.html) has one paragraph per stage;
163
+ `compute_eom(..., trace=EOMTrace())` keeps every stage, `GEOMECH_DEBUG=1` logs them, and
164
+ `GEOMECH_TRACE=1` logs every named rewrite rule that fires
165
+ ([Troubleshooting](https://geomech.readthedocs.io/en/latest/troubleshooting.html)).
166
+
167
+ ## Installation
168
+
169
+ Requires Python >= 3.10.
170
+
171
+ ```bash
172
+ pip install geomech
173
+ pip install "geomech[casadi]" # optional: the CasADi code generation target
174
+ pip install "geomech[sim]" # optional: scipy, for solve_ivp and lqr in geomech.simulation
175
+ ```
176
+
177
+ From a clone, for development: `pip install -e ".[dev]"`.
178
+
179
+ ## Quick start
180
+
181
+ **Point mass** -- derives `m * ddot_x = f - m*g*e3`:
182
+
183
+ ```python
184
+ from geomech import *
185
+ import numpy as np
186
+
187
+ m, g = getScalars('m g', attr=['Constant'])
188
+ e3 = Vector('e3', attr=['Constant'], value=np.array([0., 0., 1]))
189
+ x, f = getVectors(['x', 'f'])
190
+
191
+ v = x.t_diff()
192
+
193
+ KE = m * Dot(v, v) * 0.5
194
+ PE = m * x.dot(g * e3)
195
+ L = KE - PE
196
+
197
+ dW = Dot(x.delta(), f)
198
+
199
+ eqs = compute_eom(L, dW, SystemVariables(vectors=[x]))
200
+ print_eom(eqs)
201
+ ```
202
+
203
+ **Spherical pendulum** on S2:
204
+
205
+ ```python
206
+ m, g, l = getScalars('m g l', attr=['Constant'])
207
+ e3 = Vector('e3', attr=['Constant'], value=np.array([0., 0., 1]))
208
+
209
+ q = S2('q') # attitude on the 2-sphere
210
+ f = Vector('f')
211
+
212
+ x = l * q
213
+ v = x.t_diff()
214
+
215
+ KE = m * Dot(v, v) * 0.5
216
+ PE = m * x.dot(g * e3)
217
+ L = KE - PE
218
+ dW = Dot(q.delta(), f)
219
+
220
+ eqs = compute_eom(L, dW, SystemVariables(vectors=[q]))
221
+ print_eom(eqs)
222
+ ```
223
+
224
+ **Rigid body** on SO(3):
225
+
226
+ ```python
227
+ J = Matrix('J', attr=['Constant', 'SymmetricMatrix'])
228
+ rho = Vector('\\rho', attr=['Constant'])
229
+ m, g = getScalars('m g', attr=['Constant'])
230
+ e3 = Vector('e_3', attr=['Constant'], value=np.array([0., 0., 1]))
231
+
232
+ R = SO3('R') # rotation matrix on SO(3)
233
+ Om = R.get_tangent_vector()
234
+ eta = R.get_variation_vector()
235
+ M = Vector('M') # external torque
236
+
237
+ x = R * rho
238
+ v = x.t_diff()
239
+
240
+ KE = Dot(Om, J * Om) * 0.5 + Dot(v, v) * m * 0.5
241
+ PE = m * g * Dot(x, e3)
242
+ L = KE - PE
243
+ dW = Dot(eta, M)
244
+
245
+ eqs = compute_eom(L, dW, SystemVariables(matrices=[R]))
246
+ print_eom(eqs)
247
+ ```
248
+
249
+ More examples in [`examples/`](https://github.com/vkotaru/geomech/blob/main/examples/).
250
+
251
+ ## Expression types
252
+
253
+ | Type | Description | Constructor |
254
+ | -------- | --------------------------- | -------------------------------- |
255
+ | `Scalar` | Symbolic scalar | `Scalar('m', attr=['Constant'])` |
256
+ | `Vector` | Column vector (default 3x1) | `Vector('x')` |
257
+ | `Matrix` | Square matrix (default 3x3) | `Matrix('J', attr=['Constant'])` |
258
+ | `S2` | Unit vector on the 2-sphere | `S2('q')` |
259
+ | `SO3` | Rotation matrix on SO(3) | `SO3('R')` |
260
+
261
+ Batch constructors: `getScalars('a b c')`, `getVectors(['x', 'y'])`, `getMatrices('M N')`.
262
+
263
+ ## Operations
264
+
265
+ **Arithmetic** -- natural Python operators build expression trees:
266
+
267
+ ```python
268
+ a * b # scalar multiplication
269
+ a * x # scalar-vector multiplication
270
+ M * x # matrix-vector multiplication
271
+ x + y # vector addition
272
+ Dot(x, y) # dot product
273
+ Cross(x, y) # cross product
274
+ Hat(x) # hat map (R^3 -> so(3))
275
+ Vee(M) # vee map (so(3) -> R^3)
276
+ Transpose(x) # transpose (x.T() is the same; Transpose(Transpose(x)) is x)
277
+ R.T() * x # a transposed matrix multiplies like a matrix: R^T x
278
+ x.T() * y # row times column: the inner product x^T y; x * y.T() is the outer product
279
+ x.T() * K * x # a row times a matrix is a row: the quadratic form x^T K x
280
+ # (also in a kinetic energy, v.T() * K * v: collect turns it into Dot(v, K * v) for integration by parts)
281
+ 2 * x, a + 1, 1 - a, -x, x / 2 # numbers on either side (a number adds only to a scalar); / divides by a number only
282
+ ```
283
+
284
+ A mistyped operand raises `ExpressionMismatchError` naming both operands, e.g.
285
+ `Dot cannot combine SCALAR 'm' and VECTOR 'x'`.
286
+
287
+ **Calculus**:
288
+
289
+ ```python
290
+ x.t_diff() # time derivative
291
+ x.t_integrate() # time integral
292
+ x.delta() # variation (delta)
293
+ ```
294
+
295
+ **Transformations**:
296
+
297
+ ```python
298
+ expand(expr) # distribute products over sums
299
+ simplify(expr) # pull scalars, apply identities, eliminate zeros
300
+ full_simplify(expr) # expand + simplify until convergence
301
+ ```
302
+
303
+ **Printing** -- `str(expr)` is the internal form and the equality key (products print
304
+ `*` between their factors: `m*g*Dot(e3,x)`); for reading:
305
+
306
+ ```python
307
+ to_latex(expr) # LaTeX, parenthesised where juxtaposition would be ambiguous
308
+ print_eom(eqs) # one LaTeX integral equation per variable (via to_latex)
309
+ repr(expr) # compact: v:x vector, M:J matrix, S2:q, SO3:R; trailing * = constant;
310
+ # products joined by ·, e.g. m*·g*·<v:e3*, v:x>
311
+ print_tree(expr) # one node per line with every leaf's flags (style="topdown" for a sketch)
312
+ ```
313
+
314
+ ## Package structure
315
+
316
+ ```
317
+ geomech/
318
+ core/
319
+ base/ # Expr types, flags, scalars/vectors/matrices, manifolds
320
+ operations/ # Add, Mul, Dot, Cross, Hat, Variation, TimeDerivative, ...
321
+ transformations/# expand, simplify, pull, vector_rules
322
+ math/ # extract_linear_coeff, collect, integrate_by_parts
323
+ dynamics/ # compute_eom, to_standard_form, linearize, SystemVariables
324
+ codegen/ # StateLayout, to_source, lambdify, generate_module, energy_function
325
+ simulation/ # accelerations, simulate (RKMK4, solve_ivp), Trajectory, lqr
326
+ viz/ # Scene (3-D bodies from expressions), Report (HTML), latex_names
327
+ numeric.py # evaluate, check_eom, finite_difference_check
328
+ utils/ # print_eom, print_tree, render_eom
329
+ ```
330
+
331
+ ## Running tests
332
+
333
+ ```bash
334
+ pip install -e ".[dev]"
335
+ pytest
336
+ ```
337
+
338
+ ## Citation
339
+
340
+ If you use geomech in your research, please cite this package along with corresponding original work, [Global Formulations of Lagrangian and Hamiltonian Dynamics on Manifolds](https://link.springer.com/book/10.1007/978-3-319-56953-6) and [Symbolic Computation of Dynamics on Smooth Manifolds](https://link.springer.com/chapter/10.1007/978-3-030-43089-4_22).
341
+
342
+ ```bibtex
343
+ @software{kotaru2026geomech,
344
+ title={geomech: Symbolic toolbox for geometric mechanics on smooth manifolds},
345
+ author={Kotaru, Prasanth},
346
+ url={https://github.com/vkotaru/geomech},
347
+ year={2026}
348
+ }
349
+ ```
350
+
351
+ ## License
352
+
353
+ BSD 3-Clause. See [LICENSE](LICENSE).
@@ -0,0 +1,311 @@
1
+ # geomech
2
+
3
+ A Python library for the symbolic derivation of equations of motion and their variation-based linearization on smooth manifolds: $R$, $R^3$, $S^2$, $SO(3)$ and their products (e.g. $SE(3)$ as $R^3 \times SO(3)$).
4
+
5
+ This work builds on the methods and math described in
6
+ 1. [Global Formulations of Lagrangian and Hamiltonian Dynamics on Manifolds](https://link.springer.com/book/10.1007/978-3-319-56953-6)
7
+ 2. [Symbolic Computation of Dynamics on Smooth Manifolds](https://link.springer.com/chapter/10.1007/978-3-030-43089-4_22) and its [Scala implementation](https://github.com/HybridRobotics/dynamics_on_manifolds).
8
+
9
+ Documentation: https://geomech.readthedocs.io
10
+
11
+ ## What it does
12
+
13
+ Given a Lagrangian and virtual work, `geomech` automatically derives
14
+ globally valid, singularity-free equations of motion on smooth manifolds
15
+ (SO(3), S2) using Hamilton's principle and variational calculus --
16
+ no Euler angles or quaternions required.
17
+
18
+ The pipeline:
19
+
20
+ 1. Define symbolic scalars, vectors, and matrices
21
+ 2. Build kinetic/potential energy and virtual work expressions
22
+ 3. Call `compute_eom()` -- the library takes the variation of the action, applies the
23
+ manifold kinematics, simplifies, integrates by parts and extracts the equations
24
+ ([pipeline, stage by stage](https://geomech.readthedocs.io/en/latest/pipeline.html))
25
+ 4. Optionally call `to_standard_form()` for `M(q) a + f + G u = 0`, or
26
+ `linearize()` for the variation-based linearization `M δẋ = A δx + B δu`
27
+ along a trajectory
28
+ 5. Optionally generate NumPy (or CasADi) code for the result with `geomech.codegen`
29
+ (`generate_module`, `lambdify`, `energy_function`; [code generation](https://geomech.readthedocs.io/en/latest/codegen.html));
30
+ `examples/generated/` holds the modules of nine systems
31
+ 6. Optionally simulate a generated model on its manifold with `geomech.simulation`
32
+ (`simulate` with a Lie-group RK4 integrator or `solve_ivp`, energy checks, and `lqr`
33
+ from the linearization; [simulation](https://geomech.readthedocs.io/en/latest/simulation.html),
34
+ `examples/inverted_rigid_pendulum_lqr.py`)
35
+ 7. Optionally draw the simulated system in 3-D and write one self-contained HTML report
36
+ (animation, equations, standard form, verification numbers) with `geomech.viz`
37
+ ([visualization](https://geomech.readthedocs.io/en/latest/visualization.html), `examples/report_*.py`); an agent skill that
38
+ follows this whole procedure is in `skills/geomech/`
39
+
40
+ ## How it works
41
+
42
+ Expressions are **symbolic trees**. Python operators (`*`, `+`, etc.) build
43
+ the tree; transformations rewrite it using `match`/`case` pattern matching.
44
+
45
+ **Node arity:**
46
+
47
+ | Arity | Nodes | Example |
48
+ | ------ | ----- | ------- |
49
+ | N-ary | `Add`, `VAdd`, `MAdd` | `a + b + c` → one node, three children |
50
+ | Binary | `Mul`, `Dot`, `Cross`, `SVMul`, `MVMul`, ... | `Dot(x, y)` → two children |
51
+ | Unary | `Hat`, `Vee`, `Transpose`, `TimeDerivative`, `Variation` | `Hat(x)` → one child |
52
+
53
+ **Sample trees:**
54
+
55
+ `M * (x + y + Cross(m, n))` -- matrix-vector multiply with N-ary addition:
56
+
57
+ ```
58
+ MVMul ← Binary (matrix × vector → vector)
59
+ ╱ ╲
60
+ M VAdd ← N-ary (3 children)
61
+ ╱ | ╲
62
+ x y Cross ← Binary (vector × vector)
63
+ ╱ ╲
64
+ m n
65
+ ```
66
+
67
+ `KE = m * Dot(ẋ, ẋ) * 0.5` -- kinetic energy:
68
+
69
+ ```
70
+ Mul ← Binary (scalar × scalar)
71
+ ╱ ╲
72
+ Mul 0.5
73
+ ╱ ╲
74
+ m Dot ← Binary (vector · vector → scalar)
75
+ ╱ ╲
76
+ d/dt d/dt ← Unary (time derivative)
77
+ | |
78
+ x x ← Vector leaves
79
+ ```
80
+
81
+ **Tree rewrites** -- one function per node class, picked by the class of the node
82
+ (`match`/`case` inside a handler where a rule depends on the operands' classes):
83
+
84
+ ```python
85
+ def _elim_dot(expr): # binary
86
+ l, r = _eliminate(expr.left), _eliminate(expr.right)
87
+ if _is_zero(l) or _is_zero(r):
88
+ return rule("simplify.dot_zero", expr, Zero)
89
+ return Dot(l, r)
90
+
91
+
92
+ def _elim_hat(expr): # unary
93
+ inner = _eliminate(expr.expr)
94
+ if isinstance(inner, Vee) and _is_skew_symmetric(inner.expr):
95
+ return rule("simplify.hat_vee_skew", expr, inner.expr) # Hat(Vee(X)) = X
96
+ return Hat(inner)
97
+
98
+
99
+ _ELIMINATE = {Dot: _elim_dot, Hat: _elim_hat, Add: _elim_add, ...} # a leaf has none
100
+
101
+
102
+ def _eliminate(expr):
103
+ handler = _ELIMINATE.get(type(expr))
104
+ return _canonical_literal(expr) if handler is None else handler(expr)
105
+ ```
106
+
107
+ Nodes are frozen: a rewrite always builds new nodes, and a subtree can be shared by
108
+ several trees.
109
+
110
+ Every rewrite is logged through `rule(name, before, after)`; the names, with the hypothesis
111
+ each rule needs, are listed in the module docstrings, and `GEOMECH_TRACE=1` logs every
112
+ firing.
113
+
114
+ The full pipeline (`compute_eom`), each stage a recursive tree rewrite:
115
+
116
+ δS = δL + δW → manifold rules (δω, δΩ) → `full_simplify` → integration by parts
117
+ (`expand`, `full_simplify`, then per variation vector: `collect` → IBP → `full_simplify`) →
118
+ `expand` → extract the coefficient of each variation vector → `full_simplify`.
119
+
120
+ [Deriving equations](https://geomech.readthedocs.io/en/latest/pipeline.html) has one paragraph per stage;
121
+ `compute_eom(..., trace=EOMTrace())` keeps every stage, `GEOMECH_DEBUG=1` logs them, and
122
+ `GEOMECH_TRACE=1` logs every named rewrite rule that fires
123
+ ([Troubleshooting](https://geomech.readthedocs.io/en/latest/troubleshooting.html)).
124
+
125
+ ## Installation
126
+
127
+ Requires Python >= 3.10.
128
+
129
+ ```bash
130
+ pip install geomech
131
+ pip install "geomech[casadi]" # optional: the CasADi code generation target
132
+ pip install "geomech[sim]" # optional: scipy, for solve_ivp and lqr in geomech.simulation
133
+ ```
134
+
135
+ From a clone, for development: `pip install -e ".[dev]"`.
136
+
137
+ ## Quick start
138
+
139
+ **Point mass** -- derives `m * ddot_x = f - m*g*e3`:
140
+
141
+ ```python
142
+ from geomech import *
143
+ import numpy as np
144
+
145
+ m, g = getScalars('m g', attr=['Constant'])
146
+ e3 = Vector('e3', attr=['Constant'], value=np.array([0., 0., 1]))
147
+ x, f = getVectors(['x', 'f'])
148
+
149
+ v = x.t_diff()
150
+
151
+ KE = m * Dot(v, v) * 0.5
152
+ PE = m * x.dot(g * e3)
153
+ L = KE - PE
154
+
155
+ dW = Dot(x.delta(), f)
156
+
157
+ eqs = compute_eom(L, dW, SystemVariables(vectors=[x]))
158
+ print_eom(eqs)
159
+ ```
160
+
161
+ **Spherical pendulum** on S2:
162
+
163
+ ```python
164
+ m, g, l = getScalars('m g l', attr=['Constant'])
165
+ e3 = Vector('e3', attr=['Constant'], value=np.array([0., 0., 1]))
166
+
167
+ q = S2('q') # attitude on the 2-sphere
168
+ f = Vector('f')
169
+
170
+ x = l * q
171
+ v = x.t_diff()
172
+
173
+ KE = m * Dot(v, v) * 0.5
174
+ PE = m * x.dot(g * e3)
175
+ L = KE - PE
176
+ dW = Dot(q.delta(), f)
177
+
178
+ eqs = compute_eom(L, dW, SystemVariables(vectors=[q]))
179
+ print_eom(eqs)
180
+ ```
181
+
182
+ **Rigid body** on SO(3):
183
+
184
+ ```python
185
+ J = Matrix('J', attr=['Constant', 'SymmetricMatrix'])
186
+ rho = Vector('\\rho', attr=['Constant'])
187
+ m, g = getScalars('m g', attr=['Constant'])
188
+ e3 = Vector('e_3', attr=['Constant'], value=np.array([0., 0., 1]))
189
+
190
+ R = SO3('R') # rotation matrix on SO(3)
191
+ Om = R.get_tangent_vector()
192
+ eta = R.get_variation_vector()
193
+ M = Vector('M') # external torque
194
+
195
+ x = R * rho
196
+ v = x.t_diff()
197
+
198
+ KE = Dot(Om, J * Om) * 0.5 + Dot(v, v) * m * 0.5
199
+ PE = m * g * Dot(x, e3)
200
+ L = KE - PE
201
+ dW = Dot(eta, M)
202
+
203
+ eqs = compute_eom(L, dW, SystemVariables(matrices=[R]))
204
+ print_eom(eqs)
205
+ ```
206
+
207
+ More examples in [`examples/`](https://github.com/vkotaru/geomech/blob/main/examples/).
208
+
209
+ ## Expression types
210
+
211
+ | Type | Description | Constructor |
212
+ | -------- | --------------------------- | -------------------------------- |
213
+ | `Scalar` | Symbolic scalar | `Scalar('m', attr=['Constant'])` |
214
+ | `Vector` | Column vector (default 3x1) | `Vector('x')` |
215
+ | `Matrix` | Square matrix (default 3x3) | `Matrix('J', attr=['Constant'])` |
216
+ | `S2` | Unit vector on the 2-sphere | `S2('q')` |
217
+ | `SO3` | Rotation matrix on SO(3) | `SO3('R')` |
218
+
219
+ Batch constructors: `getScalars('a b c')`, `getVectors(['x', 'y'])`, `getMatrices('M N')`.
220
+
221
+ ## Operations
222
+
223
+ **Arithmetic** -- natural Python operators build expression trees:
224
+
225
+ ```python
226
+ a * b # scalar multiplication
227
+ a * x # scalar-vector multiplication
228
+ M * x # matrix-vector multiplication
229
+ x + y # vector addition
230
+ Dot(x, y) # dot product
231
+ Cross(x, y) # cross product
232
+ Hat(x) # hat map (R^3 -> so(3))
233
+ Vee(M) # vee map (so(3) -> R^3)
234
+ Transpose(x) # transpose (x.T() is the same; Transpose(Transpose(x)) is x)
235
+ R.T() * x # a transposed matrix multiplies like a matrix: R^T x
236
+ x.T() * y # row times column: the inner product x^T y; x * y.T() is the outer product
237
+ x.T() * K * x # a row times a matrix is a row: the quadratic form x^T K x
238
+ # (also in a kinetic energy, v.T() * K * v: collect turns it into Dot(v, K * v) for integration by parts)
239
+ 2 * x, a + 1, 1 - a, -x, x / 2 # numbers on either side (a number adds only to a scalar); / divides by a number only
240
+ ```
241
+
242
+ A mistyped operand raises `ExpressionMismatchError` naming both operands, e.g.
243
+ `Dot cannot combine SCALAR 'm' and VECTOR 'x'`.
244
+
245
+ **Calculus**:
246
+
247
+ ```python
248
+ x.t_diff() # time derivative
249
+ x.t_integrate() # time integral
250
+ x.delta() # variation (delta)
251
+ ```
252
+
253
+ **Transformations**:
254
+
255
+ ```python
256
+ expand(expr) # distribute products over sums
257
+ simplify(expr) # pull scalars, apply identities, eliminate zeros
258
+ full_simplify(expr) # expand + simplify until convergence
259
+ ```
260
+
261
+ **Printing** -- `str(expr)` is the internal form and the equality key (products print
262
+ `*` between their factors: `m*g*Dot(e3,x)`); for reading:
263
+
264
+ ```python
265
+ to_latex(expr) # LaTeX, parenthesised where juxtaposition would be ambiguous
266
+ print_eom(eqs) # one LaTeX integral equation per variable (via to_latex)
267
+ repr(expr) # compact: v:x vector, M:J matrix, S2:q, SO3:R; trailing * = constant;
268
+ # products joined by ·, e.g. m*·g*·<v:e3*, v:x>
269
+ print_tree(expr) # one node per line with every leaf's flags (style="topdown" for a sketch)
270
+ ```
271
+
272
+ ## Package structure
273
+
274
+ ```
275
+ geomech/
276
+ core/
277
+ base/ # Expr types, flags, scalars/vectors/matrices, manifolds
278
+ operations/ # Add, Mul, Dot, Cross, Hat, Variation, TimeDerivative, ...
279
+ transformations/# expand, simplify, pull, vector_rules
280
+ math/ # extract_linear_coeff, collect, integrate_by_parts
281
+ dynamics/ # compute_eom, to_standard_form, linearize, SystemVariables
282
+ codegen/ # StateLayout, to_source, lambdify, generate_module, energy_function
283
+ simulation/ # accelerations, simulate (RKMK4, solve_ivp), Trajectory, lqr
284
+ viz/ # Scene (3-D bodies from expressions), Report (HTML), latex_names
285
+ numeric.py # evaluate, check_eom, finite_difference_check
286
+ utils/ # print_eom, print_tree, render_eom
287
+ ```
288
+
289
+ ## Running tests
290
+
291
+ ```bash
292
+ pip install -e ".[dev]"
293
+ pytest
294
+ ```
295
+
296
+ ## Citation
297
+
298
+ If you use geomech in your research, please cite this package along with corresponding original work, [Global Formulations of Lagrangian and Hamiltonian Dynamics on Manifolds](https://link.springer.com/book/10.1007/978-3-319-56953-6) and [Symbolic Computation of Dynamics on Smooth Manifolds](https://link.springer.com/chapter/10.1007/978-3-030-43089-4_22).
299
+
300
+ ```bibtex
301
+ @software{kotaru2026geomech,
302
+ title={geomech: Symbolic toolbox for geometric mechanics on smooth manifolds},
303
+ author={Kotaru, Prasanth},
304
+ url={https://github.com/vkotaru/geomech},
305
+ year={2026}
306
+ }
307
+ ```
308
+
309
+ ## License
310
+
311
+ BSD 3-Clause. See [LICENSE](LICENSE).