.. _theory_barcodes_diagrams:

=========================
Barcodes and Diagrams
=========================

.. contents:: Contents
   :local:
   :depth: 2

Introduction
============

Persistence barcodes and diagrams are two equivalent ways to visualize and represent the output of persistent homology computation. They encode which topological features exist and how long they persist across the filtration.

Both representations are fundamental to understanding and using persistent homology in practice.

Persistence Barcodes
====================

What is a Barcode?
------------------

A **persistence barcode** represents each topological feature as a horizontal bar:

* **Left endpoint:** Birth time (when feature appears)
* **Right endpoint:** Death time (when feature disappears)
* **Length:** Persistence (death - birth)

**Visual Representation:**

::

    Birth                                Death
      │────────────────────────────────────│  Feature 1 (high persistence)
            │──────────────────────│          Feature 2 (medium persistence)
                  │───│                      Feature 3 (low persistence - noise)
                    │────────────────────────│  Feature 4 (high persistence)

**Interpretation:**

* **Long bars:** Significant, stable features
* **Short bars:** Noise or insignificant features
* **Infinite bars:** Features that never die (H0 only)

Barcode Format in MedTDA
-------------------------

MedTDA returns barcodes as NumPy arrays:

::

    barcode = array([[birth_1, death_1],
                     [birth_2, death_2],
                     [birth_3, death_3],
                     ...])

**Shape:** (n_features, 2)

**Example:**

::

    from medtda import BarcodeExtractor
    
    extractor = BarcodeExtractor()
    barcodes = extractor.execute(image)
    
    h1_barcode = barcodes['H1']
    print(h1_barcode)
    # [[0.12, 0.45],   # Feature 1: persistence = 0.33
    #  [0.23, 0.89],   # Feature 2: persistence = 0.66
    #  [0.67, 0.71]]   # Feature 3: persistence = 0.04 (noise?)

Computing Persistence
---------------------

::

    import numpy as np
    
    persistence = barcode[:, 1] - barcode[:, 0]
    
    # Filter by persistence threshold
    threshold = 0.1
    significant_features = barcode[persistence > threshold]
    
    print(f"Total features: {len(barcode)}")
    print(f"Significant features: {len(significant_features)}")
    print(f"Mean persistence: {persistence.mean():.3f}")
    print(f"Max persistence: {persistence.max():.3f}")

Multiple Dimensions
-------------------

Each homology dimension has its own barcode:

::

    barcodes = extractor.execute(image_3d)
    
    h0_barcode = barcodes['H0']  # Connected components
    h1_barcode = barcodes['H1']  # Loops/tunnels
    h2_barcode = barcodes['H2']  # Voids/cavities
    
    for dim, barcode in barcodes.items():
        persistence = barcode[:, 1] - barcode[:, 0]
        print(f"H{dim}: {len(barcode)} features, "
              f"max persistence = {persistence.max():.3f}")

Persistence Diagrams
====================

What is a Persistence Diagram?
-------------------------------

A **persistence diagram** is a scatter plot where each point represents a feature:

* **X-coordinate:** Birth time
* **Y-coordinate:** Death time
* **Distance from diagonal:** Persistence

**Key Properties:**

* All points lie above the diagonal (y > x)
* Distance from diagonal = persistence
* Points near diagonal = low persistence (noise)
* Points far from diagonal = high persistence (signal)

**Diagonal:** The line y = x where birth = death (zero persistence)

Interpreting Diagrams
---------------------

**Point properties:**

* **y - x = persistence:** Vertical distance from point to diagonal
* **x = birth:** Horizontal position
* **y = death:** Vertical position

**Clustering:**

* Points clustered near diagonal = Noise
* Points far from diagonal = Significant features
* Isolated points = Unique structures

Visualizing Barcodes and Diagrams
==================================

Using MedTDA Plotting
----------------------

MedTDA provides built-in visualization:

**Barcodes:**

::

    from medtda import BarcodeExtractor
    from medtda.plotting import plot_barcode
    
    barcodes = extractor.execute(image)
    
    # Plot H1 barcode
    plot_barcode(
        barcodes['H1'],
        title='H1 Barcode: Loops and Tunnels'
    )

