Skip to content

fix(mv): is_blade() now correctly handles null vectors and null blades#582

Merged
utensil merged 5 commits intopygae:masterfrom
utiberious:fix/issue-537-is-blade-null
Apr 2, 2026
Merged

fix(mv): is_blade() now correctly handles null vectors and null blades#582
utensil merged 5 commits intopygae:masterfrom
utiberious:fix/issue-537-is-blade-null

Conversation

@utiberious
Copy link
Copy Markdown
Contributor

Summary

  • is_blade() previously delegated entirely to is_versor(), which returns False for null multivectors (no inverse → not a versor)
  • This caused grade-1 null vectors like e0+e1 in G(1,1) and grade-2 null blades like (e0+e1)^e2 in G(1,2) to be incorrectly reported as non-blades
  • Blade-ness is a metric-free concept (depends only on the outer product)

Fix

  1. Not grade-homogeneous → False
  2. Grade ≤ 1 → always True (scalars and vectors are always blades)
  3. Non-null (invertible) → is_versor() path (unchanged)
  4. Null case → outer-product squaring test: B ^ B == 0 (necessary for any blade; sufficient for grade 2)

The docstring notes the known limitation: for grades ≥ 3 in algebras of sufficiently high dimension, B ^ B == 0 may give false positives; full factorizability is not yet implemented.

Test

Added test_is_blade_null covering:

  • Null 1-vector in G(1,1)
  • Non-null 1-vectors
  • Null 2-blade in G(1,2) with null constituent vector
  • Mixed (non-grade-homogeneous) multivector

Closes #537

Previously is_blade() delegated entirely to is_versor(), which returns
False for null multivectors (they have no inverse). This caused grade-1
null vectors like e0+e1 in G(1,1), and grade-2 null blades like
(e0+e1)^e2 in G(1,2), to be incorrectly reported as non-blades.

Blade-ness is a metric-free concept; the fix:
- Grade 0 and 1: always blades (no metric required)
- Grade >= 2, non-null: is_versor() path (unchanged)
- Grade >= 2, null: outer-product squaring test (B^B == 0), which is
  necessary for any blade and sufficient for grade 2

Closes pygae#537
Copy link
Copy Markdown
Member

@utensil utensil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix. Two issues to address before merging.

The not self.is_zero() guard lives in step 4 but the zero scalar never reaches it: i_grade = 0 is caught by i_grade <= 1 at step 2, returning True. Current behavior is False for mv(0).is_blade(). If zero should stay excluded (standard convention), the guard needs to be before step 2.

reflect_in_blade and project_in_blade gate on is_blade() then immediately compute / blade.qform(). For null blades, qform() is zero, so they'll divide by zero after this fix. Before, is_blade() = False for null blades effectively protected those methods. They need a null-blade guard now.

Also the test comment "null → no inverse → not a versor" is slightly off — galgebra's is_versor() checks V*V.rev() = 0, not the existence of an inverse. Minor but worth fixing.

- Move zero check before grade check to correctly exclude zero multivectors
- Add null-blade guards to reflect_in_blade() and project_in_blade()
- Update algorithm documentation to reflect new step ordering
- All tests pass (35/35)
@utensil
Copy link
Copy Markdown
Member

utensil commented Apr 2, 2026

On the grade ≥ 3 limitation: the docstring says "sufficiently high dimension" but it's worth being concrete. The standard counterexample in R⁶ (Dorst, Fontijne & Mann, Geometric Algebra for Computer Science, 2007):

B = v₁∧v₂∧v₃ + v₁∧v₄∧v₅ + v₂∧v₄∧v₆ + v₃∧v₅∧v₆

(B^B).is_zero() returns True and B.is_zero() is False, but B cannot be factored as u∧v∧w for any vectors u, v, w — so is_blade() would silently return True. A test pinning this known false positive (asserting is_blade() == True with a comment that it's a known limitation) would make the boundary explicit rather than prose-only.

…rexample

Adds a test pinning the known false positive for is_blade() in grade >= 3
with sufficiently high dimensions (R⁶). The Dorst/Fontijne/Mann counterexample
satisfies (B^B).is_zero() but is not actually a 3-blade.

This documents the known limitation as a test with a clear comment.
@utiberious utiberious force-pushed the fix/issue-537-is-blade-null branch from 47e844a to 11d01b3 Compare April 2, 2026 16:37
The test was incorrect on two counts:
- is_blade() correctly returns False for the DFM counterexample in R^6
  because B^B != 0 there (the outer-product test works fine)
- The computation took ~32 minutes, making CI unusable

The grade >= 3 limitation is documented in the is_blade() docstring only.
Copy link
Copy Markdown
Member

@utensil utensil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the feedback. Zero guard is correctly placed, reflect/project guards look good, and the test comment is accurate now.

@utensil utensil merged commit bc65e6b into pygae:master Apr 2, 2026
5 checks passed
@utensil utensil mentioned this pull request Apr 3, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multivector method .is_blade() will fail for null multivectors

2 participants