diff --git a/src/main/scala/org/appliedtopology/tda4j/SimplicialSet.scala b/src/main/scala/org/appliedtopology/tda4j/LazySimplicialSet.scala similarity index 56% rename from src/main/scala/org/appliedtopology/tda4j/SimplicialSet.scala rename to src/main/scala/org/appliedtopology/tda4j/LazySimplicialSet.scala index a59dd3f2..5ee051e8 100644 --- a/src/main/scala/org/appliedtopology/tda4j/SimplicialSet.scala +++ b/src/main/scala/org/appliedtopology/tda4j/LazySimplicialSet.scala @@ -5,6 +5,11 @@ import org.appliedtopology.tda4j.unicode import scala.annotation.tailrec import scala.math.Fractional.Implicits.infixFractionalOps +/** + * Defining trait for being an element of some simplicial set. + * Elements keep track of their own dimension, what degeneracies (if any) have been applied, + * and what the base non-degenerate element is. + */ trait SimplicialSetElement { def base: SimplicialSetElement def dim: Int @@ -12,13 +17,29 @@ trait SimplicialSetElement { override def toString: String = unicode.unicodeSuperScript(s"∆$dim") } +/** + * Convenience function for comfortably generating one-shot implementations of [[SimplicialSetElement]]. + * + * Two `SimplicialSetElement`s generated by this are unequal unless they are the exact same object instance. + * + * @param generatorDim + * @return + */ def simplicialGenerator(generatorDim: Int): SimplicialSetElement = new SimplicialSetElement: override def base: SimplicialSetElement = this override def dim: Int = generatorDim override def degeneracies: List[Int] = List.empty -case class SimplicialWrapper[T: OrderedCell](wrapped: T) extends SimplicialSetElement { +/** + * Wrapper class to allow any data type with a defined dimension function (as witnessed by + * the typeclass [[HasDimension]] to be a base element for a [[SimplicialSetElement]]. + * + * @param wrapped + * @param hasDimension$T$0 + * @tparam T + */ +case class SimplicialWrapper[T : HasDimension](wrapped: T) extends SimplicialSetElement { override def base: SimplicialSetElement = this override def dim: Int = wrapped.dim override def degeneracies: List[Int] = List.empty @@ -26,6 +47,20 @@ case class SimplicialWrapper[T: OrderedCell](wrapped: T) extends SimplicialSetEl override def toString: String = s"wrapped[${wrapped}]" } +/** + * Primary class for representing a generic [[SimplicialSetElement]]. This case class carries references + * to its base element and a list of degeneracies. (both [[simplicialGenerator]] and [[SimplicialWrapper]] + * will specifically create elements without degeneracies and are meant to be used as base elements) + * + * If you have no reason to build something else, your simplicial set elements should probably be + * instances of this case class. + * + * Instantiation is through the companion object, where the public constructor does some normalization on + * the data the object carries. + * + * @param base + * @param degeneracies + */ case class DegenerateSimplicialSetElement private (base: SimplicialSetElement, degeneracies: List[Int]) extends SimplicialSetElement: override def dim: Int = base.dim + degeneracies.size @@ -34,6 +69,8 @@ case class DegenerateSimplicialSetElement private (base: SimplicialSetElement, d .map(d => "s" + unicode.unicodeSubScript(d.toString)) .mkString("", " ", " ") + s"${base.toString}" // Handle cases where the base is also a simplicial set element + // This is very reminiscent of a monad structure - but the data type is not a data container the way most + // monads in functional programming would be def join(): DegenerateSimplicialSetElement = base match { case oldbase: DegenerateSimplicialSetElement => val newbase: SimplicialSetElement = oldbase.join() @@ -57,11 +94,36 @@ object DegenerateSimplicialSetElement { new DegenerateSimplicialSetElement(base, normalizeDegeneracies(List.empty, degeneracies)) } -trait SimplicialSetLike { +/** + * The interface for defining a simplicial set. + * + * A simplicial set **must** have: + * + * 1. A sequence of generating elements. These are considered non-degenerate in the context of this simplicial set. + * This sequence will be assumed to be in increasing order of dimension (so that things like [[nSkeleton]] can + * stop searching when it hits large enough dimensions), but this is not structurally enforced by the trait itself. + * 2. For each index $i$, a partial function from [[SimplicialSetElement]] to [[SimplicialSetElement]] encoding + * the $i$th face map. These partial functions **must** be defined on all the [[generators]]. + * + * With these building blocks, a simplicial set also has: + * + * 1. A [[contains]] function with an `∋` alias. + * 2. A method for listing all n-dimensional cells (degenerate as well as non-degenerate). + * 3. A total order of `SimplicialSetElement`s, as a `given` declaration. + * 4. An instance of the `Cell` typeclass for `SimplicialSetElement`s, as a `given` declaration. + * This instance works on the assumption that you will want to work in the **normalized Moore complex**, + * and will treat the [[generators]] as your cells. + * 5. Functions to compute an [[f_vector]] (of non-degenerate elements), a full f-vector (of all elements), + * and the Euler characteristic. + */ +trait SimplicialSet { def generators: Seq[SimplicialSetElement] def face(index: Int): PartialFunction[SimplicialSetElement, SimplicialSetElement] + + def contains(sse: SimplicialSetElement): Boolean = generators.exists(g => g.base == sse.base) + infix def ∋(sse: SimplicialSetElement): Boolean = contains(sse) def all_n_cells(n: Int): List[SimplicialSetElement] = generators @@ -73,6 +135,14 @@ trait SimplicialSetLike { } .toList + def nSkeleton(n: Int): SimplicialSet = + new SimplicialSet { + val generators: Seq[SimplicialSetElement] = this.generators.takeWhile(_.dim <= n) + + override def face(index: Int): PartialFunction[SimplicialSetElement, SimplicialSetElement] = + this.face(index) + } + given sseOrdering: Ordering[SimplicialSetElement] = Ordering.by((sse: SimplicialSetElement) => sse.dim).orElseBy(_.hashCode()) given normalizedHomologyCell(using seOrd: Ordering[SimplicialSetElement]): OrderedCell[SimplicialSetElement] with { @@ -121,15 +191,9 @@ trait SimplicialSetLike { .sum } -/** Class contract: generators are ascending in dimension. This is assumed by things like n_skeleton - * @param generators - * @param faceMapping - */ -case class SimplicialSet( - generators: LazyList[SimplicialSetElement], - faceMapping: PartialFunction[SimplicialSetElement, List[SimplicialSetElement]] -) extends SimplicialSetLike { - override def face(index: Int): PartialFunction[SimplicialSetElement, SimplicialSetElement] = { +object SimplicialSet { + def mkFaceMaps(faceMapping : PartialFunction[SimplicialSetElement, List[SimplicialSetElement]]): + Int => PartialFunction[SimplicialSetElement, SimplicialSetElement] = index => { case sse if (0 <= index) && (index <= sse.dim) => val split = sse.degeneracies .groupBy(j => (index < j, index == j || index == j + 1, index > j + 1)) @@ -157,19 +221,49 @@ case class SimplicialSet( DegenerateSimplicialSetElement(faceMapping.apply(sse.base)(i), newDegeneracies).join() } } -} -object SimplicialSet { + /** + * Factory method for [[SimplicialSet]]s using the [[LazySimplicialSet]] implementation. + * This method will ensure that + * + * 1. You have face mappings defined for all your generators, and + * 2. You have the correct number of face mappings for each generator. + * + * If your case is finite, but too expensive to do these checks, use the [[LazySimplicialSet]] constructor + * directly. + * + * @param generators + * @param faceMapping + * @return + */ def apply( - generators: List[SimplicialSetElement], - faceMapping: PartialFunction[SimplicialSetElement, List[SimplicialSetElement]] - ): SimplicialSet = { + generators: List[SimplicialSetElement], + faceMapping: PartialFunction[SimplicialSetElement, List[SimplicialSetElement]] + ): SimplicialSet = { assert(generators.forall(g => faceMapping.isDefinedAt(g))) assert(generators.forall(g => if (g.dim > 0) faceMapping.apply(g).size == g.dim + 1 else true)) - new SimplicialSet(LazyList.from(generators), faceMapping) + new LazySimplicialSet(LazyList.from(generators), faceMapping) } } +/** + * This is an implementation of the [[SimplicialSet]] trait that allows for infinite generating sets. + * It also assembles face maps for you from just defining them on the generators and inferring their + * results on degeneracies. + * @param generators + * @param faceMapping + */ +case class LazySimplicialSet( + generators: LazyList[SimplicialSetElement], + faceMapping: PartialFunction[SimplicialSetElement, List[SimplicialSetElement]] +) extends SimplicialSet { + val faceMaps = SimplicialSet.mkFaceMaps(faceMapping) + + override def face(index: Int): PartialFunction[SimplicialSetElement, SimplicialSetElement] = + faceMaps(index) +} + + /** Notes on boundaries of degenerate elements * * Suppose w = s_j_ z Suppose k > j+1, i < j Then by the simplicial set laws: d_k_ s_j_ z = s_j d_k-1_ z d_i_ s_j_ z = @@ -201,8 +295,15 @@ object SimplicialSet { */ import math.Ordering.Implicits.sortedSetOrdering -class Singular[VertexT: Ordering](val underlying: Seq[Simplex[VertexT]]) extends SimplicialSetLike { - val allSimplices = underlying.toSet.flatMap(spx => spx.vertices.subsets.filter(_.nonEmpty).map(Simplex.from)) + +/** + * The Singular Simplicial Set of a simplicial complex (seen as a sequence of simplices). + * + * @param underlying + * @param ordering$VertexT$0 + * @tparam VertexT + */ +class Singular[VertexT: Ordering] private[tda4j] (val allSimplices: Seq[Simplex[VertexT]]) extends SimplicialSet { val generators: Seq[SimplicialWrapper[Simplex[VertexT]]] = allSimplices.toSeq.sortBy(_.dim).map(spx => SimplicialWrapper(spx)) val faceMapping: Map[SimplicialSetElement, List[SimplicialSetElement]] = Map.from( @@ -213,7 +314,7 @@ class Singular[VertexT: Ordering](val underlying: Seq[Simplex[VertexT]]) extends }.toList } ) - val simplicialSet = SimplicialSet(LazyList.from(generators), faceMapping) + val simplicialSet = LazySimplicialSet(LazyList.from(generators), faceMapping) override def face(index: Int): PartialFunction[SimplicialSetElement, SimplicialSetElement] = simplicialSet.face(index) @@ -221,9 +322,43 @@ class Singular[VertexT: Ordering](val underlying: Seq[Simplex[VertexT]]) extends simplicialSet.contains(sse) } +object Singular { + /** + * Create a [[Singular]] simplicial set from the simplices in `underlying`. This method will generate all + * sub-simplices of simplices in `underlying` and weed out duplicates - which may duplicate work if you + * already have a full simplicial complex in place. Use [[fromAll]] if you have all simplices already. + * + * @param underlying + * @tparam VertexT + * @return + */ + def from[VertexT : Ordering](underlying : Seq[Simplex[VertexT]]): Singular[VertexT] = + fromAll(underlying.toSet.flatMap(spx => spx.vertices.subsets.filter(_.nonEmpty).map(Simplex.from)).toSeq) + + /** + * Create a [[Singular]] simplicial set from the simplices in `allSimplices`. This method will trust the + * caller to have generated all sub-simplices appropriately. + * + * @param allSimplices + * @tparam VertexT + * @return + */ + def fromAll[VertexT : Ordering](allSimplices : Seq[Simplex[VertexT]]): Singular[VertexT] = new Singular(allSimplices) +} + +/** + * Helper function to create a [[CellStream]] instance from any simplicial set. + * If no filtration value function is provided, it will use the constant function with the `smallest` value in the + * [[Filterable]] typeclass implementation for `FiltrationT`. + * + * @param ss + * @param filtrationValueO + * @tparam FiltrationT + * @return + */ def normalizedCellStream[FiltrationT: Ordering: Filterable]( - ss: SimplicialSetLike, - filtrationValueO: Option[PartialFunction[SimplicialSetElement, FiltrationT]] = None + ss: SimplicialSet, + filtrationValueO: Option[PartialFunction[SimplicialSetElement, FiltrationT]] = None ): CellStream[SimplicialSetElement, FiltrationT] = { val filterable: Filterable[FiltrationT] = summon[Filterable[FiltrationT]] def filtrationValues = filtrationValueO.getOrElse({ case _ => @@ -246,15 +381,31 @@ def normalizedCellStream[FiltrationT: Ordering: Filterable]( } } - -def nSkeleton(n: Int)(ss: SimplicialSet): SimplicialSet = - SimplicialSet(ss.generators.takeWhile(_.dim <= n), ss.faceMapping) - -def alternateLazyLists[A](left: LazyList[A], right: LazyList[A]): LazyList[A] = - if (left.isEmpty) right +/** + * Helper function to interleave two lazy lists -- so that we can take their union without having to run + * through one of them completely before going to the other one. + * + * @param left + * @param right + * @tparam A + * @return + */ +def alternateLazyLists[A](left: Seq[A], right: Seq[A]): LazyList[A] = + if (left.isEmpty) LazyList.from(right) else left.head #:: alternateLazyLists(right, left.tail) -case class Coproduct(left: SimplicialSet, right: SimplicialSet) extends SimplicialSetLike { +/** + * The coproduct of two simplicial sets, defined as the simplicial set generated by the union of generators + * of the two lists. + * + * If simplices happen to be in both the factors, this implementation currently makes no effort to distinguish them. + * Face maps will be tried from both factors, using the left-most that is actually defined on the given element. + * Both of these implementation choices may be unwise. + * + * @param left + * @param right + */ +case class Coproduct(left: SimplicialSet, right: SimplicialSet) extends SimplicialSet { override def generators: LazyList[SimplicialSetElement] = alternateLazyLists(left.generators, right.generators) override def face(index: Int): PartialFunction[SimplicialSetElement, SimplicialSetElement] = left.face(index).orElse(right.face(index)) @@ -277,7 +428,30 @@ case class ProductElement(left: SimplicialSetElement, right: SimplicialSetElemen s"${left.toString} × ${right.toString}" } -case class Product(left: SimplicialSet, right: SimplicialSet) extends SimplicialSetLike { +// from descending order, if the kth degeneracy is some sj it can be fronted to a (j+k)th degeneracy +// so any one element is in the images of all the j+k running over the zipWithIndex of the degeneracy list +// +// so check that **these** as sets are disjoint +// +// say, if we split a descending sequence of integers in two subsets, is this disjointness automatically +// fulfilled? +// +// Suppose after the split, we have some si in position k and sj in position m that lift out to the same +// degeneracy. This means i+k = j+m. +// +// Here's an example showing that disjoint sequences is not enough! +// s₅ s₂ s₁ ∆⁶ x s₃ s₀ ∆⁷ = s₃ s₅ s₁ ∆⁶ x s₃ s₀ ∆⁷ = s₃ ( s₅ s₁ ∆⁶ x s₀ ∆⁷) +// So we really need to generate all these lifts and compare them, so that we couldn't lift out different +// degeneracies in the candidate combination. +// +// Not only that, but here's an example showing that splitting a disjoint sequence will miss something: +// +// s₅ s₁ s₀ ∆⁶ x s₃ s₀ ∆⁷ has lifted degeneracies on the left: 5, 2, 2; on the right: 3, 1 +// So this should be a valid candidate, but is missed if we just take the sequence (n to(0, -1)) and split it + + + +case class Product(left: SimplicialSet, right: SimplicialSet) extends SimplicialSet { given productOrdering: Ordering[ProductElement] = Ordering .by((pe: ProductElement) => pe.left)(left.sseOrdering) @@ -305,10 +479,10 @@ case class Product(left: SimplicialSet, right: SimplicialSet) extends Simplicial override def compare(x: ProductElement, y: ProductElement): Int = productOrdering.compare(x, y) } - val generatorPairs: LazyList[(SimplicialSetElement, SimplicialSetElement)] = for + val generatorPairs: LazyList[(SimplicialSetElement, SimplicialSetElement)] = LazyList.from(for gL <- left.generators gR <- right.generators - yield (gL, gR) + yield (gL, gR)) def productGenerators(dimension: Int): LazyList[SimplicialSetElement] = generatorPairs .filter((gL, gR) => gL.dim + gR.dim >= dimension) @@ -316,25 +490,26 @@ case class Product(left: SimplicialSet, right: SimplicialSet) extends Simplicial .flatMap { (gL, gR) => val degeneracyIndexPool = (0 to dimension).reverse val totalCount = (dimension - gL.dim) + (dimension - gR.dim) - degeneracyIndexPool - .combinations(totalCount) - .flatMap { degeneracies => - degeneracies - .combinations(dimension - gL.dim) - .map(comb => (comb, degeneracies.diff(comb))) - .filter{(dL, dR) => - dL.reverse.zipWithIndex.forall { (d, i) => d <= gL.dim + i } && - dR.reverse.zipWithIndex.forall { (d, i) => d <= gR.dim + i } - } - .map { (dL, dR) => - SimplicialWrapper( - ProductElement( - DegenerateSimplicialSetElement(gL, dL.toList), - DegenerateSimplicialSetElement(gR, dR.toList) - ) - ) - } - } + val leftCandidates = degeneracyIndexPool + .combinations(dimension - gL.dim) + .toSeq + .filter { (comb) => comb.reverse.zipWithIndex.forall { (c,i) => c <= gL.dim+i } } + .groupBy { (comb) => comb.zipWithIndex.map(_+_).toSet } + val rightCandidates = degeneracyIndexPool + .combinations(dimension - gR.dim) + .toSeq + .filter { (comb) => comb.reverse.zipWithIndex.forall { (c,i) => c <= gR.dim+i } } + .groupBy { (comb) => comb.zipWithIndex.map(_+_).toSet } + for + (cL,ccL) <- leftCandidates + (cR,ccR) <- rightCandidates + if(cL.intersect(cR).isEmpty) + dL <- ccL + dR <- ccR + yield + SimplicialWrapper(ProductElement( + DegenerateSimplicialSetElement(gL, dL.toList), + DegenerateSimplicialSetElement(gR, dR.toList))) } val maxdim = (left.generators.map(_.dim).max) + (right.generators.map(_.dim).max) override def generators: Seq[SimplicialSetElement] = @@ -349,19 +524,19 @@ case class Product(left: SimplicialSet, right: SimplicialSet) extends Simplicial } case class SubSimplicialSet( - ss: SimplicialSetLike, - ambient: SimplicialSetLike, - inclusion: PartialFunction[SimplicialSetElement, SimplicialSetElement] -) extends SimplicialSetLike { - override def generators: LazyList[SimplicialSetElement] = ss.generators + ss: SimplicialSet, + ambient: SimplicialSet, + inclusion: PartialFunction[SimplicialSetElement, SimplicialSetElement] +) extends SimplicialSet { + override def generators: LazyList[SimplicialSetElement] = LazyList.from(ss.generators) override def face(index: Int): PartialFunction[SimplicialSetElement, SimplicialSetElement] = ss.face(index) override def contains(sse: SimplicialSetElement): Boolean = ss.contains(sse) } case class QuotientSimplicialSet( - superSet: SimplicialSetLike, - quotientSet: SimplicialSetLike, + superSet: SimplicialSet, + quotientSet: SimplicialSet, ) /** The Pushout of a diagram @@ -377,19 +552,21 @@ case class QuotientSimplicialSet( * @param g */ case class Pushout( - left: SimplicialSetLike, center: SimplicialSetLike, right: SimplicialSetLike, - f: PartialFunction[SimplicialSetElement, SimplicialSetElement], - g: PartialFunction[SimplicialSetElement, SimplicialSetElement] - ) extends SimplicialSetLike { - lazy val ambient: SimplicialSetLike = Product(left, right) - + left: SimplicialSet, center: SimplicialSet, right: SimplicialSet, + f: PartialFunction[SimplicialSetElement, SimplicialSetElement], + g: PartialFunction[SimplicialSetElement, SimplicialSetElement] + ) extends SimplicialSet { + lazy val ambient: Product = Product(left, right) + lazy val ambient_ssl : SimplicialSet = ambient + override def generators: Seq[SimplicialSetElement] = for - p : ProductElement <- ambient.generators + psse <- ambient.generators + p = psse.asInstanceOf[ProductElement] // this is ugly, but I'm stuck if(f(p.left) == g(p.right)) yield p - + override def face(index: Int): PartialFunction[SimplicialSetElement, SimplicialSetElement] = { case ProductElement(leftS, rightS) => ProductElement(left.face(index)(leftS), right.face(index)(rightS)) diff --git a/src/test/scala/org/appliedtopology/tda4j/SimplicialSetSpec.scala b/src/test/scala/org/appliedtopology/tda4j/SimplicialSetSpec.scala index d095e0d6..27671690 100644 --- a/src/test/scala/org/appliedtopology/tda4j/SimplicialSetSpec.scala +++ b/src/test/scala/org/appliedtopology/tda4j/SimplicialSetSpec.scala @@ -30,4 +30,41 @@ class SimplicialSetSpec extends mutable.Specification { (4, 0.0, Double.PositiveInfinity) )) } + + "homology of torus" >> { + val T2 = Product(sphere(1), sphere(1)) + import T2.given + given chc: CellularHomologyContext[SimplicialSetElement, Double, Double]() + import chc.{*, given} + val T2stream = normalizedCellStream[Double](T2, Some({ (sse:SimplicialSetElement) => sse.dim.toDouble })) + + val ph = persistentHomology(T2stream) + val dgm = ph.diagramAt(5.0) + + dgm must containTheSameElementsAs(List( + (0, 0.0, Double.PositiveInfinity), // essential 0-class + (1, 1.0, 2.0), // transient 1-class, cancels with the first 2-cell + (1, 1.0, Double.PositiveInfinity), // essential 1-class + (1, 1.0, Double.PositiveInfinity), // essential 1-class + (2, 2.0, Double.PositiveInfinity), // essential 2-class + )) + } + + "product of two spheres - specific failure example" >> { + val ss = Product(sphere(6), sphere(7)) + val forbidden = ss + .generators + .filter(_.dim == 9) + .filter{(sse) => sse.asInstanceOf[SimplicialWrapper[ProductElement]].wrapped.left.degeneracies == List(5,2,1)} + .filter{(sse) => sse.asInstanceOf[SimplicialWrapper[ProductElement]].wrapped.right.degeneracies == List(3,0)} + + val required = ss + .generators + .filter(_.dim == 9) + .filter { (sse) => sse.asInstanceOf[SimplicialWrapper[ProductElement]].wrapped.left.degeneracies == List(5, 1, 0) } + .filter { (sse) => sse.asInstanceOf[SimplicialWrapper[ProductElement]].wrapped.right.degeneracies == List(3, 0) } + + "s₅ s₂ s₁ ∆⁶ x s₃ s₀ ∆⁷ = s₃ ( s₅ s₁ ∆⁶ x s₀ ∆⁷) is degenerate" ==> (forbidden.isEmpty must beTrue) + "s₅ s₁ s₀ ∆⁶ x s₃ s₀ ∆⁷ is non-degenerate" ==> (required.nonEmpty must beTrue) + } }