Multiple Dimensions Together
-----------------------------

::

    import matplotlib.pyplot as plt
    from medtda.plotting import plot_barcode
    
    fig, axes = plt.subplots(1, 3, figsize=(15, 5))
    
    # Plot barcodes for each dimension
    for dim in [0, 1, 2]:
        plot_barcode(barcodes[f'H{dim}'], ax=axes[dim], title=f'H{dim} Barcode')
    
    plt.tight_layout()
    plt.show()

Interpretation Guide
====================

Reading Barcodes
----------------

**Pattern Recognition:**

1. **One very long bar (H0):**
   
   * Normal - represents entire connected image
   * Has infinite or very high persistence

2. **Many short bars:**
   
   * Likely noise
   * Consider higher persistence threshold

3. **Few long bars:**
   
   * Significant structures
   * Most informative features

4. **Hierarchical structure:**
   
   * Long bars = Global structure
   * Medium bars = Secondary features
   * Short bars = Fine details

**Example Interpretation:**

::

    H1 Barcode Analysis:
    
    Bar 1: [0.10, 0.95] - persistence = 0.85 (major loop structure)
    Bar 2: [0.20, 0.75] - persistence = 0.55 (secondary loop)
    Bar 3: [0.45, 0.50] - persistence = 0.05 (noise)
    Bar 4: [0.60, 0.62] - persistence = 0.02 (noise)

Reading Persistence Diagrams
-----------------------------

**Spatial Patterns:**

1. **Vertical line of points:**
   
   * Features born at same time
   * Dying at different times
   * Common in hierarchical structures

2. **Horizontal line of points:**
   
   * Features dying at same time
   * Born at different times

3. **Cluster near diagonal:**
   
   * Noise region
   * Low-persistence features

4. **Isolated points far from diagonal:**
   
   * Significant unique features
   * High persistence

Medical Image Examples
=======================

Example 1: Tumor Microenvironment
----------------------------------

::

    from medtda import BarcodeExtractor
    import numpy as np
    
    extractor = BarcodeExtractor(
        filtration_type='sublevel',
        max_dimension=1
    )
    
    barcodes = extractor.execute(tumor_histology_image)
    
    # H0: Individual cells/nuclei
    h0 = barcodes['H0']
    h0_persistence = h0[:, 1] - h0[:, 0]
    
    # Count significant cells (persistence > 0.1)
    num_cells = np.sum(h0_persistence > 0.1)
    mean_cell_size = h0_persistence[h0_persistence > 0.1].mean()
    
    # H1: Glandular structures (loops)
    h1 = barcodes['H1']
    h1_persistence = h1[:, 1] - h1[:, 0]
    num_glands = np.sum(h1_persistence > 0.15)
    
    print(f"Cells detected: {num_cells}")
    print(f"Average cell size (persistence): {mean_cell_size:.3f}")
    print(f"Glandular structures: {num_glands}")

**Barcode interpretation:**

* Long H0 bars = Large cells/nuclei
* Short H0 bars = Small cells or artifacts
* H1 bars = Glandular loops (cancer indicator)

Example 2: Brain Vessel Networks
---------------------------------

::

    extractor = BarcodeExtractor(
        filtration_type='sublevel',
        max_dimension=1
    )
    
    barcodes = extractor.execute(angiogram)
    
    # H1 features represent vessel loops
    h1 = barcodes['H1']
    h1_persistence = h1[:, 1] - h1[:, 0]
    
    # Classify loops by size
    small_loops = np.sum((h1_persistence > 0.05) & (h1_persistence < 0.15))
    medium_loops = np.sum((h1_persistence >= 0.15) & (h1_persistence < 0.30))
    large_loops = np.sum(h1_persistence >= 0.30)
    
    print("Vessel network analysis:")
    print(f"  Small loops (capillaries): {small_loops}")
    print(f"  Medium loops: {medium_loops}")
    print(f"  Large loops (major vessels): {large_loops}")

**Diagram interpretation:**

