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.
- persistent_homology-0.1.0/LICENSE +21 -0
- persistent_homology-0.1.0/PKG-INFO +240 -0
- persistent_homology-0.1.0/README.md +209 -0
- persistent_homology-0.1.0/persistent_homology/__init__.py +23 -0
- persistent_homology-0.1.0/persistent_homology/boundary.py +41 -0
- persistent_homology-0.1.0/persistent_homology/datasets/__init__.py +1 -0
- persistent_homology-0.1.0/persistent_homology/datasets/proteins.py +112 -0
- persistent_homology-0.1.0/persistent_homology/datasets/synthetic.py +122 -0
- persistent_homology-0.1.0/persistent_homology/filtration.py +52 -0
- persistent_homology-0.1.0/persistent_homology/persistence.py +99 -0
- persistent_homology-0.1.0/persistent_homology/plotting.py +366 -0
- persistent_homology-0.1.0/persistent_homology/reduction.py +218 -0
- persistent_homology-0.1.0/persistent_homology.egg-info/PKG-INFO +240 -0
- persistent_homology-0.1.0/persistent_homology.egg-info/SOURCES.txt +18 -0
- persistent_homology-0.1.0/persistent_homology.egg-info/dependency_links.txt +1 -0
- persistent_homology-0.1.0/persistent_homology.egg-info/requires.txt +14 -0
- persistent_homology-0.1.0/persistent_homology.egg-info/top_level.txt +1 -0
- persistent_homology-0.1.0/pyproject.toml +52 -0
- persistent_homology-0.1.0/setup.cfg +4 -0
- persistent_homology-0.1.0/tests/test_correctness.py +83 -0
|
@@ -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()
|