Repository: tBuLi/kingdon (language: python)

Issue:
Custom basis and named algebras
We would like to support custom bases, e.g. starting from `e0` for PGA's. 

Moreover, we would like the user to be able to provide custom basis, and to start an algebra simply from a name, e.g. "3DPGA".

Check with ganja.js and Clifford for their current settings.
Fancier __str__ on MultiVector for printing.
Currently kingdon always prints brackets around everything, e.g.

```python
>>> from kingdon import Algebra
>>> alg = Algebra(3, 0, 1)
>>> b = alg.bivector({'e12': 2})
>>> v = alg.vector({'e1': 3})
>>> b.cp(v)
(-6) * e2
```

Obviously the brackets are not needed here, it would be prettier to print `-6𝐞₂`. Brackets should only be needed if the coefficient is a `sympy.Expr` but not just a single `sympy.Symbol`.
add `__repr__` to map to  `__str__`



Discussion / hints:


After doing some research and soul-searching, I do not think this is the right way, although I agree with your goal. But the `__repr__` serves a higher purpose and I'd hate to give that up.

But it seems that we should define `_repr_pretty_` on our object instead: https://ipython.readthedocs.io/en/stable/api/generated/IPython.lib.pretty.html#extending.

Could you try it and report back?
yes i think you are right. looking at clifford https://github.com/pygae/clifford/blob/master/clifford/_multivector.py#L542-L565. we have a global to abuse `__repr__` and also have `_repr_pretty_`

maybe we should just re-use this? or just use _repr_pretty_?  

I'm not in favour of convoluting `__str__` and `__repr__` so I would prefer a `_repr_pretty_` only approach and then we can see if this already addresses the problem.

Fix patch:
diff --git a/README.rst b/README.rst
index a2ff759..1b2958f 100644
--- a/README.rst
+++ b/README.rst
@@ -148,7 +148,7 @@ Operators
    * - Conjugate :code:`b` by :code:`a`
      - $a b \\widetilde{a}$
      -
-     - :code:`a.conj(b)`
+     - :code:`a.sw(b)`
    * - Project :code:`a` onto :code:`b`
      - $(a \\cdot b) b^{-1}$
      - :code:`a @ b`
diff --git a/docs/_static/graph_triangle.png b/docs/_static/graph_triangle.png
new file mode 100644
index 0000000..eb0c18a
Binary files /dev/null and b/docs/_static/graph_triangle.png differ
diff --git a/docs/conf.py b/docs/conf.py
index 75a4174..2a489b8 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -113,7 +113,7 @@
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = []  # '_static'
+html_static_path = ['_static']  # '_static'
 
 
 # -- Options for HTMLHelp output ---------------------------------------
diff --git a/docs/examples/ex_2dpga_project_and_reject.ipynb b/docs/examples/ex_2dpga_project_and_reject.ipynb
new file mode 100644
index 0000000..c67fbad
--- /dev/null
+++ b/docs/examples/ex_2dpga_project_and_reject.ipynb
@@ -0,0 +1,157 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# 2DPGA: Project and reject\n",
+    "\n",
+    "This example shows how to perform projection and rejection, using 2DPGA.\n",
+    "\n",
+    "Based on https://enkimute.github.io/ganja.js/examples/coffeeshop.html#pga2d_project_and_reject."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "First we create 2DPGA, and add its basis blades to the local namespace."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from kingdon import Algebra\n",
+    "\n",
+    "alg = Algebra(2, 0, 1)\n",
+    "locals().update(alg.blades)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Next, make formulas to construct points and lines from coefficients, and to project and reject any elements of geometry."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [],
+   "source": [
+    "# Construct points and lines from coefficients\n",
+    "point = lambda x, y: (e0 + x*e1 + y*e2).dual()\n",
+    "line = lambda a, b, c: a*e1 + b*e2 + c*e0\n",
+    "\n",
+    "# The formulas for projection and rejection are the same for all elements of geometry in PGA\n",
+    "project = lambda a, b: (a | b) / b\n",
+    "reject = lambda a, b: (a | b)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Construct the points $A$, $B$, and $C$, and make the line $AC$ by joining $A$ and $C$."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "A = point(1, 1)\n",
+    "B = point(-1, 1)\n",
+    "C = point(-1, -1)\n",
+    "AC = A & C"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "data": {
+      "application/javascript": [
+       "\n",
+       "        fetch(\"https://enki.ws/ganja.js/ganja.js\")\n",
+       "        .then(x=>x.text())\n",
+       "        .then(ganja=>{\n",
+       "\n",
+       "          var f = new Function(\"module\",ganja);\n",
+       "          var module = {exports:{}};\n",
+       "          f(module);\n",
+       "          var Algebra = module.exports;\n",
+       "\n",
+       "          var canvas = Algebra(2, 0, 1,()=>{\n",
+       "              var data = [13696993, [[0, 0, 0, 0, 1, -1, 1, 0], [0, 0, 0, 0, 1, 1, 1, 0], [0, 0, 0, 0, -1, 1, 1, 0]], 8921736, [0.0, 0, 0, 0, 0.0, 0.0, 1.0, 0], \"project B onto AC\", 8921736, [0, 4.0, 2.0, -2.0, 0, 0, 0, 0.0], \"project AC onto B\", 34884, [0, 0, 2, 2, 0, 0, 0, 0], \"reject AC from B\", 43656, [0, 0, 2, -2, 0, 0, 0, 0], \"AC\", 2245768, [0, 0, 0, 0, 1, -1, 1, 0], \"A\", [0, 0, 0, 0, 1, 1, 1, 0], \"B\", [0, 0, 0, 0, -1, 1, 1, 0], \"C\"].map(x=>x.length==8?new Element(x):x);\n",
+       "              return this.graph(data, {'lineWidth': 3, 'grid': 1, 'labels': 1})\n",
+       "          })\n",
+       "\n",
+       "          element.append(canvas)\n",
+       "\n",
+       "        })\n",
+       "        "
+      ],
+      "text/plain": [
+       "<IPython.core.display.Javascript object>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "alg.graph(\n",
+    "    0xD0FFE1, [A,B,C],\n",
+    "    0x882288, project(B, AC), \"project B onto AC\",\n",
+    "    0x882288, project(AC, B), \"project AC onto B\",\n",
+    "    0x008844, reject(AC, B),  \"reject AC from B\",\n",
+    "    0x00AA88, AC, \"AC\",\n",
+    "    0x224488, A, \"A\", B, \"B\", C, \"C\",\n",
+    "    lineWidth=3, grid=1, labels=1\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.13"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
\ No newline at end of file
diff --git a/examples/ex_performance.ipynb b/docs/examples/ex_performance.ipynb
similarity index 87%
rename from examples/ex_performance.ipynb
rename to docs/examples/ex_performance.ipynb
index bc4f3f4..31ad6eb 100644
--- a/examples/ex_performance.ipynb
+++ b/docs/examples/ex_performance.ipynb
@@ -18,7 +18,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 1,
+   "execution_count": 24,
    "id": "f99b04a6",
    "metadata": {},
    "outputs": [],
@@ -33,12 +33,12 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 2,
+   "execution_count": 25,
    "id": "d5b7f616",
    "metadata": {},
    "outputs": [],
    "source": [
-    "alg = Algebra(3, 0, 1)"
+    "alg = Algebra(3, 0, 1, numba=False)"
    ]
   },
   {
@@ -53,7 +53,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 3,
+   "execution_count": 26,
    "id": "66c29e63",
    "metadata": {},
    "outputs": [],
@@ -77,7 +77,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 27,
    "id": "cf2e9507",
    "metadata": {},
    "outputs": [],
@@ -97,23 +97,21 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 28,
    "id": "9d89de62",
    "metadata": {},
    "outputs": [
     {
-     "data": {
-      "text/plain": [
-       "0.7187370999963605"
-      ]
-     },
-     "execution_count": 5,
-     "metadata": {},
-     "output_type": "execute_result"
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Time to generate: 1.06E-03\n"
+     ]
     }
    ],
    "source": [
-    "timeit.timeit('b.cp(u)', number=1, globals=globals())"
+    "t = timeit.timeit('b.cp(u)', number=1, globals=globals())\n",
+    "print(f'Time to generate: {t:.2E}')"
    ]
   },
   {
@@ -126,7 +124,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 29,
    "id": "2545fcc3",
    "metadata": {},
    "outputs": [
@@ -134,7 +132,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Total time: 0.34497990000090795, time per iteration: 3.45E-06\n"
+      "Total time: 0.6242352909999909, time per iteration: 6.24E-06\n"
      ]
     }
    ],
@@ -187,7 +185,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 30,
    "id": "5c70d76b",
    "metadata": {},
    "outputs": [],
@@ -197,7 +195,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 31,
    "id": "d032617c",
    "metadata": {},
    "outputs": [],
@@ -209,17 +207,17 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": 32,
    "id": "93ab5abd",
    "metadata": {},
    "outputs": [
     {
      "data": {
       "text/plain": [
-       "0.3909745999990264"
+       "0.10953629200000137"
       ]
      },
-     "execution_count": 11,
+     "execution_count": 32,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -230,7 +228,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": 33,
    "id": "40e174f3",
    "metadata": {},
    "outputs": [
@@ -238,7 +236,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Total time: 0.2105652000027476, time per iteration: 2.11E-06\n"
+      "Total time: 0.4329873329999998, time per iteration: 4.33E-06\n"
      ]
     }
    ],
@@ -283,7 +281,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 15,
+   "execution_count": 34,
    "id": "6adc3335",
    "metadata": {},
    "outputs": [],
@@ -299,17 +297,17 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 16,
+   "execution_count": 35,
    "id": "f6cb4a92",
    "metadata": {},
    "outputs": [],
    "source": [
-    "alg = Algebra(3, 0, 1)"
+    "alg = Algebra(3, 0, 1, numba=False)"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 17,
+   "execution_count": 36,
    "id": "cfd9a98e",
    "metadata": {},
    "outputs": [],
@@ -321,17 +319,17 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 18,
+   "execution_count": 37,
    "id": "35fc8713",
    "metadata": {},
    "outputs": [
     {
      "data": {
       "text/plain": [
-       "1.377215000000433"
+       "0.007173792000003232"
       ]
      },
-     "execution_count": 18,
+     "execution_count": 37,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -342,7 +340,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 19,
+   "execution_count": 38,
    "id": "08ca61fa",
    "metadata": {},
    "outputs": [
@@ -350,7 +348,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Total time: 1.1238376000037533, time per iteration: 1.12E-05\n"
+      "Total time: 1.5837676669999894, time per iteration: 1.58E-05\n"
      ]
     }
    ],
@@ -363,17 +361,17 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 20,
+   "execution_count": 39,
    "id": "c6961c50",
    "metadata": {},
    "outputs": [
     {
      "data": {
       "text/plain": [
-       "3.2576900857145463"
+       "2.5371325361353407"
       ]
      },
-     "execution_count": 20,
+     "execution_count": 39,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -384,7 +382,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 21,
+   "execution_count": 40,
    "id": "70b93350",
    "metadata": {},
    "outputs": [],
@@ -394,7 +392,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 22,
+   "execution_count": 41,
    "id": "17e0b204",
    "metadata": {},
    "outputs": [],
@@ -406,17 +404,17 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 23,
+   "execution_count": 42,
    "id": "8f3bc765",
    "metadata": {},
    "outputs": [
     {
      "data": {
       "text/plain": [
-       "1.6741316999978153"
+       "0.32326487500000667"
       ]
      },
-     "execution_count": 23,
+     "execution_count": 42,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -427,7 +425,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 24,
+   "execution_count": 43,
    "id": "ed892a23",
    "metadata": {},
    "outputs": [
@@ -435,7 +433,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Total time: 0.3859777999969083, time per iteration: 1.12E-05\n"
+      "Total time: 0.45447316699997486, time per iteration: 1.58E-05\n"
      ]
     }
    ],
@@ -448,17 +446,17 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 25,
+   "execution_count": 44,
    "id": "c360a93a",
    "metadata": {},
    "outputs": [
     {
      "data": {
       "text/plain": [
-       "1.83305598452105"
+       "1.0496223153945594"
       ]
      },
-     "execution_count": 25,
+     "execution_count": 44,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -492,9 +490,9 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.10.6"
+   "version": "3.8.13"
   }
  },
  "nbformat": 4,
  "nbformat_minor": 5
-}
+}
\ No newline at end of file
diff --git a/docs/examples/ex_pointcloud.ipynb b/docs/examples/ex_pointcloud.ipynb
index e43366a..09ba3c3 100644
--- a/docs/examples/ex_pointcloud.ipynb
+++ b/docs/examples/ex_pointcloud.ipynb
@@ -22,8 +22,7 @@
     "from kingdon import Algebra\n",
     "from symfit import Fit, Model, CallableModel, Variable, Parameter, Eq, Mul\n",
     "from symfit.core.minimizers import *\n",
-    "import numpy as np\n",
-    "import matplotlib.pyplot as plt"
+    "import numpy as np"
    ]
   },
   {
@@ -55,9 +54,9 @@
    "source": [
     "point_vals = np.zeros((d+1, n_points))\n",
     "noise_vals = np.zeros((d+1, n_points))\n",
-    "point_vals[0, :] = np.ones(n_points)\n",
-    "point_vals[1:d+1, :] = np.random.random((d, n_points))\n",
-    "noise_vals[1:d+1, :] = np.random.normal(0.0, sig, (d, n_points))"
+    "point_vals[-1, :] = np.ones(n_points)\n",
+    "point_vals[:d, :] = np.random.random((d, n_points))\n",
+    "noise_vals[:d, :] = np.random.normal(0.0, sig, (d, n_points))"
    ]
   },
   {
@@ -67,7 +66,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "alg = Algebra(d, 0, 1)"
+    "alg = Algebra(d, 0, 1)\n",
+    "locals().update(alg.blades)"
    ]
   },
   {
@@ -87,12 +87,12 @@
     {
      "data": {
       "text/plain": [
-       "MultiVector(algebra=Algebra(p=2, q=0, r=1, cse=True, precompute='none', numba=False, graded=False, simplify=True), _values=array([[1.        , 1.        , 1.        , 1.        , 1.        ,\n",
-       "        1.        , 1.        , 1.        , 1.        , 1.        ],\n",
-       "       [0.58458649, 0.73592531, 0.11134136, 0.68417876, 0.57177713,\n",
-       "        0.35552796, 0.76742005, 0.21321343, 0.21633547, 0.61716839],\n",
-       "       [0.55369794, 0.08997382, 0.1113173 , 0.64265498, 0.80082692,\n",
-       "        0.24724482, 0.20296079, 0.73196863, 0.62790585, 0.71005653]]), _keys=(3, 5, 6))"
+       "MultiVector(algebra=Algebra(p=2, q=0, r=1, signature=array([0, 1, 1]), cse=True, precompute='none', numba=False, graded=False, simplify=True), _values=array([[0.53012509, 0.10901969, 0.59905615, 0.32591349, 0.00989824,\n",
+       "        0.25001473, 0.45760595, 0.91721811, 0.02428319, 0.28927767],\n",
+       "       [0.76848962, 0.68648854, 0.46787397, 0.01924775, 0.9931417 ,\n",
+       "        0.59096207, 0.02953019, 0.49762635, 0.62617139, 0.31286765],\n",
+       "       [1.        , 1.        , 1.        , 1.        , 1.        ,\n",
+       "        1.        , 1.        , 1.        , 1.        , 1.        ]]), _keys=(3, 5, 6))"
       ]
      },
      "execution_count": 5,
@@ -101,8 +101,8 @@
     }
    ],
    "source": [
-    "noise = alg.purevector(noise_vals, grade=d)\n",
-    "p = alg.purevector(point_vals, grade=d)\n",
+    "noise = alg.multivector(noise_vals, grades=(d,))\n",
+    "p = alg.multivector(point_vals, grades=(d,))\n",
     "p"
    ]
   },
