```python
from sympy import symbols, init_printing, pi, sqrt, solveset, Eq
from scipy.constants import c
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import (MultipleLocator, FormatStrFormatter,
                               AutoMinorLocator)

from ipywidgets.widgets import (
    interactive_output,
    BoundedFloatText,
    BoundedIntText,
    HBox,
    VBox,
    IntRangeSlider,
    Text,
    Button,
    Label,
    Layout,
    Checkbox
)
from ipywidgets import interact

from IPython.display import display
import datetime, os
from os.path import isdir
from pathlib import Path
```

```python
init_printing()
save_location = Path(r"D:\SurfDrive\backup_for_surfacePro\Work\THz_setup\Simulations")

# Defining functions
def ghz_cm(x):
    """
    Input: freq in GHZ
    Output: freq in cm
    """
    return c * 1e2 / x * 1e-9

cm_ghz = lambda x: c*1e-9*1e2/x

print(f"Save location: {save_location}")
save_location.exists()
```

    Save location: D:\SurfDrive\backup_for_surfacePro\Work\THz_setup\Simulations

    True

```python
# Symbols
w, lam, w0, z, Z_r = symbols("w, lam, w0, z, Z_r")
M, E, d_in, d_out, w0_in, w0_out, f, f0 = symbols('M, E, d_in, d_out, w0_in, w0_out, f, f0')
```

```python
## Formulas

# Beam radius
Wz = Eq(w, w0 * sqrt(1 + (z / Z_r) ** 2))
Wz
```

```python
# Focal length
fp = Eq(f, Z_r*(d_in/(E*Z_r))*(1+sqrt(1-E*(1+(d_in/Z_r)**-2)))) # positive sign but output is negative focus
fm = Eq(f, Z_r*(d_in/(E*Z_r))*(1-sqrt(1-E*(1+(d_in/Z_r)**-2)))) # negative sign but output is positive focus

fp, fm
```

```python
# Input distance
eq_din_m = Eq(d_in, f - sqrt(f ** 2 - f0 ** 2) / M) # Negative sign
eq_din_p = Eq(d_in, f + sqrt(f ** 2 - f0 ** 2) / M) # Positive sign

# output distance
eq_dout_m = Eq(d_out, f - sqrt(f ** 2 - f0 ** 2) * M) # Negative sign
eq_dout_p = Eq(d_out, f + sqrt(f ** 2 - f0 ** 2) * M) # Positive sign
# NOTE: use same sign for output distance as chosed for input distane

# Input and output distances in-terms of each other
eq_dout2 = Eq(d_out, f * (1 + (M ** 2 * (d_in / f - 1))))
eq_din2 = Eq(d_in, f * (1 + sqrt(M ** -2 - (Z_r / f) ** 2)))

eq_din_m, eq_din_p, eq_dout_m, eq_dout_p, eq_din2, eq_dout2,
```

```python
class gaussian_simulation:

    def __init__(self):

        W = BoundedFloatText(value=0.34, min=0.1, max=1, step=0.01, description="w0:")
        L = IntRangeSlider(
            value=[325, 1100], min=50, max=1500, step=1, description="GHz"
        )

        width = BoundedIntText(value=7, min=5, max=25, step=1, description="width")
        height = BoundedIntText(value=5, min=5, max=25, step=1, description="height")
        dpi = BoundedIntText(value=200, min=70, max=250, step=1, description="Dpi")

        entry1 = BoundedFloatText(
            value=-9, min=-11, max=10, step=0.1, description="Entry1"
        )
        Mirror = BoundedIntText(value=11, min=10, max=20, step=1, description="Mirror Position")

        self.save_name = Text(value="")

        save_button = Button(description="Save", layout={"width": "300px"})

        save_button.on_click(self.save_plot)

        self.print_save = Label(value="Status: ")

        row1 = HBox([W, L])
        row2 = HBox([width, height, dpi])
        row3 = HBox([entry1, Mirror])

        out = interactive_output(
            self.simulate,
            {
                "w0_": W,
                "wn": L,
                "width": width,
                "height": height,
                "dpi": dpi,
                "entry1": entry1,
                "mirror":Mirror
            },
        )

        display(
            VBox(
                [row1, row2, row3, out, self.save_name, save_button, self.print_save]
            )
        )

    def save_plot(self, event):
        if not isdir('./Simulations'):
            os.mkdir('./Simulations')

        self.fig0.savefig(f"./Simulations/{self.save_name.value}.png")
        self.fig0.savefig(f"./Simulations/{self.save_name.value}.pdf")

        t = datetime.datetime.now()
        self.print_save.value = f'File "{self.save_name.value}.png" Saved; {t.ctime()}'

    def beam_plot(self, ax, w0_, wn):

        self.gaussian_beam(w0_, wn)

        markers = '--'
        line_width = 1
        alpha = 0.7

        for c, beam in enumerate(self.beams):
            color = f"C{c}"
            ax.plot(
                z_,
                beam,
                markers+color, lw=line_width, alpha=alpha,
                label=f"{self.wn[c]:.2f} GHz ({ghz_cm(self.wn[c])*10:.2f} mm)",
                zorder=1
            )
            ax.plot(-z_, -beam, markers+color, lw=line_width, alpha=alpha, zorder=1)
            ax.plot(-z_, beam, markers+color, lw=line_width, alpha=alpha, zorder=1)
            ax.plot(z_, -beam, markers+color, lw=line_width, alpha=alpha, zorder=1)

    def simulate(self, w0_, wn, width, height, dpi, entry1, mirror):

        entry1 = round(entry1, 1)
        entry2 = round(10.4 - abs(entry1), 1)

        self.fig0, ax0 = plt.subplots(figsize=(width, height), dpi=dpi)
        self.beam_plot(ax0, w0_, wn)

        ax0.hlines(0, xmin=-z_.max(), xmax=z_.max())

        ## Annotation begins

        fontsz = 6

        # Distance b/w two entry points
        '''ax0.hlines(-0.2, xmin=entry1, xmax=entry2)
        plt.annotate(
            s=f"d={(entry2-entry1)}",
            xy=(0, 0),
            xytext=(-3, -0.1),
            ha="center",
            va="center",
            fontsize=fontsz
        )'''

        # Annotate beam waist
        ax0.vlines(x=0, ymin=0, ymax=w0_)
        '''plt.annotate(
            s="$w_0^{in}$=%.2f cm"%w0_,
            xy=(0, w0_+0.2),
            xytext=(0, 0.8),
            ha="center",
            va="center",
            arrowprops=dict(arrowstyle="->", connectionstyle="arc3"),
            fontsize=fontsz
        )'''

        # Annotate Entry1
        ax0.vlines(x=entry1, ymin=-1, ymax=1)

        '''plt.annotate(
            s="Main entry window:\ndiameter=2cm",
            xy=(entry1, 1),
            xytext=(entry1 + 6, 1.1),
            ha="center",
            va="center",
            arrowprops=dict(arrowstyle="->", connectionstyle="arc3"),
            fontsize=fontsz
        )'''

        # Annotate Entry2
        ax0.vlines(x=entry2, ymin=-0.43, ymax=0.43)

        '''plt.annotate(
            s="Trap bender entry:\ndiameter=0.86cm",
            xy=(entry2, -0.43),
            xytext=(entry2 - 3, -0.8),
            ha="center",
            va="center",
            # rotation = 30,
            arrowprops=dict(arrowstyle="->", connectionstyle="arc3"),
            fontsize=fontsz
        )'''

        # Inner entry: diameter: 0.64cm
        ax0.vlines(x=entry2+0.7, ymin=-0.32, ymax=0.32)

        # Annotate z-position

        # plt.annotate(s="z=0", xy=(0, 0), xytext=(0, -0.05), ha="center", va="center",fontsize=fontsz)

        '''plt.annotate(
            s=f"z={entry1}",
            xy=(entry1, 0),
            xytext=(entry1 - 2, -0.2),
            ha="center",
            va="center",
            arrowprops=dict(arrowstyle="->", connectionstyle="arc3"),
            fontsize=fontsz
        )'''

        # d_in window
        mirror_pos = entry1-mirror
        ax0.vlines(x=mirror_pos, ymin=-self.beams.max(), ymax=self.beams.max())

        '''plt.annotate(
            s="Mirror($d_{in}$) at %.1f cm"%mirror_pos,
            xy=(mirror_pos+5, -self.beams.max()),
            ha="center",
            va="center",
            arrowprops=dict(arrowstyle="->", connectionstyle="arc3"),
            fontsize=fontsz
        )'''

        ## Annotation ends

        ax0.set_xlabel("z direction : Beam Propagation (cm)")
        ax0.set_ylabel("w(z) : Gaussian Beam radius (cm)")
        ax0.set_title(f"Gaussian Beam simulation: {wn[0]}-{wn[-1]} GHz")
        ax0.legend(bbox_to_anchor=(0.16, 0.86), ncol=3, fontsize=5)
        ax0.xaxis.set_minor_locator(AutoMinorLocator())

        plt.show()
        # plt.close()

        self.save_name.value = f"{w0_}cm-{wn[0]}-{wn[-1]}GHz_simulation"

    def gaussian_beam(self, w0_, wn):

        global z_
        z_ = np.linspace(0, 25, 10)

        self.wn = np.linspace(*wn, 10)

        beams = []
        for l in self.wn:
            temp = []
            zr = np.pi*w0_**2/ghz_cm(l)
            temp = np.array([float(
                    Wz.subs({w0:w0_, Z_r:zr, z:i}).rhs
                ) for i in z_])

            beams.append(temp)

        self.beams = np.array(beams)

start = gaussian_simulation()
```

    VBox(children=(HBox(children=(BoundedFloatText(value=0.34, description='w0:', max=1.0, min=0.1, step=0.01), In…

```python
class simulation:
    def __init__(self):

        # Beam properties

        W_in = BoundedFloatText(
            value=0.34, min=0.1, max=1, step=0.01, description="$w0_{in}$: "
        )

        D_in = BoundedFloatText(
            value=20, min=5, max=30, step=0.1, description="$d_{in}$: "
        )

        btn_pxl = 300
        L1 = Text(value="325-500", description="GHz", layout={"width": f"{btn_pxl}px"})

        L2 = Text(value="500-750", description="GHz", layout={"width": f"{btn_pxl}px"})

        L3 = Text(value="750-1100", description="GHz", layout={"width": f"{btn_pxl}px"})

        W_out1 = BoundedFloatText(
            value=0.15, min=0.01, max=1, step=0.001, description="$w0_{out}$: "
        )

        W_out2 = BoundedFloatText(
            value=0.099, min=0.01, max=1, step=0.001, description="$w0_{out}$: "
        )

        W_out3 = BoundedFloatText(
            value=0.066, min=0.01, max=1, step=0.001, description="$w0_{out}$: "
        )

        f_manual = Checkbox(value=False, description="f_manual")

        # Figure Properties
        Width = BoundedFloatText(value=15, min=5, max=25, step=1, description="width")
        Height = BoundedFloatText(value=6, min=5, max=25, step=1, description="height")
        Dpi = BoundedFloatText(value=100, min=70, max=300, step=1, description="Dpi")

        # Widget to save
        self.save_name = Text(value="THz_simulation_325-1100GHz")
        save_button = Button(description="Save", layout={"width": "300px"})
        save_button.on_click(self.save_plot)
        self.print_save = Label(value="Status: ")

        # Widget arrangements
        row1 = HBox([W_in, D_in])
        row2 = HBox([Width, Height, Dpi])
        row3 = HBox([L1, L2, L3])
        row4 = HBox([W_out1, W_out2, W_out3])
        row5 = VBox([self.save_name, save_button, self.print_save])

        # Plot output
        out = interactive_output(
            self.plot,
            {
                "w1": W_in,
                "d1": D_in,
                "l1": L1,
                "l2": L2,
                "l3": L3,
                "wout1": W_out1,
                "wout2": W_out2,
                "wout3": W_out3,
                "width": Width,
                "height": Height,
                "dpi": Dpi,
            },
        )

        # widget display
        display(VBox([row1, row2, row3, row4, out, row5]))

    def save_plot(self, event):

        try:
            if not save_location.exists():
                save_location.mkdir()

            self.fig.savefig(save_location.joinpath(self.save_name.value + ".png"))
            self.fig.savefig(save_location.joinpath(self.save_name.value + ".pdf"))

            self.fig.savefig(f"./Simulations/{self.save_name.value}.png")
            self.fig.savefig(f"./Simulations/{self.save_name.value}.pdf")

            t = datetime.datetime.now()
            self.print_save.value = (
                f'File "{self.save_name.value}.png" Saved; {t.ctime()}'
            )

        except:
            self.print_save.value = "Error Occured."

    def plot(self, w1, d1, l1, l2, l3, wout1, wout2, wout3, width, height, dpi):

        wn_size = 5
        wz_size = 20
        d_trap = -1.4
        d_entry = 9

        to_use = 0

        ##########################################################################################

        # opening log file

        if not isdir("./Simulations"):
            os.mkdir("./Simulations")
        t = datetime.datetime.now()
        log_file = open(f"./Simulations/log_file_{to_use}.txt", "w")
        log_file.write(f"Gaussian Simulation: {t.ctime()}\n\n")
        log_file.write(
            "##########################################################################################\n"
        )

        # 3-sets of wavelength range for 3-different horn
        l1 = tuple(np.array(l1.split("-"), dtype=np.int))
        l2 = tuple(np.array(l2.split("-"), dtype=np.int))
        l3 = tuple(np.array(l3.split("-"), dtype=np.int))

        wn_set1 = np.linspace(*l1, wn_size)
        wn_set2 = np.linspace(*l2, wn_size)
        wn_set3 = np.linspace(*l3, wn_size)
        wn_list = np.array([wn_set1, wn_set2, wn_set3])

        # 3-sets of horns waist for different wavelength
        w0_out_list = np.array([wout1, wout2, wout3])

        w0_out_in_use = w0_out_list[to_use]
        M_factor = w0_out_in_use / w1

        w0_in_list = w0_out_list / M_factor
        M_list = w0_out_list / w0_in_list

        # Focal length and d_out in-use (Taking optimal focal length for the longest wn)
        wn_in_use = wn_list[to_use, 0]
        M_in_use = M_list[to_use]

        zc_in_use = np.pi * w1 ** 2 / ghz_cm(wn_in_use)

        f_positive = float(
            fm.subs({d_in: d1, E: (1 - M_in_use ** -2), Z_r: zc_in_use}).rhs
        )
        f_negative = float(
            fp.subs({d_in: d1, E: (1 - M_in_use ** -2), Z_r: zc_in_use}).rhs
        )

        opt_d2 = float(eq_dout2.subs({f: f_positive, M: M_in_use, d_in: d1}).rhs)
        print(f_positive, opt_d2)

        log_file.write(
            f"\nFocii:\nf(+ve): {f_positive:.2f}; f(-ve): {f_negative:.2f}\n\n"
        )

        ##########################################################################################

        # Defining figure:
        self.fig, (ax1, ax2) = plt.subplots(
            nrows=1, ncols=2, sharey=True, figsize=(width, height), dpi=dpi
        )

        for ax in self.fig.axes:
            ax.grid()

        colors = "C0, C1, C2, C3, C4, C5, C6, C7, C8, C9".split(", ")
        mrkr = [".-", "x-", "--"]

        d1_beam = []
        d2_beam = []

        d1_distance = []
        d2_distance = []

        ##########################################################################################
        log_file.write(
            "##########################################################################################\n\n"
        )

        for num in range(len(w0_out_list)):

            log_file.write(f"w0_out: {w0_out_list[num]}cm\n\n")

            d1_beam_temp = []
            d2_beam_temp = []

            d1_distance_temp = []
            d2_distance_temp = []

            #################################################################################

            for c, wn in enumerate(wn_list[num]):

                log_file.write(f"Frequency: {wn}GHz\n")

                #################################################################################
                # optimised calculation
                temp_f0 = (np.pi * w0_in_list[num] * w0_out_list[num]) / ghz_cm(wn)

                temp_d1 = eq_din_p.subs(
                    {M: M_list[num], f0: temp_f0, f: f_positive}
                ).rhs
                temp_d2 = eq_dout_p.subs(
                    {M: M_list[num], f0: temp_f0, f: f_positive}
                ).rhs

                '''print(
                    f"\nw0_in: {w0_in_list[num]}; w0_out: {w0_out_list[num]}; M: {M_list[num]} ({w0_out_list[num]/w0_in_list[num]})"
                )
                print(f"For wn: {wn}\nd1: {temp_d1}\nd2: {temp_d2}\n")'''

                temp_d1 = float(temp_d1)
                temp_d2 = float(temp_d2)

                #################################################################################
                # d_in beam optimised
                d1_beam_array = np.linspace(temp_d1 - d1 + d_trap, temp_d1, wz_size)
                zr1 = (np.pi * w0_in_list[num] ** 2) / ghz_cm(wn)

                d1_beam_calculation = np.array(
                    [
                        float(Wz.subs({w0: w0_in_list[num], Z_r: zr1, z: i}).rhs)
                        for i in d1_beam_array
                    ]
                )

                #################################################################################
                # d_out beam optimised
                d2_beam_array = np.linspace(0, temp_d2, wz_size)
                zr2 = (np.pi * w0_out_list[num] ** 2) / ghz_cm(wn)

                d2_beam_calculation = np.array(
                    [
                        float(Wz.subs({w0: w0_out_list[num], Z_r: zr2, z: i}).rhs)
                        for i in d2_beam_array
                    ]
                )

                #################################################################################

                # Plotting
                d1_plot = np.linspace(d_trap, d1, wz_size)
                d2_plot = np.linspace(opt_d2 - temp_d2, opt_d2, wz_size)

                ax1.plot(
                    d2_plot,
                    d2_beam_calculation,
                    colors[c] + mrkr[num],
                    zorder=1,
                    label=f"{wn:.1f}GHz",
                )
                ax1.plot(d2_plot, -d2_beam_calculation, colors[c] + mrkr[num], zorder=1)

                ax2.plot(d1_plot, d1_beam_calculation, colors[c] + mrkr[num], zorder=1)
                ax2.plot(d1_plot, -d1_beam_calculation, colors[c] + mrkr[num], zorder=1)

                #################################################################################
                # Writing log file
                log_file.write(
                    f"d_in:\n{d1_beam_array}\nd_in_beam: {d1_beam_calculation}\n\n"
                )
                log_file.write(
                    f"d_out:\n{d2_beam_array}\nd_out_beam: {d2_beam_calculation}\n\n"
                )

                #################################################################################
                # Appending temp values
                d1_distance_temp.append(temp_d1)
                d2_distance_temp.append(temp_d2)

                d1_beam_temp.append(d1_beam_calculation)
                d2_beam_temp.append(d2_beam_calculation)

                #################################################################################
            #####################################################################################
            log_file.write(
                "##########################################################################################\n\n"
            )

            # Appending main values
            d1_distance.append(d1_distance_temp)
            d2_distance.append(d2_distance_temp)

            d1_beam.append(d1_beam_temp)
            d2_beam.append(d2_beam_temp)

            ######################################################################################
        ##########################################################################################
        # Making array
        d1_distance = np.array(d1_distance)
        d2_distance = np.array(d2_distance)

        d1_beam = np.array(d1_beam)
        d2_beam = np.array(d2_beam)

        ##########################################################################################
        log_file.write(f"w0_in_list: {w0_in_list}\n\n")
        log_file.write(f"w0_out_list: {w0_out_list}\n\n")
        log_file.write(f"Frequency list:\n{wn_list}\n\n")
        log_file.write(f"d_out: (Calibrated)\n{d2_distance}\n\n")
        log_file.write(f"d_in: (Calibrated)\n{d1_distance}\n\n")
        log_file.write(
            f"Mirror Diameter: >= {3*d1_beam[0, 0, -1]:.2f}cm\n(since longest wavelength beam waist at mirror is {d1_beam[0, 0, -1]:.2f}cm)\n\n"
        )

        ##########################################################################################
        # ax1
        ax1.set_xlim(xmax=opt_d2)

        ax1.set(
            xlabel="$d_{out}$ (cm)",
            ylabel="$w(z)$ cm",
            title=f"THz source to Mirror w0_out: {w0_out_list} cm",
        )
        ax1.legend(ncol=3)

        ax1.xaxis.set_minor_locator(AutoMinorLocator(10))

        ##########################################################################################
        # ax2
        ax2.invert_xaxis()
        ax2.set_xlim(xmin=d1)

        ax2.set(xlabel="$d_{in}$ (cm)", title="Mirror to trap")
        ax2.legend(
            title=f"f_+ = {f_positive:.2f}cm\nf_- = {f_negative:.2f}cm\nMirror Diameter >= {d2_beam.max()*3:.2f}cm"
        )

        ax2.xaxis.set_minor_locator(AutoMinorLocator(10))

        # vlines
        ax2.vlines(0, ymin=-w1, ymax=w1)
        ax2.vlines(d_trap, ymin=-0.43, ymax=0.43)
        ax2.vlines(d_entry, ymin=-1, ymax=1)
        ax2.annotate(s="Main Entry", xy=(d_entry, 0), xytext=(d_entry, 0), ha="right")

        ##########################################################################################

        plt.tight_layout()

        ##########################################################################################
        log_file.write(
            "##########################################################################################\n"
        )
        log_file.write(
            "##########################################################################################\n\n"
        )

        log_file.close()
        ##########################################################################################
        self.save_name.value = f"THz_simulation_{l1[0]}-{l3[-1]} GHz_{to_use}"

run = simulation()
```

    VBox(children=(HBox(children=(BoundedFloatText(value=0.34, description='$w0_{in}$: ', max=1.0, min=0.1, step=0…

```python
M1 = 0.15 / 0.34
zc_1 = np.pi * 0.34 ** 2 / ghz_cm(325)
f_positive = float(fm.subs({d_in: 20, E: (1 - M1 ** -2), Z_r: zc_1}).rhs)
f_negative = float(fp.subs({d_in: 20, E: (1 - M1 ** -2), Z_r: zc_1}).rhs)
temp_f0 = (np.pi * 0.15 * 0.34) / ghz_cm(325)
d1_ = eq_din_p.subs({M: 0.15/0.34, f0: (np.pi * 0.15*0.34) / ghz_cm(325), f: f_positive})
f_negative, f_positive, d1_
```

```python
wn_range = (110, 170)
wout_0 = 0.41
win_0 = wout_0/M1
```

```python
win_0 = wout_0/M1
temp_d1 = np.array([eq_din_p.subs({M: wout_0/win_0, f0: (np.pi * wout_0 * win_0) / ghz_cm(i), f: f_negative}).rhs for i in np.linspace(*wn_range, 10)])
temp_d2 = np.array([eq_dout_p.subs({M: wout_0/win_0, f0: (np.pi * wout_0 * win_0) / ghz_cm(i), f: f_negative}).rhs for i in np.linspace(*wn_range, 10)])
print(f"d1: {temp_d1}\n\nd2: {temp_d2}")
```

    d1: [18.8179676796008 18.6395519873133 18.4496329223780 18.2480188841159
     18.0345017867026 17.8088558394502 17.5708361733380 17.3201772920709
     17.0565913220015 16.7797660304816]

    d2: [-9.19022292861818 -9.22494916630390 -9.26191438222989 -9.30115586891585
     -9.34271412749541 -9.38663310511808 -9.43296046227660 -9.48174787428792
     -9.53305137192255 -9.58693172710591]

```python
temp_d3 = np.array([eq_din_p.subs({M: wout_0/win_0, f0: (np.pi * wout_0 * win_0) / ghz_cm(i), f: f_positive}).rhs for i in np.linspace(*wn_range, 10)])
temp_d4 = np.array([eq_dout_p.subs({M: wout_0/win_0, f0: (np.pi * wout_0 * win_0) / ghz_cm(i), f: f_positive}).rhs for i in np.linspace(*wn_range, 10)])
print(f"d1: {temp_d3}\n\nd2: {temp_d4}")
```

    d1: [16.5045470368126 15.8794018450265 15.1696299654552 14.3529076247493
     13.3924239970903 12.2186911690861 10.6631716441500 7.82831715910445
     6.29209498753254 + 3.88773139936198*I
     6.29209498753254 + 5.77208721729118*I]

    d2: [8.27981273068826 8.15813654716588 8.01998890624586 7.86102478283856
     7.67407944008488 7.44562798134703 7.14286682813715 6.59110016798555
     6.29209498753254 + 0.756695125308343*I
     6.29209498753254 + 1.12345988225823*I]

```python
temp1 = temp_d1/temp_d3
temp2 = temp_d2/temp_d4
temp1, temp2
```

    (array([1.14016868428005, 1.17381952854548, 1.21622168532733,
            1.27138133688327, 1.34661968517580, 1.45750928581512,
            1.64780580860083, 2.21250326731171,
            1.96182908283797 - 1.21216614508257*I,
            1.44813570461284 - 1.32845508627273*I], dtype=object),
     array([-1.10995540932412, -1.13076670302958, -1.15485376482464,
            -1.18319889910807, -1.21743776572009, -1.26069058629221,
            -1.32061267404823, -1.43856831676492,
            -1.49348392279491 + 0.179608223706818*I,
            -1.47657317794774 + 0.263643624568573*I], dtype=object))

```python
for i in range(0, len(temp1)-1):
    print(i+1,i, temp1[i+1]-temp1[i])
```

    1 0 0.0336508442654337
    2 1 0.0424021567818518
    3 2 0.0551596515559418
    4 3 0.0752383482925294
    5 4 0.110889600639314
    6 5 0.190296522785709
    7 6 0.564697458710883
    8 7 -0.250674184473737 - 1.21216614508257*I
    9 8 -0.513693378225135 - 0.116288941190153*I

```python
for i in range(0, 10, 2):
    print(i)
```

    0
    2
    4
    6
    8

```python
f_1 = f_negative
temp_d1 = [float(eq_din_p.subs({M: M1, f0: (np.pi * 0.15 * 0.34) / ghz_cm(i), f: f_1}).rhs) for i in np.linspace(325, 500, 5)]
temp_d2 = [float(eq_dout_p.subs({M: M1, f0: (np.pi * 0.15 * 0.34) / ghz_cm(i), f: f_1}).rhs) for i in np.linspace(325, 500, 5)]

fig, ax = plt.subplots()
ax.plot(np.linspace(325, 500, 5), temp_d1)
ax.twinx().plot(np.linspace(325, 500, 5), temp_d2)

temp_d1, temp_d2
```

```python
temp_d1 = [float(eq_din_p.subs({M: M1, f0: (np.pi * 0.15 * 0.34) / ghz_cm(i), f: f_negative}).rhs) for i in np.linspace(325, 500, 5)]
temp_d2 = [float(eq_dout_p.subs({M: M1, f0: (np.pi * 0.15 * 0.34) / ghz_cm(i), f: f_positive}).rhs) for i in np.linspace(325, 500, 5)]

fig, ax = plt.subplots()
ax.plot(np.linspace(325, 500, 5), temp_d1)
ax.twinx().plot(np.linspace(325, 500, 5), temp_d2)

temp_d1, temp_d2
```

```python
M1 = 0.15 / 0.34
temp0 = np.array([float(eq_din_p.subs({M: M1, f0: (np.pi * 0.15 * 0.34) / ghz_cm(i), f: f_negative}).rhs) for i in np.linspace(325, 1100, 5)])

fig, ax = plt.subplots()

x0, y0 = [], []
x1, y1 = [], []

for i in np.linspace(325, 1100, 5):

    zr = np.pi*0.34**2/ghz_cm(i)

    temp_y0 = f_positive/zr
    temp_x0 = float(eq_din_p.subs({M: M1, f0: (np.pi * 0.15 * 0.34) / ghz_cm(i), f: f_positive}).rhs)/zr
    x0.append(temp_x0)
    y0.append(temp_y0)

    temp_y1 = f_negative/zr
    temp_x1 = float(eq_din_p.subs({M: M1, f0: (np.pi * 0.15 * 0.34) / ghz_cm(i), f: f_negative}).rhs)/zr
    x1.append(temp_x1)
    y1.append(temp_y1)

print(x0, x1)
ax.plot(x1+x0, y1+y0, 'o')
```

```python
[float(eq_din_p.subs({M: M1, f0: (np.pi * 0.15 * 0.34) / ghz_cm(i), f: f_positive}).rhs) for i in np.linspace(325, 1100, 5)]
```

```python

```

```python
fe, f1 ,f2 = symbols("fe, f1 ,f2")
eq_fe = Eq(fe, (f1*f2)/(f1+f2))
eq_fe.subs({f1:15.96, f2:6.29})

M1
```

```python
eq_din_p.rhs/Z_r,
```

```python
solveset(eq_dout2.subs({d_in:20, d_out:8.9, M:0.44}), f)
```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```


# Eruption and Evolution of Geothermally Cooled Magma Bodies

David Dempsey, Darren Gravley, Julie Rowland, in *Proceedings 40th New Zealand Geothermal Workshop*, Taupo, New Zealand (2018)

This Jupyter Notebook contains an executable version of the model presented in the paper above. This model is based on the one presented in "A model for eruption frequency of silicic magma chambers", Degruyter, W., and C. Huber, *Earth Planet. Sci. Lett., 403*, 117-130 (2014). Modifications have been made to account for overlying geothermal systems. For more details on the physics, see Section **[Governing Equations](#Governing-Equations)**.

## Instructions

**Install** Anaconda Python which bundles Jupyter Notebooks (honestly, if you're reading this, I assume you got that far.)

**Construct a conceptual model** of the magma chamber you are interested in simulating. Pay particular attention to the limitations of your knowledge, the subsequent uncertainty in model parameters, and shortcomings of the model implemented here (detailed in Section **[Model Shortcomings](#Model-Shortcomings)**).

**Copy-paste** the Python cell in the Section **[Demonstration](#Demonstration)** and make modifications to the parameters to implement your conceptual model (see Section **[Model Parameters](#Model-Parameters)** for a full list).

Run your model by executing the cell (**Ctrl+Enter**) and then **use the dashboard** to investigate how the different parameters change over time. Save a figure for later using the SAVE button.

You can **compare** up to three models at a time to investigate the effect of varying a particular parameter. See Section **[Comparing Models](#Comparing-Models)**.

Model not doing what you want it to do? Python code is all open for you to modify, see file `magmageotherm.py`. If you make some cool changes, consider sharing them with me so I can reap the rewards of your hard work.

## Model Parameters

The table below is an exhaustive list of the configurable model parameters, their default values, and the corresponding Python variable to change them. ***You only need to define these if you DON'T want default values*** in your model.

Notation and values after *Degruyter and Huber* (2014), and *Dempsey et al.* (2018).

   parameter                               |   symbol          | Python variable | default              | units
  :--------------------------------------: | :---------------: | :-------------: | :-----------------:  | :---:
  **Initial conditions**                   |                   |                 |                      |
  Pressure                                 |                   | `Pi`            | $P_{lith}$           | Pa
  Temperature                              |                   | `Ti`            | 1200                 | K
  Volume                                   |                   | `Vi`            | 3$\times$10$^6$      | m$^3$
  Gas fraction                             |                   | `egi`           | 0.04                 |
  Melt density                             |                   | `pmi`           | 2400                 | kg m$^{-3}$
  Crystal density                          |                   | `pXi`           | 2600                 | kg m$^{-3}$
  **Magma body properties**                |                   |                 |                      |
  Power law exponent melting curve         | $b$               | `b`             | 0.5                  |
  Specific heat (melt)                     | $c_m$             | `cm`            | 1205                 | J kg$^{-1}$K$^{-1}$
  Specific heat (crystals)                 | $c_X$             | `cX`            | 1315                 | J kg$^{-1}$K$^{-1}$
  Specific heat (gas)                      | $c_g$             | `cg`            | 3900                 | J kg$^{-1}$K$^{-1}$
  Latent heat of exsolution                | $L_e$             | `Le`            | 6.1$\times$10$^5$    |	J kg$^{-1}$
  Latent heat of melting                   | $L_m$             | `Lm`            | 2.9$\times$10$^5$    |	J kg$^{-1}$
  Lithostatic Pressure                     | $P_{lith}$        | `Plith`         | 3$\times$10$^8$      | Pa
  Liquidus temperature                     | $T_l$             | `Tl`            | 1223                 | K
  Solidus temperature                      | $T_s$             | `Ts`            | 973                  | K
  Sill burial depth (to top)               | $z_{mb}$               | `z`             | 5000                 | m
  Thermal expansion (melt)                 | $\alpha_m$        | `am`            | 1$\times$10$^{-5}$   | K$^{-1}$
  Thermal expansion (crystals)             | $\alpha_X$        | `aX`            | 1$\times$10$^{-5}$   | K$^{-1}$
  Compressibility (melt)                   | $\beta_m$         | `bm`            | 1$\times$10$^{10}$   | Pa
  Compressibility (crystals)               | $\beta_X$         | `bX`            | 1$\times$10$^{10}$   | Pa
  Critical pressure (dike)                 | $\Delta P_{cd}$   | `Pc`            | 2$\times$10$^7$      | Pa
  Sill thickness                           | $\Delta z$        |  `dz`	         | 1000                 | m
  Critical locking fraction                | $\epsilon_{Xc}$   |  `eXc`	         | 0.5                  |
  **Boundary conditions**                  |                   |                 |                      |
  Eruption rate                            | $\dot{M}_{erupt}$ | `Merupt`        | 1$\times$10$^4$      | kg s$^{-1}$
  Magma influx rate                        | $\dot{M}_{in}$    | `Min`           | 1                    | kg s$^{-1}$
  Atmospheric temperature                  | $T_{atm}$         | `Tatm`          | 298 (25$^{\circ}$C)  | K.
  Incoming magma temperature               | $T_{in}$          | `Tin`           | 1200                 | K
  Mass fraction of incoming volatiles      |                   | `Mwin_frac`     | 0.05                 |
  Post eruption underpressure              | $\Delta P_c$      | `uPc`           | 0                    | MPa
  **Viscous shell properties**             |                   |                 |                      |
  Permeability of shell                    | $k$               | `ks`            | 1$\times$10$^{-18}$  | m$^2$
  Thermal conductivity                     | $K$               | `k`             | 2.5                  | W m$^{-1}$K$^{-1}$
  Brittle-Ductile transition temperature   | $T_{bdt}$         | `Tbdt`          | 623 (350$^{\circ}$C) | K
  Thermal expansion (rock)                 | $\alpha_r$        | `ar`            | 1$\times$10$^{-5}$   | K$^{-1}$
  Compressibility (rock)                   | $\beta_r$         | `br`            | 1$\times$10$^{10}$   | Pa
  Viscosity of water in shell              | $\eta_g$          | `etag`          | 5.2$\times$10$^{-5}$ | Pa s
  Viscosity of rock in shell               | $\eta_r$          | `etari`         | 1$\times$10$^{20}$   | Pa s
  Rock density                             | $\rho_r$          | `pr`            | 2600                 | kg m$^{-3}$
  **Geothermal circulation properties**    |                   |                 |                      |
  Mass flux of geothermal circulation      | $q_{fld}$         | `qfld`          | 7.6$\times$10$^{-7}$ (1.2 m/yr rain, 2% infil.) | kg s$^{-1}$
  Geothermal power factor                  | $\gamma$          | `gamma`         | 10                   |
  **Other**                                |                   |                 |                      |
  Gravitational acceleration               | $g$               | `g`             | 9.81                 | m s$^{-2}$
  **Simulation parameters**                |                   |                 |                      |
  Maximum time to simulate                 |                   |  `tmax`         | 1.6$\times$10$^{11}$ ($\sim$5 kyr)            | s
  Maximum number of eruptions to simulate  |                   | `Ne_max`        | 20                   |
  Relative error tolerance for solver      |                   | `rtol`          | 1$\times$10$^{-12}$  |

## Python implementation

***Run the cell below to make model objects and functions available.***

```python
from magmageotherm import*
```

### The Basics

```python
##
## SET MODEL PARAMETERS
##
Vi = 100*1.e9         # initial volume [m^3]
Ti = 800.+273         # initial temperature [K]
z = 4.5e3             # burial depth [m]
dz = 1.e3             # sill thickness [m]
Pi = z*9.81*2.7e3     # initial pressure [Pa]
Pc = 20.e6            # critical pressure, Jellinek 2003, could be as high as 40 MPa
                      # also uses Spieler 2004 fragmentation model
qfld = 1.2/(365.25*24*3600)*1.e3*0.02    # geothermal mass flux
                      # (rainfall volume rate x density x infiltration) [kg/s]
Min = 100.            # magma influx rate [kg/s]
ks = 1.e-19           # shell permeability [m^2]
etar = 1.e20          # shell viscosity (elastic host rock) [Pa s]
gamma = 10.           # strength of geothermal circulation
Merupt = 1.e7         # mass flux rate during eruption [kg/s]
Ne_max = 10           # maximum number of eruptions to model
tmax = 6.e3*365.25*24*3600           # maximum simulation time [s]

##
## CREATE AND RUN THE MODEL
##
model = MagmaChamber(Min=Min, Vi=Vi, z=z, Ti=Ti, Pc=Pc, Plith=Pi, qfld=qfld,
                     ks=ks, dz=dz, etar=etar, gamma=gamma, Ne_max=Ne_max, Merupt= Merupt, tmax=tmax)
model.run()

##
## SHOW THE MODEL OUTPUT
##
defaults = ['deltaP','V','eX','T','eg','egW']     # default to show, but can be changed interactively
show_model(model, defaults)
```

    VBox(children=(HTMLMath(value='<!DOCTYPE html><html><head><style>table, th, td {    border: 1px solid black;  …

### Comparing Models

```python
# To compare up to three models, create and run more than one model object, pass a list of them to the plot function.
# For example, the model above, but with three different rock viscosities.
# (Ensure that you have run the demonstration cell above so that parameter variables have been created.)

etars = [1.e20,1.e19,1.e21]             # viscosity sensitivity around the demo value above
models = []                             # empty list to store models

# loop over viscosities, run model for each
for etar in etars:
    model = MagmaChamber(Min=Min, Vi=Vi, z=z, Ti=Ti, Pc=Pc, Plith=Pi, qfld=qfld,
                     ks=ks, dz=dz, etar=etar, gamma=gamma, Ne_max=Ne_max, Merupt= Merupt, tmax=tmax)
    model.run()
    models.append(model)

# show the models
defaults = ['deltaP','V','eX','T','eg','egW']
show_model(models, defaults)                    # pass list of models into dashboard function
```

### Saving and Loading Models

```python
# You can run a model, save its output, and load it again later.
# For example (make sure you have run the cells above)

# run the model
model = MagmaChamber(Min=Min, Vi=Vi, z=z, Ti=Ti, Pc=Pc, Plith=Pi, qfld=qfld,
                     ks=ks, dz=dz, etar=etar, gamma=gamma, Ne_max=Ne_max, Merupt= Merupt, tmax=tmax)
model.run()

# save the model
model.save('out.mg')

# delete the model
del(model)

# load it up again
model = MagmaChamber()
model.load('out.mg')

# display the loaded model
show_model(model, defaults)
```

### Variable magma influx

```python
# Suppose magma influx rate exponetially from Min = 10 kg/s to Min = 400 kg/s over 6000 yr
# with a characteristic time of 3000 years
t = np.linspace(0,6000.*365.25*24*3600,101)
tchar,Mmin,Mmax = [t[-1]/2., 10., 400.]
M = (Mmax-Mmin)*(np.exp(t/tchar) - 1)/(np.exp(t[-1]/tchar) - 1) +Mmin

# pass time and recharge vectors as two element list to parameter Min
model = MagmaChamber(Min=[t,M], Vi=Vi, z=z, Ti=Ti, Pc=Pc, Plith=Pi, qfld=qfld,
                     ks=ks, dz=dz, etar=etar, gamma=gamma, Ne_max=Ne_max, Merupt= Merupt, tmax=tmax)

# run and show model
model.run()
defaults = ['deltaP','V','Min','T','eg','egW']
show_model(model, defaults)
```

## Model Shortcomings

This model is deficient for the following reasons (and probably others I haven't listed):

1. Unlike Degruyter and Huber (2014), we do not model the evolving temperature (or viscosity) in the viscoelastic shell surrounding the magma chamber. Instead, we prescribe a single, fixed effective viscosity, that does not change as the chamber temperature changes.
2. The magma chamber is treated as a single lumped parameter model. This means that no allowance is made for material heterogeneity, geochemical reactions, changes in pressure, temperature or state throughout its volume.
3. This model calculates the thickness of the viscoelastic shell by balancing cooling due to heat conduction and geothermal convection. It assumes that any change in temperature that would affect the thickness of the shell does so immediately.
4. Fluid leakage through the viscoelastic shell assumes a constant permeability value can be used to characterise the entire shell.
5. Heat loss from the magma body is assumed to be dominated by an upward 1D flux comprising conduction (through the shell) enhanced by geothermal convection (above the shell). Lateral and downward heat loss are not modelled.

## Governing Equations

Here, we provide a brief overview of the physics of an evolving magma chamber, as originally presented in *Degruyter and Huber* (2014), and modified to include an overlying geothermal system in *Dempsey et al.* (2018).

### Mixture and closure

A magma chamber is considered a homogeneous cylinder of cross-sectional area, $A$, vertical thickness, $\Delta z$, volume, $V=A\Delta z$, at pressure, $P$, temperature, $T$, volume fraction of melt, $\epsilon_m$, gas, $\epsilon_g$, and crystals, $\epsilon_X$. Melt, gas and crystals have densities $\rho_m$, $\rho_g$, and $\rho_X$, respectively.

The density of the mixture, $\rho$, is
\begin{equation}
\rho = \epsilon_m\rho_m+\epsilon_g\rho_g +\epsilon_X\rho_X,
\end{equation}

and the volume fractions must sum to 1
\begin{equation}
\epsilon_m+\epsilon_g+\epsilon_X=1.
\end{equation}

### Conservation of total mass

Total mass, $M$, is given by the product of mixture density and volume, $M=\rho V$. Differentiating with respect to time, we obtain

\begin{equation}
\frac{1}{\rho}\frac{d\rho}{dt}+\frac{1}{V}\frac{dV}{dt}=\frac{\dot{M}_{in}-\dot{M}_{out}}{\rho V}
\end{equation}

where $t$ is time, $\dot{M}_{in}$ is mass influx rate and $\dot{M}_{out}$ is mass outflux rate. Mass outflux comprises volatile leakage through the overlying viscoelastic shell, $\dot{M}_{w,leak}$, and a sporadic eruptive term, $\dot{M}_{erupt}$. An eruption occurs when pressure exceeds either a dike propagation, $\Delta P_{cd}$, or fragmentation threshold, $\Delta P_{cf}$, (chamber has failed), providing the crystal volume fraction is below a threshold, $\epsilon_{Xc}$, (magma is mobile).

Time derivative of the mixture density depends on changes in the component densities and their fractions

\begin{equation}
\frac{d\rho}{dt}=\epsilon_m\frac{d\rho_m}{dt}+\epsilon_X\frac{d\rho_X}{dt}+\epsilon_g\frac{d\rho_g}{dt}+(\rho_g-\rho_m)\frac{d\epsilon_g}{dt}+(\rho_X-\rho_m)\frac{d\epsilon_X}{dt}\\
\end{equation}

### Equations of state

Melt and crystal density are functions of pressure and temperature via their bulk moduli, $\beta_i$, and thermal expansion coefficients, $\alpha_i$, i.e.,

\begin{eqnarray}
\frac{1}{\rho_m}\frac{d\rho_m}{dt} &=& \frac{1}{\beta_m}\frac{dP}{dt}-\alpha_m\frac{dT}{dt},\\
\frac{1}{\rho_X}\frac{d\rho_X}{dt} &=& \frac{1}{\beta_X}\frac{dP}{dt}-\alpha_X\frac{dT}{dt}.\\
\end{eqnarray}

For the gas phase,

\begin{equation}
\frac{1}{\rho_g}\frac{d\rho_g}{dt}=\frac{1}{\rho_g}\frac{\partial\rho_g}{\partial P}\frac{dP}{dt}+\frac{1}{\rho_g}\frac{\partial \rho_g}{\partial T}\frac{dT}{dt}
\end{equation}

where gas density is calculated according to the parameterisation by Huber et al. (2010,2011) of the modified Redlich-Kwong relation (Halbach and Chatterjee, 1982)

\begin{equation}
\rho_g = 10^3\left(−112.528T^{−0.381} + 127.811P^{−1.135}+112.04T^{−0.411}P^{0.033}\right)
\end{equation}

where $T$ is in $^{\circ}$C and $P$ is in bar. Derivatives $\partial\rho_g/\partial P$ and $\partial\rho_g/\partial T$ are obtained by differentiating the expression above (and scaling the pressure derivative back into Pa). Substituting compressibility and expansion expressions back into the derivative $d\rho/dt$

\begin{equation}
\frac{1}{\rho}\frac{d\rho}{dt}=\frac{1}{\beta}\frac{dP}{dt}-\alpha\frac{dT}{dt}+\frac{\rho_X-\rho_m}{\rho}\frac{\partial\epsilon_X}{\partial T}\frac{dT}{dt}+\left((\rho_g-\rho_m)+(\rho_X-\rho_m)\frac{\epsilon_X}{\epsilon_g}\right)\frac{d\epsilon_g}{dt},
\end{equation}

where we have defined the mixture bulk modulus, $\beta$, and thermal expansion coefficient, $\alpha$,

\begin{eqnarray}
\frac{1}{\beta}&=& \frac{1}{\rho}\left(\epsilon_m\frac{\rho_m}{\beta_m}+\epsilon_X\frac{\rho_X}{\beta_X}+\epsilon_g\frac{\partial \rho_g}{\partial P}\right),\\
\alpha&=&\frac{1}{\rho}\left(\epsilon_m\rho_m\alpha_m+\epsilon_X\rho_X\alpha_X-\epsilon_g\frac{\partial \rho_g}{\partial T}\right).
\end{eqnarray}

### Evolution of chamber volume

Assuming the chamber remains spherical at all times, evolution of the chamber volume will depend only on volumetric strains.  Because our chamber is cylindrical and not spherical, and hence the expression below provides only a rough approximation. The Maxwell viscoelastic response to pressure changes inside the chamber superimposed alongside thermoelastic strains immediately outside the chamber is

\begin{equation}
\frac{1}{V}\frac{dV}{dt} = \underbrace{\frac{1}{\beta_r}\frac{dP}{dt}}_\text{elastic term} + \underbrace{\frac{\Delta P}{\eta_r}}_\text{viscous term} - \underbrace{\alpha_r \frac{dT}{dt}}_\text{thermal expansion term}
\end{equation}

where $\Delta P=P-P_{lith}$ is the chamber overpressure relative to the lithostatic pressure, $P_{lith}$, and $\eta_r$ is the effective viscosity. Substituting expressions for $dV/dt$ and $d\rho/dt$ into the mass conservation equation, we obtain

\begin{equation}
A_m \frac{dP}{dt}+B_m\frac{dT}{dt}+C_m\frac{d\epsilon_g}{dt}+D_m = 0,
\end{equation}

where the coefficients are given

\begin{eqnarray}
A_m &=& -\left(\frac{1}{\beta}+\frac{1}{\beta_r}\right)\\
B_m &=& -\left(-\alpha -\alpha_r +\frac{\rho_X-\rho_m}{\rho}\frac{\partial\epsilon_X}{\partial T}\right)\\
C_m &=& -\left(\frac{\rho_g-\rho_m}{\rho}+\frac{\rho_X-\rho_m}{\rho}\frac{\partial \epsilon_X}{\partial\epsilon_g}\right)\\
D_m &=& \frac{\dot{M}_{in}-\dot{M}_{out}}{\rho V}-\frac{\Delta P}{\eta_r}
\end{eqnarray}

### Conservation of water

The equation for conservation of water is developed in a similar manner to conservation of mass. Total water mass, $M_w$, comprises dissolved and exsolve components

\begin{equation}
M_w = \epsilon_g \rho_g V + m_{eq}\epsilon_m\rho_m V,
\end{equation}

where $m_{eq}(P,T)$ is the equilibrium dissolved water content in the melt, whose equation of state is given (*Dufek and Bergantz*, 2005)

\begin{equation}
m_{eq} = 10^{-2}\left(P^{0.5}\left(0.4874-\frac{608}{T}+\frac{489530}{T^2}\right)+P\left(-0.0602+\frac{135.6}{T}-\frac{69200}{T^2}\right)+P^{1.}\left(0.00253-\frac{4.154}{T}+\frac{1509}{T^2}\right)\right),
\end{equation}
where $P$ is in MPa and $T$ is in Kelvin.

The balance equation for water is
\begin{equation}
\frac{dM_w}{dt} = \dot{M}_{w,in} - \dot{M}_{w,erupt} - \dot{M}_{w,leak},
\end{equation}

where $\dot{M}_{w,in}$ is the incoming water dissolved in melt and $\dot{M}_{w,erupt}$ is the erupted water, assumed to erupt in the same fraction as they are represented in the magma mixture.

The governing equation for water then becomes

\begin{equation}
A_w \frac{dP}{dt}+B_w\frac{dT}{dt}+C_w\frac{d\epsilon_g}{dt}+D_w = 0,
\end{equation}

where the coefficients are given

\begin{eqnarray}
A_w &=& -\left(\frac{1}{\rho_g}\frac{d\rho_g}{dP}+\frac{1}{\beta_r}+\frac{m_{eq}\rho_m\epsilon_m}{\rho_g\epsilon_g}\left[\frac{1}{m_{eq}}\frac{\partial m_{eq}}{\partial P}+\frac{1}{\beta_m}+\frac{1}{\beta_r}\right]\right)\\
B_w &=& -\left(\frac{1}{\rho_g}\frac{d\rho_g}{dT}-\alpha_r+\frac{m_{eq}\rho_m\epsilon_m}{\rho_g\epsilon_g}\left[\frac{1}{m_{eq}}\frac{\partial m_{eq}}{\partial T}-\alpha_m-\alpha_r-\frac{1}{\epsilon_m}\frac{\partial\epsilon_X}{\partial T}\right]\right)\\
C_w &=& -\left(\frac{1}{\epsilon_g}-\frac{m_{eq}\rho_m}{\rho_g\epsilon_g}\left[\frac{\partial \epsilon_X}{\partial \epsilon_g}\right]\right)\\
D_w &=& \frac{\dot{M}_{w,in}-\dot{M}_{w,erupt}-\dot{M}_{w,leak}}{\rho \epsilon_g V}-\frac{\Delta P}{\eta_r}\left(1+\frac{m_{eq}\rho_m\epsilon_m}{\rho_g\epsilon_g}\right)
\end{eqnarray}

### Conservation of energy

Conservation of enthalpy, $H$, is expressed
\begin{equation}
\frac{dH}{dt} = \dot{H}_{in} - \dot{H}_{erupt} - \dot{H}_{cool} - \dot{H}_{leak},
\end{equation}

where the incoming enthalpy is $\dot{H}_{in} = c_{in}T_{in}\dot{M}_{in}$ and $c_{in}$ and $T_{in}$ are the specific heat and temperature of the recharging magma mixture. Enthalpy is lost via three mechanisms: eruption ($\dot{H}_{erupt}$), conductive cooling ($\dot{H}_{cool}$) and fluid leakage across the viscoelastic shell ($\dot{H}_{leak}$).

Enthalpy of the magma body comprises sensible heat, latent heat of crystallisation, $L_m$, and exsolution, $L_e$,
\begin{equation}
H = \rho cTV - L_m\rho_X\epsilon_XV - L_e m_{eq}\rho_m \epsilon_m V,
\end{equation}

where $c$ is the specific heat of the mixture
\begin{equation}
    c = \frac{1}{\rho}(\epsilon_X\rho_X c_X+\epsilon_m\rho_m c_m+\epsilon_g\rho_g c_g).
\end{equation}

The balance equation then becomes

\begin{equation}
A_h \frac{dP}{dt}+B_h\frac{dT}{dt}+C_h\frac{d\epsilon_g}{dt}+D_h = 0,
\end{equation}

where the coefficients are given

\begin{eqnarray}
A_h&=&-\left(\frac{1}{\beta}+\frac{1}{c}\frac{\partial c}{\partial P}+\frac{1}{\beta_r} -\frac{L_m \rho_X \epsilon_X}{\rho cT} \left[\frac{1}{\beta_X} +\frac{1}{\beta_r} \right]-\frac{L_e m_{eq} \rho_m \epsilon_m}{\rho cT} \left[\frac{1}{m_{eq}}\frac{\partial m_{eq}}{\partial P}+\frac{1}{\beta_m} +\frac{1}{\beta_r} \right]\right), \\
B_h&=&-\left(-\alpha+\frac{\rho_X-\rho_m}{\rho} \frac{\partial \epsilon_X}{\partial T}+\frac{1}{c}\frac{\partial c}{\partial T}+\frac{1}{T}-\alpha_r-\frac{L_m \rho_X \epsilon_X}{\rho cT} \left[-\alpha_X+\frac{1}{\epsilon_X}\frac{\partial \epsilon_X}{\partial T}-\alpha_r \right]-\frac{L_e m_{eq} \rho_m \epsilon_m}{\rho cT} \left[\frac{1}{m_{eq}}\frac{\partial m_{eq}}{\partial P}-\alpha_m-\frac{1}{\epsilon_m}\frac{\partial \epsilon_X}{\partial T}-\alpha_r \right]\right), \\
C_h&=&-\left(\frac{\rho_g-\rho_m}{\rho}+\frac{\rho_X-\rho_m}{\rho}\frac{\partial \epsilon_X}{\partial \epsilon_g}+\frac{1}{c} \frac{\partial c}{\partial \epsilon_g}-\frac{L_m \rho_X}{\rho cT}\frac{\partial \epsilon_X}{\partial \epsilon_g}+\frac{L_e m_{eq} \rho_m}{\rho cT} \left[1+\frac{\partial \epsilon_X}{\partial \epsilon_g}\right]\right), \\
D_h&=&\frac{\dot{H}_{in}-\dot{H}_{erupt}-\dot{H}_{cool}-\dot{H}_{leak}}{\rho cTV}-\frac{\Delta P}{\eta_r}  \left(1-\frac{L_m \rho_X \epsilon_X}{\rho cT}-\frac{L_e m_{eq} \rho_m \epsilon_m}{\rho cT}\right),
\end{eqnarray}

where the partial derivatives of specific heat are

\begin{eqnarray}
\frac{\partial c}{\partial P}&=&\frac{1}{\rho} \left(\frac{\rho_X \epsilon_X c_X}{\beta_X} +\epsilon_g c_g  \frac{\partial \rho_g}{\partial P}+\frac{\rho_m \epsilon_m c_m}{\beta_m} \right)-\frac{c}{\rho} \frac{\partial \rho}{\partial P}, \\
\frac{\partial c}{\partial T}&=&\frac{1}{\rho} \left(-\alpha_X \rho_X \epsilon_X c_X+\epsilon_g c_g  \frac{\partial \rho_g}{\partial T}-\alpha_m \rho_m \epsilon_m c_m \right)-\frac{c}{\rho}  \frac{\partial \rho}{\partial T}+\left(\frac{\rho_X c_X-\rho_m c_m}{\rho}-\frac{c}{\rho} \frac{\partial \rho}{\partial \epsilon_X}\right)  \frac{\partial \epsilon_X}{\partial T}, \\
\frac{\partial c}{\partial \epsilon_g}&=&\frac{\rho_g c_g-\rho_m c_m}{\rho}-\frac{c}{\rho}\frac{\partial \rho}{\partial \epsilon_g}+\left(\frac{\rho_X c_X-\rho_m c_m}{\rho}-\frac{c}{\rho} \frac{\partial \rho}{\partial \epsilon_X}\right)  \frac{\partial \epsilon_X}{\partial \epsilon_g}.
\end{eqnarray}

### Geothermal model

The original *Degruyter and Huber* (2014) model assumed heat loss from the magma chamber across a spherical viscoelastic shell that enveloped it. Heat loss was conductive, determined by a transient 1D spherical model with non-steady magma body internal boundary condition, $T(t)$, a constant external temperature, and a constant shell thickness.

In our model, we consider a static 1D linear model that accounts for both conductive and advective heat flux (from leaking fluids) through an overlying viscoelastic layer of evolving thickness. Geothermal systems are assumed to have formed in the brittle crust above this layer, and the enhanced cooling has resulted in deepening of the brittle-ductile transition depth ($T_{BDT}$ isotherm at $z_{BDT}$). The difference between $z_{BDT}$ and the top surface of the magma body, $z_{mb}$, sets the viscoelastic layer thickness. The depth of the brittle crust is determined by balancing conductive heat flux through the shell, with geothermal and conductive heat fluxes in the brittle crust:

\begin{equation}
z_{BDT}=z_{mb} \left(1+\frac{(T-T_{BDT})}{(1+γ)(T_{BDT}-T_{atm}} \right),^{-1}
\end{equation}

where $\gamma$ is the ratio of geothermal to conductive power transfer.

Solutions for steady pressure, $P'$, and temperature, $T'$, across the shell with conductivity, $K$, and permeability, $k$, are

\begin{eqnarray}
P' &=& P-\beta_g' \rho_g+\sqrt{(\beta_g' \rho_g )^2+z' \Delta P(\Delta P-2\beta_g' \rho_g )},\\
T' &=& T-\Delta T \frac{1-e^{-C(z-z_{mb})}}{1-e^{-C(z_{BDT}-z_{mb})}},
\end{eqnarray}

where $C= -c_g k\epsilon_g \Delta P(2\beta_g' \Delta P-\rho_g)/(K\eta_g (z_{BDT}-z_{mb}))$, density of water in the shell is given by a linear compressibility relation $\rho_g'=\rho_g+\frac{1}{\beta_g'} (P'-P_0 )$, and $\Delta P$ and $\Delta T$ are pressure and temperature drops across the shell, as given by the boundary conditions (magma body $P$ and $T$ at the internal, hydrostatic pressure and $T_{BDT}$ at the external).

Cooling and leakage rates are computed from $P'$ and $T'$

\begin{eqnarray}
\dot{H}_{cool} &=& -AK\frac{dT'}{dz}\bigg\rvert_{z=z_{mb}}\\
\dot{M}_{leak} &=& -A\frac{k\epsilon_g \rho_g}{\eta_g}\frac{dP'}{dz}\bigg\rvert_{z=z_{mb}},\quad\quad   \dot{H}_{leak}=c_g T\dot{M}_{leak}.
\end{eqnarray}


# DMP

Control policy:
- (traditional) state-centered representation $\mathbf{u}_t = \pi(\mathbf{s}_t)$
  - algorihms are hard to apply
- Trajectory-centered representation: control policy reduced to a desired trajectory $[\mathbf{x}_d(t), \dot{\mathbf{x}}_d(t)]$:
$$\mathbf{u}_t = K_x (\mathbf{x}_d(t) - \mathbf{x}) + K_{\dot{x}}(\dot{\mathbf{x}}_d(t) - \dot{\mathbf{x}})$$
  - Requires a (linear) feedback controller
  - Reduces complexity of the learning problem
  - Only valid around vicinity of the desired trajectory, not very flexible
  - Hard to adapt the pre-planned trajectories (time and space)

Dynamical system representation:
- Define behavior by non-linear dynamical systems $\dot{\mathbf{x}}_d = f(\mathbf{x}, \theta) $
- $[\mathbf{x}_d(t), \dot{\mathbf{x}}_d(t)]$ are converted to motor commands by a (inverse dynamics) controller
- Cancels non-linearities due to gravity or inertial forces

A DMP defines a single movement, kinematic state (q, $\dot{q}$, $\ddot{q}$), or in task space.
- Discrete DMP: The base system is a point attractor
- Rhythmic DMP: The base system is a limit cycle

# Discrete DMP
A discrete DMP is a combination of a (2nd order) basic point attractor dynamics (base system) and a (nonlinear) 'force' dynamics: A damped spring attached to a goal position $g$, perturbed by a non-linear acceleration $f$.

## Point attractor dynamics:
\begin{equation}
\ddot{x} = K(g - x) - D(\dot{x}) \\
\end{equation}
where $x$ is the (joint) position, $\dot{x}$ (joint) velocity, $\ddot{x}$ (joint) acceleration, $g$ the goal, $K$ the spring constant, and $D$ the damping constant.

- *For some $K$ and $D$ and $f=0$, $g$ is a unique point attractor *

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

```python
def attractor_dynamics(x, dx, g, K, D, T=20):
    xs = np.zeros(T)
    dxs = np.zeros(T)
    ddxs = np.zeros(T)
    xs[0] = x
    dxs[0] = dx
    for t in range(T-1):
        ddxs[t] = K*(g - xs[t]) - D*(dxs[t])
        dxs[t+1] = dxs[t] + ddxs[t]
        xs[t+1] = xs[t] + dxs[t]
    return xs, dxs, ddxs
```

```python
K = 0.1
D = 0.5
x = 0
dx = 0
g = 5

xs, dxs, ddxs = attractor_dynamics(x, dx, g, K, D)

fix, axs = plt.subplots(3, 1, sharex=True)
axs[0].plot(xs, label='x')
axs[0].legend()
axs[1].plot(dxs, label='dx')
axs[1].legend()
axs[2].plot(ddxs, label='ddx')
axs[2].legend()
plt.show()
```

## Force term dynamics
### canonical system
- Avoid explicit dependency on time for modelling $f$ with a **canonical system**.
- Models the phase/progress of the movement (1st order "leaky integrator")
$$\dot{s} = - \alpha s$$
$s$ is initially set to 1, in the limit $s\rightarrow0$

```python
def canonical_dynamics(alpha, s0=1, T=20):
    ss = np.zeros(T)
    dss = np.zeros(T)
    ss[0] = s0
    for t in range(T-1):
        dss[t] = -alpha*ss[t]
        ss[t+1] = ss[t] + dss[t]
    return ss, dss
```

```python
alpha = 0.2
ss, dss = canonical_dynamics(alpha)

fix, axs = plt.subplots(2, 1, sharex=True)
axs[0].plot(ss, label='s')
axs[0].legend()
axs[1].plot(dss, label='ds')
axs[1].legend()
plt.show()
```

### Gaussian basis functions
Model $f$ with:
$$f(s,g) = \frac{\sum_{i=1}^N \psi_i(s) w_i}{\sum_{i=1}^N \psi_i(s)} s (g - x_0)$$
where $y_0$ is the initial position of the system, $w_i$ is a weighting for a given basis function $\psi_i$:
$$\psi_i(s) = \exp(-h_i (s-c_i)^2)$$

### Diminishing term
The term $s$ guarantees that the contribution of the forcing term goes to zero over time, as the canonical system does. In the limit $s \rightarrow 0$, $f$ vanishes.

### Spatial Scaling
The term $(g-x_0)$ allows to scale the activation of each of the basis functions to be relative to the distance to the target, causing the system to cover more or less distance.

### Different numbers of basis functions
As the complexity of the trajectory increases, so does the number of basis functions.

### Spreading basis function centers
We need that the centers $c_i$ of the basis function $\psi_i$ be uniformly distributed over time, so avoid that all $\psi_i$ centers be at the beginnig of time (because $x$ decreases exponentially because the canonical dynamics) and then at the end of time there is not influence of them.
$c_i$ and $h_i$ are usually fixed by the designer. E.g., logarithmically distribute $c_i$ between 1 and 0.

## Temporal Scaling
We want to be able to follow the same trajectory at different speeds, sometimes slow, others quick, but always tracing out the same path. This is obtained by including the therm $\tau$:
$$\ddot{y} = \tau^2(\alpha_y (\beta_y(g - y) - \dot{y}) + f)$$
$$\dot{x} = \tau (- \alpha_x x)$$

Then to slow the system $\tau$ should be between 0 and 1, and to speed it up $\tau$ should be greater than 1.

# Learning with DMP
We want to found $w_i$ by supervised learning (imitation learning) or by optimization (reinforcement learning).

## Imitating a desired path

Assuming a desired trajectorya: $\mathbf{y}_d$, $\dot{\mathbf{y}}_d$, , $\ddot{\mathbf{y}}_d$, we want to get the non-linear function $f$ such

the forcing term affects the system acceleration, then:
$$\mathbf{f}_d = \ddot{\mathbf{y}}_d - \alpha_y (\beta_y(g - \mathbf{y}_d) - \mathbf{\dot{y}}_d)$$

In **locally weighted regression**, we want to minimize:
$$ \sum_t \psi_i (t) (f_d(t) - w_i (x(t) (g-y_0)))^2,$$
and the solution is
$$w_i = \frac{\mathbf{s}^T \psi_i \mathbf{f}_d}{\mathbf{s}^T \psi_i \mathbf{s}}$$
where
\begin{equation}
s = \begin{pmatrix}
x_{t0}(g - y_0)\\
\vdots \\
x_{tN}(g - y_0)
\end{pmatrix}
,
\psi_i = \begin{pmatrix}
\psi_i(t_0) & \cdots  & 0\\
0 & \ddots  & 0\\
0 & \cdots & \psi_i(t_n)
\end{pmatrix}
\end{equation}

## RL with DMPs
- We want to learn the weight vector $w$: all the algorithms exploit the linear dependence of $f$ on its parameters, call-parameters are always kept fixed.
- Work only for the stroke-based/episodic for a single motion primitive (no sequencing/combination)
- Local search methods: We need to provide a close to optimal initial solution (by IL)
- Only work for the same or at least similar initial conditions for each episode (cannot choose different parameters for different initial conditions).
- 3 main approaches can be found:
  - Policy-gradient based algorithm: hyperparams: learning rate, exploration. E.g eNAC (episodic natural actor critic)
  - Expectation-Maximization (EM)-style updates. hyperparams: exploration. PoWER (pol.learn.by weighting exploration with the returns)
  - Stochastic optimal control: hyperparams: exploration. PI2 (pol.improv. with path integrals)

# References
- DeWolf, Travis (2003): Blog: https://github.com/studywolf: [DMP (part 1)](https://studywolf.wordpress.com/2013/11/16/dynamic-movement-primitives-part-1-the-basics/),
[DMP (part 2)](https://studywolf.wordpress.com/2013/12/05/dynamic-movement-primitives-part-2-controlling-a-system-and-comparison-with-direct-trajectory-control/)
- Neumann, Gerhard (2009): DMPs for Robot Control, [slides](http://www.igi.tugraz.at/lehre/SeminarC/WS09/neumann_C_2009.pdf)

# Code
- pyDMPs: [github](https://github.com/studywolf/pydmps/tree/master/pydmps)
- Schaal matlab code: [ucd_repo](http://www-clmc.usc.edu/Resources/Software)


```python
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

from keras.models import Sequential
from keras.layers import Dense
from keras import regularizers

%matplotlib inline

## Set some values globally
# Ticks label size
label_size = 10
mpl.rcParams['xtick.labelsize'] = label_size
mpl.rcParams['ytick.labelsize'] = label_size
# Ticks width
ticks_width = 0.5
mpl.rcParams['xtick.major.width'] = ticks_width
mpl.rcParams['ytick.major.width'] = ticks_width
# Line width
mpl.rcParams['axes.linewidth'] = 0.5
```

### 2D quared-lattice Ising Hamiltonian:
\begin{equation}
H = -J \sum_{\langle i, j \rangle} S^z_i S^z_j
\end{equation}

```python
def plot_configuration(spins):
    '''
    Helper function to plot the configuration of spins given by `spins`.

    Arguments:
        spins -- a spin configuration; type: numpy array of shape (N, N).
    '''

    N = np.shape(spins)[1]

    fig, ax = plt.subplots(dpi=100, figsize=(5, 5))
    fig.add_axes()
    ax = fig.axes[0]

    # Plot spins (circles).
    colors = ['C0', 'C1']   # blue: down, orange: up.

    for i in range(N):
        for j in range(N):
            fig.gca().add_patch(plt.Circle((i,j), radius=0.35, fc=colors[int((spins[i, j]+1)/2.)],
                                           ec='k', lw=0.3))

    # Plot lattice (grid).
    for i in range(N):
        ax.plot([i, i], [0, N-1], 'k', lw=0.5)
        ax.plot([0, N-1], [i, i], 'k', lw=0.5)

    ax.set_ylim(-1, N+1)
    ax.set_xlim(-1, N+1)
    ax.set_aspect('equal')

    plt.xlabel("x")
    plt.ylabel("y")
    plt.axis('off')
    plt.savefig('ising.png', bbox_inches='tight', dpi=600)
#     fig.show()
```

```python
# size of the system NxN
N = 30
```

First we need to create training and test configurations with the script **create_configurations.py**, then run the next cells.

```python
# Training set.
train_configs = np.loadtxt("configs/train_configs_30x30.txt")
train_labels = np.loadtxt("configs/train_labels_30x30.txt")
print("Loaded %i training configurations of size %ix%i for training."%(len(train_configs), N, N))
```

    Loaded 2550 training configurations of size 30x30 for training.

```python
# Test set.
test_configs = np.array(np.loadtxt("configs/test_configs_30x30.txt"))
test_labels = np.array(np.loadtxt("configs/test_labels_30x30.txt"))
print("Loaded %i test configurations of size %ix%i for testing."%(len(test_configs), N,N))
```

    Loaded 550 test configurations of size 30x30 for testing.

```python
# Just diplay one configuration.
plot_configuration(np.reshape(np.array(train_configs[1340]), (N, N)))
```

```python
def pca(X=np.array([]), no_dims=2):
    """
    Runs PCA on the NxD array X to reduce dimension.
    """

    (n, d) = X.shape
    X = X - np.tile(np.mean(X, 0), (n, 1))
    (l, M) = np.linalg.eig(np.dot(X.T, X))
    Y = np.dot(X, M[:, 0:no_dims])
    return Y
```

```python
PCA_coord = pca(train_configs, no_dims=2)
```

```python
fig = plt.figure(dpi=100)

plt.scatter(PCA_coord[:, 0], PCA_coord[:, 1], 30, train_labels,
            marker='o', edgecolors='k', linewidths=0.5)

plt.axis('off')
plt.show()
```

## Small dense network

```python
# Prepare data for training;
# exclude temperatures between 2.1 and 2.4.

T_low = 2.1
T_high = 2.4

x = []
y = []

for i, label in enumerate(train_labels):
    if label < T_low:
        x.append(train_configs[i])
        y.append((1, 0))   # Low-T
    if label > T_high:
        x.append(train_configs[i])
        y.append((0, 1))   # High-T

x = np.array(x)
y = np.array(y)
```

```python
# Create model.
# Network architecture: input layer, hidden layer with 3 neurons, and output layer with 2 neurons.
model = Sequential()
model.add(Dense(3, input_dim=N**2, kernel_initializer='glorot_normal', activation='sigmoid',
                kernel_regularizer=regularizers.l2(0.01)))
model.add(Dense(2, kernel_initializer='glorot_normal', activation='softmax',
                kernel_regularizer=regularizers.l2(0.0)))

# Compile model
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
```

```python
model.summary()
```

    Model: "sequential_1"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #
    =================================================================
    dense_1 (Dense)              (None, 3)                 2703
    _________________________________________________________________
    dense_2 (Dense)              (None, 2)                 8
    =================================================================
    Total params: 2,711
    Trainable params: 2,711
    Non-trainable params: 0
    _________________________________________________________________

```python
# Train model.
model.fit(x, y, epochs=25, batch_size=32)
```

    Epoch 1/25
    2200/2200 [==============================] - 0s 112us/step - loss: 0.8014 - accuracy: 0.5668
    Epoch 2/25
    2200/2200 [==============================] - 0s 52us/step - loss: 0.7177 - accuracy: 0.6436
    Epoch 3/25
    2200/2200 [==============================] - 0s 47us/step - loss: 0.6806 - accuracy: 0.5732
    Epoch 4/25
    2200/2200 [==============================] - 0s 50us/step - loss: 0.6478 - accuracy: 0.6273
    Epoch 5/25
    2200/2200 [==============================] - 0s 55us/step - loss: 0.6243 - accuracy: 0.6555
    Epoch 6/25
    2200/2200 [==============================] - 0s 52us/step - loss: 0.6020 - accuracy: 0.6741
    Epoch 7/25
    2200/2200 [==============================] - 0s 65us/step - loss: 0.5807 - accuracy: 0.6845
    Epoch 8/25
    2200/2200 [==============================] - 0s 70us/step - loss: 0.5596 - accuracy: 0.6982
    Epoch 9/25
    2200/2200 [==============================] - 0s 68us/step - loss: 0.5376 - accuracy: 0.8609
    Epoch 10/25
    2200/2200 [==============================] - 0s 65us/step - loss: 0.5162 - accuracy: 0.9577
    Epoch 11/25
    2200/2200 [==============================] - 0s 65us/step - loss: 0.4949 - accuracy: 0.9623
    Epoch 12/25
    2200/2200 [==============================] - 0s 56us/step - loss: 0.4737 - accuracy: 0.9695
    Epoch 13/25
    2200/2200 [==============================] - 0s 51us/step - loss: 0.4521 - accuracy: 0.9777
    Epoch 14/25
    2200/2200 [==============================] - 0s 45us/step - loss: 0.4320 - accuracy: 0.9814
    Epoch 15/25
    2200/2200 [==============================] - 0s 44us/step - loss: 0.4121 - accuracy: 0.9832
    Epoch 16/25
    2200/2200 [==============================] - 0s 46us/step - loss: 0.3929 - accuracy: 0.9909
    Epoch 17/25
    2200/2200 [==============================] - 0s 41us/step - loss: 0.3743 - accuracy: 0.9923
    Epoch 18/25
    2200/2200 [==============================] - 0s 47us/step - loss: 0.3565 - accuracy: 0.9941
    Epoch 19/25
    2200/2200 [==============================] - 0s 43us/step - loss: 0.3399 - accuracy: 0.9955
    Epoch 20/25
    2200/2200 [==============================] - 0s 46us/step - loss: 0.3237 - accuracy: 0.9955
    Epoch 21/25
    2200/2200 [==============================] - 0s 42us/step - loss: 0.3086 - accuracy: 0.9968
    Epoch 22/25
    2200/2200 [==============================] - 0s 47us/step - loss: 0.2936 - accuracy: 0.9968
    Epoch 23/25
    2200/2200 [==============================] - 0s 45us/step - loss: 0.2800 - accuracy: 0.9977
    Epoch 24/25
    2200/2200 [==============================] - 0s 41us/step - loss: 0.2668 - accuracy: 0.9986
    Epoch 25/25
    2200/2200 [==============================] - 0s 46us/step - loss: 0.2539 - accuracy: 0.9986

    <keras.callbacks.callbacks.History at 0x63e86aef0>

```python
# Prepare data for testing;
# exclude temperatures between 2.1 and 2.4.

T_low = 2.1
T_high = 2.4

x_test = []
y_test = []

for i, label in enumerate(test_labels):
    if label < T_low:
        x_test.append(test_configs[i])
        y_test.append((1, 0))   # Low-T
    if label > T_high:
        x_test.append(test_configs[i])
        y_test.append((0, 1))   # High-T

x_test = np.array(x_test)
y_test = np.array(y_test)
```

```python
# Evaluate model.
predictions = model.predict(test_configs)
preds = model.evaluate(x=x_test, y=y_test)

print ("Loss = {0:.3f}".format(preds[0]))
print ("Test Accuracy = {0:.3f}".format(preds[1]))
```

    500/500 [==============================] - 0s 71us/step
    Loss = 0.390
    Test Accuracy = 0.898

```python
Temps = list(np.sort(list(set(test_labels))))   # Unique temperatures
NT = len(Temps)

phase1 = np.zeros(NT)
phase2 = np.zeros(NT)
points = np.zeros(NT)

m = np.zeros(NT)   # Magnetization
lastT = 0.

for i, T in enumerate(test_labels):
    j = Temps.index(T)
    phase1[j] += predictions[i:i+1, 0][0]
    phase2[j] += predictions[i:i+1, 1][0]
    m[j] += abs(np.mean(test_configs[i]))
    points[j] += 1.

for j in range(NT):
    phase1[j] /= points[j]
    phase2[j] /= points[j]
    m[j] /= points[j]
```

The NN learns representations of the low- and high-temperature Ising states (see Fig. below).

```python
fig = plt.figure(dpi=100)

plt.plot(Temps, phase1, marker='o', markeredgecolor='k', label='ordered')
plt.plot(Temps, phase2, marker='^', markeredgecolor='k', label='disordered')
plt.plot(Temps, m, marker='s', markeredgecolor='k', label='magnetization')

plt.xlim(1, 3.5)
plt.xlabel('T [J]', )
plt.ylim(0, 1)

plt.legend()
# plt.show()
plt.savefig("ising_small_NN.png", bbox_inches='tight', dpi=600)
```

## Larger dense network

```python
# Create bigger model.
# Network architecture: input layer, hidden layer with 32 neurons, and output layer with 2 neurons.

model2 = Sequential()
model2.add(Dense(32, input_dim=N**2, kernel_initializer='glorot_normal', activation='sigmoid',
                 kernel_regularizer=regularizers.l2(0.02)))
model2.add(Dense(2, kernel_initializer='glorot_normal', activation='softmax',
                 kernel_regularizer=regularizers.l2(0.01)))

# Compile model
model2.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
```

```python
# Train model.
model2.fit(x, y, epochs=25, batch_size=32)
```

    Epoch 1/25
    2200/2200 [==============================] - 0s 91us/step - loss: 1.5928 - accuracy: 0.4886
    Epoch 2/25
    2200/2200 [==============================] - 0s 58us/step - loss: 0.9532 - accuracy: 0.6155
    Epoch 3/25
    2200/2200 [==============================] - 0s 70us/step - loss: 0.7589 - accuracy: 0.7659
    Epoch 4/25
    2200/2200 [==============================] - 0s 79us/step - loss: 0.6706 - accuracy: 0.8391
    Epoch 5/25
    2200/2200 [==============================] - 0s 87us/step - loss: 0.6238 - accuracy: 0.8605
    Epoch 6/25
    2200/2200 [==============================] - 0s 90us/step - loss: 0.5811 - accuracy: 0.9218
    Epoch 7/25
    2200/2200 [==============================] - 0s 69us/step - loss: 0.5415 - accuracy: 0.9286
    Epoch 8/25
    2200/2200 [==============================] - 0s 71us/step - loss: 0.5049 - accuracy: 0.9386
    Epoch 9/25
    2200/2200 [==============================] - 0s 67us/step - loss: 0.4635 - accuracy: 0.9605
    Epoch 10/25
    2200/2200 [==============================] - 0s 75us/step - loss: 0.4252 - accuracy: 0.9686
    Epoch 11/25
    2200/2200 [==============================] - 0s 67us/step - loss: 0.3884 - accuracy: 0.9773
    Epoch 12/25
    2200/2200 [==============================] - 0s 69us/step - loss: 0.3576 - accuracy: 0.9836
    Epoch 13/25
    2200/2200 [==============================] - 0s 71us/step - loss: 0.3271 - accuracy: 0.9927
    Epoch 14/25
    2200/2200 [==============================] - 0s 63us/step - loss: 0.3022 - accuracy: 0.9941
    Epoch 15/25
    2200/2200 [==============================] - 0s 68us/step - loss: 0.2821 - accuracy: 0.9936
    Epoch 16/25
    2200/2200 [==============================] - 0s 88us/step - loss: 0.2640 - accuracy: 0.9945
    Epoch 17/25
    2200/2200 [==============================] - 0s 78us/step - loss: 0.2483 - accuracy: 0.9945
    Epoch 18/25
    2200/2200 [==============================] - 0s 77us/step - loss: 0.2371 - accuracy: 0.9941
    Epoch 19/25
    2200/2200 [==============================] - 0s 74us/step - loss: 0.2264 - accuracy: 0.9959
    Epoch 20/25
    2200/2200 [==============================] - 0s 73us/step - loss: 0.2173 - accuracy: 0.9964
    Epoch 21/25
    2200/2200 [==============================] - 0s 66us/step - loss: 0.2106 - accuracy: 0.9959
    Epoch 22/25
    2200/2200 [==============================] - 0s 55us/step - loss: 0.2038 - accuracy: 0.9968
    Epoch 23/25
    2200/2200 [==============================] - 0s 57us/step - loss: 0.1982 - accuracy: 0.9977
    Epoch 24/25
    2200/2200 [==============================] - 0s 57us/step - loss: 0.1946 - accuracy: 0.9959
    Epoch 25/25
    2200/2200 [==============================] - 0s 59us/step - loss: 0.1895 - accuracy: 0.9968

    <keras.callbacks.callbacks.History at 0x63c883f60>

```python
# Evaluate model.
predictions2 = model2.predict(test_configs)
preds2 = model2.evaluate(x=x_test, y=y_test)

print ("Loss = {0:.3f}".format(preds2[0]))
print ("Test Accuracy = {0:.3f}".format(preds2[1]))
```

    500/500 [==============================] - 0s 76us/step
    Loss = 0.203
    Test Accuracy = 0.992

```python
Temps = list(np.sort(list(set(test_labels))))   # Unique temperatures
NT = len(Temps)

phase1_2 = np.zeros(NT)
phase2_2 = np.zeros(NT)
points = np.zeros(NT)

m = np.zeros(NT)   # Magnetization
lastT = 0.

for i, T in enumerate(test_labels):
    j = Temps.index(T)
    phase1_2[j] += predictions2[i:i+1, 0][0]
    phase2_2[j] += predictions2[i:i+1, 1][0]
    m[j] += abs(np.mean(test_configs[i]))
    points[j] += 1.

for j in range(NT):
    phase1_2[j] /= points[j]
    phase2_2[j] /= points[j]
    m[j] /= points[j]
```

```python
fig = plt.figure(dpi=100)

# Small network
plt.plot(Temps, phase1, ls='--', marker='o', markeredgecolor='k', label='ordered, , #h = 3')
plt.plot(Temps, phase2, ls='--', marker='^', markeredgecolor='k', label='disordered, , #h = 3')

# Bigger network
plt.plot(Temps, phase1_2, c='C0', marker='o', markeredgecolor='k', label='ordered, #h = 32')
plt.plot(Temps, phase2_2, c='C1', marker='^', markeredgecolor='k', label='disordered, #h = 32')

plt.xlim(1, 3.5)
plt.xlabel('T [J]')

plt.ylim(0, 1)
plt.ylabel('Output layer')

plt.legend()
# plt.show()
plt.savefig("ising_large_NN.png", bbox_inches='tight', dpi=600)
```

Analytical critical temperature (thermodynamic limit): $T_c = 2/\ln(1 + \sqrt{2}) \approx 2.269$.

```python

```