Geometric Tower — What It Actually Does
Geometric Tower is a Python library that sits between raw point cloud data and machine learning models. It gives you a unified API for handling meshes, point clouds, and graphs while pushing geometry-specific operations through GPU acceleration. If you have ever tried stitching together trimesh processing, torch points, and custom CUDA kernels just to normalize a surface, you know why this exists. It is a geometric deep learning framework. The core idea is simple: represent 3D shapes as differentiable tensors, provide ops like farthest point sampling, knn search, and Laplacian computation, and let PyTorch handle the rest. The library is built around the concept that geometric data needs special treatment before it ever reaches a network. That means normals, curvatures, heat kernels, and spectral decompositions are first-class citizens, not afterthoughts. I started using it about three years ago when a project required computing discrete exterior calculus on non-uniform meshes. Standard libraries either demanded clean manifold meshes or fell apart on irregular voxel grids. Geometric Tower handled both without complaint. The API is not intuitive at first, but it is honest about what it does.
Installation
The package is available on PyPI and requires Python 3.8 or higher. pip install geometric-tower You also need PyTorch installed first. If your environment has CUDA 11.8 or later, the prebuilt wheels include GPU kernels. Without CUDA support, the library falls back to CPU, which is fine for prototyping but painful for anything above a few million points. I always install the CUDA version even when testing on CPU, because the internal tensor layouts differ slightly and mixing them causes silent dtype mismatches.
There is no pip dependency on OpenMesh or libigl. Geometric Tower reimplements the essentials in PyTorch, so you do not need to wrestle with C++ build toolchains. That is the main selling point. The tradeoff is that some niche operations run slower than their C++ counterparts.
Get the Full Details

Core Data Structures
The library revolves around two types: PointCloud and TetrahedralMesh. There is also a GraphMesh type for irregular structures. Each type carries its own metadata — normals, areas, incident relations — and validates consistency on construction. When you create a point cloud from raw coordinates, normals are not computed by default. You have to call compute_normals(), which uses a k-nearest neighbor fit. The default k is 20. That number matters more than you might expect. Set it too low on sparse scans and the normals oscillate. Set it too high and flat regions get bent toward curved ones. I settled on k=32 for LiDAR data and k=16 for dense photogrammetry meshes. One thing beginners miss: the library does not store faces as a separate polygonal structure by default. Faces are encoded as simplices in a higher-dimensional tensor. This saves memory but makes debugging harder because the shape of your face tensor is [num_faces, num_vertices, 3] for triangles, and you lose the intuitive [num_faces, 3] layout. I wrote a small helper function that flattens this into a standard numpy array whenever I need to inspect connectivity.
Practical Workflow
Here is how I actually use the library in a project. The steps are not glamorous but they work reliably. First, load your data. Geometric Tower provides readers for PLY, OBJ, and XYZ formats. It also accepts numpy arrays directly. If your file has inconsistent winding or degenerate faces, the loader will warn you but will not fix it. Run a cleanup pass first using mesh.remove_degenerate_faces() and mesh.merge_close_vertices(tol=1e-5). Second, compute per-vertex attributes. Normals, curvature, and area-weighted labels are the usual set. Call mesh.compute_curvature(method='cotangent') for mean curvature. The alternative is the 'shape operator' method, which is more accurate on smooth surfaces but fails on noisy data. I use cotangent for everything except high-quality scan reconstructions.
Third, downsample if needed. Farthest point sampling is the go-to method. pointcloud.fps_sample(N=8192) returns indices you can use to subset the original tensor. The implementation is O(N log N) and runs in about 0.3 seconds on a 500K-point cloud with an RTX 4090. Without GPU, it takes roughly 4 seconds. Worth noting: FPS does not preserve density uniformly in highly anisotropic distributions. If your data has thin spikes or long filaments, the sampler tends to undersample those regions. I discovered this the hard way on a project involving architectural scan data where stair railings kept disappearing from the point set. The workaround was to weight the sampling probability by local feature size before calling FPS. Here is the code I ended up using: weights = 1.0 / (pointcloud.estimate_feature_size() + 1e-6) pointcloud.fps_sample(N=8192, weights=weights)

