Möbius Pencil of Circles

../../_images/rendered_planar_pencil.png

In this tutorial, our aim is to visualize pencils of Möbius circles. This is explained below, expecting a bit of basic knowledge of geometry. If you are not that interested, or do not completely understand the mathematical background - that is okay too. You can still go on with the tutorial. The nice thing about visualization is, it helps us understand what is happening.

../../_images/mob_pencil_1_and_2.png

In the following, we explain how to create this python script: Möbius Pencil of Circles Script, and use it to visualize the objects in Blender.

Mathematical Background

What is a Möbius pencil of circles?

Möbius geometry in \(\widehat{\mathbb{R}^2} = \mathbb{R}^{2} \cup \{\infty\}\) corresponds to geometry on the unit sphere \(S^2\) via stereographic projection. Thus, circles in two-dimensional Möbius geometry correspond to circles on \(S^2\), which are intersections of \(S^2\) with planes. By polarity, a plane intersecting \(S^2\) corresponds to a point outside \(S^2\).

If we now consider a line in \(\mathbb{R}^3\), by polarity its points define a one-parameter-family of planes in \(\mathbb{R}^3\) and therefore a one-parameter-family of spherical circles, and hence - via stereographic projection - a so called Möbius pencil of circles in \(\widehat{\mathbb{R}^2}\).

The one-parameter-family of planes all intersect in a common line, the polar line of the given line, which again represents a Möbius pencil of circles. In \(\widehat{\mathbb{R}^2}\) this polar pencil represents a family of circles where each circle intersects all circles in the first pencil orthogonally.

Setup

Start Blender from the terminal, so you can read error-messages. More on how to start and setup Blender, you find here: Getting Started with Blender.

In the Blender GUI, create a new script, add the following imports to your new script, and execute with “Alt”+”P”.

import numpy as np

import ddg
from ddg.visualization.blender.collection import collection, clear

In this script, our aim is to produce a lot of Blender objects, and e.g. display them independently from one another. For this, we will use Blender collections. To start with a clean Blender file we clear everything in there using a mechanism that removes all the collections and clears all the objects within.

clear(remove_collections=True)

For more on the clear module, see here.

New collections can be created with the library as follows:

mob_geo_coll = collection(
    "Mobius geometry",
    children=[
        ["lines", "points_on_line", "points_on_polar_line"],
        "spherical circles",
        "polar spherical circles",
    ],
)
euc_geo_coll = collection(
    "Euclidean geometry", children=["Euclidean circles", "orthogonal Euclidean circles"]
)
projection_coll = collection("projection_with_light")

You can pass a list of children to the function collection(). If an entry of the list is a string, it will create a child with corresponding name. If an entry of the list is a list of strings, the first string determines the name of the child collection, the others will create grandchildren, i.e children of the childs collection. If the parent collection already exists, it greps the one with this name, and returns this collection object.

We are ready to create some objects.

Sphere and Line

Sphere

First, we want to create the sphere as a Quadric. Add the following to your script.

s2 = ddg.geometry.quadrics.Quadric(np.diag([1, 1, 1, -1]))

A Quadric is created by its symmetric matrix. More information on Quadrics, you can find here.

To display the sphere in Blender, we have to convert it to a Blender object. This is easily done with the helper function to_blender_object_helper()!

bobj_s2 = ddg.to_blender_object_helper(
    s2, sampling=[0.1, 40, "c"], name="s2", material="s2", collection=mob_geo_coll
)

As for any smooth object, for the quadric to be displayed in Blender, it needs to be discretized using a sampling. The sampling options, as explained here, are given as an argument. Internally the quadric is converted to a SmoothNet and then, by a sampling, converted to a DisreteNet. Have a look at the nets user’s guide. The given name determines the name of the object in Blender.

../../_images/sphere0.png

Line

Next, we want to create and display a line. For that, we use Subspace, which is a class for projective subspaces. Here we give two examples one line that intersects the sphere and another line that is tangential to the sphere. With these two you can generate the configurations of the images at the top. So let’s choose

# p1, p2 = np.array([-4, .5, -0.5, 1]), np.array([4,.5, -0.5, 1])
p1, p2 = np.array([-1, 1, 0, 1]), np.array([1, 1, 0, 1])
l = ddg.geometry.subspaces.Subspace(p1, p2)

