morphis 0.11.0__tar.gz → 0.12.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 (110) hide show
  1. {morphis-0.11.0 → morphis-0.12.0}/PKG-INFO +11 -1
  2. {morphis-0.11.0 → morphis-0.12.0}/pyproject.toml +15 -1
  3. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/algebra/solvers.py +6 -7
  4. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/__init__.py +5 -1
  5. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/base.py +28 -0
  6. morphis-0.12.0/src/morphis/elements/field.py +102 -0
  7. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/multivector.py +45 -0
  8. morphis-0.12.0/src/morphis/elements/surface.py +367 -0
  9. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/vector.py +34 -2
  10. morphis-0.12.0/src/morphis/examples/astronaut_animated.py +113 -0
  11. morphis-0.12.0/src/morphis/examples/astronaut_view.py +25 -0
  12. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/examples/operators.py +56 -55
  13. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/examples/rotations_3d.py +13 -27
  14. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/examples/rotations_4d.py +16 -30
  15. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/operator.py +3 -3
  16. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/transforms/__init__.py +2 -0
  17. morphis-0.12.0/src/morphis/transforms/rotations.py +348 -0
  18. morphis-0.12.0/src/morphis/transforms/tests/test_rotations.py +212 -0
  19. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/utils/docgen.py +1 -0
  20. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/visuals/__init__.py +30 -4
  21. morphis-0.12.0/src/morphis/visuals/backends/__init__.py +39 -0
  22. morphis-0.12.0/src/morphis/visuals/backends/protocol.py +450 -0
  23. morphis-0.12.0/src/morphis/visuals/backends/pyvista.py +861 -0
  24. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/visuals/canvas.py +99 -12
  25. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/visuals/drawing/vectors.py +54 -32
  26. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/visuals/loop.py +94 -26
  27. morphis-0.12.0/src/morphis/visuals/model.py +410 -0
  28. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/visuals/renderer.py +59 -8
  29. morphis-0.12.0/src/morphis/visuals/scene.py +644 -0
  30. morphis-0.12.0/src/morphis/visuals/tests/__init__.py +0 -0
  31. morphis-0.12.0/src/morphis/visuals/tests/test_model.py +434 -0
  32. morphis-0.12.0/src/morphis/visuals/text.py +133 -0
  33. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/visuals/theme.py +112 -0
  34. morphis-0.11.0/src/morphis/transforms/rotations.py +0 -142
  35. {morphis-0.11.0 → morphis-0.12.0}/README.md +0 -0
  36. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/__init__.py +0 -0
  37. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/_legacy/__init__.py +0 -0
  38. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/_legacy/coordinates.py +0 -0
  39. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/_legacy/rotations.py +0 -0
  40. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/_legacy/smoothing.py +0 -0
  41. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/_legacy/vectors.py +0 -0
  42. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/algebra/__init__.py +0 -0
  43. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/algebra/contraction.py +0 -0
  44. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/algebra/patterns.py +0 -0
  45. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/algebra/specs.py +0 -0
  46. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/algebra/tests/__init__.py +0 -0
  47. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/algebra/tests/test_contraction.py +0 -0
  48. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/algebra/tests/test_patterns.py +0 -0
  49. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/algebra/tests/test_solvers.py +0 -0
  50. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/algebra/tests/test_specs.py +0 -0
  51. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/config.py +0 -0
  52. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/frame.py +0 -0
  53. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/lot_indexed.py +0 -0
  54. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/metric.py +0 -0
  55. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/operator.py +0 -0
  56. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/protocols.py +0 -0
  57. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/tensor.py +0 -0
  58. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/tests/__init__.py +0 -0
  59. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/tests/test_complex_blades.py +0 -0
  60. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/tests/test_maxwell_features.py +0 -0
  61. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/tests/test_model.py +0 -0
  62. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/tests/test_operator.py +0 -0
  63. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/tests/test_operators.py +0 -0
  64. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/elements/tests/test_tensor.py +0 -0
  65. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/examples/__init__.py +0 -0
  66. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/examples/clifford.py +0 -0
  67. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/examples/duality.py +0 -0
  68. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/examples/exterior.py +0 -0
  69. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/examples/phasors.py +0 -0
  70. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/examples/transforms_pga.py +0 -0
  71. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/manifold/__init__.py +0 -0
  72. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/__init__.py +0 -0
  73. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/_helpers.py +0 -0
  74. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/duality.py +0 -0
  75. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/exponential.py +0 -0
  76. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/factorization.py +0 -0
  77. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/matrix_rep.py +0 -0
  78. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/norms.py +0 -0
  79. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/outermorphism.py +0 -0
  80. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/products.py +0 -0
  81. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/projections.py +0 -0
  82. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/spectral.py +0 -0
  83. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/structure.py +0 -0
  84. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/subspaces.py +0 -0
  85. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/tests/__init__.py +0 -0
  86. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/tests/test_complex_operations.py +0 -0
  87. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/tests/test_duality.py +0 -0
  88. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/tests/test_exponential.py +0 -0
  89. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/tests/test_matrix_rep.py +0 -0
  90. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/tests/test_norms.py +0 -0
  91. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/tests/test_operations.py +0 -0
  92. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/tests/test_outermorphism.py +0 -0
  93. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/tests/test_products.py +0 -0
  94. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/tests/test_spectral.py +0 -0
  95. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/operations/tests/test_structure.py +0 -0
  96. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/topology/__init__.py +0 -0
  97. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/transforms/actions.py +0 -0
  98. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/transforms/projective.py +0 -0
  99. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/transforms/tests/__init__.py +0 -0
  100. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/transforms/tests/test_projective.py +0 -0
  101. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/utils/__init__.py +0 -0
  102. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/utils/easing.py +0 -0
  103. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/utils/exceptions.py +0 -0
  104. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/utils/observer.py +0 -0
  105. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/utils/pretty.py +0 -0
  106. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/visuals/contexts.py +0 -0
  107. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/visuals/drawing/__init__.py +0 -0
  108. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/visuals/effects.py +0 -0
  109. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/visuals/operations.py +0 -0
  110. {morphis-0.11.0 → morphis-0.12.0}/src/morphis/visuals/projection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: morphis
