diff --git a/1d/main.py b/1d/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..5644f258ee108d0fe9dde79c0dc43672c5012413
--- /dev/null
+++ b/1d/main.py
@@ -0,0 +1,196 @@
+import myfun as my
+import numpy as np
+from matplotlib import pyplot as plt
+
+# CHOOSE METHOD
+method = 'wasserstein' #'wasserstein' or 'upwinding'
+
+T = 30*10**(-3) # time interval [0,T]
+
+Nx = 103 # choose fineness of spatial discretization
+Nt = 200 # choose fineness of temporal discretization
+hx = 1/(Nx+2) # assumption equidistant mesh, spatial mesh size
+ht = (T)/(Nt+1) # assumption equidistant steps, temporal mesh size
+
+print('hx',hx)
+print('ht',ht)
+
+faces = np.linspace(0,1,Nx+3) # Cell interfaces x_(i+1/2) for i=0,1,...,N and boundary faces x_-1/2 = 0, x_N+1/2 = 1.
+# +3 as we have N+1 interfaces and 2 boundary faces
+faces = np.array(faces) # [-1, 0,1,2,...,N ,N+1] -> length = Nx+3
+
+# compute array of midpoints
+midpoints = []
+for i in range(Nx+2) :
+    midpoints.append((faces[i]+faces[i+1])/2) # cell midpoints
+midpoints = np.array(midpoints) # -> length = Nx+2
+
+# pre-allocate 
+rho = np.zeros([Nt,Nx+2])
+cc = np.zeros([Nt,Nx+2])
+
+# CHOOSE INITIAL CONDITION
+rho_0 = lambda x: 1.3*np.sin(np.pi*x)*np.exp(-25*(x-1/2)**2) # pre-factors scales measure to sum up to 1 over midpoints
+# rho_0 = lambda x: (np.sin(np.pi*x)*np.exp(-50*(x-1/4)**2) + np.sin(np.pi*x)*np.exp(-50*(x-3/4)**2)) # double gaussian
+
+# discrete initial condition via evalutation at midpoints, i.e. projection to discrete space
+rho_h0 = rho_0(midpoints) # initial value t = 0, N+3 faces result in N+2 elements, need to sum to 1 (represents measure), may not start with uniform distribution
+
+# save initial discrete rho0
+rho[0][:] = rho_h0 # t=0
+
+# initialize object
+discr_E = []
+
+
+if method == 'upwinding' :
+     
+    for n in range(Nt-1) : # go through time intervals
+    
+        # GET C FROM RHO
+        c = my.getc(rho[n,:],midpoints,faces,hx) # paramaters to get function c_h^n(x) via c_h^n(x) = sum_i (c_i*hat_i(x))
+        cc[n,:] = c
+
+        # get discrete energy
+        discr_E.append(my.discrete_energy(c,rho[n,:],hx))
+        rhs = []
+
+        # APPLY NUMERICAL SCHEME
+        for i in range(Nx+2) : # assumption gamma = 1, len(rho) = Nx+2, going over midpoints
+            #print(i)
+            if i == Nx+2-1 : # periodic boundary data
+                dxch_irechts = c[i]*my.dxhat(i,midpoints,hx,faces[0])+c[0]*my.dxhat(0,midpoints,hx,faces[0])
+                dxch_ilinks = c[i-1]*my.dxhat(i-1,midpoints,hx,faces[i])+c[i]*my.dxhat(i,midpoints,hx,faces[i]) #x_{i-1/2}
+                
+                if dxch_irechts >= 0 :
+                    if dxch_ilinks >= 0 :
+                        rhs.append(rho[n][i] - ht/hx*(dxch_irechts*rho[n][i]-dxch_ilinks*rho[n][i-1]))
+                    else : 
+                        rhs.append(rho[n][i] - ht/hx*(dxch_irechts*rho[n][i]-dxch_ilinks*rho[n][i]))
+                else : 
+                    if dxch_ilinks >= 0:
+                        rhs.append(rho[n][i] - ht/hx*(dxch_irechts*rho[n][0]-dxch_ilinks*rho[n][i-1]))
+                    else : 
+                        rhs.append(rho[n][i] - ht/hx*(dxch_irechts*rho[n][0]-dxch_ilinks*rho[n][i]))
+                
+            else : # interior faces
+                dxch_irechts = c[i]*my.dxhat(i,midpoints,hx,faces[i+1])+c[i+1]*my.dxhat(i+1,midpoints,hx,faces[i+1])
+                dxch_ilinks = c[i-1]*my.dxhat(i-1,midpoints,hx,faces[i])+c[i]*my.dxhat(i,midpoints,hx,faces[i])
+
+                if dxch_irechts >= 0 :
+                    if dxch_ilinks >= 0:
+                        rhs.append(rho[n][i] - ht/hx*(dxch_irechts*rho[n][i]-dxch_ilinks*rho[n][i-1]))
+                    else : 
+                        rhs.append(rho[n][i] - ht/hx*(dxch_irechts*rho[n][i]-dxch_ilinks*rho[n][i]))
+                else : 
+                    if dxch_ilinks >= 0:
+                        rhs.append(rho[n][i] - ht/hx*(dxch_irechts*rho[n][i+1]-dxch_ilinks*rho[n][i-1]))
+                    else : 
+                        rhs.append(rho[n][i] - ht/hx*(dxch_irechts*rho[n][i+1]-dxch_ilinks*rho[n][i]))
+
+        rho[n+1][:] = my.update_in_time(rhs,Nx,ht,hx)
+
+    discr_E = np.array(discr_E)
+
+
+elif method == 'wasserstein' :
+
+    for n in range(Nt-1) :  # go through time intervals
+
+        # GET C FROM RHO
+        c = my.getc(rho[n,:],midpoints,faces,hx) # paramaters to get function c_h^n(x) via c_h^n(x) = sum_i (c_i*hat_i(x))
+        cc[n,:] = c
+
+        # get discrete energy
+        discr_E.append(my.discrete_energy(c,rho[n,:],hx))
+        rhs = []
+
+        # APPLY NUMERICAL SCHEME
+        for i in range(Nx+2) : # assumption gamma = 1, len(rho) = Nx+2
+            if i == Nx+2-1 : # periodic boundary data
+                dxch_iplus = c[i]*my.dxhat(i,midpoints,hx,faces[i+1])+c[0]*my.dxhat(0,midpoints,hx,faces[i+1])
+                dxch_iminus = c[i-1]*my.dxhat(i-1,midpoints,hx,faces[i])+c[i]*my.dxhat(i,midpoints,hx,faces[i])
+                if np.abs(rho[n][i]-rho[n][i-1]) < 10**-6 or rho[n][i] < 10**-6 or rho[n][i-1] < 10**-6 :
+                    if np.abs(rho[n][0]-rho[n][i]) < 10**-6 or rho[n][i] < 10**-6 or rho[n][0] < 10**-6 :
+                        rhs.append(rho[n][i] - ht/hx*(dxch_iplus*1/2*(rho[n][0]+rho[n][i])-dxch_iminus*1/2*(rho[n][i]+rho[n][i-1])))
+                    else :
+                        rhs.append(rho[n][i] - ht/hx*(dxch_iplus*(rho[n][0]-rho[n][i])/(np.log(rho[n][0])-np.log(rho[n][i]))-dxch_iminus*1/2*(rho[n][i]+rho[n][i-1])))
+                else :
+                    if np.abs(rho[n][0]-rho[n][i]) < 10**-6 or rho[n][i] < 10**-6 or rho[n][0] < 10**-6 :
+                        rhs.append(rho[n][i] - ht/hx*1/2*(dxch_iplus*(rho[n][0]+rho[n][i])-dxch_iminus*(rho[n][i]-rho[n][i-1])/(np.log(rho[n][i])-np.log(rho[n][i-1]))))
+                    else :
+                        rhs.append(rho[n][i] - ht/hx*(dxch_iplus*(rho[n][0]-rho[n][i])/(np.log(rho[n][0])-np.log(rho[n][i]))-dxch_iminus*(rho[n][i]-rho[n][i-1])/(np.log(rho[n][i])-np.log(rho[n][i-1]))))
+                
+            else : # interior faces
+                dxch_iplus = c[i]*my.dxhat(i,midpoints,hx,faces[i+1])+c[i+1]*my.dxhat(i+1,midpoints,hx,faces[i+1])
+                dxch_iminus = c[i-1]*my.dxhat(i-1,midpoints,hx,faces[i])+c[i]*my.dxhat(i,midpoints,hx,faces[i])
+                if np.abs(rho[n][i]-rho[n][i-1]) < 10**-6 or rho[n][i] < 10**-6 or rho[n][i-1] < 10**-6 :
+                    if np.abs(rho[n][i+1]-rho[n][i]) < 10**-6 or rho[n][i] < 10**-6 or rho[n][i+1] < 10**-6 :
+                        rhs.append(rho[n][i] - ht/hx*(dxch_iplus*1/2*(rho[n][i+1]+rho[n][i])-dxch_iminus*1/2*(rho[n][i]+rho[n][i-1])))
+                    else :
+                        rhs.append(rho[n][i] - ht/hx*(dxch_iplus*(rho[n][i+1]-rho[n][i])/(np.log(rho[n][i+1])-np.log(rho[n][i]))-dxch_iminus*1/2*(rho[n][i]+rho[n][i-1])))
+                else :
+                    if np.abs(rho[n][i+1]-rho[n][i]) < 10**-6 or rho[n][i] < 10**-6 or rho[n][i+1] < 10**-6 :
+                        rhs.append(rho[n][i] - ht/hx*1/2*(dxch_iplus*(rho[n][i+1]+rho[n][i])-dxch_iminus*(rho[n][i]-rho[n][i-1])/(np.log(rho[n][i])-np.log(rho[n][i-1]))))
+                    else :
+                        rhs.append(rho[n][i] - ht/hx*(dxch_iplus*(rho[n][i+1]-rho[n][i])/(np.log(rho[n][i+1])-np.log(rho[n][i]))-dxch_iminus*(rho[n][i]-rho[n][i-1])/(np.log(rho[n][i])-np.log(rho[n][i-1]))))
+                
+        rho[n+1][:] = my.update_in_time(rhs,Nx,ht,hx)
+
+        # PLOT FUNCTION PROFILES AS SNAPSHOTS AT TIME STEP n
+        # does not work for all Nx
+        if n == 42:
+            c_h = lambda x : my.cfunc(cc[n][:],midpoints,hx,x)
+            lin_rho = lambda x : my.linear_interpol(rho[n][:],midpoints,hx,x)
+            const_rho = lambda x: my.const_rho(rho[n][:],midpoints,hx,x)
+
+            #compute function values
+            xi = np.linspace(0,1,300) 
+            val1 = []   
+            val2 = []
+            val3 = []        
+            for x in xi:
+                val1.append(const_rho(x))
+                val2.append(lin_rho(x))
+                val3.append(c_h(x))
+            
+            plt.plot(xi,val1,'steelblue',xi,val2,'deepskyblue')
+            plt.plot(midpoints,np.zeros(len(midpoints)),'r.')
+            plt.xlabel('x')
+            plt.ylabel('bacterial density')
+            plt.legend(['finite volume sol.','lin. interpolation','midpoints x_i'])
+            time = n*ht
+            title = 'bacterial density at time '+'%f' % time
+            plt.title(title)
+            plt.show()
+
+            plt.plot(xi,val3,'green')
+            plt.plot(midpoints,np.zeros(len(midpoints)),'r.')
+            plt.xlabel('x')
+            plt.ylabel('chemical concentration')
+            title = 'chemical concentration at time '+'%f' % time
+            plt.title(title)
+            plt.legend(['finite element sol.','midpoints x_i'])
+            plt.show()
+
+
+    # save discrete energy for this time step
+    discr_E = np.array(discr_E)
+
+else : 
+    print('Error: Wrong string input')
+
+
+# HEATMAP PLOT OF EVOLUTION
+plt.pcolormesh(rho,cmap = 'jet')
+plt.xlabel('space')
+plt.ylabel('time')
+plt.colorbar()
+plt.show()
+
+# PLOT DISCRETE ENERGY
+plt.plot(np.linspace(0,T,Nt-1),discr_E)
+plt.xlabel('time')
+plt.ylabel('discrete Energy')
+plt.title('Discrete energy dissipation')
+plt.show()
diff --git a/1d/myfun.py b/1d/myfun.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed371dfcedb9f53fae09a09da7db2fe92082d2e7
--- /dev/null
+++ b/1d/myfun.py
@@ -0,0 +1,315 @@
+import numpy as np
+from scipy.sparse import csr_matrix 
+from scipy.sparse.linalg import spsolve
+
+
+# FINITE ELEMENT 
+
+def linear_interpol(rho,midpoints,h,x) :
+    # Input : current approximation rho^n, midpoints, mesh size h, point x
+    # output : value of linear interpolation at x
+
+    # find element in which x lies use midpoint[i] = h(i+1/2) 
+   
+    i0 = int(np.floor((x/h-1/2)))
+    i1 = int(np.ceil((x/h-1/2)))
+
+    if i0 == -1 : # periodic boundary condition
+        y1 = rho[i1]
+        x1 = midpoints[i1]
+        y0 = 1/2*(rho[0]+rho[-1])
+        x0 = 0
+
+        #get linear interpolation
+        m = (y1-y0)/(x1-x0) # /h
+        b = y1-m*x1
+    
+        y = m*x+b
+
+    elif i1 == len(midpoints) : # periodic boundary condition
+        y1 = 1/2*(rho[0]+rho[-1])
+        x1 = 1
+        y0 = rho[i0]
+        x0 = midpoints[i0]
+
+        #get linear interpolation
+        m = (y1-y0)/(x1-x0) # /h
+        b = y1-m*x1
+    
+        y = m*x+b
+    
+    else :
+
+        if i0 == i1 : # falls rundungsfehler
+            i0 -= 1
+
+        y1 = rho[i1]
+        x1 = midpoints[i1]
+        y0 = rho[i0]
+        x0 = midpoints[i0]
+
+        #get linear interpolation
+        m = (y1-y0)/(x1-x0) # /h
+        b = y1-m*x1
+        
+        y = m*x+b
+
+    return y
+
+
+def hat(i,midpoints,h,x) :
+
+    values = np.zeros(len(midpoints))
+    values[i] = 1
+
+    y = linear_interpol(values,midpoints,h,x)
+
+    return y
+
+
+def dxhat(i,midpoints,h,x) :
+
+    values = np.zeros(len(midpoints))
+    values[i] = 1
+
+    # find element in which x lies use midpoint[i] = h(i+1/2) 
+    i0 = int(np.floor((x/h-1/2)))
+    i1 = int(np.ceil((x/h-1/2)))
+
+    if i0 == -1: # periodic boundary condition
+        y1 = values[i1]
+        x1 = midpoints[i1]
+        y0 = 1/2*(values[0]+values[-1])
+        x0 = 0
+
+        #get linear interpolation
+        m = (y1-y0)/(x1-x0) # /h
+
+
+    elif i1 == len(midpoints) : # periodic boundary condition
+        y1 = 1/2*(values[0]+values[-1])
+        x1 = 1
+        y0 = values[i0]
+        x0 = midpoints[i0]
+
+        #get linear interpolation
+        m = (y1-y0)/(x1-x0) # /h
+
+    else :
+
+        y1 = values[i1]
+        x1 = midpoints[i1]
+        y0 = values[i0]
+        x0 = midpoints[i0]
+
+        #get linear interpolation
+        m = (y1-y0)/(x1-x0) # /h
+
+
+    return m
+
+
+def quadrature(I,g) :
+    # inupt: interval I = [a,b]
+
+    areaI = I[1]-I[0]
+
+    trafo = lambda y : y*(I[1]-I[0])/2+(I[1]+I[0])/2 # y in [-1,1]
+
+    xi = np.array([-np.sqrt(1/3),np.sqrt(1/3)]) #on [-1,1]
+    #xi = [-1,1]
+    wi = [1,1]
+
+    # xi = [0]
+    # wi = [2]
+
+    # xi = np.array([-np.sqrt(3/5),0,np.sqrt(3/5)]) #on [-1,1]
+    # wi = [5/9,8/9,5/9]
+
+    val = 0
+    for k in range(len(wi)) :
+        val += wi[k]*g(trafo(xi[k])) # get integral 
+
+    avrg = val/2 # get average
+    integral = avrg*areaI
+
+    return integral
+
+def midpoint_rule(I,g) :
+    # inupt: interval I = [a,b]
+
+    areaI = I[1]-I[0]
+
+    trafo = lambda y : y*(I[1]-I[0])/2+(I[1]+I[0])/2 # y in [-1,1]
+
+    xi = [0]
+    wi = [2]
+
+    val = 0
+    for k in range(len(wi)) :
+        val += wi[k]*g(trafo(xi[k])) # get integral 
+
+    avrg = val/2 # get average
+    integral = avrg*areaI
+
+    return integral
+
+def trapezoidal_rule(I,g) :
+    # inupt: interval I = [a,b]
+
+    areaI = I[1]-I[0]
+
+    trafo = lambda y : y*(I[1]-I[0])/2+(I[1]+I[0])/2 # y in [-1,1]
+
+    xi = [-1,1]
+    wi = [1,1]
+
+    val = 0
+    for k in range(len(wi)) :
+        val += wi[k]*g(trafo(xi[k])) # get integral 
+
+    avrg = val/2 # get average
+    integral = avrg*areaI
+
+    return integral
+
+
+def getc(rho,midpoints,faces,h) :
+
+
+    lin_rho = lambda x : linear_interpol(rho,midpoints,h,x)
+
+    row = []
+    col = []
+    data1 = []
+    data2 = []
+    b = []
+
+    for i in range(len(midpoints)) :
+        
+        f = lambda x: hat(i,midpoints,h,x)*lin_rho(x)
+
+        # if i == (len(midpoints)-1):
+        #    rhs = quadrature([midpoints[i-1],1],f) + quadrature([0,midpoints[1]],f) # periodic boundary
+        # elif i == 0 :
+        #     rhs = quadrature([0,midpoints[i+1]],f) + quadrature([midpoints[-1],1],f) # periodic boundary
+        # else : 
+        #     rhs = quadrature([midpoints[i-1],midpoints[i+1]],f)
+
+        rhs = midpoint_rule([faces[i],faces[i+1]],f)
+
+        for j in range(len(midpoints)) :
+            if abs(i-j) < 2 :
+                g = lambda x: hat(i,midpoints,h,x)*hat(j,midpoints,h,x)
+                dg = lambda x: dxhat(i,midpoints,h,x)*dxhat(j,midpoints,h,x)
+                
+                if i == (len(midpoints)-1) :
+                    val2 = trapezoidal_rule([midpoints[i],1],g) + trapezoidal_rule([0,midpoints[0]],g)
+                    # val2 = midpoint_rule([midpoints[i],1],g) + midpoint_rule([0,midpoints[0]],g)
+           
+                else :             
+
+                    val2 = trapezoidal_rule([midpoints[i],midpoints[i+1]],g) # for A2
+                    # val2 = midpoint_rule([midpoints[i],midpoints[i+1]],g) # for A1
+
+                val1 = trapezoidal_rule([faces[i],faces[i+1]],dg)
+                # val2 = trapezoidal_rule([midpoints[i],midpoints[i+1]],g)
+
+                row.append(i)
+                col.append(j)
+                data1.append(val1)
+                data2.append(val2)
+        b.append(rhs)
+
+
+            
+    sparseA1 = csr_matrix((data1, (row, col)), shape = (len(midpoints), len(midpoints)))#.toarray() 
+    sparseA2 = csr_matrix((data2, (row, col)), shape = (len(midpoints), len(midpoints)))#.toarray()
+
+    sparseA = sparseA1 + sparseA2
+
+    # plt.spy(sparseA2.toarray())
+    # plt.show()
+
+    c = spsolve(sparseA, b, permc_spec=None, use_umfpack=True) # solve system of equations
+    # c_h^n(x) = sum_i (c_i*hat_i(x))
+    return c
+
+
+def cfunc(cc,midpoints,h,x) :
+    
+    i0 = int(np.floor((x/h-1/2)))
+    i1 = int(np.ceil((x/h-1/2)))
+
+    if i1 == len(midpoints) :
+        i1 = 0
+
+    # print(ii)
+    val = 0
+    for i in [i0,i1] :
+        val += cc[i]*hat(i,midpoints,h,x)
+
+    return val
+
+
+def const_rho(rho,midpoints,h,x) :
+
+    i = int(np.round((x/h-1/2)))
+    val = rho[i]
+
+    return val
+
+
+def update_in_time(rhs,Nx,ht,hx) :
+
+    row = []
+    col = []
+    data = []
+
+    row.append(0) # periodic boundary condition
+    col.append(Nx+1)
+    data.append(-ht/(hx**2))
+
+    for i in range(Nx+2) :
+        for j in range(Nx+2) :
+            if abs(i-j)<2:
+                if i == j :
+                    val = 1+(2*ht)/(hx**2)
+                else :
+                    val = -ht/(hx**2)
+
+                row.append(i)
+                col.append(j)
+                data.append(val)
+
+    row.append(Nx+1) # periodic boundary condition
+    col.append(0)
+    data.append(-ht/(hx**2))
+
+    sparseAt = csr_matrix((data, (row, col)), shape = (Nx+2, Nx+2))#.toarray()
+    # plt.spy(csr_matrix((data, (row, col)), shape = (Nx+2, Nx+2)).toarray())
+    # plt.show()
+    rho_time_update = spsolve(sparseAt, rhs, permc_spec=None, use_umfpack=True) # solve system of equations 
+
+    #print(len(rho_time_update))
+    return rho_time_update
+
+
+def discrete_energy(c,rho,hx) :
+
+    # input: discrete values rho and c, and spatial step size hx
+    # discrete energey w.r.t. rho and c
+ 
+    value = 0
+
+    for i in range(len(rho)) : 
+
+        # MIDPOINT RULE
+        value += hx*(rho[i]*np.log(rho[i])-1/2*rho[i]*c[i]) # cc[i] is fine as hat(i) = 1 in midpoint[i]
+
+    
+    return value
+
+
+
+
diff --git a/2d/__pycache__/myfun.cpython-312.pyc b/2d/__pycache__/myfun.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..39b78f767f2996c22ecb00cf1bd39265a7a5aea7
Binary files /dev/null and b/2d/__pycache__/myfun.cpython-312.pyc differ
diff --git a/2d/compute_errorestimates.py b/2d/compute_errorestimates.py
new file mode 100644
index 0000000000000000000000000000000000000000..c4e7adefd1f2dd35fba50fb6a041371a1280b52f
--- /dev/null
+++ b/2d/compute_errorestimates.py
@@ -0,0 +1,252 @@
+import myfun as my
+import numpy as np
+import time
+import pickle
+
+tic = time.time()
+
+# ---------------------------------------------------- COMPUTE A POSTERIORI ERROR ESTIMATES -----------------------------------------------------
+# This script computes the individual terms of the a posteriori error estimates \theta_K for each element K \in \mathca{T}_h
+
+# SETTINGS FOR SCHEME
+nodal_avrg_choice = 'arithmetic-mean'  
+method = 'wasserstein' # using log-mean convective flux
+boundary = 'Neumann'
+interpol = 'morley' 
+
+test = 'manufactured1' # 'diff', 'blow-up', 'manufactured' or 'manufactured1'
+fineness = 0 # number of refinements of mesh before calculating numerical solution
+Nt = 8 # number of time steps
+    
+    
+# initialize different test cases
+if test == 'blow-up' :
+# CONVECTION DEOMINATED REGIME i.e. BLOW-UP 
+
+    def rho0(x) :
+        val = 10**(3)*np.exp(-((x[0]-0.5)**2+(x[1]-0.5)**2)/10**(-2))
+        return val
+    
+    T = 0.0045 # time interval [0,T]
+
+elif test == 'diff' : 
+# DIFFUSION DOMINATED REGIME
+    def rho0(x) :
+        val = 1.3*np.exp(-25*(x[0]-1/2)**2-25*(x[1]-1/2)**2)
+        return val
+
+    T = 0.05 # time interval [0,T]#
+
+elif test == 'manufactured' :
+# MANUFACTURED SOLUTION WITH T = 0.5
+    def rho0(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+
+    def exact_rho(x,t) :
+        val = 1.3/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def exact_c(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    T = 0.5 # time interval [0,T]#
+
+elif  test == 'manufactured1' :
+# MANUFACTURED SOLUTION WITH T = 3
+
+    def rho0(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+
+    def exact_rho(x,t) :
+        val = 1.3/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def exact_c(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    T = 3 # time interval [0,T]#
+
+else :
+    print('Warning: Wrong string input for test.')
+
+
+# SCHEME/PROBLEM PARAMETERS
+hx = 1/2
+ht = (T)/(Nt)
+
+
+# initialize zerost mesh for FV approximations
+[tri,points,nppoints,numtri] = my.initialmesh()
+K = nppoints[tri.simplices]
+
+for i in range(fineness) :
+    hx = 1/2*hx
+    [tri,points,nppoints,numtri] = my.refinemesh(points, K, numtri)
+    K = nppoints[tri.simplices]
+
+
+#get dual mesh for FE method to approximate chemical density
+[K_dual, tri_dual, points_dual, nppoints_dual, numtri_dual] = my.getdualmesh(points, tri, fineness, K)
+K_dual = np.array(K_dual)
+[tri_dual,K_dual] = my.orientation_dualmesh(tri_dual,K_dual) # enforce positive orientation of all triangles in dual mesh
+
+# compute mesh topology for faster find_simplex routine for dual mesh
+[oriented_edges,structure,edge_to_index] = my.get_edges_structure(K_dual)
+my.init_indexE(numtri_dual)
+
+print('Compute error estimates')
+print(interpol+'_'+boundary+'_'+method+'_'+test)
+print('fineness',fineness)
+print('numtri',numtri)
+print('hx',hx)
+print('ht',ht)
+print('numtri_dual',numtri_dual)
+print('Nt',Nt)
+
+max1 = 0 # need this for more efficiency calculating theta_omega
+max2 = 0 # need this for more efficiency calculating theta_omega
+for n in range(Nt) : # range(Nt) :
+
+    progress = n/(Nt)*100
+    print("%.2f" % progress, 'procent of progress made.')
+
+    if n == 0: # slightly different setting at early times
+       
+        #GET ARTIFICIAL MORLEY0
+        pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_c at time step'+str(n)+'.p'
+        cc_0 = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+        # compute c related objects
+        v_0 = lambda x: my.grad_cfunc(cc_0,tri_dual,K_dual,points_dual,x,oriented_edges,structure,edge_to_index) # piecewiese constant 
+        [lin_vx_0,lin_vy_0] = my.get_linv(K_dual,nppoints_dual,numtri_dual,tri_dual,v_0,nodal_avrg_choice) # get linear coefficients
+        Laplace_c_0 = lambda x : my.approx_Laplace_cn(lin_vx_0,lin_vy_0,x,oriented_edges,structure,edge_to_index) # approximation of Laplace(c)=div(v)
+
+        v_m1 = lambda x : v_0(x) # as morley0 uses c0 instaed of c^(n-1) in n = 0 case
+
+        # load data
+        pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_rho at time step'+str(n+1)+'.p'
+        [ht_0,rho_p1,qq0_p1,betaKE_p1] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+        pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_rho at time step'+str(n)+'.p'
+        [rho_0,rho0] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+        # get artificial morley interpolation
+        [qq0_0,betaKE_0] = my.get_morley0_coeff(rho_0,nppoints,numtri,tri,K)
+
+        #dummy variables
+        rho_m1 = 0*rho_0
+        ht_m1 = 0
+
+        # COMPUTE A POSTERIORI ERROR ESTIMATES
+        theta_early = my.get_early_time_error(K,tri,v_0,qq0_0,qq0_p1,betaKE_0,betaKE_p1)
+        print('extra term for early times computed.')
+        theta_res0 = my.get_theta_res(cc_0,Laplace_c_0,qq0_0,qq0_p1,betaKE_0,betaKE_p1,ht_0,K,tri,K_dual,tri_dual,points_dual,oriented_edges,structure,edge_to_index)
+        print('elementwise residual computed.')
+        theta_diff0 = my.get_theta_diff(qq0_0,betaKE_0,K,tri)
+        print('grad morley jump terms computed.')
+        [theta_time0,theta_time1] = my.get_theta_time(qq0_0,qq0_p1,betaKE_0,betaKE_p1,rho_m1,rho_0,rho_p1,ht_m1,ht_0,K,tri,n)
+        print('time contributions computed.')
+        [theta_Omega,max1,max2] = my.get_theta_omega(max1,max2,rho_0,rho_p1,betaKE_0,betaKE_p1,n)
+        print('global convective term computed.')
+
+        # SAVE A POSTERIORI ERROR ESTIMATES
+        data = []
+        data.append(theta_early)
+        data.append(theta_res0)
+        data.append(theta_diff0)
+        data.append(theta_time0)
+        data.append(theta_Omega)
+        pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error at time step'+str(n)+'.p'
+        pickle.dump(data,open('pickle_files/'+pickle_name,'wb')) # store data
+
+
+    else :
+
+        if n == 1:
+
+            # time-update objects
+            cc_m1 = cc_0
+            v_m1 = v_0
+            Laplace_c_m1 = Laplace_c_0
+            rho_m1 = rho_0
+            rho_0 = rho_p1
+            qq0_0 = qq0_p1
+            betaKE_0 = betaKE_p1
+            ht_m1 = ht_0
+
+            # load data
+            pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_c at time step'+str(n)+'.p'
+            cc_0 = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+            pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_rho at time step'+str(n+1)+'.p'
+            [ht_0,rho_p1,qq0_p1,betaKE_p1] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+            # compute c related objects
+            v_0 = lambda x: my.grad_cfunc(cc_0,tri_dual,K_dual,points_dual,x,oriented_edges,structure,edge_to_index) # piecewiese constant 
+            [lin_vx_0,lin_vy_0] = my.get_linv(K_dual,nppoints_dual,numtri_dual,tri_dual,v_0,nodal_avrg_choice) # get linear coefficients
+            Laplace_c_0 = lambda x : my.approx_Laplace_cn(lin_vx_0,lin_vy_0,x,oriented_edges,structure,edge_to_index) 
+
+            # dummy variables
+            betaKE_m1 = 0*np.array(betaKE_0)
+            v_m2 = lambda x : 0*np.array(v_m1(x))
+
+
+        else :
+
+            # time-update objects
+            v_m2 = v_m1
+            v_m1 = v_0
+            Laplace_c_m1 = Laplace_c_0
+            rho_m1 = rho_0
+            rho_0 = rho_p1
+            qq0_0 = qq0_p1
+            betaKE_m1 = betaKE_0
+            betaKE_0 = betaKE_p1
+            ht_m1 = ht_0
+
+            # load data
+            pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_c at time step'+str(n)+'.p'
+            cc_0 = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+            pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_rho at time step'+str(n+1)+'.p'
+            [ht_0,rho_p1,qq0_p1,betaKE_p1] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+            # compute c related objects
+            v_0 = lambda x: my.grad_cfunc(cc_0,tri_dual,K_dual,points_dual,x,oriented_edges,structure,edge_to_index) # piecewiese constant 
+            [lin_vx_0,lin_vy_0] = my.get_linv(K_dual,nppoints_dual,numtri_dual,tri_dual,v_0,nodal_avrg_choice) # get linear coefficients 
+            Laplace_c_0 = lambda x : my.approx_Laplace_cn(lin_vx_0,lin_vy_0,x,oriented_edges,structure,edge_to_index) 
+
+            
+        # COMPUTE A POSTERIORI ERROR ESTIMATES
+        theta_res0 = my.get_theta_res(cc_m1,Laplace_c_m1,qq0_0,qq0_p1,betaKE_0,betaKE_p1,ht_0,K,tri,K_dual,tri_dual,points_dual,oriented_edges,structure,edge_to_index)
+        print('elementwise residual computed.')
+        theta_diff0 = my.get_theta_diff(qq0_0,betaKE_0,K,tri)
+        print('grad morley jump terms computed.')
+        [theta_time0,theta_time1] = my.get_theta_time(qq0_0,qq0_p1,betaKE_0,betaKE_p1,rho_m1,rho_0,rho_p1,ht_m1,ht_0,K,tri,n)
+        print('time contributions computed.')
+        conv_terms = my.get_conv_terms(v_0,qq0_p1,betaKE_p1,rho_p1,K,tri)
+        print('convective term computed.')
+        [theta_Omega,max1,max2] = my.get_theta_omega(max1,max2,rho_0,rho_p1,betaKE_0,betaKE_p1,n)
+        print('global convective term computed.')
+
+        # SAVE A POSTERIORI ERROR ESTIMATES
+        data = []
+        data.append(theta_res0)
+        data.append(theta_diff0)
+        data.append(theta_time0)
+        data.append(theta_time1)
+        data.append(conv_terms)
+        data.append(theta_Omega)
+        pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error at time step'+str(n)+'.p'
+        pickle.dump(data,open('pickle_files/'+pickle_name,'wb')) # store data
+
+progress = 100
+print("%.2f" % progress, 'procent of progress made.')
+elapsed = time.time() - tic
+print('This took',"%.2f" % round(elapsed/60, 2), 'minutes.')
+
+
diff --git a/2d/exact_error.py b/2d/exact_error.py
new file mode 100644
index 0000000000000000000000000000000000000000..95d8e57264f7e148daee277c878e45bdab389b4f
--- /dev/null
+++ b/2d/exact_error.py
@@ -0,0 +1,201 @@
+import myfun as my
+import numpy as np
+import time
+import pickle
+
+tic = time.time()
+
+# ----------------------------------------------- COMPUTE "EXACT" ERROR OF MANUFACTURED SOLUTION -----------------------------------------------------
+# This script computes the LinfL2-L2H1 norm of the "exact" error subject to manufactured solutions. 
+
+# SETTINGS FOR SCHEME
+nodal_avrg_choice = 'least-squares'  
+method = 'wasserstein' # using log-mean convective flux
+boundary = 'Neumann'
+interpol = 'morley'
+
+test = 'manufactured1' # 'diff' or 'blow-up' or 'manufactured'
+fineness = 3 # number of refinements of mesh before calculating numerical solution
+Nt = 8 # number of time steps, [1, 2, 3, 4, 6 ,8 , 16, 32, 64]
+    
+# initialize different test cases; only works for manufactured solutions
+if test == 'manufactured' :
+# MANUFACTURED SOLUTION WITH T = 0.5
+    def rho0(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+
+    def exact_rho(x,t) :
+        val = 1.3/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def exact_c(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def grad_exact_rho(x,t) :
+        val = -1.3*120/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)*np.array([x[0]-1/2,x[1]-1/2])
+        return val
+    
+    T = 0.5 # time interval [0,T]#
+
+elif  test == 'manufactured1' :
+# MANUFACTURED SOLUTION WITH T = 3
+
+    def rho0(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+
+    def exact_rho(x,t) :
+        val = 1.3/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def exact_c(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def grad_exact_rho(x,t) :
+        val = -1.3*120/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)*np.array([x[0]-1/2,x[1]-1/2])
+        return val
+    
+    T = 3 # time interval [0,T]#
+
+else :
+    print('Warning: Wrong string input for test.')
+
+
+# SCHEME/PROBLEM PARAMETERS
+hx = 1/2
+ht = (T)/(Nt)
+
+# initialize zerost mesh for FV approximations
+[tri,points,nppoints,numtri] = my.initialmesh()
+K = nppoints[tri.simplices]
+
+# refine zerost mesh subject to fineness
+for i in range(fineness) :
+    hx = 1/2*hx
+    [tri,points,nppoints,numtri] = my.refinemesh(points, K, numtri)
+    K = nppoints[tri.simplices]
+
+
+#get dual mesh for FE method to approximate chemical density
+[K_dual, tri_dual, points_dual, nppoints_dual, numtri_dual] = my.getdualmesh(points, tri, fineness, K)
+K_dual = np.array(K_dual)
+[tri_dual,K_dual] = my.orientation_dualmesh(tri_dual,K_dual) # enforce positive orientation of all triangles in dual mesh
+
+# compute mesh topology for faster find_simplex routine for dual mesh
+[oriented_edges,structure,edge_to_index] = my.get_edges_structure(np.array(K_dual)) 
+my.init_indexE(numtri_dual)
+
+print('Compute exact error')
+print(interpol+'_'+boundary+'_'+method+'_'+test)
+print('fineness',fineness)
+print('numtri',numtri)
+print('hx',hx)
+print('ht',ht)
+print('numtri_dual',numtri_dual)
+print('Nt',Nt)
+
+# inititlize objects
+L2_array = []
+L2H1_sum = 0
+
+for n in range(Nt) : # range(Nt) :
+
+    progress = n/(Nt)*100
+    print("%.2f" % progress, 'procent of progress made.')
+
+    if n == 0 : # slightly different setting at early times
+
+       # load data
+        pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_rho at time step'+str(n)+'.p'
+        [rho_0,rho0] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+        pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_rho at time step'+str(n+1)+'.p'
+        [ht_0,rho_p1,qq0_p1,betaKE_p1] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+         #GET ARTIFICIAL MORLEY0
+        [qq0_0,betaKE_0] = my.get_morley0_coeff(rho_0,nppoints,numtri,tri,K)
+
+    else :
+
+        if n == 1:
+            
+            # time-update objects
+            qq0_0 = qq0_p1
+            betaKE_0 = betaKE_p1
+
+            # load data
+            pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_rho at time step'+str(n+1)+'.p'
+            [ht_0,rho_p1,qq0_p1,betaKE_p1] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+
+        else :
+
+            # time-update objects
+            qq0_0 = qq0_p1
+            betaKE_0 = betaKE_p1
+
+            # load data
+            pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_rho at time step'+str(n+1)+'.p'
+            [ht_0,rho_p1,qq0_p1,betaKE_p1] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+
+    # COMPUTE RECONSTRUCTION OF MORLEY-TYPE FOR BOTH TIME STEPS, INCLUDUNG ITS GRAD
+    morley_interpol0 = lambda x : my.morley_interpol(K,tri,qq0_0,betaKE_0,x,indexK=-1) # use rho0 here?
+    grad_morley_interpol0 = lambda x : my.grad_morley_interpol(K,tri,qq0_0,betaKE_0,x,indexK=-1)
+    morley_interpol1 = lambda x : my.morley_interpol(K,tri,qq0_p1,betaKE_p1,x,indexK=-1)
+    grad_morley_interpol1 = lambda x : my.grad_morley_interpol(K,tri,qq0_p1,betaKE_p1,x,indexK=-1)
+
+    # COMPUTE FULL RECONSTRUCTION (time and space), INCLUDING ITS GRAD
+    morley_spacetime = lambda x,t : morley_interpol1(x)*(t-n*ht)/((n+1)*ht-n*ht) -  morley_interpol0(x)*(t-(n+1)*ht)/((n+1)*ht-n*ht)
+    aux_error = lambda x,t : (exact_rho(x,t)-morley_spacetime(x,t))**2 # L^2 in space
+    grad_morley_spacetime = lambda x,t : grad_morley_interpol1(x)*(t-n*ht)/((n+1)*ht-n*ht) -  grad_morley_interpol0(x)*(t-(n+1)*ht)/((n+1)*ht-n*ht)
+    grad_aux_error = lambda x,t : np.linalg.norm(grad_exact_rho(x,t)-grad_morley_spacetime(x,t))**2 # H^1 in space
+
+    # COMPUTE L2 NORM in space
+    aux = lambda x : aux_error(x,n*ht) # the maximum is attaind at time steps t^n = n*ht
+    intL2 = my.get_integral_Omega(aux) # integral over total domain
+    L2_array.append(intL2)
+
+    if n == Nt-1 :
+        aux = lambda x : aux_error(x,(n+1)*ht) # the maximum is attaind at time steps t^n = n*ht
+        intL2 = my.get_integral_Omega(aux) # integral over total domain
+        L2_array.append(intL2)
+
+    # COMPUTE L2H1 NORM
+    maxiter = np.lcm.reduce([1, 2, 3, 4, 6 ,8 , 16, 32, 64]) # get maxiter as lowest common multiple of numbers of time steps used
+    for m in range(int(maxiter/Nt)) : # subintervals
+        I = np.array([(maxiter/Nt*n+m)*T/maxiter,((maxiter/Nt*n+m)+1)*T/maxiter])
+
+        #quadrature rule on each subinterval
+        areaI = I[1]-I[0]
+        trafo = lambda y : y*(I[1]-I[0])/2+(I[1]+I[0])/2 # y in [-1,1]
+        ti = np.array([-np.sqrt(1/3),np.sqrt(1/3)]) #on [-1,1]
+        wi = [1,1]
+        val = 0
+        for k in range(len(wi)) :
+            grad_aux = lambda x : grad_aux_error(x,trafo(ti[k]))
+            intH1 = my.get_integral_Omega(grad_aux)
+            val += wi[k]*intH1 
+
+        integral = val/2*areaI # get integral 
+
+        L2H1_sum += np.array(integral)
+
+LinfL2 = np.max(L2_array) # compute Linf in time   
+
+# SAVE 'EXACT' ERROR IN LinfL2-L2H1 NORM
+data = []
+data.append(L2_array)
+data.append(LinfL2)
+data.append(L2H1_sum)
+print(data)
+pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_exact error.p'
+pickle.dump(data,open('pickle_files/'+pickle_name,'wb')) # store data
+
+progress = 100
+print("%.2f" % progress, 'procent of progress made.')
+elapsed = time.time() - tic
+print('This took',"%.2f" % round(elapsed/60, 2), 'minutes.')
\ No newline at end of file
diff --git a/2d/main_morley.py b/2d/main_morley.py
new file mode 100644
index 0000000000000000000000000000000000000000..929dc8ade91ca481c9afe62491b2161d304ec614
--- /dev/null
+++ b/2d/main_morley.py
@@ -0,0 +1,304 @@
+import myfun as my
+import numpy as np
+import time
+import pickle
+
+# ---------------------------------------------------- FV-FE ALGORITHM -------------------------------------------------------------------------
+# This script performs a FV-FE Algroithm, displayed in the master thesis, for the parabolic-elliptic Keller-Segel system with an initial datum and 
+# the homogeneous Neumann boundary condition
+
+tic = time.time()
+
+# SETTINGS FOR SCHEME
+nodal_avrg_choice = 'least-squares'  # 'arithmetic-mean' or 'least-squares' -> see nasa paper
+method = 'wasserstein' # using log-mean convective flux
+boundary = 'Neumann'
+interpol = 'morley' 
+
+test = 'manufactured1' #'diff', 'blow-up', 'manufactured' or 'manufactured1'
+fineness = 3 # number of refinements of mesh before calculating numerical solution
+Nt = 8 # number of time steps
+
+print('FV-FE Algorithm')
+print(interpol+'_'+boundary+'_'+method+'_'+test)
+print('fineness',fineness)
+print('Nt',Nt)
+    
+# initialize different test cases
+if test == 'blow-up' :
+# CONVECTION DEOMINATED REGIME i.e. BLOW-UP 
+
+    def rho0(x) :
+        val = 10**(3)*np.exp(-((x[0]-0.5)**2+(x[1]-0.5)**2)/10**(-2))
+        return val
+    
+    def f(x,t) :
+        val = 0
+        return val
+    
+    def g(x,t) :
+        val = 0
+        return val
+    
+    T = 0.0045 # time interval [0,T]
+
+elif test == 'diff' : 
+# DIFFUSION DOMINATED REGIME
+    def rho0(x) :
+        val = 1.3*np.exp(-25*(x[0]-1/2)**2-25*(x[1]-1/2)**2)
+        return val
+    
+    def f(x,t) :
+        val = 0
+        return val
+    
+    def g(x,t) :
+        val = 0
+        return val
+
+    T = 0.05 # time interval [0,T]#
+
+elif test == 'manufactured' :
+# MANUFACTURED SOLUTION WITH T = 0.5
+    def rho0(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+
+    def exact_rho(x,t) :
+        val = 1.3/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def exact_c(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def delt_exact_rho(x,t) : 
+        val = -1.3/(1+t)**2*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def grad_exact_rho(x,t) :
+        val = -1.3*120/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)*np.array([x[0]-1/2,x[1]-1/2])
+        return val
+    
+    def grad_exact_c(x) :
+        val = -1.3*120*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)*np.array([x[0]-1/2,x[1]-1/2])
+        return val
+    
+    def Laplacian_exact_rho(x,t) :
+        val = 1/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)*(9048-18720*x[0]+18720*x[0]**2-18720*x[1]+18720*x[1]**2)
+        return val
+    
+    def Laplacian_exact_c(x) :
+        val = np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)*(9048-18720*x[0]+18720*x[0]**2-18720*x[1]+18720*x[1]**2)
+        return val
+
+
+    def f(x,t) :
+        val = delt_exact_rho(x,t) + np.dot(grad_exact_c(x),grad_exact_rho(x,t)) + exact_rho(x,t)*Laplacian_exact_c(x) - Laplacian_exact_rho(x,t)
+        return val
+    
+    def g(x,t) :
+        val = exact_c(x) - Laplacian_exact_c(x) - exact_rho(x,t)
+        return val
+    
+    T = 0.5 # time interval [0,T]#
+
+elif  test == 'manufactured1' :
+# MANUFACTURED SOLUTION WITH T = 3
+
+    def rho0(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+
+    def exact_rho(x,t) :
+        val = 1.3/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def exact_c(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def delt_exact_rho(x,t) : 
+        val = -1.3/(1+t)**2*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def grad_exact_rho(x,t) :
+        val = -1.3*120/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)*np.array([x[0]-1/2,x[1]-1/2])
+        return val
+    
+    def grad_exact_c(x) :
+        val = -1.3*120*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)*np.array([x[0]-1/2,x[1]-1/2])
+        return val
+    
+    def Laplacian_exact_rho(x,t) :
+        val = 1/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)*(9048-18720*x[0]+18720*x[0]**2-18720*x[1]+18720*x[1]**2)
+        return val
+    
+    def Laplacian_exact_c(x) :
+        val = np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)*(9048-18720*x[0]+18720*x[0]**2-18720*x[1]+18720*x[1]**2)
+        return val
+
+
+    def f(x,t) :
+        val = delt_exact_rho(x,t) + np.dot(grad_exact_c(x),grad_exact_rho(x,t)) + exact_rho(x,t)*Laplacian_exact_c(x) - Laplacian_exact_rho(x,t)
+        return val
+    
+    def g(x,t) :
+        val = exact_c(x) - Laplacian_exact_c(x) - exact_rho(x,t)
+        return val
+    
+    T = 3 # time interval [0,T]#
+
+else :
+    print('Warning: Wrong string input for test.')
+
+toc = time.time()
+
+# SCHEME/PROBLEM PARAMETERS
+hx = 1/2
+ht = (T)/(Nt)
+
+# GET PRIMAL MESH :
+# initialize zerost mesh for FV approximations
+[tri,points,nppoints,numtri] = my.initialmesh()
+K = nppoints[tri.simplices]
+
+# refine zerost mesh subject to fineness
+for i in range(fineness) :
+    hx = 1/2*hx
+    [tri,points,nppoints,numtri] = my.refinemesh(points, K, numtri)
+    K = nppoints[tri.simplices]
+
+#get dual mesh for FE method to approximate chemical density
+[K_dual, tri_dual, points_dual, nppoints_dual, numtri_dual] = my.getdualmesh(points, tri, fineness, K)
+K_dual = np.array(K_dual)
+
+[tri_dual,K_dual] = my.orientation_dualmesh(tri_dual,K_dual) # enforce positive orientation of all triangles in dual mesh
+
+# compute mesh topology for faster find_simplex routine for dual mesh
+[oriented_edges,structure,edge_to_index] = my.get_edges_structure(np.array(K_dual)) 
+my.init_indexE(numtri_dual)
+
+elapsed = time.time() - toc
+print('Mesh topology computed in ',"%.2f" % round(elapsed/60, 2), 'minutes.') 
+toc = time.time()
+
+# print most important information regarding problem/scheme
+print('numtri',numtri)
+print('numtri_dual',numtri_dual)
+print('Nt',Nt)
+print('hx',hx)
+print('ht',ht)
+
+# pre-allocate variables
+rho = np.zeros([Nt+1,numtri])
+cc = np.zeros([Nt+1,len(points)])
+
+toc = time.time()
+
+A = my.assemble_FE_matrix1(tri_dual,K_dual,points_dual)
+elapsed = time.time() - toc
+print('FE matrix assembled in ',"%.2f" % round(elapsed/60, 2), 'minutes.') 
+toc = time.time()
+
+# calculate c0 with rho0 as right-hand side
+c0 = my.getc_FE1(rho0,lambda x: g(x,0) ,tri_dual,K_dual,points_dual,A)
+cc[0][:] = c0
+elapsed = time.time() - toc
+print('Initial chemical concentration approximated via FE scheme in ',"%.2f" % round(elapsed/60, 2), 'minutes.') 
+toc = time.time()
+
+# get discrete rho0 by evaluating at circumcenters x_K
+for i in range(len(K)) :
+    CC = my.circumcenter(K[i])
+    rho[0][i] = rho0(CC)
+
+# SAVE INITIAL RHO
+data = []
+data.append(rho[0][:])
+data.append(rho0)
+pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_rho at time step'+str(0)+'.p'
+pickle.dump(data,open('pickle_files/'+pickle_name,'wb')) # store data
+
+hht = [0]
+
+#flag = False # for adaptive time stepping
+for n in range(Nt) : # range(Nt) :
+    toc = time.time()
+
+    progress = n/Nt*100
+    print("%.2f" % progress, 'procent of progress made.')
+
+    # SAVE DATA CHEMICAL CONCENTRATION
+    pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_c at time step'+str(n)+'.p'
+    pickle.dump(cc[n][:],open('pickle_files/'+pickle_name,'wb')) # store data
+
+    # GET RHO VIA FINITE VOLUME SCHEME
+    # set v = gradient c
+    v = lambda x: my.grad_cfunc(cc[n][:],tri_dual,K_dual,points_dual,x,oriented_edges,structure,edge_to_index) # piecewiese constant 
+    elapsed = time.time() - toc
+    print('Convection term initialized via v in ',"%.2f" % round(elapsed/60, 2), 'minutes.') 
+    toc = time.time()
+    
+    # # ADAPTIVE TIME STEPPING
+    # if (T-hht[-1]) < ht : # make sure we do not overshoot time interval [0,T]
+    #     ht = np.abs(T-hht[-1])
+    #     flag = True
+    # else :
+    #     ht = my.get_timestep(v,K,tri)
+    
+    # hht.append(hht[-1]+ht) # list of t^n
+    # print('time-step',hht)
+     
+    # EXECUTE FINITE VOLUME SCHEME
+    rho[n+1][:] = my.getrho(rho[n][:],lambda x : f(x,n*ht),ht,v,tri,K,numtri) 
+    elapsed = time.time() - toc
+    print('Bacterial denisty computed via FV scheme in ', "%.2f" % round(elapsed/60, 2), 'minutes.')
+    toc = time.time()
+
+    # GET COEFFICIENTS FOR MORLEY INTERPOLATION :
+    FKED = my.getinterpolationRHS(tri,K,rho[n+1][:])
+    elapsed = time.time() - toc
+    print('RHS for Morley coefficients calculated in ', "%.2f" % round(elapsed/60, 2), 'minutes.')
+    toc = time.time()
+
+    qq0 = my.getq0(K,nppoints,numtri,tri,rho[n+1][:],nodal_avrg_choice) # linear interpolation
+    elapsed = time.time() - toc
+    print('Linear interpolation q0 computed in ', "%.2f" % round(elapsed/60, 2), 'minutes.')
+    toc = time.time()
+
+    betaKE = my.getbetaE(K,qq0,FKED) # Jacobi_cdotn is first derivative of componentwise linear interpolation of grad c
+    elapsed = time.time() - toc
+    print('Morley coefficients computed in ', "%.2f" % round(elapsed/60, 2), 'minutes.')
+    toc = time.time()
+
+    # GET INTERPOLANT FOR RHO :
+    if test == 'manufactured1' or 'manufactured' : 
+        interpol_rho = lambda x : 0
+    else :
+        interpol_rho = lambda x: my.morley_interpol(K,tri,qq0,betaKE,x,indexK=-1) # morley interpolation
+    
+    # SAVE BACTERIAL DENSITY RHO 
+    data = []
+    data.append(ht) # time step size Delta t^n := t^{n+1} - t^n
+    data.append(rho[n+1][:])
+    data.append(qq0)
+    data.append(betaKE)
+    pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_rho at time step'+str(n+1)+'.p'
+    pickle.dump(data,open('pickle_files/'+pickle_name,'wb')) # store data
+    
+    # GET CHEMICAL DENISTY
+    cc[n+1][:] = my.getc_FE1(interpol_rho,lambda x : g(x,(n+1)*ht),tri_dual,K_dual,points_dual,A)
+    elapsed = time.time() - toc
+    print('Chemical concentration approximated via FE scheme in ', "%.2f" % round(elapsed/60, 2), 'minutes.')
+    
+    if n == Nt-1 :
+        # SAVE DATA CHEMICAL CONCENTRATION
+        pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_c at time step'+str(n+1)+'.p'
+        pickle.dump(cc[n][:],open('pickle_files/'+pickle_name,'wb')) # store data
+
+progress = 100
+print("%.2f" % progress, 'procent of progress made.')
+elapsed = time.time() - tic
+print('This took',"%.2f" % round(elapsed/60, 2), 'minutes.')
+
diff --git a/2d/myfun.py b/2d/myfun.py
new file mode 100644
index 0000000000000000000000000000000000000000..11ae8ac7aa2641c10011c1808f8c3b037eb955f3
--- /dev/null
+++ b/2d/myfun.py
@@ -0,0 +1,1837 @@
+import numpy as np
+from scipy.spatial import Delaunay
+from scipy.sparse import csr_matrix 
+from matplotlib import pyplot as plt
+from scipy.sparse.linalg import spsolve
+
+
+
+# GENERAL FUNCTIONS
+
+def initialmesh() : 
+# input : None
+# output : a very coarse triangulation (mesh size = 1/2) with property that all inner angles of the triangles are < pi/2
+  
+    points = [[0,0],[1,0],[0,1],[1,1]]
+    nppoints = np.array(points)
+
+    points.append(1/2*(nppoints[0]+nppoints[1]))
+    points.append(1/2*(nppoints[0]+nppoints[2]))
+    points.append(1/2*(nppoints[3]+nppoints[1]))
+    points.append(1/2*(nppoints[3]+nppoints[2]))
+    points.append(1/3*(nppoints[0]+nppoints[1]+nppoints[2]))
+    points.append(1/3*(nppoints[3]+nppoints[1]+nppoints[2]))
+    points.append(29/96*nppoints[1]+67/96*nppoints[2]) # where we use 7/24 as the mean of 1/3 und 1/4, to avoid angles of 90°
+    points.append(67/96*nppoints[1]+29/96*nppoints[2])
+
+    nppoints = np.array(points)
+        
+    tri = Delaunay(points) # d=2
+    # tri.simplices gives indices that form the triangles
+    # points[tri.simplices] gives vertices of the mesh elements 
+    # see documentation: https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.Delaunay.html
+
+    numtri = 14 # number of triangles in mesh
+	
+    return [tri, points, nppoints, numtri]
+
+def refinemesh(points, K, numtri) :
+# input : points that occur in triangulation, triangulation K (array of triangles), numtri number of triangles in triangulation
+# output : delaunay triangulation tri, points, nppoints (=points in format np.array), number of triangles numtri
+    for i in range(len(K)) :
+        newpoints = [1/2*(K[i][0]+K[i][1]), 1/2*(K[i][0]+K[i][2]), 1/2*(K[i][1]+K[i][2])]
+        numtri += 3
+        for j in range(3) : 
+            points.append(newpoints[j])  
+
+    auxtpl = list(set([tuple(x) for x in points])) # remove duplicates
+    points = [list(ele) for ele in auxtpl]
+    nppoints = np.array(points)
+
+    tri = Delaunay(points)
+    K = nppoints[tri.simplices] 
+
+    return [tri, points,nppoints,numtri]
+
+
+def getangles(K): 
+# need this for function circumcenter(...)
+
+# input : triangle K
+# output : radian angles of triangle order as the vertices in K, number of triangles in mesh numtri
+	
+    A = K[0]
+    B = K[1]
+    C = K[2]
+
+    # Square of lengths be a2, b2, c2 
+    a2 = lengthSquare(B, C) 
+    b2 = lengthSquare(A, C) 
+    c2 = lengthSquare(A, B) 
+
+	# length of sides be a, b, c 
+    a = np.sqrt(a2); 
+    b = np.sqrt(b2); 
+    c = np.sqrt(c2); 
+
+	# From Cosine law 
+    alpha = np.arccos((b2 + c2 - a2)/(2 * b * c)); 
+    beta = np.arccos((a2 + c2 - b2)/(2 * a * c)); 
+    gamma = np.arccos((a2 + b2 - c2)/(2 * a * b)); 
+
+    angles = [alpha, beta, gamma]
+
+    return angles
+
+def lengthSquare(X, Y): 
+# need this for function circumcenter(...)
+
+# input : points X and Y
+# output : squared distance between X and Y
+
+	xDiff = X[0] - Y[0] 
+	yDiff = X[1] - Y[1] 
+    
+	return xDiff * xDiff + yDiff * yDiff 
+
+def circumcenter(K) :
+# input : triangle K
+# output : circumcenter CC, which can be characterized as the intersection of the perpendicular bisectors of the triangle K
+    
+    angles = getangles(K)
+
+    # circumcenter forular from https://testbook.com/maths/circumcenter-of-a-triangle
+    CC = np.array([(K[0][0]*np.sin(2*angles[0])+K[1][0]*np.sin(2*angles[1])+K[2][0]*np.sin(2*angles[2])),
+          (K[0][1]*np.sin(2*angles[0])+K[1][1]*np.sin(2*angles[1])+K[2][1]*np.sin(2*angles[2]))])
+    CC = 1/(np.sin(2*angles[0])+np.sin(2*angles[1])+np.sin(2*angles[2]))*CC
+
+    return CC
+
+
+def are_neighbors(Ki,Li) :
+    # need this for function getdualmesh(...)
+
+    # input: triangles Ki and Li
+    # output: true or false, indicating if Ki and Li are neighbors
+
+    aux = len(list(set([Ki[0],Ki[1],Ki[2],Li[0],Li[1],Li[2]])))
+
+    if aux == 3 :
+        print('Warning: Triangles are the same')
+        flag = False
+    
+    elif aux == 4 : # neighbors by edge
+        flag = True 
+    
+    elif aux == 5 : # neighbors by node
+        flag = False
+    
+    else : # no neighbors
+        flag = False
+
+    return flag
+
+def linear_search(list, x):
+    # need this for function getdualmesh(...)
+    # basic line search algorithm for 2 dim vectors
+    for i in range(len(list)):
+        if list[i][0] == x[0] and list[i][1] == x[1]:
+            return i
+    return -1
+
+def linear_search_edges(list, x):
+    # need this for function find_simlex1(...)
+    # basic line search algorithm for edges
+    for i in range(len(list)):
+        if list[i][0][0] == x[0][0] and list[i][0][1] == x[0][1] and list[i][1][0] == x[1][0] and list[i][1][1] == x[1][1]:
+            return i
+    return -1
+
+def getdualmesh(points, tri, fineness, K):
+    # input: primal mesh and some of its parameters
+    # output: dual mesh for the FE scheme to induce no jumps for (primal) normal derivatives
+
+    points_primal = len(points)
+
+    hx = 1/2**(fineness+1)
+    points_dual = points
+    nppoints = np.array(points) # important that we initialite this here as list points is changed below...
+
+    for i in range(len(K)) :
+        points_dual.append(circumcenter(K[i]))
+
+    nppoints_dual = np.array(points_dual)
+
+    pt_i = -1
+    K_dual = []
+
+    class myclass :
+        def __init__(self) :
+            self.simplices = []
+
+    tri_dual = myclass()
+
+    for pt in nppoints : # go through all points in primal mesh
+        pt_i +=1
+
+        neighbors = find_neighbors(pt_i,tri)
+        taken_care_of = []
+
+        for i in neighbors :
+            taken_care_of.append(i)
+
+            for j in neighbors :
+
+                if j in taken_care_of :
+                    continue
+
+                if are_neighbors(tri.simplices[i],tri.simplices[j]) :
+                    CCi = circumcenter(K[i])
+                    CCj = circumcenter(K[j])
+                    
+                    K_dual.append([pt,CCi,CCj])
+                    tri_dual.simplices.append([pt_i,points_primal+i,points_primal+j])
+                    
+        # extra triangles on boundary
+        if pt[0] == 0 : # bdr edge x = 0
+            if pt[1] == 0: # corner [0,0]
+                x = pt+np.array([hx,0])
+                y = pt+np.array([0,hx])
+
+                midx = 1/2*(pt+x)
+                midy = 1/2*(pt+y)
+
+                i = tri.find_simplex(midx+10**-8*np.array([0,1]))
+                j = tri.find_simplex(midy+10**-8*np.array([1,0]))
+
+                CCi = circumcenter(K[i])
+                CCj = circumcenter(K[j])
+
+                K_dual.append([pt,x,CCi])
+                x_i = linear_search(nppoints,x)
+                tri_dual.simplices.append([pt_i,x_i,points_primal+i])
+
+                K_dual.append([pt,y,CCj])
+                y_i = linear_search(nppoints,y)
+                tri_dual.simplices.append([pt_i,y_i,points_primal+j])
+
+            elif pt[1] == 1 : # corner [0,1]
+                x = pt+np.array([hx,0])
+
+                midx = 1/2*(pt+x)
+
+                i = tri.find_simplex(midx-10**-8*np.array([0,1]))
+
+                CCi = circumcenter(K[i])
+                x_i = linear_search(nppoints,x)
+                tri_dual.simplices.append([pt_i,x_i,points_primal+i])
+                K_dual.append([pt,x,CCi])
+            
+            else : # no corner
+                x = pt+np.array([0,hx])
+
+                midx = 1/2*(pt+x)
+
+                i = tri.find_simplex(midx+10**-8*np.array([1,0]))
+
+                CCi = circumcenter(K[i])
+                x_i = linear_search(nppoints,x)
+                tri_dual.simplices.append([pt_i,x_i,points_primal+i])
+                K_dual.append([pt,x,CCi])
+        
+        elif pt[0] == 1 : # bdr edge x = 1
+
+            if pt[1] == 0: # corner [1,0]
+                y = pt+np.array([0,hx])
+
+                midy = 1/2*(pt+y)
+
+                j = tri.find_simplex(midy-10**-8*np.array([1,0]))
+
+                CCj = circumcenter(K[j])
+                y_i = linear_search(nppoints,y)
+                tri_dual.simplices.append([pt_i,y_i,points_primal+j])
+                K_dual.append([pt,y,CCj])
+
+            elif pt[1] == 1 : # corner [1,1]
+                nothing = 1
+            
+            else : # no corner
+                x = pt+np.array([0,hx])
+
+                midx = 1/2*(pt+x)
+
+                i = tri.find_simplex(midx-10**-8*np.array([1,0]))
+
+                CCi = circumcenter(K[i])
+                x_i = linear_search(nppoints,x)
+                tri_dual.simplices.append([pt_i,x_i,points_primal+i])
+                K_dual.append([pt,x,CCi])
+        
+        elif pt[1] == 0: # bdr edge y = 0
+            x = pt+np.array([hx,0])
+
+            midx = 1/2*(pt+x)
+
+            i = tri.find_simplex(midx+10**-8*np.array([0,1]))
+
+            CCi = circumcenter(K[i])
+            x_i = linear_search(nppoints,x)
+            tri_dual.simplices.append([pt_i,x_i,points_primal+i])
+            K_dual.append([pt,x,CCi])
+
+        elif pt[1] == 1: # bdr edge y = 1
+            x = pt+np.array([hx,0])
+
+            midx = 1/2*(pt+x)
+
+            i = tri.find_simplex(midx-10**-8*np.array([0,1]))
+
+            CCi = circumcenter(K[i])
+            x_i = linear_search(nppoints,x)
+            tri_dual.simplices.append([pt_i,x_i,points_primal+i])
+            K_dual.append([pt,x,CCi])
+
+
+    numtri_dual = len(K_dual)
+
+    return [K_dual, tri_dual, points_dual, nppoints_dual, numtri_dual]
+
+def orientation_dualmesh(tri,K) :
+# input: dual mesh
+# output: dual mesh, s.t. all triangles are oriented positively, i.e. the vertices that define a triangle are ordered counterclockwise
+    
+    poss = [[0,1,2],[0,2,1],[1,0,2],[1,2,0],[2,1,0],[2,0,1]]
+    for i in range(len(K)) :
+        for k in range(12) :
+            j = np.mod(k,6)
+            flag = -2
+
+            tau1 = K[i][poss[j][1]]-K[i][poss[j][0]]
+            tau2 = K[i][poss[j][2]]-K[i][poss[j][0]]
+
+            if (tau1[0] > 0 and tau1[1] > 0) :
+                flag += 1
+
+                if k < 6 :
+                    tau1_perp1 = [-tau1[1],tau1[0]]
+                else :
+                    tau1_perp1 = [tau1[1],-tau1[0]]
+
+                if np.dot(tau1_perp1,tau2) > 0 : 
+                    flag += 1
+            
+            if flag == 0 :
+
+                if k < 6 :
+                    oldtri = np.array(tri.simplices[i])
+                    K[i] = K[i][poss[j]]
+                    tri.simplices[i] = oldtri[poss[j]]
+                else :
+                    aux_poss = [poss[j][0],poss[j][2],poss[j][1]]
+                    oldtri = np.array(tri.simplices[i])
+                    K[i] = K[i][aux_poss]
+                    tri.simplices[i] = oldtri[aux_poss]
+
+                break
+
+            elif k == 11 :
+                print('Warning: orientation does not work')
+        
+    return [tri,K]
+
+
+def RightOf(x,E) :
+    # need tis for function get_edges_structure(...)
+
+    # says if a point is on the right of an oriented edge
+    val = (E[1][0] - E[0][0])*(x[1] - E[0][1]) - (E[1][1] - E[0][1])*(x[0] - E[0][0])
+    if val < 0 :
+        return True
+    else :
+        return False
+
+def get_edges_structure(K) :
+# need this for more efficient find_simplex1 for the dual mesh
+
+# input: dual mesh in terms of K
+# output: mesh topology of the dual mesh
+
+    oriented_edges = []
+    structure = []
+    edge_to_index = []
+
+    for i in range(len(K)) : 
+        indices = [[1,2],[0,2],[0,1]]
+        for j in range(3) :
+            
+            E = K[i][indices[j]]
+            pt = K[i][j]
+
+            if RightOf(pt,E) :
+                E = [E[1],E[0]]
+            
+            oriented_edges.append(E)
+            structure.append([[E[0],pt],[pt,E[1]]])
+            edge_to_index.append(i)
+
+    return [oriented_edges,structure,edge_to_index]
+
+
+def find_neighbors(pindex, tri):
+# input : index of point pindex, delaunay triangulation tri
+# output : indices of triangles that point pindex is part of
+
+    #neighborstri = []
+    neighborstri_index = []
+    i = -1
+    for simplex in tri.simplices:
+        i += 1
+        if pindex in simplex:
+            #neighborstri.append(simplex)
+            neighborstri_index.append(i)
+            
+    return neighborstri_index
+
+
+def min_dist(E,x) :
+# need this for function find_simplex1(...)
+    # get minimal distance between edge E and point x
+    val = np.abs(np.cross(E[1]-E[0], E[0]-x))/np.linalg.norm(E[1]-E[0])
+    return val
+
+def find_simplex1(x,oriented_edges,structure,indexE) :
+#input: point x, mesh topology of the dual mesh
+# output: edge E on which x lies or x lies in the triangle left of E
+
+    # algorithm cannot deal with edges so we weak it a bit
+    if x[0] == 0 :
+        x[0] = 10**-10
+    if x[1] == 0 :
+        x[1] = 10**-10
+    if x[0] == 1 :
+        x[0] = 1-10**-10
+    if x[1] == 1 :
+        x[1] = 1-10**-10
+    
+    E = oriented_edges[indexE]
+
+    if RightOf(x, E) :
+        E = [E[1],E[0]]
+
+    indexE = linear_search_edges(oriented_edges,E)
+    
+    list_indices = []
+
+    while True :
+        if indexE in list_indices :
+            print('x',x)
+            print('Warning: loop.')
+            break
+        list_indices.append(indexE)
+
+        if (x[0] == E[0][0] and x[1] == E[0][1]) or (x[0] == E[1][0] and x[1] == E[1][1]) :
+            return indexE
+        else :
+            whichop = 0
+            if not(RightOf(x, structure[indexE][0])) : # Onext
+                whichop += 1 
+            if not(RightOf(x, structure[indexE][1])) : #Dprev
+                whichop += 2 
+
+            if whichop == 0 : 
+                    return indexE
+            elif whichop == 1: 
+                    E = structure[indexE][0] # Onext
+            elif whichop ==  2: 
+                    E = structure[indexE][1] # Dprev
+            elif whichop ==  3:
+                if min_dist(structure[indexE][0], x) < min_dist(structure[indexE][1], x) :
+                    E = structure[indexE][0] # Onext
+                else :
+                    E = structure[indexE][1] # Dprev
+                       
+            indexE = linear_search_edges(oriented_edges,E)
+                      
+def init_indexE(numtri_dual) : 
+# initialize global variable indexE for more performent find_simplex1
+    global indexE
+    indexE = round(3*numtri_dual/2)
+    # global counter
+    # counter = np.zeros(3*numtri_dual)
+    print('indexE',indexE)
+
+
+def unitouternormal(K,E) :
+# input: element K and edge E in terms of vertices, 2-dim vector x that lies on E
+# output: unit outward normal vector to K along E
+
+    # calculate normal
+    edge = E[1]-E[0]
+    normal = np.array([edge[1],-edge[0]]) # np.dot(normal,edge) =!= 0
+    normal = 1/np.linalg.norm(normal)*normal # normalization
+    
+    # check for orientation
+    center = (K[0]+K[1]+K[2])/3 # convex compination of vertices (always in interior triangle)
+    lengthplus = np.linalg.norm((E[0]+E[1])/2+normal-center)
+    lengthminus = np.linalg.norm((E[0]+E[1])/2-normal-center)
+
+    if lengthplus<lengthminus :
+        normal = -normal
+
+    return normal
+
+
+def tritrafo(K,L,pt) :
+# need this for integrating via function avrgK(...)
+
+# input : triangles K and L, point pt from K
+# output : pt transformed to L
+
+    A = np.array([[K[0][0], K[1][0], K[2][0]],  #   | xa1 xa2 xa3 |
+                  [K[0][1], K[1][1], K[2][1]],  #A =| ya1 ya2 ya3 |
+                  [      1,       1,       1]]) #   |  1   1   1  |
+    B = np.array([[L[0][0], L[1][0], L[2][0]],  #   | xa1 xa2 xa3 |
+                  [L[0][1], L[1][1], L[2][1]],  #A =| ya1 ya2 ya3 |
+                  [      1,       1,       1]]) #   |  1   1   1  |                                                                                    
+
+    
+    invA = np.linalg.solve(A,np.eye(3))
+    #invA = np.linalg.inv(A)
+    M = np.matmul(B,invA)
+
+    auxx = np.array([pt[0],pt[1],1])
+
+    auxtrafo = np.matmul(M,auxx)
+    trafo = [auxtrafo[0],auxtrafo[1]]
+
+    return [trafo,M]
+
+def avrgK(K,g) :
+# input : element K in terms of vertices, function g
+# output : average of g:IR²->IR over K, area of K
+    S = np.cross(K[1]-K[0],K[2]-K[0]) # B = K[0], C = K[1], A = K[2]
+    areaK = np.abs(S)/2 # get area of triangle K
+
+    # wi = [1] (exactness 1 -> Order 2)
+    # xi = np.array([[1/3,1/3]])
+
+    # from https://mathsfromnothing.au/triangle-quadrature-rules/?i=1 
+    # wi = [0.223381589678011,0.223381589678011,0.223381589678011,0.109951743655322,0.109951743655322,0.109951743655322]
+    # xi = np.array([[0.445948490915965,0.108103018168070],
+    #                [0.445948490915965,0.445948490915965],
+    #                [0.108103018168070,0.445948490915965],
+    #                [0.091576213509771,0.816847572980459],
+    #                [0.091576213509771,0.091576213509771],
+    #                [0.816847572980459,0.091576213509771]])
+
+
+    wi = [1/6,1/6,1/6] # weights*area_unitK(=1/2) (Exactness 2 -> Order 3)
+    xi = np.array([[1/6,2/3],[1/6,1/6],[2/3,1/6]]) # points in unit triangle
+    
+    unitK = [[0,0],[1,0],[0,1]]
+
+    val = 0
+    for i in range(len(wi)) :
+        [trafoxi,M] = tritrafo(unitK,K,xi[i])
+        val += wi[i]*g(trafoxi)
+
+    avrg = val*2
+    integral = avrg*areaK
+ 
+    return [integral,avrg,areaK]
+
+def avrgE(E,g) :
+    # input : edge E in terms of vertices, function g
+    # output : average of g:IR²->IR over E, area of E
+
+    areaE = np.linalg.norm(E[0]-E[1])
+
+    trafo = lambda y : y*(E[1]-E[0])/2+(E[1]+E[0])/2 # y in [-1,1]
+
+    xi = np.array([-np.sqrt(1/3),np.sqrt(1/3)]) #on [-1,1]
+    wi = [1,1]
+
+    # xi = np.array([-np.sqrt(3/5),0,np.sqrt(3/5)])
+    # wi = [5/9,8/9,5/9]
+
+    val = 0
+    for k in range(len(wi)) :
+        val += wi[k]*g(trafo(xi[k])) # get integral 
+
+    avrg = val/2 # get average
+    integral = avrg*areaE
+
+    return [integral,avrg,areaE]
+
+def quadrature(I,g) :
+# inupt: interval I = [a,b], function g
+# output: approximated integral of g over I
+
+    areaI = I[1]-I[0]
+
+    trafo = lambda y : y*(I[1]-I[0])/2+(I[1]+I[0])/2 # y in [-1,1]
+
+    xi = np.array([-np.sqrt(1/3),np.sqrt(1/3)]) #on [-1,1]
+    wi = [1,1]
+
+    # xi = [0]
+    # wi = [2]
+
+    # xi = np.array([-np.sqrt(3/5),0,np.sqrt(3/5)]) #on [-1,1]
+    # wi = [5/9,8/9,5/9]
+
+    val = 0
+    for k in range(len(wi)) :
+        val += wi[k]*g(trafo(xi[k])) # get integral 
+
+    avrg = val/2 # get average
+    integral = avrg*areaI
+
+    return integral
+
+
+def get_integral_Omega(g) :
+# input: function g
+# output: integral of g over Omega
+
+    # get initial mesh for quadrature (different to one that we use to calculate numerical solution to avoid unwanted effects due to projection errors)
+    points = [[0,0],[1,0],[0.5,0.5],[0,1],[1,1]]
+    nppoints = np.array(points)
+    tri = Delaunay(points) 
+    K = nppoints[tri.simplices]
+    numtri = 2
+
+    # refine the mesh until it is finer than mesh used to calculate numerical solution, here 5
+    for i in range(5) :
+        [tri,points,nppoints,numtri] = refinemesh(points, K, numtri)
+        K = nppoints[tri.simplices]
+
+    # calculate integral over Omega as sum over inetrgals on K
+    integral = 0
+    for i in range(numtri) :
+        [aux,avrg,areaK] = avrgK(K[i],g)
+        integral += aux
+
+    return integral
+
+
+def newton_method(f,grad_f,x0,tol,maxiter) :
+# classical newton method
+# Input: function f, Jacobian of f in sparse form, starting point x0, tolerance tol and maximal amount of iteration maxiter
+# output: root of f
+
+    xnew = x0
+    for i in range(maxiter) :
+        xold = xnew
+
+        delx = spsolve(grad_f(xold), -f(xold), permc_spec=None, use_umfpack=True) 
+
+        xnew = xold + delx
+
+        if np.linalg.norm(xold-xnew) < tol :
+            break
+
+    print('Newton steps:',i)
+
+    if i == maxiter-1 :
+        print('Warning: Newton-method reached maxiter.')    
+    
+    return xnew
+
+
+def bubbleK(K,pt) :
+# input : triangle K, point pt
+# output : element bubble function
+    
+    # get reference bubble functions on unit triangle
+    refb1 = lambda x: 1-x[0]-x[1]
+    refb2 = lambda x: x[0]
+    refb3 = lambda x: x[1]
+    refb = lambda x: refb1(x)*refb2(x)*refb3(x)*27
+
+    # transform the triangle to get general bubble function
+    unitK = [[0,0],[1,0],[0,1]]
+    [trafo,M] = tritrafo(K,unitK,pt)
+
+    val = refb(trafo)
+    
+    return val
+
+def gradbubbleK(K,pt) :
+# input : triangle K, point x
+# output : gradient of element bubble function
+
+    # get reference bubble functions on unit triangle
+    refb1 = lambda x: 1-x[0]-x[1]
+    refb2 = lambda x: x[0]
+    refb3 = lambda x: x[1]
+    gradrefb1 = lambda x: [-1,-1]
+    gradrefb2 = lambda x: [1,0]
+    gradrefb3 = lambda x: [0,1]
+    gradrefb = lambda x: (np.multiply(gradrefb1(x),refb2(x))*refb3(x)+refb1(x)*np.multiply(gradrefb2(x),refb3(x))+refb1(x)*np.multiply(gradrefb3(x),refb2(x)))*27
+
+    # transform the triangle to get general bubble function
+    unitK = [[0,0],[1,0],[0,1]]
+    [trafo,M] = tritrafo(K,unitK,pt)
+    
+    # chain rule
+    DM = np.array([[M[0,0],M[0,1]],[M[1,0],M[1,1]]]) # = M[0:1,0:1] 
+    val = np.matmul(gradrefb(trafo),DM) # order of multiplikation important!
+
+    return val
+
+def LaplacebubbleK(K,pt) :
+# input : triangle K, point x
+# output : Laplace of element bubble function
+
+    # get reference bubble functions on unit triangle
+    refb1 = lambda x: 1-x[0]-x[1]
+    refb2 = lambda x: x[0]
+    refb3 = lambda x: x[1]
+    gradrefb1 = lambda x: [-1,-1]
+    gradrefb2 = lambda x: [1,0]
+    gradrefb3 = lambda x: [0,1]
+    Hesse_refb = lambda x: [[27*(refb2(x)*gradrefb1(x)[0]*gradrefb3(x)[0]+refb3(x)*gradrefb1(x)[0]*gradrefb2(x)[0]+refb1(x)*gradrefb3(x)[0]*gradrefb2(x)[0]+refb2(x)*gradrefb3(x)[0]*gradrefb1(x)[0]+refb1(x)*gradrefb2(x)[0]*gradrefb3(x)[0]+refb3(x)*gradrefb2(x)[0]*gradrefb1(x)[0])  ,  27*(refb2(x)*gradrefb1(x)[1]*gradrefb3(x)[0]+refb3(x)*gradrefb1(x)[1]*gradrefb2(x)[0]+refb1(x)*gradrefb3(x)[1]*gradrefb2(x)[0]+refb2(x)*gradrefb3(x)[1]*gradrefb1(x)[0]+refb1(x)*gradrefb2(x)[1]*gradrefb3(x)[0]+refb3(x)*gradrefb2(x)[1]*gradrefb1(x)[0])],    
+                            [27*(refb2(x)*gradrefb1(x)[0]*gradrefb3(x)[1]+refb3(x)*gradrefb1(x)[0]*gradrefb2(x)[1]+refb1(x)*gradrefb3(x)[0]*gradrefb2(x)[1]+refb2(x)*gradrefb3(x)[0]*gradrefb1(x)[1]+refb1(x)*gradrefb2(x)[0]*gradrefb3(x)[1]+refb3(x)*gradrefb2(x)[0]*gradrefb1(x)[1])  ,  27*(refb2(x)*gradrefb1(x)[1]*gradrefb3(x)[1]+refb3(x)*gradrefb1(x)[1]*gradrefb2(x)[1]+refb1(x)*gradrefb3(x)[1]*gradrefb2(x)[1]+refb2(x)*gradrefb3(x)[1]*gradrefb1(x)[1]+refb1(x)*gradrefb2(x)[1]*gradrefb3(x)[1]+refb3(x)*gradrefb2(x)[1]*gradrefb1(x)[1])]]
+   
+    # transform the triangle to get general bubble function
+    unitK = [[0,0],[1,0],[0,1]]
+    [trafo,M] = tritrafo(K,unitK,pt)
+    
+    # double chain rule
+    DM1 = np.array([M[0,0],M[1,0]]) 
+    DM2 = np.array([M[0,1],M[1,1]])
+    val = np.dot(DM1,np.matmul(Hesse_refb(trafo),DM1)) + np.dot(DM2,np.matmul(Hesse_refb(trafo),DM2)) # due to Laplace chain rule 
+
+    return val
+
+
+def bubbleE(K,index_notE,pt) :
+# input : triangle K, indexKpt gives index of point of K that is not part of E (therefore uniquely defining E), point x
+# output :edge bubble function bE
+
+    # get reference bubble functions on unit triangle
+    refb1 = lambda x: 1-x[0]-x[1]
+    refb2 = lambda x: x[0]
+    refb3 = lambda x: x[1]
+
+    if index_notE == 0 : # remove function that is associated to point not belonging to the edge
+        refb = lambda x: refb2(x)*refb3(x)*4 # eher 4
+    elif index_notE == 1 :
+        refb = lambda x : refb1(x)*refb3(x)*4
+    else :
+        refb = lambda x: refb1(x)*refb2(x)*4
+   
+    # transform the triangle to get general bubble function
+    unitK = [[0,0],[1,0],[0,1]]
+    [trafo,M] = tritrafo(K,unitK,pt)
+
+    return refb(trafo)
+
+def gradbubbleE(K,index_notE,pt) :
+    # input : triangle K, indexKpt gives index of point of K that is not part of E (therefore uniquely defining E), point x
+    # output : gradient of edge bubble function bE
+
+    # get reference bubble functions on unit triangle
+    refb1 = lambda x: 1-x[0]-x[1]
+    refb2 = lambda x: x[0]
+    refb3 = lambda x: x[1]
+    gradrefb1 = lambda x: [-1,-1]
+    gradrefb2 = lambda x: [1,0]
+    gradrefb3 = lambda x: [0,1]
+
+    if index_notE == 0 : # remove function that is associated to point not belonging to the edge
+        gradrefb = lambda x: (np.multiply(refb2(x),gradrefb3(x))+np.multiply(gradrefb2(x),refb3(x)))*4
+    elif index_notE == 1 :
+        gradrefb = lambda x : (np.multiply(refb1(x),gradrefb3(x))+np.multiply(gradrefb1(x),refb3(x)))*4
+    else :
+        gradrefb = lambda x: (np.multiply(refb1(x),gradrefb2(x))+np.multiply(gradrefb1(x),refb2(x)))*4
+    
+    # transform the triangle to get general bubble function
+    unitK = [[0,0],[1,0],[0,1]]
+    [trafo,M] = tritrafo(K,unitK,pt)
+
+    # chain rule
+    DM = [[M[0,0],M[0,1]],[M[1,0],M[1,1]]] # = M[0:1,0:1] 
+    val = np.matmul(gradrefb(trafo),DM) # order of multiplikation important!
+
+    return val
+
+def LaplacebubbleE(K,index_notE,pt) :
+    # input : triangle K, indexKpt gives index of point of K that is not part of E (therefore uniquely defining E), point x
+    # output : Laplace of edge bubble function bE
+
+    # get reference bubble functions on unit triangle
+    gradrefb1 = lambda x: [-1,-1]
+    gradrefb2 = lambda x: [1,0]
+    gradrefb3 = lambda x: [0,1]
+
+    if index_notE == 0 : # remove function that is associated to point not belonging to the edge
+        Hesse_refb = lambda x: [[4*(2*gradrefb2(x)[0]*gradrefb3(x)[0])  ,  4*(gradrefb2(x)[0]*gradrefb3(x)[1]+gradrefb2(x)[1]*gradrefb3(x)[0])],
+                                [4*(gradrefb2(x)[1]*gradrefb3(x)[0]+gradrefb2(x)[0]*gradrefb3(x)[1])  ,  4*(2*gradrefb2(x)[1]*gradrefb3(x)[1])]] 
+    elif index_notE == 1 :
+        Hesse_refb = lambda x: [[4*(2*gradrefb1(x)[0]*gradrefb3(x)[0])  ,  4*(gradrefb1(x)[0]*gradrefb3(x)[1]+gradrefb1(x)[1]*gradrefb3(x)[0])],
+                                [4*(gradrefb1(x)[1]*gradrefb3(x)[0]+gradrefb1(x)[0]*gradrefb3(x)[1])  ,  4*(2*gradrefb1(x)[1]*gradrefb3(x)[1])]] 
+    else :
+        Hesse_refb = lambda x: [[4*(2*gradrefb1(x)[0]*gradrefb2(x)[0])  ,  4*(gradrefb1(x)[0]*gradrefb2(x)[1]+gradrefb1(x)[1]*gradrefb2(x)[0])],
+                                [4*(gradrefb1(x)[1]*gradrefb2(x)[0]+gradrefb1(x)[0]*gradrefb2(x)[1])  ,  4*(2*gradrefb1(x)[1]*gradrefb2(x)[1])]] 
+    
+    # transform the triangle to get general bubble function
+    unitK = [[0,0],[1,0],[0,1]]
+    [trafo,M] = tritrafo(K,unitK,pt)
+
+    # double chain rule
+    DM1 = np.array([M[0,0],M[1,0]]) 
+    DM2 = np.array([M[0,1],M[1,1]])
+    val = np.dot(DM1,np.matmul(Hesse_refb(trafo),DM1)) + np.dot(DM2,np.matmul(Hesse_refb(trafo),DM2)) # due to Laplace chain rule 
+    return val
+
+
+def get_timestep(v,K,tri) :
+    # input : v = grad_c, triangulation
+    # output: time step
+
+    # motivated by positivity proof / CFL condition in Kolbe 
+    # go through all CCs, find adjacent triangles in dual_mesh, calculate grad_c*n_E on each of these adjacent triangles
+    # not used in simulations
+    
+    upper_list = []
+    
+    for i in range(len(K)) :
+        
+        neighbors = tri.neighbors[i] # get neighbors of triangle K[i]
+
+        indices = [[1,2],[0,2],[0,1]] 
+        val = 0
+
+        [integral,avrg,areaK] = avrgK(K[i],lambda x: np.dot(x,x))
+
+        for j in range(3):
+
+            if neighbors[j] >= 0 : # i.e. edge E is interior
+                    
+                E = K[i][indices[j]]
+                midE = 1/2*(E[0]+E[1])
+                normalKE = unitouternormal(K[i],E)
+
+                [integral,avrg,areaE] = avrgE(E,lambda x: np.dot(x,x))
+
+                val =+ np.abs(np.dot(v(midE),normalKE))*areaE
+
+            else : # i.e. edge E is part of the boundary -> vKE = 0 for Neumann bdr condition
+
+                val =+ 0
+                
+        upper_list.append(areaK/val)
+
+    
+    upper = np.min(upper_list)
+
+    return upper
+
+
+
+# CHEMICAL DENSITY RELATED FUNCTIONS
+
+def hat(i,tri,points,x,oriented_edges,structure,edge_to_index,indexK) :
+# 2D hat function on triangle
+#input: index i of vertex in points, triangulation tri,K , point x, indexK: wenn -1 muss noch gesucht werden wenn > -1 dann index von triangle
+#output: value of hat function at x
+    
+    # get triangle in which is x
+    if indexK < -0.5 :
+        global indexE
+        indexE = find_simplex1(x,oriented_edges,structure,indexE)
+        k = edge_to_index[indexE]
+    else : 
+        k = indexK
+
+    if i in tri.simplices[k] :
+
+        # get adjacent points of i
+        L = [points[j] for j in tri.simplices[k] if not j==i ]
+
+        M = np.zeros([3,3])
+        rhs = np.zeros(3)
+        rhs[0] = 1 # nodal values for hat functions
+
+        # get system of equations
+        M[0][0] = points[i][0] # a*x+b*y +c = y
+        M[0][1] = points[i][1]
+        M[0][2] = 1
+
+        M[1][0] = L[0][0]
+        M[1][1] = L[0][1]
+        M[1][2] = 1
+
+        M[2][0] = L[1][0]
+        M[2][1] = L[1][1]
+        M[2][2] = 1
+
+        coeff = np.linalg.solve(M,rhs) # solve linear system of equations
+
+        y = coeff[0]*x[0]+coeff[1]*x[1]+coeff[2]
+
+    else :
+        y = 0 # zero if outside of stencil
+
+    return y
+
+def dxhat(i,tri,points,x,oriented_edges,structure,edge_to_index,indexK) :
+# gradient 2d hat function on triangle
+#input: index i of vertex in points, triangulation tri,K , point x, indexK, wenn -1 muss noch gesucht werden wenn > -1 dann index von triangle
+#output: value of gradient of hat function at x
+    
+    if indexK < -0.5 :
+        global indexE
+        indexE = find_simplex1(x,oriented_edges,structure,indexE)
+        k = edge_to_index[indexE]
+    else : 
+        k = indexK
+
+    if i in tri.simplices[k] :
+        
+        # get adjacent points of i
+        L = [points[j] for j in tri.simplices[k] if not j==i] 
+
+        
+        M = np.zeros([3,3])
+        rhs = np.zeros(3)
+        rhs[0] = 1 # nodal values for hat functions
+
+        # get system of equations
+        M[0][0] = points[i][0] # a*x+b*y +c = y
+        M[0][1] = points[i][1]
+        M[0][2] = 1
+
+        M[1][0] = L[0][0]
+        M[1][1] = L[0][1]
+        M[1][2] = 1
+
+        M[2][0] = L[1][0]
+        M[2][1] = L[1][1]
+        M[2][2] = 1
+
+        coeff = np.linalg.solve(M,rhs) #  solve linear system of equations
+
+        grad = np.array([coeff[0],coeff[1]]) # gradient
+
+    else :
+        grad = np.zeros(2) # zero if x is outside of stencil
+
+    return grad
+
+
+def assemble_FE_matrix1(tri_dual,K_dual,points_dual):
+# see Fig. 3.14. in Bartels: Numerical Approximation of PDEs, 2015
+# input : triangulation
+# output : (affine linear) FE matrix
+
+    # initialize objects
+    iter = 0
+    iter_max = 9*len(K_dual)
+
+    I = np.zeros(iter_max)
+    J = np.zeros(iter_max)
+    X_diff = np.zeros(iter_max)
+    X_reac = np.zeros(iter_max)
+
+    m_loc = 1/12*(np.ones([3,3]) + np.array([[1,0,0],[0,1,0],[0,0,1]])) # formular from Bartels book
+
+    for i in range(len(K_dual)) :
+
+        X_K = np.array([[1, 1, 1],[K_dual[i][0][0], K_dual[i][1][0], K_dual[i][2][0]],[K_dual[i][0][1], K_dual[i][1][1], K_dual[i][2][1]]])
+        rhs = np.array([[0,0],[1,0],[0,1]])
+        grads_K = np.linalg.solve(X_K,rhs) # compute gradients 
+        vol_K = np.linalg.det(X_K)/2 # compute areaK 
+
+        # get matrix
+        for m in [0,1,2] :
+            for n in [0,1,2] :
+                I[iter] = tri_dual.simplices[i][m]
+                J[iter] = tri_dual.simplices[i][n]
+                X_diff[iter] = vol_K * np.dot(grads_K[m],grads_K[n]) # diffusion contributions (stiffness matrix)
+                X_reac[iter] = vol_K*m_loc[m][n] # reaction conrtibutions (mass matrix)
+                iter += 1
+
+    sparseA_diff = csr_matrix((X_diff[:iter], (I[:iter], J[:iter])), shape=(len(points_dual),len(points_dual)))
+
+    sparseA_reac = csr_matrix((X_reac[:iter], (I[:iter], J[:iter])), shape=(len(points_dual),len(points_dual)))
+
+    sparseA = sparseA_diff + sparseA_reac
+
+    return sparseA 
+
+def getc_FE1(interpol_rho,g,tri_dual,K_dual,points_dual,sparseA) :
+# input : interpolation of FV solution rho, rhs g for manufactured solutions, dual triangulation (tri,K,points) and the FE matrix
+# output: coefficients FE solution c
+
+    b = np.zeros(len(points_dual))
+
+    aux = lambda x : interpol_rho(x)+g(x)
+
+    for i in range(len(K_dual)) :
+
+        # get objects from Bartels book
+        X_K = np.array([[1, 1, 1],[K_dual[i][0][0], K_dual[i][1][0], K_dual[i][2][0]],[K_dual[i][0][1], K_dual[i][1][1], K_dual[i][2][1]]])
+        vol_K = np.linalg.det(X_K)/2
+        mid_K = 1/3*(K_dual[i][0]+K_dual[i][1]+K_dual[i][2]) # midpoint of triangle K_dual
+
+        for m in [0,1,2] :
+            b[tri_dual.simplices[i][m]] += 1/3* vol_K * aux(mid_K) # using midpoint rule
+       
+    c = spsolve(sparseA, b, permc_spec=None, use_umfpack=True) # solve system of equations
+    
+    # Then FE solution is c_h^n(x) = sum_i (c_i*hat_i(x))
+    return c
+
+
+def cfunc(c,tri,K,points,x,oriented_edges,structure,edge_to_index) : 
+# input: coefficients FE sol., point x, dual triangulation (tri,K,points), objects to make find_simplex for dual meshes more efficient
+# output: function value of FE solution: c_h^n(x) = sum_i (c_i*hat_i(x))
+
+    val = 0
+    global indexE
+    indexE = find_simplex1(x,oriented_edges,structure,indexE)
+    k = edge_to_index[indexE] # get index of triangle in which x lives
+
+    for i in range(len(c)) : # sum over all coefficients c (stencil would be enough)
+        val += c[i]*hat(i,tri,points,x,oriented_edges,structure,edge_to_index,indexK = k)
+
+    return val
+
+def grad_cfunc(c,tri,K,points,x,oriented_edges,structure,edge_to_index) : 
+# input: coefficients FE sol., point x, dual triangulation (tri,K,points), objects to make find_simplex for dual meshes more efficient
+# output: gradient of FE solution: c_h^n(x) = sum_i (c_i*hat_i(x))
+
+    val = 0
+    global indexE
+    indexE = find_simplex1(x,oriented_edges,structure,indexE)
+    k = edge_to_index[indexE]
+
+    for i in range(len(c)) : # sum over all coefficients c (stencil would be enough)
+        val += c[i]*dxhat(i,tri,points,x,oriented_edges,structure,edge_to_index,indexK = k)
+    return val
+
+
+def get_c_plot(pltx,tri,K,points,n,ht,cc,clim,fineness,interpol,oriented_edges,structure,edge_to_index) :
+# input: plot resolution pltx, triangulation, time step n and step size ht, FE sol. cc, fineness, interpolation type and objects to make find_simplex for the dual mesh more effient
+# output: plot of FE solution function c
+
+    # discretize space into pixels
+    x = np.linspace(0, 1, pltx)
+    y = np.linspace(0, 1, pltx)
+    X, Y = np.meshgrid(x, y)
+    Z = np.zeros([pltx,pltx])
+
+    for i in range(pltx):
+        for j in range(pltx):
+            Z[j][i] = cfunc(cc,tri,K,points,[x[i],y[j]],oriented_edges,structure,edge_to_index) # compute value of pixel
+            # Z[j][i] = grad_cfunc(cc,tri,K,points,[x[i],y[j]])[0]
+
+    # plot values Z
+    fig = plt.figure()
+    plt.pcolormesh(X,Y,Z,cmap = 'jet',vmin = clim[0],vmax = clim[1]) 
+    plt.xlabel('x1')
+    plt.ylabel('x2')
+    plt.colorbar()
+    time = n*ht
+    title = 'test1_fineness'+str(fineness)+'_'+interpol+'_c at time step'+str(n)
+    plt.title(title)
+    image_title = title+'.png'
+    plt.savefig(image_title, bbox_inches='tight')
+    plt.close()
+    # plt.show()
+    
+  
+def get_linv(K_dual,nppoints_dual,numtri_dual,tri_dual,v,nodal_avrg_choice) :
+# input: triangulation (K,points,numtri etc), v=grad_c, nodal choice for linear interpolation
+# output: approximation of gradient of v
+
+    grad_cx = []
+    grad_cy = []
+    for i in range(len(K_dual)) :
+        mid = 1/3*(K_dual[i][0]+K_dual[i][1]+K_dual[i][2]) # get  point inside K_dual[i]
+        
+        # v is constant on K_dual, so just evalute in the middle of the triangle
+        grad_cx.append(v(mid)[0]) 
+        grad_cy.append(v(mid)[1])
+
+    grad_cx = np.array(grad_cx)
+    grad_cy = np.array(grad_cy)
+
+    # linearly interpolate componentwise subject to nodal_avrg_choice
+    lin_vx = getq0(K_dual,nppoints_dual,numtri_dual,tri_dual,grad_cx,nodal_avrg_choice)
+    lin_vy = getq0(K_dual,nppoints_dual,numtri_dual,tri_dual,grad_cy,nodal_avrg_choice)
+
+    return [lin_vx,lin_vy]
+
+def eval_linv(lin_vx,lin_vy,x,oriented_edges,structure,edge_to_index) :
+# input : linearly inteprolated grad_c, point x, and objects to make find_simplex for dual mesh more efficient
+# output: point evaluation of linear interpolation of piecewise constant v = grad_c
+
+    global indexE
+    indexE = find_simplex1(x,oriented_edges,structure,indexE)
+    i = edge_to_index[indexE] # get index of triangle in which x lives
+   
+    val = [lin_vx[3*i]*x[0]+lin_vx[3*i+1]*x[1]+lin_vx[3*i+2], lin_vy[3*i]*x[0]+lin_vy[3*i+1]*x[1]+lin_vy[3*i+2]] # compute val=a*x+b*y+c componentwise
+
+    return val
+
+
+def approx_Laplace_cn(lin_vx,lin_vy,x,oriented_edges,structure,edge_to_index) :
+# input: normal vector, linear interpolation to piecewise constant v=grad_c, objects that make find_simplex for dual mesh more efficient
+# output: approximation to Laplace(c)
+
+    global indexE
+    indexE = find_simplex1(x,oriented_edges,structure,indexE)
+    i = edge_to_index[indexE]
+
+    Laplace = lin_vx[3*i]+lin_vy[3*i+1] # get divergence of linear interpolation of piecewise constant v=grad_c -> approximation to Laplace(c)
+
+    return Laplace
+
+
+
+# BACTERIAL DENSITY RELATED FUNCTIONS    
+
+def getvKE(K,E,v) :
+# need this for functions finitevolumescheme_rho(...)
+# input: triangle K, edge E, function v
+# output: coefficient vKE from nicaise-paper, area of edge E
+
+    normalKE = unitouternormal(K,E)
+
+    aux = lambda x : np.dot(v(x),normalKE)
+
+    [integral,vKE,areaE] = avrgE(E,aux) # vKE = avrg over edge E
+
+    return [vKE,areaE]
+
+def getdistE(K1,K2) :
+# need this for function finitevolumescheme_rho(...)
+# input : neigboring triangles K1 and K2
+# output : distance of their circumcenters
+
+    CC1 = circumcenter(K1)
+    CC2 = circumcenter(K2)
+    val = np.linalg.norm(CC1-CC2, ord = None) # distance
+
+    return val
+
+def getrho(cc_old,f,ht,v,tri,K,numtri):
+# input: FE sol. c from last time step as initial value for newton method, rhs f for manufactured solutions, temporal step size ht, v=grad_c, triangulation
+# output: FV solution rho
+
+    eps = 1 # parameter epsilon = 1 in Keller-Segel system
+    
+    rho = finitevolumescheme_rho(cc_old,ht,numtri,tri,K,eps,v,f)
+
+    return rho
+
+def finitevolumescheme_rho(u_old,ht,numtri,tri,K,eps,v,f) :
+# finite volume scheme to get rho with zero Neumann bdr via solving non-linear system of equations
+# input: initial value for newton method u_old, temporal step size ht, triangulation, parameter eps = 1, v=grad_c, rhs f for manufactured solutions
+# output: FV solution rho
+
+    # initialize global objects
+    global global_u_old, global_ht 
+    global_u_old = u_old
+    global_ht = ht
+
+    # get non-linear system of equation to calculate num. sol. uh via FV scheme
+    def func(u) :
+
+        sum_EK = np.zeros(len(K))
+        for i in range(len(K)) : # go through triangles
+
+            [fK,avrg,areaK] = avrgK(K[i],f) # get RHS fK subject to manufactured solutions
+
+            neighbors = tri.neighbors[i] # get neighbors of triangle i
+
+            indices = [[1,2],[0,2],[0,1]] # possible combinations of vertices to compose an edge, where j = 0,1,2 is not contained , order is impotant
+
+            for j in range(3) : # go through edges
+            
+                E = K[i][indices[j]] # choose edge that is shared by K[i] and K[neighbors[j]]
+                
+                if neighbors[j] >= 0 : # i.e. edge E is interior
+                    
+                    [vKE,areaE] = getvKE(K[i],E,v) # avrg over v*nKE ( = 0 if E on boundary due to neumann boundary w.r.t. c )
+                    dE = getdistE(K[i],K[neighbors[j]]) # get distance between circumcenters of triangles
+
+                    if np.abs(u[i]-u[neighbors[j]]) < 10**-6 or u[i] < 10**-6 or u[neighbors[j]] < 10**-6 : # in this case centered flux to avoid divide by 0 or log(0)
+                        sum_EK[i] = sum_EK[i] - eps*areaE/dE*(u[neighbors[j]]-u[i]) + vKE*areaE*1/2*(u[i]+u[neighbors[j]])
+                    else :
+                        sum_EK[i] = sum_EK[i] - eps*areaE/dE*(u[neighbors[j]]-u[i]) + vKE*areaE*((u[i]-u[neighbors[j]])/(np.log(u[i])-np.log(u[neighbors[j]])))
+
+                else : # i.e. edge E is part of the boundary -> vKE = 0 for Neumann bdr condition
+
+                    sum_EK[i] = sum_EK[i] - eps*(0 + 0) #+ 0 (D_K,E) + 0 (vKE)
+
+            sum_EK[i] = 1/areaK*sum_EK[i] - 1/areaK*fK # finite volume scheme
+
+        return (u - global_u_old)/global_ht + sum_EK # difference quotient + finite volume scheme
+    
+    # get Jacobian to solve non-linear system of equations for the newton method
+    def grad_func(u) :
+
+        #sum_EK = np.zeros(len(K))
+        row = []
+        col = []
+        data = []
+        for i in range(len(K)) :
+            
+            [fK,avrg,areaK] = avrgK(K[i],f) # need areaK for system of equations
+            neighbors = tri.neighbors[i] # get neighbors of triangle i
+
+            indices = [[1,2],[0,2],[0,1]] # possible combinations of vertices to compose an edge, where j = 0,1,2 is not contained , order is impotant
+            
+            valK = 0
+            for j in range(3) : # go through edges
+                valL = 0
+
+                E = K[i][indices[j]] # choose edge that is shared by K[i] and K[neighbors[j]]             
+
+                if neighbors[j] >= 0 : # i.e. edge E is interior
+
+                    [vKE,areaE] = getvKE(K[i],E,v) # avrg over v*nKE
+                    dE = getdistE(K[i],K[neighbors[j]]) # get distance between circumcenters of triangles
+
+                    if np.abs(u[i]-u[neighbors[j]]) < 10**-6 or u[i] < 10**-6 or u[neighbors[j]] < 10**-6 : # in this case centered flux to avoid divide by 0 or log(0)
+                        valK = valK + eps*(areaE/dE) + vKE*areaE*1/2
+                        valL = valL - eps*(areaE/dE) + vKE*areaE*1/2
+                    else :
+                        valK = valK + eps*(areaE/dE) + vKE*areaE*((np.log(u[i])-np.log(u[neighbors[j]])-1+u[neighbors[j]]/u[i])/((np.log(u[i])-np.log(u[neighbors[j]]))**2))
+                        valL = valL - eps*(areaE/dE) + vKE*areaE*((-np.log(u[i])+np.log(u[neighbors[j]])-1+u[i]/u[neighbors[j]])/((np.log(u[i])-np.log(u[neighbors[j]]))**2))
+
+                    # get sparse Jacobian
+                    row.append(i)
+                    col.append(neighbors[j])
+                    data.append(1/areaK*valL)
+
+
+                else : # i.e. edge E is part of the boundary -> vKE = 0 for Neumann bdr condition
+                    
+                    valK = valK + 0
+                    valL = valL + 0
+
+            
+            #diagonal entries
+            row.append(i)
+            col.append(i)
+            data.append(1/global_ht+1/areaK*valK)
+
+        sparseJ = csr_matrix((data, (row, col)), shape = (numtri, numtri))#.toarray()
+
+        return sparseJ # return jacobian in sparse form
+    
+    # APPLY NEWTON METHOD TO FIND ROOT U OF FUNC SUBJECT OT ITS JACOBIAN GRAD_U
+    uh = newton_method(func,grad_func, x0 = u_old ,tol = 1.5*10**-8, maxiter = 20) # tol like in fsolve
+
+    return uh
+
+
+def get_rho_plot(pltx,tri,n,ht,rho,rholim,fineness):
+# plot the finite volume solution
+
+# input: resolution of plot pltx, triangulation, time step n, step size ht, FV solution rho, plot limits rholim, fineness
+# output: plot of piecewise constatn FV sol. rho
+    
+    # initialize space discretization as pixels
+    x = np.linspace(0, 1, num = pltx)
+    y = np.linspace(0, 1, num = pltx)
+    X,Y = np.meshgrid(x,y)
+    Z = np.zeros([pltx,pltx])
+
+    for i in range(pltx):
+        for j in range(pltx):
+            pt = [x[i],y[j]]
+            k = tri.find_simplex(pt) # get triangle in which pt lies
+            Z[j][i] = rho[k] # get function value
+
+    #colormap plot
+    fig = plt.figure()
+    plt.pcolormesh(X,Y,Z,cmap = 'jet',vmin = rholim[0],vmax = rholim[1]) # limits of plot
+    plt.xlabel('x1')
+    plt.ylabel('x2')
+    plt.colorbar()
+
+    # # 3D-PLOT:   
+    # X, Y = np.meshgrid(x, y)       
+    # fig = plt.figure()
+    # ax = plt.axes(projection='3d')
+    # plt.xlabel('x1')
+    # plt.ylabel('x2')
+    # ax.axes.set_xlim3d(left=0, right=1) 
+    # ax.axes.set_ylim3d(bottom=0, top=1) 
+    # ax.axes.set_zlim3d(bottom=rholim[0], top=rholim[1]) 
+    # p = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap='jet', edgecolor='none',vmin = rholim[0], vmax = rholim[1])
+    # fig.colorbar(p, ax=ax)
+
+    time = n*ht
+    title = 'fineness'+str(fineness)+'_FV-solution_ bacterial density rho at time '+"%f" % time
+    # title_theta = 'theta_K at time t='+"%f" % time # need this for plot_aposti_estimator
+    title_png = 'FV solution rho_h at time t='+"%f" % time
+    plt.title(title_png)
+    image_title = title+'.png'
+    plt.savefig(image_title, bbox_inches='tight')
+    plt.close()
+    # plt.show()
+    
+
+
+# INTERPOLATION/RECONSTRUCTION
+
+def getinterpolationRHS(tri,K,uh) :
+# reconstruct the diffusive flux of the FV scheme D_K,E = FKED (nicaise-paper notation)
+
+# input : triangulation related values tri,K and numercial solution uh
+# output : RHS for the interpolation consisting of diffusive numerical flux D_K,E = FKED (nicaise-paper notation)
+    
+    FKED = [] # nicaise paper notation
+
+    for i in range(len(K)) :
+        neighbors = tri.neighbors[i] # get neighbors of triangle subject to index i
+
+        indices = [[1,2],[0,2],[0,1]] # possible combinations of vertices to compose an edge, where j = 0,1,2 is not contained , order is impotant
+       
+        auxFKED = []
+        for j in range(3) : # go through neighboring triangles K[neighbors[j]] / go through edges of K[i]
+            
+            E = K[i][indices[j]] # choose edge that is shared by K[i] and K[neighbors[j]]
+
+            [integral,avrg,areaE] = avrgE(E,lambda x: x)  # need areaE
+
+            if neighbors[j] >= 0 : # i.e. edge E is interior
+        
+                dE = getdistE(K[i],K[neighbors[j]]) # get distance between circumcenters of these two triangles
+
+                auxFKED.append(areaE/dE*(uh[neighbors[j]]-uh[i]))
+
+            else : # i.e. edge E is part of the boundary 
+                auxFKED.append(0) # grad_rho*normal = 0 due to zero neumann bdr
+
+        FKED.append(auxFKED)
+
+    return FKED 
+
+
+def getq0(K,nppoints,numtri,tri,uh,string: str) :
+# get linear interpolation part of the morley inteprolation q0
+
+# input : triangles K, number of triangles numtri, numerical solution uh, string on how to compute nodal values
+# output : parameters qq0 of affine linear functions q0 in form [a1,b1,c1,a2,b2,c2,...,aN,bN,cN] with N = len(K)
+
+    # initilize objects
+    rhs = [] 
+    row = []
+    col = []
+    data = []
+    
+    for i in range(len(K)) : # go through all triangles
+
+        for j in range(3): # go through all vertices
+
+            pindex =  tri.simplices[i][j] # get index of vertex 
+
+            neighborstri = find_neighbors(pindex, tri) # indices of triangles that touch vertex associated with pindex
+
+            #calculate vertex_val          
+            if string == 'least-squares' : # weighted mean with inner angles as weights -> see Batina, Rausch, Yang - Paper
+                
+                Rx, Ry, Ixx, Iyy, Ixy = 0, 0, 0, 0, 0 
+                x0 = nppoints[pindex]
+                for k in neighborstri : # go through all touching triangles 
+                    
+                    CC = circumcenter(K[k]) # calculate the cell center
+
+                    Rx += (CC[0]-x0[0])
+                    Ry += (CC[1]-x0[1])
+                    Ixx += (CC[0]-x0[0])**2
+                    Iyy += (CC[1]-x0[1])**2
+                    Ixy += (CC[0]-x0[0])*(CC[1]-x0[1])
+
+                lambdax = (Ixy*Ry-Iyy*Rx)/(Ixx*Iyy-Ixy**2)  # compute Lagrange multipliers
+                lambday = (Ixy*Rx-Ixx*Ry)/(Ixx*Iyy-Ixy**2) 
+
+                wi = []
+                qi = [] 
+                for k in neighborstri : # go through all touching triangles  
+                    CC = circumcenter(K[k])
+                    wi.append(1+lambdax*(CC[0]-x0[0])+lambday*(CC[1]-x0[1])) # get weights
+                    qi.append(uh[k])
+                    
+                wi = np.array(wi)
+                qi = np.array(qi)
+
+                if np.sum(wi) == 0 :
+                    vertex_val = np.sum(qi)/len(qi)
+                else : 
+                    vertex_val = np.dot(wi,qi)/np.sum(wi)
+
+
+            elif string == 'arithmetic-mean'  : # calssic, arithmetic mean
+                
+                vertex_val = 0
+                for k in neighborstri : # go through neighbors
+                    vertex_val += uh[k]
+                vertex_val = vertex_val/len(neighborstri) # arithmetic mean
+
+            else : 
+                print('Error: invalid string input')
+            
+            rhs.append(vertex_val) # assign mean value over all triangles touching vertex
+        
+            # get system of equaton via a*x+b*y+c, in sparse form
+            row.append(3*i+j) # for a 
+            col.append(3*i+0)
+            data.append(K[i][j][0]) # triangle i, vertex j, coordinate 0 i.e. x
+            row.append(3*i+j) # for b 
+            col.append(3*i+1)
+            data.append(K[i][j][1]) # triangle i, vertex j, coordinate 1 i.e. y
+            row.append(3*i+j) # for c
+            col.append(3*i+2)
+            data.append(1) 
+
+    sparseQ0 = csr_matrix((data, (row, col)), shape = (3*numtri, 3*numtri))
+    qq0 = spsolve(sparseQ0, rhs, permc_spec=None, use_umfpack=True) # solve system of equations
+
+    return qq0 # coefficients piecewise linear interpolation
+
+     
+def getbetaE(K,qq0,FKED) :
+# get morley coefficients betaKE
+
+# input : all tirangles K, coefficients qq0 of q0, diffusive numerical flux DKE = FKED (nicaise-paper notation)
+# output : values betaKE of form [[three-dim-array of edges of K1],[three-dim-array of edges of K2],....,[three-dim-array of edges of KN]] where N = len(K)
+    
+    betaKE = [] # initilize 
+
+    for i in range(len(K)) : # go through all triangles
+
+        gradq0 = lambda x : [qq0[3*i],qq0[3*i+1]] # = [a,b] if q0|K = a*x+b*y+c
+
+        indices = [[1,2],[0,2],[0,1]] # possible combinations of vertices to compose an edge, where j = 0,1,2 is not contained , order is impotant!!
+        betaE = []
+
+        for j in range(3) : # go through neighboring triangles K[neighbors[j]] i.e. go through edges of K[i]
+            E = K[i][indices[j]] # choose edge that is shared by K[i] and K[neighbors[j]]
+            normalKE = unitouternormal(K[i],E) # get normal
+
+            # compute betaKE via closed form by plugging Morley inteprolant into degree of freedom associated with betaKE
+            aux = lambda x : np.dot(gradq0(x),normalKE) # get integral over gradq0*normal i.e. normal derivative of q0
+            [integral,avrg, areaE] = avrgE(E,aux)
+            I = integral
+            
+            aux = lambda x : bubbleE(K[i],j,x)*np.dot(gradbubbleK(K[i],x),normalKE) # get integral bubbleE*gradbubbleK*normal
+            [integral,avrg, areaE] = avrgE(E,aux)
+            II = integral
+
+            aux_beta = (FKED[i][j]-I)/(II) # closed form for betaKE
+
+            betaE.append(aux_beta) 
+
+        betaKE.append(betaE)
+        
+    return betaKE
+
+
+def morley_interpol(K,tri,qq0,betaKE,x,indexK) :
+# input : triangulation K,tri / coefficients qq0, betaKE / point x and index of the triangles in which is lives if known
+# output : function value of gradient of morley interpolant at point x
+
+    if indexK >= -0.5 : # give possibility to indicate triangle on which we work
+        i = indexK
+    else :
+        i = tri.find_simplex(x) # get triangle K[i] in which x is contained # get triangle K[i] in which x is contained
+    
+    q0 = qq0[3*i]*x[0]+qq0[3*i+1]*x[1]+qq0[3*i+2] # get linear q0 on triangle K[i]
+
+    aux = 0
+
+    for j in range(3) : # sum over all edges of K[i]
+        
+        aux += betaKE[i][j]*bubbleE(K[i],j,x)*bubbleK(K[i],x) # betaKE contributions to morley
+
+    return q0+aux
+
+
+def grad_morley_interpol(K,tri,qq0,betaKE,x,indexK) :
+# input : triangulation K,tri / coefficients qq0, betaKE / point x and index of the triangles in which is lives if known
+# output : function value of gradient of morley interpolant at point x
+
+    if indexK >=- 0.5 : # give possibility to indicate triangle on which we work
+        i = indexK
+    else :
+        i = tri.find_simplex(x) # get triangle K[i] in which x is contained
+
+    II = 0
+    for j in range(3) : # go through edges
+
+        II += betaKE[i][j]*(gradbubbleE(K[i],j,x)*bubbleK(K[i],x)+gradbubbleK(K[i],x)*bubbleE(K[i],j,x)) # get beta contirbutions
+    
+    return np.array([qq0[3*i],qq0[3*i+1]])+II # piecewise grad_morley = grad_q0 + beta*(...) = [a,b]+beta*(...)
+
+
+def Laplace_morley(K,tri,qq0,betaKE,x,indexK) :
+# input : triangulation K,tri / coefficients qq0, betaKE / point x and index of the triangles in which is lives if known
+# output : fucntion value of the Laplacian of morley interpolant at point x
+
+    if indexK >=- 0.5 : # give possibility to indicate triangle on which we work
+        i = indexK
+    else :
+        i = tri.find_simplex(x) # get triangle K[i] in which x is contained
+    
+    II = 0
+    for j in range(3) : # go through edges
+
+        aux = lambda x : [qq0[3*i],qq0[3*i+1]] # grad_q0
+        div_grad_q0 = lambda x : get_second_derivative(aux,K,tri,x,index=i) # divergence of the component-wise linear interpolation of the piecewise constant gradient grad_q0
+        II += betaKE[i][j]*(LaplacebubbleE(K[i],j,x)*bubbleK(K[i],x)+LaplacebubbleK(K[i],x)*bubbleE(K[i],j,x)+2*np.dot(gradbubbleE(K[i],j,x),gradbubbleK(K[i],x))) # product rule on beta contributions
+
+    return div_grad_q0(x)+II # piecewise Laplace_morley = "Laplace" q0 + alpha*(...)+beta*(...)
+
+
+def get_second_derivative(grad_q0,K,tri,x,index) :
+# need this for Laplacian of Morley, which is in turn needed to compute element-wise reisduals for the a posteriori error estimates
+# input : piecewise constant grad_q0 / triangulation K,tri / point x and the index of the triangle in which it lives if known
+# output: divergence of linear interpolation of piecewise constant grad_q0
+
+    if index > -0.5 : # if primal index 
+        i = index
+    else :
+        i = tri.find_simplex(x) 
+
+    bx = []
+    by = []
+    for j in range(3) :
+        pindex =  tri.simplices[i][j] # get index of vertex 
+
+        neighborstri = find_neighbors(pindex, tri) # indices of triangles that touch vertex associated with pindex
+        
+        # get nodal values
+        vertex_valx = 0
+        vertex_valy = 0
+        for k in neighborstri :  # go thorugh neighboring points
+            midk = 1/3*(K[k][0]+K[k][1]+K[k][2]) # middle of triangle, as function is piecewise constant it doesent matter where we evaluate
+            vertex_valx += grad_q0(midk)[0] # x-direction
+            vertex_valy += grad_q0(midk)[1] # y-direction
+        vertex_valx = vertex_valx/len(neighborstri) # arithmetic mean
+        vertex_valy = vertex_valy/len(neighborstri) # arithmetic mean
+        bx.append(vertex_valx)
+        by.append(vertex_valy)
+
+    # get linear system of equation to solve for linear inteprolation a*x+b*y+c = val
+    M = [[K[i][0][0],K[i][0][1],1],[K[i][1][0],K[i][1][1],1],[K[i][2][0],K[i][2][1],1]]
+
+    lin_vx = np.linalg.solve(M,bx)
+    lin_vy = np.linalg.solve(M,by)
+
+    div = lin_vx[0]+lin_vy[1] # get divergence of linear inteprolation
+    
+    return np.array(div)
+
+
+def getmorleyplot(n,ht,K,tri,qq0,betaKE,h,rholim,fineness) : 
+# input : time step n, step size ht / triangulation K,tri,points / morley coefficients qq0, betaKE / plot mesh size h, plot limits rholim, fineness
+# output : *shows/saves plot*
+
+    def exact_rho(x,t) :
+        val = 1/(1+t)*np.exp(-25*(x[0]-1/2)**2-25*(x[1]-1/2)**2)
+        return val
+
+    x = np.linspace(0, 1, int(1/h))
+    y = np.linspace(0, 1, int(1/h))
+    X, Y = np.meshgrid(x, y)
+
+    Z = np.zeros([len(X),len(Y)])
+    for i in range(len(X)) :
+        for j in range(len(Y)) :
+            pt = [x[i],y[j]]
+            Z[j][i] = morley_interpol(K,tri,qq0,betaKE,pt,indexK=-1) # plot morley interpolant
+            #Z[j][i] = exact_rho(pt,n*ht) # plot exact solution
+            #Z[j][i] = grad_morley_interpol(K,tri,v,grad_vn,qq0,betaKE,pt,indexK=-1)[1] # plot gradient of morley in one direction
+           
+    # #colormap plot
+    # fig = plt.figure()
+    # plt.pcolormesh(Z,cmap = 'jet',vmin = rholim[0],vmax = rholim[1]) # limits of plot
+    # plt.xlabel('x1')
+    # plt.ylabel('x2')
+    # plt.colorbar()
+
+    # 3D-PLOT:          
+    fig = plt.figure()
+    ax = plt.axes(projection='3d')
+    plt.xlabel('x1')
+    plt.ylabel('x2')
+    ax.axes.set_xlim3d(left=0, right=1) 
+    ax.axes.set_ylim3d(bottom=0, top=1) 
+    ax.axes.set_zlim3d(bottom=rholim[0], top=rholim[1]) 
+    p = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap='jet', edgecolor='none',vmin = rholim[0], vmax = rholim[1])
+    fig.colorbar(p, ax=ax)
+
+    time = n*ht
+    title = 'fineness'+str(fineness)+'_morley interpolation _ bacterial density rho at time '+"%f" % time
+    title_png = 'Morley interpolation at time '+"%f" % time
+    # title = 'Exact rho at time '+"%f" % time
+    plt.title(title_png)
+    image_title = title+'.png'
+    plt.savefig(image_title, bbox_inches='tight')
+    plt.close()
+    #plt.show()
+
+
+
+# A POSTERIORI ERROR ESTIMATES 
+
+def diam(K) :
+# get approximately the diameter of triangle K, need this for various residual estimates
+
+# input: triangle K
+# output: length of the longest edge, which differs the actual diameter only by a constant 
+
+    indices = [[1,2],[0,2],[0,1]]
+    val_max = 0
+    for j in range(3) : # go through edges
+        E = K[indices[j]]
+        val = np.linalg.norm(E[1]-E[0]) # get length of edge
+        if val > val_max : # update val_max if val is larger
+            val_max = val
+
+    return val_max
+
+
+def get_theta_res(cc_m1,Laplace_v_m1,qq0_0,qq0_p1,betaKE_0,betaKE_p1,ht_0,K,tri,K_dual,tri_dual,points_dual,oriented_edges,structure,edge_to_index) :
+# get local residual for each triangle K
+# input: c^n-1, Laplace(c^n-1), morley^n, morley^n+1, time step size ht_0, primal and dual triangulation, objects to make find_simplex for dual mesh more efficient
+# output: the local residual part of the a posteriori error estimator
+
+    res_0 = []
+    for i in range(len(K)) : 
+
+        morley_0 = lambda x: morley_interpol(K,tri,qq0_0,betaKE_0,x,indexK=i) # get morley^n
+        morley_p1 = lambda x: morley_interpol(K,tri,qq0_p1,betaKE_p1,x,indexK=i) # get morley^n+1
+
+        grad_morley_0 = lambda x: grad_morley_interpol(K,tri,qq0_0,betaKE_0,x,indexK=i) # get grad_morley^n
+
+        Laplace_morley_0 = lambda x: Laplace_morley(K,tri,qq0_0,betaKE_0,x,indexK=i) # get Laplace_morley^n
+
+        grad_c_m1 = lambda x : grad_cfunc(cc_m1,tri_dual,K_dual,points_dual,x,oriented_edges,structure,edge_to_index) #get grad_c^n-1 
+
+        # GET L2 NORM OF LOCAL RESIDUAL
+        aux_0 = lambda x : ((morley_p1(x)-morley_0(x))/ht_0 + np.dot(grad_morley_0(x),grad_c_m1(x)) + morley_0(x)*Laplace_v_m1(x) - Laplace_morley_0(x))**2
+        [integral_0,avrg,areaK] = avrgK(K[i],aux_0)
+
+        hK = diam(K[i]) # get diameter of K
+
+        res_0.append(integral_0*hK**2) # full term
+    
+    return res_0
+
+
+def get_theta_diff(qq0_0,betaKE_0,K,tri) :
+# get grad_morley jump terms for each triangle K
+
+# input: morley^n and primal triangulation
+# output: grad_morley jump terms from the a posteriori error estimator
+
+    diff0 = []
+    for i in range(len(K)) : # go through triangles
+
+        indices = [[1,2],[0,2],[0,1]]  
+
+        grad_morley_0 = lambda x: grad_morley_interpol(K,tri,qq0_0,betaKE_0,x,indexK=i) # get grad_morley^n
+
+        neighbors = tri.neighbors[i] # get neighbors of triangle associated with index i
+        indices = [[1,2],[0,2],[0,1]] # possible combinations of vertices to compose an edge, where j = 0,1,2 is not contained , order is impotant
+
+        val0 = 0
+        for j in range(3) : # go through edges
+        
+            E = K[i][indices[j]] # choose edge that is shared by K[i] and K[neighbors[j]]
+            
+            if neighbors[j] >= 0 : # i.e. edge E is interior
+                
+                grad_morley_0k = lambda x: grad_morley_interpol(K,tri,qq0_0,betaKE_0,x,indexK=neighbors[j]) # get grad_morley subject to neighboring triangle
+
+                normalE = unitouternormal(K[i],E) # get normal vector
+
+                aux0 = lambda x : np.dot(grad_morley_0k(x)-grad_morley_0(x),normalE)**2 # edge L2 norm of the jump 
+                [integral0,avrg,areaE] = avrgE(E,aux0)
+
+                val0 += integral0*areaE # sum over edges
+
+            else : # i.e. edge E is part of the boundary -> vKE = 0 for Neumann bdr condition
+                
+                val0 += 0 # due to homogenous Neumann boundary condition
+
+        diff0.append(val0)
+
+    return diff0
+
+
+def get_theta_time(qq0_0,qq0_p1,betaKE_0,betaKE_p1,rho_m1,rho_0,rho_p1,ht_m1,ht_0,K,tri,n) :
+# get time terms for each triangle K
+
+# input: morley^n, morley^n+1, FV sol rho^n, FV sol rho^n-1, FV sol rho^n+1, step sizes ht^n-1 and ht^n, primal triangulation, time step n
+# output: time contributions to the a posteriori error estimator
+
+    if n == 0 : # slightly different situation at early times
+        
+        time0 = []
+        time1 = []
+        for i in range(len(K)) :
+            morley_0 = lambda x: morley_interpol(K,tri,qq0_0,betaKE_0,x,indexK=i) # get morley^n
+            morley_p1 = lambda x: morley_interpol(K,tri,qq0_p1,betaKE_p1,x,indexK=i) # get morley^n+1
+
+            aux = lambda x : ((morley_p1(x)-morley_0(x))/ht_0 - (rho_p1[i]-rho_0[i])/ht_0)**2 # L2 norm to get the first time term
+            [integral,avrg,areaK] = avrgK(K[i],aux)
+
+            time0.append(integral) # first time term
+            time1.append(0) # no second time term at early times
+           
+
+    else :
+        time0 = []
+        time1 = []
+        for i in range(len(K)) :
+            morley_0 = lambda x: morley_interpol(K,tri,qq0_0,betaKE_0,x,indexK=i) # get morley^n
+            morley_p1 = lambda x: morley_interpol(K,tri,qq0_p1,betaKE_p1,x,indexK=i) # get morley^n+1
+
+            aux = lambda x : ((morley_p1(x)-morley_0(x))/ht_0 - (rho_p1[i]-rho_0[i])/ht_0)**2 # L2 norm to get the first time term
+            [integral,avrg,areaK] = avrgK(K[i],aux)
+
+            val = areaK*((rho_p1[i]-rho_0[i])/ht_0 - (rho_0[i]-rho_m1[i])/ht_m1)**2 # # L2 norm to get the second time term
+
+            time0.append(integral) # first time term
+            time1.append(val) # second time term
+
+    return [time0,time1]
+
+
+def get_maximum_morley(rhoFV,betaKE):
+# need this for function get_theta_omega(...) 
+
+# input : FV sol rhoFV and moerley coefficients betaKE
+# output: approximated Linf(Omega)-norm of morley interpoaltion
+
+    [nodal_max,beta_max] = get_max_q0_beta(rhoFV,betaKE) 
+    
+    return nodal_max + beta_max # sum both maximal values
+
+
+def get_max_q0_beta(rhoFV,betaKE) :
+# need this for function get_theta_omega(...) 
+# idea: get maximal possible values for q0 and for beta-terms individually and then add them and use this as an upper bound
+
+# input : FV solution rhoFV and morley coefficients betaKE
+# output: maximal possible values for q0 and for beta-terms individually
+
+    nodal_max = np.max(rhoFV) # > arithmetic means to calculate nodal values for linear interpolation
+    beta_max = np.max(np.max(np.abs(betaKE),axis=0)) # > max bubbleE*bubbleK as max(bubble) = 1
+
+    return [nodal_max,beta_max]
+    
+
+def get_theta_omega(max1,max2,rho_0,rho_p1,betaKE_0,betaKE_p1,n) :
+# get theta_omega term from masterthesis
+
+# input: max1 and max2 that were already calculated in the previous time step, morley^n, morley^n+1, FV sol rho^n, FV sol rho^n+1
+# output: theta_omega 
+
+    if n == 0 : # slightly different situation at early times
+        
+        max1 = get_maximum_morley(rho_p1,betaKE_p1) # no difference
+        max2 = get_maximum_morley(np.array(rho_0)-np.array(rho_p1),np.array(betaKE_0)-np.array(betaKE_p1)) # difference
+
+        val = (max1+max2)*max2 # = theta_omega
+    
+    else :
+        max3 = max1 # update terms from previous time step
+        max4 = max2
+        max1 = get_maximum_morley(rho_p1,betaKE_p1) # no difference
+        max2 = get_maximum_morley(np.array(rho_0)-np.array(rho_p1),np.array(betaKE_0)-np.array(betaKE_p1)) # difference
+
+        val = (max1+max2)*max2+max3*max4 # = theta_omega
+
+    return [val,max1,max2] # max1 and max2 will be used to calculate theta_omega for the next time step
+
+
+def get_conv_terms(v_0,qq0_p1,betaKE_p1,rho_p1,K,tri) :
+# get convection contributions for each triangle K
+
+# input: v^n=grad_c^n, morley^n+1, FV sol rho^n+1, primal triangulation
+# output: convection contributions to the a posteriori error estimator
+
+    conv_terms = []
+    for i in range(len(K)) :
+
+        morley_p1 = lambda x: morley_interpol(K,tri,qq0_p1,betaKE_p1,x,indexK=i) # get morley^n+1
+
+        indices = [[1,2],[0,2],[0,1]]  
+        neighbors = tri.neighbors[i] # get nieghbors of triangle subject to index i
+
+        hK = diam(K[i]) # get approximated diameter of the triangle
+
+        val = 0
+        for j in range(3) : # go through all edges
+            E = K[i][indices[j]]
+
+            normalKE = unitouternormal(K[i],E) # get normal vector subject to edge E
+
+            # COMPUTE convective term F_E/areaE
+            if np.abs(rho_p1[i]-rho_p1[neighbors[j]]) < 10**-6 or rho_p1[i] < 10**-6 or rho_p1[neighbors[j]] < 10**-6 : # in this case centered flux to avoid divide by 0 or log(0)
+                F_E_over_areaE = 1/2*(rho_p1[i]+rho_p1[neighbors[j]]) 
+            else : # log-mean flux
+                F_E_over_areaE = (rho_p1[i]-rho_p1[neighbors[j]])/(np.log(rho_p1[i])-np.log(rho_p1[neighbors[j]])) 
+
+            aux = lambda x : (np.dot(v_0(x),normalKE)*(morley_p1(x)-F_E_over_areaE))**2 # edge L2 norm
+            [integral,avrg,areaE] =avrgE(E,aux)
+
+            val += np.sqrt(integral)*hK/np.sqrt(areaE) # sum over edges
+        
+        conv_terms.append(val**2)
+
+    return conv_terms
+
+
+def get_morley0_coeff(rho_0,nppoints,numtri,tri,K) :
+# get artificial morley interpolation at time zero
+# need this for extra term at early times
+
+# input: rho_0 discrete initial datum, morley^1, FV sol rho^1, primal triangulation
+# output: coefficients of artificial morley interpolation at time t=0
+
+    qq0 = getq0(K,nppoints,numtri,tri,rho_0,string = 'least-squares') # get linear interpolation q0
+
+    FKED = getinterpolationRHS(tri,K,rho_0) # get diffusive numerical fluxes DKE = FKED (nicaise-paper notation) from FV scheme
+
+    betaKE = getbetaE(K,qq0,FKED) # get coefficients betaKE
+
+    return [qq0,betaKE]
+
+
+def get_early_time_error(K,tri,v_0,qq0_0,qq0_p1,betaKE_0,betaKE_p1) :
+# get extra term at early times for each triangle K
+
+# input: primal triangulation, v^n=grad_c^n, morley^n, morley^n+1
+# output: extra term at early times for the a posteriori error estimator
+    
+    res_0 = []
+    for i in range(len(K)) : 
+
+        morley_0 = lambda x: morley_interpol(K,tri,qq0_0,betaKE_0,x,indexK=i) # get morley^0
+        morley_p1 = lambda x: morley_interpol(K,tri,qq0_p1,betaKE_p1,x,indexK=i) # get morley^1
+
+        grad_morley_0 = lambda x: grad_morley_interpol(K,tri,qq0_0,betaKE_0,x,indexK=i) # get grad_morley^0
+        grad_morley_p1 = lambda x: grad_morley_interpol(K,tri,qq0_p1,betaKE_p1,x,indexK=i) # get grad_morley^1
+
+        aux_0 = lambda x : np.linalg.norm((morley_p1(x)-morley_0(x))*v_0(x) + (grad_morley_p1(x) - grad_morley_0(x)))**2 # L2 norm
+        [integral_0,avrg,areaK] = avrgK(K[i],aux_0)
+
+        res_0.append(integral_0)
+
+
+    return res_0
\ No newline at end of file
diff --git a/2d/plot_aposti_estimator.py b/2d/plot_aposti_estimator.py
new file mode 100644
index 0000000000000000000000000000000000000000..2be7a9a742a45858497e682024ce01a25f70cfc4
--- /dev/null
+++ b/2d/plot_aposti_estimator.py
@@ -0,0 +1,94 @@
+import myfun as my
+import numpy as np
+import time
+import pickle
+
+# ------------------------------------------------------- PLOT A POSTERIORI ERROR ESTIMATOR ---------------------------------------------------
+# This script plots the element-wise a posteriori error estimator \theta_K to see which elements would be refined when following an
+# adaptive mesh refinement strategy.
+
+tic = time.time()
+
+# SETTINGS FOR SCHEME
+nodal_avrg_choice = 'least-squares'  
+method = 'wasserstein' # using log-mean convective flux
+boundary = 'Neumann'
+interpol = 'morley' 
+
+test = 'blow-up' 
+fineness = 3 # number of refinements of mesh before calculating numerical solution
+Nt = 8 # number of time steps used
+
+n = 6 # time step snapshot
+    
+if test == 'blow-up' : 
+# CONVECTION DEOMINATED REGIME i.e. BLOW-UP 
+
+    def rho0(x) :
+        val = 10**(3)*np.exp(-((x[0]-0.5)**2+(x[1]-0.5)**2)/10**(-2))
+        return val
+
+    rholim = [0,5000]
+    T = 0.0045 # time interval [0,T]
+
+else :
+    print('Warning: Wrong string input for test.')
+
+# SCHEME/PROBLEM PARAMETERS
+hx = 1/2
+ht = (T)/(Nt)
+
+# GET PRIMAL MESH :
+[tri,points,nppoints,numtri] = my.initialmesh() # initialize zerost mesh for FV approximations
+K = nppoints[tri.simplices]
+
+# refine zerost mesh subject to fineness
+for i in range(fineness) :
+    hx = 1/2*hx
+    [tri,points,nppoints,numtri] = my.refinemesh(points, K, numtri)
+    K = nppoints[tri.simplices]
+
+# print some plot/solution related quantities
+print('hx',hx)
+print('ht',ht)
+pltx = 5000
+print('pltx',pltx)
+
+# initialite object
+theta_K = []
+
+if n  == 0 : # slightly different situation at early times
+
+    # load a posteriori error estimates
+    pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error at time step'+str(n)+'.p'
+    [theta_early,theta_diff0,theta_time0,theta_Omega0] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+    pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error at time step'+str(n+1)+'.p'
+    [theta_res1,theta_diff1,theta_time1,theta_conv1,theta_conv1,theta_Omega1] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+    # compute \theta_K for each element K
+    for i in range(len(K)) : 
+        theta_K.append(theta_early[i]+theta_res1[i]+theta_diff0[i]+theta_diff1[i]+theta_time0[i]+theta_conv1[i])
+
+
+
+else : # n=/=0
+
+    # load a posteriori error estimates
+    pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error at time step'+str(n)+'.p'
+    [theta_res0,theta_diff0,theta_time0,theta_conv0,theta_conv0,theta_Omega0] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+    pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error at time step'+str(n+1)+'.p'
+    [theta_res1,theta_diff1,theta_time1,theta_conv1,theta_conv1,theta_Omega] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+    # compute \theta_K for each element K
+    for i in range(len(theta_res1)) : 
+        theta_K.append(theta_res0[i]+theta_res1[i]+theta_diff0[i]+theta_diff1[i]+theta_time0[i]+theta_time1[i]+theta_conv0[i]+theta_conv1[i])
+
+
+theta_K = np.array(theta_K)
+
+# get plot limits for error estimator
+thetalim = [0,np.max(np.max(theta_K))]
+
+# plot theta_K as piecewise constant function
+my.get_rho_plot(pltx,tri,n,ht,theta_K,thetalim,fineness)
+    
\ No newline at end of file
diff --git a/2d/plot_bubble.py b/2d/plot_bubble.py
new file mode 100644
index 0000000000000000000000000000000000000000..309a4e2fbe03dd306868bac1ffa940d53423bf53
--- /dev/null
+++ b/2d/plot_bubble.py
@@ -0,0 +1,56 @@
+import myfun as my
+import numpy as np
+from matplotlib import pyplot as plt
+
+
+#PLOT BUBBLE FUNCTIONS
+
+hplt = 200
+
+K = [[0, 0],[0, 1],[1 ,0]]
+L = [[1, 1],[0, 1],[1 ,0]]
+x = np.linspace(0, 1, hplt)
+y = np.linspace(0, 1, hplt)
+X, Y = np.meshgrid(x, y)
+
+
+Z = np.zeros([len(y),len(x)])
+for i in range(len(x)) :
+    for j in range(len(y)) :
+
+        pt = [x[i],y[j]]
+        l = 0
+        if pt[0]+pt[1]<1 :
+            # Z[j][i] = my.bubbleE(K,l,pt)
+            Z[j][i] = my.bubbleK(K,pt)
+        # else: 
+        #     Z[j][i] = my.bubbleE(L,l,pt)
+
+            
+
+# #colormap plot
+fig = plt.figure()
+plt.pcolormesh(Z,cmap = 'jet') # limits of plot
+plt.xlabel('x1')
+plt.ylabel('x2')
+plt.colorbar()
+
+# 3D-PLOT:   
+# X, Y = np.meshgrid(x, y)       
+# fig = plt.figure()
+# ax = plt.axes(projection='3d')
+# plt.xlabel('x1')
+# plt.ylabel('x2')
+# # ax.axes.set_xlim3d(left=0, right=1) 
+# # ax.axes.set_ylim3d(bottom=0, top=1) 
+# # ax.axes.set_zlim3d(bottom=rholim[0], top=rholim[1]) 
+# p = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap='jet', edgecolor='none')
+# fig.colorbar(p, ax=ax)
+# title = 'bubbleK'
+# plt.title(title)
+# image_title = title+'.png'
+#plt.savefig(image_title, bbox_inches='tight')
+# #plt.close()
+plt.show()
+
+    
\ No newline at end of file
diff --git a/2d/plot_meshes.py b/2d/plot_meshes.py
new file mode 100644
index 0000000000000000000000000000000000000000..dcceb29d72fbc7fb86824db5e875908d2d618a67
--- /dev/null
+++ b/2d/plot_meshes.py
@@ -0,0 +1,47 @@
+import myfun as my
+import numpy as np
+from scipy.spatial import Voronoi, voronoi_plot_2d
+from matplotlib import pyplot as plt
+
+
+# ------------------------------------------------------- PLOT A POSTERIORI ERROR ESTIMATOR ---------------------------------------------------
+# This script plots the primal mesh, the voronoi diagram of the primal mesh and the dual mesh in order to better understand the construction
+# of the dual mesh.
+
+
+fineness = 1 # number of refinements of mesh before calculating numerical solution
+
+[tri,points,nppoints,numtri] = my.initialmesh() # initialize zerost mesh for FV approximations
+K = nppoints[tri.simplices]
+
+hx = 1/2 # mesh size of the zerost mesh
+
+# refine zerost mesh subject to fineness
+for i in range(fineness) :
+    hx = 1/2*hx
+    [tri,points,nppoints,numtri] = my.refinemesh(points, K, numtri)
+    K = nppoints[tri.simplices]
+print('numtri',numtri)
+
+vor = Voronoi(nppoints)
+
+# get dual mesh 
+[K_dual, tri_dual, points_dual, nppoints_dual, numtri_dual] = my.getdualmesh(points, tri, fineness, K)
+K_dual = np.array(K_dual)
+[tri_dual,K_dual] = my.orientation_dualmesh(tri_dual,K_dual) # enforce positive orientation of all triangles in dual mesh
+
+
+# PLOT THE PRIMAL MESH
+plt.triplot(nppoints[:,0], nppoints[:,1], tri.simplices,color='steelblue')
+plt.show()
+
+# PLOT THE VORONOI DIAGRAM, I.E. THE DIRICHLET TESSELATION, OF THE PRIMAL MESH
+fig = voronoi_plot_2d(vor)
+plt.show()
+
+# PLOT THE DUAL MESH
+plt.triplot(nppoints_dual[:,0], nppoints_dual[:,1], tri_dual.simplices,color='green')
+
+
+# plt.savefig('test.png', bbox_inches='tight')
+plt.show()
diff --git a/2d/plot_num-sol.py b/2d/plot_num-sol.py
new file mode 100644
index 0000000000000000000000000000000000000000..42c3181caca8ab80254482394fd2985811ca2e0c
--- /dev/null
+++ b/2d/plot_num-sol.py
@@ -0,0 +1,146 @@
+import myfun as my
+import numpy as np
+import time
+import pickle
+
+
+# ----------------------------------------------------------- PLOT NUMERICAL SOLUTIONS ---------------------------------------------------
+# This script plots the numerical solutions and Morley interpolations obtained via the FV-FE algorithm.
+
+tic = time.time()
+
+# SETTINGS FOR SCHEME
+nodal_avrg_choice = 'arithmetic-mean'  # 'arithmetic-mean' or 'least-squares'
+method = 'wasserstein' # using log-mean convective flux
+boundary = 'Neumann'
+interpol = 'morley'
+
+test = 'manufactured1' #'diff', 'blow-up', 'manufactured' or 'manufactured1'
+fineness = 3 # number of refinements of mesh before calculating numerical solution
+Nt = 8 # number of time steps used
+    
+# initialize different test cases
+if test == 'blow-up' :
+# CONVECTION DEOMINATED REGIME i.e. BLOW-UP 
+
+    def rho0(x) :
+        val = 10**(3)*np.exp(-((x[0]-0.5)**2+(x[1]-0.5)**2)/10**(-2))
+        return val
+    
+    rholim = [0,5000]
+    
+    T = 0.0045 # time interval [0,T]
+
+elif test == 'diff' : 
+# DIFFUSION DOMINATED REGIME
+    def rho0(x) :
+        val = 1.3*np.exp(-25*(x[0]-1/2)**2-25*(x[1]-1/2)**2)
+        return val
+    
+    rholim = [0,1.3]
+
+    T = 0.05 # time interval [0,T]#
+
+elif test == 'manufactured' :
+# MANUFACTURED SOLUTION WITH T = 0.5
+    def rho0(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+
+    def exact_rho(x,t) :
+        val = 1.3/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def exact_c(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    rholim = [0,1.3]
+    
+    T = 0.5 # time interval [0,T]#
+
+elif  test == 'manufactured1' :
+# MANUFACTURED SOLUTION WITH T = 3
+
+    def rho0(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+
+    def exact_rho(x,t) :
+        val = 1.3/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def exact_c(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    rholim = [0,1.3]
+    
+    T = 3 # time interval [0,T]#
+
+else :
+    print('Warning: Wrong string input for test.')
+
+# SCHEME/PROBLEM PARAMETERS
+hx = 1/2
+ht = (T)/(Nt)
+
+# GET PRIMAL MESH :
+[tri,points,nppoints,numtri] = my.initialmesh() # initialize zerost mesh for FV approximations
+K = nppoints[tri.simplices]
+
+# refine zerost mesh subject to fineness
+for i in range(fineness) :
+    hx = 1/2*hx
+    [tri,points,nppoints,numtri] = my.refinemesh(points, K, numtri)
+    K = nppoints[tri.simplices]
+
+# # NEED THIS ONLY TO PLOT CHEMICAL CONCENTRATION c
+# #get dual mesh for FE method to approximate chemical density
+# [K_dual, tri_dual, points_dual, nppoints_dual, numtri_dual] = my.getdualmesh(points, tri, fineness, K)
+# K_dual = np.array(K_dual)
+# [tri_dual,K_dual] = my.orientation_dualmesh(tri_dual,K_dual) # enforce positive orientation of all triangles in dual mesh
+
+# # compute mesh topology for faster find_simplex routine for dual mesh
+# [oriented_edges,structure,edge_to_index] = my.get_edges_structure(K_dual) 
+# my.init_indexE(numtri_dual)
+
+# print some plot/solution related quantities
+print('hx',hx)
+print('ht',ht)
+pltx = 200
+print('pltx',pltx)
+
+for n in [2,4,6,8] :# time snapshots that we want to plot # [0, 5, 10, 15] in kwon paper
+
+    progress = n/Nt*100
+    print("%.2f" % progress, 'procent of progress made.')
+
+    # # PLOT CHEMICAL CONCENTRATION c
+    # # load data related to c
+    # pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_c at time step'+str(n)+'.p'
+    # cc = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+    # clim = [0,1.3] # set plot limits
+    # # plot piecewise linear FE solution c
+    # my.get_c_plot(pltx,tri_dual,K_dual,points_dual,n,ht,cc,clim,fineness,interpol,oriented_edges,structure,edge_to_index) 
+
+    # PLOT BACTERIAL DENSITY rho
+    # load data related to rho
+    pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_rho at time step'+str(n)+'.p'
+    [ht,rho,qq0,betaKE] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+    if n == 0 :
+        [rhoFV,rho0] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+        # plot FV solution rho0 aka. projected initial datum for n=0
+        my.getmorleyplot(n,ht,K,tri,qq0,betaKE,1/pltx,rholim,fineness)
+        #my.get_rho_plot(pltx,tri,n,ht,rhoFV,rholim,fineness)
+    else :
+        [ht,rho,qq0,betaKE] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+        # plot interpolation of Morley-type
+        my.getmorleyplot(n,ht,K,tri,qq0,betaKE,1/pltx,rholim,fineness)
+        # plot FV solution rho0
+        # my.get_rho_plot(pltx,tri,n,ht,rho,rholim,fineness)
+
+progress = 100
+print("%.2f" % progress, 'procent of progress made.')
+elapsed = time.time() - tic
+print('This took',"%.2f" % round(elapsed/60, 2), 'minutes.')
\ No newline at end of file
diff --git a/2d/scaling_space_errorestimates.py b/2d/scaling_space_errorestimates.py
new file mode 100644
index 0000000000000000000000000000000000000000..25b6a43e6b8e2610f46c6a40fc73eef768aa071e
--- /dev/null
+++ b/2d/scaling_space_errorestimates.py
@@ -0,0 +1,301 @@
+# import myfun as my
+import numpy as np
+from matplotlib import pyplot as plt
+import time
+import pickle
+
+# ----------------------------------------------------------- SCALING BEHAVIOR IN SPACE ---------------------------------------------------
+# This script takes the previously computed a posteriori error estimates on each element and adds them in the way in which they constitute 
+# an upper bound for the actual error in LinfL2-L2H1 norm. Here we fix a number of time steps and investigate the scaling behavior in space,
+# i.e. making the maximal mesh size small
+
+tic = time.time()
+
+# SETTINGS FOR SCHEME
+nodal_avrg_choice = 'least-squares'
+method = 'wasserstein' # using log-mean convective flux
+boundary = 'Neumann'
+interpol = 'morley' 
+
+test = 'manufactured' #'diff', 'blow-up', 'manufactured' or 'manufactured1'
+Nt = 8 # fixed number of time steps
+
+steps = 5 # number of space refinements used
+    
+# initialize different test cases
+if test == 'blow-up' :
+# CONVECTION DEOMINATED REGIME i.e. BLOW-UP 
+
+    def rho0(x) :
+        val = 10**(3)*np.exp(-((x[0]-0.5)**2+(x[1]-0.5)**2)/10**(-2))
+        return val
+    
+    T = 0.0045 # time interval [0,T]
+
+elif test == 'diff' : 
+# DIFFUSION DOMINATED REGIME
+    def rho0(x) :
+        val = 1.3*np.exp(-25*(x[0]-1/2)**2-25*(x[1]-1/2)**2)
+        return val
+    
+    T = 0.05 # time interval [0,T]#
+
+elif test == 'manufactured' :
+# MANUFACTURED SOLUTION WITH T = 0.5
+    def rho0(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+
+    def exact_rho(x,t) :
+        val = 1.3/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def exact_c(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    T = 0.5 # time interval [0,T]#
+
+elif  test == 'manufactured1' :
+# MANUFACTURED SOLUTION WITH T = 3
+
+    def rho0(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+
+    def exact_rho(x,t) :
+        val = 1.3/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def exact_c(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+
+    T = 3 # time interval [0,T]#
+
+else :
+    print('Warning: Wrong string input for test.')
+
+
+# SCHEME/PROBLEM PARAMETERS
+h = 1
+ht = (T)/(Nt)
+
+print('ht',ht)
+
+# initialize objects
+hh = []
+LinfL2_max = []
+L2H1_max = []
+theta_early_max = []
+theta_res_max = []
+theta_diff_max = []
+theta_time_max = []
+theta_time_val_max = []
+theta_conv_max = []
+theta_Omega_max = []
+
+
+for fineness in range(steps) : # varibale in space
+    h = 1/2*h
+    hh.append(h)
+
+    # load "exact" error of manufactured solution
+    pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_exact error.p'
+    [L2_array,LinfL2,L2H1] = pickle.load(open('pickle_files/'+pickle_name,'rb')) # load data
+    LinfL2_max.append(LinfL2)
+    L2H1_max.append(L2H1)
+
+    # initialize objects
+    theta_early_sum = 0
+    theta_res_sum = 0
+    theta_diff_sum = 0
+    theta_time_sum = 0
+    theta_time_val_sum = 0
+    theta_conv_sum = 0
+    theta_Omega_sum = 0
+
+    for n in range(Nt) : # go through intervals inbetween time steps
+
+        if n  == 0 :
+            
+            # load a posteriori error estimates
+            pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error at time step'+str(n)+'.p'
+            [theta_early,theta_res0,theta_diff0,theta_time0,theta_Omega0] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+            pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error at time step'+str(n+1)+'.p'
+            [theta_res1,theta_diff1,theta_time1,theta_time_val1,theta_conv1,theta_Omega1] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+            # initialize objects
+            theta_early_aux = 0
+            theta_res_aux = 0
+            theta_diff_aux = 0
+            theta_time_aux = 0
+            theta_time_val_aux = 0
+            theta_conv_aux = 0
+            theta_conv_aux = 0
+
+            # sum over all elements
+            for i in range(len(theta_early)) : 
+                theta_early_aux += theta_early[i] # this is already the squared term
+                theta_res_aux += theta_res0[i] + theta_res1[i]
+                theta_diff_aux += theta_diff0[i]+theta_diff1[i]
+                theta_time_aux += theta_time0[i]
+                theta_conv_aux += theta_conv1[i]
+
+            # sum over time steps, corresponds to L^2 norm in time
+            theta_early_sum += ht*theta_early_aux
+            theta_res_sum += ht*theta_res_aux
+            theta_diff_sum += ht*theta_diff_aux
+            theta_time_sum += ht*theta_time_aux
+            theta_conv_sum += ht*theta_conv_aux
+            theta_Omega_sum += ht*theta_Omega0**2
+        
+        elif n == Nt-1 :
+
+            # load a posteriori error estimates
+            pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error at time step'+str(n)+'.p'
+            [theta_res0,theta_diff0,theta_time0,theta_time_val0,theta_conv0,theta_Omega0] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+             # initialize objects
+            theta_res_aux = 0
+            theta_diff_aux = 0
+            theta_time_aux = 0
+            theta_time_val_aux = 0
+            theta_conv_aux = 0
+            theta_delta_aux = 0
+
+            # sum over all elements
+            for i in range(len(theta_res1)) : 
+                theta_res_aux += theta_res0[i] # this is already the squared term
+                theta_diff_aux += theta_diff0[i]
+                theta_time_aux += theta_time0[i]
+                theta_time_val_aux += theta_time_val0[i]
+                theta_conv_aux += theta_conv0[i]
+
+            # sum over time steps, corresponds to L^2 norm in time
+            theta_res_sum += ht*theta_res_aux
+            theta_diff_sum += ht*theta_diff_aux
+            theta_time_sum += ht*theta_time_aux
+            theta_time_val_sum += ht*theta_time_val_aux
+            theta_conv_sum += ht*theta_conv_aux
+            theta_Omega_sum += ht*theta_Omega0**2
+
+        else : # n=/=0
+
+            # load a posteriori error estimates
+            pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error at time step'+str(n)+'.p'
+            [theta_res0,theta_diff0,theta_time0,theta_time_val0,theta_conv0,theta_Omega0] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+            pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error at time step'+str(n+1)+'.p'
+            [theta_res1,theta_diff1,theta_time1,theta_time_val1,theta_conv1,theta_Omega] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+            # initialize objects
+            theta_res_aux = 0
+            theta_diff_aux = 0
+            theta_time_aux = 0
+            theta_time_val_aux = 0
+            theta_conv_aux = 0
+            theta_conv_aux = 0
+
+            # sum over all elements
+            for i in range(len(theta_res1)) : 
+                theta_res_aux += theta_res0[i]+theta_res1[i] # this is already the squared term
+                theta_diff_aux += theta_diff0[i]+theta_diff1[i]
+                theta_time_aux += theta_time0[i]+theta_time1[i]
+                theta_time_val_aux += theta_time_val0[i]+theta_time_val1[i]
+                theta_conv_aux += theta_conv0[i] + theta_conv1[i]
+
+            # sum over time steps, corresponds to L^2 norm in time
+            theta_res_sum += ht*theta_res_aux
+            theta_diff_sum += ht*theta_diff_aux
+            theta_time_sum += ht*theta_time_aux
+            theta_time_val_sum += ht*theta_time_val_aux
+            theta_conv_sum += ht*theta_conv_aux
+            theta_Omega_sum += ht*theta_Omega0**2
+
+    # save results in order to compute EOC later
+    theta_early_max.append(theta_early_sum)
+    theta_res_max.append(theta_res_sum)
+    theta_diff_max.append(theta_diff_sum)
+    theta_time_max.append(theta_time_sum)
+    theta_time_val_max.append(theta_time_val_sum)
+    theta_conv_max.append(theta_conv_sum)
+    theta_Omega_max.append(theta_Omega_sum)
+
+
+# print results so far
+print('LinfL2_max         ',np.round(LinfL2_max,5))
+print('L2H1_max         ',np.round(L2H1_max,5))
+print('theta_early_max     ',np.round(theta_early_max,5))
+print('theta_res_max    ',np.round(theta_res_max,5))
+print('theta_diff_max    ',np.round(theta_diff_max,40))
+print('theta_time_max    ',np.round(theta_time_max,7))
+print('theta_time_val_max    ',np.round(theta_time_val_max,5))
+print('theta_conv_max     ',np.round(theta_conv_max,5))
+print('theta_Omega_max        ',np.round(theta_Omega_max,5))
+print('mesh sizes ',hh)
+
+# initialize objects
+eoc_LinfL2 = []
+eoc_L2H1 = []
+eoc_early = []
+eoc_res = []
+eoc_diff = []
+eoc_time = []
+eoc_time_val = []
+eoc_conv = []
+eoc_Omega = []
+
+# go through all mesh sizes we used
+for i in range(steps-1) :
+    
+    # COMPUTE THE EOCs
+    aux_LinfL2 = (np.log((LinfL2_max[i])/(LinfL2_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+    aux_L2H1 = (np.log((L2H1_max[i])/(L2H1_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+    aux_early = (np.log((theta_early_max[i])/(theta_early_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+    aux_res = (np.log((theta_res_max[i])/(theta_res_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+    aux_diff = (np.log((theta_diff_max[i])/(theta_diff_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+    aux_time = (np.log((theta_time_max[i])/(theta_time_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+    aux_time_val = (np.log((theta_time_val_max[i])/(theta_time_val_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+    aux_delta = (np.log((theta_conv_max[i])/(theta_conv_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+    aux_Omega = (np.log((theta_Omega_max[i])/(theta_Omega_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+
+    # save the EOCs
+    eoc_LinfL2.append(aux_LinfL2)
+    eoc_L2H1.append(aux_L2H1)
+    eoc_early.append(aux_early)
+    eoc_res.append(aux_res)
+    eoc_diff.append(aux_diff)
+    eoc_time.append(aux_time)
+    eoc_time_val.append(aux_time_val)
+    eoc_conv.append(aux_delta)
+    eoc_Omega.append(aux_Omega)
+
+
+elapsed = time.time() - tic
+print('This took',"%.2f" % round(elapsed/60, 2), 'minutes.')
+
+# print the EOCs
+print('eoc_LinfL2      ',np.round(eoc_LinfL2,5))
+print('eoc_L2H1       ',np.round(eoc_L2H1,5))
+print('eoc_early       ',np.round(eoc_early,5))
+print('eoc_res      ',np.round(eoc_res,6))
+print('eoc_diff      ',np.round(eoc_diff,5))
+print('eoc_time      ',np.round(eoc_time,5))
+print('eoc_time_val      ',np.round(eoc_time_val,5))
+print('eoc_conv      ',np.round(eoc_conv,5))
+print('eoc_Omega      ',np.round(eoc_Omega,5))
+
+
+# plot the EOCs
+hh = np.array(hh)
+fig = plt.figure()
+plt.loglog(hh,hh,'g--',hh,hh**2,'y--',hh,LinfL2_max,'b',hh,L2H1_max,'r',hh,theta_time_max,'m',hh,theta_Omega_max,'orange',hh,theta_conv_max,'brown',hh,theta_early_max,'darkgoldenrod',hh,theta_res_max)
+
+plt.xlabel('hh')
+plt.ylabel('error estimates')
+plot_title = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error estimates'
+plt.title(plot_title)
+plt.legend(['h¹','h²','LinfL2','L2H1','theta_time','theta_Omega','theta_conv','theta_early','theta_res'])
+image_title = str(steps)+'_scaling_behavior_a-posti-error-estimates.png'#example+'_'+title+'_'+method+'_'+string+'.png'
+#plt.savefig(image_title, bbox_inches='tight')
+plt.show()
\ No newline at end of file
diff --git a/2d/scaling_time_errorestimates.py b/2d/scaling_time_errorestimates.py
new file mode 100644
index 0000000000000000000000000000000000000000..c5718be66bc2ad14682ea574a6b306f8e8d91128
--- /dev/null
+++ b/2d/scaling_time_errorestimates.py
@@ -0,0 +1,301 @@
+import numpy as np
+import myfun as my
+#from scipy.sparse.linalg import spsolve
+#from mpl_toolkits import mplot3d
+from matplotlib import pyplot as plt
+import time
+import pickle
+
+# ----------------------------------------------------------- SCALING BEHAVIOR IN SPACE ---------------------------------------------------
+# This script takes the previously computed a posteriori error estimates on each element and adds them in the way in which they constitute 
+# an upper bound for the actual error in LinfL2-L2H1 norm. Here we fix a spatial mesh size and investigate the scaling behavior in time, i.e.
+# we make the temporal step size small.
+
+tic = time.time()
+
+# SETTINGS FOR SCHEME
+nodal_avrg_choice = 'least-squares' 
+method = 'wasserstein' # using log-mean convective flux
+boundary = 'Neumann'
+interpol = 'morley'
+
+test = 'manufactured1' #'diff', 'blow-up', 'manufactured' or 'manufactured1'
+    
+    
+# initialize different test cases
+if test == 'blow-up' :
+# CONVECTION DEOMINATED REGIME i.e. BLOW-UP 
+
+    def rho0(x) :
+        val = 10**(3)*np.exp(-((x[0]-0.5)**2+(x[1]-0.5)**2)/10**(-2))
+        return val
+    
+    T = 0.0045 # time interval [0,T]
+
+elif test == 'diff' : 
+# DIFFUSION DOMINATED REGIME
+    def rho0(x) :
+        val = 1.3*np.exp(-25*(x[0]-1/2)**2-25*(x[1]-1/2)**2)
+        return val
+
+    T = 0.05 # time interval [0,T]#
+
+elif test == 'manufactured' :
+# MANUFACTURED SOLUTION WITH T = 0.5
+    def rho0(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+
+    def exact_rho(x,t) :
+        val = 1.3/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def exact_c(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    T = 0.5 # time interval [0,T]#
+
+elif  test == 'manufactured1' :
+# MANUFACTURED SOLUTION WITH T = 3
+
+    def rho0(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+
+    def exact_rho(x,t) :
+        val = 1.3/(1+t)*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    def exact_c(x) :
+        val = 1.3*np.exp(-60*(x[0]-1/2)**2-60*(x[1]-1/2)**2)
+        return val
+    
+    T = 3 # time interval [0,T]#
+
+else :
+    print('Warning: Wrong string input for test.')
+
+
+# SCHEME/PROBLEM PARAMETERS
+
+# initialize objects
+hh = []
+LinfL2_max = []
+L2H1_max = []
+theta_early_max = []
+theta_res_max = []
+theta_diff_max = []
+theta_time_max = []
+theta_time_val_max = []
+theta_conv_max = []
+theta_delta_max = []
+theta_Omega_max = []
+
+fineness = 4 # fix a number of refinements to the zerost mesh, i.e. fix a spatial mesh size
+steps = 8 # number of different time step sizes used
+
+for Nt in [2,3,4,6,8,16,32,64]: # go through numbers of time steps, [1, 2, 3, 4, 6 ,8 , 16, 32, 64]
+
+    ht = T/Nt
+    hh.append(ht)
+
+    # initialize objects
+    theta_early_sum = 0
+    theta_res_sum = 0
+    theta_diff_sum = 0
+    theta_time_sum = 0
+
+    theta_time_val_sum = 0
+    theta_conv_sum = 0
+    theta_delta_sum = 0
+    theta_Omega_sum = 0
+
+    # load 'exact' error
+    pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_exact error.p'
+    [L2_array,LinfL2,L2H1] = pickle.load(open('pickle_files/'+pickle_name,'rb')) # load data
+    LinfL2_max.append(LinfL2)
+    L2H1_max.append(L2H1)
+
+    for n in range(Nt) :  # go through intervals inbetween time steps
+
+        if n == 0 :
+
+            # load a posteriori error estimates
+            pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error at time step'+str(n)+'.p'
+            [theta_early,theta_res0,theta_diff0,theta_time0,theta_Omega0] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+            pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error at time step'+str(n+1)+'.p'
+            [theta_res1,theta_diff1,theta_time1,theta_time_val1,theta_conv1,theta_Omega1] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+            # initialize objects
+            theta_early_aux = 0
+            theta_res_aux = 0
+            theta_diff_aux = 0
+            theta_time_aux = 0
+            theta_conv_aux = 0
+            theta_delta_aux = 0
+
+            # sum over all elements
+            for i in range(len(theta_early)) : 
+                theta_early_aux += theta_early[i] # this is already the squared term
+                theta_res_aux += theta_res0[i] + theta_res1[i]
+                theta_diff_aux += theta_diff0[i]+theta_diff1[i]
+                theta_time_aux += theta_time0[i]
+                theta_conv_aux += theta_conv1[i]
+
+            # sum over time steps, corresponds to L^2 norm in time
+            theta_early_sum += ht*theta_early_aux 
+            theta_res_sum += ht*theta_res_aux
+            theta_diff_sum += ht*theta_diff_aux
+            theta_time_sum += ht*theta_time_aux
+            theta_delta_sum += ht*theta_delta_aux
+            theta_Omega_sum += ht*theta_Omega0**2
+
+        elif n == Nt-1 :
+
+            # load a posteriori error estimates
+            pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error at time step'+str(n)+'.p'
+            [theta_res0,theta_diff0,theta_time0,theta_time_val0,theta_conv0,theta_Omega0] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+             # initialize objects
+            theta_res_aux = 0
+            theta_diff_aux = 0
+            theta_time_aux = 0
+            theta_time_val_aux = 0
+            theta_conv_aux = 0
+            theta_delta_aux = 0
+
+            # sum over all elements
+            for i in range(len(theta_res1)) : 
+                theta_res_aux += theta_res0[i] # this is already the squared term
+                theta_diff_aux += theta_diff0[i]
+                theta_time_aux += theta_time0[i]
+                theta_time_val_aux += theta_time_val0[i]
+                theta_conv_aux += theta_conv0[i]
+
+            # sum over time steps, corresponds to L^2 norm in time
+            theta_res_sum += ht*theta_res_aux
+            theta_diff_sum += ht*theta_diff_aux
+            theta_time_sum += ht*theta_time_aux
+            theta_time_val_sum += ht*theta_time_val_aux
+            theta_conv_sum += ht*theta_conv_aux
+            theta_Omega_sum += ht*theta_Omega0**2
+
+        else : # n=/=0
+
+            # load a posteriori error estimates
+            pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error at time step'+str(n)+'.p'
+            [theta_res0,theta_diff0,theta_time0,theta_time_val0,theta_conv0,theta_Omega0] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+            pickle_name = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error at time step'+str(n+1)+'.p'
+            [theta_res1,theta_diff1,theta_time1,theta_time_val1,theta_conv1,theta_Omega] = pickle.load(open('pickle_files/'+pickle_name,'rb'))
+
+            # initialize objects
+            theta_res_aux = 0
+            theta_diff_aux = 0
+            theta_time_aux = 0
+            theta_time_val_aux = 0
+            theta_conv_aux = 0
+            theta_delta_aux = 0
+
+            # sum over all elements
+            for i in range(len(theta_res1)) : 
+                theta_res_aux += theta_res0[i]+theta_res1[i] # this is already the squared term
+                theta_diff_aux += theta_diff0[i]+theta_diff1[i]
+                theta_time_aux += theta_time0[i]+theta_time1[i]
+                theta_time_val_aux += theta_time_val0[i]+theta_time_val1[i]
+                theta_conv_aux += theta_conv0[i]+theta_conv1[i]
+
+            # sum over time steps, corresponds to L^2 norm in time
+            theta_res_sum += ht*theta_res_aux
+            theta_diff_sum += ht*theta_diff_aux
+            theta_time_sum += ht*theta_time_aux
+            theta_time_val_sum += ht*theta_time_val_aux
+            theta_conv_sum += ht*theta_conv_aux
+            theta_Omega_sum += ht*theta_Omega0**2
+
+    # save results in order to compute EOC later
+    theta_early_max.append(theta_early_sum)
+    theta_res_max.append(theta_res_sum)
+    theta_diff_max.append(theta_diff_sum)
+    theta_time_max.append(theta_time_sum)
+    theta_time_val_max.append(theta_time_val_sum)
+    theta_conv_max.append(theta_conv_sum)
+    theta_Omega_max.append(theta_Omega_sum)
+
+# print results so far
+print('LinfL2_max         ',np.round(LinfL2_max,5))
+print('L2H1_max         ',np.round(L2H1_max,5))
+print('theta_early_max     ',np.round(theta_early_max,5))
+print('theta_res_max    ',np.round(theta_res_max,5))
+print('theta_diff_max    ',np.round(theta_diff_max,40))
+print('theta_time_max    ',np.round(theta_time_max,6))
+print('theta_time_val_max    ',np.round(theta_time_val_max,5))
+print('theta_conv_max     ',np.round(theta_conv_max,5))
+print('theta_Omega_max        ',np.round(theta_Omega_max,5))
+print('mesh sizes ',hh)
+
+# initialize objects
+eoc_LinfL2 = []
+eoc_L2H1 = []
+eoc_early = []
+eoc_res = []
+eoc_diff = []
+eoc_time = []
+eoc_time_val = []
+eoc_conv = []
+eoc_Omega = []
+
+# go through all numbers of number of step sizes we used
+for i in range(steps-1) :
+
+    # COMPUTE EOCs
+    aux_LinfL2 = (np.log((LinfL2_max[i])/(LinfL2_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+    aux_L2H1 = (np.log((L2H1_max[i])/(L2H1_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+    aux_early = (np.log((theta_early_max[i])/(theta_early_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+    aux_res = (np.log((theta_res_max[i])/(theta_res_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+    aux_diff = (np.log((theta_diff_max[i])/(theta_diff_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+    aux_time = (np.log((theta_time_max[i])/(theta_time_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+    aux_time_val = (np.log((theta_time_val_max[i])/(theta_time_val_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+    aux_conv = (np.log((theta_conv_max[i])/(theta_conv_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+    aux_Omega = (np.log((theta_Omega_max[i])/(theta_Omega_max[i+1])))/(np.log((hh[i])/(hh[i+1])))
+
+    # save the EOCs
+    eoc_LinfL2.append(aux_LinfL2)
+    eoc_L2H1.append(aux_L2H1)
+    eoc_early.append(aux_early)
+    eoc_res.append(aux_res)
+    eoc_diff.append(aux_diff)
+    eoc_time.append(aux_time)
+    eoc_time_val.append(aux_time_val)
+    eoc_conv.append(aux_conv)
+    eoc_Omega.append(aux_Omega)
+
+
+elapsed = time.time() - tic
+print('This took',"%.2f" % round(elapsed/60, 2), 'minutes.')
+
+# print the EOCs
+print('eoc_LinfL2      ',np.round(eoc_LinfL2,5))
+print('eoc_L2H1       ',np.round(eoc_L2H1,5))
+print('eoc_early       ',np.round(eoc_early,5))
+print('eoc_res         ',np.round(eoc_res,5))
+print('eoc_diff        ',np.round(eoc_diff,5))
+print('eoc_time        ',np.round(eoc_time,5))
+print('eoc_time_val        ',np.round(eoc_time_val,5))
+print('eoc_conv        ',np.round(eoc_conv,5))
+print('eoc_Omega       ',np.round(eoc_Omega,5))
+
+
+# plot the EOCs
+hh = np.array(hh)
+fig = plt.figure()
+plt.loglog(hh,hh,'g--',hh,hh**2,'y--',hh,LinfL2_max,'b',hh,L2H1_max,'r',hh,theta_time_max,'m',hh,theta_Omega_max,'orange',hh,theta_conv_max,'brown',hh,theta_early_max,'darkgoldenrod',hh,theta_res_max)
+
+plt.xlabel('hh')
+plt.ylabel('error estimates')
+plot_title = test+'_fineness'+str(fineness)+'_Nt'+str(Nt)+'_'+interpol+'_a posti error estimates'
+plt.title(plot_title)
+plt.legend(['h¹','h²','LinfL2','L2H1','theta_time','theta_Omega','theta_conv','theta_early','theta_res'])
+image_title = str(steps)+'_scaling_behavior_a-posti-error-estimates.png'#example+'_'+title+'_'+method+'_'+string+'.png'
+#plt.savefig(image_title, bbox_inches='tight')
+# plt.show()