geomech 0.2.0__py3-none-any.whl

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 (61) hide show
  1. geomech/__init__.py +128 -0
  2. geomech/codegen/__init__.py +29 -0
  3. geomech/codegen/casadi_target.py +171 -0
  4. geomech/codegen/emitter.py +298 -0
  5. geomech/codegen/energy.py +111 -0
  6. geomech/codegen/layout.py +553 -0
  7. geomech/codegen/module.py +403 -0
  8. geomech/codegen/numpy_target.py +186 -0
  9. geomech/codegen/source.py +159 -0
  10. geomech/core/__init__.py +0 -0
  11. geomech/core/base/__init__.py +27 -0
  12. geomech/core/base/constants.py +58 -0
  13. geomech/core/base/expressions.py +814 -0
  14. geomech/core/base/types.py +96 -0
  15. geomech/core/math/__init__.py +16 -0
  16. geomech/core/math/collecting.py +274 -0
  17. geomech/core/math/extract.py +721 -0
  18. geomech/core/math/ibp.py +221 -0
  19. geomech/core/operations/__init__.py +24 -0
  20. geomech/core/operations/addition.py +124 -0
  21. geomech/core/operations/calculus.py +115 -0
  22. geomech/core/operations/geometry.py +257 -0
  23. geomech/core/operations/mixins.py +201 -0
  24. geomech/core/operations/multiplication.py +274 -0
  25. geomech/core/transformations/__init__.py +29 -0
  26. geomech/core/transformations/expansion.py +238 -0
  27. geomech/core/transformations/manifold_rules.py +118 -0
  28. geomech/core/transformations/pulling.py +237 -0
  29. geomech/core/transformations/simplification.py +732 -0
  30. geomech/core/transformations/utils.py +118 -0
  31. geomech/core/transformations/vector_identities.py +207 -0
  32. geomech/dynamics/__init__.py +14 -0
  33. geomech/dynamics/eom.py +1062 -0
  34. geomech/dynamics/standard_form.py +251 -0
  35. geomech/dynamics/variables.py +41 -0
  36. geomech/dynamics/vbl.py +654 -0
  37. geomech/numeric.py +893 -0
  38. geomech/simulation/__init__.py +24 -0
  39. geomech/simulation/control.py +358 -0
  40. geomech/simulation/integrators.py +469 -0
  41. geomech/simulation/model.py +270 -0
  42. geomech/systems/__init__.py +98 -0
  43. geomech/systems/_base.py +392 -0
  44. geomech/systems/aerial.py +512 -0
  45. geomech/systems/rigid.py +408 -0
  46. geomech/systems/spheres.py +381 -0
  47. geomech/utils/__init__.py +18 -0
  48. geomech/utils/errors.py +107 -0
  49. geomech/utils/invariants.py +267 -0
  50. geomech/utils/printing.py +998 -0
  51. geomech/utils/trace.py +417 -0
  52. geomech/viz/__init__.py +41 -0
  53. geomech/viz/latex.py +168 -0
  54. geomech/viz/report.py +750 -0
  55. geomech/viz/scene.py +691 -0
  56. geomech/viz/template.html +688 -0
  57. geomech-0.2.0.dist-info/METADATA +353 -0
  58. geomech-0.2.0.dist-info/RECORD +61 -0
  59. geomech-0.2.0.dist-info/WHEEL +5 -0
  60. geomech-0.2.0.dist-info/licenses/LICENSE +29 -0
  61. geomech-0.2.0.dist-info/top_level.txt +1 -0
