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.
Files changed (108) hide show
  1. {morphis-0.6.0 → morphis-0.9.0}/PKG-INFO +40 -7
  2. {morphis-0.6.0 → morphis-0.9.0}/README.md +39 -6
  3. {morphis-0.6.0 → morphis-0.9.0}/pyproject.toml +1 -1
  4. morphis-0.9.0/src/morphis/__init__.py +56 -0
  5. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/_legacy/rotations.py +5 -5
  6. morphis-0.9.0/src/morphis/algebra/__init__.py +27 -0
  7. morphis-0.9.0/src/morphis/algebra/contraction.py +263 -0
  8. morphis-0.9.0/src/morphis/algebra/patterns.py +196 -0
  9. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/algebra/solvers.py +47 -48
  10. morphis-0.9.0/src/morphis/algebra/specs.py +165 -0
  11. morphis-0.9.0/src/morphis/algebra/tests/test_contraction.py +224 -0
  12. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/algebra/tests/test_patterns.py +72 -71
  13. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/algebra/tests/test_solvers.py +80 -79
  14. morphis-0.9.0/src/morphis/algebra/tests/test_specs.py +169 -0
  15. morphis-0.9.0/src/morphis/config.py +12 -0
  16. morphis-0.9.0/src/morphis/elements/__init__.py +49 -0
  17. morphis-0.6.0/src/morphis/elements/elements.py → morphis-0.9.0/src/morphis/elements/base.py +81 -17
  18. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/elements/frame.py +111 -113
  19. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/elements/metric.py +25 -25
  20. morphis-0.9.0/src/morphis/elements/multivector.py +555 -0
  21. morphis-0.9.0/src/morphis/elements/operator.py +10 -0
  22. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/elements/protocols.py +52 -4
  23. morphis-0.9.0/src/morphis/elements/tensor.py +248 -0
  24. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/elements/tests/test_complex_blades.py +41 -41
  25. morphis-0.9.0/src/morphis/elements/tests/test_model.py +642 -0
  26. morphis-0.9.0/src/morphis/elements/tests/test_operator.py +389 -0
  27. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/elements/tests/test_operators.py +250 -243
  28. morphis-0.9.0/src/morphis/elements/tests/test_tensor.py +187 -0
  29. morphis-0.9.0/src/morphis/elements/vector.py +971 -0
  30. morphis-0.6.0/src/morphis/examples/ga_geometric.py → morphis-0.9.0/src/morphis/examples/clifford.py +79 -59
  31. morphis-0.9.0/src/morphis/examples/duality.py +258 -0
  32. morphis-0.6.0/src/morphis/examples/ga_ops.py → morphis-0.9.0/src/morphis/examples/exterior.py +139 -93
  33. morphis-0.6.0/src/morphis/examples/linear_operators.py → morphis-0.9.0/src/morphis/examples/operators.py +106 -90
  34. morphis-0.6.0/src/morphis/examples/ga_phasors.py → morphis-0.9.0/src/morphis/examples/phasors.py +84 -59
  35. morphis-0.6.0/src/morphis/examples/animate_3d.py → morphis-0.9.0/src/morphis/examples/rotations_3d.py +9 -10
  36. morphis-0.6.0/src/morphis/examples/animate_4d.py → morphis-0.9.0/src/morphis/examples/rotations_4d.py +10 -14
  37. morphis-0.9.0/src/morphis/examples/transforms_pga.py +319 -0
  38. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/__init__.py +12 -14
  39. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/_helpers.py +31 -31
  40. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/duality.py +33 -33
  41. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/exponential.py +33 -38
  42. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/factorization.py +56 -52
  43. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/matrix_rep.py +32 -32
  44. morphis-0.9.0/src/morphis/operations/norms.py +226 -0
  45. {morphis-0.6.0/src/morphis/elements → morphis-0.9.0/src/morphis/operations}/operator.py +138 -90
  46. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/outermorphism.py +9 -9
  47. morphis-0.9.0/src/morphis/operations/products.py +816 -0
  48. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/projections.py +35 -34
  49. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/spectral.py +20 -15
  50. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/subspaces.py +1 -1
  51. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_complex_operations.py +50 -50
  52. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_duality.py +22 -22
  53. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_exponential.py +72 -72
  54. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_matrix_rep.py +93 -92
  55. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_norms.py +91 -91
  56. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_operations.py +110 -110
  57. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_outermorphism.py +106 -100
  58. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_products.py +65 -65
  59. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_spectral.py +41 -41
  60. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/transforms/__init__.py +1 -1
  61. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/transforms/actions.py +26 -26
  62. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/transforms/projective.py +76 -71
  63. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/transforms/rotations.py +16 -14
  64. morphis-0.9.0/src/morphis/utils/docgen.py +461 -0
  65. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/utils/observer.py +24 -23
  66. morphis-0.9.0/src/morphis/utils/pretty.py +332 -0
  67. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/__init__.py +3 -3
  68. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/canvas.py +7 -6
  69. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/contexts.py +13 -16
  70. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/drawing/__init__.py +2 -2
  71. morphis-0.6.0/src/morphis/visuals/drawing/blades.py → morphis-0.9.0/src/morphis/visuals/drawing/vectors.py +52 -51
  72. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/effects.py +6 -7
  73. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/loop.py +35 -32
  74. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/operations.py +26 -24
  75. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/projection.py +28 -27
  76. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/renderer.py +11 -9
  77. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/visuals/theme.py +47 -38
  78. morphis-0.6.0/src/morphis/__init__.py +0 -2
  79. morphis-0.6.0/src/morphis/algebra/__init__.py +0 -43
  80. morphis-0.6.0/src/morphis/algebra/patterns.py +0 -181
  81. morphis-0.6.0/src/morphis/algebra/specs.py +0 -114
  82. morphis-0.6.0/src/morphis/algebra/tests/test_specs.py +0 -135
  83. morphis-0.6.0/src/morphis/elements/__init__.py +0 -71
  84. morphis-0.6.0/src/morphis/elements/blade.py +0 -710
  85. morphis-0.6.0/src/morphis/elements/multivector.py +0 -413
  86. morphis-0.6.0/src/morphis/elements/tests/test_model.py +0 -396
  87. morphis-0.6.0/src/morphis/elements/tests/test_operator.py +0 -380
  88. morphis-0.6.0/src/morphis/operations/norms.py +0 -145
  89. morphis-0.6.0/src/morphis/operations/products.py +0 -608
  90. morphis-0.6.0/src/morphis/transforms/tests/__init__.py +0 -0
  91. morphis-0.6.0/src/morphis/utils/pretty.py +0 -153
  92. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/_legacy/__init__.py +0 -0
  93. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/_legacy/coordinates.py +0 -0
  94. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/_legacy/smoothing.py +0 -0
  95. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/_legacy/vectors.py +0 -0
  96. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/algebra/tests/__init__.py +0 -0
  97. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/elements/tests/__init__.py +0 -0
  98. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/examples/__init__.py +0 -0
  99. {morphis-0.6.0/src/morphis/groups → morphis-0.9.0/src/morphis/manifold}/__init__.py +0 -0
  100. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/structure.py +0 -0
  101. {morphis-0.6.0/src/morphis/manifold → morphis-0.9.0/src/morphis/operations/tests}/__init__.py +0 -0
  102. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/operations/tests/test_structure.py +0 -0
  103. {morphis-0.6.0/src/morphis/operations/tests → morphis-0.9.0/src/morphis/topology}/__init__.py +0 -0
  104. {morphis-0.6.0/src/morphis/topology → morphis-0.9.0/src/morphis/transforms/tests}/__init__.py +0 -0
  105. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/transforms/tests/test_projective.py +0 -0
  106. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/utils/__init__.py +0 -0
  107. {morphis-0.6.0 → morphis-0.9.0}/src/morphis/utils/easing.py +0 -0
  108. {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.6.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**: Blades, multivectors, and operations (wedge, geometric product, duality)
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 blade spaces with SVD, pseudoinverse, least squares
59
- - **Visualization**: 3D rendering of blades with PyVista, timeline-based animation, 4D projection
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: Blade, MultiVector, Frame, Operator, Metric
116
+ │ ├── elements/ # Core GA objects: Vector, MultiVector, Frame, Metric
84
117
  │ │ └── tests/
85
- │ ├── algebra/ # Linear algebra: BladeSpec, einsum patterns, solvers
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/ # Blade mesh generation
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**: Blades, multivectors, and operations (wedge, geometric product, duality)
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 blade spaces with SVD, pseudoinverse, least squares
22
- - **Visualization**: 3D rendering of blades with PyVista, timeline-based animation, 4D projection
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: Blade, MultiVector, Frame, Operator, Metric
79
+ │ ├── elements/ # Core GA objects: Vector, MultiVector, Frame, Metric
47
80
  │ │ └── tests/
48
- │ ├── algebra/ # Linear algebra: BladeSpec, einsum patterns, solvers
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/ # Blade mesh generation
88
+ │ │ └── drawing/ # Vector mesh generation
56
89
  │ ├── examples/ # Runnable demos
57
90
  │ └── utils/ # Easing functions, observers, pretty printing
58
91
  ├── docs/ # Design documents
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "morphis"
3
- version = "0.6.0"
3
+ version = "0.9.0"
4
4
  description = "A unified mathematical framework for geometric computation"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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
- # Blade Visual Transform Operations
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: Blade to rotate (its visual_transform is modified)
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: Blade to translate (its visual_transform is modified)
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: Blade to position (its visual_transform is modified)
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: Blade to reset (its visual_transform is modified)
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