# Exercise 1: Make a function class

```python
class F:
    def __init__(self, a, w):
        self.a, self.w = a, w

    def __call__(self, x):
        from math import exp, sin
        return exp(-self.a * x) * sin(self.w * x)

f = F(a=1, w=0.1)
from math import pi
f(pi)
```

    0.01335383513703555

```python
f.a = 2
f(pi)
```

    0.0005770715401197441

# Exercise 2: Add a data attribute to a class

```python
class Account(object):
    def __init__(self, name, account_number, initial_amount):
        self.name = name
        self.no = account_number
        self.balance = initial_amount
        self.transactions = 0

    def deposit(self, amount):
        self.balance += amount
        self.transactions += 1

    def withdraw(self, amount):
        self.balance -= amount
        self.transactions += 1

    def dump(self):
        print("%s, %s, balance: %s, transactions: %d" % (self.name, self.no, self.balance, self.transactions))

    @staticmethod
    def test():
        a = Account("H.P.L.", "121344312", 1000)
        a.withdraw(3000)
        a.deposit(20)
        assert a.transactions == 2

Account.test()
```

# Exercise 3: Add functionality to a class

```python
from datetime import datetime as dt
import dateutil.parser as dup

class Account(object):
    def __init__(self, name, account_number, initial_amount):
        self._name = name
        self._no = account_number
        self._transactions = {str(dt.utcnow()): initial_amount}

    def deposit(self, amount):
        self._transactions[str(dt.utcnow())] = amount

    def withdraw(self, amount):
        self._transactions[str(dt.utcnow())] = -amount

    def get_balance(self):
        return sum(self._transactions.values())

    def print_transactions(self):
        for time, amount in self._transactions.items():
            print(f"%s: %6.f" % (dup.parse(time).ctime(), amount))

    def dump(self):
        print("%s, %s, balance: %s" % (self._name, self._no, self.get_balance()))
```

```python
import time
a = Account("Onno", "112312535", 20)
a.deposit(333)
time.sleep(1)
a.withdraw(34)
time.sleep(1)
a.deposit(90)
time.sleep(1)
a.withdraw(100)
time.sleep(1)
a.print_transactions()
a.dump()
```

    Sun Mar 25 18:15:30 2018:     20
    Sun Mar 25 18:15:30 2018:    333
    Sun Mar 25 18:15:31 2018:    -34
    Sun Mar 25 18:15:32 2018:     90
    Sun Mar 25 18:15:33 2018:   -100
    Onno, 112312535, balance: 309

```python
# time stuff
import time
s = time.time()                                           # seconds since epoch
print(s)
print(time.ctime())                                       # print current time
print(time.ctime(2*s))                                    # print time t
l = time.localtime(s)                                     # convert seconds to time_struct (local), s is optional
print(l)
print(time.gmtime(s))                                     # convert seconds to time_struct (adjust to gmt), s is optional
print(time.mktime(l))                                     # convert time_struct to seconde (local), s is optional
p = time.strftime("time: %a, %d %b %Y %H:%M:%S", l)       # formatted string from time_struct, l is optional
print(p)
print(time.strptime(p, "time: %a, %d %b %Y %H:%M:%S"))    # make time_struct from formatted string
print(time.strptime(time.ctime()))                        # ctime format is default format for strptime

from datetime import datetime as dt
print(dt.now())     # local datetime
d = dt.utcnow()     # utc datetime
print(d)
print(d.ctime())    # print datetime (strftime works as well)

import dateutil.parser as dup
d2 = dup.parse(str(d))    # parse datetime from iso string (datetime.strptime works as well)
print(d2.ctime())
print(d.tzinfo)
print(d2.tzinfo)
```

    1522001734.293254
    Sun Mar 25 20:15:34 2018
    Thu Jun 17 14:31:08 2066
    time.struct_time(tm_year=2018, tm_mon=3, tm_mday=25, tm_hour=20, tm_min=15, tm_sec=34, tm_wday=6, tm_yday=84, tm_isdst=1)
    time.struct_time(tm_year=2018, tm_mon=3, tm_mday=25, tm_hour=18, tm_min=15, tm_sec=34, tm_wday=6, tm_yday=84, tm_isdst=0)
    1522001734.0
    time: Sun, 25 Mar 2018 20:15:34
    time.struct_time(tm_year=2018, tm_mon=3, tm_mday=25, tm_hour=20, tm_min=15, tm_sec=34, tm_wday=6, tm_yday=84, tm_isdst=-1)
    time.struct_time(tm_year=2018, tm_mon=3, tm_mday=25, tm_hour=20, tm_min=15, tm_sec=34, tm_wday=6, tm_yday=84, tm_isdst=-1)
    2018-03-25 20:15:34.301804
    2018-03-25 18:15:34.301922
    Sun Mar 25 18:15:34 2018
    Sun Mar 25 18:15:34 2018
    None
    None

# Exercise 7.4: Make classes for a rectangle and a triangle

```python
class Rectangle:
    def __init__(self, width, height, corner):
        self.width, self.height, self.corner = width, height, corner

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2*self.width + 2*self.height

    @staticmethod
    def test():
        r = Rectangle(2, 3, (0, 0))
        import numpy as np
        assert np.allclose((r.area(), r.perimeter()), (6, 10))

Rectangle.test()
```

```python
class Triangle:
    def __init__(self, vertices):
        self.x = []; self.y = []
        for x, y in vertices:
            self.x.append(x)
            self.y.append(y)

    def area(self):
        x, y = self.x, self.y
        return 1/2 * sum(abs(x[i - 1]*y[i] - y[i - 1]*x[i]) for i in range(3))

    def perimeter(self):
        from math import sqrt
        x, y = self.x, self.y
        return sum(sqrt((x[i] - x[i - 1])**2 + (y[i] - y[i - 1])**2) for i in range(3))

    @staticmethod
    def test():
        import numpy as np
        t = Triangle([(0, 0), (1, 0), (0, 1)])
        assert np.allclose((t.area(), t.perimeter()), (0.5, 3.414213562373095))

Triangle.test()
```

# Exercise 7.5: Make a class for quadratic functions

```python
class Quadratic:
    def __init__(self, a, b, c):
        self.a, self.b, self.c = a, b, c

    def __call__(self, x):
        return (self.a * x**2) + self.b*x + self.c

    def table(self, l, r, n=10):
        from numpy import linspace
        print("          x |       f(x) ")
        print("------------+------------")
        for x in linspace(l, r, n):
            print(f" {x:10.6f} | {self(x):10.6f} ")

    def roots(self):
        from math import sqrt
        a, b, c = self.a, self.b, self.c
        d = -b / 2 / a; e = sqrt(b**2 - 4*a*c)
        return a + e, a - e

q = Quadratic(1, 2, -3)
q.table(0, 10)
print("\nroots:", q.roots())
```

              x |       f(x)
    ------------+------------
       0.000000 |  -3.000000
       1.111111 |   0.456790
       2.222222 |   6.382716
       3.333333 |  14.777778
       4.444444 |  25.641975
       5.555556 |  38.975309
       6.666667 |  54.777778
       7.777778 |  73.049383
       8.888889 |  93.790123
      10.000000 | 117.000000

    roots: (5.0, -3.0)

# Exercise 7.6: Make a class for straight lines

```python
class Line:
    def __init__(self, p1, p2):
        x0, y0 = p1; x1, y1 = p2
        a = (y1 - y0) / (x1 - x0)
        self.__class__.__call__ = lambda self, x: y0 + a*(x - x0)

    @staticmethod
    def test():
        import numpy as np
        line = Line((0, -1), (2, 4))
        assert np.allclose((line(0.5), line(0), line(1)), (0.25, -1, 1.5))

Line.test()
```

# Exercise 7.7: Flexible handling of function arguments

```python
class Line:
    def __init__(self, p1, p2):
        if all(isinstance(p, tuple) for p in (p1, p2)):
            x0, y0 = p1; x1, y1 = p2
        elif isinstance(p1, tuple):
            x0, y0 = p1
            x1, y1 = x0 + 1, y0 + p2
        elif isinstance(p2, tuple):
            x0, y0 = p2
            x1, y1 = x0 + 1, y0 + p1
        else:
            x0, y0 = 0, p1
            x1, y1 = x0 + 1, y0 + p2
        a = (y1 - y0) / (x1 - x0)
        self.__class__.__call__ = lambda self, x: y0 + a*(x - x0)

    @staticmethod
    def test():
        import numpy as np
        line = Line((0, -1), (2, 4))
        assert np.allclose((line(0.5), line(0), line(1)), (0.25, -1, 1.5))

        line = Line((0, -1), 5/2)
        assert np.allclose((line(0.5), line(0), line(1)), (0.25, -1, 1.5))

        line = Line(-1, 5/2)
        assert np.allclose((line(0.5), line(0), line(1)), (0.25, -1, 1.5))

Line.test()
```

# Exercise 7.8: Wrap functions in a class

