diff --git a/HISTORY.rst b/HISTORY.rst index a05f89a..6649bd4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,12 +5,21 @@ History 0.1.2 (Current version) ----------------------- +API changes +~~~~~~~~~~~ + * :py:func:`impax.csvv.get_gammas` has been deprecated. Use :py:func:`impax.read_csvv` instead (:issue:`37`) * :py:meth:`~impax.csvv.Gammas._prep_gammas` has been removed, and :py:meth:`~impax.csvv.Gammas.sample` now takes no arguments and returns a sample by default. Seeding the random number generator is now left up to the user (:issue:`36`) + + +Bug fixes +~~~~~~~~~ + * fix py3k compatability issues (:issue:`39`) * fix travis status icon in README +* add tests for :py:func:`impax.mins._minimize_polynomial`, fix major math errors causing a failure to find minima in :py:mod:`impax.mins` module, and clarify documentation (:issue:`58`) 0.1.0 (2017-10-12) diff --git a/impax/__init__.py b/impax/__init__.py index 3399c06..c4080b4 100644 --- a/impax/__init__.py +++ b/impax/__init__.py @@ -5,13 +5,13 @@ __author__ = """Justin Simcock""" __email__ = 'jsimcock@rhg.com' -__version__ = '0.1.1' +__version__ = '0.1.2' _module_imports = ( minimize_polynomial, construct_covars, - construct_weather, + construct_weather, read_csvv, MultivariateNormalEstimator ) diff --git a/impax/estimate.py b/impax/estimate.py index 7cf0615..b8d388f 100644 --- a/impax/estimate.py +++ b/impax/estimate.py @@ -1,9 +1,9 @@ from __future__ import absolute_import -import csv -import xarray as xr + import pandas as pd import numpy as np -from scipy.stats import multivariate_normal as mn +import csv +from scipy.stats import multivariate_normal import warnings @@ -20,7 +20,8 @@ def read_csvv(csvv_path): Returns ------- estimator : MultivariateNormalEstimator - :py:class:`Gamma` object with median and VCV matrix indexed by prednames, covarnames, and outcomes + :py:class:`Gamma` object with median and VCV matrix indexed by + prednames, covarnames, and outcomes ''' @@ -40,38 +41,46 @@ def read_csvv(csvv_path): data['prednames'] = [i.strip() for i in next(reader)] if row[0] == 'covarnames': data['covarnames'] = [i.strip() for i in next(reader)] - if row[0] == 'outcome': - data['outcome'] =[cv.strip() for cv in next(reader)] + if row[0] == 'outcome': + data['outcome'] = [cv.strip() for cv in next(reader)] index = pd.MultiIndex.from_tuples( - list(zip(data['outcome'], data['prednames'], data['covarnames'])), + list(zip(data['outcome'], data['prednames'], data['covarnames'])), names=['outcome', 'prednames', 'covarnames']) g = MultivariateNormalEstimator(data['gamma'], data['gammavcv'], index) - return g + return g def get_gammas(*args, **kwargs): - warnings.warn('get_gammas has been deprecated, and has been replaced with read_csvv', DeprecationWarning) + warnings.warn( + 'get_gammas has been deprecated, and has been replaced with read_csvv', + DeprecationWarning) + return read_csvv(*args, **kwargs) class MultivariateNormalEstimator(object): ''' - Stores a median and residual VCV matrix for multidimensional variables with named indices - and provides multivariate sampling and statistical analysis functions + Stores a median and residual VCV matrix for multidimensional variables with + named indices and provides multivariate sampling and statistical analysis + functions Parameters ---------- - coefficients: array - length :math:`(m_1*m_2*\cdots*m_n)` 1-d :py:class:`numpy.ndarray` with regression coefficients + coefficients: array + length :math:`(m_1*m_2*\cdots*m_n)` 1-d :py:class:`numpy.ndarray` with + regression coefficients vcv: array - :math:`(m_1*m_2*\cdots*m_n) x (m_1*m_2*\cdots*m_n)` :py:class:`numpy.ndarray` with variance-covariance matrix for multivariate distribution + :math:`(m_1*m_2*\cdots*m_n) x (m_1*m_2*\cdots*m_n)` + :py:class:`numpy.ndarray` with variance-covariance matrix for + multivariate distribution index: Index - :py:class:`~pandas.Index` or :math:`(m_1*m_2*\cdots*m_n)` 1-d :py:class:`~pandas.MultiIndex` describing the multivariate space + :py:class:`~pandas.Index` or :math:`(m_1*m_2*\cdots*m_n)` 1-d + :py:class:`~pandas.MultiIndex` describing the multivariate space ''' @@ -95,20 +104,24 @@ def median(self): def sample(self, seed=None): ''' Sample from the multivariate normal distribution - + Takes a draw from a multivariate distribution and returns an :py:class:`xarray.DataArray` of parameter estimates. Returns ---------- draw : DataArray - :py:class:`~xarray.DataArray` of parameter estimates drawn from the multivariate normal + :py:class:`~xarray.DataArray` of parameter estimates drawn from the + multivariate normal ''' if seed is not None: - warnings.warn( - 'Sampling with a seed has been deprecated. In future releases, this will be up to the user.', + warnings.warn(( + 'Sampling with a seed has been deprecated. In future releases,' + ' this will be up to the user.'), DeprecationWarning) np.random.seed(seed) - return pd.Series(mn.rvs(self.coefficients, self.vcv), index=self.index).to_xarray() + return pd.Series( + multivariate_normal.rvs(self.coefficients, self.vcv), + index=self.index).to_xarray() diff --git a/impax/impax.py b/impax/impax.py index 280b3d3..04e56ae 100644 --- a/impax/impax.py +++ b/impax/impax.py @@ -3,10 +3,7 @@ import xarray as xr import pandas as pd import numpy as np -from toolz import memoize from impax.mins import minimize_polynomial -import time - def construct_weather(**weather): @@ -27,7 +24,6 @@ def construct_weather(**weather): variables, with variables concatenated along the new `prednames` dimension - ''' prednames = [] weather_data = [] @@ -43,6 +39,7 @@ def construct_weather(**weather): return xr.concat(weather_data, pd.Index(prednames, name='prednames')) + def construct_covars(add_constant=True, **covars): ''' Helper function to construct the covariates dataarray @@ -50,12 +47,12 @@ def construct_covars(add_constant=True, **covars): Parameters ----------- add_constant : bool - flag indicating whether a constant term should be added. The constant term will have the - same shape as the other covariate DataArrays - + flag indicating whether a constant term should be added. The constant + term will have the same shape as the other covariate DataArrays + covars: dict - dictionary of covariate name, covariate (``str`` path or :py:class:`xarray.DataArray`) - pairs + dictionary of covariate name, covariate (``str`` path or + :py:class:`xarray.DataArray`) pairs Returns ------- @@ -66,7 +63,7 @@ def construct_covars(add_constant=True, **covars): ''' covarnames = [] covar_data = [] - for covar, path in covars.items(): + for covar, path in covars.items(): if hasattr(path, 'dims'): covar_data.append(path) @@ -77,24 +74,24 @@ def construct_covars(add_constant=True, **covars): covarnames.append(covar) if add_constant: - ones = xr.DataArray(np.ones(shape=covar_data[0].shape), + ones = xr.DataArray( + np.ones(shape=covar_data[0].shape), coords=covar_data[0].coords, dims=covar_data[0].dims) covarnames.append('1') covar_data.append(ones) - + return xr.concat(covar_data, pd.Index(covarnames, name='covarnames')) - + class Impact(object): ''' Base class for computing an impact as specified by the Climate Impact Lab - + ''' min_function = NotImplementedError - def impact_function(self, betas, weather): ''' computes the dot product of betas and annual weather by outcome group @@ -118,65 +115,69 @@ def impact_function(self, betas, weather): overrides `impact_function` method in Impact base class ''' - + return (betas*weather).sum(dim='prednames') - - def compute(self, + def compute( + self, weather, betas, clip_flat_curve=True, t_star=None): ''' - Computes an impact for a unique set of gdp, climate, weather and gamma coefficient inputs. - For each set of these, we take the analytic minimum value between two points, - save t_star to disk and compute analytical min for function m_star for a givene covariate set - This operation is called for every adaptation scenario specified in the run script + Computes an impact for a unique set of gdp, climate, weather and gamma + coefficient inputs. For each set of these, we take the analytic minimum + value between two points, save t_star to disk and compute analytical + min for function m_star for a given covariate set. + + This operation is called for every adaptation scenario specified in the + run script. Parameters ---------- - + weather: DataArray weather :py:class:`~xarray.DataArray` betas: DataArray covarname by outcome :py:class:`~xarray.DataArray` - + clip_flat_curve: bool flag indicating that flat-curve clipping should be performed on the result t_star: DataArray - :py:class:`xarray.DataArray` with minimum temperatures used for clipping + :py:class:`xarray.DataArray` with minimum temperatures used for + clipping Returns ------- - :py:class `~xarray.Dataset` of impacts by hierid by outcome group + :py:class `~xarray.Dataset` of impacts by hierid by outcome group ''' - #Compute Raw Impact + # Compute Raw Impact impact = self.impact_function(betas, weather) if clip_flat_curve: - #Compute the min for flat curve adaptation + # Compute the min for flat curve adaptation impact_flatcurve = self.impact_function(betas, t_star) - #Compare values and evaluate a max + # Compare values and evaluate a max impact = xr.ufuncs.maximum((impact - impact_flatcurve), 0) impact = self.postprocess_daily(impact) - #Sum to annual + # Sum to annual impact = impact.sum(dim='time') - impact_annual = self.postprocess_annual(impact) + impact_annual = self.postprocess_annual(impact) return impact_annual - def get_t_star(self,betas, bounds, t_star_path=None): + def get_t_star(self, betas, bounds, t_star_path=None): ''' Read precomputed t_star @@ -190,10 +191,10 @@ def get_t_star(self,betas, bounds, t_star_path=None): values between which to evaluate function path: str - place to load t-star from + place to load t-star from ''' - + try: with xr.open_dataarray(t_star_path) as t_star: return t_star.load() @@ -204,17 +205,17 @@ def get_t_star(self,betas, bounds, t_star_path=None): except (IOError, ValueError): try: os.remove(t_star_path) - except: + except (IOError, OSError): pass - #Compute t_star according to min function + # Compute t_star according to min function t_star = self.compute_t_star(betas, bounds=bounds) - #write to disk + # write to disk if t_star_path is not None: if not os.path.isdir(os.path.dirname(t_star_path)): os.makedirs(os.path.dirname(t_star_path)) - + t_star.to_netcdf(t_star_path) return t_star @@ -237,8 +238,9 @@ class PolynomialImpact(Impact): @staticmethod def min_function(*args, **kwargs): ''' - helper function to call minimization function for given mortality polynomial spec - mortality_polynomial implements findpolymin through `np.apply_along_axis` + helper function to call minimization function for given mortality + polynomial spec mortality_polynomial implements findpolymin through + `np.apply_along_axis` Parameters ---------- @@ -260,4 +262,3 @@ def min_function(*args, **kwargs): ''' return minimize_polynomial(*args, **kwargs) - \ No newline at end of file diff --git a/impax/mins.py b/impax/mins.py index 5f07161..e64a7ea 100644 --- a/impax/mins.py +++ b/impax/mins.py @@ -1,90 +1,166 @@ + from __future__ import absolute_import -import os + import xarray as xr import numpy as np import warnings - -def _findpolymin(coeffs, min_max): +def _findpolymin(coeffs, bounds=(-np.inf, np.inf)): ''' - Computes the min value `t_star` for a set of coefficients (gammas) - for a polynomial damage function + Minimize a polynomial given the coefficients on [x^1, x^2, ...] + + .. note:: + + The coefficients given in the ``coeffs`` list must be in _ascending_ + power order and must not contain the zeroth-order term. Parameters ---------- coeffs: :py:class `~xarray.DataArray` - coefficients for the gammas used to compute the analytic min + coefficients on a len(coeffs)-order polynomial in ascending power order + _not_ including the zeroth-order term. - min_max: list - min and max temp values to evaluate derivative at + bounds: list + min and max temp values at which to find minimum Returns ------- - int: t_star + int: minimizing value of the polynomial (not the minimum value) ''' - minx = np.asarray(min_max).min() - maxx = np.asarray(min_max).max() + minx = float(min(bounds)) + maxx = float(max(bounds)) # Construct the derivative - derivcoeffs = np.array(coeffs[1:]) * np.arange(1, len(coeffs)) - roots = np.roots(derivcoeffs[::-1]) + # derivcoeffs = np.array(coeffs) * np.arange(1, len(coeffs) + 1) + # roots = np.roots(derivcoeffs[::-1]) + + roots = np.poly1d(list(coeffs[::-1]) + [0]).deriv().roots # Filter out complex roots; note: have to apply real_if_close to individual # values, not array until filtered possibles = ( list(filter( - lambda root: np.real_if_close(root).imag == 0 and np.real_if_close(root) >= minx and np.real_if_close(root) <= maxx, + lambda root: + np.real_if_close(root).imag == 0 + and np.real_if_close(root) >= minx + and np.real_if_close(root) <= maxx, roots))) possibles = list(np.real_if_close(possibles)) + [minx, maxx] - with warnings.catch_warnings(): # catch warning from using infs + with warnings.catch_warnings(): # catch warning from using infs warnings.simplefilter("ignore") - - values = np.polyval(coeffs[::-1], np.real_if_close(possibles)) - + + values = np.polyval( + list(coeffs[::-1]) + [0], + np.real_if_close(possibles)) + # polyval doesn't handle infs well if minx == -np.inf: - if len(coeffs) % 2 == 1: # largest power is even + if len(coeffs) % 2 == 0: # largest power is even values[-2] = -np.inf if coeffs[-1] < 0 else np.inf - else: # largest power is odd + else: # largest power is odd values[-2] = np.inf if coeffs[-1] < 0 else -np.inf - + if maxx == np.inf: values[-1] = np.inf if coeffs[-1] > 0 else -np.inf - + index = np.argmin(values) return possibles[index] -def minimize_polynomial(da, dim='prednames', bounds=None): + +def minimize_polynomial(da, dim='prednames', bounds=(-np.inf, np.inf)): ''' - Constructs the t_star-based weather data array by applying - `np.apply_along_axis` to each predictor dimension and construcing data - variables up to the order specified in `prednames` + Finds the minimizing values of polynomials given an array of coefficients + + .. note:: + + The coefficients along the dimension ``dim`` must be in _ascending_ + power order and must not contain the zeroth-order term. + Parameters ---------- da: DataArray - :py:class:`~xarray.DataArray` of betas by hierid by predname by outcome + :py:class:`~xarray.DataArray` of coefficients of a + ``(da.size[dim])``-order polynomial in ascending power order along the + dimension ``dim``. The coefficients must not contain the zeroth-order + term. - dim: str - dimension to evaluate the coefficients at + dim: str, optional + dimension along which to evaluate the coefficients (default + ``prednames``) - bounds: list - values to evaluate between + bounds: list, optional + domain on the polynomial within which to search for the minimum value, + default ``(-inf, inf)`` Returns ------- DataArray - :py:class:`~xarray.DataArray` of reconstructed weather at t_star + :py:class:`~xarray.DataArray` in the same shape as da, with the + minimizing value of the polynomial raised to the appropriate power + in place of each coefficient + + Examples + -------- + + Create an array with two functions: + + ..math:: + + \begin{array}{rcl} + f_1 & = & x^2 \\ + f_2 & = & -x^2 + 2x + \end{array} + + This is specified as a 2-dimensional :py:class:`xarray.DataArray`: + + .. code-block:: python + + >>> da = xr.DataArray( + ... [[0, 1], # x^2 + ... [2, -1]], # -x^2 + 2x + ... dims=('spec', 'x'), + ... coords={'spec': ['f1', 'f2'], 'x': ['x1', 'x2']}) + ... + + These functions can be minimized using + :py:func:`impax.mins.minimize_polynomial`: + + .. code-block:: python + + >>> minimize_polynomial( + ... da, dim='x') # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE + ... + + array([[ 0., 0.], + [-inf, inf]]) + Coordinates: + * x (x) ... 'x1' 'x2' + * spec (spec) ... 'f1' 'f2' + + Use the same function, but impose the domain limit :math:`[2, 4]`: + + .. code-block:: python + + >>> minimize_polynomial( + ... da, dim='x', bounds=[2, 4]) + ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE + + array([[ 2., 4.], + [ 4., 16.]]) + Coordinates: + * x (x) ... 'x1' 'x2' + * spec (spec) ... 'f1' 'f2' ''' t_star_values = np.apply_along_axis( - _findpolymin, da.get_axis_num(dim), da.values, min_max = bounds) + _findpolymin, da.get_axis_num(dim), da.values, bounds=bounds) if t_star_values.shape != tuple( [s for i, s in enumerate(da.shape) if i != da.get_axis_num(dim)]): diff --git a/setup.cfg b/setup.cfg index c517361..b3c914a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.1 +current_version = 0.1.2 commit = True tag = True diff --git a/setup.py b/setup.py index a3b3886..fc208de 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name='impax', - version='0.1.1', + version='0.1.2', description="Impact Forecasting for the Climate Impact Lab", long_description=readme + '\n\n' + history, author="Justin Simcock", diff --git a/tests/test_impax.py b/tests/test_impax.py index da961a9..77a9d43 100644 --- a/tests/test_impax.py +++ b/tests/test_impax.py @@ -6,9 +6,9 @@ import pytest from click.testing import CliRunner -from impax import impax from impax import cli + @pytest.fixture def response(): """Sample pytest fixture. diff --git a/tests/test_minimize.py b/tests/test_minimize.py new file mode 100644 index 0000000..d35116b --- /dev/null +++ b/tests/test_minimize.py @@ -0,0 +1,210 @@ + +from __future__ import absolute_import +import impax.mins +import xarray as xr +import numpy as np + +import pytest + + +def find_polymin_from_roots(roots, bounds=(-np.inf, np.inf), scale=1): + r''' + Find mimimum value of integral of polynomial defined by roots + + Helper function used in test fixtures to take integral of polynomial + created from a list of roots and to find the minimizing value + + Parameters + ---------- + roots : list + List of roots of the polynomial to be integrated + bounds : tuple + Bounds to place on the minizing step + scale : int + Scalar to multiply by the polynomial created from roots + + Examples + -------- + The simplest polynomial that can be examined here has no roots, e.g.: + + .. math:: + + [] \rightarrow f(x)=1 \rightarrow \integral_x f(x) = 1x + + This function has no roots, and thus its integral has no minimum: + + .. code-block:: python + + >>> minimizer, minimum_value = find_polymin_from_roots([]) + >>> minimizer + -inf + >>> minimum_value + -inf + + A more complex polynomial may have multiple roots: + + .. math:: + + [0, 2]\rightarrow f(x)=(x-0)(x-2)\rightarrow\integral_x f(x)=1/3x^3-x^2 + + This function has roots 0, 2, but still has an infinite minimum value. + Bounds can be placed on the minimization problem with the `bounds` + argument: + + ..code-block:: python + + >>> minimizer, minimum_value = find_polymin_from_roots( + ... [0, 2], + ... bounds=(0, np.inf)) + ... + >>> minimizer + 2.0 + >>> minimum_value == 8.0/3.0 - 4 + True + ''' + + roots = np.array(roots) + + p = np.poly1d(roots, r=True) * scale + + p2 = np.poly1d(list(p.coeffs / np.arange(len(p.coeffs), 0, -1)) + [0]) + + candidates = np.array( + list(roots[(roots >= bounds[0]) & (roots <= bounds[1])]) + + list(bounds)) + + vals = np.array([ + p2(c) + if not np.isinf(c) + else ( + np.sign(p2.coeffs[0]) * np.sign(c) * np.inf + if (len(p2.coeffs) % 2 == 0) + else np.sign(p2.coeffs[0]) * np.inf) + for c in candidates]) + + return candidates[vals.argmin()], min(vals) + + +def test_roots_generator(): + ''' + Test function to minimize integral of polynomial created from list of roots + ''' + + assert find_polymin_from_roots([0])[0] == 0 + assert find_polymin_from_roots([1])[0] == 1 + assert find_polymin_from_roots([-1])[0] == -1 + assert find_polymin_from_roots([0], scale=-1)[0] == -np.inf + assert find_polymin_from_roots([0], bounds=(-10, 5), scale=-1)[0] == -10 + assert find_polymin_from_roots([], bounds=(0, np.inf))[0] == 0 + assert find_polymin_from_roots([], bounds=(1, np.inf))[0] == 1 + + +@pytest.fixture(params=range(4)) +def known_minimized_polynomial(request): + + if request.param == 0: + test_roots = np.array([3, 5, -1, -23]) + bounds = (-np.inf, np.inf) + scale = 1 + known_minimizer = -np.inf + + elif request.param == 1: + test_roots = np.array([3, 5, -1, -23]) + bounds = (0, np.inf) + scale = 1 + known_minimizer = 0 + + elif request.param == 2: + test_roots = np.array([3, 5, -1, -23]) + bounds = (-23, 50) + scale = 1 + known_minimizer = -1 + + elif request.param == 3: + test_roots = np.array([3, 5, -1, -23]) + bounds = (-23, 5) + scale = -1 + known_minimizer = -23 + + minimizer, minimized = find_polymin_from_roots( + test_roots, bounds=bounds, scale=scale) + + assert known_minimizer == minimizer + + p = np.poly1d(test_roots, r=True) * scale + p2 = np.poly1d(list(p.coeffs / np.arange(len(p.coeffs), 0, -1)) + [0]) + + da = xr.DataArray( + p2.coeffs[slice(-2, None, -1)], + dims=('predname', ), + coords={'predname': ['x', 'x2', 'x3', 'x4', 'x5']}) + + yield da, 'predname', bounds, minimizer + + +def test_polymin_for_function_constructed_from_roots( + known_minimized_polynomial): + + da, dim, bounds, polymin = known_minimized_polynomial + + minned = float( + impax.mins.minimize_polynomial(da, 'predname', bounds) + .sel(predname='x')) + + assert (polymin == minned) + + +def test_polymin_for_arbitrary_polynomial_within_ranges(): + test_da = xr.DataArray( + (np.random.random((6, 10, 10)) - 0.5) * 12, + dims=('coeffs', 'var1', 'var2'), + coords={'coeffs': range(1, 7)}) + + minx = impax.mins.minimize_polynomial(test_da, 'coeffs', bounds=[-10, 20]) + + control_field = xr.DataArray( + np.vstack([np.arange(-15, 25, 0.1)**i for i in range(1, 7)]), + dims=('coeffs', 'x'), + coords={'x': np.arange(-15, 25, 0.1), 'coeffs': range(1, 7)}) + + controlled = ( + (test_da * control_field).sum(dim='coeffs') + - (test_da * minx).sum(dim='coeffs')) + + limrange = controlled.where((controlled.x >= -10) & (controlled.x <= 20)) + + assert limrange.fillna(0).min() >= 0 + assert (limrange.min(dim='x') >= 0).all() + + +def test_polymin_for_polynomials_with_known_minima(): + ''' + Tests impax.mins._findpolymin for functions with known minima + ''' + + # x^2 --> 0 + assert impax.mins._findpolymin([0, 1], (-np.inf, np.inf)) == 0 + + # x^2 - 2x --> 1 + assert impax.mins._findpolymin([-2, 1], (-np.inf, np.inf)) == 1 + + # x^3 --> -np.inf + assert impax.mins._findpolymin([0, 0, 1], (-np.inf, np.inf)) == -np.inf + + +def test_polymin_for_polynomials_with_known_minima_and_bounds(): + + # -x^2, [0, 5] --> 5 + assert impax.mins._findpolymin([0, -1], (0, 5)) == 5 + + # x^2, [3, 9] --> 3 + assert impax.mins._findpolymin([0, 1], (3, 9)) == 3 + + +def test_ambiguous_cases(): + + # -x^2, [-1, 1] --> -1? + assert impax.mins._findpolymin([0, -1], (-1, 1)) == -1 + + # 0, [-1, 1] --> -1? + assert impax.mins._findpolymin([0, 0], (-1, 1)) == -1 diff --git a/tox.ini b/tox.ini index 83090da..1305cf5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,10 @@ [tox] -envlist = py27, py34, py35, flake8 +envlist = py27, py35, py36, flake8 [travis] python = + 3.6: py36 3.5: py35 - 3.4: py34 2.7: py27 [testenv:flake8]