Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding jitted scalar maximization routine, first build #416

Merged
merged 12 commits into from Jun 29, 2018
1 change: 1 addition & 0 deletions quantecon/__init__.py
Expand Up @@ -12,6 +12,7 @@
from . import game_theory
from . import quad
from . import random
from . import optimize

#-Objects-#
from .compute_fp import compute_fixed_point
Expand Down
6 changes: 6 additions & 0 deletions quantecon/optimize/__init__.py
@@ -0,0 +1,6 @@
"""
Initialization of the optimize subpackage
"""

from .scalar_maximization import maximize_scalar

138 changes: 138 additions & 0 deletions quantecon/optimize/scalar_maximization.py
@@ -0,0 +1,138 @@
import numpy as np
from numba import jit, njit

@njit
def maximize_scalar(func, a, b, args=(), xtol=1e-5, maxiter=500):
"""
Uses a jitted version of the maximization routine from SciPy's fminbound.
The algorithm is identical except that it's been switched to maximization
rather than minimization, and the tests for convergence have been stripped
out to allow for jit compilation.

Note that the input function `func` must be jitted or the call will fail.

Parameters
----------
func : jitted function
a : scalar
Lower bound for search
b : scalar
Upper bound for search
args : tuple, optional
Extra arguments passed to the objective function.
maxiter : int, optional
Maximum number of iterations to perform.
xtol : float, optional
Absolute error in solution `xopt` acceptable for convergence.

Returns
-------
fval : float
The maximum value attained
xf : float
The maximizer

Example
-------

```
@njit
def f(x):
return -(x + 2.0)**2 + 1.0

fval, xf = maximize_scalar(f, -2, 2)
```

"""

maxfun = maxiter

sqrt_eps = np.sqrt(2.2e-16)
golden_mean = 0.5 * (3.0 - np.sqrt(5.0))

fulc = a + golden_mean * (b - a)
nfc, xf = fulc, fulc
rat = e = 0.0
x = xf
fx = -func(x, *args)
num = 1
fmin_data = (1, xf, fx)

ffulc = fnfc = fx
xm = 0.5 * (a + b)
tol1 = sqrt_eps * np.abs(xf) + xtol / 3.0
tol2 = 2.0 * tol1

while (np.abs(xf - xm) > (tol2 - 0.5 * (b - a))):
golden = 1
# Check for parabolic fit
if np.abs(e) > tol1:
golden = 0
r = (xf - nfc) * (fx - ffulc)
q = (xf - fulc) * (fx - fnfc)
p = (xf - fulc) * q - (xf - nfc) * r
q = 2.0 * (q - r)
if q > 0.0:
p = -p
q = np.abs(q)
r = e
e = rat

# Check for acceptability of parabola
if ((np.abs(p) < np.abs(0.5*q*r)) and (p > q*(a - xf)) and
(p < q * (b - xf))):
rat = (p + 0.0) / q
x = xf + rat

if ((x - a) < tol2) or ((b - x) < tol2):
si = np.sign(xm - xf) + ((xm - xf) == 0)
rat = tol1 * si
else: # do a golden section step
golden = 1

if golden: # Do a golden-section step
if xf >= xm:
e = a - xf
else:
e = b - xf
rat = golden_mean*e

if rat == 0:
si = np.sign(rat) + 1
else:
si = np.sign(rat)

x = xf + si * np.maximum(np.abs(rat), tol1)
fu = -func(x, *args)
num += 1
fmin_data = (num, x, fu)

if fu <= fx:
if x >= xf:
a = xf
else:
b = xf
fulc, ffulc = nfc, fnfc
nfc, fnfc = xf, fx
xf, fx = x, fu
else:
if x < xf:
a = x
else:
b = x
if (fu <= fnfc) or (nfc == xf):
fulc, ffulc = nfc, fnfc
nfc, fnfc = x, fu
elif (fu <= ffulc) or (fulc == xf) or (fulc == nfc):
fulc, ffulc = x, fu

xm = 0.5 * (a + b)
tol1 = sqrt_eps * np.abs(xf) + xtol / 3.0
tol2 = 2.0 * tol1

if num >= maxfun:
break

fval = -fx

return fval, xf
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a status flag (ierr in scipy.fminbound) should also be returned, which in scipy.fminbound is 0 if converged and 1 if maxiter is reached.

58 changes: 58 additions & 0 deletions quantecon/optimize/tests/test_scalar_max.py
@@ -0,0 +1,58 @@
"""
Tests for scalar maximization.

"""
import numpy as np
from numpy.testing import assert_almost_equal
from numba import njit

from quantecon.optimize import maximize_scalar

@njit
def f(x):
"""
A function for testing on.
"""
return -(x + 2.0)**2 + 1.0

def test_maximize_scalar():
"""
Uses the function f defined above to test the scalar maximization
routine.
"""
true_fval = 1.0
true_xf = -2.0
fval, xf = maximize_scalar(f, -2, 2)
assert_almost_equal(true_fval, fval, decimal=4)
assert_almost_equal(true_xf, xf, decimal=4)

@njit
def g(x, y):
"""
A multivariate function for testing on.
"""
return -x**2 + y

def test_maximize_scalar_multivariate():
"""
Uses the function f defined above to test the scalar maximization
routine.
"""
y = 5
true_fval = 5.0
true_xf = -0.0
fval, xf = maximize_scalar(g, -10, 10, args=(y,))
assert_almost_equal(true_fval, fval, decimal=4)
assert_almost_equal(true_xf, xf, decimal=4)


if __name__ == '__main__':
import sys
import nose

argv = sys.argv[:]
argv.append('--verbose')
argv.append('--nocapture')
nose.main(argv=argv, defaultTest=__file__)


2 changes: 1 addition & 1 deletion quantecon/version.py
@@ -1,4 +1,4 @@
"""
This is a VERSION file and should NOT be manually altered
"""
version = '0.3.7'
version = '0.3.8'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be set in setup.py
I will update this tomorrow morning.

2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -113,7 +113,7 @@ def write_version_py(filename=None):
download_url='https://github.com/QuantEcon/QuantEcon.py/tarball/' + VERSION,
keywords=['quantitative', 'economics'],
install_requires=[
'numba>=0.36.2',
'numba>=0.38',
'numpy',
'scipy>=1.0.0',
'sympy',
Expand Down