Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
4 changes: 2 additions & 2 deletions doc/module-components.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down
136 changes: 117 additions & 19 deletions examples/ipython/Smith Sphere.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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,
Expand All @@ -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": {
Expand All @@ -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": {
Expand Down Expand Up @@ -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": {
Expand All @@ -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": {
Expand Down Expand Up @@ -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": {
Expand All @@ -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": {
Expand All @@ -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": {
Expand All @@ -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": {
Expand All @@ -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": {
Expand All @@ -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": {
Expand All @@ -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": {
Expand Down Expand Up @@ -385,7 +483,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.0"
"version": "3.11.14"
}
},
"nbformat": 4,
Expand Down
6 changes: 3 additions & 3 deletions galgebra/ga.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]',
Expand All @@ -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
Expand Down
5 changes: 2 additions & 3 deletions galgebra/primer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}\\
Expand Down
16 changes: 14 additions & 2 deletions test/test_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,21 +530,33 @@ 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')

d_default = e1.dual()

# 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()
Expand Down
Loading