3
- Version: 0.11.0
3
+ Version: 0.12.0
4
4
  Summary: A unified mathematical framework for geometric computation
5
5
  Keywords: geometric-algebra,mathematics,visualization,multivector,pga
6
6
  Author: ctl-alt-leist
@@ -12,14 +12,17 @@ Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Topic :: Scientific/Engineering :: Mathematics
14
14
  Classifier: Topic :: Scientific/Engineering :: Visualization
15
+ Requires-Dist: dracopy>=1.4.0
15
16
  Requires-Dist: imageio>=2.37.2
16
17
  Requires-Dist: imageio-ffmpeg>=0.6.0
17
18
  Requires-Dist: matplotlib>=3.10.8
18
19
  Requires-Dist: numpy>=2.3.5
19
20
  Requires-Dist: pydantic>=2.12.5
21
+ Requires-Dist: pygltflib>=1.16.5
20
22
  Requires-Dist: pyvista>=0.46.4
21
23
  Requires-Dist: scipy>=1.16.3
22
24
  Requires-Dist: tomli>=2.3.0
25
+ Requires-Dist: trimesh>=4.11.1
23
26
  Requires-Dist: build ; extra == 'dev'
24
27
  Requires-Dist: flake8 ; extra == 'dev'
25
28
  Requires-Dist: ipython ; extra == 'dev'
@@ -28,11 +31,18 @@ Requires-Dist: pre-commit ; extra == 'dev'
28
31
  Requires-Dist: pytest ; extra == 'dev'
29
32
  Requires-Dist: python-lsp-server ; extra == 'dev'
30
33
  Requires-Dist: ruff ; extra == 'dev'
34
+ Requires-Dist: pyvista>=0.46.4 ; extra == 'visuals'
35
+ Requires-Dist: imageio>=2.37.2 ; extra == 'visuals'
36
+ Requires-Dist: imageio-ffmpeg>=0.6.0 ; extra == 'visuals'
37
+ Requires-Dist: trimesh>=4.11.1 ; extra == 'visuals'
38
+ Requires-Dist: pygltflib>=1.16.5 ; extra == 'visuals'
39
+ Requires-Dist: dracopy>=1.4.0 ; extra == 'visuals'
31
40
  Requires-Python: >=3.12
