/*
* JaamSim Discrete Event Simulation
* Copyright (C) 2014 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.Thresholds;
import com.jaamsim.Samples.TimeSeriesConstantDouble;
import com.jaamsim.basicsim.EntityTarget;
import com.jaamsim.events.EventManager;
import com.jaamsim.events.ProcessTarget;
import com.jaamsim.input.Input;
import com.jaamsim.input.InputAgent;
import com.jaamsim.input.InputErrorException;
import com.jaamsim.input.Keyword;
import com.jaamsim.input.TimeSeriesInput;
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 TimeSeriesThreshold extends Threshold {
@Keyword(description = "The TimeSeries object whose values are to be tested.",
exampleList = {"TimeSeries1"})
private final TimeSeriesInput timeSeries;
@Keyword(description = "The largest TimeSeries value for which the threshold is open. "
+ "The threshold is closed for TimeSeries values greater than "
+ "MaxOpenLimit.",
exampleList = {"2.0 m", "TimeSeries2"})
private final TimeSeriesInput maxOpenLimit;
@Keyword(description = "The smallest TimeSeries value for which the threshold is open. "
+ "The threshold is closed for TimeSeries values smaller than "
+ "MinOpenLimit.",
exampleList = {"2.0 m", "TimeSeries3"})
private final TimeSeriesInput minOpenLimit;
@Keyword(description = "The length of time over which the TimeSeries values must be "
+ ">= MinOpenLimit and <= MaxOpenLimit.\n"
+ "The threshold is open if the TimeSeries values x(t) satisfy "
+ "MinOpenLimit <= x(t) <= MaxOpenLimit for simulation times t from "
+ "(SimTime + Offset) to (SimTime + Offset + LookAhead).",
exampleList = {"5.0 h"})
private final ValueInput lookAhead;
@Keyword(description = "The amount of time that the threshold adds on to every time series "
+ "lookup.\n"
+ "The threshold is open if the TimeSeries values x(t) satisfy "
+ "MinOpenLimit <= x(t) <= MaxOpenLimit for simulation times t from "
+ "(SimTime + Offset) to (SimTime + Offset + LookAhead).",
exampleList = {"5.0 h"})
private final ValueInput offset;
@Keyword(description = "The unit type for the threshold (e.g. DistanceUnit, TimeUnit, MassUnit).",
exampleList = {"DistanceUnit"})
private final UnitTypeInput unitType;
{
unitType = new UnitTypeInput("UnitType", "Key Inputs", UserSpecifiedUnit.class);
unitType.setRequired(true);
this.addInput(unitType);
timeSeries = new TimeSeriesInput("TimeSeries", "Key Inputs", null);
timeSeries.setUnitType(UserSpecifiedUnit.class);
timeSeries.setRequired(true);
this.addInput(timeSeries);
maxOpenLimit = new TimeSeriesInput("MaxOpenLimit", "Key Inputs", new TimeSeriesConstantDouble(Double.POSITIVE_INFINITY));
maxOpenLimit.setUnitType(UserSpecifiedUnit.class);
this.addInput( maxOpenLimit );
minOpenLimit = new TimeSeriesInput("MinOpenLimit", "Key Inputs", new TimeSeriesConstantDouble(Double.NEGATIVE_INFINITY));
minOpenLimit.setUnitType(UserSpecifiedUnit.class);
this.addInput( minOpenLimit );
lookAhead = new ValueInput( "LookAhead", "Key Inputs", 0.0d );
lookAhead.setUnitType(TimeUnit.class);
this.addInput( lookAhead );
offset = new ValueInput( "Offset", "Key Inputs", 0.0d );
offset.setUnitType(TimeUnit.class);
this.addInput( offset );
}
@Override
public void updateForInput( Input<?> in ) {
super.updateForInput( in );
if (in == unitType) {
timeSeries.setUnitType(this.getUnitType());
maxOpenLimit.setUnitType(this.getUnitType());
minOpenLimit.setUnitType(this.getUnitType());
}
}
@Override
public void validate() throws InputErrorException {
super.validate();
if( (maxOpenLimit.getValue().getMinValue() == Double.POSITIVE_INFINITY) &&
(minOpenLimit.getValue().getMaxValue() == Double.NEGATIVE_INFINITY) ) {
throw new InputErrorException( "Missing Limit" );
}
if (minOpenLimit.getValue().getMaxValue() > maxOpenLimit.getValue().getMaxValue())
throw new InputErrorException("MaxOpenLimit must be larger than MinOpenLimit");
if( timeSeries.getValue().getUnitType() != this.getUnitType() )
throw new InputErrorException("Time Series unitType (%s) does not match the Threshold Unit type (%s)",
timeSeries.getValue().getUnitType(), this.getUnitType());
if (timeSeries.getValue().getMinValue() > maxOpenLimit.getValue().getMaxValue())
InputAgent.logWarning("Threshold %s is closed forever. MaxOpenLimit = %f Max TimeSeries Value = %f",
this, maxOpenLimit.getValue().getMaxValue(), timeSeries.getValue().getMaxValue());
if (timeSeries.getValue().getMaxValue() < minOpenLimit.getValue().getMaxValue())
InputAgent.logWarning("Threshold %s is closed forever. MinOpenLimit = %f Min TimeSeries Value = %f",
this, minOpenLimit.getValue().getMaxValue(), timeSeries.getValue().getMinValue());
}
@Override
public void startUp() {
super.startUp();
this.doOpenClose();
}
public Class<? extends Unit> getUnitType() {
return unitType.getUnitType();
}
private static class DoOpenCloseTarget extends EntityTarget<TimeSeriesThreshold> {
public DoOpenCloseTarget(TimeSeriesThreshold ent, String method) {
super(ent, method);
}
@Override
public void process() {
ent.doOpenClose();
}
}
private final ProcessTarget doOpenClose = new DoOpenCloseTarget(this, "doOpenClose");
/**
* The process loop that opens and closes the threshold.
*/
public void doOpenClose() {
long wait;
if (this.isOpenAtTicks(getSimTicks())) {
setOpen(true);
wait = this.calcOpenTicksFromTicks(getSimTicks());
}
else {
setOpen(false);
wait = this.calcClosedTicksFromTicks(getSimTicks());
}
if (wait == Long.MAX_VALUE)
return;
this.scheduleProcessTicks(wait, 1, doOpenClose);
}
/**
* Return TRUE if the threshold is open at the given time
* @param simTime - simulation time in seconds
* @return TRUE if open, FALSE if closed
*/
public boolean isOpenAtTime(double simTime) {
return isOpenAtTicks(EventManager.secsToNearestTick(simTime));
}
/**
* Returns TRUE if the threshold is open at the given time.
* <p>
* Note: if lookahead > 0, the condition for open is that opentime >= lookahead.
* However, if the lookahead == 0, the condition for open is that opentime > lookahead.
* @param ticks - simulation time in clock ticks
* @return TRUE if open, FALSE if closed
*/
private boolean isOpenAtTicks(long ticks) {
// Add offset from input
ticks += EventManager.secsToNearestTick(offset.getValue());
ticks = Math.max(ticks, 0);
long changeTime = ticks;
// if the current point is closed, we are done
if (!this.isPointOpenAtTicks(changeTime))
return false;
// If there is no lookahead, then the threshold is open
long lookAheadInTicks = EventManager.secsToNearestTick(lookAhead.getValue());
if (lookAheadInTicks == 0)
return true;
while( true ) {
// If the next point is closed, determine if open long enough too satisfy lookahead
changeTime = this.getNextChangeAfterTicks(changeTime);
if (!this.isPointOpenAtTicks(changeTime))
return (changeTime - ticks >= lookAheadInTicks);
// The next point is open, determine whether the lookahead is already satisfied
if (changeTime - ticks >= lookAheadInTicks)
return true;
}
}
/**
* Return the time during which the threshold is closed starting from the given time.
* @param ticks - simulation time in clock ticks
* @return the time in clock ticks that the threshold is closed
*/
private long calcClosedTicksFromTicks(long ticks) {
// If the series is always outside the limits, the threshold is closed forever
if (isAlwaysClosed())
return Long.MAX_VALUE;
// If the series is always within the limits, the threshold is open forever
if (this.isAlwaysOpen())
return 0;
// If the threshold is not closed at the given time, return 0.0
// This check must occur before adding the offset because isClosedAtTicks also adds the offset
if (this.isOpenAtTicks(ticks))
return 0;
// Add offset from input
ticks += EventManager.secsToNearestTick(offset.getValue());
ticks = Math.max(ticks, 0);
// Threshold is currently closed. Find the next open point
long openTime = -1;
long changeTime = ticks;
long maxTicksValueFromTimeSeries = this.getMaxTicksValueFromTimeSeries();
long lookAheadInTicks = EventManager.secsToNearestTick(lookAhead.getValue());
while( true ) {
changeTime = this.getNextChangeAfterTicks(changeTime);
if (changeTime == Long.MAX_VALUE) {
if( openTime == -1 )
return Long.MAX_VALUE;
else
return openTime - ticks;
}
// if have already searched the longest cycle, the threshold will never open
if (changeTime > ticks + maxTicksValueFromTimeSeries + lookAheadInTicks)
return Long.MAX_VALUE;
// Closed index
if (!this.isPointOpenAtTicks(changeTime)) {
// If an open point has not been found yet, keep looking
if (openTime == -1)
continue;
// Has enough time been gathered to satisfy the lookahead?
if (changeTime - openTime >= lookAheadInTicks)
return openTime - ticks;
// not enough time, need to start again
else
openTime = -1;
}
// Open index
else {
// Keep track of the first open index.
if (openTime == -1)
openTime = changeTime;
}
}
}
private boolean isAlwaysOpen() {
double tsMin = timeSeries.getValue().getMinValue();
double tsMax = timeSeries.getValue().getMaxValue();
double maxMinOpen = minOpenLimit.getValue().getMaxValue();
double minMaxOpen = maxOpenLimit.getValue().getMinValue();
return (tsMin >= maxMinOpen && tsMax <= minMaxOpen);
}
private boolean isAlwaysClosed() {
double tsMin = timeSeries.getValue().getMinValue();
double tsMax = timeSeries.getValue().getMaxValue();
double minMinOpen = minOpenLimit.getValue().getMinValue();
double maxMaxOpen = maxOpenLimit.getValue().getMaxValue();
return (tsMax < minMinOpen || tsMin > maxMaxOpen);
}
/**
* Return the time during which the threshold is open starting from the given time.
* @param ticks - simulation time in clock ticks
* @return the time in clock ticks that the threshold is open
*/
private long calcOpenTicksFromTicks(long ticks) {
// If the series is always outside the limits, the threshold is closed forever
if (isAlwaysClosed())
return 0;
// If the series is always within the limits, the threshold is open forever
if (this.isAlwaysOpen())
return Long.MAX_VALUE;
// If the threshold is closed at the given time, return 0.0
// This check must occur before adding the offset because isClosedAtTIme also adds the offset
if (!this.isOpenAtTicks(ticks))
return 0;
// Add offset from input
ticks += EventManager.secsToNearestTick(offset.getValue());
ticks = Math.max(ticks, 0);
// Find the next change point after startTime
long changeTime = ticks;
long maxTicksValueFromTimeSeries = this.getMaxTicksValueFromTimeSeries();
long lookAheadInTicks = EventManager.secsToNearestTick(lookAhead.getValue());
while( true ) {
changeTime = this.getNextChangeAfterTicks(changeTime);
if( changeTime == Long.MAX_VALUE )
return Long.MAX_VALUE;
// if have already searched the longest cycle, the threshold will never close
if( changeTime > ticks + maxTicksValueFromTimeSeries )
return Long.MAX_VALUE;
// Closed index
if (!this.isPointOpenAtTicks(changeTime)) {
if (lookAheadInTicks == 0)
return changeTime - ticks;
else
return changeTime - lookAheadInTicks - ticks + 1;
}
}
}
/**
* Return the time in seconds during which the threshold is open
* from the given start time to the given end time
* @param startTime - simulation start time in seconds
* @param endTime - simulation end time in seconds
* @return the time in seconds that the threshold is open
*/
public double calcOpenTimeFromTimeToTime(double startTime, double endTime) {
// If the series is always outside the limits, the threshold is closed forever
if (isAlwaysClosed())
return 0;
// If the series is always within the limits, the threshold is open forever
if (this.isAlwaysOpen())
return endTime - startTime;
long ticks = EventManager.secsToNearestTick(startTime);
long endTicks = EventManager.secsToNearestTick(endTime);
long openTicks = 0;
boolean done = false;
while (! done) {
if (this.isOpenAtTicks(ticks)) {
long tempTicks = this.calcOpenTicksFromTicks(ticks);
if (ticks + tempTicks >= endTicks) {
openTicks += Math.min( tempTicks, endTicks - ticks);
done = true;
}
else {
openTicks += tempTicks;
ticks += tempTicks;
}
}
else {
long tempTicks = this.calcClosedTicksFromTicks(ticks);
if (ticks + tempTicks >= endTicks) {
done = true;
}
else {
ticks += tempTicks;
}
}
}
return EventManager.ticksToSecs(openTicks);
}
/**
* Returns the next time that one of the parameters TimeSeries, MaxOpenLimit, or MinOpenLimit
* will change, after the given time.
* @param ticks - simulation time in clock ticks.
* @return the next time in clock ticks that a change will occur
*/
private long getNextChangeAfterTicks(long ticks) {
long firstChange = timeSeries.getValue().getNextChangeAfterTicks(ticks);
firstChange = Math.min(firstChange, maxOpenLimit.getValue().getNextChangeAfterTicks(ticks));
firstChange = Math.min(firstChange, minOpenLimit.getValue().getNextChangeAfterTicks(ticks));
return firstChange;
}
/**
* Returns the largest time in TimeSeries, MaxOpenLimit, and MinOpenLimit time series.
* This value is used to determine whether the series has cycled around once while finding the next open/close time.
* @return the last time in clock ticks that a change will occur
*/
private long getMaxTicksValueFromTimeSeries() {
long maxCycle = timeSeries.getValue().getMaxTicksValue();
maxCycle = Math.max(maxCycle, maxOpenLimit.getValue().getMaxTicksValue());
maxCycle = Math.max(maxCycle, minOpenLimit.getValue().getMaxTicksValue());
return maxCycle;
}
/**
* Return TRUE if, at the given time, the TimeSeries input value falls outside of the values for MaxOpenLimit and
* MinOpenLimit. Does not include the effect of the offset value.
* @param ticks - simulation time in clock ticks.
* @return TRUE if open, FALSE if closed
*/
private boolean isPointOpenAtTicks(long ticks) {
double value = timeSeries.getValue().getValueForTicks(ticks);
double minOpenLimitVal = minOpenLimit.getValue().getValueForTicks(ticks);
double maxOpenLimitVal = maxOpenLimit.getValue().getValueForTicks(ticks);
// Error check that threshold limits remain consistent
if (minOpenLimitVal > maxOpenLimitVal)
error("MaxOpenLimit must be larger than MinOpenLimit. MaxOpenLimit: %s, MinOpenLimit: %s, time: %s",
maxOpenLimitVal, minOpenLimitVal, EventManager.ticksToSecs(ticks));
return (value >= minOpenLimitVal) && (value <= maxOpenLimitVal);
}
}