<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Working with custom algebras
This notebook explores the algebra defined in The Lie Model for Euclidean Geometry (Hongbo Li), and its application to solving Apollonius' Problem. It also shows 
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$.
This is an extension of a standard conformal algebra, with an extra $e_{n+1}$ basis vector.
Note that we permuted the order in the source code below to make ConformalLayout happy.
Step1:  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.
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.
Step2:  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.
Step3:  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.
Visualization
Finally, we'll define a plotting function, which plots the problem and solution circles in suitable colors via pyganja.
Note that this all works because of our definition of the to_conformal BasisVectorMap.
Step4:  Apollonius' problem in $\mathbb{R}^2$ with circles
Step5:  This gives us the Layout l2 with the desired metric,
Step6:  Now we can build some dual circles
Step7:  Compute the space orthogonal to all of them, which is an object of grade 2
Step8:  We hypothesize that this object is of the form l2.ups(c4) ^ l2.ups(c5).
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 <cite data-cite="lasenby-covariant-approach">A Covariant Approach to Geometry using Geometric Algebra</cite>.
Here, we normalize with $e_{n+1}$ instead of the usual $n_\infty$
Step9:  And finally, plot our circles
Step10:  This works for colinear circles too
Step11:  Apollonius' problem in $\mathbb{R}^3$ with spheres
Step12:  Again, we can check the metric
Step13:  And apply the solution to some spheres, noting that we now need 4 in order to constrain our solution
<ASSISTANT_TASK:>
Python Code:
from clifford import ConformalLayout, BasisVectorIds, MultiVector, transformations
class OurCustomLayout(ConformalLayout):
    def __init__(self, ndims):
        self.ndims = ndims
        
        euclidean_vectors = [str(i + 1) for i in range(ndims)]
        conformal_vectors = ['m2', 'm1']
        # Construct our custom algebra. Note that ConformalLayout requires the e- and e+ basis vectors to be last.
        ConformalLayout.__init__(
            self,
            [1]*ndims + [-1] + [1, -1],
            ids=BasisVectorIds(euclidean_vectors + ['np1'] + conformal_vectors)
        )
        self.enp1 = self.basis_vectors_lst[ndims]
        # Construct a base algebra without the extra `enp1`, which would not be understood by pyganja.
        self.conformal_base = ConformalLayout(
            [1]*ndims + [1, -1],
            ids=BasisVectorIds(euclidean_vectors + conformal_vectors)
        )
        
        # this lets us convert between the two layouts
        self.to_conformal = transformations.between_basis_vectors(self, self.conformal_base)
Explanation: Working with custom algebras
This notebook explores the algebra defined in The Lie Model for Euclidean Geometry (Hongbo Li), and its application to solving Apollonius' Problem. It also shows 
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$.
This is an extension of a standard conformal algebra, with an extra $e_{n+1}$ basis vector.
Note that we permuted the order in the source code below to make ConformalLayout happy.
End of explanation
def ups(self, s):
    return s + self.enp1*abs(s)
OurCustomLayout.ups = ups; del ups
def downs(self, mv):
    if (mv | self.enp1)[()] > 0:
        mv = -mv
    return mv
OurCustomLayout.downs = downs; del downs
Explanation: 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.
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.
End of explanation
def dual_round(at, r):
    l = at.layout
    return l.up(at) - 0.5*l.einf*r*r
Explanation: 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.
End of explanation
import itertools
from pyganja import GanjaScene, draw
def plot_rounds(in_rounds, out_rounds, scale=1):
    colors = itertools.cycle([
        (255, 0, 0),
        (0, 255, 0),
        (0, 0, 255),
        (0, 255, 255),
    ])
    # note: .dual() neede here because we're passing in dual rounds, but ganja expects direct rounds
    s = GanjaScene()
    for r, color in zip(in_rounds, colors):
        s.add_object(r.layout.to_conformal(r).dual(), color=color)
    for r in out_rounds:
        s.add_object(r.layout.to_conformal(r).dual(), color=(64, 64, 64))
    draw(s, sig=r.layout.conformal_base.sig, scale=scale)
