energies
triwarp.energies
Energies over a mesh: the scalar regularizers, and the operators the solvers minimize.
The line between this module and triwarp.laplacian is order: laplacian
builds the first-order operators -- the cotangent stiffness matrix, its mass matrix and the
intrinsic repairs that keep them finite -- and everything here is assembled out of those.
Four families, and the first is the only one that returns a number rather than a matrix:
-
Scalar mesh regularizers.
edge_length_loss,normal_consistency_lossandlaplacian_smoothing_lossare the priors a mesh optimization adds to a data term -- one per edge length, one per dihedral angle and one per vertex Laplacian residual, each reduced to a singlefloat. They are here rather than intriwarp.metrics, which hosts the data terms they pair with, because their machinery is this module's:laplacian_smoothing_loss's two cotangent variants consumecotmatrixandmass_matrix_entries, which is exactly the import set the quadratic forms below use. -
Smoothness energies over vertices.
k_harmonicis the integratedk-harmonic form --k = 1is Dirichlet,k = 2the biharmonic operator behind smooth interpolation -- and it distorts a field near the boundary, because clamping a biharmonic solve there is not a natural condition.hessian_energyandcurved_hessian_energyare the alternatives that do not: both integrate a squared Hessian instead, so linear (respectively, locally linear) functions sit exactly in the null space, boundary or not. - The edge-based Crouzeix-Raviart pair.
crouzeix_raviart_cotmatrixandcrouzeix_raviart_massmatrixput the degrees of freedom on edge midpoints rather than vertices, which is the nonconforming-FEM discretizationcurved_hessian_energyis built on. They are the siblings ofcotmatrixandmass_matrix-- a reader arriving from libigl, where all four sit together, should start there. - The LSCM operator.
lscm_hessianis the(2n, 2n)form behind the least-squares conformal map, andvector_area_matrixis the boundary term that couples its two coordinate blocks.lscmis the solve; these are what it minimizes.
Every operator is assembled in float64 in a single bsr_from_triplets, because the
conjugate-gradient solves they feed run in float64 for determinism and a float32
intermediate would be the accuracy floor. The three scalar regularizers are float32 throughout:
they are read by a human or by an optimizer's stopping rule, not solved with.
edge_length_loss
edge_length_loss(
vertices: array[vec3],
faces: array[int32],
target_length: float = 0.0,
) -> float
Mean squared deviation of the undirected edge lengths from a resting length.
The edge regularizer of the deformation losses: mean((||e|| - L0)^2) over the unique
undirected edges, so an interior edge counts once rather than twice. At the default
target_length = 0.0 it is the mean squared edge length, which is what a shrinking prior
wants; a positive value pulls the mesh toward uniform edges of that size instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
target_length
|
float
|
Resting edge length |
0.0
|
Returns:
| Type | Description |
|---|---|
float
|
The mean, as a host scalar. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
See Also
normal_consistency_loss
laplacian_smoothing_loss
edges_unique_length
The per-edge lengths this reduces, if the distribution rather than the mean is wanted.
triwarp.metrics
The data terms these regularizers are added to, and the differentiable Chamfer family.
Notes
Matches pytorch3d.loss.mesh_edge_loss. Its per-mesh 1 / E weighting collapses to a
plain mean for one mesh, which is triwarp's only case, so there is no batch weighting to port.
Source code in triwarp/energies.py
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |
normal_consistency_loss
normal_consistency_loss(
vertices: array[vec3], faces: array[int32]
) -> float
Mean 1 - cos(theta) over the pairs of faces sharing an edge.
The dihedral regularizer of the deformation losses, and the one that penalizes a fold: it is
0 for a flat pair, 1 at a right angle and 2 for a face doubled back on itself. Read
it against edge_length_loss, which constrains the
sizes; this one constrains the orientations and says nothing about scale.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
Returns:
| Type | Description |
|---|---|
float
|
The mean, as a host scalar. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
See Also
edge_length_loss
laplacian_smoothing_loss
face_adjacency_angles
The per-pair dihedral angles this reduces.
Notes
Matches pytorch3d.loss.mesh_normal_consistency on edge-manifold input. The restriction
is real and is not a tolerance -- the reference enumerates every pair of faces sharing an
edge, so an edge with k incident faces contributes C(k, 2) terms where
face_adjacency_angles reports one pair per
adjacency. The two coincide exactly wherever every edge has at most two faces.
Source code in triwarp/energies.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
laplacian_smoothing_loss
laplacian_smoothing_loss(
vertices: array[vec3],
faces: array[int32],
method: Literal[
"uniform", "cot", "cotcurv"
] = "uniform",
) -> float
Mean magnitude of a Laplacian residual per vertex, under one of three normalizations.
The smoothness regularizer of the deformation losses. The three methods are three different quantities, not one with a tuning knob, and they differ by roughly an order of magnitude:
"uniform":|| (A v)_i - v_i ||withAthe row-normalized 1-ring average (laplacianwithequal_weight=True) -- the umbrella residual, which is a length and therefore scales with the mesh."cot": the same residual against the cotangent-weighted neighbour average,|| (L v)_i / s_i ||withLthe cotangent stiffness matrix ands_iits off-diagonal row sum. Also a length, and the geometry-aware version of the above."cotcurv":|| (L v)_i / (6 M_ii) ||withMthe barycentric lumped mass -- the mean curvature magnitude, so it carries units of one over length and is the largest of the three on a unit-scale mesh.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
method
|
Literal['uniform', 'cot', 'cotcurv']
|
Which normalization, as above. |
'uniform'
|
Returns:
| Type | Description |
|---|---|
float
|
The mean, as a host scalar. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
See Also
edge_length_loss
normal_consistency_loss
cotmatrix
The operator the two cotangent variants are built from.
filter_laplacian
The smoother that minimizes this, rather than measuring it.
Notes
Matches pytorch3d.loss.mesh_laplacian_smoothing on all three methods. Two conventions are
inherited from it rather than chosen here, both because they are what makes the numbers
comparable at all: the
reference's cot and cotcurv read a cotangent Laplacian whose off-diagonal is twice
triwarp's half-cotangent table and whose diagonal is identically zero, which cancels out of
both ratios above -- and where a vertex's row sum is not positive its averaging is undefined,
so "cot" falls back to || v_i || there, matching the reference's norm_w = 0 branch.
Source code in triwarp/energies.py
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | |
k_harmonic
k_harmonic(
laplacian: BsrMatrix[float32],
mass: Array1dFloat | None = None,
k: int = 2,
) -> wps.BsrMatrix[wp.float32]
Integrated k-harmonic operator Q = (-L) (M^-1 (-L))^(k-1) from a Laplacian and a mass.
The quadratic form whose minimizers are k-harmonic functions: k == 1 gives the Dirichlet
energy -L (positive semi-definite for a cotmatrix-sign
Laplacian), k == 2 the biharmonic operator L M^-1 L behind
harmonic's smooth interpolation, and so on
(igl::harmonic_integrated_from_laplacian_and_mass). Like igl's, the composition is not
numerically robust for k > 2 — the entries grow as the k-th power of the inverse mesh
size — so high powers want a float64 laplacian.
Named for the operator, not the map. This used to be laplacian.harmonic_integrated,
which put a second unrelated "harmonic" in the package beside
harmonic — a map into the plane, not an operator. The
two are related (harmonic minimizes this form with the boundary pinned) but they are not
interchangeable, and neither docstring named the other.
Each power is assembled by one triplet pass over matching CSR rows —
(A M^-1 B)_ij = sum_t A_ti M_t^-1 B_tj with both operands symmetric — followed by a single
bsr_from_triplets, so the product is built without warp.sparse.bsr_mm.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
laplacian
|
BsrMatrix[float32]
|
Square 1x1-block BSR Laplacian in igl's sign convention (negative diagonal, each row
summing to zero), e.g. from |
required |
mass
|
Array1dFloat | None
|
Length- |
None
|
k
|
int
|
Harmonic power ( |
2
|
Returns:
| Type | Description |
|---|---|
BsrMatrix
|
Square |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
Source code in triwarp/energies.py
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 | |
hessian_energy
hessian_energy(
vertices: array[vec3],
faces: array[int32],
dtype: type = wp.float64,
*,
vertex_faces: tuple[array[int32], array[int32]]
| None = None,
) -> wps.BsrMatrix[wp.float64]
Hessian smoothness energy with natural boundary conditions.
The mixed-FEM quadratic form Q = H^T M^-1 H of Stein et al. 2018, Natural Boundary
Conditions for Smoothing in Geometry Processing: x' Q x integrates the squared Hessian of
the piecewise-linear field x, so minimizing it smooths without the boundary distortion
the clamped biharmonic operator
(k_harmonic at k == 2) produces —
linear functions are exactly in its null space, boundary or not. M is the Voronoi lumped
mass with boundary degrees of freedom killed, per the reference.
Rather than materializing the sparse (9 n_faces, n_vertices) stacked Hessian H, the
product is contracted analytically over its nine component pairs and assembled in one triplet
pass per vertex: Q_ij = sum_k M_k^-1 sum_{f,g ni k} A_f A_g (g_fk . g_gk)(g_fi . g_gj)
with g_fc corner c's hat-function gradient in face f. The per-vertex triplet count
is 9 * valence^2, so cost is quadratic in valence. A degenerate face contributes nothing
(igl emits NaN there).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
dtype
|
type
|
Scalar block type of the assembled matrix: |
float64
|
vertex_faces
|
tuple[array[int32], array[int32]] | None
|
Optional precomputed |
None
|
Returns:
| Type | Description |
|---|---|
BsrMatrix
|
Square |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
See Also
Notes
Matches igl::hessian_energy except on degenerate faces, which contribute nothing here and
NaN there.
Source code in triwarp/energies.py
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 | |
curved_hessian_energy
curved_hessian_energy(
vertices: array[vec3],
faces: array[int32],
dtype: type = wp.float64,
) -> wps.BsrMatrix[wp.float64]
Curved Hessian smoothness energy on the Crouzeix-Raviart discretization.
igl::curved_hessian_energy, from Stein et al. 2020, A Smoothness Energy without Boundary
Distortion for Curved Surfaces: where hessian_energy
treats the surface as locally flat, this one carries the Gaussian curvature into the operator
through a per-vertex angle-defect correction, so the energy is intrinsic to the curved surface
rather than to its triangles' planes. Constant functions are exactly in its null space.
Assembled as Q = D^T M^-1 (L + K) M^-1 D over edge-based Crouzeix-Raviart vector elements
— D the scalar-to-CR-vector gradient, M the CR vector mass, L the CR vector
Laplacian and K the curvature correction — but contracted per face in one pass: L + K
couples edges within a face only, so each face emits its own 6x6 block sandwiched between its
edges' gradient rows (a fixed 144 triplets per face), and no intermediate (2 n_edges, ...)
matrix or sparse product exists. Requires an edge-manifold mesh, like the igl original (which
asserts it) — raises ValueError otherwise; a degenerate face contributes nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
dtype
|
type
|
Scalar block type of the assembled matrix: |
float64
|
Returns:
| Type | Description |
|---|---|
BsrMatrix
|
Square |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the mesh is not edge-manifold (some edge is shared by more than two faces). |
RuntimeError
|
If |
Source code in triwarp/energies.py
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 | |
crouzeix_raviart_cotmatrix
crouzeix_raviart_cotmatrix(
vertices: array[vec3],
faces: array[int32],
cot_entries: Array2dFloat | None = None,
dtype: type = wp.float32,
*,
unique_edges: Array2dInt32 | None = None,
edge_map: array[int32] | None = None,
) -> wps.BsrMatrix[wp.float32]
Edge-based Crouzeix-Raviart cotangent stiffness matrix.
The nonconforming-FEM sibling of cotmatrix: degrees of
freedom live on edge midpoints, so the matrix is (n_edges, n_edges) and each face couples
its three edges pairwise with minus four times the half-cotangent at their shared corner
(positive diagonal — the igl sign convention for this operator, opposite to cotmatrix's).
Rows follow edges_unique's edge numbering. Requires an
edge-manifold mesh, like the igl original (which asserts it) — raises ValueError
otherwise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
cot_entries
|
Array2dFloat | None
|
Optional precomputed |
None
|
dtype
|
type
|
Scalar block type of the assembled matrix: |
float32
|
unique_edges
|
Array2dInt32 | None
|
Optional precomputed edge numbering from
|
None
|
edge_map
|
Array2dInt32 | None
|
Optional precomputed edge numbering from
|
None
|
Returns:
| Type | Description |
|---|---|
BsrMatrix
|
Square |
Raises:
| Type | Description |
|---|---|
ValueError
|
If exactly one of |
RuntimeError
|
If |
Notes
Matches igl::crouzeix_raviart_cotmatrix up to the edge numbering, which follows
edges_unique rather than igl::unique_edge_map.
Source code in triwarp/energies.py
676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 | |
crouzeix_raviart_massmatrix
crouzeix_raviart_massmatrix(
vertices: array[vec3],
faces: array[int32],
dtype: type = wp.float32,
*,
unique_edges: Array2dInt32 | None = None,
edge_map: array[int32] | None = None,
) -> wps.BsrMatrix[wp.float32]
Edge-based Crouzeix-Raviart mass matrix.
Diagonal (n_edges, n_edges): each face donates a third of its area to each of its three
edges, so an interior edge's entry is a third of its two incident faces' summed area. Rows
follow edges_unique's edge numbering, the same numbering
crouzeix_raviart_cotmatrix uses.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
dtype
|
type
|
Scalar block type of the assembled matrix: |
float32
|
unique_edges
|
Array2dInt32 | None
|
Optional precomputed edge numbering from
|
None
|
edge_map
|
Array2dInt32 | None
|
Optional precomputed edge numbering from
|
None
|
Returns:
| Type | Description |
|---|---|
BsrMatrix
|
Square |
Raises:
| Type | Description |
|---|---|
ValueError
|
If exactly one of |
RuntimeError
|
If |
Notes
Matches igl::crouzeix_raviart_massmatrix up to the edge numbering, as above.
Source code in triwarp/energies.py
775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 | |
lscm_hessian
lscm_hessian(
vertices: array[vec3], faces: array[int32]
) -> wps.BsrMatrix[wp.float64]
LSCM Hessian Q = -repdiag(L, 2) - 2 A.
Assembles the (2n, 2n) symmetric operator behind the least-squares conformal map, where
L is the cotangent Laplacian cotmatrix (negative-diagonal
convention), repdiag(L, 2) is the block-diagonal [[L, 0], [0, L]], and A is the
boundary vector_area_matrix. Built natively in
float64 in a single bsr_from_triplets (the within-quadrant repdiag triplets and the
cross-quadrant -2 A triplets never collide), so it feeds the float64 conjugate-gradient
solve directly. Matches the Q returned by igl.lscm exactly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
Returns:
| Type | Description |
|---|---|
BsrMatrix
|
Square |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
See Also
Notes
Matches igl::lscm_hessian.
Source code in triwarp/energies.py
862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 | |
vector_area_matrix
vector_area_matrix(
vertices: array[vec3], faces: array[int32]
) -> wps.BsrMatrix[wp.float64]
Boundary vector-area matrix A: the signed area enclosed by the UV boundary curve.
Assembles the (2n, 2n) matrix that turns the [u; v] quadratic form into the signed area
enclosed by the boundary UV curve: for each oriented boundary edge (i, j) (from the face
winding, via oriented_boundary_edges) it adds the
cross-quadrant entries (i+n, j, -1/4), (j, i+n, -1/4), (i, j+n, +1/4),
(j+n, i, +1/4). On a closed mesh (no boundary) A is the zero matrix. Built natively in
float64 in a single bsr_from_triplets.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
Returns:
| Type | Description |
|---|---|
BsrMatrix
|
Square |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
See Also
Notes
Matches igl::vector_area_matrix.
Source code in triwarp/energies.py
943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 | |