@@ -111,7 +111,7 @@
    "id": "c75f5df2",
    "metadata": {},
    "source": [
-    "As the input rotor $R$, we use a translation by $2$ in the $\\mathbb{e}_{13}$ direction, followed by a rotation around $\\mathbb{e}_{12}$."
+    "As the input rotor $R$, we use a translation by $0.5$ in the $\\mathbb{e}_{20}$ direction, followed by a rotation around $\\mathbb{e}_{12}$."
    ]
   },
   {
@@ -124,20 +124,18 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "(1) * 1 + (1) * e13\n",
-      "(0.5000000000000001) * 1 + (0.8660254037844386) * e12 + (0.5000000000000001) * e13 + (-0.8660254037844386) * e23\n",
-      "(1.0) * 1\n"
+      "T=1 e + -0.5e02\n",
+      "S=0.5e + 0.866e12\n"
      ]
     }
    ],
    "source": [
     "t = np.pi/3\n",
-    "T = alg.multivector({0: 1, 5: 1})\n",
-    "print(T)\n",
-    "R = alg.multivector({0: np.cos(t), 3: np.sin(t)})\n",
-    "R = R*T\n",
-    "print(R)\n",
-    "print(R.normsq())"
+    "T = alg.multivector({'e': 1, 'e02': -0.5})\n",
+    "print(f'{T=!s}')\n",
+    "S = alg.multivector({'e': np.cos(t), 'e12': np.sin(t)})\n",
+    "print(f'{S=!s}')\n",
+    "R = S*T\n"
    ]
   },
   {
@@ -145,7 +143,7 @@
    "id": "88942d1a",
    "metadata": {},
    "source": [
-    "We can now create the transformed pointcloud $q$."
+    "We can now create the transformed pointcloud $q$, and visualize both both $p$ and $q$."
    ]
   },
   {
@@ -153,20 +151,9 @@
    "execution_count": 7,
    "id": "7bfb9aa6",
    "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "([1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]) * e12 + ([1.93770314 1.46211814 1.77184637 1.94216034 2.1288009  1.76473758\n",
-      " 1.54224206 2.22762334 2.1530351  2.05956487]) * e13 + ([-1.76162814 -1.65760549 -1.18332194 -1.92659531 -1.88520711 -1.45905418\n",
-      " -1.75711176 -1.54669495 -1.4820069  -1.91854545]) * e23\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
-    "q = R.conj(p) + noise\n",
-    "print(q)"
+    "q = R.sw(p) + noise"
    ]
   },
   {
@@ -177,19 +164,29 @@
    "outputs": [
     {
      "data": {
+      "application/javascript": [
+       "\n",
+       "        fetch(\"https://enki.ws/ganja.js/ganja.js\")\n",
+       "        .then(x=>x.text())\n",
+       "        .then(ganja=>{\n",
+       "\n",
+       "          var f = new Function(\"module\",ganja);\n",
+       "          var module = {exports:{}};\n",
+       "          f(module);\n",
+       "          var Algebra = module.exports;\n",
+       "\n",
+       "          var canvas = Algebra(2, 0, 1,()=>{\n",
+       "              var data = [16711680, [0, 0, 0, 0, 0.5301250896410105, 0.7684896160983193, 1.0, 0], [0, 0, 0, 0, 0.10901968536025819, 0.6864885443010335, 1.0, 0], [0, 0, 0, 0, 0.5990561468325517, 0.4678739671067561, 1.0, 0], [0, 0, 0, 0, 0.3259134901847519, 0.019247750198401303, 1.0, 0], [0, 0, 0, 0, 0.009898241005036557, 0.9931416990708005, 1.0, 0], [0, 0, 0, 0, 0.25001472981051953, 0.5909620653257089, 1.0, 0], [0, 0, 0, 0, 0.45760594583049574, 0.029530187528896246, 1.0, 0], [0, 0, 0, 0, 0.9172181129391489, 0.4976263539876348, 1.0, 0], [0, 0, 0, 0, 0.024283185446231537, 0.6261713924581105, 1.0, 0], [0, 0, 0, 0, 0.2892776716500949, 0.3128676541656735, 1.0, 0], \"p\", 255, [0, 0, 0, 0, -0.10674589255345503, -1.6735766965820282, 1.0, 0], [0, 0, 0, 0, -0.00729324443553498, -1.3182308420803064, 1.0, 0], [0, 0, 0, 0, -0.36317690233601563, -1.6143593827654934, 1.0, 0], [0, 0, 0, 0, -0.5987852021685599, -1.1726065717331904, 1.0, 0], [0, 0, 0, 0, 0.36703453110502965, -1.390524019835093, 1.0, 0], [0, 0, 0, 0, -0.08428416803890966, -1.3621902989398107, 1.0, 0], [0, 0, 0, 0, -0.7083140409743893, -1.276487953786359, 1.0, 0], [0, 0, 0, 0, -0.5518862670829539, -1.9183244757410058, 1.0, 0], [0, 0, 0, 0, 0.009467688080572377, -1.1810221555381157, 1.0, 0], [0, 0, 0, 0, -0.38067087145585315, -1.298899457535901, 1.0, 0], \"q\", 0, [0, 0, 0, 0, -0.4330127018922193, -0.25000000000000006, 0.8660254037844386, 0], \"axis\"].map(x=>x.length==8?new Element(x):x);\n",
+       "              return this.graph(data, {})\n",
+       "          })\n",
+       "\n",
+       "          element.append(canvas)\n",
+       "\n",
+       "        })\n",
+       "        "
+      ],
       "text/plain": [
-       "<matplotlib.legend.Legend at 0x235ee61ff10>"
-      ]
-     },
-     "execution_count": 8,
-     "metadata": {},
-     "output_type": "execute_result"
-    },
-    {
-     "data": {
-      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXQAAAD4CAYAAAD8Zh1EAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAAAWbElEQVR4nO3dfXBV9Z3H8c9XDE0QFyykKs84YwFJeEgCodJiqLpQqoK060g7K3bHYWp19mF28aFOsaOdsbt2bBdbRbpbrTNbrK2QYk0H6wMFZ0QNBlERRJEOCYxAWqJIWIH97h/3EkK4Se69uU/nd9+vmTu595xzz/kdjn5y8ju/8z3m7gIARN9Z+W4AACAzCHQACASBDgCBINABIBAEOgAE4ux8bXjo0KE+ZsyYfG0eACJp8+bNB929PNG8vAX6mDFj1NjYmK/NA0Akmdmfu5tHlwsABIJAB4BAEOgAEIi89aEDiJ5jx46publZR48ezXdTgldaWqoRI0aopKQk6e8Q6ACS1tzcrHPPPVdjxoyRmeW7OcFyd7W2tqq5uVljx45N+nt0uWTL1ielH1dI3x8c+7n1yXy3COizo0ePasiQIYR5lpmZhgwZkvJfQpyhZ8PWJ6Wn/1E61h773LYn9lmSJl2Xv3YBGUCY50Y6/86coWfD8/ecCvOTjrXHpgNAlhDo2dDWnNp0ABk1b948HTp0qMdlli1bpueeey6t9a9fv15XXXVVWt/NJrpcsmHQiFg3S6LpALLG3eXuamho6HXZe+4J7y9mztCz4fJlUknZ6dNKymLTgSJS39SimT98QWPveEYzf/iC6pta+rzOBx54QBUVFaqoqNBPfvIT7d69WxMmTNB3vvMdVVVVac+ePRozZowOHjwoSbr33ns1fvx4XXnllVq0aJF+9KMfSZJuvPFG/fa3v5UUK0Vy9913q6qqSpWVldq+fbsk6dVXX9Wll16qqVOn6tJLL9WOHTv63P5sItCzYdJ10tXLpUEjJVns59XLuSCKolLf1KI7V7+plkPtckkth9p15+o3+xTqmzdv1qOPPqpXXnlFmzZt0s9//nP99a9/1Y4dO3TDDTeoqalJo0eP7li+sbFRTz31lJqamrR69eoe60cNHTpUr7/+um6++eaO0B8/frw2bNigpqYm3XPPPfrud7+bdttzgS6XbJl0XfcBvvXJ2AXStuZYN8zlywh7BOf+dTvUfuzEadPaj53Q/et2aMHU4Wmt86WXXtK1116rc845R5K0cOFCbdy4UaNHj9aMGTMSLj9//nyVlcX+Yr766qu7XffChQslSdXV1Vq9erUkqa2tTYsXL9bOnTtlZjp27Fha7c4VztBz7eSQxrY9kvzUkEbGqSMwew+1pzQ9Gd091P5kwCe7fCKf+cxnJEn9+vXT8ePHJUnf+973NHv2bL311lt6+umnC/4O2egHetRu4GFII4rEsMFlKU1PxqxZs1RfX68jR47ok08+0Zo1a/SlL32p2+W/+MUvdgTx4cOH9cwzz6S0vba2Ng0fHvtr4rHHHku73bkS7UCP4tkuQxpRJJbOGaeykn6nTSsr6aelc8alvc6qqirdeOONmj59umpra3XTTTfpvPPO63b5adOm6ZprrtHkyZO1cOFC1dTUaNCgQUlv77bbbtOdd96pmTNn6sSJE71/Ic8slT9JMqmmpsb7/ICLH1d0MzxwpPQvb/Vt3dkSxTYDce+8844mTJiQ9PL1TS26f90O7T3UrmGDy7R0zri0+8/TdfjwYQ0cOFBHjhzRrFmztHLlSlVVVeW0DelK9O9tZpvdvSbR8tG+KBrFs93Ll51eFkBiSCOCtWDq8JwHeFdLlizRtm3bdPToUS1evDgyYZ6OaAd6FG/gOTmahVEuQE786le/yncTcibagR7Vs92ehjQCQJqifVGUG3gAoEP0ztAT3ZTDxUQAiFigU2ccALoVrS4XbsoBitqhQ4f00EMPZWx9Bw4cUG1traZOnaqNGzdmbL2p6FwkrK+iFehRHKYIIGO6C/R0b/p5/vnnNX78eDU1NfV4x2kmtpUL0Qr07oYjFvIwRaCYZbg0xx133KH3339fU6ZM0bRp0zR79mx94xvfUGVlpSRpwYIFqq6u1sSJE7Vy5cqO7w0cOFB33XWXJk+erBkzZujDDz/Uli1bdNttt6mhoUFTpkxRe3u7Vq1apcrKSlVUVOj2228/7fvLli1TbW2tXn75ZQ0cOFC33367qqurdcUVV+jVV19VXV2dLrroIq1du1ZSLPiXLl2qadOmadKkSXrkkUckxerL3Hrrrbrkkkv01a9+Vfv37+/Tv8lpThaE7+4laaSkFyW9I+ltSf+UYBmTtFzSe5K2Sqrqbb3V1dWesjd+7f6D893v/ptTrx+cH5sOIOu2bduW/MJZ+P/1gw8+8IkTJ7q7+4svvugDBgzwXbt2dcxvbW11d/cjR474xIkT/eDBg+7uLsnXrl3r7u5Lly71e++9193dH330Ub/lllvc3b2lpcVHjhzp+/fv92PHjvns2bN9zZo1Hd//9a9PtVuSNzQ0uLv7ggUL/Morr/RPP/3Ut2zZ4pMnT3Z390ceeaRjO0ePHvXq6mrftWuXP/XUU37FFVf48ePHvaWlxQcNGuS/+c1vEu5von9vSY3eTa4mc4Z+XNK/uvsESTMk3WJml3RZ5iuSLo6/lkh6uE+/ZbrDMEUgOnJwzWv69OkaO3Zsx+fly5d3nIXv2bNHO3fulCT179+/45Fx1dXV2r179xnreu2111RXV6fy8nKdffbZ+uY3v6kNGzZIilVg/NrXvtaxbP/+/TV37lxJUmVlpS677DKVlJSosrKyY93PPvusHn/8cU2ZMkW1tbVqbW3Vzp07tWHDBi1atEj9+vXTsGHD9OUvfzlj/x69jnJx932S9sXff2xm70gaLmlbp8XmS3o8/ttjk5kNNrML49/NLG7KAaIhB9e8OpfNXb9+vZ577jm9/PLLGjBggOrq6jrK3ZaUlMjMJJ1eHrcz76GuVWlpqfr1O1VorPP6zjrrrI7Su2eddVbHut1dDz74oObMmXPauhoaGjq+m2kp9aGb2RhJUyW90mXWcEmd78Fvjk8DUKyycM3r3HPP1ccff5xwXltbm8477zwNGDBA27dv16ZNm1Jad21trf70pz/p4MGDOnHihFatWqXLLrss7bbOmTNHDz/8cMdDMd5991198sknmjVrlp544gmdOHFC+/bt04svvpj2NrpKehy6mQ2U9JSkf3b3j7rOTvCVM37dmdkSxbpkNGrUqBSaCSByslCaY8iQIZo5c6YqKipUVlam888/v2Pe3LlztWLFCk2aNEnjxo1L+ASjnlx44YW67777NHv2bLm75s2bp/nz56fd1ptuukm7d+9WVVWV3F3l5eWqr6/XtddeqxdeeEGVlZX6/Oc/36dfGl0lVT7XzEok/V7SOnd/IMH8RyStd/dV8c87JNX11OWSkfK5AHIq1fK5PG6xbzJePtdinT3/LemdRGEet1bSrWb2hKRaSW1Z6T/P5H8c/IcGZB/XvHIqmS6XmZL+XtKbZrYlPu27kkZJkruvkNQgaZ5iwxaPSPpWxluaydv+KSEAIEDJjHJ5SYn7yDsv45JuyVSjEuppCFSqIZzJdQFFxt2zNkoDpyTTHd5VdO4UzeQQKEoIAGkpLS1Va2trWmGD5Lm7WltbVVpamtL3olNtMZNPJ4rik46AAjBixAg1NzfrwIED+W5K8EpLSzViRGqZFJ1Az+QQqKg+6QjIs5KSktPuzERhiU6XSyZv+6eEAIAAJTUOPRsYhw4AqetpHHp0ztABAD0i0AEgEAQ6AASCQAeAQBDoABAIAh0AAkGgA0AgCHQACASBnm9bn5R+XCF9f3Ds59Ynw94ugKyJTi2XEOWrLjv14IEgcYaeTz3VZQ9xuwCyikDPp3zVZacePBAkAj2fuqu/nu267PnaLoCsItDz6fJlsTrsneWiLnu+tgsgqwj0fMpXXXbqwQNBoh46AEQI9dABoAgQ6AAQCAIdAAJBoANAIAh0AAgEgQ4AgSDQASAQBDoABIJAB4BAEOgAEAgCHQACQaADQCAIdAAIRPEGOg9JBhCY4nxINA9JBhCg4jxD5yHJAAJUnIHOQ5IBBKjXQDezX5jZfjN7q5v5dWbWZmZb4q/CfzAlD0kGEKBkztAfkzS3l2U2uvuU+Kvw+y14SDKAAPUa6O6+QdJfctCW3OEhyQAClKlRLl8wszck7ZX0b+7+dqKFzGyJpCWSNGrUqAxtOk2TriPAAQQlExdFX5c02t0nS3pQUn13C7r7Snevcfea8vLyDGw6RxizDiAC+hzo7v6Rux+Ov2+QVGJmQ/vcskJxcsx62x5JfmrMOqEOoMD0OdDN7AIzs/j76fF1tvZ1vQWDMesAIqLXPnQzWyWpTtJQM2uWdLekEkly9xWSvi7pZjM7Lqld0vXu7llrca4xZh1ARPQa6O6+qJf5P5X004y1qNAMGhHvbkkwHQAKSHHeKZoKxqwDiAgCvTeMWQcQEcVZbTFVjFkHEAGcoQNAIAh0AAgEgQ4AgSDQASAQBDoABIJAB4BAEOgAEAgCHQACQaADQCAIdAAIBIEOAIEg0AEgEAQ6AASCQAeAQBDoABAIAh0AAkGgA0AgCHQACASBDgCBINABIBAEOgAEgkAHgEAQ6AAQCAIdAAJBoANAIAh0AAgEgQ4AgSDQASAQBDoABIJAB4BAEOgAEAgCHQACQaADQCAIdAAIBIEOAIHoNdDN7Bdmtt/M3upmvpnZcjN7z8y2mllV5psJAOhNMmfoj0ma28P8r0i6OP5aIunhvjcLAJCqXgPd3TdI+ksPi8yX9LjHbJI02MwuzFQDAQDJyUQf+nBJezp9bo5PO4OZLTGzRjNrPHDgQAY2DQA4KROBbgmmeaIF3X2lu9e4e015eXkGNg0AOCkTgd4saWSnzyMk7c3AegEAKchEoK+VdEN8tMsMSW3uvi8D6wUApODs3hYws1WS6iQNNbNmSXdLKpEkd18hqUHSPEnvSToi6VvZaiwAoHu9Brq7L+plvku6JWMtAgCkhTtFASAQBDoABIJAB4BAEOgAEAgCHQACQaADQCAIdAAIBIEOAIEg0AEgEAQ6AASCQAeAQBDoABAIAh0AAtFrtUUA6Iv6phbdv26H9h5q17DBZVo6Z5wWTE34lEr0EYEOIGvqm1p05+o31X7shCSp5VC77lz9piQR6llAlwuArLl/3Y6OMD+p/dgJ3b9uR55aFDbO0AEkLdXuk72H2lOajr7hDB1AUk52n7QcapfrVPdJfVNLt98ZNrgspenoGwIdQFLS6T5ZOmecykr6nTatrKSfls4Zl5U2Fju6XAAkJZ3uk5PdMYxyyQ0CHUBShg0uU0uC8O6t+2TB1OGRD/CoDL2kywVAUoq1+ySdawf5QqADSMqCqcN138JKDR9cJpM0fHCZ7ltYWZBnqpkUpaGXdLkASFoI3SepitLQS87QAaAHURp6SaADQA+idO2ALhcA6EGUhl4S6ADQi96uHRTKsEYCHQD6oJAqStKHDgB9UEjDGjlDB1C0MtFVUkjDGgl0AEUpU10lqZREyHZfO10uAIpSprpKkh3WmIsSAgQ6gKKUqa6SZEsi5KKvnS4XAEUp3eqRiSRTEiEXfe2coQMoSrm+AzQXJQQIdABFKdfVI3PxCySpLhczmyvpPyX1k/Rf7v7DLvPrJP1O0gfxSavd/Z6MtRIAsiCX1SNzUUKg10A3s36SfibpSknNkl4zs7Xuvq3Lohvd/aqMtQwAApPtXyDJdLlMl/Seu+9y908lPSFpftZaBABISzKBPlzSnk6fm+PTuvqCmb1hZn8ws4kZaR0AIGnJ9KFbgmne5fPrkka7+2EzmyepXtLFZ6zIbImkJZI0atSo1FoKAOhRMmfozZJGdvo8QtLezgu4+0fufjj+vkFSiZkN7boid1/p7jXuXlNeXt6HZgMAukom0F+TdLGZjTWz/pKul7S28wJmdoGZWfz99Ph6WzPdWABA93rtcnH342Z2q6R1ig1b/IW7v21m347PXyHp65JuNrPjktolXe/uXbtlAABZZPnK3ZqaGm9sbMzLtgEgqsxss7vXJJrHnaIAEAgCHQACQaADQCAIdAAIBIEOAIHgARcAci7bz9YsVgQ6gJzK1MOZcSa6XADkVC6erVmsCHQAOZWLZ2sWKwIdQE7l4tmaxYpAB5BTuX44czHhoiiAnMrFszWLFYEOIOdy+XDmYkKXCwAEgkAHgEAQ6AAQCAIdAAJBoANAIBjlAgDdSLWIWL6LjhHoAJBAqkXECqHoGF0uAJBAqkXECqHoGIEOAAmkWkSsEIqOEegAkECqRcQKoegYgQ4ACaRaRKwQio5xURQAEki1iFghFB0zd8/ZxjqrqanxxsbGvGwbAKLKzDa7e02ieXS5AEAgCHQACAR96ADSku+7InEmAh1AygrhrkiciS4XACkrhLsicSYCHUDKCuGuSJyJQAeQskK4KxJnItABpKwQ7orEmbgoCiBlhXBXJM5EoANIy4KpwwnwAkOXCwAEgkAHgEAQ6AAQCAIdAAJBoANAIPJWD93MDkj6czezh0o6mMPm5Br7F20h71/I+yaFsX+j3b080Yy8BXpPzKyxuwLuIWD/oi3k/Qt536Tw948uFwAIBIEOAIEo1EBfme8GZBn7F20h71/I+yYFvn8F2YcOAEhdoZ6hAwBSRKADQCAKItDN7H4z225mW81sjZkN7ma5uWa2w8zeM7M7ctzMtJnZ35nZ22b2f2bW7ZApM9ttZm+a2RYza8xlG/sihf2L6vH7rJn90cx2xn+e181ykTl+vR0Li1ken7/VzKry0c50JbF/dWbWFj9WW8xsWT7amXHunveXpL+VdHb8/b9L+vcEy/ST9L6kiyT1l/SGpEvy3fYk92+CpHGS1kuq6WG53ZKG5ru92di/iB+//5B0R/z9HYn++4zS8UvmWEiaJ+kPkkzSDEmv5LvdGd6/Okm/z3dbM/0qiDN0d3/W3Y/HP26SNCLBYtMlvefuu9z9U0lPSJqfqzb2hbu/4+7BPj03yf2L7PFTrJ2/jL//paQF+WtKRiRzLOZLetxjNkkabGYX5rqhaYryf2t9UhCB3sU/KHZm0NVwSXs6fW6OTwuJS3rWzDab2ZJ8NybDonz8znf3fZIU//m5bpaLyvFL5lhE+Xgl2/YvmNkbZvYHM5uYm6ZlV86eWGRmz0m6IMGsu9z9d/Fl7pJ0XNL/JFpFgmkFM+Yymf1Lwkx332tmn5P0RzPb7u4bMtfK9GVg/yJ7/FJYTcEevy6SORYFfbx6kUzbX1esJsphM5snqV7SxdluWLblLNDd/Yqe5pvZYklXSbrc451cXTRLGtnp8whJezPXwr7pbf+SXMfe+M/9ZrZGsT8dCyIQMrB/kT1+ZvahmV3o7vvi3Q77u1lHwR6/LpI5FgV9vHrRa9vd/aNO7xvM7CEzG+rukS7cVRBdLmY2V9Ltkq5x9yPdLPaapIvNbKyZ9Zd0vaS1uWpjtpnZOWZ27sn3il0ofiu/rcqoKB+/tZIWx98vlnTGXyQRO37JHIu1km6Ij3aZIantZLdTBPS6f2Z2gZlZ/P10xbKwNectzbR8X5WNn4y/p1if15b4a0V8+jBJDZ2WmyfpXcWuYN+V73ansH/XKnbW8L+SPpS0ruv+KXZF/o346+3Q9i/ix2+IpOcl7Yz//GzUj1+iYyHp25K+HX9vkn4Wn/+mehidVYivJPbv1vhxekOxgRiX5rvNmXhx6z8ABKIgulwAAH1HoANAIAh0AAgEgQ4AgSDQASAQBDoABIJAB4BA/D+caQa6uIP1VwAAAABJRU5ErkJggg==\n",
-      "text/plain": [
-       "<Figure size 432x288 with 1 Axes>"
+       "<IPython.core.display.Javascript object>"
       ]
      },
      "metadata": {
@@ -199,9 +196,7 @@
     }
    ],
    "source": [
-    "plt.scatter(p[6], p[5], label='original')\n",
-    "plt.scatter(q[6], q[5], label='tranformed')\n",
-    "plt.legend()"
+    "alg.graph(0xff0000, p, 'p', 0x0000ff, q, 'q', 0x000000, R.grade(2), 'axis')"
    ]
   },
   {
@@ -222,16 +217,16 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "(R) * 1 + (R12) * e12 + (R13) * e13 + (R23) * e23\n",
-      "(p12) * e12 + (p13) * e13 + (p23) * e23\n",
-      "(q12) * e12 + (q13) * e13 + (q23) * e23\n"
+      "Re + R01e01 + R02e02 + R12e12\n",
+      "p01e01 + p02e02 + p12e12\n",
+      "q01e01 + q02e02 + q12e12\n"
      ]
     }
    ],
    "source": [
     "R_par = alg.evenmv(name='R', symbolcls=Parameter)\n",
-    "p_var = alg.purevector(name='p', symbolcls=Variable, grade=d)\n",
-    "q_var = alg.purevector(name='q', symbolcls=Variable, grade=d)\n",
+    "p_var = alg.multivector(name='p', symbolcls=Variable, grades=(d,))\n",
+    "q_var = alg.multivector(name='q', symbolcls=Variable, grades=(d,))\n",
     "print(R_par)\n",
     "print(p_var)\n",
     "print(q_var)"
@@ -248,10 +243,10 @@
     {
      "data": {
       "text/latex": [
-       "\\begin{align}q_{12}(p_{12}; R, R_{12}) & = p_{12} \\left(R^{2} + R_{12}^{2}\\right)\\\\q_{13}(p_{12}, p_{13}, p_{23}; R, R_{12}, R_{13}, R_{23}) & = R^{2} p_{13} + 2 R R_{12} p_{23} - 2 R R_{23} p_{12} - R_{12}^{2} p_{13} + 2 R_{12} R_{13} p_{12}\\\\q_{23}(p_{12}, p_{13}, p_{23}; R, R_{12}, R_{13}, R_{23}) & = R^{2} p_{23} - 2 R R_{12} p_{13} + 2 R R_{13} p_{12} - R_{12}^{2} p_{23} + 2 R_{12} R_{23} p_{12}\\end{align}"
+       "\\begin{align}q_{01}(p_{01}, p_{02}, p_{12}; R, R_{01}, R_{02}, R_{12}) & = R^{2} p_{01} - 2 R R_{02} p_{12} + 2 R R_{12} p_{02} + 2 R_{01} R_{12} p_{12} - R_{12}^{2} p_{01}\\\\q_{02}(p_{01}, p_{02}, p_{12}; R, R_{01}, R_{02}, R_{12}) & = R^{2} p_{02} + 2 R R_{01} p_{12} - 2 R R_{12} p_{01} + 2 R_{02} R_{12} p_{12} - R_{12}^{2} p_{02}\\\\q_{12}(p_{12}; R, R_{12}) & = p_{12} \\left(R^{2} + R_{12}^{2}\\right)\\end{align}"
       ],
       "text/plain": [
-       "<symfit.core.models.Model at 0x235eecf96d0>"
+       "<symfit.core.models.Model at 0x7f9132b7b4c0>"
       ]
      },
      "execution_count": 10,
@@ -260,7 +255,7 @@
     }
    ],
    "source": [
-    "p_var_trans = R_par.conj(p_var)\n",
+    "p_var_trans = R_par.sw(p_var)\n",
     "model = Model({q_var[k]: expr for k, expr in p_var_trans.items()})\n",
     "model"
    ]
