{ "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, transformations\n", "\n", "class OurCustomLayout(ConformalLayout):\n", " def __init__(self, ndims):\n", " self.ndims = ndims\n", " \n", " euclidean_vectors = [str(i + 1) for i in range(ndims)]\n", " conformal_vectors = ['m2', 'm1']\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(euclidean_vectors + ['np1'] + conformal_vectors)\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(euclidean_vectors + conformal_vectors)\n", " )\n", " \n", " # this lets us convert between the two layouts\n", " self.to_conformal = transformations.between_basis_vectors(self, self.conformal_base)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The code above also defines a stardard conformal $\\mathbb{R}^{N+1,1}$ layout without this new basis vector. This is primarily to support rendering with `pyganja`, which doesn't support the presence of this extra vector. `BasisVectorMap` defaults to preserving vectors by name between one algebra and another, while throwing away blades containing vectors missing from the destination algebra." ] }, { "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": [ "\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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Visualization\n", "\n", "Finally, we'll define a plotting function, which plots the problem and solution circles in suitable colors via `pyganja`.\n", "Note that this all works because of our definition of the `to_conformal` `BasisVectorMap`." ] }, { "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.\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 }