SheehyRipsSimplexStream

org.appliedtopology.tda4j.streams.SheehyRipsSimplexStream
See theSheehyRipsSimplexStream companion object
class SheehyRipsSimplexStream(val ambientMetricSpace: FiniteMetricSpace[Int], val permutation: GreedyPermutation, val epsilon: Double, keepCriterion: PartialFunction[Simplex[Int], Boolean] = ..., maxFiltrationValue: Option[Double] = ..., parallelFiltrationValue: Boolean = ...) extends RipserCofaceSimplexStream

The linear-size approximate/sparse Vietoris-Rips filtration of Cavanna, Jahanseir & Sheehy, "A Geometric Perspective on Sparse Filtrations" (arXiv:1506.03797, CCCG 2015) -- checked directly against the paper's own Definitions/Algorithm 1-4 and Lemma 1/Corollary 2/Theorem 5 (.claude/WORKLOG-sheehy-rips.md has the fetched PDF pages and the derivation). This is the GREEDY-PERMUTATION reformulation of Sheehy's original net-tree-based construction (D.R. Sheehy, "Linear-Size Approximations to the Vietoris-Rips Filtration," Discrete & Computational Geometry 49(4), 2013, arXiv:1203.6786) -- simpler to implement correctly and to verify, at the cost of the two papers' own epsilon parameters NOT being directly comparable (Sheehy 2013's approximation factor is 1/(1-2*epsilon); this class's, following CJS 2015, is (1+epsilon)).

'''What this buys you''': a filtration whose persistence barcode is a genuine (1+epsilon)-multiplicative approximation (Theorem 5) to the plain Vietoris-Rips barcode, built from a complex whose SIZE is linear in n (CJS 2015 Lemma 6/7/Theorem 9/10) for point sets of bounded doubling dimension -- dramatically fewer simplices to reduce than plain VR at the same scale range, in exchange for a controlled, quantified loss of precision.

