diff --git a/doc/changelog.rst b/doc/changelog.rst index ee20e9b0..58a4bf9b 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -8,6 +8,11 @@ Changelog \newcommand {\es}[1] {\mathbf{e}_{#1}} \newcommand {\til}[1] {\widetilde{#1}} +- :bug:`514` The default dual mode has changed from ``'I+'`` to ``'Iinv+'`` so that + ``B * B.dual() == I`` holds for unit blades, matching the standard definition + (Doran & Lasenby). To preserve the old behavior, call ``Ga.dual_mode('I+')`` + at the start of your program. + - :bug:`518` :class:`~galgebra.mv.Mv` now correctly returns ``Mv`` instance when raise to power of zero. But in general, if one needs to call ``Mv`` methods on a result returned by some GA operations, it would be more prudent to initialize it as an ``Mv`` instance first, as sometimes the result becomes a ``sympy`` object. - :bug:`516` :attr:`~galgebra.mv.Mv.grades` no longer incorrectly returns ``None`` under some circumstances, as now all initialization branch will correctly call :meth:`~galgebra.mv.Mv.characterise_Mv`. diff --git a/doc/module-components.rst b/doc/module-components.rst index 22315e1a..0c02168c 100644 --- a/doc/module-components.rst +++ b/doc/module-components.rst @@ -339,7 +339,7 @@ If we can instantiate multivectors we can use all the multivector class function .. method:: galgebra.mv.Mv.dual() :noindex: - The mode of the ``dual()`` function is set by the ``Ga`` class static member function, ``GA.dual_mode(mode='I+')`` of the ``GA`` geometric galgebra which sets the following return values (:math:`I` is the pseudo-scalar for the geometric algebra ``GA``) + The mode of the ``dual()`` function is set by the ``Ga`` class static member function, ``GA.dual_mode(mode='Iinv+')`` of the ``GA`` geometric galgebra which sets the following return values (:math:`I` is the pseudo-scalar for the geometric algebra ``GA``) =========== ================ ``mode`` Return Value @@ -356,7 +356,7 @@ If we can instantiate multivectors we can use all the multivector class function For example if the geometric algebra is ``o3d``, ``A`` is a multivector in ``o3d``, and we wish to use ``mode='I-'``. We set the mode with the function ``o3d.dual('I-')`` and get the dual of ``A`` with the function ``A.dual()`` which returns :math:`-AI`. - If ``o3d.dual(mode)`` is not called the default for the dual mode is ``mode='I+'`` and ``A*I`` is returned. + If ``o3d.dual(mode)`` is not called the default for the dual mode is ``mode='Iinv+'`` and ``A*I^{-1}`` is returned. Note that ``Ga.dual(mode)`` used the function ``Ga.I()`` to calculate the normalized pseudoscalar. Thus if the metric tensor is not numerical and orthogonal the correct hint for then ``sig`` input of the *Ga* constructor is required. diff --git a/examples/ipython/Smith Sphere.ipynb b/examples/ipython/Smith Sphere.ipynb index 75b993e6..57fc02dc 100644 --- a/examples/ipython/Smith Sphere.ipynb +++ b/examples/ipython/Smith Sphere.ipynb @@ -19,7 +19,14 @@ { "cell_type": "code", "execution_count": 1, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T07:43:51.081888Z", + "iopub.status.busy": "2026-03-30T07:43:51.081668Z", + "iopub.status.idle": "2026-03-30T07:43:52.052483Z", + "shell.execute_reply": "2026-03-30T07:43:52.051405Z" + } + }, "outputs": [], "source": [ "#from IPython.display import SVG\n", @@ -30,7 +37,14 @@ { "cell_type": "code", "execution_count": 2, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T07:43:52.056481Z", + "iopub.status.busy": "2026-03-30T07:43:52.056286Z", + "iopub.status.idle": "2026-03-30T07:43:52.110490Z", + "shell.execute_reply": "2026-03-30T07:43:52.109778Z" + } + }, "outputs": [], "source": [ "from galgebra import ga\n", @@ -50,7 +64,7 @@ " '''\n", " stereographically project a vector in G3 downto the bivector N\n", " '''\n", - " n= -1*N.dual()\n", + " n= N.dual()\n", " return -(n^p)*(n-n*(n|p)).inv() \n", "\n", "def up(p):\n", @@ -62,7 +76,7 @@ " elif (p^Bs).obj == 0:\n", " N = Bs\n", " \n", - " n = -N.dual()\n", + " n = N.dual()\n", " \n", " return n + 2*(p*p + 1).inv()*(p-n)\n", " \n", @@ -81,15 +95,22 @@ { "cell_type": "code", "execution_count": 3, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T07:43:52.114557Z", + "iopub.status.busy": "2026-03-30T07:43:52.114284Z", + "iopub.status.idle": "2026-03-30T07:43:52.121067Z", + "shell.execute_reply": "2026-03-30T07:43:52.119835Z" + } + }, "outputs": [ { "data": { "text/latex": [ - "\\begin{equation*}- \\boldsymbol{e}_{s}\\end{equation*}" + "\\begin{equation*} \\boldsymbol{e}_{s}\\end{equation*}" ], "text/plain": [ - "-e_s" + "e_s" ] }, "execution_count": 3, @@ -104,7 +125,14 @@ { "cell_type": "code", "execution_count": 4, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T07:43:52.181509Z", + "iopub.status.busy": "2026-03-30T07:43:52.181279Z", + "iopub.status.idle": "2026-03-30T07:43:52.185078Z", + "shell.execute_reply": "2026-03-30T07:43:52.184386Z" + } + }, "outputs": [ { "data": { @@ -124,7 +152,14 @@ { "cell_type": "code", "execution_count": 5, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T07:43:52.190028Z", + "iopub.status.busy": "2026-03-30T07:43:52.189840Z", + "iopub.status.idle": "2026-03-30T07:43:52.194750Z", + "shell.execute_reply": "2026-03-30T07:43:52.193673Z" + } + }, "outputs": [ { "data": { @@ -155,7 +190,14 @@ { "cell_type": "code", "execution_count": 6, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T07:43:52.197764Z", + "iopub.status.busy": "2026-03-30T07:43:52.197554Z", + "iopub.status.idle": "2026-03-30T07:43:52.378352Z", + "shell.execute_reply": "2026-03-30T07:43:52.377614Z" + } + }, "outputs": [ { "data": { @@ -179,7 +221,14 @@ { "cell_type": "code", "execution_count": 7, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T07:43:52.383180Z", + "iopub.status.busy": "2026-03-30T07:43:52.382998Z", + "iopub.status.idle": "2026-03-30T07:43:52.431466Z", + "shell.execute_reply": "2026-03-30T07:43:52.430376Z" + } + }, "outputs": [ { "data": { @@ -209,7 +258,14 @@ { "cell_type": "code", "execution_count": 8, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T07:43:52.435047Z", + "iopub.status.busy": "2026-03-30T07:43:52.434874Z", + "iopub.status.idle": "2026-03-30T07:43:52.529889Z", + "shell.execute_reply": "2026-03-30T07:43:52.529102Z" + } + }, "outputs": [ { "data": { @@ -232,7 +288,14 @@ { "cell_type": "code", "execution_count": 9, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T07:43:52.534863Z", + "iopub.status.busy": "2026-03-30T07:43:52.534694Z", + "iopub.status.idle": "2026-03-30T07:43:52.958048Z", + "shell.execute_reply": "2026-03-30T07:43:52.957068Z" + } + }, "outputs": [ { "data": { @@ -255,7 +318,14 @@ { "cell_type": "code", "execution_count": 10, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T07:43:52.961133Z", + "iopub.status.busy": "2026-03-30T07:43:52.960973Z", + "iopub.status.idle": "2026-03-30T07:43:53.012279Z", + "shell.execute_reply": "2026-03-30T07:43:53.011641Z" + } + }, "outputs": [ { "data": { @@ -278,7 +348,14 @@ { "cell_type": "code", "execution_count": 11, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T07:43:53.016181Z", + "iopub.status.busy": "2026-03-30T07:43:53.015975Z", + "iopub.status.idle": "2026-03-30T07:43:53.067710Z", + "shell.execute_reply": "2026-03-30T07:43:53.065599Z" + } + }, "outputs": [ { "data": { @@ -301,7 +378,14 @@ { "cell_type": "code", "execution_count": 12, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T07:43:53.072173Z", + "iopub.status.busy": "2026-03-30T07:43:53.072020Z", + "iopub.status.idle": "2026-03-30T07:43:53.097579Z", + "shell.execute_reply": "2026-03-30T07:43:53.096840Z" + } + }, "outputs": [ { "data": { @@ -325,7 +409,14 @@ { "cell_type": "code", "execution_count": 13, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T07:43:53.101304Z", + "iopub.status.busy": "2026-03-30T07:43:53.100943Z", + "iopub.status.idle": "2026-03-30T07:43:53.171192Z", + "shell.execute_reply": "2026-03-30T07:43:53.169464Z" + } + }, "outputs": [ { "data": { @@ -348,7 +439,14 @@ { "cell_type": "code", "execution_count": 14, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T07:43:53.174960Z", + "iopub.status.busy": "2026-03-30T07:43:53.174821Z", + "iopub.status.idle": "2026-03-30T07:43:54.002785Z", + "shell.execute_reply": "2026-03-30T07:43:54.001491Z" + } + }, "outputs": [ { "data": { @@ -385,7 +483,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.0" + "version": "3.11.14" } }, "nbformat": 4, diff --git a/galgebra/ga.py b/galgebra/ga.py index feb8b40e..4607553a 100644 --- a/galgebra/ga.py +++ b/galgebra/ga.py @@ -565,7 +565,7 @@ class Ga(metric.Metric): ======= ====================== """ - dual_mode_value = 'I+' + dual_mode_value = 'Iinv+' dual_mode_lst = ['+I', 'I+', '-I', 'I-', '+Iinv', 'Iinv+', '-Iinv', 'Iinv-'] presets = {'o3d': 'x,y,z:[1,1,1]:[1,1,0]', @@ -574,12 +574,12 @@ class Ga(metric.Metric): 'para3d': 'u,v,z:[u**2+v**2,u**2+v**2,1]:[1,1,0]:norm=True'} @staticmethod - def dual_mode(mode='I+'): + def dual_mode(mode='Iinv+'): """ Sets mode of multivector dual function for all geometric algebras in users program. - If Ga.dual_mode(mode) not called the default mode is ``'I+'``. + If Ga.dual_mode(mode) not called the default mode is ``'Iinv+'``. ===== ============ mode return value diff --git a/galgebra/primer.py b/galgebra/primer.py index 30305457..fb38fcd8 100644 --- a/galgebra/primer.py +++ b/galgebra/primer.py @@ -32,9 +32,8 @@ # Default `Dmode=True` causes partial differentiation # operators to be displayed in shortened form. Ga.dual_mode('Iinv+') -# Sets multivector dualization to be right multiplication by the -# by the inverse unit pseudoscalar (the convention used in the -# textbooks LAGA, VAGC, and GACS). +# Confirms the default dual mode: right multiplication by the inverse +# pseudoscalar (convention of LAGA, VAGC, and GACS). initializations_list = r"""\textsf{The following initialization commands were executed:}\\ \quad\texttt{from sys import version}\\ \quad\texttt{import sympy}\\ diff --git a/test/test_test.py b/test/test_test.py index aa8d77a6..2bddcf09 100644 --- a/test/test_test.py +++ b/test/test_test.py @@ -530,7 +530,7 @@ def test_dual_mode(self): ga, e1, e2 = Ga.build('e*1|2', g=[1, 1]) default = Ga.dual_mode_value - assert default == 'I+' + assert default == 'Iinv+' with pytest.raises(ValueError): Ga.dual_mode('illegal') @@ -538,13 +538,25 @@ def test_dual_mode(self): # note: this is a global setting, so we have to make sure we put it back try: - Ga.dual_mode('I-') + Ga.dual_mode('Iinv-') d_negated = e1.dual() finally: Ga.dual_mode(default) assert d_negated == -d_default + def test_dual_correctness(self): + """Test Iinv+ dual mode on a unit bivector in 3D Euclidean space (issue 514).""" + from sympy import symbols as S_symbols + ga = Ga('e', g=[1, 1, 1], coords=S_symbols('x y z', real=True), wedge=False) + ex, ey, ez = ga.mv() + I = ga.I() + + # For a unit bivector B with B*B = -1, B * dual(B) == I under Iinv+ mode + bivector = ex * I # = e_yz, a unit bivector + assert bivector * bivector.dual() == I + assert bivector.dual() * bivector == I + def test_basis_dict(self): ga = Ga('e*1|2', g=[1, 1]) b = ga.bases_dict()