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
                }
            }
        }
    }
}