AbstractSimulationScript.java

package org.opentrafficsim.swing.script;

import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.djunits.value.vdouble.scalar.Duration;
import org.djunits.value.vdouble.scalar.Time;
import org.djutils.cli.Checkable;
import org.djutils.cli.CliException;
import org.djutils.cli.CliUtil;
import org.djutils.event.Event;
import org.djutils.event.EventListener;
import org.djutils.exceptions.Throw;
import org.djutils.exceptions.Try;
import org.djutils.reflection.ClassUtil;
import org.opentrafficsim.animation.DefaultAnimationFactory;
import org.opentrafficsim.base.OtsRuntimeException;
import org.opentrafficsim.base.logger.Logger;
import org.opentrafficsim.core.dsol.AbstractOtsModel;
import org.opentrafficsim.core.dsol.OtsAnimator;
import org.opentrafficsim.core.dsol.OtsSimulator;
import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
import org.opentrafficsim.core.gtu.Gtu;
import org.opentrafficsim.core.gtu.GtuType;
import org.opentrafficsim.core.network.Network;
import org.opentrafficsim.core.perception.HistoryManagerDevs;
import org.opentrafficsim.draw.colorer.Colorer;
import org.opentrafficsim.draw.gtu.DefaultCarAnimation.GtuData.GtuMarker;
import org.opentrafficsim.road.network.RoadNetwork;
import org.opentrafficsim.swing.gui.AnimationToggles;
import org.opentrafficsim.swing.gui.OtsAnimationPanel;
import org.opentrafficsim.swing.gui.OtsSimulationApplication;
import org.opentrafficsim.swing.gui.OtsSwingApplication;

import nl.tudelft.simulation.dsol.SimRuntimeException;
import nl.tudelft.simulation.dsol.experiment.Replication;
import nl.tudelft.simulation.jstats.streams.MersenneTwister;
import nl.tudelft.simulation.jstats.streams.StreamInterface;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;

/**
 * Template for simulation script. This class allows the user to run a single visualized simulation, or to batch-run the same
 * model. Parameters can be given through the command-line using djutils-ext. Fields can be added to sub-classes using the
 * {@code @Options} and similar annotations. Default values of the properties in this abstract class can be overwritten by the
 * sub-class using {@code CliUtil.changeDefaultValue()}.
 * <p>
 * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
 * </p>
 * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
 * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
 * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
 */
@Command(description = "Test program for CLI", name = "Program", mixinStandardHelpOptions = true, showDefaultValues = true)
public abstract class AbstractSimulationScript implements EventListener, Checkable
{
    /** Name. */
    private final String name;

    /** Description. */
    private final String description;

    /** The simulator. */
    private OtsSimulatorInterface simulator;

    /** The network. */
    private RoadNetwork network;

    /** GTU colorers. */
    private List<Colorer<? super Gtu>> gtuColorers = OtsSwingApplication.DEFAULT_GTU_COLORERS;

    /** Seed. */
    @Option(names = "--seed", description = "Seed", defaultValue = "1")
    private long seed;

    /** Start time. */
    @Option(names = {"-s", "--startTime"}, description = "Start time", defaultValue = "0s")
    private Time startTime;

    /** Warm-up time. */
    @Option(names = {"-w", "--warmupTime"}, description = "Warm-up time", defaultValue = "0s")
    private Duration warmupTime;

    /** Simulation time. */
    @Option(names = {"-t", "--simulationTime"}, description = "Simulation time (including warm-up time)",
            defaultValue = "3600s")
    private Duration simulationTime;

    /** Simulation time. */
    @Option(names = {"-h", "--history"}, description = "Guaranteed history time", defaultValue = "0s")
    private Duration historyTime;

    /** Autorun. */
    @Option(names = {"-a", "--autorun"}, description = "Autorun", negatable = true, defaultValue = "false")
    private boolean autorun;

    /**
     * Constructor.
     * @param name name
     * @param description description
     */
    protected AbstractSimulationScript(final String name, final String description)
    {
        this.name = name;
        this.description = description;
        try
        {
            CliUtil.changeCommandName(this, this.name);
            CliUtil.changeCommandDescription(this, this.description);
            SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            CliUtil.changeCommandVersion(this,
                    formatter.format(new Date(ClassUtil.classFileDescriptorForClass(this.getClass()).getLastChangedDate())));
        }
        catch (IllegalStateException | IllegalArgumentException | CliException exception)
        {
            throw new OtsRuntimeException("Exception while setting properties in @Command annotation.", exception);
        }
    }

