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.
- geomech-0.2.0/LICENSE +29 -0
- geomech-0.2.0/PKG-INFO +353 -0
- geomech-0.2.0/README.md +311 -0
- geomech-0.2.0/geomech/__init__.py +128 -0
- geomech-0.2.0/geomech/codegen/__init__.py +29 -0
- geomech-0.2.0/geomech/codegen/casadi_target.py +171 -0
- geomech-0.2.0/geomech/codegen/emitter.py +298 -0
- geomech-0.2.0/geomech/codegen/energy.py +111 -0
- geomech-0.2.0/geomech/codegen/layout.py +553 -0
- geomech-0.2.0/geomech/codegen/module.py +403 -0
- geomech-0.2.0/geomech/codegen/numpy_target.py +186 -0
- geomech-0.2.0/geomech/codegen/source.py +159 -0
- geomech-0.2.0/geomech/core/__init__.py +0 -0
- geomech-0.2.0/geomech/core/base/__init__.py +27 -0
- geomech-0.2.0/geomech/core/base/constants.py +58 -0
- geomech-0.2.0/geomech/core/base/expressions.py +814 -0
- geomech-0.2.0/geomech/core/base/types.py +96 -0
- geomech-0.2.0/geomech/core/math/__init__.py +16 -0
- geomech-0.2.0/geomech/core/math/collecting.py +274 -0
- geomech-0.2.0/geomech/core/math/extract.py +721 -0
- geomech-0.2.0/geomech/core/math/ibp.py +221 -0
- geomech-0.2.0/geomech/core/operations/__init__.py +24 -0
- geomech-0.2.0/geomech/core/operations/addition.py +124 -0
- geomech-0.2.0/geomech/core/operations/calculus.py +115 -0
- geomech-0.2.0/geomech/core/operations/geometry.py +257 -0
- geomech-0.2.0/geomech/core/operations/mixins.py +201 -0
- geomech-0.2.0/geomech/core/operations/multiplication.py +274 -0
- geomech-0.2.0/geomech/core/transformations/__init__.py +29 -0
- geomech-0.2.0/geomech/core/transformations/expansion.py +238 -0
- geomech-0.2.0/geomech/core/transformations/manifold_rules.py +118 -0
- geomech-0.2.0/geomech/core/transformations/pulling.py +237 -0
- geomech-0.2.0/geomech/core/transformations/simplification.py +732 -0
- geomech-0.2.0/geomech/core/transformations/utils.py +118 -0
- geomech-0.2.0/geomech/core/transformations/vector_identities.py +207 -0
- geomech-0.2.0/geomech/dynamics/__init__.py +14 -0
- geomech-0.2.0/geomech/dynamics/eom.py +1062 -0
- geomech-0.2.0/geomech/dynamics/standard_form.py +251 -0
- geomech-0.2.0/geomech/dynamics/variables.py +41 -0
- geomech-0.2.0/geomech/dynamics/vbl.py +654 -0
- geomech-0.2.0/geomech/numeric.py +893 -0
- geomech-0.2.0/geomech/simulation/__init__.py +24 -0
- geomech-0.2.0/geomech/simulation/control.py +358 -0
- geomech-0.2.0/geomech/simulation/integrators.py +469 -0
- geomech-0.2.0/geomech/simulation/model.py +270 -0
- geomech-0.2.0/geomech/systems/__init__.py +98 -0
- geomech-0.2.0/geomech/systems/_base.py +392 -0
- geomech-0.2.0/geomech/systems/aerial.py +512 -0
- geomech-0.2.0/geomech/systems/rigid.py +408 -0
- geomech-0.2.0/geomech/systems/spheres.py +381 -0
- geomech-0.2.0/geomech/utils/__init__.py +18 -0
- geomech-0.2.0/geomech/utils/errors.py +107 -0
- geomech-0.2.0/geomech/utils/invariants.py +267 -0
- geomech-0.2.0/geomech/utils/printing.py +998 -0
- geomech-0.2.0/geomech/utils/trace.py +417 -0
- geomech-0.2.0/geomech/viz/__init__.py +41 -0
- geomech-0.2.0/geomech/viz/latex.py +168 -0
- geomech-0.2.0/geomech/viz/report.py +750 -0
- geomech-0.2.0/geomech/viz/scene.py +691 -0
- geomech-0.2.0/geomech/viz/template.html +688 -0
- geomech-0.2.0/geomech.egg-info/PKG-INFO +353 -0
- geomech-0.2.0/geomech.egg-info/SOURCES.txt +151 -0
- geomech-0.2.0/geomech.egg-info/dependency_links.txt +1 -0
- geomech-0.2.0/geomech.egg-info/requires.txt +20 -0
- geomech-0.2.0/geomech.egg-info/top_level.txt +1 -0
- geomech-0.2.0/pyproject.toml +96 -0
- geomech-0.2.0/setup.cfg +4 -0
- geomech-0.2.0/tests/test_addition.py +523 -0
- geomech-0.2.0/tests/test_calculus.py +372 -0
- geomech-0.2.0/tests/test_codegen_adversarial.py +643 -0
- geomech-0.2.0/tests/test_codegen_arguments.py +331 -0
- geomech-0.2.0/tests/test_codegen_casadi.py +79 -0
- geomech-0.2.0/tests/test_codegen_energy.py +149 -0
- geomech-0.2.0/tests/test_codegen_generated.py +152 -0
- geomech-0.2.0/tests/test_codegen_layout.py +232 -0
- geomech-0.2.0/tests/test_codegen_numeric.py +133 -0
- geomech-0.2.0/tests/test_codegen_simulation_integration.py +384 -0
- geomech-0.2.0/tests/test_codegen_translation.py +148 -0
- geomech-0.2.0/tests/test_collect.py +264 -0
- geomech-0.2.0/tests/test_collect_fix.py +222 -0
- geomech-0.2.0/tests/test_collect_termination.py +66 -0
- geomech-0.2.0/tests/test_eom.py +188 -0
- geomech-0.2.0/tests/test_eom_oracle.py +278 -0
- geomech-0.2.0/tests/test_error_context.py +263 -0
- geomech-0.2.0/tests/test_error_messages.py +277 -0
- geomech-0.2.0/tests/test_expand.py +428 -0
- geomech-0.2.0/tests/test_expand_vvmul.py +147 -0
- geomech-0.2.0/tests/test_extract.py +1180 -0
- geomech-0.2.0/tests/test_extract_fix.py +197 -0
- geomech-0.2.0/tests/test_geometry.py +627 -0
- geomech-0.2.0/tests/test_ibp.py +168 -0
- geomech-0.2.0/tests/test_ibp_prefactor.py +126 -0
- geomech-0.2.0/tests/test_input_name_clash.py +153 -0
- geomech-0.2.0/tests/test_input_validation_shared.py +269 -0
- geomech-0.2.0/tests/test_inputs_adversarial.py +305 -0
- geomech-0.2.0/tests/test_invariants.py +278 -0
- geomech-0.2.0/tests/test_latex_precedence.py +179 -0
- geomech-0.2.0/tests/test_logging.py +126 -0
- geomech-0.2.0/tests/test_manifold_pipelines.py +846 -0
- geomech-0.2.0/tests/test_manifold_rules.py +338 -0
- geomech-0.2.0/tests/test_manifolds.py +98 -0
- geomech-0.2.0/tests/test_matrices.py +225 -0
- geomech-0.2.0/tests/test_mixins.py +362 -0
- geomech-0.2.0/tests/test_multiplication.py +804 -0
- geomech-0.2.0/tests/test_numeric.py +299 -0
- geomech-0.2.0/tests/test_operations.py +209 -0
- geomech-0.2.0/tests/test_operators.py +351 -0
- geomech-0.2.0/tests/test_phase1_adversarial.py +669 -0
- geomech-0.2.0/tests/test_phase2_adversarial.py +720 -0
- geomech-0.2.0/tests/test_phase3_adversarial.py +369 -0
- geomech-0.2.0/tests/test_phase3_fixes.py +111 -0
- geomech-0.2.0/tests/test_phase4_adversarial.py +1023 -0
- geomech-0.2.0/tests/test_phase5a_adversarial.py +701 -0
- geomech-0.2.0/tests/test_phase5a_simplifier.py +276 -0
- geomech-0.2.0/tests/test_phase5b_adversarial.py +412 -0
- geomech-0.2.0/tests/test_phase5b_structure.py +389 -0
- geomech-0.2.0/tests/test_printing.py +105 -0
- geomech-0.2.0/tests/test_pull.py +329 -0
- geomech-0.2.0/tests/test_repr_debug.py +153 -0
- geomech-0.2.0/tests/test_row_forms.py +133 -0
- geomech-0.2.0/tests/test_rule_coverage.py +110 -0
- geomech-0.2.0/tests/test_rule_names.py +216 -0
- geomech-0.2.0/tests/test_s2_variation_fix.py +239 -0
- geomech-0.2.0/tests/test_scalar_adversarial.py +1002 -0
- geomech-0.2.0/tests/test_scalars.py +164 -0
- geomech-0.2.0/tests/test_simplification.py +86 -0
- geomech-0.2.0/tests/test_simplify.py +786 -0
- geomech-0.2.0/tests/test_simplify_numbers.py +97 -0
- geomech-0.2.0/tests/test_simulation.py +406 -0
- geomech-0.2.0/tests/test_simulation_adversarial.py +639 -0
- geomech-0.2.0/tests/test_simulation_conservation.py +168 -0
- geomech-0.2.0/tests/test_simulation_lqr.py +196 -0
- geomech-0.2.0/tests/test_standard_form.py +435 -0
- geomech-0.2.0/tests/test_systems.py +313 -0
- geomech-0.2.0/tests/test_systems_book.py +265 -0
- geomech-0.2.0/tests/test_systems_golden.py +115 -0
- geomech-0.2.0/tests/test_systems_physics.py +358 -0
- geomech-0.2.0/tests/test_systems_pipeline.py +138 -0
- geomech-0.2.0/tests/test_trace.py +207 -0
- geomech-0.2.0/tests/test_transpose.py +52 -0
- geomech-0.2.0/tests/test_troubleshooting_doc.py +107 -0
- geomech-0.2.0/tests/test_utils.py +64 -0
- geomech-0.2.0/tests/test_validation.py +652 -0
- geomech-0.2.0/tests/test_vbl.py +897 -0
- geomech-0.2.0/tests/test_vbl_adversarial.py +748 -0
- geomech-0.2.0/tests/test_vbl_validation.py +138 -0
- geomech-0.2.0/tests/test_vector_rules.py +225 -0
- geomech-0.2.0/tests/test_vectors.py +221 -0
- geomech-0.2.0/tests/test_version.py +26 -0
- geomech-0.2.0/tests/test_viz.py +503 -0
- geomech-0.2.0/tests/test_viz_adversarial.py +856 -0
- geomech-0.2.0/tests/test_viz_final.py +121 -0
- geomech-0.2.0/tests/test_viz_fixes.py +618 -0
- 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).
|
geomech-0.2.0/README.md
ADDED
|
@@ -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).
|