32
41
  Project-URL: Homepage, https://github.com/ctl-alt-leist/morphis
33
42
  Project-URL: Repository, https://github.com/ctl-alt-leist/morphis
34
43
  Project-URL: Issues, https://github.com/ctl-alt-leist/morphis/issues
35
44
  Provides-Extra: dev
45
+ Provides-Extra: visuals
36
46
  Description-Content-Type: text/markdown
37
47
 
38
48
  # Morphis
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "morphis"
3
- version = "0.11.0"
3
+ version = "0.12.0"
4
4
  description = "A unified mathematical framework for geometric computation"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -18,17 +18,31 @@ classifiers = [
18
18
  "Topic :: Scientific/Engineering :: Visualization",
19
19
  ]
20
20
  dependencies = [
21
+ "DracoPy>=1.4.0",
21
22
  "imageio>=2.37.2",
22
23
  "imageio-ffmpeg>=0.6.0",
23
24
  "matplotlib>=3.10.8",
24
25
  "numpy>=2.3.5",
25
26
  "pydantic>=2.12.5",
27
+ "pygltflib>=1.16.5",
26
28
  "pyvista>=0.46.4",
27
29
  "scipy>=1.16.3",
28
30
  "tomli>=2.3.0",
31
+ "trimesh>=4.11.1",
29
32
  ]
30
33
 
31
34
  [project.optional-dependencies]
