Repository: differint/differint

Issue:
Caputo Fractional Derivative - reg
HI! Good Morning!

Is it applicable for caputo derivative? In specific, predictor-corrector algorithm.

Comments:

Patch:
diff --git a/differint/differint.py b/differint/differint.py
index b503322..f001e13 100644
--- a/differint/differint.py
+++ b/differint/differint.py
@@ -145,6 +145,49 @@ def Beta(x,y):
     
     return Gamma(x)*Gamma(y)/Gamma(x+y)
     
+def MittagLeffler(a, b, x, num_terms=50, *, ignore_special_cases=False):
+    ''' Calculate the Mittag-Leffler function by checking for special cases, and trying to 
+        reduce the parameters. If neither of those work, it just brute forces it.
+        
+        Parameters
+       ==========
+        a : float
+            The first parameter of the Mittag-Leffler function.
+        b : float
+            The second parameter of the Mittag-Leffler function
+        x : float or 1D-array of floats
+            The value or values to be evaluated at.
+        num_terms : int
+            The number of terms to calculate in the sum. Ignored if 
+            a special case can be used instead. Default value is 100.
+        ignore_special_cases : bool
+            Don't use the special cases, use the series definition.
+            Probably only useful for testing. Default value is False.
+    '''
+    # check for quick special cases
+    if not ignore_special_cases:
+        if a == 0:
+            if (np.abs(x) < 1).all():
+                return 1 / Gamma(b) * 1 / (1 - x)
+            return x * np.inf
+        elif a == 0.5 and b == 1:
+            # requires calculation of the complementary error function
+            pass
+        elif a == 1 and b == 1:
+            return np.exp(x)
+        elif a == 2 and b == 1:
+            return np.cosh(np.sqrt(x))
+        elif a == 1 and b == 2:
+            return (np.exp(x) - 1) / x
+        elif a == 2 and b == 2:
+            return np.sinh(np.sqrt(x)) / np.sqrt(x)
+    # manually calculate with series definition
+    exponents = np.arange(num_terms)
+    exp_vals = np.array([x]).T ** exponents
+    gamma_vals = np.array([Gamma(exponent * a + b) for exponent in exponents])
+    return np.sum(exp_vals / gamma_vals, axis=1)
+
+
 def GLcoeffs(alpha,n):
     """ Computes the GL coefficient array of size n. 
     
@@ -489,3 +532,93 @@ class GLIinterpolat:
         self.nxt = alpha*(2+alpha)/8
         self.crr = (4-alpha*alpha)/4
         self.prv = alpha*(alpha-2)/8
+
+def PCcoeffs(alpha, j, n):
+    if 1 < alpha:
+        if j == 0:
+            return (n+1)**alpha * (alpha - n) + n**alpha * (2 * n - alpha - 1) - (n - 1)**(alpha + 1)
+        elif j == n:
+            return 2 ** (alpha + 1) - alpha - 3
+        return (n - j + 2) ** (alpha + 1) + 3 * (n - j) ** (alpha + 1) - 3 * (n - j + 1) ** (alpha + 1) - (n - j - 1) ** (alpha + 1)
+
+def PCsolver(initial_values, alpha, f_name, domain_start=0, domain_end=1, num_points=100):
+    """ Solve an equation of the form D[y(x)]=f(x, y(x)) using the predictor-corrector
+        method, modified to be compatible with fractional derivatives.
+
+    see Deng, W. (2007) Short memory principle and a predictor–corrector approach for 
+        fractional differential equations. Journal of Computational and Applied 
+        Mathematics.
+
+    test examples from
+        Baskonus, H.M., Bulut, H. (2015) On the numerical solutions of some fractional
+        ordinary differential equations by fractional Adams-Bashforth-Moulton method.
+        De Gruyter.
+        Weilbeer, M. (2005) Efficient Numerical Methods for Fractional Differential
+        Equations and their Analytical Background. 
+
+
+        
+    Parameters
+    ==========
+        initial_values : float 1d-array
+            A list of initial values for the IVP. There should be as many IVs
+            as ceil(alpha).
+        alpha : float
+            The order of the differintegral in the equation to be computed.
+        f_name : function handle or lambda function
+            This is the function on the right side of the equation, and should
+            accept two variables; first the independant variable, and second
+            the equation to be solved.
+        domain_start : float
+            The left-endpoint of the function domain. Default value is 0.
+        domain_end : float
+            The right-endpoint of the function domain; the point at which the 
+            differintegral is being evaluated. Default value is 1.
+        num_points : integer
+            The number of points in the domain. Default value is 100.
+            
+    Output
+    ======
+        y_correction : float 1d-array
+            The calculated solution to the IVP at each of the points 
+            between the left and right endpoint.
+
+    Examples:
+        >>> f_name = lambda x, y : y - x - 1
+        >>> initial_values = [1, 1]
+        >>> y_solved = PCsolver(initial_values, 1.5, f_name)
+        >>> theoretical = np.linspace(0, 1, 100) + 1
+        >>> np.allclose(y_solved, theoretical)
+        True
+    """
+    x_points = np.linspace(domain_start, domain_end, num_points)
+    step_size = x_points[1] - x_points[0]
+    y_correction = np.zeros(num_points, dtype='complex_')
+    y_prediction = np.zeros(num_points, dtype='complex_')
+    
+    y_prediction[0] = initial_values[0]            
+    y_correction[0] = initial_values[0]
+    for x_index in range(num_points - 1):
+        initial_value_contribution = 0
+        if 1 < alpha and alpha < 2:
+            initial_value_contribution = initial_values[1] * step_size
+        elif 2 < alpha:
+            for k in range(1, int(np.ceil(alpha))):
+                initial_value_contribution += initial_values[k] / np.math.factorial(k) * (x_points[x_index + 1] ** k - x_points[x_index] ** k) 
+        elif alpha < 1:
+            raise ValueError('Not yet supported!')
+        y_prediction[x_index + 1] += initial_value_contribution
+        y_prediction[x_index + 1] += y_correction[x_index]
+        y_prediction[x_index + 1] += step_size ** alpha / Gamma(alpha + 1) * f_name(x_points[x_index], y_correction[x_index])
+        subsum = 0
+        for j in range(x_index + 1):
+            subsum += PCcoeffs(alpha, j, x_index) * f_name(x_points[j], y_correction[j])
+        y_prediction[x_index + 1] += step_size ** alpha / Gamma(alpha + 2) * subsum
+
+        y_correction[x_index + 1] += initial_value_contribution
+        y_correction[x_index + 1] += y_correction[x_index]
+        y_correction[x_index + 1] += step_size ** alpha / Gamma(alpha + 2) * alpha * f_name(x_points[x_index], y_correction[x_index])
+        y_correction[x_index + 1] += step_size ** alpha / Gamma(alpha + 2) * f_name(x_points[x_index + 1], y_prediction[x_index + 1])
+        y_correction[x_index + 1] += step_size ** alpha / Gamma(alpha + 2) * subsum
+    
+    return y_correction


Test patch:
diff --git a/tests/test.py b/tests/test.py
index d663249..af03cbd 100644
--- a/tests/test.py
+++ b/tests/test.py
@@ -12,6 +12,7 @@ size_coefficient_array = 20
 test_N = 512
 sqrtpi2 = 0.88622692545275794
 truevaluepoly = 0.94031597258
+PC_x_power = np.linspace(0, 1, 100) ** 5.5
 
 INTER = GLIinterpolat(1)
 
@@ -33,6 +34,10 @@ RL_r = RL(0.5, lambda x: np.sqrt(x), 0, 1, test_N)
 RL_result = RL_r[-1]
 RL_length = len(RL_r)
 
+# Get FODE function for solving.
+PC_func_power = lambda x, y : 1/24 * Gamma(5 + 1.5) * x**4 + x**(8 + 2 * 1.5) - y**2
+PC_func_ML = lambda x,y : y
+
 class HelperTestCases(unittest.TestCase):
     """ Tests for helper functions. """
     
@@ -89,6 +94,23 @@ class HelperTestCases(unittest.TestCase):
 
     def testComplexValue(self):
         self.assertEqual(np.round(Gamma(1j), 4), -0.1549-0.498j)
+
+    """ Unit tests for Mittag-Leffler function. """
+
+    def test_ML_cosh_root(self):
+        xs = np.arange(10, 0.1)
+        self.assertTrue((np.abs(MittagLeffler(2, 1, xs, ignore_special_cases=True)\
+                                        - np.cosh(np.sqrt(xs))) <= 1e-3).all())
+
+    def test_ML_exp(self):
+        xs = np.arange(10, 0.1)
+        self.assertTrue((np.abs(MittagLeffler(1, 1, xs, ignore_special_cases=True)\
+                                        - np.exp(xs)) <= 1e-3).all())
+
+    def test_ML_geometric(self):
+        xs = np.arange(1, 0.05)
+        self.assertTrue((np.abs(MittagLeffler(0, 1, xs, ignore_special_cases=True)\
+                                        - 1 / (1 - xs)) <= 1e-3).all())
         
 class TestInterpolantCoefficients(unittest.TestCase):
     """ Test the correctness of the interpolant coefficients. """
@@ -131,7 +153,21 @@ class TestAlgorithms(unittest.TestCase):
         
     def test_RL_accuracy_sqrt(self):
         self.assertTrue(abs(RL_result - sqrtpi2) <= 1e-4)
-        
+
+class TestSolvers(unittest.TestCase):
+    """ Tests for the correct solution to the equations. """
+    def test_PC_solution_three_halves(self):
+        self.assertTrue((np.abs(PCsolver([0, 0], 1.5, PC_func_power, 0, 1, 100)-PC_x_power) <= 1e-2).all())
+
+    def test_PC_solution_ML(self):
+        xs = np.linspace(0, 1, 100)
+        ML_alpha = MittagLeffler(5.5, 1, xs ** 5.5)
+        self.assertTrue((np.abs(PCsolver([1, 0, 0, 0, 0, 0], 5.5, PC_func_ML)-ML_alpha) <= 1e-2).all())
+
+    def test_PC_solution_linear(self):
+        xs = np.linspace(0, 1, 100)
+        self.assertTrue((np.abs(PCsolver([1, 1], 1.5, lambda x,y : y-x-1)-(xs+1)) <= 1e-2).all())
+
 if __name__ == '__main__':
     unittest.main(argv=['first-arg-is-ignored'], exit=False)