Simulator.java
package nl.tudelft.simulation.dsol.simulators;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.djutils.event.EventType;
import org.djutils.event.LocalEventProducer;
import org.djutils.exceptions.Throw;
import org.djutils.logger.CategoryLogger;
import org.pmw.tinylog.Level;
import org.pmw.tinylog.Logger;
import nl.tudelft.simulation.dsol.SimRuntimeException;
import nl.tudelft.simulation.dsol.experiment.Replication;
import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEvent;
import nl.tudelft.simulation.dsol.logger.SimLogger;
import nl.tudelft.simulation.dsol.model.DsolModel;
/**
* The Simulator class is an abstract implementation of the SimulatorInterface.
* <p>
* Copyright (c) 2002-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
* for project information <a href="https://simulation.tudelft.nl/" target="_blank"> https://simulation.tudelft.nl</a>. The DSOL
* project is distributed under a three-clause BSD-style license, which can be found at
* <a href="https://https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">
* https://https://simulation.tudelft.nl/dsol/docs/latest/license.html</a>.
* </p>
* @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a> relative types are the same.
* @param <T> the time type
*/
public abstract class Simulator<T extends Number & Comparable<T>> extends LocalEventProducer
implements SimulatorInterface<T>, Runnable
{
/** */
private static final long serialVersionUID = 20140805L;
/** simulatorTime represents the simulationTime. */
@SuppressWarnings("checkstyle:visibilitymodifier")
protected T simulatorTime;
/** The runUntil time in case we want to stop before the end of the replication time. */
@SuppressWarnings("checkstyle:visibilitymodifier")
protected T runUntilTime;
/** whether the runUntilTime should carry out the calculation(s) for that time or not. */
@SuppressWarnings("checkstyle:visibilitymodifier")
protected boolean runUntilIncluding = true;
/** The run state of the simulator, that indicates the state of the Simulator state machine. */
@SuppressWarnings("checkstyle:visibilitymodifier")
protected RunState runState = RunState.NOT_INITIALIZED;
/** The replication state of the simulator, that indicates the state of the Replication state machine. */
@SuppressWarnings("checkstyle:visibilitymodifier")
protected ReplicationState replicationState = ReplicationState.NOT_INITIALIZED;
/** The currently active replication; is null before initialize() has been called. */
@SuppressWarnings("checkstyle:visibilitymodifier")
protected Replication<T> replication = null;
/** The model that is currently active; is null before initialize() has been called. */
@SuppressWarnings("checkstyle:visibilitymodifier")
protected DsolModel<T, ? extends SimulatorInterface<T>> model = null;
/** a worker. */
@SuppressWarnings("checkstyle:visibilitymodifier")
protected transient SimulatorWorkerThread worker = null;
/** the simulatorSemaphore. */
@SuppressWarnings("checkstyle:visibilitymodifier")
protected transient Object semaphore = new Object();
/** the logger. */
private transient SimLogger logger;
/** the simulator id. */
private Serializable id;
/** the methods to execute after model initialization, e.g., to set-up the initial events. */
private final List<SimEvent<Long>> initialmethodCalls = new ArrayList<>();
/** The error handling strategy. */
private ErrorStrategy errorStrategy = ErrorStrategy.WARN_AND_PAUSE;
/** The error strategy's log level. */
private Level errorLogLevel = Level.ERROR;
/** the run flag semaphore indicating that the run() method has started (and might have stopped). */
@SuppressWarnings("checkstyle:visibilitymodifier")
protected boolean runflag = false;
/**
* Constructs a new Simulator.
* @param id the id of the simulator, used in logging and firing of events.
*/
public Simulator(final Serializable id)
{
Throw.whenNull(id, "id cannot be null");
this.id = id;
this.logger = new SimLogger(this);
}
/** {@inheritDoc} */
@Override
public void initialize(final DsolModel<T, ? extends SimulatorInterface<T>> model, final Replication<T> replication)
throws SimRuntimeException
{
Throw.whenNull(model, "Simulator.initialize: model cannot be null");
Throw.whenNull(replication, "Simulator.initialize: replication cannot be null");
Throw.when(isStartingOrRunning(), SimRuntimeException.class, "Cannot initialize a running simulator");
synchronized (this.semaphore)
{
if (this.worker != null)
{
cleanUp();
}
this.worker = new SimulatorWorkerThread(this.id.toString(), this);
this.replication = replication;
this.model = model;
this.simulatorTime = replication.getStartTime();
this.model.getOutputStatistics().clear();
this.model.constructModel();
this.runState = RunState.INITIALIZED;
this.replicationState = ReplicationState.INITIALIZED;
this.runflag = false;
for (SimEvent<Long> initialMethodCall : this.initialmethodCalls)
{
initialMethodCall.execute();
}
}
// sleep maximally 1 second till the SimulatorWorkerThread gets into the WAITING state
int count = 0;
while (!this.worker.isWaiting() && this.worker.isAlive() && count < 1000)
{
try
{
Thread.sleep(1);
count++;
}
catch (InterruptedException exception)
{
// ignore
}
}
}
/** {@inheritDoc} */
@Override
public void addScheduledMethodOnInitialize(final Object target, final String method, final Object[] args)
throws SimRuntimeException
{
this.initialmethodCalls.add(new SimEvent<Long>(0L, target, method, args));
}
/**
* Implementation of the start method. Checks preconditions for running and fires the right events.
* @throws SimRuntimeException when the simulator is already running, or when the replication is missing or has ended
*/
public void startImpl() throws SimRuntimeException
{
Throw.when(isStartingOrRunning(), SimRuntimeException.class, "Cannot start a running simulator");
Throw.when(this.replication == null, SimRuntimeException.class, "Cannot start a simulator without replication details");
Throw.when(!isInitialized(), SimRuntimeException.class, "Cannot start an uninitialized simulator");
Throw.when(
!(this.replicationState == ReplicationState.INITIALIZED || this.replicationState == ReplicationState.STARTED),
SimRuntimeException.class, "State of the replication should be INITIALIZED or STARTED to run a simulationF");
Throw.when(this.simulatorTime.compareTo(this.replication.getEndTime()) >= 0, SimRuntimeException.class,
"Cannot start simulator : simulatorTime >= runLength");
synchronized (this.semaphore)
{
this.runState = RunState.STARTING;
if (this.replicationState == ReplicationState.INITIALIZED)
{
fireTimedEvent(Replication.START_REPLICATION_EVENT, null, getSimulatorTime());
this.replicationState = ReplicationState.STARTED;
}
this.fireEvent(SimulatorInterface.STARTING_EVENT, null);
// continue the run() of the SimulatorWorkerThread that will start the Simulator's run() method
this.worker.interrupt();
// wait maximally 1 second till the Simulator.run() method has been called
int count = 0;
while (!this.runflag && count < 1000)
{
try
{
Thread.sleep(1);
count++;
}
catch (InterruptedException exception)
{
// ignore
}
}
// clear the flag
this.runflag = false;
}
}
/** {@inheritDoc} */
@Override
public void start() throws SimRuntimeException
{
this.runUntilTime = this.replication.getEndTime();
this.runUntilIncluding = true;
startImpl();
}
/** {@inheritDoc} */
@Override
public void runUpTo(final T stopTime) throws SimRuntimeException
{
this.runUntilTime = stopTime;
this.runUntilIncluding = false;
startImpl();
}
/** {@inheritDoc} */
@Override
public void runUpToAndIncluding(final T stopTime) throws SimRuntimeException
{
this.runUntilTime = stopTime;
this.runUntilIncluding = true;
startImpl();
}
/**
* The implementation body of the step() method. The stepImpl() method should fire the TIME_CHANGED_EVENT before the
* execution of the simulation event, or before executing the integration of the differential equation for the next
* timestep. So the time is changed first to match the logic carried out for that time, and then the action for that time is
* carried out. This is INDEPENDENT of the fact whether the time changes or not. The TIME_CHANGED_EVENT is always fired.
*/
protected abstract void stepImpl();
/** {@inheritDoc} */
@Override
public void step() throws SimRuntimeException
{
Throw.when(isStartingOrRunning(), SimRuntimeException.class, "Cannot step a running simulator");
Throw.when(!isInitialized(), SimRuntimeException.class, "Cannot start an uninitialized simulator");
Throw.when(
!(this.replicationState == ReplicationState.INITIALIZED || this.replicationState == ReplicationState.STARTED),
SimRuntimeException.class, "State of the replication should be INITIALIZED or STARTED to run a simulation");
Throw.when(this.simulatorTime.compareTo(this.replication.getEndTime()) >= 0, SimRuntimeException.class,
"Cannot step simulator : simulatorTime >= runLength");
try
{
synchronized (this.semaphore)
{
if (this.replicationState == ReplicationState.INITIALIZED)
{
fireTimedEvent(Replication.START_REPLICATION_EVENT, null, getSimulatorTime());
this.replicationState = ReplicationState.STARTED;
}
this.runState = RunState.STARTED;
fireTimedEvent(SimulatorInterface.START_EVENT, null, getSimulatorTime());
stepImpl();
}
}
finally
{
fireTimedEvent(SimulatorInterface.STOP_EVENT, null, getSimulatorTime());
this.runState = RunState.STOPPED;
}
}
/**
* Implementation of the stop behavior.
*/
protected void stopImpl()
{
this.runState = RunState.STOPPING;
// sleep maximally 1 second till the SimulatorWorkerThread gets into the WAITING state
int count = 0;
while (!this.worker.isWaiting() && this.worker.isAlive() && count < 1000)
{
try
{
Thread.sleep(1);
count++;
}
catch (InterruptedException exception)
{
// ignore
}
}
}
/** {@inheritDoc} */
@Override
public void stop() throws SimRuntimeException
{
Throw.when(isStoppingOrStopped(), SimRuntimeException.class, "Cannot stop an already stopped simulator");
this.fireEvent(SimulatorInterface.STOPPING_EVENT, null);
stopImpl();
}
/**
* Fire the WARMUP event to clear the statistics after the warmup period. Note that for a discrete event simulator, the
* warmup event can be scheduled, whereas for a continuous simulator, the warmup event must be detected based on the
* simulation time.
*/
public void warmup()
{
fireTimedEvent(Replication.WARMUP_EVENT, null, getSimulatorTime());
}
/** {@inheritDoc} */
@Override
public void cleanUp()
{
stopImpl();
if (hasListeners())
{
this.removeAllListeners();
}
if (this.worker != null)
{
this.worker.cleanUp();
this.worker = null;
}
this.runState = RunState.NOT_INITIALIZED;
this.replicationState = ReplicationState.NOT_INITIALIZED;
}
/** {@inheritDoc} */
@Override
public void endReplication() throws SimRuntimeException
{
Throw.when(!isInitialized(), SimRuntimeException.class, "Cannot end the replication of an uninitialized simulator");
Throw.when(
!(this.replicationState == ReplicationState.INITIALIZED || this.replicationState == ReplicationState.STARTED),
SimRuntimeException.class, "State of the replication should be INITIALIZED or STARTED to end it");
this.replicationState = ReplicationState.ENDING;
if (isStartingOrRunning())
{
this.runState = RunState.STOPPING;
}
this.worker.interrupt(); // just to be sure that the run will end, and the state will be moved to 'ENDED'
if (this.simulatorTime.compareTo(this.getReplication().getEndTime()) < 0)
{
Logger.warn("endReplication executed, but the simulation time " + this.simulatorTime
+ " is earlier than the replication length " + this.getReplication().getEndTime());
this.simulatorTime = this.getReplication().getEndTime();
}
// sleep maximally 1 second till the SimulatorWorkerThread finalizes
int count = 0;
while (this.worker.isAlive() && count < 1000)
{
try
{
Thread.sleep(1);
count++;
}
catch (InterruptedException exception)
{
// ignore
}
}
}
/** {@inheritDoc} */
@Override
public final ErrorStrategy getErrorStrategy()
{
return this.errorStrategy;
}
/** {@inheritDoc} */
@Override
public final void setErrorStrategy(final ErrorStrategy errorStrategy)
{
this.errorStrategy = errorStrategy;
this.errorLogLevel = errorStrategy.getDefaultLogLevel();
}
/** {@inheritDoc} */
@Override
public final void setErrorStrategy(final ErrorStrategy newErrorStrategy, final Level newErrorLogLevel)
{
this.errorStrategy = newErrorStrategy;
this.errorLogLevel = newErrorLogLevel;
}
/** {@inheritDoc} */
@Override
public final Level getErrorLogLevel()
{
return this.errorLogLevel;
}
/** {@inheritDoc} */
@Override
public final void setErrorLogLevel(final Level errorLogLevel)
{
this.errorLogLevel = errorLogLevel;
}
/**
* Handle an exception thrown by executing a SimEvent according to the ErrorStrategy. A call to this method needs to be
* built into the run() method of every Simulator subclass.
* @param exception Exception; the exception that was thrown when executing the SimEvent
*/
protected void handleSimulationException(final Exception exception)
{
String s = "Exception during simulation at t=" + getSimulatorTime() + ": " + exception.getMessage();
switch (this.errorLogLevel)
{
case DEBUG:
CategoryLogger.always().debug(s);
break;
case TRACE:
CategoryLogger.always().trace(s);
break;
case INFO:
CategoryLogger.always().info(s);
break;
case WARNING:
CategoryLogger.always().warn(s);
break;
case ERROR:
CategoryLogger.always().error(s);
break;
default:
break;
}
if (this.errorStrategy.equals(ErrorStrategy.LOG_AND_CONTINUE))
{
return;
}
System.err.println(s);
exception.printStackTrace();
if (this.errorStrategy.equals(ErrorStrategy.WARN_AND_PAUSE))
{
this.runState = RunState.STOPPING;
}
if (this.errorStrategy.equals(ErrorStrategy.WARN_AND_END))
{
cleanUp();
}
if (this.errorStrategy.equals(ErrorStrategy.WARN_AND_EXIT))
{
System.exit(-1);
}
}
/**
* The run method defines the actual time step mechanism of the simulator. The implementation of this method depends on the
* formalism. Where discrete event formalisms loop over an event list, continuous simulators take predefined time steps.
* Make sure that:<br>
* - SimulatorInterface.TIME_CHANGED_EVENT is fired when the time of the simulator changes<br>
* - the warmup() method is called when the warmup period has expired (through an event or based on simulation time)<br>
* - the endReplication() method is called when the replication has ended<br>
* - the simulator runs until the runUntil time, which is also set by the start() method.
*/
@Override
public abstract void run();
/** {@inheritDoc} */
@Override
public T getSimulatorTime()
{
return this.simulatorTime == null ? null : this.simulatorTime;
}
/** {@inheritDoc} */
@Override
public Replication<T> getReplication()
{
return this.replication;
}
/** {@inheritDoc} */
@Override
public DsolModel<T, ? extends SimulatorInterface<T>> getModel()
{
return this.model;
}
/** {@inheritDoc} */
@Override
public SimLogger getLogger()
{
return this.logger;
}
/** {@inheritDoc} */
@Override
public RunState getRunState()
{
return this.runState;
}
/** {@inheritDoc} */
@Override
public ReplicationState getReplicationState()
{
return this.replicationState;
}
/**
* fireTimedEvent method to be called for a no-payload TimedEvent.
* @param event the event to fire at the current time
*/
protected void fireTimedEvent(final EventType event)
{
fireTimedEvent(event, null, getSimulatorTime());
}
/**
* writes a serializable method to stream.
* @param out ObjectOutputStream; the outputstream
* @throws IOException on IOException
*/
private synchronized void writeObject(final ObjectOutputStream out) throws IOException
{
out.writeObject(this.id);
out.writeObject(this.simulatorTime);
out.writeObject(this.replication);
}
/**
* reads a serializable method from stream.
* @param in java.io.ObjectInputStream; the inputstream
* @throws IOException on IOException
*/
@SuppressWarnings("unchecked")
private synchronized void readObject(final java.io.ObjectInputStream in) throws IOException
{
try
{
this.id = (Serializable) in.readObject();
this.simulatorTime = (T) in.readObject();
this.replication = (Replication<T>) in.readObject();
this.semaphore = new Object();
this.worker = new SimulatorWorkerThread(this.id.toString(), this);
this.logger = new SimLogger(this);
}
catch (Exception exception)
{
throw new IOException(exception.getMessage());
}
}
/** The worker thread to execute the run() method of the Simulator and to start/stop the simulation. */
protected static class SimulatorWorkerThread extends Thread
{
/** the job to execute. */
private Simulator<?> job = null;
/** finalized. */
private boolean finalized = false;
/** running. */
private AtomicBoolean running = new AtomicBoolean(false);
/**
* constructs a new SimulatorRunThread.
* @param name String; the name of the thread
* @param job Runnable; the job to run
*/
protected SimulatorWorkerThread(final String name, final Simulator<?> job)
{
super(name);
this.job = job;
this.setDaemon(false);
this.setPriority(Thread.NORM_PRIORITY);
this.start();
}
/**
* Clean up the worker thread. synchronized method, otherwise it does not own the Monitor on the wait.
*/
public synchronized void cleanUp()
{
this.running.set(false);
this.finalized = true;
if (!this.isInterrupted())
{
this.notify(); // in case it is in the 'wait' state
}
}
/**
* @return whether the run method of the job is running or not
*/
public synchronized boolean isRunning()
{
return this.running.get();
}
/**
* @return whether the thread is in the waiting state
*/
public synchronized boolean isWaiting()
{
return this.getState().equals(Thread.State.WAITING);
}
/** {@inheritDoc} */
@Override
public synchronized void run()
{
while (!this.finalized) // always until finalized
{
try
{
this.wait(); // as long as possible
}
catch (InterruptedException interruptedException)
{
if (!this.finalized)
{
if (this.job.replicationState != ReplicationState.ENDING)
{
this.running.set(true);
try
{
this.job.fireTimedEvent(SimulatorInterface.START_EVENT);
this.job.runState = RunState.STARTED;
this.job.run();
this.job.fireTimedEvent(SimulatorInterface.STOP_EVENT);
this.job.runState = RunState.STOPPED;
}
catch (Exception exception)
{
CategoryLogger.always().error(exception);
exception.printStackTrace();
}
}
this.running.set(false);
if (this.job.replicationState == ReplicationState.ENDING)
{
this.job.replicationState = ReplicationState.ENDED;
this.job.runState = RunState.ENDED;
this.job.fireTimedEvent(Replication.END_REPLICATION_EVENT);
this.finalized = true;
}
}
Thread.interrupted(); // clear the interrupted flag
}
}
}
}
}