@@ -278,31 +273,10 @@
    "execution_count": 11,
    "id": "c8d31d48",
    "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "{'p12': array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]),\n",
-       " 'p13': array([0.58458649, 0.73592531, 0.11134136, 0.68417876, 0.57177713,\n",
-       "        0.35552796, 0.76742005, 0.21321343, 0.21633547, 0.61716839]),\n",
-       " 'p23': array([0.55369794, 0.08997382, 0.1113173 , 0.64265498, 0.80082692,\n",
-       "        0.24724482, 0.20296079, 0.73196863, 0.62790585, 0.71005653]),\n",
-       " 'q12': array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]),\n",
-       " 'q13': array([1.93770314, 1.46211814, 1.77184637, 1.94216034, 2.1288009 ,\n",
-       "        1.76473758, 1.54224206, 2.22762334, 2.1530351 , 2.05956487]),\n",
-       " 'q23': array([-1.76162814, -1.65760549, -1.18332194, -1.92659531, -1.88520711,\n",
-       "        -1.45905418, -1.75711176, -1.54669495, -1.4820069 , -1.91854545])}"
-      ]
-     },
-     "execution_count": 11,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
+   "outputs": [],
    "source": [
     "datadict = {p_var[k].name: p[k] for k in p_var.keys()}\n",
-    "datadict.update({q_var[k].name: q[k] for k in q_var.keys()})\n",
-    "datadict"
+    "datadict.update({q_var[k].name: q[k] for k in q_var.keys()})"
    ]
   },
   {
@@ -338,24 +312,24 @@
      "text": [
       "\n",
       "Parameter Value        Standard Deviation\n",
-      "R         5.039768e-01 1.392512e-02\n",
-      "R12       8.637172e-01 9.139166e-03\n",
-      "R13       4.932221e-01 2.474936e-02\n",
-      "R23       -8.682665e-01 1.074836e-02\n",
+      "R         4.993037e-01 1.517596e-02\n",
+      "R01       -4.321739e-01 1.649308e-02\n",
+      "R02       -2.509357e-01 1.204050e-02\n",
+      "R12       8.664270e-01 1.014764e-02\n",
       "Status message         Optimization terminated successfully\n",
-      "Number of iterations   22\n",
-      "Objective              <symfit.core.objectives.LeastSquares object at 0x00000235EE213070>\n",
-      "Minimizer              <symfit.core.minimizers.SLSQP object at 0x00000235EECFEAC0>\n",
+      "Number of iterations   14\n",
+      "Objective              <symfit.core.objectives.LeastSquares object at 0x7f9132b8fee0>\n",
+      "Minimizer              <symfit.core.minimizers.SLSQP object at 0x7f9132b8f6a0>\n",
       "\n",
       "Goodness of fit qualifiers:\n",
-      "chi_squared            0.0071557500481054595\n",
-      "objective_value        0.0035778750240527298\n",
-      "r_squared              0.9936522373747326\n",
+      "chi_squared            0.011043507892626378\n",
+      "objective_value        0.005521753946313189\n",
+      "r_squared              0.9927614045795606\n",
       "\n",
       "Constraints:\n",
       "--------------------\n",
       "Question: R**2 + R12**2 - 1 == 0?\n",
-      "Answer:   6.727698398378834e-10\n",
+      "Answer:   9.477574280936096e-11\n",
       "\n",
       "\n"
      ]
@@ -384,8 +358,8 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "(0.5039767984432324) * 1 + (0.8637171917379435) * e12 + (0.49322210183114706) * e13 + (-0.8682664685672835) * e23\n",
-      "(1.0000000006727698) * 1\n"
+      "0.499e + -0.432e01 + -0.251e02 + 0.866e12\n",
+      "1.0e\n"
      ]
     }
    ],
