/*
* JaamSim Discrete Event Simulation
* Copyright (C) 2013 Ausenco Engineering Canada Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jaamsim.Samples;
import java.util.Arrays;
import com.jaamsim.Graphics.DisplayEntity;
import com.jaamsim.basicsim.Simulation;
import com.jaamsim.events.EventManager;
import com.jaamsim.input.Input;
import com.jaamsim.input.InputErrorException;
import com.jaamsim.input.Keyword;
import com.jaamsim.input.Output;
import com.jaamsim.input.TimeSeriesDataInput;
import com.jaamsim.input.UnitTypeInput;
import com.jaamsim.input.ValueInput;
import com.jaamsim.units.TimeUnit;
import com.jaamsim.units.Unit;
import com.jaamsim.units.UserSpecifiedUnit;
public class TimeSeries extends DisplayEntity implements TimeSeriesProvider {
@Keyword(description = "The unit type for the time series. The UnitType input must be "
+ "specified before the Value input.",
exampleList = {"DistanceUnit", "MassUnit", "DimensionlessUnit"})
private final UnitTypeInput unitType;
@Keyword(description = "A list of time series records with format { time value }, where: "
+ "'time' is the time stamp for the record and 'value' is the time "
+ "series value. Records are entered in order of increasing simulation "
+ "time. The appropriate units should be included with both the time "
+ "and value inputs.\n\n"
+ "The first time stamp MUST be zero simulation time or "
+ "January 1 00:00:00 of an arbitrary year. If a non-zero year is "
+ "entered, e.g. '2010-01-01 00:00:00', then the TimeSeries considers "
+ "this date to be time zero of the simulation and all other timestamps "
+ "are offset accordingly.",
exampleList = {"{ 0 h 1 } { 3 h 0 }",
"{ 0 h 0.5 m } { 3 h 1.5 m }",
"{ '2010-01-01 00:00:00' 0.5 m } { '2010-01-01 03:00:00' 1.5 m }"} )
private final TimeSeriesDataInput value;
@Keyword(description = "The time at which the time series will repeat from the start.",
exampleList = {"8760.0 h"})
private final ValueInput cycleTime;
{
unitType = new UnitTypeInput("UnitType", "Key Inputs", UserSpecifiedUnit.class);
unitType.setRequired(true);
this.addInput(unitType);
value = new TimeSeriesDataInput("Value", "Key Inputs", null);
value.setUnitType(UserSpecifiedUnit.class);
value.setRequired(true);
this.addInput(value);
cycleTime = new ValueInput("CycleTime", "Key Inputs", Double.POSITIVE_INFINITY);
cycleTime.setUnitType(TimeUnit.class);
this.addInput(cycleTime);
}
public TimeSeries() { }
@Override
public void validate() {
super.validate();
if (value.getTickLength() != Simulation.getTickLength())
throw new InputErrorException("A new value was entered for the Simulation keyword TickLength " +
"after the TimeSeries data had been loaded.%n" +
"The configuration file must be saved and reloaded before the simulation can be executed.");
long[] ticksList = value.getValue().ticksList;
if (getTicks(cycleTime.getValue()) < ticksList[ticksList.length - 1])
throw new InputErrorException( "CycleTime must be larger than the last time in the series" );
}
@Override
public void updateForInput( Input<?> in ) {
super.updateForInput( in );
if (in == unitType) {
value.setUnitType( unitType.getUnitType() );
this.getOutputHandle("PresentValue").setUnitType( unitType.getUnitType() );
return;
}
}
@Override
public Class<? extends Unit> getUserUnitType() {
return unitType.getUnitType();
}
private long getTicks(double simTime) {
return EventManager.secsToNearestTick(simTime);
}
private double getSimTime(long ticks) {
if (ticks == Long.MAX_VALUE)
return Double.POSITIVE_INFINITY;
return EventManager.ticksToSecs(ticks);
}
/**
* Returns the value for time series at the given simulation time.
* @param ticks - simulation time in clock ticks.
*/
@Override
public double getValueForTicks(long ticks) {
return getValue(getTSPointForTicks(ticks));
}
/**
* Return the first time that the value will be updated, after the given
* simulation time.
* @param ticks - simulation time in clock ticks.
* @return simulation time in clock ticks at which the time series value will change.
*/
@Override
public long getNextChangeAfterTicks(long ticks) {
return getTicks(getTSPointAfter(getTSPointForTicks(ticks)));
}
@Override
public double getNextTimeAfter(double simTime) {
return getSimTime( getNextChangeAfterTicks(getTicks(simTime)) );
}
@Override
public long getMaxTicksValue() {
if (cycleTime.getValue() < Double.POSITIVE_INFINITY)
return getTicks(cycleTime.getValue());
long[] ticksList = value.getValue().ticksList;
return ticksList[ ticksList.length-1 ];
}
@Override
public Class<? extends Unit> getUnitType() {
return unitType.getUnitType();
}
@Override
public double getMaxValue() {
return value.getValue().getMaxValue();
}
@Override
public double getMinValue() {
return value.getValue().getMinValue();
}
@Override
public double getMeanValue(double simTime) {
return this.getNextSample(simTime);
}
/**
* Returns the position in the time series that corresponds to the specified
* time in simulation clock ticks.
* <p>
* The position returned is the largest one whose ticks value is less than
* or equal to the specified ticks.
* @param ticks - simulation time in clock ticks.
* @return position in the TimeSeries.
*/
private TSPoint getTSPointForTicks(long ticks) {
long[] ticksList = value.getValue().ticksList;
if (ticks == Long.MAX_VALUE) {
if (cycleTime.getValue() == Double.POSITIVE_INFINITY)
return new TSPoint(ticksList.length - 1, 0);
return new TSPoint(ticksList.length - 1, Long.MAX_VALUE);
}
// Find the time within the present cycle
final long cycleTicks = getTicks(cycleTime.getValue());
long numberOfCycles = ticks / cycleTicks;
long ticksInCycle = ticks % cycleTicks;
// If the time in the cycle is greater than the last time, return the last value
if (ticksInCycle >= ticksList[ticksList.length - 1]) {
return new TSPoint(ticksList.length - 1, numberOfCycles);
}
// Find the index by binary search
int k = Arrays.binarySearch(ticksList, ticksInCycle);
// If the returned index is greater or equal to zero,
// then an exact match was found
if (k >= 0)
return new TSPoint(k, numberOfCycles);
if (k == -1)
error("No value found at time: %f", getSimTime(ticks));
// If the returned index is negative, then (insertion index) = -k-1
// Return the index before the insertion index
return new TSPoint(-k - 2, numberOfCycles);
}
/**
* Returns the position in the time series that corresponds to the specified value.
* <p>
* The TimeSeries values must increase monotonically. The position returned
* is the largest one whose value is less than or equal to the specified value.
* @param val - specified value.
* @return position in the TimeSeries.
*/
private TSPoint getTSPointForValue(double val) {
double[] valueList = value.getValue().valueList;
if (val > getMaxValue() && cycleTime.getValue() == Double.POSITIVE_INFINITY)
return new TSPoint(valueList.length - 1, 0);
// Find the value within the present cycle
double valInCycle = val % getMaxValue();
long numberOfCycles = Math.round((val - valInCycle) / getMaxValue());
// If the value in the cycle is greater than or equal to the last value, return the last index
if (valInCycle >= valueList[valueList.length - 1])
return new TSPoint(valueList.length - 1, numberOfCycles);
// Find the index by binary search
int k = Arrays.binarySearch(valueList, valInCycle);
// If the returned index is greater or equal to zero,
// then an exact match was found
if (k >= 0)
return new TSPoint(k, numberOfCycles);
if (k == -1)
error("No entry found for value: %f", val);
// If the returned index is negative, then (insertion index) = -k-1
// Return the index before the insertion index
return new TSPoint(-k - 2, numberOfCycles);
}
/**
* Returns the simulation time in clock ticks for the specified position in
* the time series.
* @param pt - position in the time series.
* @return simulation time in clock ticks.
*/
private long getTicks(TSPoint pt) {
if (pt.index == -1)
return Long.MAX_VALUE;
if (cycleTime.getValue() == Double.POSITIVE_INFINITY)
return value.getValue().ticksList[pt.index];
return value.getValue().ticksList[pt.index] + pt.numberOfCycles*getTicks(cycleTime.getValue());
}
/**
* Returns the time series value for the specified position in the time
* series.
* @param pt - position in the time series.
* @return value for the time series.
*/
private double getValue(TSPoint pt) {
double valueList[] = value.getValue().valueList;
if (pt.index == -1)
return valueList[ valueList.length - 1 ];
return valueList[pt.index];
}
/**
* Returns the total value for the time series at the specified position.
* <p>
* If a cycle time has been specified, then the total time increases
* with each pass through the time series.
* @param pt - position in the time series.
* @return total value for the time series.
*/
private double getCumulativeValue(TSPoint pt) {
if (cycleTime.getValue() == Double.POSITIVE_INFINITY)
return getValue(pt);
return getValue(pt) + pt.numberOfCycles*getMaxValue();
}
/**
* Returns the position in the time series that follows the specified
* position.
* <p>
* An index of -1 is returned if the specified position is a the end
* of the time series data and a cycle time is not specified.
* @param pt - specified position in the time series.
* @return next position in the time series.
*/
private TSPoint getTSPointAfter(TSPoint pt) {
if (pt.index == -1)
return new TSPoint(pt.index, pt.numberOfCycles);
if (pt.index == value.getValue().ticksList.length - 1) {
if (cycleTime.getValue() == Double.POSITIVE_INFINITY)
return new TSPoint(-1, pt.numberOfCycles);
return new TSPoint(0, pt.numberOfCycles + 1);
}
return new TSPoint(pt.index + 1, pt.numberOfCycles);
}
@Override
public long getInterpolatedTicksForValue(double val) {
TSPoint low = getTSPointForValue(val);
TSPoint high = getTSPointAfter(low);
if (high.index == -1)
return Long.MAX_VALUE;
long ticksLow = getTicks(low);
long ticksHigh = getTicks(high);
double valueLow = getCumulativeValue(low);
double valueHigh = getCumulativeValue(high);
// The value at the end of the cycle is equal to the value at the start of the next cycle
if (valueHigh == valueLow) {
high = getTSPointAfter(high);
ticksHigh = getTicks(high);
valueHigh = getCumulativeValue(high);
}
return ticksLow + Math.round((val - valueLow)*(ticksHigh - ticksLow)/(valueHigh - valueLow));
}
@Override
public double getInterpolatedCumulativeValueForTicks(long ticks) {
TSPoint low = getTSPointForTicks(ticks);
TSPoint high = getTSPointAfter(low);
if (high.index == -1) {
double valueList[] = value.getValue().valueList;
return valueList[ valueList.length - 1 ];
}
long ticksLow = getTicks(low);
long ticksHigh = getTicks(high);
double valueLow = getCumulativeValue(low);
double valueHigh = getCumulativeValue(high);
// The value at the end of the cycle is equal to the value at the start of the next cycle
if (valueHigh == valueLow) {
high = getTSPointAfter(high);
ticksHigh = getTicks(high);
valueHigh = getCumulativeValue(high);
}
return valueLow + (ticks - ticksLow)*(valueHigh - valueLow)/(ticksHigh - ticksLow);
}
@Override
public final double getNextSample(double simTime) {
return getValue(getTSPointForTicks(getTicks(simTime)));
}
// ******************************************************************************************************
// OUTPUTS
// ******************************************************************************************************
@Output(name = "PresentValue",
description = "The time series value for the present time.",
unitType = UserSpecifiedUnit.class)
public final double getPresentValue(double simTime) {
if (value.getValue() == null)
return Double.NaN;
return this.getNextSample(simTime);
}
}