galaga-matrix 1.7.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.
@@ -0,0 +1,21 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ .coverage
12
+ .DS_Store
13
+ __marimo__/
14
+ # Vim
15
+ *.swp
16
+ *.swo
17
+ *~
18
+ test.py
19
+ .kiro/
20
+ .session.jsonl
21
+ .spacebot/
@@ -0,0 +1,652 @@
1
+ This is a copy of a shared ChatGPT conversation
2
+
3
+ Report conversation
4
+
5
+
6
+
7
+ Uploaded a file
8
+
9
+ I wrote: Review this spec for my Geometric Algebra library, and it's matrix conversion package. Mathematically correctness is critical
10
+
11
+ Chat GPT replied:
12
+
13
+ I’ll treat this as a correctness review rather than a wording pass: I’m going to check the classification, dimension counts, idempotent/ideal logic, and the worked physics examples against standard Clifford algebra facts.
14
+
15
+ First significant finding: the general “dimension ≥ dimension” test is not sufficient by itself. You also need the restricted map from the even subalgebra into the chosen projected column(s) to be injective; otherwise the dimensions can match but the convention can still alias elements.
16
+
17
+ Thought for 55s
18
+
19
+ I’d **not ship this spec as-is**. The implementation idea is salvageable, but there are several mathematically serious issues, especially around **primitive idempotents, Cl(1,3), quaternionic packaging, normalization, and chirality**.
20
+
21
+ ## High-priority correctness issues
22
+
23
+ ### 1. “Dimension ≥ even algebra dimension” is not a sufficient roundtrip test
24
+
25
+ The spec says faithful roundtrip requires:
26
+ $$
27
+ \dim_\mathbb{R}(\text{spinor}) \ge \dim_\mathbb{R}(\mathrm{Cl}^0(p,q))
28
+ $$
29
+ and then uses that as the gate for whether `from_spinor_matrix(to_spinor_matrix(mv)) == mv` should work. That is only a **necessary** condition, not sufficient. You also need the concrete linear map
30
+ $$
31
+ \mathrm{Cl}^0(p,q) \to \text{chosen projected column space}
32
+ $$
33
+ to be **injective** for the chosen representation, idempotent, and extracted column(s). The spec later assumes the system is full-rank “where roundtrip is possible,” but that must be tested directly by rank, not inferred from dimension.
34
+
35
+ Recommended fix: define `_spinor_roundtrip_possible` as a **computed rank check** over the even blade columns:
36
+
37
+ ```
38
+ rank(A) == number_of_even_blades
39
+ ```
40
+
41
+ Dimension tables can remain explanatory, but not authoritative.
42
+
43
+ ------
44
+
45
+ ### 2. The idempotent story is inconsistent for Cl(1,3)
46
+
47
+ The spec says a primitive idempotent gives a minimal left ideal and “one nonzero column,” then for Cl(1,3) chooses:
48
+ $$
49
+ p = \frac12(1 + \gamma^0) = \operatorname{diag}(1,1,0,0)
50
+ $$
51
+ That is **not primitive as a complex 4×4 projector**. It has complex rank 2, not rank 1. Multiplying by it gives a matrix with **two nonzero columns**, not one. The spec notices this by saying “columns 0,1 as a pair,” but the algorithm returns only `projected[:, c:c+1]`, i.e. one column.
52
+
53
+ So there are two different constructions mixed together:
54
+
55
+ 1. **Minimal-left-ideal construction**: choose a primitive idempotent and keep the resulting ideal element.
56
+ 2. **Reference-column construction**: choose a reference column vector $u$, then map $M(\psi)u$.
57
+
58
+ For Cl(3,0), these collapse nicely because $p=\operatorname{diag}(1,0)$ is rank 1. For Cl(1,3), your chosen $p$ is not doing the same thing.
59
+
60
+ Recommended fix: rewrite the algorithm around a **reference spinor vector** rather than pretending the Cl(1,3) projector is primitive:
61
+
62
+ ```
63
+ Choose a reference column u.
64
+ Map even multivector ψ to M(ψ) u.
65
+ Recover ψ by solving the real linear system over even blades.
66
+ Require rank(A) == dim Cl⁰.
67
+ ```
68
+
69
+ Then Cl(1,3) identity mapping to $(1,0,0,0)^T$ is clean: you are using $u=e_0$, not extracting a minimal ideal from $p=\frac12(1+\gamma^0)$.
70
+
71
+ ------
72
+
73
+ ### 3. The Cl(1,3) complex matrix description is potentially misleading
74
+
75
+ The spec says Cl(1,3) is $M(2,\mathbb H)$, but then talks about a compact 4×4 complex Dirac-matrix representation. That is okay **only if you are clear** that the 4×4 complex representation is a faithful **real** representation of $M(2,\mathbb H)$, not the full complex matrix algebra $M(4,\mathbb C)$. The image is a constrained real subalgebra inside $M(4,\mathbb C)$, not arbitrary complex 4×4 matrices.
76
+
77
+ Recommended wording:
78
+
79
+ > For quaternionic cases, the implementation uses the standard embedding $M(k,\mathbb H) \hookrightarrow M(2k,\mathbb C)$. This is faithful as a real algebra but its image is a proper real subalgebra of $M(2k,\mathbb C)$.
80
+
81
+ ------
82
+
83
+ ### 4. The Cl(3,0) Pauli signs are inconsistent
84
+
85
+ The worked Cl(3,0) section has a sign/basis-order problem.
86
+
87
+ It says:
88
+ $$
89
+ e_{12} \mapsto \sigma_1\sigma_2 = i\sigma_3
90
+ $$
91
+ and
92
+ $$
93
+ e_{23} \mapsto i\sigma_1
94
+ $$
95
+ Those are consistent with $e_i \mapsto \sigma_i$. But then it says:
96
+ $$
97
+ e_{31} \mapsto -i\sigma_2
98
+ $$
99
+ That is **not** consistent with $e_{31}=e_3e_1$, because:
100
+ $$
101
+ \sigma_3\sigma_1 = i\sigma_2
102
+ $$
103
+ while
104
+ $$
105
+ \sigma_1\sigma_3 = -i\sigma_2
106
+ $$
107
+ So either the blade is really $e_{13}$, or the sign is wrong. This affects the inverse formula and the Bloch sphere formula.
108
+
109
+ Recommended fix: choose one convention and state it explicitly. For canonical ascending blade order, use $e_{13}$, not $e_{31}$, or adjust the signs.
110
+
111
+ For example, with $e_{31}=e_3e_1$:
112
+
113
+ ```
114
+ e12 ↦ iσ3
115
+ e23 ↦ iσ1
116
+ e31 ↦ iσ2
117
+ ```
118
+
119
+ Then the extracted column for $e_{31}$ is $(0,-1)^T$, not $(0,1)^T$.
120
+
121
+ ------
122
+
123
+ ### 5. The normalization table is false for Lorentzian spinors
124
+
125
+ This is probably the most important physics correctness issue.
126
+
127
+ The spec says:
128
+ $$
129
+ \psi\tilde\psi = 1 \quad \Longleftrightarrow \quad \langle \psi|\psi\rangle = 1
130
+ $$
131
+ and claims a normalized rotor always maps to a unit-normalized column spinor.
132
+
133
+ That is true for **Euclidean Pauli rotations** under the usual Hermitian norm. It is **not true in Cl(1,3)** for Lorentz boosts. Lorentz rotor matrices are not unitary under the ordinary positive-definite Hermitian inner product. A boost rotor satisfies:
134
+ $$
135
+ R\tilde R = 1
136
+ $$
137
+ but the ordinary column norm $\psi^\dagger\psi$ generally changes under boosts. The invariant Dirac bilinear uses the **Dirac adjoint**, not the Hilbert-space norm:
138
+ $$
139
+ \bar\psi \psi = \psi^\dagger \gamma^0 \psi
140
+ $$
141
+ Recommended fix: split the normalization section by signature:
142
+
143
+ ```
144
+ For Cl(3,0) Pauli spinors, rotor normalization corresponds to unit Hermitian norm for the projected reference state.
145
+
146
+ For Cl(1,3), R\tilde R = 1 corresponds to Lorentz-group normalization, not ordinary Hermitian unit norm. The relevant invariant bilinears use the Dirac adjoint or GA scalar bilinears.
147
+ ```
148
+
149
+ ------
150
+
151
+ ### 6. The quaternionic “two components = Weyl chiralities” claim is wrong as stated
152
+
153
+ The spec says the two quaternion components of a Dirac spinor correspond to the two Weyl chiralities, and that boosts mix those chiralities.
154
+
155
+ That is not right.
156
+
157
+ A Dirac spinor can be written as two Weyl spinors in the **chiral/Weyl basis**:
158
+ $$
159
+ \psi =
160
+ \begin{pmatrix}
161
+ \psi_L \\
162
+ \psi_R
163
+ \end{pmatrix}
164
+ $$
165
+ But your earlier Cl(1,3) section chooses a Dirac-style projector $p=\frac12(1+\gamma^0)$, where the upper/lower split is not automatically the left/right chirality split. More importantly, proper Lorentz transformations do **not** mix left and right Weyl representations. Rotations and boosts act separately on $\psi_L$ and $\psi_R$, with related but inequivalent $SL(2,\mathbb C)$ actions. Mass terms, parity, or basis changes can mix interpretations, but a pure Lorentz boost does not “couple the two chiralities.”
166
+
167
+ Recommended fix: remove that physical interpretation unless you explicitly choose the chiral basis and define the quaternion packaging accordingly. Safer wording:
168
+
169
+ ```
170
+ A complex Dirac spinor can be packaged as two quaternions, but the physical interpretation of those two quaternionic components depends on the chosen gamma-matrix basis and reference spinor. They should not be identified with Weyl chiralities unless the representation and basis are chosen to make that identification explicit.
171
+ ```
172
+
173
+ ------
174
+
175
+ ## Medium-priority issues
176
+
177
+ ### 7. “Physically, Cl(1,3) is the standard choice…” is too strong
178
+
179
+ The spec says Cl(1,3) is the standard choice for the Dirac equation precisely because its spinor representation has the right dimension.
180
+
181
+ That is misleading. Physics often works with the **complexified** Clifford algebra, where the real-signature distinction largely disappears:
182
+ $$
183
+ \mathrm{Cl}_{1,3}(\mathbb R)\otimes \mathbb C
184
+ \cong
185
+ \mathrm{Cl}_{3,1}(\mathbb R)\otimes \mathbb C
186
+ \cong
187
+ M(4,\mathbb C)
188
+ $$
189
+ The real algebra distinction matters for Majorana/quaternionic structures and for GA formulations, but saying Cl(1,3) is “the standard choice precisely because…” overstates the case.
190
+
191
+ Recommended fix:
192
+
193
+ > In this library’s real-algebra convention, Cl(1,3) has a quaternionic structure that lets an even multivector be encoded in an 8-real-dimensional spinor column. Cl(3,1), using its minimal real column representation, does not provide a faithful roundtrip by this particular column-extraction method.
194
+
195
+ ------
196
+
197
+ ### 8. “Minimal left ideal = matrices with only one nonzero column” only works cleanly for $M(k,F)$ over $F$
198
+
199
+ This statement is fine for $M(k,\mathbb R)$, $M(k,\mathbb C)$, or $M(k,\mathbb H)$ when viewed over the same scalar division ring. But once you embed quaternionic matrices into complex matrices, a quaternionic rank-1 column corresponds to **two complex columns**.
200
+
201
+ Recommended fix: add a note:
202
+
203
+ ```
204
+ For quaternionic algebras represented via complex 2×2 blocks, a rank-1 quaternionic projector appears as a rank-2 complex projector. Therefore “one nonzero column” means one quaternionic column, not one complex column.
205
+ ```
206
+
207
+ ------
208
+
209
+ ### 9. The API name `to_spinor_matrix` may obscure what is returned
210
+
211
+ It returns a **column vector**, not a matrix. That may be okay if the package already has naming conventions, but mathematically `to_spinor_column` or `to_column_spinor` would be clearer.
212
+
213
+ ------
214
+
215
+ ## What I would change structurally
216
+
217
+ I’d rewrite the central algorithm like this:
218
+
219
+ ```
220
+ Given an even multivector ψ ∈ Cl⁰(p,q):
221
+
222
+ 1. Build a faithful real matrix representation ρ: Cl(p,q) → Mat_N(K),
223
+ implemented over real or complex NumPy arrays.
224
+
225
+ 2. Choose a fixed reference column u.
226
+
227
+ 3. Define:
228
+ spinor(ψ) = ρ(ψ) u
229
+
230
+ 4. Precompute the real basis matrix A whose columns are spinor(E_i)
231
+ for each even basis blade E_i.
232
+
233
+ 5. The conversion is faithful iff rank_R(A) = dim_R Cl⁰(p,q).
234
+
235
+ 6. Forward conversion is multiplication by A.
236
+
237
+ 7. Inverse conversion solves A c = spinor.
238
+ ```
239
+
240
+ That avoids the ambiguity around primitive idempotents, rank-2 complex projectors, and whether you are extracting one column or a minimal ideal.
241
+
242
+ ## Verdict
243
+
244
+ The broad idea is good:
245
+
246
+ > represent an even GA spinor by its action on a fixed reference spinor column, then reconstruct by solving over the even blade basis.
247
+
248
+ But the spec currently presents that as a minimal-left-ideal/idempotent construction, and that framing breaks down in the Cl(1,3) quaternionic/complex-embedded case.
249
+
250
+ The must-fix items are:
251
+
252
+ 1. Replace the dimension-only roundtrip test with an actual rank test.
253
+ 2. Reframe Cl(1,3) around a reference column, not a primitive idempotent.
254
+ 3. Fix the Cl(3,0) $e_{31}$ sign convention.
255
+ 4. Remove or heavily qualify the normalization claim.
256
+ 5. Remove the “quaternion components = Weyl chiralities” claim unless you explicitly switch to a chiral basis and prove the mapping.
257
+ 6. Clarify quaternionic complex embedding: one quaternionic column = two complex rows/columns depending on representation, not a naive single complex column.
258
+
259
+ Uploaded a file
260
+
261
+ I wrote: Review this code to implement
262
+
263
+ Chat GPT replied:
264
+
265
+ I’ll review this against the spec issues from the previous pass: not just “does it run,” but whether the implementation preserves the intended Clifford/spinor mapping without hidden dimension or convention errors.
266
+
267
+ A second concrete issue: the code still implements the old dimension-only roundtrip predicate. That means it can declare a spinor conversion valid without checking the actual rank of the even-blade-to-column map.
268
+
269
+ Thought for 1m 58s
270
+
271
+ I’d **not merge this yet**. The core full-matrix machinery is mostly reasonable, but the spinor/quaternion parts still encode the mathematical mistakes from the spec, and there is one extra serious implementation issue: the **Cl(1,3) Dirac matrices are not in the quaternionic block convention that `to_quaternion_matrix()` assumes**.
272
+
273
+ I couldn’t run end-to-end tests because `galaga` is not installed in the sandbox, but I did static review and rank-checked the matrix construction by stubbing the minimal `Algebra` surface.
274
+
275
+ ## Biggest issues
276
+
277
+ ### 1. `_spinor_roundtrip_possible()` is still dimension-only
278
+
279
+ Lines 524–555 decide roundtrippability from the ABS type and real dimension count. That was the main issue in the spec: dimension is necessary but not sufficient.
280
+
281
+ You should make this a **rank check over the actual chosen spinor map**:
282
+
283
+ ```
284
+ def _even_indices(dim: int) -> np.ndarray:
285
+ return np.array([i for i in range(dim) if i.bit_count() % 2 == 0])
286
+
287
+
288
+ def _spinor_reference_vector(alg: Algebra) -> np.ndarray:
289
+ gammas = compact_basis(alg)
290
+ k = gammas[0].shape[0]
291
+ p = _choose_idempotent(gammas, k)
292
+
293
+ # Use the first nonzero column of p as the reference spinor.
294
+ # This works for diagonal and non-diagonal projectors.
295
+ norms = np.linalg.norm(p, axis=0)
296
+ col = int(np.argmax(norms))
297
+ if norms[col] < 1e-14:
298
+ u = np.zeros((k, 1), dtype=complex)
299
+ u[0, 0] = 1.0
300
+ return u
301
+
302
+ return p[:, col : col + 1]
303
+
304
+
305
+ def _spinor_system_matrix(alg: Algebra) -> tuple[np.ndarray, np.ndarray]:
306
+ blade_mats = _build_compact_blade_matrices(alg)
307
+ even_indices = _even_indices(alg.dim)
308
+ u = _spinor_reference_vector(alg)
309
+
310
+ cols = (blade_mats[even_indices] @ u)[:, :, 0] # (n_even, k)
311
+ A_complex = cols.T # (k, n_even)
312
+ A_real = np.vstack([A_complex.real, A_complex.imag])
313
+ return A_real, even_indices
314
+
315
+
316
+ def _spinor_roundtrip_possible(alg: Algebra) -> bool:
317
+ try:
318
+ A, even_indices = _spinor_system_matrix(alg)
319
+ except NotImplementedError:
320
+ return False
321
+
322
+ return np.linalg.matrix_rank(A, tol=1e-10) == len(even_indices)
323
+ ```
324
+
325
+ Then `from_spinor_matrix()` should use the same `A` and `even_indices`, not rebuild a slightly different column-selection path.
326
+
327
+ ------
328
+
329
+ ### 2. `to_spinor_matrix()` should use a reference vector, not “extract first diagonal column”
330
+
331
+ Lines 608–614 do:
332
+
333
+ ```
334
+ projected = mat @ p
335
+ for col in range(k):
336
+ if np.abs(p[col, col]) > 0.5:
337
+ return projected[:, col : col + 1]
338
+ ```
339
+
340
+ This works for Cl(3,0) and happens to work for Cl(1,3), but it is conceptually brittle. For a non-diagonal idempotent, the diagonal can be `0.5`, so the `> 0.5` test misses it. Then the fallback chooses the first nonzero projected column depending on the **input multivector**, which is a bad property: the reference spinor convention should not depend on the value being converted.
341
+
342
+ Use:
343
+
344
+ ```
345
+ def to_spinor_matrix(mv: Multivector) -> np.ndarray:
346
+ from galaga import is_even
347
+
348
+ if not is_even(mv):
349
+ raise ValueError(
350
+ "to_spinor_matrix requires an even-grade multivector. "
351
+ "Use galaga.even_grades(mv) to project to the even part first."
352
+ )
353
+
354
+ if not _spinor_roundtrip_possible(mv.algebra):
355
+ p = sum(1 for s in mv.algebra.signature if s > 0)
356
+ q = sum(1 for s in mv.algebra.signature if s < 0)
357
+ etype, _ = _classify(p, q)
358
+ raise TypeError(
359
+ f"Cl({p},{q}) (type '{etype}') does not support faithful spinor roundtrip "
360
+ f"with the selected compact representation and reference spinor. "
361
+ f"Use to_matrix(mv, mode='compact') for the full matrix representation."
362
+ )
363
+
364
+ return _to_compact(mv) @ _spinor_reference_vector(mv.algebra)
365
+ ```
366
+
367
+ And `from_spinor_matrix()` becomes:
368
+
369
+ ```
370
+ def from_spinor_matrix(alg: Algebra, spinor: np.ndarray) -> Multivector:
371
+ A, even_indices = _spinor_system_matrix(alg)
372
+
373
+ if np.linalg.matrix_rank(A, tol=1e-10) != len(even_indices):
374
+ p = sum(1 for s in alg.signature if s > 0)
375
+ q = sum(1 for s in alg.signature if s < 0)
376
+ etype, _ = _classify(p, q)
377
+ raise TypeError(
378
+ f"Cl({p},{q}) (type '{etype}') does not support faithful spinor roundtrip "
379
+ f"with the selected compact representation and reference spinor."
380
+ )
381
+
382
+ k = A.shape[0] // 2
383
+
384
+ spinor = np.asarray(spinor, dtype=complex)
385
+ if spinor.shape == (k,):
386
+ spinor = spinor.reshape(k, 1)
387
+ if spinor.shape != (k, 1):
388
+ raise ValueError(f"Expected ({k}, 1) spinor, got {spinor.shape}")
389
+
390
+ b = np.concatenate([spinor[:, 0].real, spinor[:, 0].imag])
391
+ coeffs_even, residuals, rank, _ = np.linalg.lstsq(A, b, rcond=None)
392
+
393
+ if rank != len(even_indices):
394
+ raise TypeError("Selected spinor map is rank-deficient.")
395
+
396
+ data = np.zeros(alg.dim)
397
+ data[even_indices] = coeffs_even
398
+ return Multivector(alg, data)
399
+ ```
400
+
401
+ This also removes duplicated column-selection logic.
402
+
403
+ ------
404
+
405
+ ### 3. `to_quaternion_matrix()` is wrong for your Cl(1,3) Dirac basis
406
+
407
+ This is the most serious new code-level issue.
408
+
409
+ Your quaternion embedding convention is:
410
+
411
+ ```
412
+ q = a + bi + cj + dk
413
+ → [[a+bi, c+di],
414
+ [-c+di, a-bi]]
415
+ ```
416
+
417
+ That is lines 152–159 and 483–494.
418
+
419
+ But your Cl(1,3) Dirac matrices in lines 210–216 use blocks like:
420
+
421
+ ```
422
+ [[0, σᵢ],
423
+ [-σᵢ, 0]]
424
+ ```
425
+
426
+ The Pauli matrix `σ1 = [[0,1],[1,0]]` is **not** a quaternion block under your embedding, because a quaternion block with top-right `+1` would have bottom-left `-1`, not `+1`.
427
+
428
+ So this function:
429
+
430
+ ```
431
+ block = cmat[2*i:2*i+2, 2*j:2*j+2]
432
+ row.append(_extract_quat(block))
433
+ ```
434
+
435
+ silently reads non-quaternionic 2×2 complex blocks as if they were quaternions. That gives mathematically false quaternion matrices for Cl(1,3).
436
+
437
+ You have two viable options.
438
+
439
+ Option A: keep standard Dirac matrices, but disable quaternion-matrix extraction for them:
440
+
441
+ ```
442
+ def to_quaternion_matrix(mv: Multivector) -> list[list[Quat]]:
443
+ p = sum(1 for s in mv.algebra.signature if s > 0)
444
+ q = sum(1 for s in mv.algebra.signature if s < 0)
445
+ etype, qdim = _classify(p, q)
446
+
447
+ if etype != "quaternion":
448
+ raise TypeError(
449
+ f"Cl({p},{q}) is type '{etype}', not a single quaternionic matrix algebra."
450
+ )
451
+
452
+ # Important: this requires the compact representation to be in quaternionic
453
+ # 2×2 block form. The standard Dirac basis used for Cl(1,3) is not.
454
+ if (p, q) == (1, 3):
455
+ raise TypeError(
456
+ "to_quaternion_matrix is not valid for the current Cl(1,3) Dirac basis; "
457
+ "use a quaternionic-block gamma basis or to_matrix(..., mode='compact')."
458
+ )
459
+ ```
460
+
461
+ Option B: introduce a separate **quaternionic-block Cl(1,3) basis** and use that for quaternion matrix/spinor APIs. For example, over $M(2,\mathbb H)$:
462
+ $$
463
+ \gamma^0 =
464
+ \begin{pmatrix}
465
+ 1 & 0 \\
466
+ 0 & -1
467
+ \end{pmatrix},
468
+ \quad
469
+ \gamma^i =
470
+ \begin{pmatrix}
471
+ 0 & u_i \\
472
+ u_i & 0
473
+ \end{pmatrix}
474
+ $$
475
+ where $u_i \in \{i,j,k\}$. Since $u_i^2=-1$, the spatial gammas square to $-1$, and they anticommute correctly.
476
+
477
+ Then embed each quaternion block using your `_quat_to_complex()` function.
478
+
479
+ ------
480
+
481
+ ### 4. `to_spinor_quaternion()` has the same representation-convention problem
482
+
483
+ Lines 760–768 pair consecutive complex entries and interpret them as quaternions. That is only meaningful if the complex representation is arranged as a quaternionic module in the same block convention.
484
+
485
+ For Cl(0,2), that is okay. For Cl(1,3) using your current Dirac matrices, it is at best a packaging of four complex entries into two `Quat` objects, not a mathematically natural quaternionic spinor compatible with your `M(2,H)` matrix extraction.
486
+
487
+ I’d either rename it to make the weaker meaning explicit:
488
+
489
+ ```
490
+ to_spinor_quaternion_packed()
491
+ ```
492
+
493
+ or require a quaternionic-block representation before exposing it as a real quaternionic spinor.
494
+
495
+ ------
496
+
497
+ ### 5. Quaternionic double algebras accidentally pass the “quaternionic” check
498
+
499
+ Lines 754 and 825 check:
500
+
501
+ ```
502
+ if "quaternion" not in etype:
503
+ ```
504
+
505
+ That allows `"quaternion+quaternion"`.
506
+
507
+ But the docstring says only $(q-p)\bmod 8 \in \{2,4\}$. For $(q-p)\bmod 8=3$, the algebra is $A \oplus A$, and your compact representation chooses one summand. That can be okay for some even-subalgebra spinor maps, but it is not the same claim.
508
+
509
+ Use:
510
+
511
+ ```
512
+ if etype != "quaternion":
513
+ raise TypeError(...)
514
+ ```
515
+
516
+ unless you deliberately want a `one_summand=True` mode.
517
+
518
+ ------
519
+
520
+ ### 6. `_from_compact()` should check rank and residuals
521
+
522
+ Lines 417–435 recover coefficients using least squares, but for double algebras or invalid matrices this can silently return a “best fit” multivector. That is dangerous for a math library.
523
+
524
+ Add rank/residual validation:
525
+
526
+ ```
527
+ coeffs, residuals, rank, _ = np.linalg.lstsq(A, b, rcond=None)
528
+
529
+ if rank < dim:
530
+ raise TypeError(
531
+ "Compact representation is not injective for this algebra; "
532
+ "cannot recover a unique multivector."
533
+ )
534
+
535
+ err = np.linalg.norm(A @ coeffs - b)
536
+ if err > 1e-9:
537
+ raise ValueError(
538
+ "Matrix is not in the image of this Clifford representation."
539
+ )
540
+ ```
541
+
542
+ Or expose a `strict: bool = True` flag.
543
+
544
+ ------
545
+
546
+ ### 7. `compact_basis()` says “minimal-dimension faithful” but double algebras are not faithful
547
+
548
+ Lines 358–362 say:
549
+
550
+ > minimal-dimension faithful matrix representation
551
+
552
+ But the module docstring already admits double algebras use one summand and may not roundtrip. The function docstring should say:
553
+
554
+ ```
555
+ Return compact gamma matrices for a low-dimensional representation.
556
+
557
+ For simple Clifford algebras this is faithful. For double algebras
558
+ (s = 3 or 7) this returns one irreducible summand and is not faithful
559
+ on the full algebra.
560
+ ```
561
+
562
+ Otherwise callers will trust `from_matrix(..., mode="compact")` when they shouldn’t.
563
+
564
+ ------
565
+
566
+ ### 8. Cl(0,0) edge case breaks
567
+
568
+ `_general_compact_basis(0,0)` returns `[]`, then `_build_compact_blade_matrices()` does:
569
+
570
+ ```
571
+ k = gammas[0].shape[0]
572
+ ```
573
+
574
+ So scalar algebra compact mode crashes. Either reject it explicitly or handle it:
575
+
576
+ ```
577
+ if alg.n == 0:
578
+ return np.ones((1, 1, 1), dtype=complex)
579
+ ```
580
+
581
+ ------
582
+
583
+ ## What looks good
584
+
585
+ The left-regular representation is straightforward and mathematically sound: first column recovery in `_from_left_regular()` is valid because left multiplication by $M$ sends $1$ to $M$.
586
+
587
+ The blade construction in `_build_compact_blade_matrices()` is also conventionally correct assuming your algebra’s bitmask blade order is ascending basis-vector order. It fixes the earlier `e31` sign ambiguity by always building canonical bitmask blades as ascending products.
588
+
589
+ The rank results I checked for the current selected-column map were encouraging for the headline cases:
590
+
591
+ ```
592
+ Cl(3,0): rank 4 / even dim 4
593
+ Cl(1,3): rank 8 / even dim 8
594
+ Cl(0,2): rank 2 / even dim 2
595
+ Cl(4,0): rank 8 / even dim 8
596
+ Cl(3,1): rank 4 / even dim 8
597
+ Cl(2,2): rank 4 / even dim 8
598
+ ```
599
+
600
+ So the **column-spinor roundtrip idea works for the important cases**, but the code should prove it by rank, not assert it by classification.
601
+
602
+ ## Recommended merge gate
603
+
604
+ Before shipping, I’d add tests like these:
605
+
606
+ ```
607
+ @pytest.mark.parametrize("signature", [
608
+ [1, 1, 1], # Cl(3,0)
609
+ [1, -1, -1, -1], # Cl(1,3)
610
+ [-1, -1], # Cl(0,2)
611
+ [1, 1, 1, 1], # Cl(4,0)
612
+ ])
613
+ def test_even_spinor_roundtrip(signature):
614
+ alg = Algebra(signature)
615
+ rng = np.random.default_rng(0)
616
+
617
+ data = np.zeros(alg.dim)
618
+ for i in range(alg.dim):
619
+ if i.bit_count() % 2 == 0:
620
+ data[i] = rng.normal()
621
+
622
+ mv = Multivector(alg, data)
623
+ out = from_spinor_matrix(alg, to_spinor_matrix(mv))
624
+
625
+ assert np.allclose(out.data, mv.data, atol=1e-9)
626
+ ```
627
+
628
+ And negative tests:
629
+
630
+ ```
631
+ @pytest.mark.parametrize("signature", [
632
+ [1, 1, 1, -1], # Cl(3,1)
633
+ [1, 1, -1, -1], # Cl(2,2)
634
+ [1, 1, 1, 1, -1], # Cl(4,1)
635
+ ])
636
+ def test_non_roundtrippable_spinor_rejected(signature):
637
+ alg = Algebra(signature)
638
+ assert not _spinor_roundtrip_possible(alg)
639
+ ```
640
+
641
+ ## Bottom line
642
+
643
+ The implementation is close for `to_spinor_matrix()` / `from_spinor_matrix()` if you reframe it as:
644
+ $$
645
+ \psi \mapsto \rho(\psi)u
646
+ $$
647
+ for a fixed reference spinor $u$, with a rank-checked inverse.
648
+
649
+ But I would **block the quaternion APIs** until you either:
650
+
651
+ 1. provide a genuine quaternionic-block representation, or
652
+ 2. clearly mark them as simple complex-pair packing rather than quaternionic Clifford matrix/spinor conversion.
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: galaga-matrix
3
+ Version: 1.7.0
4
+ Summary: Matrix representations for galaga Clifford algebras
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: galaga>=1.7.0
8
+ Requires-Dist: numpy>=1.24.0