A subspace can be simply defined by the vectors that span it. Note, that we want a line in 3D projective space, which corresponds to a 2D vector subspace of \(\mathbb{R}^4\). That’s why we have two 4D vectors. More on subspaces, you can find here.

Next, we create a SmoothNet out of the subspace, to obtain an affine image of the projective subspace, that is a line in 3D space. Before we do so, we orthonormalize and center the subspace.

# orthonormalize and center l
l = l.orthonormalize_and_center((p1 + p2) / 2)

# create smooth net
l_net = ddg.to_smooth_net(l, domain=[[-4, 4]], affine=True, convex=True)

The argument affine is given to obtain an affine image of the projective line. The convex=True argument means, that the net function of the resulting SmoothNet is the convex combination of the vectors spanning the subspace. By setting the domain of the SmoothNet to domain=[[-4,4]], we obtain a line segment symmetrical around the center of the line.

And finally, we convert it to a Blender object by:

# to blender
bobj_l = ddg.to_blender_object_helper(
    l,
    domain=[[-4, 4]],
    sampling=[0.1, 40, "c"],
    name="line",
    collection=mob_geo_coll.children[0],
    material="line",
    curve_properties={"bevel_depth": 0.01},
)

The given domain is interpreted as domain of the net function and determines the length of the line. We add the line to the collection we created. The last argument, given in this form of a dictionary, determines the thickness of the line. Feel free to play with it.

../../_images/sphere1.png

Spherical and Planar Circles

Next, we will see how a point on the line l corresponds to a circle on s2, and how the spheric circle corresponds to a planar circle.

Spherical Circles

For a given point, we want to calculate its polar plane with respect to s2, as well as the intersection of the plane with s2. These intersection should then be converted to blender objects, so that we can see them. And this gives us our spherical circles.

We choose a point on our line l. When given a point of its domain as an argument, a SmoothNet returns its value at this point:

# get a point
pt = l_net(2)

Now, we want to display the sampled point in Blender, as a little sphere. The sampled point is just an affine image of a point on l. We homogenize it,

# homogenize pt
pt_homogenized = ddg.math.projective.homogenize(pt)

and convert it to a Subspace.

# convert point to subspace
pt_projective = ddg.geometry.subspaces.Subspace(pt_homogenized)

We give the radius for the size of the sphere that visualizes the point.

# display point in blender
bobj_pt = ddg.to_blender_object_helper(
    pt_projective,
    name="point",
    sphere_radius=0.05,
    material="point",
    collection=mob_geo_coll.children[0].children[0],
)

Now, we can make use of the methods for Subspaces and calculate the polar plane

# calculate its polar plane
plane = s2.polarize(pt_projective)

and calculate the intersection of it with the sphere.

# calculate intersection circle
circ_on_sphere = ddg.geometry.intersection.meet(plane, s2)

Trying to convert an empty intersection to a blender object yields an error, so first, we check if the intersection is empty by checking its dimension.

# check if intersection is not empty
if circ_on_sphere.dimension != -1:
    bobj_circ_on_sphere = ddg.to_blender_object_helper(
        circ_on_sphere,
        name="spherical circle",
        sampling=[0.1, 100, "c"],
        collection=mob_geo_coll.children[1],
        material="spherical circle",
        curve_properties={"bevel_depth": 0.005},
    )

This gives us:

../../_images/sphere2.png

Planar Circles

Next, we want to stereographically project the spherical circle onto the plane.

For this we create a plane to project on as a subspace and stereographically project the circle:

# stereographic project
euc_plane = ddg.geometry.subspaces.subspace_from_affine_points_and_directions(
    [[0, 0, 0]], [[1, 0, 0], [0, 1, 0]]
)
circ_on_plane = ddg.geometry.projections.stereographic_project(
    circ_on_sphere, euc_plane
)

# convert to blender
bobj_circ_on_plane = ddg.to_blender_object_helper(
    circ_on_plane,
    name="planar circle",
    sampling=[0.1, 100, "c"],
    collection=euc_geo_coll.children[0],
    material="planar circle",
    curve_properties={"bevel_depth": 0.005},
)

bobj_euc_plane = ddg.to_blender_object_helper(
    euc_plane,
    name="euc_plane",
    domain=[[-30, 30], [-30, 30]],
    sampling=[9, "t"],
    material="euc plane",
    collection=euc_geo_coll,
)

