Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
it.unibo.alchemist.model.protelis.ProtelisIncarnation Maven / Gradle / Ivy
Go to download
Implementation of the Alchemist's meta-meta model supporting execution of Protelis programs
/*
* 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.protelis;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import it.unibo.alchemist.model.Action;
import it.unibo.alchemist.model.Actionable;
import it.unibo.alchemist.model.Condition;
import it.unibo.alchemist.model.Environment;
import it.unibo.alchemist.model.Incarnation;
import it.unibo.alchemist.model.Molecule;
import it.unibo.alchemist.model.Node;
import it.unibo.alchemist.model.NodeProperty;
import it.unibo.alchemist.model.Position;
import it.unibo.alchemist.model.Reaction;
import it.unibo.alchemist.model.Time;
import it.unibo.alchemist.model.TimeDistribution;
import it.unibo.alchemist.protelis.actions.RunProtelisProgram;
import it.unibo.alchemist.model.protelis.actions.SendToNeighbor;
import it.unibo.alchemist.model.protelis.conditions.ComputationalRoundComplete;
import it.unibo.alchemist.model.molecules.SimpleMolecule;
import it.unibo.alchemist.model.nodes.GenericNode;
import it.unibo.alchemist.protelis.properties.ProtelisDevice;
import it.unibo.alchemist.model.reactions.ChemicalReaction;
import it.unibo.alchemist.model.reactions.Event;
import it.unibo.alchemist.model.timedistributions.DiracComb;
import it.unibo.alchemist.model.timedistributions.ExponentialTime;
import it.unibo.alchemist.model.times.DoubleTime;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.math3.random.MersenneTwister;
import org.apache.commons.math3.random.RandomGenerator;
import org.jetbrains.annotations.NotNull;
import org.protelis.lang.ProtelisLoader;
import org.protelis.lang.datatype.DeviceUID;
import org.protelis.vm.CodePath;
import org.protelis.vm.ExecutionEnvironment;
import org.protelis.vm.NetworkManager;
import org.protelis.vm.ProtelisVM;
import org.protelis.vm.impl.AbstractExecutionContext;
import org.protelis.vm.impl.SimpleExecutionEnvironment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Serial;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @param position type
*/
public final class ProtelisIncarnation
> implements Incarnation {
private static final Logger LOGGER = LoggerFactory.getLogger(ProtelisIncarnation.class);
/**
* The name that can be used in a property to refer to the extracted value.
*/
public static final String VALUE_TOKEN = "";
/**
* Statically-referenceable instance. This incarnation *can* work as a singleton, and doing so may save some
* memory. However, it is not strictly a singleton (multiple instances do not do harm).
*/
public static final ProtelisIncarnation> INSTANCE = new ProtelisIncarnation<>();
private final LoadingCache cache = CacheBuilder
.newBuilder()
.expireAfterAccess(10, TimeUnit.MINUTES)
.build(new CacheLoader<>() {
@NotNull
@Override
public SynchronizedVM load(@Nonnull final CacheKey key) {
return new SynchronizedVM(key);
}
});
@Nonnull
private static List> getIncomplete(
final Node> protelisNode,
final List> alreadyDone
) {
return protelisNode.getReactions().stream()
/*
* Get the actions
*/
.flatMap(r -> r.getActions().stream())
/*
* Get only the ProtelisPrograms
*/
.filter(a -> a instanceof RunProtelisProgram)
.map(a -> (RunProtelisProgram>) a)
/*
* Retain only those ProtelisPrograms that have no associated ComputationalRoundComplete.
*
* Only one should be available.
*/
.filter(prog -> !alreadyDone.contains(prog))
.collect(Collectors.toList());
}
@SuppressWarnings("unchecked")
private void checkIsProtelisNode(final Node node, final String exceptionMessage) {
if (node == null || node.asPropertyOrNull(ProtelisDevice.class) == null) {
throw new IllegalArgumentException(exceptionMessage);
}
}
@Override
public Action createAction(
final RandomGenerator randomGenerator,
final Environment environment,
final Node node,
final TimeDistribution time,
final Actionable actionable,
final @Nullable Object additionalParameters
) {
final String parameters = additionalParameters == null ? null : additionalParameters.toString();
if (actionable instanceof Reaction) {
Objects.requireNonNull(additionalParameters);
final ProtelisDevice device = node.asPropertyOrNull(ProtelisDevice.class);
if (device == null) {
throw new IllegalArgumentException("The node must be a " + ProtelisDevice.class.getSimpleName());
}
if ("send".equalsIgnoreCase(parameters)) {
final List> alreadyDone = node.getReactions()
.stream()
.flatMap(r -> r.getActions().stream())
.filter(a -> a instanceof SendToNeighbor)
.map(c -> ((SendToNeighbor) c).getProtelisProgram())
.collect(Collectors.toList());
final List> pList = getIncomplete(node, alreadyDone);
if (pList.isEmpty()) {
throw new IllegalStateException(
"There is no program requiring a " + SendToNeighbor.class.getSimpleName() + " action"
);
}
if (pList.size() > 1) {
throw new IllegalStateException(
"There are too many programs requiring a " + SendToNeighbor.class.getName()
+ " action: " + pList
);
}
return new SendToNeighbor(node, (Reaction) actionable, pList.get(0));
} else {
try {
return new RunProtelisProgram<>(
randomGenerator,
environment,
device,
(Reaction) actionable,
parameters
);
} catch (RuntimeException exception) { // NOPMD AvoidCatchingGenericException
throw new IllegalArgumentException(
"Could not create the requested Protelis program: " + additionalParameters,
exception
);
}
}
}
throw new IllegalArgumentException(
"The provided actionable must be an instance of " + Reaction.class.getSimpleName()
);
}
@Override
public Object createConcentration(@Nullable final Object descriptor) {
try {
if (descriptor != null) {
final var program = descriptor.toString();
final SynchronizedVM vm = new SynchronizedVM(
new CacheKey(NoNode.INSTANCE, createMolecule(program), program)
);
return vm.runCycle();
}
} catch (IllegalArgumentException e) {
/*
* Not a valid program: inject the Object itself
*/
LOGGER.warn("Invalid Protelis program injected as concentration:\n" + descriptor, e);
}
return descriptor;
}
@Override
public Object createConcentration() {
return null;
}
@Override
public Condition createCondition(
final RandomGenerator randomGenerator,
final Environment environment,
final Node node,
final TimeDistribution time,
final Actionable actionable,
final @Nullable Object additionalParameters
) {
if (actionable instanceof Reaction) {
checkIsProtelisNode(node, "The node must have a " + ProtelisDevice.class.getSimpleName());
/*
* The list of ProtelisPrograms that have already been completed with a ComputationalRoundComplete condition
*/
final List> alreadyDone = node.getReactions()
.stream()
.flatMap(r -> r.getConditions().stream())
.filter(c -> c instanceof ComputationalRoundComplete)
.map(c -> ((ComputationalRoundComplete) c).getProgram())
.collect(Collectors.toList());
final List> pList = getIncomplete(node, alreadyDone);
if (pList.isEmpty()) {
throw new IllegalStateException(
"There is no program requiring a " + ComputationalRoundComplete.class.getSimpleName() + " condition"
);
}
if (pList.size() > 1) {
throw new IllegalStateException(
"There are too many programs requiring a " + ComputationalRoundComplete.class.getSimpleName()
+ " condition: " + pList
);
}
return new ComputationalRoundComplete(node, pList.get(0));
}
throw new IllegalArgumentException(
"The provided actionable should be an instance of " + Reaction.class.getSimpleName()
);
}
@Override
public Molecule createMolecule(final String s) {
return new SimpleMolecule(Objects.requireNonNull(s));
}
@Override
public Node createNode(
final RandomGenerator randomGenerator,
final Environment environment,
final @Nullable Object parameter
) {
final Node node = new GenericNode<>(this, environment);
node.addProperty(new ProtelisDevice<>(environment, node));
return node;
}
@Override
public Reaction createReaction(
final RandomGenerator randomGenerator,
final Environment environment,
final Node node,
final TimeDistribution timeDistribution,
final @Nullable Object parameter
) {
final String parameterString = parameter == null ? null : parameter.toString();
final boolean isSend = "send".equalsIgnoreCase(parameterString);
final Reaction result = isSend
? new ChemicalReaction<>(Objects.requireNonNull(node), Objects.requireNonNull(timeDistribution))
: new Event<>(node, timeDistribution);
if (parameter != null) {
result.setActions(
Lists.newArrayList(
createAction(randomGenerator, environment, node, timeDistribution, result, parameter)
)
);
}
if (isSend) {
result.setConditions(
Lists.newArrayList(createCondition(
randomGenerator,
environment,
node,
timeDistribution,
result,
null
)
)
);
}
return result;
}
@Override
public TimeDistribution createTimeDistribution(
final RandomGenerator randomGenerator,
final Environment environment,
final Node node,
final @Nullable Object parameter
) {
if (parameter == null) {
return new ExponentialTime<>(Double.POSITIVE_INFINITY, randomGenerator);
}
try {
final double frequency = parameter instanceof Number ? ((Number) parameter).doubleValue()
: Double.parseDouble(parameter.toString());
return new DiracComb<>(new DoubleTime(randomGenerator.nextDouble() / frequency), frequency);
} catch (final NumberFormatException e) {
LOGGER.error("Unable to convert {} to a double", parameter);
throw e;
}
}
@Override
public double getProperty(final Node node, final Molecule molecule, final String property) {
try {
final SynchronizedVM vm = cache.get(
new CacheKey(
Objects.requireNonNull(node),
Objects.requireNonNull(molecule),
property == null ? "" : property
)
);
final Object val = vm.runCycle();
if (val instanceof Number) {
return ((Number) val).doubleValue();
} else if (val instanceof String) {
try {
return Double.parseDouble(val.toString());
} catch (final NumberFormatException e) {
if (val.equals(property)) {
return 1;
}
return 0;
}
} else if (val instanceof final Boolean cond) {
if (cond) {
return 1d;
} else {
return 0d;
}
}
} catch (ExecutionException | RuntimeException e) { // NOPMD: we never want getProperty to fail
LOGGER.error(
"Intercepted interpreter exception when computing: \n"
+ property + "\n"
+ e.getMessage()
);
}
return Double.NaN;
}
@Override
public String toString() {
return getClass().getSimpleName();
}
private static final class CacheKey {
private final Molecule molecule;
private final WeakReference> node;
private final String property;
private final int hash;
private CacheKey(final Node node, final Molecule mol, final String prop) {
this.node = new WeakReference<>(node);
molecule = mol;
property = prop;
hash = molecule.hashCode() ^ property.hashCode() ^ (node == null ? 0 : node.hashCode());
}
@Override
public boolean equals(final Object obj) {
return obj instanceof CacheKey
&& ((CacheKey) obj).node.get() == node.get() // NOPMD: this comparison is intentional
&& ((CacheKey) obj).molecule.equals(molecule)
&& ((CacheKey) obj).property.equals(property);
}
@Override
public int hashCode() {
return hash;
}
}
/**
* An {@link org.protelis.vm.ExecutionContext} that operates over a node, but does not
* modify it.
*/
private static final class DummyContext extends AbstractExecutionContext {
private static final Semaphore MUTEX = new Semaphore(1);
private static final int SEED = -241_837_578;
private static final RandomGenerator RNG = new MersenneTwister(SEED);
private static final DeviceUID NO_NODE_ID = new DeviceUID() {
@Override
public String toString() {
return "Wapper over a non-ProtelisNode for an invalid DeviceUID, meant to host local-only computation.";
}
};
private final Node node;
private DummyContext(final Node node) {
super(new ProtectedExecutionEnvironment(node), new NetworkManager() {
@Override
public Map> getNeighborState() {
return Collections.emptyMap();
}
@Override
public void shareState(final Map toSend) {
}
});
this.node = node;
}
@Override
public Number getCurrentTime() {
return 0;
}
@Override
@SuppressFBWarnings("EI_EXPOSE_REP")
public DeviceUID getDeviceUID() {
final ProtelisDevice> protelisProperty = node.asPropertyOrNull(ProtelisDevice.class);
return protelisProperty != null ? protelisProperty : NO_NODE_ID;
}
@Override
protected DummyContext instance() {
return this;
}
@Override
public double nextRandomDouble() {
final double result;
MUTEX.acquireUninterruptibly();
result = RNG.nextDouble();
MUTEX.release();
return result;
}
}
/**
* An {@link ExecutionEnvironment} that can read and shadow the content of a
* Node, but cannot modify it. This is used to prevent badly written
* properties to interact with the simulation flow.
*/
public static final class ProtectedExecutionEnvironment implements ExecutionEnvironment {
private final Node> node;
private final ExecutionEnvironment shadow = new SimpleExecutionEnvironment();
/**
* @param node the {@link Node}
*/
@SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "This is intentional")
public ProtectedExecutionEnvironment(final Node> node) {
this.node = node;
}
@Override
public void commit() {
}
@Override
public Object get(final String id) {
return shadow.get(id, node.getConcentration(new SimpleMolecule(id)));
}
@Override
public Object get(final String id, final Object defaultValue) {
return Optional.ofNullable(get(id)).orElse(defaultValue);
}
@Override
public boolean has(final String id) {
return shadow.has(id) || node.contains(new SimpleMolecule(id));
}
@Override
public boolean put(final String id, final Object v) {
return shadow.put(id, v);
}
@Override
public Object remove(final String id) {
return shadow.remove(id);
}
@Override
public void setup() {
}
@Override
public Set keySet() {
return Sets.union(
node.getContents().keySet().stream()
.map(Molecule::getName)
.collect(Collectors.toSet()), shadow.keySet()
);
}
}
private static final class SynchronizedVM {
private final CacheKey key;
private final Semaphore mutex = new Semaphore(1);
private final Optional vm;
private SynchronizedVM(final CacheKey key) {
this.key = key;
ProtelisVM myVM = null;
if (!StringUtils.isBlank(key.property)) {
try {
final String baseProgram = "env.get(\"" + key.molecule.getName() + "\")";
myVM = new ProtelisVM(
ProtelisLoader.parse(key.property.replace(VALUE_TOKEN, baseProgram)),
new DummyContext(key.node.get()));
} catch (RuntimeException ex) { // NOPMD AvoidCatchingGenericException
LOGGER.warn("Program ignored as invalid: \n" + key.property);
LOGGER.debug("Debug information", ex);
}
}
vm = Optional.ofNullable(myVM);
}
public Object runCycle() {
final Node node = key.node.get();
if (node == null) {
throw new IllegalStateException("The node should never be null");
}
if (vm.isPresent()) {
final ProtelisVM myVM = vm.get();
mutex.acquireUninterruptibly();
try {
myVM.runCycle();
return myVM.getCurrentValue();
} finally {
mutex.release();
}
}
if (node instanceof NoNode) {
return key.property;
}
return node.getConcentration(key.molecule);
}
}
private static final class NoNode implements Node {
public static final NoNode INSTANCE = new NoNode();
@Serial
private static final long serialVersionUID = 1L;
private A notImplemented() {
throw new UnsupportedOperationException("Method can't be invoked in this context.");
}
@Override
@Nonnull
public Iterator> iterator() {
return notImplemented();
}
@Override
public int compareTo(@Nonnull final Node o) {
return notImplemented();
}
@Override
public void addReaction(@NotNull final Reaction r) {
notImplemented();
}
@Override
public boolean contains(@NotNull final Molecule mol) {
return notImplemented();
}
@Override
public int getMoleculeCount() {
return notImplemented();
}
@Override
public Object getConcentration(@NotNull final Molecule mol) {
return notImplemented();
}
@NotNull
@Override
public Map getContents() {
return notImplemented();
}
@Override
public int getId() {
return notImplemented();
}
@NotNull
@Override
public List> getReactions() {
return Collections.emptyList();
}
@Override
public void removeConcentration(@NotNull final Molecule mol) {
notImplemented();
}
@Override
public void removeReaction(@NotNull final Reaction r) {
notImplemented();
}
@Override
public void setConcentration(@NotNull final Molecule mol, final Object c) {
notImplemented();
}
@NotNull
@Override
public Node cloneNode(@NotNull final Time t) {
return notImplemented();
}
@Override
public boolean equals(final Object obj) {
return obj instanceof NoNode;
}
@Override
public int hashCode() {
return -1;
}
@Override
public void addProperty(@NotNull final NodeProperty nodeProperty) {
notImplemented();
}
@NotNull
@Override
public List> getProperties() {
return Collections.emptyList();
}
}
}