Skip to content

Commit 60c9708

Browse files
committed
alpha=1 integration and other issues. fixes #11
1 parent ed27245 commit 60c9708

File tree

7 files changed

+101
-110
lines changed

7 files changed

+101
-110
lines changed

levy/__init__.py

Lines changed: 92 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@
1414
# along with this program; if not, write to the Free Software
1515
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
1616

17-
# 2017-08-31 Paul Kienzle
18-
# - cache tables so repeated calls are faster
19-
# - support scalar and vector inputs
20-
# - code linting
21-
2217
"""
2318
This is a package for calculation of Levy stable distributions
2419
(probability density function and cumulative density function) and for
@@ -48,15 +43,13 @@
4843
- pylevy does not support alpha values less than 0.5.
4944
"""
5045

51-
from __future__ import print_function, division
52-
5346
import sys
5447
import os
5548
import numpy as np
5649
from scipy.special import gamma
5750
from scipy import optimize
5851

59-
__version__ = "1.0"
52+
__version__ = "1.1"
6053

6154
# Some constants of the program.
6255
# Dimensions: 0 - x, 1 - alpha, 2 - beta
@@ -86,28 +79,13 @@
8679
_data_cache = {}
8780

8881

89-
def _cdf():
90-
try:
91-
return _data_cache['cdf']
92-
except KeyError:
93-
_data_cache['cdf'] = np.load(os.path.join(ROOT, 'cdf.npz'))['arr_0']
94-
return _data_cache['cdf']
95-
96-
97-
def _pdf():
98-
try:
99-
return _data_cache['pdf']
100-
except KeyError:
101-
_data_cache['pdf'] = np.load(os.path.join(ROOT, 'pdf.npz'))['arr_0']
102-
return _data_cache['pdf']
103-
104-
105-
def _limits():
82+
def _read_from_cache(key):
83+
""" Loads the file given by key """
10684
try:
107-
return _data_cache['limits']
85+
return _data_cache[key]
10886
except KeyError:
109-
_data_cache['limits'] = np.load(os.path.join(ROOT, 'limits.npz'))['arr_0']
110-
return _data_cache['limits']
87+
_data_cache[key] = np.load(os.path.join(ROOT, '{}.npz'.format(key)))['arr_0']
88+
return _data_cache[key]
11189

11290

11391
def _reflect(x, lower, upper):
@@ -149,8 +127,8 @@ def _interpolate(points, grid, lower, upper):
149127
ravel_offset = 0
150128
for j in range(dims):
151129
n = (i >> (j * 2)) % 4
152-
ravel_offset = ravel_offset * grid_shape[j] + \
153-
np.maximum(0, np.minimum(grid_shape[j] - 1, floors[:, j] + (n - 1)))
130+
ravel_offset = ravel_offset * grid_shape[j] + np.maximum(0, np.minimum(grid_shape[j] - 1, floors[:, j] +
131+
(n - 1)))
154132
weights *= weighters[n][:, j]
155133

156134
result += weights * np.take(ravel_grid, ravel_offset)
@@ -191,7 +169,7 @@ def _phi(alpha, beta):
191169
x[0],
192170
np.tan(x[1] * _psi(x[0])) / np.tan(x[0] * np.pi / 2),
193171
x[3] * (x[2] + np.sin(x[1] * _psi(x[0]))),
194-
(x[3] * np.cos(x[1] * _psi(x[0]))) ** (1/x[0])
172+
(x[3] * np.cos(x[1] * _psi(x[0]))) ** (1 / x[0])
195173
])
196174
}
197175

@@ -232,7 +210,6 @@ class Parameters(object):
232210
The only useful function to be used directly is `convert`, which allows
233211
to transform parameters from one parametrization to another.
234212
Available parametrizations are {0, 1, A, B, M}.
235-
236213
"""
237214

238215
@classmethod
@@ -251,7 +228,7 @@ def convert(cls, pars, par_in, par_out):
251228
>>> np.testing.assert_allclose(a, c)
252229
253230
:param pars: array of parameters to be converted
254-
:type x: :class:`~numpy.ndarray`
231+
:type pars: :class:`~numpy.ndarray`
255232
:param par_in: parametrization of the input array
256233
:type par_in: str
257234
:param par_out: parametrization of the output array
@@ -317,38 +294,46 @@ def _calculate_levy(x, alpha, beta, cdf=False):
317294
This is used in the creation of the lookup table.
318295
Notice that to compute it in a 'true' x, the tangent must be applied.
319296
Example: levy(2, 1.5, 0) = _calculate_levy(np.tan(2), 1.5, 0)
320-
"0" parameterization as per http://academic2.americanp.edu/~jpnolan/stable/stable.html
321-
Note: fails for alpha=1.0 (so make sure alpha=1.0 isn't exactly on the interpolation grid)
297+
"0" parametrization as per http://academic2.americanp.edu/~jpnolan/stable/stable.html
298+
Addition: the special case alpha=1.0 was added. Due to an error in the
299+
numerical integration, the limit was changed from 0 to 1e-10.
322300
"""
323301
from scipy import integrate
324302

