morphis 0.8.0__py3-none-any.whl → 0.9.0__py3-none-any.whl
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/algebra/__init__.py +5 -1
- morphis/algebra/contraction.py +263 -0
- morphis/algebra/patterns.py +82 -67
- morphis/algebra/solvers.py +35 -36
- morphis/algebra/tests/test_contraction.py +224 -0
- morphis/algebra/tests/test_patterns.py +71 -70
- morphis/algebra/tests/test_solvers.py +49 -49
- morphis/elements/__init__.py +6 -1
- morphis/elements/base.py +60 -1
- morphis/elements/frame.py +2 -2
- morphis/elements/multivector.py +44 -0
- morphis/elements/protocols.py +49 -1
- morphis/elements/tests/test_model.py +149 -0
- morphis/elements/tests/test_operator.py +82 -74
- morphis/elements/tests/test_operators.py +37 -33
- morphis/elements/vector.py +187 -19
- morphis/examples/exterior.py +8 -8
- morphis/examples/operators.py +45 -39
- morphis/examples/phasors.py +19 -19
- morphis/examples/rotations_3d.py +2 -2
- morphis/examples/rotations_4d.py +3 -3
- morphis/operations/__init__.py +3 -3
- morphis/operations/factorization.py +6 -6
- morphis/operations/norms.py +133 -53
- morphis/operations/operator.py +100 -52
- morphis/operations/projections.py +3 -3
- morphis/operations/spectral.py +3 -3
- morphis/operations/tests/test_matrix_rep.py +11 -10
- morphis/operations/tests/test_norms.py +46 -46
- morphis/operations/tests/test_outermorphism.py +50 -45
- morphis/utils/docgen.py +1 -0
- {morphis-0.8.0.dist-info → morphis-0.9.0.dist-info}/METADATA +1 -1
- {morphis-0.8.0.dist-info → morphis-0.9.0.dist-info}/RECORD +35 -33
- {morphis-0.8.0.dist-info → morphis-0.9.0.dist-info}/WHEEL +0 -0
- {morphis-0.8.0.dist-info → morphis-0.9.0.dist-info}/entry_points.txt +0 -0
morphis/algebra/__init__.py
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
Linear Algebra Module
|
|
3
3
|
|
|
4
4
|
Provides structured linear algebra utilities for geometric algebra operators.
|
|
5
|
-
Includes vector specifications, einsum pattern generation, and
|
|
5
|
+
Includes vector specifications, einsum pattern generation, solvers, and contraction.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from morphis.algebra.contraction import (
|
|
9
|
+
IndexedTensor as IndexedTensor,
|
|
10
|
+
contract as contract,
|
|
11
|
+
)
|
|
8
12
|
from morphis.algebra.patterns import (
|
|
9
13
|
INPUT_COLLECTION as INPUT_COLLECTION,
|
|
10
14
|
INPUT_GEOMETRIC as INPUT_GEOMETRIC,
|
|
@@ -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
|
morphis/algebra/patterns.py
CHANGED
|
@@ -5,13 +5,14 @@ Generates einsum signatures for linear operator operations. Uses disjoint index
|
|
|
5
5
|
pools to avoid collisions between input and output indices.
|
|
6
6
|
|
|
7
7
|
Index naming convention:
|
|
8
|
+
- OUTPUT_LOT: "KLMN" (up to 4 output lot dims)
|
|
9
|
+
- INPUT_LOT: "nopq" (up to 4 input lot dims)
|
|
8
10
|
- OUTPUT_GEOMETRIC: "WXYZ" (up to grade-4 output blades)
|
|
9
|
-
- OUTPUT_COLLECTION: "KLMN" (up to 4 output collection dims)
|
|
10
|
-
- INPUT_COLLECTION: "nopq" (up to 4 input collection dims)
|
|
11
11
|
- INPUT_GEOMETRIC: "abcd" (up to grade-4 input blades)
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
Storage conventions (lot-first, matching Vector layout):
|
|
14
|
+
- Operator: (*out_lot, *in_lot, *out_geo, *in_geo)
|
|
15
|
+
- Vector: (*lot, *geo)
|
|
15
16
|
"""
|
|
16
17
|
|
|
17
18
|
from functools import lru_cache
|
|
@@ -20,67 +21,71 @@ from morphis.algebra.specs import VectorSpec
|
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
# Index pools (disjoint to avoid collisions)
|
|
24
|
+
OUTPUT_LOT = "KLMN"
|
|
25
|
+
INPUT_LOT = "nopq"
|
|
23
26
|
OUTPUT_GEOMETRIC = "WXYZ"
|
|
24
|
-
OUTPUT_COLLECTION = "KLMN"
|
|
25
|
-
INPUT_COLLECTION = "nopq"
|
|
26
27
|
INPUT_GEOMETRIC = "abcd"
|
|
27
28
|
|
|
29
|
+
# Backwards compatibility aliases
|
|
30
|
+
OUTPUT_COLLECTION = OUTPUT_LOT
|
|
31
|
+
INPUT_COLLECTION = INPUT_LOT
|
|
32
|
+
|
|
28
33
|
|
|
29
34
|
@lru_cache(maxsize=128)
|
|
30
35
|
def forward_signature(input_spec: VectorSpec, output_spec: VectorSpec) -> str:
|
|
31
36
|
"""
|
|
32
37
|
Generate einsum signature for forward operator application: y = L * x
|
|
33
38
|
|
|
34
|
-
Operator data has shape: (*
|
|
35
|
-
Input
|
|
36
|
-
Output
|
|
39
|
+
Operator data has shape: (*out_lot, *in_lot, *out_geo, *in_geo)
|
|
40
|
+
Input Vector has shape: (*in_lot, *in_geo)
|
|
41
|
+
Output Vector has shape: (*out_lot, *out_geo)
|
|
37
42
|
|
|
38
43
|
Args:
|
|
39
|
-
input_spec: Specification of input
|
|
40
|
-
output_spec: Specification of output
|
|
44
|
+
input_spec: Specification of input Vector
|
|
45
|
+
output_spec: Specification of output Vector
|
|
41
46
|
|
|
42
47
|
Returns:
|
|
43
|
-
Einsum signature string, e.g., "
|
|
48
|
+
Einsum signature string, e.g., "KnWX,n->KWX" for scalar->bivector
|
|
44
49
|
|
|
45
50
|
Examples:
|
|
46
51
|
>>> # Scalar currents (N,) to bivector fields (M, 3, 3)
|
|
47
52
|
>>> sig = forward_signature(
|
|
48
|
-
... VectorSpec(grade=0,
|
|
49
|
-
... VectorSpec(grade=2,
|
|
53
|
+
... VectorSpec(grade=0, lot=(1,), dim=3),
|
|
54
|
+
... VectorSpec(grade=2, lot=(1,), dim=3),
|
|
50
55
|
... )
|
|
51
56
|
>>> sig
|
|
52
|
-
'
|
|
57
|
+
'KnWX,n->KWX'
|
|
53
58
|
|
|
54
59
|
>>> # Vector (N, 3) to bivector (M, 3, 3)
|
|
55
60
|
>>> sig = forward_signature(
|
|
56
|
-
... VectorSpec(grade=1,
|
|
57
|
-
... VectorSpec(grade=2,
|
|
61
|
+
... VectorSpec(grade=1, lot=(1,), dim=3),
|
|
62
|
+
... VectorSpec(grade=2, lot=(1,), dim=3),
|
|
58
63
|
... )
|
|
59
64
|
>>> sig
|
|
60
|
-
'
|
|
65
|
+
'KnWXa,na->KWX'
|
|
61
66
|
"""
|
|
62
67
|
_validate_spec_limits(input_spec, output_spec)
|
|
63
68
|
|
|
64
|
-
# Output
|
|
65
|
-
|
|
69
|
+
# Output lot indices
|
|
70
|
+
out_lot = OUTPUT_LOT[: output_spec.collection]
|
|
66
71
|
|
|
67
|
-
#
|
|
68
|
-
|
|
72
|
+
# Input lot indices (contracted)
|
|
73
|
+
in_lot = INPUT_LOT[: input_spec.collection]
|
|
69
74
|
|
|
70
|
-
#
|
|
71
|
-
|
|
75
|
+
# Output geometric indices
|
|
76
|
+
out_geo = OUTPUT_GEOMETRIC[: output_spec.grade]
|
|
72
77
|
|
|
73
78
|
# Input geometric indices (contracted)
|
|
74
79
|
in_geo = INPUT_GEOMETRIC[: input_spec.grade]
|
|
75
80
|
|
|
76
|
-
# Build operator signature:
|
|
77
|
-
op_indices =
|
|
81
|
+
# Build operator signature: out_lot + in_lot + out_geo + in_geo (lot-first)
|
|
82
|
+
op_indices = out_lot + in_lot + out_geo + in_geo
|
|
78
83
|
|
|
79
|
-
# Build input signature:
|
|
80
|
-
input_indices =
|
|
84
|
+
# Build input signature: in_lot + in_geo (Vector storage order)
|
|
85
|
+
input_indices = in_lot + in_geo
|
|
81
86
|
|
|
82
|
-
# Build output signature:
|
|
83
|
-
output_indices =
|
|
87
|
+
# Build output signature: out_lot + out_geo (Vector storage order)
|
|
88
|
+
output_indices = out_lot + out_geo
|
|
84
89
|
|
|
85
90
|
return f"{op_indices},{input_indices}->{output_indices}"
|
|
86
91
|
|
|
@@ -102,28 +107,28 @@ def adjoint_signature(input_spec: VectorSpec, output_spec: VectorSpec) -> str:
|
|
|
102
107
|
Examples:
|
|
103
108
|
>>> # Adjoint of scalar->bivector: bivector->scalar
|
|
104
109
|
>>> sig = adjoint_signature(
|
|
105
|
-
... VectorSpec(grade=0,
|
|
106
|
-
... VectorSpec(grade=2,
|
|
110
|
+
... VectorSpec(grade=0, lot=(1,), dim=3),
|
|
111
|
+
... VectorSpec(grade=2, lot=(1,), dim=3),
|
|
107
112
|
... )
|
|
108
113
|
>>> sig
|
|
109
|
-
'
|
|
114
|
+
'KnWX,KWX->n'
|
|
110
115
|
"""
|
|
111
116
|
_validate_spec_limits(input_spec, output_spec)
|
|
112
117
|
|
|
113
118
|
# Same index allocation as forward
|
|
119
|
+
out_lot = OUTPUT_LOT[: output_spec.collection]
|
|
120
|
+
in_lot = INPUT_LOT[: input_spec.collection]
|
|
114
121
|
out_geo = OUTPUT_GEOMETRIC[: output_spec.grade]
|
|
115
|
-
out_coll = OUTPUT_COLLECTION[: output_spec.collection]
|
|
116
|
-
in_coll = INPUT_COLLECTION[: input_spec.collection]
|
|
117
122
|
in_geo = INPUT_GEOMETRIC[: input_spec.grade]
|
|
118
123
|
|
|
119
|
-
# Operator indices
|
|
120
|
-
op_indices =
|
|
124
|
+
# Operator indices: out_lot + in_lot + out_geo + in_geo (lot-first)
|
|
125
|
+
op_indices = out_lot + in_lot + out_geo + in_geo
|
|
121
126
|
|
|
122
|
-
# Adjoint input (original output vec):
|
|
123
|
-
adjoint_input =
|
|
127
|
+
# Adjoint input (original output vec): out_lot + out_geo
|
|
128
|
+
adjoint_input = out_lot + out_geo
|
|
124
129
|
|
|
125
|
-
# Adjoint output (original input space):
|
|
126
|
-
adjoint_output =
|
|
130
|
+
# Adjoint output (original input space): in_lot + in_geo
|
|
131
|
+
adjoint_output = in_lot + in_geo
|
|
127
132
|
|
|
128
133
|
return f"{op_indices},{adjoint_input}->{adjoint_output}"
|
|
129
134
|
|
|
@@ -131,42 +136,52 @@ def adjoint_signature(input_spec: VectorSpec, output_spec: VectorSpec) -> str:
|
|
|
131
136
|
def operator_shape(
|
|
132
137
|
input_spec: VectorSpec,
|
|
133
138
|
output_spec: VectorSpec,
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
input_lot: tuple[int, ...] | None = None,
|
|
140
|
+
output_lot: tuple[int, ...] | None = None,
|
|
141
|
+
*,
|
|
142
|
+
input_collection: tuple[int, ...] | None = None,
|
|
143
|
+
output_collection: tuple[int, ...] | None = None,
|
|
136
144
|
) -> tuple[int, ...]:
|
|
137
145
|
"""
|
|
138
|
-
Compute the expected shape of operator data given specs and
|
|
146
|
+
Compute the expected shape of operator data given specs and lot shapes.
|
|
139
147
|
|
|
140
148
|
Args:
|
|
141
|
-
input_spec: Specification of input
|
|
142
|
-
output_spec: Specification of output
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
input_spec: Specification of input Vector
|
|
150
|
+
output_spec: Specification of output Vector
|
|
151
|
+
input_lot: Shape of input lot dimensions
|
|
152
|
+
output_lot: Shape of output lot dimensions
|
|
153
|
+
input_collection: DEPRECATED alias for input_lot
|
|
154
|
+
output_collection: DEPRECATED alias for output_lot
|
|
145
155
|
|
|
146
156
|
Returns:
|
|
147
|
-
Operator data shape: (*
|
|
157
|
+
Operator data shape: (*out_lot, *in_lot, *out_geo, *in_geo)
|
|
148
158
|
|
|
149
159
|
Examples:
|
|
150
160
|
>>> # Scalar (N=5) to bivector (M=10) in 3D
|
|
151
161
|
>>> shape = operator_shape(
|
|
152
|
-
... VectorSpec(grade=0,
|
|
153
|
-
... VectorSpec(grade=2,
|
|
154
|
-
...
|
|
155
|
-
...
|
|
162
|
+
... VectorSpec(grade=0, lot=(1,), dim=3),
|
|
163
|
+
... VectorSpec(grade=2, lot=(1,), dim=3),
|
|
164
|
+
... input_lot=(5,),
|
|
165
|
+
... output_lot=(10,),
|
|
156
166
|
... )
|
|
157
167
|
>>> shape
|
|
158
|
-
(
|
|
168
|
+
(10, 5, 3, 3)
|
|
159
169
|
"""
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
)
|
|
170
|
+
# Handle deprecated aliases
|
|
171
|
+
if input_lot is None and input_collection is not None:
|
|
172
|
+
input_lot = input_collection
|
|
173
|
+
if output_lot is None and output_collection is not None:
|
|
174
|
+
output_lot = output_collection
|
|
175
|
+
|
|
176
|
+
if input_lot is None or output_lot is None:
|
|
177
|
+
raise ValueError("input_lot and output_lot are required")
|
|
178
|
+
|
|
179
|
+
if len(input_lot) != input_spec.collection:
|
|
180
|
+
raise ValueError(f"input_lot has {len(input_lot)} dims, but input_spec expects {input_spec.collection}")
|
|
181
|
+
if len(output_lot) != output_spec.collection:
|
|
182
|
+
raise ValueError(f"output_lot has {len(output_lot)} dims, but output_spec expects {output_spec.collection}")
|
|
168
183
|
|
|
169
|
-
return
|
|
184
|
+
return output_lot + input_lot + output_spec.geo + input_spec.geo
|
|
170
185
|
|
|
171
186
|
|
|
172
187
|
def _validate_spec_limits(input_spec: VectorSpec, output_spec: VectorSpec) -> None:
|
|
@@ -175,7 +190,7 @@ def _validate_spec_limits(input_spec: VectorSpec, output_spec: VectorSpec) -> No
|
|
|
175
190
|
raise ValueError(f"Input grade {input_spec.grade} exceeds index pool limit {len(INPUT_GEOMETRIC)}")
|
|
176
191
|
if output_spec.grade > len(OUTPUT_GEOMETRIC):
|
|
177
192
|
raise ValueError(f"Output grade {output_spec.grade} exceeds index pool limit {len(OUTPUT_GEOMETRIC)}")
|
|
178
|
-
if input_spec.collection > len(
|
|
179
|
-
raise ValueError(f"Input
|
|
180
|
-
if output_spec.collection > len(
|
|
181
|
-
raise ValueError(f"Output
|
|
193
|
+
if input_spec.collection > len(INPUT_LOT):
|
|
194
|
+
raise ValueError(f"Input lot dims {input_spec.collection} exceeds limit {len(INPUT_LOT)}")
|
|
195
|
+
if output_spec.collection > len(OUTPUT_LOT):
|
|
196
|
+
raise ValueError(f"Output lot dims {output_spec.collection} exceeds limit {len(OUTPUT_LOT)}")
|
morphis/algebra/solvers.py
CHANGED
|
@@ -30,7 +30,7 @@ def _to_matrix(op: "Operator") -> NDArray:
|
|
|
30
30
|
"""
|
|
31
31
|
Flatten operator to matrix form for linear algebra operations.
|
|
32
32
|
|
|
33
|
-
Reshapes from (*
|
|
33
|
+
Reshapes from (*out_lot, *in_lot, *out_geo, *in_geo) to (out_flat, in_flat).
|
|
34
34
|
|
|
35
35
|
Args:
|
|
36
36
|
op: Operator to flatten
|
|
@@ -41,22 +41,20 @@ def _to_matrix(op: "Operator") -> NDArray:
|
|
|
41
41
|
out_flat = int(prod(op.output_shape))
|
|
42
42
|
in_flat = int(prod(op.input_shape))
|
|
43
43
|
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
in_coll_axes = list(range(in_coll_start, in_coll_start + op.input_spec.collection))
|
|
55
|
-
in_geo_start = in_coll_start + op.input_spec.collection
|
|
44
|
+
# Current lot-first layout: (*out_lot, *in_lot, *out_geo, *in_geo)
|
|
45
|
+
# Target for reshape: (*out_lot, *out_geo, *in_lot, *in_geo)
|
|
46
|
+
# This groups output dims together (lot, geo) and input dims together (lot, geo)
|
|
47
|
+
|
|
48
|
+
out_lot_axes = list(range(op.output_spec.collection))
|
|
49
|
+
in_lot_start = op.output_spec.collection
|
|
50
|
+
in_lot_axes = list(range(in_lot_start, in_lot_start + op.input_spec.collection))
|
|
51
|
+
out_geo_start = in_lot_start + op.input_spec.collection
|
|
52
|
+
out_geo_axes = list(range(out_geo_start, out_geo_start + op.output_spec.grade))
|
|
53
|
+
in_geo_start = out_geo_start + op.output_spec.grade
|
|
56
54
|
in_geo_axes = list(range(in_geo_start, in_geo_start + op.input_spec.grade))
|
|
57
55
|
|
|
58
|
-
# Reorder to: (*
|
|
59
|
-
perm =
|
|
56
|
+
# Reorder to: (*out_lot, *out_geo, *in_lot, *in_geo)
|
|
57
|
+
perm = out_lot_axes + out_geo_axes + in_lot_axes + in_geo_axes
|
|
60
58
|
reordered = op.data.transpose(perm)
|
|
61
59
|
|
|
62
60
|
return reordered.reshape(out_flat, in_flat)
|
|
@@ -86,22 +84,22 @@ def _from_matrix(
|
|
|
86
84
|
"""
|
|
87
85
|
from morphis.operations.operator import Operator
|
|
88
86
|
|
|
89
|
-
# Reshape from (out_flat, in_flat) to (*
|
|
87
|
+
# Reshape from (out_flat, in_flat) to (*out_lot, *out_geo, *in_lot, *in_geo)
|
|
90
88
|
intermediate_shape = output_collection + output_spec.geometric_shape + input_collection + input_spec.geometric_shape
|
|
91
89
|
tensor = matrix.reshape(intermediate_shape)
|
|
92
90
|
|
|
93
|
-
# Reorder from (*
|
|
94
|
-
# to (*
|
|
95
|
-
|
|
91
|
+
# Reorder from (*out_lot, *out_geo, *in_lot, *in_geo)
|
|
92
|
+
# to (*out_lot, *in_lot, *out_geo, *in_geo)
|
|
93
|
+
out_lot_axes = list(range(output_spec.collection))
|
|
96
94
|
out_geo_start = output_spec.collection
|
|
97
95
|
out_geo_axes = list(range(out_geo_start, out_geo_start + output_spec.grade))
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
in_geo_start =
|
|
96
|
+
in_lot_start = out_geo_start + output_spec.grade
|
|
97
|
+
in_lot_axes = list(range(in_lot_start, in_lot_start + input_spec.collection))
|
|
98
|
+
in_geo_start = in_lot_start + input_spec.collection
|
|
101
99
|
in_geo_axes = list(range(in_geo_start, in_geo_start + input_spec.grade))
|
|
102
100
|
|
|
103
|
-
# Target order: (*
|
|
104
|
-
perm =
|
|
101
|
+
# Target order: (*out_lot, *in_lot, *out_geo, *in_geo)
|
|
102
|
+
perm = out_lot_axes + in_lot_axes + out_geo_axes + in_geo_axes
|
|
105
103
|
data = tensor.transpose(perm)
|
|
106
104
|
|
|
107
105
|
return Operator(
|
|
@@ -266,24 +264,25 @@ def structured_svd(
|
|
|
266
264
|
dim = op.output_spec.dim
|
|
267
265
|
|
|
268
266
|
# U maps from reduced space (r,) to output space
|
|
269
|
-
u_input_spec = VectorSpec(grade=0,
|
|
267
|
+
u_input_spec = VectorSpec(grade=0, lot=(1,), dim=dim)
|
|
270
268
|
|
|
271
269
|
# Vt maps from input space to reduced space (r,)
|
|
272
|
-
vt_output_spec = VectorSpec(grade=0,
|
|
270
|
+
vt_output_spec = VectorSpec(grade=0, lot=(1,), dim=dim)
|
|
273
271
|
|
|
274
272
|
# Wrap U as Operator
|
|
275
273
|
# U_mat has shape (out_flat, r)
|
|
276
|
-
#
|
|
277
|
-
#
|
|
274
|
+
# Reshape to (*out_lot, *out_geo, r) then reorder to lot-first: (*out_lot, r, *out_geo)
|
|
275
|
+
# out_flat = prod(out_lot) * prod(out_geo), input is (r,) which is just lot dims
|
|
278
276
|
U_intermediate = U_mat.reshape(op.output_collection + op.output_spec.geometric_shape + (r,))
|
|
279
277
|
|
|
280
|
-
|
|
278
|
+
out_lot_axes = list(range(op.output_spec.collection))
|
|
281
279
|
out_geo_start = op.output_spec.collection
|
|
282
280
|
out_geo_axes = list(range(out_geo_start, out_geo_start + op.output_spec.grade))
|
|
283
281
|
r_axis = [out_geo_start + op.output_spec.grade]
|
|
284
282
|
|
|
285
|
-
# Target: (*
|
|
286
|
-
|
|
283
|
+
# Target lot-first: (*out_lot, r, *out_geo, no_in_geo)
|
|
284
|
+
# Since in_spec is grade=0, no in_geo axes
|
|
285
|
+
U_perm = out_lot_axes + r_axis + out_geo_axes
|
|
287
286
|
U_data = U_intermediate.transpose(U_perm)
|
|
288
287
|
|
|
289
288
|
U_op = Operator(
|
|
@@ -295,13 +294,13 @@ def structured_svd(
|
|
|
295
294
|
|
|
296
295
|
# Wrap Vt as Operator
|
|
297
296
|
# Vt_mat has shape (r, in_flat)
|
|
298
|
-
#
|
|
297
|
+
# Reshape to (r, *in_lot, *in_geo)
|
|
299
298
|
Vt_intermediate = Vt_mat.reshape((r,) + op.input_collection + op.input_spec.geometric_shape)
|
|
300
299
|
|
|
301
|
-
# Current order: (r, *
|
|
302
|
-
# Target
|
|
303
|
-
# For Vt, out is
|
|
304
|
-
# So target: (r, *
|
|
300
|
+
# Current order: (r, *in_lot, *in_geo)
|
|
301
|
+
# Target lot-first: (*out_lot, *in_lot, *out_geo, *in_geo)
|
|
302
|
+
# For Vt, out is (r,) so out_lot=(r,) and out_geo=() since grade=0
|
|
303
|
+
# So target: (r, *in_lot, *in_geo) which is already correct!
|
|
305
304
|
Vt_data = Vt_intermediate
|
|
306
305
|
|
|
307
306
|
Vt_op = Operator(
|