diff --git a/.gitignore b/.gitignore
index aee26506690023b02e63b3f2b626e96aeff42820..e23ad529d75c8d41c1b90aab4a5ad50ff027b132 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,7 +16,7 @@ MATLAB/*.asv
 MATLAB/**/*.asv
 
 # Python generated files
-py/__pycache__
+py/**/__pycache__
 
 # Automatic generated file on Windows
 desktop.ini
diff --git a/py/datafit.py b/py/datafit.py
deleted file mode 100644
index 8e48066cffd6305d1fd4206f45dd02b2edb994fb..0000000000000000000000000000000000000000
--- a/py/datafit.py
+++ /dev/null
@@ -1,154 +0,0 @@
-"""
-In this file, the four measurements from Julian are analysed and temperature
-dependent models fitted onto these results.
-"""
-
-import numpy as np
-import matplotlib.pyplot as plt
-import scipy.optimize
-from scripts import linreg, chisq_ndf
-from measurementclasses import Experiment
-
-# As their layer was really thin, assume 2D theories
-dimensions = 2;
-
-# Literature values for melting temperature, latent heat and interfacial energy
-class Literature:
-    Tm = 817
-    L = 878.9e6;
-    IE = 5.5e-2 *3
-
-# Some physial constants
-kB = 1.380649e-23
-eV = 1.6021766208e-19
-zeroC = 273.15
-
-# Function for bulk free energy
-dGv = lambda T,L,Tm: L*(Tm-T)/Tm*2*T/(Tm+T)
-
-
-# The temperatures and geometries the data was taken at
-Ts = np.array([115,120,125,130]) + zeroC
-height = 30e-9
-
-# Read all the data
-A = Experiment("../data/experiment/Sb2Te_asd_20W_115C.csv", height)
-B = Experiment("../data/experiment/Sb2Te_asd_20W_120C.csv", height)
-C = Experiment("../data/experiment/Sb2Te_asd_20W_125C.csv", height)
-D = Experiment("../data/experiment/Sb2Te_asd_20W_130C.csv", height)
-
-
-# Get all JMAK fits and growth velocities and calculate nucleation rate from
-# them; get all nucleation rates and calculate nucleation rates from them too
-Js, vs, uvs = np.ones(Ts.shape), np.ones(Ts.shape), np.ones(Ts.shape)
-for i,M in enumerate([A,B,C,D]):
-    v, uv = M.all_rfits(show=False)
-    vs[i] = v
-    uvs[i] = uv
-    [(dNdt,_)] = M.nucleationratefit(show=False)
-    if dimensions == 2:
-        J = dNdt / M.A
-    elif dimensions == 3:
-        J = dNdt / (M.A * height)
-    else:
-        print("Dimensions need to be 2 or 3.")
-    Js[i] = J
-
-
-def velocityfit(show=True):
-    """ Plot velocities over temperature and fit arrhenius behaviour. 
-    Argument show can be 2 to show the arrhenius plot. """
-    fitfunc = lambda x,a,b: a*np.exp(-b/x)
-    m0, _, c0, _, _, _ = linreg(1/Ts, np.log(vs), uvs)
-    popt, pcov = scipy.optimize.curve_fit(fitfunc, Ts, vs, p0=[np.exp(c0),-m0],
-                                          sigma=uvs, absolute_sigma=True)
-    if show==2:
-        plt.errorbar(1/Ts, np.log(vs), uvs/vs, linestyle="none", marker="_",
-                     label="data")
-        m, c = -popt[1], np.log(popt[0])
-        plt.plot(1/Ts, m/Ts+c, label="fit")
-        plt.xlabel("$1/(T \\ [{}^{\circ}C])$")
-        plt.ylabel("$\\log(v \\ [m/s])$")
-    elif show:
-        plt.errorbar(Ts, vs, uvs, linestyle="none", marker="_", label="data")
-        pxs = np.linspace(np.min(Ts)-1,np.max(Ts)+1,100)
-        plt.plot(pxs,fitfunc(pxs,*popt), label="fit")
-        plt.xlabel("$T \\ [{}^{\circ}C]$")
-        plt.ylabel("$v \\ [m/s]$")
-    if show:
-        print("Fitting v = a*exp(-b/T)")
-        print("a:",popt[0],"+-",np.sqrt(pcov[0,0]))
-        print("b:",popt[1],"+-",np.sqrt(pcov[1,1]))
-        Ea, uEa = [x/eV*kB for x in [popt[1], np.sqrt(pcov[1,1])]]
-        print("   = (",Ea,"+-",uEa,")eV/kB")
-        print("chisq/ndf:", chisq_ndf(Ts,vs,uvs,fitfunc,popt))
-        plt.legend()
-        plt.tight_layout()
-        plt.show()
-    return popt, pcov
-
-def nucleationratefit(plotrange=None, show=True):
-    """ 
-    Fit classical nucleation theory formula to J(T).
-    Use either JMAK theory fit  results(second argument False) or counted
-    nuclei (second argument True).
-    A range of temperatures to plot the calculated rate over can be
-    given manually as a 2 tuple.
-    """
-    
-    # Formulas for critical energy and nucleation rate
-    dGc = lambda T,IE,L,Tm: 16*np.pi*IE**3/(3*dGv(T,L,Tm)**2)
-    J = lambda T,b,IE,L,Tm : (b*dGv(T,L,Tm)**2/(8*np.pi) *
-                              1/np.sqrt(IE**3*kB*T) * 
-                              np.exp(-dGc(T,IE,L,Tm)/kB/T))
-    # Fit with a free prefactor and interfacial energy, other should be known
-    fitfunc = lambda T,b,IE : J(T,b,IE,Literature.L,Literature.Tm)
-#    fitfunc = lambda T,b,IE,L,Tm : J(T,b,IE,L,Tm)
-    
-    # Guess for IE is literature value, choose initial prefactor to fit some
-    # arbitary point. Do the fit.
-    b0 = Js[3]/fitfunc(Ts[3],1,Literature.IE)
-    popt, pcov = scipy.optimize.curve_fit(fitfunc, Ts, Js, maxfev=60000,
-                                          p0=[b0,Literature.IE])
-#                              p0=[b0,Literature.IE,Literature.L,Literature.Tm]
-    if show:
-        print("Fitting J = b*dGv**2/(8*pi)/sqrt(IE**3*kB*T) * exp(-dGc/kB/T)")
-        print("        dGc = 16*pi*IE^3/(3*dGv)")
-        print("        dGv = L*(Tm-T)/Tm*2*T/(Tm+T)")
-        print("b: ",popt[0],"+-",np.sqrt(pcov[0,0]))
-        print("IE:",popt[1],"+-",np.sqrt(pcov[1,1]))
-#        print("L: ",popt[2]," +- ",np.sqrt(pcov[2,2]))
-#        print("Tm:",popt[3]," +- ",np.sqrt(pcov[3,3]))
-        plt.plot(Ts, Js, linestyle="none", marker="x", label="mean rates")
-        if plotrange is None:
-            pxs = np.linspace(np.min(Ts)-3,np.max(Ts)+3,50)
-        else:
-            pxs = np.linspace(plotrange[0],plotrange[-1],200)
-        plt.plot(pxs,fitfunc(pxs,*popt), label="fit")
-        plt.xlabel("$T \\ [{}^{\circ}C]$")
-        plt.ylabel("$J \\ [1/m^{}/s]$".format(dimensions))
-        plt.tight_layout()
-        plt.legend()
-        plt.show()
-    return popt, pcov
-
-def mobilityfit(show=True):
-    """ Calculate mobility (as defined in the AIST paper), plot over
-    temperature and fit arrhenius behaviour """
-    
-    M = v/dGv(Ts,Literature.L,Literature.Tm)
-    
-    plt.plot(Ts, M, linestyle="none", marker="x", label="data")
-    expfunc = lambda x,a,b: a*np.exp(b/x)
-    m0, _, c0, _, _, _ = linreg(1/Ts, np.log(M), uvs)
-    popt, pcov = scipy.optimize.curve_fit(expfunc, Ts, M, p0=[np.exp(c0),m0],
-                                          sigma=uvs)
-    pxs = np.linspace(np.min(Ts)-3,np.max(Ts)+3,50)
-    plt.plot(pxs,expfunc(pxs,np.exp(c0),m0),label="fit")
-    plt.xlabel("T [C]")
-    plt.ylabel("M [m/s / (J/m^3)]")
-
-#A.nucleationratefit(splitat=[1.2e3,10e3,19e3,28e3])
-#B.nucleationratefit(splitat=[1000,8888])
-#C.nucleationratefit(splitat=[2000,3500])
-#D.nucleationratefit(splitat=[250,1100,1900])
diff --git a/py/experimentalfits.py b/py/experimentalfits.py
new file mode 100644
index 0000000000000000000000000000000000000000..c3ed03beef8ea5fb07dda76224ccb56050ce48a4
--- /dev/null
+++ b/py/experimentalfits.py
@@ -0,0 +1,115 @@
+import numpy as np
+import matplotlib.pyplot as plt
+import fits
+
+# Set font size and stuff for images in the tex document
+plt.rcParams.update({'font.size': 16,
+                     'lines.linewidth': 2,
+                     'lines.markersize': 10,
+                     'lines.markeredgewidth': 2})
+
+# Read the experiments
+height = 30e-9
+Ex115 = fits.Experiment("../data/experiment/Sb2Te_asd_20W_115C.csv", height)
+Ex120 = fits.Experiment("../data/experiment/Sb2Te_asd_20W_120C.csv", height)
+Ex125 = fits.Experiment("../data/experiment/Sb2Te_asd_20W_125C.csv", height)
+Ex130 = fits.Experiment("../data/experiment/Sb2Te_asd_20W_130C.csv", height)
+
+# Methods performing fits on this data, giving images and information ready to
+# use in tex
+
+def show_incubationtime():
+    axes = Ex120.plot_N()
+    fig = axes.figure
+    line = axes.lines[0]
+    line.set_linestyle(":")
+    axes.set_xlim(-120, 3500)
+    axes.set_ylim(-5, 120)
+    axes.set_title("Sb${}_2$Te, 120°C")
+    fig.canvas.draw()
+    fig.tight_layout()
+
+def fit_v(Ex=Ex130, i=2):
+    vfit = Ex.velocityFit()
+    vfit.fitall()
+    vfit.info()
+    axes = vfit.plot(i)
+    Ex.plot_r(i, axes)
+    [fitline, expline] = axes.lines
+    expline.set_color("red")
+    fitline.set_color("black")
+    axes.legend()
+    fig = axes.figure
+    fig.tight_layout()
+
+def fit_J_115():
+    jfit = Ex115.nucleationRateFit([1.7e3,9.7e3,20e3]).fit()
+    jfit.info()
+    axes = jfit.plotJ()
+    axes.set_xlim(-500,31800)
+    axes.set_ylim(-0.001, 0.048)
+    fig = axes.figure
+    fig.tight_layout()
+
+def fit_Nc_115():
+    jfit = Ex115.nucleationRateFit([1.7e3,9.7e3,20e3]).fit()
+    jfit.info()
+    axes = jfit.plotN()
+    axes.figure.tight_layout()
+
+def comp_120():
+    axes = Ex120.plot_fc()
+    axes.lines[0].set_markersize(6)
+    fit = Ex120.jmakFit().fit(2)
+    rec = Ex120.jmakBuild(2)
+    fit.plot(axes)
+    rec.plot(axes)
+    axes.legend()
+    axes.figure.tight_layout()
+
+def comp_125():
+    axes = Ex125.plot_fc()
+    axes.lines[0].set_markersize(6)
+    fit = Ex125.jmakFit().fit(2)
+    rec = Ex125.jmakBuild(2)
+    fit.plot(axes)
+    rec.plot(axes)
+    axes.legend()
+    axes.figure.tight_layout()
+
+# Combine the measurements and perform fits to their relations
+
+S = fits.SeriesOfMeasurements(np.array([115,120,125,130]) + 273.15,
+                              [Ex115,Ex120,Ex125,Ex130], 2)
+S.load()
+V = fits.ArrheniusFit(S).fit()
+C = fits.SimpleCntFit(S, 296.4e6, 837, 9e-2).fit()
+
+def fit_cnt():
+    C.info()
+    fig, axes = plt.subplots(1,1)
+    axes.set_xlim(384.7,405.3)
+    axes.set_ylim(-1e12,4e13)
+    C.plot(axes)
+    axes.legend()
+    fig.tight_layout()
+
+def fit_arrhenius():
+    V.info()
+    axes = V.plot()
+    axes.legend()
+    axes.figure.tight_layout()
+
+def fit_arrhenius_lin():
+    V.info()
+    axes = V.plot_lin()
+    axes.set_xticks(np.linspace(2.48e-3, 2.58e-3, 3))
+    axes.legend()
+    axes.figure.tight_layout()
+
+def fit_mobility(L=296.4e6, Tm=837):
+    mf = S.mobilityFit(lambda T: L*(Tm-T)/(2*Tm)*2*T/(Tm+T) ).fit()
+    mf.info()
+    axes = mf.plot()
+    axes.legend()
+    axes.figure.tight_layout()
diff --git a/py/fits/__init__.py b/py/fits/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..269a1989138b65b1e992df994243b93b52297e25
--- /dev/null
+++ b/py/fits/__init__.py
@@ -0,0 +1,15 @@
+"""
+Analysis of experiments on phase change materials.
+"""
+
+# Import classes for single measurements and fits
+from .measurements import *
+from .measurements import __all__
+
+# Import classes for combining several measurements
+from .series import *
+from .series import __all__ as __all2__
+
+# Correct list of available classes
+__all__.extend(__all2__)
+del __all2__
diff --git a/py/fits/measurements.py b/py/fits/measurements.py
new file mode 100644
index 0000000000000000000000000000000000000000..c7d6ec352e51b17ae563dbe88c75da344b1cae0a
--- /dev/null
+++ b/py/fits/measurements.py
@@ -0,0 +1,551 @@
+"""
+This file provides classes to analyse experimental or simulated data on the
+crystallisation of a phase change material.
+The first type of classes represent measurements:
+    Measurement - Parent class for all types of measurements
+    Experiment - Class for experimental results as I got from Julian
+    Simulation - Class for simulation results as returned from the export
+        methods in my simulation code.
+The second type represents fits to several quantities:
+    VelocityFit - Fit to the grain sizes
+    NucleationRateFit - Fit to the number of counted nuclei
+    JmakFit - Fit of a JMAK model to the crystallised fraction
+    JmakBuild - Conterpart to JmakFit: Get a crystallised fraction
+        from JMAK model and fit results for v and J
+"""
+
+import csv
+import numpy as np
+import matplotlib.pyplot as plt
+import scipy.optimize
+from .scripts import linreg, weightedmean, chisq_ndf
+
+NaN = float('nan')
+
+def lininterp(a):
+    """
+    Linear inperpolation of an array.
+    The length of the returned array is one smaller as the one of the given.
+    Each element is the mean of two neighbouring elements of a.
+    """
+    return (a[1:] + a[:-1]) / 2
+
+class Measurement:
+    """
+    A class for a measurement for one temperature as I got from Julian or as
+    returned from one simulation run.
+    Properties:
+      times - times of measurements [s]
+      nuclei - number of seen nuclei
+      radii - 2D array of radii [m]
+      afract - amorphous fraction [1]
+      cfract - crystallized fraction [1]
+    """
+
+    def plot_fc(self, axes=None):
+        """ Plot the crystallised fraction over time. """
+        if axes is None:
+            fig, axes = plt.subplots(1,1)
+        axes.plot(self.times, self.cfract, linestyle="none", marker="x",
+                  label="data")
+        axes.set_xlabel("$t \\ [s]$")
+        axes.set_ylabel("$f_c$")
+        return axes
+        
+    def plot_N(self, axes=None):
+        """ Plot the counted nuclei over time. """
+        if axes is None:
+            fig, axes = plt.subplots(1,1)
+        axes.plot(self.times, self.nuclei, linestyle="none", marker="x",
+                  label="data")
+        axes.set_xlabel("$t \\ [s]$")
+        axes.set_ylabel("$N$")
+        return axes
+    
+    def plot_r(self, i, axes=None):
+        """ Plot the size of the i-th nucleus over time. """
+        if axes is None:
+            fig, axes = plt.subplots(1,1)
+        axes.plot(self.times, self.radii[i]*1e6, linestyle="none", marker="x",
+                  label="$r_{{{}}}$".format(i))
+        axes.set_xlabel("$t \\ [s]$")
+        axes.set_ylabel("$r \\ [\\mu m]$")
+        return axes
+    
+    def jmakFit(self):
+        """ Return an object for a JMAK fit to measurement. """
+        return JmakFit(self)
+    
+    def velocityFit(self):
+        """ Return an object for a velocity fit to measurement. """
+        return VelocityFit(self)
+    
+    def nucleationRateFit(self, splitat=None):
+        """ Return an object for a nucleation rate fit to measurement. """
+        return NucleationRateFit(self,splitat)
+    
+    def jmakBuild(self, dimensions):
+        """ 
+        Return an object giving a function for the crystallised fraction
+        calculated in JMAK theory with velocity and nucleation rate from fits.
+        """
+        return JmakBuild(self.velocityFit().fitall(),
+                         self.nucleationRateFit(splitat=[]).fit(),
+                         dimensions)
+
+
+class VelocityFit:
+    """
+    A class to fit a linear growht velocity to one measurement, to get
+    information about the fit and to visualise it.
+    Properties:
+        vs  - Array of velocities for the individual grains [m/s]
+        uvs - Array of uncertainties to vs [m/s]
+        v   - Mean velocity [m/s]
+        uv  - Uncertainty to v [m/s]
+    """
+    
+    def __init__(self, M):
+        """ Build an object for a measurement M. """
+        self.M = M
+        # Extract the necessary data
+        self.rs = M.radii
+        self.ts = M.times
+        # Initialise results
+        self.vs = np.zeros(len(M.radii)) * NaN
+        self.uvs = np.zeros(len(M.radii)) * NaN
+        self._cs = np.zeros(len(M.radii)) * NaN
+        self.v, self.uv = NaN, NaN
+    
+    def fit(self, i, maxtime=None):
+        """
+        Fit a linear groth to the i-th grain.
+        If the growth is not linear anymore after a time maxtime, the fit only
+        takes earlier points into account.
+        """
+        if maxtime is None:
+            maxtime = float('inf')
+        # Extract points where t < tmax
+        cond = np.logical_and(np.logical_not( np.isnan(self.rs[i]) ),
+                              self.ts < maxtime)
+        ts = np.compress(cond, self.ts)
+        rs = np.compress(cond, self.rs[i])
+        # Linear regression
+        v, uv, c, _, _, chiq = linreg(ts, rs, np.ones(rs.shape))
+        # Scale to make chisquared/ndf equals 1
+        chiq = chiq/(len(ts)-2)
+        uv = uv * np.sqrt(chiq)
+        # Save results
+        self.vs[i], self.uvs[i], self._cs[i] = v, uv, c
+        return self
+    
+    def fitall(self):
+        """ Fit all grains and calculate mean growht velocity """
+        for i,_ in enumerate(self.vs):
+            self.fit(i)
+        self.v, self.uv = np.mean(self.vs), np.std(self.vs)
+        return self
+    
+    def info(self):
+        """ Print some information about this fit. """
+        print("Velocity fit:")
+        for i, (v, uv) in enumerate(zip(self.vs, self.uvs)):
+            print("  {}-th grain: v={}+-{}".format(i,v,uv))
+        print("mean uncertainty:", np.mean(self.uvs))
+        print("overall result: v =", self.v, "+-", self.uv)
+        return self
+    
+    def plot(self, i, axes=None):
+        """ Plot the fit to the i-th grain """
+        if axes is None:
+            fig, axes = plt.subplots(1,1)
+        ts, rs, v, c = self.ts, self.rs[i], self.vs[i], self._cs[i]
+        ts = np.compress(np.logical_not(np.isnan(rs)), ts)
+        axes.plot(ts, (v*ts+c)*1e6, label="$v_{{{}}}\\cdot t$".format(i),
+                  alpha=0.7)
+        axes.set_xlabel("$t \\ [s]$")
+        axes.set_ylabel("$r \\ [\\mu m]$")
+        return axes
+
+class NucleationRateFit:
+    """
+    Fit one or more nucleation rates on the data.
+    Properties:
+        Js - List of (J,uJ) tupels where J is a nucleation rate [1/s] and uJ
+            its uncertainty
+        domainboundaries - List of (tmin, tmax) tupels defining the borders of
+            the domains a constant rate was fittet to
+    """
+    
+    def __init__(self, M, splitat):
+        """ Build an object for a measurement M. """
+        self.M = M
+        
+         # If no points to split at are given, create an empty list
+        if splitat is None:
+            splitat = []
+        # Calculate domain boundarys: Insert first time at front, cut out every
+        # time thats out of available times and insert last time at end
+        splitat = np.insert(splitat, 0, M.times[0])
+        splitat = np.compress(splitat<M.times[-1], splitat)
+        splitat = np.append(splitat,M.times[-1])
+        self.domainboundaries = list(zip(splitat[0:-1], splitat[1:]))
+        
+        # Extract necessary data
+        self._dN = np.diff(M.nuclei)
+        self._dt = np.diff(M.times)
+        self._tm = lininterp(M.times) # interpolated times
+        self._ta = M.times            # all times
+        self._fa = lininterp(M.afract)
+        self._Js = self._dN/self._dt * 1/self._fa
+        self._uJs = np.sqrt(np.mean(self._Js)/(self._dt * self._fa))
+        
+        self.Js = [(NaN,NaN)]
+    
+    def fit(self):
+        """ Perform the fit. """
+        res = []
+        # For each of the domains
+        for tmin, tmax in self.domainboundaries:
+            # extract data in this domain
+            cond = np.logical_and(self._tm >= tmin, self._tm <= tmax)
+            Js_i = np.compress(cond, self._Js)
+            dN_i = np.compress(cond, self._dN)
+            dt_i = np.compress(cond, self._dt)
+            fa_i = np.compress(cond, self._fa)
+            # Assume poisson uncertainty with the mean rate on all points,
+            # override the original guessed uncertainty
+            uJ_i = np.sqrt(np.maximum(np.mean(dN_i),1))/dt_i * 1/fa_i
+            self._uJs[cond] = uJ_i
+            # Mean rate
+            J, uJ = weightedmean(Js_i, uJ_i)
+            res.append((J,uJ))
+        self.Js = res
+        return self
+    
+    def info(self):
+        """ Print some information about this fit. """
+        for (tmin, tmax), (J,uJ) in zip(self.domainboundaries, self.Js):
+            print("from",tmin,"to",tmax,":")
+            print("   rate:",J,"+-",uJ)
+        return self
+    
+    def plotN(self, axes=None):
+        """ Plot the integrated rates. """
+        if axes is None:
+            fig, axes = plt.subplots(1,1)
+        Js, dt = self._Js, self._dt
+        # Integrate again
+        N2 = np.cumsum(Js * dt)
+        N2 = np.insert(N2,0,0)
+        # Assume poisson uncertainty
+        uN2 = np.maximum(np.sqrt(N2), 1)
+        axes.errorbar(self._ta, N2, uN2, linestyle=":", label="data")
+        y0, y1 = 0, 0
+        for (tmin, tmax), (J,_) in zip(self.domainboundaries, self.Js):
+            y0, y1 = y1, y1+J*(tmax-tmin)
+            plt.plot([tmin,tmax], [y0, y1], label="integrated mean rate")
+        axes.set_xlabel("$t \\ [s]$")
+        axes.set_ylabel("$N_{corrected}$")
+        return axes
+    
+    def plotJ(self, axes=None):
+        """ Plot momentary and mean rates. """
+        if axes is None:
+            fig, axes = plt.subplots(1,1)
+        t, Js, uJs = self._tm, self._Js, self._uJs
+        axes.errorbar(t, Js, uJs, linestyle="none", marker="_")
+        for (tmin, tmax), (J,_) in zip(self.domainboundaries, self.Js):
+            axes.hlines(J,tmin,tmax)
+        axes.set_xlabel("$t\\ [s]$")
+        axes.set_ylabel("$\\frac{1}{f_a}\\frac{\\Delta N}{\\Delta t}$")
+        return axes
+
+
+class JmakFit:
+    """
+    A class to fit a JMAK model to the crystallised fraction of a measurement.
+    Properties:
+        n     - JMAK exponent [1]
+        k     - JMAK coefficient [s^(-n)]
+        un    - Uncertainty on n [1]
+        uk    - Uncertainty on k [s^(-n)]
+        corr  - Correlation coefficient of n and k
+        chisq - Chisquared of the fit
+    """
+    
+    def __init__(self, M):
+        """ Build an object for a measurement M. """
+        self.M = M
+        # Initialise used values
+        self.n, self.un = NaN, NaN
+        self.k, self.uk = NaN, NaN
+        self.corr, self.chisq = NaN, NaN
+    
+    def linearise(self):
+        """
+        Get the linearised vaiables:
+            xs = log( t )
+            ys = log( - log( 1 - f_c ))
+            sigmas = const / ( log(f_a) * f_a )
+        """
+        # Extract points where log() is well defined
+        M = self.M
+        cond = np.logical_and( np.logical_and( M.afract > 0,
+                                               M.afract < 1),
+                               M.times > 0)
+        # Linearise
+        xs = np.log( np.compress(cond, M.times))
+        fs = np.compress(cond, M.afract)
+        ys = np.log( - np.log( fs ))
+        # Assume equal uncertainty (=1) on area values, transform to y
+        sigmas = np.abs(np.ones(xs.shape)/(np.log(fs)*fs))
+        return xs, ys, sigmas
+    
+    def linfit(self):
+        """
+        Perform a fit to the linearised problem and save the result.
+        """
+        xs, ys, sigmas = self.linearise()
+        n, un, c, uc, corr, chisq = linreg(xs, ys, sigmas)
+        chisqndf = chisq / (len(xs)-2)
+        # correct uncertainties, calculate k
+        un, uc = un*np.sqrt(chisqndf), uc*np.sqrt(chisqndf)
+        k, uk = np.exp(c), np.exp(c)*uc
+        self.n, self.un = n, un
+        self.k, self.uk = k, uk
+        self.corr, self.chisq = corr, 1
+        return self
+    
+    def fit(self, dimensions=None):
+        """
+        Perform a fit on the original data, considering also values where the
+        linear problem is not defined
+        """
+        # Initial values from linear problem
+        if self.k is NaN:
+            self.linfit()
+        n0, k0 = self.n, self.k
+        # Get data
+        times, fa = self.M.times, self.M.afract
+        # Define function to fit onto
+        fitfunc = lambda t,n,k: np.exp(-k*t**n)
+        if dimensions is None:
+            popt, pcov = scipy.optimize.curve_fit(fitfunc, times, fa,
+                                                  p0=[n0,k0])
+            corr = pcov[0,1]/np.sqrt(pcov[0,0]*pcov[1,1])
+        else:
+            # If a dimension is given, calculate exponent and put it into
+            # the fitfunction
+            n = dimensions + 1
+            fitfunc2 = lambda t,k: fitfunc(t,n,k)
+            popt, pcov = scipy.optimize.curve_fit(fitfunc2, times, fa, p0=[k0])
+            # Fill popt and pcov to the same size as from above
+            popt = np.insert(popt, 0, n)
+            pcov = np.array([[0, 0], [0, pcov[0,0]]])
+            corr = 0
+        # Get chisquared and correct uncertainties, save
+        chisq = chisq_ndf(times, fa, np.ones(fa.shape), fitfunc, popt)
+        pcov = pcov*chisq
+        self.n, self.k = popt
+        self.un, self.uk = np.sqrt(pcov[0,0]), np.sqrt(pcov[1,1])
+        self.corr, self.chisq = corr, 1
+        return self
+    
+    def info(self):
+        """ Print some information about the fit. """
+        print("f_a = exp(-k*t^n)")
+        print("<=> ln(-ln(f_a)) = n * ln(t) + ln(k)")
+        print("Result:")
+        print("  n:", self.n, "+-", self.un)
+        print("  k:", self.k, "+-", self.uk)
+        print("  correlation:", self.corr)
+        return self
+    
+    def resfun(self, t):
+        """
+        Return the crystalline fraction to a time t as calculated from the
+        model and the results from the fit.
+        """
+        return 1-np.exp(-self.k*t**self.n)
+    
+    def linplot(self, axes=None):
+        """ Plot the linearised problem and the fit. """
+        if axes is None:
+            fig, axes = plt.subplots(1,1)
+        xs, ys, _ = self.linearise()
+        n, k = self.n, self.k
+        c = np.log(k)
+        axes.plot(xs, ys, linestyle="none", marker="x", label="data")
+        axes.plot(xs, n*xs+c, label="fit")
+        axes.set_xlabel('$\ln(t \\ [s])$')
+        axes.set_ylabel('$\ln( - \ln( 1 - f_c ))$')
+        return axes
+    
+    def plot(self, axes=None):
+        """ Plot the fit. """
+        if axes is None:
+            fig, axes = plt.subplots(1,1)
+        plottimes = np.linspace(np.min(self.M.times), np.max(self.M.times), 100)
+        axes.plot(plottimes, self.resfun(plottimes), label="JMAK fit")
+        axes.set_xlabel("$t \\ [s]$")
+        axes.set_ylabel("$f_c$")
+        return axes
+
+
+class JmakBuild:
+    """
+    This class gives a counterpart to JmakFit coming from a defined velocity
+    and nucleation rate.
+    Properties:
+        M  - Measurement
+        n  - JMAK exponent [1]
+        k  - JMAK coefficient [s^(-n)]
+        un - Uncertainty on n [1]
+        uk - Uncertainty on k [s^(-n)]
+    """
+    
+    def __init__(self, vFit, jFit, dimensions):
+        """
+        Initialise. Needs a velocity and a nucleation rate fit object
+        and a number of dimensions to use.
+        """
+        # Save the fits
+        self.M = jFit.M
+        self.vFit = vFit
+        self.jFit = jFit
+        # Extract velocity with uncertainty
+        v, uv = vFit.v, vFit.uv
+        if v is NaN:
+            raise AttributeError("Velocity not yet fitted.")
+        # Try to get a single value for J
+        try:
+            [(dNdt, udNdt)] = jFit.Js
+        except ValueError as err:
+            print("Probably more than one one nucleation rate was fitted.")
+            raise err
+        # Try to get an area and a height
+        try:
+            A, h = self.M.A, self.M.h
+        except AttributeError as err:
+            print("Data regarding the volume missing in the used measurement.")
+            raise err
+        # Calculate J
+        J  =  dNdt / (A*h)
+        uJ = udNdt / (A*h)
+        # Calculate JMAK formula according to choosen dimension
+        if dimensions == 1:
+            self.k = J*A*v
+            self.uk = np.sqrt( (uJ*A*v)**2 + (J*A*uv)**2 )
+        elif dimensions == 2:
+            self.k = np.pi/3 * J * h * v**2
+            self.uk = np.pi/3 * np.sqrt( (uJ*h*v**2)**2 + (2*J*h*v*uv)**2 )
+        elif dimensions == 3:
+            self.k = np.pi/3 * J * v**3
+            self.uk = np.pi/3 * np.sqrt( (uJ*v**3)**2 + (3*J*v**2*uv)**2 )
+        else:
+            raise ValueError("Wrong number of dimensions")
+        # Exponent with uncertainty
+        self.n, self.un = dimensions + 1, 0
+    
+    def resfun(self, t):
+        """
+        Time dependtent equation for the crystallised fraction from the fit
+        results.
+        """
+        return 1-np.exp(-self.k*t**self.n)
+    
+    def plot(self, axes=None):
+        """ Plot the fit. """
+        if axes is None:
+            fig, axes = plt.subplots(1,1)
+        plottimes = np.linspace(np.min(self.M.times), np.max(self.M.times), 100)
+        axes.plot(plottimes, self.resfun(plottimes),
+                  label="JMAK $\\left(v,\\ \\frac{dN}{dt} \\right)$")
+        axes.set_xlabel("$t \\ [s]$")
+        axes.set_ylabel("$f_c$")
+        return axes
+
+
+# Calculate a length from the values in pixels, according to the mail I got.
+# First method is the correction I got in the second mail
+correctedpx = lambda px: 300/72*px
+px2len = lambda p: (0.3876 * correctedpx(p) - 0.3802) * 1e-6
+
+class Experiment(Measurement):
+    """
+    Class for experimental data as I got from Julian.
+    Additional properties:
+      columntitles - column titles from the csv
+      aarea - amorphous area [m^2]
+      A - overall observed area [m^2]
+      h - thickness of the material [m]
+    """
+    
+    def __init__(self, filename, height):
+        """ Read all data from the file and build variables of interest. """
+         
+        # Open file and build csv reader
+        file = open(filename, newline="")
+        reader = csv.reader(file, delimiter=",")
+        # First line has titles to the columns
+        self.columntitles = list(map(lambda x: x.strip(), next(reader)))
+        # Rest is data structured in columns
+        columns = [ [] for _ in self.columntitles]
+        for row in reader:
+            for column, value in zip(columns, row):
+                # Replace empty cells with nan to parse them to float
+                if value == "":
+                    value = "nan"
+                column.append(value.replace(",","."))
+        # First column are times in minutes, convert to seconds
+        self.times = np.array(columns[0], dtype=np.float) * 60
+        # Second column is amporphous area. The square root is a length and can
+        # be converted according to the mail, then square the result again
+        self.aarea = (px2len(np.sqrt(np.array(columns[1], dtype=np.float))))**2
+        # Third column has number of grains, here no value means probably zero
+        self.nuclei = np.array(columns[2], dtype=np.float)
+        self.nuclei[np.isnan(self.nuclei)] = 0
+        # Rest of the file are radii of grains
+        self.radii = px2len( np.array(columns[3:], dtype=np.float) )
+        # Overall observed area
+        self.A = max(self.aarea)
+        self.h = height
+        # Fractions of amorphous and crystaline area
+        self.afract = self.aarea/self.A
+        self.cfract = 1-self.afract
+
+
+class Simulation(Measurement):
+    """
+    Class for simulation results as received from my code.
+    Additional properties:
+        columntitles - column titles from the csv
+    """
+    
+    def __init__(self, filename):
+        """ Read all data from the file and build variables of interest. """
+        
+        # Open file and build csv reader
+        file = open(filename, newline="")
+        reader = csv.reader(file, delimiter=",")
+        # First line has titles to the columns
+        self.columntitles = list(map(lambda x: x.strip(), next(reader)))
+        # Rest is data structured in columns
+        lines = []
+        for row in reader:
+            lines.append(row)
+        data = np.array(lines, dtype=np.float)
+        # First column are times
+        self.times = data[:,0]
+        # Second column has number of grains
+        self.nuclei = data[:,1]
+        # Third column is crystalline fraction
+        self.cfract = data[:,2]
+        self.afract = 1 - self.cfract
+        # Rest of the file are sizes of grains
+        self.radii = np.sqrt(data[:,3:]/np.pi).transpose()
+
+
+__all__ = ["Measurement", "Experiment", "Simulation", "VelocityFit",
+           "NucleationRateFit", "JmakFit", "JmakBuild"]
diff --git a/py/scripts.py b/py/fits/scripts.py
similarity index 70%
rename from py/scripts.py
rename to py/fits/scripts.py
index 44783ba2219207009638cd52f85dde2884375386..219bbb510e6374a196c70b7a7d502499ded93c32 100644
--- a/py/scripts.py
+++ b/py/fits/scripts.py
@@ -1,4 +1,10 @@
-# Some math stuff
+"""
+This file provides some general mathematical methods:
+    linreg - Linear regression
+    weightedmean - Weighted mean value of values with uncertainties
+    chisq_ndf - Chisquared per degree of freedom for given data, function and
+        parameters
+"""
 
 import numpy as np
 
@@ -41,4 +47,13 @@ def chisq_ndf(x,y,uncert_y,f,popt):
     Returns the chisquared per degree of freedom for data x, y with
     uncertainty uncert_y and a function f(x,*popt) with parameters popt.
     """
-    return np.sum(((y-f(x,*popt))/uncert_y)**2) / (len(x) - len(popt))
\ No newline at end of file
+    return np.sum(((y-f(x,*popt))/uncert_y)**2) / (len(x) - len(popt))
+
+def enlargedLinspace(left,right,num=50,factor=1):
+    """
+    Get evenly spaced numbers over the interval from left to right, enlarged by
+    a factor.
+    Default factor is one, default amount of points is 50.
+    """
+    shift = (right-left)*factor
+    return np.linspace(left-shift, right+shift, num)
\ No newline at end of file
diff --git a/py/fits/series.py b/py/fits/series.py
new file mode 100644
index 0000000000000000000000000000000000000000..f6eb2dea942916bc6035ce831442e23e5deb53ff
--- /dev/null
+++ b/py/fits/series.py
@@ -0,0 +1,298 @@
+"""
+This file provides classes to fit temperature dependent models to data obtained
+from several measurements at different temperatures.
+"""
+
+import numpy as np
+import matplotlib.pyplot as plt
+import scipy.optimize
+from .scripts import linreg, chisq_ndf
+
+# Some physial constants
+kB = 1.380649e-23
+eV = 1.6021766208e-19
+
+class SeriesOfMeasurements:
+    """
+    A class for a series of several measurements at different temperatures.
+    """
+    
+    def __init__(self, temperatures, measurements, dimensions):
+        """
+        Initialise a series of measurements (given as a list) at different
+        temperatures (given as a list).
+        Analyse assuming a given number of dimensions.
+        """
+        self.Ts = np.array(temperatures)
+        self.Ms = measurements
+        self.dimensions = dimensions
+    
+    def load(self):
+        """
+        Do all the fits for all the measurements.
+        """
+        self.vs = np.zeros(len(self.Ms))
+        self.uvs  = np.zeros(len(self.Ms))
+        self.Js  = np.zeros(len(self.Ms))
+        self.uJs  = np.zeros(len(self.Ms))
+        for i,M in enumerate(self.Ms):
+            vFit = M.velocityFit().fitall()
+            self.vs[i], self.uvs[i] = vFit.v, vFit.uv
+            volume = M.A * M.h
+            jFit = M.nucleationRateFit().fit()
+            [(J,uJ)] = jFit.Js
+            self.Js[i], self.uJs[i] = J/volume, uJ/volume
+        return self
+    
+    def arrheniusFit(self):
+        """
+        Return an arrhenius behaviour fit for the growth velocities of this
+        series of measurements.
+        """
+        return ArrheniusFit(self)
+    
+    def simpleCntFit(self, L, Tm, IE):
+        """
+        Return an object to fit classical nucleation theory to the rates of
+        this series, using given values for the latent heat L, the melting
+        temperature Tm and an initial guess for the interfacial energy IE.
+        """
+        return SimpleCntFit(self, L, Tm, IE)
+    
+    def mobilityFit(self, dGv):
+        """
+        Return an arrhenius behaviour fit for the mobility v/dGv of this
+        series of measurements, using a given temperature dependent function
+        for the free energy.
+        """
+        return MobilityFit(self, dGv)
+
+
+class ArrheniusFit:
+    """ Class to fit an Arrhenius behaviour to the growth velocity. """
+    
+    def __init__(self, series):
+        """ Build an object of a given series of measurements. """
+        self.Ts = series.Ts
+        self.vs = series.vs
+        self.uvs = series.uvs
+    
+    def linfit(self):
+        """ Fit in the linearised problem. """
+        m, um, c, uc, _, _ = linreg(1/self.Ts, np.log(self.vs), self.uvs)
+        self.vinf, self.uvinf = np.exp(c), np.exp(c)*uc
+        self.Ea, self.uEa = -m, um
+        return self
+    
+    def fit(self):
+        """ Perform a fit. """
+        self.linfit()
+        fitfunc = lambda x,a,b: a*np.exp(-b/x)
+        Ts, vs, uvs = self.Ts, self.vs, self.uvs
+        p0 = [self.vinf, self.Ea]
+        popt, pcov = scipy.optimize.curve_fit(fitfunc, Ts, vs, p0=p0,
+                                              sigma=uvs, absolute_sigma=True)
+        self.vinf, self.Ea = popt[0], popt[1]
+        self.uvinf, self.uEa = np.sqrt(pcov[0,0]), np.sqrt(pcov[1,1])
+        return self
+    
+    def v(self,T):
+        """
+        Velocity as a function of temperature according to the fit result.
+        """
+        return self.vinf * np.exp(-self.Ea/T)
+    
+    def info(self):
+        """ Print some information. """
+        print("Fitting v = vinf*exp(-Ea/T)")
+        print("vinf = {} +- {}".format(self.vinf, self.uvinf))
+        print("Ea   = {} +- {}".format(self.Ea, self.uEa))
+        Ea, uEa = [x/eV*kB for x in [self.Ea, self.uEa]]
+        print("   = (",Ea,"+-",uEa,")eV/kB")
+        print("chisq/ndf:", chisq_ndf(self.Ts, self.vs, self.uvs,
+                                      (lambda x,a,b: a*np.exp(-b/x)),
+                                      [self.vinf, self.Ea]))
+        return self
+    
+    def plot_lin(self, axes=None):
+        """ Plot the linearised problem. """
+        if axes is None:
+            fig, axes = plt.subplots(1,1)
+        invTs = 1/self.Ts
+        m, c = -self.Ea, np.log(self.vinf)
+        axes.errorbar(invTs, np.log(self.vs), self.uvs/self.vs,
+                      linestyle="none", marker="_", label="data")
+        axes.plot(invTs, m*invTs+c, label="fit")
+        axes.set_xlabel("$1/(T \\ [K])$")
+        axes.set_ylabel("$\\log(v \\ [m/s])$")
+        return axes
+    
+    def plot(self, axes=None):
+        """ Show the data and fit. """
+        if axes is None:
+            fig, axes = plt.subplots(1,1)
+        axes.errorbar(self.Ts, self.vs, self.uvs, linestyle="none", marker="_",
+                     label="data")
+        pxs = np.linspace(np.min(self.Ts)-1,np.max(self.Ts)+1,100)
+        axes.plot(pxs,self.v(pxs), label="fit")
+        axes.set_xlabel("$T \\ [K]$")
+        axes.set_ylabel("$v \\ [m/s]$")
+        return axes
+
+
+# Function for bulk free energy and critical energy
+def bulkFreeEnergy(T,L,Tm):
+    return L*(Tm-T)/Tm*2*T/(Tm+T)
+
+def criticalFreeEnergy(T,IE,L,Tm):
+    return 16*np.pi*IE**3/(3*bulkFreeEnergy(T,L,Tm)**2)
+
+class SimpleCntFit:
+    """
+    A simple fit of classical nucleation theory onto the nucleation rates,
+    using given values for latent heat L and melting temperature Tm and leaving
+    the prefactor b and interfacial energy IE as fit parameters.
+    """
+    
+    def __init__(self, series, L, Tm, IE):
+        """
+        Build an object of a given series of measurements, using given values
+        for the latent heat L, melting temperature Tm and an initial guess for
+        the interfacial energy IE.
+        """
+        self.Ts = series.Ts
+        self.Js = series.Js
+        self.uJs = series.uJs
+        self.L, self.Tm, self.IE = L, Tm, IE
+    
+    def fit(self):
+        """ Perform the fit. """
+        Ts, Js, uJs = self.Ts, self.Js, self.uJs
+        # Functions for nucleation rate from CNT
+        J = lambda T,b,IE,L,Tm: (b*bulkFreeEnergy(T,L,Tm)**2/(8*np.pi) *
+                                1/np.sqrt(IE**3*kB*T) * 
+                                np.exp(-criticalFreeEnergy(T,IE,L,Tm)/kB/T))
+        # Fitfunction with L and Tm from literature
+        fitfunc = lambda T,b,IE: J(T,b,IE,self.L,self.Tm)
+        # Guess for IE is literature value, choose initial prefactor to fit some
+        # arbitary point. Do the fit.
+        b0 = Js[len(Js)//2]/fitfunc(Ts[len(Js)//2],1,self.IE)
+        popt, pcov = scipy.optimize.curve_fit(fitfunc, Ts, Js, maxfev=60000,
+                                              p0=[b0,self.IE])
+        self.b, self.IE = popt
+        self.ub, self.uIE = np.sqrt(pcov[0,0]), np.sqrt(pcov[1,1])
+#        self.b, self.IE = b0, self.IE
+#        self.ub, self.uIE = 1, 1
+        return self
+    
+    def J(self,T):
+        """
+        Nucleation rate as a temperature dependent function from this fit.
+        """
+        b, IE, L, Tm = self.b, self.IE, self.L, self.Tm
+        return (b*bulkFreeEnergy(T,L,Tm)**2/(8*np.pi) *
+                1/np.sqrt(IE**3*kB*T) * 
+                np.exp(-criticalFreeEnergy(T,IE,L,Tm)/kB/T))
+    
+    def info(self):
+        """ Print some information. """
+        print("Fitting J = b*dGv**2/(8*pi)/sqrt(IE**3*kB*T) * exp(-dGc/kB/T)")
+        print("        dGc = 16*pi*IE^3/(3*dGv)")
+        print("        dGv = L*(Tm-T)/Tm*2*T/(Tm+T)")
+        print("b: ",self.b,"+-",self.ub)
+        print("IE:",self.IE,"+-",self.uIE)
+        return self
+    
+    def plot(self, axes=None):
+        """ Plot the problem and fit. """
+        Ts, Js = self.Ts, self.Js
+        if axes is None:
+            fig, axes = plt.subplots(1,1)
+            plotrange = np.min(Ts), np.max(Ts)
+        else:
+            plotrange = axes.get_xlim()
+        axes.plot(Ts, Js, linestyle="none", marker=".", label="mean rates")
+        pxs = np.linspace(plotrange[0], plotrange[1], 200)
+        axes.plot(pxs, self.J(pxs), label="fit")
+        axes.set_xlabel("$T \\ [K]$")
+        axes.set_ylabel("$J \\ [1/m^3/s]$")
+        return axes
+
+class MobilityFit:
+    """ Fitting an Arrhenius behaviour to the mobility M = v / dGv """
+    
+    def __init__(self, series, dGv):
+        """
+        Build an object of a given series of measurements and a given model for
+        the free energy dGv as a function fo temperature.
+        """
+        self.Ts = series.Ts
+        self.vs = series.vs
+        self.uvs = series.uvs
+        self.Ms = self.vs / dGv(self.Ts)
+        self.uMs = self.uvs / dGv(self.Ts)
+    
+    def linfit(self):
+        """ Fit the linear problem. """
+        m, um, c, uc, _, _ = linreg(1/self.Ts, np.log(self.Ms), self.uMs)
+        self.Minf, self.uMinf = np.exp(c), np.exp(c)*uc
+        self.Ea, self.uEa = -m, um
+        return self
+    
+    def fit(self):
+        """ Perform a fit. """
+        self.linfit()
+        fitfunc = lambda x,a,b: a*np.exp(-b/x)
+        Ts, Ms, uMs = self.Ts, self.Ms, self.uMs
+        p0 = [self.Minf, self.Ea]
+        popt, pcov = scipy.optimize.curve_fit(fitfunc, Ts, Ms, p0=p0,
+                                              sigma=uMs, absolute_sigma=True)
+        self.Minf, self.Ea = popt[0], popt[1]
+        self.uMinf, self.uEa = np.sqrt(pcov[0,0]), np.sqrt(pcov[1,1])
+        return self
+    
+    def M(self,T):
+        """
+        Mobility as temperature dependent function according to this fit results.
+        """
+        return self.Minf * np.exp(-self.Ea/T)
+    
+    def info(self):
+        """ Print some information. """
+        print("Fitting M = Minf*exp(-Ea/T)")
+        print("Minf = {} +- {}".format(self.Minf, self.uMinf))
+        print("Ea   = {} +- {}".format(self.Ea, self.uEa))
+        Ea, uEa = [x/eV*kB for x in [self.Ea, self.uEa]]
+        print("   = (",Ea,"+-",uEa,")eV/kB")
+        print("chisq/ndf:", chisq_ndf(self.Ts, self.Ms, self.uMs,
+                                      (lambda x,a,b: a*np.exp(-b/x)),
+                                      [self.Minf, self.Ea]))
+        return self
+    
+    def plot_lin(self, axes=None):
+        """ Plot the linearised problem. """
+        if axes is None:
+            fig, axes = plt.subplots(1,1)
+        invTs = 1/self.Ts
+        m, c = -self.Ea, np.log(self.Minf)
+        axes.errorbar(invTs, np.log(self.Ms), self.uMs/self.Ms,
+                      linestyle="none", marker="_", label="data")
+        axes.plot(invTs, m*invTs+c, label="fit")
+        axes.set_xlabel("$1/(T \\ [K])$")
+        axes.set_ylabel("$\\log(M \\ \\left[\\frac{m^3}{J s}\\right])$")
+        return axes
+    
+    def plot(self, axes=None):
+        """ Show a plot. """
+        if axes is None:
+            fig, axes = plt.subplots(1,1)
+        axes.errorbar(self.Ts, self.Ms, self.uMs, linestyle="none", marker="_",
+                     label="data")
+        pxs = np.linspace(np.min(self.Ts)-1,np.max(self.Ts)+1,100)
+        axes.plot(pxs,self.M(pxs), label="fit")
+        axes.set_xlabel("$T \\ [K]$")
+        axes.set_ylabel("$M \\ \\left[\\frac{m^3}{J s}\\right]$")
+        return axes        
+
+__all__ = ["SeriesOfMeasurements", "ArrheniusFit", "SimpleCntFit",
+           "MobilityFit"]
diff --git a/py/measurementclasses.py b/py/measurementclasses.py
deleted file mode 100644
index 89513e90a4ba2915f2814fe19e48b5ff055b0260..0000000000000000000000000000000000000000
--- a/py/measurementclasses.py
+++ /dev/null
@@ -1,351 +0,0 @@
-"""
-This file provides classes to analyse experimental or simulated data on the
-crystallisation of a phase change material.
-"""
-
-import csv
-import numpy as np
-import matplotlib.pyplot as plt
-import scipy.optimize
-from scripts import linreg, weightedmean, chisq_ndf
-
-
-class Measurement:
-    """
-    A class for a measurement for one temperature as I got from Julian or as
-    returned from one simulation run.
-    Properties:
-      times - times of measurements [s]
-      nuclei - number of seen nuclei
-      radii - 2D array of radii [m]
-      afract - amorphous fraction
-      cfract - crystallized fraction
-    Functions:
-      plot_fc
-      plot_N
-      plot_r
-      JMAK_linfit
-      JMAK_fit
-      rfit
-      all_rfits
-      nucleationratefit
-    """
-
-    def plot_fc(self):
-        """ Plot the crystallised fraction over time. """
-        plt.plot(self.times, self.cfract, linestyle="none", marker="x")
-        plt.xlabel("$t \\ [s]$")
-        plt.ylabel("$f_c$")
-        plt.tight_layout()
-        
-    def plot_N(self):
-        """ Plot the counted nuclei over time. """
-        plt.plot(self.times, self.nuclei, linestyle="none", marker="x")
-        plt.xlabel("$t \\ [s]$")
-        plt.ylabel("$N$")
-        plt.tight_layout()
-    
-    def plot_r(self, i):
-        """ Plot the size of the i-th nucleus over time. """
-        plt.plot(self.times, self.radii[i]*1e6, linestyle="none", marker="x")
-        plt.xlabel("$t \\ [s]$")
-        plt.ylabel("$r \\ [\\mu m]$")
-        plt.tight_layout()
-    
-    def JMAK_linfit(self, show=True):
-        """
-        Linearise the fit to a JMAK model and use linear regression.
-        Uncertainties are given so that chisquared per degree of freedom is 1.
-        Returns (n, k) for f_a = exp(-k*t^n)
-        """
-        
-        # Extract points where log() is well defined
-        cond = np.logical_and( np.logical_and( self.afract > 0,
-                                               self.afract < 1),
-                               self.times > 0)
-        xs = np.log( np.compress(cond, self.times))
-        fs = np.compress(cond, self.afract)
-        ys = np.log( - np.log( fs ))
-        # Assume equal uncertainty on area values, transform to y
-        sigmas = np.abs(np.ones(xs.shape)/np.log(fs)/fs)
-        n, un, c, uc, corr, chisq = linreg(xs, ys, sigmas)
-        chisqndf = chisq / (len(xs)-2)
-        # correct uncertainties, calculate k
-        un, uc = un*np.sqrt(chisqndf), uc*np.sqrt(chisqndf)
-        k, uk = np.exp(c), np.exp(c)*uc
-        if show:
-            print("f_a = exp(-k*t^n), linearise:")
-            print("ln(-ln(f_a)) = n * ln(t) + ln(k)")
-            print("n:", n, "+-", un)
-            print("k:", k, "+-", uk)
-            plt.plot(xs, ys, linestyle="none", marker="x", label="data")
-            plt.plot(xs, n*xs+c, label="fit")
-            plt.xlabel('$\ln(t \\ [s])$')
-            plt.ylabel('$\ln( - \ln( 1 - f_c ))$')
-            plt.legend()
-            plt.tight_layout()
-        return n, k
-        
-    
-    def JMAK_fit(self, dimensions=None, show=True):
-        """
-        Fit to a JMAK model.
-        Either with a fixed number of dimensions or using this as another free
-        parameter.
-        Uncertainties are given so that chisquared per degree of freedom is 1.
-        Returns ([n,k],pcov) where pcov is the covariance matix.
-        """
-        
-        # Initial values from linear problem, function to fit onto
-        n0, k0 = self.JMAK_linfit(show=False)
-        fitfunc = lambda t,n,k: np.exp(-k*t**n)
-        if dimensions is None:
-            popt, pcov = scipy.optimize.curve_fit(fitfunc, self.times,
-                                self.afract, p0=[n0,k0])
-        else:
-            # If dimension is given, calculate exponent and put into fitfunction
-            n = dimensions + 1
-            fitfunc2 = lambda t,k: fitfunc(t,n,k)
-            popt, pcov = scipy.optimize.curve_fit(fitfunc2, self.times, 
-                                                  self.afract, p0=[k0])
-            # Fill popt, pcov to the same size as from above
-            popt = np.insert(popt, 0, n)
-            pcov = np.array([[0, 0], [0, pcov[0,0]]])
-        # Get chisquared and correct uncertainties
-        chisq = chisq_ndf(self.times, self.afract, np.ones(self.afract.shape),
-                          fitfunc, popt)
-        pcov = pcov*chisq
-        if show:
-            print("f_a = exp(-k*t^n)")
-            print("n:", popt[0], "+-", np.sqrt(pcov[0,0]))
-            print("k:", popt[1], "+-", np.sqrt(pcov[1,1]))
-            print("covariance: ", pcov[0,1])
-            plt.plot(self.times, self.afract, linestyle="none", marker="x",
-                     label="data")
-            plottimes = np.linspace(np.min(self.times), np.max(self.times), 100)
-            plt.plot(plottimes, fitfunc(plottimes, *popt), label="fit")
-            plt.xlabel("$t \\ [s]$")
-            plt.ylabel("$1-f_c$")
-            plt.legend()
-            plt.tight_layout()
-        return popt, pcov
-    
-    def rfit(self, i, show=True):
-        """
-        Fit a linear growth to the i-th grain.
-        Uncertainties are given so that chisquared per degree of freedom is 1.
-        Returns (v, sigma_v).
-        """
-        
-        cond = np.logical_not( np.isnan(self.radii[i]) )
-        ts = np.compress(cond, self.times)
-        rs = np.compress(cond, self.radii[i])
-        v, uv, c, uc, corr, chiq = linreg(ts, rs, np.ones(rs.shape))
-        # Scale to make chisquared/ndf equals 1
-        chiq = chiq/(len(ts)-2)
-        uv, uc = [x * np.sqrt(chiq) for x in [uv, uc]]
-        if show:
-            print("v:", v, "+-", uv)
-            plt.plot(ts, rs*1e6, linestyle="none", marker="x", label="data")
-            plt.plot(ts, (v*ts+c)*1e6, label="fit", color="black", alpha=0.7)
-            plt.xlabel("$t \\ [s]$")
-            plt.ylabel("$r \\ [\\mu m]$")
-            if show != 2:
-                plt.legend()
-            plt.tight_layout()
-            plt.show()
-        return v, uv
-    
-    def all_rfits(self, show=True):
-        """ Fit all grains and calculate mean growht velocity """
-        
-        # Initialise results array and do first fit
-        vs = np.zeros(len(self.radii))
-        vs[0] = self.rfit(0, show=show)[0]
-        # For all other fits, do not show a legend anymore
-        for i in range(1,len(self.radii)):
-            vs[i] = self.rfit(i, show=show*2)[0]
-        v, uv = np.mean(vs), np.std(vs)
-        return v, uv
-    
-    def nucleationratefit(self, splitat=None, show=True):
-        """
-        Fit a nucleation rate to the number of grains. Either to the whole
-        domain, or give points to split at. Will return a list of 2-tuples
-        each containing a rate and an uncertainty.
-        """
-        
-        # Calculate the new nuclei per time step
-        dN = np.diff(self.nuclei)
-        dt = np.diff(self.times)
-        meantimes = (self.times[1:]+self.times[:-1])/2
-        # Scale with fraction of material where nucleation can occour
-        fa = (self.afract[1:] + self.afract[:-1]) / 2
-        Js = dN/dt * 1/fa
-        # Integrate again
-        N2 = np.cumsum(Js * dt)
-        N2 = np.insert(N2,0,0)
-        # Assume poisson uncertainty
-        uN2 = np.maximum(np.sqrt(N2), 1)
-        
-        # If no points to split at are given, create an empty list
-        if splitat is None:
-            splitat = []
-        # Calculate domain boundarys: Insert first time at front, cut out every
-        # time thats out of available times and insert last time at end
-        splitat = np.insert(splitat, 0, self.times[0])
-        splitat = np.compress(splitat<self.times[-1], splitat)
-        splitat = np.append(splitat,self.times[-1])
-        
-        # Show the data
-        if show:
-            plt.errorbar(self.times, N2, uN2, linestyle=":", label="data")
-            plt.xlabel("$t \\ [s]$")
-            plt.ylabel("$N_{corrected}$")
-            plt.tight_layout()
-        
-        # For each of the domains
-        res = []
-        y0, y1 = 0, 0
-        for tmin, tmax in zip(splitat[0:-1],splitat[1:]):
-            # extract data in this domain
-            cond = np.logical_and(meantimes >= tmin,
-                                  meantimes <= tmax)
-            Js_i = np.compress(cond, Js)
-            dN_i = np.compress(cond, dN)
-            dt_i = np.compress(cond, dt)
-            fa_i = np.compress(cond,fa)
-            
-            # Assume poisson uncertainty with the mean rate on all points
-            uJ_i = np.sqrt(np.maximum(np.mean(dN_i),1))/dt_i * 1/fa_i
-            
-            # Mean rate
-            J, uJ = weightedmean(Js_i, uJ_i)
-            
-            if show:
-                print("from",tmin,"to",tmax,":")
-                print("   rate:",J,"+-",uJ)
-                y0, y1 = y1, y1+J*(tmax-tmin)
-                plt.plot([tmin,tmax], [y0, y1], label="integrated mean rate")
-                
-            res.append((J,uJ))
-            
-        return res
-
-
-# Calculate a length from the values in pixels, according to the mail I got.
-# First method is the correction I got in the second mail
-correctedpx = lambda px: 300/72*px
-px2len = lambda p: (0.3876 * correctedpx(p) - 0.3802) * 1e-6
-
-class Experiment(Measurement):
-    """
-    Class for experimental data as I got from Julian.
-    Additional properties:
-      columntitles - column titles from the csv
-      aarea - amorphous area [m^2]
-      A - overall observed area [m^2]
-      h - thickness of the material [m]
-    Additional methods:
-      compare_results
-    """
-    
-    def __init__(self, filename, height):
-        """ Read all data from the file and build variables of interest. """
-         
-        # Open file and build csv reader
-        file = open(filename, newline="")
-        reader = csv.reader(file, delimiter=",")
-        # First line has titles to the columns
-        self.columntitles = list(map(lambda x: x.strip(), next(reader)))
-        # Rest is data structured in columns
-        columns = [ [] for _ in self.columntitles]
-        for row in reader:
-            for column, value in zip(columns, row):
-                # Replace empty cells with nan to parse them to float
-                if value == "":
-                    value = "nan"
-                column.append(value.replace(",","."))
-        # First column are times in minutes, convert to seconds
-        self.times = np.array(columns[0], dtype=np.float) * 60
-        # Second column is amporphous area. The square root is a length and can
-        # be converted according to the mail, then square the result again
-        self.aarea = (px2len(np.sqrt(np.array(columns[1], dtype=np.float))))**2
-        # Third column has number of grains, here no value means probably zero
-        self.nuclei = np.array(columns[2], dtype=np.float)
-        self.nuclei[np.isnan(self.nuclei)] = 0
-        # Rest of the file are radii of grains
-        self.radii = px2len( np.array(columns[3:], dtype=np.float) )
-        # Overall observed area
-        self.A = max(self.aarea)
-        self.h = height
-        # Fractions of amorphous and crystaline area
-        self.afract = self.aarea/self.A
-        self.cfract = 1-self.afract
-    
-    def compare_results(self, dimensions):
-        """
-        Compare the results from the JMAK fit with the ones obtained from
-        fitting on grain count and sizes.
-        """
-        v, uv = self.all_rfits(show=False)
-        [(dNdt, udNdt)] = self.nucleationratefit(show=False)
-        J  =  dNdt / (self.A*self.h)
-        uJ = udNdt / (self.A*self.h)
-        [n,k1], pcov = self.JMAK_fit(dimensions,show=False)
-        
-        if dimensions == 1:
-            func = lambda t: np.exp(-J*self.h**2*v*t**2)
-            k2 = J*self.h**2*v
-            uk2 = k2 * np.sqrt(uJ**2/J**2 + uv**2/v**2)
-        elif dimensions == 2:
-            func = lambda t: np.exp(-np.pi/3*J*self.h*v**2*t**3)
-            k2 = np.pi/3*J*self.h*v**2
-            uk2 = k2 * np.sqrt(uJ**2/J**2 + 4*uv**2/v**2)
-        elif dimensions == 3:
-            func = lambda t: np.exp(-np.pi/3*J*v**3*t**4)
-            k2 = np.pi/3*J*v**3
-            uk2 = k2 * np.sqrt(uJ**2/J**2 + 9*uv**2/v**2)
-        
-        print("JMAK fit:   k = {} +- {}".format(k1,np.sqrt(pcov[0,0])))
-        print("v & J fits: k = {} +- {}".format(k2,uk2))
-        
-        plottimes = np.linspace(np.min(self.times), np.max(self.times), 100)
-        plt.plot(self.times, self.afract, linestyle="none", marker="x", 
-                 label="data")
-        plt.plot(plottimes, np.exp(-k1*plottimes**n),
-                 label="fit on $f_c$")
-        plt.plot(plottimes, func(plottimes),
-                 label="fits on $\\frac{dN}{dt}$ and $v$")
-        plt.legend()
-
-
-class Simulation(Measurement):
-    """
-    Class for simulation results as received from my code.
-    Additional properties:
-        columntitles - column titles from the csv
-    """
-    
-    def __init__(self, filename):
-        """ Read all data from the file and build variables of interest. """
-        
-        # Open file and build csv reader
-        file = open(filename, newline="")
-        reader = csv.reader(file, delimiter=",")
-        # First line has titles to the columns
-        self.columntitles = list(map(lambda x: x.strip(), next(reader)))
-        # Rest is data structured in columns
-        lines = []
-        for row in reader:
-            lines.append(row)
-        data = np.array(lines, dtype=np.float)
-        # First column are times
-        self.times = data[:,0]
-        # Second column has number of grains
-        self.nuclei = data[:,1]
-        # Third column is crystalline fraction
-        self.cfract = data[:,2]
-        self.afract = 1 - self.cfract
-        # Rest of the file are sizes of grains
-        self.radii = np.sqrt(data[:,3:]/np.pi).transpose()