def test_solve(self): testtimes = np.array([0.0, 1.0]) testparams = np.array([0.01, 0.03]) s = Solver([0.0, 1.0], [0.0, 1.0], 100, lambda X, Y: [X, Y]) u, v = s.solve(testtimes, testparams) self.assertTupleEqual(u.shape, (2, 100, 100)) self.assertTupleEqual(v.shape, (2, 100, 100))
def test_solve_oscillation(self): def uniform_initial_conditions(X, Y): """ Creates uniform initial conditions with u = 1, v = 0 """ return [np.ones_like(X), np.zeros_like(X)] def oscillation_reactionfunction(u, v, parameters): """ Reaction function with a sinusoidal exact solution. """ w = parameters[0] # angular speed return [w * v, -w * u] testtimes = np.linspace(0, 2 * np.pi, 20) for w in [1.0, 2.0, 3.0]: # angular speed testparams = np.array([0.01, 0.01, w]) s = Solver([0.0, 1.0], [0.0, 1.0], 20, uniform_initial_conditions) s.set_reactionFunction(oscillation_reactionfunction) s.set_timeStepLength(0.0001) u, v = s.solve(testtimes, testparams) for i in range(len(testtimes)): # solution remains uniform self.assertAlmostEqual(np.std(u[i]), 0) self.assertAlmostEqual(np.std(v[i]), 0) # solution is sine & cosine with given angular speed self.assertAlmostEqual(u[i, 10, 10], np.cos(w * testtimes[i]), places=3) self.assertAlmostEqual(v[i, 10, 10], -np.sin(w * testtimes[i]), places=3)
def test_solve_exponential(self): t = np.linspace(0, 5.0, 6) testparams = np.array([1.0, 2.0]) k1, k2 = testparams def reaction_function(u, v, K): return [-K[0] * u, -K[1] * v] def exact(x, y, t): return [1.0 * np.exp(-k1 * t), 2.0 * np.exp(-k2 * t)] def initial_conditions(x, y): return exact(x, y, 0) solver = Solver([0.0, 1.0], [0.0, 1.0], 30, initial_conditions) solver.set_timeStepLength(0.0001) solver.set_reactionFunction(reaction_function) u, v = solver.solve(t, [5.0, 1.0, k1, k2]) T, Y, X = np.meshgrid(t, solver.y, solver.x, indexing='ij') solution = exact(X, Y, T) error_u = np.max(np.abs((u - solution[0]))) error_v = np.max(np.abs((v - solution[1]))) print(error_u) print(error_v) # Check Uniform for i in range(len(t)): self.assertAlmostEqual(np.std(u[i]), 0) self.assertAlmostEqual(np.std(v[i]), 0) self.assertLess(error_u, 1e-4) self.assertLess(error_v, 1e-4)
def test_solve_2dheatequation(self): def sinusoidal_initial_conditions(X, Y): """ Creates initial conditions with a sinusoidal wave in the x and y directions. """ return [ np.sin(4 * np.pi * X) * np.sin(2 * np.pi * Y), np.sin(4 * np.pi * X) * np.sin(2 * np.pi * Y) ] testtimes = np.linspace(0.0, 1.0, 20) testparams = [0.05, 0.01] # diffusion coefficients s = Solver([0.0, 1.0], [0.0, 1.0], 50, sinusoidal_initial_conditions) s.set_timeStepLength(0.0001) u, v = s.solve(testtimes, testparams) for i in range(len(testtimes)): for j in range(50): for k in range(50): self.assertAlmostEqual( u[i, j, k], np.sin(4 * np.pi * s.X[j, k]) * np.sin(2 * np.pi * s.Y[j, k]) * np.exp(-testparams[0] * 20 * np.pi**2 * testtimes[i]), places=2) self.assertAlmostEqual( v[i, j, k], np.sin(4 * np.pi * s.X[j, k]) * np.sin(2 * np.pi * s.Y[j, k]) * np.exp(-testparams[1] * 20 * np.pi**2 * testtimes[i]), places=2)
def test_solve_1ddiffusion(self): mode = 1.0 D1 = 0.05 D2 = 0.0 t = np.linspace(0, 1.0, 11) def exact(x, y, t): return [ np.sin(4 * np.pi * mode * x) * np.exp(-D1 * t * (4 * np.pi * mode)**2), 1 ] def initial_conditions(x, y): return exact(x, y, 0) solver = Solver([0.0, 1.0], [0.0, 1.0], 200, initial_conditions) solver.set_timeStepLength(0.0001) u, v = solver.solve(t, [D1, D2, 0]) T, Y, X = np.meshgrid(t, solver.y, solver.x, indexing='ij') solution = exact(X, Y, T) error_u = np.max(np.abs((u - solution[0]))) error_v = np.max(np.abs((v - solution[1]))) # Check Uniform for i in range(len(t)): for j in range(len(solver.x)): self.assertAlmostEqual(np.std(u[i, :, j]), 0) self.assertAlmostEqual(np.std(v), 0) self.assertAlmostEqual(v[1, 1, 1], 1) self.assertLess(error_u, 3 * 1e-4) self.assertLess(error_v, 1e-4)
# Define Reaction Function def fitzhugh_naguomo(u, v, parameters): e = parameters[0] d = parameters[1] return [(3 * u - u**3 - v) / e, u - d * v] # Initialise Solver # Set x, y bounds and grid size # Set initial condition function to obtain spiralling behavior solver = Solver(xbounds, ybounds, 256, initial_conditions_spiral) # Set reaction terms solver.set_reactionFunction(fitzhugh_naguomo) # Set time step lengths for backward Euler integration scheme solver.set_timeStepLength(0.01) # Define array of times at which to record solution t = np.linspace(0, 30, 100) # Run solve method of solver. # Input time array of times to record solution and list of parameters for the system. # First two parameters are diffusion coefficients, remaining parameters are passed to diffusion function # Return solution arrays for u, v u, v = solver.solve(t, [Du, Dv, eps, delta]) T, Y, X = np.meshgrid(t, solver.y, solver.x, indexing='ij') # Produce animation of function and save to file as 'FitzhughNagumoAnimation' animate(u, 10, "FitzhughNagumoAnimation")
class Inference(Solver): def __init__(self, u_data, v_data, times): ''' Initializes our inference class by receiving the observed grids of u and v values and the times in the time series. We set the x and y boundaries and the initial conditions separately. :param u_data: observed values of u at each point in the grid at each time in the time series :type u_data: n x gridsize x gridsize numpy array :param u_data: observed values of v at each point in the grid at each time in the time series :type u_data: n x gridsize x gridsize numpy array :param times: array of n evenly-spaced times :type times: 1-dimensional numpy array ''' self.u_data = u_data self.v_data = v_data self.times = times self.gridsize = u_data.shape[ 1] #infer the gridsize from this dimension def set_model(self, xBounds, yBounds, initial_conditions_function): ''' Sets up the reaction diffusion model we wish to consider. We determine the x and y boundaries for consideration and provide some initial conditions for our system. :param xBounds: x-range of the problem :type u_data: list of 2 floats :param yBounds: y-range of the problem :type u_data: list of 2 floats :param initial_condition_function: calculates the values of u and v at t=0 at each gridpoint :type initial_condition_function: function that takes two 2d numpy arrays and returns a list of two 2d numpy arrays ''' self.solver = Solver(xBounds, yBounds, self.gridsize, initial_conditions_function) def set_reaction_function(self, function): ''' This is optional. We can choose to provide a nonlinear term to our system. :param function: calculates the value of the reaction terms at the given u and v :type function: function that takes two numpy arrays (containing values of u and v) and a list of parameters and returns a list of two numpy arrays ''' self.solver.set_reactionFunction(function) def _error_func(self): ''' Returns L2 error between output using current proposed set of parameters and the observed data :return tot_error: the total sum of L2 errors at each timestep between the true u,v values and the solved values ''' tot_error = 0 for i in range(len(self.times)): u_diff_mat = self.u_data[i, :, :] - self.u_output[i, :, :] v_diff_mat = self.v_data[i, :, :] - self.v_output[i, :, :] tot_error += np.sum(np.square(u_diff_mat)) + np.sum( np.square(v_diff_mat)) return tot_error def cost_func(self, parametervalues): ''' Takes newly proposed set of parameter values and returns the L2 error between the values of u,v given by the solver with these values and the values of u,v from the input :param parametervalues: the proposed set of parameter values at this point in the optimization :return: the L2 error with these parameters ''' self.u_output, self.v_output = self.solver.solve( self.times, parametervalues) return self._error_func() def fit_params(self, x0): ''' The master function to call. Fits parameters from some initial estimate by minimizing the cost function using the Nelder-Mead method. :param x0: the initial estimate of parameters :return best_fit_parameters: a vector of the fitted parameters ''' res = optimize.minimize(self.cost_func, x0, method='Nelder-Mead') best_fit_parameters = res.x return best_fit_parameters