The flatsurf.geometry.categories Package¶
Categories of Surfaces and Polygons.
sage-flatsurf uses SageMath categories to distinguish different kinds of surfaces such as hyperbolic surfaces, translation surfaces, …. See https://doc.sagemath.org/html/en/reference/categories/sage/categories/primer.html for a detailed introduction of categories in SageMath. In short, “categories” are not so much mathematical categories but more similar to a normal class hierarchy in Python; however, they extend the idea of a classical class hierarchy by allowing us to dynamically change the category (and methods) of a surface as we learn more about it.
Note that you normally don’t have to create categories explicitly. Categories
are deduced automatically (at least for surfaces of finite type.) You should
think of categories as an implementation detail. As a user of sage-flatsurf,
you don’t need to know about them. As a developer of sage-flatsurf, they
provide entry points to place your code; e.g., to add a method to all
translation surfaces, actually add a method to
translation_surfaces.TranslationSurfaces.ParentMethods
.
A similar but smaller hierarchy of categories exists for polygons, Euclidean polygons, Hyperbolic polygons.
Note
Categories are deduced by calling methods such as
is_orientable()
,
is_with_boundary()
,
is_compact()
,
is_connected()
,
is_finite_type()
,
is_cone_surface()
,
is_dilation_surface()
,
is_translation_surface()
,
and
is_rational_surface()
.
There are default implementations for these for finite
type surfaces. Once a surfaces has been found to be in a certain
subcategory, these methods are replaced to simply return True
instead
of computing anything. If a class explicitly overrides these methods, then
the category machinery cannot replace that method anymore when the category
of the surface gets refined. Consequently, it can be beneficial for the
override to shortcut the question by querying the category, e.g.,
is_rational
could start with if "Rational" in
self.category().axioms(): return True
before actually performing any
computation.
EXAMPLES:
A single square without any gluings:
sage: from flatsurf import MutableOrientedSimilaritySurface
sage: S = MutableOrientedSimilaritySurface(QQ)
sage: from flatsurf import polygons
sage: S.add_polygon(polygons.square(), label=0)
0
This is considered to be a surface built from polygons with all gluings being similarities (however there are none):
sage: S.category()
Category of finite type oriented similarity surfaces
It does not really make sense to ask which stratum this surface belongs to:
sage: S.stratum()
Traceback (most recent call last):
...
AttributeError: ... has no attribute 'stratum'...
Once we add gluings, this turns into a square torus:
sage: S.glue((0, 0), (0, 2))
sage: S.glue((0, 1), (0, 3))
We signal to sage-flatsurf that we are done building this surface, and its category gets refined:
sage: S.set_immutable()
sage: S.category()
Category of connected without boundary finite type translation surfaces
Since this is now a translation surface, we can ask for its stratum again:
sage: S.stratum()
H_1(0)
There are a number of workarounds if you want to compute things such as
.stratum()
on a mutable surface. For the sake of this demonstration, lets
make our surface mutable again:
sage: S = MutableOrientedSimilaritySurface.from_surface(S)
The recommended approach is to create a copy of the surface, make it immutable and then call the methods you need:
sage: T = MutableOrientedSimilaritySurface.from_surface(S)
sage: T.set_immutable()
sage: T.stratum()
H_1(0)
You might be worried about the performance implications but most of the time that might be a premature optimization.
Often enough, the copy is actually not the problem but the time that is spent to figure out that that copy is actually a translation surface. If you already know that to be true, you can simplify things to:
sage: from flatsurf.geometry.categories import TranslationSurfaces
sage: T = MutableOrientedSimilaritySurface.from_surface(S, category=TranslationSurfaces().WithoutBoundary())
sage: T.stratum()
H_1(0)
You can also change the category of a mutable surface to provide all the functionality that is available to surfaces in that category:
sage: S._refine_category_(TranslationSurfaces().WithoutBoundary())
sage: S.stratum()
H_1(0)
Note however, that the category of a surface cannot be generalized anymore. This surface is now at least a “translation surface”, no matter what mutations you make to it. (And the system will not check that it is indeed a translation surface.) That approach might still be beneficial if you make lots of minor changes to the surface, e.g., lots of edge flips, and want to query such methods frequently.
Finally, we can try to call the
stratum()
method directly but it might have dependencies on other methods that are not
available:
sage: S = MutableOrientedSimilaritySurface.from_surface(S)
sage: TranslationSurfaces.FiniteType.WithoutBoundary.ParentMethods.stratum(S)
H_1(0)
While this works, this approach is quite brittle and might sometimes need a mix with the above to work:
sage: from flatsurf.geometry.categories import ConeSurfaces
sage: S._refine_category_(ConeSurfaces().WithoutBoundary())
sage: TranslationSurfaces.FiniteType.WithoutBoundary.ParentMethods.stratum(S)
H_1(0)