From 5392a4228c1988d5bb3285a8a6e4b5a0fd69853b Mon Sep 17 00:00:00 2001 From: Ulysses Tiberious Date: Mon, 30 Mar 2026 00:42:22 +0800 Subject: [PATCH 1/3] Add Cl(p,q,r) kingdon-like interface for Clifford algebras Adds a Cl(p, q, r) convenience function that constructs a Clifford algebra Cl(p,q,r) from its signature: p positive, q negative, r degenerate basis vectors. This follows the interface popularized by ganja.js and kingdon. The function is importable from both galgebra.ga and the top-level galgebra package. Fixes utiberious/galgebra#10 --- galgebra/__init__.py | 1 + galgebra/ga.py | 53 ++++++++++++++++++++++++++++++++++++++++++++ test/test_test.py | 30 +++++++++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/galgebra/__init__.py b/galgebra/__init__.py index 405c5fae..9e238db2 100644 --- a/galgebra/__init__.py +++ b/galgebra/__init__.py @@ -36,3 +36,4 @@ """ from ._version import __version__ # noqa: F401 +from .ga import Cl # noqa: F401 diff --git a/galgebra/ga.py b/galgebra/ga.py index feb8b40e..4438144a 100644 --- a/galgebra/ga.py +++ b/galgebra/ga.py @@ -2364,3 +2364,56 @@ def vpds(self) -> Tuple[_mv.Dop, _mv.Dop]: self.vpd = mv.Dop(r_basis, pdx, ga=self) self.rvpd = mv.Dop(r_basis, pdx, ga=self, cmpflg=True) return self.vpd, self.rvpd + + +def Cl(p: int, q: int = 0, r: int = 0, root: str = 'e', **kwargs): + r""" + Construct a Clifford algebra :math:`Cl(p,q,r)` using a kingdon-like + interface. + + Parameters + ---------- + p : int + Number of basis vectors that square to +1. + q : int + Number of basis vectors that square to -1. + r : int + Number of basis vectors that square to 0 (degenerate). + root : str + Root name for basis vectors (default ``'e'``). + **kwargs : + Additional keyword arguments passed to :class:`Ga`. + + Returns + ------- + tuple + ``(ga, e_1, ..., e_n)`` -- the geometric algebra and basis vectors. + + Examples + -------- + 3D Euclidean algebra:: + + >>> ga, e1, e2, e3 = Cl(3) + + Spacetime algebra (Minkowski, +---):: + + >>> ga, e0, e1, e2, e3 = Cl(1, 3) + + Projective Geometric Algebra:: + + >>> ga, e0, e1, e2, e3 = Cl(3, 0, 1) + """ + n = p + q + r + if n == 0: + raise ValueError("Total dimension p + q + r must be positive.") + + # Build diagonal metric: p ones, q negative ones, r zeros + g = [1] * p + [-1] * q + [0] * r + + # Build basis name string + if n <= 9: + basis = root + '*' + '|'.join(str(i + 1) for i in range(n)) + else: + basis = ' '.join(root + '_' + str(i + 1) for i in range(n)) + + return Ga.build(basis, g=g, **kwargs) diff --git a/test/test_test.py b/test/test_test.py index fb6b0405..a580363d 100644 --- a/test/test_test.py +++ b/test/test_test.py @@ -687,3 +687,33 @@ def test_deprecations(self): assert ga_ortho.dot_product_basis_blades((e_1.obj, e_12.obj), mode='<') == (e_1 < e_12).obj with pytest.warns(DeprecationWarning): assert ga_ortho.dot_product_basis_blades((e_1.obj, e_12.obj), mode='>') == (e_1 > e_12).obj + + def test_Cl(self): + """Test the Cl(p, q, r) kingdon-like interface (issue 524).""" + from galgebra.ga import Cl + + # 3D Euclidean: Cl(3) + ga3, e1, e2, e3 = Cl(3) + assert e1 * e1 == ga3.mv(1) + assert e2 * e2 == ga3.mv(1) + assert e3 * e3 == ga3.mv(1) + + # 2D Minkowski: Cl(1, 1) + ga11, e1, e2 = Cl(1, 1) + assert e1 * e1 == ga11.mv(1) + assert e2 * e2 == ga11.mv(-1) + + # Degenerate: Cl(2, 0, 1) + ga201, e1, e2, e3 = Cl(2, 0, 1) + assert e1 * e1 == ga201.mv(1) + assert e2 * e2 == ga201.mv(1) + assert e3 * e3 == ga201.mv(0) + + # Import from top-level package + from galgebra import Cl as Cl2 + ga_top, e1_top, e2_top = Cl2(2) + assert e1_top * e1_top == ga_top.mv(1) + + # Error on zero dimension + with pytest.raises(ValueError): + Cl(0) From 35026b6b80553e876083908477fb303baed0f7e5 Mon Sep 17 00:00:00 2001 From: Ulysses Tiberious Date: Mon, 30 Mar 2026 18:39:38 +0800 Subject: [PATCH 2/3] Move Cl to galgebra.interop, add kingdon-convention namespace - galgebra.interop.Cl: galgebra defaults (1-indexed, I+ dual) - galgebra.interop.kingdon.Cl: kingdon conventions (0-indexed, Iinv+ dual) - Remove Cl from ga.py (keep ga.py for core algebra only) - Document known incompatibilities (basis naming, dual convention) --- galgebra/__init__.py | 2 +- galgebra/ga.py | 53 ---------------------------- galgebra/interop/__init__.py | 22 ++++++++++++ galgebra/interop/_cl.py | 64 +++++++++++++++++++++++++++++++++ galgebra/interop/kingdon.py | 68 ++++++++++++++++++++++++++++++++++++ test/test_test.py | 21 +++++++++-- 6 files changed, 174 insertions(+), 56 deletions(-) create mode 100644 galgebra/interop/__init__.py create mode 100644 galgebra/interop/_cl.py create mode 100644 galgebra/interop/kingdon.py diff --git a/galgebra/__init__.py b/galgebra/__init__.py index 9e238db2..b2481927 100644 --- a/galgebra/__init__.py +++ b/galgebra/__init__.py @@ -36,4 +36,4 @@ """ from ._version import __version__ # noqa: F401 -from .ga import Cl # noqa: F401 +from .interop import Cl # noqa: F401 diff --git a/galgebra/ga.py b/galgebra/ga.py index 4438144a..feb8b40e 100644 --- a/galgebra/ga.py +++ b/galgebra/ga.py @@ -2364,56 +2364,3 @@ def vpds(self) -> Tuple[_mv.Dop, _mv.Dop]: self.vpd = mv.Dop(r_basis, pdx, ga=self) self.rvpd = mv.Dop(r_basis, pdx, ga=self, cmpflg=True) return self.vpd, self.rvpd - - -def Cl(p: int, q: int = 0, r: int = 0, root: str = 'e', **kwargs): - r""" - Construct a Clifford algebra :math:`Cl(p,q,r)` using a kingdon-like - interface. - - Parameters - ---------- - p : int - Number of basis vectors that square to +1. - q : int - Number of basis vectors that square to -1. - r : int - Number of basis vectors that square to 0 (degenerate). - root : str - Root name for basis vectors (default ``'e'``). - **kwargs : - Additional keyword arguments passed to :class:`Ga`. - - Returns - ------- - tuple - ``(ga, e_1, ..., e_n)`` -- the geometric algebra and basis vectors. - - Examples - -------- - 3D Euclidean algebra:: - - >>> ga, e1, e2, e3 = Cl(3) - - Spacetime algebra (Minkowski, +---):: - - >>> ga, e0, e1, e2, e3 = Cl(1, 3) - - Projective Geometric Algebra:: - - >>> ga, e0, e1, e2, e3 = Cl(3, 0, 1) - """ - n = p + q + r - if n == 0: - raise ValueError("Total dimension p + q + r must be positive.") - - # Build diagonal metric: p ones, q negative ones, r zeros - g = [1] * p + [-1] * q + [0] * r - - # Build basis name string - if n <= 9: - basis = root + '*' + '|'.join(str(i + 1) for i in range(n)) - else: - basis = ' '.join(root + '_' + str(i + 1) for i in range(n)) - - return Ga.build(basis, g=g, **kwargs) diff --git a/galgebra/interop/__init__.py b/galgebra/interop/__init__.py new file mode 100644 index 00000000..8237d73e --- /dev/null +++ b/galgebra/interop/__init__.py @@ -0,0 +1,22 @@ +""" +Interoperability interfaces for creating geometric algebras using +conventions from other libraries. + +The ``Cl(p, q, r)`` signature interface was popularized by ganja.js and +adopted by kingdon. Two flavours are provided: + +- ``galgebra.interop.Cl`` uses galgebra defaults (no surprises for + existing users). +- ``galgebra.interop.kingdon.Cl`` uses kingdon conventions + (``dual_mode='Iinv+'``, 0-indexed basis names). + +Known incompatibilities between galgebra and kingdon: + +- **Basis naming**: galgebra numbers from 1 (``e_1, e_2, ...``); + kingdon defaults to 0-indexed for PGA (``e0, e1, e2, e3``). +- **Dual convention**: galgebra's default is ``'I+'`` (``dual(A) = I * A``); + kingdon uses ``'Iinv+'`` (``dual(A) = A * I^{-1}``) via its + ``codegen_polarity``. +""" + +from ._cl import Cl # noqa: F401 diff --git a/galgebra/interop/_cl.py b/galgebra/interop/_cl.py new file mode 100644 index 00000000..cecff2c1 --- /dev/null +++ b/galgebra/interop/_cl.py @@ -0,0 +1,64 @@ +""" +Core ``Cl(p, q, r)`` factory with galgebra defaults. +""" + +from ..ga import Ga + + +def Cl(p: int, q: int = 0, r: int = 0, root: str = 'e', **kwargs): + r""" + Construct a Clifford algebra :math:`Cl(p,q,r)` from its signature. + + This interface was popularized by ganja.js and adopted by kingdon. + It provides a concise way to create a geometric algebra without + specifying a full metric tensor. + + Uses galgebra defaults (basis numbered from 1, dual mode ``'I+'``). + For kingdon conventions, use ``galgebra.interop.kingdon.Cl`` instead. + + Parameters + ---------- + p : int + Number of basis vectors that square to +1. + q : int + Number of basis vectors that square to -1. + r : int + Number of basis vectors that square to 0 (degenerate). + root : str + Root name for basis vectors (default ``'e'``). + **kwargs : + Additional keyword arguments passed to :class:`Ga`. + + Returns + ------- + tuple + ``(ga, e_1, ..., e_n)`` -- the geometric algebra and basis vectors. + + Examples + -------- + 3D Euclidean algebra:: + + >>> ga, e1, e2, e3 = Cl(3) + + Spacetime algebra (Minkowski, +---):: + + >>> ga, e1, e2, e3, e4 = Cl(1, 3) + + Projective Geometric Algebra:: + + >>> ga, e1, e2, e3 = Cl(2, 0, 1) + """ + n = p + q + r + if n == 0: + raise ValueError("Total dimension p + q + r must be positive.") + + # Build diagonal metric: p ones, q negative ones, r zeros + g = [1] * p + [-1] * q + [0] * r + + # Build basis name string (1-indexed) + if n <= 9: + basis = root + '*' + '|'.join(str(i + 1) for i in range(n)) + else: + basis = ' '.join(root + '_' + str(i + 1) for i in range(n)) + + return Ga.build(basis, g=g, **kwargs) diff --git a/galgebra/interop/kingdon.py b/galgebra/interop/kingdon.py new file mode 100644 index 00000000..27045447 --- /dev/null +++ b/galgebra/interop/kingdon.py @@ -0,0 +1,68 @@ +""" +Kingdon-compatible ``Cl(p, q, r)`` factory. + +Uses kingdon conventions: +- Dual mode ``'Iinv+'`` (polarity dual: ``dual(A) = A * I^{-1}``) +- 0-indexed basis names for PGA (``e0, e1, ...``) +""" + +from ..ga import Ga + + +def Cl(p: int, q: int = 0, r: int = 0, root: str = 'e', **kwargs): + r""" + Construct a Clifford algebra :math:`Cl(p,q,r)` using kingdon conventions. + + Differences from ``galgebra.interop.Cl``: + + - **Dual mode**: sets ``'Iinv+'`` globally so that + ``dual(A) = A * I^{-1}``, matching kingdon's ``codegen_polarity``. + - **Basis naming**: uses 0-indexed names (``e_0, e_1, ...``) to match + kingdon's default for PGA algebras. + + Parameters + ---------- + p : int + Number of basis vectors that square to +1. + q : int + Number of basis vectors that square to -1. + r : int + Number of basis vectors that square to 0 (degenerate). + root : str + Root name for basis vectors (default ``'e'``). + **kwargs : + Additional keyword arguments passed to :class:`Ga`. + + Returns + ------- + tuple + ``(ga, e_0, ..., e_{n-1})`` -- the geometric algebra and basis vectors. + + Examples + -------- + 3D Euclidean algebra:: + + >>> from galgebra.interop.kingdon import Cl + >>> ga, e1, e2, e3 = Cl(3) + + 3D PGA (0-indexed, degenerate metric):: + + >>> ga, e0, e1, e2, e3 = Cl(3, 0, 1) + """ + n = p + q + r + if n == 0: + raise ValueError("Total dimension p + q + r must be positive.") + + # Set kingdon dual convention + Ga.dual_mode('Iinv+') + + # Build diagonal metric: p ones, q negative ones, r zeros + g = [1] * p + [-1] * q + [0] * r + + # Build basis name string (0-indexed, matching kingdon) + if n <= 10: + basis = root + '*' + '|'.join(str(i) for i in range(n)) + else: + basis = ' '.join(root + '_' + str(i) for i in range(n)) + + return Ga.build(basis, g=g, **kwargs) diff --git a/test/test_test.py b/test/test_test.py index a580363d..aa8d77a6 100644 --- a/test/test_test.py +++ b/test/test_test.py @@ -689,8 +689,8 @@ def test_deprecations(self): assert ga_ortho.dot_product_basis_blades((e_1.obj, e_12.obj), mode='>') == (e_1 > e_12).obj def test_Cl(self): - """Test the Cl(p, q, r) kingdon-like interface (issue 524).""" - from galgebra.ga import Cl + """Test the Cl(p, q, r) interface (issue 524).""" + from galgebra.interop import Cl # 3D Euclidean: Cl(3) ga3, e1, e2, e3 = Cl(3) @@ -717,3 +717,20 @@ def test_Cl(self): # Error on zero dimension with pytest.raises(ValueError): Cl(0) + + def test_Cl_kingdon(self): + """Test the kingdon-convention Cl (0-indexed, Iinv+ dual).""" + from galgebra.interop.kingdon import Cl as KCl + from galgebra.ga import Ga + + # Save and restore global dual mode + saved = Ga.dual_mode_value + try: + # 3D PGA: Cl(3, 0, 1) with 0-indexed basis + ga, e0, e1, e2, e3 = KCl(3, 0, 1) + assert e0 * e0 == ga.mv(1) + assert e3 * e3 == ga.mv(0) + # Dual mode should be Iinv+ + assert Ga.dual_mode_value == 'Iinv+' + finally: + Ga.dual_mode(saved) From 524e3f2f3fbc18006649de0f576897abe747854b Mon Sep 17 00:00:00 2001 From: Ulysses Tiberious Date: Mon, 30 Mar 2026 19:10:41 +0800 Subject: [PATCH 3/3] Add warning about session-wide dual mode side effect in kingdon.Cl --- galgebra/interop/kingdon.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/galgebra/interop/kingdon.py b/galgebra/interop/kingdon.py index 27045447..e245c87b 100644 --- a/galgebra/interop/kingdon.py +++ b/galgebra/interop/kingdon.py @@ -20,6 +20,19 @@ def Cl(p: int, q: int = 0, r: int = 0, root: str = 'e', **kwargs): - **Basis naming**: uses 0-indexed names (``e_0, e_1, ...``) to match kingdon's default for PGA algebras. + .. warning:: + + The dual mode change is session-wide. If you mix kingdon and + galgebra conventions in the same session, save and restore + ``Ga.dual_mode_value`` around the kingdon block:: + + saved = Ga.dual_mode_value + try: + ga, *basis = Cl(3, 0, 1) + # ... kingdon-convention code ... + finally: + Ga.dual_mode(saved) + Parameters ---------- p : int