This took an extra 0.1 seconds but preserved the delicate geometry that mattered.
Training a Geometric Tower Network
The library ships with a small collection of pretrained models for segmentation, classification, and registration. If you are building from scratch, start with a PointNet++ variant or a MeshGraphNet depending on your data type. PointCloud-based tasks use the built-in GeometricTowerClassifier wrapper, while mesh tasks need GeometricTowerMeshEncoder. The training loop is standard PyTorch. The one thing to watch is memory. A single high-resolution mesh with 2 million vertices can consume over 12 GB of VRAM during forward passes if you do not apply memory-efficient attention. I recommend setting attn_mode='linear' on the encoder when working with meshes above 500K faces. This trades a small accuracy hit for roughly 40 percent less memory usage. I ran into a bug once where the batch normalization layers in the encoder were using running statistics computed across different mesh resolutions in the same batch. The fix was to disable batch norm and replace it with group norm, or to ensure every batch contains meshes with similar vertex counts. I chose the latter. It is simpler and does not change model capacity in any meaningful way.
Common Pitfalls
There are a few things that trip people up regularly. The first is coordinate convention. Geometric Tower assumes right-handed coordinates with Y as up. If your data uses Z-up, rotate it before feeding it in. The library does not auto-detect or correct this. I wasted half a day debugging a registration failure that turned out to be a coordinate system mismatch. The second is the treatment of boundary edges. On open meshes, boundary edges are not treated as special by default. If you need them for loss functions or regularization, call mesh.extract_boundary_edges() and include them explicitly. Otherwise your Laplacian will leak gradients across the boundary.

The third is the lack of built-in boolean operations. Geometric Tower is not a CAD kernel. If you need to subtract one shape from another, use a separate library like CGAL or OpenCASCADE and import the result.
Performance Reality
Geometric Tower is fast but not free. The GPU kernels are well-optimized for batched operations. A single farthest point sampling on 1 million points takes about 0.5 seconds on an A100. The same operation on CPU takes 8 to 10 seconds. If you are doing repeated sampling inside a training loop, precompute and cache the results. The library does not do this automatically. For inference at scale, the bottleneck is usually data loading, not the model. Use DataLoader(prefetch_factor=4) and pin memory. I also recommend converting your meshes to tensor format once at the start of training and keeping them in GPU memory. Reloads from disk between epochs add up quickly.
When Not to Use It
If your task is purely 2D image-based and does not involve 3D geometry, this library is the wrong tool. If you need real-time mesh reconstruction from raw sensor streams, look at methods built on Kaolin or PyTorch3D instead. Geometric Tower is designed for offline processing and research-grade experiments, not production inference pipelines. It also struggles with extremely high-genus meshes. The spectral operations behind the Laplacian eigenmaps become numerically unstable above genus 20 on meshes with more than 1 million vertices. I encountered NaN outputs in the eigenvalue solver on a toroidal structure with heavy topological complexity. The workaround was to decompose the mesh into topological handles and process each separately, then stitch the results. It added complexity but eliminated the instability.

Summary of Key Functions
geometric_tower.PointCloud(points, normals=None) geometric_tower.Mesh(vertices, faces) pointcloud.compute_normals(k=20)
mesh.compute_curvature(method='cotangent') pointcloud.fps_sample(N, weights=None) mesh.extract_boundary_edges()
geometric_tower.GeoClassifier backbone='pointnetpp' pretrained=True That covers the basics. The documentation is sparse but the code is readable. If you hit an edge case not covered here, the GitHub issues page has answers for most of the common ones. I contribute fixes when I find bugs, so if something breaks, check the repo before assuming it is your fault.
