homeotopy 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) 2025 Erik Brinkman
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,39 @@
1
+ Metadata-Version: 2.1
2
+ Name: homeotopy
3
+ Version: 0.1.0
4
+ Summary: A library for computing homeomorphisms between some common stnandard topologies
5
+ License: MIT
6
+ Author: Erik Brinkman
7
+ Author-email: erik.brinkman@gmail.com
8
+ Requires-Python: >=3.12,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Dist: numba (>=0.61.0,<0.62.0)
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Homotopy
16
+
17
+ A python library for computing homeomorphisms between some common continuous
18
+ spaces.
19
+
20
+ ## Installation
21
+
22
+ ```sh
23
+ pip install homeotopy
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```py
29
+ import homeotopy
30
+
31
+ points = ...
32
+ # create a mapping from the simplex to the surface of the sphere
33
+ mapping = homeotopy.homeomorphism(homeotopy.simplex(), homeotopy.sphere())
34
+ sphere_points = mapping(points)
35
+
36
+ rev_mapping = reversed(mapping)
37
+ duplicate_points = rev_mapping(sphere_points)
38
+ ```
39
+
@@ -0,0 +1,24 @@
1
+ # Homotopy
2
+
3
+ A python library for computing homeomorphisms between some common continuous
4
+ spaces.
5
+
6
+ ## Installation
7
+
8
+ ```sh
9
+ pip install homeotopy
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```py
15
+ import homeotopy
16
+
17
+ points = ...
18
+ # create a mapping from the simplex to the surface of the sphere
19
+ mapping = homeotopy.homeomorphism(homeotopy.simplex(), homeotopy.sphere())
20
+ sphere_points = mapping(points)
21
+
22
+ rev_mapping = reversed(mapping)
23
+ duplicate_points = rev_mapping(sphere_points)
24
+ ```
@@ -0,0 +1,39 @@
1
+ """A library for creating some standard homeomorphisms.
2
+
3
+ This library is based around a set of `Topologies`. Calling `homeomorphism` with
4
+ two Topologies creates a homeomorphism from one topology to the other.
5
+ Topoligies should specify their domains, but where unspecified, the topoligies
6
+ try to conform to a reasonable standard domain. The homeomorphism should work
7
+ for closed set elements too, but thos elements may not be bijective.
8
+
9
+ Remarks
10
+ -------
11
+ It's probably important to note that floating point numbers are not real
12
+ numbers, and so none of these are really bijective at all.
13
+
14
+ Also note that this library does not define homeotopies. It's just named this
15
+ have to "py" in the name.
16
+ """
17
+
18
+ from ._ball import Ball, ball
19
+ from ._cube import Cube, cube
20
+ from ._homeomorphism import Homeomorphism, Topology, homeomorphism
21
+ from ._plane import Plane, plane
22
+ from ._simplex import Simplex, simplex
23
+ from ._sphere import Sphere, sphere
24
+
25
+ __all__ = (
26
+ "Homeomorphism",
27
+ "Topology",
28
+ "homeomorphism",
29
+ "Ball",
30
+ "ball",
31
+ "Cube",
32
+ "cube",
33
+ "Plane",
34
+ "plane",
35
+ "Sphere",
36
+ "sphere",
37
+ "Simplex",
38
+ "simplex",
39
+ )
@@ -0,0 +1,45 @@
1
+ import math
2
+ from dataclasses import dataclass
3
+
4
+ import numpy as np
5
+ from numpy.typing import NDArray
6
+
7
+ from ._homeomorphism import Topology
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class Ball(Topology):
12
+ """The topology of the p-norm ball.
13
+
14
+ This represents all points in R^n s.t. ||x||_p < 1, although it should also
15
+ work fo the boundary.
16
+ """
17
+
18
+ p: float
19
+
20
+ def __post_init__(self):
21
+ if self.p <= 0:
22
+ raise ValueError(f"p must be greater than or equal to 0: {self.p:g}")
23
+
24
+ def to_inf_ball(self, points: NDArray[np.float64]) -> NDArray[np.float64]:
25
+ if self.p == math.inf:
26
+ return points
27
+ else:
28
+ tiny = np.finfo(points.dtype).smallest_normal
29
+ source = np.linalg.norm(points, self.p, -1)
30
+ target = np.abs(points).max(-1) + tiny
31
+ return np.clip(points * (source / target)[..., None], -1, 1)
32
+
33
+ def from_inf_ball(self, points: NDArray[np.float64]) -> NDArray[np.float64]:
34
+ if self.p == math.inf:
35
+ return points
36
+ else:
37
+ tiny = np.finfo(points.dtype).smallest_normal
38
+ source = np.abs(points).max(-1)
39
+ target = np.linalg.norm(points, self.p, -1) + tiny
40
+ return points * (source / target)[..., None]
41
+
42
+
43
+ def ball(p: float = 2.0) -> Ball:
44
+ """Create a topology of the interior of the p-norm unit ball."""
45
+ return Ball(p)
@@ -0,0 +1,28 @@
1
+ from dataclasses import dataclass
2
+ from functools import cache
3
+
4
+ import numpy as np
5
+ from numpy.typing import NDArray
6
+
7
+ from ._homeomorphism import Topology
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class Cube(Topology):
12
+ """The topology of the unit hyper cube.
13
+
14
+ This represents all points in R^n s.t 0 < x_i < 1, although the boundary
15
+ should also work.
16
+ """
17
+
18
+ def to_inf_ball(self, points: NDArray[np.float64]) -> NDArray[np.float64]:
19
+ return np.clip(points * 2 - 1, -1, 1)
20
+
21
+ def from_inf_ball(self, points: NDArray[np.float64]) -> NDArray[np.float64]:
22
+ return np.clip((points + 1) / 2, 0, 1)
23
+
24
+
25
+ @cache
26
+ def cube() -> Cube:
27
+ """Create a topology of the unit cube."""
28
+ return Cube()
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+
6
+ import numpy as np
7
+ from numpy.typing import NDArray
8
+
9
+
10
+ class Topology(ABC):
11
+ """An abstract topological space.
12
+
13
+ For this library to work, each Topologiy should define a homeomorphism from
14
+ it to the inf-norm ball.
15
+
16
+ Remarks
17
+ -------
18
+ `to_inf_ball` and `from_inf_ball` are not meant to be called in isolation,
19
+ but rather used in combination with `homeomorphism`.
20
+ """
21
+
22
+ @abstractmethod
23
+ def to_inf_ball(self, points: NDArray[np.float64]) -> NDArray[np.float64]:
24
+ """Map a set of points in this topology to the inf-ball.
25
+
26
+ Parameters
27
+ ----------
28
+ points : (..., d_in)
29
+ A set of points in the input topological space
30
+
31
+ Returns
32
+ -------
33
+ points : (..., d_out)
34
+ A set of points in the inf-norm ball, e.g. -1 < x_i < 1 for points
35
+ in the open topology, but points can be mapped to the border for
36
+ border points in the source topology.
37
+ """
38
+ pass
39
+
40
+ @abstractmethod
41
+ def from_inf_ball(self, points: NDArray[np.float64]) -> NDArray[np.float64]:
42
+ """Map a set of points from the inf-ball to this topology.
43
+
44
+ Parameters
45
+ ----------
46
+ points : (..., d_in)
47
+ A set of points in the inf-norm ball, e.g. -1 < x_i < 1 for points
48
+ in the open topology, but points on the boarder should be handled as
49
+ well.
50
+
51
+ Returns
52
+ -------
53
+ points : (..., d_out)
54
+ A set of points in the topological space.
55
+ """
56
+ pass
57
+
58
+
59
+ @dataclass(frozen=True, slots=True)
60
+ class Homeomorphism:
61
+ """A homeomorphism from source to target.
62
+
63
+ Homeomorphisms can be called on points in the source domain to map them to
64
+ points in the target domain. They can also be `reversed` to create the
65
+ inverse mapping.
66
+
67
+
68
+ Example
69
+ -------
70
+
71
+ from homeotopy import homeomorphism, ball, simplex
72
+ import numpy as np
73
+
74
+ forward = homeomorphism(ball(1), simplex())
75
+ backward = reversed(forward)
76
+
77
+ ball_points = ...
78
+ simplex_points = forwad(ball_points)
79
+ backward(simplex_points)
80
+
81
+ """
82
+
83
+ source: Topology
84
+ target: Topology
85
+
86
+ def __reversed__(self) -> Homeomorphism:
87
+ return Homeomorphism(self.target, self.source)
88
+
89
+ def __call__(self, points: NDArray[np.float64]) -> NDArray[np.float64]:
90
+ return self.target.from_inf_ball(self.source.to_inf_ball(points))
91
+
92
+
93
+ def homeomorphism(source: Topology, target: Topology) -> Homeomorphism:
94
+ """Create a Homeomorphism from a source and a target topology."""
95
+ return Homeomorphism(source, target)
@@ -0,0 +1,34 @@
1
+ from dataclasses import dataclass
2
+ from functools import cache
3
+
4
+ import numpy as np
5
+ from numpy.typing import NDArray
6
+
7
+ from ._homeomorphism import Topology
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class Plane(Topology):
12
+ """The topology of the euclidian plane.
13
+
14
+ This represents all points in R^n, but boundary points map to inf.
15
+
16
+ Remarks
17
+ -------
18
+ While translations will keep all points valid, this will try to keep points
19
+ at the "center" of the space mapped to (0, 0, ..., 0).
20
+ """
21
+
22
+ def to_inf_ball(self, points: NDArray[np.float64]) -> NDArray[np.float64]:
23
+ return np.tanh(points)
24
+
25
+ def from_inf_ball(self, points: NDArray[np.float64]) -> NDArray[np.float64]:
26
+ # -1 and 1 raise this warning but produce the correct result, we clip in case things are a little outside
27
+ with np.errstate(divide="ignore"):
28
+ return np.arctanh(np.clip(points, -1, 1))
29
+
30
+
31
+ @cache
32
+ def plane() -> Plane:
33
+ """Create a topology of the euclidian plane."""
34
+ return Plane()
@@ -0,0 +1,62 @@
1
+ from dataclasses import dataclass
2
+ from functools import cache
3
+
4
+ import numba as nb
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+
8
+ from ._homeomorphism import Topology
9
+
10
+ # NOTE we use this to avoid divide by zero errors
11
+
12
+
13
+ @nb.jit(nb.float64[:, ::1](nb.int64), parallel=True, cache=True, nogil=True)
14
+ def basis(dim: int) -> NDArray[np.float64]:
15
+ """Create a basis for rotating the simplex onto the origin of dim - 1"""
16
+ res = np.empty((dim, dim))
17
+ for i in nb.prange(0, dim - 1):
18
+ num = i + 1
19
+ frac = 1 / (num + 1)
20
+ res[i, :num] = -((frac / num) ** 0.5)
21
+ res[i, num] = (1 - frac) ** 0.5
22
+ res[i, num + 1 :] = 0
23
+ res[-1] = (1 / dim) ** 0.5
24
+ return res
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class Simplex(Topology):
29
+ """The topology of the simplex
30
+
31
+ This represents all points in R^n s.t. 0 < x_i and Σx_i = 1.
32
+ """
33
+
34
+ def to_inf_ball(self, points: NDArray[np.float64]) -> NDArray[np.float64]:
35
+ tiny = np.finfo(points.dtype).smallest_normal
36
+ d = points.shape[-1]
37
+
38
+ simp_direc = points - np.full(d, 1 / d)
39
+ isimp_a = d * np.maximum(-simp_direc, simp_direc / (d - 1)).max(-1)
40
+
41
+ cube_direc = simp_direc @ basis(d)[: d - 1].T
42
+ icube_a = np.max(np.abs(cube_direc), -1) + tiny
43
+
44
+ return np.clip(cube_direc * (isimp_a / icube_a)[..., None], -1, 1)
45
+
46
+ def from_inf_ball(self, points: NDArray[np.float64]) -> NDArray[np.float64]:
47
+ tiny = np.finfo(points.dtype).smallest_normal
48
+ d = points.shape[-1]
49
+
50
+ icube_a = np.max(np.abs(points), -1)
51
+
52
+ simp_direc = np.insert(points, d, 0, -1) @ basis(d + 1)
53
+ isimp_a = (d + 1) * np.maximum(-simp_direc, simp_direc / d).max(-1) + tiny
54
+
55
+ raw = simp_direc * (icube_a / isimp_a)[..., None] + np.full(d + 1, 1 / (d + 1))
56
+ return np.clip(raw, 0, 1)
57
+
58
+
59
+ @cache
60
+ def simplex() -> Simplex:
61
+ """Create the topology of the simplex."""
62
+ return Simplex()
@@ -0,0 +1,49 @@
1
+ from dataclasses import dataclass
2
+ from functools import cache
3
+
4
+ import numpy as np
5
+ from numpy.typing import NDArray
6
+
7
+ from ._homeomorphism import Topology
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class Sphere(Topology):
12
+ """The topology of the unit sphere.
13
+
14
+ This represents all points in R^n s.t. ||x||_2 = 1, except for the point
15
+ (1, 0, ..., 0). That point is considered the boundary of the space, and will
16
+ be mapped the largest closed point in some other topologies.
17
+
18
+ Remarks
19
+ -------
20
+ If you had points on the surface of another p-ball, you could use the ball
21
+ homeomorphism to first map them onto the surface of the 2-ball, and then
22
+ apply this homeomorphism.
23
+ """
24
+
25
+ # NOTE for both of these we need to special case (1, 0, ..., 0) to (1, 1, ..., 1) and vice versa
26
+ def to_inf_ball(self, points: NDArray[np.float64]) -> NDArray[np.float64]:
27
+ tiny = np.finfo(points.dtype).smallest_normal
28
+ scale = 1 - points[..., :1]
29
+
30
+ normal = np.clip(np.tanh(points[..., 1:] / np.maximum(scale, tiny)), -1, 1)
31
+ return np.where(scale <= 0, 1, normal)
32
+
33
+ def from_inf_ball(self, points: NDArray[np.float64]) -> NDArray[np.float64]:
34
+ big = np.finfo(points.dtype).max
35
+ with np.errstate(divide="ignore"):
36
+ plane = np.arctanh(points)
37
+
38
+ s2 = (plane[..., None, :] @ plane[..., None])[..., 0]
39
+ s2p = np.minimum(s2 + 1, big)
40
+
41
+ x0 = np.where(s2 == np.inf, 1, (s2 - 1) / s2p)
42
+ xns = np.where(s2 == np.inf, 0, 2 * plane / s2p)
43
+ return np.concatenate([x0, xns], -1)
44
+
45
+
46
+ @cache
47
+ def sphere() -> Sphere:
48
+ """Create a topology fot the unit sphere."""
49
+ return Sphere()
@@ -0,0 +1,23 @@
1
+ [tool.poetry]
2
+ name = "homeotopy"
3
+ version = "0.1.0"
4
+ description = "A library for computing homeomorphisms between some common stnandard topologies"
5
+ authors = ["Erik Brinkman <erik.brinkman@gmail.com>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.12"
11
+ numba = "^0.61.0"
12
+
13
+
14
+ [tool.poetry.group.dev.dependencies]
15
+ pytest = "^8.3.4"
16
+ ruff = "^0.9.2"
17
+ ipykernel = "^6.29.5"
18
+ sphinx = "^8.1.3"
19
+ myst-parser = "^4.0.0"
20
+
21
+ [build-system]
22
+ requires = ["poetry-core"]
23
+ build-backend = "poetry.core.masonry.api"