moonlight-homology 0.1.0.1 → 0.1.0.2
raw patch · 18 files changed
+1722/−496 lines, 18 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Moonlight.Homology: FilteredFiniteChainComplex :: FiniteChainComplex r -> Map BasisCellRef FiltrationValue -> FilteredFiniteChainComplex r
- Moonlight.Homology: [filteredBaseComplex] :: FilteredFiniteChainComplex r -> FiniteChainComplex r
- Moonlight.Homology: [filteredCellBirths] :: FilteredFiniteChainComplex r -> Map BasisCellRef FiltrationValue
- Moonlight.Homology.Persistence: FilteredFiniteChainComplex :: FiniteChainComplex r -> Map BasisCellRef FiltrationValue -> FilteredFiniteChainComplex r
- Moonlight.Homology.Persistence: [filteredBaseComplex] :: FilteredFiniteChainComplex r -> FiniteChainComplex r
- Moonlight.Homology.Persistence: [filteredCellBirths] :: FilteredFiniteChainComplex r -> Map BasisCellRef FiltrationValue
+ Moonlight.Homology: criticalBettiDegreeCount :: CriticalBettiTable -> Int
+ Moonlight.Homology: criticalBettiRankCount :: CriticalBettiTable -> Int
+ Moonlight.Homology: criticalBettiTableValues :: CriticalBettiTable -> Vector Int
+ Moonlight.Homology: criticalBettiVectors :: CriticalBettiTable -> [[Int]]
+ Moonlight.Homology: data CriticalBettiTable
+ Moonlight.Homology: filteredBaseComplex :: FilteredFiniteChainComplex filtration r -> FiniteChainComplex r
+ Moonlight.Homology: filteredCellBirths :: FilteredFiniteChainComplex filtration r -> Map BasisCellRef filtration
+ Moonlight.Homology: filteredCriticalValues :: FilteredFiniteChainComplex filtration r -> [filtration]
+ Moonlight.Homology: mkBoundaryIncidenceFromOrderedColumns :: (Eq r, Semiring r) => Natural -> Natural -> Vector [BoundaryEntry r] -> Either BoundaryIncidenceShapeError (BoundaryIncidence r)
+ Moonlight.Homology: mod2PersistentPairsWithCriticalBettiTable :: Integral r => FilteredFiniteChainComplex filtration r -> Either HomologyFailure ([PersistencePair filtration], CriticalBettiTable)
+ Moonlight.Homology: persistentBettiAt :: Ord filtration => filtration -> [PersistencePair filtration] -> Map HomologicalDegree Int
+ Moonlight.Homology: persistentBettiAtCriticalValues :: Ord filtration => FilteredFiniteChainComplex filtration r -> [PersistencePair filtration] -> Either HomologyFailure [Map HomologicalDegree Int]
+ Moonlight.Homology: persistentBettiAtMany :: Ord filtration => [filtration] -> [PersistencePair filtration] -> [Map HomologicalDegree Int]
+ Moonlight.Homology.Boundary: mkBoundaryIncidenceFromOrderedColumns :: (Eq r, Semiring r) => Natural -> Natural -> Vector [BoundaryEntry r] -> Either BoundaryIncidenceShapeError (BoundaryIncidence r)
+ Moonlight.Homology.Persistence: criticalBettiDegreeCount :: CriticalBettiTable -> Int
+ Moonlight.Homology.Persistence: criticalBettiRankCount :: CriticalBettiTable -> Int
+ Moonlight.Homology.Persistence: criticalBettiTableValues :: CriticalBettiTable -> Vector Int
+ Moonlight.Homology.Persistence: criticalBettiVectors :: CriticalBettiTable -> [[Int]]
+ Moonlight.Homology.Persistence: data CriticalBettiTable
+ Moonlight.Homology.Persistence: filteredBaseComplex :: FilteredFiniteChainComplex filtration r -> FiniteChainComplex r
+ Moonlight.Homology.Persistence: filteredCellBirths :: FilteredFiniteChainComplex filtration r -> Map BasisCellRef filtration
+ Moonlight.Homology.Persistence: filteredCriticalValues :: FilteredFiniteChainComplex filtration r -> [filtration]
+ Moonlight.Homology.Persistence: mod2PersistentPairsWithCriticalBettiTable :: Integral r => FilteredFiniteChainComplex filtration r -> Either HomologyFailure ([PersistencePair filtration], CriticalBettiTable)
+ Moonlight.Homology.Persistence: persistentBettiAt :: Ord filtration => filtration -> [PersistencePair filtration] -> Map HomologicalDegree Int
+ Moonlight.Homology.Persistence: persistentBettiAtCriticalValues :: Ord filtration => FilteredFiniteChainComplex filtration r -> [PersistencePair filtration] -> Either HomologyFailure [Map HomologicalDegree Int]
+ Moonlight.Homology.Persistence: persistentBettiAtMany :: Ord filtration => [filtration] -> [PersistencePair filtration] -> [Map HomologicalDegree Int]
- Moonlight.Homology: GraphTopologySeed :: Graph1Skeleton -> Maybe (FilteredFiniteChainComplex r) -> Maybe ScalarPotentialField -> Int -> TopologyWitnessSeed r
+ Moonlight.Homology: GraphTopologySeed :: Graph1Skeleton -> Maybe (FilteredFiniteChainComplex FiltrationValue r) -> Maybe ScalarPotentialField -> Int -> TopologyWitnessSeed r
- Moonlight.Homology: TopologyObservationConfig :: Maybe (FilteredFiniteChainComplex r) -> Maybe ScalarPotentialField -> Int -> TopologyObservationConfig r
+ Moonlight.Homology: TopologyObservationConfig :: Maybe (FilteredFiniteChainComplex FiltrationValue r) -> Maybe ScalarPotentialField -> Int -> TopologyObservationConfig r
- Moonlight.Homology: [observationFiltration] :: TopologyObservationConfig r -> Maybe (FilteredFiniteChainComplex r)
+ Moonlight.Homology: [observationFiltration] :: TopologyObservationConfig r -> Maybe (FilteredFiniteChainComplex FiltrationValue r)
- Moonlight.Homology: data FilteredFiniteChainComplex r
+ Moonlight.Homology: data FilteredFiniteChainComplex filtration r
- Moonlight.Homology: mkFilteredFiniteChainComplex :: Integral r => FiniteChainComplex r -> [(BasisCellRef, FiltrationValue)] -> Either HomologyFailure (FilteredFiniteChainComplex r)
+ Moonlight.Homology: mkFilteredFiniteChainComplex :: (Integral r, Ord filtration) => FiniteChainComplex r -> [(BasisCellRef, filtration)] -> Either HomologyFailure (FilteredFiniteChainComplex filtration r)
- Moonlight.Homology: mod2PersistenceTopologyWitness :: Integral r => FilteredFiniteChainComplex r -> Either HomologyFailure (TopologyWitness scaffold spectral FiltrationValue coefficient basis)
+ Moonlight.Homology: mod2PersistenceTopologyWitness :: Integral r => FilteredFiniteChainComplex filtration r -> Either HomologyFailure (TopologyWitness scaffold spectral filtration coefficient basis)
- Moonlight.Homology: mod2PersistentPairs :: Integral r => FilteredFiniteChainComplex r -> Either HomologyFailure [PersistencePair FiltrationValue]
+ Moonlight.Homology: mod2PersistentPairs :: Integral r => FilteredFiniteChainComplex filtration r -> Either HomologyFailure [PersistencePair filtration]
- Moonlight.Homology: observeGraphTopologyWitness :: Integral r => Maybe (FilteredFiniteChainComplex r) -> Maybe ScalarPotentialField -> Int -> Graph1Skeleton -> Either HomologyFailure (TopologyWitness MacroScaffoldIR GraphSpectralMode FiltrationValue Rational Int)
+ Moonlight.Homology: observeGraphTopologyWitness :: Integral r => Maybe (FilteredFiniteChainComplex FiltrationValue r) -> Maybe ScalarPotentialField -> Int -> Graph1Skeleton -> Either HomologyFailure (TopologyWitness MacroScaffoldIR GraphSpectralMode FiltrationValue Rational Int)
- Moonlight.Homology.Persistence: data FilteredFiniteChainComplex r
+ Moonlight.Homology.Persistence: data FilteredFiniteChainComplex filtration r
- Moonlight.Homology.Persistence: mkFilteredFiniteChainComplex :: Integral r => FiniteChainComplex r -> [(BasisCellRef, FiltrationValue)] -> Either HomologyFailure (FilteredFiniteChainComplex r)
+ Moonlight.Homology.Persistence: mkFilteredFiniteChainComplex :: (Integral r, Ord filtration) => FiniteChainComplex r -> [(BasisCellRef, filtration)] -> Either HomologyFailure (FilteredFiniteChainComplex filtration r)
- Moonlight.Homology.Persistence: mod2PersistenceTopologyWitness :: Integral r => FilteredFiniteChainComplex r -> Either HomologyFailure (TopologyWitness scaffold spectral FiltrationValue coefficient basis)
+ Moonlight.Homology.Persistence: mod2PersistenceTopologyWitness :: Integral r => FilteredFiniteChainComplex filtration r -> Either HomologyFailure (TopologyWitness scaffold spectral filtration coefficient basis)
- Moonlight.Homology.Persistence: mod2PersistentPairs :: Integral r => FilteredFiniteChainComplex r -> Either HomologyFailure [PersistencePair FiltrationValue]
+ Moonlight.Homology.Persistence: mod2PersistentPairs :: Integral r => FilteredFiniteChainComplex filtration r -> Either HomologyFailure [PersistencePair filtration]
- Moonlight.Homology.Topology: GraphTopologySeed :: Graph1Skeleton -> Maybe (FilteredFiniteChainComplex r) -> Maybe ScalarPotentialField -> Int -> TopologyWitnessSeed r
+ Moonlight.Homology.Topology: GraphTopologySeed :: Graph1Skeleton -> Maybe (FilteredFiniteChainComplex FiltrationValue r) -> Maybe ScalarPotentialField -> Int -> TopologyWitnessSeed r
- Moonlight.Homology.Topology: TopologyObservationConfig :: Maybe (FilteredFiniteChainComplex r) -> Maybe ScalarPotentialField -> Int -> TopologyObservationConfig r
+ Moonlight.Homology.Topology: TopologyObservationConfig :: Maybe (FilteredFiniteChainComplex FiltrationValue r) -> Maybe ScalarPotentialField -> Int -> TopologyObservationConfig r
- Moonlight.Homology.Topology: [observationFiltration] :: TopologyObservationConfig r -> Maybe (FilteredFiniteChainComplex r)
+ Moonlight.Homology.Topology: [observationFiltration] :: TopologyObservationConfig r -> Maybe (FilteredFiniteChainComplex FiltrationValue r)
- Moonlight.Homology.Topology: observeGraphTopologyWitness :: Integral r => Maybe (FilteredFiniteChainComplex r) -> Maybe ScalarPotentialField -> Int -> Graph1Skeleton -> Either HomologyFailure (TopologyWitness MacroScaffoldIR GraphSpectralMode FiltrationValue Rational Int)
+ Moonlight.Homology.Topology: observeGraphTopologyWitness :: Integral r => Maybe (FilteredFiniteChainComplex FiltrationValue r) -> Maybe ScalarPotentialField -> Int -> Graph1Skeleton -> Either HomologyFailure (TopologyWitness MacroScaffoldIR GraphSpectralMode FiltrationValue Rational Int)
Files
- CHANGELOG.md +19/−0
- README.md +39/−43
- moonlight-homology.cabal +4/−2
- src-matrix/Moonlight/Homology/Boundary/Finite.hs +8/−14
- src-matrix/Moonlight/Homology/Boundary/LinAlg.hs +203/−3
- src-matrix/Moonlight/Homology/Pure/Rank/Field.hs +18/−14
- src-public/Moonlight/Homology.hs +14/−1
- src-public/Moonlight/Homology/Boundary.hs +1/−0
- src-public/Moonlight/Homology/Persistence.hs +28/−2
- src-topology/Moonlight/Homology/Pure/Topology.hs +11/−2
- src-topology/Moonlight/Homology/Pure/Topology/BlockSchur.hs +7/−15
- src-topology/Moonlight/Homology/Pure/Topology/Core.hs +18/−15
- src-topology/Moonlight/Homology/Pure/Topology/Graph/Skeleton.hs +5/−13
- src-topology/Moonlight/Homology/Pure/Topology/Observation.hs +2/−1
- src-topology/Moonlight/Homology/Pure/Topology/Persistence.hs +1125/−370
- test/Main.hs +39/−0
- test/facade/CompileFailSpec.hs +1/−1
- test/topology/TopologySpec.hs +180/−0
CHANGELOG.md view
@@ -2,6 +2,25 @@ All notable changes to `moonlight-homology` are documented here. +## 0.1.0.2 - 2026-08-22++- Generalize `FilteredFiniteChainComplex` from the fixed binary64+ `FiltrationValue` to any ordered birth-key type, preserving exact geometric+ filtration order through the canonical persistence reducer. Its constructor+ is now sealed; callers retain read-only projections and construct only+ through the coverage- and monotonicity-checking boundary.+- Add `persistentBettiAt`, the batched `persistentBettiAtMany`, and+ `persistentBettiAtCriticalValues`. Filtered complexes cache exact critical+ equivalence classes and dense complex-relative ranks, so all critical Betti+ profiles are one ordered event sweep rather than one barcode scan per birth.+- Lower mod-two persistence through dense integer indices, `IntMap`/`IntSet`+ columns, degree-wise clearing, and a sealed union-find specialization for+ graph boundaries, with the general sparse reducer retained for incompatible+ boundary fibers.+- Keep the checked finite-chain constructor as the sole public entrance while+ deciding sparse boundary-composition zero directly, without materializing a+ throwaway product matrix on the valid path.+ ## 0.1.0.1 - 2026-08-21 - Rebuilt the multi-library Haddock archive with external Algebra, Core, and
README.md view
@@ -28,8 +28,8 @@ backend is a compile error. - **Exact and spectral sequences.** Filtered spectral families with page-by-page reduction and convergence tracking; exact-sequence helpers; Block–Schur reductions.-- **Persistence.** One- and two-parameter filtered complexes and mod-2 persistence- pairs.+- **Persistence.** Arbitrary ordered one-parameter birth keys, mod-2+ persistence pairs and closed-sublevel Betti queries; two-parameter vocabulary. - **Discrete Morse theory.** Acyclic matchings that reduce a complex to its critical cells while preserving homology. - **Topological carriers.** Cell complexes, graph 1-skeletons, Reeb/macro-scaffold@@ -49,37 +49,30 @@ each oriented edge to `head − tail`; every degree above 1 is empty. ```haskell-{-# LANGUAGE DataKinds #-}--import Moonlight.Homology+import Moonlight.Homology (FiniteChainComplex)+import Moonlight.Homology.Presentation -circle :: Either BoundaryIncidenceShapeError (FiniteChainComplex Rational)-circle = do- d1 <-- mkBoundaryIncidence 3 3- [ mkBoundaryEntry 0 0 (-1), mkBoundaryEntry 0 1 1,- mkBoundaryEntry 1 1 (-1), mkBoundaryEntry 1 2 1,- mkBoundaryEntry 2 2 (-1), mkBoundaryEntry 2 0 1- ]- pure $- mkFiniteChainComplex (HomologicalDegree 1) $ \degree ->- case degree of- HomologicalDegree 1 -> d1- HomologicalDegree 0 -> emptyBoundaryIncidenceOf 3 0- _ -> emptyBoundaryIncidence+circle :: Either ChainBuildError (FiniteChainComplex Rational)+circle =+ compileChain+ ChainSpec+ { chainCellCounts = [3, 3],+ chainBoundaries =+ [ [ (0, 0, -1), (0, 1, 1),+ (1, 1, -1), (1, 2, 1),+ (2, 2, -1), (2, 0, 1)+ ]+ ]+ } ``` -`mkBoundaryIncidence sourceDim targetDim entries` builds the validated matrix of ∂ₙ,-the boundary map from the `sourceDim` cells of degree *n* to the `targetDim` cells of-degree *n − 1*. Each `mkBoundaryEntry source target coefficient` is one nonzero-incidence: the degree-*n* cell `source` contains the degree-*(n − 1)* cell `target` in-its boundary with that `coefficient`. Edge 0, entries `(0, 0, -1)` and+`chainCellCounts` lists cell counts from degree zero upward;+`chainBoundaries` supplies one sparse `(source, target, coefficient)` section for+each positive degree. Edge 0, entries `(0, 0, -1)` and `(0, 1, 1)`, encodes ∂(edge₀) = vertex₁ − vertex₀, running from vertex 0 (tail, `-1`) to vertex 1 (head, `+1`); edges 1 and 2 close the loop v₀ → v₁ → v₂ → v₀. Construction-is total: mismatched shapes are rejected as `BoundaryIncidenceShapeError`. `emptyBoundaryIncidenceOf sourceDim targetDim` is the zero map of that shape-(here ∂₀, three vertices to nothing), and `emptyBoundaryIncidence` the empty map used-above the top degree. `mkFiniteChainComplex topDegree atDegree` then assembles the-complex from its boundary at each degree: a top degree and one `∂` per degree.+is total: `compileChain` reports malformed incidence, shape mismatch, or failure of+∂ ∘ ∂ = 0 through `ChainBuildError`; the unchecked matrix constructor remains private. ### Betti numbers over a field @@ -109,20 +102,16 @@ with the 2-cell attached by a degree-2 map, giving H₁(RP²) = ℤ/2. ```haskell-{-# LANGUAGE DataKinds #-}- import Moonlight.Homology+import Moonlight.Homology.Presentation -realProjectivePlane :: Either BoundaryIncidenceShapeError (FiniteChainComplex Integer)-realProjectivePlane = do- d2 <- mkBoundaryIncidence 1 1 [mkBoundaryEntry 0 0 2]- pure $- mkFiniteChainComplex (HomologicalDegree 2) $ \degree ->- case degree of- HomologicalDegree 2 -> d2- HomologicalDegree 1 -> emptyBoundaryIncidenceOf 1 1- HomologicalDegree 0 -> emptyBoundaryIncidenceOf 1 0- _ -> emptyBoundaryIncidence+realProjectivePlane :: Either ChainBuildError (FiniteChainComplex Integer)+realProjectivePlane =+ compileChain+ ChainSpec+ { chainCellCounts = [1, 1, 1],+ chainBoundaries = [[], [(0, 0, 2)]]+ } integralHomology :: FiniteChainComplex Integer -> Either HomologyFailure [HomologyGroup Integer]@@ -154,8 +143,15 @@ `Moonlight.Homology` umbrella, or from the narrower module noted below. - **Persistence.** `mkFilteredFiniteChainComplex` builds a filtered complex;- `mod2PersistentPairs` reads its birth/death pairs, and `BiPersistencePair` carries- the two-parameter case. In `Moonlight.Homology.Persistence`.+ its birth key may be any ordered type, while `FiltrationValue` remains the+ binary64 convenience specialization. `mod2PersistentPairs` reads exact+ birth/death pairs, `persistentBettiAt` answers one closed-sublevel query, and+ `persistentBettiAtMany` sweeps an arbitrary threshold family without+ rescanning the barcode. For every admitted critical value,+ `persistentBettiAtCriticalValues` uses the filtered complex's dense derived+ ranks while retaining the exact births as the public authority.+ `BiPersistencePair` carries the two-parameter case. In+ `Moonlight.Homology.Persistence`. - **Spectral sequences.** `mkSpectralSource` and `spectralFamilyPages` produce the page-by-page family; `spectralFamilyLimitPage`, `spectralFamilyStableFrom`, and `convergenceDepth` track convergence. In `Moonlight.Homology.Sequence`.@@ -216,7 +212,7 @@ | `Moonlight.Homology.Backend` | The `HomologyBackend` dispatcher: Smith / rational / GF(2). | | `Moonlight.Homology.Sequence` | Exact and spectral sequences, Block–Schur reductions, graph spectral helpers. | | `Moonlight.Homology.Topology` | Cell complexes, graph skeletons, macro-scaffolds, discrete Morse, persistence values, observers, and constraints. |-| `Moonlight.Homology.Persistence` | Filtered complexes and mod-2 persistence pairs. |+| `Moonlight.Homology.Persistence` | Ordered filtered complexes, mod-2 persistence pairs, and barcode Betti queries. | | `Moonlight.Homology.Pure.Topology.CellComplex` | Generic two-dimensional cell incidence; requires `moonlight-homology:cell-complex`. | | `Moonlight.Homology.Pure.Topology.CellCategory` | Finite incidence category for a `CellComplex2D`; requires `moonlight-homology:cell-category`. | | `Moonlight.Homology.Effect.Laws` | Boundary-nilpotence and reduction law harnesses. |
moonlight-homology.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: moonlight-homology-version: 0.1.0.1+version: 0.1.0.2 homepage: https://github.com/PaleRoses/moonlight bug-reports: https://github.com/PaleRoses/moonlight/issues synopsis: Chain complexes, phase-gated homology interfaces, and spectral scaffolding.@@ -90,6 +90,7 @@ , moonlight-linalg:moonlight-linalg-dense >= 0.1 && < 0.2 , moonlight-pale:diagnostic >= 0.1 && < 0.2 , moonlight-homology:moonlight-homology-chain+ , vector >= 0.13 && < 0.14 -- The incidence interface is independently useful to mesh and visualization -- consumers. Keep it outside the matrix/diagnostic topology closure.@@ -289,6 +290,7 @@ , tasty-hunit >= 0.10 && < 0.11 , tasty-quickcheck >= 0.10 && < 0.12 , text >= 2.0 && < 2.2+ , vector >= 0.13 && < 0.14 benchmark moonlight-homology-bench import: shared-properties@@ -315,5 +317,5 @@ source-repository this type: git location: https://github.com/PaleRoses/moonlight.git- tag: moonlight-homology-0.1.0.1+ tag: moonlight-homology-0.1.0.2 subdir: moonlight-homology
src-matrix/Moonlight/Homology/Boundary/Finite.hs view
@@ -21,17 +21,17 @@ import qualified Data.Map.Strict as Map import Data.Set (Set) import qualified Data.Set as Set+import Data.Vector qualified as Vector import Moonlight.Core (Semiring) import Moonlight.Homology.Boundary.LinAlg ( BoundaryIncidence, boundaryCoefficient,- boundaryEntries,- composeBoundaryIncidence,+ boundaryEntriesBySource,+ boundaryIncidenceCompositionIsZero, emptyBoundaryIncidence, emptyBoundaryIncidenceOf, materializeIncidenceBoundary, sourceCardinality,- sourceIndex, targetCardinality, targetIndex, )@@ -69,13 +69,13 @@ Int -> Either HomologyFailure () adjacentNilpotenceAt finite degreeIndex =- composeBoundaryIncidence+ boundaryIncidenceCompositionIsZero (incidenceMatrixAt finite (HomologicalDegree degreeIndex)) (incidenceMatrixAt finite (HomologicalDegree (degreeIndex + 1))) & either (Left . InvalidBoundaryIncidence . show)- ( \composed ->- if null (boundaryEntries composed)+ ( \compositionIsZero ->+ if compositionIsZero then Right () else Left (ChainComplexNilpotenceViolation degreeIndex) )@@ -184,15 +184,9 @@ ) | otherwise = let incidence = incidenceMatrixAt finiteComplex (HomologicalDegree dimensionValue)- entriesBySource =- foldl'- ( \accumulator entryValue ->- Map.insertWith (<>) (sourceIndex entryValue) [entryValue] accumulator- )- Map.empty- (boundaryEntries incidence)+ entriesBySource = boundaryEntriesBySource incidence restrictedEntriesOf sourceCell =- Map.findWithDefault [] (cellIndex sourceCell) entriesBySource+ maybe [] id (entriesBySource Vector.!? cellIndex sourceCell) & mapMaybe ( \entryValue -> let targetCell =
src-matrix/Moonlight/Homology/Boundary/LinAlg.hs view
@@ -12,8 +12,10 @@ sourceCardinality, targetCardinality, boundaryEntries,+ boundaryEntriesBySource, mkBoundaryIncidence, mkBoundaryIncidenceFromOrderedEntries,+ mkBoundaryIncidenceFromOrderedColumns, overlapBoundaryIncidence, emptyBoundaryIncidence, emptyBoundaryIncidenceOf,@@ -23,6 +25,7 @@ boundaryIncidenceApply, transposeBoundaryIncidence, composeBoundaryIncidence,+ boundaryIncidenceCompositionIsZero, boundaryIncidenceDiagonal, addBoundaryIncidence, mapBoundaryCoefficients,@@ -43,10 +46,15 @@ ) where +import Control.Monad (foldM)+import Control.Monad.ST (ST, runST) import Data.Function ((&)) import Data.Kind (Type)+import Data.List qualified as List import qualified Data.Map.Strict as Map import Data.Maybe (listToMaybe, mapMaybe)+import Data.Vector qualified as Vector+import Data.Vector.Mutable qualified as MutableVector import Moonlight.Core (AdditiveMonoid (..), Semiring) import Moonlight.Homology.Pure.Failure (HomologyFailure (..), HomologyLaw (..)) import Numeric.Natural (Natural)@@ -93,10 +101,36 @@ data BoundaryIncidence r = BoundaryIncidence { sourceCardinality :: Int, targetCardinality :: Int,- boundaryEntries :: [BoundaryEntry r]+ boundaryEntries :: [BoundaryEntry r],+ boundaryEntriesBySourceCache :: Vector.Vector [BoundaryEntry r] }- deriving stock (Eq, Show) +instance Eq r => Eq (BoundaryIncidence r) where+ left == right =+ sourceCardinality left == sourceCardinality right+ && targetCardinality left == targetCardinality right+ && boundaryEntries left == boundaryEntries right++instance Show r => Show (BoundaryIncidence r) where+ showsPrec precedence incidence =+ showParen+ (precedence > 10)+ ( showString "BoundaryIncidence {sourceCardinality = "+ . shows (sourceCardinality incidence)+ . showString ", targetCardinality = "+ . shows (targetCardinality incidence)+ . showString ", boundaryEntries = "+ . shows (boundaryEntries incidence)+ . showString "}"+ )++-- | Canonical entries sectioned by source column. The projection is cached+-- lazily by the abstract incidence owner, so chain-law checking, restriction,+-- graph extraction, and persistence share one grouping without making it+-- semantic state.+boundaryEntriesBySource :: BoundaryIncidence r -> Vector.Vector [BoundaryEntry r]+boundaryEntriesBySource = boundaryEntriesBySourceCache+ mkBoundaryIncidence :: (Eq r, Semiring r) => Natural -> Natural -> [BoundaryEntry r] -> Either BoundaryIncidenceShapeError (BoundaryIncidence r) mkBoundaryIncidence sourceCardinalityValue targetCardinalityValue entries = let sourceDimension = fromIntegral sourceCardinalityValue@@ -160,6 +194,10 @@ = OrderedEntriesOutOfOrder | OrderedCanonicalization !(Maybe (BoundaryEntry r)) ![BoundaryEntry r] +data OrderedColumnState+ = OrderedColumnInvalid+ | OrderedColumnPrefix !(Maybe Int)+ canonicalizeOrderedEntries :: (Eq r, Semiring r) => [BoundaryEntry r] -> Maybe [BoundaryEntry r] canonicalizeOrderedEntries entries = finalizeOrderedCanonicalization@@ -218,7 +256,55 @@ (sourceIndex left, targetIndex left) (sourceIndex right, targetIndex right) +-- | Admit source-indexed canonical columns without flattening and regrouping+-- them. A noncanonical cover descends to the general ordered-entry constructor;+-- the authoritative 'BoundaryIncidence' and every typed shape obstruction stay+-- identical.+mkBoundaryIncidenceFromOrderedColumns ::+ (Eq r, Semiring r) =>+ Natural ->+ Natural ->+ Vector.Vector [BoundaryEntry r] ->+ Either BoundaryIncidenceShapeError (BoundaryIncidence r)+mkBoundaryIncidenceFromOrderedColumns sourceCardinalityValue targetCardinalityValue columns =+ let sourceDimension = fromIntegral sourceCardinalityValue+ targetDimension = fromIntegral targetCardinalityValue+ flattenedEntries = concat (Vector.toList columns)+ in if Vector.length columns == sourceDimension+ && Vector.and (Vector.imap (columnIsCanonical targetDimension) columns)+ then+ Right+ ( uncheckedBoundaryIncidenceFromColumns+ sourceDimension+ targetDimension+ flattenedEntries+ columns+ )+ else+ mkBoundaryIncidenceFromOrderedEntries+ sourceCardinalityValue+ targetCardinalityValue+ flattenedEntries +columnIsCanonical :: (Eq r, Semiring r) => Int -> Int -> [BoundaryEntry r] -> Bool+columnIsCanonical targetDimension sourceIndexValue entries =+ case List.foldl' advanceOrderedColumn (OrderedColumnPrefix Nothing) entries of+ OrderedColumnInvalid -> False+ OrderedColumnPrefix _ -> True+ where+ advanceOrderedColumn OrderedColumnInvalid _ = OrderedColumnInvalid+ advanceOrderedColumn (OrderedColumnPrefix previousTarget) entry =+ let currentTarget = targetIndex entry+ targetFollows = maybe True (< currentTarget) previousTarget+ in if sourceIndex entry == sourceIndexValue+ && currentTarget >= 0+ && currentTarget < targetDimension+ && boundaryCoefficient entry /= zero+ && targetFollows+ then OrderedColumnPrefix (Just currentTarget)+ else OrderedColumnInvalid++ emptyBoundaryIncidence :: BoundaryIncidence r emptyBoundaryIncidence = uncheckedBoundaryIncidence 0 0 []@@ -361,6 +447,100 @@ (fromIntegral (targetCardinality left)) composedEntries +-- | Decide whether a sparse composite is zero without materializing the+-- composite matrix. The local columns are the cover: each upper source is+-- reduced independently, and the result descends exactly when every overlap+-- coefficient cancels.+boundaryIncidenceCompositionIsZero ::+ (Eq r, Num r, Semiring r) =>+ BoundaryIncidence r ->+ BoundaryIncidence r ->+ Either BoundaryIncidenceShapeError Bool+boundaryIncidenceCompositionIsZero left right+ | targetCardinality right /= sourceCardinality left =+ Left+ ( BoundaryIncidenceShapeMismatch+ (sourceCardinality left)+ (targetCardinality left)+ (sourceCardinality right)+ (targetCardinality right)+ )+ | Vector.all null (boundaryEntriesBySource left)+ || Vector.all null (boundaryEntriesBySource right) = Right True+ | otherwise =+ let leftTermsBySource = boundaryEntriesBySource left+ rightTermsBySource = boundaryEntriesBySource right+ in Right+ ( runST $ do+ coefficientsByTarget <-+ MutableVector.replicate (targetCardinality left) zero+ Vector.foldM+ (checkCompositeColumn coefficientsByTarget leftTermsBySource)+ True+ rightTermsBySource+ )++checkCompositeColumn ::+ (Eq r, Num r, Semiring r) =>+ MutableVector.MVector s r ->+ Vector.Vector [BoundaryEntry r] ->+ Bool ->+ [BoundaryEntry r] ->+ ST s Bool+checkCompositeColumn coefficientsByTarget leftTermsBySource precedingColumnsAreZero rightTerms =+ if precedingColumnsAreZero+ then do+ touchedTargets <-+ foldM+ (accumulateRightTerm coefficientsByTarget leftTermsBySource)+ []+ rightTerms+ foldM+ (inspectAndClearTarget coefficientsByTarget)+ True+ touchedTargets+ else pure False++accumulateRightTerm ::+ Num r =>+ MutableVector.MVector s r ->+ Vector.Vector [BoundaryEntry r] ->+ [Int] ->+ BoundaryEntry r ->+ ST s [Int]+accumulateRightTerm coefficientsByTarget leftTermsBySource touchedTargets rightEntry =+ foldM+ (accumulateLeftTerm coefficientsByTarget (boundaryCoefficient rightEntry))+ touchedTargets+ (Vector.unsafeIndex leftTermsBySource (targetIndex rightEntry))++accumulateLeftTerm ::+ Num r =>+ MutableVector.MVector s r ->+ r ->+ [Int] ->+ BoundaryEntry r ->+ ST s [Int]+accumulateLeftTerm coefficientsByTarget rightCoefficient touchedTargets leftEntry = do+ let targetIndexValue = targetIndex leftEntry+ accumulatedCoefficient <- MutableVector.unsafeRead coefficientsByTarget targetIndexValue+ MutableVector.unsafeWrite+ coefficientsByTarget+ targetIndexValue+ (accumulatedCoefficient + boundaryCoefficient leftEntry * rightCoefficient)+ pure (targetIndexValue : touchedTargets)++inspectAndClearTarget ::+ (Eq r, Semiring r) =>+ MutableVector.MVector s r ->+ Bool ->+ Int ->+ ST s Bool+inspectAndClearTarget coefficientsByTarget precedingTargetsAreZero targetIndexValue = do+ coefficientValue <- MutableVector.unsafeRead coefficientsByTarget targetIndexValue+ MutableVector.unsafeWrite coefficientsByTarget targetIndexValue zero+ pure (precedingTargetsAreZero && coefficientValue == zero)+ boundaryIncidenceDiagonal :: Num r => BoundaryIncidence r -> Map.Map Int r boundaryIncidenceDiagonal incidence = boundaryEntries incidence@@ -551,10 +731,30 @@ uncheckedBoundaryIncidence :: Int -> Int -> [BoundaryEntry r] -> BoundaryIncidence r uncheckedBoundaryIncidence sourceDimension targetDimension entries =+ uncheckedBoundaryIncidenceFromColumns+ sourceDimension+ targetDimension+ entries+ ( fmap reverse+ ( Vector.accum+ (flip (:))+ (Vector.replicate sourceDimension [])+ (fmap (\entry -> (sourceIndex entry, entry)) entries)+ )+ )++uncheckedBoundaryIncidenceFromColumns ::+ Int ->+ Int ->+ [BoundaryEntry r] ->+ Vector.Vector [BoundaryEntry r] ->+ BoundaryIncidence r+uncheckedBoundaryIncidenceFromColumns sourceDimension targetDimension entries columns = BoundaryIncidence { sourceCardinality = sourceDimension, targetCardinality = targetDimension,- boundaryEntries = entries+ boundaryEntries = entries,+ boundaryEntriesBySourceCache = columns } firstOutOfBoundsEntry :: Int -> Int -> [BoundaryEntry r] -> Maybe (BoundaryEntry r)
src-matrix/Moonlight/Homology/Pure/Rank/Field.hs view
@@ -14,7 +14,6 @@ import Data.IntMap.Strict (IntMap) import Data.IntMap.Strict qualified as IntMap import Data.Kind (Type)-import Data.Maybe (listToMaybe) import Moonlight.Core (Semiring, mkCapability) import Moonlight.Homology.Boundary.Finite ( FiniteChainComplex,@@ -26,7 +25,7 @@ import Moonlight.Homology.Boundary.LinAlg ( BoundaryIncidence, BoundaryIncidenceShapeError,- boundaryEntries,+ boundaryIncidenceCompositionIsZero, composeBoundaryIncidence, ) import Moonlight.Homology.Pure.Matrix.Reducer@@ -217,19 +216,24 @@ HomologicalDegree -> Either (FieldHomologyFailure coeff) () validateNilpotenceAt finite degreeValue@(HomologicalDegree degreeInt) =- case- composeBoundaryIncidence- (incidenceMatrixAt finite (HomologicalDegree (degreeInt - 1)))- (incidenceMatrixAt finite degreeValue)- of- Left shapeError ->- Left (FieldHomologyBoundaryShapeFailed degreeValue shapeError)- Right composite ->- case listToMaybe (boundaryEntries composite) of- Nothing ->+ let lowerBoundary =+ incidenceMatrixAt finite (HomologicalDegree (degreeInt - 1))+ upperBoundary = incidenceMatrixAt finite degreeValue+ in case+ boundaryIncidenceCompositionIsZero+ lowerBoundary+ upperBoundary+ of+ Left shapeError ->+ Left (FieldHomologyBoundaryShapeFailed degreeValue shapeError)+ Right True -> Right ()- Just _ ->- Left (FieldHomologyNonNilpotent degreeValue composite)+ Right False ->+ case composeBoundaryIncidence lowerBoundary upperBoundary of+ Left shapeError ->+ Left (FieldHomologyBoundaryShapeFailed degreeValue shapeError)+ Right composite ->+ Left (FieldHomologyNonNilpotent degreeValue composite) {-# INLINEABLE validateNilpotenceAt #-} rationalRank ::
src-public/Moonlight/Homology.hs view
@@ -36,6 +36,7 @@ boundaryEntries, mkBoundaryIncidence, mkBoundaryIncidenceFromOrderedEntries,+ mkBoundaryIncidenceFromOrderedColumns, overlapBoundaryIncidence, emptyBoundaryIncidence, emptyBoundaryIncidenceOf,@@ -448,9 +449,21 @@ BiFilteredCell (..), BiPersistencePair (..), FiltrationValue (..),- FilteredFiniteChainComplex (..),+ FilteredFiniteChainComplex,+ filteredBaseComplex,+ filteredCellBirths,+ filteredCriticalValues, mkFilteredFiniteChainComplex, mod2PersistentPairs,+ CriticalBettiTable,+ criticalBettiDegreeCount,+ criticalBettiRankCount,+ criticalBettiTableValues,+ criticalBettiVectors,+ mod2PersistentPairsWithCriticalBettiTable,+ persistentBettiAt,+ persistentBettiAtMany,+ persistentBettiAtCriticalValues, mod2PersistenceTopologyWitness, Bound (..), TargetBetti (..),
src-public/Moonlight/Homology/Boundary.hs view
@@ -32,6 +32,7 @@ boundaryEntries, mkBoundaryIncidence, mkBoundaryIncidenceFromOrderedEntries,+ mkBoundaryIncidenceFromOrderedColumns, overlapBoundaryIncidence, emptyBoundaryIncidence, emptyBoundaryIncidenceOf,
src-public/Moonlight/Homology/Persistence.hs view
@@ -2,9 +2,21 @@ ( BiFilteredCell (..), BiPersistencePair (..), FiltrationValue (..),- FilteredFiniteChainComplex (..),+ FilteredFiniteChainComplex,+ filteredBaseComplex,+ filteredCellBirths,+ filteredCriticalValues, mkFilteredFiniteChainComplex, mod2PersistentPairs,+ CriticalBettiTable,+ criticalBettiDegreeCount,+ criticalBettiRankCount,+ criticalBettiTableValues,+ criticalBettiVectors,+ mod2PersistentPairsWithCriticalBettiTable,+ persistentBettiAt,+ persistentBettiAtMany,+ persistentBettiAtCriticalValues, mod2PersistenceTopologyWitness, ) where@@ -13,10 +25,24 @@ import Moonlight.Homology.Pure.Chain (HomologicalDegree) import Moonlight.Homology.Topology (BasisCellRef) import Moonlight.Homology.Pure.Filtration (FiltrationValue (..))-import Moonlight.Homology.Pure.Topology.Core (FilteredFiniteChainComplex (..))+import Moonlight.Homology.Pure.Topology.Core+ ( FilteredFiniteChainComplex,+ filteredBaseComplex,+ filteredCellBirths,+ filteredCriticalValues,+ ) import Moonlight.Homology.Pure.Topology.Persistence ( mkFilteredFiniteChainComplex, mod2PersistentPairs,+ CriticalBettiTable,+ criticalBettiDegreeCount,+ criticalBettiRankCount,+ criticalBettiTableValues,+ criticalBettiVectors,+ mod2PersistentPairsWithCriticalBettiTable,+ persistentBettiAt,+ persistentBettiAtMany,+ persistentBettiAtCriticalValues, mod2PersistenceTopologyWitness, )
src-topology/Moonlight/Homology/Pure/Topology.hs view
@@ -126,6 +126,15 @@ representativeToVector, exactTopologyWitness, mod2PersistentPairs,+ CriticalBettiTable,+ criticalBettiDegreeCount,+ criticalBettiRankCount,+ criticalBettiTableValues,+ criticalBettiVectors,+ mod2PersistentPairsWithCriticalBettiTable,+ persistentBettiAt,+ persistentBettiAtMany,+ persistentBettiAtCriticalValues, mod2PersistenceTopologyWitness, TopologyTarget (..), TargetViolation (..),@@ -218,7 +227,7 @@ type TopologyWitnessSeed :: Type -> Type data TopologyWitnessSeed r- = GraphTopologySeed Graph1Skeleton (Maybe (FilteredFiniteChainComplex r)) (Maybe ScalarPotentialField) Int+ = GraphTopologySeed Graph1Skeleton (Maybe (FilteredFiniteChainComplex FiltrationValue r)) (Maybe ScalarPotentialField) Int | FiniteTopologySeed (FiniteChainComplex r) (TopologyObservationConfig r) observeTopologyWitnessSeed ::@@ -234,7 +243,7 @@ observeGraphTopologyWitness :: Integral r =>- Maybe (FilteredFiniteChainComplex r) ->+ Maybe (FilteredFiniteChainComplex FiltrationValue r) -> Maybe ScalarPotentialField -> Int -> Graph1Skeleton ->
src-topology/Moonlight/Homology/Pure/Topology/BlockSchur.hs view
@@ -17,13 +17,13 @@ import Data.Bifunctor (first) import Data.Foldable (traverse_)-import Data.IntMap.Strict qualified as IntMap import Data.Kind (Type) import Data.List (transpose) import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Data.Maybe (mapMaybe) import Data.Set qualified as Set+import Data.Vector qualified as Vector import Moonlight.Core (Semiring) import Moonlight.Homology.Boundary.Finite ( FiniteChainComplex,@@ -38,6 +38,7 @@ BoundaryIncidenceShapeError (..), boundaryCoefficient, boundaryEntries,+ boundaryEntriesBySource, composeBoundaryIncidence, emptyBoundaryIncidenceOf, mkBoundaryEntry,@@ -690,7 +691,7 @@ } boundaryOf ::- Map HomologicalDegree (IntMap.IntMap [BoundaryEntry coefficient]) ->+ Map HomologicalDegree (Vector.Vector [BoundaryEntry coefficient]) -> BasisCellRef -> [(coefficient, BasisCellRef)] boundaryOf groupedBoundaryEntries basisRef =@@ -699,26 +700,17 @@ | degreeInt <= 0 -> [] | otherwise -> let targetDegree = HomologicalDegree (degreeInt - 1)- degreeEntries = Map.findWithDefault IntMap.empty (HomologicalDegree degreeInt) groupedBoundaryEntries+ degreeEntries = Map.findWithDefault Vector.empty (HomologicalDegree degreeInt) groupedBoundaryEntries in [ (boundaryCoefficient entry, basisRefAt targetDegree (targetIndex entry))- | entry <- IntMap.findWithDefault [] (cellIndex basisRef) degreeEntries+ | entry <- maybe [] id (degreeEntries Vector.!? cellIndex basisRef) ] boundaryEntriesByDegree :: FiniteChainComplex coefficient ->- Map HomologicalDegree (IntMap.IntMap [BoundaryEntry coefficient])+ Map HomologicalDegree (Vector.Vector [BoundaryEntry coefficient]) boundaryEntriesByDegree complex = Map.fromList ( fmap- (\degreeValue -> (degreeValue, entriesBySource (incidenceMatrixAt complex degreeValue)))+ (\degreeValue -> (degreeValue, boundaryEntriesBySource (incidenceMatrixAt complex degreeValue))) (dimensionsOf complex) )--entriesBySource :: BoundaryIncidence coefficient -> IntMap.IntMap [BoundaryEntry coefficient]-entriesBySource incidence =- foldr- ( \entryValue ->- IntMap.insertWith (<>) (sourceIndex entryValue) [entryValue]- )- IntMap.empty- (boundaryEntries incidence)
src-topology/Moonlight/Homology/Pure/Topology/Core.hs view
@@ -24,8 +24,6 @@ enumerateFromZero, toRationalFromIntegral, mapMaybeWithLookup,- symmetricDifference,- lowIndex, ) where @@ -46,10 +44,25 @@ import Moonlight.Homology.Pure.Matrix.Shape (cellCountAtDegree, dimensionsOf) -type FilteredFiniteChainComplex :: Type -> Type-data FilteredFiniteChainComplex r = FilteredFiniteChainComplex+-- | A finite chain complex indexed by an arbitrary ordered filtration key.+-- The key is deliberately independent of the chain coefficient: exact+-- geometric filtrations must not be rounded through 'FiltrationValue' merely+-- to reach the persistence reducer.+type FilteredFiniteChainComplex :: Type -> Type -> Type+data FilteredFiniteChainComplex filtration r = FilteredFiniteChainComplex { filteredBaseComplex :: FiniteChainComplex r,- filteredCellBirths :: Map.Map BasisCellRef FiltrationValue+ filteredCellBirths :: Map.Map BasisCellRef filtration,+ -- | Exact critical-value equivalence classes in ascending order.+ filteredCriticalValues :: ![filtration],+ -- | Exact critical value to its dense, complex-relative rank.+ filteredBirthValueRanks :: !(Map.Map filtration Int),+ -- | Dense ranks local to this filtered complex. They are a derived+ -- reduction index, never a semantic replacement for the exact birth.+ filteredCellBirthRanks :: Map.Map BasisCellRef Int,+ -- | Cells in the canonical @(birth rank, degree, basis index)@ reduction+ -- order. Construction derives this once from the exact birth quotient;+ -- reducers must not rediscover it through map lookups and a second sort.+ filteredRankedCells :: ![(BasisCellRef, filtration, Int)] } type GraphEdge :: Type@@ -188,13 +201,3 @@ mapMaybeWithLookup :: Ord key => Map.Map key value -> [key] -> [value] mapMaybeWithLookup valueMap = mapMaybe (flip Map.lookup valueMap)--symmetricDifference :: Ord a => Set.Set a -> Set.Set a -> Set.Set a-symmetricDifference left right =- (left `Set.difference` right) `Set.union` (right `Set.difference` left)--lowIndex :: Set.Set Int -> Maybe Int-lowIndex rowSet =- case Set.maxView rowSet of- Nothing -> Nothing- Just (maximumIndexValue, _) -> Just maximumIndexValue
src-topology/Moonlight/Homology/Pure/Topology/Graph/Skeleton.hs view
@@ -17,10 +17,10 @@ import Data.Function ((&)) import Data.Graph qualified as Graph-import Data.IntMap.Strict qualified as IntMap import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Tree qualified as Tree+import Data.Vector qualified as Vector import Moonlight.Core (Semiring) import Moonlight.Homology.Boundary.Finite ( FiniteChainComplex,@@ -32,6 +32,7 @@ BoundaryIncidence, boundaryCoefficient, boundaryEntries,+ boundaryEntriesBySource, emptyBoundaryIncidence, emptyBoundaryIncidenceOf, materializeIncidenceBoundary,@@ -130,20 +131,11 @@ orientedUnitEdgeSupports :: Integral r => BoundaryIncidence r -> Either GraphSkeletonExtractionFailure [(Int, Int)] orientedUnitEdgeSupports edgeBoundary = enumerateFromZero (sourceCardinality edgeBoundary)- & traverse (unitEdgeSupport (entriesBySource edgeBoundary))--entriesBySource :: BoundaryIncidence r -> IntMap.IntMap [BoundaryEntry r]-entriesBySource incidence =- boundaryEntries incidence- & foldr- ( \entryValue ->- IntMap.insertWith (<>) (sourceIndex entryValue) [entryValue]- )- IntMap.empty+ & traverse (unitEdgeSupport (boundaryEntriesBySource edgeBoundary)) -unitEdgeSupport :: Integral r => IntMap.IntMap [BoundaryEntry r] -> Int -> Either GraphSkeletonExtractionFailure (Int, Int)+unitEdgeSupport :: Integral r => Vector.Vector [BoundaryEntry r] -> Int -> Either GraphSkeletonExtractionFailure (Int, Int) unitEdgeSupport groupedEntries edgeIndex =- case traverse signedUnitTarget (filter ((/= 0) . boundaryCoefficient) (IntMap.findWithDefault [] edgeIndex groupedEntries)) of+ case traverse signedUnitTarget (filter ((/= 0) . boundaryCoefficient) (maybe [] id (groupedEntries Vector.!? edgeIndex))) of Just [(leftTarget, leftSign), (rightTarget, rightSign)] | leftTarget /= rightTarget && leftSign + rightSign == 0 -> Right (leftTarget, rightTarget)
src-topology/Moonlight/Homology/Pure/Topology/Observation.hs view
@@ -5,12 +5,13 @@ where import Data.Kind (Type)+import Moonlight.Homology.Pure.Filtration (FiltrationValue) import Moonlight.Homology.Pure.Topology.Core (FilteredFiniteChainComplex) import Moonlight.Homology.Pure.Topology.MacroScaffold (ScalarPotentialField) type TopologyObservationConfig :: Type -> Type data TopologyObservationConfig r = TopologyObservationConfig- { observationFiltration :: Maybe (FilteredFiniteChainComplex r),+ { observationFiltration :: Maybe (FilteredFiniteChainComplex FiltrationValue r), observationPotential :: Maybe ScalarPotentialField, observationLowModeCount :: Int }
src-topology/Moonlight/Homology/Pure/Topology/Persistence.hs view
@@ -1,373 +1,1128 @@ module Moonlight.Homology.Pure.Topology.Persistence ( mkFilteredFiniteChainComplex, mod2PersistentPairs,- mod2PersistenceTopologyWitness,- mod2PersistentBoundaryColumn,- persistentPairs,- persistenceTopologyWitness,- persistentBoundaryColumn,- persistenceEssentialBirths,- reducePersistentColumn,- reduceBoundaryColumn,- materializeFinitePersistencePair,- materializeEssentialPersistencePair,- orderedFilteredCells,- validateBirthCoverage,- validateBirthExactness,- validateFiltrationMonotonicity,- )-where--import Data.Function ((&))-import Data.IntMap.Strict qualified as IntMap-import Data.Kind (Type)-import qualified Data.List as List-import qualified Data.Map.Strict as Map-import Data.Maybe (mapMaybe)-import qualified Data.Set as Set-import Moonlight.Homology.Boundary.Finite- ( FiniteChainComplex,- incidenceMatrixAt,- )-import Moonlight.Homology.Boundary.LinAlg- ( BoundaryEntry,- BoundaryIncidence,- boundaryCoefficient,- boundaryEntries,- sourceIndex,- targetIndex,- )-import Moonlight.Homology.Pure.Chain- ( HomologicalDegree (..),- PersistencePair (..),- TopologyWitness (..),- decrementDegree,- emptyTopologyWitness,- )-import Moonlight.Homology.Pure.Failure (HomologyFailure (..))-import Moonlight.Homology.Pure.Topology.Core--type OrderedFilteredCell :: Type-data OrderedFilteredCell = OrderedFilteredCell- { orderedCellIdentity :: BasisCellRef,- orderedCellBirth :: FiltrationValue- }- deriving stock (Eq, Show)--type PersistenceState :: Type-data PersistenceState = PersistenceState- { persistenceLowColumns :: !(Map.Map Int (Set.Set Int)),- persistencePairsByIndex :: ![(Int, Int)],- persistenceCreators :: !(Set.Set Int)- }--emptyPersistenceState :: PersistenceState-emptyPersistenceState =- PersistenceState- { persistenceLowColumns = Map.empty,- persistencePairsByIndex = [],- persistenceCreators = Set.empty- }--mkFilteredFiniteChainComplex ::- Integral r =>- FiniteChainComplex r ->- [(BasisCellRef, FiltrationValue)] ->- Either HomologyFailure (FilteredFiniteChainComplex r)-mkFilteredFiniteChainComplex finite births = do- let birthMap = Map.fromList births- validateBirthUniqueness births birthMap- validateBirthCoverage finite birthMap- validateBirthExactness finite birthMap- validateFiltrationMonotonicity finite birthMap- pure- FilteredFiniteChainComplex- { filteredBaseComplex = finite,- filteredCellBirths = birthMap- }--mod2PersistentPairs ::- Integral r =>- FilteredFiniteChainComplex r ->- Either HomologyFailure [PersistencePair FiltrationValue]-mod2PersistentPairs filtered = do- let orderedCells = orderedFilteredCells filtered- orderedCellByIndex =- orderedCells- & zip [0 :: Int ..]- & Map.fromList- globalIndexByCell =- orderedCells- & zip [0 :: Int ..]- & fmap (\(globalIndexValue, orderedCell) -> (orderedCellIdentity orderedCell, globalIndexValue))- & Map.fromList- groupedBoundaryEntries = boundaryEntriesByDegree (filteredBaseComplex filtered)- boundaryColumns = fmap (mod2PersistentBoundaryColumnFromIndex groupedBoundaryEntries globalIndexByCell) orderedCells- stateAfterReduction = foldl' reducePersistentColumn emptyPersistenceState (zip [0 :: Int ..] boundaryColumns)- finitePairs =- persistencePairsByIndex stateAfterReduction- & reverse- & mapMaybe (uncurry (materializeFinitePersistencePair orderedCellByIndex))- essentialPairs =- persistenceEssentialBirths stateAfterReduction- & Set.toAscList- & mapMaybe (materializeEssentialPersistencePair orderedCellByIndex)- pure (finitePairs <> essentialPairs)--persistentPairs ::- Integral r =>- FilteredFiniteChainComplex r ->- Either HomologyFailure [PersistencePair FiltrationValue]-{-# DEPRECATED persistentPairs "Use mod2PersistentPairs — this computes mod-2 persistence despite its Integral constraint" #-}-persistentPairs = mod2PersistentPairs--mod2PersistenceTopologyWitness ::- Integral r =>- FilteredFiniteChainComplex r ->- Either HomologyFailure (TopologyWitness scaffold spectral FiltrationValue coefficient basis)-mod2PersistenceTopologyWitness filtered = do- pairs <- mod2PersistentPairs filtered- pure- emptyTopologyWitness- { topologyPersistencePairs = pairs- }--persistenceTopologyWitness ::- Integral r =>- FilteredFiniteChainComplex r ->- Either HomologyFailure (TopologyWitness scaffold spectral FiltrationValue coefficient basis)-{-# DEPRECATED persistenceTopologyWitness "Use mod2PersistenceTopologyWitness — this computes mod-2 persistence despite its Integral constraint" #-}-persistenceTopologyWitness = mod2PersistenceTopologyWitness--persistenceEssentialBirths :: PersistenceState -> Set.Set Int-persistenceEssentialBirths stateValue =- let pairedBirths = persistencePairsByIndex stateValue & fmap fst & Set.fromList- in persistenceCreators stateValue `Set.difference` pairedBirths--reducePersistentColumn :: PersistenceState -> (Int, Set.Set Int) -> PersistenceState-reducePersistentColumn stateValue (columnIndexValue, initialColumn) =- let reducedColumn = reduceBoundaryColumn (persistenceLowColumns stateValue) initialColumn- in case lowIndex reducedColumn of- Nothing ->- stateValue- { persistenceCreators = Set.insert columnIndexValue (persistenceCreators stateValue)- }- Just lowValue ->- stateValue- { persistenceLowColumns = Map.insert lowValue reducedColumn (persistenceLowColumns stateValue),- persistencePairsByIndex = (lowValue, columnIndexValue) : persistencePairsByIndex stateValue- }--reduceBoundaryColumn :: Map.Map Int (Set.Set Int) -> Set.Set Int -> Set.Set Int-reduceBoundaryColumn lowColumns columnValue =- case lowIndex columnValue >>= (`Map.lookup` lowColumns) of- Nothing -> columnValue- Just pivotColumn -> reduceBoundaryColumn lowColumns (symmetricDifference columnValue pivotColumn)--materializeFinitePersistencePair ::- Map.Map Int OrderedFilteredCell ->- Int ->- Int ->- Maybe (PersistencePair FiltrationValue)-materializeFinitePersistencePair orderedCellByIndex birthIndexValue deathIndexValue =- case (Map.lookup birthIndexValue orderedCellByIndex, Map.lookup deathIndexValue orderedCellByIndex) of- (Just birthCell, Just deathCell) ->- Just- PersistencePair- { persistenceDegree = cellDegree (orderedCellIdentity birthCell),- persistenceBirth = orderedCellBirth birthCell,- persistenceDeath = Just (orderedCellBirth deathCell)- }- _ -> Nothing--materializeEssentialPersistencePair ::- Map.Map Int OrderedFilteredCell ->- Int ->- Maybe (PersistencePair FiltrationValue)-materializeEssentialPersistencePair orderedCellByIndex birthIndexValue =- Map.lookup birthIndexValue orderedCellByIndex- & fmap- ( \birthCell ->- PersistencePair- { persistenceDegree = cellDegree (orderedCellIdentity birthCell),- persistenceBirth = orderedCellBirth birthCell,- persistenceDeath = Nothing- }- )--orderedFilteredCells :: FilteredFiniteChainComplex r -> [OrderedFilteredCell]-orderedFilteredCells filtered =- allBasisCellRefs (filteredBaseComplex filtered)- & mapMaybe- ( \cellRefValue ->- fmap (OrderedFilteredCell cellRefValue)- (Map.lookup cellRefValue (filteredCellBirths filtered))- )- & List.sortOn- ( \orderedCell ->- let degreeValue = cellDegree (orderedCellIdentity orderedCell)- in ( orderedCellBirth orderedCell,- unHomologicalDegree degreeValue,- cellIndex (orderedCellIdentity orderedCell)- )- )--mod2PersistentBoundaryColumn ::- Integral r =>- FilteredFiniteChainComplex r ->- Map.Map BasisCellRef Int ->- OrderedFilteredCell ->- Set.Set Int-mod2PersistentBoundaryColumn filtered globalIndexByCell orderedCell =- let cellRefValue = orderedCellIdentity orderedCell- degreeValue = cellDegree cellRefValue- incidence = incidenceMatrixAt (filteredBaseComplex filtered) degreeValue- in mod2PersistentBoundaryColumnFromEntries (entriesBySource incidence) globalIndexByCell orderedCell--mod2PersistentBoundaryColumnFromIndex ::- Integral r =>- Map.Map HomologicalDegree (IntMap.IntMap [BoundaryEntry r]) ->- Map.Map BasisCellRef Int ->- OrderedFilteredCell ->- Set.Set Int-mod2PersistentBoundaryColumnFromIndex groupedBoundaryEntries globalIndexByCell orderedCell =- let cellRefValue = orderedCellIdentity orderedCell- degreeEntries = Map.findWithDefault IntMap.empty (cellDegree cellRefValue) groupedBoundaryEntries- in mod2PersistentBoundaryColumnFromEntries degreeEntries globalIndexByCell orderedCell--mod2PersistentBoundaryColumnFromEntries ::- Integral r =>- IntMap.IntMap [BoundaryEntry r] ->- Map.Map BasisCellRef Int ->- OrderedFilteredCell ->- Set.Set Int-mod2PersistentBoundaryColumnFromEntries groupedEntries globalIndexByCell orderedCell =- let cellRefValue = orderedCellIdentity orderedCell- degreeValue = cellDegree cellRefValue- in IntMap.findWithDefault [] (cellIndex cellRefValue) groupedEntries- & filter (\entry -> odd (abs (boundaryCoefficient entry)))- & fmap- ( \entry ->- BasisCellRef- { cellDegree = decrementDegree degreeValue,- cellIndex = targetIndex entry- }- )- & mapMaybeWithLookup globalIndexByCell- & Set.fromList--boundaryEntriesByDegree ::- FiniteChainComplex r ->- Map.Map HomologicalDegree (IntMap.IntMap [BoundaryEntry r])-boundaryEntriesByDegree finite =- dimensionsOf finite- & fmap (\degreeValue -> (degreeValue, entriesBySource (incidenceMatrixAt finite degreeValue)))- & Map.fromList--entriesBySource :: BoundaryIncidence r -> IntMap.IntMap [BoundaryEntry r]-entriesBySource incidence =- boundaryEntries incidence- & foldr- ( \entryValue ->- IntMap.insertWith (<>) (sourceIndex entryValue) [entryValue]- )- IntMap.empty--persistentBoundaryColumn ::- Integral r =>- FilteredFiniteChainComplex r ->- Map.Map BasisCellRef Int ->- OrderedFilteredCell ->- Set.Set Int-{-# DEPRECATED persistentBoundaryColumn "Use mod2PersistentBoundaryColumn — this computes mod-2 boundary despite its Integral constraint" #-}-persistentBoundaryColumn = mod2PersistentBoundaryColumn--validateBirthUniqueness ::- [(BasisCellRef, FiltrationValue)] ->- Map.Map BasisCellRef FiltrationValue ->- Either HomologyFailure ()-validateBirthUniqueness births birthMap =- if length births == Map.size birthMap- then Right ()- else Left (InvalidTopologyInput "duplicate birth assignments for the same cell")--validateBirthCoverage ::- FiniteChainComplex r ->- Map.Map BasisCellRef FiltrationValue ->- Either HomologyFailure ()-validateBirthCoverage finite birthMap =- allBasisCellRefs finite- & List.find (\cellRefValue -> Map.notMember cellRefValue birthMap)- & maybe (Right ()) missingCellFailure- where- missingCellFailure :: BasisCellRef -> Either HomologyFailure ()- missingCellFailure cellRefValue =- Left- ( InvalidTopologyInput- ( "missing filtration value for cell "- <> show cellRefValue- )- )--validateBirthExactness ::- FiniteChainComplex r ->- Map.Map BasisCellRef FiltrationValue ->- Either HomologyFailure ()-validateBirthExactness finite birthMap =- let basisSet = Set.fromList (allBasisCellRefs finite)- extraKeys = Map.keysSet birthMap `Set.difference` basisSet- in if Set.null extraKeys- then Right ()- else- Left- ( InvalidTopologyInput- ( "birth map contains cells absent from the chain complex: "- <> show (Set.toList extraKeys)- )- )--validateFiltrationMonotonicity ::- Integral r =>- FiniteChainComplex r ->- Map.Map BasisCellRef FiltrationValue ->- Either HomologyFailure ()-validateFiltrationMonotonicity finite birthMap =- filtrationViolations- & List.find (const True)- & maybe (Right ()) (Left . InvalidTopologyInput)- where- filtrationViolations =- dimensionsOf finite- >>= ( \degreeValue@(HomologicalDegree degreeIndex) ->- if degreeIndex <= 0- then []- else- let incidence = incidenceMatrixAt finite degreeValue- in boundaryEntries incidence- & filter (\entry -> boundaryCoefficient entry /= 0)- & mapMaybe- ( \entry ->- let sourceCell =- BasisCellRef- { cellDegree = degreeValue,- cellIndex = sourceIndex entry- }- targetCell =- BasisCellRef- { cellDegree = decrementDegree degreeValue,- cellIndex = targetIndex entry- }- in case (Map.lookup sourceCell birthMap, Map.lookup targetCell birthMap) of- (Just sourceBirth, Just targetBirth) ->- if targetBirth <= sourceBirth- then Nothing- else- Just- ( "filtration violates face monotonicity for "- <> show sourceCell- <> " -> "- <> show targetCell- )- _ -> Nothing- )- )+ CriticalBettiTable,+ criticalBettiDegreeCount,+ criticalBettiRankCount,+ criticalBettiTableValues,+ criticalBettiVectors,+ mod2PersistentPairsWithCriticalBettiTable,+ persistentBettiAt,+ persistentBettiAtMany,+ persistentBettiAtCriticalValues,+ mod2PersistenceTopologyWitness,+ mod2PersistentBoundaryColumn,+ persistentPairs,+ persistenceTopologyWitness,+ persistentBoundaryColumn,+ persistenceEssentialBirths,+ materializeFinitePersistencePair,+ materializeEssentialPersistencePair,+ orderedFilteredCells,+ validateBirthCoverage,+ validateBirthExactness,+ validateFiltrationMonotonicity,+ )+where++import Control.Monad (foldM)+import Control.Monad.ST (ST, runST)+import Data.Foldable (traverse_)+import Data.Function ((&))+import Data.IntMap.Strict qualified as IntMap+import Data.IntSet qualified as IntSet+import Data.Kind (Type)+import qualified Data.List as List+import qualified Data.Map.Strict as Map+import Data.Maybe (mapMaybe)+import qualified Data.Set as Set+import Data.Vector qualified as Vector+import Data.Vector.Mutable qualified as BoxedMutableVector+import Data.Vector.Unboxed qualified as UnboxedVector+import Data.Vector.Unboxed.Mutable qualified as MutableVector+import Moonlight.Homology.Boundary.Finite+ ( FiniteChainComplex,+ incidenceMatrixAt,+ )+import Moonlight.Homology.Boundary.LinAlg+ ( BoundaryEntry,+ BoundaryIncidence,+ boundaryCoefficient,+ boundaryEntriesBySource,+ sourceIndex,+ targetIndex,+ )+import Moonlight.Homology.Pure.Chain+ ( HomologicalDegree (..),+ PersistencePair (..),+ TopologyWitness (..),+ decrementDegree,+ emptyTopologyWitness,+ )+import Moonlight.Homology.Pure.Failure (HomologyFailure (..))+import Moonlight.Homology.Pure.Topology.Core++type OrderedFilteredCell :: Type -> Type+data OrderedFilteredCell filtration = OrderedFilteredCell+ { orderedCellIdentity :: BasisCellRef,+ orderedCellBirth :: filtration,+ orderedCellBirthRank :: Int+ }+ deriving stock (Eq, Show)++-- | Dense Betti profiles over the finite complex's critical-rank and degree+-- cover. Values are stored rank-major in one unboxed vector; the constructor+-- is withheld so the two dimensions cannot disagree with its storage.+data CriticalBettiTable = CriticalBettiTable+ { criticalBettiDegreeCount :: !Int,+ criticalBettiRankCount :: !Int,+ criticalBettiTableValues :: !(UnboxedVector.Vector Int)+ }+ deriving stock (Eq, Show)++-- | Materialize the familiar rank-indexed Betti vectors from the dense table.+criticalBettiVectors :: CriticalBettiTable -> [[Int]]+criticalBettiVectors table =+ fmap profileAt [0 .. criticalBettiRankCount table - 1]+ where+ degreeCount = criticalBettiDegreeCount table+ values = criticalBettiTableValues table+ profileAt rankValue =+ values+ & UnboxedVector.drop (rankValue * degreeCount)+ & UnboxedVector.take degreeCount+ & UnboxedVector.toList++type PersistenceState :: Type+data PersistenceState = PersistenceState+ { persistencePairsByIndex :: ![(Int, Int)],+ persistencePairedBirths :: !IntSet.IntSet,+ persistenceCreators :: !IntSet.IntSet+ }++type BettiSweepPoint :: Type+data BettiSweepPoint = BettiSweepPoint+ { bettiSweepDelta :: !(Map.Map HomologicalDegree Int),+ bettiSweepRequested :: !Bool+ }++emptyPersistenceState :: PersistenceState+emptyPersistenceState =+ PersistenceState+ { persistencePairsByIndex = [],+ persistencePairedBirths = IntSet.empty,+ persistenceCreators = IntSet.empty+ }++mkFilteredFiniteChainComplex ::+ (Integral r, Ord filtration) =>+ FiniteChainComplex r ->+ [(BasisCellRef, filtration)] ->+ Either HomologyFailure (FilteredFiniteChainComplex filtration r)+mkFilteredFiniteChainComplex finite births = do+ let birthMap = Map.fromList births+ validateBirthUniqueness births birthMap+ denseBirthsByDegree <- denseBirthSection finite birthMap+ let expectedCellCount = Vector.sum (fmap Vector.length denseBirthsByDegree)+ if Map.size birthMap == expectedCellCount+ then Right ()+ else validateBirthExactness finite birthMap+ let birthSections =+ Map.foldlWithKey'+ (\sections cellRefValue birthValue -> Map.insertWith (<>) birthValue [cellRefValue] sections)+ Map.empty+ birthMap+ & Map.toAscList+ rankedBirthSections = zip [0 :: Int ..] birthSections+ criticalValues = fmap (fst . snd) rankedBirthSections+ birthValueRanks =+ rankedBirthSections+ & fmap (\(rankValue, (birthValue, _)) -> (birthValue, rankValue))+ & Map.fromDistinctAscList+ cellBirthRanks =+ rankedBirthSections+ >>= ( \(rankValue, (_, cellsAtBirth)) ->+ fmap (\cellRefValue -> (cellRefValue, rankValue)) cellsAtBirth+ )+ & Map.fromList+ rankedCells =+ rankedBirthSections+ >>= ( \(rankValue, (birthValue, cellsAtBirth)) ->+ reverse cellsAtBirth+ & fmap (\cellRefValue -> (cellRefValue, birthValue, rankValue))+ )+ validateDenseFiltrationMonotonicity finite denseBirthsByDegree+ pure+ FilteredFiniteChainComplex+ { filteredBaseComplex = finite,+ filteredCellBirths = birthMap,+ filteredCriticalValues = criticalValues,+ filteredBirthValueRanks = birthValueRanks,+ filteredCellBirthRanks = cellBirthRanks,+ filteredRankedCells = rankedCells+ }++mod2PersistentPairs ::+ Integral r =>+ FilteredFiniteChainComplex filtration r ->+ Either HomologyFailure [PersistencePair filtration]+mod2PersistentPairs filtered =+ let (orderedCellByIndex, stateAfterReduction) = reduceMod2Persistence filtered+ in Right (materializePersistencePairs orderedCellByIndex stateAfterReduction)++-- | Reduce once, then derive both the exact barcode and all critical-rank+-- Betti profiles from the same indexed witness. The fused observation avoids+-- throwing away proven cell ranks only to recover them through exact-value+-- map lookups in 'persistentBettiAtCriticalValues'.+mod2PersistentPairsWithCriticalBettiTable ::+ Integral r =>+ FilteredFiniteChainComplex filtration r ->+ Either+ HomologyFailure+ ( [PersistencePair filtration]+ , CriticalBettiTable+ )+mod2PersistentPairsWithCriticalBettiTable filtered =+ let (orderedCellByIndex, stateAfterReduction) = reduceMod2Persistence filtered+ pairs = materializePersistencePairs orderedCellByIndex stateAfterReduction+ criticalBettiTable =+ criticalBettiTableFromRankedEvents+ (length (dimensionsOf (filteredBaseComplex filtered)))+ (length (filteredCriticalValues filtered))+ (persistenceStateRankedEvents orderedCellByIndex stateAfterReduction)+ in Right (pairs, criticalBettiTable)++reduceMod2Persistence ::+ Integral r =>+ FilteredFiniteChainComplex filtration r ->+ (Vector.Vector (OrderedFilteredCell filtration), PersistenceState)+reduceMod2Persistence filtered =+ let orderedCells = orderedFilteredCells filtered+ orderedCellByIndex =+ orderedCells+ & Vector.fromList+ globalIndicesByDegree =+ denseGlobalIndicesByDegree+ (filteredBaseComplex filtered)+ (zip [0 :: Int ..] orderedCells)+ groupedBoundaryEntries = boundaryEntriesByDegree (filteredBaseComplex filtered)+ degreeCount = length (dimensionsOf (filteredBaseComplex filtered))+ indexedCellsByDegree =+ zip [0 :: Int ..] orderedCells+ & mapMaybe (indexedCellDegreeUpdate degreeCount)+ & Vector.accum+ (flip (:))+ (Vector.replicate degreeCount [])+ & fmap reverse+ higherDegreeColumns =+ indexedCellsByDegree+ & Vector.drop 2+ & fmap+ ( fmap+ ( \(columnIndexValue, orderedCell) ->+ ( columnIndexValue+ , mod2PersistentBoundaryColumnFromIndex+ groupedBoundaryEntries+ globalIndicesByDegree+ orderedCell+ )+ )+ )+ & Vector.toList+ & reverse+ degreeOneColumns =+ maybe [] id (indexedCellsByDegree Vector.!? 1)+ & fmap+ ( \(columnIndexValue, orderedCell) ->+ ( columnIndexValue+ , mod2PersistentBoundaryTargetsFromIndex+ groupedBoundaryEntries+ globalIndicesByDegree+ orderedCell+ )+ )+ degreeZeroIndices =+ maybe [] id (indexedCellsByDegree Vector.!? 0)+ & fmap fst+ stateAfterReduction =+ reducePersistentColumnsByDegree+ (length orderedCells)+ higherDegreeColumns+ degreeOneColumns+ degreeZeroIndices+ in (orderedCellByIndex, stateAfterReduction)++indexedCellDegreeUpdate ::+ Int ->+ (Int, OrderedFilteredCell filtration) ->+ Maybe (Int, (Int, OrderedFilteredCell filtration))+indexedCellDegreeUpdate degreeCount indexedCell@(_, orderedCell) =+ let degreeIndex =+ unHomologicalDegree (cellDegree (orderedCellIdentity orderedCell))+ in if degreeIndex < 0 || degreeIndex >= degreeCount+ then Nothing+ else Just (degreeIndex, indexedCell)++materializePersistencePairs ::+ Vector.Vector (OrderedFilteredCell filtration) ->+ PersistenceState ->+ [PersistencePair filtration]+materializePersistencePairs orderedCellByIndex stateAfterReduction =+ let finitePairs =+ persistencePairsByIndex stateAfterReduction+ & List.sortOn snd+ & mapMaybe (uncurry (materializeFinitePersistencePair orderedCellByIndex))+ essentialPairs =+ persistenceEssentialBirths stateAfterReduction+ & IntSet.toAscList+ & mapMaybe (materializeEssentialPersistencePair orderedCellByIndex)+ in finitePairs <> essentialPairs++persistenceStateRankedEvents ::+ Vector.Vector (OrderedFilteredCell filtration) ->+ PersistenceState ->+ [(Int, (Int, Int))]+persistenceStateRankedEvents orderedCellByIndex stateAfterReduction =+ let finiteEvents =+ persistencePairsByIndex stateAfterReduction+ >>= ( \(birthIndexValue, deathIndexValue) ->+ case+ ( orderedCellByIndex Vector.!? birthIndexValue+ , orderedCellByIndex Vector.!? deathIndexValue+ )+ of+ (Just birthCell, Just deathCell) ->+ rankedIntervalEvents+ (cellDegree (orderedCellIdentity birthCell))+ (orderedCellBirthRank birthCell)+ (Just (orderedCellBirthRank deathCell))+ _ -> []+ )+ essentialEvents =+ persistenceEssentialBirths stateAfterReduction+ & IntSet.toAscList+ >>= ( \birthIndexValue ->+ maybe+ []+ ( \birthCell ->+ rankedIntervalEvents+ (cellDegree (orderedCellIdentity birthCell))+ (orderedCellBirthRank birthCell)+ Nothing+ )+ (orderedCellByIndex Vector.!? birthIndexValue)+ )+ in finiteEvents <> essentialEvents++-- | Read the Betti numbers of one closed filtration sublevel from an already+-- reduced barcode. The returned map is sparse: absent degrees have Betti+-- number zero. Finite persistence intervals are half-open, so a class whose+-- death equals the requested threshold is no longer present after every cell+-- at that threshold has entered the complex.+persistentBettiAt ::+ Ord filtration =>+ filtration ->+ [PersistencePair filtration] ->+ Map.Map HomologicalDegree Int+persistentBettiAt threshold =+ Map.fromListWith (+)+ . fmap (\pairValue -> (persistenceDegree pairValue, 1))+ . filter (persistencePairAliveAt threshold)++-- | Read several closed filtration sublevels in one ordered sweep over the+-- barcode. Results retain the supplied threshold order and multiplicity.+-- Unlike mapping 'persistentBettiAt', this does not rescan every persistence+-- interval for every requested threshold.+--+-- Finite intervals use the same half-open semantics as 'persistentBettiAt'. A+-- birth and death at the same threshold cancel before that threshold is+-- observed. Malformed finite intervals whose death does not follow their birth+-- contribute nowhere, matching the pointwise query.+persistentBettiAtMany ::+ Ord filtration =>+ [filtration] ->+ [PersistencePair filtration] ->+ [Map.Map HomologicalDegree Int]+persistentBettiAtMany thresholds pairs =+ let requestedPoints =+ thresholds+ & fmap (\threshold -> (threshold, requestedBettiSweepPoint))+ & Map.fromListWith mergeBettiSweepPoints+ eventPoints =+ foldl'+ insertPersistencePairSweepPoints+ Map.empty+ pairs+ sweepPoints = Map.unionWith mergeBettiSweepPoints eventPoints requestedPoints+ observedByThreshold =+ sweepPoints+ & Map.mapAccumWithKey applyBettiSweepPoint Map.empty+ & snd+ & Map.mapMaybe id+ in fmap+ (\threshold -> Map.findWithDefault Map.empty threshold observedByThreshold)+ thresholds++-- | Read every exact critical sublevel of one admitted filtered complex. The+-- complex's cached exact-value quotient transports pair endpoints to dense+-- ranks, performs the sweep over 'Int', then returns profiles aligned with+-- 'filteredCriticalValues'. Exact births remain authoritative; ranks never+-- escape this derived observation.+persistentBettiAtCriticalValues ::+ Ord filtration =>+ FilteredFiniteChainComplex filtration r ->+ [PersistencePair filtration] ->+ Either HomologyFailure [Map.Map HomologicalDegree Int]+persistentBettiAtCriticalValues filtered pairs = do+ rankedPairs <- traverse (rankPersistencePair filtered) pairs+ pure+ ( criticalProfilesFromRankedEvents+ (length (filteredCriticalValues filtered))+ (rankedPairs >>= rankedPersistencePairEvents)+ )++criticalProfilesFromRankedEvents ::+ Int ->+ [(Int, (Int, Int))] ->+ [Map.Map HomologicalDegree Int]+criticalProfilesFromRankedEvents criticalRankCount rankedEvents =+ let eventDeltas =+ Vector.accum+ insertRankedBettiEvent+ (Vector.replicate criticalRankCount IntMap.empty)+ rankedEvents+ (_, criticalProfiles) =+ List.mapAccumL+ applyRankedBettiEvents+ IntMap.empty+ (Vector.toList eventDeltas)+ in criticalProfiles++criticalBettiTableFromRankedEvents ::+ Int ->+ Int ->+ [(Int, (Int, Int))] ->+ CriticalBettiTable+criticalBettiTableFromRankedEvents degreeCount criticalRankCount rankedEvents =+ let eventDeltas =+ Vector.accum+ insertRankedBettiEvent+ (Vector.replicate criticalRankCount IntMap.empty)+ rankedEvents+ degreeIndices = [0 .. degreeCount - 1]+ values =+ UnboxedVector.create $ do+ mutableValues <-+ MutableVector.new (max 0 (degreeCount * criticalRankCount))+ _ <-+ Vector.ifoldM'+ ( \currentBetti rankValue rankEvents -> do+ let nextBetti = nextRankedBetti currentBetti rankEvents+ traverse_+ ( \degreeValue ->+ MutableVector.write+ mutableValues+ (rankValue * degreeCount + degreeValue)+ (IntMap.findWithDefault 0 degreeValue nextBetti)+ )+ degreeIndices+ pure nextBetti+ )+ IntMap.empty+ eventDeltas+ pure mutableValues+ in CriticalBettiTable+ { criticalBettiDegreeCount = degreeCount,+ criticalBettiRankCount = criticalRankCount,+ criticalBettiTableValues = values+ }++rankPersistencePair ::+ Ord filtration =>+ FilteredFiniteChainComplex filtration r ->+ PersistencePair filtration ->+ Either HomologyFailure (PersistencePair Int)+rankPersistencePair filtered pairValue = do+ birthRank <-+ maybe+ (Left (InvalidTopologyInput "persistence-pair birth is absent from the filtered complex"))+ Right+ (Map.lookup (persistenceBirth pairValue) (filteredBirthValueRanks filtered))+ deathRank <-+ traverse+ ( \deathValue ->+ maybe+ (Left (InvalidTopologyInput "persistence-pair death is absent from the filtered complex"))+ Right+ (Map.lookup deathValue (filteredBirthValueRanks filtered))+ )+ (persistenceDeath pairValue)+ pure+ PersistencePair+ { persistenceDegree = persistenceDegree pairValue,+ persistenceBirth = birthRank,+ persistenceDeath = deathRank+ }++rankedPersistencePairEvents ::+ PersistencePair Int ->+ [(Int, (Int, Int))]+rankedPersistencePairEvents pairValue =+ rankedIntervalEvents+ (persistenceDegree pairValue)+ (persistenceBirth pairValue)+ (persistenceDeath pairValue)++rankedIntervalEvents ::+ HomologicalDegree ->+ Int ->+ Maybe Int ->+ [(Int, (Int, Int))]+rankedIntervalEvents degreeValue birthRank deathRank =+ case deathRank of+ Nothing -> [rankedBettiEventAt 1 birthRank degreeValue]+ Just finiteDeathRank ->+ if birthRank < finiteDeathRank+ then+ [ rankedBettiEventAt 1 birthRank degreeValue+ , rankedBettiEventAt (-1) finiteDeathRank degreeValue+ ]+ else []++rankedBettiEventAt ::+ Int ->+ Int ->+ HomologicalDegree ->+ (Int, (Int, Int))+rankedBettiEventAt deltaValue criticalRank degreeValue =+ (criticalRank, (unHomologicalDegree degreeValue, deltaValue))++insertRankedBettiEvent ::+ IntMap.IntMap Int ->+ (Int, Int) ->+ IntMap.IntMap Int+insertRankedBettiEvent eventDeltas (degreeValue, deltaValue) =+ IntMap.insertWith+ (+)+ degreeValue+ deltaValue+ eventDeltas++applyRankedBettiEvents ::+ IntMap.IntMap Int ->+ IntMap.IntMap Int ->+ (IntMap.IntMap Int, Map.Map HomologicalDegree Int)+applyRankedBettiEvents currentBetti eventDeltas =+ let nextBetti = nextRankedBetti currentBetti eventDeltas+ publicBetti =+ nextBetti+ & IntMap.foldrWithKey+ ( \degreeValue countValue remainingDegrees ->+ (HomologicalDegree degreeValue, countValue) : remainingDegrees+ )+ []+ & Map.fromDistinctAscList+ in (nextBetti, publicBetti)++nextRankedBetti ::+ IntMap.IntMap Int ->+ IntMap.IntMap Int ->+ IntMap.IntMap Int+nextRankedBetti currentBetti eventDeltas =+ IntMap.unionWith (+) currentBetti eventDeltas+ & IntMap.filter (/= 0)++requestedBettiSweepPoint :: BettiSweepPoint+requestedBettiSweepPoint =+ BettiSweepPoint+ { bettiSweepDelta = Map.empty,+ bettiSweepRequested = True+ }++insertPersistencePairSweepPoints ::+ Ord filtration =>+ Map.Map filtration BettiSweepPoint ->+ PersistencePair filtration ->+ Map.Map filtration BettiSweepPoint+insertPersistencePairSweepPoints eventPoints pairValue =+ case persistenceDeath pairValue of+ Nothing -> insertPersistencePairDelta 1 (persistenceBirth pairValue) pairValue eventPoints+ Just deathValue ->+ if persistenceBirth pairValue < deathValue+ then+ eventPoints+ & insertPersistencePairDelta 1 (persistenceBirth pairValue) pairValue+ & insertPersistencePairDelta (-1) deathValue pairValue+ else eventPoints++insertPersistencePairDelta ::+ Ord filtration =>+ Int ->+ filtration ->+ PersistencePair filtration ->+ Map.Map filtration BettiSweepPoint ->+ Map.Map filtration BettiSweepPoint+insertPersistencePairDelta deltaValue threshold pairValue =+ Map.insertWith+ mergeBettiSweepPoints+ threshold+ (persistencePairDelta deltaValue pairValue)++persistencePairDelta :: Int -> PersistencePair filtration -> BettiSweepPoint+persistencePairDelta deltaValue pairValue =+ BettiSweepPoint+ { bettiSweepDelta = Map.singleton (persistenceDegree pairValue) deltaValue,+ bettiSweepRequested = False+ }++mergeBettiSweepPoints :: BettiSweepPoint -> BettiSweepPoint -> BettiSweepPoint+mergeBettiSweepPoints leftPoint rightPoint =+ BettiSweepPoint+ { bettiSweepDelta = Map.unionWith (+) (bettiSweepDelta leftPoint) (bettiSweepDelta rightPoint),+ bettiSweepRequested = bettiSweepRequested leftPoint || bettiSweepRequested rightPoint+ }++applyBettiSweepPoint ::+ Map.Map HomologicalDegree Int ->+ filtration ->+ BettiSweepPoint ->+ (Map.Map HomologicalDegree Int, Maybe (Map.Map HomologicalDegree Int))+applyBettiSweepPoint currentBetti _ pointValue =+ let nextBetti =+ Map.unionWith (+) currentBetti (bettiSweepDelta pointValue)+ & Map.filter (/= 0)+ observation =+ if bettiSweepRequested pointValue+ then Just nextBetti+ else Nothing+ in (nextBetti, observation)++persistencePairAliveAt ::+ Ord filtration =>+ filtration ->+ PersistencePair filtration ->+ Bool+persistencePairAliveAt threshold pairValue =+ persistenceBirth pairValue <= threshold+ && maybe True (threshold <) (persistenceDeath pairValue)++persistentPairs ::+ Integral r =>+ FilteredFiniteChainComplex filtration r ->+ Either HomologyFailure [PersistencePair filtration]+{-# DEPRECATED persistentPairs "Use mod2PersistentPairs — this computes mod-2 persistence despite its Integral constraint" #-}+persistentPairs = mod2PersistentPairs++mod2PersistenceTopologyWitness ::+ Integral r =>+ FilteredFiniteChainComplex filtration r ->+ Either HomologyFailure (TopologyWitness scaffold spectral filtration coefficient basis)+mod2PersistenceTopologyWitness filtered = do+ pairs <- mod2PersistentPairs filtered+ pure+ emptyTopologyWitness+ { topologyPersistencePairs = pairs+ }++persistenceTopologyWitness ::+ Integral r =>+ FilteredFiniteChainComplex filtration r ->+ Either HomologyFailure (TopologyWitness scaffold spectral filtration coefficient basis)+{-# DEPRECATED persistenceTopologyWitness "Use mod2PersistenceTopologyWitness — this computes mod-2 persistence despite its Integral constraint" #-}+persistenceTopologyWitness = mod2PersistenceTopologyWitness++persistenceEssentialBirths :: PersistenceState -> IntSet.IntSet+persistenceEssentialBirths stateValue =+ persistenceCreators stateValue+ `IntSet.difference` persistencePairedBirths stateValue++-- | Reduce one homological degree after all higher-degree pairings are known.+-- A cell already paired as the low simplex of a higher-dimensional column is+-- a proven creator, so the standard persistence clearing theorem lets us+-- replace its column by zero rather than rediscovering that fact by reduction.+reducePersistentDegree ::+ Int ->+ PersistenceState ->+ [(Int, IntSet.IntSet)] ->+ PersistenceState+reducePersistentDegree totalCellCount stateValue indexedColumns =+ let clearedColumnIndices = persistencePairedBirths stateValue+ in runST $ do+ pivotColumns <- BoxedMutableVector.replicate totalCellCount Nothing+ foldM+ (reduceOrClearPersistentColumn pivotColumns clearedColumnIndices)+ stateValue+ indexedColumns++reduceOrClearPersistentColumn ::+ BoxedMutableVector.MVector s (Maybe IntSet.IntSet) ->+ IntSet.IntSet ->+ PersistenceState ->+ (Int, IntSet.IntSet) ->+ ST s PersistenceState+reduceOrClearPersistentColumn pivotColumns clearedColumnIndices stateValue indexedColumn@(columnIndexValue, _) =+ if IntSet.member columnIndexValue clearedColumnIndices+ then+ pure+ stateValue+ { persistenceCreators = IntSet.insert columnIndexValue (persistenceCreators stateValue)+ }+ else reducePersistentColumn pivotColumns stateValue indexedColumn++-- | Reduce one sparse column against the dense pivot section for its+-- homological degree. Global row identifiers are assigned from @[0 .. n)@+-- before boundary materialization, so the fresh arena is a total local view;+-- it cannot escape this degree's sealed 'ST' descent.+reducePersistentColumn ::+ BoxedMutableVector.MVector s (Maybe IntSet.IntSet) ->+ PersistenceState ->+ (Int, IntSet.IntSet) ->+ ST s PersistenceState+reducePersistentColumn pivotColumns stateValue (columnIndexValue, initialColumn) = do+ reducedColumn <- reduceBoundaryColumn pivotColumns initialColumn+ case IntSet.lookupMax reducedColumn of+ Nothing ->+ pure+ stateValue+ { persistenceCreators = IntSet.insert columnIndexValue (persistenceCreators stateValue)+ }+ Just lowValue -> do+ BoxedMutableVector.write pivotColumns lowValue (Just reducedColumn)+ pure+ stateValue+ { persistencePairsByIndex = (lowValue, columnIndexValue) : persistencePairsByIndex stateValue,+ persistencePairedBirths = IntSet.insert lowValue (persistencePairedBirths stateValue)+ }++-- | Descend through the degree cover. Higher-dimensional columns use the+-- general sparse reducer; the graph boundary uses its equivalent elder-rule+-- union-find section when every mod-two one-cell boundary has cardinality zero+-- or two. Any non-graph fiber is a typed local incompatibility with that+-- specialization and glues back to the general reducer instead.+reducePersistentColumnsByDegree ::+ Int ->+ [[(Int, IntSet.IntSet)]] ->+ [(Int, [Int])] ->+ [Int] ->+ PersistenceState+reducePersistentColumnsByDegree totalCellCount higherDegreeColumns degreeOneColumns degreeZeroIndices =+ let stateAfterHigherDegrees =+ foldl'+ (reducePersistentDegree totalCellCount)+ emptyPersistenceState+ higherDegreeColumns+ clearedDegreeOneIndices =+ persistencePairedBirths stateAfterHigherDegrees+ graphReduction =+ reduceGraphBoundaryDegree+ totalCellCount+ clearedDegreeOneIndices+ degreeZeroIndices+ degreeOneColumns+ in case graphReduction of+ Just (graphPairs, graphCreators) ->+ stateAfterHigherDegrees+ { persistencePairsByIndex = graphPairs <> persistencePairsByIndex stateAfterHigherDegrees,+ persistencePairedBirths =+ IntSet.union+ (IntSet.fromList (fmap fst graphPairs))+ (persistencePairedBirths stateAfterHigherDegrees),+ persistenceCreators = IntSet.union graphCreators (persistenceCreators stateAfterHigherDegrees)+ }+ Nothing ->+ let sparseDegreeOneColumns =+ fmap+ (\(columnIndexValue, targets) -> (columnIndexValue, IntSet.fromList targets))+ degreeOneColumns+ degreeZeroColumns =+ fmap (\columnIndexValue -> (columnIndexValue, IntSet.empty)) degreeZeroIndices+ in foldl'+ (reducePersistentDegree totalCellCount)+ stateAfterHigherDegrees+ [sparseDegreeOneColumns, degreeZeroColumns]++reduceGraphBoundaryDegree ::+ Int ->+ IntSet.IntSet ->+ [Int] ->+ [(Int, [Int])] ->+ Maybe ([(Int, Int)], IntSet.IntSet)+reduceGraphBoundaryDegree totalCellCount clearedEdgeIndices vertexIndices edgeColumns =+ runST $ do+ parents <- MutableVector.generate totalCellCount id+ foldM+ (reduceGraphBoundaryColumn parents clearedEdgeIndices)+ (Just ([], IntSet.fromList vertexIndices))+ edgeColumns++reduceGraphBoundaryColumn ::+ MutableVector.MVector s Int ->+ IntSet.IntSet ->+ Maybe ([(Int, Int)], IntSet.IntSet) ->+ (Int, [Int]) ->+ ST s (Maybe ([(Int, Int)], IntSet.IntSet))+reduceGraphBoundaryColumn _ _ Nothing _ = pure Nothing+reduceGraphBoundaryColumn parents clearedEdgeIndices (Just (pairsByIndex, creators)) (edgeIndex, boundaryTargets) =+ if IntSet.member edgeIndex clearedEdgeIndices+ then pure (Just (pairsByIndex, IntSet.insert edgeIndex creators))+ else+ case boundaryTargets of+ [] -> pure (Just (pairsByIndex, IntSet.insert edgeIndex creators))+ [firstVertex, secondVertex] -> do+ firstRoot <- findGraphComponentRoot parents firstVertex+ secondRoot <- findGraphComponentRoot parents secondVertex+ case (firstRoot, secondRoot) of+ (Just firstRootIndex, Just secondRootIndex) ->+ if firstRootIndex == secondRootIndex+ then pure (Just (pairsByIndex, IntSet.insert edgeIndex creators))+ else do+ let olderRoot = min firstRootIndex secondRootIndex+ youngerRoot = max firstRootIndex secondRootIndex+ MutableVector.write parents youngerRoot olderRoot+ pure (Just ((youngerRoot, edgeIndex) : pairsByIndex, creators))+ _ -> pure Nothing+ _ -> pure Nothing++findGraphComponentRoot ::+ MutableVector.MVector s Int ->+ Int ->+ ST s (Maybe Int)+findGraphComponentRoot parents indexValue =+ if indexValue < 0 || indexValue >= MutableVector.length parents+ then pure Nothing+ else do+ parentIndex <- MutableVector.read parents indexValue+ if parentIndex == indexValue+ then pure (Just indexValue)+ else do+ rootIndex <- findGraphComponentRoot parents parentIndex+ traverse+ (\rootValue -> MutableVector.write parents indexValue rootValue >> pure rootValue)+ rootIndex++reduceBoundaryColumn ::+ BoxedMutableVector.MVector s (Maybe IntSet.IntSet) ->+ IntSet.IntSet ->+ ST s IntSet.IntSet+reduceBoundaryColumn pivotColumns columnValue =+ case IntSet.lookupMax columnValue of+ Nothing -> pure columnValue+ Just lowValue -> do+ pivotColumn <- BoxedMutableVector.read pivotColumns lowValue+ case pivotColumn of+ Nothing -> pure columnValue+ Just existingPivot ->+ reduceBoundaryColumn+ pivotColumns+ (IntSet.symmetricDifference columnValue existingPivot)++materializeFinitePersistencePair ::+ Vector.Vector (OrderedFilteredCell filtration) ->+ Int ->+ Int ->+ Maybe (PersistencePair filtration)+materializeFinitePersistencePair orderedCellByIndex birthIndexValue deathIndexValue =+ case (orderedCellByIndex Vector.!? birthIndexValue, orderedCellByIndex Vector.!? deathIndexValue) of+ (Just birthCell, Just deathCell) ->+ Just+ PersistencePair+ { persistenceDegree = cellDegree (orderedCellIdentity birthCell),+ persistenceBirth = orderedCellBirth birthCell,+ persistenceDeath = Just (orderedCellBirth deathCell)+ }+ _ -> Nothing++materializeEssentialPersistencePair ::+ Vector.Vector (OrderedFilteredCell filtration) ->+ Int ->+ Maybe (PersistencePair filtration)+materializeEssentialPersistencePair orderedCellByIndex birthIndexValue =+ orderedCellByIndex Vector.!? birthIndexValue+ & fmap+ ( \birthCell ->+ PersistencePair+ { persistenceDegree = cellDegree (orderedCellIdentity birthCell),+ persistenceBirth = orderedCellBirth birthCell,+ persistenceDeath = Nothing+ }+ )++orderedFilteredCells ::+ FilteredFiniteChainComplex filtration r ->+ [OrderedFilteredCell filtration]+orderedFilteredCells filtered =+ filteredRankedCells filtered+ & fmap+ ( \(cellRefValue, birthValue, birthRank) ->+ OrderedFilteredCell+ { orderedCellIdentity = cellRefValue,+ orderedCellBirth = birthValue,+ orderedCellBirthRank = birthRank+ }+ )++mod2PersistentBoundaryColumn ::+ Integral r =>+ FilteredFiniteChainComplex filtration r ->+ Map.Map BasisCellRef Int ->+ OrderedFilteredCell filtration ->+ IntSet.IntSet+mod2PersistentBoundaryColumn filtered globalIndexByCell orderedCell =+ let cellRefValue = orderedCellIdentity orderedCell+ degreeValue = cellDegree cellRefValue+ incidence = incidenceMatrixAt (filteredBaseComplex filtered) degreeValue+ in mod2PersistentBoundaryColumnFromEntries (boundaryEntriesBySource incidence) globalIndexByCell orderedCell++mod2PersistentBoundaryColumnFromIndex ::+ Integral r =>+ Vector.Vector (Vector.Vector [BoundaryEntry r]) ->+ Vector.Vector (Vector.Vector (Maybe Int)) ->+ OrderedFilteredCell filtration ->+ IntSet.IntSet+mod2PersistentBoundaryColumnFromIndex groupedBoundaryEntries globalIndicesByDegree =+ IntSet.fromList+ . mod2PersistentBoundaryTargetsFromIndex groupedBoundaryEntries globalIndicesByDegree++mod2PersistentBoundaryTargetsFromIndex ::+ Integral r =>+ Vector.Vector (Vector.Vector [BoundaryEntry r]) ->+ Vector.Vector (Vector.Vector (Maybe Int)) ->+ OrderedFilteredCell filtration ->+ [Int]+mod2PersistentBoundaryTargetsFromIndex groupedBoundaryEntries globalIndicesByDegree orderedCell =+ let cellRefValue = orderedCellIdentity orderedCell+ degreeValue = unHomologicalDegree (cellDegree cellRefValue)+ degreeEntries =+ maybe Vector.empty id (groupedBoundaryEntries Vector.!? degreeValue)+ targetIndices =+ maybe+ Vector.empty+ id+ (globalIndicesByDegree Vector.!? (degreeValue - 1))+ in maybe [] id (degreeEntries Vector.!? cellIndex cellRefValue)+ & filter (\entry -> odd (abs (boundaryCoefficient entry)))+ & mapMaybe (\entry -> targetIndices Vector.!? (targetIndex entry) >>= id)++mod2PersistentBoundaryColumnFromEntries ::+ Integral r =>+ Vector.Vector [BoundaryEntry r] ->+ Map.Map BasisCellRef Int ->+ OrderedFilteredCell filtration ->+ IntSet.IntSet+mod2PersistentBoundaryColumnFromEntries groupedEntries globalIndexByCell orderedCell =+ let cellRefValue = orderedCellIdentity orderedCell+ degreeValue = cellDegree cellRefValue+ in maybe [] id (groupedEntries Vector.!? cellIndex cellRefValue)+ & filter (\entry -> odd (abs (boundaryCoefficient entry)))+ & fmap+ ( \entry ->+ BasisCellRef+ { cellDegree = decrementDegree degreeValue,+ cellIndex = targetIndex entry+ }+ )+ & mapMaybeWithLookup globalIndexByCell+ & IntSet.fromList++boundaryEntriesByDegree ::+ FiniteChainComplex r ->+ Vector.Vector (Vector.Vector [BoundaryEntry r])+boundaryEntriesByDegree finite =+ dimensionsOf finite+ & fmap+ ( \degreeValue ->+ boundaryEntriesBySource (incidenceMatrixAt finite degreeValue)+ )+ & Vector.fromList++denseGlobalIndicesByDegree ::+ FiniteChainComplex r ->+ [(Int, OrderedFilteredCell filtration)] ->+ Vector.Vector (Vector.Vector (Maybe Int))+denseGlobalIndicesByDegree finite indexedCells =+ let degreeCount = length (dimensionsOf finite)+ updatesByDegree =+ Vector.accum+ (flip (:))+ (Vector.replicate degreeCount [])+ (mapMaybe (globalIndexUpdate degreeCount) indexedCells)+ in Vector.imap+ ( \degreeIndex updates ->+ Vector.accum+ (\_ globalIndexValue -> Just globalIndexValue)+ ( Vector.replicate+ (cellCountAtDegree finite (HomologicalDegree degreeIndex))+ Nothing+ )+ updates+ )+ updatesByDegree+ where+ globalIndexUpdate ::+ Int ->+ (Int, OrderedFilteredCell filtration) ->+ Maybe (Int, (Int, Int))+ globalIndexUpdate degreeCountValue (globalIndexValue, orderedCell) =+ let cellRefValue = orderedCellIdentity orderedCell+ degreeIndex = unHomologicalDegree (cellDegree cellRefValue)+ in if degreeIndex < 0 || degreeIndex >= degreeCountValue+ then Nothing+ else Just (degreeIndex, (cellIndex cellRefValue, globalIndexValue))++persistentBoundaryColumn ::+ Integral r =>+ FilteredFiniteChainComplex filtration r ->+ Map.Map BasisCellRef Int ->+ OrderedFilteredCell filtration ->+ IntSet.IntSet+{-# DEPRECATED persistentBoundaryColumn "Use mod2PersistentBoundaryColumn — this computes mod-2 boundary despite its Integral constraint" #-}+persistentBoundaryColumn = mod2PersistentBoundaryColumn++validateBirthUniqueness ::+ [(BasisCellRef, filtration)] ->+ Map.Map BasisCellRef filtration ->+ Either HomologyFailure ()+validateBirthUniqueness births birthMap =+ if length births == Map.size birthMap+ then Right ()+ else Left (InvalidTopologyInput "duplicate birth assignments for the same cell")++validateBirthCoverage ::+ FiniteChainComplex r ->+ Map.Map BasisCellRef filtration ->+ Either HomologyFailure ()+validateBirthCoverage finite birthMap =+ () <$ denseBirthSection finite birthMap++validateBirthExactness ::+ FiniteChainComplex r ->+ Map.Map BasisCellRef filtration ->+ Either HomologyFailure ()+validateBirthExactness finite birthMap =+ let basisSet = Set.fromList (allBasisCellRefs finite)+ extraKeys = Map.keysSet birthMap `Set.difference` basisSet+ in if Set.null extraKeys+ then Right ()+ else+ Left+ ( InvalidTopologyInput+ ( "birth map contains cells absent from the chain complex: "+ <> show (Set.toList extraKeys)+ )+ )++validateFiltrationMonotonicity ::+ (Integral r, Ord filtration) =>+ FiniteChainComplex r ->+ Map.Map BasisCellRef filtration ->+ Either HomologyFailure ()+validateFiltrationMonotonicity finite birthMap = do+ denseBirthsByDegree <- denseBirthSection finite birthMap+ validateDenseFiltrationMonotonicity finite denseBirthsByDegree++denseBirthSection ::+ FiniteChainComplex r ->+ Map.Map BasisCellRef filtration ->+ Either HomologyFailure (Vector.Vector (Vector.Vector filtration))+denseBirthSection finite birthMap =+ dimensionsOf finite+ & traverse+ ( \degreeValue ->+ Vector.generateM+ (cellCountAtDegree finite degreeValue)+ ( \indexValue ->+ let cellRefValue = BasisCellRef degreeValue indexValue+ in maybe+ ( Left+ ( InvalidTopologyInput+ ("missing filtration value for cell " <> show cellRefValue)+ )+ )+ Right+ (Map.lookup cellRefValue birthMap)+ )+ )+ & fmap Vector.fromList++validateDenseFiltrationMonotonicity ::+ (Integral r, Ord filtration) =>+ FiniteChainComplex r ->+ Vector.Vector (Vector.Vector filtration) ->+ Either HomologyFailure ()+validateDenseFiltrationMonotonicity finite denseBirthsByDegree =+ dimensionsOf finite+ & mapMaybe+ ( \degreeValue ->+ firstFiltrationViolation+ denseBirthsByDegree+ degreeValue+ (incidenceMatrixAt finite degreeValue)+ )+ & List.find (const True)+ & maybe (Right ()) (Left . InvalidTopologyInput)++firstFiltrationViolation ::+ (Integral r, Ord filtration) =>+ Vector.Vector (Vector.Vector filtration) ->+ HomologicalDegree ->+ BoundaryIncidence r ->+ Maybe String+firstFiltrationViolation denseBirthsByDegree degreeValue@(HomologicalDegree degreeIndex) incidence+ | degreeIndex <= 0 = Nothing+ | otherwise =+ Vector.foldr+ (firstColumnFiltrationViolation denseBirthsByDegree degreeValue)+ Nothing+ (boundaryEntriesBySource incidence)++firstColumnFiltrationViolation ::+ (Integral r, Ord filtration) =>+ Vector.Vector (Vector.Vector filtration) ->+ HomologicalDegree ->+ [BoundaryEntry r] ->+ Maybe String ->+ Maybe String+firstColumnFiltrationViolation denseBirthsByDegree degreeValue entries remainingViolation =+ case List.find (const True) (mapMaybe (filtrationViolationAtEntry denseBirthsByDegree degreeValue) entries) of+ Just violation -> Just violation+ Nothing -> remainingViolation++filtrationViolationAtEntry ::+ (Integral r, Ord filtration) =>+ Vector.Vector (Vector.Vector filtration) ->+ HomologicalDegree ->+ BoundaryEntry r ->+ Maybe String+filtrationViolationAtEntry denseBirthsByDegree degreeValue entry+ | boundaryCoefficient entry == 0 = Nothing+ | otherwise =+ let sourceCell =+ BasisCellRef+ { cellDegree = degreeValue,+ cellIndex = sourceIndex entry+ }+ targetCell =+ BasisCellRef+ { cellDegree = decrementDegree degreeValue,+ cellIndex = targetIndex entry+ }+ in case (denseBirthAt denseBirthsByDegree sourceCell, denseBirthAt denseBirthsByDegree targetCell) of+ (Just sourceBirth, Just targetBirth)+ | targetBirth > sourceBirth ->+ Just+ ( "filtration violates face monotonicity for "+ <> show sourceCell+ <> " -> "+ <> show targetCell+ )+ _ -> Nothing++denseBirthAt ::+ Vector.Vector (Vector.Vector filtration) ->+ BasisCellRef ->+ Maybe filtration+denseBirthAt denseBirthsByDegree cellRefValue =+ case cellDegree cellRefValue of+ HomologicalDegree degreeIndex ->+ denseBirthsByDegree Vector.!? degreeIndex+ >>= (\birthsAtDegree -> birthsAtDegree Vector.!? cellIndex cellRefValue)
test/Main.hs view
@@ -6,9 +6,13 @@ import Data.Kind (Type) import Data.List.NonEmpty (toList) import qualified Data.Map.Strict as Map+import Data.Vector qualified as Vector import Moonlight.Core (mkCapability) import Moonlight.Homology import Moonlight.Homology.Boundary.Finite (mkFiniteChainComplex)+import Moonlight.Homology.Boundary.LinAlg+ ( boundaryEntriesBySource,+ ) import Moonlight.Homology.Effect.Laws import Moonlight.Homology.Effect.Determinism import BlockSchurSpec qualified@@ -131,6 +135,41 @@ "unordered entries still use canonical semantics" (mkBoundaryIncidence 2 2 entries) (mkBoundaryIncidenceFromOrderedEntries 2 2 entries),+ testCase "glues ordered columns to the canonical source projection" $ do+ let columns =+ Vector.fromList+ [ [mkBoundaryEntry 0 0 (2 :: Int), mkBoundaryEntry 0 1 3],+ [mkBoundaryEntry 1 1 5]+ ]+ flattenedEntries = concat (Vector.toList columns)+ assertEqual+ "ordered columns retain the same canonical sections"+ (fmap boundaryEntriesBySource (mkBoundaryIncidence 2 2 flattenedEntries))+ (fmap boundaryEntriesBySource (mkBoundaryIncidenceFromOrderedColumns 2 2 columns)),+ testCase "canonicalizes a noncanonical column cover through the general owner" $ do+ let columns =+ Vector.fromList+ [ [mkBoundaryEntry 0 1 (3 :: Int), mkBoundaryEntry 0 0 2],+ [mkBoundaryEntry 1 1 5]+ ]+ flattenedEntries = concat (Vector.toList columns)+ assertEqual+ "noncanonical columns descend to the canonical source sections"+ (fmap boundaryEntriesBySource (mkBoundaryIncidence 2 2 flattenedEntries))+ (fmap boundaryEntriesBySource (mkBoundaryIncidenceFromOrderedColumns 2 2 columns)),+ testCase "preserves typed bounds obstructions for malformed columns" $+ assertEqual+ "out-of-bounds column entry"+ (Left (BoundaryIncidenceEntryOutOfBounds 0 2 2 2))+ ( mkBoundaryIncidenceFromOrderedColumns+ 2+ 2+ ( Vector.fromList+ [ [mkBoundaryEntry 0 2 (1 :: Int)],+ []+ ]+ )+ ), testCase "rejects out-of-bounds entries before fast construction" $ assertEqual "out-of-bounds entry"
test/facade/CompileFailSpec.hs view
@@ -155,7 +155,7 @@ -- fixture verdict. fixturePackageIds :: [GhcPackageSpec] fixturePackageIds =- [GhcPackageId "moonlight-homology-0.1.0.1-inplace"]+ [GhcPackageId "moonlight-homology-0.1.0.2-inplace"] expectRight :: Show left =>
test/topology/TopologySpec.hs view
@@ -14,6 +14,7 @@ import qualified Moonlight.Homology as H import qualified Moonlight.Homology.Boundary.Finite as H (mkFiniteChainComplex) import Moonlight.Homology.Pure.Topology.Algebra (mkQuotientPresentation)+import Moonlight.Homology.Pure.Topology.Core qualified as HomologyCore import Moonlight.Homology.Pure.Topology.Harmonic (harmonicBasisAt) import TestFixtures ( intervalComplex,@@ -42,6 +43,10 @@ testCase "exact witness recovers torsion and exact classes for Moore complex" testExactWitnessMoore, testCase "projective plane cellular attaching map exposes Z/2 torsion" testProjectivePlaneTorsionAnchor, testCase "persistent witness tracks the essential loop birth" testPersistentTriangleLoop,+ testCase "persistence retains a non-binary64 exact filtration order" testExactOrderedPersistence,+ testCase "critical Betti tables cancel same-rank interval pairs" testCriticalBettiSameRankCancellation,+ testCase "persistence falls back for non-graph-shaped degree-one columns" testPersistentGraphFallback,+ testCase "graph specialization preserves higher-degree clearing" testPersistentTetrahedronClearing, testCase "graph witness extracts scaffold and low modes on interval" testGraphWitnessInterval, testCase "graph witness remains convergent on branched five-vertex skeletons" testGraphWitnessBranchedFiveVertexSkeleton, testCase "graph witness seed enriches graph scaffolds with exact loop data" testGraphWitnessSeedTriangle,@@ -263,6 +268,156 @@ length essentialLoops @?= 1 fmap H.persistenceBirth essentialLoops @?= [H.FiltrationValue 2.0] +newtype ExactTestBirth = ExactTestBirth Rational+ deriving stock (Eq, Ord, Show)++testExactOrderedPersistence :: Assertion+testExactOrderedPersistence = do+ filteredComplex <-+ expectRight+ (H.mkFilteredFiniteChainComplex triangleCycleComplex exactTriangleFiltration)+ persistencePairs <- expectRight (H.mod2PersistentPairs filteredComplex)+ assertCriticalBettiTableAgrees filteredComplex persistencePairs+ let criticalValues = fmap ExactTestBirth [0, 1, 2]+ H.filteredCriticalValues filteredComplex @?= criticalValues+ HomologyCore.filteredBirthValueRanks filteredComplex+ @?= Map.fromList (zip criticalValues [0, 1, 2])+ fmap+ (\(cellRefValue, _) -> Map.lookup cellRefValue (HomologyCore.filteredCellBirthRanks filteredComplex))+ exactTriangleFiltration+ @?= fmap Just [0, 0, 0, 1, 1, 2]+ let essentialLoops =+ persistencePairs+ & filter (\pairValue -> H.persistenceDegree pairValue == H.HomologicalDegree 1)+ & filter (isNothing . H.persistenceDeath)+ fmap H.persistenceBirth essentialLoops @?= [ExactTestBirth 2]+ H.persistentBettiAt (ExactTestBirth 1) persistencePairs+ @?= Map.fromList [(H.HomologicalDegree 0, 1)]+ H.persistentBettiAt (ExactTestBirth 2) persistencePairs+ @?= Map.fromList+ [ (H.HomologicalDegree 0, 1),+ (H.HomologicalDegree 1, 1)+ ]+ let requestedThresholds = fmap ExactTestBirth [2, 0, 1, 2]+ H.persistentBettiAtMany requestedThresholds persistencePairs+ @?= fmap (`H.persistentBettiAt` persistencePairs) requestedThresholds+ criticalProfiles <-+ expectRight (H.persistentBettiAtCriticalValues filteredComplex persistencePairs)+ criticalProfiles+ @?= fmap (`H.persistentBettiAt` persistencePairs) criticalValues+ let malformedPairs =+ [ H.PersistencePair+ { H.persistenceDegree = H.HomologicalDegree 0,+ H.persistenceBirth = ExactTestBirth 1,+ H.persistenceDeath = Just (ExactTestBirth 1)+ },+ H.PersistencePair+ { H.persistenceDegree = H.HomologicalDegree 1,+ H.persistenceBirth = ExactTestBirth 2,+ H.persistenceDeath = Just (ExactTestBirth 1)+ }+ ]+ H.persistentBettiAtMany requestedThresholds malformedPairs+ @?= fmap (`H.persistentBettiAt` malformedPairs) requestedThresholds++testCriticalBettiSameRankCancellation :: Assertion+testCriticalBettiSameRankCancellation = do+ filteredComplex <-+ expectRight+ ( H.mkFilteredFiniteChainComplex+ intervalComplex+ (filtrationByDegree intervalComplex (const (ExactTestBirth 0)))+ )+ (persistencePairs, criticalBettiTable) <-+ expectRight (H.mod2PersistentPairsWithCriticalBettiTable filteredComplex)+ persistencePairs+ @?= [ H.PersistencePair+ { H.persistenceDegree = H.HomologicalDegree 0,+ H.persistenceBirth = ExactTestBirth 0,+ H.persistenceDeath = Just (ExactTestBirth 0)+ },+ H.PersistencePair+ { H.persistenceDegree = H.HomologicalDegree 0,+ H.persistenceBirth = ExactTestBirth 0,+ H.persistenceDeath = Nothing+ }+ ]+ H.criticalBettiVectors criticalBettiTable @?= [[1, 0]]++testPersistentGraphFallback :: Assertion+testPersistentGraphFallback = do+ singleEndpointGraph <-+ expectRight (boundaryOneComplex 2 1 [boundaryEntry 0 0 1])+ filteredComplex <-+ expectRight+ ( H.mkFilteredFiniteChainComplex+ singleEndpointGraph+ ( filtrationByDegree singleEndpointGraph $ \degreeValue ->+ ExactTestBirth+ (if degreeValue == H.HomologicalDegree 0 then 0 else 1)+ )+ )+ (persistencePairs, criticalBettiTable) <-+ expectRight (H.mod2PersistentPairsWithCriticalBettiTable filteredComplex)+ persistencePairs+ @?= [ H.PersistencePair+ { H.persistenceDegree = H.HomologicalDegree 0,+ H.persistenceBirth = ExactTestBirth 0,+ H.persistenceDeath = Just (ExactTestBirth 1)+ },+ H.PersistencePair+ { H.persistenceDegree = H.HomologicalDegree 0,+ H.persistenceBirth = ExactTestBirth 0,+ H.persistenceDeath = Nothing+ }+ ]+ H.criticalBettiVectors criticalBettiTable @?= [[2, 0], [1, 0]]++testPersistentTetrahedronClearing :: Assertion+testPersistentTetrahedronClearing = do+ filteredComplex <-+ expectRight+ ( H.mkFilteredFiniteChainComplex+ tetrahedronBoundaryComplex+ ( filtrationByDegree+ tetrahedronBoundaryComplex+ (ExactTestBirth . fromIntegral . H.unHomologicalDegree)+ )+ )+ persistencePairs <- expectRight (H.mod2PersistentPairs filteredComplex)+ assertCriticalBettiTableAgrees filteredComplex persistencePairs+ let pairsAt degreeValue =+ filter ((== degreeValue) . H.persistenceDegree) persistencePairs+ degreeOnePairs = pairsAt (H.HomologicalDegree 1)+ degreeTwoPairs = pairsAt (H.HomologicalDegree 2)+ length degreeOnePairs @?= 3+ fmap H.persistenceBirth degreeOnePairs @?= replicate 3 (ExactTestBirth 1)+ fmap H.persistenceDeath degreeOnePairs @?= replicate 3 (Just (ExactTestBirth 2))+ fmap H.persistenceBirth degreeTwoPairs @?= [ExactTestBirth 2]+ fmap H.persistenceDeath degreeTwoPairs @?= [Nothing]++assertCriticalBettiTableAgrees ::+ (Integral r, Ord filtration, Show filtration) =>+ H.FilteredFiniteChainComplex filtration r ->+ [H.PersistencePair filtration] ->+ Assertion+assertCriticalBettiTableAgrees filteredComplex persistencePairs = do+ (fusedPairs, criticalBettiTable) <-+ expectRight (H.mod2PersistentPairsWithCriticalBettiTable filteredComplex)+ criticalProfiles <-+ expectRight (H.persistentBettiAtCriticalValues filteredComplex persistencePairs)+ let H.HomologicalDegree maximumDegree =+ H.maxHomologicalDegree (H.filteredBaseComplex filteredComplex)+ degreeValues = fmap H.HomologicalDegree [0 .. maximumDegree]+ denseProfiles =+ fmap+ (\profile -> fmap (\degreeValue -> Map.findWithDefault 0 degreeValue profile) degreeValues)+ criticalProfiles+ fusedPairs @?= persistencePairs+ H.criticalBettiDegreeCount criticalBettiTable @?= maximumDegree + 1+ H.criticalBettiRankCount criticalBettiTable @?= length criticalProfiles+ H.criticalBettiVectors criticalBettiTable @?= denseProfiles+ testGraphWitnessInterval :: Assertion testGraphWitnessInterval = withIntervalObservation $ \intervalObservationValue -> do@@ -856,6 +1011,31 @@ (H.BasisCellRef {H.cellDegree = H.HomologicalDegree 1, H.cellIndex = 1}, H.FiltrationValue 1.0), (H.BasisCellRef {H.cellDegree = H.HomologicalDegree 1, H.cellIndex = 2}, H.FiltrationValue 2.0) ]++exactTriangleFiltration :: [(H.BasisCellRef, ExactTestBirth)]+exactTriangleFiltration =+ [ (H.BasisCellRef {H.cellDegree = H.HomologicalDegree 0, H.cellIndex = 0}, ExactTestBirth 0),+ (H.BasisCellRef {H.cellDegree = H.HomologicalDegree 0, H.cellIndex = 1}, ExactTestBirth 0),+ (H.BasisCellRef {H.cellDegree = H.HomologicalDegree 0, H.cellIndex = 2}, ExactTestBirth 0),+ (H.BasisCellRef {H.cellDegree = H.HomologicalDegree 1, H.cellIndex = 0}, ExactTestBirth 1),+ (H.BasisCellRef {H.cellDegree = H.HomologicalDegree 1, H.cellIndex = 1}, ExactTestBirth 1),+ (H.BasisCellRef {H.cellDegree = H.HomologicalDegree 1, H.cellIndex = 2}, ExactTestBirth 2)+ ]++filtrationByDegree ::+ H.FiniteChainComplex coefficient ->+ (H.HomologicalDegree -> filtration) ->+ [(H.BasisCellRef, filtration)]+filtrationByDegree finite birthAtDegree =+ let H.HomologicalDegree maximumDegree = H.maxHomologicalDegree finite+ in concatMap+ ( \degreeIndex ->+ let degreeValue = H.HomologicalDegree degreeIndex+ in fmap+ (\cellRefValue -> (cellRefValue, birthAtDegree degreeValue))+ (H.finiteChainBasisRefsAtDegree finite degreeValue)+ )+ [0 .. maximumDegree] withIntervalObservation :: (H.TopologyObservationConfig Integer -> Assertion) -> Assertion withIntervalObservation assertion = do