325303
beta = -beta
326-
C = _phi(alpha, beta)
327304

328-
def func_cos(u):
329-
ua = u ** alpha
330-
# if ua > 700.0: return 0.0
331-
return np.exp(-ua) * np.cos(C * ua - C * u)
305+
if alpha == 1:
306+
li = 1e-10
307+
308+
def func_cos(u):
309+
return np.exp(-u) * np.cos(-beta * 2 / np.pi * (u * np.log(u) - u))
310+
311+
def func_sin(u):
312+
return np.exp(-u) * np.sin(-beta * 2 / np.pi * (u * np.log(u) - u))
313+
314+
else:
315+
li = 0
316+
317+
def func_cos(u):
318+
ua = u ** alpha
319+
return np.exp(-ua) * np.cos(_phi(alpha, beta) * (ua - u))
332320

333-
def func_sin(u):
334-
ua = u ** alpha
335-
# if ua > 700.0: return 0.0
336-
return np.exp(-ua) * np.sin(C * ua - C * u)
321+
def func_sin(u):
322+
ua = u ** alpha
323+
return np.exp(-ua) * np.sin(_phi(alpha, beta) * (ua - u))
337324

338325
if cdf:
339326
# Cumulative density function
340-
return (integrate.quad(
341-
lambda u: u and func_cos(u) / u or 0.0, 0.0, np.Inf, weight="sin", wvar=x, limlst=1000)[0]
342-
+ integrate.quad(
343-
lambda u: u and func_sin(u) / u or 0.0, 0.0, np.Inf, weight="cos", wvar=x, limlst=1000)[0]
344-
) / np.pi + 0.5
327+
return (
328+
integrate.quad(lambda u: u and func_cos(u) / u or 0.0, li, np.Inf, weight="sin", wvar=x, limlst=1000)[0]
329+
+ integrate.quad(lambda u: u and func_sin(u) / u or 0.0, li, np.Inf, weight="cos", wvar=x, limlst=1000)[0]
330+
) / np.pi + 0.5
345331
else:
346332
# Probability density function
347-
return (integrate.quad(
348-
func_cos, 0.0, np.Inf, weight="cos", wvar=x, limlst=1000)[0]
349-
- integrate.quad(
350-
func_sin, 0.0, np.Inf, weight="sin", wvar=x, limlst=1000)[0]
351-
) / np.pi
333+
return (
334+
integrate.quad(func_cos, li, np.Inf, weight="cos", wvar=x, limlst=1000)[0]
335+
- integrate.quad(func_sin, li, np.Inf, weight="sin", wvar=x, limlst=1000)[0]
336+
) / np.pi
352337

353338

354339
def _approximate(x, alpha, beta, cdf=False):
@@ -357,7 +342,7 @@ def _approximate(x, alpha, beta, cdf=False):
357342
values[mask] *= (1.0 + beta)
358343
values[~mask] *= (1.0 - beta)
359344
if cdf:
360-
return 1.0 - values
345+
return 1.0 - values * x
361346
else:
362347
return values * alpha
363348

@@ -367,15 +352,16 @@ def _make_dist_data_file():
367352

368353
xs, alphas, betas = [np.linspace(_lower[i], _upper[i], size[i], endpoint=True) for i in [0, 1, 2]]
369354
ts = np.tan(xs)
370-
print("Generating levy_data.py ...")
371355

356+
print("Generating pdf.npz ...")
372357
pdf = np.zeros(size, 'float64')
373358
for i, alpha in enumerate(alphas):
374359
for j, beta in enumerate(betas):
375360
print("Calculating alpha={:.2f}, beta={:.2f}".format(alpha, beta))
376361
pdf[:, i, j] = [_calculate_levy(t, alpha, beta, False) for t in ts]
377362
np.savez(os.path.join(ROOT, 'pdf.npz'), pdf)
378363

364+
print("Generating cdf.npz ...")
379365
cdf = np.zeros(size, 'float64')
380366
for i, alpha in enumerate(alphas):
381367
for j, beta in enumerate(betas):
@@ -395,34 +381,42 @@ def _int_levy(x, alpha, beta, cdf=False):
395381
points = np.empty(np.shape(x) + (3,), 'float64')
396382
points[..., 0] = np.arctan(x)
397383
points[..., 1] = alpha
398-
points[..., 2] = np.abs(beta)
384+
points[..., 2] = beta
399385

400-
what = _cdf() if cdf else _pdf()
386+
what = _read_from_cache('cdf') if cdf else _read_from_cache('pdf')
401387
return _interpolate(points, what, _lower, _upper)
402388