```python
class LagrangeInterpolation:
    def __init__(self, xp, yp):
        self.xp, self.yp = xp, yp

    def __call__(self, x):
        x = np.asarray(x)
        k = arange(len(self.xp))
        return sum(self.yp[k] * self.L_k(x, k), axis=1)

    def L_k(self, x, k):
        xp = self.xp
        i = empty([len(k) - 1, len(k)], dtype=int)
        for k_ in k:
            i[:, k_] = np.delete(k, k_)
        g = x.reshape(-1, 1, 1) - np.resize(xp[i], (x.size, xp[i].shape[0], xp[i].shape[1]))
        return prod(g / (xp[k] - xp[i]), axis=1)

    def plot(self):
        import matplotlib.pyplot as plt
        xp = self.xp
        plt.figure()
        plt.scatter(xp, self.yp)
        x = linspace(xp[0], xp[-1], 1001)
        plt.plot(x, self(x))
        plt.show()
```

```python
import numpy as np
from numpy import arange, empty, prod, sum, linspace
xp = np.linspace(0, 3*np.pi, 15)
yp = np.sin(xp)
f = LagrangeInterpolation(xp, yp)
x = 1.2
print("p_L(%g)=%g" % (x, f(x)))
print("sin(%g)=%g" % (x, np.sin(x)))
f.plot()
```

# Exercise 7.9: Flexible handling of function arguments

```python
class LagrangeInterpolation:
    def __init__(self, f, x, n=10):
        self.xp = np.linspace(x[0], x[1], n)
        self.yp = f(self.xp)

    def __call__(self, x):
        x = np.asarray(x)
        k = arange(len(self.xp))
        return sum(self.yp[k] * self.L_k(x, k), axis=1)

    def L_k(self, x, k):
        xp = self.xp
        i = empty([len(k) - 1, len(k)], dtype=int)
        for k_ in k:
            i[:, k_] = np.delete(k, k_)
        g = x.reshape(-1, 1, 1) - np.resize(xp[i], (x.size, xp[i].shape[0], xp[i].shape[1]))
        return prod(g / (xp[k] - xp[i]), axis=1)

    def plot(self):
        import matplotlib.pyplot as plt
        xp = self.xp
        plt.figure()
        plt.scatter(xp, self.yp)
        x = linspace(xp[0], xp[-1], 1001)
        plt.plot(x, self(x))
        plt.show()
```

```python
from numpy import exp, sin, pi

def myfunction(x):
    return exp(-x/2.0)*sin(x)

f = LagrangeInterpolation(myfunction, x=[0, 2*pi], n=11)
f.plot()
```

# Exercise 7.10: Deduce a class implementation

```python
class Hello:
    def __call__(self, x):
        return f"Hello, {x}!"

    def __str__(self):
        return "Hello, World!"

a = Hello()
print(a("students"))
print(a)
```

    Hello, students!
    Hello, World!

# Exercise 7.11: Implement special methods in a classm

```python
class F:
    def __init__(self, a, w):
        self.a, self.w = a, w

    def __call__(self, x):
        from math import exp, sin
        return exp(-self.a * x) * sin(self.w * x)

    def __str__(self):
        return "exp(-self.a * x) * sin(self.w * x)"

f = F(a=1, w=0.1)
from math import pi
f(pi)
```

    0.01335383513703555

```python
f.a = 2
f(pi)
```

    0.0005770715401197441

```python
print(f)
```

    exp(-self.a * x) * sin(self.w * x)

# Exercise 7.12: Make a class for summation of series

```python
def test_Sum():
    term = lambda k, x: (-x)**k
    x = 0.5
    S = Sum(term, M=0, N=3)

    assert abs(S(x) - 5/8) < 1e-12
    assert abs(S.term(k=4, x=x) - term(k=4, x=x)) < 1e-12
```

```python
class Sum():
    def __init__(self, term, M, N):
        self.term, self.M, self.N = term, M, N

    def __call__(self, x):
        return sum(self.term(k, x) for k in range(self.M, self.N + 1))

test_Sum()
```

```python
import matplotlib.pyplot as plt
import numpy as np
from scipy.special import factorial
plt.figure()
x = np.linspace(-10, 10, 100)
y = Sum(lambda k, x: (-1)**k / factorial(2*k + 1) * x**(2*k + 1), 0, 10)(x)
plt.plot(x, y)
plt.show()
```

# Exercise 7.13: Apply a numerical differentitation class

```python
pwd
```

    '/Users/onnoeberhard/Desktop/Scientific Python'

```python
from package.Y import Y
from package.Derivative import Derivative
import matplotlib.pyplot as plt
import numpy as np

y = Y(20)
dy = Derivative(y.value)

t = np.linspace(0, y.v0/y.g)
plt.figure()
plt.plot(t, y.value(t))
plt.plot(t, dy(t))
plt.show()
```

# Exercise 7.14: Implement an addition operator

```python
class Rope:
    def __init__(self, knots):
        self.knots = knots

    def __add__(self, other):
        return Rope(self.knots + other.knots + 1)

    def __str__(self):
        return str(self.knots)
```

```python
r1 = Rope(2)
r2 = Rope(2)
r3 = r1 + r2
print(r3)
```

    5

```python
def test_Rope():
    r1 = Rope(2)
    r2 = Rope(2)
    r3 = r1 + r2
    assert r3.knots == 5
```

# Exercise 7.15: Implement in-place += and -= operators

```python
from datetime import datetime as dt
import dateutil.parser as dup

class Account(object):
    def __init__(self, name, account_number, initial_amount):
        self._name = name
        self._no = account_number
        self._transactions = {str(dt.utcnow()): initial_amount}

    def __iadd__(self, amount):
        self._transactions[str(dt.utcnow())] = amount
        return self

    def __isub__(self, amount):
        self._transactions[str(dt.utcnow())] = -amount
        return self

    def get_balance(self):
        return sum(v for v in self._transactions.values())

    def print_transactions(self):
        for time, amount in self._transactions.items():
            print(f"%s: %6.f" % (dup.parse(time).ctime(), amount))

    def __str__(self):
        return "%s, %s, balance: %s" % (self._name, self._no, self.get_balance())

    def __repr__(self):
        return f"Account('{self._name}', {self._no}, {self.get_balance()})"
```

```python
import time
a = Account("Onno", "112312535", 20)
a += 333
time.sleep(1)
a -= 34
time.sleep(1)
a += 90
time.sleep(1)
a -= 100
time.sleep(1)
a.print_transactions()
print(a)
```

    Sun Mar 25 18:15:36 2018:     20
    Sun Mar 25 18:15:36 2018:    333
    Sun Mar 25 18:15:37 2018:    -34
    Sun Mar 25 18:15:38 2018:     90
    Sun Mar 25 18:15:39 2018:   -100
    Onno, 112312535, balance: 309

```python
repr(a)
```

    "Account('Onno', 112312535, 309)"

```python
b = eval(repr(a))
```

```python
print(a)
print(b)
```

    Onno, 112312535, balance: 309
    Onno, 112312535, balance: 309

```python
b == a
```

    False

# Exercise 7.16: Implement a class for numerical differentiation

```python
def test_Central():
    def f(x):
        return 1/2 * x**2

    df = Central(f)
    x = 2
    print(df(x))
    print(x)
    assert abs(df(x) - x) < 1e-6
```

```python
class Central:
    def __init__(self, f, h=1e-6):
        self.f, self.h = f, h

    def __call__(self, x):
        f, h = self.f, self.h
        return (f(x + h) - f(x - h)) / 2/h
```

```python
test_Central()
```

    2.0000000000575113
    2

```python
import sympy as sp

def table(f, x, h=1e-6):
    x_list = x
    x = sp.Symbol('x')
    df_exact = sp.lambdify(x, sp.diff(f))
    df_numeric = Central(sp.lambdify(x, f), h=h)
    print("         x |         Error ")
    print("-----------+---------------")
    for x in x_list:
        print(" {:9.6f} | {:13.6e} ".format(x, df_exact(x) - df_numeric(x)))
```

```python
x = sp.Symbol('x')
table(2*x*sp.cos(2*x) + sp.sin(2*x), np.linspace(-4, 4, 10))
```

             x |         Error
    -----------+---------------
     -4.000000 |  2.290324e-09
     -3.111111 | -5.667182e-10
     -2.222222 | -9.131380e-10
     -1.333333 | -3.966854e-10
     -0.444444 |  3.963496e-12
      0.444444 |  3.963496e-12
      1.333333 | -3.966871e-10
      2.222222 | -1.135199e-09
      3.111111 | -1.010797e-09
      4.000000 |  2.290324e-09

```python
f = lambda x: 2*x*np.cos(2*x) + np.sin(2*x)
x = np.linspace(-5, 5, 100)
plt.figure()
plt.plot(x, f(x))
plt.show()
```

# Exercise 7.17: Examine a program

