redzed-tda 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,28 @@
1
+ cmake_minimum_required(VERSION 3.15...3.31)
2
+ project(redzed LANGUAGES CXX)
3
+
4
+ set(CMAKE_CXX_STANDARD 17)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+ set(CMAKE_CXX_EXTENSIONS OFF)
7
+
8
+ # Development.Module (not the full Development) is what a extension module needs,
9
+ # and is the component that works when there are no static Python libraries around
10
+ find_package(Python REQUIRED COMPONENTS Interpreter Development.Module)
11
+ find_package(pybind11 CONFIG REQUIRED)
12
+
13
+ pybind11_add_module(_core cpp/bindings.cpp)
14
+
15
+ target_compile_definitions(_core PRIVATE
16
+ REDZED_NO_MAIN # drop the command line driver in redzed.cpp
17
+ NDEBUG
18
+ )
19
+
20
+ if(MSVC)
21
+ target_compile_options(_core PRIVATE /O2)
22
+ # windows.h defines min and max as macros, which breaks std::max in redzed.cpp
23
+ target_compile_definitions(_core PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN)
24
+ else()
25
+ target_compile_options(_core PRIVATE -O3)
26
+ endif()
27
+
28
+ install(TARGETS _core DESTINATION redzed_tda)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nathan Kershaw
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
13
+ all 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
21
+ THE SOFTWARE.
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: redzed-tda
3
+ Version: 0.1.0
4
+ Summary: Fast Vietoris-Rips persistent homology
5
+ Keywords: topology,persistent homology,vietoris-rips,tda
6
+ Author-Email: Chris Kapulkin <krzysztof.r.kapulkin@vanderbilt.edu>, Nathan Kershaw <nkershaw@uwo.ca>
7
+ Maintainer-Email: Nathan Kershaw <nkershaw@uwo.ca>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: C++
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
21
+ Classifier: Typing :: Typed
22
+ Project-URL: Homepage, https://github.com/nkershaw01/RedZeD
23
+ Project-URL: Source, https://github.com/nkershaw01/RedZeD/tree/main/python
24
+ Requires-Python: >=3.9
25
+ Requires-Dist: numpy>=1.21
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest>=7; extra == "test"
28
+ Description-Content-Type: text/markdown
29
+
30
+ # redzed-tda
31
+
32
+ Fast Vietoris–Rips persistent homology, implemented in C++ with a NumPy interface.
33
+
34
+ This is the Python package for **RedZeD**, an algorithm to compute Vietoris-Rips
35
+ persistent homology. A key technique is *active enumeration*, allowing one to avoid enumerating
36
+ almost all (n+1) birth simplices in a filtration.
37
+ RedZeD often gives a considerable improvement in both time and memory over
38
+ existing implementations. Full details are in the paper:
39
+
40
+ > Chris Kapulkin and Nathan Kershaw.
41
+ > *RedZeD: Computing persistent homology by Reduction to Zero Differentials.*
42
+ > arXiv:2606.06310 (2026). <https://arxiv.org/abs/2606.06310>
43
+
44
+ The [main repository](https://github.com/nkershaw01/RedZeD) also holds the
45
+ reference Julia implementation.
46
+
47
+ If you encounter any errors or bugs, please email nkershaw@uwo.ca.
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pip install redzed-tda
53
+ ```
54
+
55
+ The distribution is `redzed-tda` and the import name is `redzed_tda`. (The name
56
+ `redzed` on PyPI belongs to an unrelated project.)
57
+
58
+ Wheels are provided for Linux, macOS and Windows, so no compiler is needed.
59
+
60
+ If no wheel matches your platform, or your Python version is newer than the last
61
+ release, pip falls back to building from source. That needs a C++17 compiler on
62
+ your machine. On Windows the compiler must be MSVC, not MinGW.
63
+
64
+ ## Quick start
65
+
66
+ ```python
67
+ import numpy as np
68
+ import redzed_tda as rz
69
+
70
+ points = np.random.default_rng(0).normal(size=(200, 3))
71
+ distances = rz.pairwise_distances(points)
72
+
73
+ h0, h1 = rz.rips(distances, maxdim=1)
74
+
75
+ print(h1) # (k, 2) array of [birth, death] rows
76
+ ```
77
+
78
+ ## Input
79
+
80
+ `rips` takes a **distance matrix, not coordinates**. If your data is an array of
81
+ points in R^n, either form works:
82
+
83
+ ```python
84
+ # square (n, n) matrix
85
+ h0, h1 = rz.rips(rz.pairwise_distances(points), maxdim=1)
86
+
87
+ # condensed vector of n(n-1)/2 values, in scipy pdist order
88
+ from scipy.spatial.distance import pdist
89
+ h0, h1 = rz.rips(pdist(points), maxdim=1)
90
+ ```
91
+
92
+ pairwise_distances and pdist both take in an array of points and return the
93
+ distance matrix. You may also pass redzed_tda.rips a square matrix constructed
94
+ yourself. Only the lower triangle of a square input is read, so your matrix is
95
+ assumed symmetric with a zero diagonal. Passing a non-square array raises
96
+ `ValueError`, but symmetry is not checked, so the user is responsible for
97
+ ensuring the matrix is symmetric.
98
+
99
+ ## Options
100
+
101
+ | Argument | Default | Meaning |
102
+ | --- | --- | --- |
103
+ | `distances` | — | Square `(n, n)` matrix or condensed `n(n-1)/2` vector. |
104
+ | `maxdim` | `1` | Highest homology dimension. `maxdim=1` returns H0 and H1. |
105
+ | `threshold` | `inf` | Ignore simplices with greater diameter. |
106
+ | `simplex_pairs` | `False` | Also return the birth and death simplex associated to every interval. |
107
+
108
+ ## Output
109
+
110
+ A list of `maxdim + 1` arrays. Entry `d` has shape `(k, 2)` and holds the
111
+ `[birth, death]` pairs in dimension `d`:
112
+
113
+ ```python
114
+ diagrams = rz.rips(distances, maxdim=2)
115
+ h0, h1, h2 = diagrams
116
+ ```
117
+
118
+ With `simplex_pairs=True` you get `(diagrams, simplices)` instead, where
119
+ `simplices[d][i]` is a `(birth_vertices, death_vertices)` tuple corresponding to
120
+ `diagrams[d][i]`. The death tuple is empty for an infinite interval.
121
+
122
+ ## Building from source
123
+
124
+ ```bash
125
+ git clone https://github.com/nkershaw01/RedZeD
126
+ cd RedZeD/python
127
+ pip install -e ".[test]"
128
+ pytest
129
+ ```
130
+
131
+ Requires a C++17 compiler. On Windows that must be MSVC.
132
+
133
+ ## Citing
134
+
135
+ If you use this software in published work, please cite the paper:
136
+
137
+ ```bibtex
138
+ @misc{kapulkin2026redzed,
139
+ title = {RedZeD: Computing persistent homology by Reduction to Zero Differentials},
140
+ author = {Kapulkin, Chris and Kershaw, Nathan},
141
+ year = {2026},
142
+ eprint = {2606.06310},
143
+ archivePrefix = {arXiv},
144
+ primaryClass = {cs.CG},
145
+ doi = {10.48550/arXiv.2606.06310},
146
+ url = {https://arxiv.org/abs/2606.06310}
147
+ }
148
+ ```
149
+
150
+ ## License
151
+
152
+ MIT. See [LICENSE](https://github.com/nkershaw01/RedZeD/blob/main/python/LICENSE).
@@ -0,0 +1,123 @@
1
+ # redzed-tda
2
+
3
+ Fast Vietoris–Rips persistent homology, implemented in C++ with a NumPy interface.
4
+
5
+ This is the Python package for **RedZeD**, an algorithm to compute Vietoris-Rips
6
+ persistent homology. A key technique is *active enumeration*, allowing one to avoid enumerating
7
+ almost all (n+1) birth simplices in a filtration.
8
+ RedZeD often gives a considerable improvement in both time and memory over
9
+ existing implementations. Full details are in the paper:
10
+
11
+ > Chris Kapulkin and Nathan Kershaw.
12
+ > *RedZeD: Computing persistent homology by Reduction to Zero Differentials.*
13
+ > arXiv:2606.06310 (2026). <https://arxiv.org/abs/2606.06310>
14
+
15
+ The [main repository](https://github.com/nkershaw01/RedZeD) also holds the
16
+ reference Julia implementation.
17
+
18
+ If you encounter any errors or bugs, please email nkershaw@uwo.ca.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install redzed-tda
24
+ ```
25
+
26
+ The distribution is `redzed-tda` and the import name is `redzed_tda`. (The name
27
+ `redzed` on PyPI belongs to an unrelated project.)
28
+
29
+ Wheels are provided for Linux, macOS and Windows, so no compiler is needed.
30
+
31
+ If no wheel matches your platform, or your Python version is newer than the last
32
+ release, pip falls back to building from source. That needs a C++17 compiler on
33
+ your machine. On Windows the compiler must be MSVC, not MinGW.
34
+
35
+ ## Quick start
36
+
37
+ ```python
38
+ import numpy as np
39
+ import redzed_tda as rz
40
+
41
+ points = np.random.default_rng(0).normal(size=(200, 3))
42
+ distances = rz.pairwise_distances(points)
43
+
44
+ h0, h1 = rz.rips(distances, maxdim=1)
45
+
46
+ print(h1) # (k, 2) array of [birth, death] rows
47
+ ```
48
+
49
+ ## Input
50
+
51
+ `rips` takes a **distance matrix, not coordinates**. If your data is an array of
52
+ points in R^n, either form works:
53
+
54
+ ```python
55
+ # square (n, n) matrix
56
+ h0, h1 = rz.rips(rz.pairwise_distances(points), maxdim=1)
57
+
58
+ # condensed vector of n(n-1)/2 values, in scipy pdist order
59
+ from scipy.spatial.distance import pdist
60
+ h0, h1 = rz.rips(pdist(points), maxdim=1)
61
+ ```
62
+
63
+ pairwise_distances and pdist both take in an array of points and return the
64
+ distance matrix. You may also pass redzed_tda.rips a square matrix constructed
65
+ yourself. Only the lower triangle of a square input is read, so your matrix is
66
+ assumed symmetric with a zero diagonal. Passing a non-square array raises
67
+ `ValueError`, but symmetry is not checked, so the user is responsible for
68
+ ensuring the matrix is symmetric.
69
+
70
+ ## Options
71
+
72
+ | Argument | Default | Meaning |
73
+ | --- | --- | --- |
74
+ | `distances` | — | Square `(n, n)` matrix or condensed `n(n-1)/2` vector. |
75
+ | `maxdim` | `1` | Highest homology dimension. `maxdim=1` returns H0 and H1. |
76
+ | `threshold` | `inf` | Ignore simplices with greater diameter. |
77
+ | `simplex_pairs` | `False` | Also return the birth and death simplex associated to every interval. |
78
+
79
+ ## Output
80
+
81
+ A list of `maxdim + 1` arrays. Entry `d` has shape `(k, 2)` and holds the
82
+ `[birth, death]` pairs in dimension `d`:
83
+
84
+ ```python
85
+ diagrams = rz.rips(distances, maxdim=2)
86
+ h0, h1, h2 = diagrams
87
+ ```
88
+
89
+ With `simplex_pairs=True` you get `(diagrams, simplices)` instead, where
90
+ `simplices[d][i]` is a `(birth_vertices, death_vertices)` tuple corresponding to
91
+ `diagrams[d][i]`. The death tuple is empty for an infinite interval.
92
+
93
+ ## Building from source
94
+
95
+ ```bash
96
+ git clone https://github.com/nkershaw01/RedZeD
97
+ cd RedZeD/python
98
+ pip install -e ".[test]"
99
+ pytest
100
+ ```
101
+
102
+ Requires a C++17 compiler. On Windows that must be MSVC.
103
+
104
+ ## Citing
105
+
106
+ If you use this software in published work, please cite the paper:
107
+
108
+ ```bibtex
109
+ @misc{kapulkin2026redzed,
110
+ title = {RedZeD: Computing persistent homology by Reduction to Zero Differentials},
111
+ author = {Kapulkin, Chris and Kershaw, Nathan},
112
+ year = {2026},
113
+ eprint = {2606.06310},
114
+ archivePrefix = {arXiv},
115
+ primaryClass = {cs.CG},
116
+ doi = {10.48550/arXiv.2606.06310},
117
+ url = {https://arxiv.org/abs/2606.06310}
118
+ }
119
+ ```
120
+
121
+ ## License
122
+
123
+ MIT. See [LICENSE](https://github.com/nkershaw01/RedZeD/blob/main/python/LICENSE).
@@ -0,0 +1,119 @@
1
+ // Python bindings for redzed
2
+
3
+ #include <pybind11/pybind11.h>
4
+ #include <pybind11/numpy.h>
5
+ #include <pybind11/stl.h>
6
+
7
+ #include "redzed.cpp"
8
+
9
+ namespace py = pybind11;
10
+
11
+ typedef py::array_t<double, py::array::c_style | py::array::forcecast> DoubleArray;
12
+
13
+ // ------------------------------------------------------------
14
+ // Input conversion
15
+ // ------------------------------------------------------------
16
+
17
+ // Accepts either a square (n, n) matrix or a condensed 1-D vector in scipy's
18
+ // pdist order
19
+ static DistanceMatrix matrix_from_array(const DoubleArray& arr) {
20
+ DistanceMatrix D;
21
+
22
+ if (arr.ndim() == 2) {
23
+ if (arr.shape(0) != arr.shape(1))
24
+ throw std::invalid_argument("a 2-D distance matrix must be square, got shape (" +
25
+ std::to_string(arr.shape(0)) + ", " +
26
+ std::to_string(arr.shape(1)) + ")");
27
+ int nv = (int)arr.shape(0);
28
+ D.resize_lower(nv);
29
+ auto a = arr.unchecked<2>();
30
+ // only the lower triangle is read, so an asymmetric input is not an error here
31
+ for (int j = 1; j < nv; ++j) {
32
+ value_t* dst = D.mat.data() + DistanceMatrix::offset(0, j);
33
+ for (int i = 0; i < j; ++i) dst[i] = (value_t)a(j, i);
34
+ }
35
+ } else if (arr.ndim() == 1) {
36
+ int64_t m = (int64_t)arr.shape(0);
37
+ // invert m = nv(nv-1)/2
38
+ int64_t nv = (int64_t)((1.0 + std::sqrt(1.0 + 8.0 * (double)m)) / 2.0 + 0.5);
39
+ if (nv < 1 || nv * (nv - 1) / 2 != m)
40
+ throw std::invalid_argument("a condensed distance vector must hold n(n-1)/2 values, "
41
+ "and " + std::to_string(m) + " is not such a length");
42
+ D.resize_lower((int)nv);
43
+ auto a = arr.unchecked<1>();
44
+ int64_t k = 0;
45
+ for (int64_t i = 0; i < nv; ++i)
46
+ for (int64_t j = i + 1; j < nv; ++j)
47
+ D.mat[DistanceMatrix::offset((int)i, (int)j)] = (value_t)a((py::ssize_t)k++);
48
+ } else {
49
+ throw std::invalid_argument("expected a 2-D distance matrix or a 1-D condensed "
50
+ "distance vector, got a " + std::to_string(arr.ndim()) +
51
+ "-D array");
52
+ }
53
+ return D;
54
+ }
55
+
56
+ // ------------------------------------------------------------
57
+ // Output conversion
58
+ // ------------------------------------------------------------
59
+
60
+ // one array of [birth, death] intervals per dimension
61
+ static py::array_t<double> pairs_to_array(const std::vector<std::pair<value_t, value_t>>& v) {
62
+ py::array_t<double> out({(py::ssize_t)v.size(), (py::ssize_t)2});
63
+ auto r = out.mutable_unchecked<2>();
64
+ for (size_t t = 0; t < v.size(); ++t) {
65
+ r((py::ssize_t)t, 0) = (double)v[t].first;
66
+ r((py::ssize_t)t, 1) = (double)v[t].second;
67
+ }
68
+ return out;
69
+ }
70
+
71
+ // (birth_vertices, death_vertices)
72
+ static py::list spx_to_list(const std::vector<SimplexPair>& v) {
73
+ py::list out;
74
+ for (const SimplexPair& sp : v)
75
+ out.append(py::make_tuple(py::cast(sp.first), py::cast(sp.second)));
76
+ return out;
77
+ }
78
+
79
+ // ------------------------------------------------------------
80
+ // Entry point
81
+ // ------------------------------------------------------------
82
+
83
+ static py::object rips_impl(const DoubleArray& distances, int maxdim, double threshold,
84
+ bool simplex_pairs) {
85
+ if (maxdim < 0)
86
+ throw std::invalid_argument("maxdim must be >= 0, got " + std::to_string(maxdim));
87
+
88
+ DistanceMatrix D = matrix_from_array(distances);
89
+ if (D.n < 1)
90
+ throw std::invalid_argument("need at least one point");
91
+
92
+ std::vector<std::vector<std::pair<value_t, value_t>>> pairs;
93
+ std::vector<std::vector<SimplexPair>> spx;
94
+ {
95
+ // the solver never touches Python, so other threads can run meanwhile
96
+ py::gil_scoped_release unlocked;
97
+ pairs = redzed(D, maxdim, (value_t)threshold, simplex_pairs,
98
+ simplex_pairs ? &spx : nullptr);
99
+ }
100
+
101
+ py::list diagrams;
102
+ for (int d = 0; d <= maxdim; ++d) diagrams.append(pairs_to_array(pairs[d]));
103
+ if (!simplex_pairs) return diagrams;
104
+
105
+ py::list simplices;
106
+ for (int d = 0; d <= maxdim; ++d) simplices.append(spx_to_list(spx[d]));
107
+ return py::make_tuple(diagrams, simplices);
108
+ }
109
+
110
+ PYBIND11_MODULE(_core, m) {
111
+ m.doc() = "Compiled core of redzed: Vietoris-Rips persistent homology.";
112
+
113
+ m.def("rips", &rips_impl,
114
+ py::arg("distances"),
115
+ py::arg("maxdim") = 1,
116
+ py::arg("threshold") = std::numeric_limits<double>::infinity(),
117
+ py::arg("simplex_pairs") = false,
118
+ "Compute Vietoris-Rips persistence from a distance matrix.");
119
+ }