```python
import sys
from sympy import *
from numpy import identity
import textwrap
```

```python
WRITE_TO_FILE = False
```

```python
Ident = Matrix(3,3, lambda i,j: 1 if i==j else 0)
ij_from_I = {0:(0,0),1:(1,1),2:(2,2),3:(0,1),4:(1,2),5:(0,2)}
I_from_ij = lambda m,n: dict([(v,k) for k,v in ij_from_I.items()])[m,n]
ij_from_I_9 = {0:(0,0),1:(0,1),2:(0,2),3:(1,0),4:(1,1),5:(1,2),6:(2,0),7:(2,1),8:(2,2)}
I_from_ij_9 = lambda m,n: dict([(v,k) for k,v in ij_from_I_9.items()])[m,n]
def _Matrix(M,ij):
    return Matrix(3,3,[Symbol('{}[{}]'.format(M,i)) for i in ij])
def SymMatrix(M, full=0):
    return _Matrix(M, (0,3,5,3,1,4,5,4,2))
def UnsymMatrix(M):
    return _Matrix(M, (0,1,2,3,4,5,6,7,8))
```

```python
if WRITE_TO_FILE:
    STREAM = open('../materials/tensor.py', 'w')
else:
    STREAM = sys.stdout
```

```python
def wrap(lines):
    kwds = {'width': 78,
            'break_long_words': False,
            'replace_whitespace': False}
    lines = lines.split('\n')
    for (i, line) in enumerate(lines):
        if len(line) <= kwds['width']:
            continue
        kwds['subsequent_indent'] = None
        if line.strip()[0].strip() and ' = ' in line:
            var, expr = line.split('=')
            kwds['subsequent_indent'] = ' ' * (len(var)+3)
            line = var + '= ({})'.format(expr.strip())
        lines[i] = textwrap.fill(line, **kwds)
    return '\n'.join(lines)

def write(string, wrapit=1):
    if wrapit:
        string = wrap(string)
    STREAM.write(string)
    STREAM.write('\n')
    STREAM.flush()
```

```python
imports = """\
import numpy as np
from numpy import array, zeros"""
write(imports)
man = """tensor.py

THIS FILE IS AUTOMATICALLY GENERATED - DO NOT EDIT IT DIRECTLY!

Constants and functions having to deal with 3D symmetric and nonsymmetric
second order tensors fourth-order tensors with minor and major symmetries.
All of the functions are written with the following assumptions:

o Symmetric second-order tensors are stored as arrays of length 6 with the
  following component ordering

  [XX, YY, ZZ, XY, YZ, ZZ]

o Nonsymmetric second-order tensors are stored as arrays of length 9 with the
  following component ordering

  [XX, XY, XZ, YX, YY, YZ, ZX, ZY, ZZ]

o Fourth-order tensors are stored as 6x6 matrices using the same component
  transformations as second-order symmetric tensors

"""
write('"""{}"""'.format(man), wrapit=0)
```

    import numpy as np
    from numpy import array, zeros
    """tensor.py

    THIS FILE IS AUTOMATICALLY GENERATED - DO NOT EDIT IT DIRECTLY!

    Constants and functions having to deal with 3D symmetric and nonsymmetric
    second order tensors fourth-order tensors with minor and major symmetries.
    All of the functions are written with the following assumptions:

    o Symmetric second-order tensors are stored as arrays of length 6 with the
      following component ordering

      [XX, YY, ZZ, XY, YZ, ZZ]

    o Nonsymmetric second-order tensors are stored as arrays of length 9 with the
      following component ordering

      [XX, XY, XZ, YX, YY, YZ, ZX, ZY, ZZ]

    o Fourth-order tensors are stored as 6x6 matrices using the same component
      transformations as second-order symmetric tensors

    """

```python
d = identity(3, dtype=float)
II1 = []
II2 = []
II3 = []
II5 = []
for I in range(6):
    for J in range(6):
        i,j = ij_from_I[I]
        k,l = ij_from_I[J]
        II1.append('{0:.1f}'.format(d[i,j] * d[k,l]))
        II2.append('{0:.1f}'.format(d[i,k] * d[j,l]))
        II3.append('{0:.1f}'.format(d[i,l] * d[j,k]))
        II5.append('{0:.1f}'.format((d[i,k] * d[j,l] + d[i,l]*d[j,k])/2.))

write('# Fourth-order "identities"')
II1 = ', '.join(II1)
write('# II1: I[i,j] I[k,l]')
write('II1 = array([{}]).reshape(6,6)'.format(II1))

II2 = ', '.join(II2)
write('# II2: I[i,k] I[j,l]')
write('II2 = array([{}]).reshape(6,6)'.format(II2))

II3 = ', '.join(II3)
write('# II3: I[i,l] I[j,k]')
write('II3 = array([{}]).reshape(6,6)'.format(II3))

write('II4 = (II2 + II3) / 2')
II5 = ', '.join(II5)
write('# II5 = (I[i,k] I[j,l] + I[i,l] I[j,k]) / 2')
write('II5 = array([{}]).reshape(6,6)'.format(II5))

write('# Second-order identities')
write('I6 = array([1., 1., 1., 0., 0., 0.])')
write('I9 = array([1., 0., 0., 0., 1., 0., 0., 0., 1.])')
write('I3x3 = I9.reshape(3,3)')
```

    # Fourth-order "identities"
    # II1: I[i,j] I[k,l]
    II1 = (array([1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0,
           1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
           0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]).reshape(6,6))
    # II2: I[i,k] I[j,l]
    II2 = (array([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
           0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
           0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]).reshape(6,6))
    # II3: I[i,l] I[j,k]
    II3 = (array([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
           0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
           0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]).reshape(6,6))
    II4 = (II2 + II3) / 2
    # II5 = (I[i,k] I[j,l] + I[i,l] I[j,k]) / 2
    II5 = (array([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
           0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0,
           0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5]).reshape(6,6))
    # Second-order identities
    I6 = array([1., 1., 1., 0., 0., 0.])
    I9 = array([1., 0., 0., 0., 1., 0., 0., 0., 1.])
    I3x3 = I9.reshape(3,3)

```python
F = UnsymMatrix('F')
FtF = F.T * F
write('def symsq(F):')
write('    """ Computes dot(F.T, F)"""')

write('    X = zeros(6)')
for I in range(6):
    i,j = ij_from_I[I]
    write('    X[{}] = {}'.format(I, FtF[i,j]))
write('    return X')
```

    def symsq(F):
        """ Computes dot(F.T, F)"""
        X = zeros(6)
        X[0] = F[0]**2 + F[3]**2 + F[6]**2
        X[1] = F[1]**2 + F[4]**2 + F[7]**2
        X[2] = F[2]**2 + F[5]**2 + F[8]**2
        X[3] = F[0]*F[1] + F[3]*F[4] + F[6]*F[7]
        X[4] = F[1]*F[2] + F[4]*F[5] + F[7]*F[8]
        X[5] = F[0]*F[2] + F[3]*F[5] + F[6]*F[8]
        return X

```python
F = UnsymMatrix('F')
A = SymMatrix('A')
dA = A.det()
dF = F.det()
write('def det(A):')
write('    """ Computes the determininant of A"""')
write('    if A.size == 6:')
write('        X = {}'.format(dA))
write('    else:')
write('        X = {}'.format(dF).replace('F', 'A'))
write('    return X')
```

    def det(A):
        """ Computes the determininant of A"""
        if A.size == 6:
            X = (A[0]*A[1]*A[2] - A[0]*A[4]**2 - A[1]*A[5]**2 - A[2]*A[3]**2 +
                 2*A[3]*A[4]*A[5])
        else:
            X = (A[0]*A[4]*A[8] - A[0]*A[5]*A[7] - A[1]*A[3]*A[8] + A[1]*A[5]*A[6]
                 + A[2]*A[3]*A[7] - A[2]*A[4]*A[6])
        return X

```python
As = SymMatrix('A')
Bs = SymMatrix('B')
Au = UnsymMatrix('A')
Bu = UnsymMatrix('B')
AsBs = ' + '.join([str(As[i,j]*Bs[i,j]) for i in range(3) for j in range(3)])
AsBu = ' + '.join([str(As[i,j]*Bu[i,j]) for i in range(3) for j in range(3)])
AuBu = ' + '.join([str(Au[i,j]*Bu[i,j]) for i in range(3) for j in range(3)])
write('def ddot(A, B):')
write('    """ Computes A:B"""')
write('    if B.size == 6 and A.size == 9:')
write('        A, B = B, A')
write('    if A.size == 6 and B.size == 6:')
write('        X = {}'.format(AsBs))
write('    elif A.size == 6 and B.size == 9:')
write('        X = {}'.format(AsBu))
write('    elif A.size == 9 and B.size == 9:')
write('        X = {}'.format(AuBu))
write('    else:\n        raise NotImplementedError')
write('    return X')
```

    def ddot(A, B):
        """ Computes A:B"""
        if B.size == 6 and A.size == 9:
            A, B = B, A
        if A.size == 6 and B.size == 6:
            X = (A[0]*B[0] + A[3]*B[3] + A[5]*B[5] + A[3]*B[3] + A[1]*B[1] +
                 A[4]*B[4] + A[5]*B[5] + A[4]*B[4] + A[2]*B[2])
        elif A.size == 6 and B.size == 9:
            X = (A[0]*B[0] + A[3]*B[1] + A[5]*B[2] + A[3]*B[3] + A[1]*B[4] +
                 A[4]*B[5] + A[5]*B[6] + A[4]*B[7] + A[2]*B[8])
        elif A.size == 9 and B.size == 9:
            X = (A[0]*B[0] + A[1]*B[1] + A[2]*B[2] + A[3]*B[3] + A[4]*B[4] +
                 A[5]*B[5] + A[6]*B[6] + A[7]*B[7] + A[8]*B[8])
        else:
            raise NotImplementedError
        return X

```python
write('def trace(A, metric=None):')
write('    """Computes the trace of a tensor"""', wrapit=0)
write('    if A.size not in (6, 9):')
write('        raise NotImplementedError')
write('    if A.size == 6 and metric is None:')
write('        metric, X = I6, I6')
write('    elif A.size == 9 and metric is None:')
write('        metric, X = I9, I9')
write('    else:')
write('        X = inv(metric)')
write('    return ddot(A, metric)')
write('def iso(A, metric=None):')
write('    """Computes the isotropic part of a tensor"""', wrapit=0)
write('    if A.size not in (6, 9):')
write('        raise NotImplementedError')
write('    if A.size == 6 and metric is None:')
write('        metric, X = I6, I6')
write('    elif A.size == 9 and metric is None:')
write('        metric, X = I9, I9')
write('    else:')
write('        X = inv(metric)')
write('    return trace(A, metric) / 3. * X')
write('def dev(A, metric=None):')
write('    """Computes the isotropic part of a tensor"""', wrapit=0)
write('    return A - iso(A, metric=metric)')
write('def mag(A):\n    """Computes the magnitude of a tensor"""\n    return np.sqrt(ddot(A, A))', wrapit=0)
```

    def trace(A, metric=None):
        """Computes the trace of a tensor"""
        if A.size not in (6, 9):
            raise NotImplementedError
        if A.size == 6 and metric is None:
            metric, X = I6, I6
        elif A.size == 9 and metric is None:
            metric, X = I9, I9
        else:
            X = inv(metric)
        return ddot(A, metric)
    def iso(A, metric=None):
        """Computes the isotropic part of a tensor"""
        if A.size not in (6, 9):
            raise NotImplementedError
        if A.size == 6 and metric is None:
            metric, X = I6, I6
        elif A.size == 9 and metric is None:
            metric, X = I9, I9
        else:
            X = inv(metric)
        return trace(A, metric) / 3. * X
    def dev(A, metric=None):
        """Computes the isotropic part of a tensor"""
        return A - iso(A, metric=metric)
    def mag(A):
        """Computes the magnitude of a tensor"""
        return np.sqrt(ddot(A, A))