'''What this class does NOT do''': the paper's own O(n log n) algorithm (Section 5, Algorithms 1-4) builds the edges of the sparse filtration directly from the greedy permutation's neighbor structure, touching only kappa^O(d) candidates per point. This class instead computes every pairwise edgeBirth directly (O(n^2), like plain VietorisRips/RipserCofaceSimplexStream's own default candidate enumeration) and lets RipserCofaceSimplexStream's ordinary combinatorial coface generation do the rest. The payoff here is a SMALLER complex to reduce, not a faster one to build -- see the class doc's own honest framing on this point in every other "not the fastest construction" case in this codebase (Cech, Witness, alpha).

==The construction==

Given a greedy permutation (streams.GreedyPermutation, computed by LandmarkSelector.maxmin run to numLandmarks = ambientMetricSpace.size) with insertion radii lambda_p (lambda of the very first point is Double.PositiveInfinity by convention -- it must never be pruned away, since it anchors the whole construction), and a sparsity parameter epsilon in (0,1):

  • each point p's ball radius at scale alpha is r_p(alpha) := min(alpha, lambda_p*(1+epsilon)/epsilon) (CJS 2015 Section 3) -- grows with alpha until it saturates;
  • p's ball becomes and stays EMPTY once alpha > lambda_p*(1+epsilon)^2/epsilon (vanish(p) below) -- no new simplex may use p past that scale, though the filtration itself never removes anything already present (S^alpha := union_{delta = 1) is born at max over its own edges' birth times, PROVIDED that value is <= min_{p in sigma} vanish(p) -- otherwise it never appears at all (CJS 2015 Section 5.3, "SimplexBirthTime" -- the max/min intersection is valid because balls are convex and pairwise-intersecting convex sets have a common intersection, the same Helly-type fact that makes plain Rips itself a flag/nerve complex).

'''A real gap in CJS 2015's own Algorithm 3, verified, not just suspected''': that algorithm computes an edge's birth from only the TWO endpoints' own thresholds, with no check against vanish -- but Section 5.3's own SimplexBirthTime definition (the general k-simplex rule above) requires exactly that check, and an edge is simply its k=1 case, not a special one. Two independent counterexamples confirm this is a real gap in the published algorithm, not an artifact of skipping its neighbor-search prefilter: epsilon=1, lambda_p=1, lambda_q=10, d=10 (raw formula gives 8, but p vanishes at 4); and, checked directly against the paper's OWN Lemma 6/7 neighbor bound (kappa = (epsilon^2+3*epsilon+2)/epsilon, d(p_i,p_j) <= kappa*2^ceil(lg lambda_i) puts p_j in p_i's own candidate neighbor list), epsilon=1, lambda_p=1.1, lambda_q=10, d=10 -- this pair passes the paper's own restricted neighbor search AND its Algorithm 3 (returning 7.8), while p vanishes at 4.4. Whether the paper's full O(n log n) pipeline compensates for this some other way was not checked; only the gap itself was verified. edgeBirth here applies the vanish clamp Section 5.3 describes to every edge, not just higher simplices.

'''Units''': every OTHER stream in this codebase records filtration values in "diameter" units (an edge's own value is the raw ambient distance, not half of it) -- but CJS 2015's own alpha is a RADIUS parameter (R_alpha := {J : max d(p,q) <= 2*alpha}; their own Algorithm 3 literally returns d/2 in its first branch). Every output this class reports -- edgeBirth's return value and vanish -- is therefore the paper's own value DOUBLED, never lambda itself (an ordinary ambient distance already in diameter-comparable units, entering the formulas unchanged). After doubling, the first (unsparsified) branch collapses to exactly d, matching plain VR's own edge value -- a useful internal sanity check, exercised directly by SheehyRipsStreamSpec's "reduces to plain VR" fixture.

'''maxFiltrationValue''' is always clamped to maxFiniteFiltrationValue -- the largest FINITE simplex birth this construction can ever produce (an honest, data-dependent bound: any finite simplex's value is a max over its own edges, so the largest finite EDGE birth bounds every finite simplex) -- REGARDLESS of what the caller passes (None resolves to exactly that bound; an explicit Some(x) is min(x, maxFiniteFiltrationValue)). This is deliberately NOT metricSpace.minimumEnclosingRadius (this is not a cone construction -- an edge to the anchor point can be unboundedly large, so no point has a finite max distance to every other point), and the clamp is unconditional rather than only a None-default specifically so that an explicit Some(Double.PositiveInfinity) -- how every other stream in this codebase spells "untruncated" -- stays safe here too: keptByThresholdAndCriterion's own <= comparison is plain IEEE-754 Double comparison, under which Double.PositiveInfinity <= Double.PositiveInfinity is true, so an UNCLAMPED literal infinite threshold would silently readmit every simplex this construction is supposed to exclude forever.

'''Not a diameter-only construction''': unlike plain VR (and like the general, non-flag WitnessCofaceSimplexStream and DtmRipsSimplexStream), a simplex's value here is not simply the maximum ambient pairwise distance among its vertices, so matlab.TDA4j refuses engine=ripser for this complex (both Ripser engines' incremental insertionDiameter/apparent-pairs machinery assume the filtration functional literally IS MaximumDistanceFiltrationValue on the metric space handed to them). naive/chunks/cohomology all consume it like any other CofaceSimplexStream[Int, Double]; chunks is cross-validated fresh against naive (SheehyRipsStreamSpec), not assumed to carry over.

Attributes

Companion
object
Experimental
true
Graph
Supertypes
trait CofaceSimplexStream[Int, Double]
trait StratifiedSimplexStream[Int, Double]
trait StratifiedCellStream[Simplex[Int], Double]
trait CellStream[Simplex[Int], Double]
trait IterableOnce[Simplex[Int]]
trait Filtration[Simplex[Int], Double]
trait Filterable[Double]
class Object
trait Matchable
class Any
Show all

Members list

Value members

Inherited methods

override def iterateDimension: PartialFunction[Int, Iterator[Simplex[Int]]]

Contract .iterator below relies on: the domain must be contiguous starting at 0 -- defined for 0, 1, ..., k for some k (or empty, or all of the non-negative integers), never with a gap. .iterator stops at the first dimension this is undefined for, so a non-contiguous domain (defined at d but not at d - 1) would silently truncate iteration instead of skipping the gap. Every implementation in this codebase already satisfies this (a simplicial complex can't have a d-simplex without its (d-1)-dimensional faces, so "no cells at d" implies "no cells at any dimension beyond d" too); a new implementation must preserve it.

Contract .iterator below relies on: the domain must be contiguous starting at 0 -- defined for 0, 1, ..., k for some k (or empty, or all of the non-negative integers), never with a gap. .iterator stops at the first dimension this is undefined for, so a non-contiguous domain (defined at d but not at d - 1) would silently truncate iteration instead of skipping the gap. Every implementation in this codebase already satisfies this (a simplicial complex can't have a d-simplex without its (d-1)-dimensional faces, so "no cells at d" implies "no cells at any dimension beyond d" too); a new implementation must preserve it.

Attributes

Definition Classes
Inherited from:
RipserCofaceSimplexStream
override def iterator: Iterator[Simplex[Int]]

Dimension-major: all of dimension d before any of dimension d + 1.

Dimension-major: all of dimension d before any of dimension d + 1.

MUST NOT be implemented as Iterator.from(0).filter(iterateDimension.isDefinedAt)....fold(...) (a real, confirmed bug this replaced -- see .claude/WORKLOG-cohomology.md): Iterator.filter on an infinite source can never prove "no more matches ahead", so once past the last dimension iterateDimension is defined for, it spins forever searching for a d that will never come -- and Int silently wrapping from Int.MaxValue to Int.MinValue after ~2^31 iterations can eventually feed a huge negative d straight to iterateDimension instead, surfacing as a BinomialCoefficient range exception rather than a hang. .takeWhile instead stops at the first d this is undefined for and never asks about any d beyond it, relying on exactly the contiguous-domain contract documented on iterateDimension above.

Attributes

Definition Classes
StratifiedCellStream -> IterableOnce
Inherited from:
StratifiedCellStream
def knownSize: Int

The number of elements in this collection, if it can be cheaply computed, -1 otherwise. Cheaply usually means: Not requiring a collection traversal.

The number of elements in this collection, if it can be cheaply computed, -1 otherwise. Cheaply usually means: Not requiring a collection traversal.

Attributes

Inherited from:
IterableOnce
def stepper[S <: Stepper[_]](implicit shape: StepperShape[Simplex[Int], S]): S

Returns a scala.collection.Stepper for the elements of this collection.

Returns a scala.collection.Stepper for the elements of this collection.

The Stepper enables creating a Java stream to operate on the collection, see scala.jdk.StreamConverters. For collections holding primitive values, the Stepper can be used as an iterator which doesn't box the elements.

The implicit scala.collection.StepperShape parameter defines the resulting Stepper type according to the element type of this collection.

  • For collections of Int, Short, Byte or Char, an scala.collection.IntStepper is returned
  • For collections of Double or Float, a scala.collection.DoubleStepper is returned
  • For collections of Long a scala.collection.LongStepper is returned
  • For any other element type, an scala.collection.AnyStepper is returned

Note that this method is overridden in subclasses and the return type is refined to S with EfficientSplit, for example scala.collection.IndexedSeqOps.stepper. For Steppers marked with scala.collection.Stepper.EfficientSplit, the converters in scala.jdk.StreamConverters allow creating parallel streams, whereas bare Steppers can be converted only to sequential streams.

Type parameters

S

the type of the returned Stepper, determined by the implicit StepperShape

Attributes

Inherited from:
IterableOnce

Concrete fields

val epsilon: Double

Inherited fields

var currentDimension: Int

Attributes

Inherited from:
EnumeratingCofaceSimplexStream
var currentDimensionCache: Queue[Simplex[Int]]

Attributes

Inherited from:
EnumeratingCofaceSimplexStream
lazy val edges: Iterable[Simplex[Int]]

Attributes

Inherited from:
EnumeratingCofaceSimplexStream
override val filtrationOrdering: Ordering[Simplex[Int]]

Filtration value, reversed (so smaller-under-this-ordering means YOUNGER, matching SimplexStream's own established convention), then dimension, then COLEXICOGRAPHIC order on the vertex set (via simplexIndexing's own combinatorial-number-system index) -- the "lexicographically refined" tie-break Ripser's own apparent-pairs machinery (Definition 3.2/Proposition 3.9, see RipserCohomologyContext) is defined in terms of, so using it here keeps this stream's ordering consistent with every other Ripser-flavored piece of this codebase, not just internally self-consistent -- deliberately not the plain lexicographic tie-break FilteredSimplexOrdering uses.

Filtration value, reversed (so smaller-under-this-ordering means YOUNGER, matching SimplexStream's own established convention), then dimension, then COLEXICOGRAPHIC order on the vertex set (via simplexIndexing's own combinatorial-number-system index) -- the "lexicographically refined" tie-break Ripser's own apparent-pairs machinery (Definition 3.2/Proposition 3.9, see RipserCohomologyContext) is defined in terms of, so using it here keeps this stream's ordering consistent with every other Ripser-flavored piece of this codebase, not just internally self-consistent -- deliberately not the plain lexicographic tie-break FilteredSimplexOrdering uses.

Fixes a real, previously-confirmed bug (.claude/WORKLOG-cohomology.md): a bare Ordering.by(filtrationValue) has no tie-break at all, so two DIFFERENT simplices tied at the same filtration value compare as equal -- not a total order. This happens by construction on any Vietoris-Rips complex with a triangle, since a triangle's filtration value always equals that of its own longest edge; CellularHomologyContext bakes a stream's filtrationOrdering into Chain.reduceBy's SortedMap, so two cells that compare equal collide as a single map key and the reduction silently garbles pairings for that complex.

iterateDimension sorts each dimension's bucket by filtrationOrdering.reverse -- deliberately .reverse on this SAME Ordering object, not an independently-built "oldest first" comparator: two individually-valid orderings that disagree on tie-break direction let a coface sort before its own tied facet, corrupting Chain.reduceBy's pivot table the same way the no-tie-break bug did. A stream's iteration order and its filtrationOrdering (pivot order) must be THE SAME total order, one the consistent reverse of the other.

Attributes

Inherited from:
EnumeratingCofaceSimplexStream
override val filtrationValue: PartialFunction[Simplex[Int], Double]

Attributes

Inherited from:
EnumeratingCofaceSimplexStream
val largest: Double

Attributes

Inherited from:
DoubleFiltration
var lastDimensionCache: IndexedSeq[Simplex[Int]]

Attributes

Inherited from:
EnumeratingCofaceSimplexStream

Attributes

Inherited from:
EnumeratingCofaceSimplexStream

Attributes

Inherited from:
EnumeratingCofaceSimplexStream
val smallest: Double

Attributes

Inherited from:
DoubleFiltration