geomech/__init__.py ADDED
@@ -0,0 +1,128 @@
1
+ """
2
+ geomech is a python library for symbolic computation of Dynamics on Manifolds.
3
+ """
4
+
5
+ import sys
6
+
7
+ if sys.version_info < (3, 10):
8
+ raise ImportError("Python version >= 3.10 is required")
9
+ del sys
10
+
11
+ from importlib.metadata import PackageNotFoundError as _PackageNotFoundError
12
+ from importlib.metadata import version as _version
13
+
14
+ try:
15
+ __version__ = _version("geomech")
16
+ except _PackageNotFoundError: # running from a source tree that is not installed
17
+ __version__ = "unknown"
18
+ del _version, _PackageNotFoundError
19
+
20
+ # --- Layer 1: types & leaf nodes ---
21
+ from geomech.core.base import (
22
+ S2,
23
+ SO3,
24
+ TS2,
25
+ TSO3,
26
+ Expr,
27
+ ExprFlags,
28
+ ExprType,
29
+ I,
30
+ IdentityMatrix,
31
+ ManifoldInfo,
32
+ Matrix,
33
+ MatrixExpr,
34
+ Number,
35
+ O,
36
+ One,
37
+ Scalar,
38
+ ScalarExpr,
39
+ SkewSymmMatrix,
40
+ Vector,
41
+ VectorExpr,
42
+ Zero,
43
+ ZeroMatrix,
44
+ ZeroVector,
45
+ getMatrices,
46
+ getScalars,
47
+ getVectors,
48
+ )
49
+
50
+ # --- Layer 4: symbolic math ---
51
+ from geomech.core.math.collecting import collect
52
+ from geomech.core.math.extract import extract_linear_coeff
53
+ from geomech.core.math.ibp import integrate_by_parts
54
+
55
+ # --- Layer 2: expression tree nodes ---
56
+ from geomech.core.operations import (
57
+ Add,
58
+ Cross,
59
+ Dot,
60
+ Hat,
61
+ MAdd,
62
+ MMMul,
63
+ Mul,
64
+ MVMul,
65
+ SMMul,
66
+ SVMul,
67
+ TimeDerivative,
68
+ TimeIntegral,
69
+ Transpose,
70
+ VAdd,
71
+ Variation,
72
+ Vee,
73
+ VVMul,
74
+ )
75
+
76
+ # --- Layer 3: algorithms ---
77
+ from geomech.core.transformations.expansion import expand
78
+ from geomech.core.transformations.manifold_rules import apply_manifold_rules
79
+ from geomech.core.transformations.pulling import pull
80
+ from geomech.core.transformations.simplification import full_simplify, simplify
81
+ from geomech.core.transformations.vector_identities import vector_rules
82
+
83
+ # --- Layer 5: dynamics pipeline ---
84
+ from geomech.dynamics import (
85
+ LinearizedSystem,
86
+ StandardFormEquation,
87
+ SystemVariables,
88
+ compute_eom,
89
+ linearize,
90
+ separate_variations,
91
+ to_standard_form,
92
+ )
93
+
94
+ # --- Numeric oracle (imports only layers 1-2 itself, so it may sit anywhere after them) ---
95
+ from geomech.numeric import (
96
+ Evaluator,
97
+ assert_eom,
98
+ check_eom,
99
+ evaluate,
100
+ finite_difference_check,
101
+ )
102
+
103
+ # --- Utilities ---
104
+ from geomech.utils import (
105
+ display_eom,
106
+ display_latex,
107
+ display_standard_form,
108
+ eom_to_latex,
109
+ print_eom,
110
+ print_tree,
111
+ render_eom,
112
+ to_latex,
113
+ tree_str,
114
+ )
115
+ from geomech.utils.errors import (
116
+ AlgebraicError,
117
+ ExpressionMismatchError,
118
+ InvariantError,
119
+ NonLinearError,
120
+ SizeMismatchError,
121
+ )
122
+ from geomech.utils.invariants import check_tree
123
+ from geomech.utils.trace import EOMTrace, LinearizationTrace, set_trace
124
+ from geomech.utils.trace import configure_from_env as _configure_from_env
125
+
126
+ # GEOMECH_DEBUG=1: the "geomech" logger at DEBUG and check_tree at pipeline entry/exit;
127
+ # GEOMECH_TRACE=1: log every named rewrite rule (see geomech/utils/trace.py)
128
+ _configure_from_env()
@@ -0,0 +1,29 @@
1
+ """Code generation: numeric functions from the symbolic output of the pipeline.
2
+
3
+ ``StateLayout`` maps the symbols of a system to flat arrays; ``to_source``/``lambdify``
4
+ turn one expression into Python source or a callable; ``generate_module`` writes a module
5
+ with the blocks of a ``compute_eom``, ``to_standard_form`` or ``linearize`` result;
6
+ ``energy_function`` generates the total energy of a Lagrangian. Two targets: ``"numpy"``
7
+ (default, no dependency beyond numpy) and ``"casadi"`` (optional extra
8
+ ``geomech[casadi]``; imported only when that target is used). See ``docs/codegen.md``.
9
+ """
10
+
11
+ from geomech.codegen.energy import energy_expr, energy_function, energy_source, momenta
12
+ from geomech.codegen.layout import Slot, StateLayout, collect_params
13
+ from geomech.codegen.module import generate_module, load_module, module_source
14
+ from geomech.codegen.source import lambdify, to_source
15
+
16
+ __all__ = [
17
+ "StateLayout",
18
+ "Slot",
19
+ "collect_params",
20
+ "to_source",
21
+ "lambdify",
22
+ "generate_module",
23
+ "module_source",
24
+ "load_module",
25
+ "energy_expr",
26
+ "energy_function",
27
+ "energy_source",
28
+ "momenta",
29
+ ]
@@ -0,0 +1,171 @@
1
+ """The CasADi target: the NumPy translation table written with ``casadi`` operations.
2
+
3
+ Vectors are ``3×1`` columns, matrices ``3×3``; ``ca.mtimes`` for matrix products,
4
+ ``ca.dot``, ``ca.cross``, ``ca.vertcat``/``ca.horzcat`` for ``_hat``/``_vee`` and the block
5
+ assembly. The generated functions accept NumPy arrays or CasADi expressions (``SX``,
6
+ ``MX``, ``DM``) for ``state``, ``a``, ``u`` and the values of ``p``, and return CasADi
7
+ objects (``DM`` for numeric arguments), so ``ca.Function`` and ``ca.jacobian`` apply to
8
+ them directly. The generated module imports ``casadi`` (extra ``geomech[casadi]``);
9
+ ``geomech`` itself does not.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from geomech.codegen.emitter import Code
15
+ from geomech.codegen.layout import Slot
16
+ from geomech.codegen.numpy_target import NumpyTarget
17
+
18
+ __all__ = ["CasadiTarget", "require_casadi"]
19
+
20
+ _HELPERS = """
21
+ def _is_ca(x):
22
+ return isinstance(x, (ca.SX, ca.MX, ca.DM))
23
+
24
+
25
+ def _arr(x, n, name):
26
+ if x is None:
27
+ raise ValueError(f"{name} must have {n} entries, got None ({name} is required here)")
28
+ if not _is_ca(x):
29
+ x = ca.DM(np.asarray(x, dtype=float).reshape(-1, 1))
30
+ if x.numel() != n:
31
+ raise ValueError(f"{name} must have {n} entries, got {x.numel()}")
32
+ return ca.reshape(x, n, 1)
33
+
34
+
35
+ def _scalar(x, name):
36
+ if _is_ca(x):
37
+ if x.numel() != 1:
38
+ raise ValueError(f"parameter {name!r} is a scalar, got {x.numel()} entries")
39
+ return ca.reshape(x, 1, 1)
40
+ x = np.asarray(x, dtype=float)
41
+ if x.size != 1:
42
+ raise ValueError(f"parameter {name!r} is a scalar, got {x.size} entries")
43
+ return float(x.reshape(()))
44
+
45
+
46
+ def _vec(x):
47
+ return _arr(x, 3, "parameter")
48
+
49
+
50
+ def _mat(x):
51
+ if _is_ca(x) and x.shape == (3, 3):
52
+ return x
53
+ if _is_ca(x):
54
+ return ca.reshape(x, 3, 3).T
55
+ return ca.DM(np.asarray(x, dtype=float).reshape(3, 3))
56
+
57
+
58
+ def _hat(v):
59
+ return ca.vertcat(
60
+ ca.horzcat(0, -v[2], v[1]),
61
+ ca.horzcat(v[2], 0, -v[0]),
62
+ ca.horzcat(-v[1], v[0], 0),
63
+ )
64
+
65
+
66
+ def _vee(M):
67
+ return ca.vertcat(M[2, 1], M[0, 2], M[1, 0])
68
+
69
+
70
+ def _check_params(p, names):
71
+ p = {} if p is None else p
72
+ missing = [k for k in names if k not in p]
73
+ if missing:
74
+ raise KeyError(f"missing parameter {missing[0]!r} (expected {list(names)})")
75
+ extra = [k for k in p if k not in names]
76
+ if extra:
77
+ raise ValueError(f"unknown parameter {extra[0]!r} (expected {list(names)})")
78
+ """
79
+
80
+
81
+ def require_casadi():
82
+ """Import ``casadi`` or raise ``ImportError`` saying how to install it."""
83
+ try:
84
+ import casadi
85
+ except ImportError as err: # pragma: no cover - depends on the environment
86
+ raise ImportError(
87
+ "geomech.codegen: the CasADi target needs the optional dependency 'casadi';"
88
+ " install it with `pip install geomech[casadi]` (or `pip install casadi`)"
89
+ ) from err
90
+ return casadi
91
+
92
+
93
+ class CasadiTarget(NumpyTarget):
94
+ """Writes each operation of the translation table as CasADi source."""
95
+
96
+ name = "casadi"
97
+ imports = ("import casadi as ca", "import numpy as np")
98
+ helpers = _HELPERS
99
+
100
+ def literal(self, node) -> Code:
101
+ code = super().literal(node).text
102
+ code = {
103
+ "np.zeros((3, 3))": "ca.DM.zeros(3, 3)",
104
+ "np.eye(3)": "ca.DM.eye(3)",
105
+ "np.zeros(3)": "ca.DM.zeros(3, 1)",
106
+ "np.inf": "ca.inf",
107
+ "-np.inf": "-ca.inf",
108
+ }.get(code, code)
109
+ return Code(code, atomic=True)
110
+
111
+ def unpack(self, slot: Slot) -> str:
112
+ if slot.array == "p":
113
+ return super().unpack(slot)
114
+ if slot.shape == (3, 3):
115
+ return f"ca.reshape({slot.array}[{slot.start}:{slot.stop}], 3, 3).T"
116
+ return super().unpack(slot)
117
+
118
+ def matmul(self, a: Code, b: Code) -> Code:
119
+ return Code(f"ca.mtimes({a.text}, {b.text})", atomic=True)
120
+
121
+ matvec = matmul
122
+
123
+ def dot(self, a: Code, b: Code) -> Code:
124
+ return Code(f"ca.dot({a.text}, {b.text})", atomic=True)
125
+
126
+ def outer(self, a: Code, b: Code) -> Code:
127
+ return Code(f"ca.mtimes({a.text}, {b.operand()}.T)", atomic=True)
128
+
129
+ def cross(self, a: Code, b: Code) -> Code:
130
+ return Code(f"ca.cross({a.text}, {b.text})", atomic=True)
131
+
132
+ def flatten_matrix(self, a: Code) -> Code:
133
+ return Code(f"ca.reshape({a.operand()}.T, 9, 1)", atomic=True)
134
+
135
+ def stack(self, n: int, parts) -> list[str]:
136
+ if not parts:
137
+ return [f"return ca.DM.zeros({n}, 1)"]
138
+ lines, items, pos = [], [], 0
139
+ for start, size, code in parts:
140
+ if start > pos:
141
+ items.append(f"ca.DM.zeros({start - pos}, 1)")
142
+ items.append(code.text)
143
+ pos = start + size
144
+ if pos < n:
145
+ items.append(f"ca.DM.zeros({n - pos}, 1)")
146
+ lines.append("return ca.vertcat(")
147
+ lines += [f" {item}," for item in items]
148
+ return lines + [")"]
149
+
150
+ def block_matrix(self, row_spans, col_spans, blocks) -> list[str]:
151
+ """``ca.vertcat`` of one ``ca.horzcat`` per block row; a missing block is a zero
152
+ ``DM``, and a scalar row's 3-vector block is transposed to ``1×3``."""
153
+ if not col_spans or not row_spans:
154
+ n_rows, n_cols = sum(n for _, n in row_spans), sum(n for _, n in col_spans)
155
+ return [f"return ca.DM.zeros({n_rows}, {n_cols})"]
156
+ lines = []
157
+ for i, (_, nr) in enumerate(row_spans):
158
+ items = []
159
+ for j, (_, nc) in enumerate(col_spans):
160
+ code = blocks.get((i, j))
161
+ if code is None:
162
+ items.append(f"ca.DM.zeros({nr}, {nc})")
163
+ elif nr == 1 and nc == 3:
164
+ items.append(f"{code.operand()}.T")
165
+ else:
166
+ items.append(code.text)
167
+ lines.append(f"_r{i} = ca.horzcat(")
168
+ lines += [f" {item}," for item in items]
169
+ lines.append(")")
170
+ rows = ", ".join(f"_r{i}" for i in range(len(row_spans)))
171
+ return lines + [f"return ca.vertcat({rows})"]
@@ -0,0 +1,298 @@
1
+ """Translate expression trees into the body of a generated Python function.
2
+
3
+ ``Emitter`` walks a tree once per node class (the same ``match``/``case`` dispatch as the
4
+ rest of the engine) and produces Python source through a *target* (``NumpyTarget`` or
5
+ ``CasadiTarget``) that knows how to write each operation. Shared by both targets:
6
+
7
+ - **Leaves** are resolved through the ``StateLayout``: configuration, velocity,
8
+ acceleration, input and parameter symbols are bound once at the top of the function,
9
+ numeric literals and the zero/identity constants are inlined. Anything else raises
10
+ ``ValueError`` naming the leaf (the codegen analogue of the stray-symbol check).
11
+ - **Rows**: there is one ``VECTOR`` type; every vector, row or column, is generated as the
12
+ same 3-vector, and the row/column structure (``is_row_vector``) only decides between the
13
+ inner and the outer product of a ``VVMul`` and the side of an ``MVMul`` (``vᵀM`` is
14
+ ``Mᵀv``).
15
+ - **Common subexpressions**: a subtree that occurs more than once is computed once into a
16
+ local ``_tN``. Subtrees are identified structurally: a leaf by its class and ``str``
17
+ (the engine's identity), an operation by its class and the identities of its operands.
18
+ The identity of each node object is computed once per emission pass (a dict keyed by
19
+ ``id(node)``), so generation is linear in the number of distinct node objects, also on
20
+ trees that share subtrees, whose ``str`` grows exponentially with depth. A long
21
+ expression is also split into locals so that every generated line stays short (and
22
+ ruff-clean).
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ from collections import Counter
29
+ from dataclasses import dataclass
30
+ from typing import Any, Iterable
31
+
32
+ from geomech.codegen.layout import Slot, StateLayout, base_symbol, is_literal_leaf
33
+ from geomech.core.base.expressions import TS2, TSO3, Matrix, Scalar, Vector
34
+ from geomech.core.base.types import ExprType
35
+ from geomech.core.operations.addition import Add, MAdd, VAdd
36
+ from geomech.core.operations.calculus import TimeDerivative, TimeIntegral, Variation
37
+ from geomech.core.operations.geometry import Cross, Dot, Hat, Transpose, Vee, is_row_vector
38
+ from geomech.core.operations.multiplication import MMMul, Mul, MVMul, SMMul, SVMul, VVMul
39
+
40
+ __all__ = ["ADD", "ALL_KINDS", "ATOM", "MUL", "Code", "Emitter", "pystr"]
41
+
42
+ #: Length above which a generated expression is moved into a local of its own.
43
+ MAX_INLINE = 72
44
+ #: Length above which an operand of another operation is moved into a local first.
45
+ MAX_OPERAND = 30
46
+
47
+ ALL_KINDS = frozenset({"configuration", "velocity", "acceleration", "input", "param"})
48
+
49
+
50
+ def pystr(s: str) -> str:
51
+ """A double-quoted Python string literal (the form ruff format keeps)."""
52
+ return json.dumps(s)
53
+
54
+
55
+ #: Operator precedence of generated code: a sum, a product (``*``/``@``), an atom.
56
+ ADD, MUL, ATOM = 1, 2, 3
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class Code:
61
+ """A generated expression: its source and its precedence (``ADD``, ``MUL``, ``ATOM``),
62
+ which decides where it needs parentheses as an operand."""
63
+
64
+ text: str
65
+ atomic: bool = False
66
+ prec: int = ADD
67
+
68
+ def __post_init__(self):
69
+ if self.atomic:
70
+ object.__setattr__(self, "prec", ATOM)
71
+
72
+ def wrap(self, minimum: int) -> str:
73
+ """The text, parenthesized when its precedence is below *minimum*."""
74
+ return self.text if self.prec >= minimum else f"({self.text})"
75
+
76
+ def operand(self) -> str:
77
+ return self.wrap(ATOM)
78
+
79
+
80
+ def _children(node):
81
+ return getattr(node, "nodes", None) or ()
82
+
83
+
84
+ class Emitter:
85
+ """Generate the statements that compute a set of expression trees.
86
+
87
+ Args:
88
+ layout: the ``StateLayout`` that resolves the leaves.
89
+ target: a ``NumpyTarget`` or ``CasadiTarget``.
90
+ kinds: the slot kinds the expressions may use (``ALL_KINDS`` by default); a leaf
91
+ of another kind raises ``ValueError`` (a mass block with an input in it).
92
+ what: a name for the expressions, used in error messages.
93
+ """
94
+
95
+ def __init__(
96
+ self,
97
+ layout: StateLayout,
98
+ target: Any,
99
+ kinds: Iterable[str] = ALL_KINDS,
100
+ what: str = "expression",
101
+ ):
102
+ self.layout = layout
103
+ self.target = target
104
+ self.kinds = frozenset(kinds)
105
+ self.what = what
106
+ self.lines: list[str] = []
107
+ self.used: dict[str, Slot] = {}
108
+ self._cache: dict[int, Code] = {}
109
+ self._counts: Counter = Counter()
110
+ # id(node) -> (structural key, node); the node is kept so that its id is not reused
111
+ self._keys: dict[int, tuple[int, Any]] = {}
112
+ self._intern: dict[tuple, int] = {}
113
+ self._n_temps = 0
114
+
115
+ # ------------------------------------------------------------------ public
116
+
117
+ def prepare(self, exprs) -> None:
118
+ """Count the subtrees of every expression of the function (for CSE)."""
119
+ for e in exprs:
120
+ self._count(e)
121
+
122
+ def emit(self, node) -> Code:
123
+ """The code of *node*; statements it needs are appended to ``self.lines``."""
124
+ key = self._key(node)
125
+ if key in self._cache:
126
+ return self._cache[key]
127
+ code = self._dispatch(node)
128
+ if _children(node) and (self._counts[key] > 1 or len(code.text) > MAX_INLINE):
129
+ code = self.assign(code)
130
+ self._cache[key] = code
131
+ return code
132
+
133
+ def assign(self, code: Code) -> Code:
134
+ """Bind *code* to a new local and return the local's name."""
135
+ if code.atomic and code.text.isidentifier():
136
+ return code
137
+ name = f"_t{self._n_temps}"
138
+ self._n_temps += 1
139
+ self.lines.append(f"{name} = {code.text}")
140
+ return Code(name, atomic=True)
141
+
142
+ def preamble(self) -> list[str]:
143
+ """The statements that bind every slot the expressions used, in layout order."""
144
+ order = {key: i for i, key in enumerate(s.key for s in self.layout)}
145
+ slots = sorted(self.used.values(), key=lambda s: order[s.key])
146
+ arrays = [a for a in ("state", "a", "u") if any(s.array == a for s in slots)]
147
+ lines = [self.target.convert_array(a, self._array_size(a)) for a in arrays]
148
+ lines += [f"{s.name} = {self.target.unpack(s)}" for s in slots]
149
+ return lines
150
+
151
+ # ------------------------------------------------------------------ counting
152
+
153
+ def _key(self, node) -> int:
154
+ """The structural identity of *node*: equal for equal subtrees (see the module
155
+ docstring), computed once per node object."""
156
+ hit = self._keys.get(id(node))
157
+ if hit is not None:
158
+ return hit[0]
159
+ children = _children(node)
160
+ if children:
161
+ sig: tuple = (type(node), tuple(self._key(c) for c in children))
162
+ else:
163
+ sig = (type(node), str(node))
164
+ key = self._intern.setdefault(sig, len(self._intern))
165
+ self._keys[id(node)] = (key, node)
166
+ return key
167
+
168
+ def _count(self, node):
169
+ key = self._key(node)
170
+ self._counts[key] += 1
171
+ if self._counts[key] == 1:
172
+ for child in _children(node):
173
+ self._count(child)
174
+
175
+ def _array_size(self, array):
176
+ return {"state": self.layout.n_state, "a": self.layout.n_accel}.get(
177
+ array, self.layout.n_inputs
178
+ )
179
+
180
+ # ------------------------------------------------------------------ dispatch
181
+
182
+ def _dispatch(self, node) -> Code:
183
+ t = self.target
184
+ match node:
185
+ case Add() | VAdd() | MAdd():
186
+ return self._sum(node)
187
+ case Mul() | SVMul() | SMMul():
188
+ return t.scale(self._op(node.left), self._op(node.right))
189
+ case MVMul() if is_row_vector(node.left):
190
+ return t.matvec(t.transpose_matrix(self._op(node.right)), self._op(node.left))
191
+ case MVMul() | MMMul():
192
+ return t.matmul(self._op(node.left), self._op(node.right))
193
+ case VVMul() if node.type == ExprType.SCALAR:
194
+ return t.dot(self._op(node.left), self._op(node.right))
195
+ case VVMul():
196
+ return t.outer(self._op(node.left), self._op(node.right))
197
+ case Dot():
198
+ return t.dot(self._op(node.left), self._op(node.right))
199
+ case Cross():
200
+ return t.cross(self._op(node.left), self._op(node.right))
201
+ case Hat():
202
+ return t.call("_hat", self._op(node.expr))
203
+ case Vee():
204
+ return t.call("_vee", self._op(node.expr))
205
+ case Transpose():
206
+ return self._transpose(node)
207
+ case TimeDerivative():
208
+ return self._time_derivative(node)
209
+ case Variation() | TimeIntegral():
210
+ raise ValueError(
211
+ f"codegen: {self.what} contains the {type(node).__name__} '{node}'; only"
212
+ " configuration, velocity, acceleration, input and parameter symbols can"
213
+ " be generated (a variation or an integral has no numeric value here)"
214
+ )
215
+ case Scalar() | Vector() | Matrix():
216
+ return self._leaf(node)
217
+ case _:
218
+ raise TypeError(
219
+ f"codegen: no translation for the node class {type(node).__name__} '{node}'"
220
+ )
221
+
222
+ def _op(self, node) -> Code:
223
+ """The code of an operand, moved into a local when it is long (short lines)."""
224
+ code = self.emit(node)
225
+ if not code.atomic and len(code.text) > MAX_OPERAND:
226
+ code = self.assign(code)
227
+ return code
228
+
229
+ def _sum(self, node) -> Code:
230
+ """``a + b + …``; a long sum is accumulated in pieces that each fit on a line."""
231
+ codes = [self._op(n) for n in node.nodes]
232
+ parts = [codes[0].wrap(ADD)] + [c.wrap(MUL) for c in codes[1:]]
233
+ text = parts[0]
234
+ for part in parts[1:]:
235
+ if len(text) + 3 + len(part) > MAX_INLINE:
236
+ text = self.assign(Code(text)).text
237
+ text = f"{text} + {part}"
238
+ return Code(text)
239
+
240
+ def _transpose(self, node) -> Code:
241
+ inner = self._op(node.expr)
242
+ if node.expr.type == ExprType.MATRIX:
243
+ return self.target.transpose_matrix(inner)
244
+ return inner # a scalar, or the row vᵀ of a column: one 3-vector either way
245
+
246
+ # ------------------------------------------------------------------ leaves
247
+
248
+ def _time_derivative(self, node) -> Code:
249
+ base = base_symbol(node)
250
+ if base is not None and self.layout.slot(node) is not None:
251
+ return self._slot(node)
252
+ if base is not None:
253
+ raise ValueError(
254
+ f"codegen: {self.what} contains '{node}', the time derivative of the"
255
+ f" leaf '{base}', which is neither a velocity nor an acceleration of"
256
+ " the layout (list a prescribed rate in params)"
257
+ )
258
+ raise ValueError(
259
+ f"codegen: {self.what} contains '{node}', a TimeDerivative of the non-leaf"
260
+ f" expression '{node.expr}'; only the time derivative of a configuration"
261
+ " variable or of a velocity can be generated (expand the derivative first)"
262
+ )
263
+
264
+ def _leaf(self, node) -> Code:
265
+ if is_literal_leaf(node):
266
+ return self.target.literal(node)
267
+ if self.layout.slot(node) is None and isinstance(node, (TS2, TSO3)):
268
+ raise ValueError(
269
+ f"codegen: {self.what} contains '{node}', a tangent or variation vector that"
270
+ " is not a velocity of the layout; only configuration, velocity,"
271
+ " acceleration, input and parameter symbols can be generated (a variation"
272
+ " vector such as ξ or η, the Variation of a manifold point, has no numeric"
273
+ " value here, and is not a parameter)"
274
+ )
275
+ if self.layout.slot(node) is None:
276
+ raise ValueError(
277
+ f"codegen: {self.what} contains the symbol '{node}' ({type(node).__name__}),"
278
+ " which is not a configuration, velocity, acceleration, input or parameter"
279
+ " of the layout; add it to params (a constant) or inputs"
280
+ )
281
+ return self._slot(node)
282
+
283
+ def _slot(self, node) -> Code:
284
+ slot = self.layout.slot(node)
285
+ assert slot is not None
286
+ if slot.symbol.type != node.type:
287
+ raise ValueError(
288
+ f"codegen: '{node}' is a {node.type.name} in the {self.what} but the layout's"
289
+ f" {slot.kind} '{slot.key}' is a {slot.symbol.type.name}"
290
+ )
291
+ if slot.kind not in self.kinds:
292
+ allowed = ", ".join(sorted(self.kinds))
293
+ raise ValueError(
294
+ f"codegen: {self.what} contains the {slot.kind} '{slot.key}'; it may only"
295
+ f" depend on: {allowed}"
296
+ )
297
+ self.used[slot.key] = slot
298
+ return Code(slot.name, atomic=True)