diff --git a/galgebra/__init__.py b/galgebra/__init__.py index 405c5fae..b2481927 100644 --- a/galgebra/__init__.py +++ b/galgebra/__init__.py @@ -36,3 +36,4 @@ """ from ._version import __version__ # noqa: F401 +from .interop import Cl # noqa: F401 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..e245c87b --- /dev/null +++ b/galgebra/interop/kingdon.py @@ -0,0 +1,81 @@ +""" +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. + + .. 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 + 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 fb6b0405..aa8d77a6 100644 --- a/test/test_test.py +++ b/test/test_test.py @@ -687,3 +687,50 @@ 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) interface (issue 524).""" + from galgebra.interop 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) + + 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)