```python
from math import *

class Backward(object):
    def __init__(self, f, h=1e-9):
        self.f, self.h = f, h

    def __call__(self, x):
        h, f = self.h, self.f
        return (f(x) - f(x-h))/h    # finite difference

dsin = Backward(sin)
e = dsin(0) - cos(0); print("error:", e)
dexp = Backward(exp, h=1e-7)
e = dexp(0) - exp(0); print("error:", e)
```

    error: 0.0
    error: -5.048639195592841e-08

# Exercise 7.18: Modify a class for numerical differentiation

```python
class Derivative(object):
    def __init__(self, f, h=1E-5):
        self._f = f
        self._h = float(h)

    def __call__(self, x):
        f, h = self._f, self._h
        return (f(x+h) - f(x))/h

    def set_precision(self, h):
        self._h = h

    def get_precision(self):
        return self._h

def test_Derivative():
    d = Derivative(lambda x: x)
    d.set_precision(1e-2)
    assert d.get_precision() == 1e-2

test_Derivative()
```

# Exercise 7.19: Make a class for the Heaviside function

```python
class Heaviside():
    import numpy as np

    def __init__(self, eps=None):
        self.eps = eps

    def __call__(self, x):
        eps = self.eps

        if eps:
            from numpy import sin, pi
            y = 0
            y = np.where((-eps <= x) & (x < eps), (1 + x/eps + sin(pi * x / eps)/pi) / 2, y)
            y = np.where(x >= eps, 1, y)
        else:
            y = 1 * (x >= 0)

        return y

    def plot(self, xmin, xmax):
        import matplotlib.pyplot as plt

        plt.figure()
        x = np.linspace(xmin, xmax, 200)
        plt.plot(x, self(x))
        plt.show()
```

```python
H = Heaviside()
H(0.1)
```

    1

```python
H = Heaviside(eps=0.8)
H(0.1)
```

    array(0.623405959900277)

```python
H = Heaviside()
x = np.linspace(-1, 1, 11)
H(x)
```

    array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1])

```python
H = Heaviside(eps=0.8)
H(x)
```

    array([  0.00000000e+00,  -1.94908592e-17,   1.24604605e-02,
             9.08450569e-02,   2.62460460e-01,   5.00000000e-01,
             7.37539540e-01,   9.09154943e-01,   9.87539540e-01,
             1.00000000e+00,   1.00000000e+00])

```python
H = Heaviside(eps=1)
H.plot(xmin=-4, xmax=4)
```

# Exercise 7.20: Make a class for the indicator function

```python
class Indicator():
    import numpy as np

    def __init__(self, a, b, eps=None):
        self.eps, self.a, self.b = eps, a, b

    def __call__(self, x):
        H = Heaviside(self.eps)
        y = H(x - self.a) * H(-(x - self.b))
        return y

    def plot(self, xmin, xmax):
        import matplotlib.pyplot as plt

        plt.figure()
        x = np.linspace(xmin, xmax, 200)
        plt.plot(x, self(x))
        plt.show()
```

```python
I = Indicator(-2, 3, eps=1)
I.plot(-5, 6)
```

# Exercise 7.21: Make a class for piecewise constant functions

```python
class PiecewiseConstant():
    import numpy as np

    def __init__(self, points, xmax):
        self.points, self.xmax = points, xmax

    def __call__(self, x):
        y = np.nan
        for v, xi in self.points:
            y = np.where((xi <= x) & (x < self.xmax), v, y)
        return y

    def plot(self):
        import matplotlib.pyplot as plt

        plt.figure()
        x = np.linspace(self.points[0][1], self.xmax, 1000)
        plt.plot(x, self(x))
        plt.show()
```

```python
f = PiecewiseConstant([(0.4, 1), (0.2, 1.5), (0.1, 3)], xmax=4)
print(f(1.5), f(1.75), f(4))
x = np.linspace(0, 4, 21)
print(f(x))
```

    0.2 0.2 nan
    [ nan  nan  nan  nan  nan  0.4  0.4  0.4  0.2  0.2  0.2  0.2  0.2  0.2  0.2
      0.1  0.1  0.1  0.1  0.1  nan]

```python
f.plot()
```

# Exercise 7.22: Speed up repeated integral calculations

```python
class Integral(object):
    def __init__(self, f, a, n=100):
        self.f, self.a, self.n = f, a, n

    def __call__(self, x):
        if isinstance(x, np.ndarray):
            nk = int(self.n / len(x))
            I = [trapezoidal(self.f, self.a, x[0], nk)]
            for i, xi in enumerate(x[1:]):
                I.append(I[i] + trapezoidal(self.f, x[i], xi, nk))
            return I
        else:
            return trapezoidal(self.f, self.a, x, self.n)

def trapezoidal(f, a, x, n):
    dx = (x - a) / n
    I = dx / 2 * sum(f(a + (k - 1)*dx) + f(a + k*dx) for k in range(n))
    return I
```

```python
from numpy import sin, pi
I = Integral(sin, 0, n=1000)
I(np.array([pi/2, pi, 3/2*pi, 2*pi]))
```

    [0.99369384775594749,
     1.999953942101556,
     1.0062600943456086,
     -2.2204460492503131e-16]

# Exercise 7.23: Apply a class for polynomials

```python
class Polynomial(object):
    def __init__(self, coefficients):
        self.coeff = coefficients

    def __call__(self, x):
        """Evaluate the polynomial."""
        s = 0
        for i, c in enumerate(self.coeff):
            s += c*x**i
        return s

    def __add__(self, other):
        """Return self + other as Polynomial object."""

        # Start with the longest list and add in the other
        if len(self.coeff) > len(other.coeff):
            result_coeff = self.coeff[:]  # copy!
            for i in range(len(other.coeff)):
                result_coeff[i] += other.coeff[i]
        else:
            result_coeff = other.coeff[:] # copy!
            for i in range(len(self.coeff)):
                result_coeff[i] += self.coeff[i]

        return Polynomial(result_coeff)
```

```python
def evaluate(x, N):
    from math import factorial
    coef = [1/factorial(k) for k in range(N + 1)]
    p = Polynomial(coef)
    print(f"N: {N:2d}, p({x:05.2f}) = {p(x):f}")
```

```python
from math import exp
for x in [0.5, 3, 10]:
    print("exact:", exp(x))
    for N in [2, 5, 10, 15, 20, 25]:
        evaluate(x, N)
```

    exact: 1.6487212707001282
    N:  2, p(00.50) = 1.625000
    N:  5, p(00.50) = 1.648698
    N: 10, p(00.50) = 1.648721
    N: 15, p(00.50) = 1.648721
    N: 20, p(00.50) = 1.648721
    N: 25, p(00.50) = 1.648721
    exact: 20.085536923187668
    N:  2, p(03.00) = 8.500000
    N:  5, p(03.00) = 18.400000
    N: 10, p(03.00) = 20.079665
    N: 15, p(03.00) = 20.085534
    N: 20, p(03.00) = 20.085537
    N: 25, p(03.00) = 20.085537
    exact: 22026.465794806718
    N:  2, p(10.00) = 61.000000
    N:  5, p(10.00) = 1477.666667
    N: 10, p(10.00) = 12842.305115
    N: 15, p(10.00) = 20952.886969
    N: 20, p(10.00) = 21991.482026
    N: 25, p(10.00) = 22026.076361

# Exercise 7.24: Find a bug in a class for polynomials

```python
class Polynomial(object):
    def __init__(self, coefficients):
        self.coeff = coefficients

    def __call__(self, x):
        return sum([c*x**i for i, c in enumerate(self.coeff)])

    def __add__(self, other):
        maxlength = max(len(self.coeff), len(other.coeff))
        # Extend both lists with zeros to this maxlength
        self.coeff += [0]*(maxlength - len(self.coeff))
        other.coeff += [0]*(maxlength - len(other.coeff))
        result_coeff = self.coeff
        for i in range(maxlength):
            result_coeff[i] += other.coeff[i]
        return Polynomial(result_coeff)
```

```python
p1 = Polynomial([1, 2, 3])
p2 = Polynomial([0, 4, 0])
(p1 + p2)(3)
```

    46

# Exercise 7.25: Implement subtraction of polynomials

```python
class Polynomial(object):
    def __init__(self, coefficients):
        self.coeff = coefficients

    def __call__(self, x):
        return sum([c*x**i for i, c in enumerate(self.coeff)])

    def __add__(self, other):
        maxlength = max(len(self.coeff), len(other.coeff))
        # Extend both lists with zeros to this maxlength
        self.coeff += [0]*(maxlength - len(self.coeff))
        other.coeff += [0]*(maxlength - len(other.coeff))
        result_coeff = self.coeff
        for i in range(maxlength):
            result_coeff[i] += other.coeff[i]
        return Polynomial(result_coeff)

    def __sub__(self, other):
        from copy import copy
        other_ = copy(other)
        other_.coeff = [-c for c in other_.coeff]
        return self.__add__(other_)

    def __str__(self):
        s = ""
        for i in range(0, len(self.coeff)):
            if self.coeff[i] != 0:
                s += " + %g*x^%d" % (self.coeff[i], i)
        # Fix layout
        s = s.replace("+ -", "- ")
        s = s.replace("x^0", "1")
        s = s.replace(" 1*", " ")
        s = s.replace("x^1 ", "x ")
        if s[0:3] == " + ":  # remove initial +
            s = s[3:]
        if s[0:3] == " - ":  # fix spaces for initial -
            s = "-" + s[3:]
        return s
```

