morphis 0.10.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.10.0 → morphis-0.11.0}/PKG-INFO +1 -1
  2. {morphis-0.10.0 → morphis-0.11.0}/pyproject.toml +1 -1
  3. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/algebra/contraction.py +24 -17
  4. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/algebra/tests/test_contraction.py +84 -0
  5. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/vector.py +3 -1
  6. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/operator.py +7 -1
  7. {morphis-0.10.0 → morphis-0.11.0}/README.md +0 -0
  8. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/__init__.py +0 -0
  9. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/_legacy/__init__.py +0 -0
  10. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/_legacy/coordinates.py +0 -0
  11. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/_legacy/rotations.py +0 -0
  12. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/_legacy/smoothing.py +0 -0
  13. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/_legacy/vectors.py +0 -0
  14. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/algebra/__init__.py +0 -0
  15. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/algebra/patterns.py +0 -0
  16. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/algebra/solvers.py +0 -0
  17. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/algebra/specs.py +0 -0
  18. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/algebra/tests/__init__.py +0 -0
  19. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/algebra/tests/test_patterns.py +0 -0
  20. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/algebra/tests/test_solvers.py +0 -0
  21. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/algebra/tests/test_specs.py +0 -0
  22. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/config.py +0 -0
  23. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/__init__.py +0 -0
  24. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/base.py +0 -0
  25. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/frame.py +0 -0
  26. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/lot_indexed.py +0 -0
  27. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/metric.py +0 -0
  28. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/multivector.py +0 -0
  29. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/operator.py +0 -0
  30. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/protocols.py +0 -0
  31. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/tensor.py +0 -0
  32. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/tests/__init__.py +0 -0
  33. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/tests/test_complex_blades.py +0 -0
  34. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/tests/test_maxwell_features.py +0 -0
  35. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/tests/test_model.py +0 -0
  36. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/tests/test_operator.py +0 -0
  37. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/tests/test_operators.py +0 -0
  38. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/elements/tests/test_tensor.py +0 -0
  39. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/examples/__init__.py +0 -0
  40. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/examples/clifford.py +0 -0
  41. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/examples/duality.py +0 -0
  42. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/examples/exterior.py +0 -0
  43. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/examples/operators.py +0 -0
  44. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/examples/phasors.py +0 -0
  45. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/examples/rotations_3d.py +0 -0
  46. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/examples/rotations_4d.py +0 -0
  47. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/examples/transforms_pga.py +0 -0
  48. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/manifold/__init__.py +0 -0
  49. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/__init__.py +0 -0
  50. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/_helpers.py +0 -0
  51. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/duality.py +0 -0
  52. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/exponential.py +0 -0
  53. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/factorization.py +0 -0
  54. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/matrix_rep.py +0 -0
  55. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/norms.py +0 -0
  56. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/outermorphism.py +0 -0
  57. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/products.py +0 -0
  58. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/projections.py +0 -0
  59. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/spectral.py +0 -0
  60. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/structure.py +0 -0
  61. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/subspaces.py +0 -0
  62. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/tests/__init__.py +0 -0
  63. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/tests/test_complex_operations.py +0 -0
  64. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/tests/test_duality.py +0 -0
  65. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/tests/test_exponential.py +0 -0
  66. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/tests/test_matrix_rep.py +0 -0
  67. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/tests/test_norms.py +0 -0
  68. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/tests/test_operations.py +0 -0
  69. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/tests/test_outermorphism.py +0 -0
  70. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/tests/test_products.py +0 -0
  71. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/tests/test_spectral.py +0 -0
  72. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/operations/tests/test_structure.py +0 -0
  73. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/topology/__init__.py +0 -0
  74. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/transforms/__init__.py +0 -0
  75. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/transforms/actions.py +0 -0
  76. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/transforms/projective.py +0 -0
  77. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/transforms/rotations.py +0 -0
  78. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/transforms/tests/__init__.py +0 -0
  79. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/transforms/tests/test_projective.py +0 -0
  80. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/utils/__init__.py +0 -0
  81. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/utils/docgen.py +0 -0
  82. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/utils/easing.py +0 -0
  83. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/utils/exceptions.py +0 -0
  84. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/utils/observer.py +0 -0
  85. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/utils/pretty.py +0 -0
  86. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/visuals/__init__.py +0 -0
  87. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/visuals/canvas.py +0 -0
  88. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/visuals/contexts.py +0 -0
  89. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/visuals/drawing/__init__.py +0 -0
  90. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/visuals/drawing/vectors.py +0 -0
  91. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/visuals/effects.py +0 -0
  92. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/visuals/loop.py +0 -0
  93. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/visuals/operations.py +0 -0
  94. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/visuals/projection.py +0 -0
  95. {morphis-0.10.0 → morphis-0.11.0}/src/morphis/visuals/renderer.py +0 -0
  96. {morphis-0.10.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.10.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.10.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"
@@ -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(other.vector, other.indices + geo_labels)
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
- """Infer grade for IndexedTensor contraction result."""
156
- from morphis.elements.vector import Vector
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
- if isinstance(it.tensor, Vector):
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
- # Count geometric indices in output
170
- result_grade = sum(1 for idx in output_indices if idx in geo_indices)
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
  # =============================================================================
@@ -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)
@@ -375,9 +375,11 @@ class Vector(IndexableMixin, Tensor):
375
375
  return LotIndexed(self, indices)
376
376
  elif len(indices) == n_total:
377
377
  # Full indexing -> IndexedTensor
378
+ # All geometric indices (after lot) are "output geometric" for Vectors
378
379
  from morphis.algebra.contraction import IndexedTensor
379
380
 
380
- return IndexedTensor(self, indices)
381
+ output_geo_indices = indices[n_lot:]
382
+ return IndexedTensor(self, indices, output_geo_indices=output_geo_indices)
381
383
  else:
382
384
  raise ValueError(
383
385
  f"Index string '{indices}' has {len(indices)} indices, but expected "
@@ -249,7 +249,13 @@ class Operator(IndexableMixin):
249
249
  """
250
250
  from morphis.algebra.contraction import IndexedTensor
251
251
 
252
- return IndexedTensor(self, indices)
252
+ # Operator layout: (*out_lot, *in_lot, *out_geo, *in_geo)
253
+ # Output geometric indices start after lot dimensions
254
+ lot_end = self.output_spec.collection + self.input_spec.collection
255
+ out_geo_end = lot_end + self.output_spec.grade
256
+ output_geo_indices = indices[lot_end:out_geo_end]
257
+
258
+ return IndexedTensor(self, indices, output_geo_indices=output_geo_indices)
253
259
 
254
260
  def _slice(self, key):
255
261
  """
File without changes
File without changes