diff --git a/UB2/MyIntegrator b/UB2/MyIntegrator
index 026b9ee91cf3c24edc6d2e0cd1b732abd64b7a49..50c1a570180b960467c9c23fed2122ee6a925bda 100755
Binary files a/UB2/MyIntegrator and b/UB2/MyIntegrator differ
diff --git a/UB2/QuadratureRule.cpp b/UB2/QuadratureRule.cpp
index 3063ce3d9a36a75a91b0b606ab3125b1dc9e508a..7591a8de2bd0424d5a297974c0128388f7b49198 100644
--- a/UB2/QuadratureRule.cpp
+++ b/UB2/QuadratureRule.cpp
@@ -5,13 +5,16 @@ QuadratureRule::QuadratureRule(const std::vector<double>& w, const std::vector<d
     : weights(w), points(x) {}
 
     // the integrate method takes a callable function f and integrates it over the domain R
-double QuadratureRule::integrate(const std::function<double(double)>& f) {
-    double integral = 0.0;
+double QuadratureRule::integrate(const std::function<double(double)>& f, double a, double b) {
+    double integral = 0;
+    double stepSize = (b-a)/2;
 
     // the integration is a weighted sum of function values at the specified points
     for (size_t i = 0; i < weights.size(); i++){
         integral += weights[i] * f(points[i]);
     }
+    
+    integral *= stepSize;
 
     return integral;
 }
\ No newline at end of file
diff --git a/UB2/QuadratureRule.h b/UB2/QuadratureRule.h
index 134ae1a31e428c3e3ea5ad0d2d0cc6895af68258..f90d8b3aee369b142399ad1f2ee524b9196214a7 100644
--- a/UB2/QuadratureRule.h
+++ b/UB2/QuadratureRule.h
@@ -12,7 +12,7 @@ private:
 
 public:
     QuadratureRule(const std::vector<double>& w, const std::vector<double>& x);
-    double integrate(const std::function<double(double)>& f);
+    double integrate(const std::function<double(double)>& f, double a, double b);
 };
 
 #endif // QUADRATURERULE_H
diff --git a/UB2/main.cpp b/UB2/main.cpp
index 9709cf5797eced7e31b51c738abd8758663e3415..1501401880bf4647a9779f9f1f768f8305fdd267 100644
--- a/UB2/main.cpp
+++ b/UB2/main.cpp
@@ -4,12 +4,14 @@
 int main() {
     std::function<double(double)> func = [](double x) { return x * x; };
 
-    std::vector<double> weights = {1/3, 4/3, 1/3};
+    std::vector<double> weights = {1.0/3.0, 4.0/3.0, 1.0/3.0};
     std::vector<double> points = {-1,0,1};
 
     QuadratureRule quadrature(weights, points);
+    double a = -1;
+    double b = 1;
 
-    double result = quadrature.integrate(func);
+    double result = quadrature.integrate(func,a,b);
 
     std::cout << "Approximate integral of f(x) = x^2 over [-1, 1] is: " << result << std::endl;
 
diff --git a/UB2/polynomial_new.py b/UB2/polynomial_new.py
new file mode 100644
index 0000000000000000000000000000000000000000..310787899e587834d1161308dc92e696dd662f36
--- /dev/null
+++ b/UB2/polynomial_new.py
@@ -0,0 +1,65 @@
+import numpy as np
+
+class polynomial:
+    
+    def __init__(self, coeff):
+        self.coeff = coeff
+        self.deg = len(coeff) - 1
+        self._diff_coeff = np.zeros(self.deg + 1)
+        self._diff_coeff[:-1] = np.arange(1, self.deg + 1) * self.coeff[1:]
+        
+    def __call__(self, x):
+        if isinstance(x, float):
+            x = np.array([x])
+            X = self._evaluate_powers(x)
+            return (self.coeff @ X)[0]
+        else:
+            X = self._evaluate_powers(x)
+            return self.coeff @ X
+    
+    def diff_p(self, x):
+        X = self._evaluate_powers(x)
+        
+        return self._diff_coeff @ X
+        
+    def _evaluate_powers(self, x):
+        X = np.zeros((self.deg + 1, len(x)))
+        for ii in range(self.deg + 1):
+            X[ii, :] = x**ii
+        
+        return X
+
+class polynomial_new(polynomial):
+    
+    def __init__(self, coeff):
+        # This method calls the constructor of the parent class:
+        super(polynomial_new, self).__init__(coeff)
+        # Overwrite array of derivative coefficients, they are replaced by
+        # a matrix of coefficients for all (relevant) derivatives:
+        self._diff_coeff = self._eval_diff_coeff()
+        
+    
+    # Overwrite derivative method:
+    def diff_p(self, x, order=0):
+        
+        if order > self.deg:
+            return np.zeros(x.shape)
+        else:
+            # Evaluate all powers of x at all data sites:
+            X = self._evaluate_powers(x)
+
+            return self._diff_coeff[order, :] @ X
+    
+    # New function to generate derivative coefficients:
+    def _eval_diff_coeff(self):
+        # Compute all derivative coefficients:
+        diff_coeff = np.zeros((self.deg + 1, self.deg + 1))
+        diff_coeff[0, :] = self.coeff
+        for ii in range(1, self.deg + 1):
+            if ii == 1:
+                upper_ind = None
+            else:
+                upper_ind = -ii + 1
+            diff_coeff[ii, :-ii] = np.arange(1, self.deg + 2 - ii) * diff_coeff[ii-1, 1:upper_ind]
+        
+        return diff_coeff
\ No newline at end of file
diff --git a/UB2/quadrature_full.py b/UB2/quadrature_full.py
new file mode 100644
index 0000000000000000000000000000000000000000..dbe04a0cc3c15fd54cf31f270551f393f9d0b117
--- /dev/null
+++ b/UB2/quadrature_full.py
@@ -0,0 +1,60 @@
+import numpy as np
+
+class QuadratureRule:
+
+    def __init__(self, x, w):
+        self.x = x
+        self.w = w
+        self._n = self.x.shape[0]
+
+    def integrate(self, f):
+        # Evaluate f on all grid points:
+        fx = np.array([f(self.x[i]) for i in range(self._n)])
+
+        return np.dot(self.w, fx)
+    
+
+
+class LinearTransformed(QuadratureRule):
+
+    def integrate(self, f, T, dT):
+        # Evaluate f on transformed grid points:
+        fx = np.array([f(T(self.x[i])) for i in range(self._n)])
+        # Transform weights:
+        wT = np.array([self.w[i] * dT for i in range(self._n)])
+
+        return np.dot(wT, fx)
+
+
+class CompositeQuadrature:
+
+    def __init__(self, a, b, n, x, w):
+        """ Composite quadrature on interval [a, b] using n sub-
+         intervals, and a quadrature rule on reference interval [-1, 1].
+        
+        Parameters:
+        a, b:   lower and upper bounds of integration domain
+        n:      number of sub-intervals
+        x:      grid points for reference quadrature rule
+        w:      weights for reference quadrature rule
+        """
+        self.a = a
+        self.b = b
+        self.n = n
+        # Create sub-division of [a, b]:
+        self.h = (b - a) / n
+        self.y = np.array([a + i * self.h for i in range(self.n+1)])
+        # Mid-points of the sub-intervals:
+        self.c = 0.5 * (self.y[:-1] + self.y[1:])
+        # Create quadrature rule object:
+        self.QR = LinearTransformed(x, w)
+
+    def integrate(self, f):
+        I = 0.0
+        # Iterate over sub-intervals:
+        for i in range(self.n):
+            # Linear transformation:
+            T = lambda x: 0.5 * self.h * x + self.c[i]
+            I += self.QR.integrate(f, T, 0.5*self.h)
+        
+        return I
\ No newline at end of file
diff --git a/UB2/shell_script.sh b/UB2/shell_script.sh
new file mode 100644
index 0000000000000000000000000000000000000000..1c3085330c44f04ad39e51260175ce65e4d72ce1
--- /dev/null
+++ b/UB2/shell_script.sh
@@ -0,0 +1,2 @@
+g++ main.cpp QuadratureRule.cpp -o MyIntegrator
+./MyIntegrator
diff --git a/UB2/test_quad.py b/UB2/test_quad.py
new file mode 100644
index 0000000000000000000000000000000000000000..280f47f37c4af467d9151397b3a8b96fc56d38cf
--- /dev/null
+++ b/UB2/test_quad.py
@@ -0,0 +1,38 @@
+import numpy as np
+import quadrature
+import polynomial_new as poly
+
+""" Settings: """
+# Create an instance of the quadrature rule:
+x = np.array([-1.0, 0.0, 1.0])
+w = np.array([1.0/3, 4.0/3, 1.0/3])
+S = quadrature.QuadratureRule(x, w)
+
+# Boundary values of interval:
+a = -1.0
+b = 1.0
+# Number of test:
+ntest = 10
+
+""" Function to integrate polynomial: """
+def int_poly(P, a, b):
+    # Coefficients of the primitive function:
+    pc = P.coeff / np.arange(1, P.deg+2, dtype=float)
+    # Evaluate powers at both boundary points:
+    bx = np.array([b**i for i in range(1, P.deg+2)])
+    ax = np.array([a**i for i in range(1, P.deg+2)])
+
+    return (pc @ bx - pc @ ax)
+
+""" Main Test: """
+for ii in range(ntest):
+    # Generate random third order polynomial:
+    #c = np.array([1.0, 0.0, 0.0, 1.0])
+    c = np.random.randn(4)
+    print("Test %d: coeffs: "%ii, c)
+    P = poly.polynomial(c)
+    # Compute integral:
+    I = int_poly(P, a, b)
+    # Approximation with Simpson rule:
+    ISimp = S.integrate(P)
+    print("Test %d: diff = "%ii, np.abs(I - ISimp))
\ No newline at end of file
diff --git a/UB2/test_quad_composite.py b/UB2/test_quad_composite.py
new file mode 100644
index 0000000000000000000000000000000000000000..e56026ee37c19772f4a6baa44ec56cdfb8f11b1f
--- /dev/null
+++ b/UB2/test_quad_composite.py
@@ -0,0 +1,46 @@
+import numpy as np
+import matplotlib.pyplot as plt
+
+import quadrature_full
+
+""" Settings: """
+# Function to integrate:
+f = lambda x: np.arctan(x)
+# Anti-derivative:
+F = lambda x: x * np.arctan(x) - 0.5 * np.log(1 + x**2)
+
+# Interval:
+a = -1.0
+b = 3.0
+
+# Array of grid sizes:
+n_array = np.array([5, 10, 50, 100, 500, 1000, 5000, 10000])
+# Reference quadrature:
+x = np.array([-1.0, 0.0, 1.0])
+w = np.array([1.0/3, 4.0/3, 1.0/3])
+
+""" Demonstration: """
+# Compute reference:
+Iref = F(b) - F(a)
+# Convert grid sizes to step sizes:
+h_array = (b - a) / n_array
+# Apply composite quadrature:
+E = np.zeros(h_array.shape[0])
+for i in range(h_array.shape[0]):
+    n = n_array[i]
+    QC = quadrature_full.CompositeQuadrature(a, b, n, x, w)
+    I = QC.integrate(f)
+    E[i] = np.abs(I - Iref)
+
+""" Figure: """
+plt.figure(figsize=(6, 4))
+plt.plot(h_array, E, "o--", label="Error Simpson")
+plt.plot(h_array, h_array**4, "ro--", label=r"$h^4$")
+plt.xscale("log")
+plt.yscale("log")
+plt.xlabel(r"$h$")
+plt.ylim([1e-16, 1e-2])
+plt.legend(loc=2)
+plt.show()
+
+print(I, Iref)
\ No newline at end of file
diff --git a/UB2/test_quad_transformed.py b/UB2/test_quad_transformed.py
new file mode 100644
index 0000000000000000000000000000000000000000..c03aadb91203c5915f5b5594b969432813a01b27
--- /dev/null
+++ b/UB2/test_quad_transformed.py
@@ -0,0 +1,42 @@
+import numpy as np
+import quadrature
+import polynomial_new as poly
+
+""" Settings: """
+# Parameters of the quadrature rule on reference interval:
+x = np.array([-1.0, 0.0, 1.0])
+w = np.array([1.0/3, 4.0/3, 1.0/3])
+
+# Define linear transformation of [-1, 1] to [1, 5]:
+a = 1.0
+b = 5.0
+T = lambda x: 2 * x + 3
+dT = 2.0
+# Number of tests:
+ntest = 10
+
+""" Function to integrate polynomial: """
+def int_poly(P, a, b):
+    # Coefficients of the primitive function:
+    pc = P.coeff / np.arange(1, P.deg+2, dtype=float)
+    # Evaluate powers at both boundary points:
+    bx = np.array([b**i for i in range(1, P.deg+2)])
+    ax = np.array([a**i for i in range(1, P.deg+2)])
+
+    return (pc @ bx - pc @ ax)
+
+
+""" Main Test: """
+S = quadrature.LinearTransformed(x, w)
+
+for ii in range(ntest):
+    # Generate random third order polynomial:
+    #c = np.array([1.0, 0.0, 0.0, 1.0])
+    c = np.random.randn(4)
+    print("Test %d: coeffs: "%ii, c)
+    P = poly.polynomial(c)
+    # Compute integral:
+    I = int_poly(P, a, b)
+    # Approximation with Simpson rule:
+    ISimp = S.integrate(P, T, dT)
+    print("Test %d: diff = "%ii, np.abs(I - ISimp))
\ No newline at end of file
diff --git a/UB3/__pycache__/run_rw2d.cpython-310.pyc b/UB3/__pycache__/run_rw2d.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..eb602d2a62b12a374abbab7324c6114c71e11aed
Binary files /dev/null and b/UB3/__pycache__/run_rw2d.cpython-310.pyc differ
diff --git a/UB3/random_walk.py b/UB3/random_walk.py
new file mode 100644
index 0000000000000000000000000000000000000000..3fd6fbb4963e74929f84de07bde458548508441b
--- /dev/null
+++ b/UB3/random_walk.py
@@ -0,0 +1,58 @@
+import numpy as np
+import random
+import matplotlib.pyplot as plt
+
+from run_rw2d import _update_figure
+
+class RandomWalk2D:
+    def __init__(self, dx: float, N: int, x0: np.ndarray, dt: float):
+        """
+        :param x0: vector of initial positions
+        :param N: population size
+        """
+        assert x0.shape == (N,2), "x0 must have the shape (N,2)"
+        self.positions = x0
+        self.time = [0]
+        self.dt = dt
+        self.N = N
+        self.dx = dx
+    
+    def simulate(self, K: int = 1):
+        """
+        param K: number of discrete steps
+        """
+
+        choices = [(-1, 0), (1, 0), (0, 1), (0, -1)]
+
+        for _ in range(K):
+            self.time.append(self.time[-1]+ self.dt)
+            directions = np.array([random.choice(choices) for _ in range(self.N)])* self.dx
+            self.positions += directions
+
+
+### testing ###
+N=100
+K=20
+dt=4e-2
+dx=4e-1   
+RW = RandomWalk2D(dx=dx, N=N, x0 = np.zeros((N,2)), dt=dt)
+
+### visualisation ###
+
+# Axes limits for figure:
+xmin = -8.0
+xmax = 8.0
+X = RW.positions
+fig = plt.figure(figsize=(6, 4))
+fig = _update_figure(fig, X, 0.0, xmin, xmax)
+for _ in range(1, K+1):
+    # Update positions:
+    RW.simulate()
+    X = RW.positions
+    print(f'{X=}')
+    t = RW.time[-1]
+    # Update figure:
+    fig = _update_figure(fig, X, t, xmin, xmax)
+    plt.pause(0.25)
+
+plt.show()
diff --git a/UB3/run_rw2d.py b/UB3/run_rw2d.py
new file mode 100644
index 0000000000000000000000000000000000000000..24ac93b7b5bea6b0187e3f9f3e3aef2fcdd8d441
--- /dev/null
+++ b/UB3/run_rw2d.py
@@ -0,0 +1,48 @@
+import numpy as np
+import matplotlib.pyplot as plt
+
+# import random_walk_2d as rw
+
+""" Settings: """
+# Mesh sizes:
+dx = 0.4
+dt = 0.04
+# Population size:
+N = 1000
+# Initial conditions:
+x0 = np.zeros((2, N))
+t0 = 0.0
+# Number of discrete time steps:
+K = 20
+# Axes limits for figure:
+xmin = -8.0
+xmax = 8.0
+
+""" Function to update the figure: """
+def _update_figure(fig, X, t, xmin, xmax):
+    fig.clear()
+    plt.scatter(X[:, 0], X[:, 1])
+    plt.title("Population at time t = %.3f"%t)
+    plt.xlim([xmin, xmax])
+    plt.ylim([xmin, xmax])
+
+    return fig
+
+# """ Instantiate the class: """
+# RW = rw.RandomWalk2D(dx, dt, N, x0)
+
+# """ Simulation: """
+# X = RW.positions()
+# fig = plt.figure(figsize=(6, 4))
+# fig = _update_figure(fig, X, 0.0, xmin, xmax)
+
+# for ii in range(1, K+1):
+#     # Update positions:
+#     RW.simulate()
+#     X = RW.positions()
+#     t = RW.time()
+#     # Update figure:
+#     fig = _update_figure(fig, X, t, xmin, xmax)
+#     plt.pause(0.25)
+
+# plt.show()
diff --git a/UB4/UB4.py b/UB4/UB4.py
new file mode 100644
index 0000000000000000000000000000000000000000..402e74d170d879f7d2f022f3ee6d13c364ce385a
--- /dev/null
+++ b/UB4/UB4.py
@@ -0,0 +1,123 @@
+import numpy as np
+import matplotlib.pyplot  as plt
+
+# Ex1: Numerical integrators 
+
+def implicit_euler(f, y0: np.ndarray, t: float, dt: float) -> np.ndarray:
+    """
+    A-stable
+    :param f: function to be integrated
+    :param y0: initial value
+    :param t: time interval
+    :param dt: time step
+    """
+    no_of_steps = int(t // dt)
+    y = np.zeros((no_of_steps, *y0.shape))
+    y[0] = y0
+    
+    for i in range(1, no_of_steps):
+        y[i] = y[i-1] + f(y[i-1]) * dt
+        y[i] = y[i-1] + f(y[i]) * dt
+
+    return y
+
+def explicit_euler(f, y0: np.ndarray, t: float, dt: float) -> np.ndarray:
+    """
+    :param f: function to be integrated
+    :param y0: initial value
+    :param t: time interval
+    :param dt: time step
+    """
+    no_of_steps = int(t // dt)
+    y = np.zeros((no_of_steps, *y0.shape))
+    y[0] = y0
+
+    for i in range(1, no_of_steps):
+        y[i] = y[i-1] + f(y[i-1]) * dt
+    
+    return y
+
+def implicit_midpoint(f, y0: np.ndarray, t: float, dt: float) -> np.ndarray:
+    """
+    Not L-stable, doesn't decay properly - oscillation
+    :param f: function to be integrated
+    :param y0: initial value
+    :param t: time interval
+    :param dt: time step
+    """
+    no_of_steps = int(t // dt)
+    y = np.zeros((no_of_steps, *y0.shape))
+    y[0] = y0
+    
+    for i in range(1, no_of_steps):
+        y[i] = y[i-1] + dt * f(y[i-1] + dt/2 * f(y[i-1]))
+        y[i] = y[i-1] + dt * f(1/2 *  (y[i-1] + y[i]))
+    
+    return y
+
+# Ex2: dy/dt = lambda * y, y(0) = 1, lambda = -1
+y0 = np.array([1])
+lamda = -1
+t = 100
+dt = 1e-2
+f = lambda y: lamda * y
+y = [implicit_euler(f, y0, t, dt), explicit_euler(f, y0, t, dt), implicit_midpoint(f, y0, t ,dt)]
+integrators = ['implicit euler', 'explicit euler', 'implicit midpoint']
+
+# for i in range(3):
+#     plt.figure()
+#     plt.plot(y[i])
+#     plt.title(integrators[i])
+#     plt.show()
+
+# Ex3: 
+def f(y: np.ndarray, k: float = 1) -> np.ndarray:
+    assert y.shape[0] == 3, 'y has the wrong dimension. It should be 3'
+
+    A = np.array([[-1, 1, 0],
+                    [1, -1-k, k],
+                    [0, k, -k]])
+    
+    return np.dot(A, y)
+
+dt = [1, 1e-1, 1e-2, 1e-3]
+t = [10,100]
+y0 = np.array([1,0,0])
+
+# short time
+fig = plt.figure()
+for dt_i in dt:
+    y = implicit_euler(f, y0, t[0], dt_i)
+    plt.plot(y[:,0], '--',label = 'State 1')
+    plt.plot(y[:,1], 'x', label = 'State 2')
+    plt.plot(y[:,2], '>', label = 'State 3')
+    plt.legend()
+    plt.title(f'{integrators[0]}, dt = {dt_i}')
+    plt.show()
+
+
+
+
+
+
+fig = plt.figure()
+for dt_i in dt:
+    y = explicit_euler(f, y0, t[0], dt_i)
+    plt.plot(y[:,0], '--', label = 'State 1')
+    plt.plot(y[:,1], 'x', label = 'State 2')
+    plt.plot(y[:,2], '>', label = 'State 3')
+    plt.legend()
+    plt.title(f'{integrators[1]}, dt = {dt_i}')
+    plt.show()
+
+fig = plt.figure()
+for dt_i in dt:
+    y = implicit_midpoint(f, y0, t[0], dt_i)
+    plt.plot(y[:,0], '--', label = 'State 1')
+    plt.plot(y[:,1], 'x', label = 'State 2')
+    plt.plot(y[:,2], '>', label = 'State 3')
+    plt.legend()
+    plt.title(f'{integrators[2]}, dt = {dt_i}')
+    plt.show()
+
+
diff --git a/shell_script.sh b/shell_script.sh
deleted file mode 100644
index 6d1b172c5a5264eadcd945b2b999bc3b9984156d..0000000000000000000000000000000000000000
--- a/shell_script.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-g++ main.cpp QuadratureRule.cpp -o MyApp
-./MyIntegrator