@@ -402,7 +376,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "p_reconstructed = (~R_re).conj(q)"
+    "p_reconstructed = (~R_re).sw(q)"
    ]
   },
   {
@@ -413,19 +387,29 @@
    "outputs": [
     {
      "data": {
+      "application/javascript": [
+       "\n",
+       "        fetch(\"https://enki.ws/ganja.js/ganja.js\")\n",
+       "        .then(x=>x.text())\n",
+       "        .then(ganja=>{\n",
+       "\n",
+       "          var f = new Function(\"module\",ganja);\n",
+       "          var module = {exports:{}};\n",
+       "          f(module);\n",
+       "          var Algebra = module.exports;\n",
+       "\n",
+       "          var canvas = Algebra(2, 0, 1,()=>{\n",
+       "              var data = [16711680, [0, 0, 0, 0, 0.5301250896410105, 0.7684896160983193, 1.0, 0], [0, 0, 0, 0, 0.10901968536025819, 0.6864885443010335, 1.0, 0], [0, 0, 0, 0, 0.5990561468325517, 0.4678739671067561, 1.0, 0], [0, 0, 0, 0, 0.3259134901847519, 0.019247750198401303, 1.0, 0], [0, 0, 0, 0, 0.009898241005036557, 0.9931416990708005, 1.0, 0], [0, 0, 0, 0, 0.25001472981051953, 0.5909620653257089, 1.0, 0], [0, 0, 0, 0, 0.45760594583049574, 0.029530187528896246, 1.0, 0], [0, 0, 0, 0, 0.9172181129391489, 0.4976263539876348, 1.0, 0], [0, 0, 0, 0, 0.024283185446231537, 0.6261713924581105, 1.0, 0], [0, 0, 0, 0, 0.2892776716500949, 0.3128676541656735, 1.0, 0], 255, [0, 0, 0, 0, -0.10674589255345503, -1.6735766965820282, 1.0, 0], [0, 0, 0, 0, -0.00729324443553498, -1.3182308420803064, 1.0, 0], [0, 0, 0, 0, -0.36317690233601563, -1.6143593827654934, 1.0, 0], [0, 0, 0, 0, -0.5987852021685599, -1.1726065717331904, 1.0, 0], [0, 0, 0, 0, 0.36703453110502965, -1.390524019835093, 1.0, 0], [0, 0, 0, 0, -0.08428416803890966, -1.3621902989398107, 1.0, 0], [0, 0, 0, 0, -0.7083140409743893, -1.276487953786359, 1.0, 0], [0, 0, 0, 0, -0.5518862670829539, -1.9183244757410058, 1.0, 0], [0, 0, 0, 0, 0.009467688080572377, -1.1810221555381157, 1.0, 0], [0, 0, 0, 0, -0.38067087145585315, -1.298899457535901, 1.0, 0], \"q\", 8913032, [0, 0, 0, 0, 0.5020537847004662, 0.7434955923788895, 1.0000000000947757, 0], [0, 0, 0, 0, 0.14473655369252014, 0.6513766367842371, 1.0000000000947757, 0], [0, 0, 0, 0, 0.5793901040884348, 0.49193516872206544, 1.0000000000947757, 0], [0, 0, 0, 0, 0.3153085466070046, 0.06659089868822776, 1.0000000000947757, 0], [0, 0, 0, 0, 0.019601292624804867, 1.0114998845149246, 1.0000000000947757, 0], [0, 0, 0, 0, 0.22137377755237236, 0.606803415168248, 1.0000000000947757, 0], [0, 0, 0, 0, 0.460105684197738, 0.0239095559577166, 1.0000000000947757, 0], [0, 0, 0, 0, 0.9370042152428144, 0.48106550367057455, 1.0000000000947757, 0], [0, 0, 0, 0, 0.017616997918553947, 0.5970832572164302, 1.0000000000947757, 0], [0, 0, 0, 0, 0.3152190462440523, 0.31863007474694005, 1.0000000000947757, 0], \"p reconstructed\", 0, [0, 0, 0, 0, -0.4321738715112142, -0.25093573185216, 0.8664270282827907, 0], \"reconst. axis\"].map(x=>x.length==8?new Element(x):x);\n",
+       "              return this.graph(data, {})\n",
+       "          })\n",
+       "\n",
+       "          element.append(canvas)\n",
+       "\n",
+       "        })\n",
+       "        "
+      ],
       "text/plain": [
-       "<matplotlib.legend.Legend at 0x235eee31700>"
-      ]
-     },
-     "execution_count": 16,
-     "metadata": {},
-     "output_type": "execute_result"
-    },
-    {
-     "data": {
-      "image/png": "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\n",
-      "text/plain": [
-       "<Figure size 432x288 with 1 Axes>"
+       "<IPython.core.display.Javascript object>"
       ]
      },
      "metadata": {
@@ -435,10 +419,7 @@
     }
    ],
    "source": [
-    "plt.scatter(p[6], p[5], label='original')\n",
-    "plt.scatter(q[6], q[5], label='tranformed')\n",
-    "plt.scatter(p_reconstructed[6], p_reconstructed[5], label='reconstructed')\n",
-    "plt.legend()"
+    "alg.graph(0xff0000, p, 0x0000ff, q, 'q', 0x880088, p_reconstructed, 'p reconstructed', 0x000000, R_re.grade(2), 'reconst. axis')"
    ]
   },
   {
@@ -482,7 +463,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.10"
+   "version": "3.8.13"
   }
  },
  "nbformat": 4,
