All Downloads are FREE. Search and download functionalities are using the official Maven repository.

it.unibo.alchemist.core.Engine Maven / Gradle / Ivy

There is a newer version: 35.0.0
Show newest version
/*
 * Copyright (C) 2010-2023, Danilo Pianini and contributors
 * listed, for each module, in the respective subproject's build.gradle.kts file.
 *
 * This file is part of Alchemist, and is distributed under the terms of the
 * GNU General Public License, with a linking exception,
 * as described in the file LICENSE in the Alchemist distribution's top directory.
 */
package it.unibo.alchemist.core;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import it.unibo.alchemist.boundary.OutputMonitor;
import it.unibo.alchemist.model.Actionable;
import it.unibo.alchemist.model.Context;
import it.unibo.alchemist.model.Dependency;
import it.unibo.alchemist.model.Environment;
import it.unibo.alchemist.model.Neighborhood;
import it.unibo.alchemist.model.Node;
import it.unibo.alchemist.model.Position;
import it.unibo.alchemist.model.Reaction;
import it.unibo.alchemist.model.Time;
import org.jooq.lambda.fi.lang.CheckedRunnable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;

import static it.unibo.alchemist.core.Status.PAUSED;
import static it.unibo.alchemist.core.Status.RUNNING;
import static it.unibo.alchemist.core.Status.TERMINATED;

