/* * Created on Aug 14, 2006 Copyright (C) 2001-6, Anthony Harrison anh23@pitt.edu * (jactr.org) This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of the License, * or (at your option) any later version. This library 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 GNU Lesser General Public License for more details. You should have * received a copy of the GNU Lesser General Public License along with this * library; if not, write to the Free Software Foundation, Inc., 59 Temple * Place, Suite 330, Boston, MA 02111-1307 USA */ package org.jactr.core.module.declarative.basic; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import javolution.util.FastList; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jactr.core.buffer.BufferUtilities; import org.jactr.core.buffer.IActivationBuffer; import org.jactr.core.buffer.event.ActivationBufferEvent; import org.jactr.core.buffer.event.IActivationBufferListener; import org.jactr.core.chunk.ChunkActivationComparator; import org.jactr.core.chunk.IChunk; import org.jactr.core.chunk.ISubsymbolicChunk; import org.jactr.core.chunk.ISymbolicChunk; import org.jactr.core.chunk.basic.AbstractChunk; import org.jactr.core.chunk.event.ChunkEvent; import org.jactr.core.chunk.five.DefaultChunk5; import org.jactr.core.chunk.five.ISubsymbolicChunk5; import org.jactr.core.chunktype.IChunkType; import org.jactr.core.chunktype.ISymbolicChunkType; import org.jactr.core.chunktype.five.DefaultChunkType5; import org.jactr.core.concurrent.ExecutorServices; import org.jactr.core.event.IParameterEvent; import org.jactr.core.logging.Logger; import org.jactr.core.model.IModel; import org.jactr.core.model.event.IModelListener; import org.jactr.core.model.event.ModelEvent; import org.jactr.core.model.event.ModelListenerAdaptor; import org.jactr.core.module.declarative.IDeclarativeModule; import org.jactr.core.module.declarative.search.local.DefaultSearchSystem; import org.jactr.core.production.request.ChunkTypeRequest; import org.jactr.core.runtime.ACTRRuntime; import org.jactr.core.slot.ChunkSlot; import org.jactr.core.slot.ISlot; import org.jactr.core.utils.StringUtilities; import org.jactr.core.utils.parameter.IParameterized; public class DefaultDeclarativeModule extends AbstractDeclarativeModule implements IDeclarativeModule, IParameterized { /** * logger definition */ static final Log LOGGER = LogFactory .getLog(DefaultDeclarativeModule.class); static private final String COPIED_FROM_KEY = "CopiedFrom"; static private final String SUSPEND_DISPOSAL_KEY = DefaultDeclarativeModule.class + ".suspendDisposal"; /** * there is a grey area between the creation of a chunk and it's use in a * buffer or encoding. Most never encounter it, but it can occur in the time * between a perceptual search (i.e. visual-location) and encoding, where the * system may want to dispose of the chunk (i.e. the underlying percept has * changed too much) in order to create a new one. However, if the encoding * process has already started, it is possible that the system will try to add * the disposed chunk to a buffer.<br> * This mechanism is a recommendation only, that the declarative module can * use to temporarily suspend disposal. * * @param chunk */ static public void setDisposalSuspended(IChunk chunk, boolean suspend) { if (suspend) chunk.setMetaData(SUSPEND_DISPOSAL_KEY, Boolean.TRUE); else chunk.setMetaData(SUSPEND_DISPOSAL_KEY, null); } static public boolean isDisposalSuspended(IChunk chunk) { return Boolean.TRUE.equals(chunk.getMetaData(SUSPEND_DISPOSAL_KEY)); } protected ReentrantReadWriteLock _chunkTypeLock; protected ReentrantReadWriteLock _chunkLock; protected DefaultSearchSystem _searchSystem; protected Map<String, IChunk> _allChunks; protected Map<String, IChunkType> _allChunkTypes; protected ChunkActivationComparator _activationSorter; protected List<IChunk> _chunksToDispose; /** * used to encode chunks after removal */ protected IActivationBufferListener _encodeChunksOnRemove; /** * delayed chunks to encode in response to buffer remove */ protected List<IChunk> _chunksToEncode; /** * we actually encode the chunks at the end of the cycle */ protected IModelListener _chunkEncoder; public DefaultDeclarativeModule() { super("declarative"); _allChunks = new TreeMap<String, IChunk>(); _allChunkTypes = new TreeMap<String, IChunkType>(); _activationSorter = new ChunkActivationComparator(); _chunksToDispose = FastList.newInstance(); _searchSystem = new DefaultSearchSystem(this); _chunkLock = new ReentrantReadWriteLock(); _chunkTypeLock = new ReentrantReadWriteLock(); _chunksToEncode = FastList.newInstance(); _encodeChunksOnRemove = new IActivationBufferListener() { public void chunkMatched(ActivationBufferEvent abe) { // noop } public void requestAccepted(ActivationBufferEvent abe) { // noop } public void sourceChunkAdded(ActivationBufferEvent abe) { // noop } public void sourceChunkRemoved(ActivationBufferEvent abe) { /* * queue up the encoding. we dont encode it here so that any inline * listeners after this one will get the actual instance of the removed * chunk and not the merged version after encoding (if a merge occurs) */ try { _chunkLock.writeLock().lock(); if (!abe.getSource().handlesEncoding()) _chunksToEncode.addAll(abe.getSourceChunks()); } finally { _chunkLock.writeLock().unlock(); } } public void sourceChunksCleared(ActivationBufferEvent abe) { sourceChunkRemoved(abe); } public void statusSlotChanged(ActivationBufferEvent abe) { // noop } @SuppressWarnings("unchecked") public void parameterChanged(IParameterEvent pe) { // noop } }; _chunkEncoder = new ModelListenerAdaptor() { @Override public void cycleStopped(ModelEvent event) { /** * encode those that need encoding and dispose of those that need * disposing */ FastList<IChunk> chunkList = FastList.newInstance(); // assignment, check thread safety try { _chunkLock.writeLock().lock(); chunkList.addAll(_chunksToEncode); _chunksToEncode.clear(); } finally { _chunkLock.writeLock().unlock(); } FastList<IActivationBuffer> containingBuffers = FastList.newInstance(); // fast, destructive iterator where processing order does not matter for (IChunk chunk = null; !chunkList.isEmpty() && (chunk = chunkList.removeLast()) != null;) { /* * because this chunk might get merged, effectively changing the lock * instance, we do grab a reference to the lock temporarily */ Lock lock = chunk.getWriteLock(); try { lock.lock(); if (chunk.hasBeenDisposed()) continue; if (chunk.isEncoded()) chunk.getSubsymbolicChunk().accessed(event.getSimulationTime()); else { BufferUtilities.getContainingBuffers(chunk, true, containingBuffers); if (containingBuffers.size() == 0) addChunk(chunk); } } finally { lock.unlock(); containingBuffers.clear(); } } /** * now for the disposal */ // assignment, check thread safety try { _chunkLock.writeLock().lock(); chunkList.addAll(_chunksToDispose); _chunksToDispose.clear(); } finally { _chunkLock.writeLock().unlock(); } // fast, destructive iterator where processing order does not matter for (IChunk chunk = null; !chunkList.isEmpty() && (chunk = chunkList.removeLast()) != null;) try { chunk.getWriteLock().lock(); if (chunk.isEncoded()) continue; if (chunk.hasBeenDisposed()) continue; // requeue if (isDisposalSuspended(chunk)) dispose(chunk); else { BufferUtilities.getContainingBuffers(chunk, true, containingBuffers); if (containingBuffers.size() == 0) disposeInternal(chunk); } } finally { containingBuffers.clear(); chunk.getWriteLock().unlock(); } FastList.recycle(containingBuffers); FastList.recycle(chunkList); } }; } public boolean willEncode(IChunk chunk) { try { _chunkLock.readLock().lock(); return _chunksToEncode.contains(chunk); } finally { _chunkLock.readLock().unlock(); } } @Override synchronized public void dispose() { for (IActivationBuffer buffer : getModel().getActivationBuffers()) buffer.removeListener(_encodeChunksOnRemove); _searchSystem.clear(); _searchSystem = null; try { _chunkTypeLock.writeLock().lock(); // dispose of all the chunktypes (and chunks by extension) for (IChunkType chunkType : _allChunkTypes.values()) chunkType.dispose(); _allChunkTypes.clear(); } finally { _chunkTypeLock.writeLock().unlock(); } try { _chunkLock.writeLock().lock(); _allChunks.clear(); } finally { _chunkLock.writeLock().unlock(); } super.dispose(); } protected IChunk merge(IChunk originalChunk, IChunk newChunk) { /** * double check */ if (originalChunk.equals(newChunk)) return originalChunk; if (LOGGER.isDebugEnabled()) LOGGER.debug("Merging new chunk " + newChunk + " into existing chunk " + originalChunk); String msg = null; if (Logger.hasLoggers(getModel())) msg = "Merged " + newChunk + " into " + StringUtilities.toString(originalChunk); originalChunk.dispatch(new ChunkEvent(originalChunk, ChunkEvent.Type.MERGING_WITH, newChunk)); newChunk.dispatch(new ChunkEvent(newChunk, ChunkEvent.Type.MERGING_INTO, originalChunk)); try { _chunkLock.writeLock().lock(); mergeChunks(originalChunk, newChunk); } finally { _chunkLock.writeLock().unlock(); } if (msg != null) Logger.log(getModel(), Logger.Stream.DECLARATIVE, msg); fireChunksMerged(originalChunk, newChunk); return originalChunk; } /** * actually do the work. * * @param originalChunk * @param newChunk */ protected void mergeChunks(IChunk originalChunk, IChunk newChunk) { if (LOGGER.isDebugEnabled()) LOGGER.debug("Replacing the guts of " + newChunk + " with " + originalChunk); /* * ok, here we do something that is a little strange.. think of meta data - * who's metadata (which may be different) should take precedence? the new * or the old? Because the perceptual modules use the metadata to keep track * of connections to actual objects in the environment, we actually want to * copy the new metadata over the existing metadata */ for (String meta : newChunk.getMetaDataKeys()) originalChunk.setMetaData(meta, newChunk.getMetaData(meta)); if (newChunk instanceof AbstractChunk) ((AbstractChunk) newChunk).replaceContents(originalChunk); } /** * actually perform the disposal * * @param chunk */ protected void disposeInternal(IChunk chunk) { try { if (LOGGER.isDebugEnabled()) LOGGER.debug("Disposing of " + chunk); chunk.dispose(); } catch (Exception e) { LOGGER.error("Failed to dispose of chunk " + chunk + " ", e); } } @Override protected IChunk addChunkInternal(IChunk chunk) { IChunkType chunkType = chunk.getSymbolicChunk().getChunkType(); Collection<? extends ISlot> slots = chunk.getSymbolicChunk().getSlots(); /* * we don't check for duplicates when a chunk has no slots these chunks are * often flags for some bit of simplistic knowledge such as "new" */ if (slots.size() != 0) { Collection<IChunk> matches = _searchSystem.findExact( new ChunkTypeRequest(chunkType, slots), _activationSorter); if (matches.size() > 0) { if (LOGGER.isDebugEnabled()) LOGGER.debug("chunk " + chunk + " has yielded " + matches.size() + " matches " + matches); return merge(matches.iterator().next(), chunk); } // matches.size() == 0 } // slots.size() > 0 /* * we're here, so either we aren't checking for duplicates or there was no * matching chunk */ double now = ACTRRuntime.getRuntime().getClock(getModel()).getTime(); boolean added = false; try { _chunkLock.writeLock().lock(); String name = chunk.getSymbolicChunk().getName(); String newName = getSafeName(name, _allChunks); if (LOGGER.isDebugEnabled()) LOGGER.debug("Safe name for chunk " + name + " is " + newName); chunk.getSymbolicChunk().setName(newName); /* * notify the chunktype of this chunk */ chunk.setMetaData(COPIED_FROM_KEY, null); chunk.getSymbolicChunk().getChunkType().getSymbolicChunkType().addChunk( chunk); _allChunks.put(newName.toLowerCase(), chunk); added = true; return chunk; } finally { _chunkLock.writeLock().unlock(); // this will fire some chunk event.. so get out of the lock if (added) { if (LOGGER.isDebugEnabled()) LOGGER.debug("Encoding " + chunk); chunk.encode(now); if (Logger.hasLoggers(getModel())) Logger.log(getModel(), Logger.Stream.DECLARATIVE, "Encoded " + StringUtilities.toString(chunk)); /* * we index after since an unencoded chunk wont be indexed */ _searchSystem.index(chunk); fireChunkAdded(chunk); } } } /** * create a callable that will do all the work of adding a chunktype to the * model and firing the appropriate events * * @param chunkType * @return */ @Override protected IChunkType addChunkTypeInternal(IChunkType chunkType) { if (LOGGER.isDebugEnabled()) LOGGER.debug("Adding chunkType " + chunkType); boolean added = false; try { _chunkTypeLock.writeLock().lock(); ISymbolicChunkType sct = chunkType.getSymbolicChunkType(); String name = sct.getName(); name = getSafeName(name, _allChunkTypes); sct.setName(name); _allChunkTypes.put(name.toLowerCase(), chunkType); IChunkType parent = chunkType.getSymbolicChunkType().getParent(); while (parent != null) { parent.getSymbolicChunkType().addChild(chunkType); parent = parent.getSymbolicChunkType().getParent(); } added = true; return chunkType; } finally { _chunkTypeLock.writeLock().unlock(); // this will fire an event, so get out of the lock if (added) { chunkType.encode(); if (Logger.hasLoggers(getModel())) Logger.log(getModel(), Logger.Stream.DECLARATIVE, "Encoded " + chunkType); fireChunkTypeAdded(chunkType); } } } @Override protected IChunk createChunkInternal(IChunkType parent, String name) { IChunk rtn = new DefaultChunk5(parent); rtn.getSymbolicChunk().setName(name); // rtn.addListener(new ChunkListenerAdaptor() { // @Override // public void slotChanged(ChunkEvent nce) // { // /* // * we only index encoded chunks.. // */ // IChunk source = nce.getSource(); // if (source.isEncoded() && !source.isMutable()) // _searchSystem.update(nce.getSource(), nce.getSlotName(), nce // .getOldSlotValue(), nce.getNewSlotValue()); // } // }, getExecutor()); fireChunkCreated(rtn); return rtn; } @Override protected IChunkType createChunkTypeInternal(IChunkType parent, String name) { IModel model = getModel(); IChunkType rtn = null; if (parent != null) rtn = new DefaultChunkType5(model, parent); else rtn = new DefaultChunkType5(model); rtn.getSymbolicChunkType().setName(name); fireChunkTypeCreated(rtn); return rtn; } protected Collection<IChunk> findExactMatchesInternal( ChunkTypeRequest pattern, final Comparator<IChunk> sorter, double activationThreshold, boolean bestOne) { Collection<IChunk> candidates = _searchSystem.findExact(pattern, sorter); if (LOGGER.isDebugEnabled()) LOGGER.debug("find exact matches (" + pattern + ") evaluating " + candidates.size() + " candidates"); StringBuilder logMessage = null; if (Logger.hasLoggers(getModel())) { logMessage = new StringBuilder("Evaluating exact search matches "); logMessage.append(candidates).append("\n "); } ArrayList<IChunk> finalChunks = new ArrayList<IChunk>(); /* * we can't be sure that the sorting used is actually relevant to us so we * have to zip through the entire results */ double highestActivation = Double.NEGATIVE_INFINITY; IChunk bestChunk = null; for (IChunk chunk : candidates) { /* * snag the activation and see if this is the highest chunk so far */ double tmpAct = chunk.getSubsymbolicChunk().getActivation(); if (tmpAct > highestActivation) { bestChunk = chunk; highestActivation = tmpAct; if (logMessage != null) logMessage.append(bestChunk).append(" has highest activation (") .append(highestActivation).append(")\n "); } else if (logMessage != null) logMessage.append(chunk).append(" doesn't have highest activation (") .append(tmpAct).append(")\n "); /* * if we are selecting the best one only, don't add it to the list */ if (!bestOne && tmpAct >= activationThreshold) finalChunks.add(chunk); } /* * here's the best one, assuming we only want one */ if (bestOne && bestChunk != null && highestActivation >= activationThreshold) finalChunks.add(bestChunk); if (LOGGER.isDebugEnabled()) LOGGER.debug("find exact matches returning " + finalChunks); if (logMessage != null) Logger.log(getModel(), Logger.Stream.DECLARATIVE, logMessage.toString()); return finalChunks; } protected Collection<IChunk> findPartialMatchesInternal( ChunkTypeRequest pattern, Comparator<IChunk> sorter, double activationThreshold, boolean bestOne) { Collection<IChunk> candidates = _searchSystem.findFuzzy(pattern, sorter); if (LOGGER.isDebugEnabled()) LOGGER.debug("find partial matches evaluating " + candidates.size() + " candidates"); ArrayList<IChunk> finalChunks = new ArrayList<IChunk>(); StringBuilder logMessage = null; if (Logger.hasLoggers(getModel())) { logMessage = new StringBuilder("Evaluating partial search matches "); logMessage.append(candidates).append("\n "); } /* * we can't be sure that the sorting used is actually relevant to us so we * have to zip through the entire results */ double highestActivation = Double.NEGATIVE_INFINITY; IChunk bestChunk = null; for (IChunk chunk : candidates) { /* * snag the activation and see if this is the highest chunk so far we need * to use the pattern to evaluate mismatch with activation */ double tmpAct = ((ISubsymbolicChunk5) chunk.getSubsymbolicChunk()) .getActivation(pattern); if (tmpAct > highestActivation) { bestChunk = chunk; highestActivation = tmpAct; if (logMessage != null) logMessage.append(bestChunk).append(" has highest activation (") .append(highestActivation).append(")\n "); } else if (logMessage != null) logMessage.append(chunk).append(" doesn't have highest activation (") .append(tmpAct).append(")\n "); /* * if we are selecting the best one only, don't add it to the list */ if (!bestOne && tmpAct >= activationThreshold) finalChunks.add(chunk); } /* * here's the best one, assuming we only want one */ if (bestOne && bestChunk != null && highestActivation >= activationThreshold) finalChunks.add(bestChunk); if (LOGGER.isDebugEnabled()) LOGGER.debug("find partial matches returning " + finalChunks); if (logMessage != null) Logger.log(getModel(), Logger.Stream.DECLARATIVE, logMessage.toString()); return finalChunks; } @Override protected IChunk getChunkInternal(String name) { return _allChunks.get(name.toLowerCase()); } @Override protected IChunkType getChunkTypeInternal(String name) { return _allChunkTypes.get(name.toLowerCase()); } @Override protected Collection<IChunkType> getChunkTypesInternal() { return new ArrayList<IChunkType>(_allChunkTypes.values()); } @Override protected Collection<IChunk> getChunksInternal() { return new ArrayList<IChunk>(_allChunks.values()); } public long getNumberOfChunks() { try { _chunkLock.readLock().lock(); return _allChunks.size(); } finally { _chunkLock.readLock().unlock(); } } /** * here we attach a buffer listener to all the buffers and catch the removal * notifications to see if we should encode the chunk.. * * @see org.jactr.core.module.AbstractModule#initialize() */ @Override public void initialize() { super.initialize(); IModel model = getModel(); model.addListener(_chunkEncoder, ExecutorServices.INLINE_EXECUTOR); for (IActivationBuffer buffer : model.getActivationBuffers()) buffer.addListener(_encodeChunksOnRemove, ExecutorServices.INLINE_EXECUTOR); } /** * @see org.jactr.core.module.declarative.six.AbstractDeclarativeModule#copyChunkInternal(org.jactr.core.chunk.IChunk, * org.jactr.core.chunk.IChunk) */ @Override protected void copyChunkInternal(IChunk sourceChunk, IChunk destination) { /* * copy the meta data */ for (String key : sourceChunk.getMetaDataKeys()) destination.setMetaData(key, sourceChunk.getMetaData(key)); /* * set the symbolic contents */ ISymbolicChunk sourceSC = sourceChunk.getSymbolicChunk(); ISymbolicChunk destinationSC = destination.getSymbolicChunk(); String newName = sourceSC.getName(); try { _chunkLock.readLock().lock(); newName = getSafeName(newName, _allChunks); } finally { _chunkLock.readLock().unlock(); } destinationSC.setName(newName); for (ISlot slot : sourceSC.getSlots()) { // this is the actual backing slot.. ChunkSlot cs = (ChunkSlot) destinationSC.getSlot(slot.getName()); cs.setValue(slot.getValue()); } /* * we need to deal with the subsymbolics since it will have associative * links necessary for the propogation of activation when a chunk copy is in * a buffer... */ ISubsymbolicChunk destinationSSC = destination.getSubsymbolicChunk(); /* * set all the parameters this should handle the associative links as * well... */ for (String parameterName : destinationSSC.getSetableParameters()) { String parameterValue = destinationSSC.getParameter(parameterName); try { destinationSSC.setParameter(parameterName, parameterValue); } catch (Exception e) { LOGGER.warn("Could not set parameter " + parameterName + " to " + parameterValue, e); } } if (Logger.hasLoggers(getModel())) Logger.log(getModel(), Logger.Stream.DECLARATIVE, "Copied " + StringUtilities.toString(destination)); destination.setMetaData(COPIED_FROM_KEY, sourceChunk); } /** * @see org.jactr.core.module.declarative.IDeclarativeModule#findExactMatches(ChunkTypeRequest, * java.util.Comparator, double, boolean) */ public Future<Collection<IChunk>> findExactMatches( final ChunkTypeRequest request, final Comparator<IChunk> sorter, final double activationThreshold, final boolean bestOne) { return delayedFuture(new Callable<Collection<IChunk>>() { public Collection<IChunk> call() throws Exception { return findExactMatchesInternal(request, sorter, activationThreshold, bestOne); } }, getExecutor()); } /** * @see org.jactr.core.module.declarative.IDeclarativeModule#findPartialMatches(ChunkTypeRequest, * java.util.Comparator, double, boolean) */ public Future<Collection<IChunk>> findPartialMatches( final ChunkTypeRequest request, final Comparator<IChunk> sorter, final double activationThreshold, final boolean bestOne) { return delayedFuture(new Callable<Collection<IChunk>>() { public Collection<IChunk> call() throws Exception { return findPartialMatchesInternal(request, sorter, activationThreshold, bestOne); } }, getExecutor()); } public void dispose(IChunk chunk) { try { _chunkLock.writeLock().lock(); _chunksToDispose.add(chunk); } finally { _chunkLock.writeLock().unlock(); } } public String getParameter(String key) { return null; } public Collection<String> getPossibleParameters() { return getSetableParameters(); } public Collection<String> getSetableParameters() { return Collections.emptyList(); } public void setParameter(String key, String value) { } public void reset() {// noop } }