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

it.unibo.alchemist.model.environments.AbstractEnvironment Maven / Gradle / Ivy

Go to download

Abstract, incarnation independent implementations of the Alchemist's interfaces. Provides support for those who want to write incarnations.

There is a newer version: 35.0.1
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.model.environments;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.collect.Sets;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import gnu.trove.map.hash.TIntObjectHashMap;
import gnu.trove.set.TIntSet;
import gnu.trove.set.hash.TIntHashSet;
import it.unibo.alchemist.core.Simulation;
import it.unibo.alchemist.model.SupportedIncarnations;
import it.unibo.alchemist.model.Environment;
import it.unibo.alchemist.model.GlobalReaction;
import it.unibo.alchemist.model.Incarnation;
import it.unibo.alchemist.model.Layer;
import it.unibo.alchemist.model.LinkingRule;
import it.unibo.alchemist.model.Molecule;
import it.unibo.alchemist.model.Neighborhood;
import it.unibo.alchemist.model.Node;
import it.unibo.alchemist.model.Position;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.danilopianini.util.ArrayListSet;
import org.danilopianini.util.LinkedListSet;
import org.danilopianini.util.ListSet;
import org.danilopianini.util.ListSets;
import org.danilopianini.util.SpatialIndex;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serial;
import java.io.Serializable;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
 * Very generic and basic implementation for an environment. Basically, only
 * manages an internal set of nodes and their position.
 *
 * @param 
 *            concentration type
 * @param 