diff --git a/docs/examples/ex_symbolic.ipynb b/docs/examples/ex_symbolic.ipynb
index 9b8cc41..16313f0 100644
--- a/docs/examples/ex_symbolic.ipynb
+++ b/docs/examples/ex_symbolic.ipynb
@@ -179,7 +179,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.9.12"
+   "version": "3.8.13"
   }
  },
  "nbformat": 4,
diff --git a/docs/examples/index.rst b/docs/examples/index.rst
index fd28eba..cae8e08 100644
--- a/docs/examples/index.rst
+++ b/docs/examples/index.rst
@@ -3,8 +3,22 @@ Examples
 ========
 
 
+2DPGA
+-----
+
+.. toctree::
+    :maxdepth: 1
+
+    ex_symbolic.ipynb
+    ex_pointcloud.ipynb
+    ex_2dpga_project_and_reject.ipynb
+
+3DPGA
+-----
+
 .. toctree::
     :maxdepth: 1
 
     ex_symbolic.ipynb
     ex_pointcloud.ipynb
+    ex_2dpga_project_and_reject.ipynb
diff --git a/kingdon/algebra.py b/kingdon/algebra.py
index a96e633..117342c 100644
--- a/kingdon/algebra.py
+++ b/kingdon/algebra.py
@@ -2,6 +2,7 @@
 from functools import partial
 from collections import Counter
 from dataclasses import dataclass, field, fields
+import json
 try:
     from functools import cached_property
 except ImportError:
@@ -11,16 +12,19 @@ def cached_property(func):
         return property(lru_cache()(func))
 
 import numpy as np
+from IPython.display import Javascript, display
 
 from kingdon.codegen import (
-    codegen_gp, codegen_conj, codegen_cp, codegen_ip, codegen_op, codegen_div,
+    codegen_gp, codegen_sw, codegen_cp, codegen_ip, codegen_op, codegen_div,
     codegen_rp, codegen_acp, codegen_proj, codegen_sp, codegen_lc, codegen_inv,
     codegen_rc, codegen_normsq,
     codegen_outerexp, codegen_outersin, codegen_outercos, codegen_outertan,
 )
 from kingdon.operator_dict import OperatorDict, UnaryOperatorDict
 from kingdon.matrixreps import matrix_rep
+from kingdon.multivector_json import MultiVectorEncoder
 from kingdon.multivector import MultiVector
+
 # from kingdon.module_builder import predefined_modules
 
 operation_field = partial(field, default_factory=dict, init=False, repr=False, compare=False)
