From 2677ff9bd9571bb9fdd9bbbe2447cf915cab576c Mon Sep 17 00:00:00 2001 From: Ulysses Tiberious Date: Mon, 30 Mar 2026 00:24:31 +0800 Subject: [PATCH 1/4] Fix Mv.dual() to use conventional I-inverse by default The default dual_mode was 'I+' which computes self*I. The conventional Hodge dual in geometric algebra is self*I^{-1}. Changed the default to 'Iinv+' so that for any blade B, B * B.dual() == I holds, matching standard textbook definitions (Doran & Lasenby, Macdonald). Fixes utiberious/galgebra#2 --- galgebra/ga.py | 6 +++--- test/test_test.py | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) 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/test/test_test.py b/test/test_test.py index aa8d77a6..c9962fd1 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 that v * v.dual() == I 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 any blade B, B * B.dual() should equal the pseudoscalar I + bivector = ex * I # = e_yz + 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() From 024a97c5a4f24b959a5bd7a2f638f747423438b7 Mon Sep 17 00:00:00 2001 From: Ulysses Tiberious Date: Mon, 30 Mar 2026 08:26:48 +0800 Subject: [PATCH 2/4] Update Smith Sphere notebook output to match corrected dual operation The dual fix (issue #514) changes sign conventions in the dual operation, which propagates through the Smith Sphere calculations. Update saved outputs to reflect the corrected results. --- examples/ipython/Smith Sphere.ipynb | 152 +++++++++++++++++++++++----- 1 file changed, 125 insertions(+), 27 deletions(-) diff --git a/examples/ipython/Smith Sphere.ipynb b/examples/ipython/Smith Sphere.ipynb index 75b993e6..55f2608c 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-30T00:26:36.122329Z", + "iopub.status.busy": "2026-03-30T00:26:36.122252Z", + "iopub.status.idle": "2026-03-30T00:26:36.232096Z", + "shell.execute_reply": "2026-03-30T00:26:36.231397Z" + } + }, "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-30T00:26:36.234234Z", + "iopub.status.busy": "2026-03-30T00:26:36.234103Z", + "iopub.status.idle": "2026-03-30T00:26:36.254295Z", + "shell.execute_reply": "2026-03-30T00:26:36.253833Z" + } + }, "outputs": [], "source": [ "from galgebra import ga\n", @@ -81,15 +95,22 @@ { "cell_type": "code", "execution_count": 3, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T00:26:36.256486Z", + "iopub.status.busy": "2026-03-30T00:26:36.256380Z", + "iopub.status.idle": "2026-03-30T00:26:36.259455Z", + "shell.execute_reply": "2026-03-30T00:26:36.259117Z" + } + }, "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-30T00:26:36.287609Z", + "iopub.status.busy": "2026-03-30T00:26:36.287430Z", + "iopub.status.idle": "2026-03-30T00:26:36.289740Z", + "shell.execute_reply": "2026-03-30T00:26:36.289341Z" + } + }, "outputs": [ { "data": { @@ -124,7 +152,14 @@ { "cell_type": "code", "execution_count": 5, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T00:26:36.291720Z", + "iopub.status.busy": "2026-03-30T00:26:36.291625Z", + "iopub.status.idle": "2026-03-30T00:26:36.294129Z", + "shell.execute_reply": "2026-03-30T00:26:36.293636Z" + } + }, "outputs": [ { "data": { @@ -155,15 +190,22 @@ { "cell_type": "code", "execution_count": 6, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T00:26:36.296348Z", + "iopub.status.busy": "2026-03-30T00:26:36.296245Z", + "iopub.status.idle": "2026-03-30T00:26:36.370589Z", + "shell.execute_reply": "2026-03-30T00:26:36.370034Z" + } + }, "outputs": [ { "data": { "text/latex": [ - "\\begin{equation*}\\frac{2 z^{r}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{r} + \\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x} + \\frac{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} - 1}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{s}\\end{equation*}" + "\\begin{equation*}\\frac{2 z^{r}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{r} + \\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x} + \\frac{- {\\left ( z^{r} \\right )}^{2} - {\\left ( z^{x} \\right )}^{2} + 1}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{s}\\end{equation*}" ], "text/plain": [ - "2*z__r*e_r/(z__r**2 + z__x**2 + 1) + 2*z__x*e_x/(z__r**2 + z__x**2 + 1) + (z__r**2 + z__x**2 - 1)*e_s/(z__r**2 + z__x**2 + 1)" + "2*z__r*e_r/(z__r**2 + z__x**2 + 1) + 2*z__x*e_x/(z__r**2 + z__x**2 + 1) + (-z__r**2 - z__x**2 + 1)*e_s/(z__r**2 + z__x**2 + 1)" ] }, "execution_count": 6, @@ -179,7 +221,14 @@ { "cell_type": "code", "execution_count": 7, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T00:26:36.372493Z", + "iopub.status.busy": "2026-03-30T00:26:36.372392Z", + "iopub.status.idle": "2026-03-30T00:26:36.395662Z", + "shell.execute_reply": "2026-03-30T00:26:36.395079Z" + } + }, "outputs": [ { "data": { @@ -209,7 +258,14 @@ { "cell_type": "code", "execution_count": 8, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T00:26:36.397682Z", + "iopub.status.busy": "2026-03-30T00:26:36.397566Z", + "iopub.status.idle": "2026-03-30T00:26:36.444447Z", + "shell.execute_reply": "2026-03-30T00:26:36.444132Z" + } + }, "outputs": [ { "data": { @@ -232,15 +288,22 @@ { "cell_type": "code", "execution_count": 9, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T00:26:36.446459Z", + "iopub.status.busy": "2026-03-30T00:26:36.446325Z", + "iopub.status.idle": "2026-03-30T00:26:36.680448Z", + "shell.execute_reply": "2026-03-30T00:26:36.679967Z" + } + }, "outputs": [ { "data": { "text/latex": [ - "\\begin{equation*}\\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} + 2 z^{r} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x} + \\frac{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} - 1}{{\\left ( z^{r} \\right )}^{2} + 2 z^{r} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{s}\\end{equation*}" + "\\begin{equation*}\\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} - 2 z^{r} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x} + \\frac{- {\\left ( z^{r} \\right )}^{2} - {\\left ( z^{x} \\right )}^{2} + 1}{{\\left ( z^{r} \\right )}^{2} - 2 z^{r} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{s}\\end{equation*}" ], "text/plain": [ - "2*z__x*e_x/(z__r**2 + 2*z__r + z__x**2 + 1) + (z__r**2 + z__x**2 - 1)*e_s/(z__r**2 + 2*z__r + z__x**2 + 1)" + "2*z__x*e_x/(z__r**2 - 2*z__r + z__x**2 + 1) + (-z__r**2 - z__x**2 + 1)*e_s/(z__r**2 - 2*z__r + z__x**2 + 1)" ] }, "execution_count": 9, @@ -255,7 +318,14 @@ { "cell_type": "code", "execution_count": 10, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T00:26:36.682572Z", + "iopub.status.busy": "2026-03-30T00:26:36.682437Z", + "iopub.status.idle": "2026-03-30T00:26:36.711358Z", + "shell.execute_reply": "2026-03-30T00:26:36.710919Z" + } + }, "outputs": [ { "data": { @@ -278,15 +348,22 @@ { "cell_type": "code", "execution_count": 11, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T00:26:36.713552Z", + "iopub.status.busy": "2026-03-30T00:26:36.713451Z", + "iopub.status.idle": "2026-03-30T00:26:36.738353Z", + "shell.execute_reply": "2026-03-30T00:26:36.737902Z" + } + }, "outputs": [ { "data": { "text/latex": [ - "\\begin{equation*}\\frac{2 z^{r}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{r} + \\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x} + \\frac{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} - 1}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{s}\\end{equation*}" + "\\begin{equation*}\\frac{2 z^{r}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{r} + \\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x} + \\frac{- {\\left ( z^{r} \\right )}^{2} - {\\left ( z^{x} \\right )}^{2} + 1}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{s}\\end{equation*}" ], "text/plain": [ - "2*z__r*e_r/(z__r**2 + z__x**2 + 1) + 2*z__x*e_x/(z__r**2 + z__x**2 + 1) + (z__r**2 + z__x**2 - 1)*e_s/(z__r**2 + z__x**2 + 1)" + "2*z__r*e_r/(z__r**2 + z__x**2 + 1) + 2*z__x*e_x/(z__r**2 + z__x**2 + 1) + (-z__r**2 - z__x**2 + 1)*e_s/(z__r**2 + z__x**2 + 1)" ] }, "execution_count": 11, @@ -301,7 +378,14 @@ { "cell_type": "code", "execution_count": 12, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T00:26:36.740263Z", + "iopub.status.busy": "2026-03-30T00:26:36.740131Z", + "iopub.status.idle": "2026-03-30T00:26:36.751617Z", + "shell.execute_reply": "2026-03-30T00:26:36.751238Z" + } + }, "outputs": [ { "data": { @@ -325,15 +409,22 @@ { "cell_type": "code", "execution_count": 13, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T00:26:36.753692Z", + "iopub.status.busy": "2026-03-30T00:26:36.753550Z", + "iopub.status.idle": "2026-03-30T00:26:36.784005Z", + "shell.execute_reply": "2026-03-30T00:26:36.783539Z" + } + }, "outputs": [ { "data": { "text/latex": [ - "\\begin{equation*}\\frac{- {\\left ( z^{r} \\right )}^{2} - {\\left ( z^{x} \\right )}^{2} + 1}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{r} + \\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x} + \\frac{2 z^{r}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{s}\\end{equation*}" + "\\begin{equation*}\\frac{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} - 1}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{r} + \\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x} + \\frac{2 z^{r}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{s}\\end{equation*}" ], "text/plain": [ - "(-z__r**2 - z__x**2 + 1)*e_r/(z__r**2 + z__x**2 + 1) + 2*z__x*e_x/(z__r**2 + z__x**2 + 1) + 2*z__r*e_s/(z__r**2 + z__x**2 + 1)" + "(z__r**2 + z__x**2 - 1)*e_r/(z__r**2 + z__x**2 + 1) + 2*z__x*e_x/(z__r**2 + z__x**2 + 1) + 2*z__r*e_s/(z__r**2 + z__x**2 + 1)" ] }, "execution_count": 13, @@ -348,15 +439,22 @@ { "cell_type": "code", "execution_count": 14, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-30T00:26:36.785992Z", + "iopub.status.busy": "2026-03-30T00:26:36.785890Z", + "iopub.status.idle": "2026-03-30T00:26:37.128033Z", + "shell.execute_reply": "2026-03-30T00:26:37.127627Z" + } + }, "outputs": [ { "data": { "text/latex": [ - "\\begin{equation*}\\frac{- {\\left ( z^{r} \\right )}^{2} - {\\left ( z^{x} \\right )}^{2} + 1}{{\\left ( z^{r} \\right )}^{2} - 2 z^{r} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{r} + \\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} - 2 z^{r} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x}\\end{equation*}" + "\\begin{equation*}\\frac{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} - 1}{{\\left ( z^{r} \\right )}^{2} + 2 z^{r} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{r} + \\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} + 2 z^{r} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x}\\end{equation*}" ], "text/plain": [ - "(-z__r**2 - z__x**2 + 1)*e_r/(z__r**2 - 2*z__r + z__x**2 + 1) + 2*z__x*e_x/(z__r**2 - 2*z__r + z__x**2 + 1)" + "(z__r**2 + z__x**2 - 1)*e_r/(z__r**2 + 2*z__r + z__x**2 + 1) + 2*z__x*e_x/(z__r**2 + 2*z__r + z__x**2 + 1)" ] }, "execution_count": 14, @@ -385,7 +483,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.0" + "version": "3.11.14" } }, "nbformat": 4, From 4456f1128bb69ebdea31c0f6d1d1e0bd68379b60 Mon Sep 17 00:00:00 2001 From: Ulysses Tiberious Date: Mon, 30 Mar 2026 15:44:51 +0800 Subject: [PATCH 3/4] Fix test comment and Smith Sphere notebook dual formula - Correct test_dual_correctness docstring: B*dual(B)=I holds for unit bivectors, not all blades - Remove negation in down()/up() functions: with Iinv+ mode, N.dual() already gives the correct outward normal --- examples/ipython/Smith Sphere.ipynb | 136 ++++++++++++++-------------- test/test_test.py | 6 +- 2 files changed, 71 insertions(+), 71 deletions(-) diff --git a/examples/ipython/Smith Sphere.ipynb b/examples/ipython/Smith Sphere.ipynb index 55f2608c..57fc02dc 100644 --- a/examples/ipython/Smith Sphere.ipynb +++ b/examples/ipython/Smith Sphere.ipynb @@ -21,10 +21,10 @@ "execution_count": 1, "metadata": { "execution": { - "iopub.execute_input": "2026-03-30T00:26:36.122329Z", - "iopub.status.busy": "2026-03-30T00:26:36.122252Z", - "iopub.status.idle": "2026-03-30T00:26:36.232096Z", - "shell.execute_reply": "2026-03-30T00:26:36.231397Z" + "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": [], @@ -39,10 +39,10 @@ "execution_count": 2, "metadata": { "execution": { - "iopub.execute_input": "2026-03-30T00:26:36.234234Z", - "iopub.status.busy": "2026-03-30T00:26:36.234103Z", - "iopub.status.idle": "2026-03-30T00:26:36.254295Z", - "shell.execute_reply": "2026-03-30T00:26:36.253833Z" + "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": [], @@ -64,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", @@ -76,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", @@ -97,10 +97,10 @@ "execution_count": 3, "metadata": { "execution": { - "iopub.execute_input": "2026-03-30T00:26:36.256486Z", - "iopub.status.busy": "2026-03-30T00:26:36.256380Z", - "iopub.status.idle": "2026-03-30T00:26:36.259455Z", - "shell.execute_reply": "2026-03-30T00:26:36.259117Z" + "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": [ @@ -127,10 +127,10 @@ "execution_count": 4, "metadata": { "execution": { - "iopub.execute_input": "2026-03-30T00:26:36.287609Z", - "iopub.status.busy": "2026-03-30T00:26:36.287430Z", - "iopub.status.idle": "2026-03-30T00:26:36.289740Z", - "shell.execute_reply": "2026-03-30T00:26:36.289341Z" + "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": [ @@ -154,10 +154,10 @@ "execution_count": 5, "metadata": { "execution": { - "iopub.execute_input": "2026-03-30T00:26:36.291720Z", - "iopub.status.busy": "2026-03-30T00:26:36.291625Z", - "iopub.status.idle": "2026-03-30T00:26:36.294129Z", - "shell.execute_reply": "2026-03-30T00:26:36.293636Z" + "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": [ @@ -192,20 +192,20 @@ "execution_count": 6, "metadata": { "execution": { - "iopub.execute_input": "2026-03-30T00:26:36.296348Z", - "iopub.status.busy": "2026-03-30T00:26:36.296245Z", - "iopub.status.idle": "2026-03-30T00:26:36.370589Z", - "shell.execute_reply": "2026-03-30T00:26:36.370034Z" + "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": { "text/latex": [ - "\\begin{equation*}\\frac{2 z^{r}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{r} + \\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x} + \\frac{- {\\left ( z^{r} \\right )}^{2} - {\\left ( z^{x} \\right )}^{2} + 1}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{s}\\end{equation*}" + "\\begin{equation*}\\frac{2 z^{r}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{r} + \\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x} + \\frac{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} - 1}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{s}\\end{equation*}" ], "text/plain": [ - "2*z__r*e_r/(z__r**2 + z__x**2 + 1) + 2*z__x*e_x/(z__r**2 + z__x**2 + 1) + (-z__r**2 - z__x**2 + 1)*e_s/(z__r**2 + z__x**2 + 1)" + "2*z__r*e_r/(z__r**2 + z__x**2 + 1) + 2*z__x*e_x/(z__r**2 + z__x**2 + 1) + (z__r**2 + z__x**2 - 1)*e_s/(z__r**2 + z__x**2 + 1)" ] }, "execution_count": 6, @@ -223,10 +223,10 @@ "execution_count": 7, "metadata": { "execution": { - "iopub.execute_input": "2026-03-30T00:26:36.372493Z", - "iopub.status.busy": "2026-03-30T00:26:36.372392Z", - "iopub.status.idle": "2026-03-30T00:26:36.395662Z", - "shell.execute_reply": "2026-03-30T00:26:36.395079Z" + "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": [ @@ -260,10 +260,10 @@ "execution_count": 8, "metadata": { "execution": { - "iopub.execute_input": "2026-03-30T00:26:36.397682Z", - "iopub.status.busy": "2026-03-30T00:26:36.397566Z", - "iopub.status.idle": "2026-03-30T00:26:36.444447Z", - "shell.execute_reply": "2026-03-30T00:26:36.444132Z" + "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": [ @@ -290,20 +290,20 @@ "execution_count": 9, "metadata": { "execution": { - "iopub.execute_input": "2026-03-30T00:26:36.446459Z", - "iopub.status.busy": "2026-03-30T00:26:36.446325Z", - "iopub.status.idle": "2026-03-30T00:26:36.680448Z", - "shell.execute_reply": "2026-03-30T00:26:36.679967Z" + "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": { "text/latex": [ - "\\begin{equation*}\\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} - 2 z^{r} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x} + \\frac{- {\\left ( z^{r} \\right )}^{2} - {\\left ( z^{x} \\right )}^{2} + 1}{{\\left ( z^{r} \\right )}^{2} - 2 z^{r} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{s}\\end{equation*}" + "\\begin{equation*}\\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} + 2 z^{r} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x} + \\frac{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} - 1}{{\\left ( z^{r} \\right )}^{2} + 2 z^{r} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{s}\\end{equation*}" ], "text/plain": [ - "2*z__x*e_x/(z__r**2 - 2*z__r + z__x**2 + 1) + (-z__r**2 - z__x**2 + 1)*e_s/(z__r**2 - 2*z__r + z__x**2 + 1)" + "2*z__x*e_x/(z__r**2 + 2*z__r + z__x**2 + 1) + (z__r**2 + z__x**2 - 1)*e_s/(z__r**2 + 2*z__r + z__x**2 + 1)" ] }, "execution_count": 9, @@ -320,10 +320,10 @@ "execution_count": 10, "metadata": { "execution": { - "iopub.execute_input": "2026-03-30T00:26:36.682572Z", - "iopub.status.busy": "2026-03-30T00:26:36.682437Z", - "iopub.status.idle": "2026-03-30T00:26:36.711358Z", - "shell.execute_reply": "2026-03-30T00:26:36.710919Z" + "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": [ @@ -350,20 +350,20 @@ "execution_count": 11, "metadata": { "execution": { - "iopub.execute_input": "2026-03-30T00:26:36.713552Z", - "iopub.status.busy": "2026-03-30T00:26:36.713451Z", - "iopub.status.idle": "2026-03-30T00:26:36.738353Z", - "shell.execute_reply": "2026-03-30T00:26:36.737902Z" + "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": { "text/latex": [ - "\\begin{equation*}\\frac{2 z^{r}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{r} + \\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x} + \\frac{- {\\left ( z^{r} \\right )}^{2} - {\\left ( z^{x} \\right )}^{2} + 1}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{s}\\end{equation*}" + "\\begin{equation*}\\frac{2 z^{r}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{r} + \\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x} + \\frac{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} - 1}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{s}\\end{equation*}" ], "text/plain": [ - "2*z__r*e_r/(z__r**2 + z__x**2 + 1) + 2*z__x*e_x/(z__r**2 + z__x**2 + 1) + (-z__r**2 - z__x**2 + 1)*e_s/(z__r**2 + z__x**2 + 1)" + "2*z__r*e_r/(z__r**2 + z__x**2 + 1) + 2*z__x*e_x/(z__r**2 + z__x**2 + 1) + (z__r**2 + z__x**2 - 1)*e_s/(z__r**2 + z__x**2 + 1)" ] }, "execution_count": 11, @@ -380,10 +380,10 @@ "execution_count": 12, "metadata": { "execution": { - "iopub.execute_input": "2026-03-30T00:26:36.740263Z", - "iopub.status.busy": "2026-03-30T00:26:36.740131Z", - "iopub.status.idle": "2026-03-30T00:26:36.751617Z", - "shell.execute_reply": "2026-03-30T00:26:36.751238Z" + "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": [ @@ -411,20 +411,20 @@ "execution_count": 13, "metadata": { "execution": { - "iopub.execute_input": "2026-03-30T00:26:36.753692Z", - "iopub.status.busy": "2026-03-30T00:26:36.753550Z", - "iopub.status.idle": "2026-03-30T00:26:36.784005Z", - "shell.execute_reply": "2026-03-30T00:26:36.783539Z" + "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": { "text/latex": [ - "\\begin{equation*}\\frac{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} - 1}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{r} + \\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x} + \\frac{2 z^{r}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{s}\\end{equation*}" + "\\begin{equation*}\\frac{- {\\left ( z^{r} \\right )}^{2} - {\\left ( z^{x} \\right )}^{2} + 1}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{r} + \\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x} + \\frac{2 z^{r}}{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{s}\\end{equation*}" ], "text/plain": [ - "(z__r**2 + z__x**2 - 1)*e_r/(z__r**2 + z__x**2 + 1) + 2*z__x*e_x/(z__r**2 + z__x**2 + 1) + 2*z__r*e_s/(z__r**2 + z__x**2 + 1)" + "(-z__r**2 - z__x**2 + 1)*e_r/(z__r**2 + z__x**2 + 1) + 2*z__x*e_x/(z__r**2 + z__x**2 + 1) + 2*z__r*e_s/(z__r**2 + z__x**2 + 1)" ] }, "execution_count": 13, @@ -441,20 +441,20 @@ "execution_count": 14, "metadata": { "execution": { - "iopub.execute_input": "2026-03-30T00:26:36.785992Z", - "iopub.status.busy": "2026-03-30T00:26:36.785890Z", - "iopub.status.idle": "2026-03-30T00:26:37.128033Z", - "shell.execute_reply": "2026-03-30T00:26:37.127627Z" + "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": { "text/latex": [ - "\\begin{equation*}\\frac{{\\left ( z^{r} \\right )}^{2} + {\\left ( z^{x} \\right )}^{2} - 1}{{\\left ( z^{r} \\right )}^{2} + 2 z^{r} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{r} + \\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} + 2 z^{r} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x}\\end{equation*}" + "\\begin{equation*}\\frac{- {\\left ( z^{r} \\right )}^{2} - {\\left ( z^{x} \\right )}^{2} + 1}{{\\left ( z^{r} \\right )}^{2} - 2 z^{r} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{r} + \\frac{2 z^{x}}{{\\left ( z^{r} \\right )}^{2} - 2 z^{r} + {\\left ( z^{x} \\right )}^{2} + 1} \\boldsymbol{e}_{x}\\end{equation*}" ], "text/plain": [ - "(z__r**2 + z__x**2 - 1)*e_r/(z__r**2 + 2*z__r + z__x**2 + 1) + 2*z__x*e_x/(z__r**2 + 2*z__r + z__x**2 + 1)" + "(-z__r**2 - z__x**2 + 1)*e_r/(z__r**2 - 2*z__r + z__x**2 + 1) + 2*z__x*e_x/(z__r**2 - 2*z__r + z__x**2 + 1)" ] }, "execution_count": 14, diff --git a/test/test_test.py b/test/test_test.py index c9962fd1..2bddcf09 100644 --- a/test/test_test.py +++ b/test/test_test.py @@ -546,14 +546,14 @@ def test_dual_mode(self): assert d_negated == -d_default def test_dual_correctness(self): - """Test that v * v.dual() == I in 3D Euclidean space (issue 514).""" + """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 any blade B, B * B.dual() should equal the pseudoscalar I - bivector = ex * I # = e_yz + # 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 From c1478097663052ace7360abde4ef10d073103432 Mon Sep 17 00:00:00 2001 From: Ulysses Tiberious Date: Tue, 31 Mar 2026 16:52:23 +0800 Subject: [PATCH 4/4] docs: update dual mode default from I+ to Iinv+ in docs and changelog - doc/module-components.rst: update method signature and prose to show Iinv+ as default - doc/changelog.rst: add bug#514 entry noting the breaking change and migration path - galgebra/primer.py: update comment to reflect Iinv+ is now the default, not a workaround --- doc/changelog.rst | 5 +++++ doc/module-components.rst | 4 ++-- galgebra/primer.py | 5 ++--- 3 files changed, 9 insertions(+), 5 deletions(-) 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/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}\\