morphis 0.9.0__tar.gz → 0.11.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 (96) hide show
  1. {morphis-0.9.0 → morphis-0.11.0}/PKG-INFO +1 -1
  2. {morphis-0.9.0 → morphis-0.11.0}/pyproject.toml +7 -1
  3. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/algebra/contraction.py +32 -17
  4. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/algebra/tests/test_contraction.py +84 -0
  5. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/elements/__init__.py +1 -0
  6. morphis-0.11.0/src/morphis/elements/lot_indexed.py +566 -0
  7. morphis-0.11.0/src/morphis/elements/tests/test_maxwell_features.py +337 -0
  8. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/elements/vector.py +111 -8
  9. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/operator.py +7 -1
  10. {morphis-0.9.0 → morphis-0.11.0}/README.md +0 -0
  11. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/__init__.py +0 -0
  12. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/_legacy/__init__.py +0 -0
  13. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/_legacy/coordinates.py +0 -0
  14. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/_legacy/rotations.py +0 -0
  15. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/_legacy/smoothing.py +0 -0
  16. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/_legacy/vectors.py +0 -0
  17. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/algebra/__init__.py +0 -0
  18. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/algebra/patterns.py +0 -0
  19. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/algebra/solvers.py +0 -0
  20. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/algebra/specs.py +0 -0
  21. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/algebra/tests/__init__.py +0 -0
  22. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/algebra/tests/test_patterns.py +0 -0
  23. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/algebra/tests/test_solvers.py +0 -0
  24. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/algebra/tests/test_specs.py +0 -0
  25. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/config.py +0 -0
  26. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/elements/base.py +0 -0
  27. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/elements/frame.py +0 -0
  28. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/elements/metric.py +0 -0
  29. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/elements/multivector.py +0 -0
  30. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/elements/operator.py +0 -0
  31. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/elements/protocols.py +0 -0
  32. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/elements/tensor.py +0 -0
  33. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/elements/tests/__init__.py +0 -0
  34. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/elements/tests/test_complex_blades.py +0 -0
  35. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/elements/tests/test_model.py +0 -0
  36. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/elements/tests/test_operator.py +0 -0
  37. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/elements/tests/test_operators.py +0 -0
  38. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/elements/tests/test_tensor.py +0 -0
  39. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/examples/__init__.py +0 -0
  40. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/examples/clifford.py +0 -0
  41. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/examples/duality.py +0 -0
  42. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/examples/exterior.py +0 -0
  43. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/examples/operators.py +0 -0
  44. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/examples/phasors.py +0 -0
  45. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/examples/rotations_3d.py +0 -0
  46. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/examples/rotations_4d.py +0 -0
  47. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/examples/transforms_pga.py +0 -0
  48. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/manifold/__init__.py +0 -0
  49. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/__init__.py +0 -0
  50. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/_helpers.py +0 -0
  51. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/duality.py +0 -0
  52. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/exponential.py +0 -0
  53. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/factorization.py +0 -0
  54. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/matrix_rep.py +0 -0
  55. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/norms.py +0 -0
  56. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/outermorphism.py +0 -0
  57. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/products.py +0 -0
  58. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/projections.py +0 -0
  59. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/spectral.py +0 -0
  60. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/structure.py +0 -0
  61. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/subspaces.py +0 -0
  62. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/tests/__init__.py +0 -0
  63. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/tests/test_complex_operations.py +0 -0
  64. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/tests/test_duality.py +0 -0
  65. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/tests/test_exponential.py +0 -0
  66. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/tests/test_matrix_rep.py +0 -0
  67. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/tests/test_norms.py +0 -0
  68. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/tests/test_operations.py +0 -0
  69. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/tests/test_outermorphism.py +0 -0
  70. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/tests/test_products.py +0 -0
  71. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/tests/test_spectral.py +0 -0
  72. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/operations/tests/test_structure.py +0 -0
  73. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/topology/__init__.py +0 -0
  74. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/transforms/__init__.py +0 -0
  75. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/transforms/actions.py +0 -0
  76. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/transforms/projective.py +0 -0
  77. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/transforms/rotations.py +0 -0
  78. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/transforms/tests/__init__.py +0 -0
  79. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/transforms/tests/test_projective.py +0 -0
  80. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/utils/__init__.py +0 -0
  81. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/utils/docgen.py +0 -0
  82. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/utils/easing.py +0 -0
  83. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/utils/exceptions.py +0 -0
  84. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/utils/observer.py +0 -0
  85. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/utils/pretty.py +0 -0
  86. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/visuals/__init__.py +0 -0
  87. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/visuals/canvas.py +0 -0
  88. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/visuals/contexts.py +0 -0
  89. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/visuals/drawing/__init__.py +0 -0
  90. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/visuals/drawing/vectors.py +0 -0
  91. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/visuals/effects.py +0 -0
  92. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/visuals/loop.py +0 -0
  93. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/visuals/operations.py +0 -0
  94. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/visuals/projection.py +0 -0
  95. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/visuals/renderer.py +0 -0
  96. {morphis-0.9.0 → morphis-0.11.0}/src/morphis/visuals/theme.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: morphis
3
- Version: 0.9.0
3
+ Version: 0.11.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "morphis"
3
- version = "0.9.0"
3
+ version = "0.11.0"
4
4
  description = "A unified mathematical framework for geometric computation"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -51,3 +51,9 @@ Issues = "https://github.com/ctl-alt-leist/morphis/issues"
51
51
  [build-system]
52
52
  requires = ["uv_build>=0.9.18,<0.10.0"]
53
53
  build-backend = "uv_build"
54
+
55
+ [dependency-groups]
56
+ dev = [
57
+ "pre-commit>=4.5.1",
58
+ "ruff>=0.14.10",
59
+ ]
@@ -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
@@ -69,11 +74,24 @@ class IndexedTensor:
69
74
  Contract two indexed tensors on matching indices.
70
75
 
71
76
  Args:
72
- other: Another IndexedTensor to contract with
77
+ other: Another IndexedTensor or LotIndexed to contract with
73
78
 
74
79
  Returns:
75
80
  Vector with the contracted result
76
81
  """
82
+ from morphis.elements.lot_indexed import LotIndexed
83
+
84
+ if isinstance(other, LotIndexed):
85
+ # Convert LotIndexed to IndexedTensor by adding geo indices
86
+ # All of a Vector's geometric indices are "output geometric"
87
+ n_geo = other.vector.grade
88
+ geo_labels = "".join(chr(ord("A") + i) for i in range(n_geo))
89
+ other = IndexedTensor(
90
+ other.vector,
91
+ other.indices + geo_labels,
92
+ output_geo_indices=geo_labels,
93
+ )
94
+
77
95
  if not isinstance(other, IndexedTensor):
78
96
  return NotImplemented
79
97
 
@@ -144,23 +162,20 @@ def _contract_indexed(*indexed_tensors: IndexedTensor) -> "Vector":
144
162
 
145
163
 
146
164
  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()
165
+ """
166
+ Infer grade for IndexedTensor contraction result.
152
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()
153
174
  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)
175
+ all_output_geo.update(it.output_geo_indices)
160
176
 
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
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)
164
179
 
165
180
 
166
181
  # =============================================================================
@@ -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)
@@ -10,6 +10,7 @@ from morphis.elements.base import (
10
10
  GradedElement as GradedElement,
11
11
  )
12
12
  from morphis.elements.frame import Frame
13
+ from morphis.elements.lot_indexed import LotIndexed as LotIndexed
13
14
  from morphis.elements.metric import (
14
15
  PGA as PGA,
15
16
  STA as STA,