* {@link Position} type * */ public abstract class AbstractEnvironment> implements Environment { @Serial private static final long serialVersionUID = 0L; private final Map> layers = new LinkedHashMap<>(); private final TIntObjectHashMap> neighCache = new TIntObjectHashMap<>(); private final ListSet> globalReactions = new ArrayListSet<>(); private final ListSet> nodes = new ArrayListSet<>(); private final TIntObjectHashMap

nodeToPos = new TIntObjectHashMap<>(); private final SpatialIndex> spatialIndex; private transient LoadingCache, ListSet>> cache; private transient Incarnation incarnation; private LinkingRule rule; private transient Simulation simulation; private SerializablePredicate terminator = c -> false; /** * @param incarnation the incarnation to be used. * @param internalIndex * the {@link SpatialIndex} to use in order to efficiently * retrieve nodes. */ protected AbstractEnvironment( @Nonnull final Incarnation incarnation, @Nonnull final SpatialIndex> internalIndex ) { spatialIndex = Objects.requireNonNull(internalIndex); this.incarnation = Objects.requireNonNull(incarnation); } @Override public final void addLayer(final Molecule m, final Layer l) { if (layers.put(m, l) != null) { throw new IllegalStateException("Two layers have been associated to " + m); } } /** * {@inheritDoc} */ @Override public void addGlobalReaction(final GlobalReaction reaction) { globalReactions.add(reaction); ifEngineAvailable(simulation -> simulation.reactionAdded(reaction)); } /** * {@inheritDoc} */ @Override public void removeGlobalReaction(final GlobalReaction reaction) { globalReactions.remove(reaction); ifEngineAvailable(simulation -> simulation.reactionRemoved(reaction)); } /** * {@inheritDoc} */ @Override public ListSet> getGlobalReactions() { return ListSets.unmodifiableListSet(globalReactions); } @Override public final boolean addNode(final Node node, final P p) { if (nodeShouldBeAdded(node, p)) { final P actualPosition = computeActualInsertionPosition(node, p); setPosition(node, actualPosition); if (!nodes.add(node)) { throw new IllegalArgumentException("Node with id " + node.getId() + " was already existing in this environment."); } spatialIndex.insert(node, actualPosition.getCoordinates()); /* * Neighborhood computation */ updateNeighborhood(node, true); /* * Reaction and dependencies creation on the engine. This must be * executed only when the neighborhoods have been correctly computed, * and only if a simulation engine has actually been attached. */ ifEngineAvailable(s -> s.nodeAdded(node)); /* * Call the subclass method. */ nodeAdded(node, p, getNeighborhood(node)); return true; } return false; } @Override public final void addTerminator(final Predicate> terminator) { this.terminator = this.terminator.orPredicate(terminator); } /** * Allows subclasses to tune the actual position of a node, applying spatial * constrains at node addition. * * @param node * the node * @param p * the original (requested) position * @return the actual position where the node should be located */ protected abstract P computeActualInsertionPosition(Node node, P p); @Override public final void forEach(final Consumer> action) { getNodes().forEach(action); } private Stream foundNeighbors( final Node center, final Neighborhood oldNeighborhood, final Neighborhood newNeighborhood ) { return newNeighborhood.getNeighbors().stream() .filter(neigh -> oldNeighborhood == null || !oldNeighborhood.contains(neigh)) .filter(neigh -> !getNeighborhood(neigh).contains(center)) .map(n -> new Operation(center, n, true)); } private ListSet> getAllNodesInRange(final P center, final double range) { if (range <= 0) { throw new IllegalArgumentException("Range query must be positive (provided: " + range + ")"); } if (cache == null) { cache = Caffeine.newBuilder() .maximumSize(1000) .build(pair -> runQuery(pair.left, pair.right)); } return cache.get(new ImmutablePair<>(center, range)); } @Override public final double getDistanceBetweenNodes(final Node n1, final Node n2) { return getPosition(n1).distanceTo(getPosition(n2)); } @Nonnull @Override public final Incarnation getIncarnation() { return incarnation; } @Override public final Optional> getLayer(final Molecule m) { return Optional.ofNullable(layers.get(m)); } @Override public final ListSet> getLayers() { return new ArrayListSet<>(layers.values()); } @Override public final LinkingRule getLinkingRule() { return rule; } @Override public final void setLinkingRule(final LinkingRule r) { rule = Objects.requireNonNull(r); } @Override public final Neighborhood getNeighborhood(@Nonnull final Node center) { final Neighborhood result = neighCache.get(Objects.requireNonNull(center).getId()); if (result == null) { if (getNodes().contains(center)) { throw new IllegalStateException("The environment state is inconsistent. " + center + " is among the nodes, but apparently has no position."); } throw new IllegalArgumentException(center + " is not part of the environment."); } return result; } @Override public final Node getNodeByID(final int id) { return (nodes.size() > 1000 ? nodes.parallelStream() : nodes.stream()) .filter(n -> n.getId() == id) .findAny() .orElseThrow(() -> new IllegalArgumentException("Node with id " + id + "does not exist in environment")); } @Override public final ListSet> getNodes() { return ListSets.unmodifiableListSet(nodes); } @Override public final int getNodeCount() { return nodes.size(); } @Override public final ListSet> getNodesWithinRange(final Node center, final double range) { final P centerPosition = getPosition(center); final ListSet> res = new LinkedListSet<>(getAllNodesInRange(centerPosition, range)); if (!res.remove(center)) { throw new IllegalStateException("Either the provided range (" + range + ") is too small" + " for queries to work without losses of precision, or the environment is an inconsistent state." + " Node " + center + " located at " + centerPosition + " was the query center, but within range " + range + " only nodes " + res + " were found in the environment."); } return res; } @Override public final ListSet> getNodesWithinRange(final P center, final double range) { /* * Collect every node in range */ return getAllNodesInRange(center, range); } @Nonnull @Override public final P getPosition(final Node node) { final var position = nodeToPos.get(Objects.requireNonNull(node).getId()); if (position == null) { final var nodeExists = nodes.contains(node); if (nodeExists) { throw new IllegalStateException( "Node " + node + " is registered in the environment, but it has no position." + " This could be a bug in Alchemist, please open an issue report" + " at https://github.com/AlchemistSimulator/Alchemist/issues/new/choose" ); } else { final var nodeType = node.getClass().getSimpleName(); throw new IllegalArgumentException("Node " + node + ": " + nodeType + " does not exist in the environment."); } } return position; } @Override @Nonnull @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "This is intentional") public final Simulation getSimulation() { return Objects.requireNonNull( simulation, "This environment is not attached to any simulation." ); } @Override @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "This is intentional") public final void setSimulation(final Simulation simulation) { if (this.simulation == null) { this.simulation = simulation; } else if (!this.simulation.equals(simulation)) { throw new IllegalStateException( "Inconsistent simulation configuration for " + this + ": simulation was set to " + this.simulation + " (id: " + System.identityHashCode(this.simulation) + ") " + "and then switched to " + simulation + " (id: " + System.identityHashCode(simulation) + ")" ); } } /** * Override this method if units measuring distance do not match with units used * for coordinates. For instance, if your space is non-Euclidean, or if you are * using polar coordinates. A notable example is using geographical * latitude-longitude as y-x coordinates and meters as distance measure. */ @Override public double[] getSizeInDistanceUnits() { return getSize(); } /** * If this environment is attached to a simulation engine, executes consumer. * * @param action the {@link Consumer} to execute */ protected final void ifEngineAvailable(final Consumer> action) { Optional.ofNullable(simulation).ifPresent(action); } private void invalidateCache() { if (cache != null) { cache.invalidateAll(); } } @Override public final boolean isTerminated() { return terminator.test(this); } @Nonnull @Override public final Iterator> iterator() { return getNodes().iterator(); } private Stream lostNeighbors( final Node center, final Neighborhood oldNeighborhood, final Neighborhood newNeighborhood ) { return Optional.ofNullable(oldNeighborhood) .map(Neighborhood::getNeighbors) .orElse(ListSets.emptyListSet()) .stream() .filter(neigh -> !newNeighborhood.contains(neigh)) .filter(neigh -> getNeighborhood(neigh).contains(center)) .map(n -> new Operation(center, n, false)); } /** * This method gets called once a node has been added, and its neighborhood has been computed and memorized. * * @param node the node * @param position the position of the node * @param neighborhood the current neighborhood of the node */ protected abstract void nodeAdded(Node node, P position, Neighborhood neighborhood); /** * This method gets called once a node has been removed. * * @param node * the node * @param neighborhood * the OLD neighborhood of the node (it is no longer in sync with * the {@link Environment} status) */ protected void nodeRemoved(final Node node, final Neighborhood neighborhood) { } /** * Allows subclasses to determine whether or not a {@link Node} should * actually get added to this environment. * * @param node * the node * @param p * the original (requested) position * @return true if the node should be added to this environment, false * otherwise */ protected boolean nodeShouldBeAdded(final Node node, final P p) { return true; } @Serial private void readObject(final ObjectInputStream in) throws ClassNotFoundException, IOException { in.defaultReadObject(); final String name = in.readObject().toString(); incarnation = SupportedIncarnations.get(name).orElseThrow(() -> new IllegalStateException("Unknown incarnation " + name) ); } private Queue recursiveOperation(final Node origin) { final Neighborhood newNeighborhood = rule.computeNeighborhood(Objects.requireNonNull(origin), this); final Neighborhood oldNeighborhood = neighCache.put(origin.getId(), newNeighborhood); return toQueue(origin, oldNeighborhood, newNeighborhood); } private Queue recursiveOperation(final Node origin, final Node destination, final boolean isAdd) { if (isAdd) { ifEngineAvailable(s -> s.neighborAdded(origin, destination)); } else { ifEngineAvailable(s -> s.neighborRemoved(origin, destination)); } final Neighborhood newNeighborhood = rule.computeNeighborhood(Objects.requireNonNull(destination), this); final Neighborhood oldNeighborhood = neighCache.put(destination.getId(), newNeighborhood); return toQueue(destination, oldNeighborhood, newNeighborhood); } @Override public final void removeNode(@Nonnull final Node node) { invalidateCache(); nodes.remove(Objects.requireNonNull(node)); final P pos = nodeToPos.remove(node.getId()); spatialIndex.remove(node, pos.getCoordinates()); /* * Neighborhood update */ final Neighborhood neigh = neighCache.remove(node.getId()); for (final Node n : neigh) { final Neighborhood target = neighCache.remove(n.getId()); neighCache.put(n.getId(), target.remove(node)); } /* * Update all the reactions which may have been affected by the node * removal */ ifEngineAvailable(s -> s.nodeRemoved(node, neigh)); /* * Call subclass remover */ nodeRemoved(node, neigh); } private ListSet> runQuery(final P center, final double range) { final List> result = spatialIndex.query(center.boundingBox(range).stream() .map(Position::getCoordinates) .toArray(double[][]::new)); final int size = result.size(); return ListSets.unmodifiableListSet(result.stream() .filter(it -> getPosition(it).distanceTo(center) <= range) .collect(Collectors.toCollection(() -> new ArrayListSet<>(size)))); } /** * Adds or changes a position entry in the position map. * * @param n * the node * @param p * its new position */ protected final void setPosition(final Node n, final P p) { final P pos = nodeToPos.put(Objects.requireNonNull(n).getId(), Objects.requireNonNull(p)); if (!p.equals(pos)) { invalidateCache(); } if (pos != null && !spatialIndex.move(n, pos.getCoordinates(), p.getCoordinates())) { throw new IllegalArgumentException("Tried to move a node not previously present in the environment: \n" + "Node: " + n + "\n" + "Requested position" + p); } } @Override public final Spliterator> spliterator() { return getNodes().spliterator(); } private Queue toQueue( final Node center, final Neighborhood oldNeighborhood, final Neighborhood newNeighborhood ) { return Stream.concat( lostNeighbors(center, oldNeighborhood, newNeighborhood), foundNeighbors(center, oldNeighborhood, newNeighborhood)) .collect(Collectors.toCollection(LinkedList::new)); } /** * Not used internally. Override as you please. */ @Override public String toString() { return getClass().getSimpleName(); } /** * After a node movement, recomputes the neighborhood, also notifying the * running simulation about the modifications. This allows movement actions * to be defined as LOCAL (they should be normally considered GLOBAL). * * @param node * the node that has been moved * @param isNewNode * true if the node is a new node, false otherwise */ protected final void updateNeighborhood(final Node node, final boolean isNewNode) { /* * The following optimization allows to define as local the context of * reactions which are actually including a move, which should be * normally considered global. This because for each node which is * detached, all the dependencies are updated, ensuring soundness. */ if (Objects.requireNonNull(rule, "No linking rule / network model set.").isLocallyConsistent()) { final Neighborhood newNeighborhood = rule.computeNeighborhood(Objects.requireNonNull(node), this); final Neighborhood oldNeighborhood = neighCache.put(node.getId(), newNeighborhood); /* * Remove the node from all lost neighbors' neighborhoods. */ if (oldNeighborhood != null) { StreamSupport.stream(oldNeighborhood.spliterator(), false) .filter(formerNeighbor -> !newNeighborhood.contains(formerNeighbor)) .map(this::getNeighborhood) .filter(neigh -> neigh.contains(node)) .forEachOrdered(neighborhoodToChange -> { final Node formerNeighbor = neighborhoodToChange.getCenter(); neighCache.put(formerNeighbor.getId(), neighborhoodToChange.remove(node)); if (!isNewNode) { ifEngineAvailable(s -> s.neighborRemoved(node, formerNeighbor)); } }); } /* * Add the node to all gained neighbors' neighborhoods */ final var newNeighbors = Sets.difference( newNeighborhood.getNeighbors(), Optional.ofNullable(oldNeighborhood) .map(Neighborhood::getNeighbors) .orElse(ListSets.emptyListSet()) ); for (final Node newNeighbor: newNeighbors) { neighCache.put(newNeighbor.getId(), neighCache.get(newNeighbor.getId()).add(node)); if (!isNewNode) { ifEngineAvailable(s -> s.neighborAdded(node, newNeighbor)); } } } else { final Queue operations = recursiveOperation(node); final TIntSet processed = new TIntHashSet(getNodeCount()); processed.add(node.getId()); while (!operations.isEmpty()) { final Operation next = operations.poll(); final Node dest = next.destination; final int destId = dest.getId(); if (!processed.contains(destId)) { operations.addAll(recursiveOperation(next.origin, next.destination, next.isAdd)); processed.add(destId); } } } } @Serial private void writeObject(final ObjectOutputStream out) throws IOException { out.defaultWriteObject(); out.writeObject(incarnation.getClass().getSimpleName()); } @FunctionalInterface private interface SerializablePredicate> extends Predicate>, Serializable { default SerializablePredicate orPredicate(final Predicate> other) { return e -> this.test(e) || other.test(e); } } private final class Operation { private final Node destination; private final boolean isAdd; private final Node origin; private Operation(final Node origin, final Node destination, final boolean isAdd) { this.origin = origin; this.destination = destination; this.isAdd = isAdd; } @Override public String toString() { return origin + (isAdd ? " discovered " : " lost ") + destination; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy