Skip to content

Commit fdd6497

Browse files
authored
add the affine heat method (#219)
* initial work on affine * add center option to logmap strategy * use source initial guess for surface centers * Revert "use source initial guess for surface centers" This reverts commit cb3dea1. * add adaptive affine heat method * docs for affine heat method * add caveat about intrinsic tangent space alignment
1 parent 002ea9c commit fdd6497

5 files changed

Lines changed: 280 additions & 29 deletions

File tree

docs/docs/surface/algorithms/vector_heat_method.md

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ This section describes the _Vector Heat Method_ in geometry-central, which compu
22

33
Note that these quantities all depend on the _intrinsic_ geometry of a surface (via the `IntrinsicGeometryInterface`). Therefore, these routines can be run on abstract geometric domains as well as traditional surfaces in 3D.
44

5-
These algorithms are described in [The Vector Heat Method](http://www.cs.cmu.edu/~kmcrane/Projects/VectorHeatMethod/paper.pdf).
5+
These algorithms are described in [The Vector Heat Method](http://www.cs.cmu.edu/~kmcrane/Projects/VectorHeatMethod/paper.pdf) and [The Affine Heat Method](https://www.yousufsoliman.com/projects/the-affine-heat-method.html).
66

77
`#include "geometrycentral/surface/vector_heat_method.h"`
88

@@ -50,11 +50,11 @@ for (/* ... some inputs ... */ ) {
5050
VertexData<double> scalarExtension = vhmSolver->extendScalar(points);
5151
```
5252
53-
??? func "`#!cpp VertexData<double> VectorHeatSolver::extendScalar( const std::vector<std::tuple<Vertex, double>>& sources)`"
53+
??? func "`#!cpp VertexData<double> VectorHeatSolver::extendScalar(const std::vector<std::tuple<Vertex, double>>& sources)`"
5454
5555
Compute the nearest-neighbor extension of scalar data defined at isolated vertices to the entire domain. The input is a list of vertices and their corresponding values.
5656
57-
??? func "`#!cpp VertexData<double> VectorHeatSolver::extendScalar( const std::vector<std::tuple<SurfacePoint, double>>& sources)`"
57+
??? func "`#!cpp VertexData<double> VectorHeatSolver::extendScalar(const std::vector<std::tuple<SurfacePoint, double>>& sources)`"
5858
5959
Compute the nearest-neighbor extension of scalar data defined at isolated points to the entire domain. The input is a list of [surface points](../../utilities/surface_point/) and their corresponding values.
6060
@@ -83,25 +83,56 @@ The _logarithmic map_ is a very special 2D local parameterization of a surface a
8383
8484
![octopus logmap](/media/octopus_logmap.jpg){: style="height:300px; display: block; margin-left: auto; margin-right: auto;"}
8585
86-
These routines compute the logarithmic map using the vector heat method.
86+
These routines compute the logarithmic map using the vector heat method or the affine heat method. Several strategies are available, specified by the `LogMapStrategy` enum:
8787
88-
??? func "`#!cpp VertexData<Vector2> VectorHeatSolver::computeLogMap(const Vertex& sourceVert)`"
88+
- `LogMapStrategy::VectorHeat`: the original algorithm from "The Vector Heat Method". Fast, but may have some distortion and warping.
89+
- `LogMapStrategy::AffineLocal`: the fast local algorithm from "The Affine Heat Method". Fast & highly accurate near source. Generates extremely regular coordinates, but may have artifacts if you do not use an intrinsic Delaunay triangulation (see below).
90+
- `LogMapStrategy::AffineAdaptive`: the global algorithm from "The Affine Heat Method". Highest quality. Requires factoring a new matrix each time, so it is significantly slower for repeated solves, though roughly similar cost if just solving once.
91+
92+
??? func "`#!cpp VertexData<Vector2> VectorHeatSolver::computeLogMap(const Vertex& sourceVert, LogMapStrategy strategy = LogMapStrategy::VectorHeat)`"
8993
9094
Compute the logarithmic map with respect to the given source vertex.
9195
9296
The angular coordinate of the log map will be respect to the tangent space of the source vertex.
9397
9498
95-
??? func "`#!cpp VertexData<Vector2> VectorHeatSolver::computeLogMap(const SurfacePoint& sourceP)`"
99+
??? func "`#!cpp VertexData<Vector2> VectorHeatSolver::computeLogMap(const SurfacePoint& sourceP, LogMapStrategy strategy = LogMapStrategy::VectorHeat)`"
96100
97101
Compute the logarithmic map with respect to the given source point, which is a general [surface point](../../utilities/surface_point/).
98102
99103
The angular coordinate of the log map will be respect to the tangent space of the source vertex, edge, or face.
100104
105+
!!! note "intrinsic triangulations make logmaps much more accurate"
106+
107+
Using an [intrinsic triangulation](/surface/intrinsic_triangulations/basics) will make your logarithmic maps dramatically more accurate, especially on meshes with irregular triangles, at little cost. The `AffineLocal` strategy in particular benefits greatly from operating on a Delaunay intrinsic triangulation.
108+
109+
**Example: log maps with intrinsic triangulations**
110+
```cpp
111+
#include <#include "geometrycentral/surface/signpost_intrinsic_triangulation.h">
112+
113+
// Construct an intrinsic triangulation
114+
SignpostIntrinsicTriangulation signpostTri(*mesh, *geometry);
115+
signpostTri.flipToDelaunay(); // this is the important step, makes it numerically nice!
116+
117+
// Remap the vertex to the intrinsic triangulation, if using
118+
Vertex origSourceVert = /* your vertex */;
119+
Vertex intrinsicSourceVert = signpostTri->equivalentPointOnIntrinsic(SurfacePoint(origSourceVert)).vertex;
120+
121+
// Run the algorithm
122+
VectorHeatMethodSolver solver(signpostTri);
123+
VertexData<Vector2> logmapIntrinsic = solver->computeLogMap(sourceVert, logMapStrategy);
124+
125+
// Copy the logmap back to your original mesh
126+
VertexData<Vector2> logmap = signpostTri->restrictToInput(logmapIntrinsic);
127+
```
128+
129+
By default, the resulting logarithmic map has coordinates defined in the tangent space of the source point on the intrinsic mesh. At vertices these spaces are the same, but at a general `SurfacePoint` inside some face you might want to align tangent spaces; see the [intrinsic triangulation](/surface/intrinsic_triangulations/basics) documentation for details. There may or may not be a builtin method to do the alignment, depending on your setting.
130+
131+
101132
102133
## Citation
103134
104-
If these algorithms contribute to academic work, please cite the following paper:
135+
If these algorithms contribute to academic work, please cite the following paper(s):
105136
106137
```bib
107138
@article{sharp2019vector,
@@ -114,4 +145,13 @@ If these algorithms contribute to academic work, please cite the following paper
114145
year={2019},
115146
publisher={ACM}
116147
}
148+
149+
@article{soliman2025affine,
150+
title={The Affine Heat Method},
151+
author={Yousuf Soliman, Nicholas Sharp,
152+
booktitle={Computer Graphics Forum},
153+
volume={44},
154+
number={5},
155+
year={2025}
156+
}
117157
```
Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
#pragma once
22

33
#include "geometrycentral/surface/intrinsic_geometry_interface.h"
4-
#include "geometrycentral/surface/vector_heat_method.h"
54
#include "geometrycentral/surface/manifold_surface_mesh.h"
5+
#include "geometrycentral/surface/vector_heat_method.h"
66

77
namespace geometrycentral {
88
namespace surface {
99

1010
// Find a center of a collection of points at vertices
11-
SurfacePoint findCenter(ManifoldSurfaceMesh& mesh, IntrinsicGeometryInterface& geom, const std::vector<Vertex>& vertexPts, int p = 2);
11+
SurfacePoint findCenter(ManifoldSurfaceMesh& mesh, IntrinsicGeometryInterface& geom,
12+
const std::vector<Vertex>& vertexPts, int p = 2,
13+
LogMapStrategy strategy = LogMapStrategy::VectorHeat);
1214
SurfacePoint findCenter(ManifoldSurfaceMesh& mesh, IntrinsicGeometryInterface& geom, VectorHeatMethodSolver& solver,
13-
const std::vector<Vertex>& vertexPts, int p = 2);
15+
const std::vector<Vertex>& vertexPts, int p = 2,
16+
LogMapStrategy strategy = LogMapStrategy::VectorHeat);
1417

1518
// Find a center of distribution
16-
SurfacePoint findCenter(ManifoldSurfaceMesh& mesh, IntrinsicGeometryInterface& geom, const VertexData<double>& distribution, int p = 2);
19+
SurfacePoint findCenter(ManifoldSurfaceMesh& mesh, IntrinsicGeometryInterface& geom,
20+
const VertexData<double>& distribution, int p = 2,
21+
LogMapStrategy strategy = LogMapStrategy::VectorHeat);
1722
SurfacePoint findCenter(ManifoldSurfaceMesh& mesh, IntrinsicGeometryInterface& geom, VectorHeatMethodSolver& solver,
18-
const VertexData<double>& distribution, int p = 2);
23+
const VertexData<double>& distribution, int p = 2,
24+
LogMapStrategy strategy = LogMapStrategy::VectorHeat);
1925

2026
} // namespace surface
2127
} // namespace geometrycentral

include/geometrycentral/surface/vector_heat_method.h

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ namespace surface {
1919

2020
// Stateful class. Allows efficient repeated solves
2121

22+
enum class LogMapStrategy {
23+
VectorHeat, // the logmap proposed in the original Vector Heat Method paper
24+
AffineLocal, // the logmap from the Affine Heat Method, allows fast prefactoring for repeated solves, more accurate
25+
// near source
26+
AffineAdaptive, // the logmap from the Affine Heat Method, no prefactoring for repeated solves, but most accurate
27+
};
28+
2229
class VectorHeatMethodSolver {
2330

2431
public:
@@ -36,10 +43,9 @@ class VectorHeatMethodSolver {
3643
VertexData<Vector2> transportTangentVectors(const std::vector<std::tuple<Vertex, Vector2>>& sources);
3744
VertexData<Vector2> transportTangentVectors(const std::vector<std::tuple<SurfacePoint, Vector2>>& sources);
3845

39-
4046
// === The Logarithmic map
41-
VertexData<Vector2> computeLogMap(const Vertex& sourceVert, double vertexDistanceShift = 0.);
42-
VertexData<Vector2> computeLogMap(const SurfacePoint& sourceP);
47+
VertexData<Vector2> computeLogMap(const Vertex& sourceVert, LogMapStrategy strategy = LogMapStrategy::VectorHeat);
48+
VertexData<Vector2> computeLogMap(const SurfacePoint& sourceP, LogMapStrategy strategy = LogMapStrategy::VectorHeat);
4349

4450
// === Options and parameters
4551
const double tCoef; // the time parameter used for heat flow, measured as time = tCoef * mean_edge_length^2
@@ -65,14 +71,21 @@ class VectorHeatMethodSolver {
6571
// Solvers
6672
std::unique_ptr<PositiveDefiniteSolver<double>> scalarHeatSolver;
6773
std::unique_ptr<LinearSolver<std::complex<double>>> vectorHeatSolver;
74+
std::unique_ptr<LinearSolver<double>> affineHeatSolver;
6875
std::unique_ptr<PositiveDefiniteSolver<double>> poissonSolver;
6976
SparseMatrix<double> massMat;
7077

7178
// Helpers
7279
void ensureHaveScalarHeatSolver();
7380
void ensureHaveVectorHeatSolver();
81+
void ensureHaveAffineHeatSolver();
7482
void ensureHavePoissonSolver();
7583

84+
// Log map approaches, see documentation on the strategy enum
85+
VertexData<Vector2> computeLogMap_VectorHeat(const Vertex& sourceVert, double vertexDistanceShift = 0.);
86+
VertexData<Vector2> computeLogMap_AffineLocal(const Vertex& sourceVert);
87+
VertexData<Vector2> computeLogMap_AffineAdaptive(const Vertex& sourceVert);
88+
7689
void addVertexOutwardBall(Vertex v, Vector<std::complex<double>>& distGradRHS);
7790
};
7891

src/surface/surface_centers.cpp

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,37 +10,39 @@ namespace geometrycentral {
1010
namespace surface {
1111

1212

13-
SurfacePoint findCenter(ManifoldSurfaceMesh& mesh, IntrinsicGeometryInterface& geom, const std::vector<Vertex>& vertexPts, int p) {
13+
SurfacePoint findCenter(ManifoldSurfaceMesh& mesh, IntrinsicGeometryInterface& geom,
14+
const std::vector<Vertex>& vertexPts, int p, LogMapStrategy strategy) {
1415
VertexData<double> dist(geom.mesh, 0.);
1516
for (Vertex v : vertexPts) {
1617
dist[v] += 1.;
1718
}
1819

1920
// Forward to the general version
20-
return findCenter(mesh, geom, dist, p);
21+
return findCenter(mesh, geom, dist, p, strategy);
2122
}
2223

2324
SurfacePoint findCenter(ManifoldSurfaceMesh& mesh, IntrinsicGeometryInterface& geom, VectorHeatMethodSolver& solver,
24-
const std::vector<Vertex>& vertexPts, int p) {
25+
const std::vector<Vertex>& vertexPts, int p, LogMapStrategy strategy) {
2526
VertexData<double> dist(geom.mesh, 0.);
2627
for (Vertex v : vertexPts) {
2728
dist[v] += 1.;
2829
}
2930

3031
// Forward to the general version
31-
return findCenter(mesh, geom, solver, dist, p);
32+
return findCenter(mesh, geom, solver, dist, p, strategy);
3233
}
3334

34-
SurfacePoint findCenter(ManifoldSurfaceMesh& mesh, IntrinsicGeometryInterface& geom, const VertexData<double>& distribution, int p) {
35+
SurfacePoint findCenter(ManifoldSurfaceMesh& mesh, IntrinsicGeometryInterface& geom,
36+
const VertexData<double>& distribution, int p, LogMapStrategy strategy) {
3537
VectorHeatMethodSolver solver(geom);
3638

3739
// Forward to the general version
38-
return findCenter(mesh, geom, solver, distribution, p);
40+
return findCenter(mesh, geom, solver, distribution, p, strategy);
3941
}
4042

4143

4244
SurfacePoint findCenter(ManifoldSurfaceMesh& mesh, IntrinsicGeometryInterface& geom, VectorHeatMethodSolver& solver,
43-
const VertexData<double>& distribution, int p) {
45+
const VertexData<double>& distribution, int p, LogMapStrategy strategy) {
4446

4547
if (p != 1 && p != 2) {
4648
throw std::logic_error("only p=1 or p=2 is supported");
@@ -87,7 +89,7 @@ SurfacePoint findCenter(ManifoldSurfaceMesh& mesh, IntrinsicGeometryInterface& g
8789
auto evalEnergyAndUpdate = [&](SurfacePoint aboutPoint) -> std::tuple<double, Vector2> {
8890
// Compute the current log map
8991
// Solve at the face point
90-
VertexData<Vector2> logmap = solver.computeLogMap(aboutPoint);
92+
VertexData<Vector2> logmap = solver.computeLogMap(aboutPoint, strategy);
9193

9294
// Evaluate energy and update step
9395
double thisEnergy = 0.;

0 commit comments

Comments
 (0)