* Points far from diagonal = Large vessel loops
* Points near diagonal = Small capillary loops

Example 3: Lung CT Analysis
----------------------------

::

    extractor = BarcodeExtractor(
        filtration_type='sublevel',
        max_dimension=2
    )
    
    barcodes = extractor.execute(lung_ct)
    
    # H2 features represent air-filled cavities
    h2 = barcodes['H2']
    h2_persistence = h2[:, 1] - h2[:, 0]
    
    # Detect cysts or emphysema
    significant_voids = h2[h2_persistence > 0.2]
    
    print(f"Air-filled cavities detected: {len(significant_voids)}")
    print(f"Largest cavity persistence: {h2_persistence.max():.3f}")
    
    # Track birth times (characteristic intensities)
    birth_times = significant_voids[:, 0]
    print(f"Cavity formation range: [{birth_times.min():.3f}, {birth_times.max():.3f}]")

**3D barcode interpretation:**

* H2 bars → Enclosed air pockets
* Bar length → Cavity size/significance
* Birth time → Characteristic air intensity

Noise Filtering
===============

Persistence Threshold
---------------------

Most common filtering approach:

::

    threshold = 0.1
    
    for dim in barcodes:
        barcode = barcodes[dim]
        persistence = barcode[:, 1] - barcode[:, 0]
        
        # Keep only significant features
        barcodes[dim] = barcode[persistence > threshold]

Adaptive Thresholding
---------------------

Set threshold based on persistence distribution:

::

    import numpy as np
    
    persistence = barcode[:, 1] - barcode[:, 0]
    
    # Use percentile-based threshold
    threshold = np.percentile(persistence, 75)
    
    # Or use standard deviation
    threshold = persistence.mean() + persistence.std()
    
    significant = barcode[persistence > threshold]

Top-K Features
--------------

Keep only the most persistent features:

::

    k = 10  # Keep top 10 features
    
    persistence = barcode[:, 1] - barcode[:, 0]
    top_k_indices = np.argsort(persistence)[-k:]
    
    top_features = barcode[top_k_indices]

Statistical Analysis
====================

Barcode Summary Statistics
---------------------------

::

    def barcode_statistics(barcode):
        """Compute barcode summary statistics."""
        persistence = barcode[:, 1] - barcode[:, 0]
        births = barcode[:, 0]
        deaths = barcode[:, 1]
        
        stats = {
            'num_features': len(barcode),
            'mean_persistence': persistence.mean(),
            'std_persistence': persistence.std(),
            'max_persistence': persistence.max(),
            'mean_birth': births.mean(),
            'mean_death': deaths.mean(),
            'total_persistence': persistence.sum()
        }
        
        return stats

**Usage:**

::

    for dim in [0, 1, 2]:
        stats = barcode_statistics(barcodes[dim])
        print(f"\nH{dim} Statistics:")
        for key, value in stats.items():
            print(f"  {key}: {value:.3f}")

Comparing Barcodes
------------------

Compare barcodes from different images:

::

    # Extract features from two images
    barcodes_1 = extractor.execute(image_1)
    barcodes_2 = extractor.execute(image_2)
    
    # Compare H1 features
    h1_stats_1 = barcode_statistics(barcodes_1['H1'])
    h1_stats_2 = barcode_statistics(barcodes_2['H1'])
    
    print("Image 1 vs Image 2:")
    print(f"  Features: {h1_stats_1['num_features']} vs {h1_stats_2['num_features']}")
    print(f"  Mean persistence: {h1_stats_1['mean_persistence']:.3f} vs "
          f"{h1_stats_2['mean_persistence']:.3f}")

Mathematical Foundations
========================

The following mathematical formulations provide the theoretical underpinnings of persistence barcodes and diagrams. For comprehensive treatment and proofs, see [Ali2023]_ and the classical references [Edel2010]_, [Zomo2005]_.

Persistence Modules
-------------------

Persistent homology is fundamentally built on **persistence modules** - algebraic structures that encode how topology evolves across a filtration.

**Definition:** A persistence module (V, a) consists of:

* A sequence of vector spaces :math:`V = \{V_i \mid 0 \leq i \leq n\}`
* Linear maps :math:`a = \{a_i : V_{i-1} \to V_i \mid 1 \leq i \leq n\}` connecting them sequentially

This can be visualized as:

.. math::

   V_0 \xrightarrow{a_1} V_1 \xrightarrow{a_2} \cdots \xrightarrow{a_n} V_n

For real-indexed filtrations, the indices become real numbers instead of integers.

Structure Theorem
-----------------

The **fundamental structure theorem** for persistence modules states that every persistence module decomposes uniquely into simple building blocks.

**Interval Modules:** For each pair of indices :math:`p \leq q`, define the interval module :math:`I^{[p,q]}` where:

* :math:`\text{dim}(I^{[p,q]}_i) = 1` if :math:`p \leq i \leq q`, and 0 otherwise
* The connecting maps are identity when both spaces are 1-dimensional

**Decomposition Theorem:** Every persistence module (V, a) decomposes uniquely as:

.. math::

   (V,a) \simeq \bigoplus_{[p,q] \in \text{Barc}(V,a)} \left(I^{[p,q]}\right)^{\mu_{p,q}}

where:

* Barc(V,a) is a multiset of intervals (the **barcode**)
* :math:`\mu_{p,q}` is the multiplicity (how many copies of interval [p,q])

**Significance:** This means the complex algebraic object (V,a) can be completely recovered from the purely combinatorial data of the barcode. The barcode is both a complete invariant and a practical representation.

Stability Theorem
-----------------

The **stability theorem** is one of the most important results in topological data analysis, guaranteeing robustness to noise.

**Interleaving Distance:** On persistence modules, there is a metric called the interleaving distance that measures how different two modules are.

**Bottleneck Distance:** On barcodes, the bottleneck distance :math:`d_B(B_1, B_2)` between two barcodes is:

.. math::

   d_B(B_1, B_2) = \inf_{\gamma} \sup_{p \in B_1} \|p - \gamma(p)\|_\infty

where:

* γ ranges over all bijections (matchings) between :math:`B_1` and :math:`B_2`
* Points can be matched to the diagonal (representing 0-persistence)
* :math:`\|\cdot\|_\infty` is the maximum of the absolute differences in coordinates for matched points

**Stability Theorem:** The barcode map is an isometry:

.. math::

   d_B(\text{Barc}(V_1), \text{Barc}(V_2)) = d_I(V_1, V_2)

where :math:`d_I` is the interleaving distance on persistence modules.

**Practical Implication:** Small changes in the input (e.g., noise in an image) cause only small changes in the barcode, measured by bottleneck distance. Features with high persistence are more robust to perturbations than features with low persistence.

Barcode Metrics
---------------

Several metrics can be defined between persistence barcodes:

**1. Bottleneck Distance:**

The bottleneck distance finds the optimal matching between features that minimizes the maximum distance any feature must move:

* **Advantages:** Stable (satisfies the stability theorem), theoretically well-founded
* **Disadvantages:** Only considers the worst-matched pair, ignores most features
* **Use case:** Theoretical guarantees, robust comparison

**2. Wasserstein Distance (d_p):**

Generalizes bottleneck distance by considering all matched pairs:

.. math::

   d_p(B_1, B_2) = \left(\inf_{\gamma} \sum_{x \in B_1} \|x - \gamma(x)\|_\infty^p\right)^{1/p}

where p \u2265 1 is a parameter.

* **p = 1:** Emphasizes total displacement
* **p = 2:** Quadratic penalty (commonly used)
* **p \u2192 \u221e:** Recovers bottleneck distance

**Advantages:** Considers all features, more discriminative than bottleneck

**Disadvantages:** More sensitive to outliers for small p

See Also
========

* :doc:`persistent_homology` - Computing persistent homology
* :doc:`filtrations` - Building filtrations from images
* :doc:`vectorization_methods` - Converting barcodes to feature vectors
* :doc:`../user_guide/persistent_homology` - PH user guide
* :doc:`../api/plotting` - Plotting functions
