Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ depending on nothing but `nalgebra-sparse`, joining the ladder only where
| `gramian` | inner-product / metric structure | `Gramian` (non-degenerate symmetric, any signature), `Metric` (the pseudo-Riemannian metric tensor, any signature; Riemannian is $q = 0$), `CausalType` |
| `coorder` | typed affine coordinates | `Coords<S>` (coordinates tagged by their space), `affine::AffineTransform` |
| `exterior` | the exterior algebra $Lambda^k$ | `ExteriorElement<V>`, `Variance` (`Covariant`/`Contravariant`), `exterior_power`, wedge, interior product, musicals, Hodge star, `pullback`/`pushforward` of a value along a linear map |
| `simplicial` | the simplicial manifold $M_h$ | `topology::` (`Complex`, `Skeleton`, `SimplexRef`, the `role::` witnesses `Cell`/`Facet`/..., boundary operators, `orientation::Orientation`, `ordering::CellOrdering`, `refine::Subdivision`), `atlas::` (`Chart`, `MeshPoint`, `Transition`, `Bary`/`Local`, `SimplexQuadRule`), `geometry::` (`MeshLengthsSq` the intrinsic Regge primitive the engine consumes, `MeshCoords` and `CellGramians` the sources that convert into it) and `linalg::` (the dense/sparse nalgebra aliases and `CooMatrixExt` block-matrix builder every crate above it reuses) |
| `simplicial` | the simplicial manifold $M_h$ | `topology::` (`Complex`, `Skeleton`, `SimplexRef`, the `role::` witnesses `Cell`/`Facet`/..., boundary operators, `orientation::Orientation`, `ordering::CellOrdering`, `refine::Subdivision`), `atlas::` (`Chart`, `MeshPoint`, `Transition`, `Bary`/`Local`, `SimplexQuadRule`), `geometry::` (`MeshLengthsSq` the intrinsic Regge primitive the engine consumes, `MeshCoords` and `CellGramians` the sources that convert into it, `connection::Transport` the Levi-Civita connection and the hinge deficit angle) and `linalg::` (the dense/sparse nalgebra aliases and `CooMatrixExt` block-matrix builder every crate above it reuses) |
| `glatt` | the continuum manifold $M$ | `Parametrization` (forward map $phi$, derived nearest-point chart, `sphere`/`ball`/`torus`/`graph`), `field::CoordField<V, S>` (analytic data *on* $M$: `DiffFormClosure`, ...) |
| `derham` | discrete differential forms | `Cochain`, `section::Section<V>` (sections over the simplicial manifold) with the `Pullback` bridge (`pullback_on`/`pullback_through`) and `Sampler`, `interpolate::` (`WhitneyForm`, `WhitneyInterpolant`), `project::derham_map` |
| `iterative` | matrix-free iterative solving | one object, an approximate inverse, reused as solver, preconditioner or smoother: stationary iteration, `Jacobi`, preconditioned `CG`, `MINRES` (symmetric indefinite), block-diagonal preconditioner; backend is `nalgebra-sparse` alone, no faer |
Expand Down Expand Up @@ -191,6 +191,20 @@ and passes tests.
pinning a metric accessor to the `Cell` witness would conflate *has a metric*
(all simplices) with *carries a frame* (cells only).

This is also what makes the manifold have a **connection** at all. Two cells
sharing a facet read that facet's metric off the *same* edge lengths, so their
restrictions agree, and there is a unique isometry of their frames fixing the
facet and putting the two on opposite sides of it — the unfolding,
`MeshLengthsSq::transport`. A bag of unrelated `CellGramians` would admit no
such gluing, so the Levi-Civita connection is a *derived* quantity of the
Regge primitive and never an input. Being piecewise flat, the connection has
no degrees of freedom inside a cell and none across a contractible dual loop:
all of it lives on the facets, and all of its curvature on the codimension-2
hinges, where the holonomy is a rotation by the deficit angle
(`Ridge::fan`, `deficit_angle`, `holonomy`). Curvature is a 2-form, hence
measured against area, which is *why* the hinges are codimension 2 — vertices
in 2D, edges in 3D, triangles in 4D, one mechanism and no special case.

A **point of the simplicial manifold** is therefore `MeshPoint` — a `Chart`
plus barycentric coordinates — never a global coordinate, which on a Regge
manifold does not exist. A **field** is a `Section<V>`: a section of the
Expand Down
68 changes: 31 additions & 37 deletions crates/simplicial/src/geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ pub mod coord;
pub mod metric;
pub mod refine;

use crate::{Dim, atlas::refsimp_vol, topology::complex::Complex};
use crate::{
Dim,
atlas::refsimp_vol,
topology::{complex::Complex, role::roles},
};