@@ -46,11 +50,12 @@ class Algebra:
     q: int = 0
     r: int = 0
     d: int = field(init=False, repr=False, compare=False)  # Total number of dimensions
-    signature: list = field(init=False, repr=False, compare=False)
+    signature: np.ndarray = field(default=None, compare=False)
+    start_index: int = field(default=None, repr=False, compare=False)
 
     # Clever dictionaries that cache previously symbolically optimized lambda functions between elements.
     gp: OperatorDict = operation_field(metadata={'codegen': codegen_gp})  # geometric product
-    conj: OperatorDict = operation_field(metadata={'codegen': codegen_conj})  # conjugation
+    sw: OperatorDict = operation_field(metadata={'codegen': codegen_sw})  # conjugation
     cp: OperatorDict = operation_field(metadata={'codegen': codegen_cp})  # commutator product
     acp: OperatorDict = operation_field(metadata={'codegen': codegen_acp})  # anti-commutator product
     ip: OperatorDict = operation_field(metadata={'codegen': codegen_ip})  # inner product
@@ -71,6 +76,7 @@ class Algebra:
     # Mappings from binary to canonical reps. e.g. 0b01 = 1 <-> 'e1', 0b11 = 3 <-> 'e12'.
     canon2bin: dict = field(init=False, repr=False, compare=False)
     bin2canon: dict = field(init=False, repr=False, compare=False)
+    _bin2canon_prettystr: dict = field(init=False, repr=False, compare=False)
 
     # Options for the algebra
     cse: bool = field(default=True)  # Common Subexpression Elimination (CSE)
@@ -85,14 +91,36 @@ class Algebra:
     pss: object = field(init=False, repr=False, compare=False)
 
     def __post_init__(self):
+        if self.signature is not None:
+            counts = Counter(self.signature)
+            self.p, self.q, self.r = counts[1], counts[-1], counts[0]
+            if self.p + self.q + self.r != len(self.signature):
+                raise TypeError('Unsupported signature.')
+            self.signature = np.array(self.signature)
+        else:
+            if self.r == 1:  # PGA, so put r first.
+                self.signature = np.array([0] * self.r + [1] * self.p + [-1] * self.q)
+            else:
+                self.signature = np.array([1] * self.p + [-1] * self.q + [0] * self.r)
+
+        if self.start_index is None:
+            self.start_index = 0 if self.r == 1 else 1
+
         self.d = self.p + self.q + self.r
-        self.signature = np.array([1]*self.p + [-1]*self.q + [0]*self.r)
 
         # Setup mapping from binary to canonical string rep and vise versa
-        self.bin2canon = {eJ: 'e' + ''.join(str((eJ & 2**ei).bit_length()).replace('0', '') for ei in range(0, self.d))
-                          for eJ in range(2 ** self.d)}
-        self.bin2canon[0] = '1'
+        self.bin2canon = {
+            eJ: 'e' + ''.join(str(num + self.start_index - 1) for ei in range(0, self.d) if (num := (eJ & 2**ei).bit_length()))
+            for eJ in range(2 ** self.d)
+        }
         self.canon2bin = dict(sorted({c: b for b, c in self.bin2canon.items()}.items(), key=lambda x: (len(x[0]), x[0])))
+        def pretty_blade(blade):
+            if blade == 'e':
+                return '1'
+            for old, new in tuple(zip("e0123456789", "𝐞₀₁₂₃₄₅₆₇₈₉")):
+                blade = blade.replace(old, new)
+            return blade
+        self._bin2canon_prettystr = {k: pretty_blade(v) for k, v in self.bin2canon.items()}
 
         self.swaps, self.signs, self.cayley = self._prepare_signs_and_cayley()
 
@@ -112,7 +140,10 @@ def __len__(self):
     def indices_for_grade(self):
         """
         Mapping from the grades to the indices for that grade. E.g. in 2D VGA, this returns
-        {0: (0,), 1: (1, 2), 2: (3,)}
+
+        .. code-block ::
+
+            {0: (0,), 1: (1, 2), 2: (3,)}
         """
         key = lambda i: bin(i).count('1')
         sorted_inds = sorted(range(len(self)), key=key)
@@ -124,8 +155,10 @@ def indices_for_grades(self):
         Mapping from a sequence of grades to the corresponding indices.
         E.g. in 2D VGA, this returns
 
-        {(0,): (0,), (1,): (1, 2), (2,): (3,), (0, 1): (0, 1, 2),
-         (0, 2): (0, 3), (1, 2): (1, 2, 3), (0, 1, 2): (0, 1, 2, 3)}
+        .. code-block ::
+
+            {(0,): (0,), (1,): (1, 2), (2,): (3,), (0, 1): (0, 1, 2),
+             (0, 2): (0, 3), (1, 2): (1, 2, 3), (0, 1, 2): (0, 1, 2, 3)}
         """
         all_grade_combs = chain(*(combinations(range(0, self.d + 1), r=j) for j in range(1, len(self) + 1)))
         return {comb: sum((self.indices_for_grade[grade] for grade in comb), ())
@@ -169,7 +202,7 @@ def _prepare_signs_and_cayley(self):
             count = Counter(prod)
             for key, value in count.items():
                 if value // 2:
-                    sign *= self.signature[int(key) - 1]
+                    sign *= self.signature[int(key) - self.start_index]
                 count[key] = value % 2
             signs[self.canon2bin[eI], self.canon2bin[eJ]] = sign
 
@@ -177,7 +210,7 @@ def _prepare_signs_and_cayley(self):
             if sign:
                 prod = ''.join(key*value for key, value in count.items())
                 sign = '-' if sign == -1 else ''
-                cayley[eI, eJ] = f'{sign}{"e" if prod != "" else "1"}{prod}'
+                cayley[eI, eJ] = f'{sign}e{prod}'
             else:
                 cayley[eI, eJ] = f'0'
         return swaps_arr, signs, cayley
@@ -237,3 +270,72 @@ def pseudotrivector(self, *args, **kwargs) -> MultiVector:
 
     def pseudoquadvector(self, *args, **kwargs) -> MultiVector:
         return self.purevector(*args, grade=self.d - 4, **kwargs)
+
+    def graph(self, *subjects, **options):
+        """
+        The graph function outputs :code:`ganja.js` renders and is meant
+        for use in jupyter notebooks. The syntax of the graph function will feel
+        familiar to users of :code:`ganja.js`: all position arguments are considered
+        as subjects to graph, while all keyword arguments are interpreted as options
+        to :code:`ganja.js`'s :code:`Algebra.graph` method.
+
+        Example usage:
+
+        .. code-block ::
+
+            alg.graph(
+                0xD0FFE1, [A,B,C],
+                0x224488, A, "A", B, "B", C, "C",
+                lineWidth=3, grid=1, labels=1
+            )
+
+        Will create
+
+        .. image :: ../docs/_static/graph_triangle.png
+            :scale: 50%
+            :align: center
+
+        Not all features of :code:`ganja.js` are supported yet. Most notably,
+        only static graphs can be made. While ganja also accepts functions as
+        input, this syntax is not currently supported in Kingdon.
+
+        :param `*subjects`: Subjects to be graphed.
+            Can be strings, hexadecimal colors, (lists of) MultiVector.
+        :param `**options`: Options passed to :code:`ganja.js`'s :code:`Algebra.graph`.
+        """
+        # Flatten multidimensional multivectors
+        flat_subjects = []
+        for subject in subjects:
+            if isinstance(subject, MultiVector) and len(subject.shape()) > 1:
+                flat_subjects.extend(subject.itermv())
+            else:
+                flat_subjects.append(subject)
+
+        json_subjects = json.dumps(flat_subjects, cls=MultiVectorEncoder)
+
+        cayley_table = [[s if (s := self.cayley[eJ, eI])[-1] != 'e' else f"{s[:-1]}1"
+                         for eI in self.canon2bin]
+                        for eJ in self.canon2bin]
+        cayley_table = json.dumps(cayley_table)
+        metric = json.dumps(list(self.signature), cls=MultiVectorEncoder)
+
+        src = f"""
+        fetch("https://enki.ws/ganja.js/ganja.js")
+        .then(x=>x.text())
+        .then(ganja=>{{
+
+          var f = new Function("module",ganja);
+          var module = {{exports:{{}}}};
+          f(module);
+          var Algebra = module.exports;
+
+          var canvas = Algebra({{metric:{metric}, Cayley:{cayley_table}}},()=>{{
+              var data = {json_subjects}.map(x=>x.length=={len(self)}?new Element(x):x);
+              return this.graph(data, {options})
+          }})
+
+          element.append(canvas)
+
+        }})
+        """
+        display(Javascript(src))
diff --git a/kingdon/codegen.py b/kingdon/codegen.py
index 1dcb47a..98a45d5 100644
--- a/kingdon/codegen.py
+++ b/kingdon/codegen.py
@@ -117,13 +117,13 @@ def codegen_gp(x, y, symbolic=False):
     return CodegenOutput(keys_out, func)
 
 
-def codegen_conj(x, y):
+def codegen_sw(x, y):
     if x.algebra.simplify:
         res = codegen_product(x, y, ~x, name_base='gp', asdict=True, sympy=True)
         res = {k: str(simp_expr) for k, expr in res.items() if (simp_expr := simplify(expr))}
     else:
         res = codegen_product(x, y, ~x, name_base='gp', asdict=True)
-    return _func_builder(res, x, y, name_base="conj")
+    return _func_builder(res, x, y, name_base="sw")
 
 
 def codegen_cp(x, y, symbolic=False):
diff --git a/kingdon/multivector.py b/kingdon/multivector.py
index be5b50f..a6c78d4 100644
--- a/kingdon/multivector.py
+++ b/kingdon/multivector.py
@@ -2,6 +2,8 @@
 from collections.abc import Mapping
 from dataclasses import dataclass, field
 from functools import reduce, cached_property
+from typing import Generator
+from itertools import product
 
 from sympy import Symbol, Expr, sympify
 
@@ -98,6 +100,32 @@ def items(self):
     def __len__(self):
         return len(self._values)
 
+    def itermv(self, axis=None) -> Generator["MultiVector", None, None]:
+        """
+        Returns an iterator over the multivectors within this multivector.
+
+        :param axis: Axis over which to iterate. Default is to iterate over all possible mv.
+        """
+        shape = self.shape()[1:]
+        if not shape:
+            return self
+        elif axis is None:
+            return (
+                MultiVector.fromkeysvalues(self.algebra, keys=self.keys(), values=self[(slice(None), *indices)])
+                for indices in product(*(range(n) for n in shape))
+            )
+        else:
+            raise NotImplementedError
+
+    def shape(self):
+        """ Return the shape of the .values() attribute of this multivector. """
+        if hasattr(self._values, 'shape'):
+            return self._values.shape
+        elif hasattr(self._values[0], 'shape'):
+            return len(self), *self._values[0].shape
+        else:
+            return len(self),
+
     @cached_property
     def grades(self):
         """ Tuple of the grades present in `self`. """
@@ -138,14 +166,17 @@ def __invert__(self):  # reversion
     def normsq(self):
         return self.algebra.normsq(self)
 
-    def normalized(self):
-        """ Normalized version of this multivector. """
+    def norm(self):
         normsq = self.normsq()
         if normsq.grades == (0,):
-            return self / normsq[0] ** 0.5
+            return normsq[0] ** 0.5
         else:
             raise NotImplementedError
 
+    def normalized(self):
+        """ Normalized version of this multivector. """
+        return self / self.norm()
+
     def inv(self):
         """ Inverse of this multivector. """
         return self.algebra.inv(self)
@@ -173,12 +204,34 @@ def __truediv__(self, other):
         return self.algebra.div(self, other)
 
     def __str__(self):
-        if len(self.values()):
-            canon_sorted_vals = sorted(self.items(), key=lambda x: (len(self.algebra.bin2canon[x[0]]), self.algebra.bin2canon[x[0]]))
-            return ' + '.join([f'({val}) * {self.algebra.bin2canon[key]}' for key, val in canon_sorted_vals])
-        else:
+        if not len(self.values()):
             return '0'
 
+        def print_value(val):
+            if isinstance(val, Expr):
+                if val.is_Symbol:
+                    return f"{val}"
+                else:
+                    return f"({val})"
+            elif isinstance(val, float):
+                return f'{val:.3}'
+            else:
+                return f'{val}'
+
+        canon_vals = {self.algebra._bin2canon_prettystr[key]: val for key, val in self.items()}
+        canon_sorted_vals = sorted(canon_vals.items(), key=lambda x: (len(x[0]), x[0]))
+        str_repr = ' + '.join(
+            [f'{print_value(val)} {blade}' if blade != '1' else f'{print_value(val)}'
+             for blade, val in canon_sorted_vals if (val.any() if hasattr(val, 'any') else val)]
+        )
+        return str_repr
+
+    def _repr_pretty_(self, p, cycle):
+        if cycle:
+            p.text(f'{self.__class__.__name__}(...)')
+        else:
+            p.text(str(self))
+
     def __format__(self, format_spec):
         if format_spec == 'keys_binary':
             iden = '_'.join(''.join('1' if i in self.keys() else '0' for i in bin_blades)
@@ -187,26 +240,34 @@ def __format__(self, format_spec):
 
     def __getitem__(self, item):
         if isinstance(item, tuple):
-            key, slicing = item
+            key, *subslices = item
         else:
-            key, slicing = item, None
+            key, subslices = item, tuple()
 
-        if key == slice(None, None, None):
-            values = self.values()
-        else:
+        # TODO: We could turn slices into the valid range in binary rep.
+        #  This is complicated by the fact that the binary keys do not
+        #  form a consecutive range.
+        if not isinstance(key, slice):
             # Convert key from a basis-blade in binary rep to a valid index in values.
             key = key if key in self.algebra.bin2canon else self.algebra.canon2bin[key]
             try:
                 key = self.keys().index(key)
             except ValueError:
                 return 0
-            else:
-                values = self.values()
 
-        if slicing is None:
-            return values[key]
+        values = self.values()
+        if isinstance(values, (tuple, list)):
+            keys = [key] if key != slice(None) else [self.keys().index(k) for k in self.keys()]
+            return_values = []
+            for key in keys:
+                return_values.append(values[key])
+                for subslice in subslices:
+                    return_values[key] = return_values[key][subslice]
+            if len(keys) == 1:
+                return_values = return_values[0]
         else:
-            return values[key][slicing] if isinstance(values, (tuple, list)) else values[key, slicing]
+            return_values = values[(key, *subslices)]
+        return return_values
 
     def __contains__(self, item):
         item = item if item in self.algebra.bin2canon else self.algebra.canon2bin[item]
@@ -232,14 +293,33 @@ def asmatrix(self):
         """ Returns a matrix representation of this multivector. """
         return sum(v * self.algebra.matrix_basis[k] for k, v in self.items())
 
+    def asdensemv(self, canonical=True):
+        """
+        Returns a dense version of the same multivector.
+
+        :param canonical: If True (default) the values are in canonical order,
+          even if the mutivector was already dense.
+        """
+        if canonical:
+            keys = self.algebra.indices_for_grades[tuple(range(self.algebra.d + 1))]
+        else:
+            keys = tuple(range(len(self.algebra)))
+        values = tuple(self[k] for k in keys)
+        return self.fromkeysvalues(self.algebra, keys=keys, values=values)
+
     def gp(self, other):
         return self.algebra.gp(self, other)
 
     __mul__ = __rmul__ = gp
 
-    def conj(self, other):
-        """ Apply `x := self` to `y := other` under conjugation: `x*y*~x`. """
-        return self.algebra.conj(self, other)
+    def sw(self, other):
+        """
+        Apply :code:`x := self` to :code:`y := other` under conjugation:
+        :code:`x.sw(y) = x*y*~x`.
+        """
+        return self.algebra.sw(self, other)
+
+    __rshift__ = sw
 
     def proj(self, other):
         """