403389

404-
def _get_closest_approx(alpha, beta):
405-
x0, x1, n = -50.0, 10000.0 - 50.0, 100000
406-
dx = (x1 - x0) / n
407-
x = np.linspace(x0, x1, num=n, endpoint=True)
390+
def _get_closest_approx(alpha, beta, upper=True):
391+
n = 100000
392+
x1, x2 = -50.0, 1e4 - 50.0
393+
li1, li2 = 10, 500
394+
if upper is False:
395+
x1, x2 = -1e4 + 50, 50
396+
li1, li2 = -500, -10
397+
dx = (x2 - x1) / n
398+
x = np.linspace(x1, x2, num=n + 1, endpoint=True)
408399
y = 1.0 - _int_levy(x, alpha, beta, cdf=True)
409400
z = 1.0 - _approximate(x, alpha, beta, cdf=True)
410-
mask = (10.0 < x) & (x < 500.0)
411-
return 10.0 + dx * np.argmin((np.log(z[mask]) - np.log(y[mask])) ** 2.0)
401+
mask = (li1 < x) & (x < li2)
402+
return li1 + dx * np.argmin((np.log(z[mask]) - np.log(y[mask])) ** 2.0)
412403

413404

414-
def _make_limit_data_file():
415-
limits = np.zeros(size[1:], 'float64')
416-
alphas, betas = [np.linspace(_lower[i], _upper[i], size[i], endpoint=True) for i in [1, 2]]
405+
def _make_limit_data_files():
406+
for upper in [True, False]:
407+
string = 'lower' if upper is False else 'upper'
417408

418-
print("Generating levy_approx_data.py ...")
409+
limits = np.zeros(size[1:], 'float64')
410+
alphas, betas = [np.linspace(_lower[i], _upper[i], size[i], endpoint=True) for i in [1, 2]]
419411

420-
for i, alpha in enumerate(alphas):
421-
for j, beta in enumerate(betas):
422-
limits[i, j] = _get_closest_approx(alpha, beta)
423-
print("Calculating alpha={:.2f}, beta={:.2f}, limit={:.2f}".format(alpha, beta, limits[i, j]))
412+
print("Generating {}_limit.npz ...".format(string))
413+
414+
for i, alpha in enumerate(alphas):
415+
for j, beta in enumerate(betas):
416+
limits[i, j] = _get_closest_approx(alpha, beta, upper=upper)
417+
print("Calculating alpha={:.2f}, beta={:.2f}, limit={:.2f}".format(alpha, beta, limits[i, j]))
424418

425-
np.savez(os.path.join(ROOT, 'limits.npz'), limits)
419+
np.savez(os.path.join(ROOT, '{}_limit.npz'.format(string)), limits)
426420

427421

428422
def levy(x, alpha, beta, mu=0.0, sigma=1.0, cdf=False):
@@ -441,7 +435,7 @@ def levy(x, alpha, beta, mu=0.0, sigma=1.0, cdf=False):
441435
Example:
442436
>>> x = np.array([1, 2, 3])
443437
>>> levy(x, 1.5, 0, cdf=True)
444-
array([0.20203811, 0.08453908, 0.03150926])
438+
array([0.75634202, 0.89496045, 0.94840227])
445439
446440
:param x: values where the function is evaluated
447441
:type x: :class:`~numpy.ndarray`
@@ -461,20 +455,24 @@ def levy(x, alpha, beta, mu=0.0, sigma=1.0, cdf=False):
461455

462456
loc = mu
463457

464-
what = _cdf() if cdf else _pdf()
465-
limits = _limits()
458+
what = _read_from_cache('cdf') if cdf else _read_from_cache('pdf')
459+
# limits = _limits()
460+
lower_limit = _read_from_cache('lower_limit')
461+
upper_limit = _read_from_cache('upper_limit')
466462

467463
xr = (np.asarray(x, 'd') - loc) / sigma
468-
alpha_index = int((alpha -_lower[1]) / (_upper[1] - _lower[1]) * (size[1] - 1))
464+
alpha_index = int((alpha - _lower[1]) / (_upper[1] - _lower[1]) * (size[1] - 1))
469465
beta_index = int((beta - _lower[2]) / (_upper[2] - _lower[2]) * (size[2] - 1))
470466
try:
471-
l = limits[alpha_index, beta_index]
467+
# lims = limits[alpha_index, beta_index]
468+
low_lims = lower_limit[alpha_index, beta_index]
469+
up_lims = upper_limit[alpha_index, beta_index]
472470
except IndexError:
473471
print(alpha, alpha_index)
474472
print(beta, beta_index)
475473
print('This should not happen! If so, please open an issue in the pylevy github page please.')
476474
raise
477-
mask = (np.abs(xr) < l)
475+
mask = (low_lims <= xr) & (xr <= up_lims)
478476
z = xr[mask]
479477