use gramian::Metric;

Expand All @@ -27,25 +31,20 @@ pub fn cell_volume(metric: &Metric) -> f64 {
}

/// Discrete Gaussian curvature at every vertex of a 2-dimensional simplicial
/// manifold, by the angle defect: $K(v) = (2 pi - sum_(f ni v) theta_f (v)) \/
/// A(v)$ at an interior vertex, or $(pi - sum_f theta_f (v)) \/ A(v)$ at a
/// boundary one -- the standard convention when a mesh has a rim, folding the
/// boundary's own geodesic curvature into $K$ rather than tracking it apart.
/// $A(v)$ is the barycentric lumped area $sum_(K ni v) "vol"(K) \/ 3$, the
/// standard mass-lumping convention.
/// manifold: the Regge deficit angle divided by the area it is spread over,
/// $K(v) = epsilon_v \/ A(v)$.
///
/// Intrinsic: reads the Regge edge lengths, not an embedding, since
/// [`Gramian::vertex_angle`](gramian::Gramian::vertex_angle) needs no
/// coordinates -- a Regge manifold given only as [`MeshLengthsSq`] has a
/// Gaussian curvature exactly as well as an embedded one, which is why the
/// primitive is what this consumes. This
/// Regge's curvature, concentrated at the codimension-2 hinges; in 2D the
/// hinges are vertices, which is the one case implemented here. Generalizing
/// to an $(n-2)$-dimensional hinge of an $n$-manifold needs dihedral angles
/// between codimension-1 facets, not corner angles between edges, and this
/// crate does not yet carry that computation -- fixed at 2D for the same
/// reason [`crate::mesher::sphere`] is: the concept itself, not a shortcut, is
/// what is 2-dimensional here.
/// Nothing here is 2-dimensional except the packaging. The deficit angle
/// ([`MeshLengthsSq::deficit_angle`]) is Regge curvature at a hinge in any
/// dimension, and a vertex is what a hinge is when $n = 2$; what this adds is
/// the density, $A(v) = sum_(K in v) vol(K) \/ 3$, the barycentric lumped area
/// under the standard mass-lumping convention. Gaussian curvature is a
/// *scalar field*, so it needs that division, and only in 2D is the deficit's
/// hinge a point for the density to sit at.
///
/// Intrinsic: reads the Regge edge lengths, not an embedding -- a Regge
/// manifold given only as [`MeshLengthsSq`] has a Gaussian curvature exactly as
/// well as an embedded one, which is why the primitive is what this consumes.
///
/// Exact, not an approximation of the smooth quantity: this is what
/// Gauss-Bonnet defines discrete curvature to be, with
Expand All @@ -58,30 +57,25 @@ pub fn vertex_gaussian_curvature(topology: &Complex, geometry: &MeshLengthsSq) -
"Gaussian curvature is a 2D-surface quantity."
);
let nvertices = topology.skeleton_raw(Dim::ZERO).len();
let boundary: std::collections::HashSet<usize> =
topology.boundary_vertices().into_iter().collect();

let mut angle_sum = vec![0.0; nvertices];
let mut areas = vec![0.0; nvertices];
for cell in topology.cells().handle_iter() {
let metric = geometry.cell_metric(cell);
let vol = cell_volume(&metric);
let verts = &cell.simplex().vertices;
for m in 0..3 {
let (a, b) = ((m + 1) % 3, (m + 2) % 3);
angle_sum[verts[m]] += metric.vector_gramian().vertex_angle(m, a, b);
areas[verts[m]] += vol / 3.0;
let vol = cell_volume(&geometry.cell_metric(cell));
for &vertex in &cell.simplex().vertices {
areas[vertex] += vol / 3.0;
}
}

(0..nvertices)
.map(|v| {
let target = if boundary.contains(&v) {
std::f64::consts::PI
} else {
std::f64::consts::TAU
};
(target - angle_sum[v]) / areas[v]
let hinges = topology
.role_skeleton::<roles::Ridge>()
.expect("a 2-complex has ridges");
hinges
.handle_iter()
.map(|hinge| {
let deficit = geometry
.deficit_angle(hinge)
.expect("a Riemannian surface has dihedral angles");
deficit / areas[hinge.kidx()]
})
.collect()
}
Expand Down
2 changes: 2 additions & 0 deletions crates/simplicial/src/geometry/metric.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
pub mod connection;
pub mod geometry;
pub mod mesh;
pub mod simplex;

pub use connection::Transport;
pub use geometry::CellGramians;

pub type EdgeIdx = usize;
Loading