/**
 * This class implements a simulation. It offers a wide number of static
 * factories to ease the creation process.
 *
 * @param  concentration type
 * @param 

{@link Position} type */ public class Engine> implements Simulation { /** * Logger. */ protected static final Logger LOGGER = LoggerFactory.getLogger(Engine.class); private final Lock statusLock = new ReentrantLock(); private final ImmutableMap statusLocks = Arrays.stream(Status.values()) .collect(ImmutableMap.toImmutableMap(Function.identity(), it -> new SynchBox())); private final BlockingQueue commands = new LinkedBlockingQueue<>(); private final Queue afterExecutionUpdates = new ArrayDeque<>(); private final Environment environment; private final DependencyGraph dependencyGraph; private final Scheduler scheduler; private final List> monitors = new CopyOnWriteArrayList<>(); private volatile Status status = Status.INIT; private Optional error = Optional.empty(); private Time currentTime = Time.ZERO; private long currentStep; private Thread simulationThread; /** * Builds a simulation for a given environment. By default, it uses a * DependencyGraph and an IndexedPriorityQueue internally. If you want to * use your own implementations of {@link DependencyGraph} and * {@link Scheduler} interfaces, don't use this constructor. * * @param environment the environment at the initial time */ public Engine(final Environment environment) { this(environment, new ArrayIndexedPriorityQueue<>()); } /** * Builds a simulation for a given environment. By default, it uses a * DependencyGraph and an IndexedPriorityBatchQueue internally. If you want to * use your own implementations of {@link DependencyGraph} and * {@link Scheduler} interfaces, don'maxTime use this constructor. * * @param environment maxTime * he environment at the initial time * @param scheduler the scheduler implementation to be used */ @SuppressFBWarnings( value = {"EI_EXPOSE_REP2", "MC_OVERRIDABLE_METHOD_CALL_IN_CONSTRUCTOR"}, justification = "Environment and scheduler are not clonable, setSimulation is not final") public Engine( final Environment environment, final Scheduler scheduler ) { LOGGER.trace("Engine created"); this.environment = environment; this.environment.setSimulation(this); dependencyGraph = new JGraphTDependencyGraph<>(this.environment); this.scheduler = scheduler; } /** * @param op the OutputMonitor to add */ @Override public void addOutputMonitor(final OutputMonitor op) { monitors.add(op); } private void checkCaller() { if (this.getClass() != BatchEngine.class && !Thread.currentThread().equals(simulationThread)) { throw new IllegalMonitorStateException("This method must get called from the simulation thread."); } } private R doOnStatus(final Supplier fun) { try { statusLock.lock(); return fun.get(); } finally { statusLock.unlock(); } } private void doOnStatus(final Runnable fun) { doOnStatus(() -> { fun.run(); return 0; }); } /** * Perform a single step of the simulation. */ protected void doStep() { final Actionable nextEvent = scheduler.getNext(); if (nextEvent == null) { this.newStatus(TERMINATED); LOGGER.info("No more reactions."); } else { final Time scheduledTime = nextEvent.getTau(); if (scheduledTime.compareTo(getTime()) < 0) { throw new IllegalStateException( nextEvent + " is scheduled in the past at time " + scheduledTime + ", current time is " + getTime() + ". Problem occurred at step " + getStep() ); } setCurrentTime(scheduledTime); if (nextEvent.canExecute()) { /* * This must be taken before execution, because the reaction * might remove itself (or its node) from the environment. */ nextEvent.getConditions().forEach(it.unibo.alchemist.model.Condition::reactionReady); nextEvent.execute(); Set> toUpdate = dependencyGraph.outboundDependencies(nextEvent); if (!afterExecutionUpdates.isEmpty()) { afterExecutionUpdates.forEach(Update::performChanges); afterExecutionUpdates.clear(); toUpdate = Sets.union(toUpdate, dependencyGraph.outboundDependencies(nextEvent)); } toUpdate.forEach(this::updateReaction); } nextEvent.update(getTime(), true, environment); scheduler.updateReaction(nextEvent); for (final OutputMonitor monitor : monitors) { monitor.stepDone(environment, nextEvent, getTime(), getStep()); } } if (environment.isTerminated()) { newStatus(TERMINATED); LOGGER.info("Termination condition reached."); } final var newStep = getStep() + 1; setCurrentStep(newStep); } private void finalizeConstructor() { this.environment.getGlobalReactions().forEach(this::reactionAdded); for (final Node n : environment) { for (final Reaction r : n.getReactions()) { scheduleReaction(r); } } } /** * @return environment */ @Override @SuppressFBWarnings("EI_EXPOSE_REP") public Environment getEnvironment() { return environment; } /** * @return error */ @Override public Optional getError() { return error; } /** * @return status */ @Override public Status getStatus() { return status; } /** * thread-safe. * * @return step */ @Override public synchronized long getStep() { return currentStep; } /** * thread-safe. * * @return time */ @Override public synchronized Time getTime() { return currentTime; } /** * @param step the number of steps to execute */ @Override public CompletableFuture goToStep(final long step) { return pauseWhen(() -> getStep() >= step); } /** * @param t the target time */ @Override public CompletableFuture goToTime(final Time t) { return pauseWhen(() -> getTime().compareTo(t) >= 0); } private void idleProcessSingleCommand() throws Throwable { CheckedRunnable nextCommand = null; // This is for spurious wakeups. Blame Java. while (nextCommand == null) { try { nextCommand = commands.take(); processCommand(nextCommand); } catch (InterruptedException e) { LOGGER.debug("Look! A spurious wakeup! :-)"); } } } /** * @param node the node * @param n the second node */ @Override public void neighborAdded(final Node node, final Node n) { checkCaller(); afterExecutionUpdates.add(new NeigborAdded(node, n)); } /** * @param node the node * @param n the second node */ @Override public void neighborRemoved(final Node node, final Node n) { checkCaller(); afterExecutionUpdates.add(new NeigborRemoved(node, n)); } /** * Sets new status. * * @param next next status * * @return a future that completes when the status is set to {@code next} */ protected CompletableFuture newStatus(final Status next) { final var future = new CompletableFuture(); schedule(() -> doOnStatus(() -> { if (next.isReachableFrom(status)) { status = next; lockForStatus(next).releaseAll(); } future.complete(null); })); return future; } /** * @param node the freshly added node */ @Override public void nodeAdded(final Node node) { checkCaller(); afterExecutionUpdates.add(new NodeAddition(node)); } /** * @param node the node */ @Override public void nodeMoved(final Node node) { checkCaller(); afterExecutionUpdates.add(new Movement(node)); } /** * @param node the freshly removed node * @param oldNeighborhood the neighborhood of the node as it was before it was removed * (used to calculate reverse dependencies) */ @Override public void nodeRemoved(final Node node, final Neighborhood oldNeighborhood) { checkCaller(); afterExecutionUpdates.add(new NodeRemoval(node)); } /** * pause. */ @Override public CompletableFuture pause() { return newStatus(PAUSED); } /** * play. */ @Override public CompletableFuture play() { return newStatus(RUNNING); } /** * @param reactionToAdd the reaction to add */ @Override public void reactionAdded(final Actionable reactionToAdd) { reactionChanged(new ReactionAddition(reactionToAdd)); } /** * @param reactionToRemove the reaction to remove */ @Override public void reactionRemoved(final Actionable reactionToRemove) { reactionChanged(new ReactionRemoval(reactionToRemove)); } private void reactionChanged(final UpdateOnReaction update) { checkCaller(); afterExecutionUpdates.add(update); } private Stream> reactionsToUpdateAfterExecution() { return afterExecutionUpdates.stream() .flatMap(Update::getReactionsToUpdate) .distinct(); } private void processCommand(final CheckedRunnable command) throws Throwable { command.run(); // Update all reactions before applying dependency graph updates final Set> updated = new LinkedHashSet<>(); reactionsToUpdateAfterExecution().forEach(r -> { updated.add(r); updateReaction(r); }); // Now update the dependency graph as needed afterExecutionUpdates.forEach(Update::performChanges); afterExecutionUpdates.clear(); // Now update the new reactions reactionsToUpdateAfterExecution().forEach(r -> { if (!updated.contains(r)) { updateReaction(r); } }); } /** * @param op the OutputMonitor to add */ @Override public void removeOutputMonitor(final OutputMonitor op) { monitors.remove(op); } /** * run simulation. */ @Override public void run() { synchronized (environment) { try { LOGGER.info("Starting engine {} with scheduler {}", this.getClass(), scheduler.getClass()); simulationThread = Thread.currentThread(); finalizeConstructor(); status = Status.READY; final long currentThread = Thread.currentThread().getId(); LOGGER.trace("Thread {} started running.", currentThread); for (final OutputMonitor m : monitors) { m.initialized(environment); } while (status.equals(Status.READY)) { idleProcessSingleCommand(); } while (status != TERMINATED && getTime().compareTo(Time.INFINITY) < 0) { while (!commands.isEmpty()) { processCommand(commands.poll()); } if (status.equals(RUNNING)) { doStep(); } while (status.equals(PAUSED)) { idleProcessSingleCommand(); } } } catch (Throwable e) { // NOPMD: forced by CheckedRunnable error = Optional.of(e); LOGGER.error("The simulation engine crashed.", e); } finally { status = TERMINATED; commands.clear(); try { for (final OutputMonitor m : monitors) { m.finished(environment, getTime(), getStep()); } } catch (Throwable e) { //NOPMD: we need to catch everything error.ifPresentOrElse( error -> error.addSuppressed(e), () -> error = Optional.of(e) ); } afterRun(); } } } /** * Override this to execute something after simulation run. */ protected void afterRun() { // do nothing, leave for override... } private CompletableFuture pauseWhen(final BooleanSupplier condition) { final var future = new CompletableFuture(); addOutputMonitor(new OutputMonitor<>() { @Override public void initialized(@Nonnull final Environment environment) { if (condition.getAsBoolean()) { monitors.remove(this); pause().thenRun(() -> future.complete(null)); } } @Override public void stepDone( @Nonnull final Environment environment, @Nullable final Actionable reaction, @Nonnull final Time time, final long step ) { initialized(environment); } }); return future; } /** * @param runnable the runnable to execute */ @Override public void schedule(final CheckedRunnable runnable) { if (getStatus().equals(TERMINATED)) { throw new IllegalStateException("This simulation is terminated and can not get resumed."); } commands.add(runnable); } private void scheduleReaction(final Actionable reaction) { dependencyGraph.createDependencies(reaction); reaction.initializationComplete(getTime(), environment); scheduler.addReaction(reaction); } /** * terminate. */ @Override public CompletableFuture terminate() { return newStatus(TERMINATED); } /** * @return string representation of the engine */ @Override public String toString() { return getClass().getSimpleName() + " t: " + getTime() + ", s: " + getStep(); } /** * update reaction. * * @param r reaction to be updated */ protected void updateReaction(final Actionable r) { final Time t = r.getTau(); r.update(getTime(), false, environment); if (!r.getTau().equals(t)) { scheduler.updateReaction(r); } } private SynchBox lockForStatus(final Status status) { final var statusLock = statusLocks.get(status); if (statusLock == null) { throw new IllegalStateException( "Inconsistent state, the Alchemist engine tried to synchronize on a non-existing lock" + "searching for status: " + status + ", available locks: " + statusLocks ); } return statusLock; } /** * @param next The {@link Status} the simulation should reach before returning from this method * @param timeout The maximum lapse of time the caller wants to wait before being resumed * @param tu The {@link TimeUnit} used to define "timeout" * @return status */ @Override public Status waitFor(final Status next, final long timeout, final TimeUnit tu) { return lockForStatus(next).waitFor(next, timeout, tu); } /** * Returns output monitors. * * @return output monitors */ @Override public List> getOutputMonitors() { return ImmutableList.copyOf(monitors); } /** * Returns after execution updates. * * @return after execution updates */ protected Queue getAfterExecutionUpdates() { return afterExecutionUpdates; } /** * Returns dependency graph. * * @return dependency graph */ protected DependencyGraph getDependencyGraph() { return dependencyGraph; } /** * Returns scheduler. * * @return scheduler */ protected Scheduler getScheduler() { return scheduler; } /** * Returns status lock. * * @return status locks */ protected ImmutableMap getStatusLocks() { return statusLocks; } /** * Returns monitors. * * @return monitors */ protected List> getMonitors() { return monitors; } /** * thread safe. Updates current time * * @param currentTime new current time */ protected synchronized void setCurrentTime(final Time currentTime) { this.currentTime = currentTime; } /** * thread safe. Updates current step. * * @param currentStep new current step */ protected synchronized void setCurrentStep(final long currentStep) { this.currentStep = currentStep; } /** * Class representing an update. */ // CHECKSTYLE: FinalClassCheck OFF protected class Update { /** * Perform changes. */ public void performChanges() { // override me } /** * @return reactions to update */ public Stream> getReactionsToUpdate() { return Stream.empty(); } } private final class Movement extends Update { private final @Nonnull Node sourceNode; private Movement(final @Nonnull Node sourceNode) { this.sourceNode = sourceNode; } @Override public Stream> getReactionsToUpdate() { return getReactionsRelatedTo(this.sourceNode, environment.getNeighborhood(this.sourceNode)) .filter(it -> it.getInboundDependencies().stream() .anyMatch(dependency -> dependency.dependsOn(Dependency.MOVEMENT)) ); } private Stream> getReactionsRelatedTo( final Node source, final Neighborhood neighborhood ) { return Stream.of( source.getReactions().stream(), neighborhood.getNeighbors().stream() .flatMap(node -> node.getReactions().stream()) .filter(it -> it.getInputContext() == Context.NEIGHBORHOOD), dependencyGraph.globalInputContextReactions().stream()) .reduce(Stream.empty(), Stream::concat); } } private class UpdateOnNode extends Update { private final Function, Update> reactionLevelOperation; private final Node sourceNode; private UpdateOnNode(final Node sourceNode, final Function, Update> reactionLevelOperation) { this.sourceNode = sourceNode; this.reactionLevelOperation = reactionLevelOperation; } @Override public final void performChanges() { this.sourceNode.getReactions().stream() .map(reactionLevelOperation) .forEach(Update::performChanges); } } private final class NodeRemoval extends UpdateOnNode { private NodeRemoval(final Node sourceNode) { super(sourceNode, ReactionRemoval::new); } } private final class NodeAddition extends UpdateOnNode { private NodeAddition(final Node sourceNode) { super(sourceNode, ReactionAddition::new); } } private abstract class UpdateOnReaction extends Update { private final Actionable sourceActionable; private UpdateOnReaction(final Actionable sourceActionable) { this.sourceActionable = sourceActionable; } @Override public final Stream> getReactionsToUpdate() { return Stream.of(sourceActionable); } protected Actionable getSourceReaction() { return sourceActionable; } } private final class ReactionRemoval extends UpdateOnReaction { private ReactionRemoval(final Actionable source) { super(source); } @Override public void performChanges() { dependencyGraph.removeDependencies(getSourceReaction()); scheduler.removeReaction(getSourceReaction()); } } private final class ReactionAddition extends UpdateOnReaction { private ReactionAddition(final Actionable source) { super(source); } @Override public void performChanges() { Engine.this.scheduleReaction(getSourceReaction()); } } private class NeighborhoodChanged extends Update { private final Node sourceNode; private final Node targetNode; private NeighborhoodChanged(final Node sourceNode, final Node targetNode) { this.sourceNode = sourceNode; this.targetNode = targetNode; } public Node getTargetNode() { return targetNode; } public Node getSourceNode() { return sourceNode; } @Override public Stream> getReactionsToUpdate() { return Stream.of( Stream.concat( // source, target, and all their neighbors are candidates. Stream.of(sourceNode, targetNode), Stream.of(environment.getNeighborhood(sourceNode), environment.getNeighborhood(getTargetNode())) .flatMap(it -> it.getNeighbors().stream())) .distinct() .flatMap(it -> it.getReactions().stream()) .filter(it -> it.getInputContext() == Context.NEIGHBORHOOD), // Global reactions dependencyGraph.globalInputContextReactions().stream()) .reduce(Stream.empty(), Stream::concat); } } private final class NeigborAdded extends NeighborhoodChanged { private NeigborAdded(final Node source, final Node target) { super(source, target); } @Override public void performChanges() { dependencyGraph.addNeighbor(getSourceNode(), getTargetNode()); } } private final class NeigborRemoved extends NeighborhoodChanged { private NeigborRemoved(final Node source, final Node target) { super(source, target); } @Override public void performChanges() { dependencyGraph.removeNeighbor(getSourceNode(), getTargetNode()); } } private final class SynchBox { private final AtomicInteger queueLength = new AtomicInteger(); private final Condition statusReached = statusLock.newCondition(); private final Condition allReleased = statusLock.newCondition(); public Status waitFor(final Status next, final long timeout, final TimeUnit tu) { return doOnStatus(() -> { boolean notTimedOut = true; while (notTimedOut && next != status && next.isReachableFrom(status)) { try { queueLength.getAndIncrement(); notTimedOut = statusReached.await(timeout, tu); queueLength.getAndDecrement(); } catch (InterruptedException e) { LOGGER.info("Spurious wakeup?", e); } } if (queueLength.get() == 0) { allReleased.signal(); } return status; }); } public void releaseAll() { doOnStatus(() -> { while (queueLength.get() != 0) { statusReached.signalAll(); allReleased.awaitUninterruptibly(); } }); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy