persistent-homology 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 <Roy ABALLO>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,240 @@
1
+ Metadata-Version: 2.4
2
+ Name: persistent-homology
3
+ Version: 0.1.0
4
+ Summary: A from-scratch implementation of persistent homology for Vietoris-Rips filtrations.
5
+ Author-email: Roy Aballo <abalo.g.roy@aims-senegal.org>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/royaballo/persistent-homology
8
+ Project-URL: Repository, https://github.com/royaballo/persistent-homology
9
+ Project-URL: Issues, https://github.com/royaballo/persistent-homology/issues
10
+ Keywords: persistent homology,topological data analysis,Vietoris-Rips,TDA,algebraic topology
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
15
+ Classifier: Intended Audience :: Science/Research
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: numpy>=2.0
20
+ Requires-Dist: scipy>=1.15
21
+ Requires-Dist: matplotlib>=3.8
22
+ Requires-Dist: pandas>=2.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8.0; extra == "dev"
25
+ Provides-Extra: benchmark
26
+ Requires-Dist: ripser>=0.6.14; extra == "benchmark"
27
+ Requires-Dist: gudhi>=3.11.0; extra == "benchmark"
28
+ Provides-Extra: bio
29
+ Requires-Dist: biopython>=1.85; extra == "bio"
30
+ Dynamic: license-file
31
+
32
+ # Persistent Homology
33
+
34
+ A from-scratch, readable implementation of persistent homology for
35
+ Vietoris–Rips filtrations, written as the computational half of an MSc thesis
36
+ in Topological Data Analysis.
37
+
38
+ The point of this code is not to be fast. Production libraries like Ripser and
39
+ GUDHI already are, and they are very hard to read. The point is the opposite:
40
+ every function here lines up with a definition or an algorithm from the thesis,
41
+ so that someone can follow the path from "a point cloud" to "a persistence
42
+ diagram" without taking any step on faith. Where there was a choice between a
43
+ clever trick and a transparent one, the transparent one won.
44
+
45
+ ## What it does
46
+
47
+ Given a finite point cloud, the pipeline:
48
+
49
+ 1. **Builds the Vietoris–Rips filtration** up to a chosen dimension and scale.
50
+ VR is the only filtration the code implements; Čech and Alpha are discussed
51
+ in the thesis as context but are not built here.
52
+ 2. **Assembles the combined boundary matrix** over $\mathbb{Z}/2\mathbb{Z}$,
53
+ stored sparsely as columns of row indices.
54
+ 3. **Reduces it** by one of three interchangeable strategies, and reads off the
55
+ birth–death pairs.
56
+ 4. **Returns persistence diagrams** per dimension, as `{dimension: [(birth,
57
+ death), ...]}`, ready to plot.
58
+
59
+ The three reduction strategies are the heart of the project, and you can switch
60
+ between them with a single argument:
61
+
62
+ | Strategy | What it is | Where it shines |
63
+ |---------------|-------------------------------------------------------------------|-----------------|
64
+ | `standard` | Plain left-to-right column reduction (Zomorodian–Carlsson) | The baseline; easiest to read |
65
+ | `clearing` | Standard reduction, but births in dimension *k* are zeroed once dimension *k+1* is done | Skips a lot of wasted work in high dimensions |
66
+ | `cohomology` | Reduces the anti-transpose instead (pCoh, de Silva–Morozov–Vejdemo-Johansson) | Much shorter columns on VR complexes; usually the fastest of the three |
67
+
68
+ All three return the *same* diagram — that is the whole content of the duality
69
+ results, and the test suite checks it explicitly on every dataset.
70
+
71
+ ## Installation
72
+
73
+ ```bash
74
+ git clone https://github.com/royaballo/persistent-homology.git
75
+ cd persistent-homology
76
+ pip install -e .
77
+ ```
78
+
79
+ That installs the `persistent_homology` package in editable mode along with its
80
+ dependencies (NumPy, SciPy, Matplotlib; `ripser` and `gudhi` are pulled in for
81
+ the correctness tests and benchmarks). Python 3.11+ is expected.
82
+
83
+ ## A first example
84
+
85
+ ```python
86
+ from persistent_homology.persistence import compute_persistence
87
+ from datasets.synthetic import make_circle
88
+
89
+ # 30 points sampled on a noisy circle
90
+ points = make_circle(n=30, noise=0.05)
91
+
92
+ # one long-lived H1 class is the circle's hole
93
+ diagram = compute_persistence(
94
+ points,
95
+ max_dim=2,
96
+ max_eps=2.0,
97
+ algorithm="cohomology", # or "standard", "clearing"
98
+ )
99
+
100
+ for dim, dgm in diagram.items():
101
+ print(f"H{dim}: {len(dgm)} features")
102
+ ```
103
+
104
+ To see it rather than read it:
105
+
106
+ ```python
107
+ from persistent_homology.plotting import plot_persistence_diagram
108
+
109
+ plot_persistence_diagram(diagram, max_dim=2)
110
+ ```
111
+
112
+ The circle gives one point sitting well above the diagonal in $H_1$ (the hole)
113
+ and a cloud of short-lived noise near it. That gap between signal and noise is
114
+ the thing persistent homology is for. Essential features — the ones that never
115
+ die — are drawn as triangles along a dashed `∞` line near the top, since their
116
+ true death is infinite and cannot be placed on the axis.
117
+
118
+ ## Repository structure
119
+
120
+ ```
121
+ persistent_homology/ the actual library
122
+ filtration.py Vietoris–Rips construction (flag complex from the 1-skeleton)
123
+ boundary.py boundary and coboundary matrices over Z/2Z
124
+ reduction.py standard, clearing, and cohomology (pCoh) reductions
125
+ persistence.py end-to-end entry point: points in, diagrams out
126
+ plotting.py persistence diagrams (and the log-log benchmark plots)
127
+
128
+ datasets/
129
+ synthetic.py circle, sphere, torus, cube samplers (known topology)
130
+ proteins.py loads Cα coordinates from PDB files
131
+
132
+ tests/
133
+ test_correctness.py checks our diagrams against Ripser and GUDHI
134
+
135
+ benchmarks/ timing and memory scripts; results land in benchmarks/results/
136
+ applications/ the protein case study (analyse_proteins.py, benchmark_lysozyme.py)
137
+ notebooks/ correctness, benchmark, and protein analysis walkthroughs
138
+ data/proteins/ PDB files, extracted Cα coordinates, and computed diagrams
139
+ figures/ generated plots (regenerated by the scripts; not meant to be edited by hand)
140
+ ```
141
+
142
+ If you only want to understand the method, read `filtration.py`, then
143
+ `boundary.py`, then `reduction.py`, in that order. They mirror the thesis
144
+ chapter on algorithms section by section.
145
+
146
+ ## How the thesis was built, in order
147
+
148
+ The work went in five steps, and the repository is laid out so that each one can
149
+ be re-run on its own.
150
+
151
+ **1. Implement the pipeline.** Build the VR filtration, the boundary matrices,
152
+ and the three reductions — the `persistent_homology/` package. The guiding rule
153
+ was a one-to-one match between code and theory rather than speed.
154
+
155
+ **2. Prove it is correct.** Before any benchmark means anything, the output has
156
+ to be right. `tests/test_correctness.py` runs our three algorithms on the
157
+ synthetic datasets and on small random complexes, and checks that the diagrams
158
+ agree with both Ripser and GUDHI up to the usual reordering. The
159
+ `correctness_verification.ipynb` notebook shows this in a readable form.
160
+
161
+ **3. Benchmark it.** With correctness settled, the `benchmarks/` scripts measure
162
+ how the three strategies scale. `computation_time.py` and `memory_usage.py`
163
+ sweep dataset size on circle, sphere, torus, and cube; `effect_filtration_scope.py`
164
+ isolates how much capping the homology dimension actually saves. Fitted scaling
165
+ exponents are written to `benchmarks/results/`.
166
+
167
+ **4. Put it to work on real data.** The protein case study in `applications/`
168
+ asks a concrete question: can persistent homology pick out structural features
169
+ of proteins straight from their Cα atomic coordinates, with no
170
+ chemistry-specific input? It runs on three PDB structures (hemoglobin `1A3N`,
171
+ lysozyme `1AKI`, GFP `1EMA`), computes $H_0$, $H_1$, $H_2$ diagrams, and tallies
172
+ the resulting Betti counts.
173
+
174
+ **5. Write it up.** The thesis ties the theory, the correctness evidence, the
175
+ scaling results, and the protein study together. The notebooks are the bridge:
176
+ they regenerate every figure the thesis uses.
177
+
178
+ ## Reproducing the results
179
+
180
+ Each step is one command. Run them from the repository root.
181
+
182
+ ```bash
183
+ # correctness: should report all-pass against Ripser and GUDHI
184
+ pytest
185
+
186
+ # timing and memory across synthetic datasets (writes CSVs to benchmarks/results/)
187
+ python benchmarks/computation_time.py
188
+ python benchmarks/memory_usage.py
189
+ python benchmarks/effect_filtration_scope.py
190
+
191
+ # protein case study (writes diagrams to data/proteins/results/ and plots to figures/)
192
+ python applications/analyse_proteins.py
193
+ python applications/benchmark_lysozyme.py
194
+ ```
195
+
196
+ The three notebooks in `notebooks/` read the CSVs and `.npy` diagrams produced
197
+ above and assemble the figures and tables exactly as they appear in the thesis.
198
+ Run the scripts first, then the notebooks.
199
+
200
+ A note on figures: everything in `figures/` is generated. Delete the folder and
201
+ re-run the scripts and you should get it back byte-for-byte, give or take
202
+ matplotlib versions.
203
+
204
+ ## What the results say
205
+
206
+ The short version, with the detail left for the thesis:
207
+
208
+ - **The three algorithms agree, always.** This is the duality between homology
209
+ and cohomology made concrete: same pairs, birth and death simplices swapped.
210
+ - **Empirical scaling sits well below the cubic worst case.** On the synthetic
211
+ datasets the fitted exponents come out far smaller than the $O(m^3)$ bound,
212
+ because the boundary matrices are sparse and pivot conflicts are rare. The
213
+ cohomology route is the quickest, since coboundary columns on VR complexes are
214
+ short.
215
+ - **Capping the dimension matters.** Most of the simplices, and most of the
216
+ cost, live in the top dimensions; not computing homology you do not need is
217
+ the cheapest optimisation available.
218
+ - **The topology of the proteins is legible.** The persistence diagrams pick out
219
+ components, loops, and voids straight from the coordinates, encouraging for
220
+ topology as a coordinate-free structural descriptor, though this is a small
221
+ proof-of-concept and the thesis is careful about not overclaiming.
222
+
223
+ ## References
224
+
225
+ The algorithms and theory come mainly from:
226
+
227
+ - H. Edelsbrunner, D. Letscher, A. Zomorodian. *Topological persistence and
228
+ simplification* (2002).
229
+ - A. Zomorodian, G. Carlsson. *Computing persistent homology* (2005).
230
+ - V. de Silva, D. Morozov, M. Vejdemo-Johansson. *Dualities in persistent
231
+ (co)homology* (2011).
232
+ - U. Bauer. *Ripser: efficient computation of Vietoris–Rips persistence
233
+ barcodes* (2021).
234
+ - C. Maria et al. *The GUDHI library* (2014).
235
+
236
+ Full citations are in the thesis bibliography.
237
+
238
+ ## License
239
+
240
+ Released under the MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,209 @@
1
+ # Persistent Homology
2
+
3
+ A from-scratch, readable implementation of persistent homology for
4
+ Vietoris–Rips filtrations, written as the computational half of an MSc thesis
5
+ in Topological Data Analysis.
6
+
7
+ The point of this code is not to be fast. Production libraries like Ripser and
8
+ GUDHI already are, and they are very hard to read. The point is the opposite:
9
+ every function here lines up with a definition or an algorithm from the thesis,
10
+ so that someone can follow the path from "a point cloud" to "a persistence
11
+ diagram" without taking any step on faith. Where there was a choice between a
12
+ clever trick and a transparent one, the transparent one won.
13
+
14
+ ## What it does
15
+
16
+ Given a finite point cloud, the pipeline:
17
+
18
+ 1. **Builds the Vietoris–Rips filtration** up to a chosen dimension and scale.
19
+ VR is the only filtration the code implements; Čech and Alpha are discussed
20
+ in the thesis as context but are not built here.
21
+ 2. **Assembles the combined boundary matrix** over $\mathbb{Z}/2\mathbb{Z}$,
22
+ stored sparsely as columns of row indices.
23
+ 3. **Reduces it** by one of three interchangeable strategies, and reads off the
24
+ birth–death pairs.
25
+ 4. **Returns persistence diagrams** per dimension, as `{dimension: [(birth,
26
+ death), ...]}`, ready to plot.
27
+
28
+ The three reduction strategies are the heart of the project, and you can switch
29
+ between them with a single argument:
30
+
31
+ | Strategy | What it is | Where it shines |
32
+ |---------------|-------------------------------------------------------------------|-----------------|
33
+ | `standard` | Plain left-to-right column reduction (Zomorodian–Carlsson) | The baseline; easiest to read |
34
+ | `clearing` | Standard reduction, but births in dimension *k* are zeroed once dimension *k+1* is done | Skips a lot of wasted work in high dimensions |
35
+ | `cohomology` | Reduces the anti-transpose instead (pCoh, de Silva–Morozov–Vejdemo-Johansson) | Much shorter columns on VR complexes; usually the fastest of the three |
36
+
37
+ All three return the *same* diagram — that is the whole content of the duality
38
+ results, and the test suite checks it explicitly on every dataset.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ git clone https://github.com/royaballo/persistent-homology.git
44
+ cd persistent-homology
45
+ pip install -e .
46
+ ```
47
+
48
+ That installs the `persistent_homology` package in editable mode along with its
49
+ dependencies (NumPy, SciPy, Matplotlib; `ripser` and `gudhi` are pulled in for
50
+ the correctness tests and benchmarks). Python 3.11+ is expected.
51
+
52
+ ## A first example
53
+
54
+ ```python
55
+ from persistent_homology.persistence import compute_persistence
56
+ from datasets.synthetic import make_circle
57
+
58
+ # 30 points sampled on a noisy circle
59
+ points = make_circle(n=30, noise=0.05)
60
+
61
+ # one long-lived H1 class is the circle's hole
62
+ diagram = compute_persistence(
63
+ points,
64
+ max_dim=2,
65
+ max_eps=2.0,
66
+ algorithm="cohomology", # or "standard", "clearing"
67
+ )
68
+
69
+ for dim, dgm in diagram.items():
70
+ print(f"H{dim}: {len(dgm)} features")
71
+ ```
72
+
73
+ To see it rather than read it:
74
+
75
+ ```python
76
+ from persistent_homology.plotting import plot_persistence_diagram
77
+
78
+ plot_persistence_diagram(diagram, max_dim=2)
79
+ ```
80
+
81
+ The circle gives one point sitting well above the diagonal in $H_1$ (the hole)
82
+ and a cloud of short-lived noise near it. That gap between signal and noise is
83
+ the thing persistent homology is for. Essential features — the ones that never
84
+ die — are drawn as triangles along a dashed `∞` line near the top, since their
85
+ true death is infinite and cannot be placed on the axis.
86
+
87
+ ## Repository structure
88
+
89
+ ```
90
+ persistent_homology/ the actual library
91
+ filtration.py Vietoris–Rips construction (flag complex from the 1-skeleton)
92
+ boundary.py boundary and coboundary matrices over Z/2Z
93
+ reduction.py standard, clearing, and cohomology (pCoh) reductions
94
+ persistence.py end-to-end entry point: points in, diagrams out
95
+ plotting.py persistence diagrams (and the log-log benchmark plots)
96
+
97
+ datasets/
98
+ synthetic.py circle, sphere, torus, cube samplers (known topology)
99
+ proteins.py loads Cα coordinates from PDB files
100
+
101
+ tests/
102
+ test_correctness.py checks our diagrams against Ripser and GUDHI
103
+
104
+ benchmarks/ timing and memory scripts; results land in benchmarks/results/
105
+ applications/ the protein case study (analyse_proteins.py, benchmark_lysozyme.py)
106
+ notebooks/ correctness, benchmark, and protein analysis walkthroughs
107
+ data/proteins/ PDB files, extracted Cα coordinates, and computed diagrams
108
+ figures/ generated plots (regenerated by the scripts; not meant to be edited by hand)
109
+ ```
110
+
111
+ If you only want to understand the method, read `filtration.py`, then
112
+ `boundary.py`, then `reduction.py`, in that order. They mirror the thesis
113
+ chapter on algorithms section by section.
114
+
115
+ ## How the thesis was built, in order
116
+
117
+ The work went in five steps, and the repository is laid out so that each one can
118
+ be re-run on its own.
119
+
120
+ **1. Implement the pipeline.** Build the VR filtration, the boundary matrices,
121
+ and the three reductions — the `persistent_homology/` package. The guiding rule
122
+ was a one-to-one match between code and theory rather than speed.
123
+
124
+ **2. Prove it is correct.** Before any benchmark means anything, the output has
125
+ to be right. `tests/test_correctness.py` runs our three algorithms on the
126
+ synthetic datasets and on small random complexes, and checks that the diagrams
127
+ agree with both Ripser and GUDHI up to the usual reordering. The
128
+ `correctness_verification.ipynb` notebook shows this in a readable form.
129
+
130
+ **3. Benchmark it.** With correctness settled, the `benchmarks/` scripts measure
131
+ how the three strategies scale. `computation_time.py` and `memory_usage.py`
132
+ sweep dataset size on circle, sphere, torus, and cube; `effect_filtration_scope.py`
133
+ isolates how much capping the homology dimension actually saves. Fitted scaling
134
+ exponents are written to `benchmarks/results/`.
135
+
136
+ **4. Put it to work on real data.** The protein case study in `applications/`
137
+ asks a concrete question: can persistent homology pick out structural features
138
+ of proteins straight from their Cα atomic coordinates, with no
139
+ chemistry-specific input? It runs on three PDB structures (hemoglobin `1A3N`,
140
+ lysozyme `1AKI`, GFP `1EMA`), computes $H_0$, $H_1$, $H_2$ diagrams, and tallies
141
+ the resulting Betti counts.
142
+
143
+ **5. Write it up.** The thesis ties the theory, the correctness evidence, the
144
+ scaling results, and the protein study together. The notebooks are the bridge:
145
+ they regenerate every figure the thesis uses.
146
+
147
+ ## Reproducing the results
148
+
149
+ Each step is one command. Run them from the repository root.
150
+
151
+ ```bash
152
+ # correctness: should report all-pass against Ripser and GUDHI
153
+ pytest
154
+
155
+ # timing and memory across synthetic datasets (writes CSVs to benchmarks/results/)
156
+ python benchmarks/computation_time.py
157
+ python benchmarks/memory_usage.py
158
+ python benchmarks/effect_filtration_scope.py
159
+
160
+ # protein case study (writes diagrams to data/proteins/results/ and plots to figures/)
161
+ python applications/analyse_proteins.py
162
+ python applications/benchmark_lysozyme.py
163
+ ```
164
+
165
+ The three notebooks in `notebooks/` read the CSVs and `.npy` diagrams produced
166
+ above and assemble the figures and tables exactly as they appear in the thesis.
167
+ Run the scripts first, then the notebooks.
168
+
169
+ A note on figures: everything in `figures/` is generated. Delete the folder and
170
+ re-run the scripts and you should get it back byte-for-byte, give or take
171
+ matplotlib versions.
172
+
173
+ ## What the results say
174
+
175
+ The short version, with the detail left for the thesis:
176
+
177
+ - **The three algorithms agree, always.** This is the duality between homology
178
+ and cohomology made concrete: same pairs, birth and death simplices swapped.
179
+ - **Empirical scaling sits well below the cubic worst case.** On the synthetic
180
+ datasets the fitted exponents come out far smaller than the $O(m^3)$ bound,
181
+ because the boundary matrices are sparse and pivot conflicts are rare. The
182
+ cohomology route is the quickest, since coboundary columns on VR complexes are
183
+ short.
184
+ - **Capping the dimension matters.** Most of the simplices, and most of the
185
+ cost, live in the top dimensions; not computing homology you do not need is
186
+ the cheapest optimisation available.
187
+ - **The topology of the proteins is legible.** The persistence diagrams pick out
188
+ components, loops, and voids straight from the coordinates, encouraging for
189
+ topology as a coordinate-free structural descriptor, though this is a small
190
+ proof-of-concept and the thesis is careful about not overclaiming.
191
+
192
+ ## References
193
+
194
+ The algorithms and theory come mainly from:
195
+
196
+ - H. Edelsbrunner, D. Letscher, A. Zomorodian. *Topological persistence and
197
+ simplification* (2002).
198
+ - A. Zomorodian, G. Carlsson. *Computing persistent homology* (2005).
199
+ - V. de Silva, D. Morozov, M. Vejdemo-Johansson. *Dualities in persistent
200
+ (co)homology* (2011).
201
+ - U. Bauer. *Ripser: efficient computation of Vietoris–Rips persistence
202
+ barcodes* (2021).
203
+ - C. Maria et al. *The GUDHI library* (2014).
204
+
205
+ Full citations are in the thesis bibliography.
206
+
207
+ ## License
208
+
209
+ Released under the MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,23 @@
1
+ """Persistent homology: a from-scratch implementation for Vietoris-Rips filtrations.
2
+
3
+ The package computes persistent homology of point clouds through three
4
+ interchangeable reduction strategies (standard, clearing, cohomology),
5
+ exposed through the single entry point :func:`compute_persistence`.
6
+ """
7
+
8
+ from .boundary import build_boundary_matrix
9
+ from .filtration import vr_filtration
10
+ from .persistence import compute_persistence, extract_diagram
11
+ from .reduction import (add_columns_z2, reduce_pcoh, reduce_standard,
12
+ reduce_with_clearing)
13
+
14
+ __all__ = [
15
+ "compute_persistence",
16
+ "vr_filtration",
17
+ "build_boundary_matrix",
18
+ "add_columns_z2",
19
+ "reduce_standard",
20
+ "reduce_with_clearing",
21
+ "reduce_pcoh",
22
+ "extract_diagram",
23
+ ]
@@ -0,0 +1,41 @@
1
+ """Boundary matrix construction and Z/2Z column arithmetic.
2
+
3
+ The boundary matrix encodes, for each simplex, its codimension-1 faces.
4
+ Over Z/2Z a column is stored simply as the sorted list of row indices
5
+ where it is non-zero, and column addition is symmetric difference.
6
+ """
7
+
8
+
9
+ def build_boundary_matrix(filtration):
10
+ """Build the combined boundary matrix.
11
+
12
+ Parameters
13
+ ----------
14
+ filtration : list of (filtration_value, simplex_tuple)
15
+ The filtration, as returned by :func:`vr_filtration`.
16
+
17
+ Returns
18
+ -------
19
+ columns : list of lists
20
+ "columns[j]" is the sorted list of row indices "i" such that
21
+ simplex "i" is a codimension-1 face of simplex "j".
22
+ """
23
+ # Map each simplex to its index in the filtration
24
+ simplex_to_idx = {}
25
+ for idx, (_, simplex) in enumerate(filtration):
26
+ simplex_to_idx[simplex] = idx
27
+
28
+ m = len(filtration)
29
+ columns = [[] for _ in range(m)]
30
+
31
+ for j, (_, simplex) in enumerate(filtration):
32
+ if len(simplex) <= 1:
33
+ continue # 0-simplices have empty boundary
34
+ # Faces: remove each vertex in turn
35
+ for i in range(len(simplex)):
36
+ face = simplex[:i] + simplex[i + 1:]
37
+ face_idx = simplex_to_idx[face]
38
+ columns[j].append(face_idx)
39
+ columns[j].sort()
40
+
41
+ return columns
@@ -0,0 +1 @@
1
+ """Dataset generators and loaders for the persistent homology experiments."""
@@ -0,0 +1,112 @@
1
+ """Download protein structures from the RCSB Protein Data Bank.
2
+
3
+ Downloads the three proteins used in the Chapter 6 case study:
4
+
5
+ * 1AKI: Lysozyme (single-domain enzyme, 129 residues)
6
+ * 1EMA: Green Fluorescent Protein (beta-barrel, 221 residues)
7
+ * 1A3N: Hemoglobin (tetramer, 572 residues)
8
+
9
+ The PDB files are cached locally; rerunning the script does not
10
+ re-download them. The script also extracts the C-alpha coordinates and
11
+ saves them as plain .npy arrays for downstream use.
12
+ """
13
+
14
+ from pathlib import Path
15
+
16
+ import numpy as np
17
+
18
+ from Bio.PDB import PDBList, PDBParser # type: ignore[import-not-found]
19
+
20
+ # Where to store the downloaded files and the extracted coordinates.
21
+ DATA_DIR = Path("data/proteins")
22
+ PDB_DIR = DATA_DIR / "pdb"
23
+ COORDS_DIR = DATA_DIR / "ca_coords"
24
+
25
+ # The three proteins used in the case study, with metadata for reference.
26
+ PROTEINS = [
27
+ {"id": "1AKI", "name": "Lysozyme", "expected_residues": 129},
28
+ {"id": "1EMA", "name": "Green Fluorescent Protein", "expected_residues": 221},
29
+ {"id": "1A3N", "name": "Hemoglobin", "expected_residues": 572},
30
+ ]
31
+
32
+
33
+ def download_pdb(pdb_id, target_dir):
34
+ """Download a PDB file via Biopython, return the local path.
35
+
36
+ Biopython caches the file by default; if the file is already present
37
+ it will not re-download. The filename pattern produced is
38
+ ``pdb{pdb_id_lower}.ent``; we rename it to ``{pdb_id}.pdb`` for
39
+ clarity.
40
+ """
41
+ target_dir.mkdir(parents=True, exist_ok=True)
42
+ target = target_dir / f"{pdb_id}.pdb"
43
+
44
+ if target.exists():
45
+ print(f" {pdb_id}: already present at {target}")
46
+ return target
47
+
48
+ pdbl = PDBList(verbose=False)
49
+ # retrieve_pdb_file writes to pdir as 'pdb{id_lower}.ent' (or .cif).
50
+ raw = pdbl.retrieve_pdb_file(
51
+ pdb_id,
52
+ pdir=str(target_dir),
53
+ file_format="pdb",
54
+ overwrite=False,
55
+ )
56
+ # Rename for consistency.
57
+ Path(raw).rename(target)
58
+ print(f" {pdb_id}: downloaded to {target}")
59
+ return target
60
+
61
+
62
+ def extract_ca_coordinates(pdb_path, pdb_id):
63
+ """Return an (n, 3) array of C-alpha coordinates from a PDB file.
64
+
65
+ Only the first model is used (most PDB files have one model;
66
+ NMR ensembles have several, of which we keep the first). All chains
67
+ are included, so multi-chain proteins like hemoglobin yield the
68
+ union of C-alpha atoms across subunits. Non-standard residues and
69
+ HETATM records are skipped.
70
+ """
71
+ parser = PDBParser(QUIET=True)
72
+ structure = parser.get_structure(pdb_id, str(pdb_path))
73
+
74
+ coords = []
75
+ model = next(structure.get_models()) # first model only
76
+ for chain in model:
77
+ for residue in chain:
78
+ # Skip waters, ligands, etc. Standard residues have
79
+ # hetflag == ' '.
80
+ if residue.id[0] != " ":
81
+ continue
82
+ if "CA" in residue:
83
+ coords.append(residue["CA"].get_coord())
84
+
85
+ if not coords:
86
+ raise RuntimeError(f"No C-alpha atoms found in {pdb_path}")
87
+ return np.array(coords, dtype=np.float64)
88
+
89
+
90
+ def main():
91
+ PDB_DIR.mkdir(parents=True, exist_ok=True)
92
+ COORDS_DIR.mkdir(parents=True, exist_ok=True)
93
+
94
+ for protein in PROTEINS:
95
+ pdb_id = protein["id"]
96
+ print(f"\n[{pdb_id}] {protein['name']}")
97
+
98
+ pdb_path = download_pdb(pdb_id, PDB_DIR)
99
+ coords = extract_ca_coordinates(pdb_path, pdb_id)
100
+
101
+ n = coords.shape[0]
102
+ expected = protein["expected_residues"]
103
+ status = "OK" if n == expected else f"WARNING: expected {expected}"
104
+ print(f" Extracted {n} C-alpha atoms [{status}]")
105
+
106
+ out = COORDS_DIR / f"{pdb_id}_ca.npy"
107
+ np.save(out, coords)
108
+ print(f" Saved coordinates to {out}")
109
+
110
+
111
+ if __name__ == "__main__":
112
+ main()