package org.jactr.modules.temporal.six;
/*
* default logging
*/
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jactr.core.buffer.IActivationBuffer;
import org.jactr.core.chunk.IChunk;
import org.jactr.core.chunktype.IChunkType;
import org.jactr.core.logging.Logger;
import org.jactr.core.model.IModel;
import org.jactr.core.module.AbstractModule;
import org.jactr.core.module.IllegalModuleStateException;
import org.jactr.core.module.random.IRandomModule;
import org.jactr.core.module.random.six.DefaultRandomModule;
import org.jactr.core.production.condition.ChunkPattern;
import org.jactr.core.queue.ITimedEvent;
import org.jactr.core.queue.TimedEventQueue;
import org.jactr.core.queue.timedevents.AbstractTimedEvent;
import org.jactr.core.runtime.ACTRRuntime;
import org.jactr.core.slot.IMutableSlot;
import org.jactr.core.utils.parameter.IParameterized;
import org.jactr.core.utils.parameter.NumericParameterHandler;
import org.jactr.core.utils.parameter.ParameterHandler;
import org.jactr.modules.temporal.ITemporalModule;
import org.jactr.modules.temporal.buffer.DefaultTemporalActivationBuffer;
/**
* implementation of the temporal module. This module is very basic. By calling
* {@link #startTimer()} a new time {@link IChunk} is added to the temporal
* {@link IActivationBuffer}. At the same time, an {@link ITimedEvent} is
* posted to the {@link TimedEventQueue}. It will expire after the initial tick
* duration. Uponing firing, it will increment the tick count of the
* {@link IChunk} and then post a new {@link ITimedEvent} that will expire
* later. The durations are computed by {@link #computeTickDuration(double)}.
* The {@link ITimedEvent}s are posted by {@link #nextTickTimedEvent()}.<br>
* <br>
* All the modifications by this buffer are done through the {@link ITimedEvent}s
* (necessarily on the model thread) or as a result of {@link ChunkPattern}
* requests made to the {@link IActivationBuffer} (again, on the model thread).
* So, thread safety is not much of a concern here.<br>
* <br>
* <br>
* This module is dynamically resolved via the modules extension point in
* plugin.xml
*
* @author harrison
*/
public class DefaultTemporalModule6 extends AbstractModule implements
ITemporalModule, IParameterized
{
/**
* Logger definition
*/
static private final transient Log LOGGER = LogFactory
.getLog(DefaultTemporalModule6.class);
static public final String TIME_MULTIPLIER_PARAM = "TemporalMultiplier";
static public final String TIME_NOISE_PARAM = "TemporalNoise";
static public final String TIME_START_PARAM = "InitialTemporalDuration";
private IActivationBuffer _temporalBuffer;
private IChunkType _timeChunkType;
private double _timeMultiplier = 1.1;
private double _timeNoise = 0.015;
private double _initialTickTime = 0.011;
private IRandomModule _randomModule;
private TickTimedEvent _currentTimedEvent;
private long _count = 0;
private long _currentTicks = 0;
public DefaultTemporalModule6()
{
super("temporal");
}
/**
* initialize the module and make sure we've got the time {@link IChunkType}
* as well as a reference to an {@link IRandomModule}. If not
* {@link IRandomModule} is installed in the model, a default instance is
* used.
*
* @see org.jactr.core.module.AbstractModule#initialize()
*/
@Override
public void initialize()
{
try
{
_timeChunkType = getModel().getDeclarativeModule().getChunkType("time")
.get();
}
catch (Exception e)
{
throw new IllegalModuleStateException(
"Could not get time chunktype from declarative memory ", e);
}
/*
* make sure that there is a random module to use
*/
_randomModule = (IRandomModule) getModel().getModule(IRandomModule.class);
if (_randomModule == null)
_randomModule = DefaultRandomModule.getInstance();
}
/**
* create the temporal buffer {@link DefaultTemporalActivationBuffer}
*
* @return
* @see org.jactr.core.module.AbstractModule#createBuffers()
*/
@Override
protected Collection<IActivationBuffer> createBuffers()
{
_temporalBuffer = new DefaultTemporalActivationBuffer("temporal", this);
return Collections.singleton(_temporalBuffer);
}
/**
* return the buffer
*
* @return
* @see org.jactr.modules.temporal.ITemporalModule#getBuffer()
*/
public IActivationBuffer getBuffer()
{
return _temporalBuffer;
}
/**
* return the current tick count, or 0 if not time chunk is in the buffer
*
* @return
* @see org.jactr.modules.temporal.ITemporalModule#getTicks()
*/
public long getTicks()
{
return _currentTicks;
}
/**
* reset the clock, and abort any pending {@link ITimedEvent} that is used to
* increment the ticks
*
* @see org.jactr.modules.temporal.ITemporalModule#reset()
*/
public void reset()
{
if (_currentTimedEvent != null)
{
_currentTimedEvent.abort();
_currentTimedEvent = null;
}
_temporalBuffer.clear();
_currentTicks = 0;
}
/**
* start a timer. The current timer is removed, a new one created and added to
* the buffer.
*
* @see org.jactr.modules.temporal.ITemporalModule#startTimer()
*/
public void startTimer(int initialTicks)
{
try
{
_count++;
/*
* is there a tick pending? abort
*/
if (_currentTimedEvent != null) _currentTimedEvent.abort();
/*
* create the chunk and add it to the buffer. If there is a chunk in the
* buffer already it will be removed first
*/
IChunk timer = getModel().getDeclarativeModule().createChunk(
_timeChunkType, "timer-" + _count).get();
IMutableSlot tickSlot = (IMutableSlot) timer.getSymbolicChunk().getSlot(
"ticks");
tickSlot.setValue(new Double(initialTicks));
IModel model = getModel();
if (Logger.hasLoggers(model))
Logger.log(model, TEMPORAL_LOG, "Starting clock at " + initialTicks);
_temporalBuffer.addSourceChunk(timer);
/*
* and queue up the ticker
*/
double previousDuration = 0;
nextTickTimedEvent(ACTRRuntime.getRuntime().getClock(getModel())
.getTime(), previousDuration);
}
catch (Exception e)
{
throw new RuntimeException("Could not start new timer ", e);
}
}
/**
* compute the duration of the next tick given the previous one. if
* previousDuration is 0, t0 is recomputed
*
* @param previousDuration
* @return
*/
protected double computeTickDuration(double previousDuration)
{
double duration = _initialTickTime;
double s = 5 * _timeNoise * _initialTickTime;
if (previousDuration > 0)
{
duration = _timeMultiplier * previousDuration;
s = _timeNoise * _timeMultiplier * previousDuration;
}
double noise = _randomModule.logisticNoise(s);
if (LOGGER.isDebugEnabled())
LOGGER.debug("Previous duration : " + previousDuration + " current : "
+ duration + " noise : " + noise);
duration += noise;
return duration;
}
/**
* internal method to increment the number of ticks of the current source
* chunk in the buffer. This is called by {@link TickTimedEvent#fire(double)}
*/
protected void incrementTicks(double startTime, double endTime,
double duration)
{
double ticks = 0;
/**
* this is necessary if (endTime-startTime) >= duration, i.e. more than one
* tick may have elapsed
*/
while (endTime - startTime >= duration)
{
ticks++;
startTime += duration;
duration = computeTickDuration(duration);
}
_currentTicks += ticks;
IChunk time = getBuffer().getSourceChunk();
if (time != null)
{
IModel model = getModel();
IMutableSlot tickSlot = (IMutableSlot) time.getSymbolicChunk().getSlot(
"ticks");
double value = ticks + (Double) tickSlot.getValue();
tickSlot.setValue(new Double(value));
if (LOGGER.isDebugEnabled() || Logger.hasLoggers(model))
{
String msg = "Ticks incremented by " + ticks + " to " + value
+ " next duration : " + duration;
Logger.log(model, TEMPORAL_LOG, msg);
LOGGER.debug(msg);
}
}
}
/**
* creates a new timed event which will actually perform the tick increments.
* called by {@link TickTimedEvent#fire(double)} and {@link #startTimer()}
*/
protected void nextTickTimedEvent(double now, double previousDuration)
{
_currentTimedEvent = new TickTimedEvent(now,
computeTickDuration(previousDuration));
getModel().getTimedEventQueue().enqueue(_currentTimedEvent);
}
/**
*
*/
public String getParameter(String key)
{
if (TIME_MULTIPLIER_PARAM.equalsIgnoreCase(key))
return "" + _timeMultiplier;
else if (TIME_NOISE_PARAM.equalsIgnoreCase(key))
return "" + _timeNoise;
else if (TIME_START_PARAM.equalsIgnoreCase(key))
return "" + _initialTickTime;
return null;
}
public Collection<String> getPossibleParameters()
{
return getSetableParameters();
}
public Collection<String> getSetableParameters()
{
return Arrays.asList(TIME_MULTIPLIER_PARAM, TIME_NOISE_PARAM,
TIME_START_PARAM);
}
public void setParameter(String key, String value)
{
NumericParameterHandler nph = ParameterHandler.numberInstance();
if (TIME_MULTIPLIER_PARAM.equalsIgnoreCase(key))
_timeMultiplier = nph.coerce(value).doubleValue();
else if (TIME_NOISE_PARAM.equalsIgnoreCase(key))
_timeNoise = nph.coerce(value).doubleValue();
else if (TIME_START_PARAM.equalsIgnoreCase(key))
_initialTickTime = nph.coerce(value).doubleValue();
}
/**
* custom timed event that upon {@link #fire(double)} will call
* {@link DefaultTemporalModule6#incrementTicks()} and
* {@link DefaultTemporalModule6#nextTickTimedEvent()}
*
* @author harrison
*/
private class TickTimedEvent extends AbstractTimedEvent
{
public TickTimedEvent(double now, double duration)
{
super();
setTimes(now, now + duration);
}
@Override
public void fire(double currentTime)
{
super.fire(currentTime);
/*
* we use the current time as end time, in case more than duration time
* has elapsed endtime (could be true if running realtime)
*/
incrementTicks(getStartTime(), currentTime, getEndTime() - getStartTime());
nextTickTimedEvent(currentTime, getEndTime() - getStartTime());
}
}
}