In general the function stereographic_project() takes the point to project from and the plane to project to as arguments. In our case the projection point, the north pole, is precisely the default argument of the function.

And we get:

../../_images/sphere3.png

We can also obtain the planar circles in a different way, using a Blender light and looking at its shadow.

# stereographic project
light = ddg.visualization.blender.light.light(
    type_="POINT", location=[0, 0, 1], energy=600, collection=projection_coll
)
light.data.shadow_soft_size = 0

bobj_euc_plane.is_shadow_catcher = True

Here you may want to move the plane euc_plane a little bit lower so it does not intersect s2 so you can see the shadow of all circles. If you want to see the shadow in Blender you have to select the Rendered Viewport Shading.

../../_images/rendered_viewport.png

Möbius Pencil of Circles

If we now consider the corresponding circles for all points on a line, that is called a Möbius pencil of circles. To visualize this pencil, we need to choose some points on the line, and then for each point proceed as above.

Circles Function for a Point

First, we write a function that takes a point and creates the corresponding spherical and planar circle. This function does the job:

def circles_from_point(
    pt,
    pt_coll=None,
    spherical_coll=None,
    planar_coll=None,
    material_spherical=None,
    material_planar=None,
    link=True,
):
    """
    Create the point, the corresp. spherical circle,
    and the corresp. planar circle.

    Parameters
    ----------
    pt : array of length 3

    pt_coll,
    spherical_coll,
    planar_ coll : Blender Collections (default=None)

    material_spherical: string or bpy.tupes.Material
    material_planar: string or bpy.types.Material

    link : bool (default=True)

    Returns
    -------
    None or list
        depending on link
    """

    # display point in blender
    bobj_pt = ddg.to_blender_object_helper(
        ddg.datastructures.nets.PointNet(pt),
        name="point",
        sphere_radius=0.05,
        collection=pt_coll,
        material="point",
        link=link,
    )

    # homogenize pt
    pt_homogenized = ddg.math.projective.homogenize(pt)

    # convert point to subspace
    pt_homogenized = ddg.geometry.subspaces.Subspace(pt_homogenized)

    # calculate its polar plane
    plane = s2.polarize(pt_homogenized)

    # calculate intersection circle
    circ_on_sphere = ddg.geometry.intersection.meet(plane, s2)

    # check if intersection is not empty
    if circ_on_sphere.dimension != -1:

        # convert spherical circle to blender
        bobj_circ_on_sphere = ddg.to_blender_object_helper(
            circ_on_sphere,
            name="spherical circle",
            sampling=[0.1, 100, "c"],
            curve_properties={"bevel_depth": 0.006},
            collection=spherical_coll,
            material=material_spherical,
            link=link,
        )

        # stereographic project
        circ_on_plane = ddg.geometry.projections.stereographic_project_quadric(
            circ_on_sphere, euc_plane
        )

        # convert planar circle to blender
        bobj_circ_on_plane = ddg.to_blender_object_helper(
            circ_on_plane,
            name="planar circle",
            sampling=[0.1, 100, "c"],
            curve_properties={"bevel_depth": 0.0060},
            collection=planar_coll,
            material=material_planar,
            link=link,
        )

        return [bobj_pt, bobj_circ_on_sphere, bobj_circ_on_plane]

    else:
        return [bobj_pt]

Wrapper Function for Points on a Line

We want to apply the above funtion to multiple points on a line. For that, we write a function that takes a line and a parameter as input, and calls the spheric_and_planar_circles function to create the corresponding circles:

def circles_from_line_and_value(
    line,
    t,
    pt_coll=None,
    spherical_coll=None,
    planar_coll=None,
    material_spherical=None,
    material_planar=None,
    link=True,
):
    """
    Create the point line(t), the corresp. spherical circle,
    and the corresp. planar circle.

    Parameters
    ----------
    line : SmoothNet

    t : float
        point in domain of line

    pt_coll,
    spheric_coll,
    planar_ coll : Blender Collections (default=None)

    link : bool (default=True)

    Returns
    -------
    None or list
        depending on link
    """
    return circles_from_point(
        line(t),
        pt_coll=pt_coll,
        spherical_coll=spherical_coll,
        planar_coll=planar_coll,
        material_spherical=material_spherical,
        material_planar=material_planar,
        link=link,
    )

