/************************************************************************/
/*                                                                      */
/*    vspline - a set of generic tools for creation and evaluation      */
/*              of uniform b-splines                                    */
/*                                                                      */
/*            Copyright 2018 - 2020 by Kay F. Jahnke                    */
/*                                                                      */
/*    Permission is hereby granted, free of charge, to any person       */
/*    obtaining a copy of this software and associated documentation    */
/*    files (the "Software"), to deal in the Software without           */
/*    restriction, including without limitation the rights to use,      */
/*    copy, modify, merge, publish, distribute, sublicense, and/or      */
/*    sell copies of the Software, and to permit persons to whom the    */
/*    Software is furnished to do so, subject to the following          */
/*    conditions:                                                       */
/*                                                                      */
/*    The above copyright notice and this permission notice shall be    */
/*    included in all copies or substantial portions of the             */
/*    Software.                                                         */
/*                                                                      */
/*    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND    */
/*    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES   */
/*    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND          */
/*    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT       */
/*    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,      */
/*    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING      */
/*    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR     */
/*    OTHER DEALINGS IN THE SOFTWARE.                                   */
/*                                                                      */
/************************************************************************/

/// \file bls.cpp
///
/// \brief fidelity test
/// 
/// This is a test to see how much a signal degrades when it is submitted
/// to a sequence of operations:
///
/// - create a b-spline over the signal
/// - evaluate the spline at unit-spaced locations with an arbitrary offset
/// - yielding a shifted signal, for which the process is repeated
///
/// Finally, a last shift is performed which samples the penultimate version
/// of the signal at points coinciding with coordinates 0...N-1 of the
/// original signal. This last iteration should ideally recreate the
/// original sequence.
///
/// The test is done with a periodic signal to avoid margin effects.
/// The initial sequence is created by performing an IFFT on random
/// data up to a given cutoff frequency, producing a band-limited signal.
/// Alternatively, by modifying the initial values which are put into
/// the frequency-domain representation of the test signal, more specific
/// scenarios can be investigated (try setting the values to 1 uniformly etc)
/// With the cutoff factor approaching 1.0, the signal contains more and higher
/// high frequencies, making it ever more difficult for the spline to
/// represent the data faithfully. With a cutoff of >= .5, fidelity is
/// very high for high-degree b-splines; even after many iterations
/// the spline is nearly unchanged and faithfully represents the
/// original signal.
///
/// There are two factors to observe: high-degree splines cope better with
/// high frequency components, but they suffer from the large magnitude of
/// coefficient values, making the evaluation error larger - especially since
/// high-degree splines have wide support.
///
/// An important conclusion is that even though all input signals produced
/// with this test are band-limited by design, this quality is not sufficient
/// to produce a b-spline from them which is 'stable' in the sense of being
/// immune to the multiple shifting operation, unless the signal is free from
/// high frequencies.
///
/// If the input contains high frequencies, and yet a faithful b-spline
/// representation has to be provided which is immune to shifting, one
/// solution is to create a high-degree spline over the signal, sample it
/// at half unit steps and use the resulting signal to create the 'working'
/// spline. By creating the high-order 'super-spline', a signal is created
/// which is low in high-frequency content and can be faithfully represented
/// by the working spline. The upsampling can also be done in any other
/// way, like FFT/IFFT. See n_shift.cc for an implementation of upsampling
/// using a high-degree b-spline as 'super-spline'.
///
/// note that this example requires fftw3.
///
/// compile with: clang++ -O3 -std=c++11 -obls bls.cpp -pthread -lvigraimpex -lfftw3
///
/// invoke with: bls <spline degree> <number of iterations> [ <frequency cutoff> ]

#include <random>
#include <vigra/accumulator.hxx>
#include <vigra/multi_math.hxx>
#include <vspline/vspline.h>
#include <vigra/multi_fft.hxx>

int main ( int argc , char * argv[] )
{
  if ( argc < 3 )
  {
    std::cerr << "pass the spline's degree and the number of iterations" 
              << std::endl
              << "and, optionally, the cutoff frequency"
              << std::endl ;
    exit ( -1 ) ;
  }

  int degree = std::atoi ( argv[1] ) ;
  
  assert ( degree >= 0 && degree <= vspline_constants::max_degree ) ;
  
  int iterations = std::max ( 1 , std::atoi ( argv[2] ) ) ;
  
  double f_cutoff = .5 ;
  if ( argc == 4 )
  {
    f_cutoff = std::atof ( argv[3] ) ;
    std::cout << "using frequency cutoff " << f_cutoff << std::endl ;
  }
  
  const int sz = 1024 ;
  
  vigra::MultiArray < 1 , double > original ( sz ) ;
  vigra::MultiArray < 1 , double > target ( sz ) ;
  
  vigra::MultiArray < 1 , vigra::FFTWComplex<double> > fourier ( original.shape() / 2 + 1 ) ;
  
  std::random_device rd ;
  std::mt19937 gen ( rd() ) ;
  gen.seed ( 42 ) ;              // level playing field
  std::uniform_real_distribution<> dis ( -1 , 1 ) ;
  
  int fill = f_cutoff * sz / 2.0 ;
  for ( auto & e : fourier )
  {
    if ( fill < 0 )
      e = vigra::FFTWComplex<double> ( 0.0 , 0.0 ) ;
    else
      e = vigra::FFTWComplex<double> ( 1.0 , 0.0 ) ; // ( dis(gen) , dis(gen) ) ;
    --fill ;
  }
  
  vigra::fourierTransformInverse ( fourier , original ) ;

  // now we set up the working spline

  auto m = original.norm ( 0 ) ; // . minimax ( &a , &b ) ;
  
  original /= m ;
  
  vspline::bspline < double ,    // spline's data type
                     1 >         // one dimension
    bspw ( sz ,                  // sz values
           degree ,              // degree as per command line
           vspline::PERIODIC ,   // periodic boundary conditions
           0.0 ) ;               // no tolerance

  // we pull in the working data we've just generated

  bspw.prefilter ( original ) ;
  
  using namespace vigra::multi_math ;
  using namespace vigra::acc;
  
  vigra::MultiArray < 1 , double > error_array
    ( vigra::multi_math::squaredNorm ( bspw.core ) ) ;

  AccumulatorChain < double , Select < Mean, Maximum > > ac ;
  extractFeatures ( error_array.begin() , error_array.end() , ac ) ;
  {
    std::cout << "coefficients Mean:    "
              << sqrt(get<Mean>(ac)) << std::endl;
    std::cout << "coefficients Maximum: "
              << sqrt(get<Maximum>(ac)) << std::endl;
  }
  
  // create an evaluator to obtain interpolated values
  
  typedef vspline::evaluator < double , double > ev_type ;
  
  // and set up the evaluator for the test
  
  ev_type evw ( bspw ) ;
  
  // we want to map the incoming coordinates into the defined range.
  // Since we're using a periodic spline, the range is fom 1...N,
  // rather than 1...N-1 for non-periodic splines

  auto gate = vspline::periodic ( 0.0 , double(sz) ) ;

  // now we do a bit of functional programming.
  // we chain gate and evaluator:

  auto periodic_ev = gate + evw ;
  
  // we cumulate the offsets so we can 'undo' the cumulated offset
  // in the last iteration
    
  double cumulated_offset = 0.0 ;
  
  for ( int n = 0 ; n < iterations ; n++ )
  {
    // use a random, largish offset (+/- 1000). any offset
    // will do, since we have a periodic gate, mapping the
    // coordinates for evaluation into the spline's range
    
    double offset = 1000.0 * dis ( gen ) ;
    
    // with the last iteration, we shift back to the original
    // 0-based locations. This last shift should recreate the
    // original signal as best as a spline of this degree can
    // do after so many iterations.

    if ( n == iterations - 1 )
      offset = - cumulated_offset ;

    cumulated_offset += offset ;

    if ( n > ( iterations - 10 ) )
      std::cout << "iteration " << n << " offset " << offset
                << " cumulated offset " << cumulated_offset << std::endl ;
    
    // we evaluate the spline at unit-stepped offsetted locations,
    // so, 0 + offset , 1 + offset ...
    // in the last iteration, this should ideally reproduce the original
    // signal.
    
    for ( int x = 0 ; x < sz ; x++ )
    {
      auto arg = x + offset ;
      target [ x ] = periodic_ev ( arg ) ;
//       std::cout << "eval: " << arg << " -> " << target[x] << std::endl ;
    }
    
    // now we create a new spline over target, reusing bspw
    // note how this merely changes the coefficients of the spline,
    // the container for the coefficients is reused, and therefore
    // the evaluator (evw) will look at the new set of coefficients.
    // So we don't need to create a new evaluator.

    bspw.prefilter ( target ) ;
    
    // to convince ourselves that we really are working on a different
    // sampling of the signal signal - and to see how close we get to the
    // original signal after n iterations, when we use a last offset to get
    // the sampling locations back to 0, 1, ...
    // Before the 'final' result, we echo the statistics for the last ten
    // iterations, to demonstrate that we aren't accidentally fooling
    // ourselves ;)
      
    vigra::MultiArray < 1 , double > error_array
      ( vigra::multi_math::squaredNorm ( target - original ) ) ;

    AccumulatorChain < double , Select < Mean, Maximum > > ac ;
    extractFeatures ( error_array.begin() , error_array.end() , ac ) ;
    if ( n > ( iterations - 10 ) )
    {
      if ( n == iterations - 1 )
        std::cout << "final result, evaluating at original unit steps" << std::endl ;
      std::cout << "signal difference Mean:    "
                << sqrt(get<Mean>(ac)) << std::endl;
      std::cout << "signal difference Maximum: "
                << sqrt(get<Maximum>(ac)) << std::endl;
    }
  }
}