```python
p1 = Polynomial([1, 2, 3])
p2 = Polynomial([0, 4, 0])
(p1 - p2).coeff
```

    [1, -2, 3]

# Exercise 7.26: Test the functionality of pretty print of polynomials

```python
print(p1 - p2)    # looks fine.
```

    1 - 6*x + 3*x^2

# Exercise 7.27: Vectorize a class for polynomials

```python
class Polynomial(object):
    import numpy as np

    def __init__(self, coefficients):
        self.coeff = np.asarray(coefficients)

    def __call__(self, x):
        return self.coeff @ x**np.arange(len(self.coeff))

    def __add__(self, other):
        coeffs = sorted([self.coeff, other.coeff], key=len)
        r = coeffs[0] + coeffs[1][:len(coeffs[0])]
        r = np.append(r, coeffs[1][len(r):])
        return Polynomial(r)

    def __sub__(self, other):
        from copy import copy
        other_ = copy(other)
        other_.coeff = [-c for c in other_.coeff]
        return self.__add__(other_)

    def __str__(self):
        s = ""
        for i in range(0, len(self.coeff)):
            if self.coeff[i] != 0:
                s += " + %g*x^%d" % (self.coeff[i], i)
        # Fix layout
        s = s.replace("+ -", "- ")
        s = s.replace("x^0", "1")
        s = s.replace(" 1*", " ")
        s = s.replace("x^1 ", "x ")
        if s[0:3] == " + ":  # remove initial +
            s = s[3:]
        if s[0:3] == " - ":  # fix spaces for initial -
            s = "-" + s[3:]
        return s
```

```python
p1 = Polynomial([1, 2, 3, 5, 7])
p2 = Polynomial([0, 4, 0, 1])
(p1 + p2).coeff
```

    array([1, 6, 3, 6, 7])

```python
(p1 + p2)(2)
```

    185

# Exercise 7.28: Use a dict to hold polynomial coefficients

```python
class Polynomial:
    import numpy as np

    def __init__(self, coefficients):
        self.coeff = coefficients

    def __call__(self, x):
        return sum(c * x**i for i, c in self.coeff.items())

    def __add__(self, other):
        cs = self.coeff.copy()
        for c in other.coeff:
            if c in cs:
                cs[c] += other.coeff[c]
            else:
                cs[c] = other.coeff[c]
        return Polynomial(cs)

    def __sub__(self, other):
        from copy import copy
        other_ = copy(other)
        other_.coeff = {k: -c for k, c in other_.coeff.items()}
        return self.__add__(other_)

    def __mul__(self, other):
        from collections import defaultdict
        cs = defaultdict(float)
        for i, c in self.coeff.items():
            for ii, cc in other.coeff.items():
                cs[i + ii] += c * cc
        return Polynomial(dict(cs))

    @staticmethod
    def test():
        # test __call__
        p1 = Polynomial({4: 1, 2: -2, 0: 3})
        assert abs(p(2) - 11) < 1e-12

        # test __add__
        p2 = Polynomial({0: 1, 3: 1})
        p3 = p1 + p2
        assert set(p3.coeff.keys()) == {0, 2, 3, 4}
        assert np.allclose([p3.coeff[k] for k in sorted(p3.coeff)], [4, -2, 1, 1])

        # test __mul__
        p4 = Polynomial({1: -2, 2: 3})
        p5 = p2 * p4
        assert set(p5.coeff.keys()) == {1, 2, 4, 5}
        assert np.allclose([p5.coeff[k] for k in sorted(p5.coeff)], [-2, 3, -2, 3])
```

```python
p = Polynomial({4: 1, 2: -2, 0: 3})
p(2)
```

    11

```python
p1 = Polynomial({0: 1, 3: 1})
p2 = Polynomial({1: -2, 2: 3})
(p1 * p2).coeff
```

    {1: -2.0, 2: 3.0, 4: -2.0, 5: 3.0}

```python
Polynomial.test()
```

# Exercise 7.29: Extend class Vec2D to work with lists/tuples

```python
class Vec2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        if isinstance(other, Vec2D):
            return Vec2D(self.x + other.x, self.y + other.y)
        else:
            return Vec2D(self.x + other[0], self.y + other[1])

    def __radd__(self, other):
        return self.__add__(other)

    def __sub__(self, other):
        if isinstance(other, Vec2D):
            return Vec2D(self.x - other.x, self.y - other.y)
        else:
            return Vec2D(self.x - other[0], self.y - other[1])

    def __rsub__(self, other):
        if isinstance(other, Vec2D):
            return Vec2D(other.x - self.x, other.y - self.y)
        else:
            return Vec2D(other[0] - self.x, other[1] - self.y)

    def __mul__(self, other):
        return self.x*other.x + self.y*other.y

    def __abs__(self):
        return math.sqrt(self.x**2 + self.y**2)

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __str__(self):
        return "(%g, %g)" % (self.x, self.y)
```

```python
u = Vec2D(-2, 4)
v = u + (1, 1.5)
w = [-3, 2] - v
```

# Exercise 7.30: Extend class Vec2D to 3D vectors

```python
class Vec3D:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

    def __add__(self, other):
        return Vec3D(self.x + other.x, self.y + other.y, self.z + other.z)

    def __sub__(self, other):
        return Vec3D(self.x - other.x, self.y - other.y, self.z - other.z)

    def __mul__(self, other):
        return self.x*other.x + self.y*other.y + self.z*other.z

    def __abs__(self):
        return math.sqrt(self.x**2 + self.y**2 + self.z**2)

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y and self.z == other.z

    def __str__(self):
        return "(%g, %g, %g)" % (self.x, self.y, self.z)

    def cross(self, other):
        return Vec3D(self.y*other.z - self.z*other.y, self.z*other.x - self.x*other.z, self.x*other.y - self.y*other.x)
```

```python
print(Vec3D(0, 1, 0).cross(Vec3D(1, 0, 0)))
```

    (0, 0, -1)

# Exercise 7.31: Use NumPy arrays in class Vec2D

```python
class Vec:
    import math

    def __init__(self, *vec):
        self.v = np.asarray(vec).flatten()

    def __add__(self, other):
        return Vec(self.v + other.v)

    def __sub__(self, other):
        return Vec(self.v - other.v)

    def __mul__(self, other):
        return self.v @ other.v

    def __abs__(self):
        return math.sqrt(sum(c**2 for c in self.v))

    def __eq__(self, other):
        return (self.v == other.v).all()
```

```python
v1 = v2 = Vec([1, 2, 3])
v1 * v2
```

    14

# Exercise 7.32: Impreciseness of interval arithmetics

```python
class Interval(object):
    def __init__(self, lower, upper):
        self.lo = float(lower)
        self.up = float(upper)

    def __add__(self, other):
        if not isinstance(other, Interval):
            other = Interval(other, other)
        a, b, c, d = self.lo, self.up, other.lo, other.up
        return Interval(a + c, b + d)

    def __radd__(self, other):
        return self.__add__(other)

    def __sub__(self, other):
        a, b, c, d = self.lo, self.up, other.lo, other.up
        return Interval(a - d, b - c)

    def __mul__(self, other):
        a, b, c, d = self.lo, self.up, other.lo, other.up
        return Interval(min(a*c, a*d, b*c, b*d), max(a*c, a*d, b*c, b*d))

    def __truediv__(self, other):
        a, b, c, d = self.lo, self.up, other.lo, other.up
        if c * d <= 0:
            raise ValueError("Interval %s cannot be denominator because it contains zero" % other)
        return Interval(min(a/c, a/d, b/c, b/d), max(a/c, a/d, b/c, b/d))

    def __str__(self):
        return "[%g, %g]" % (self.lo, self.up)
```

```python
x = Interval(1, 2)
print(x / (1 + x))
```

    [0.333333, 1]

# Exercise 7.33: Make classes for students and courses

```python
class Student:
    def __init__(self, name, courses):
        self.__dict__.update({k: v for k, v in locals().items() if k != 'self'})

    def __str__(self):
        s = f"Name: {self.name}\n"
        for c in self.courses:
            s += str(c) + "\n"
        return s

class Course:
    def __init__(self, title, semester, credits, grade):
        self.__dict__.update({k: v for k, v in locals().items() if k != 'self'})

    def __str__(self):
        return f"{self.title:30} {self.semester:11} {self.credits:2} {self.grade}"
```

```python
print(Student("John Doe", [Course("Astronomy", "2003 fall", 10, "A"),
                           Course("Quantum Mechanics II", "2005 spring", 5, "C")]))
```

    Name: John Doe
    Astronomy                      2003 fall   10 A
    Quantum Mechanics II           2005 spring  5 C