35
+ visuals = [
36
+ # These are currently in main dependencies but documented here
37
+ # for explicit dependency declaration. In a future major version,
38
+ # these may be moved out of main dependencies.
39
+ "pyvista>=0.46.4",
40
+ "imageio>=2.37.2",
41
+ "imageio-ffmpeg>=0.6.0",
42
+ "trimesh>=4.11.1",
43
+ "pygltflib>=1.16.5",
44
+ "DracoPy>=1.4.0",
45
+ ]
32
46
  dev = [
33
47
  "build",
34
48
  "flake8",
@@ -8,9 +8,8 @@ structured Operators for the results.
8
8
 
9
9
  from typing import TYPE_CHECKING
10
10
 
11
- import numpy as np
12
- from numpy import prod
13
- from numpy.linalg import lstsq, svd
11
+ from numpy import diag_indices_from, prod
12
+ from numpy.linalg import lstsq, pinv, solve, svd
14
13
  from numpy.typing import NDArray
15
14
 
16
15
  from morphis.algebra.specs import VectorSpec
@@ -143,9 +142,9 @@ def structured_lstsq(
143
142
  if alpha > 0:
144
143
  # Regularized: (G^H G + alpha*I) x = G^H y
145
144
  GhG = G_matrix.conj().T @ G_matrix
146
- GhG[np.diag_indices_from(GhG)] += alpha
145
+ GhG[diag_indices_from(GhG)] += alpha
147
146
  Ghy = G_matrix.conj().T @ y_vector
148
- x_vector = np.linalg.solve(GhG, Ghy)
147
+ x_vector = solve(GhG, Ghy)
149
148
  else:
150
149
  # Standard least squares
151
150
  x_vector, _, _, _ = lstsq(G_matrix, y_vector, rcond=None)
@@ -211,9 +210,9 @@ def structured_pinv(
211
210
 
212
211
  # Compute pseudoinverse
213
212
  if r_cond is None:
214
- G_pinv = np.linalg.pinv(G_matrix)
213
+ G_pinv = pinv(G_matrix)
215
214
  else:
216
- G_pinv = np.linalg.pinv(G_matrix, rcond=r_cond)
215
+ G_pinv = pinv(G_matrix, rcond=r_cond)
217
216
 
218
217
  # Reconstruct as Operator with swapped specs
219
218
  return _from_matrix(
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Geometric Algebra - Elements
3
3
 
4
- Core geometric algebra objects: Vectors, MultiVectors, Frames, and Metrics.
4
+ Core geometric algebra objects: Vectors, MultiVectors, Frames, Surfaces, and Metrics.
5
5
  """
6
6
 
7
7
  from morphis.elements.base import (
@@ -9,6 +9,7 @@ from morphis.elements.base import (
9
9
  Element as Element,
10
10
  GradedElement as GradedElement,
11
11
  )
12
+ from morphis.elements.field import Field
12
13
  from morphis.elements.frame import Frame
13
14
  from morphis.elements.lot_indexed import LotIndexed as LotIndexed
14
15
  from morphis.elements.metric import (
@@ -32,6 +33,7 @@ from morphis.elements.protocols import (
32
33
  Spanning as Spanning,
33
34
  Transformable as Transformable,
34
35
  )
36
+ from morphis.elements.surface import Surface
35
37
  from morphis.elements.tensor import Tensor
36
38
  from morphis.elements.vector import (
37
39
  Vector,
@@ -48,3 +50,5 @@ Vector.model_rebuild()
48
50
  MultiVector.model_rebuild()
49
51
  Frame.model_rebuild()
50
52
  Tensor.model_rebuild()
53
+ Surface.model_rebuild()
54
+ Field.model_rebuild()
@@ -30,6 +30,7 @@ from morphis.elements.metric import Metric
30
30
 
31
31
  if TYPE_CHECKING:
32
32
  from morphis.algebra.contraction import IndexedTensor
33
+ from morphis.elements.multivector import MultiVector
33
34
  from morphis.elements.vector import Vector
34
35
 
35
36
 
@@ -124,6 +125,33 @@ class Element(BaseModel):
124
125
  """Dimension of the underlying vector space."""
125
126
  return self.metric.dim
126
127
 
128
+ def apply_similarity(
129
+ self,
130
+ S: "MultiVector",
131
+ t: "Vector | None" = None,
132
+ ) -> Self:
133
+ """
134
+ Apply a similarity transformation to this element.
135
+
136
+ Computes: transform(self, S) + t
137
+
138
+ The similarity versor S (from align_vectors or a rotor) is applied via
139
+ sandwich product. The optional translation t is added after.
140
+
141
+ Args:
142
+ S: Similarity versor or rotor (MultiVector with grades {0, 2})
143
+ t: Optional translation vector (grade-1). Added after sandwich product.
144
+
145
+ Returns:
146
+ New element with the transformation applied.
147
+
148
+ Example:
149
+ S = align_vectors(u, v) # Similarity versor
150
+ t = Vector([1, 0, 0], grade=1, metric=g) # Translation
151
+ v_transformed = v.apply_similarity(S, t)
152
+ """
153
+ raise NotImplementedError("Subclasses must implement apply_similarity()")
154
+
127
155
 
128
156
  class GradedElement(Element):
129
157
  """
@@ -0,0 +1,102 @@
1
+ """
2
+ Field Element - Positions with Values (Skeleton)
3
+
4
+ Field represents a collection of positions in space, each with an associated
5
+ value (scalar, vector, frame, or multivector). This is a skeleton for future
6
+ implementation.
7
+
8
+ Typical uses:
9
+ - Scalar fields: temperature, pressure, density
10
+ - Vector fields: velocity, electric field, force
11
+ - Frame fields: stress tensors, rotation fields
12
+ - Multivector fields: electromagnetic field (F = E + IcB)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import TYPE_CHECKING, Self
18
+
19
+ from pydantic import ConfigDict, model_validator
20
+
21
+ from morphis.elements.base import Element
22
+ from morphis.elements.vector import Vector
23
+
24
+
25
+ if TYPE_CHECKING:
26
+ from morphis.elements.frame import Frame
27
+ from morphis.elements.metric import Metric
28
+ from morphis.elements.multivector import MultiVector
29
+
30
+
31
+ class Field(Element):
32
+ """
33
+ A field of values at positions in space.
34
+
35
+ SKELETON: This class structure is defined for future implementation.
36
+ Full implementation is deferred.
37
+
38
+ Attributes:
39
+ positions: Grade-1 Vector with lot=(N,) representing N sample positions
40
+ values: The field values at each position (Vector, Frame, or MultiVector)
41
+
42
+ The values must have lot matching positions.lot, so each position has
43
+ exactly one associated value.
44
+
45
+ Examples:
46
+ # Velocity field (3D vectors at 3D positions)
47
+ positions = Vector(sample_points, grade=1, metric=g) # lot=(100,)
48
+ velocities = Vector(velocity_data, grade=1, metric=g) # lot=(100,)
49
+ field = Field(positions=positions, values=velocities)
50
+
51
+ # Scalar field (scalars at positions)
52
+ temperatures = Vector(temp_data, grade=0, metric=g) # lot=(100,)
53
+ field = Field(positions=positions, values=temperatures)
54
+ """
55
+
56
+ model_config = ConfigDict(
57
+ arbitrary_types_allowed=True,
58
+ frozen=False,
59
+ )
60
+
61
+ positions: Vector
62
+ values: "Vector | Frame | MultiVector"
63
+
64
+ @model_validator(mode="after")
65
+ def _validate_field(self):
66
+ """Validate positions and values are compatible."""
67
+ # Positions must be grade-1 vectors
68
+ if self.positions.grade != 1:
69
+ raise ValueError(f"Positions must be grade-1 Vectors, got grade={self.positions.grade}")
70
+
71
+ # Lots must match
72
+ if self.positions.lot != self.values.lot:
73
+ raise ValueError(f"Positions lot {self.positions.lot} != values lot {self.values.lot}")
74
+
75
+ # Sync metric and lot from positions
76
+ object.__setattr__(self, "metric", self.positions.metric)
77
+ object.__setattr__(self, "lot", self.positions.lot)
78
+
79
+ return self
80
+
81
+ @property
82
+ def n_samples(self) -> int:
83
+ """Number of sample positions in the field."""
84
+ return self.positions.lot[0] if self.positions.lot else 1
85
+
86
+ def copy(self) -> Self:
87
+ """Create a deep copy of this field."""
88
+ return Field(
89
+ positions=self.positions.copy(),
90
+ values=self.values.copy(),
91
+ )
92
+
93
+ def with_metric(self, metric: "Metric") -> Self:
94
+ """Return a new Field with the specified metric context."""
95
+ return Field(
96
+ positions=self.positions.with_metric(metric),
97
+ values=self.values.with_metric(metric),
98
+ )
99
+
100
+ def __repr__(self) -> str:
101
+ value_type = type(self.values).__name__
102
+ return f"Field(n_samples={self.n_samples}, value_type={value_type})"
@@ -526,6 +526,51 @@ class MultiVector(CompositeElement):
526
526
 
527
527
  return unit(self)
528
528
 
529
+ def apply_similarity(
530
+ self,
531
+ S: MultiVector,
532
+ t: "Vector | None" = None,
533
+ ) -> MultiVector:
534
+ """
535
+ Apply a similarity transformation to this MultiVector.
536
+
537
+ Computes: S * self * ~S + t (translation on grade-1 component only)
538
+
539
+ The similarity versor S (from align_vectors or a rotor) is applied via
540
+ sandwich product. The optional translation t is added to the grade-1
541
+ component only (translations affect positions, not orientations).
542
+
543
+ Args:
544
+ S: Similarity versor or rotor (MultiVector with grades {0, 2})
545
+ t: Optional translation vector (grade-1). Added to grade-1 component.
546
+
547
+ Returns:
548
+ New MultiVector with the transformation applied.
549
+
550
+ Example:
551
+ S = align_vectors(u, v)
552
+ t = Vector([1, 0, 0], grade=1, metric=g)
553
+ M_transformed = M.apply_similarity(S, t)
554
+ """
555
+ from morphis.operations.products import geometric, reverse
556
+
557
+ # Sandwich product: S * self * ~S
558
+ S_rev = reverse(S)
559
+ temp = geometric(S, self)
560
+ result = geometric(temp, S_rev)
561
+
562
+ if t is not None:
563
+ # Translation applies to grade-1 component only
564
+ grade1 = result.grade_select(1)
565
+ if grade1 is not None:
566
+ new_grade1 = grade1 + t
567
+ result = MultiVector(
568
+ data={**result.data, 1: new_grade1},
569
+ metric=result.metric,
570
+ lot=result.lot,
571
+ )
572
+ return result
573
+
529
574
  # =========================================================================
530
575
  # Utility Methods
531
576
  # =========================================================================