org.integratedmodelling.engine.modelling.ObservationController Maven / Gradle / Ivy
The newest version!
/*******************************************************************************
* Copyright (C) 2007, 2015:
*
* - Ferdinando Villa - integratedmodelling.org - any
* other authors listed in @author annotations
*
* All rights reserved. This file is part of the k.LAB software suite, meant to enable
* modular, collaborative, integrated development of interoperable data and model
* components. For details, see http://integratedmodelling.org.
*
* This program is free software; you can redistribute it and/or modify it under the terms
* of the Affero General Public License Version 3 or any later version.
*
* This program is distributed in the hope that it will be useful, but without any
* warranty; without even the implied warranty of merchantability or fitness for a
* particular purpose. See the Affero General Public License for more details.
*
* You should have received a copy of the Affero General Public License along with this
* program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite
* 330, Boston, MA 02111-1307, USA. The license is also available at:
* https://www.gnu.org/licenses/agpl.html
*******************************************************************************/
package org.integratedmodelling.engine.modelling;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Set;
import org.integratedmodelling.api.modelling.IActiveDirectObservation;
import org.integratedmodelling.api.modelling.IActiveSubject;
import org.integratedmodelling.api.modelling.IDirectObservation;
import org.integratedmodelling.api.modelling.agents.IAgentState;
import org.integratedmodelling.api.modelling.agents.ICollision;
import org.integratedmodelling.api.modelling.agents.IObservationController;
import org.integratedmodelling.api.modelling.agents.IObservationGraphNode;
import org.integratedmodelling.api.modelling.agents.IObservationGraphNodeSubscriber;
import org.integratedmodelling.api.modelling.agents.IObservationTask;
import org.integratedmodelling.api.modelling.scheduling.ITransition;
import org.integratedmodelling.api.monitoring.IMonitor;
import org.integratedmodelling.api.time.ITemporalSeries;
import org.integratedmodelling.api.time.ITimeInstant;
import org.integratedmodelling.api.time.ITimePeriod;
import org.integratedmodelling.collections.Pair;
import org.integratedmodelling.common.configuration.KLAB;
import org.integratedmodelling.engine.introspection.CallTracer;
import org.integratedmodelling.engine.modelling.TemporalCausalGraph.InvalidationResult;
import org.integratedmodelling.engine.time.literals.PeriodValueUnboundedEnd;
import org.integratedmodelling.exceptions.KlabException;
import org.integratedmodelling.exceptions.KlabResourceNotFoundException;
import org.integratedmodelling.exceptions.KlabValidationException;
public class ObservationController implements IObservationController {
private final TemporalCausalGraph causalGraph;
private final PriorityQueue taskQueue = new PriorityQueue();
private final Set currentlyProcessing = new HashSet();
private final HashMap agentDetails = new HashMap();
private final IMonitor monitor;
private final ITimeInstant simulationEndTime;
/*
* this disables/enables the generation of collision detection tasks. Re-enable in testing
* when they actually do anything.
*/
private boolean collisionDetectionEnabled = false;
private class AgentDetail {
IActiveDirectObservation subject;
ITimeInstant birth;
ITimeInstant death = null;
ITimeInstant lastKnownAliveTime;
HashMap subscribers = new HashMap();
public AgentDetail(IActiveDirectObservation subject, ITimePeriod initialTimePeriod) {
this.subject = subject;
birth = initialTimePeriod.getStart();
lastKnownAliveTime = initialTimePeriod.getEnd();
}
}
/**
* represents the agent's status as of a specific point in time: is it alive? dead?
* has it been evaluated yet?
*/
public enum AgentStatus {
nonExistent,
evaluated,
notYetEvaluated,
dead
}
public ObservationController(
TemporalCausalGraph causalGraph,
IMonitor monitor, ITimeInstant simulationEndTime) {
this.causalGraph = causalGraph;
this.monitor = monitor;
// this.scenarios = scenarios;
this.simulationEndTime = simulationEndTime;
}
/**
* TODO the recursion should be here and ensure that 1. tasks respect the sequence in
* the dataflow; 2. each task re-enqueues its next task in the same order.
*/
@Override
public IObservationGraphNode createAgent(IActiveDirectObservation subject, IAgentState initialState, ITimePeriod initialTimePeriod, IObservationGraphNode parentNode, IObservationTask creationTask, boolean enqueueSubsequentTask) {
IObservationGraphNode node = new ObservationGraphNode(initialState, creationTask);
causalGraph.createAgent(subject, node, initialTimePeriod, parentNode);
agentDetails.put(subject, new AgentDetail(subject, initialTimePeriod));
// will the agent remain alive for another observation? (this code assumes yes)
if (enqueueSubsequentTask) {
IObservationTask subsequentTaskForThisAgent = new ObservationTask(subject, initialTimePeriod
.getEnd(), node, this);
enqueueTask(subsequentTaskForThisAgent);
}
return node;
}
private void enqueueTask(IObservationTask task) {
// KLAB.info("TASKING " + task);
taskQueue.add(task);
task.getParentNode().addTaskCausedByThisNodeState(task);
}
@Override
public IObservationTask getNext() {
IObservationTask task = taskQueue.poll();
while (task != null && !task.isValid()
&& task.getObservationTime().compareTo(simulationEndTime) >= 0) {
// poll until we find a task which has not been invalidated by a collision
// (exits if the queue is empty)
task = taskQueue.poll();
}
if (task != null) {
// we found a valid task before the queue was empty, so mark it as 'working'
// and return it
task.startedWorkAt(System.currentTimeMillis());
currentlyProcessing.add(task);
}
monitor.debug("ObservationController is returning task " + CallTracer.detailedDescription(task));
return task;
}
@Override
public void setResult(IObservationTask task, ITransition result) throws KlabValidationException {
// make sure we're dealing with the local task (so its state is accurate)
task = getThreadlocalTaskObject(task);
currentlyProcessing.remove(task);
if (!task.isValid()) {
// the task has been invalidated by a collision while it was processing, so
// don't do anything with
// its result
return;
}
// Sanity check - have any of the dependencies become invalid during observation?
IObservationGraphNode dependencyNode;
Collection> dependencies = result
.getObservationDependencies();
for (Pair dependency : dependencies) {
dependencyNode = dependency.getSecond();
// make sure we're dealing with a local graph node (so its state is accurate)
dependencyNode = getThreadlocalGraphNodeObject(dependencyNode);
// has the dependency been removed or shortened by an invalidation?
if (dependencyNode == null
|| !dependencyNode.getAgentState().getTimePeriod().contains(dependency.getFirst())) {
// the dependency node has been removed, or it is no longer valid for the
// time which it was
// originally queried.
// re-evaluate the result which has been returned (without setting the
// current result)
enqueueTask(task);
return;
}
}
IActiveDirectObservation agent = task.getSubject();
AgentDetail agentDetail = agentDetails.get(agent); // null if agent doesn't exist
// yet
IObservationGraphNode node;
ITimePeriod timePeriod;
if (result.agentSurvives()) {
// set in the causal graph (also sets parent causal link with the agent's
// previous state)
IAgentState agentState = result.getAgentState();
timePeriod = agentState.getTimePeriod();
ITimeInstant agentStateEndTime = timePeriod.getEnd();
if (agentDetail == null) {
// agent does not yet exist
node = createAgent(agent, agentState, timePeriod, task.getParentNode(), task, false);
agentDetail = agentDetails.get(agent);
} else {
// agent exists - add a new node in its agent-state series
node = new ObservationGraphNode(agentState, task, result);
}
// queue up collision detection tasks for all agent-states which overlap the
// one just created
if (collisionDetectionEnabled) {
/*
* FIXME - if these are enabled the queue priorities get messed up, so ensure
* that the comparator in ObservationTask is up to scratch before enabling.
*/
for (IObservationGraphNode overlappingNode : getOverlappingAgentStateNodes(node)) {
if (overlappingNode.canCollideWithAnything()) { // returns false for
// agent-death nodes
IAgentState overlappingAgentState = overlappingNode.getAgentState();
// use getEnd() because collision detection happens at the END of
// the
// overlapping period
ITimeInstant endInstant = overlappingAgentState.getTimePeriod().getEnd();
if (agentStateEndTime.compareTo(endInstant) < 0) {
endInstant = agentStateEndTime;
}
ObservationTaskCollisionDetection collisionDetectionTask = new ObservationTaskCollisionDetection(endInstant, node, overlappingNode, node, this);
enqueueTask(collisionDetectionTask);
}
}
}
} else {
// agent goes
ITimeInstant timeOfDeath = task.getObservationTime();
timePeriod = new PeriodValueUnboundedEnd(timeOfDeath.getMillis());
node = new ObservationGraphNodeAgentDeath(task, result);
agentDetail.death = timeOfDeath;
}
causalGraph.addStateNode(agent, node, timePeriod, task.getParentNode());
// set any other *influential* links (i.e. what did the observer observe during
// observation?)
for (Pair dependency : dependencies) {
causalGraph.addInfluentialRelationship(dependency.getSecond(), node);
}
// queue up the next tasks which are a result of this observation (does not
// include collision
// detection).
// possible outcomes of a task are:
// next observation task for this agent
// agent creation/destruction (messaging, childbirth, etc)
// misc. observation tasks (we haven't limited the possible reasons)
for (IObservationTask nextTask : result.getFurtherObservationTasks()) {
nextTask.setParentNode(node);
enqueueTask(nextTask);
}
// notify any listeners that this agent has changed
notify(agent);
}
/**
* TODO in multi-threaded environments, use this method to convert an arbitrary task
* object into the equivalent task which is "owned" by the observation controller, so
* that its state can be updated.
*
* The most straightforward way to do this would be to implement task.hashCode() and
* task.equals(other) and find the original in a hash index.
*
* @param task
* @return
*/
protected IObservationTask getThreadlocalTaskObject(IObservationTask task) {
return task;
}
/**
* TODO this does the same as getThreadlocalTaskObject, but for graph nodes. TODO
* return null if the node has been removed from the graph
*
* @see ObservationController#getThreadlocalTaskObject()
*/
private IObservationGraphNode getThreadlocalGraphNodeObject(IObservationGraphNode node) {
return node;
}
private Collection getOverlappingAgentStateNodes(IObservationGraphNode node) {
Collection overlaps = causalGraph.getOverlapping(node.getAgentState()
.getTimePeriod());
overlaps.remove(node); // above method will return the original agentState
return overlaps;
}
@Override
public void collide(IObservationGraphNode node1, IObservationGraphNode node2, ICollision collision)
throws KlabException {
collide(node1, collision);
collide(node2, collision);
}
@Override
public void collide(IObservationGraphNode node, ICollision collision) throws KlabException {
IActiveDirectObservation subject = node.getAgentState().getSubject();
boolean stateMustChange = ((IActiveSubject) subject)
.doesThisCollisionAffectYou(node.getAgentState(), collision);
if (stateMustChange) {
ITimeInstant collisionTime = collision.getCollisionTime();
invalidateChildTasksAndSubscriberCaches(node, collisionTime);
// terminate the agent-state at the collision time
InvalidationResult statesToReEvaluate = causalGraph
.invalidate(node, collisionTime);
// queue up the new observation which is now required.
// All CAUSALLY dependent states and observation tasks which have been deleted
// will be re-computed
// by this task.
ObservationTaskCollisionHandling postCollisionTask = new ObservationTaskCollisionHandling(agentDetails
.get(subject).subject, collision, node, this);
enqueueTask(postCollisionTask);
// re-queue the INFLUENTIALLY dependent observations which have become
// necessary due to other
// observations becoming invalid
for (IObservationGraphNode state : statesToReEvaluate.getInfluential()) {
invalidateChildTasksAndSubscriberCaches(state, collisionTime);
enqueueTask(state.getTaskCreatingThisNodeState());
}
// CAUSALLY dependent observations should be deleted along with subsequent
// tasks, without
// re-queuing an observation.
for (IObservationGraphNode state : statesToReEvaluate.getCausal()) {
invalidateChildTasksAndSubscriberCaches(state, collisionTime);
}
}
}
/**
* invalidate observation tasks which were a product of the post-collision time
* period, and invalidate any IScaleMediator objects which have cached the state in
* question.
*
* @param agentState
* @param collisionTime
*/
private void invalidateChildTasksAndSubscriberCaches(IObservationGraphNode agentState, ITimeInstant collisionTime) {
// NOTE: tasks can be in either the priority queue or the currently-processing
// queue
for (IObservationTask task : agentState.getTasksCausedByThisNodeState()) {
if (task.getObservationTime().compareTo(collisionTime) > 0) {
// the task was created as a result of the invalid partition of the
// agent-state,
// so invalidate it.
// NOTE: don't remove it from any queues; this way is better for
// performance.
task.setInvalid();
}
}
// invalidate the ScaleMediator listeners' caches
// TODO it is essential that the cache invalidations are CONFIRMED before adding
// any more tasks to the
// queue
// (this is trivially satisfied in single-threaded execution)
for (IObservationGraphNodeSubscriber listener : agentState.getSubscribers()) {
listener.invalidate(collisionTime);
}
}
/**
*
* @param agent
* @param startTime
* set to null to start listening at the beginning.
* @throws KlabResourceNotFoundException
*/
@Override
public void subscribe(IObservationGraphNodeSubscriber listener, IActiveDirectObservation agent, ITimeInstant startTime)
throws KlabResourceNotFoundException {
// register the subscriber (interpolator)
AgentDetail agentDetail = agentDetails.get(agent);
if (agentDetail == null) {
throw new KlabResourceNotFoundException("Agent " + agent.toString());
}
agentDetail.subscribers.put(listener, startTime);
// catch the listener up with what has been published since its start time
notify(agent, listener);
}
private void notify(IActiveDirectObservation agent) {
HashMap listeners = agentDetails
.get(agent).subscribers;
if (listeners == null) {
return;
}
for (IObservationGraphNodeSubscriber listener : listeners.keySet()) {
notify(agent, listener);
}
}
private void notify(IActiveDirectObservation agent, IObservationGraphNodeSubscriber listener) {
ITemporalSeries agentStateSeries = causalGraph.getAgentStateSeries(agent);
HashMap agentListenerUpdateTimes = agentDetails
.get(agent).subscribers;
ITimeInstant lastNotified = agentListenerUpdateTimes.get(listener);
IObservationGraphNode node;
if (lastNotified == null) {
node = agentStateSeries.getFirst();
} else {
node = agentStateSeries.getFollowing(lastNotified);
}
// iterate through the agent states which start AFTER the last-notified agent
// state for this listener
while (node != null) {
listener.notify(node);
lastNotified = node.getAgentState().getTimePeriod().getEnd();
agentListenerUpdateTimes.put(listener, lastNotified);
node = agentStateSeries.getFollowing(lastNotified);
}
}
/**
* is the agent alive? dead? non-existent?
*
* @param agent
* @param time
* @return status
*/
public AgentStatus getAgentStatus(IActiveDirectObservation agent, ITimeInstant time) {
AgentDetail agentDetail = agentDetails.get(agent);
if (agentDetail == null || agentDetail.birth.compareTo(time) >= 0) {
// agent not created, or is born after the time given
return AgentStatus.nonExistent;
}
// agent was born before the time given
if (agentDetail.lastKnownAliveTime.compareTo(time) >= 0) {
return AgentStatus.evaluated;
}
if (agentDetail.death != null && agentDetail.death.compareTo(time) < 0) {
return AgentStatus.dead;
}
return AgentStatus.notYetEvaluated;
}
}