% Eric LaForest, March 2010
% laforest AT eecg.utoronto.ca

function [results] = project2()
% Looking for chaos in driven FHN system

    function output = driven_FHN(t, input, A, omega) 
        V = input(1);
        W = input(2);
        X = input(3);
        Y = input(4);
        
        % Input Stimulus
        F = @(t) A * cos(omega * t);

        % Forced Negative Resistance Oscillator
        Ydot = 0.2 * (1 - X^2) * Y - X^3 + F(t);
        Xdot = Y;
        
        % Pre-set FitzHugh-Nagumo System driven by X
        Vdot = V - (V^3 / 3) - W + X;
        Wdot = 0.08 * (V + 0.7 - 0.8 * W);
        
        output = [Vdot; Wdot; Xdot; Ydot];
    end

    function  [T,R] = solve_driven_FHN(time_span, initial_conditions, A, omega)
        this_FHN = @(t,input) driven_FHN(t, input, A, omega);
        [T,R] = ode15s(this_FHN, time_span, initial_conditions);
    end



%
% Wolf's algorithm on analytic models
%

    function [time_span, deltaR1, R0_fiducial, R0_perturbed] = trajectories(time_span, deltaR0, system_function, R0_fiducial, R0_perturbed)
        % Guess based on log2(3) > 1 bit new information
        delta_target = deltaR0 * 3;
        
        time_step = time_span(end) - time_span(1);

        while(true)
            [T1_fiducial, R1_fiducial] = system_function(time_span, R0_fiducial);
            [T1_perturbed, R1_perturbed] = system_function(time_span, R0_perturbed);

            % Keep only the final values
            R1_fiducial = R1_fiducial(end,:);
            R1_perturbed = R1_perturbed(end,:);

            R0_fiducial = R1_fiducial;
            R0_perturbed = R1_perturbed;
            time_span = time_span + time_step;

            deltaR1 = R1_perturbed - R1_fiducial;

            for i=1:length(deltaR1)
                this_delta = deltaR1(i);
                if(abs(this_delta) > delta_target)
                    R0_perturbed(i) = R1_fiducial(i) + ( deltaR0 * sign(this_delta) );
                end
            end

            if(any(abs(deltaR1) > delta_target))
                break;
            end
        end
    end
    
    function [total_time, LLE] = converge(time_span, deltaR0, system_function, R0_fiducial, R0_perturbed)
        zero_template = zeros(size(R0_fiducial));

        LLE = zero_template; 
        LLE_previous = zero_template;
        LLE_accumulator = zero_template;
        
        convergence_epsilon =  0.01;
        convergence_count = 0;
        convergence_target = 10;
 
        total_time = 0;
        
        while(convergence_count < convergence_target)
            [time_span, deltaR1, R0_fiducial, R0_perturbed] = trajectories(time_span, deltaR0, system_function, R0_fiducial, R0_perturbed);

            lambda_local = log2(abs(deltaR1) ./ deltaR0);
            LLE_accumulator = LLE_accumulator + lambda_local;
            
            time_step = time_span(end) - time_span(1);
            total_time = total_time + time_step;
            
            LLE = LLE_accumulator ./ total_time;

            %{
            if(mod(total_time,100) == 0)
                disp(LLE);
            end
            %}
            
            divergence = abs(LLE - LLE_previous);
            LLE_previous = LLE;

            if (all(divergence < convergence_epsilon))
                convergence_count = convergence_count + 1;
            else
                convergence_count = 0;
            end
        end
    end
    
    function [total_time, LLE] = find_LLE_numeric(system_function, initial_conditions)
        % Should be small enough to avoid going through folding regions most of the time,
        % but large enough to have sufficient numerical accuracy.
        % Determined empirically as smallest interval that does not yield negative exponents, 
        % but needs verification.
        time_span = [0 8];

        % From Sprott (2003) Sec. 5.6
        % suitable for double-precision floats
        deltaR0 = 10^-10;
        
        R0_fiducial = initial_conditions;
        R0_perturbed = R0_fiducial + deltaR0;
        
        [total_time, LLE] = converge(time_span, deltaR0, system_function, R0_fiducial, R0_perturbed);
    end

    function [Vdot_LLE, Xdot_LLE] = find_LLE_Wolf_analytic(initial_conditions, A, omega)
        time_series_function = @(time_span, initial_conditions) solve_driven_FHN(time_span, initial_conditions, A, omega);
        [total_time, LLE] = find_LLE_numeric(time_series_function, initial_conditions);
        Vdot_LLE = LLE(1);
        Xdot_LLE = LLE(3);
    end



%
% Rosenstein's algorithm on experimentally-generated data
%

    function [points, count] = time_series_sizes(time_series)
       time_series_size = size(time_series);
       points = time_series_size(1);
       count = time_series_size(2);
    end

    function [time, delay_embedding] = delay_embed(time, time_series)
       lag = 1;
       embedding_dimension = 4;
       
       [time_series_points, time_series_count] = time_series_sizes(time_series);
       
       delay_embedding_points = time_series_points - (lag * embedding_dimension);
       time = time(1:delay_embedding_points);
       
       delay_embedding = zeros(delay_embedding_points, embedding_dimension, time_series_count);
       
       for i = 1:time_series_count
           for j = 1:delay_embedding_points
               for k = 1:embedding_dimension
                   delay_embedding(j,k,i) = time_series(j+k,i);
               end
           end
       end
    end

    function distance = neighbour_distance(A, B)
        distance = norm(A - B);
    end

    function mean_period = time_series_mean_period(time_series, end_time)
        [time_series_points, time_series_count] = time_series_sizes(time_series); 
        NFFT = 2^nextpow2(time_series_points);
        freq_domain = fft(time_series, NFFT) / time_series_points;
        mean_frequency = mean(2*abs(freq_domain(1:NFFT/2,:)));
        mean_period = (1 ./ mean_frequency) / (time_series_points / end_time);
    end

    function [index] = find_nearest_neighbour(time, delay_embeddings, mean_period, reference_point_index)
        % some huge distance never to be seen between two points in a series...
        distance = 1000;
        index = 0;
        %delta_t = 0;
        reference_point = delay_embeddings(reference_point_index);
        reference_time = time(reference_point_index);
        
        end_index = length(time);       
        for i = reference_point_index:end_index
            candidate = delay_embeddings(i);
            candidate_time = time(i);
            time_delta = candidate_time - reference_time;
            if(time_delta < mean_period)
                continue;
            end
            candidate_distance = neighbour_distance(reference_point, candidate);
            if(candidate_distance < distance)
                distance = candidate_distance;
                index = i;
                %delta_t = time_delta;
            end
        end
    end

    function [nearest_neighbours] = find_all_nearest_neighbours(time, delay_embedding, mean_period)
       neighbours_count = length(delay_embedding);
       nearest_neighbours = zeros(neighbours_count,1);
       for i = 1:neighbours_count
           index = find_nearest_neighbour(time, delay_embedding, mean_period, i);
           nearest_neighbours(i) = index;
       end
    end
    
    function [avg_dist] = find_avg_dist(delay_embedding, nearest_neighbour_indices, trajectory_step)
        distance_accumulator = 0;
        count = 0;
        last_index = length(delay_embedding);
        for i = 1:last_index
            reference_index = i + trajectory_step;
            if(reference_index > last_index)
                break;
            end
            reference = delay_embedding(reference_index);
            
            neighbour_index = nearest_neighbour_indices(i) + trajectory_step;
            if(neighbour_index > last_index)
                continue;
            end
            % points near the end have no nearest neighbour in the embedding
            % at a distance greater than the mean period
            if(neighbour_index == 0)
                continue;
            end
            neighbour = delay_embedding(neighbour_index);
            
            dist = log2(neighbour_distance(reference, neighbour));
            distance_accumulator = distance_accumulator + dist;
            count = count + 1;
        end
        avg_dist = distance_accumulator / count;
    end

    function [distances] = find_all_avg_dist(delay_embedding, nearest_neighbour_indices)
        % only the first few steps are valid, after that the lambda tends towards zero
        % as the trajectory folds over the attractor
        step_count = 5;
        for trajectory_step = 1:step_count
            distances(trajectory_step) = find_avg_dist(delay_embedding, nearest_neighbour_indices, trajectory_step);
        end
    end

    function [LLE] = fit_line(distances)
        x = [1:length(distances)];
        LLE = polyfit(x, distances, 1);
        % assume flat line if no fit possible (ie: LLE of zero)
        if(any(isnan(LLE)))
            LLE = [0 0];
        end
    end

    function [Vdot_LLE, Xdot_LLE] = find_LLE_Rosenstein_experimental(initial_conditions, A, omega)
        time_series_function = @(time_span, initial_conditions) solve_driven_FHN(time_span, initial_conditions, A, omega);
        delta_t = 0.1;
        [T,R] = time_series_function(0:delta_t:100, initial_conditions);

        [time, delay_embedding] = delay_embed(T, R);
        mean_period = time_series_mean_period(R, T(end));
    
        Vdot_embedding = delay_embedding(:,:,1);
        Vdot_mean_period = mean_period(1);
    
        Xdot_embedding = delay_embedding(:,:,3);
        Xdot_mean_period = mean_period(3);
    
        Vdot_nearest_neighbour_indices = find_all_nearest_neighbours(time, Vdot_embedding, Vdot_mean_period);
        Xdot_nearest_neighbour_indices = find_all_nearest_neighbours(time, Xdot_embedding, Xdot_mean_period);
    
        Vdot_distances = find_all_avg_dist(Vdot_embedding, Vdot_nearest_neighbour_indices);
        Xdot_distances = find_all_avg_dist(Xdot_embedding, Xdot_nearest_neighbour_indices);
    
        Vdot_line_coefficients = fit_line(Vdot_distances);
        Xdot_line_coefficients = fit_line(Xdot_distances);
        
        Vdot_LLE = Vdot_line_coefficients(1);
        Xdot_LLE = Xdot_line_coefficients(1);
    end


% Plotting

    % Because we deal with both large and small numbers
    format short g

    initial_conditions = [0,0,0,0];
    A = 17;     A_step = 0.1;    A_min = 16;     A_max = 18;
    omega = 4;  o_step = 0.01;    o_min = 2;      o_max = 5;

    A_all = A_min:A_step:A_max;
    omega_all = o_min:o_step:o_max;

    tic;
    points = length(A_all) * length(omega_all)
    % based on ~2sec per point on my machine
    estimated_seconds = (points*2)
    estimated_minutes = estimated_seconds / 60
    estimated_hours = estimated_minutes / 60
    results = zeros(points, 4);
    foo = 1;
    for Ai = A_all
        for omegai = omega_all
            [Vdot_LLE, Xdot_LLE] = find_LLE_Rosenstein_experimental(initial_conditions, Ai, omegai);
            results(foo,:) = [Ai, omegai, Vdot_LLE, Xdot_LLE];
            foo = foo + 1;
            percent_remaining = (points - foo) / points
        end
    end
    toc;
    save('rosenstein_results', 'results');
    
    subplot(4,1,1), plot(results(:,1));
    subplot(4,1,2), plot(results(:,2));
    subplot(4,1,3), plot(results(:,3));
    subplot(4,1,4), plot(results(:,4));
    
end