# Exercise 7.34: Find local and global extrema of a function

```python
class MinMax:
    import numpy as np

    def __init__(self, f, a, b, n):
        self.__dict__.update({k: v for k, v in locals().items() if k != 'self'})
        self._find_extrema()

    def _find_extrema(self):
        f, a, b = self.f, self.a, self.b

        self.Pmin, self.Pmax, self.Fmin, self.Fmax = Pmin, Pmax, Fmin, Fmax = [], [], [], []

        x = np.linspace(a, b, self.n)
        for i, xi in enumerate(x[1:-1]):
            if f(x[i]) < f(xi) > f(x[i + 2]):
                Pmax.append(xi)
                Fmax.append(f(xi))
            elif f(x[i]) > f(xi) < f(x[i + 2]):
                Pmin.append(xi)
                Fmin.append(f(xi))

        if f(a) > f(x[1]):
            Pmax.insert(1, a)
            Fmax.insert(1, f(a))
        elif f(a) < f(x[1]):
            Pmin.insert(1, a)
            Fmin.insert(1, f(a))
        if f(b) > f(x[-2]):
            Pmax.append(b)
            Fmax.append(f(b))
        elif f(b) < f(x[-2]):
            Pmin.append(b)
            Fmin.append(f(b))

    def _refine_extrema(self):
        f, a, b, n = self.f, self.a, self.b, self.n
        Pmin, Pmax, Fmin, Fmax = self.Pmin, self.Pmax, self.Fmin, self.Fmax
        for i, p in enumerate(Pmin):
            if p != a and p != b:
                df = Derivative(f, h= (b - a) / n**2 / 1000)
                xmin, dfmin = p, df(p)
                for x in np.linspace(p - (b - a)/n, p + (b - a)/n, n):    # just reuse `n` as precision basis
                    if df(x) < dfmin:
                        xmin, dfmin = x, df(x)
                Pmin[i] = xmin
                Fmin[i] = f(xmin)

        for i, p in enumerate(Pmax):
            if p != a and p != b:
                df = Derivative(f, h= (b - a) / n**2 / 1000)
                xmax, dfmin = p, df(p)
                for x in np.linspace(p - (b - a)/n, p + (b - a)/n, n):    # just reuse `n` as precision basis
                    if df(x) < dfmin:
                        xmax, dfmin = x, df(x)
                Pmax[i] = xmax
                Fmax[i] = f(xmax)

    def get_global_minimum(self):
        return min(self.get_all_minima(), key=lambda t: t[1])

    def get_global_maximum(self):
        return max(self.get_all_maxima(), key=lambda t: t[1])

    def get_all_minima(self):
        return [(x, y) for x, y in zip(self.Pmin, self.Fmin)]

    def get_all_maxima(self):
        return [(x, y) for x, y in zip(self.Pmax, self.Fmax)]

    def __str__(self):
        from textwrap import dedent as dd
        return dd(f"""
        All minima: {', '.join(f'{p:.4f}' for p, v in self.get_all_minima())}
        All maxima: {', '.join(f'{p:.4f}' for p, v in self.get_all_maxima())}
        Global minimum: {self.get_global_minimum()[0]}
        Global maximum: {self.get_global_maximum()[0]}
        """)
```

```python
from math import exp, sin, pi
mm = MinMax(f=lambda x: x**2 * exp(-0.2 * x) * sin(2 * pi * x), a=0, b=4, n=5001)
print(mm)
mm._refine_extrema()
print(mm)
```

    All minima: 0.8056, 0.0000, 1.7736, 2.7632, 3.7584
    All maxima: 0.3616, 1.2840, 2.2672, 3.2608, 4.0000
    Global minimum: 3.7584
    Global maximum: 3.2608

    All minima: 0.8048, 0.0000, 1.7728, 2.7624, 3.7576
    All maxima: 0.3624, 1.2848, 2.2680, 3.2616, 4.0000
    Global minimum: 3.7576001599680064
    Global maximum: 3.2615998400319937

# Exercise 7.35: Find the optimal production for a company

```python
f = lambda x, y: 45*x + 14*y
fa = lambda x, alpha: alpha/14 - 45*x/14
```

```python
import matplotlib.pyplot as plt
from numpy import linspace
x = linspace(0, 20, 1000)
plt.figure()
plt.fill_between(x, 100 - 2*x, alpha=0.3)
plt.fill_between(x, 80/3 - 5/3*x, alpha=0.3)
plt.fill_between(x, 150/4, alpha=0.3)
for alpha in [1, 100, 1000, 500, 700]:
    plt.plot(x, fa(x, alpha), label=f"alpha = {alpha}")
plt.ylabel("y")
plt.xlabel("x")
plt.legend()
plt.show()
```

```python
f(x=16, y=0)    # optimal solution.
```

    720

```python
import sympy as sp
x, y = sp.symbols('x y')

conditions = [
    "2*x +   y <= 100",
    "5*x + 3*y <=  80",
    "      4*y <= 150",
    "  x       >=   0",
    "        y >=   0"
]

# Compute the intersections
intersections = []
eqs = [
    sp.Eq(eval(a), eval(b)) for a, b in
        [e.split("=") for e in
            [c.replace("<=", "=").replace(">=", "=") for c in conditions]
]]

for e1 in eqs:
    for e2 in eqs:
        i = sp.solve([e1, e2])
        if len(i) > 1 and i not in intersections:
            intersections.append(i)

# Check conditions on intersections
corners = []

for i in intersections:
    corners.append(tuple(i.values()))
    x, y = sp.symbols('x y')
    x, y = i[x], i[y]
    for c in conditions:
        if not eval(c):
            del corners[-1]
            break
```

```python
corners
```

    [(0, 80/3), (16, 0), (0, 0)]

```python
f = lambda c: 45*c[0] + 14*c[1]
x, y = max(corners, key=f)
print(x, y)    # yay!
```

    16 0


```javascript
%%javascript
    MathJax.Hub.Config({
      TeX: { equationNumbers: { autoNumber: "AMS" } }
    });
```

    <IPython.core.display.Javascript object>

```python
from IPython.display import HTML

HTML('''
<form action="javascript:code_toggle()"><input type="submit" value="Code Toggle"></form>''')
```

<form action="javascript:code_toggle()"><input type="submit" value="Code Toggle"></form>

```python
from IPython.display import HTML

HTML('''
<a href="https://github.com/usnistgov/pfhub/raw/master/benchmarks/benchmark8.ipynb"
   download>
<button type="submit">Download Notebook</button>
</a>
''')
```

<button type="submit">Download Notebook</button>

# Benchmark Problem 8: Homogeneous Nucleation

```python
from IPython.display import HTML

HTML('''{% include jupyter_benchmark_table.html num="[8]" revision=1 %}''')
```

{% include jupyter_benchmark_table.html num="[8]" revision=1 %}

See the journal publication entitled ["Phase Field Benchmark Problems for Nucleation"][paper] for more details about the benchmark problems. Furthermore, read [the extended essay][benchmarks] for a discussion about the need for benchmark problems.

[benchmarks]: ../
[paper]: https://doi.org/10.1016/j.commatsci.2021.110371

# Table of Contents

