morphis 0.6.0__tar.gz → 0.9.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.6.0 → morphis-0.9.0}/PKG-INFO +40 -7
- {morphis-0.6.0 → morphis-0.9.0}/README.md +39 -6
- {morphis-0.6.0 → morphis-0.9.0}/pyproject.toml +1 -1
- morphis-0.9.0/src/morphis/__init__.py +56 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/_legacy/rotations.py +5 -5
- morphis-0.9.0/src/morphis/algebra/__init__.py +27 -0
- morphis-0.9.0/src/morphis/algebra/contraction.py +263 -0
- morphis-0.9.0/src/morphis/algebra/patterns.py +196 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/algebra/solvers.py +47 -48
- morphis-0.9.0/src/morphis/algebra/specs.py +165 -0
- morphis-0.9.0/src/morphis/algebra/tests/test_contraction.py +224 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/algebra/tests/test_patterns.py +72 -71
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/algebra/tests/test_solvers.py +80 -79
- morphis-0.9.0/src/morphis/algebra/tests/test_specs.py +169 -0
- morphis-0.9.0/src/morphis/config.py +12 -0
- morphis-0.9.0/src/morphis/elements/__init__.py +49 -0
- morphis-0.6.0/src/morphis/elements/elements.py → morphis-0.9.0/src/morphis/elements/base.py +81 -17
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/elements/frame.py +111 -113
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/elements/metric.py +25 -25
- morphis-0.9.0/src/morphis/elements/multivector.py +555 -0
- morphis-0.9.0/src/morphis/elements/operator.py +10 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/elements/protocols.py +52 -4
- morphis-0.9.0/src/morphis/elements/tensor.py +248 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/elements/tests/test_complex_blades.py +41 -41
- morphis-0.9.0/src/morphis/elements/tests/test_model.py +642 -0
- morphis-0.9.0/src/morphis/elements/tests/test_operator.py +389 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/elements/tests/test_operators.py +250 -243
- morphis-0.9.0/src/morphis/elements/tests/test_tensor.py +187 -0
- morphis-0.9.0/src/morphis/elements/vector.py +971 -0
- morphis-0.6.0/src/morphis/examples/ga_geometric.py → morphis-0.9.0/src/morphis/examples/clifford.py +79 -59
- morphis-0.9.0/src/morphis/examples/duality.py +258 -0
- morphis-0.6.0/src/morphis/examples/ga_ops.py → morphis-0.9.0/src/morphis/examples/exterior.py +139 -93
- morphis-0.6.0/src/morphis/examples/linear_operators.py → morphis-0.9.0/src/morphis/examples/operators.py +106 -90
- morphis-0.6.0/src/morphis/examples/ga_phasors.py → morphis-0.9.0/src/morphis/examples/phasors.py +84 -59
- morphis-0.6.0/src/morphis/examples/animate_3d.py → morphis-0.9.0/src/morphis/examples/rotations_3d.py +9 -10
- morphis-0.6.0/src/morphis/examples/animate_4d.py → morphis-0.9.0/src/morphis/examples/rotations_4d.py +10 -14
- morphis-0.9.0/src/morphis/examples/transforms_pga.py +319 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/__init__.py +12 -14
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/_helpers.py +31 -31
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/duality.py +33 -33
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/exponential.py +33 -38
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/factorization.py +56 -52
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/matrix_rep.py +32 -32
- morphis-0.9.0/src/morphis/operations/norms.py +226 -0
- {morphis-0.6.0/src/morphis/elements → morphis-0.9.0/src/morphis/operations}/operator.py +138 -90
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/outermorphism.py +9 -9
- morphis-0.9.0/src/morphis/operations/products.py +816 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/projections.py +35 -34
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/spectral.py +20 -15
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/subspaces.py +1 -1
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_complex_operations.py +50 -50
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_duality.py +22 -22
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_exponential.py +72 -72
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_matrix_rep.py +93 -92
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_norms.py +91 -91
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_operations.py +110 -110
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_outermorphism.py +106 -100
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_products.py +65 -65
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_spectral.py +41 -41
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/transforms/__init__.py +1 -1
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/transforms/actions.py +26 -26
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/transforms/projective.py +76 -71
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/transforms/rotations.py +16 -14
- morphis-0.9.0/src/morphis/utils/docgen.py +461 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/utils/observer.py +24 -23
- morphis-0.9.0/src/morphis/utils/pretty.py +332 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/__init__.py +3 -3
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/canvas.py +7 -6
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/contexts.py +13 -16
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/drawing/__init__.py +2 -2
- morphis-0.6.0/src/morphis/visuals/drawing/blades.py → morphis-0.9.0/src/morphis/visuals/drawing/vectors.py +52 -51
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/effects.py +6 -7
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/loop.py +35 -32
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/operations.py +26 -24
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/projection.py +28 -27
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/renderer.py +11 -9
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/theme.py +47 -38
- morphis-0.6.0/src/morphis/__init__.py +0 -2
- morphis-0.6.0/src/morphis/algebra/__init__.py +0 -43
- morphis-0.6.0/src/morphis/algebra/patterns.py +0 -181
- morphis-0.6.0/src/morphis/algebra/specs.py +0 -114
- morphis-0.6.0/src/morphis/algebra/tests/test_specs.py +0 -135
- morphis-0.6.0/src/morphis/elements/__init__.py +0 -71
- morphis-0.6.0/src/morphis/elements/blade.py +0 -710
- morphis-0.6.0/src/morphis/elements/multivector.py +0 -413
- morphis-0.6.0/src/morphis/elements/tests/test_model.py +0 -396
- morphis-0.6.0/src/morphis/elements/tests/test_operator.py +0 -380
- morphis-0.6.0/src/morphis/operations/norms.py +0 -145
- morphis-0.6.0/src/morphis/operations/products.py +0 -608
- morphis-0.6.0/src/morphis/transforms/tests/__init__.py +0 -0
- morphis-0.6.0/src/morphis/utils/pretty.py +0 -153
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/_legacy/__init__.py +0 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/_legacy/coordinates.py +0 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/_legacy/smoothing.py +0 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/_legacy/vectors.py +0 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/algebra/tests/__init__.py +0 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/elements/tests/__init__.py +0 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/examples/__init__.py +0 -0
- {morphis-0.6.0/src/morphis/groups → morphis-0.9.0/src/morphis/manifold}/__init__.py +0 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/structure.py +0 -0
- {morphis-0.6.0/src/morphis/manifold → morphis-0.9.0/src/morphis/operations/tests}/__init__.py +0 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_structure.py +0 -0
- {morphis-0.6.0/src/morphis/operations/tests → morphis-0.9.0/src/morphis/topology}/__init__.py +0 -0
- {morphis-0.6.0/src/morphis/topology → morphis-0.9.0/src/morphis/transforms/tests}/__init__.py +0 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/transforms/tests/test_projective.py +0 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/utils/__init__.py +0 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/utils/easing.py +0 -0
- {morphis-0.6.0 → morphis-0.9.0}/src/morphis/utils/exceptions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: morphis
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -53,12 +53,45 @@ essential nature.
|
|
|
53
53
|
|
|
54
54
|
## Features
|
|
55
55
|
|
|
56
|
-
- **Geometric Algebra Core**:
|
|
56
|
+
- **Geometric Algebra Core**: Vectors (k-vectors), multivectors, and operations (wedge, geometric product, duality)
|
|
57
57
|
- **Metric-Aware**: Objects carry their metric context (Euclidean, projective, etc.)
|
|
58
|
-
- **Linear Operators**: Structured linear maps between
|
|
59
|
-
- **Visualization**: 3D rendering of
|
|
58
|
+
- **Linear Operators**: Structured linear maps between vector spaces with SVD, pseudoinverse, least squares
|
|
59
|
+
- **Visualization**: 3D rendering of vectors with PyVista, timeline-based animation, 4D projection
|
|
60
60
|
- **Motor Transforms**: Rotors and translations via sandwich product
|
|
61
61
|
|
|
62
|
+
## Documentation
|
|
63
|
+
|
|
64
|
+
- [Project Overview](docs/0_project-overview.md) — Vision and scope
|
|
65
|
+
- [Concepts](docs/1_concepts/) — Mathematical foundations (vectors, products, duality, transforms)
|
|
66
|
+
- [API Reference](docs/3_api/api.md) — Public interface
|
|
67
|
+
- [Architecture](docs/5_dev/1_architecture.md) — Design philosophy and decisions
|
|
68
|
+
|
|
69
|
+
## Quick Start
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from morphis.elements import Frame, basis_vectors, euclidean_metric
|
|
73
|
+
from morphis.operations import normalize
|
|
74
|
+
from morphis.transforms import rotor
|
|
75
|
+
from numpy import pi
|
|
76
|
+
|
|
77
|
+
# Create a 3D Euclidean metric and basis vectors
|
|
78
|
+
g = euclidean_metric(3)
|
|
79
|
+
e1, e2, e3 = basis_vectors(g)
|
|
80
|
+
|
|
81
|
+
# Bivector: oriented plane of rotation
|
|
82
|
+
b = (e1 ^ e2).normalize()
|
|
83
|
+
|
|
84
|
+
# Frame: ordered collection of vectors
|
|
85
|
+
F = Frame(e1, e2, e3)
|
|
86
|
+
|
|
87
|
+
# Rotor: multivector that performs rotation
|
|
88
|
+
R = rotor(b, pi / 4)
|
|
89
|
+
|
|
90
|
+
# Transform vector and frame via sandwich product
|
|
91
|
+
e1_rotated = e1.transform(R)
|
|
92
|
+
F_rotated = F.transform(R)
|
|
93
|
+
```
|
|
94
|
+
|
|
62
95
|
## Installation
|
|
63
96
|
|
|
64
97
|
Requires Python 3.12+.
|
|
@@ -80,16 +113,16 @@ make install
|
|
|
80
113
|
```
|
|
81
114
|
morphis/
|
|
82
115
|
├── src/morphis/
|
|
83
|
-
│ ├── elements/ # Core GA objects:
|
|
116
|
+
│ ├── elements/ # Core GA objects: Vector, MultiVector, Frame, Metric
|
|
84
117
|
│ │ └── tests/
|
|
85
|
-
│ ├── algebra/ # Linear algebra:
|
|
118
|
+
│ ├── algebra/ # Linear algebra: VectorSpec, einsum patterns, solvers
|
|
86
119
|
│ │ └── tests/
|
|
87
120
|
│ ├── operations/ # GA operations: wedge, geometric product, duality, norms
|
|
88
121
|
│ │ └── tests/
|
|
89
122
|
│ ├── transforms/ # Rotors, translators, PGA, motor constructors
|
|
90
123
|
│ │ └── tests/
|
|
91
124
|
│ ├── visuals/ # PyVista rendering, animation, themes
|
|
92
|
-
│ │ └── drawing/ #
|
|
125
|
+
│ │ └── drawing/ # Vector mesh generation
|
|
93
126
|
│ ├── examples/ # Runnable demos
|
|
94
127
|
│ └── utils/ # Easing functions, observers, pretty printing
|
|
95
128
|
├── docs/ # Design documents
|
|
@@ -16,12 +16,45 @@ essential nature.
|
|
|
16
16
|
|
|
17
17
|
## Features
|
|
18
18
|
|
|
19
|
-
- **Geometric Algebra Core**:
|
|
19
|
+
- **Geometric Algebra Core**: Vectors (k-vectors), multivectors, and operations (wedge, geometric product, duality)
|
|
20
20
|
- **Metric-Aware**: Objects carry their metric context (Euclidean, projective, etc.)
|
|
21
|
-
- **Linear Operators**: Structured linear maps between
|
|
22
|
-
- **Visualization**: 3D rendering of
|
|
21
|
+
- **Linear Operators**: Structured linear maps between vector spaces with SVD, pseudoinverse, least squares
|
|
22
|
+
- **Visualization**: 3D rendering of vectors with PyVista, timeline-based animation, 4D projection
|
|
23
23
|
- **Motor Transforms**: Rotors and translations via sandwich product
|
|
24
24
|
|
|
25
|
+
## Documentation
|
|
26
|
+
|
|
27
|
+
- [Project Overview](docs/0_project-overview.md) — Vision and scope
|
|
28
|
+
- [Concepts](docs/1_concepts/) — Mathematical foundations (vectors, products, duality, transforms)
|
|
29
|
+
- [API Reference](docs/3_api/api.md) — Public interface
|
|
30
|
+
- [Architecture](docs/5_dev/1_architecture.md) — Design philosophy and decisions
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from morphis.elements import Frame, basis_vectors, euclidean_metric
|
|
36
|
+
from morphis.operations import normalize
|
|
37
|
+
from morphis.transforms import rotor
|
|
38
|
+
from numpy import pi
|
|
39
|
+
|
|
40
|
+
# Create a 3D Euclidean metric and basis vectors
|
|
41
|
+
g = euclidean_metric(3)
|
|
42
|
+
e1, e2, e3 = basis_vectors(g)
|
|
43
|
+
|
|
44
|
+
# Bivector: oriented plane of rotation
|
|
45
|
+
b = (e1 ^ e2).normalize()
|
|
46
|
+
|
|
47
|
+
# Frame: ordered collection of vectors
|
|
48
|
+
F = Frame(e1, e2, e3)
|
|
49
|
+
|
|
50
|
+
# Rotor: multivector that performs rotation
|
|
51
|
+
R = rotor(b, pi / 4)
|
|
52
|
+
|
|
53
|
+
# Transform vector and frame via sandwich product
|
|
54
|
+
e1_rotated = e1.transform(R)
|
|
55
|
+
F_rotated = F.transform(R)
|
|
56
|
+
```
|
|
57
|
+
|
|
25
58
|
## Installation
|
|
26
59
|
|
|
27
60
|
Requires Python 3.12+.
|
|
@@ -43,16 +76,16 @@ make install
|
|
|
43
76
|
```
|
|
44
77
|
morphis/
|
|
45
78
|
├── src/morphis/
|
|
46
|
-
│ ├── elements/ # Core GA objects:
|
|
79
|
+
│ ├── elements/ # Core GA objects: Vector, MultiVector, Frame, Metric
|
|
47
80
|
│ │ └── tests/
|
|
48
|
-
│ ├── algebra/ # Linear algebra:
|
|
81
|
+
│ ├── algebra/ # Linear algebra: VectorSpec, einsum patterns, solvers
|
|
49
82
|
│ │ └── tests/
|
|
50
83
|
│ ├── operations/ # GA operations: wedge, geometric product, duality, norms
|
|
51
84
|
│ │ └── tests/
|
|
52
85
|
│ ├── transforms/ # Rotors, translators, PGA, motor constructors
|
|
53
86
|
│ │ └── tests/
|
|
54
87
|
│ ├── visuals/ # PyVista rendering, animation, themes
|
|
55
|
-
│ │ └── drawing/ #
|
|
88
|
+
│ │ └── drawing/ # Vector mesh generation
|
|
56
89
|
│ ├── examples/ # Runnable demos
|
|
57
90
|
│ └── utils/ # Easing functions, observers, pretty printing
|
|
58
91
|
├── docs/ # Design documents
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Morphis - Geometric Algebra Library
|
|
3
|
+
|
|
4
|
+
A unified mathematical framework for geometric computation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import runpy
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
EXAMPLES = [
|
|
12
|
+
"clifford",
|
|
13
|
+
"duality",
|
|
14
|
+
"exterior",
|
|
15
|
+
"operators",
|
|
16
|
+
"phasors",
|
|
17
|
+
"rotations_3d",
|
|
18
|
+
"rotations_4d",
|
|
19
|
+
"transforms_pga",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main() -> None:
|
|
24
|
+
"""CLI entry point: morphis example [name]"""
|
|
25
|
+
args = sys.argv[1:]
|
|
26
|
+
|
|
27
|
+
# morphis (no args) or morphis example (no name)
|
|
28
|
+
if not args or args == ["example"]:
|
|
29
|
+
print("Usage: morphis example <name>")
|
|
30
|
+
print()
|
|
31
|
+
print("Available examples:")
|
|
32
|
+
for name in EXAMPLES:
|
|
33
|
+
print(f" {name}")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
# morphis example <name> [args...]
|
|
37
|
+
if args[0] == "example":
|
|
38
|
+
name = args[1] if len(args) > 1 else None
|
|
39
|
+
remaining = args[2:]
|
|
40
|
+
|
|
41
|
+
if name not in EXAMPLES:
|
|
42
|
+
print(f"Unknown example: {name}")
|
|
43
|
+
print(f"Available: {', '.join(EXAMPLES)}")
|
|
44
|
+
sys.exit(1)
|
|
45
|
+
|
|
46
|
+
module = f"morphis.examples.{name}"
|
|
47
|
+
sys.argv = [module] + remaining
|
|
48
|
+
runpy.run_module(module, run_name="__main__", alter_sys=True)
|
|
49
|
+
else:
|
|
50
|
+
print(f"Unknown command: {args[0]}")
|
|
51
|
+
print("Usage: morphis example <name>")
|
|
52
|
+
sys.exit(1)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
main()
|
|
@@ -192,7 +192,7 @@ def solve_rotation_angle(u: NDArray, v: NDArray, axis: NDArray) -> float:
|
|
|
192
192
|
|
|
193
193
|
|
|
194
194
|
# =============================================================================
|
|
195
|
-
#
|
|
195
|
+
# Vector Visual Transform Operations
|
|
196
196
|
# =============================================================================
|
|
197
197
|
|
|
198
198
|
|
|
@@ -204,7 +204,7 @@ def rotate_blade(blade, axis: NDArray, angle: float) -> None:
|
|
|
204
204
|
(composed with existing rotation).
|
|
205
205
|
|
|
206
206
|
Args:
|
|
207
|
-
blade:
|
|
207
|
+
blade: Vector to rotate (its visual_transform is modified)
|
|
208
208
|
axis: Rotation axis (will be normalized)
|
|
209
209
|
angle: Rotation angle in radians
|
|
210
210
|
"""
|
|
@@ -220,7 +220,7 @@ def translate_blade(blade, delta: NDArray) -> None:
|
|
|
220
220
|
This modifies blade.visual_transform in place. The translation is accumulated.
|
|
221
221
|
|
|
222
222
|
Args:
|
|
223
|
-
blade:
|
|
223
|
+
blade: Vector to translate (its visual_transform is modified)
|
|
224
224
|
delta: Translation vector
|
|
225
225
|
"""
|
|
226
226
|
blade.visual_transform.translation = blade.visual_transform.translation + array(delta)
|
|
@@ -231,7 +231,7 @@ def set_blade_position(blade, position: NDArray) -> None:
|
|
|
231
231
|
Set a blade's visual position (absolute, not relative).
|
|
232
232
|
|
|
233
233
|
Args:
|
|
234
|
-
blade:
|
|
234
|
+
blade: Vector to position (its visual_transform is modified)
|
|
235
235
|
position: New position vector
|
|
236
236
|
"""
|
|
237
237
|
blade.visual_transform.translation = array(position)
|
|
@@ -242,6 +242,6 @@ def reset_blade_transform(blade) -> None:
|
|
|
242
242
|
Reset a blade's visual transform to identity.
|
|
243
243
|
|
|
244
244
|
Args:
|
|
245
|
-
blade:
|
|
245
|
+
blade: Vector to reset (its visual_transform is modified)
|
|
246
246
|
"""
|
|
247
247
|
blade.visual_transform.reset()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Linear Algebra Module
|
|
3
|
+
|
|
4
|
+
Provides structured linear algebra utilities for geometric algebra operators.
|
|
5
|
+
Includes vector specifications, einsum pattern generation, solvers, and contraction.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from morphis.algebra.contraction import (
|
|
9
|
+
IndexedTensor as IndexedTensor,
|
|
10
|
+
contract as contract,
|
|
11
|
+
)
|
|
12
|
+
from morphis.algebra.patterns import (
|
|
13
|
+
INPUT_COLLECTION as INPUT_COLLECTION,
|
|
14
|
+
INPUT_GEOMETRIC as INPUT_GEOMETRIC,
|
|
15
|
+
OUTPUT_COLLECTION as OUTPUT_COLLECTION,
|
|
16
|
+
OUTPUT_GEOMETRIC as OUTPUT_GEOMETRIC,
|
|
17
|
+
adjoint_signature as adjoint_signature,
|
|
18
|
+
forward_signature as forward_signature,
|
|
19
|
+
operator_shape as operator_shape,
|
|
20
|
+
)
|
|
21
|
+
from morphis.algebra.solvers import (
|
|
22
|
+
structured_lstsq as structured_lstsq,
|
|
23
|
+
structured_pinv as structured_pinv,
|
|
24
|
+
structured_pinv_solve as structured_pinv_solve,
|
|
25
|
+
structured_svd as structured_svd,
|
|
26
|
+
)
|
|
27
|
+
from morphis.algebra.specs import VectorSpec as VectorSpec, vector_spec as vector_spec
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Linear Algebra - Tensor Contraction
|
|
3
|
+
|
|
4
|
+
Provides two contraction APIs for Morphis tensors:
|
|
5
|
+
1. Index notation: G["mnab"] * q["n"] - bracket syntax with IndexedTensor
|
|
6
|
+
2. Einsum-style: contract("mnab, n -> mab", G, q) - explicit signature
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from numpy import einsum
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from morphis.elements.vector import Vector
|
|
18
|
+
from morphis.operations.operator import Operator
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# =============================================================================
|
|
22
|
+
# IndexedTensor - Bracket Syntax API
|
|
23
|
+
# =============================================================================
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class IndexedTensor:
|
|
27
|
+
"""
|
|
28
|
+
Lightweight wrapper that pairs a tensor with index labels for contraction.
|
|
29
|
+
|
|
30
|
+
This class enables einsum-style syntax:
|
|
31
|
+
G["mnab"] * q["n"] # contracts on index 'n'
|
|
32
|
+
|
|
33
|
+
The wrapper holds a reference (not a copy) to the underlying tensor,
|
|
34
|
+
making indexing O(1). Computation only happens when two IndexedTensors
|
|
35
|
+
are multiplied.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
tensor: The underlying Vector or Operator (reference, not copy)
|
|
39
|
+
indices: String of index labels (e.g., "mnab")
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
>>> G = Operator(...) # lot=(M, N), grade=2 output
|
|
43
|
+
>>> q = Vector(...) # lot=(N,), grade=0
|
|
44
|
+
>>> b = G["mnab"] * q["n"] # contracts on 'n', result has indices "mab"
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
__slots__ = ("tensor", "indices")
|
|
48
|
+
|
|
49
|
+
def __init__(self, tensor: "Vector | Operator", indices: str):
|
|
50
|
+
"""
|
|
51
|
+
Create an indexed tensor wrapper.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
tensor: The underlying Vector or Operator
|
|
55
|
+
indices: String of index labels, one per axis of tensor.data
|
|
56
|
+
"""
|
|
57
|
+
self.tensor = tensor
|
|
58
|
+
self.indices = indices
|
|
59
|
+
|
|
60
|
+
# Validate index count matches tensor dimensions
|
|
61
|
+
expected_ndim = tensor.data.ndim
|
|
62
|
+
if len(indices) != expected_ndim:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Index string '{indices}' has {len(indices)} indices, but tensor has {expected_ndim} dimensions"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def __mul__(self, other: "IndexedTensor") -> "Vector":
|
|
68
|
+
"""
|
|
69
|
+
Contract two indexed tensors on matching indices.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
other: Another IndexedTensor to contract with
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Vector with the contracted result
|
|
76
|
+
"""
|
|
77
|
+
if not isinstance(other, IndexedTensor):
|
|
78
|
+
return NotImplemented
|
|
79
|
+
|
|
80
|
+
return _contract_indexed(self, other)
|
|
81
|
+
|
|
82
|
+
def __rmul__(self, other: "IndexedTensor") -> "Vector":
|
|
83
|
+
"""Right multiplication for contraction."""
|
|
84
|
+
if not isinstance(other, IndexedTensor):
|
|
85
|
+
return NotImplemented
|
|
86
|
+
|
|
87
|
+
return _contract_indexed(other, self)
|
|
88
|
+
|
|
89
|
+
def __repr__(self) -> str:
|
|
90
|
+
tensor_type = type(self.tensor).__name__
|
|
91
|
+
return f"IndexedTensor({tensor_type}, indices='{self.indices}')"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _contract_indexed(*indexed_tensors: IndexedTensor) -> "Vector":
|
|
95
|
+
"""
|
|
96
|
+
Contract multiple IndexedTensor objects.
|
|
97
|
+
|
|
98
|
+
Internal function that performs the actual contraction for bracket syntax.
|
|
99
|
+
"""
|
|
100
|
+
from morphis.elements.vector import Vector
|
|
101
|
+
|
|
102
|
+
if len(indexed_tensors) < 2:
|
|
103
|
+
raise ValueError("Contraction requires at least 2 indexed tensors")
|
|
104
|
+
|
|
105
|
+
# Collect all index information
|
|
106
|
+
all_indices = [it.indices for it in indexed_tensors]
|
|
107
|
+
all_data = [it.tensor.data for it in indexed_tensors]
|
|
108
|
+
|
|
109
|
+
# Count index occurrences to determine output indices
|
|
110
|
+
index_counts: dict[str, int] = {}
|
|
111
|
+
for indices in all_indices:
|
|
112
|
+
for idx in indices:
|
|
113
|
+
index_counts[idx] = index_counts.get(idx, 0) + 1
|
|
114
|
+
|
|
115
|
+
# Output indices are those that appear exactly once (not contracted)
|
|
116
|
+
# Preserve order of first appearance
|
|
117
|
+
seen = set()
|
|
118
|
+
output_indices = ""
|
|
119
|
+
for indices in all_indices:
|
|
120
|
+
for idx in indices:
|
|
121
|
+
if idx not in seen:
|
|
122
|
+
seen.add(idx)
|
|
123
|
+
if index_counts[idx] == 1:
|
|
124
|
+
output_indices += idx
|
|
125
|
+
|
|
126
|
+
# Build einsum signature
|
|
127
|
+
input_sig = ",".join(all_indices)
|
|
128
|
+
einsum_sig = f"{input_sig}->{output_indices}"
|
|
129
|
+
|
|
130
|
+
# Perform contraction
|
|
131
|
+
result_data = einsum(einsum_sig, *all_data)
|
|
132
|
+
|
|
133
|
+
# Get metric from first tensor that has one
|
|
134
|
+
metric = None
|
|
135
|
+
for it in indexed_tensors:
|
|
136
|
+
if hasattr(it.tensor, "metric") and it.tensor.metric is not None:
|
|
137
|
+
metric = it.tensor.metric
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
# Infer grade from output
|
|
141
|
+
result_grade = _infer_grade_from_indexed(indexed_tensors, output_indices)
|
|
142
|
+
|
|
143
|
+
return Vector(data=result_data, grade=result_grade, metric=metric)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _infer_grade_from_indexed(indexed_tensors: tuple[IndexedTensor, ...], output_indices: str) -> int:
|
|
147
|
+
"""Infer grade for IndexedTensor contraction result."""
|
|
148
|
+
from morphis.elements.vector import Vector
|
|
149
|
+
|
|
150
|
+
# Track which indices are geometric (vs lot)
|
|
151
|
+
geo_indices = set()
|
|
152
|
+
|
|
153
|
+
for it in indexed_tensors:
|
|
154
|
+
if isinstance(it.tensor, Vector):
|
|
155
|
+
n_lot = len(it.tensor.lot)
|
|
156
|
+
n_geo = it.tensor.grade
|
|
157
|
+
# Geometric indices are the last 'grade' indices
|
|
158
|
+
geo_part = it.indices[n_lot : n_lot + n_geo]
|
|
159
|
+
geo_indices.update(geo_part)
|
|
160
|
+
|
|
161
|
+
# Count geometric indices in output
|
|
162
|
+
result_grade = sum(1 for idx in output_indices if idx in geo_indices)
|
|
163
|
+
return result_grade
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# =============================================================================
|
|
167
|
+
# contract() - Einsum-Style API
|
|
168
|
+
# =============================================================================
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def contract(signature: str, *tensors: "Vector | Operator") -> "Vector":
|
|
172
|
+
"""
|
|
173
|
+
Einsum-style contraction for Morphis tensors.
|
|
174
|
+
|
|
175
|
+
Works exactly like numpy.einsum, but accepts Vector and Operator objects.
|
|
176
|
+
Extracts the underlying data, performs the einsum, and wraps the result
|
|
177
|
+
back into a Vector.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
signature: Einsum signature string (e.g., "mn, n -> m")
|
|
181
|
+
*tensors: Morphis objects (Vector or Operator) to contract
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Vector containing the contracted result
|
|
185
|
+
|
|
186
|
+
Examples:
|
|
187
|
+
>>> g = euclidean_metric(3)
|
|
188
|
+
>>> u = Vector([1, 2, 3], grade=1, metric=g)
|
|
189
|
+
>>> v = Vector([4, 5, 6], grade=1, metric=g)
|
|
190
|
+
|
|
191
|
+
>>> # Dot product
|
|
192
|
+
>>> s = contract("a, a ->", u, v)
|
|
193
|
+
>>> s.data # 1*4 + 2*5 + 3*6 = 32
|
|
194
|
+
|
|
195
|
+
>>> # Matrix-vector product
|
|
196
|
+
>>> M = Vector(data, grade=2, metric=g) # shape (3, 3)
|
|
197
|
+
>>> w = contract("ab, b -> a", M, v)
|
|
198
|
+
|
|
199
|
+
>>> # Outer product
|
|
200
|
+
>>> outer = contract("a, b -> ab", u, v)
|
|
201
|
+
|
|
202
|
+
>>> # Batch contraction
|
|
203
|
+
>>> G = Vector(data, grade=2, lot=(M, N), metric=g) # shape (M, N, 3, 3)
|
|
204
|
+
>>> q = Vector(data, grade=0, lot=(N,), metric=g) # shape (N,)
|
|
205
|
+
>>> b = contract("mnab, n -> mab", G, q)
|
|
206
|
+
"""
|
|
207
|
+
from morphis.elements.vector import Vector
|
|
208
|
+
|
|
209
|
+
if len(tensors) < 1:
|
|
210
|
+
raise ValueError("contract() requires at least 1 tensor")
|
|
211
|
+
|
|
212
|
+
# Extract data arrays from tensors
|
|
213
|
+
data_arrays = [t.data for t in tensors]
|
|
214
|
+
|
|
215
|
+
# Normalize signature: allow spaces around comma and arrow
|
|
216
|
+
sig = signature.replace(" ", "")
|
|
217
|
+
|
|
218
|
+
# Perform einsum
|
|
219
|
+
result_data = einsum(sig, *data_arrays)
|
|
220
|
+
|
|
221
|
+
# Get metric from first tensor that has one
|
|
222
|
+
metric = None
|
|
223
|
+
for t in tensors:
|
|
224
|
+
if hasattr(t, "metric") and t.metric is not None:
|
|
225
|
+
metric = t.metric
|
|
226
|
+
break
|
|
227
|
+
|
|
228
|
+
# Infer grade from output shape
|
|
229
|
+
result_grade = _infer_grade_from_signature(sig, tensors, result_data)
|
|
230
|
+
|
|
231
|
+
return Vector(data=result_data, grade=result_grade, metric=metric)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _infer_grade_from_signature(signature: str, tensors: tuple, result_data) -> int:
|
|
235
|
+
"""Infer grade for einsum-style contraction result."""
|
|
236
|
+
from morphis.elements.vector import Vector
|
|
237
|
+
|
|
238
|
+
# Parse signature to get output indices
|
|
239
|
+
if "->" in signature:
|
|
240
|
+
input_part, output_indices = signature.split("->")
|
|
241
|
+
else:
|
|
242
|
+
# No explicit output - numpy determines it
|
|
243
|
+
return 0 if result_data.ndim == 0 else result_data.ndim
|
|
244
|
+
|
|
245
|
+
input_parts = input_part.split(",")
|
|
246
|
+
|
|
247
|
+
# Track which indices are geometric (vs lot)
|
|
248
|
+
geo_indices = set()
|
|
249
|
+
|
|
250
|
+
for k, t in enumerate(tensors):
|
|
251
|
+
if k < len(input_parts) and isinstance(t, Vector):
|
|
252
|
+
indices = input_parts[k]
|
|
253
|
+
n_lot = len(t.lot)
|
|
254
|
+
n_geo = t.grade
|
|
255
|
+
# Geometric indices are the last 'grade' indices
|
|
256
|
+
if len(indices) >= n_lot + n_geo:
|
|
257
|
+
geo_part = indices[n_lot : n_lot + n_geo]
|
|
258
|
+
geo_indices.update(geo_part)
|
|
259
|
+
|
|
260
|
+
# Count geometric indices in output
|
|
261
|
+
result_grade = sum(1 for idx in output_indices if idx in geo_indices)
|
|
262
|
+
|
|
263
|
+
return result_grade
|