    /**
     * Returns the seed.
     * @return seed
     */
    public long getSeed()
    {
        return this.seed;
    }

    /**
     * Returns the start time.
     * @return start time
     */
    public Time getStartTime()
    {
        return this.startTime;
    }

    /**
     * Returns the warm-up time.
     * @return warm-up time
     */
    public Duration getWarmupTime()
    {
        return this.warmupTime;
    }

    /**
     * Returns the simulation time.
     * @return simulation time
     */
    public Duration getSimulationTime()
    {
        return this.simulationTime;
    }

    /**
     * Returns whether to autorun.
     * @return whether to autorun
     */
    public boolean isAutorun()
    {
        return this.autorun;
    }

    /**
     * Set GTU colorers.
     * @param colorers GTU colorers
     */
    public final void setGtuColorers(final List<Colorer<? super Gtu>> colorers)
    {
        this.gtuColorers = colorers;
    }

    /**
     * Returns the GTU colorers.
     * @return returns the GTU colorers
     */
    public final List<Colorer<? super Gtu>> getGtuColorers()
    {
        return this.gtuColorers;
    }

    /**
     * Returns map of (non-default) GTU type markers. The default implementation of this method returns an empty map.
     * @return map of GTU type markers
     */
    public Map<GtuType, GtuMarker> getGtuMarkers()
    {
        return Collections.emptyMap();
    }

    @Override
    public void check() throws Exception
    {
        Throw.when(this.seed < 0, IllegalArgumentException.class, "Seed should be positive");
        Throw.when(this.warmupTime.si < 0.0, IllegalArgumentException.class, "Warm-up time should be positive");
        Throw.when(this.simulationTime.si < 0.0, IllegalArgumentException.class, "Simulation time should be positive");
        Throw.when(this.simulationTime.si < this.warmupTime.si, IllegalArgumentException.class,
                "Simulation time should be longer than warm-up time");
    }

    /**
     * Starts the simulation.
     * @throws Exception on any exception
     */
    public final void start() throws Exception
    {
        if (isAutorun())
        {
            // TODO: wait until simulation control buttons are enabled (indicating that the tabs have been added)
            this.simulator = new OtsSimulator(this.name);
            final ScriptModel scriptModel = new ScriptModel(this.simulator);
            this.simulator.initialize(this.startTime, this.warmupTime, this.simulationTime, scriptModel,
                    new HistoryManagerDevs(this.simulator, this.historyTime, Duration.ofSI(10.0)));
            this.simulator.addListener(this, Replication.END_REPLICATION_EVENT);
            double tReport = 60.0;
            Duration t = this.simulator.getSimulatorTime();
            while (t.si < this.simulationTime.si)
            {
                this.simulator.step();
                t = this.simulator.getSimulatorTime();
                if (t.si >= tReport)
                {
                    Logger.ots().info("Simulation time is " + t);
                    tReport += 60.0;
                }
            }
            // sim.stop(); // end of simulation event
            onSimulationEnd(); // TODO this is temporary for as long as stop() gives an exception
            System.exit(0);
        }
        else
        {
            this.simulator = new OtsAnimator(this.name);
            final ScriptModel scriptModel = new ScriptModel(this.simulator);
            this.simulator.initialize(this.startTime, this.warmupTime, this.simulationTime, scriptModel,
                    new HistoryManagerDevs(this.simulator, this.historyTime, Duration.ofSI(10.0)));
            OtsAnimationPanel animationPanel = new OtsAnimationPanel(scriptModel.getNetwork().getExtent(),
                    (OtsAnimator) this.simulator, scriptModel, getGtuColorers(), scriptModel.getNetwork());
            setAnimationToggles(animationPanel);
            setupDemo(animationPanel, scriptModel.getNetwork());
            OtsSimulationApplication<ScriptModel> app =
                    new OtsSimulationApplication<ScriptModel>(scriptModel, animationPanel, getGtuMarkers())
                    {
                        /** */
                        private static final long serialVersionUID = 20190130L;

                        @Override
                        protected void setAnimationToggles()
                        {
                            // override with nothing to prevent double toggles
                        }
                    };
            addTabs(this.simulator, app);
            app.setExitOnClose(true);
            animationPanel.enableSimulationControlButtons();
        }
    }