@@ -277,13 +357,9 @@ def op(self, other):
     def lc(self, other):
         return self.algebra.lc(self, other)
 
-    __lshift__ = lc
-
     def rc(self, other):
         return self.algebra.rc(self, other)
 
-    __rshift__ = rc
-
     def sp(self, other):
         """ Scalar product: :math:`\langle x \cdot y \rangle`. """
         return self.algebra.sp(self, other)
diff --git a/kingdon/multivector_json.py b/kingdon/multivector_json.py
new file mode 100644
index 0000000..754975b
--- /dev/null
+++ b/kingdon/multivector_json.py
@@ -0,0 +1,17 @@
+import json
+
+import numpy as np
+
+
+class MultiVectorEncoder(json.JSONEncoder):
+    def default(self, item):
+        from kingdon import MultiVector
+        if isinstance(item, MultiVector):
+            item = item.asdensemv()
+            return list(item.values())
+
+        if isinstance(item, np.integer):
+            return int(item)
+
+        # Let the base class default method raise the TypeError
+        return super().default(item)
diff --git a/requirements.txt b/requirements.txt
index 0c29ef9..83d9a27 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
 numpy
 sympy
 numba
+IPython
diff --git a/requirements_docs.txt b/requirements_docs.txt
index 2f516b8..3e3d928 100644
--- a/requirements_docs.txt
+++ b/requirements_docs.txt
@@ -1,7 +1,6 @@
 -r requirements.txt
 sphinx
 nbsphinx
-IPython
 ipykernel
 symfit
 matplotlib


Test patch:
diff --git a/tests/performance_check.py b/tests/performance_check.py
index 9563331..a87edb2 100644
--- a/tests/performance_check.py
+++ b/tests/performance_check.py
@@ -18,7 +18,7 @@
     shape_v = (2**d, num_rows) if num_rows != 1 else 2**d
 
     print("Kingdon", end='\n\n')
-    operations = ['b*v', 'b.cp(v)', 'b.conj(v)', 'b.proj(v)', 'b^v', 'b|v', 'b+v', 'b-v', 'b.inv()', 'b / v', '~b']
+    operations = ['b*v', 'b.cp(v)', 'b.sw(v)', 'b.proj(v)', 'b^v', 'b|v', 'b+v', 'b-v', 'b.inv()', 'b / v', '~b']
     times = defaultdict(list)
     for operation in operations:
         print(operation)
@@ -63,7 +63,7 @@
     # b = alg.multivector(bvals)
     # v = alg.multivector(vvals)
     # # print(b.cp(v))
-    # operation = 'b.conj(v)'
+    # operation = 'b.sw(v)'
     # init = timeit.timeit(operation, number=1, globals=globals())
     # print('init', init)
     # prof = cProfile.run(f'for _ in range({num_iter}): {operation}', 'restats')
diff --git a/tests/test_kingdon.py b/tests/test_kingdon.py
index b446921..37b7a79 100644
--- a/tests/test_kingdon.py
+++ b/tests/test_kingdon.py
@@ -8,22 +8,23 @@
 
 from sympy import Symbol, simplify, factor, expand, collect, sympify
 from kingdon import Algebra, MultiVector, symbols
+from kingdon.multivector_json import MultiVectorEncoder
 
 import timeit
 
 @pytest.fixture
 def pga1d():
-    return Algebra(1, 0, 1)
+    return Algebra(signature=np.array([1, 0]), start_index=1)
 
 
 @pytest.fixture
 def pga2d():
-    return Algebra(2, 0, 1)
+    return Algebra(signature=np.array([1, 1, 0]), start_index=1)
 
 
 @pytest.fixture
 def pga3d():
-    return Algebra(3, 0, 1)
+    return Algebra(signature=np.array([1, 1, 1, 0]), start_index=1)
 
 
 @pytest.fixture
@@ -72,24 +73,24 @@ def test_anticommutation(pga1d, vga11, vga2d):
 
 def test_gp(pga1d):
     # Multiply two multivectors
-    X = MultiVector(values={'1': 2, 'e12': 3}, algebra=pga1d)
-    Y = MultiVector(values={'1': 7, 'e12': 5}, algebra=pga1d)
+    X = MultiVector(values={'e': 2, 'e12': 3}, algebra=pga1d)
+    Y = MultiVector(values={'e': 7, 'e12': 5}, algebra=pga1d)
     Z = X.gp(Y)
     assert dict(Z.items()) == {0: 2*7, 3: 2*5 + 3*7}
 
 def test_cayley(pga1d, vga2d, vga11):
