Repository: pberkes/big_O

Issue:
Polynomial.compute() and Exponential.compute() Return Incorrect Results
## Description
`big_o.complexities.Polynomial.compute()` and `big_o.complexities.Exponential.compute()` consistently return unreasonable values.

This issue is present both when the class is used by itself, and when it is returned by `big_o.big_o()`
 
## Steps to reproduce:
Run the following example code to test the `Polynomial` class:

```python
import big_o
import numpy as np

polynomial_complexity = big_o.complexities.Polynomial()
n = np.array(range(1,11)) # array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10])

t = 3.7*n**4.5 
# array([3.70000000e+00, 8.37214429e+01, 5.19095627e+02, 1.89440000e+03,
#        5.17090720e+03, 1.17457932e+04, 2.35040609e+04, 4.28653788e+04,
#        7.28271000e+04, 1.17004273e+05])

polynomial_complexity.fit(n, t) # 2.753078336902666e-29

t_computed = polynomial_complexity.compute(n)
# array([ 1.30833282,  4.42749513,  6.25208812,  7.54665744,  8.55080343,
#         9.37125043, 10.06492849, 10.66581976, 11.19584342, 11.66996574])
```

Observe `t_computed` is very different from the values `t`

The issue can be plotted as follows:

```python
import matplotlib.pyplot as plt
plt.plot(n, t, "-", n, t_computed, "--")
plt.show()
```
![polynomial](https://user-images.githubusercontent.com/22862051/181407159-817e1e72-0fc3-4d2c-80a3-9318733dfd2f.png)

In blue is `t`, the original function. In orange is `t_computed`.
Observe that, while `t` is consistent with a polynomial function, the `t_computed` line is negative and roughly linear/constant. The `t_computed` line is not consistent with a polynomial.

## Expected Behavior:
Result of `big_o.complexities.Polynomial()` and `big_o.complexities.Exponential()` should resemble a polynomial/exponential function that matches the fitted data.


Comments:

Patch:
diff --git a/big_o/complexities.py b/big_o/complexities.py
index a75e4ab..7c58e5d 100755
--- a/big_o/complexities.py
+++ b/big_o/complexities.py
@@ -48,14 +48,21 @@ class ComplexityClass(object):
         tot = 0
         for i in range(len(self.coeff)):
             tot += self.coeff[i] * x[:, i]
-        return tot
+        return self._inverse_transform_time(tot)
+
+    def coefficients(self):
+        """ Return coefficients in standard form. """
+        if self.coeff is None:
+            raise NotFittedError()
+        return self.coeff
 
     def __str__(self):
         prefix = '{}: '.format(self.__class__.__name__)
 
         if self.coeff is None:
-            return prefix + ': not yet fitted'
-        return prefix + self.format_str().format(*tuple(self.coeff)) + ' (sec)'
+            return prefix + 'not yet fitted'
+        return prefix + self.format_str().format(
+            *self.coefficients()) + ' (sec)'
 
     # --- abstract methods
 
@@ -80,6 +87,12 @@ class ComplexityClass(object):
         """
         return t
 
+    def _inverse_transform_time(self, t):
+        """ Inverse transform time as needed for compute.
+        (e.g., t->exp(t)) for exponential class.
+        """
+        return t
+
     def __gt__(self, other):
         return self.order > other.order
 
@@ -176,10 +189,26 @@ class Polynomial(ComplexityClass):
     def _transform_time(self, t):
         return np.log(t)
 
+    def _inverse_transform_time(self, t):
+        return np.exp(t)
+
     @classmethod
     def format_str(cls):
         return 'time = {:.2G} * x^{:.2G}'
 
+    def coefficients(self):
+        """ Return coefficients in standard form. """
+        # The polynomial is stored in the format
+        # exp(a)*n^b where [a, b] are the coefficients
+        # Technical full format is exp(a+b*ln(n))
+        #
+        # Standard form is a*n^b
+        if self.coeff is None:
+            raise NotFittedError()
+
+        a, b = self.coeff
+        return np.exp(a), b
+
 
 class Exponential(ComplexityClass):
     order = 80
@@ -190,10 +219,26 @@ class Exponential(ComplexityClass):
     def _transform_time(self, t):
         return np.log(t)
 
+    def _inverse_transform_time(self, t):
+        return np.exp(t)
+
     @classmethod
     def format_str(cls):
         return 'time = {:.2G} * {:.2G}^n'
 
+    def coefficients(self):
+        """ Return coefficients in standard form. """
+        # The polynomial is stored in the format
+        # exp(a)*exp(b)^n where [a, b] are the coefficients
+        # Technical full format is exp(a+b*n)
+        #
+        # Standard form is a*b^n
+        if self.coeff is None:
+            raise NotFittedError()
+
+        a, b = self.coeff
+        return np.exp(a), np.exp(b)
+
 
 ALL_CLASSES = [Constant, Linear, Quadratic, Cubic, Polynomial,
                Logarithmic, Linearithmic, Exponential]


Test patch:
diff --git a/big_o/test/test_complexities.py b/big_o/test/test_complexities.py
index 4de6035..2e68399 100644
--- a/big_o/test/test_complexities.py
+++ b/big_o/test/test_complexities.py
@@ -1,6 +1,6 @@
 import unittest
 import numpy as np
-from numpy.testing import assert_array_almost_equal
+from numpy.testing import assert_allclose
 
 from big_o import complexities
 
@@ -8,15 +8,28 @@ from big_o import complexities
 class TestComplexities(unittest.TestCase):
 
     def test_compute(self):
+        desired = [
+            (lambda x: 2.+x*0., complexities.Constant),
+            (lambda x: 5.*x+3., complexities.Linear),
+            (lambda x: 8.1*x**2.+0.9, complexities.Quadratic),
+            (lambda x: 1.0*x**3+11.0, complexities.Cubic),
+            (lambda x: 5.2*x**2.5, complexities.Polynomial),
+            (lambda x: 8.5*np.log(x)+99.0, complexities.Logarithmic),
+            (lambda x: 1.7*x*np.log(x)+2.74, complexities.Linearithmic),
+            (lambda x: 3.14**x, complexities.Exponential)
+        ]
+
         x = np.linspace(10, 100, 100)
-        y = 3.0 * x + 2.0
-        linear = complexities.Linear()
-        linear.fit(x, y)
-        assert_array_almost_equal(linear.compute(x), y, 10)
+        for f, class_ in desired:
+            y = f(x)
+            complexity = class_()
+            complexity.fit(x, y)
+            assert_allclose(y, complexity.compute(x),
+                err_msg = "compute() failed to match expected values for class %r" % class_)
 
     def test_not_fitted(self):
-        linear = complexities.Linear()
-        self.assertRaises(complexities.NotFittedError, linear.compute, 100)
+        for class_ in complexities.ALL_CLASSES:
+            self.assertRaises(complexities.NotFittedError, class_().compute, 100)
 
     def test_str_includes_units(self):
         x = np.linspace(10, 100, 100)