```python
A = SymMatrix('A')
write('def invariants(A, itype=0):', wrapit=0)
write('''\
    """ Computes the invariants of A

        Parameters
        ----------
        itype : int
            Type of invariants to compute

        Returns
        -------
        A, B, C : float

        Notes
        -----
        The values A, B, C depend on itype:
            =0 -> I1, I2, I3
            =1 -> I1, J2, J3
    """
''', wrapit=0)
I1 = A.trace()
I2 = (I1 ** 2 - (A * A).trace()) / 2
S = A - A.trace() / 3 * Ident
J2 = (S * S).trace() / 2
J3 = (S * S * S).trace() / 3
write('    if itype == 0:')
write('        I1 = {}'.format(I1))
write('        I2 = {}'.format(I2))
write('        I3 = det(A)')
write('        A, B, C = I1, I2, I3')
write('    elif itype == 1:')
write('        I1 = {}'.format(I1))
write('        J2 = {}'.format(J2))
write('        J3 = {}'.format(J3))
write('        A, B, C = I1, J2, J3')
write('    else:\n        raise NotImplementedError')
write('    return A, B, C')
```

    def invariants(A, itype=0):
        """ Computes the invariants of A

            Parameters
            ----------
            itype : int
                Type of invariants to compute

            Returns
            -------
            A, B, C : float

            Notes
            -----
            The values A, B, C depend on itype:
                =0 -> I1, I2, I3
                =1 -> I1, J2, J3
        """

        if itype == 0:
            I1 = A[0] + A[1] + A[2]
            I2 = (-A[0]**2/2 - A[1]**2/2 - A[2]**2/2 - A[3]**2 - A[4]**2 - A[5]**2
                  + (A[0] + A[1] + A[2])**2/2)
            I3 = det(A)
            A, B, C = I1, I2, I3
        elif itype == 1:
            I1 = A[0] + A[1] + A[2]
            J2 = (A[3]**2 + A[4]**2 + A[5]**2 + (-A[0]/3 - A[1]/3 + 2*A[2]/3)**2/2
                  + (-A[0]/3 + 2*A[1]/3 - A[2]/3)**2/2 + (2*A[0]/3 - A[1]/3 -
                  A[2]/3)**2/2)
            J3 = (2*A[3]*(A[3]*(-A[0]/3 + 2*A[1]/3 - A[2]/3) + A[3]*(2*A[0]/3 -
                  A[1]/3 - A[2]/3) + A[4]*A[5])/3 + 2*A[4]*(A[3]*A[5] +
                  A[4]*(-A[0]/3 - A[1]/3 + 2*A[2]/3) + A[4]*(-A[0]/3 + 2*A[1]/3 -
                  A[2]/3))/3 + 2*A[5]*(A[3]*A[4] + A[5]*(-A[0]/3 - A[1]/3 +
                  2*A[2]/3) + A[5]*(2*A[0]/3 - A[1]/3 - A[2]/3))/3 + (-A[0]/3 -
                  A[1]/3 + 2*A[2]/3)*(A[4]**2 + A[5]**2 + (-A[0]/3 - A[1]/3 +
                  2*A[2]/3)**2)/3 + (-A[0]/3 + 2*A[1]/3 - A[2]/3)*(A[3]**2 +
                  A[4]**2 + (-A[0]/3 + 2*A[1]/3 - A[2]/3)**2)/3 + (2*A[0]/3 -
                  A[1]/3 - A[2]/3)*(A[3]**2 + A[5]**2 + (2*A[0]/3 - A[1]/3 -
                  A[2]/3)**2)/3)
            A, B, C = I1, J2, J3
        else:
            raise NotImplementedError
        return A, B, C

```python
F = UnsymMatrix('F')
A = SymMatrix('A')
X = F * A * F.T
write('def push(F, A):')
write('    """Computes the push operation F A F.T / J"""', wrapit=0)
write('    if A.size == 6:\n        return push6(F, A)')
write('    elif A.size == 36:\n        return push66(F, A)')
write('    raise NotImplementedError')
write('def push6(F, A):')
write('    X = zeros(6)')
for I in range(6):
    i,j = ij_from_I[I]
    write('    X[{}] = {}'.format(I, X[i,j]))
write('    return X / det(F)')
write('def push66(F, A):')
write('    Q = symleaf(F)')
write('    X = np.dot(np.dot(Q, A), Q.T)')
write('    return X / det(F)')
```

    def push(F, A):
        """Computes the push operation F A F.T / J"""
        if A.size == 6:
            return push6(F, A)
        elif A.size == 36:
            return push66(F, A)
        raise NotImplementedError
    def push6(F, A):
        X = zeros(6)
        X[0] = (F[0]*(A[0]*F[0] + A[3]*F[1] + A[5]*F[2]) + F[1]*(A[1]*F[1] +
                A[3]*F[0] + A[4]*F[2]) + F[2]*(A[2]*F[2] + A[4]*F[1] + A[5]*F[0]))
        X[1] = (F[3]*(A[0]*F[3] + A[3]*F[4] + A[5]*F[5]) + F[4]*(A[1]*F[4] +
                A[3]*F[3] + A[4]*F[5]) + F[5]*(A[2]*F[5] + A[4]*F[4] + A[5]*F[3]))
        X[2] = (F[6]*(A[0]*F[6] + A[3]*F[7] + A[5]*F[8]) + F[7]*(A[1]*F[7] +
                A[3]*F[6] + A[4]*F[8]) + F[8]*(A[2]*F[8] + A[4]*F[7] + A[5]*F[6]))
        X[3] = (F[3]*(A[0]*F[0] + A[3]*F[1] + A[5]*F[2]) + F[4]*(A[1]*F[1] +
                A[3]*F[0] + A[4]*F[2]) + F[5]*(A[2]*F[2] + A[4]*F[1] + A[5]*F[0]))
        X[4] = (F[6]*(A[0]*F[3] + A[3]*F[4] + A[5]*F[5]) + F[7]*(A[1]*F[4] +
                A[3]*F[3] + A[4]*F[5]) + F[8]*(A[2]*F[5] + A[4]*F[4] + A[5]*F[3]))
        X[5] = (F[6]*(A[0]*F[0] + A[3]*F[1] + A[5]*F[2]) + F[7]*(A[1]*F[1] +
                A[3]*F[0] + A[4]*F[2]) + F[8]*(A[2]*F[2] + A[4]*F[1] + A[5]*F[0]))
        return X / det(F)
    def push66(F, A):
        Q = symleaf(F)
        X = np.dot(np.dot(Q, A), Q.T)
        return X / det(F)

```python
F = UnsymMatrix('F')
A = SymMatrix('A')
Ai = A.inv()
Fi = F.inv()
write('def inv(A):')
write('    """Computes the inverse of A"""', wrapit=0)
write('    if A.size == 6:')
write('        X = zeros(6)')
for I in range(6):
    i,j = ij_from_I[I]
    write('        X[{}] = {}'.format(I, Ai[i,j]))
write('    elif A.size == 9:')
write('        X = zeros(9)')
for I in range(9):
    i,j = ij_from_I_9[I]
    write('        X[{}] = {}'.format(I, Fi[i,j]))
#write('        X = np.linalg.inv(A.reshape(3,3))')
#write('        X = X.reshape(9)')
write('    else:\n        raise NotImplementedError')
write('    return X')
```

    def inv(A):
        """Computes the inverse of A"""
        if A.size == 6:
            X = zeros(6)
            X[0] = (-(A[0]*A[1] - A[3]**2)*(-A[3]*(A[4] -
                    A[3]*A[5]/A[0])/(A[0]*(A[1] - A[3]**2/A[0])) +
                    A[5]/A[0])*(A[3]*(A[4] - A[3]*A[5]/A[0])/(A[0]*(A[1] -
                    A[3]**2/A[0])) - A[5]/A[0])/(A[0]*A[1]*A[2] - A[0]*A[4]**2 -
                    A[1]*A[5]**2 - A[2]*A[3]**2 + 2*A[3]*A[4]*A[5]) + 1/A[0] +
                    A[3]**2/(A[0]**2*(A[1] - A[3]**2/A[0])))
            X[1] = (1/(A[1] - A[3]**2/A[0]) + (A[4] -
                    A[3]*A[5]/A[0])**2*(A[0]*A[1] - A[3]**2)/((A[1] -
                    A[3]**2/A[0])**2*(A[0]*A[1]*A[2] - A[0]*A[4]**2 - A[1]*A[5]**2
                    - A[2]*A[3]**2 + 2*A[3]*A[4]*A[5])))
            X[2] = ((A[0]*A[1] - A[3]**2)/(A[0]*A[1]*A[2] - A[0]*A[4]**2 -
                    A[1]*A[5]**2 - A[2]*A[3]**2 + 2*A[3]*A[4]*A[5]))
            X[3] = ((A[4] - A[3]*A[5]/A[0])*(A[0]*A[1] - A[3]**2)*(-A[3]*(A[4] -
                    A[3]*A[5]/A[0])/(A[0]*(A[1] - A[3]**2/A[0])) +
                    A[5]/A[0])/((A[1] - A[3]**2/A[0])*(A[0]*A[1]*A[2] -
                    A[0]*A[4]**2 - A[1]*A[5]**2 - A[2]*A[3]**2 +
                    2*A[3]*A[4]*A[5])) - A[3]/(A[0]*(A[1] - A[3]**2/A[0])))
            X[4] = (-(A[4] - A[3]*A[5]/A[0])*(A[0]*A[1] - A[3]**2)/((A[1] -
                    A[3]**2/A[0])*(A[0]*A[1]*A[2] - A[0]*A[4]**2 - A[1]*A[5]**2 -
                    A[2]*A[3]**2 + 2*A[3]*A[4]*A[5])))
            X[5] = (-(A[0]*A[1] - A[3]**2)*(-A[3]*(A[4] -
                    A[3]*A[5]/A[0])/(A[0]*(A[1] - A[3]**2/A[0])) +
                    A[5]/A[0])/(A[0]*A[1]*A[2] - A[0]*A[4]**2 - A[1]*A[5]**2 -
                    A[2]*A[3]**2 + 2*A[3]*A[4]*A[5]))
        elif A.size == 9:
            X = zeros(9)
            X[0] = (-(F[0]*F[4] - F[1]*F[3])*(-F[1]*(F[5] -
                    F[2]*F[3]/F[0])/(F[0]*(F[4] - F[1]*F[3]/F[0])) +
                    F[2]/F[0])*(F[3]*(F[7] - F[1]*F[6]/F[0])/(F[0]*(F[4] -
                    F[1]*F[3]/F[0])) - F[6]/F[0])/(F[0]*F[4]*F[8] - F[0]*F[5]*F[7]
                    - F[1]*F[3]*F[8] + F[1]*F[5]*F[6] + F[2]*F[3]*F[7] -
                    F[2]*F[4]*F[6]) + 1/F[0] + F[1]*F[3]/(F[0]**2*(F[4] -
                    F[1]*F[3]/F[0])))
            X[1] = ((F[7] - F[1]*F[6]/F[0])*(F[0]*F[4] - F[1]*F[3])*(-F[1]*(F[5] -
                    F[2]*F[3]/F[0])/(F[0]*(F[4] - F[1]*F[3]/F[0])) +
                    F[2]/F[0])/((F[4] - F[1]*F[3]/F[0])*(F[0]*F[4]*F[8] -
                    F[0]*F[5]*F[7] - F[1]*F[3]*F[8] + F[1]*F[5]*F[6] +
                    F[2]*F[3]*F[7] - F[2]*F[4]*F[6])) - F[1]/(F[0]*(F[4] -
                    F[1]*F[3]/F[0])))
            X[2] = (-(F[0]*F[4] - F[1]*F[3])*(-F[1]*(F[5] -
                    F[2]*F[3]/F[0])/(F[0]*(F[4] - F[1]*F[3]/F[0])) +
                    F[2]/F[0])/(F[0]*F[4]*F[8] - F[0]*F[5]*F[7] - F[1]*F[3]*F[8] +
                    F[1]*F[5]*F[6] + F[2]*F[3]*F[7] - F[2]*F[4]*F[6]))
            X[3] = (-(F[5] - F[2]*F[3]/F[0])*(F[0]*F[4] - F[1]*F[3])*(F[3]*(F[7] -
                    F[1]*F[6]/F[0])/(F[0]*(F[4] - F[1]*F[3]/F[0])) -
                    F[6]/F[0])/((F[4] - F[1]*F[3]/F[0])*(F[0]*F[4]*F[8] -
                    F[0]*F[5]*F[7] - F[1]*F[3]*F[8] + F[1]*F[5]*F[6] +
                    F[2]*F[3]*F[7] - F[2]*F[4]*F[6])) - F[3]/(F[0]*(F[4] -
                    F[1]*F[3]/F[0])))
            X[4] = (1/(F[4] - F[1]*F[3]/F[0]) + (F[5] - F[2]*F[3]/F[0])*(F[7] -
                    F[1]*F[6]/F[0])*(F[0]*F[4] - F[1]*F[3])/((F[4] -
                    F[1]*F[3]/F[0])**2*(F[0]*F[4]*F[8] - F[0]*F[5]*F[7] -
                    F[1]*F[3]*F[8] + F[1]*F[5]*F[6] + F[2]*F[3]*F[7] -
                    F[2]*F[4]*F[6])))
            X[5] = (-(F[5] - F[2]*F[3]/F[0])*(F[0]*F[4] - F[1]*F[3])/((F[4] -
                    F[1]*F[3]/F[0])*(F[0]*F[4]*F[8] - F[0]*F[5]*F[7] -
                    F[1]*F[3]*F[8] + F[1]*F[5]*F[6] + F[2]*F[3]*F[7] -
                    F[2]*F[4]*F[6])))
            X[6] = ((F[0]*F[4] - F[1]*F[3])*(F[3]*(F[7] -
                    F[1]*F[6]/F[0])/(F[0]*(F[4] - F[1]*F[3]/F[0])) -
                    F[6]/F[0])/(F[0]*F[4]*F[8] - F[0]*F[5]*F[7] - F[1]*F[3]*F[8] +
                    F[1]*F[5]*F[6] + F[2]*F[3]*F[7] - F[2]*F[4]*F[6]))
            X[7] = (-(F[7] - F[1]*F[6]/F[0])*(F[0]*F[4] - F[1]*F[3])/((F[4] -
                    F[1]*F[3]/F[0])*(F[0]*F[4]*F[8] - F[0]*F[5]*F[7] -
                    F[1]*F[3]*F[8] + F[1]*F[5]*F[6] + F[2]*F[3]*F[7] -
                    F[2]*F[4]*F[6])))
            X[8] = ((F[0]*F[4] - F[1]*F[3])/(F[0]*F[4]*F[8] - F[0]*F[5]*F[7] -
                    F[1]*F[3]*F[8] + F[1]*F[5]*F[6] + F[2]*F[3]*F[7] -
                    F[2]*F[4]*F[6]))
        else:
            raise NotImplementedError
        return X

```python
write('def dyad(A, B):')
write('    """Computes the outer product of A and B"""', wrapit=0)
write('    if A.size == 6 and B.size == 6:')
write('        X = zeros((6,6))')
for I in range(6):
    for J in range(6):
        ij = I_from_ij(*sorted(ij_from_I[I]))
        kl = I_from_ij(*sorted(ij_from_I[J]))
        write('        X[{},{}] = A[{}] * B[{}]'.format(I,J,ij,kl))
write('    elif A.size == 3 and B.size == 3:')
write('        X = zeros(6)')

for I in range(6):
    i,j = ij_from_I[I]
    write('        X[{}] = A[{}] * B[{}]'.format(I,i,j))
write('    else:\n        raise NotImplementedError')
write('    return X')
```

    def dyad(A, B):
        """Computes the outer product of A and B"""
        if A.size == 6 and B.size == 6:
            X = zeros((6,6))
            X[0,0] = A[0] * B[0]
            X[0,1] = A[0] * B[1]
            X[0,2] = A[0] * B[2]
            X[0,3] = A[0] * B[3]
            X[0,4] = A[0] * B[4]
            X[0,5] = A[0] * B[5]
            X[1,0] = A[1] * B[0]
            X[1,1] = A[1] * B[1]
            X[1,2] = A[1] * B[2]
            X[1,3] = A[1] * B[3]
            X[1,4] = A[1] * B[4]
            X[1,5] = A[1] * B[5]
            X[2,0] = A[2] * B[0]
            X[2,1] = A[2] * B[1]
            X[2,2] = A[2] * B[2]
            X[2,3] = A[2] * B[3]
            X[2,4] = A[2] * B[4]
            X[2,5] = A[2] * B[5]
            X[3,0] = A[3] * B[0]
            X[3,1] = A[3] * B[1]
            X[3,2] = A[3] * B[2]
            X[3,3] = A[3] * B[3]
            X[3,4] = A[3] * B[4]
            X[3,5] = A[3] * B[5]
            X[4,0] = A[4] * B[0]
            X[4,1] = A[4] * B[1]
            X[4,2] = A[4] * B[2]
            X[4,3] = A[4] * B[3]
            X[4,4] = A[4] * B[4]
            X[4,5] = A[4] * B[5]
            X[5,0] = A[5] * B[0]
            X[5,1] = A[5] * B[1]
            X[5,2] = A[5] * B[2]
            X[5,3] = A[5] * B[3]
            X[5,4] = A[5] * B[4]
            X[5,5] = A[5] * B[5]
        elif A.size == 3 and B.size == 3:
            X = zeros(6)
            X[0] = A[0] * B[0]
            X[1] = A[1] * B[1]
            X[2] = A[2] * B[2]
            X[3] = A[0] * B[1]
            X[4] = A[1] * B[2]
            X[5] = A[0] * B[2]
        else:
            raise NotImplementedError
        return X

```python
write('def symshuffle(A, B):')
write('    """ Computes the product Xijkl = .5 (Aik Bjl + Ail Bjk)"""', wrapit=0)
write('    X = zeros((6,6))')
write('    if A.size == 6 and B.size == 6:')
for I in range(6):
    for J in range(6):
        i,j = ij_from_I[I]
        k,l = ij_from_I[J]
        ik = I_from_ij(*sorted((i,k)))
        jl = I_from_ij(*sorted((j,l)))
        il = I_from_ij(*sorted((i,l)))
        jk = I_from_ij(*sorted((j,k)))
        write('        X[{},{}] = (A[{}] * B[{}] + A[{}] * B[{}]) / 2.'.format(I,J,ik,jl,il,jk))
write('    else:')
write('        raise NotImplementedError')
write('    return X')
write('odot = symshuffle')
```

    def symshuffle(A, B):
        """ Computes the product Xijkl = .5 (Aik Bjl + Ail Bjk)"""
        X = zeros((6,6))
        if A.size == 6 and B.size == 6:
            X[0,0] = (A[0] * B[0] + A[0] * B[0]) / 2.
            X[0,1] = (A[3] * B[3] + A[3] * B[3]) / 2.
            X[0,2] = (A[5] * B[5] + A[5] * B[5]) / 2.
            X[0,3] = (A[0] * B[3] + A[3] * B[0]) / 2.
            X[0,4] = (A[3] * B[5] + A[5] * B[3]) / 2.
            X[0,5] = (A[0] * B[5] + A[5] * B[0]) / 2.
            X[1,0] = (A[3] * B[3] + A[3] * B[3]) / 2.
            X[1,1] = (A[1] * B[1] + A[1] * B[1]) / 2.
            X[1,2] = (A[4] * B[4] + A[4] * B[4]) / 2.
            X[1,3] = (A[3] * B[1] + A[1] * B[3]) / 2.
            X[1,4] = (A[1] * B[4] + A[4] * B[1]) / 2.
            X[1,5] = (A[3] * B[4] + A[4] * B[3]) / 2.
            X[2,0] = (A[5] * B[5] + A[5] * B[5]) / 2.
            X[2,1] = (A[4] * B[4] + A[4] * B[4]) / 2.
            X[2,2] = (A[2] * B[2] + A[2] * B[2]) / 2.
            X[2,3] = (A[5] * B[4] + A[4] * B[5]) / 2.
            X[2,4] = (A[4] * B[2] + A[2] * B[4]) / 2.
            X[2,5] = (A[5] * B[2] + A[2] * B[5]) / 2.
            X[3,0] = (A[0] * B[3] + A[0] * B[3]) / 2.
            X[3,1] = (A[3] * B[1] + A[3] * B[1]) / 2.
            X[3,2] = (A[5] * B[4] + A[5] * B[4]) / 2.
            X[3,3] = (A[0] * B[1] + A[3] * B[3]) / 2.
            X[3,4] = (A[3] * B[4] + A[5] * B[1]) / 2.
            X[3,5] = (A[0] * B[4] + A[5] * B[3]) / 2.
            X[4,0] = (A[3] * B[5] + A[3] * B[5]) / 2.
            X[4,1] = (A[1] * B[4] + A[1] * B[4]) / 2.
            X[4,2] = (A[4] * B[2] + A[4] * B[2]) / 2.
            X[4,3] = (A[3] * B[4] + A[1] * B[5]) / 2.
            X[4,4] = (A[1] * B[2] + A[4] * B[4]) / 2.
            X[4,5] = (A[3] * B[2] + A[4] * B[5]) / 2.
            X[5,0] = (A[0] * B[5] + A[0] * B[5]) / 2.
            X[5,1] = (A[3] * B[4] + A[3] * B[4]) / 2.
            X[5,2] = (A[5] * B[2] + A[5] * B[2]) / 2.
            X[5,3] = (A[0] * B[4] + A[3] * B[5]) / 2.
            X[5,4] = (A[3] * B[2] + A[5] * B[4]) / 2.
            X[5,5] = (A[0] * B[2] + A[5] * B[5]) / 2.
        else:
            raise NotImplementedError
        return X
    odot = symshuffle

## Push transformation

$$A_{ij} = F_{im}C_{mn}F_{jn} = L_{imjn}C_{mn}$$

where

$$ L_{imjn} = F_{im}F_{jn}$$

```python
L = {}
for i in range(3):
    for j in range(3):
        p, q = sorted((i,j))
        I = I_from_ij(p,q)
        row = []
        for m in range(3):
            for n in range(3):
                p, q = sorted((m,n))
                J = I_from_ij(p,q)
                im = I_from_ij_9(i,m)
                jn = I_from_ij_9(j,n)
                L.setdefault((I,J), []).append('F[{}] * F[{}]'.format(im,jn))

for (k, v) in L.items():
    L[k] = ' + '.join(v[:2])

write('def symleaf(F):')
write('''\
    """ COMPUTE A 6X6 MANDEL MATRIX THAT IS THE SYM-LEAF TRANSFORMATION OF THE
    INPUT 3X3 MATRIX F.

    Parameters
    ----------
    F : ANY 3X3 MATRIX (IN CONVENTIONAL 3X3 STORAGE)

    Returns
    -------
    X : 6X6 MANDEL MATRIX FOR THE SYM-LEAF TRANSFORMATION MATRIX

    Notes
    -----
    IF A IS ANY SYMMETRIC TENSOR, AND IF {A} IS ITS 6X1 MANDEL ARRAY, THEN THE
    6X1 MANDEL ARRAY FOR THE TENSOR B=F.A.TRANSPOSE[F] MAY BE COMPUTED BY

                          {B}=[FF]{A}

    IF F IS A DEFORMATION F, THEN B IS THE "PUSH" (SPATIAL) TRANSFORMATION OF
    THE REFERENCE TENSOR A IF F IS Inverse[F], THEN B IS THE "PULL"
    (REFERENCE) TRANSFORMATION OF THE SPATIAL TENSOR A, AND THEREFORE B WOULD
    BE Inverse[FF]{A}.

    IF F IS A ROTATION, THEN B IS THE ROTATION OF A, AND
    FF WOULD BE BE A 6X6 ORTHOGONAL MATRIX, JUST AS IS F

    """''', wrapit=0)
write('    X = zeros((6,6))')
for i in range(6):
    for j in range(6):
        write('    X[{},{}] = {}'.format(i,j,L[i,j]))
write('    return X')
```

    def symleaf(F):
        """ COMPUTE A 6X6 MANDEL MATRIX THAT IS THE SYM-LEAF TRANSFORMATION OF THE
        INPUT 3X3 MATRIX F.

        Parameters
        ----------
        F : ANY 3X3 MATRIX (IN CONVENTIONAL 3X3 STORAGE)

        Returns
        -------
        X : 6X6 MANDEL MATRIX FOR THE SYM-LEAF TRANSFORMATION MATRIX

        Notes
        -----
        IF A IS ANY SYMMETRIC TENSOR, AND IF {A} IS ITS 6X1 MANDEL ARRAY, THEN THE
        6X1 MANDEL ARRAY FOR THE TENSOR B=F.A.TRANSPOSE[F] MAY BE COMPUTED BY

                              {B}=[FF]{A}

        IF F IS A DEFORMATION F, THEN B IS THE "PUSH" (SPATIAL) TRANSFORMATION OF
        THE REFERENCE TENSOR A IF F IS Inverse[F], THEN B IS THE "PULL"
        (REFERENCE) TRANSFORMATION OF THE SPATIAL TENSOR A, AND THEREFORE B WOULD
        BE Inverse[FF]{A}.

        IF F IS A ROTATION, THEN B IS THE ROTATION OF A, AND
        FF WOULD BE BE A 6X6 ORTHOGONAL MATRIX, JUST AS IS F

        """
        X = zeros((6,6))
        X[0,0] = F[0] * F[0]
        X[0,1] = F[1] * F[1]
        X[0,2] = F[2] * F[2]
        X[0,3] = F[0] * F[1] + F[1] * F[0]
        X[0,4] = F[1] * F[2] + F[2] * F[1]
        X[0,5] = F[0] * F[2] + F[2] * F[0]
        X[1,0] = F[3] * F[3]
        X[1,1] = F[4] * F[4]
        X[1,2] = F[5] * F[5]
        X[1,3] = F[3] * F[4] + F[4] * F[3]
        X[1,4] = F[4] * F[5] + F[5] * F[4]
        X[1,5] = F[3] * F[5] + F[5] * F[3]
        X[2,0] = F[6] * F[6]
        X[2,1] = F[7] * F[7]
        X[2,2] = F[8] * F[8]
        X[2,3] = F[6] * F[7] + F[7] * F[6]
        X[2,4] = F[7] * F[8] + F[8] * F[7]
        X[2,5] = F[6] * F[8] + F[8] * F[6]
        X[3,0] = F[0] * F[3] + F[3] * F[0]
        X[3,1] = F[1] * F[4] + F[4] * F[1]
        X[3,2] = F[2] * F[5] + F[5] * F[2]
        X[3,3] = F[0] * F[4] + F[1] * F[3]
        X[3,4] = F[1] * F[5] + F[2] * F[4]
        X[3,5] = F[0] * F[5] + F[2] * F[3]
        X[4,0] = F[3] * F[6] + F[6] * F[3]
        X[4,1] = F[4] * F[7] + F[7] * F[4]
        X[4,2] = F[5] * F[8] + F[8] * F[5]
        X[4,3] = F[3] * F[7] + F[4] * F[6]
        X[4,4] = F[4] * F[8] + F[5] * F[7]
        X[4,5] = F[3] * F[8] + F[5] * F[6]
        X[5,0] = F[0] * F[6] + F[6] * F[0]
        X[5,1] = F[1] * F[7] + F[7] * F[1]
        X[5,2] = F[2] * F[8] + F[8] * F[2]
        X[5,3] = F[0] * F[7] + F[1] * F[6]
        X[5,4] = F[1] * F[8] + F[2] * F[7]
        X[5,5] = F[0] * F[8] + F[2] * F[6]
        return X

```python
print('    @classmethod')
print('    def param_names(cls, n):')
N = 3
M = 3
ij = []
for n in range(N+2):
    ij.extend([(i,j) for i in range(n)[::-1] for j in range(n) if i+j==n-1])
ij = ij[1:]
cij = ["'C{}{}'".format(i,j) for i,j in ij]
dij = ['D{0}'.format(i) for i in range(M)]
params = '        return [{}]'.format(','.join(cij + dij))
print(params)

NCI = [x[0] for x in ij]
NCJ = [x[1] for x in ij]
nc = len(NCI)
nd = len(dij)

u = ['u = zeros(2)']
for k in range(nc):
    i = NCI[k]
    j = NCJ[k]
    w = 'c[{0}] * ((IB1 - 3.) ** {1}) * ((IB2 - 3.) ** {2})'.format(k, i, j)
    u.append('u[1] += {0}'.format(w))
for k in range(nd):
    K = k + 1
    w = '1. / c[{0}] * (Jac - 1.) ** {1}'.format(nc+k, 2*K)
    u.append('if c[{0}] > 0.: u[0] += {1}'.format(nc+k, w))
u.append('u[0] = u[0] + u[1]')

du = []
for k in range(nc):
    i = NCI[k]
    j = NCJ[k]
    if i > 0:
        fac = i
        w1 = '(IB1 - 3.) ** {0}'.format(i-1)
        w2 = '(IB2 - 3.) ** {0}'.format(j)
        w = '{0:.1f} * c[{1}] * ({2}) * ({3})'.format(fac, k, w1, w2)
        du.append('du[0] += {0}'.format(w))

    if j > 0:
        fac = j
        w1 = '(IB1 - 3.) ** {0}'.format(i)
        w2 = '(IB2 - 3.) ** {0}'.format(j-1)
        w = '{0:.1f} * c[{1}] * ({2}) * ({3})'.format(fac, k, w1, w2)
        du.append('du[1] += {0}'.format(w))
du = ['du = zeros(3)'] + sorted(du, key=lambda x: int(x[3]))

for k in range(nd):
    K = k + 1
    fac = 2 * K
    if fac <= 0:
        continue
    w = '{0:.1f} / c[{1}] * (Jac - 1.) ** {2}'.format(fac, nc+k, 2*K-1)
    du.append('if c[{0}] > 0.: du[2] += {1}'.format(nc+k, w))

d2u = []
for k in range(nc):
    i = NCI[k]
    j = NCJ[k]

    fac = i * (i - 1)
    if fac > 0:
        w1 = '(IB1 - 3.) ** {0}'.format(i-2)
        w2 = '(IB2 - 3.) ** {0}'.format(j)
        w = '{0:.1f} * c[{1}] * ({2}) * ({3})'.format(fac, k, w1, w2)
        d2u.append('ddu[0] += {0}'.format(w))

    fac = j * (j - 1)
    if fac > 0:
        w1 = '(IB1 - 3.) ** {0}'.format(i)
        w2 = '(IB2 - 3.) ** {0}'.format(j-2)
        w = '{0:.1f} * c[{1}] * ({2}) * ({3})'.format(fac, k, w1, w2)
        d2u.append('ddu[1] += {0}'.format(w))

    fac = i * j
    if fac > 0:
        w1 = '(IB1 - 3.) ** {0}'.format(i-1)
        w2 = '(IB2 - 3.) ** {0}'.format(j-1)
        w = '{0:.1f} * c[{1}] * ({2}) * ({3})'.format(fac, k, w1, w2)
        d2u.append('ddu[3] += {0}'.format(w))

d2u = ['ddu = zeros(6)'] + sorted(d2u, key=lambda x: int(x[4]))
for k in range(nd):
    K = k + 1
    fac = 2 * K * (2 * K - 1)
    if fac <= 0:
        continue
    w = '{0:.1f} / c[{1}] * (Jac - 1.) ** {2}'.format(fac, nc+k, 2*K-2)
    d2u.append('if c[{0}] > 0.: ddu[2] += {1}'.format(nc+k, w))

d3u = ['dddu = zeros(6)']
for k in range(nd):
    K = k + 1
    fac = 4 * K * (2 * K - 1) * (K - 1)
    if fac <= 0:
        continue
    w = '{0:.1f} / c[{1}] * (Jac - 1.) ** {2}'.format(fac, nc+k, 2*K-3)
    d3u.append('if c[{0}] > 0.: dddu[5] += {1}'.format(nc+k, w))

print()
print('def uhyper(c, IB1, IB2, Jac):')
print('    """Computes the energies and derivatives of the isotropic')
print('       hyperelastic energy"""\n')
print('       THIS FUNCTION WAS GENERATED AUTOMATICALLY, DO NOT EDIT IT DIRECTLY')
print('    """')

s = 'Energies'
print('    # {0}'.format(s))
print('    {0}'.format('\n    '.join(u)))

s = 'First Derivatives'
print('\n    # {0}'.format(s))
print('    {0}'.format('\n    '.join(du)))

s = 'Second Derivatives'
print('\n    # {0}'.format(s))
print('    {0}'.format('\n    '.join(d2u)))

s = 'Third Derivatives'
print('\n    # {0}'.format(s))
print('    {0}'.format('\n    '.join(d3u)))

print('    return u, du, ddu, dddu')
```

        @classmethod
        def param_names(cls, n):
            return ['C10','C01','C20','C11','C02','C30','C21','C12','C03',D0,D1,D2]

    def uhyper(c, IB1, IB2, Jac):
        """Computes the energies and derivatives of the isotropic
           hyperelastic energy"""

           THIS FUNCTION WAS GENERATED AUTOMATICALLY, DO NOT EDIT IT DIRECTLY
        """
        # Energies
        u = zeros(2)
        u[1] += c[0] * ((IB1 - 3.) ** 1) * ((IB2 - 3.) ** 0)
        u[1] += c[1] * ((IB1 - 3.) ** 0) * ((IB2 - 3.) ** 1)
        u[1] += c[2] * ((IB1 - 3.) ** 2) * ((IB2 - 3.) ** 0)
        u[1] += c[3] * ((IB1 - 3.) ** 1) * ((IB2 - 3.) ** 1)
        u[1] += c[4] * ((IB1 - 3.) ** 0) * ((IB2 - 3.) ** 2)
        u[1] += c[5] * ((IB1 - 3.) ** 3) * ((IB2 - 3.) ** 0)
        u[1] += c[6] * ((IB1 - 3.) ** 2) * ((IB2 - 3.) ** 1)
        u[1] += c[7] * ((IB1 - 3.) ** 1) * ((IB2 - 3.) ** 2)
        u[1] += c[8] * ((IB1 - 3.) ** 0) * ((IB2 - 3.) ** 3)
        if c[9] > 0.: u[0] += 1. / c[9] * (Jac - 1.) ** 2
        if c[10] > 0.: u[0] += 1. / c[10] * (Jac - 1.) ** 4
        if c[11] > 0.: u[0] += 1. / c[11] * (Jac - 1.) ** 6
        u[0] = u[0] + u[1]

        # First Derivatives
        du = zeros(3)
        du[0] += 1.0 * c[0] * ((IB1 - 3.) ** 0) * ((IB2 - 3.) ** 0)
        du[0] += 2.0 * c[2] * ((IB1 - 3.) ** 1) * ((IB2 - 3.) ** 0)
        du[0] += 1.0 * c[3] * ((IB1 - 3.) ** 0) * ((IB2 - 3.) ** 1)
        du[0] += 3.0 * c[5] * ((IB1 - 3.) ** 2) * ((IB2 - 3.) ** 0)
        du[0] += 2.0 * c[6] * ((IB1 - 3.) ** 1) * ((IB2 - 3.) ** 1)
        du[0] += 1.0 * c[7] * ((IB1 - 3.) ** 0) * ((IB2 - 3.) ** 2)
        du[1] += 1.0 * c[1] * ((IB1 - 3.) ** 0) * ((IB2 - 3.) ** 0)
        du[1] += 1.0 * c[3] * ((IB1 - 3.) ** 1) * ((IB2 - 3.) ** 0)
        du[1] += 2.0 * c[4] * ((IB1 - 3.) ** 0) * ((IB2 - 3.) ** 1)
        du[1] += 1.0 * c[6] * ((IB1 - 3.) ** 2) * ((IB2 - 3.) ** 0)
        du[1] += 2.0 * c[7] * ((IB1 - 3.) ** 1) * ((IB2 - 3.) ** 1)
        du[1] += 3.0 * c[8] * ((IB1 - 3.) ** 0) * ((IB2 - 3.) ** 2)
        if c[9] > 0.: du[2] += 2.0 / c[9] * (Jac - 1.) ** 1
        if c[10] > 0.: du[2] += 4.0 / c[10] * (Jac - 1.) ** 3
        if c[11] > 0.: du[2] += 6.0 / c[11] * (Jac - 1.) ** 5

        # Second Derivatives
        ddu = zeros(6)
        ddu[0] += 2.0 * c[2] * ((IB1 - 3.) ** 0) * ((IB2 - 3.) ** 0)
        ddu[0] += 6.0 * c[5] * ((IB1 - 3.) ** 1) * ((IB2 - 3.) ** 0)
        ddu[0] += 2.0 * c[6] * ((IB1 - 3.) ** 0) * ((IB2 - 3.) ** 1)
        ddu[1] += 2.0 * c[4] * ((IB1 - 3.) ** 0) * ((IB2 - 3.) ** 0)
        ddu[1] += 2.0 * c[7] * ((IB1 - 3.) ** 1) * ((IB2 - 3.) ** 0)
        ddu[1] += 6.0 * c[8] * ((IB1 - 3.) ** 0) * ((IB2 - 3.) ** 1)
        ddu[3] += 1.0 * c[3] * ((IB1 - 3.) ** 0) * ((IB2 - 3.) ** 0)
        ddu[3] += 2.0 * c[6] * ((IB1 - 3.) ** 1) * ((IB2 - 3.) ** 0)
        ddu[3] += 2.0 * c[7] * ((IB1 - 3.) ** 0) * ((IB2 - 3.) ** 1)
        if c[9] > 0.: ddu[2] += 2.0 / c[9] * (Jac - 1.) ** 0
        if c[10] > 0.: ddu[2] += 12.0 / c[10] * (Jac - 1.) ** 2
        if c[11] > 0.: ddu[2] += 30.0 / c[11] * (Jac - 1.) ** 4

        # Third Derivatives
        dddu = zeros(6)
        if c[10] > 0.: dddu[5] += 24.0 / c[10] * (Jac - 1.) ** 1
        if c[11] > 0.: dddu[5] += 120.0 / c[11] * (Jac - 1.) ** 3
        return u, du, ddu, dddu

$$\begin{align}
\dot{Y} &= Y_1 \dot{\epsilon}^p_{eq} \\
&= Y_1\sqrt{\frac{2}{3}}\lVert{\rm dev}\left(\dot{\pmb{\epsilon}}^p\right)\rVert \\
&= Y_1\sqrt{\frac{2}{3}}\sqrt{\pmb{m}{:}\pmb{m}}\dot{\lambda} \\
&= Y_1\sqrt{\frac{2}{3}}\sqrt{\frac{1}{\sqrt{2J_2}}\frac{1}{\sqrt{2J_2}}\pmb{s}{:}\pmb{s}}\dot{\lambda}\\
&= Y_1\sqrt{\frac{2}{3}}\frac{1}{\sqrt{2J_2}}\sqrt{\pmb{s}{:}\pmb{s}}\dot{\lambda} \\
&= Y_1\frac{1}{\sqrt{3J_2}}\sqrt{\pmb{s}{:}\pmb{s}}\dot{\lambda} \\
&= Y_1\frac{1}{\sqrt{3/2}\sqrt{2J_2}}\sqrt{\pmb{s}{:}\pmb{s}}\dot{\lambda}\\
&= Y_1\sqrt{\frac{2}{3}}\dot{\lambda}\\
&= H_Y\dot{\lambda}
\end{align}$$

```python

```


```python
from tectosaur2.nb_config import setup

setup()
```

# Quasidynamic earthquake cycles: antiplane

## Summary
* Solving the SCEC SEAS problem BP1-QD for earthquake cycle simulation.
* Computing traction given fault slip by rearranging the discretized integral equation.
* Solving the quasidynamic rate-and-state friction equations with a protected Newton solver.
* Time-stepping over several earthquake ruptures with an adaptive time-stepping routine.
* Comparing against results from SCEC SEAS benchmark participants.

## Setting up the geometry.

Previously, we built the infrastructure for solving for displacement and stress everywhere in the domain given slip on a fault in an antiplane half space. This time, we'll go a step further and simulate rate and state friction and slip on that fault. We'll consider the quasidynamic approximation where we use a zero-th order approximation to the stress changes caused by fault slip. We'll be solving [SCEC SEAS problem BP1-QD](https://strike.scec.org/cvws/seas/download/SEAS_BP1_QD.pdf). Click the link!

For a bit more background on the ideas here, you could take a look at [my previous explanation of modeling a quasidynamic spring-block-slider](https://tbenthompson.com/post/block_slider/).

```python
import sympy as sp
import numpy as np
import matplotlib.pyplot as plt
from tectosaur2 import (
    gauss_rule,
    refine_surfaces,
    integrate_term,
    panelize_symbolic_surface,
)
from tectosaur2.laplace2d import double_layer, hypersingular

```

To start, we'll build a heavily refined fault mesh and surface mesh. The surface mesh will be quite long to make sure that we're capturing the half space assumption.

```python
surf_half_L = 10000000
fault_bottom = 40000
max_panel_length = 200
shear_modulus = 3.2e10

quad_rule = gauss_rule(5)
t = sp.var("t")

panel_bounds = np.stack((edges[:-1],edges[1:]), axis=1)

fault = panelize_symbolic_surface(
    t, t * 0, fault_bottom * (t + 1) * -0.5,
    quad_rule,
    n_panels=400
)

free = refine_surfaces(
    [
        (t, -t * surf_half_L, 0 * t)  # free surface
    ],
    (quad_rule),
    control_points = [
        # nearfield surface panels and fault panels will be limited to 200m
        # at 200m per panel, we have ~40m per solution node because the panels
        # have 5 nodes each
        (0, 0, 1.5 * fault_bottom, 200),
        # farfield panels will be limited to 200000 m per panel at most
        (0, 0, surf_half_L, 200000),
    ]
)
print(
    f"The free surface mesh has {free.n_panels} panels with a total of {free.n_pts} points."
)
print(
    f"The fault mesh has {fault.n_panels} panels with a total of {fault.n_pts} points."
)
```

    The free surface mesh has 958 panels with a total of 4790 points.
    The fault mesh has 400 panels with a total of 2000 points.

```python
plt.plot(free.pts[:,0]/1000, free.pts[:,1]/1000, 'k-')
plt.plot(fault.pts[:,0]/1000, fault.pts[:,1]/1000, 'r-')
plt.xlabel(r'$x ~ \mathrm{(km)}$')
plt.ylabel(r'$y ~ \mathrm{(km)}$')
plt.axis('scaled')
plt.xlim([-100, 100])
plt.ylim([-80, 20])
plt.show()
```

And, to start off the integration, we'll construct the operators necessary for solving for free surface displacement from fault slip.

```python
singularities = np.array(
    [
        [-surf_half_L, 0],
        [surf_half_L, 0],
        [0, 0],
        [0, -fault_bottom],
    ]
)

(free_disp_to_free_disp, fault_slip_to_free_disp), report = integrate_term(
    double_layer, free.pts, free, fault, singularities=singularities, return_report=True
)
fault_slip_to_free_disp = fault_slip_to_free_disp[:, 0, :, 0]
free_disp_solve_mat = (
    np.eye(free_disp_to_free_disp.shape[0]) + free_disp_to_free_disp[:, 0, :, 0]
)
```

## Solving for fault traction given fault slip

The previous section should have been familiar, but now we need to go a step further. The goal is to construct a single matrix that computes the fault tractions given an input fault slip distribution. This matrix will be used during each time step of our fault simulation.

But because we're using fullspace Green's function, this matrix needs to include both the direct fault slip effect and the indirect effect that is mediated through the free surface. So, we're actually going to be composing several matrices together to get our final `total_fault_slip_to_fault_stress` matrix:
* Combining `fault_slip_to_free_disp` and `free_disp_solve_mat`, we can compute the surface displacement given a fault slip distribution.
* We will construct `free_disp_to_fault_stress` for computing the fault stress caused by a surface displacement field.
* We also construct the direct fault slip effect matrix, `fault_slip_to_fault_stress`.

Renaming these wordy matrix names to some simple letters:
```
A = fault_slip_to_fault_traction
B = free_disp_to_fault_traction
C = fault_slip_to_free_disp
D = free_disp_solve_mat
```
we can write the full system:

\begin{bmatrix}
   A & B \\
   C & D
\end{bmatrix} \begin{bmatrix} s \\ u \end{bmatrix} = \begin{bmatrix} t \\ 0 \end{bmatrix}

The second equation expands to the same linear system that we used before to solve for surface displacement: $u = -D^{-1}Cs$. Plugging that in, we get:

\begin{equation}
t = As - BD^{-1}Cs = (A - BD^{-1}C)s
\end{equation}

Thus, our goal matrix operator is $A - BD^{-1}C$ and we will name this `total_fault_slip_to_fault_traction`. You might recognize this as the [Schur complement](https://en.wikipedia.org/wiki/Schur_complement) of the matrix system above. We performed one of step of block matrix Gaussian elimination!

```python
(free_disp_to_fault_stress, fault_slip_to_fault_stress), report = integrate_term(
    hypersingular,
    fault.pts,
    free,
    fault,
    tol=1e-12,
    singularities=singularities,
    return_report=True,
)
fault_slip_to_fault_stress *= shear_modulus
free_disp_to_fault_stress *= shear_modulus

fault_slip_to_fault_traction = np.sum(
    fault_slip_to_fault_stress[:,:,:,0] * fault.normals[:, :, None], axis=1
)
free_disp_to_fault_traction = np.sum(
    free_disp_to_fault_stress[:,:,:,0] * fault.normals[:, :, None], axis=1
)
```

    /Users/tbent/Dropbox/active/eq/tectosaur2/tectosaur2/integrate.py:201: UserWarning: Some integrals failed to converge during adaptive integration. This an indication of a problem in either the integration or the problem formulation.
      warnings.warn(
    /Users/tbent/Dropbox/active/eq/tectosaur2/tectosaur2/integrate.py:208: UserWarning: Some expanded integrals reached maximum expansion order. These integrals may be inaccurate.
      warnings.warn(

```python
A = -fault_slip_to_fault_traction
B = -free_disp_to_fault_traction
C = fault_slip_to_free_disp
Dinv = np.linalg.inv(free_disp_solve_mat)

total_fault_slip_to_fault_traction = A - B.dot(Dinv.dot(C))
```

I'll just do a quick check to confirm that this operator is accurate:

```python
analytical_sd = -np.arctan(-fault_bottom / free.pts[:,0]) / np.pi
numerical_sd = Dinv.dot(-C).sum(axis=1)
rp = (fault.pts[:,1] + 1) ** 2
analytical_ft = -(shear_modulus / (2 * np.pi)) * ((1.0 / (fault.pts[:,1] + fault_bottom)) - (1.0 / (fault.pts[:,1] - fault_bottom)))
numerical_ft = total_fault_slip_to_fault_traction.sum(axis=1)
plt.figure(figsize=(10,5))
plt.subplot(1,2,1)
plt.title('Stress error')
plt.plot(fault.pts[:,1], np.log10(np.abs((numerical_ft - analytical_ft)/(analytical_ft))), 'b-')
plt.xlim([-fault_bottom, 0])
plt.xlabel('$\mathrm{depth ~~ (m)}$')
plt.ylabel('$\log_{10}(|E_{\sigma}|)$')
plt.subplot(1,2,2)
plt.title('Displacement error')
plt.plot(free.pts[:,0], np.log10(np.abs(numerical_sd - analytical_sd)), 'b-')
plt.xlim([-100000, 100000])
plt.xlabel('$x ~~ \mathrm{(m)}$')
plt.ylabel('$\log_{10}(|E_u|)$')
plt.show()
```

## Rate and state friction

Okay, now that we've constructed the necessary boundary integral operators, we get to move on to describing the frictional behavior on the fault.

This isn't intended to be an introducion to earthquake physics or rate-and-state friction. Instead, we're focusing on the numerical implementation. If you'd like to learn more about rate-and-state friction, I'd recommend... (SUGGEST A BOOK OR PAPER HERE)

```python
siay = 31556952 # seconds in a year
density = 2670  # rock density (kg/m^3)
cs = np.sqrt(shear_modulus / density)  # Shear wave speed (m/s)
eta = shear_modulus / (2 * cs)  # The radiation damping coefficient (kg / (m^2 * s))
Vp = 1e-9  # Rate of plate motion
sigma_n = 50e6  # Normal stress (Pa)

# parameters describing "a", the coefficient of the direct velocity strengthening effect
a0 = 0.01
amax = 0.025
fy = fault.pts[:, 1]
H = 15000
h = 3000
fp_a = np.where(
    fy > -H, a0, np.where(fy > -(H + h), a0 + (amax - a0) * (fy + H) / -h, amax)
)

# state-based velocity weakening effect
fp_b = 0.015

# state evolution length scale (m)
fp_Dc = 0.008

# baseline coefficient of friction
fp_f0 = 0.6

# if V = V0 and at steady state, then f = f0, units are m/s
fp_V0 = 1e-6
```

```python
plt.figure(figsize=(3, 5))
plt.plot(fp_a, fy/1000, label='a')
plt.plot(np.full(fy.shape[0], fp_b), fy/1000, label='b')
plt.xlim([0, 0.03])
plt.ylabel('depth')
plt.legend()
plt.show()
```

First, a Python implementation of the quasidynamic shear stress equations. There are two components to a standard definition of rate and state friction:

\begin{align}
\tau_{qd} = F(V, \theta)\\
\frac{d\theta}{dt} = G(V, \theta)
\end{align}

where $F(V, \theta)$ is the strength of friction, depending on slip rate and state and $G(V, \theta)$ gives the state evolution. For $G$, we will use the aging law, given by the `aging_law` function below.

We also need to relate the quasidynamic shear stress to the quasistatic shear stress:
\begin{equation}
\tau_{qd} = \tau_{qs} - \eta * V
\end{equation}
which results in the full quasidynamic stress balance equation:
\begin{equation}
\tau_{qs} - \eta * V - F(V, \theta) = 0
\end{equation}
The specific form we use here is  implemented below in the `qd_equation` function.

At each time step, we'll need to compute the velocity, $V$ and the state derivative $d\theta/dt$. Computing the state derivative is easy because it's directly given by the aging law. But, computing the velocity is harder because its only given implicitly in the frictional strength equation. The `solve_friction` function below implements Newton's method with a backtracking line search to solve the friction equation for the current velocity. The `qd_equation_dV` function implements the derivative of the stress balance with respect to velocity. This derivative is necessary for Newton's method.

```python
# The regularized form of the aging law.
def aging_law(fp_a, V, state):
    return (fp_b * fp_V0 / fp_Dc) * (np.exp((fp_f0 - state) / fp_b) - (V / fp_V0))

def qd_equation(fp_a, shear_stress, V, state):
    # The regularized rate and state friction equation
    F = sigma_n * fp_a * np.arcsinh(V / (2 * fp_V0) * np.exp(state / fp_a))

    # The full shear stress balance:
    return shear_stress - eta * V - F

def qd_equation_dV(fp_a, V, state):
    # First calculate the derivative of the friction law with respect to velocity
    # This is helpful for equation solving using Newton's method
    expsa = np.exp(state / fp_a)
    Q = (V * expsa) / (2 * fp_V0)
    dFdV = fp_a * expsa * sigma_n / (2 * fp_V0 * np.sqrt(1 + Q * Q))

    # The derivative of the full shear stress balance.
    return -eta - dFdV

def solve_friction(fp_a, shear, V_old, state, tol=1e-10):
    V = V_old
    max_iter = 150
    for i in range(max_iter):
        # Newton's method step!
        f = qd_equation(fp_a, shear, V, state)
        dfdv = qd_equation_dV(fp_a, V, state)
        step = f / dfdv

        # We know that slip velocity should not be negative so any step that
        # would lead to negative slip velocities is "bad". In those cases, we
        # cut the step size in half iteratively until the step is no longer
        # bad. This is a backtracking line search.
        while True:
            bad = step > V
            if np.any(bad):
                step[bad] *= 0.5
            else:
                break

        # Take the step and check if everything has converged.
        Vn = V - step
        if np.max(np.abs(step) / Vn) < tol:
            break
        V = Vn
        if i == max_iter - 1:
            raise Exception("Failed to converge.")

    # Return the solution and the number of iterations required.
    return Vn, i
```

We'll also check $h^*$ which is the minimum length scale of an instability and $L_b$, the length scale of the rupture process zone. Both these length scales need to be well resolved by the fault discretization. Here we have approximately eight point within the process zone and almost 80 points within $h^*$.

```python
mesh_L = np.max(np.abs(np.diff(fault.pts[:, 1])))
Lb = shear_modulus * fp_Dc / (sigma_n * fp_b)
hstar = (np.pi * shear_modulus * fp_Dc) / (sigma_n * (fp_b - fp_a))
mesh_L, Lb, np.min(hstar[hstar > 0])
```

    (26.92346550528964, 341.3333333333333, 3216.990877275949)

## Quasidynamic earthquake cycle derivatives

Let's set of the last few pieces to do a full earthquake cycle simulation:
- initial conditions.
- a function to put the pieces together and calculate the full system state at each time step, including the time derivatives of slip and frictional state.
- the time stepping algorithm itself.

First, **initial conditions**. This initial state is exactly as specified in the BP-1 description linked at the top:
1. We solve for the steady frictional state at each point using `scipy.optimize.fsolve`. This is the initial state.
2. We identify the value of shear stress that will result in steady plate rate slip rates in the deeper portions of the fault: `tau_amax`. This is the initial shear stress.
3. The initial slip deficit is zero.

```python
from scipy.optimize import fsolve
import copy

init_state_scalar = fsolve(lambda S: aging_law(fp_a, Vp, S), 0.7)[0]
init_state = np.full(fault.n_pts, init_state_scalar)
tau_amax = -qd_equation(amax, 0, Vp, init_state_scalar)

init_traction = np.full(fault.n_pts, tau_amax)
init_slip_deficit = np.zeros(fault.n_pts)
init_conditions = np.concatenate((init_slip_deficit, init_state))
```

Next, solving for system state. This ties the pieces together by:
1. Solving for the quasistatic shear stress using the boundary integral matrices derived at the beginning of this notebook.
2. Solving the rate and state friction equations for the slip rate.
3. Calculating the state evolution using the aging law.

The middle lines of `SystemState.calc` do these three steps. There's a bunch of other code surrounding those three lines in order to deal with invalid inputs and transform from slip to slip deficit.

```python
class SystemState:

    V_old = np.full(fault.n_pts, Vp)
    state = None

    def calc(self, t, y, verbose=False):
        # Separate the slip_deficit and state sub components of the
        # time integration state.
        slip_deficit = y[: init_slip_deficit.shape[0]]
        state = y[init_slip_deficit.shape[0] :]

        # If the state values are bad, then the adaptive integrator probably
        # took a bad step.
        if np.any((state < 0) | (state > 2.0)):
            return False

        # The big three lines solving for quasistatic shear stress, slip rate
        # and state evolution
        tau_qs = init_traction - total_fault_slip_to_fault_traction.dot(slip_deficit)
        V = solve_friction(fp_a, tau_qs, self.V_old, state)[0]
        dstatedt = aging_law(fp_a, V, state)
        if not np.all(np.isfinite(V)):
            return False
        self.V_old = V

        slip_deficit_rate = Vp - V
        out = slip_deficit, state, tau_qs, V, slip_deficit_rate, dstatedt
        self.state = out
        return self.state

def plot_system_state(t, SS, xlim=None):
    """This is just a helper function that creates some rough plots of the
    current state to help with debugging"""
    slip_deficit, state, tau_qs, V, slip_deficit_rate, dstatedt = SS

    slip = Vp * t - slip_deficit

    fd = -np.linalg.norm(fault.pts, axis=1)

    plt.figure(figsize=(15, 9))
    plt.suptitle(f't={t/siay}')
    plt.subplot(3, 3, 1)
    plt.title("slip")
    plt.plot(fd, slip)
    plt.xlim(xlim)

    plt.subplot(3, 3, 2)
    plt.title("slip deficit")
    plt.plot(fd, slip_deficit)
    plt.xlim(xlim)

    # plt.subplot(3, 3, 2)
    # plt.title("slip deficit rate")
    # plt.plot(fd, slip_deficit_rate)
    # plt.xlim(xlim)

    # plt.subplot(3, 3, 2)
    # plt.title("strength")
    # plt.plot(fd, tau_qs/sigma_qs)
    # plt.xlim(xlim)

    plt.subplot(3, 3, 3)
    plt.title("V")
    plt.plot(fd, V)
    plt.xlim(xlim)

    plt.subplot(3, 3, 5)
    plt.title(r"$\tau_{qs}$")
    plt.plot(fd, tau_qs)
    plt.xlim(xlim)

    plt.subplot(3, 3, 6)
    plt.title("state")
    plt.plot(fd, state)
    plt.xlim(xlim)

    plt.subplot(3, 3, 9)
    plt.title("dstatedt")
    plt.plot(fd, dstatedt)
    plt.xlim(xlim)
    plt.tight_layout()

    plt.show()

def calc_derivatives(state, t, y):
    """
    This helper function calculates the system state and then extracts the
    relevant derivatives that the integrator needs. It also intentionally
    returns infinite derivatives when the `y` vector provided by the integrator
    is invalid.
    """
    if not np.all(np.isfinite(y)):
        return np.inf * y
    state = state.calc(t, y)
    if not state:
        return np.inf * y
    derivatives = np.concatenate((state[-2], state[-1]))
    return derivatives
```

## Integrating through time

```python
%%time
from scipy.integrate import RK45

# We use a 5th order adaptive Runge Kutta method and pass the derivative function to it
# the relative tolerance will be 1e-11 to make sure that even
state = SystemState()
derivs = lambda t, y: calc_derivatives(state, t, y)
integrator = RK45
atol = Vp * 1e-6
rtol = 1e-11
rk = integrator(derivs, 0, init_conditions, 1e50, atol=atol, rtol=rtol)

# Set the initial time step to one day.
rk.h_abs = 60 * 60 * 24

# Integrate for 1000 years.
max_T = 1000 * siay

n_steps = 1000000
t_history = [0]
y_history = [init_conditions.copy()]
for i in range(n_steps):

    # Take a time step and store the result
    if rk.step() != None:
        raise Exception("TIME STEPPING FAILED")
        break
    t_history.append(rk.t)
    y_history.append(rk.y.copy())

    # Print the time every 5000 steps
    if i % 5000 == 0:
        print(f"step={i}, time={rk.t / siay} yrs, step={(rk.t - t_history[-2]) / siay}")
    if rk.t > max_T:
        break

y_history = np.array(y_history)
t_history = np.array(t_history)
```

    step=0, time=0.0021812680462764775 yrs, step=0.0021812680462764775
    step=5000, time=140.7230427507717 yrs, step=0.02945013453871661
    step=10000, time=196.52497632980683 yrs, step=6.699937178573698e-11
    step=15000, time=196.5249766930108 yrs, step=7.13511577745264e-11
    step=20000, time=196.52498488866365 yrs, step=7.762718868876156e-08
    step=25000, time=272.17487700592767 yrs, step=1.3297123854634314e-10
    step=30000, time=272.1748773997298 yrs, step=7.935965282334024e-11
    step=35000, time=272.1748778087133 yrs, step=4.735589336411811e-11
    step=40000, time=292.5353440506115 yrs, step=0.025529773554574715
    step=45000, time=350.0284817740895 yrs, step=7.585404744348211e-11
    step=50000, time=350.0284822570143 yrs, step=1.143552651498551e-10
    step=55000, time=350.0284828072335 yrs, step=1.5738959326121706e-09
    step=60000, time=428.3603641171298 yrs, step=1.396734966774442e-06
    step=65000, time=428.3603895166313 yrs, step=8.38323217562627e-11
    step=70000, time=428.36038998662326 yrs, step=6.908460257203191e-11
    step=75000, time=432.77635516149826 yrs, step=0.023396389027961796
    step=80000, time=506.74075025565014 yrs, step=6.93868099323645e-11
    step=85000, time=506.74075072729175 yrs, step=1.3369653621114137e-10
    step=90000, time=506.7407511239481 yrs, step=1.6760420204045886e-10
    step=95000, time=571.1772883200007 yrs, step=0.03115928106939193
    step=100000, time=585.1249321168854 yrs, step=8.195863612220059e-11
    step=105000, time=585.1249326058276 yrs, step=7.277153236808961e-11
    step=110000, time=585.1651243047899 yrs, step=0.00020757875702647396
    step=115000, time=663.509382683838 yrs, step=7.482654241835127e-11
    step=120000, time=663.5093831323696 yrs, step=1.1858616819451146e-10
    step=125000, time=663.5093835144739 yrs, step=7.228800059155745e-11
    step=130000, time=708.7786006395438 yrs, step=0.0306217858381618
    step=135000, time=741.8938514583174 yrs, step=9.815695063602784e-11
    step=140000, time=741.8938519540883 yrs, step=8.812366627298559e-11
    step=145000, time=741.8944130341275 yrs, step=4.4345806513532365e-06
    step=150000, time=820.2783209816516 yrs, step=6.962857582063059e-11
    step=155000, time=820.2783214059152 yrs, step=1.1133319154652912e-10
    step=160000, time=820.2783218379793 yrs, step=4.2550796334829805e-11
    step=165000, time=846.4938094052974 yrs, step=0.026173168524190365
    step=170000, time=898.6627909507023 yrs, step=7.45847765300852e-11
    step=175000, time=898.662791441536 yrs, step=9.126662282044461e-11
    step=180000, time=898.6627924229714 yrs, step=6.237439034320691e-09
    step=185000, time=977.047260512177 yrs, step=1.2691500304527786e-09
    step=190000, time=977.0472609600538 yrs, step=9.042044221151333e-11
    step=195000, time=977.0472614254056 yrs, step=6.068323795478569e-11
    step=200000, time=984.7989573452244 yrs, step=0.02885660199627129
    CPU times: user 1h 28min 3s, sys: 28min 22s, total: 1h 56min 26s
    Wall time: 1h 15min 56s

## Plotting the results

Now that we've solved for 1000 years of fault slip evolution, let's plot some of the results. I'll start with a super simple plot of the maximum log slip rate over time.

```python
derivs_history = np.diff(y_history, axis=0) / np.diff(t_history)[:, None]
max_vel = np.max(np.abs(derivs_history), axis=1)
plt.plot(t_history[1:] / siay, np.log10(max_vel))
plt.xlabel('$t ~~ \mathrm{(yrs)}$')
plt.ylabel('$\log_{10}(V)$')
plt.show()
```

And next, we'll make the classic plot showing the spatial distribution of slip over time:
- the blue lines show interseismic slip evolution and are plotted every fifteen years
- the red lines show evolution during rupture every three seconds.

```python
plt.figure(figsize=(10, 4))
last_plt_t = -1000
last_plt_slip = init_slip_deficit
event_times = []
for i in range(len(y_history) - 1):
    y = y_history[i]
    t = t_history[i]
    slip_deficit = y[: init_slip_deficit.shape[0]]
    should_plot = False

    # Plot a red line every three second if the slip rate is over 0.1 mm/s.
    if (
        max_vel[i] >= 0.0001 and t - last_plt_t > 3
    ):
        if len(event_times) == 0 or t - event_times[-1] > siay:
            event_times.append(t)
        should_plot = True
        color = "r"

    # Plot a blue line every fifteen years during the interseismic period
    if t - last_plt_t > 15 * siay:
        should_plot = True
        color = "b"

    if should_plot:
        # Convert from slip deficit to slip:
        slip = -slip_deficit + Vp * t
        plt.plot(slip, fy / 1000.0, color + "-", linewidth=0.5)
        last_plt_t = t
        last_plt_slip = slip
plt.xlim([0, np.max(last_plt_slip)])
plt.ylim([-40, 0])
plt.ylabel(r"$\textrm{z (km)}$")
plt.xlabel(r"$\textrm{slip (m)}$")
plt.tight_layout()
plt.savefig("halfspace.png", dpi=300)
plt.show()
```

And a plot of recurrence interval:

```python
plt.title("Recurrence interval")
plt.plot(np.diff(event_times) / siay, "k-*")
plt.xticks(np.arange(0, 10, 1))
plt.yticks(np.arange(75, 80, 0.5))
plt.xlabel("Event number")
plt.ylabel("Time between events (yr)")
plt.show()
```

## Comparison against SCEC SEAS results

Finally, we will compare against the results reported as part of the SEAS project. Here, I've grabbed the time series for a point at 7500m depth on the fault from results produced by Junle Jiang.

To compare, I grab the fault history for just the first event and then interpolate the values to a depth of exactly 7500m.

```python
jiang_data = np.loadtxt("jiang7500.txt")
jiang_slip_rate = 10 ** jiang_data[:, 2]
jiang_stress = jiang_data[:, 3]
```

```python
t_start_idx = np.argmax(max_vel > 1e-4)
t_end_idx = np.argmax(max_vel[t_start_idx:] < 1e-6)
n_steps = t_end_idx - t_start_idx
t_chunk = t_history[t_start_idx : t_end_idx]
```

```python
shear_chunk = []
slip_rate_chunk = []
for i in range(n_steps):
    system_state = SystemState().calc(t_history[t_start_idx + i], y_history[t_start_idx + i])
    slip_deficit, state, tau_qs, V, slip_deficit_rate, dstatedt = system_state
    shear_chunk.append((tau_qs - eta * V))
    slip_rate_chunk.append(V)
shear_chunk = np.array(shear_chunk)
slip_rate_chunk = np.array(slip_rate_chunk)
```

```python
fault_idx = np.argmax((-7480 > fy) & (fy > -7500))
VAvg = np.mean(slip_rate_chunk[:, fault_idx:(fault_idx+2)], axis=1)
SAvg = np.mean(shear_chunk[:, fault_idx:(fault_idx+2)], axis=1)
```

```python
t_align = t_chunk[np.argmax(VAvg > 0.1)]
jiang_t_align = np.argmax(jiang_slip_rate > 0.1)

for lims in [(-1, 1), (-15, 30)]:
    plt.figure(figsize=(12, 8))
    plt.subplot(2, 1, 1)
    plt.plot(t_chunk - t_align, SAvg / 1e6, "k-o", markersize=0.5, linewidth=0.5, label='here')
    plt.plot(
        jiang_data[:, 0] - jiang_data[jiang_t_align, 0],
        jiang_stress,
        "b-*",
        markersize=0.5,
        linewidth=0.5,
        label='Jiang'
    )
    plt.legend()
    plt.xlim(lims)
    plt.xlabel("Time (s)")
    plt.ylabel("Shear Stress (MPa)")
    # plt.show()

    plt.subplot(2, 1, 2)
    plt.plot(t_chunk - t_align, VAvg, "k-o", markersize=0.5, linewidth=0.5, label='here')
    plt.plot(
        jiang_data[:, 0] - jiang_data[jiang_t_align, 0],
        jiang_slip_rate[:],
        "b-*",
        markersize=0.5,
        linewidth=0.5,
        label='Jiang'
    )
    plt.legend()
    plt.xlim(lims)
    plt.xlabel("Time (s)")
    plt.ylabel("Slip rate (m/s)")
    plt.tight_layout()
    plt.show()
```

Why don't the solutions match more precisely? I think the most likely answer is that the Jiang result is using a 3D model that is very long in the $z$ direction but not truly infinite like the antiplane model here.


 ILI285 - Computación Científica I  / INF285 - Computación Científica

 Polynomial Interpolation: Vandermonde, Lagrange, Newton, Chebyshev

  [S]cientific [C]omputing [T]eam

 Version: 1.27

## Table of Contents
* [Introduction](#intro)
* [Vandermonde Matrix](#vander)
* [Lagrange Interpolation](#lagrange)
* [Runge Phenomenon](#runge)
* [Newton's Divided Difference](#DDN)
* [Interpolation Error](#Error)
* [Chebyshev Interpolation](#cheby)
* [Python Modules and Functions](#py)
* [Acknowledgements](#acknowledgements)

```python
import numpy as np
import matplotlib.pyplot as plt
import sympy as sp
from functools import reduce
import matplotlib as mpl
mpl.rcParams['font.size'] = 14
mpl.rcParams['axes.labelsize'] = 20
mpl.rcParams['xtick.labelsize'] = 14
mpl.rcParams['ytick.labelsize'] = 14
%matplotlib inline
from ipywidgets import interact, fixed, IntSlider
```

## Introduction

Hello! In this notebook we will learn how to interpolate 1D data with polynomials. A polynomial interpolation consists in finding a polynomial that fits a discrete set of known data points, allowing us to construct new data points within the range of the data. Formally, a polynomial $P(x)$ interpolate the data $(x_1,y_1),...,(x_n,y_n)$ if $P(x_i)=y_i$ for all $i$ in $1,...,n$.

```python
def Y(D, xi):
    # Function that evaluates the xi's points in the polynomial
    if D['M']=='Vandermonde':
        P = lambda i: i**np.arange(len(D['P']))
    elif D['M']=='Lagrange':
        P = lambda i: [np.prod(i - np.delete(D['x'],j)) for j in range(len(D['x']))]
    elif D['M']=='Newton':
        P = lambda i: np.append([1],[np.prod(i-D['x'][:j]) for j in range(1,len(D['P']))])
    return [np.dot(D['P'], P(i)) for i in xi]

def Interpolation_Plot(D,ylim=None):
    # Function that shows the data points and the function that interpolates them.
    xi = np.linspace(min(D['x']),max(D['x']),1000)
    yi = Y(D,xi)
    plt.figure(figsize=(8,8))
    plt.plot(D['x'],D['y'],'ro',label='Interpolation points')
    plt.plot(xi,yi,'b-',label='$P(x)$')
    plt.xlim(min(xi)-0.5, max(xi)+0.5)
    if ylim:
        plt.ylim(ylim[0], ylim[1])
    else:
        plt.ylim(min(yi)-0.5, max(yi)+0.5)
    plt.grid(True)
    plt.legend(loc='best')
    plt.xlabel('$x$')
    #plt.ylabel('$P(x)$')
    plt.show()
```

## Vandermonde Matrix

First, we are going to learn the Vandermonde Matrix method. This is a $m \times m$ matrix (with $m$ being the length of the set of known data points) with the terms of a geometric progression in each row. It allows us to construct a system of linear equations with the objective of find the coefficients of the polynomial function that interpolates our data.

Example:

Given the set of known data points: $(x_1,y_1),(x_2,y_2),(x_3,y_3)$

Our system of linear equations will be:

$$ \begin{bmatrix}
    1 & x_1 & x_1^2  \\[0.3em]
    1 & x_2 & x_2^2  \\[0.3em]
    1 & x_3 & x_3^2  \end{bmatrix}
    \begin{bmatrix}
    a_1  \\[0.3em]
    a_2  \\[0.3em]
    a_3  \end{bmatrix} =
    \begin{bmatrix}
    y_1  \\[0.3em]
    y_2  \\[0.3em]
    y_3  \end{bmatrix}$$

And solving it we will find the coefficients $a_1,a_2,a_3$ that we need to construct the polynomial $P(x)=a_1+a_2x+a_3x^2$ that interpolates our data.

```python
def Vandermonde(x, y, show=False):
    # We construct the matrix and solve the system of linear equations
    A = np.array([xi**np.arange(len(x)) for xi in x])
    b = y
    xsol = np.linalg.solve(A,b)

    # The function shows the data if the flag is true
    if show:
        print('Data Points: '); print([(x[i],y[i]) for i in range(len(x))])
        print('A = '); print(np.array_str(A, precision=2, suppress_small=True))
        print("cond(A) = "+str(np.linalg.cond(A)))
        print('b = '); print(np.array_str(b, precision=2, suppress_small=True))
        print('x = '); print(np.array_str(xsol, precision=2, suppress_small=True))
        xS = sp.Symbol('x')
        F = np.dot(xS**np.arange(len(x)),xsol)
        print('Interpolation Function: ')
        print('F(x) = ')
        print(F)

    # Finally, we return a data structure with our interpolating polynomial
    D = {'M':'Vandermonde',
         'P':xsol,
         'x':x,
         'y':y}
    return D
```

```python
def show_time_V(epsilon=0):
    x = np.array([1.0,2.0,3.0+epsilon,5.0,6.5])
    y = np.array([2.0,5.0,4.0,6.0,2.0])
    D = Vandermonde(x,y,True)
    Interpolation_Plot(D,[-4,10])
interact(show_time_V,epsilon=(-1,2,0.1))
```

    interactive(children=(FloatSlider(value=0.0, description='epsilon', max=2.0, min=-1.0), Output()), _dom_classe…

    <function __main__.show_time_V(epsilon=0)>

## Lagrange Interpolation

With this method, we can interpolate data thanks to the Lagrange basis polynomials. Given a set of $n$ data points $(x_1,y_1),...,(x_n,y_n)$, the Lagrange interpolation polynomial is the following:

$$ P(x) = \sum^n_{i=1} y_i\,L_i(x),$$

where $L_i(x)$ are the Lagrange basis polynomials:

$$ L_i(x) = \prod^n_{j=1,j \neq i} \frac{x-x_j}{x_i-x_j} = \frac{x-x_1}{x_i-x_1} \cdot ... \cdot \frac{x-x_{i-1}}{x_i-x_{i-1}} \cdot \frac{x-x_{i+1}}{x_i-x_{i+1}} \cdot ... \cdot \frac{x-x_n}{x_i-x_n}$$

or simply $L_i(x)=\dfrac{l_i(x)}{l_i(x_i)}$, where $l_i(x)=\displaystyle{\prod^n_{j=1,j \neq i} (x-x_j)}$.

The most important property of these basis polynomials is:

$$ L_{j \neq i}(x_i) = 0 $$

$$ L_i(x_i) = 1 $$

So, we assure that $L(x_i) = y_i$, which indeed interpolates the data.

```python
def Lagrange(x, y, show=False):
    # We calculate the li's
    p = np.array([y[i]/np.prod(x[i] - np.delete(x,i)) for i in range(len(x))])

    # The function shows the data if the flag is true
    if show:
        print('Data Points: '); print([(x[i],y[i]) for i in range(len(x))])
        xS = sp.Symbol('x')
        L = np.dot(np.array([np.prod(xS - np.delete(x,i))/np.prod(x[i] - np.delete(x,i)) for i in range(len(x))]),y)
        print('Interpolation Function: ');
        print(L)

    # Finally, we return a data structure with our interpolating polynomial
    D = {'M':'Lagrange',
         'P':p,
         'x':x,
         'y':y}
    return D
```

```python
def show_time_L(epsilon=0):
    x = np.array([1.0,2.0,3.0+epsilon,4.0,5.0,7.0,6.0])
    y = np.array([2.0,5.0,4.0,6.0,7.0,3.0,8.0])
    D = Lagrange(x,y,True)
    Interpolation_Plot(D,[0,10])
interact(show_time_L,epsilon=(-1,1,0.1))
```

    interactive(children=(FloatSlider(value=0.0, description='epsilon', max=1.0, min=-1.0), Output()), _dom_classe…

    <function __main__.show_time_L(epsilon=0)>

```python
def show_time_Li(i=0, N=7):
    x = np.arange(N+1)
    y = np.zeros(N+1)
    y[i]=1
    D = Lagrange(x,y,True)
    Interpolation_Plot(D,[-1,2])

i_widget = IntSlider(min=0, max=7, step=1, value=0)
N_widget = IntSlider(min=5, max=20, step=1, value=7)
def update_i_range(*args):
    i_widget.max = N_widget.value
N_widget.observe(update_i_range, 'value')

interact(show_time_Li,i=i_widget,N=N_widget)
```

    interactive(children=(IntSlider(value=0, description='i', max=7), IntSlider(value=7, description='N', max=20, …

    <function __main__.show_time_Li(i=0, N=7)>

Here you get some questions about Lagrange Interpolation:
- Explain what happens with the interpolator polynomial when you add a new point to the set of points to interpolate. **Answer: We need to recalculate the polynomial**
- Why it is not a good idea to use Lagrange interpolation for a set of points which is constantly changing? **A: Because we need to compute the whole interpolation again**
- What is the operation count of obtaining the interpolator polynomial using Lagrange? What happens with the error?

## Newton's Divided Difference

In this interpolation method we will use divided differences to calculate the coefficients of our interpolation polynomial. Given a set of $n$ data points $(x_1,y_1),...,(x_n,y_n)$, the Newton polynomial is:

$$ P(x) = \sum^n_{i=1} (f[x_1 ... x_i] \cdot \prod^{i-1}_{j=1} (x-x_j)) ,$$

where $ \prod^{0}_{j=1} (x-x_j) = 0 $, and:

$$ f[x_i] = y_i $$

$$ f[x_j...x_i] = \frac{f[x_{j+1}...x_i]-f[x_j...x_{i-1}]}{x_i-x_j}$$

```python
def Divided_Differences(x, y):
    dd = np.array([y])
    for i in range(len(x)-1):
        ddi = []
        for a in range(len(x)-i-1):
            ddi.append((dd[i][a+1]-dd[i][a])/(x[a+i+1]-x[a]))
        ddi = np.append(ddi,np.full((len(x)-len(ddi),),0.0))
        dd = np.append(dd,[ddi],axis=0)
    return np.array(dd)

def Newton(x, y, show=False):
    # We calculate the divided differences and store them in a data structure
    dd = Divided_Differences(x,y)

    # The function shows the data if the flag is true
    if show:
        print('Data Points: '); print([(x[i],y[i]) for i in range(len(x))])
        xS = sp.Symbol('x')
        N = np.dot(dd[:,0],np.append([1],[np.prod(xS-x[:i]) for i in range(1,len(dd))]))
        print('Interpolation Function: ');
        print(N)

    # Finally, we return a data structure with our interpolating polynomial
    D = {'M':'Newton',
         'P':dd[:,0],
         'x':x,
         'y':y}
    return D
```

```python
def show_time_N(epsilon=0):
    x = np.array([0.0,2.0,3.0+epsilon,4.0,5.0,6.0])
    y = np.array([1.0,3.0,0.0,6.0,8.0,4.0])
    D = Newton(x,y,True)
    Interpolation_Plot(D)
interact(show_time_N,epsilon=(-1,1,0.1))
```

    interactive(children=(FloatSlider(value=0.0, description='epsilon', max=1.0, min=-1.0), Output()), _dom_classe…

    <function __main__.show_time_N(epsilon=0)>

Questions about Newton's DD:
- What is the main problem using this method (and Lagrange)? How can you fix it? **A: A problem with polynomial interpolation with equispaced date is the Runge phenomenon and can be handle with Chebyshev points**
- What to do when a new point is added? **A: Pro, is not necessary re-calculate the whole polynomial only a small piece**

## Polynomial Interpolation Error

The interpolation error is given by:

$$ f(x)-P(x) = \frac{(x-x_1) \cdot (x-x_2) \cdot ... \cdot (x-x_n)}{n!} \cdot f^{(n)}(c) ,$$

where $c$ is within the interval from the minimun value of $x$ and the maximum one.

```python
def Error(f, n, xmin, xmax, method=Lagrange, points=np.linspace, plot_flag=True):
    # This function plots f(x), the interpolating polynomial, and the associated error
    # points can be np.linspace to equidistant points or Chebyshev to get Chebyshev points
    x = points(xmin,xmax,n)
    y = f(x)

    xe = np.linspace(xmin,xmax,100)
    ye = f(xe)

    D = method(x,y)
    yi = Y(D, xe)

    if plot_flag:
        plt.figure(figsize=(5,10))
        f, (ax1, ax2) = plt.subplots(1, 2, figsize=(15,5), sharey = False)

        ax1.plot(xe, ye,'r-', label='f(x)')
        ax1.plot(x, y,'ro', label='Interpolation points')
        ax1.plot(xe, yi,'b-', label='Interpolation')
        ax1.set_xlim(xmin-0.5,xmax+0.5)
        ax1.set_ylim(min(yi)-0.5,max(yi)+0.5)
        ax1.set_title('Interpolation')
        ax1.grid(True)
        ax1.set_xlabel('$x$')
        ax1.legend(loc='best')

        ax2.semilogy(xe, abs(ye-yi),'b-', label='Absolute Error')
        ax2.set_xlim(xmin-0.5,xmax+0.5)
        ax2.set_title('Absolute Error')
        ax2.set_xlabel('$x$')
        ax2.grid(True)
        #ax2.legend(loc='best')
        plt.show()

    return max(abs(ye-yi))
```

```python
def test_error_Newton(n=5):
    #me = Error(lambda x: np.sin(x)**3, n, 1, 7, Newton)
    me = Error(lambda x: (1/(1+12*x**2)), n, -1, 1, Newton)
    print("Max Error:", me)
```

```python
interact(test_error_Newton,n=(5,25))
```

    interactive(children=(IntSlider(value=5, description='n', max=25, min=5), Output()), _dom_classes=('widget-int…

    <function __main__.test_error_Newton(n=5)>

##  **Runge's Phenomenon**: It is a problem of oscillation of polynomials at the edges of the interval.

We are interpolating a data that is 0 almost everywhere and 1 at the middle point, notice that when $n$ increases the oscilations increase and the red dots seems to be at 0 everywhere but it is just an artifact, there must be a 1 at the middle. The oscillations you see at the end of the interval is the Runge phenomenon.

```python
def Runge(n=9):
    x = np.linspace(0,1,n)
    y = np.zeros(n)
    y[int((n-1.0)/2.)]=1
    D = Newton(x,y,False)
    Interpolation_Plot(D)
interact(Runge,n=(5,25,2))
```

    interactive(children=(IntSlider(value=9, description='n', max=25, min=5, step=2), Output()), _dom_classes=('wi…

    <function __main__.Runge(n=9)>

## Chebyshev Interpolation

With the objective of reducing the error of the polynomial interpolation, we need to find the values of $x_1,x_2,...,x_n$ that minimize $(x-x_1) \cdot (x-x_2) \cdot ... \cdot (x-x_n)$.

To choose these values of $-1 \leq x_1,x_2,...,x_n \leq 1$ (to use another interval we just need to do a change of variables) that minimize the error, we will use the roots of the Chebyshev polynomials, also called **Chebyshev nodes** (of the first kind), which are defined by:

$$ x_i = \cos\left(\frac{(2i-1)\pi}{2n}\right), i = 1,...,n $$

```python
def Chebyshev(xmin,xmax,n=5):
    # This function calculates the n Chebyshev points and plots or returns them depending on ax
    ns = np.arange(1,n+1)
    x = np.cos((2*ns-1)*np.pi/(2*n))
    y = np.sin((2*ns-1)*np.pi/(2*n))
    plt.figure(figsize=(10,5))
    plt.ylim(-0.1,1.1)
    plt.xlim(-1.1,1.1)
    plt.plot(np.cos(np.linspace(0,np.pi)),np.sin(np.linspace(0,np.pi)),'k-')
    plt.plot([-2,2],[0,0],'k-')
    plt.plot([0,0],[-1,2],'k-')
    for i in range(len(y)):
        plt.plot([x[i],x[i]],[0,y[i]],'r-')
        plt.plot([0,x[i]],[0,y[i]],'r-')
    plt.plot(x,[0]*len(x),'bo',label='Chebyshev points')
    plt.plot(x,y,'ro')
    plt.xlabel('$x$')
    plt.title('n = '+str(n))
    plt.grid(True)
    plt.legend(loc='best')
    plt.show()
def Chebyshev_points(xmin,xmax,n):
    ns = np.arange(1,n+1)
    x = np.cos((2*ns-1)*np.pi/(2*n))
    #y = np.sin((2*ns-1)*np.pi/(2*n))
    return (xmin+xmax)/2 + (xmax-xmin)*x/2
def Chebyshev_points_histogram(n=50,nbins=20):
    xCheb=Chebyshev_points(-1,1,n)
    plt.figure()
    plt.hist(xCheb,bins=nbins,density=True)
    plt.grid(True)
    plt.show()
```

```python
interact(Chebyshev,xmin=fixed(-1),xmax=fixed(1),n=(2,50))
```

    interactive(children=(IntSlider(value=5, description='n', max=50, min=2), Output()), _dom_classes=('widget-int…

    <function __main__.Chebyshev(xmin, xmax, n=5)>

```python
interact(Chebyshev_points_histogram,n=(20,10000),nbins=(20,200))
```

    interactive(children=(IntSlider(value=50, description='n', max=10000, min=20), IntSlider(value=20, description…

    <function __main__.Chebyshev_points_histogram(n=50, nbins=20)>

By using these points, we reduce the numerator of the interpolation error formula:

$$ (x-x_1) \cdot (x-x_2) \cdot ... \cdot (x-x_n) = \dfrac{1}{2^{n-1}} \cdot T_n(x), $$

where $T(x) = \cos (n \cdot \arccos (x))$ is the n-th Chebyshev polynomial.

$$ T_0(x) = 1 $$
$$ T_1(x) = x $$
$$ T_2(x) = 2x^2 -1 $$
$$...$$
$$ T_{n+1}(x) = 2 \cdot x \cdot T_n(x) - T_{n-1}(x) $$

```python
def T(n,x):
    # Recursive function that returns the n-th Chebyshev polynomial evaluated at x
    if n == 0:
        return x**0
    elif n == 1:
        return x
    else:
        return 2*x*T(n-1,x)-T(n-2,x)

def Chebyshev_Polynomials(n=2, Flag_All_Tn=False):
    # This function plots the first n Chebyshev polynomials
    x = np.linspace(-1,1,1000)
    plt.figure(figsize=(10,5))
    plt.xlim(-1, 1)
    plt.ylim(-1.1, 1.1)
    if Flag_All_Tn:
        for i in np.arange(n+1):
            y = T(i,x)
            plt.plot(x,y,label='$T_{'+str(i)+'}(x)$')
    else:
        y = T(n,x)
        plt.plot(x,y,label='$T_{'+str(n)+'}(x)$')
    #    plt.title('$T_${:}$(x)$'.format(n))
    plt.legend(loc='right')
    plt.grid(True)
    plt.xlabel('$x$')
    plt.show()
```

```python
interact(Chebyshev_Polynomials,n=(0,12),Flag_All_Tn=True)
```

    interactive(children=(IntSlider(value=2, description='n', max=12), Checkbox(value=True, description='Flag_All_…

    <function __main__.Chebyshev_Polynomials(n=2, Flag_All_Tn=False)>

```python
n=9
xmin=1
xmax=9

mee = Error(lambda x: np.sin(x)**3, n, xmin, xmax, method=Lagrange)
mec = Error(lambda x: np.sin(x)**3, n, xmin, xmax, method=Lagrange, points=Chebyshev_points)

print("Max error (equidistants points):", mee)
print("Max error (Chebyshev nodes):", mec)
```

```python
def test_error_chebyshev(n=5):
    mee = Error(lambda x: (1/(1+12*x**2)), n, -1, 1, Lagrange)
    mec = Error(lambda x: (1/(1+12*x**2)), n, -1, 1, method=Lagrange, points=Chebyshev_points)
    print("Max error (equidistants points):", mee)
    print("Max error (Chebyshev nodes):", mec)
```

```python
interact(test_error_chebyshev,n=(5,100,2))
```

    interactive(children=(IntSlider(value=5, description='n', min=5, step=2), Output()), _dom_classes=('widget-int…

    <function __main__.test_error_chebyshev(n=5)>

Questions about Chebyshev:
- How can you calculate the Chebyshev points in the interval [a,b] instead of [-1,1]? **A: Using a change of variable**

## Convergence analysis

```python
n=50
shift=2

my_functions={0:lambda x: (x)**10,
              1:lambda x: np.abs((x)**3),
              2:lambda x: np.exp(-((x)**-2)),
              3:lambda x: 1/(1+x**2),
              4:lambda x: np.sin(x)**3}
labels = {0: "x^{10}",
          1: "|x^3|",
          2: "\exp(-x^{-2})",
          3: "1/(1+x^2)",
          4: "\sin^3(x)"}

n_points=np.arange(shift,n)

for k in np.arange(5):
    max_error=np.zeros(n-shift)
    max_error_es=np.zeros(n-shift)
    for i in n_points:
        max_error[i-shift] = Error(my_functions[k], i, -1, 1, Newton, Chebyshev_points, plot_flag=False)
        max_error_es[i-shift] = Error(my_functions[k], i, -1, 1, Newton, points=np.linspace, plot_flag=False)
    axis=plt.figure()
    plt.semilogy(n_points,max_error,'kd',label='Chebyshev points')
    plt.semilogy(n_points,max_error_es,'k.',label='Equalspaced poins')
    plt.ylim(10**-16,10**4)
    plt.grid(True)
    plt.title('Interpolation Error of $f(x)='+str(labels[k])+"$")
    plt.xlabel('Number of points used in the interpolation')
    plt.ylabel('Max error on domain')
    plt.legend(loc='best')
    plt.show()
```

## Python Modules and Functions

Interpolation:

http://docs.scipy.org/doc/numpy-1.10.0/reference/generated/numpy.polyfit.html

Vandermonde Matrix:

http://docs.scipy.org/doc/numpy-1.10.0/reference/generated/numpy.vander.html

Lagrange:

http://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.interpolate.lagrange.html

Chebyshev Points:

http://docs.scipy.org/doc/numpy-1.10.1/reference/generated/numpy.polynomial.chebyshev.chebroots.html#numpy.polynomial.chebyshev.chebroots

# Acknowledgements
* _Material created by professor Claudio Torres_ (`ctorres@inf.utfsm.cl`) _and assistants: Laura Bermeo, Alvaro Salinas, Axel Simonsen and Martín Villanueva. DI UTFSM. April 2016._
* _Material modified by Cristopher Arenas. May 2017._
* _Material modified by Claudio Torres. May 2017._
* _Bug fixed by Cristobal Carmona. April 2018._
* _Update June 2020 - v1.25 - C.Torres_ : Fixing formatting issues.
* _Update June 2020 - v1.26 - C.Torres_ : Adding "ylim" argumento to Interpolation_Plot(D,ylim=None) and addint "show_time_Li".
* _Update June 2020 - v1.27 - C.Torres_ : Adding comment that the Chebyshev nodes used are of the first kind and "Chebyshev_points_histogram".

```python

```