Explanation: 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.
Visualization
Finally, we'll define a plotting function, which plots the problem and solution circles in suitable colors via pyganja.
Note that this all works because of our definition of the to_conformal BasisVectorMap.
End of explanation
l2 = OurCustomLayout(ndims=2)
e1, e2 = l2.basis_vectors_lst[:2]
Explanation: Apollonius' problem in $\mathbb{R}^2$ with circles
End of explanation
import pandas as pd  # convenient but somewhat slow trick for showing tables
pd.DataFrame(l2.metric, index=l2.basis_names, columns=l2.basis_names)
Explanation: This gives us the Layout l2 with the desired metric,
End of explanation
# add minus signs before `dual_round` to flip circle directions
c1 = dual_round(-e1-e2, 1)
c2 = dual_round(e1-e2, 0.75)
c3 = dual_round(e2, 0.5)
Explanation: Now we can build some dual circles:
End of explanation
pp = (l2.ups(c1) ^ l2.ups(c2) ^ l2.ups(c3)).dual()
pp.grades()
Explanation: Compute the space orthogonal to all of them, which is an object of grade 2:
End of explanation
def pp_ends(pp):
    P = (1 + pp.normal()) / 2
    return P * (pp | pp.layout.enp1), ~P * (pp | pp.layout.enp1)
c4u, c5u = pp_ends(pp)
Explanation: We hypothesize that this object is of the form l2.ups(c4) ^ l2.ups(c5).
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 <cite data-cite="lasenby-covariant-approach">A Covariant Approach to Geometry using Geometric Algebra</cite>.
Here, we normalize with $e_{n+1}$ instead of the usual $n_\infty$:
End of explanation
plot_rounds([c1, c2, c3], [l2.downs(c4u), l2.downs(c5u)], scale=0.75)
Explanation: And finally, plot our circles:
End of explanation
c1 = dual_round(-1.5*e1, 0.5)
c2 = dual_round(e1*0, 0.5)
c3 = dual_round(1.5*e1, 0.5)
c4u, c5u = pp_ends((l2.ups(c1) ^ l2.ups(c2) ^ l2.ups(c3)).dual())
plot_rounds([c1, c2, c3], [l2.downs(c4u), l2.downs(c5u)])
c1 = dual_round(-3*e1, 1.5)
c2 = dual_round(-2*e1, 1)
c3 = -dual_round(2*e1, 1)
c4u, c5u = pp_ends((l2.ups(c1) ^ l2.ups(c2) ^ l2.ups(c3)).dual())
plot_rounds([c1, c2, c3], [l2.downs(c4u), l2.downs(c5u)])
Explanation: This works for colinear circles too:
End of explanation
l3 = OurCustomLayout(ndims=3)
e1, e2, e3 = l3.basis_vectors_lst[:3]
Explanation: Apollonius' problem in $\mathbb{R}^3$ with spheres
End of explanation
pd.DataFrame(l3.metric, index=l3.basis_names, columns=l3.basis_names)
Explanation: Again, we can check the metric:
End of explanation
c1 = dual_round(e1+e2+e3, 1)
c2 = dual_round(-e1+e2-e3, 0.25)
c3 = dual_round(e1-e2-e3, 0.5)
c4 = dual_round(-e1-e2+e3, 1)
c5u, c6u = pp_ends((l3.ups(c1) ^ l3.ups(c2) ^ l3.ups(c3) ^ l3.ups(c4)).dual())
plot_rounds([c1, c2, c3, c4], [l3.downs(c6u), l3.downs(c5u)], scale=0.25)
Explanation: And apply the solution to some spheres, noting that we now need 4 in order to constrain our solution
End of explanation
<END_TASK>