From 3ce2ef1d7c4334f61e1eb644a3c47153f719f303 Mon Sep 17 00:00:00 2001 From: Eric Wieser Date: Thu, 2 Apr 2020 16:23:04 +0100 Subject: [PATCH] Add a tutorial notebook showing how to hack together custom algebras (#298) --- docs/index.rst | 1 + docs/requirements.txt | 1 + docs/tutorials/apollonius-cga-augmented.ipynb | 393 ++++++++++++++++++ 3 files changed, 395 insertions(+) create mode 100644 docs/tutorials/apollonius-cga-augmented.ipynb diff --git a/docs/index.rst b/docs/index.rst index 2086d5f7..49d38dc6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -51,6 +51,7 @@ Scalars, vectors, and higher-grade entities can be mixed freely and consistently tutorials/InterfacingOtherMathSystems tutorials/PerformanceCliffordTutorial tutorials/cga/index + tutorials/apollonius-cga-augmented .. toctree:: :maxdepth: 1 diff --git a/docs/requirements.txt b/docs/requirements.txt index a6052a50..946d1caf 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,6 @@ # extra requiremenets needed by the docs themselves # numpydoc==0.4 +pandas ipykernel nbsphinx ipywidgets diff --git a/docs/tutorials/apollonius-cga-augmented.ipynb b/docs/tutorials/apollonius-cga-augmented.ipynb new file mode 100644 index 00000000..d14093e9 --- /dev/null +++ b/docs/tutorials/apollonius-cga-augmented.ipynb @@ -0,0 +1,393 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Working with custom algebras\n", + "\n", + "This notebook explores the algebra defined in [The Lie Model for Euclidean Geometry (Hongbo Li)](https://link.springer.com/chapter/10.1007/10722492_7), and its application to solving [Apollonius' Problem](https://mathworld.wolfram.com/ApolloniusProblem.html). It also shows \n", + "\n", + "The algebra is constructed with basis elements $e_{-2}, e_{-1}, e_1, \\cdots, e_n, e_{n+1}$, where $e_{-2}^2 = -e_{-1}^2 = -e_{n+1}^2 = 1$.\n", + "This is an extension of a standard conformal algebra, with an extra $e_{n+1}$ basis vector.\n", + "\n", + "Note that we permuted the order in the source code below to make `ConformalLayout` happy." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from clifford import ConformalLayout, BasisVectorIds, MultiVector\n", + "\n", + "class OurCustomLayout(ConformalLayout):\n", + " def __init__(self, ndims):\n", + " self.ndims = ndims\n", + "\n", + " # Construct our custom algebra. Note that ConformalLayout requires the e- and e+ basis vectors to be last.\n", + " ConformalLayout.__init__(\n", + " self,\n", + " [1]*ndims + [-1] + [1, -1],\n", + " ids=BasisVectorIds([str(i + 1) for i in range(ndims)] + ['np1', 'm2', 'm1'])\n", + " )\n", + " self.enp1 = self.basis_vectors_lst[ndims]\n", + "\n", + " # Construct a base algebra without the extra `enp1`, which would not be understood by pyganja.\n", + " self.conformal_base = ConformalLayout(\n", + " [1]*ndims + [1, -1],\n", + " ids=BasisVectorIds([str(i + 1) for i in range(ndims)] + ['m2', 'm1'])\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We define an `ups` function which maps conformal dual-spheres into this algebra, as $s^\\prime = s + \\left|s\\right|e_{n+1}$, and a `downs` that applies the correct sign. The `s` suffix here is chosen to mean `sphere`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def ups(self, s):\n", + " return s + self.enp1*abs(s)\n", + "\n", + "OurCustomLayout.ups = ups; del ups\n", + "\n", + "def downs(self, mv):\n", + " if (mv | self.enp1)[()] > 0:\n", + " mv = -mv\n", + " return mv\n", + "\n", + "OurCustomLayout.downs = downs; del downs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before we start looking at specified dimensions of euclidean space, we build a helper to construct conformal dual circles and spheres, with the word `round` being a general term intended to cover both circles and spheres." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def dual_round(at, r):\n", + " l = at.layout\n", + " return l.up(at) - 0.5*l.einf*r*r" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualization of custom algebras\n", + "\n", + "In order to render with `pyganja`, we'll need a helper to convert from our custom $\\mathbb{R}^{N+1,2}$ layout into a standard conformal $\\mathbb{R}^{N+1,1}$ layout. `clifford` maps indices in `.value` to basis blades via `layout._basis_blade_order.index_to_bitmap`, which we can use to convert the indices in one layout to the indices in another.\n", + "\n", + "
\n", + "\n", + "Warning\n", + "\n", + "This function uses the private attribute `Layout._basis_blade_order`, which may change or be removed in future.\n", + "Future releases of `clifford` may contain tools to make switching between algebras easier.\n", + "\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def to_conformal(self, mv: MultiVector) -> MultiVector:\n", + " \"\"\" Convert a mv with `mv.layout == self` to one with `mv_new.layout == self.conformal_base`\n", + " \n", + " This is done by discarding coefficients of blades containing enp1\n", + " \"\"\"\n", + " bits_base = self.conformal_base._basis_blade_order.index_to_bitmap\n", + " # insert the np1 bit into each basis blade bitmask, by...\n", + " bits = (\n", + " # ... leaving e1...en in place ...\n", + " bits_base & ~(~0 << self.ndims) |\n", + " # and moving em1 and em2 one bit to the left, leaving a 0 in the np1 bit\n", + " (bits_base & (0b11 << self.ndims)) << 1\n", + " )\n", + " # convert the new bitmaps into indices into the augmented value\n", + " inds = self._basis_blade_order.bitmap_to_index[bits]\n", + "\n", + " # and pick only those indicess\n", + " return self.conformal_base.MultiVector(mv.value[inds])\n", + "\n", + "OurCustomLayout.to_conformal = to_conformal; del to_conformal" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we'll define a plotting function, which plots the problem and solution circles in suitable colors via `pyganja`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import itertools\n", + "from pyganja import GanjaScene, draw\n", + "\n", + "def plot_rounds(in_rounds, out_rounds, scale=1):\n", + " colors = itertools.cycle([\n", + " (255, 0, 0),\n", + " (0, 255, 0),\n", + " (0, 0, 255),\n", + " (0, 255, 255),\n", + " ])\n", + " # note: .dual() neede here because we're passing in dual rounds, but ganja expects direct rounds\n", + " s = GanjaScene()\n", + " for r, color in zip(in_rounds, colors):\n", + " s.add_object(r.layout.to_conformal(r).dual(), color=color)\n", + " for r in out_rounds:\n", + " s.add_object(r.layout.to_conformal(r).dual(), color=(64, 64, 64))\n", + " draw(s, sig=r.layout.conformal_base.sig, scale=scale)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Apollonius' problem in $\\mathbb{R}^2$ with circles" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "l2 = OurCustomLayout(ndims=2)\n", + "e1, e2 = l2.basis_vectors_lst[:2]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This gives us the `Layout` `l2` with the desired metric," + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd # convenient but somewhat slow trick for showing tables\n", + "pd.DataFrame(l2.metric, index=l2.basis_names, columns=l2.basis_names)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can build some dual circles:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# add minus signs before `dual_round` to flip circle directions\n", + "c1 = dual_round(-e1-e2, 1)\n", + "c2 = dual_round(e1-e2, 0.75)\n", + "c3 = dual_round(e2, 0.5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Compute the space orthogonal to all of them, which is an object of grade 2:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pp = (l2.ups(c1) ^ l2.ups(c2) ^ l2.ups(c3)).dual()\n", + "pp.grades()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We hypothesize that this object is of the form `l2.ups(c4) ^ l2.ups(c5)`.\n", + "Taking a step not mentioned in the original paper, we decide to treat this as a regular conformal point pair, which allows us to project out the two factors with the approach taken in [A Covariant Approach to Geometry using Geometric Algebra](https://pdfs.semanticscholar.org/baba/976fd7f6577eeaa1d3ef488c1db13ec24652.pdf).\n", + "Here, we normalize with $e_{n+1}$ instead of the usual $n_\\infty$:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def pp_ends(pp):\n", + " P = (1 + pp.normal()) / 2\n", + " return P * (pp | pp.layout.enp1), ~P * (pp | pp.layout.enp1)\n", + "\n", + "c4u, c5u = pp_ends(pp)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And finally, plot our circles:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_rounds([c1, c2, c3], [l2.downs(c4u), l2.downs(c5u)], scale=0.75)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This works for colinear circles too:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c1 = dual_round(-1.5*e1, 0.5)\n", + "c2 = dual_round(e1*0, 0.5)\n", + "c3 = dual_round(1.5*e1, 0.5)\n", + "c4u, c5u = pp_ends((l2.ups(c1) ^ l2.ups(c2) ^ l2.ups(c3)).dual())\n", + "\n", + "plot_rounds([c1, c2, c3], [l2.downs(c4u), l2.downs(c5u)])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c1 = dual_round(-3*e1, 1.5)\n", + "c2 = dual_round(-2*e1, 1)\n", + "c3 = -dual_round(2*e1, 1)\n", + "c4u, c5u = pp_ends((l2.ups(c1) ^ l2.ups(c2) ^ l2.ups(c3)).dual())\n", + "\n", + "plot_rounds([c1, c2, c3], [l2.downs(c4u), l2.downs(c5u)])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Apollonius' problem in $\\mathbb{R}^3$ with spheres" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "l3 = OurCustomLayout(ndims=3)\n", + "e1, e2, e3 = l3.basis_vectors_lst[:3]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Again, we can check the metric:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pd.DataFrame(l3.metric, index=l3.basis_names, columns=l3.basis_names)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And apply the solution to some spheres, noting that we now need 4 in order to constrain our solution" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c1 = dual_round(e1+e2+e3, 1)\n", + "c2 = dual_round(-e1+e2-e3, 0.25)\n", + "c3 = dual_round(e1-e2-e3, 0.5)\n", + "c4 = dual_round(-e1-e2+e3, 1)\n", + "c5u, c6u = pp_ends((l3.ups(c1) ^ l3.ups(c2) ^ l3.ups(c3) ^ l3.ups(c4)).dual())\n", + "\n", + "plot_rounds([c1, c2, c3, c4], [l3.downs(c6u), l3.downs(c5u)], scale=0.25)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the figure above can be rotated!" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.0" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +}