/FileExtraction/Java_unseen/crisis-economics_CRISIS/CRISIS/src/eu/crisis_economics/abm/model/parameters/FromFileTimeseriesParameter.java
/*
 * This file is part of CRISIS, an economics simulator.
 * 
 * Copyright (C) 2015 John Kieran Phillips
 *
 * CRISIS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * CRISIS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with CRISIS.  If not, see <http://www.gnu.org/licenses/>.
 */
package eu.crisis_economics.abm.model.parameters;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.math3.analysis.UnivariateFunction;
import org.apache.commons.math3.analysis.interpolation.LinearInterpolator;
import org.apache.commons.math3.analysis.interpolation.SplineInterpolator;
import org.apache.commons.math3.analysis.interpolation.UnivariateInterpolator;

import com.google.common.base.Preconditions;
import com.google.inject.Inject;
import com.google.inject.name.Named;

import eu.crisis_economics.abm.markets.clearing.heterogeneous.BoundedDomainMixin;
import eu.crisis_economics.abm.markets.clearing.heterogeneous.SimpleDomainBounds;
import eu.crisis_economics.abm.simulation.Simulation;
import eu.crisis_economics.utilities.ArrayUtil;

/**
  * A simple implementation of the {@link Parameter}{@code <Double>}
  * interface.<br><br>
  * 
  * This implementations reads a timeseries from an external file. Calls to
  * {@link #get()} are resolved as follows:<br><br>
  * 
  * {@code (a)}
  *    Query the simulation time {@code T}: {@link Simulation#getTime()};<br>
  * {@code (b)}
  *    Evaluate the serialized timeseries at time {@code T}.<br><br>
  * 
  * This object is created with reference to a file containing the timeseries
  * data. The file should be a two-column data table with one record per line.
  * The first column should be ordered ascending and represents keypoints in
  * time {@code T}. The second colum represents the timeseries evaluation
  * {@code f(T)} at time {@code T}. The timeseries should contain at least two
  * keypoints. Columns in the file can be separated by any whitespace, colon,
  * semicolon, comma or tab. Newlines indicate the beginning of a new record.<br><br>
  * 
  * An example of a simple valid input file is as follows:<br><br>
  * 
  * <table style="width:100pt">
  *  <tr>
  *    <td>1.0</td>
  *    <td>1.0</td> 
  *  </tr>
  *  <tr>
  *    <td>2.0</td>
  *    <td>4.0</td> 
  *  </tr>
  *  <tr>
  *    <td>3.0</td>
  *    <td>9.0</td> 
  *  </tr>
  * </table>
  * 
  * @author phillips
  */
public final class FromFileTimeseriesParameter extends AbstractParameter<Double> {
   
   private UnivariateFunction
      interpolation;
   
   private BoundedDomainMixin
      bounds,
      outOfRangeEvaluation;
   
   /**
     * Create a {@link FromFileTimeseriesParameter} object using the specified
     * file.<br><br>
     * 
     * See also {@link FromFileTimeseriesParameter} and {@link Parameter}.
     * 
     * @param filename
     *        The name of the file from which to read timeseries data.
     * @param parameterName
     *        The name of the resulting {@link Parameter}. It is acceptable
     *        for this argument to be the empty string.
     * @param interpolator
     *        An instance of a {@link UnivariateInterpolator}. This interpolator
     *        is used to convert timeseries keypoints into a queryable function
     *        {@code f(T)} for all {@code T} in the domain of the interpolation
     *        keys. Possible examples, among others, include the {@link SplineInterpolator}
     *        and the {@link LinearInterpolator}.
     * 
     * @throws IOException
     *         This method raises {@link IOException} if, for any reason, the stated
     *         file cannot be processed. Reasons for such failure include: (a) the
     *         file does not exist; (b) the file is inaccessible to the read stream;
     *         (c) the file does not contain any data or contains fewer than two 
     *         records; (d) the file exists and can be accessed but does not have
     *         the required format. See {@link FromFileTimeseriesParameter}.
     */
   @Inject
   public FromFileTimeseriesParameter(
   @Named("FROM_FILE_TIMESERIES_PARAMETER_FILENAME")
      final String filename,
   @Named("FROM_FILE_TIMESERIES_PARAMETER_NAME")
      final String parameterName,
   @Named("FROM_FILE_TIMESERIES_PARAMETER_INTERPOLATOR")
      final UnivariateInterpolator interpolator
      ) throws IOException {
      super(parameterName);
      Preconditions.checkNotNull(filename);
      final BufferedReader
         reader = new BufferedReader(new FileReader(filename));
      String
         line = null;
      final List<Double>
         x = new ArrayList<Double>(),
         y = new ArrayList<Double>();
      int
         numInterpolationPoints = 0;
      try {
         while((line = reader.readLine()) != null) {
            if(line.isEmpty()) continue;
            final String[]
               entries = line.split("[\\s:;,\t]+");
            if(entries.length != 2)
               throw new IOException(
                  getClass().getSimpleName() + ": serialized timeseries are expected to "
                + "consist of exactly two data columns. The input data: " + entries + " does "
                + "not conform to this template. The data in file " + filename + " could not "
                + "be parsed."
                );
            x.add(Double.parseDouble(entries[0]));
            y.add(Double.parseDouble(entries[1]));
            ++numInterpolationPoints;
         }
      }
      catch(final NumberFormatException e) {
         throw new IOException(e.getMessage());
      }
      finally {
         reader.close();
      }
      
      if(numInterpolationPoints <= 1)
         throw new IllegalArgumentException(
            getClass().getSimpleName() + ": file " + filename + " exists and has valid format,"
          + " but contains fewer than 2 records. At least two interpolation points are required."
            );
      final UnivariateFunction function =
         interpolator.interpolate(ArrayUtil.toPrimitive(x), ArrayUtil.toPrimitive(y));
      this.interpolation = function;
      this.bounds = new SimpleDomainBounds(x.get(0), x.get(x.size()-1));
      this.outOfRangeEvaluation = new SimpleDomainBounds(y.get(0), y.get(y.size() - 1));
   }
   
   @Override
   public Double get() {
      final double
         time = Simulation.getTime();
      if(time < bounds.getMinimumInDomain())
         return outOfRangeEvaluation.getMinimumInDomain();
      else if(time > bounds.getMaximumInDomain())
         return outOfRangeEvaluation.getMaximumInDomain();
      else
         return interpolation.value(Simulation.getTime());
   }
   
   @Override
   public Class<?> getParameterType() {
      return Double.class;
   }
}