    @Override
    public void notify(final Event event)
    {
        if (event.getType().equals(Replication.END_REPLICATION_EVENT))
        {
            // try
            // {
            // getSimulator().scheduleEventNow(this, this, "onSimulationEnd", null);
            // }
            // catch (SimRuntimeException exception)
            // {
            // throw new OtsRuntimeException(exception);
            // }
            onSimulationEnd();
            // solve bug that event is fired twice
            AbstractSimulationScript.this.simulator.removeListener(AbstractSimulationScript.this,
                    Replication.END_REPLICATION_EVENT);
        }
    }

    /**
     * Returns the simulator.
     * @return simulator
     */
    public final OtsSimulatorInterface getSimulator()
    {
        return AbstractSimulationScript.this.simulator;
    }

    /**
     * Returns the network.
     * @return network
     */
    public final RoadNetwork getNetwork()
    {
        return AbstractSimulationScript.this.network;
    }

    // Overridable methods

    /**
     * Creates animations for nodes, links and lanes. This can be used if the network is not read from XML.
     * @param net network
     * @param animationPanel animation panel
     */
    protected void animateNetwork(final Network net, final OtsAnimationPanel animationPanel)
    {
        DefaultAnimationFactory.animateNetwork(net, net.getSimulator(),
                animationPanel.getColorControlPanel().getGtuColorerManager(), getGtuMarkers());
    }

    /**
     * Adds tabs to the animation. May be overridden.
     * @param sim simulator
     * @param animation animation to add tabs to
     */
    protected void addTabs(final OtsSimulatorInterface sim, final OtsSimulationApplication<?> animation)
    {
        //
    }

    /**
     * Method that is called when the simulation has ended. This can be used to store data.
     */
    protected void onSimulationEnd()
    {
        //
    }

    /**
     * Method that is called when the animation has been created, to add components for a demo.
     * @param animationPanel animation panel
     * @param net network
     */
    protected void setupDemo(final OtsAnimationPanel animationPanel, final RoadNetwork net)
    {
        //
    }

    /**
     * Sets the animation toggles. May be overridden.
     * @param animation animation to set the toggle on
     */
    protected void setAnimationToggles(final OtsAnimationPanel animation)
    {
        AnimationToggles.setIconAnimationTogglesStandard(animation);
    }

    // Abstract methods

    /**
     * Sets up the simulation based on provided properties. Properties can be obtained with {@code getProperty()}. Setting up a
     * simulation should at least create a network and some demand. Additionally this may setup traffic control, sampling, etc.
     * @param sim simulator
     * @return network
     * @throws Exception on any exception
     */
    protected abstract RoadNetwork setupSimulation(OtsSimulatorInterface sim) throws Exception;

    // Nested classes

    /**
     * Model.
     * <p>
     * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
     * <br>
     * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
     * </p>
     * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
     * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
     * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
     */
    private class ScriptModel extends AbstractOtsModel
    {
        /**
         * @param simulator the simulator
         */
        @SuppressWarnings("synthetic-access")
        ScriptModel(final OtsSimulatorInterface simulator)
        {
            super(simulator);
            AbstractSimulationScript.this.simulator = simulator;
        }

        @SuppressWarnings("synthetic-access")
        @Override
        public void constructModel() throws SimRuntimeException
        {
            Map<String, StreamInterface> streams = new LinkedHashMap<>();
            StreamInterface stream = new MersenneTwister(getSeed());
            streams.put("generation", stream);
            stream = new MersenneTwister(getSeed() + 1);
            streams.put("default", stream);
            AbstractSimulationScript.this.simulator.getModel().getStreams().putAll(streams);
            AbstractSimulationScript.this.network =
                    Try.assign(() -> AbstractSimulationScript.this.setupSimulation(AbstractSimulationScript.this.simulator),
                            OtsRuntimeException.class, "Exception while setting up simulation.");
            AbstractSimulationScript.this.simulator.addListener(AbstractSimulationScript.this,
                    Replication.END_REPLICATION_EVENT);
        }

        @SuppressWarnings("synthetic-access")
        @Override
        public RoadNetwork getNetwork()
        {
            return AbstractSimulationScript.this.network;
        }

    }

}