Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions RELEASE.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
Unreleased
- Preserve integer persistence-image pixel counts and pad constructor ranges consistently with parameter updates. Correct division roundoff against the actual covered width. Reassigning an unchanged grid parameter keeps the grid unchanged.

0.4.1
- Correct Lp norms of signed persistence landscapes, including zero crossings and fractional p. Compute segment integrals from endpoint magnitudes to avoid coordinate-intercept cancellation.

Expand Down
60 changes: 30 additions & 30 deletions persim/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@
__all__ = ["PersImage", "PersistenceImager"]


def _pixel_count(bounds, pixel_size):
"""Count covering pixels without quotient-rounding an exact multiple up."""
span = bounds[1] - bounds[0]
count = int(np.ceil(span / pixel_size))
if count > 0 and (count - 1) * pixel_size >= span:
return count - 1
return count


@deprecated(
reason="""Replaced with the class :class:`persim.PersistenceImager`.""",
version="0.1.5",
Expand Down Expand Up @@ -345,12 +354,13 @@ def __init__(
self._pixel_size = pixel_size
self._birth_range = birth_range
self._pers_range = pers_range
self._width = birth_range[1] - birth_range[0]
self._height = pers_range[1] - pers_range[0]
# Retain integer counts: dividing rounded widths can lose a pixel.
self._resolution = (
int(self._width / self._pixel_size),
int(self._height / self._pixel_size),
_pixel_count(birth_range, pixel_size),
_pixel_count(pers_range, pixel_size),
)
self._width = self._resolution[0] * pixel_size
self._height = self._resolution[1] * pixel_size
self._create_mesh()

@property
Expand Down Expand Up @@ -403,19 +413,15 @@ def pixel_size(self):

@pixel_size.setter
def pixel_size(self, val):
if val == self._pixel_size:
return
self._pixel_size = val
self._width = (
int(np.ceil((self.birth_range[1] - self.birth_range[0]) / self.pixel_size))
* self.pixel_size
)
self._height = (
int(np.ceil((self.pers_range[1] - self.pers_range[0]) / self.pixel_size))
* self.pixel_size
)
self._resolution = (
int(self.width / self.pixel_size),
int(self.height / self.pixel_size),
_pixel_count(self.birth_range, self.pixel_size),
_pixel_count(self.pers_range, self.pixel_size),
)
self._width = self._resolution[0] * self.pixel_size
self._height = self._resolution[1] * self.pixel_size
self._create_mesh()

@property
Expand All @@ -432,15 +438,12 @@ def birth_range(self):

@birth_range.setter
def birth_range(self, val):
if np.array_equal(val, self._birth_range):
return
self._birth_range = val
self._width = (
int(np.ceil((self.birth_range[1] - self.birth_range[0]) / self.pixel_size))
* self._pixel_size
)
self._resolution = (
int(self.width / self.pixel_size),
int(self.height / self.pixel_size),
)
count = _pixel_count(self.birth_range, self.pixel_size)
self._width = count * self.pixel_size
self._resolution = (count, self.resolution[1])
self._create_mesh()

@property
Expand All @@ -457,15 +460,12 @@ def pers_range(self):

@pers_range.setter
def pers_range(self, val):
if np.array_equal(val, self._pers_range):
return
self._pers_range = val
self._height = (
int(np.ceil((self.pers_range[1] - self.pers_range[0]) / self.pixel_size))
* self._pixel_size
)
self._resolution = (
int(self.width / self.pixel_size),
int(self.height / self.pixel_size),
)
count = _pixel_count(self.pers_range, self.pixel_size)
self._height = count * self.pixel_size
self._resolution = (self.resolution[0], count)
self._create_mesh()

def __repr__(self):
Expand Down
61 changes: 61 additions & 0 deletions test/test_persistence_imager.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import numpy as np
import pytest

from persim import PersistenceImager, images_kernels, images_weights

Expand All @@ -7,6 +8,66 @@
# ----------------------------------------


@pytest.mark.parametrize(
"width,pixel_size,count",
[(0.3, 0.1, 3), (4.3, 0.1, 43), (0.95, 0.2, 5), (1.3, 0.3, 5), (0.9, 0.3, 3)],
)
@pytest.mark.parametrize("operation", ["constructor", "pixel_size", "birth_range", "pers_range"])
def test_pixel_grid_dimensions(width, pixel_size, count, operation):
if operation == "constructor":
imager = PersistenceImager(
birth_range=(0, width), pers_range=(0, width), pixel_size=pixel_size
)
expected = (count, count)
elif operation == "pixel_size":
imager = PersistenceImager(
birth_range=(0, width), pers_range=(0, width), pixel_size=width
)
imager.pixel_size = pixel_size
expected = (count, count)
else:
imager = PersistenceImager(
birth_range=(0, pixel_size),
pers_range=(0, pixel_size),
pixel_size=pixel_size,
)
setattr(imager, operation, (0, width))
expected = (count, 1) if operation == "birth_range" else (1, count)

assert imager.resolution == expected
assert imager.transform(np.array([[0.1, 0.2]])).shape == expected
np.testing.assert_allclose(np.diff(imager._bpnts), pixel_size, rtol=1e-13)
np.testing.assert_allclose(np.diff(imager._ppnts), pixel_size, rtol=1e-13)
assert imager._bpnts[-1] == pytest.approx(imager.birth_range[1])
assert imager._ppnts[-1] == pytest.approx(imager.pers_range[1])


@pytest.mark.parametrize("attribute", ["pixel_size", "birth_range", "pers_range"])
def test_unchanged_grid_parameter(attribute):
imager = PersistenceImager(birth_range=(0, 0.3), pers_range=(0, 0.3), pixel_size=0.1)
before = (imager.resolution, imager.birth_range, imager.pers_range)
birth_points, persistence_points = imager._bpnts.copy(), imager._ppnts.copy()
setattr(imager, attribute, getattr(imager, attribute))
assert (imager.resolution, imager.birth_range, imager.pers_range) == before
np.testing.assert_array_equal(imager._bpnts, birth_points)
np.testing.assert_array_equal(imager._ppnts, persistence_points)


@pytest.mark.parametrize("pixel_size", [0.1, 0.2, 0.3, 0.05])
@pytest.mark.parametrize("operation", ["constructor", "pixel_size"])
def test_integer_pixel_multiple(pixel_size, operation):
width = 3 * pixel_size
imager = PersistenceImager(
birth_range=(0, width),
pers_range=(0, width),
pixel_size=pixel_size if operation == "constructor" else width,
)
if operation == "pixel_size":
imager.pixel_size = pixel_size
assert imager.resolution == (3, 3)
np.testing.assert_allclose(np.diff(imager._bpnts), pixel_size, rtol=1e-13)


def test_empty_diagram():
dgm = np.zeros((0, 2))
persimgr = PersistenceImager(pixel_size=0.1)
Expand Down
Loading