morphis 0.10.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.
- {morphis-0.10.0 → morphis-0.12.0}/PKG-INFO +11 -1
- {morphis-0.10.0 → morphis-0.12.0}/pyproject.toml +15 -1
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/algebra/contraction.py +24 -17
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/algebra/solvers.py +6 -7
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/algebra/tests/test_contraction.py +84 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/__init__.py +5 -1
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/base.py +28 -0
- morphis-0.12.0/src/morphis/elements/field.py +102 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/multivector.py +45 -0
- morphis-0.12.0/src/morphis/elements/surface.py +367 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/vector.py +37 -3
- morphis-0.12.0/src/morphis/examples/astronaut_animated.py +113 -0
- morphis-0.12.0/src/morphis/examples/astronaut_view.py +25 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/examples/operators.py +56 -55
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/examples/rotations_3d.py +13 -27
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/examples/rotations_4d.py +16 -30
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/operator.py +10 -4
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/transforms/__init__.py +2 -0
- morphis-0.12.0/src/morphis/transforms/rotations.py +348 -0
- morphis-0.12.0/src/morphis/transforms/tests/test_rotations.py +212 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/utils/docgen.py +1 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/visuals/__init__.py +30 -4
- morphis-0.12.0/src/morphis/visuals/backends/__init__.py +39 -0
- morphis-0.12.0/src/morphis/visuals/backends/protocol.py +450 -0
- morphis-0.12.0/src/morphis/visuals/backends/pyvista.py +861 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/visuals/canvas.py +99 -12
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/visuals/drawing/vectors.py +54 -32
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/visuals/loop.py +94 -26
- morphis-0.12.0/src/morphis/visuals/model.py +410 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/visuals/renderer.py +59 -8
- morphis-0.12.0/src/morphis/visuals/scene.py +644 -0
- morphis-0.12.0/src/morphis/visuals/tests/__init__.py +0 -0
- morphis-0.12.0/src/morphis/visuals/tests/test_model.py +434 -0
- morphis-0.12.0/src/morphis/visuals/text.py +133 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/visuals/theme.py +112 -0
- morphis-0.10.0/src/morphis/transforms/rotations.py +0 -142
- {morphis-0.10.0 → morphis-0.12.0}/README.md +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/__init__.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/_legacy/__init__.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/_legacy/coordinates.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/_legacy/rotations.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/_legacy/smoothing.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/_legacy/vectors.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/algebra/__init__.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/algebra/patterns.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/algebra/specs.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/algebra/tests/__init__.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/algebra/tests/test_patterns.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/algebra/tests/test_solvers.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/algebra/tests/test_specs.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/config.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/frame.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/lot_indexed.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/metric.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/operator.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/protocols.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/tensor.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/tests/__init__.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/tests/test_complex_blades.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/tests/test_maxwell_features.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/tests/test_model.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/tests/test_operator.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/tests/test_operators.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/elements/tests/test_tensor.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/examples/__init__.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/examples/clifford.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/examples/duality.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/examples/exterior.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/examples/phasors.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/examples/transforms_pga.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/manifold/__init__.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/__init__.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/_helpers.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/duality.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/exponential.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/factorization.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/matrix_rep.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/norms.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/outermorphism.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/products.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/projections.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/spectral.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/structure.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/subspaces.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/tests/__init__.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/tests/test_complex_operations.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/tests/test_duality.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/tests/test_exponential.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/tests/test_matrix_rep.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/tests/test_norms.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/tests/test_operations.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/tests/test_outermorphism.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/tests/test_products.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/tests/test_spectral.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/operations/tests/test_structure.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/topology/__init__.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/transforms/actions.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/transforms/projective.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/transforms/tests/__init__.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/transforms/tests/test_projective.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/utils/__init__.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/utils/easing.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/utils/exceptions.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/utils/observer.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/utils/pretty.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/visuals/contexts.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/visuals/drawing/__init__.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/visuals/effects.py +0 -0
- {morphis-0.10.0 → morphis-0.12.0}/src/morphis/visuals/operations.py +0 -0
- {morphis-0.10.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.
|
|
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.
|
|
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",
|
|
@@ -37,6 +37,8 @@ class IndexedTensor:
|
|
|
37
37
|
Attributes:
|
|
38
38
|
tensor: The underlying Vector or Operator (reference, not copy)
|
|
39
39
|
indices: String of index labels (e.g., "mnab")
|
|
40
|
+
output_geo_indices: Indices that represent output geometric dimensions.
|
|
41
|
+
These determine the result grade when present in contraction output.
|
|
40
42
|
|
|
41
43
|
Examples:
|
|
42
44
|
>>> G = Operator(...) # lot=(M, N), grade=2 output
|
|
@@ -44,18 +46,21 @@ class IndexedTensor:
|
|
|
44
46
|
>>> b = G["mnab"] * q["n"] # contracts on 'n', result has indices "mab"
|
|
45
47
|
"""
|
|
46
48
|
|
|
47
|
-
__slots__ = ("tensor", "indices")
|
|
49
|
+
__slots__ = ("tensor", "indices", "output_geo_indices")
|
|
48
50
|
|
|
49
|
-
def __init__(self, tensor: "Vector | Operator", indices: str):
|
|
51
|
+
def __init__(self, tensor: "Vector | Operator", indices: str, output_geo_indices: str = ""):
|
|
50
52
|
"""
|
|
51
53
|
Create an indexed tensor wrapper.
|
|
52
54
|
|
|
53
55
|
Args:
|
|
54
56
|
tensor: The underlying Vector or Operator
|
|
55
57
|
indices: String of index labels, one per axis of tensor.data
|
|
58
|
+
output_geo_indices: Subset of indices representing output geometric
|
|
59
|
+
dimensions. If empty, will be inferred during contraction (legacy).
|
|
56
60
|
"""
|
|
57
61
|
self.tensor = tensor
|
|
58
62
|
self.indices = indices
|
|
63
|
+
self.output_geo_indices = output_geo_indices
|
|
59
64
|
|
|
60
65
|
# Validate index count matches tensor dimensions
|
|
61
66
|
expected_ndim = tensor.data.ndim
|
|
@@ -78,9 +83,14 @@ class IndexedTensor:
|
|
|
78
83
|
|
|
79
84
|
if isinstance(other, LotIndexed):
|
|
80
85
|
# Convert LotIndexed to IndexedTensor by adding geo indices
|
|
86
|
+
# All of a Vector's geometric indices are "output geometric"
|
|
81
87
|
n_geo = other.vector.grade
|
|
82
88
|
geo_labels = "".join(chr(ord("A") + i) for i in range(n_geo))
|
|
83
|
-
other = IndexedTensor(
|
|
89
|
+
other = IndexedTensor(
|
|
90
|
+
other.vector,
|
|
91
|
+
other.indices + geo_labels,
|
|
92
|
+
output_geo_indices=geo_labels,
|
|
93
|
+
)
|
|
84
94
|
|
|
85
95
|
if not isinstance(other, IndexedTensor):
|
|
86
96
|
return NotImplemented
|
|
@@ -152,23 +162,20 @@ def _contract_indexed(*indexed_tensors: IndexedTensor) -> "Vector":
|
|
|
152
162
|
|
|
153
163
|
|
|
154
164
|
def _infer_grade_from_indexed(indexed_tensors: tuple[IndexedTensor, ...], output_indices: str) -> int:
|
|
155
|
-
"""
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
# Track which indices are geometric (vs lot)
|
|
159
|
-
geo_indices = set()
|
|
165
|
+
"""
|
|
166
|
+
Infer grade for IndexedTensor contraction result.
|
|
160
167
|
|
|
168
|
+
The result grade equals the number of output geometric indices that appear
|
|
169
|
+
in the result. Each IndexedTensor carries its output_geo_indices, which
|
|
170
|
+
identify which indices represent geometric (grade-contributing) dimensions.
|
|
171
|
+
"""
|
|
172
|
+
# Collect output geometric indices from all tensors
|
|
173
|
+
all_output_geo = set()
|
|
161
174
|
for it in indexed_tensors:
|
|
162
|
-
|
|
163
|
-
n_lot = len(it.tensor.lot)
|
|
164
|
-
n_geo = it.tensor.grade
|
|
165
|
-
# Geometric indices are the last 'grade' indices
|
|
166
|
-
geo_part = it.indices[n_lot : n_lot + n_geo]
|
|
167
|
-
geo_indices.update(geo_part)
|
|
175
|
+
all_output_geo.update(it.output_geo_indices)
|
|
168
176
|
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
return result_grade
|
|
177
|
+
# Result grade = count of output geometric indices in the result
|
|
178
|
+
return sum(1 for idx in output_indices if idx in all_output_geo)
|
|
172
179
|
|
|
173
180
|
|
|
174
181
|
# =============================================================================
|
|
@@ -8,9 +8,8 @@ structured Operators for the results.
|
|
|
8
8
|
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
from numpy import
|
|
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[
|
|
145
|
+
GhG[diag_indices_from(GhG)] += alpha
|
|
147
146
|
Ghy = G_matrix.conj().T @ y_vector
|
|
148
|
-
x_vector =
|
|
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 =
|
|
213
|
+
G_pinv = pinv(G_matrix)
|
|
215
214
|
else:
|
|
216
|
-
G_pinv =
|
|
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,11 +1,14 @@
|
|
|
1
1
|
"""Tests for tensor contraction with both bracket and einsum-style APIs."""
|
|
2
2
|
|
|
3
|
+
import numpy as np
|
|
3
4
|
import pytest
|
|
4
5
|
from numpy import array, ones
|
|
5
6
|
from numpy.testing import assert_allclose, assert_array_equal
|
|
6
7
|
|
|
7
8
|
from morphis.algebra.contraction import IndexedTensor, contract
|
|
9
|
+
from morphis.algebra.specs import VectorSpec
|
|
8
10
|
from morphis.elements import Vector, euclidean_metric
|
|
11
|
+
from morphis.operations.operator import Operator
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
# =============================================================================
|
|
@@ -222,3 +225,84 @@ class TestSlicingStillWorks:
|
|
|
222
225
|
|
|
223
226
|
assert isinstance(result, Vector)
|
|
224
227
|
assert result.shape == (5, 3)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# =============================================================================
|
|
231
|
+
# Operator Indexed Contraction
|
|
232
|
+
# =============================================================================
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class TestOperatorIndexedContraction:
|
|
236
|
+
"""Tests for indexed contraction between Operators and Vectors."""
|
|
237
|
+
|
|
238
|
+
def test_operator_vector_contraction_preserves_output_grade(self):
|
|
239
|
+
"""
|
|
240
|
+
Operator contraction should preserve output_spec.grade in result.
|
|
241
|
+
|
|
242
|
+
This is the key regression test: when an Operator with output_spec.grade=2
|
|
243
|
+
contracts with a Vector, the result must have grade=2, not grade=0.
|
|
244
|
+
"""
|
|
245
|
+
g = euclidean_metric(3)
|
|
246
|
+
|
|
247
|
+
# Operator: maps grade-0 lot (N,) to grade-2 lot (M,)
|
|
248
|
+
M, N = 2, 3
|
|
249
|
+
O = Operator(
|
|
250
|
+
data=np.random.randn(M, N, 3, 3), # shape (M, N, 3, 3)
|
|
251
|
+
input_spec=VectorSpec(grade=0, lot=(N,), dim=3),
|
|
252
|
+
output_spec=VectorSpec(grade=2, lot=(M,), dim=3),
|
|
253
|
+
metric=g,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Input: grade-0 with lot (N,)
|
|
257
|
+
v = Vector(np.random.randn(N), grade=0, metric=g)
|
|
258
|
+
|
|
259
|
+
# Contract on "n" (the input lot index)
|
|
260
|
+
result = O["mnab"] * v["n"]
|
|
261
|
+
|
|
262
|
+
# Result should have: grade=2, lot=(M,)
|
|
263
|
+
assert result.grade == 2, f"Expected grade=2, got grade={result.grade}"
|
|
264
|
+
assert result.lot == (M,), f"Expected lot=(2,), got lot={result.lot}"
|
|
265
|
+
assert result.shape == (M, 3, 3)
|
|
266
|
+
|
|
267
|
+
def test_operator_lot_indexed_vector_contraction(self):
|
|
268
|
+
"""Operator contraction via LotIndexed syntax also preserves grade."""
|
|
269
|
+
g = euclidean_metric(3)
|
|
270
|
+
|
|
271
|
+
M, N = 4, 5
|
|
272
|
+
O = Operator(
|
|
273
|
+
data=np.ones((M, N, 3, 3)),
|
|
274
|
+
input_spec=VectorSpec(grade=0, lot=(N,), dim=3),
|
|
275
|
+
output_spec=VectorSpec(grade=2, lot=(M,), dim=3),
|
|
276
|
+
metric=g,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Input as grade-0 vector with lot
|
|
280
|
+
v = Vector(np.ones(N), grade=0, metric=g)
|
|
281
|
+
|
|
282
|
+
result = O["mnab"] * v["n"]
|
|
283
|
+
|
|
284
|
+
assert result.grade == 2
|
|
285
|
+
assert result.lot == (M,)
|
|
286
|
+
# Each element should be sum over N: N * 1 = 5
|
|
287
|
+
assert_allclose(result.data, np.ones((M, 3, 3)) * N)
|
|
288
|
+
|
|
289
|
+
def test_operator_vector_to_vector_contraction(self):
|
|
290
|
+
"""Operator mapping grade-1 to grade-1 preserves grade in contraction."""
|
|
291
|
+
g = euclidean_metric(3)
|
|
292
|
+
|
|
293
|
+
M, N = 2, 3
|
|
294
|
+
# grade-1 -> grade-1 operator (outermorphism)
|
|
295
|
+
O = Operator(
|
|
296
|
+
data=np.eye(3).reshape(1, 1, 3, 3) * np.ones((M, N, 1, 1)),
|
|
297
|
+
input_spec=VectorSpec(grade=1, lot=(N,), dim=3),
|
|
298
|
+
output_spec=VectorSpec(grade=1, lot=(M,), dim=3),
|
|
299
|
+
metric=g,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
v = Vector(np.ones((N, 3)), grade=1, metric=g)
|
|
303
|
+
|
|
304
|
+
result = O["mnab"] * v["nb"]
|
|
305
|
+
|
|
306
|
+
assert result.grade == 1
|
|
307
|
+
assert result.lot == (M,)
|
|
308
|
+
assert result.shape == (M, 3)
|
|
@@ -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
|
# =========================================================================
|