Skip to content

Commit 70cc1f8

Browse files
author
Francis Motta
committed
- Moved kernels and weights to their own files.
- Modified PersistenceImager().__repr__ to create a valid constructor. - Removed PersistenceImager().dict_print and replaced with prettyprint. - Changed default kernel function name from bvncdf to gaussian to make more understandable. - Added a new test to ensure guassian parameter sigma can be either a numpy array or a list of lists. - Renamed some kernel functions by removing leading underscore to ensure proper loading. - Updated block comments in PersistenceImager() to reflect updated __repr__ method.
1 parent 018515c commit 70cc1f8

File tree

7 files changed

+337
-399
lines changed

7 files changed

+337
-399
lines changed

docs/notebooks/Classification with persistence images.ipynb

Lines changed: 5 additions & 14 deletions
Large diffs are not rendered by default.

docs/notebooks/PersImage_update.ipynb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@
168168
},
169169
{
170170
"cell_type": "code",
171-
"execution_count": 9,
171+
"execution_count": 14,
172172
"metadata": {},
173173
"outputs": [],
174174
"source": [
@@ -178,7 +178,7 @@
178178
},
179179
{
180180
"cell_type": "code",
181-
"execution_count": 10,
181+
"execution_count": 15,
182182
"metadata": {},
183183
"outputs": [],
184184
"source": [
@@ -195,9 +195,10 @@
195195
"TestWeightFunctions().test_linear_ramp()\n",
196196
"TestWeightFunctions().test_persistence()\n",
197197
"\n",
198-
"TestKernelFunctions().test_bvncdf()\n",
198+
"TestKernelFunctions().test_gaussian()\n",
199199
"TestKernelFunctions().test_norm_cdf()\n",
200200
"TestKernelFunctions().test_uniform()\n",
201+
"TestKernelFunctions().test_sigma()\n",
201202
"\n",
202203
"TestTransformOutput().test_lists_of_lists()\n",
203204
"TestTransformOutput().test_n_pixels()\n",

docs/notebooks/Persistence Images.ipynb

Lines changed: 38 additions & 66 deletions
Large diffs are not rendered by default.

persim/images.py

Lines changed: 31 additions & 310 deletions
Large diffs are not rendered by default.

persim/images_kernels.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
"""
2+
Kernel functions for PersistenceImager() transformer:
3+
4+
A valid kernel is a Python function of the form
5+
6+
kernel(x, y, mu=(birth, persistence), **kwargs)
7+
8+
defining a cumulative distribution function(CDF) such that kernel(x, y) = P(X <= x, Y <=y), where x and y are numpy arrays of equal length.
9+
10+
The required parameter mu defines the dependance of the kernel on the location of a persistence pair and is usually taken to be the mean of the probability distribution function associated to kernel CDF.
11+
"""
12+
import numpy as np
13+
from scipy.special import erfc
14+
15+
def uniform(x, y, mu=None, width=1, height=1):
16+
w1 = np.maximum(x - (mu[0] - width/2), 0)
17+
h1 = np.maximum(y - (mu[1] - height/2), 0)
18+
19+
w = np.minimum(w1, width)
20+
h = np.minimum(h1, height)
21+
22+
return w*h / (width*height)
23+
24+
25+
def gaussian(birth, pers, mu=None, sigma=None):
26+
"""
27+
Optimized bivariate normal cumulative distribution function for computing persistence images using a Gaussian kernel
28+
:param birth: birth-coordinate(s) of pixel corners
29+
:param pers: persistence-coordinate(s) of pixel corners
30+
:param mu: (2,)-numpy array specifying x and y coordinates of distribution means (birth-persistence pairs)
31+
:param sigma: (2,2)-numpy array specifying distribution covariance matrix or numeric if distribution is isotropic
32+
:return: P(X <= birth, Y <= pers)
33+
"""
34+
if mu is None:
35+
mu = np.array([0.0, 0.0], dtype=np.float64)
36+
if sigma is None:
37+
sigma = np.array([[1.0, 0.0], [0.0, 1.0]], dtype=np.float64)
38+
39+
if sigma[0][1] == 0.0:
40+
return sbvn_cdf(birth, pers,
41+
mu_x=mu[0], mu_y=mu[1], sigma_x=sigma[0][0], sigma_y=sigma[1][1])
42+
else:
43+
return bvn_cdf(birth, pers,
44+
mu_x=mu[0], mu_y=mu[1], sigma_xx=sigma[0][0], sigma_yy=sigma[1][1], sigma_xy=sigma[0][1])
45+
46+
47+
def norm_cdf(x):
48+
"""
49+
Univariate normal cumulative distribution function with mean 0.0 and standard deviation 1.0
50+
:param x: value at which to evaluate the cdf (upper limit)
51+
:return: P(X <= x), for X ~ N[0,1]
52+
"""
53+
return erfc(-x / np.sqrt(2.0)) / 2.0
54+
55+
56+
def sbvn_cdf(x, y, mu_x=0.0, mu_y=0.0, sigma_x=1.0, sigma_y=1.0):
57+
"""
58+
Standard bivariate normal cumulative distribution function with specified mean and variances
59+
:param x: x-coordinate(s) at which to evaluate the cdf (upper limit)
60+
:param y: y-coordinate(s) at which to evaluate the cdf (upper limit)
61+
:param mu_x: x-coordinate of mean of bivariate normal
62+
:param mu_y: y-coordinate of mean of bivariate normal
63+
:param sigma_x: variance in x
64+
:param sigma_y: variance in y
65+
:return: P(X <= x, Y <= y)
66+
"""
67+
x = (x - mu_x) / np.sqrt(sigma_x)
68+
y = (y - mu_y) / np.sqrt(sigma_y)
69+
return norm_cdf(x) * norm_cdf(y)
70+
71+
72+
def bvn_cdf(x, y, mu_x=0.0, mu_y=0.0, sigma_xx=1.0, sigma_yy=1.0, sigma_xy=0.0):
73+
"""
74+
Bivariate normal cumulative distribution function with specified mean and covariance matrix based on the Matlab
75+
implementations by Thomas H. Jørgensen (http://www.tjeconomics.com/code/) and Alan Genz
76+
(http://www.math.wsu.edu/math/faculty/genz/software/matlab/bvnl.m) using the approach described by Drezner
77+
and Wesolowsky (https://doi.org/10.1080/00949659008811236)
78+
:param x: x-coordinate(s) at which to evaluate the cdf (upper limit)
79+
:param y: y-coordinate(s) at which to evaluate the cdf (upper limit)
80+
:param mu_x: x-coordinate of mean of bivariate normal
81+
:param mu_y: y-coordinate of mean of bivariate normal
82+
:param sigma_xx: variance in x
83+
:param sigma_yy: variance in y
84+
:param sigma_xy: covariance of x and y
85+
:return: P(X <= x, Y <= y)
86+
"""
87+
dh = -(x - mu_x) / np.sqrt(sigma_xx)
88+
dk = -(y - mu_y) / np.sqrt(sigma_yy)
89+
90+
hk = np.multiply(dh, dk)
91+
r = sigma_xy / np.sqrt(sigma_xx * sigma_yy)
92+
93+
lg, w, x = gauss_legendre_quad(r)
94+
95+
dim1 = np.ones((len(dh),), dtype=np.float64)
96+
dim2 = np.ones((lg,), dtype=np.float64)
97+
bvn = np.zeros((len(dh),), dtype=np.float64)
98+
99+
if abs(r) < 0.925:
100+
hs = (np.multiply(dh, dh) + np.multiply(dk, dk)) / 2.0
101+
asr = np.arcsin(r)
102+
sn1 = np.sin(asr * (1.0 - x) / 2.0)
103+
sn2 = np.sin(asr * (1.0 + x) / 2.0)
104+
dim1w = np.outer(dim1, w)
105+
hkdim2 = np.outer(hk, dim2)
106+
hsdim2 = np.outer(hs, dim2)
107+
dim1sn1 = np.outer(dim1, sn1)
108+
dim1sn2 = np.outer(dim1, sn2)
109+
sn12 = np.multiply(sn1, sn1)
110+
sn22 = np.multiply(sn2, sn2)
111+
bvn = asr * np.sum(np.multiply(dim1w, np.exp(np.divide(np.multiply(dim1sn1, hkdim2) - hsdim2,
112+
(1 - np.outer(dim1, sn12))))) +
113+
np.multiply(dim1w, np.exp(np.divide(np.multiply(dim1sn2, hkdim2) - hsdim2,
114+
(1 - np.outer(dim1, sn22))))), axis=1) / (4 * np.pi) \
115+
+ np.multiply(norm_cdf(-dh), norm_cdf(-dk))
116+
else:
117+
if r < 0:
118+
dk = -dk
119+
hk = -hk
120+
121+
if abs(r) < 1:
122+
opmr = (1.0 - r) * (1.0 + r)
123+
sopmr = np.sqrt(opmr)
124+
xmy2 = np.multiply(dh - dk, dh - dk)
125+
xmy = np.sqrt(xmy2)
126+
rhk8 = (4.0 - hk) / 8.0
127+
rhk16 = (12.0 - hk) / 16.0
128+
asr = -1.0 * (np.divide(xmy2, opmr) + hk) / 2.0
129+
130+
ind = asr > 100
131+
bvn[ind] = sopmr * np.multiply(np.exp(asr[ind]),
132+
1.0 - np.multiply(np.multiply(rhk8[ind], xmy2[ind] - opmr),
133+
(1.0 - np.multiply(rhk16[ind], xmy2[ind]) / 5.0) / 3.0)
134+
+ np.multiply(rhk8[ind], rhk16[ind]) * opmr * opmr / 5.0)
135+
136+
ind = hk > -100
137+
ncdfxmyt = np.sqrt(2.0 * np.pi) * norm_cdf(-xmy / sopmr)
138+
bvn[ind] = bvn[ind] - np.multiply(np.multiply(np.multiply(np.exp(-hk[ind] / 2.0), ncdfxmyt[ind]), xmy[ind]),
139+
1.0 - np.multiply(np.multiply(rhk8[ind], xmy2[ind]),
140+
(1.0 - np.multiply(rhk16[ind], xmy2[ind]) / 5.0) / 3.0))
141+
sopmr = sopmr / 2
142+
for ix in [-1, 1]:
143+
xs = np.multiply(sopmr + sopmr * ix * x, sopmr + sopmr * ix * x)
144+
rs = np.sqrt(1 - xs)
145+
xmy2dim2 = np.outer(xmy2, dim2)
146+
dim1xs = np.outer(dim1, xs)
147+
dim1rs = np.outer(dim1, rs)
148+
dim1w = np.outer(dim1, w)
149+
rhk16dim2 = np.outer(rhk16, dim2)
150+
hkdim2 = np.outer(hk, dim2)
151+
asr1 = -1.0 * (np.divide(xmy2dim2, dim1xs) + hkdim2) / 2.0
152+
153+
ind1 = asr1 > -100
154+
cdim2 = np.outer(rhk8, dim2)
155+
sp1 = 1.0 + np.multiply(np.multiply(cdim2, dim1xs), 1.0 + np.multiply(rhk16dim2, dim1xs))
156+
ep1 = np.divide(np.exp(np.divide(-np.multiply(hkdim2, (1.0 - dim1rs)),
157+
2.0 * (1.0 + dim1rs))), dim1rs)
158+
bvn = bvn + np.sum(np.multiply(np.multiply(np.multiply(sopmr, dim1w), np.exp(np.multiply(asr1, ind1))),
159+
np.multiply(ep1, ind1) - np.multiply(sp1, ind1)), axis=1)
160+
bvn = -bvn / (2.0 * np.pi)
161+
162+
if r > 0:
163+
bvn = bvn + norm_cdf(-np.maximum(dh, dk))
164+
elif r < 0:
165+
bvn = -bvn + np.maximum(0, norm_cdf(-dh) - norm_cdf(-dk))
166+
167+
return bvn
168+
169+
170+
def gauss_legendre_quad(r):
171+
"""
172+
Return weights and abscissae for the Legendre-Gauss quadrature integral approximation
173+
:param r: correlation
174+
:return:
175+
"""
176+
if np.abs(r) < 0.3:
177+
lg = 3
178+
w = np.array([0.1713244923791705, 0.3607615730481384, 0.4679139345726904])
179+
x = np.array([0.9324695142031522, 0.6612093864662647, 0.2386191860831970])
180+
elif np.abs(r) < 0.75:
181+
lg = 6
182+
w = np.array([.04717533638651177, 0.1069393259953183, 0.1600783285433464,
183+
0.2031674267230659, 0.2334925365383547, 0.2491470458134029])
184+
x = np.array([0.9815606342467191, 0.9041172563704750, 0.7699026741943050,
185+
0.5873179542866171, 0.3678314989981802, 0.1252334085114692])
186+
else:
187+
lg = 10
188+
w = np.array([0.01761400713915212, 0.04060142980038694, 0.06267204833410906,
189+
0.08327674157670475, 0.1019301198172404, 0.1181945319615184,
190+
0.1316886384491766, 0.1420961093183821, 0.1491729864726037,
191+
0.1527533871307259])
192+
x = np.array([0.9931285991850949, 0.9639719272779138, 0.9122344282513259,
193+
0.8391169718222188, 0.7463319064601508, 0.6360536807265150,
194+
0.5108670019508271, 0.3737060887154196, 0.2277858511416451,
195+
0.07652652113349733])
196+
197+
return lg, w, x

persim/images_weights.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
Weight Functions for PersistenceImager() transformer:
3+
4+
A valid weight function is a Python function of the form
5+
6+
weight(birth, persistence, **kwargs)
7+
8+
defining a scalar-valued function over the birth-persistence plane, where birth and persistence are assumed to be numpy arrays of equal length. To ensure stability, functions should vanish continuously at the line persistence = 0.
9+
"""
10+
import numpy as np
11+
12+
def linear_ramp(birth, pers, low=0.0, high=1.0, start=0.0, end=1.0):
13+
"""
14+
Continuous peicewise linear ramp function which is constant below and above specified input values
15+
:param birth: birth coordinates
16+
:param pers: persistence coordinates
17+
:param low: minimal weight
18+
:param high: maximal weight
19+
:param start: start persistence value of linear transition from low to high weight
20+
:param end: end persistence value of linear transition from low to high weight
21+
:return: weight at persistence pair
22+
"""
23+
try:
24+
n = len(birth)
25+
except:
26+
n = 1
27+
birth = [birth]
28+
pers = [pers]
29+
30+
w = np.zeros((n,))
31+
for i in range(n):
32+
if pers[i] < start:
33+
w[i] = low
34+
elif pers[i] > end:
35+
w[i] = high
36+
else:
37+
w[i] = (pers[i] - start) * (high - low) / (end - start) + low
38+
39+
return w
40+
41+
def persistence(birth, pers, n=1.0):
42+
"""
43+
Continuous monotonic function which weight a persistence pair (b,p) by p^n for some n > 0
44+
:param birth: birth coordinates
45+
:param pers: persistence coordinates
46+
:param n: positive float
47+
:return: weight at persistence pair
48+
"""
49+
return pers ** n

test/test_persim_update.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import numpy as np
44
from persim import PersistenceImager
5-
from persim.images import persistence, linear_ramp, uniform, bvncdf, _norm_cdf, _sbvn_cdf, _bvn_cdf
5+
from persim.images_weights import *
6+
from persim.images_kernels import *
67

78
# ----------------------------------------
89
# New PersistenceImager Tests
@@ -211,12 +212,12 @@ def test_persistence(self):
211212
np.testing.assert_equal(wf(1, x, **wf_params), x ** 1.5)
212213

213214
class TestKernelFunctions:
214-
def test_bvncdf(self):
215-
kernel = bvncdf
215+
def test_gaussian(self):
216+
kernel = gaussian
216217
kernel_params = {'mu':[1, 1], 'sigma':np.array([[1,0],[0,1]])}
217218
np.testing.assert_almost_equal(kernel(np.array([1]), np.array([1]), **kernel_params), 1/4, 8)
218219

219-
kernel = _bvn_cdf
220+
kernel = bvn_cdf
220221
kernel_params = {'mu_x':1.0, 'mu_y':1.0, 'sigma_xx':1.0, 'sigma_yy':1.0, 'sigma_xy':0.0}
221222
np.testing.assert_almost_equal(kernel(np.array([1]), np.array([1]), **kernel_params), 1/4, 8)
222223

@@ -230,8 +231,8 @@ def test_bvncdf(self):
230231
np.testing.assert_equal(kernel(np.array([1]), np.array([1]), **kernel_params), .375)
231232

232233
def test_norm_cdf(self):
233-
np.testing.assert_equal(_norm_cdf(0), .5)
234-
np.testing.assert_almost_equal(_norm_cdf(1), 0.8413447460685429, 8)
234+
np.testing.assert_equal(norm_cdf(0), .5)
235+
np.testing.assert_almost_equal(norm_cdf(1), 0.8413447460685429, 8)
235236

236237
def test_uniform(self):
237238
kernel = uniform
@@ -241,6 +242,12 @@ def test_uniform(self):
241242
np.testing.assert_equal(kernel(np.array([3]), np.array([1]), mu=(0,0), **kernel_params), 1)
242243
np.testing.assert_equal(kernel(np.array([5]), np.array([5]), mu=(0,0), **kernel_params), 1)
243244

245+
def test_sigma(self):
246+
kernel = gaussian
247+
kernel_params1 = {'sigma':np.array([[1,0],[0,1]])}
248+
kernel_params2 = {'sigma': [[1,0],[0,1]]}
249+
np.testing.assert_equal(kernel(np.array([1]), np.array([1]), **kernel_params1), kernel(np.array([1]), np.array([1]), **kernel_params2))
250+
244251
class TestTransformOutput:
245252
def test_lists_of_lists(self):
246253
persimgr = PersistenceImager(birth_range=(0,3), pers_range=(0,3), pixel_size=1)

0 commit comments

Comments
 (0)