Get the corresponding circles for a point on l by:

circles_from_line_and_value(
    l_net,
    1.3,
    pt_coll=mob_geo_coll.children[0].children[0],
    spherical_coll=mob_geo_coll.children[1],
    planar_coll=euc_geo_coll.children[0],
    material_spherical="spherical cirlce",
    material_planar="planar circle",
)

Adding a Slider for a Varying Point on Line

Next, we will add a slider to the blender GUI, with which we can regulate certain parameters in the creation of our objects. This is explained in more detail in this docs entry. In our case, we want to vary the parameter which determines the point on the line l.

The following code will add such a slider:



def slider_fct_for_l(t):
    return circles_from_line_and_value(l_net, t, link=False)


callback = ddg.visualization.blender.props.clear_callback(
    "slider circles", slider_fct_for_l
)

ddg.visualization.blender.props.add_props_with_callback(callback, ("t",), 1.0)

To find the slider in the blender GUI, go to the viewport, press “N” to open the side menu, and go to the “DDG” tab. There it is.

../../_images/find_slider.png

Change the parameter t, to see how the point and the circles change! But be careful sliders don’t work (well) with collections as they modify collections themselves. Its better not to work with collections when using sliders.

Visualize some Circles in Pencil

To get a nice image of the pencil, we can sample points on the corresponding line, and then plot the circles for each of the points.

First, we need a sampling of the domain of the line. That is, a list with numbers. Let’s use linear sampling here (equidistant points). Of course you could try something else at home. It’s also fun to play with the number of samples.

# LINEAR SAMPLING OF DOMAIN
# this function takes arg3 samples equidistantly of the intervall [arg1,ag2]

sample = np.linspace(-3, 3, 30)

In the sampling, the first two arguments correspond to the domain of the lines, and the third is the number of samples.

Now, we can easily create some circles in the pencil:

# visualize pencil
for t in sample:
    circles_from_line_and_value(
        l_net,
        t,
        pt_coll=mob_geo_coll.children[0].children[0],
        spherical_coll=mob_geo_coll.children[1],
        planar_coll=euc_geo_coll.children[0],
        material_spherical="spherical cirlce",
        material_planar="planar circle",
    )

It is very useful, to pass collections here, so we can hide and show the different groups of objects as we wish. For example, hiding the collections with the points gives us:

../../_images/visualize_pencil.png

Nice!

Orthogonal Pencil

Consider the Möbius pencil of circles for the polar line of l. All circles of this pencil intersect all circles in the pencil corresponding to l orthogonally.

With pyddg, we can easily polarize the Subspace l with respect to the Quadric s2 to obtain the polar line as a Subspace. We then proceed as before, to create the corresponding orthogonal pencil!

# get polar subspace of l
polar_l = s2.polarize(l)

# orthonormalize
polar_l = polar_l.orthonormalize()

# convert to net
polar_l_net = ddg.to_smooth_net(polar_l, domain=[[-4, 4]], affine=True, convex=True)

# convert to blender
bobj_polar = ddg.to_blender_object_helper(
    polar_l,
    sampling=[0.1, 40, "c"],
    name="polar line",
    curve_properties={"bevel_depth": 0.01},
    collection=mob_geo_coll.children[0],
)

# visualize orthogonal pencil
for t in sample:
    circles_from_line_and_value(
        polar_l_net,
        t,
        pt_coll=mob_geo_coll.children[0].children[1],
        spherical_coll=mob_geo_coll.children[2],
        planar_coll=euc_geo_coll.children[1],
        material_spherical="polar spherical cirlce",
        material_planar="polar planar circle",
    )

And we get:

../../_images/visualize_orth_pencil.png

Render an Image

Before we render an image, we need to add a camera and light to the Blender scene and create materials for our objects. How to create a setup and render an image is explained here: Render a Picture of a Geometric Object.

Materials can be created and assigned to objects in the blender GUI. If you have a lot of objects, it’s convenient to assign materials via script! Basic materials can also be created in the script, as explained here: Creating and Setting Materials.

By hiding and showing different collections in the blend file we created, we can then render beautiful images, like:

../../_images/rendered_planar_pencil.png