480478
points = np.empty(np.shape(z) + (3,), 'float64')
@@ -535,36 +533,29 @@ def fit_levy(x, par='0', **kwargs):
535533
536534
Examples:
537535
>>> np.random.seed(0)
538-
>>> x = random(1.5, 0, 0, 1, shape=200)
536+
>>> x = random(1.5, 0, 0, 1, shape=(200,))
539537
>>> fit_levy(x) # -- Fit a stable distribution to x
540-
(par=0, alpha=1.52, beta=-0.08, mu=0.05, sigma=0.99, 402.44279062026806)
538+
(par=0, alpha=1.52, beta=-0.08, mu=0.05, sigma=0.99, 402.37150603509247)
541539
542540
>>> fit_levy(x, beta=0.0) # -- Fit a symmetric stable distribution to x
543-
(par=0, alpha=1.53, beta=0.00, mu=0.03, sigma=0.99, 402.5204574692911)
541+
(par=0, alpha=1.53, beta=0.00, mu=0.03, sigma=0.99, 402.43833088693725)
544542
545543
>>> fit_levy(x, beta=0.0, mu=0.0) # -- Fit a symmetric distribution centered on zero to x
546-
(par=0, alpha=1.53, beta=0.00, mu=0.00, sigma=0.99, 402.5557826203093)
544+
(par=0, alpha=1.53, beta=0.00, mu=0.00, sigma=0.99, 402.4736618823546)
547545
548546
>>> fit_levy(x, alpha=1.0, beta=0.0) # -- Fit a Cauchy distribution to x
549-
(par=0, alpha=1.00, beta=0.00, mu=0.10, sigma=0.90, 416.5364751270402)
547+
(par=0, alpha=1.00, beta=0.00, mu=0.10, sigma=0.90, 416.54249079255976)
550548
551549
:param x: values to be fitted
552550
:type x: :class:`~numpy.ndarray`
553-
:param alpha: alpha
554-
:type alpha: float
555-
:param beta: beta
556-
:type beta: float
557-
:param mu: mu
558-
:type mu: float
559-
:param sigma: sigma
560-
:type sigma: float
561551
:param par: parametrization
562-
:type par: int
552+
:type par: str
563553
:return: a tuple with a `Parameters` object and the negative log likelihood of the data.
564554
:rtype: tuple
565555
"""
566556

567-
values = {par_name: None if par_name not in kwargs else kwargs[par_name] for i, par_name in enumerate(par_names[par])}
557+
values = {par_name: None if par_name not in kwargs else kwargs[par_name] for i, par_name in
558+
enumerate(par_names[par])}
568559

569560
parameters = Parameters(par=par, **values)
570561
temp = Parameters(par=par, **values)
@@ -589,8 +580,9 @@ def random(alpha, beta, mu=0.0, sigma=1.0, shape=()):
589580
It uses parametrization 0 (to get it from another parametrization, convert).
590581
591582
Example:
592-
>>> x = random(1.5, 0, shape=100) # parametrization 0 is implicit
593-
>>> x = random(*Parameters.convert([1.5, 0.905, 0.707, 1.414] ,'B' ,'0'), shape=100) # example with conversion
583+
>>> rnd = random(1.5, 0, shape=(100,)) # parametrization 0 is implicit
584+
>>> par = np.array([1.5, 0.905, 0.707, 1.414])
585+
>>> rnd = random(*Parameters.convert(par ,'B' ,'0'), shape=(100,)) # example with convert
594586
595587
:param alpha: alpha
596588
:type alpha: float
@@ -638,13 +630,13 @@ def random(alpha, beta, mu=0.0, sigma=1.0, shape=()):
638630
if __name__ == "__main__":
639631
if "build" in sys.argv[1:]:
640632
_make_dist_data_file()
641-
_make_limit_data_file()
633+
_make_limit_data_files()
642634

643635
print("Testing fit_levy using parametrization 0 and fixed alpha (1.5).")
644636

645637
N = 1000
646638
print("{} points, result should be (1.5, 0.5, 0.0, 1.0).".format(N))
647-
x = random(1.5, 0.5, 0.0, 1.0, 1000)
639+
x0 = random(1.5, 0.5, 0.0, 1.0, shape=(1000))
648640

649-
result = fit_levy(x, par='0', alpha=1.5)
650-
print(result)
641+
result0 = fit_levy(x0, par='0', alpha=1.5)
642+
print(result0)

levy/cdf.npz

48 Bytes
Binary file not shown.

levy/limits.npz

-60.2 KB
Binary file not shown.

levy/lower_limit.npz

60.2 KB
Binary file not shown.

levy/pdf.npz

48 Bytes
Binary file not shown.

levy/upper_limit.npz

60.2 KB
Binary file not shown.

0 commit comments

Comments
 (0)