-    assert pga1d.cayley == {('1', '1'): '1', ('1', 'e1'): 'e1', ('1', 'e12'): 'e12', ('1', 'e2'): 'e2',
-                          ('e1', '1'): 'e1', ('e1', 'e1'): '1', ('e1', 'e12'): 'e2', ('e1', 'e2'): 'e12',
-                          ('e12', '1'): 'e12', ('e12', 'e1'): '-e2', ('e12', 'e12'): '0', ('e12', 'e2'): '0',
-                          ('e2', '1'): 'e2', ('e2', 'e1'): '-e12', ('e2', 'e12'): '0', ('e2', 'e2'): '0'}
-    assert vga2d.cayley == {('1', '1'): '1', ('1', 'e1'): 'e1', ('1', 'e2'): 'e2', ('1', 'e12'): 'e12',
-                          ('e1', '1'): 'e1', ('e1', 'e1'): '1', ('e1', 'e2'): 'e12', ('e1', 'e12'): 'e2',
-                          ('e2', '1'): 'e2', ('e2', 'e1'): '-e12', ('e2', 'e2'): '1', ('e2', 'e12'): '-e1',
-                          ('e12', '1'): 'e12', ('e12', 'e1'): '-e2', ('e12', 'e2'): 'e1', ('e12', 'e12'): '-1'}
-    assert vga11.cayley == {('1', '1'): '1', ('1', 'e1'): 'e1', ('1', 'e2'): 'e2', ('1', 'e12'): 'e12',
-                          ('e1', '1'): 'e1', ('e1', 'e1'): '1', ('e1', 'e2'): 'e12', ('e1', 'e12'): 'e2',
-                          ('e2', '1'): 'e2', ('e2', 'e1'): '-e12', ('e2', 'e2'): '-1', ('e2', 'e12'): 'e1',
-                          ('e12', '1'): 'e12', ('e12', 'e1'): '-e2', ('e12', 'e2'): '-e1', ('e12', 'e12'): '1'}
+    assert pga1d.cayley == {('e', 'e'): 'e', ('e', 'e1'): 'e1', ('e', 'e12'): 'e12', ('e', 'e2'): 'e2',
+                          ('e1', 'e'): 'e1', ('e1', 'e1'): 'e', ('e1', 'e12'): 'e2', ('e1', 'e2'): 'e12',
+                          ('e12', 'e'): 'e12', ('e12', 'e1'): '-e2', ('e12', 'e12'): '0', ('e12', 'e2'): '0',
+                          ('e2', 'e'): 'e2', ('e2', 'e1'): '-e12', ('e2', 'e12'): '0', ('e2', 'e2'): '0'}
+    assert vga2d.cayley == {('e', 'e'): 'e', ('e', 'e1'): 'e1', ('e', 'e2'): 'e2', ('e', 'e12'): 'e12',
+                          ('e1', 'e'): 'e1', ('e1', 'e1'): 'e', ('e1', 'e2'): 'e12', ('e1', 'e12'): 'e2',
+                          ('e2', 'e'): 'e2', ('e2', 'e1'): '-e12', ('e2', 'e2'): 'e', ('e2', 'e12'): '-e1',
+                          ('e12', 'e'): 'e12', ('e12', 'e1'): '-e2', ('e12', 'e2'): 'e1', ('e12', 'e12'): '-e'}
+    assert vga11.cayley == {('e', 'e'): 'e', ('e', 'e1'): 'e1', ('e', 'e2'): 'e2', ('e', 'e12'): 'e12',
+                          ('e1', 'e'): 'e1', ('e1', 'e1'): 'e', ('e1', 'e2'): 'e12', ('e1', 'e12'): 'e2',
+                          ('e2', 'e'): 'e2', ('e2', 'e1'): '-e12', ('e2', 'e2'): '-e', ('e2', 'e12'): 'e1',
+                          ('e12', 'e'): 'e12', ('e12', 'e1'): '-e2', ('e12', 'e2'): '-e1', ('e12', 'e12'): 'e'}
 
 def test_purevector(pga1d):
     with pytest.raises(TypeError):
@@ -124,9 +125,9 @@ def test_broadcasting(vga2d):
 
     # Test broadcasting a rotor across a tensor-valued element
     R = vga2d.multivector({0: np.cos(np.pi / 3), 3: np.sin(np.pi / 3)})
-    Z3 = R.conj(X)
+    Z3 = R.sw(X)
     for i, xrow in enumerate(valsX.T):
-        Rx = R.conj(vga2d.vector(xrow))
+        Rx = R.sw(vga2d.vector(xrow))
         assert Rx[1] == Z3[1][i]
 
 def test_reverse(R6):
@@ -138,7 +139,7 @@ def test_reverse(R6):
 def test_indexing(pga1d):
     # Test indexing of a mv with canonical and binary indices.
     X = pga1d.multivector({0: 2, 'e12': 3})
-    assert X['1'] == 2 and X[3] == 3
+    assert X['e'] == 2 and X[3] == 3
 
 def test_gp_symbolic(vga2d):
     u = vga2d.vector(name='u')
@@ -168,11 +169,11 @@ def test_gp_symbolic(vga2d):
     assert 0 in Rnormsq
     assert Rnormsq['e12'] == 0
 
-def test_conj_symbolic(vga2d):
+def test_sw_symbolic(vga2d):
     u = vga2d.vector(name='u')
     v = vga2d.vector(name='v')
     # Pure vector
-    assert u.conj(v).grades == (1,)
+    assert u.sw(v).grades == (1,)
 
 def test_cp_symbolic(R6):
     b = R6.bivector(name='B')
@@ -184,7 +185,7 @@ def test_cp_symbolic(R6):
     assert w.grades == (1,)
 
 def test_blades(vga2d):
-    assert vga2d.blades['1'] == vga2d.multivector({'1': 1})
+    assert vga2d.blades['e'] == vga2d.multivector({'e': 1})
     assert vga2d.blades['e1'] == vga2d.multivector({'e1': 1})
     assert vga2d.blades['e2'] == vga2d.multivector({'e2': 1})
     assert vga2d.blades['e12'] == vga2d.multivector({'e12': 1})
@@ -282,7 +283,7 @@ def test_regressive(pga3d):
 
     # Known output from GAmphetamine.js
     known_vals = {
-        "1": (x1*y16-x10*y7+x11*y6+x12*y5-x13*y4+x14*y3-x15*y2+x16*y1+x2*y15-x3*y14+x4*y13-x5*y12+x6*y11-x7*y10+x8*y9+x9*y8),
+        "e": (x1*y16-x10*y7+x11*y6+x12*y5-x13*y4+x14*y3-x15*y2+x16*y1+x2*y15-x3*y14+x4*y13-x5*y12+x6*y11-x7*y10+x8*y9+x9*y8),
         "e1": (x12*y8-x13*y7+x14*y6+x16*y2+x2*y16+x6*y14-x7*y13+x8*y12),
         "e2": (x10*y12+x12*y10-x13*y9+x15*y6+x16*y3+x3*y16+x6*y15-x9*y13),
         "e3": (x11*y12+x12*y11-x14*y9+x15*y7+x16*y4+x4*y16+x7*y15-x9*y14),
@@ -409,7 +410,7 @@ def test_conjugation():
     y = alg.multivector(name='y')
 
     xconjy_expected = x*y*(~x)
-    xconjy = x.conj(y)
+    xconjy = x.sw(y)
     for i in range(len(alg)):
         assert expand(xconjy[i]) == expand(xconjy_expected[i])
 
@@ -418,10 +419,10 @@ def test_projection():
     x = alg.multivector(name='x')  # multivector
     y = alg.multivector(name='y')
 
-    xconjy_expected = (x | y) * ~y
-    xconjy = x.proj(y)
+    xprojy_expected = (x | y) * ~y
+    xprojy = x.proj(y)
     for i in range(len(alg)):
-        assert expand(xconjy[i]) == expand(xconjy_expected[i])
+        assert expand(xprojy[i]) == expand(xprojy_expected[i])
 
 
 def test_outerexp(R6):
@@ -456,20 +457,24 @@ def test_outertrig(R6):
 def test_indexing():
     alg = Algebra(4)
     nrows = 3
-    bvals = np.random.random((len(alg.indices_for_grade[2]), nrows))
+    ncolumns = 4
+    shape = (len(alg.indices_for_grade[2]), nrows, ncolumns)
+    bvals = np.random.random(shape)
     B = alg.bivector(bvals)
     np.testing.assert_allclose(B[3, 2:4], bvals[0, 2:4])
     np.testing.assert_allclose(B[3, 2], bvals[0, 2])
     np.testing.assert_allclose(B[3, :], bvals[0, :])
     np.testing.assert_allclose(B[:, 0], bvals[:, 0])
+    np.testing.assert_allclose(B[3, 2:4, 0], bvals[0, 2:4, 0])
+    # Same tests but without using a numpy array, instead use a tuple of sub np.ndarray.
+    B = alg.bivector(tuple(bvals))
+    np.testing.assert_allclose(B[3, 2:4], bvals[0, 2:4])
+    np.testing.assert_allclose(B[3, 2], bvals[0, 2])
+    np.testing.assert_allclose(B[3, :], bvals[0, :])
+    np.testing.assert_allclose(B[:, 0], bvals[:, 0])
+    # np.testing.assert_allclose(B[3, 2:4, 0], bvals[0, 2:4, 0]) # Still fails, fix in the future
+    np.testing.assert_allclose(B[:, 0, 0], bvals[:, 0, 0])
 
-    #TODO: same tests but without using a numpy array
-    bvals = tuple(np.random.random(nrows) for _ in range(len(alg.indices_for_grade[2])))
-    B = alg.bivector(bvals)
-    np.testing.assert_allclose(B[3, 2:4], bvals[0][2:4])
-    np.testing.assert_allclose(B[3, 2], bvals[0][2])
-    np.testing.assert_allclose(B[3, :], bvals[0][:])
-    np.testing.assert_allclose(B[:, 0], bvals[:][0])
 
 def test_normalization(pga3d):
     vvals = np.random.random(len(pga3d.indices_for_grade[1]))
@@ -480,3 +485,60 @@ def test_normalization(pga3d):
     bvals = np.random.random(len(pga3d.indices_for_grade[2]))
     with pytest.raises(NotImplementedError):
         B = pga3d.bivector(bvals).normalized()
+
+
+def test_itermv():
+    alg = Algebra(4)
+    nrows = 3
+    # ncolumns = 5
+    shape = (len(alg.indices_for_grade[2]), nrows)#, ncolumns)
+    bvals = np.random.random(shape)
+    B = alg.bivector(bvals)
+    for i, b in enumerate(B.itermv()):
+        np.testing.assert_allclose(b.values(), bvals[:, i])
+    assert i + 1 == nrows
+
+
+def test_fromsignature():
+    alg = Algebra(signature=[0, -1, 1, 1])
+    assert alg.start_index == 0
+    assert isinstance(alg.signature, np.ndarray)
+    assert np.all(alg.signature == [0, -1, 1, 1])
+    assert (alg.p, alg.q, alg.r) == (2, 1, 1)
+
+
+def test_start_index():
+    pga2d = Algebra(signature=[0, 1, 1], start_index=0)
+    alg = Algebra(signature=[0, 1, 1], start_index=1)
+    for ei, fi in zip(pga2d.blades.values(), alg.blades.values()):
+        assert fi**2 == ei**2
+
+
+def test_asdensemv():
+    alg = Algebra(2, 0, 1)
+    xvals = np.random.random(3)
+    x = alg.vector(xvals)
+    # Manually make the expected dense x
+    x_densevals = np.zeros(len(alg))
+    x_densevals[np.array([1, 2, 4])] = xvals
+    x_dense = alg.multivector(x_densevals)
+    # Compare to asdensemv method.
+    y = x.asdensemv(canonical=False)
+    assert y.keys() == x_dense.keys()
+    np.testing.assert_equal(y.values(), x_dense.values())
+
+    # Manually make the expected dense x in canonical ordering
+    x_densevals = np.zeros(len(alg))
+    x_densevals[np.array([1, 2, 3])] = xvals
+    # Compare to asdensemv method.
+    y = x.asdensemv()
+    np.testing.assert_equal(y.values(), x_densevals)
+
+
+def test_json():
+    import json
+    alg = Algebra(signature=[0, 1, 1], start_index=0)
+    xvals = np.array([2, 3, 4])
+    x = alg.vector(xvals)
+    xjson = json.dumps(x, cls=MultiVectorEncoder)
+    assert xjson == "[0, 2, 3, 4, 0, 0, 0, 0]"