/*
 *       File name:  cnrun/model-cycle.cc
 *         Project:  cnrun
 *          Author:  Andrei Zavada <johnhommer@gmail.com>
 * Initial version:  2008-08-02
 *
 *         Purpose:  CModel top cycle
 *
 *         License:  GPL-2+
 */

#if HAVE_CONFIG_H && !defined(VERSION)
#  include "config.h"
#endif

#include <ctime>
#include <cstdlib>
#include <iostream>

#include "libstilton/lang.hh"
#include "integrate-rk65.hh"
#include "model.hh"


using namespace std;


/*--------------------------------------------------------------------------
  Implementation of a 6-5 Runge-Kutta method with adaptive time step
  mostly taken from the book "The numerical analysis of ordinary differential
  equations - Runge-Kutta and general linear methods" by J.C. Butcher, Wiley,
  Chichester, 1987 and a free adaptation to a 6 order Runge Kutta method
  of an ODE system with additive white noise
--------------------------------------------------------------------------*/

inline namespace {

double __Butchers_a[9][8] = {
        { },
        { 1./9 },
        { .5/9,        .5/9 },
        { 0.416666666666667e-1,        0., 0.125 },
        { 1./6, 0., -0.5, 2./3 },
        { 0.1875e+1, 0., -0.7875e+1, 0.7e+1, -0.5 },
        { -0.4227272727272727e+1, 0., 0.176995738636364e+2, -0.142883522727273e+2, 0.522017045454545, 0.104403409090909e+1 },
        { 0.840622673179752e+1, 0., -0.337303717185049e+2, 0.271460231129622e+2, 0.342046929709216, -0.184653767923258e+1, 0.577349465373733 },
        { 0.128104575163399, 0., 0., -0.108433734939759, 0.669375, -0.146666666666667, 0.284444444444444, 0.173176381998583 },
};


double __Butchers_b[9] = {
        0.567119155354449e-1,
        0.,
        0.,
        0.210909572355356,
        0.141490384615385,
        0.202051282051282,
        0.253186813186813,
        0.843679809736684e-1,
        0.512820512820513e-1
};
} // inline namespace



void
cnrun::CIntegrateRK65::
prepare()
{
        for ( unsigned short i = 0; i < 9; ++i ) {
                Y[i].resize( model->_var_cnt);
                F[i].resize( model->_var_cnt);
        }
        y5.resize( model->_var_cnt);

        if ( model->n_standalone_units() > 0 )
                if ( _dt_max > model->_discrete_dt ) {
                        _dt_max = model->_discrete_dt;
                        model->vp( 1, "CIntegrateRK65: Set dt_max to model->discrete_dt: %g\n", _dt_max);
                }
}


void
__attribute__ ((hot))
cnrun::CIntegrateRK65::
cycle()
{
      // omp stuff found inapplicable due to considerable overhead in sys time
      // (thread creation)
        unsigned int i, j, k;

        double  aF;

      // calculate iterative terms rk65_Y[__i] and rk65_F[__i] (to sixth order)
        for ( i = 0; i < 9; ++i ) {
//#pragma omp parallel for schedule(static,model->_var_cnt/2+1) firstprivate(aF,j,i)
                for ( k = 0; k < model->_var_cnt; ++k ) {
                        aF = 0.0;
                        for ( j = 0; j < i; ++j )
                                aF += __Butchers_a[i][j] * F[j][k];
                        Y[i][k] = model->V[k] + dt * aF;
                }
              // see to this vector's dt
                F[i][0] = 1.;

//#pragma omp consider...
                for ( auto& N : model->hosted_neurons )
                        N -> derivative( Y[i], F[i]);
                for ( auto& S : model->hosted_synapses )
                        S -> derivative( Y[i], F[i]);
        }

      // sum up Y[i] and F[i] to build 5th order scheme -> y5
//#pragma omp parallel for private(aF,j)
        for ( k = 0; k < model->_var_cnt; ++k ) {
                aF = 0.0;
                for ( j = 0; j < 8; ++j )
                        aF += __Butchers_a[8][j] * F[j][k];
                y5[k] = model->V[k] + dt * aF;
        }

      // sum up Y[i] and F[i] to build 6th order scheme -> W
//#pragma omp parallel for schedule(static,model->_var_cnt/2+1) private(aF,j)
        for ( k = 0; k < model->_var_cnt; ++k ) {
                aF = 0.0;
                for ( j = 0; j < 9; ++j )
                        aF += __Butchers_b[j] * F[j][k];
                model->W[k] = model->V[k] + dt * aF;
        }

      // kinkiness in synapses causes dt to rocket
        double  dtx = min( _dt_max, dt * _dt_cap);

      // determine minimal necessary new dt to get error < eps based on the
      // difference between results in y5 and W
        double try_eps, delta, try_dt;
      // exclude time (at index 0)
//#pragma omp parallel for private(try_eps,delta,try_dtj)
        for ( k = 1; k < model->_var_cnt; ++k ) {
                try_eps = max( _eps_abs, min (_eps, abs(_eps_rel * model->W[k])));
                delta = abs( model->W[k] - y5[k]);
                if ( delta > DBL_EPSILON * y5[k] ) {
                        try_dt = exp( (log(try_eps) - log(delta)) / 6) * dt;
                        if ( try_dt < dtx )
                                dtx = try_dt;
                }
        }
      // make sure we don't grind to a halt
        if ( dtx < _dt_min )
                dtx = _dt_min;

      // set the new step
        dt = dtx;
}








// -------------- CModel::advance and dependents

unsigned int
cnrun::CModel::
advance( const double dist, double * const cpu_time_used_p)
{
        if ( units.size() == 0 ) {
                vp( 1, "Model is empty\n");
                return 0;
        }
        if ( is_ready )
                prepare_advance();

        bool    have_hosted_units = !!n_hosted_units(),
                have_standalone_units = !!n_standalone_units(),
                have_ddtbound_units = !!n_ddtbound_units();

        if ( have_hosted_units && !have_standalone_units && !have_ddtbound_units )
                return _do_advance_on_pure_hosted( dist, cpu_time_used_p);
        if ( !have_hosted_units && have_standalone_units && !have_ddtbound_units )
                return _do_advance_on_pure_standalone( dist, cpu_time_used_p);
        if ( !have_hosted_units && !have_standalone_units && have_ddtbound_units )
                return _do_advance_on_pure_ddtbound( dist, cpu_time_used_p);

        unsigned int retval = _do_advance_on_mixed( dist, cpu_time_used_p);
        return retval;
}

void
__attribute__ ((hot))
cnrun::CModel::
_setup_schedulers()
{
        regular_periods.clear();
        regular_periods_last_checked.clear();
        if ( units_with_periodic_sources.size() ) { // determine period(s) at which to wake up reader update loop
                for ( auto& U : units_with_periodic_sources )
                        for ( auto& S : U -> _sources )
                                regular_periods.push_back(
                                        (reinterpret_cast<CSourcePeriodic*>(S.source)) -> period());
                regular_periods.unique();
                regular_periods.sort();
                regular_periods_last_checked.resize( regular_periods.size());
        }

        if ( regular_periods.size() > 0 )
                vp( 2, "%zd timepoint(s) in scheduler_update_periods: %s\n\n",
                    regular_periods.size(),
                    stilton::str::join( regular_periods).c_str());

      // ensure all schedulers are effective at the beginning, too
        for ( auto& U : units_with_periodic_sources )
                U->apprise_from_sources();
}


void
cnrun::CModel::
prepare_advance()
{
        if ( options.log_dt && !_dt_logger )
                _dt_logger = new ofstream( string(name + ".dt").c_str());
        if ( options.log_spikers && !_spike_logger )
                _spike_logger = new ofstream( string(name + ".spikes").c_str());

        _setup_schedulers();

        if ( !n_hosted_units() )
                _integrator->dt = _discrete_dt;

        is_ready = true;

        vp( 5, stderr, "Model prepared\n");
}



// comment concerning for_all_conscious_neurons loop:
// these have no next_time_E or suchlike, have `fixate' implicit herein; also,
// conscious neurons fire irrespective of whatever happens elsewhere in the model, and
// they, logically, have no inputs

#define _DO_ADVANCE_COMMON_INLOOP_BEGIN \
        make_units_with_continuous_sources_apprise_from_sources();      \
        {                                                               \
                auto I = regular_periods.begin();                       \
                auto Ic = regular_periods_last_checked.begin();         \
                for ( ; I != regular_periods.end(); ++I, ++Ic )         \
                        if ( unlikely (model_time() >= *I * (*Ic + 1)) ) { \
                                (*Ic)++;                                \
                                make_units_with_periodic_sources_apprise_from_sources(); \
                        }                                               \
        }                                                               \
        make_conscious_neurons_possibly_fire();                         \
                                                                        \
        for ( auto& Y : multiplexing_synapses ) \
                if ( Y->_source )                                        \
                        Y->update_queue();


#define _DO_ADVANCE_COMMON_INLOOP_MID \
        if ( have_listeners ) {                                         \
                if ( have_discrete_listen_dt ) {                        \
                        if ( model_time() - last_made_listen >= options.listen_dt ) { \
                                make_listening_units_tell();            \
                                last_made_listen += options.listen_dt;  \
                        }                                               \
                } else                                                  \
                        make_listening_units_tell();                    \
        }                                                               \
        if ( unlikely (options.log_dt) )                                \
                (*_dt_logger) << model_time() << "\t" << dt() << endl;  \
                                                                        \
        for ( auto& N : spikelogging_neurons ) {                        \
                N -> do_detect_spike_or_whatever();                     \
                if ( !is_diskless &&                                    \
                     N->n_spikes_in_last_dt() &&                        \
                     options.log_spikers ) {                            \
                        (*_spike_logger) << model_time() << "\t";       \
                        if ( options.log_spikers_use_serial_id )        \
                                (*_spike_logger) << N->_serial_id << endl; \
                        else                                            \
                                (*_spike_logger) << N->_label << endl;  \
                }                                                       \
        }


#define _DO_ADVANCE_COMMON_INLOOP_END \
        ++_cycle;                                                       \
        ++steps;                                                        \
        if ( options.verbosely != 0 ) {                                 \
                if ( unlikely (((double)(clock() - cpu_time_lastchecked)) / CLOCKS_PER_SEC > 2) ) { \
                        cpu_time_lastchecked = clock();                 \
                        if ( options.display_progress_percent && !options.display_progress_time ) \
                                fprintf( stderr, "\r\033[%dC%4.1f%%\r", \
                                         (options.verbosely < 0) ? -(options.verbosely+1)*8 : 0, \
                                         100 - (model_time() - time_ending) / (time_started - time_ending) * 100); \
                        else if ( options.display_progress_time && !options.display_progress_percent ) \
                                fprintf( stderr, "\r\033[%dC%'6.0fms\r", \
                                         (options.verbosely < 0) ? -(options.verbosely+1)*16 : 0, \
                                         model_time());                 \
                        else if ( options.display_progress_percent && options.display_progress_time ) \
                                fprintf( stderr, "\r\033[%dC%'6.0fms %4.1f%%\r", \
                                         (options.verbosely < 0) ? -(options.verbosely+1)*24 : 0, \
                                         model_time(),                  \
                                         100 - (model_time() - time_ending) / (time_started - time_ending) * 100); \
                        fflush( stderr);                                \
                }                                                       \
        }


#define _DO_ADVANCE_COMMON_EPILOG \
        make_spikeloggers_sync_history();                               \
        cpu_time_ended = clock();                                        \
        double cpu_time_taken_seconds = ((double) (cpu_time_ended - cpu_time_started)) / CLOCKS_PER_SEC; \
        if ( cpu_time_used_p )                                                \
                *cpu_time_used_p = cpu_time_taken_seconds;                \
        if ( options.verbosely > 0 || options.verbosely <= -1 ) {                        \
                fprintf( stderr, "\r\033[K");                                \
                fflush( stderr);                                        \
        }                                                                \
        vp( 0, "@%.1fmsec (+%.1f in %lu cycles in %.2f sec CPU time:"   \
            " avg %.3g \302\265s/cyc, ratio to CPU time %.2g)\n\n",     \
            model_time(), dist, steps, cpu_time_taken_seconds,          \
            model_time()/_cycle * 1e3, model_time() / cpu_time_taken_seconds / 1e3);





unsigned int
__attribute__ ((hot))
cnrun::CModel::
_do_advance_on_pure_hosted( const double dist, double * const cpu_time_used_p)
{
        bool    have_listeners = (listening_units.size() > 0),
                have_discrete_listen_dt = (options.listen_dt > 0.);

        clock_t cpu_time_started = clock(),
                cpu_time_ended,
                cpu_time_lastchecked = cpu_time_started;

        double  time_started = model_time(),
                time_ending = time_started + dist,
                last_made_listen = time_started;

        unsigned long steps = 0;
        do {
                _DO_ADVANCE_COMMON_INLOOP_BEGIN

                _integrator->cycle();

                _DO_ADVANCE_COMMON_INLOOP_MID

              // fixate
                _integrator->fixate();

                _DO_ADVANCE_COMMON_INLOOP_END

              // model_time is advanced implicitly in _integrator->cycle()
        } while ( model_time() < time_ending );

        _DO_ADVANCE_COMMON_EPILOG

        return steps;
}



unsigned int
__attribute__ ((hot))
cnrun::CModel::
_do_advance_on_pure_standalone( const double dist, double * const cpu_time_used_p)
{
        bool    have_listeners = !!listening_units.size(),
                have_discrete_listen_dt = (options.listen_dt > 0.);

        clock_t cpu_time_started = clock(),
                cpu_time_ended,
                cpu_time_lastchecked = cpu_time_started;

        double  time_started = model_time(),
                time_ending = time_started + dist,
                last_made_listen = time_started;

        unsigned long steps = 0;
        do {
                _DO_ADVANCE_COMMON_INLOOP_BEGIN

              // service simple units w/out any vars on the integration vector V
                for ( auto& N : standalone_neurons )
                        if ( !N->is_conscious() )
                                N -> preadvance();
                for ( auto& Y : standalone_synapses )
                        Y -> preadvance();

              // even in the case of n_hosted_{neurons,units} == 0, we would need _integrator->cycle() to advance V[0],
              // which is our model_time(); which is kind of expensive, so here's a shortcut
                V[0] += _discrete_dt;
                // _discrete_time += _discrete_dt;  // not necessary

                _DO_ADVANCE_COMMON_INLOOP_MID

              // fixate
                for ( auto& N : standalone_neurons )
                        if ( !N->is_conscious() )
                                N -> fixate();
                for ( auto& Y : standalone_synapses )
                        Y -> fixate();

                _DO_ADVANCE_COMMON_INLOOP_END

        } while ( model_time() < time_ending );

        _DO_ADVANCE_COMMON_EPILOG

        return steps;
}







unsigned int
__attribute__ ((hot))
cnrun::CModel::
_do_advance_on_pure_ddtbound( const double dist, double * const cpu_time_used_p)
{
        bool    have_listeners = (listening_units.size() > 0),
                have_discrete_listen_dt = (options.listen_dt > 0.);

        clock_t cpu_time_started = clock(),
                cpu_time_ended,
                cpu_time_lastchecked = cpu_time_started;

        double  time_started = model_time(),
                time_ending = time_started + dist,
                last_made_listen = time_started;

        unsigned long steps = 0;
        do {
                _DO_ADVANCE_COMMON_INLOOP_BEGIN

              // lastly, service units only serviceable at discrete dt
                for ( auto& N : ddtbound_neurons )
                        if ( !N->is_conscious() )
                                N -> preadvance();
                for ( auto& Y : ddtbound_synapses )
                        Y -> preadvance();

                V[0] += _discrete_dt;
                _discrete_time += _discrete_dt;

                _DO_ADVANCE_COMMON_INLOOP_MID

              // fixate
                for ( auto& N : ddtbound_neurons )
                        if ( !N->is_conscious() )
                                N -> fixate();
                for ( auto& Y : ddtbound_synapses )
                        Y -> fixate();

                _DO_ADVANCE_COMMON_INLOOP_END

        } while ( model_time() < time_ending );

        _DO_ADVANCE_COMMON_EPILOG

        return steps;
}





unsigned int
__attribute__ ((hot))
cnrun::CModel::
_do_advance_on_mixed( const double dist, double * const cpu_time_used_p)
{
        bool    have_hosted_units = !!n_hosted_units(),
                have_listeners = !!listening_units.size(),
                have_discrete_listen_dt = (options.listen_dt > 0.),
                need_fixate_ddtbound_units;

        clock_t cpu_time_started = clock(),
                cpu_time_ended,
                cpu_time_lastchecked = cpu_time_started;

        double  time_started = model_time(),
                time_ending = time_started + dist,
                last_made_listen = time_started;

        unsigned long steps = 0;
        do {
                _DO_ADVANCE_COMMON_INLOOP_BEGIN

                _integrator->cycle();

              // service simple units w/out any vars on the integration vector V
                for ( auto& N : standalone_neurons )
                        if ( !N->is_conscious() )
                                N -> preadvance();
                for ( auto& Y : standalone_synapses )
                        Y -> preadvance();

              // lastly, service units only serviceable at discrete dt
                if ( this->have_ddtb_units && model_time() >= _discrete_time ) {
                        for ( auto& N : ddtbound_neurons )
                                if ( !N->is_conscious() )
                                        N -> preadvance();
                        for ( auto& Y : ddtbound_synapses )
                                Y -> preadvance();

                        _discrete_time += _discrete_dt;
                        need_fixate_ddtbound_units = true;
                } else
                        need_fixate_ddtbound_units = false;

                if ( !have_hosted_units )
                        V[0] += _discrete_dt;

                _DO_ADVANCE_COMMON_INLOOP_MID

              // fixate
                _integrator->fixate();

                for ( auto& N : standalone_neurons )
                        if ( !N->is_conscious() )
                                N -> fixate();
                for ( auto& Y : standalone_synapses )
                        Y -> fixate();

                if ( need_fixate_ddtbound_units ) {
                        for ( auto& N : ddtbound_neurons )
                                if ( !N->is_conscious() )
                                        N -> fixate();
                        for ( auto& Y : ddtbound_synapses )
                                Y -> fixate();
                }

                _DO_ADVANCE_COMMON_INLOOP_END

        } while ( model_time() < time_ending );

        _DO_ADVANCE_COMMON_EPILOG

        return steps;
}

// Local Variables:
// Mode: c++
// indent-tabs-mode: nil
// tab-width: 8
// c-basic-offset: 8
// End:
