package org.jactr.modules.pm.common.memory.impl; /* * default logging */ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import javolution.util.FastCollection; import javolution.util.FastList; import javolution.util.FastSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.commonreality.agents.IAgent; import org.commonreality.identifier.IIdentifier; import org.commonreality.object.manager.IAfferentObjectManager; import org.jactr.core.buffer.six.IStatusBuffer; import org.jactr.core.chunk.IChunk; import org.jactr.core.concurrent.ExecutorServices; import org.jactr.core.event.ACTREventDispatcher; import org.jactr.core.production.request.ChunkTypeRequest; import org.jactr.core.runtime.ACTRRuntime; import org.jactr.core.slot.BasicSlot; import org.jactr.core.slot.ISlot; import org.jactr.core.utils.ChainedComparator; import org.jactr.core.utils.collections.FastCollectionFactory; import org.jactr.core.utils.parameter.NumericParameterHandler; import org.jactr.core.utils.parameter.ParameterHandler; import org.jactr.modules.pm.IPerceptualModule; import org.jactr.modules.pm.common.afferent.DefaultAfferentObjectListener; import org.jactr.modules.pm.common.memory.IActivePerceptListener; import org.jactr.modules.pm.common.memory.IPerceptualEncoder; import org.jactr.modules.pm.common.memory.IPerceptualMemory; import org.jactr.modules.pm.common.memory.PerceptualSearchResult; import org.jactr.modules.pm.common.memory.event.ActivePerceptEvent; import org.jactr.modules.pm.common.memory.filter.IIndexFilter; import org.jactr.modules.pm.common.memory.map.IFINSTFeatureMap; import org.jactr.modules.pm.common.memory.map.IFeatureMap; public abstract class AbstractPerceptualMemory implements IPerceptualMemory { /** * Logger definition */ static private final transient Log LOGGER = LogFactory .getLog(AbstractPerceptualMemory.class); static public final String PRECODE_ONSET_TIME = ":precode-onset"; private final Collection<IPerceptualEncoder> _encoders; private final Collection<PerceptualEncoderBridge> _bridges; @SuppressWarnings("unchecked") private final Collection<IFeatureMap> _featureMaps; private final Collection<IIndexFilter> _filters; private IFINSTFeatureMap _finstFeatureMap; private int _finstLimit = 4; private double _finstDuration = 3; private double _onsetDuration = 0.5; private final IPerceptualModule _module; private final IIndexManager _indexManager; private final ACTREventDispatcher<IPerceptualMemory, IActivePerceptListener> _dispatcher = new ACTREventDispatcher<IPerceptualMemory, IActivePerceptListener>(); private IAgent _agent; private DelayableAfferentObjectListener _agentListener; private List<PerceptualSearchResult> _recentResults; private Map<String, IChunk> _namedChunkCache = new TreeMap<String, IChunk>(); @SuppressWarnings("unchecked") public AbstractPerceptualMemory(IPerceptualModule module, IIndexManager manager) { _encoders = new FastList<IPerceptualEncoder>(); _bridges = new FastList<PerceptualEncoderBridge>(); _featureMaps = new FastList<IFeatureMap>(); _filters = new FastList<IIndexFilter>(); _module = module; _indexManager = manager; _recentResults = Collections .synchronizedList(new FastList<PerceptualSearchResult>()); } public void addListener(IActivePerceptListener listener, Executor executor) { _dispatcher.addListener(listener, executor); } public void removeListener(IActivePerceptListener listener) { _dispatcher.removeListener(listener); } protected boolean hasListeners() { return _dispatcher.hasListeners(); } protected void dispatch(ActivePerceptEvent event) { _dispatcher.fire(event); } public int getFINSTLimit() { return _finstLimit; } public double getFINSTSpan() { return _finstDuration; } public double getNewFINSTOnsetDuration() { return _onsetDuration; } public void setFINSTLimit(int max) { _finstLimit = max; if (_finstFeatureMap != null) _finstFeatureMap.setMaximumFINSTs(max); } public void setFINSTSpan(double duration) { _finstDuration = duration; } public void setNewFINSTOnsetDuration(double duration) { _onsetDuration = duration; } public double getLastChangeTime() { if (_agentListener != null) return _agentListener.getLastChangeTime(); return -1; } public boolean isAttached() { return _agent != null; } /** * attach must be called after the model has been connected to commmon reality * * @param agent */ @SuppressWarnings("unchecked") public void attach(IAgent agent) { if (_agent != null) throw new IllegalStateException("Already attached!"); _agent = agent; _agentListener = new DelayableAfferentObjectListener(_module.getModel(), _agent, getModule().getCommonRealityExecutor()); for (PerceptualEncoderBridge bridge : _bridges) { _agentListener.add(bridge); IPerceptualEncoder encoder = bridge.getEncoder(); if (encoder instanceof INeedsAgent) ((INeedsAgent) encoder).setAgent(agent); } for (IFeatureMap map : _featureMaps) { _agentListener.add(map); if (map instanceof INeedsAgent) ((INeedsAgent) map).setAgent(agent); } /* * the listener is processed as the messages come in, on the io thread, but * the actual work is done on the CR executor */ _agent.getAfferentObjectManager().addListener(_agentListener, ExecutorServices.INLINE_EXECUTOR); // handle any potentially missed _agentListener.processExistingObjects(); } protected DefaultAfferentObjectListener getAfferentObjectListener() { return _agentListener; } @SuppressWarnings("unchecked") public void detach() { _agent.getAfferentObjectManager().removeListener(_agentListener); for (PerceptualEncoderBridge bridge : _bridges) { IPerceptualEncoder encoder = bridge.getEncoder(); if (encoder instanceof INeedsAgent) ((INeedsAgent) encoder).setAgent(null); } for (IFeatureMap map : _featureMaps) if (map instanceof INeedsAgent) ((INeedsAgent) map).setAgent(null); _agent = null; _agentListener = null; } protected DelayableAfferentObjectListener getObjectListener() { return _agentListener; } public int getPendingUpdates() { DelayableAfferentObjectListener listener = getObjectListener(); if (listener != null) return listener.getPendingUpdates(); return 0; } public IPerceptualModule getModule() { return _module; } /** * this is the chunk that is used as an error code to invalidate * PerceptualSearchResults. * * @return */ abstract protected IChunk getRemovedErrorCodeChunk(); public void addEncoder(IPerceptualEncoder encoder) { if (_agent != null) throw new IllegalStateException("Cannot add encoders to a running model"); _encoders.add(encoder); PerceptualEncoderBridge bridge = new PerceptualEncoderBridge(encoder, this, getRemovedErrorCodeChunk()); _bridges.add(bridge); } @SuppressWarnings("unchecked") public void addFeatureMap(IFeatureMap featureMap) { if (_agent != null) throw new IllegalStateException( "Cannot add featuremaps to a running model"); _featureMaps.add(featureMap); featureMap.setPerceptualMemory(this); if (featureMap instanceof IFINSTFeatureMap) { if (_finstFeatureMap != null) LOGGER.warn("FINST feature map is being redefined with " + featureMap.getClass().getName()); _finstFeatureMap = (IFINSTFeatureMap) featureMap; _finstFeatureMap.setMaximumFINSTs(getFINSTLimit()); } } public void addFilter(IIndexFilter filter) { filter.setPerceptualMemory(this); _filters.add(filter); } public Collection<IPerceptualEncoder> getEncoders( Collection<IPerceptualEncoder> container) { if (container == null) container = new ArrayList<IPerceptualEncoder>(_encoders.size()); container.addAll(_encoders); return container; } public IFINSTFeatureMap getFINSTFeatureMap() { return _finstFeatureMap; } @SuppressWarnings({ "rawtypes" }) public Collection<IFeatureMap> getFeatureMaps( Collection<IFeatureMap> container) { if (container == null) container = new ArrayList<IFeatureMap>(_featureMaps.size()); container.addAll(_featureMaps); return container; } public Collection<IIndexFilter> getFilters(Collection<IIndexFilter> container) { if (container == null) container = new ArrayList<IIndexFilter>(_filters.size()); container.addAll(_filters); return container; } public void removeEncoder(IPerceptualEncoder encoder) { _encoders.remove(encoder); for (PerceptualEncoderBridge bridge : _bridges) if (bridge.getEncoder() == encoder) { _bridges.remove(bridge); break; } } @SuppressWarnings("unchecked") public void removeFeatureMap(IFeatureMap featureMap) { _featureMaps.remove(featureMap); } public void removeFilter(IIndexFilter filter) { _filters.remove(filter); } public Collection<IChunk> getEncodings(IIdentifier identifier, Collection<IChunk> container) { if (container == null) container = new ArrayList<IChunk>(); for (PerceptualEncoderBridge bridge : _bridges) { IChunk chunk = bridge.get(identifier, true); if (chunk == null) continue; container.add(chunk); } return container; } public IIndexManager getIndexManager() { return _indexManager; } /** * search, merely delegates to searchInternal on the common reality executor * * @param request * @return * @see org.jactr.modules.pm.common.memory.IPerceptualMemory#search(org.jactr.core.production.request.ChunkTypeRequest) */ public Future<PerceptualSearchResult> search(final ChunkTypeRequest request) { FutureTask<PerceptualSearchResult> future = new FutureTask<PerceptualSearchResult>( new Callable<PerceptualSearchResult>() { public PerceptualSearchResult call() throws Exception { return searchInternal(request); } }); getModule().getCommonRealityExecutor().execute(future); return future; } public PerceptualSearchResult searchNow(ChunkTypeRequest request) { return searchInternal(request); } private void buildSort(ChunkTypeRequest request, Collection<IIndexFilter> container) { FastList<IIndexFilter> filters = FastList.newInstance(); TreeMap<Integer, IIndexFilter> sorted = new TreeMap<Integer, IIndexFilter>(); getFilters(filters); for (IIndexFilter filter : filters) { // normalize filter.normalizeRequest(request); IIndexFilter actualFilter = filter.instantiate(request); if (actualFilter == null) continue; int weight = actualFilter.getWeight(); while (sorted.containsKey(weight)) weight++; sorted.put(weight, actualFilter); } FastList.recycle(filters); container.addAll(sorted.values()); } @SuppressWarnings("unchecked") private void getCandidateIdentifiers(ChunkTypeRequest request, Collection<IFeatureMap> featureMaps, Collection<IIdentifier> container) { FastSet<IIdentifier> candidates = FastSet.newInstance(); boolean firstRun = true; for (IFeatureMap featureMap : featureMaps) try { if (featureMap.isInterestedIn(request)) { featureMap.normalizeRequest(request); candidates.clear(); featureMap.getCandidateRealObjects(request, candidates); if (firstRun) container.addAll(candidates); else container.retainAll(candidates); firstRun = false; if (container.size() == 0) { if (LOGGER.isDebugEnabled()) LOGGER.debug("No candidates were found after checking " + featureMap + ". aborting search."); break; } } } catch (Exception e) { LOGGER.error(String.format("Failed to extract candidates from %s ", featureMap.getClass().getSimpleName()), e); } FastSet.recycle(candidates); if (LOGGER.isDebugEnabled()) LOGGER.debug("Returning candidates : " + container); } /** * create a default comparator when none is specified from the search request. * by default this returns null. A good alternative is * {@link #createLatestOnsetComparator()} * * @return null */ protected Comparator<ChunkTypeRequest> createDefaultComparator() { return null; } protected Comparator<ChunkTypeRequest> createLatestOnsetComparator() { return new Comparator<ChunkTypeRequest>() { private double getPrecodeTime(ChunkTypeRequest request) { FastCollection<ISlot> container = FastCollectionFactory.newInstance(); try { for (ISlot slot : request.getSlots(container)) if (slot.getName().equals(PRECODE_ONSET_TIME)) return ((Number) slot.getValue()).doubleValue(); return Double.NEGATIVE_INFINITY; } catch (Exception e) { return Double.NEGATIVE_INFINITY; } finally { FastCollectionFactory.recycle(container); } } @Override public int compare(ChunkTypeRequest o1, ChunkTypeRequest o2) { double one = getPrecodeTime(o1); double two = getPrecodeTime(o2); return -Double.compare(one, two); } }; } /** * provides a hook to set the slot values of the returned index chunk in case * they are recycled.. default impl does nothing * * @param indexChunk * @param encodedChunk * @param originalRequest * @param expandedRequest */ protected void fillIndexChunk(IChunk indexChunk, IChunk encodedChunk, ChunkTypeRequest originalRequest, ChunkTypeRequest expandedRequest) { } /** * hook to verify that an encoded chunk should be returned. this is the last * filter to be called on search results * * @param encodedChunk * @param originalRequest * @return */ protected boolean isAcceptable(IChunk encodedChunk, ChunkTypeRequest originalRequest) { return true; } @SuppressWarnings("unchecked") protected PerceptualSearchResult searchInternal(ChunkTypeRequest request) { /* * if the spec was empty, can we just take the first one that passes? */ boolean earlyExit = request.getSlots().size() == 0; FastList<IIndexFilter> filters = FastList.newInstance(); /* * build the temproary list of index filters */ buildSort(request, filters); if (LOGGER.isDebugEnabled()) LOGGER.debug("Searching perceptual memory : " + request); /* * which then build the priority sort */ ChainedComparator<ChunkTypeRequest> prioritySort = new ChainedComparator<ChunkTypeRequest>( true); for (IIndexFilter filter : filters) { Comparator<ChunkTypeRequest> comparator = filter.getComparator(); if (comparator == null) continue; prioritySort.add(comparator); } /* * and if there is no comparator, use the default */ if (prioritySort.size() == 0) { Comparator<ChunkTypeRequest> comparator = createDefaultComparator(); if (comparator != null) prioritySort.add(comparator); } TreeMap<ChunkTypeRequest, Collection<PerceptualSearchResult>> prioritizedResults = new TreeMap<ChunkTypeRequest, Collection<PerceptualSearchResult>>( prioritySort); /* * now lets get the initial set based on feature maps */ FastList<IFeatureMap> featureMaps = FastList.newInstance(); getFeatureMaps(featureMaps); FastList<IIdentifier> candidateIdentifiers = FastList.newInstance(); getCandidateIdentifiers(request, featureMaps, candidateIdentifiers); IAgent agent = ACTRRuntime.getRuntime().getConnector() .getAgent(getModule().getModel()); if (agent == null) { if (LOGGER.isWarnEnabled()) LOGGER .warn(String .format("No IAgent found, model is in the midst of a shutdown. Ignoring request")); PerceptualSearchResult psr = new PerceptualSearchResult(null, null, null, request, request); psr.setErrorCode(getNamedChunk(IStatusBuffer.ERROR_UNKNOWN_CHUNK)); return psr; } /* * now we need to check the actual chunks that have been encoded */ IAfferentObjectManager objectManager = agent.getAfferentObjectManager(); /** * nothing to match. */ if (objectManager.getIdentifiers().size() == 0) { if (LOGGER.isDebugEnabled()) LOGGER.debug(String.format("Nothing to match")); PerceptualSearchResult psr = new PerceptualSearchResult(null, null, null, request, request); psr.setErrorCode(getNamedChunk(IStatusBuffer.ERROR_NOTHING_AVAILABLE_CHUNK)); return psr; } for (IIdentifier identifier : candidateIdentifiers) { // early exit if (earlyExit && prioritizedResults.size() > 1) break; // object no longer exists if (objectManager.get(identifier) == null) { if (LOGGER.isDebugEnabled()) LOGGER.debug("No afferent object associated with " + identifier); continue; } for (PerceptualEncoderBridge bridge : _bridges) { IChunk encodedPercept = bridge.get(identifier, true); if (encodedPercept == null) { if (LOGGER.isDebugEnabled()) LOGGER.debug(bridge.getEncoder() + " did not encode " + identifier); continue; } if (LOGGER.isDebugEnabled()) LOGGER.debug(bridge.getEncoder() + " encoded " + identifier); ChunkTypeRequest template = new ChunkTypeRequest(request.getChunkType()); /* * explicitly add precode onset. except that this could screw up if * onsets actually span two cycles. This is not a problem with the lisp * since all updates are in the same cycle. But by defaulting to * prioritizing the most recent, we could get a literal recency effect. */ template.addSlot(new BasicSlot(PRECODE_ONSET_TIME, encodedPercept .getMetaData(IPerceptualEncoder.COMMONREALITY_ONSET_TIME_KEY))); /* * we build up the templates based on the features of the object, the * encoding, and even the request (should need be) - but that can still * produce a stable ordering by the comparator, so we add some extra * entropy here to break things up. */ // long random = (long) (Math.random() * System.nanoTime()); // template.addSlot(new BasicSlot(String.format(":%d", random), // random)); for (IFeatureMap featureMap : featureMaps) featureMap.fillSlotValues(template, identifier, encodedPercept, request); /* * now we need to make sure it is acceptable */ boolean isAcceptable = true; for (IIndexFilter filter : filters) if (!filter.accept(template)) { if (LOGGER.isDebugEnabled()) LOGGER.debug(filter + " rejected " + template); isAcceptable = false; break; } if (!isAcceptable) continue; IChunk indexChunk = _indexManager.getIndexChunk(encodedPercept); if (indexChunk == null) { if (LOGGER.isDebugEnabled()) LOGGER.debug("No index chunk availble for " + identifier); continue; } if (!isAcceptable(encodedPercept, request)) { if (LOGGER.isDebugEnabled()) LOGGER.debug("Perceptual memory rejected " + encodedPercept); continue; } /* * stash the result for sorting */ PerceptualSearchResult result = new PerceptualSearchResult( encodedPercept, indexChunk, identifier, request, template); // performance optimization to prevent recalc template.lockHash(); /** * if this has a value, the template was ranked as equivalent. */ Collection<PerceptualSearchResult> equivalentResults = prioritizedResults .get(template); if (equivalentResults == null) { equivalentResults = FastList.newInstance(); prioritizedResults.put(template, equivalentResults); } equivalentResults.add(result); } } /** * no match */ if (prioritizedResults.size() == 0) { if (LOGGER.isDebugEnabled()) LOGGER.debug(String.format("Nothing matches %s", request)); PerceptualSearchResult psr = new PerceptualSearchResult(null, null, null, request, request); psr.setErrorCode(getNamedChunk(IStatusBuffer.ERROR_NOTHING_MATCHES_CHUNK)); return psr; } PerceptualSearchResult rtn = select(prioritizedResults.firstEntry() .getValue()); if (rtn != null) try { fillIndexChunk(rtn.getLocation(), rtn.getPercept(), rtn.getRequest(), rtn.getLocationRequest()); rtn.getLocation().setMetaData(SEARCH_RESULT_IDENTIFIER_KEY, rtn.getPerceptIdentifier()); addRecentSearch(rtn); } catch (Exception e) { if (LOGGER.isWarnEnabled()) LOGGER .warn( String .format( "Processing of search result (%s) @ %s of %s failed, returning null", rtn.getRequest(), rtn.getLocation(), rtn.getPerceptIdentifier()), e); rtn = null; } if (rtn == null) { if (LOGGER.isDebugEnabled()) LOGGER.debug(String.format("Nothing matches %s", request)); PerceptualSearchResult psr = new PerceptualSearchResult(null, null, null, request, request); psr.setErrorCode(getNamedChunk(IStatusBuffer.ERROR_NOTHING_MATCHES_CHUNK)); return psr; } FastList.recycle(candidateIdentifiers); FastList.recycle(featureMaps); FastList.recycle(filters); return rtn; } /** * select the best option. currently just uses the first key (if any) * * @param results * @return */ protected PerceptualSearchResult select( Collection<PerceptualSearchResult> results) { if (LOGGER.isDebugEnabled()) LOGGER.debug("All results : " + results); if (results.size() == 0) return null; return results.iterator().next(); } protected void addRecentSearch(PerceptualSearchResult result) { int limit = 1; IFINSTFeatureMap f = getFINSTFeatureMap(); if (f != null) limit = f.getMaximumFINSTs(); synchronized (_recentResults) { limit--; /* * zip through the list looking for any other searches with the same * location and remove. any additional will be removed beyond limit */ Iterator<PerceptualSearchResult> iterator = _recentResults.iterator(); while (iterator.hasNext()) { PerceptualSearchResult oldResult = iterator.next(); if (limit <= 0 || oldResult.getLocation() == result.getLocation()) iterator.remove(); limit--; } _recentResults.add(0, result); } } public void getRecentSearchResults(List<PerceptualSearchResult> results) { synchronized (_recentResults) { results.addAll(_recentResults); } } public PerceptualSearchResult getLastSearchResult() { synchronized (_recentResults) { if (_recentResults.size() == 0) return null; return _recentResults.get(0); } } protected IChunk getNamedChunk(String name) { IChunk rtn = _namedChunkCache.get(name); if (rtn == null) try { rtn = _module.getModel().getDeclarativeModule().getChunk(name).get(); _namedChunkCache.put(name, rtn); } catch (Exception e) { LOGGER.error(String.format("Failed to get chunk %s from model", name), e); } return rtn; } public String getParameter(String key) { if (NEW_FINST_ONSET_DURATION_TIME_PARAM.equalsIgnoreCase(key)) return String.format("%f", getNewFINSTOnsetDuration()); else if (NUMBER_OF_FINSTS_PARAM.equalsIgnoreCase(key)) return String.format("%d", getFINSTLimit()); else if (FINST_DURATION_TIME_PARAM.equalsIgnoreCase(key)) return String.format("%f", getFINSTSpan()); return null; } public void setParameter(String key, String value) { NumericParameterHandler nph = ParameterHandler.numberInstance(); if (NEW_FINST_ONSET_DURATION_TIME_PARAM.equalsIgnoreCase(key)) setNewFINSTOnsetDuration(nph.coerce(value).doubleValue()); else if (NUMBER_OF_FINSTS_PARAM.equalsIgnoreCase(key)) setFINSTLimit(nph.coerce(value).intValue()); else if (FINST_DURATION_TIME_PARAM.equalsIgnoreCase(key)) setFINSTSpan(nph.coerce(value).doubleValue()); else if (LOGGER.isWarnEnabled()) LOGGER.warn("No clue how set " + key + " = " + value); } public Collection<String> getSetableParameters() { return Arrays.asList(NEW_FINST_ONSET_DURATION_TIME_PARAM, NUMBER_OF_FINSTS_PARAM, FINST_DURATION_TIME_PARAM); } public Collection<String> getPossibleParameters() { return getSetableParameters(); } }