import numpy as np
import matplotlib.pyplot as plt
import time

class ReactionDiffusionModel:
    def __init__(self, Nx=128, Ny=128, P=0.06):
        self.Nx = Nx
        self.Ny = Ny
        self.dx = 1.0
        self.dy = 1.0
        
        self.epsilon = 4.0
        self.beta = 0.9
        self.rho = 0.25
        self.T = 1.0
        self.D_par = 0.1
        self.P = P
        
        self.dt = 0.01
        self.t_max = 2000
        self.steps = int(self.t_max / self.dt)
        
        np.random.seed(42)
        self.x = 0.5 + 0.1 * (np.random.rand(self.Nx, self.Ny) - 0.5)
        
        self.time_points = []
        self.mean_history = []
        self.variance_history = []
        
        self.snapshots = {}
        
        if abs(self.P - 0.12) < 1e-5:
            self.snapshot_times = [20, 40, 60, 80, 100, 500, 1000, 2000]
        else:
            self.snapshot_times = [20, 100, 120, 160, 200, 500, 1000, 2000]

    def laplacian(self, field):
        top = np.roll(field, 1, axis=0)
        bottom = np.roll(field, -1, axis=0)
        left = np.roll(field, 1, axis=1)
        right = np.roll(field, -1, axis=1)
        return (top + bottom + left + right - 4 * field) / (self.dx ** 2)

    def nonlinear_flux_divergence(self, M, field):
        M_ip1 = np.roll(M, -1, axis=0)
        M_im1 = np.roll(M, 1, axis=0)
        M_jp1 = np.roll(M, -1, axis=1)
        M_jm1 = np.roll(M, 1, axis=1)
        
        f_ip1 = np.roll(field, -1, axis=0)
        f_im1 = np.roll(field, 1, axis=0)
        f_jp1 = np.roll(field, -1, axis=1)
        f_jm1 = np.roll(field, 1, axis=1)
        
        term_x = ( (M_ip1 + M)/2.0 * (f_ip1 - field) - 
                   (M + M_im1)/2.0 * (field - f_im1) ) / (self.dx**2)
                   
        term_y = ( (M_jp1 + M)/2.0 * (f_jp1 - field) - 
                   (M + M_jm1)/2.0 * (field - f_jm1) ) / (self.dy**2)
                   
        return term_x + term_y

    def reaction_term(self, x):
        term1 = self.P * (1 - x) * (1 - self.beta * x)
        term2 = x * (1 - self.beta * x) * np.exp(- (2 * self.epsilon / self.T) * x)
        term3 = self.D_par * x * (1 - self.beta)
        return term1 - term2 - term3

    def run(self):
        print(f"Початок симуляції для P={self.P}...")
        print(f"  Цільові знімки на t: {self.snapshot_times}")
        start_time = time.time()
        
        for step in range(self.steps + 1):
            t = step * self.dt
            
            if step % 100 == 0:
                mean_val = np.mean(self.x)
                sq_mean = np.mean(self.x**2)
                variance = sq_mean - mean_val**2
                
                self.time_points.append(t)
                self.mean_history.append(mean_val)
                self.variance_history.append(variance)
            
            for st in self.snapshot_times:
                if abs(t - st) < self.dt / 1.5: 
                    if st not in self.snapshots:
                        self.snapshots[st] = self.x.copy()

            M = self.x * (1 - self.x)
            lap_x = self.laplacian(self.x)
            
            factor = 2 * self.epsilon / self.T
            term_nabla_M_nabla_x = self.nonlinear_flux_divergence(M, self.x)
            term_nabla_M_nabla_lapX = self.nonlinear_flux_divergence(M, lap_x)
            
            Diffusion_total = lap_x - factor * term_nabla_M_nabla_x - factor * (self.rho**2) * term_nabla_M_nabla_lapX
            Reaction = self.reaction_term(self.x)
            
            self.x += self.dt * (Reaction + Diffusion_total)
            self.x = np.clip(self.x, 0, 1)

            if step % 50000 == 0 and step > 0:
                print(f"  Прогрес: {t:.0f}/{self.t_max} a.u.")

        print(f"Симуляцію завершено за {time.time() - start_time:.2f} с.")


def main():
    sim1 = ReactionDiffusionModel(P=0.06)
    sim1.run()
    
    sim2 = ReactionDiffusionModel(P=0.12)
    sim2.run()
    
    print("Виведення графіків...")
    
    plt.figure(figsize=(8, 6))
    plt.plot(sim1.time_points, sim1.mean_history, 'k-', label='P = 0.06')
    plt.plot(sim2.time_points, sim2.mean_history, 'r-', label='P = 0.12')
    plt.xlabel('time [a.u.]')
    plt.ylabel('<x>')
    plt.title('Середня концентрація')
    plt.legend()
    plt.grid(True, linestyle=':', alpha=0.6)
    
    plt.figure(figsize=(8, 6))
    plt.plot(sim1.time_points, sim1.variance_history, 'k-', label='P = 0.06')
    plt.plot(sim2.time_points, sim2.variance_history, 'r-', label='P = 0.12')
    plt.xlabel('time [a.u.]')
    plt.ylabel('<($\delta$x)$^2$>')
    plt.title('Дисперсія')
    plt.legend()
    plt.grid(True, linestyle=':', alpha=0.6)
    
    def show_8_snapshots(sim_obj, title):
        times = sorted(sim_obj.snapshots.keys())
        fig, axes = plt.subplots(2, 4, figsize=(16, 8))
        fig.suptitle(title, fontsize=16)
        
        ax_flat = axes.flatten()
        last_im = None
        
        for i, ax in enumerate(ax_flat):
            if i < len(times):
                t = times[i]
                last_im = ax.imshow(sim_obj.snapshots[t], cmap='hot', origin='lower', interpolation='bilinear', vmin=0, vmax=1)
                ax.set_title(f"t = {t}")
                ax.axis('off')
            else:
                ax.axis('off')
        
        if last_im:
            cbar = fig.colorbar(last_im, ax=axes, orientation='vertical', fraction=0.025, pad=0.04)
            cbar.set_label('x(r)', fontsize=12)
            
    show_8_snapshots(sim2, f"Еволюція для P=0.12")
    
    show_8_snapshots(sim1, f"Еволюція для P=0.06")
    
    plt.show()

if __name__ == "__main__":
    main()