* [Overview](#Overview)
* [Model Formulation](#Model-Formulation)
	* [The phase field model in classical nucleation theory](#The-phase-field-model-in-classical-nucleation-theory)
	* [The properties of the classical nucleus](#The-properties-of-the-classical-nucleus)
	* [Avrami plots](#Avrami-plots)
* [Benchmark 8 Specification](#Benchmark-8-Specification)
	* [Part (a): single seed](#Part-%28a%29:-single-seed)
    * [Part (b): multiple initial seeds](#Part-%28b%29:-multiple-initial-seeds)
    * [Part (c): multiple seeds at random times](#Part-%28c%29:-multiple-seeds-at-random-times)
* [Submission Guidelines](#Submission-Guidelines)
	* [Part (a)](#Part-%28a%29)
	* [Part (b)](#Part-%28b%29)
	* [Part (c)](#Part-%28c%29)
* [Results](#Results)
* [References](#References)

## Overview

Phase field modeling of nucleation has a long history and is covered
in a number of reviews (Gránásy 2002,
Castro 2003,
Simmons 2004,
Gránásy 2007,
Warren 2009,
Heo 2014,
Gránásy 2019). The problem formulation of crystallization of an ideal pure liquid cooled
below its melting point starts with homogeneous nucleation, a process
in which the internal fluctuations of the undercooled liquid lead to
the formation of crystal-like seeds able to grow to macroscopic
sizes. The nucleation can be assisted by the presence of surfaces
(container walls, foreign particles, etc.), in which case the process
is termed heterogeneous nucleation. We note that homogeneous
nucleation is an idealized formulation, and it is unlikely homogeneous
nucleation occurs, due to impurities present in experimental
apparatus. However, creating benchmark problems for homogeneous
nucleation is still needed since the focus in nucleation research
often lies on the underlying homogeneous nucleation, as it is the
basis for advancing theoretical approaches to the much more complex
phenomena governing heterogeneous nucleation. This benchmark
(benchmark 8) targets homogeneous nucleation while benchmark 9
targets heterogeneous nucleation.

There are two main modeling approaches to introduce nuclei into a
metastable system: the Langevin noise method
(Kubo 1966,
Gránásy 2002)
and the explicit nucleation
method
(Simmons 2000,
Shi 2019)
. We focus on the explicit
method, where the critical nucleus size and the nucleation energy are
determined by the classical nucleation theory. We break down our
consideration of homogeneous nucleation into three parts. Part (a)
considers the simple case of single-seed homogeneous nucleation. We
explore how the particle size influences the evolution of the nucleus
when the thermodynamic driving force is close to the critical value
where we can observe whether the particle grows, dissolves or remains
stationary. In parts (b) and (c) we consider multiple-seed homogeneous
nucleation, with nuclei appearing at fixed time $t=0$ or at random
times distributed uniformly, respectively, with the latter part
illustrating a scenario with constant nucleation rate. The Benchmark
Problem probes the differences between transformation kinetics of the
two cases and summarizes them using the Avrami plots based on the
Johnson-Mehl-Avrami-Kolmogorov (JMAK)
theory
(Johnson 1939,
Avrami 1939,
Avrami 1940,
Avrami 1941,
Kolmogorov 1937).

## Model Formulation

### The phase field model in classical nucleation theory

For this benchmark problems we use the simplest possible phase field model with a single non-conserved phase field $\phi$, which describes an isothermal pure substance with one liquid ($\phi=0$) and one solid ($\phi=1$) phase. The free energy of this system is
\begin{equation}
    \mathcal{F}(\phi)=\int \left[\frac{\epsilon^2}{2}(\nabla \phi)^2+wg(\phi)-\Delta f p(\phi) \right] d{V},
    \label{eqn:Fphi}
\end{equation}
set $g(\phi)=\phi^2(1-\phi)^2$, a simple double well function with minima at $\phi=0$ and $\phi=1$, $ w$ controls the double-well barrier height, $\epsilon^2$ is the gradient energy coefficient, let $p(\phi)=\phi^3(10-15\phi+6\phi^2)$, which ensures that $p(0)=p'(0)=p'(1)=0$ and $p(1)=1$, and $\Delta f$ is the driving force for solidification at the simulation temperature ($\Delta f$ is positive below the melting point). The time evolution of $\phi$ is given by the Allen-Cahn equation (Allen 1979)
\begin{equation}
    \frac{\partial \phi}{\partial t}=-M\frac{\delta \mathcal{F}}{\delta \phi}=M\left[\epsilon^2\nabla^2\phi-wg'(\phi)+\Delta fp'(\phi)\right]
    \label{eqn:dphidt}
\end{equation}
where $M$ is the mobility parameter. We will restrict the problem to two dimensions (2D). For a planar interface, one can show that the equilibrium ($\Delta f$=0) solid-liquid interface profile with the interface centered at $x=x_0$ is given by
\begin{equation}
    \phi(x)=\frac{1}{2}\left[1-\tanh\left(\frac{x-x_0}{\sqrt{2}\ell}\right)\right],
    \label{eqn:phir}
\end{equation}
where $x$ is the perpendicular distance from the interface.
We use this expression as the initial condition when we introduce a nucleus. We can also obtain the width $\ell$ and the free energy of the interface, $\gamma$, as
\begin{equation}
    \ell=\sqrt{\frac{\epsilon^2}{w}}
    \label{eqn:l}
\end{equation}
and
\begin{equation}
    \gamma=\frac{\sqrt{\epsilon^2w}}{3\sqrt{2}}.
    \label{eqn:gamma}
\end{equation}

Choosing a characteristic length scale $\xi=\ell$ and a characteristic time scale $\tau=1/(Mw)$, we can obtain a nondimensional form of the problem (the nondimensional quantities are denoted by tildes),
\begin{equation}
    \tilde{\mathcal{F}}(\phi)=\int \left[\frac{1}{2}(\tilde{\nabla} \phi)^2+g(\phi)- \tilde{\Delta f} p(\phi) \right] d{\tilde{V}}
    \label{eqn:tFphi}
\end{equation}
and
\begin{equation}
    \frac{\partial \phi}{\partial \tilde{t}}=\tilde{\nabla}^2\phi-g'(\phi)+\tilde{\Delta f}p'(\phi),
    \label{eqn:dphidtt}
\end{equation}
with $\tilde{\Delta f}=\Delta f/w$.

### The properties of the classical nucleus

The explicit nucleation method that we employ introduces nuclei into a metastable system, with their radii and nucleation barrier determined from classical nucleation theory. Classical nucleation theory views crystallite fluctuations appearing in the undercooled liquid as small spherical domains of the bulk crystalline phase bounded by a mathematically sharp solid-liquid interface. For a 2D system, the free energy of a circular solid particle of radius $r$ is
\begin{equation}
    \Delta G(r) = 2 \pi r \gamma - \pi r^2 \Delta f
    \label{eqn:deltaG}
\end{equation}
where $\Delta f$ is the nucleation driving force that is used in our phase field model, and $\gamma$ is the free energy of the interface. The free energy of the particle is a balance between the energy cost in forming the solid-liquid interface, and the free energy from the driving force which is released when the crystalline particle forms. Once the rate of change of free energy with respect to particle size becomes negative, the particle can grow. Taking the derivative of $\Delta G(r)$ we get the rate of change of free energy with respect to radius as
\begin{equation}
    \frac{d\Delta G}{dr}=2\pi \gamma - 2\pi r\Delta f.
    \label{eqn:ddeltaGdr}
\end{equation}
By setting $d\Delta G/dr$ to zero, we obtain the critical radius $r^*$ as
\begin{equation}
    r^* = \frac{\gamma}{\Delta f}.
    \label{eqn:rc}
\end{equation}
The corresponding critical nucleation free energy is
\begin{equation}
    \Delta G^* = \frac{\pi \gamma^2}{\Delta f}.
    \label{eqn:deltaGc}
\end{equation}
Using the same units as before, the nondimensional forms of these quantities are
\begin{equation}
    \tilde{r^*} = \frac{1}{3\sqrt{2}}\frac{1}{\tilde{\Delta f}}
    \label{eqn:trc}
\end{equation}
and
\begin{equation}
    \tilde{\Delta G^*} = \frac{\pi}{18}\frac{1}{\tilde{\Delta f}}
    \label{eqn:tdeltaGc},
\end{equation}
respectively.

### Avrami plots

Avrami plots describe how solids transform from one phase to another at constant temperature. In particular, they can describe the kinetics of nucleation. The Avrami plots come from the JMAK theory
(Johnson 1939,
Avrami 1939,
Avrami 1940,
Avrami 1941,
Kolmogorov 1937), which makes a number of
assumptions and simplifications: (i)  nucleation occurs randomly and homogeneously over the entire un-transformed portion of the material, (ii) the growth rate is constant and does not depend on the extent of transformation, (iii) the particles have convex shape with the same orientation, and (iv) the size of the system is infinite (both in space and time).

The basis of the Avrami plots is the transformed fraction (solid fraction) $Y(t)$ vs. time. According to the JMAK theory,
\begin{equation}
    Y(t)=1-\exp (-Kt^n),
    \label{eqn:Xt}
\end{equation}
where $K$ is a constant depending on the nucleation and growth rates, $n=d+1$ for continuous nucleation and $n=d$ if nucleation happens only at $t=0$, and $d$ is the number of spatial dimensions. If we plot $\log(-\log(1-Y))$ vs. $\log(t)$, then for the JMAK kinetics (Eq. \ref{eqn:Xt}) we get a straight line with slope $n$.

## Benchmark 8 Specification

From here on, we will use only the nondimensional forms of the phase field (Eqs. \ref{eqn:tFphi}, \ref{eqn:dphidtt}, and \ref{eqn:phir}) and nucleation (Eqs. \ref{eqn:trc} and \ref{eqn:tdeltaGc}) equations, but we will drop the tildes.

### Part (a): single seed

In this problem we examine the morphology change of the nucleus for different initial radii. We consider a 2D simulation domain of size $100 \times 100$ units centered at $x=y=0$ with Neumann boundary conditions. The driving force is set to $\Delta f = \sqrt{2}/30$, which corresponds to a critical radius of $r^*=5$ (Eq. \ref{eqn:trc}). Next, we place a circular seed at the center of the domain.
Incorporating a nucleus into a diffuse interface phase field model leads to a small offset from a classical, sharp-interface model of a nucleus. To account for this,
we incorporate a diffuse-interface seed using the profile $\phi(r)$ given by modifying Eq. \ref{eqn:phir} to 2D where $r=\sqrt{x^2+y^2}$, with a radius $r_0=r^*$. This seed is the diffuse-interface approximation of the classical sharp interface nucleus corresponding to the given $\Delta f$, and therefore it should be fairly close to (an unstable) equilibrium.
Part (a) is then defined by the Allen-Cahn equation
\begin{equation}
    \frac{\partial \phi}{\partial {t}}={\nabla}^2\phi-g'(\phi)+{\Delta f}p'(\phi)
    \label{eqn:dphidtt_2}
\end{equation}
for the phase field $\phi$, and the three different initial conditions: in the first case with the seed radius $r_0$ corresponding exactly to the critical one (critical nucleus), and in the other two cases, slightly below and above the critical radius (subcritical and supercritical nuclei):
\begin{equation}
    \phi(r)=\frac{1}{2}\left[1-\tanh\left(
    \frac{r-r_0}{\sqrt{2}}
    \right)\right],
    \label{eqn:phitr_2}
\end{equation}
where $r_0=\{r^*, 0.99r^*, 1.01r^*\}$. [Figure 1](#fig1) shows the computational domain with an initial seed of radius $r=r^*$. The time evolution of the system is then followed for times up to $t = 200$ units, and the solid fraction $Y$ and the total free energy $F$ are plotted as functions of time. Finally, the problem also includes a convergence test with respect to mesh (spatial resolution). The closer the initial radius $r$ is to the critical radius $r^*$, the more sensitive the numerical integration will be to round-off errors. Therefore, a convergence test is included for $r_0=1.01r^*$ using successive runs each halving the average spatial mesh size $\Delta x$ between consecutive runs. At least 3 runs are required to estimate the rate of convergence based on the $L^2$ error with the finest mesh being used as a gold standard. The simulations should be run to $t=200$ and the final phase field (at $t=200$) for each successive run should be submitted.

To submit results to the PFHub website, see the [submission guidelines](#Part-%28a%29) below.

         Illustration of the $100 \times 100$ computational domain in 2D.

## Part (b): multiple initial seeds

The second part of the homogeneous nucleation benchmark problem focuses on the kinetics of nucleation using Avrami plots and compares them to JMAK theory. To obtain reasonable statistics for these two parts, the simulation volume needs to be larger so it can encompass a larger number of smaller nuclei. We therefore increase the domain size to $500 \times 500$ units of length, and the driving force to $\Delta f = 1/(3 \sqrt{2})$, which corresponds to a critical radius of $r^*=1$. Random initial positions $\mathbf{r}_i$ of 25 supercritical seeds $i, i=1,\ldots,25$ are generated with $r_0=2.2r^*$ drawn from a uniform distribution on the $500\times500$ domain. The distribution of the phase field $\phi$ is the sum of the phase fields $\phi_i$ with profiles
\begin{equation}
    \phi_i(r)=\frac{1}{2}\left[1-\tanh\left(
    \frac{|\mathbf{r}-\mathbf{r}_i|-2.2r^*}{\sqrt{2}}
    \right)\right].
    \label{eqn:phitr_3}
\end{equation}
from the different seeds $i$. Overlaps between different seeds are managed by setting $\phi=1$ in all regions where the sums of $\phi_i >1$. After placing initial seeds and adjusting the initial phase field $\phi$ so that $\phi\leq1$ everywhere, the simulation is run up to total time $t=200$, at which time the whole domain is transformed to a solid ($\phi=1$).  This is repeated four times, each time with different random seeds, and the total free energy, time evolution of the solid fraction $Y$, discrete particle count $N$  (the number of disjoint regions with $\phi=1$) are plotted for the five simulations, and Avrami plots are generated from the five simulations; from the Avrami plot the exponent $n$ is estimated and compared to the JMAK theory.

Both Part (b) and Part (c) involve a random number generator. Because different executions will in general generate different random numbers, specific individual solutions will in general not be reproduced. However, statistics such as the average slope of Avrami plots can be meaningfully compared.

To submit results to the PFHub website, see the [submission guidelines](#Part-%28b%29) below.

## Part (c): multiple seeds at random times

For the third part of the homogeneous benchmark problem, the computational domain is first expanded to $1000 \times1000$. Instead of inserting nuclei at fixed time $t = 0$ as in part two of the problem, in this case 100 random nucleation times $t_i$ are generated, $i=1,\ldots,100$, drawn from a uniform distribution in the interval $t_i \in [0, 600)$ with centers $\mathbf{r}_i$ drawn from a uniform distribution on the $1000\times1000$ domain.
We decrease the driving force to $\Delta f=1/(6\sqrt{2})$, which corresponds to a critical radius or $r^*=2$, but keep the initial radii of the inserted nuclei the same as those in the second part of the problem
\begin{equation}
    \phi_i(r)=\frac{1}{2}\left[1-\tanh\left(
    \frac{|\mathbf{r}-\mathbf{r}_i|-1.1r^*}{\sqrt{2}}
    \right)\right].
\end{equation}

Again, $\phi$ is set to unity in regions of overlaps of nuclei. The simulations are run up to times $t=600$, and repeated for four more random initial conditions.  The total free energy plot the total free energy, the time evolution of the solid fraction $Y$ and the discrete particle count $N$, are plotted as functions of time, and an  Avrami plot is generated for the five runs.

To submit results to the PFHub website, see the [submission guidelines](#Part-%28c%29) below.

## Submission Guidelines

Please use the [upload form]({{ site.baseurl }}/simulations/upload_form/) to upload your results.

In addition to that specified, further data to upload can include a Youtube video, images of the nuclei at different times, or the entire phase field variable at different times. This data can be uploaded directly to the website or stored at a secondary location and only the link to the data provided. This data is not required, but will help others view your work.

### Part (a)

Part (a) requires running three simulations with a single seed with different values of $r_0$ using the model formulation [defined above](#Part-%28a%29:-single-seed). Optionally, a further two simulations can be run to check convergence, but this is not required. The three required  simulations and two optional simulations are

 1. $r_0 = 0.99 r^*$, ${\Delta x} = {\Delta x}_0$
 1. $r_0 = r^*$, ${\Delta x} = {\Delta x}_0$
 1. $r_0 = 1.01 r^*$, ${\Delta x} = {\Delta x}_0$
 1. $r_0 = 1.01 r^*$, ${\Delta x} = {\Delta x}_1$ (optional)
 1. $r_0 = 1.01 r^*$, ${\Delta x} = {\Delta x}_2$ (optional)

where ${\Delta x}_0, {\Delta x}_1$ and ${\Delta x}_2$ represent simulations at different mesh sizes. $\Delta x$ refinement should be selected so that successively refined meshes have roughly $4\times$ as many elements. When uploading the results, the $\Delta x$ values should be provided in the datafile alongside the values for $\phi$ at each location in the domain. Each simulation should be run to $t=200$ and the following data should be collected.

 - The solid fraction in the domain (the integral $\int_V \phi dV / \int_V dV$) during the transient (not necessarily at every time step but for ~100 data points).

 - The free energy integrated over the whole domain, $\mathcal{F}$, during the transient.

 - The phase field, $\phi$, at each grid point in the domain at $t=200$.

 - The grid spacing, $\Delta x$ at each grid point in the domain at $t=200$ if submitting the optional simulations.

One PFHub upload is required for all the simulations. The solid fraction and free energy data can be included in the same CSV file or in separate data files, but either way still require 2 entries in the "Data Files" section on the [upload form]({{ site.baseurl }}/simulations/upload_form/). The entries should be labeled as `solid_fraction_{i}` or `free_energy_{i}` depending on the simulation number (1 to 5). The CSV (JSON or TSV are also fine) files should have a format similar to the following.

```
time,fraction,energy
0.000000000000000000e+00,8.214174263100689974e-03,3.579809478015085311e+00
1.000000000000000056e-01,8.205027223394931180e-03,3.577696619091880859e+00
2.000000000000000111e-01,8.197361418448430651e-03,3.576379039721957032e+00
...
```

The names of the columns are arbitrary as long as they are given in "Name of x-axis column" and "Name of y-axis column" in the form. The phase field data should be named `phase_field_{i}` in the "Short name of data" entry and have the following format

```
x,y,phi,delta_x
0.0,1.0,0.5,0.44
0.0,1.5,0.49,0.44
...
```

where `x` and `y` is a position in the domain and `phi` is the value at that position. `delta_x` is the nominal grid spacing in that region given by the coordinates. This may be constant across the domain for gridded data, but vary for adaptive meshing. `delta_x` is only a representative value of the grid spacing in that region. The "3D" radio button should be selected for the `phase_field_{i}` entry.

The grid spacing data should be named `grid_spacing_{i}` and have exactly the same data as above. Again the same file can be used, but two "Data Files" entries are required for both phase field and grid spacing. The "3D" radio button should be selected for the `grid_spacing_{i}` entry.

The `grid_spacing_{i}` entries are not required, but if included will show convergence data for simulations 3, 4 and 5.

### Part (b)

Part (b) requires running 5 simulations with multiple seeds randomly positioned at $t=0$, see the [defintion above](#Part-%28b%29:-multiple-initial-seeds). The simulations are run to $t=200$. The following data should be collected.

 - The solid fraction in the domain (the integral $\int_V \phi dV / \int_V dV$) during the transient (not necessarily at every time step but for ~100 data points).

 - The free energy integrated over the whole domain, $\mathcal{F}$, during the transient.

 - The discrete particle count, $N$, during the transient.

One PFHub upload is required with five data files, but with a total of 15 entries in the "Data Files" section. The solid fraction, free energy and particle count can be included in the same data file for each simulation. The entries should be labeled as `solid_fraction_{i}`, `free_energy_{i}` or `particle_count_{i}` depending on the simulation number (1 to 5). The CSV (JSON or TSV are also fine) files should have a format similar to the following.

```
time,fraction,energy,particle_count
0.000000000000000000e+00,8.214174263100689974e-03,3.579809478015085311e+00,24
1.000000000000000056e-01,8.205027223394931180e-03,3.577696619091880859e+00,24
2.000000000000000111e-01,8.197361418448430651e-03,3.576379039721957032e+00,23
...
```

### Part (c)

Part (c) requires running 5 simulations with seeds randomly appearing between $t=0$ and $t=600$, see the [definition above](#Part-%28c%29:-multiple-seeds-at-random-times). The simulations are run to $t=600$. The following data should be collected.

 - The solid fraction in the domain (the integral $\int_V \phi dV / \int_V dV$) during the transient (not necessarily at every time step but for ~100 data points).

 - The free energy integrated over the whole domain, $\mathcal{F}$, during the transient.

 - The discrete particle count, $N$, during the transient.

One PFHub upload is required with five data files, but with a total of 15 entries in the "Data Files" section. The solid fraction, free energy and particle count can be included in the same data file for each simulation. The entries should be labeled as `solid_fraction_{i}`, `free_energy_{i}` or `particle_count_{i}` depending on the simulation number (1 to 5). The CSV (JSON or TSV are also fine) files should have a format similar to the following.

```
time,fraction,energy,particle_count
0.000000000000000000e+00,8.214174263100689974e-03,3.579809478015085311e+00,0
1.000000000000000056e-01,8.205027223394931180e-03,3.577696619091880859e+00,1
2.000000000000000111e-01,8.197361418448430651e-03,3.576379039721957032e+00,1
...
```

Please use the [upload form]({{ site.baseurl }}/simulations/upload_form/) to upload your results.

## Results

Results from this benchmark problem are displayed on the [simulation result page]({{ site.baseurl }}/simulations/8a.1) for different codes.

## References

[^][^]Gránásy, L and and Börzsönyi, T and Pusztai, T. 2002. _Nucleation and bulk crystallization in binary phase field theory_. [URL](https://doi.org/10.1103/PhysRevLett.88.206105)

[^]Castro, Mario. 2003. _Phase-field approach to heterogeneous nucleation_. [URL](https://link.aps.org/doi/10.1103/PhysRevB.67.035412)

[^]Simmons, J P and Wen, Y and Shen, C and Wang, Y. 2004. _Microstructural development involving nucleation and growth phenomena simulated with the Phase Field method_. [URL](https://doi.org/10.1016/j.msea.2003.09.019)

[^]Gránásy, L and T and Pusztai, T and Saylor, D and Warren, J A. 2007. _Phase field theory of heterogeneous crystal nucleation_. [URL](https://doi.org/10.1103/PhysRevLett.98.035703)

[^]Warren, J A and Pusztai, T and Gránásy, L K and Gránásy, L. 2009. _Phase field approach to heterogeneous crystal nucleation in alloys_. [URL](https://doi.org/10.1103/PhysRevB.79.014204)

[^]Heo, T W and Chen, L. 2014. _Phase-field modeling of nucleation in solid-state phase transformations_. [URL](https://doi.org/10.1007/s11837-014-1033-9)

[^]Gránásy, L and Tóth, G and Warren, J A and Podmaniczky, F and Tegze, G and Rátkai, L and Pusztai, T. 2019. _Phase-field modeling of crystal nucleation in undercooled liquids -- A review_. [URL](https://doi.org/10.1016/j.pmatsci.2019.05.002)

[^]Kubo, R. 1966. _The fluctuation-dissipation theorem_. [URL](https://doi.org/10.1088%2F0034-4885%2F29%2F1%2F306)

[^]Simmons, J P and Shen, C and Wang, Y. 2000. _Phase field modeling of simultaneous nucleation and growth by explicitly incorporating nucleation events_. [URL][simmons_url]

[^]Shi, R and Khairallah, S and Heo, T W and Rolchigo, M and McKeown, J T and Matthews, M J. 2019. _Integrated simulation framework for additively manufactured Ti-6Al-4V: melt pool dynamics, microstructure, solid-state phase transformation, and microelastic response_. [URL](https://doi.org/10.1007/s11837-019-03618-1)

[^][^]Johnson, William A and Mehl, R F. 1939. _Reaction kinetics in processes of nucleation and growth_. [URL](https://doi.org/10.1007/s11663-010-9421-1)

[^][^]Avrami, Melvin. 1939. _Kinetics of phase change. I General theory_. [URL](https://doi.org/10.1063/1.1750380)

[^][^]Avrami, Melvin. 1940. _Kinetics of phase change. II transformation-time relations for random distribution of nuclei_. [URL](https://doi.org/10.1063/1.1750631)

[^][^]Avrami, Melvin. 1941. _Granulation, phase change, and microstructure kinetics of phase change. III_. [URL](https://doi.org/10.1063/1.1750872)

[^][^]Kolmogorov, A N. 1937. _On the Statistical Theory of the Crystallization of Metals_. [URL](https://doi.org/10.1007/978-94-011-2260-3_22)

[^]Allen, Samuel M and Cahn, John W. 1979. _A microscopic theory for antiphase boundary motion and its application to antiphase domain coarsening_.
[URL][allen_url]

[allen_url]: https://doi.org/10.1016/0001-6160(79)90196-2
[simmons_url]: https://doi.org/10.1016/S1359-6462(00)00517-0

## Appendix

The following code is given as a resource for those implementing the benchmark to help count the particles in Part (b) and Part (c). One possibility as outlined here is to use [`scipy.ndimage.label`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.label.html#scipy.ndimage.label). This returns a tuple with a labeled array of unique features as the first item and a count of features as the second item. In the case of the phase field the values will require thresholding to 0 or 1 before using `scipy.ndimage.label`.

```Python
import numpy as np
import scipy.ndimage
import matplotlib.pyplot as plt

def make_particles(shape, n_particles, max_radius, seed=99):
    """Make an array with random particles

    Args:
      shape: shape of array
      n_particles: number of particles in arrau
      max_radius: maximum possible radius of particles
      seed: random seed

    Returns: numpy array mask with particles
    """
    np.random.seed(seed)

    def xy_grid(shape):
        """Generate a grid of x, y values in a single array

        The returned array has shape (len(shape),) + shape + (1,). The last axis is
        for the operations with particles.
        """
        linspace = lambda x: np.linspace(0, x - 1, x) + 0.5
        return np.array(np.meshgrid(*map(linspace, shape), indexing="ij"))[..., None]

    # first axis is dimensions
    # second and third axes are shape of domain
    # last axis is the number of particles.
    centers = (np.random.random((len(shape), n_particles)) * np.array(shape)[:, None])[:, None, None]
    radii = (np.random.random(n_particles) * max_radius)[None, None]
    return numpy.any(((xy_grid(shape) - centers)**2).sum(0) < radii**2, axis=-1)

arr = make_particles((100, 200), 20, 10)
plt.imshow(arr)
print('Actual number of particles:', scipy.ndimage.label(arr)[1])
```

```python
import numpy as np
import scipy.ndimage
import matplotlib.pyplot as plt

def make_particles(shape, n_particles, max_radius, seed=99):
    """Make an array with random particles

    Args:
      shape: shape of array
      n_particles: number of particles in arrau
      max_radius: maximum possible radius of particles
      seed: random seed

    Returns: numpy array mask with particles
    """
    np.random.seed(seed)

    def xy_grid(shape):
        """Generate a grid of x, y values in a single array

        The returned array has shape (len(shape),) + shape + (1,). The last axis is
        for the operations with particles.
        """
        linspace = lambda x: np.linspace(0, x - 1, x) + 0.5
        return np.array(np.meshgrid(*map(linspace, shape), indexing="ij"))[..., None]

    # first axis is dimensions
    # second and third axes are shape of domain
    # last axis is the number of particles.
    centers = (np.random.random((len(shape), n_particles)) * np.array(shape)[:, None])[:, None, None]
    radii = (np.random.random(n_particles) * max_radius)[None, None]
    return numpy.any(((xy_grid(shape) - centers)**2).sum(0) < radii**2, axis=-1)

arr = make_particles((100, 200), 20, 10)
plt.imshow(arr)
print('Actual number of particles:', scipy.ndimage.label(arr)[1])
```