diff --git a/.gitignore b/.gitignore index 36b13f1..2fe854c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,176 +1,43 @@ -# ---> Python -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class +*.py[co] -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg +# Packages *.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg # Installer logs pip-log.txt -pip-delete-this-directory.txt # Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ .coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ +.tox +.log +copycat.log -# Translations -*.mo -*.pot +# Other filesystems +.svn +.DS_Store -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc +# Editors +.*.swp +# Output +output/* +<<<<<<< HEAD +copycat.log +papers/*.log +papers/*.pdf +papers/*.out +papers/*.aux +papers/words +*.txt +======= +>>>>>>> develop diff --git a/.ipynb_checkpoints/Copycat-checkpoint.ipynb b/.ipynb_checkpoints/Copycat-checkpoint.ipynb new file mode 100644 index 0000000..2fd6442 --- /dev/null +++ b/.ipynb_checkpoints/Copycat-checkpoint.ipynb @@ -0,0 +1,6 @@ +{ + "cells": [], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/.landscape.yaml b/.landscape.yaml new file mode 100644 index 0000000..1586827 --- /dev/null +++ b/.landscape.yaml @@ -0,0 +1,10 @@ +pylint: + options: + dummy-variables-rgx: _ + max-branchs: 30 + +pyflakes: + run: false + +mccabe: + run: false diff --git a/.old_distributions b/.old_distributions new file mode 100644 index 0000000..fa5e2c3 Binary files /dev/null and b/.old_distributions differ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..45fbf65 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: python +branches: + only: + - "develop" + - "master" +python: + - "3.6" +install: + - pip3 install -r requirements.txt +script: + - python3 tests.py + diff --git a/Copycat.ipynb b/Copycat.ipynb new file mode 100644 index 0000000..2225b63 --- /dev/null +++ b/Copycat.ipynb @@ -0,0 +1,81 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Copycat \n", + "\n", + "Just type your copycat example, and the number of iterations." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Answered iijjkl (time 1374, final temperature 13.5)\n", + "Answered iijjll (time 665, final temperature 19.6)\n", + "Answered iijjll (time 406, final temperature 16.6)\n", + "Answered iijjkl (time 379, final temperature 47.9)\n", + "Answered iijjll (time 556, final temperature 19.2)\n", + "Answered iijjkl (time 813, final temperature 42.8)\n", + "Answered iijjll (time 934, final temperature 15.5)\n", + "Answered iijjkl (time 1050, final temperature 49.5)\n", + "Answered iijjkl (time 700, final temperature 44.0)\n", + "Answered iijjkl (time 510, final temperature 34.8)\n", + "Answered iijjkl (time 673, final temperature 18.1)\n", + "Answered iijjkl (time 1128, final temperature 19.8)\n", + "Answered iijjll (time 961, final temperature 19.9)\n", + "Answered iijjll (time 780, final temperature 16.5)\n", + "Answered iijjll (time 607, final temperature 17.8)\n", + "Answered iijjll (time 594, final temperature 39.7)\n", + "Answered iijjll (time 736, final temperature 18.4)\n", + "Answered iijjll (time 903, final temperature 18.6)\n", + "Answered iijjll (time 601, final temperature 20.6)\n", + "Answered iijjll (time 949, final temperature 42.4)\n", + "iijjll: 12 (avg time 724.3, avg temp 22.1)\n", + "iijjkl: 8 (avg time 828.4, avg temp 33.8)\n" + ] + } + ], + "source": [ + "%run main.py abc abd iijjkk --iterations 20" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dfa4065 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2013 J Alan Brogan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index cdd4e69..60cc8cf 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,104 @@ -# copycat +Copycat +========= -Open Source copycat (python) \ No newline at end of file +![GUI](https://i.imgur.com/AhhpzVQ.png) + +An implementation of [Douglas Hofstadter](http://prelectur.stanford.edu/lecturers/hofstadter/) and [Melanie Mitchell](https://melaniemitchell.me/)'s Copycat algorithm. +The Copycat algorithm is explained [on Wikipedia](https://en.wikipedia.org/wiki/Copycat_%28software%29), in Melanie Mitchell's Book [Analogy-making as perception](https://www.amazon.com/Analogy-Making-Perception-Computer-Modeling-Connectionism/dp/026251544X/ref=sr_1_5?crid=1FC76DCS33513&dib=eyJ2IjoiMSJ9.TQVbRbFf696j7ZYj_sb4tIM3ZbFbuCIdtdYCy-Mq3EmJI6xbG5hhVXuyOPjeb7E4b8jhKiJlfr6NnD_O09rEEkNMwD_1zFxkLT9OkF81RSFL4kMCLOT7K-7KnPwBFbrc9tZuhLKFOWbxMGNL75koMcetQl2Lf6V7xsNYLYLCHBlXMCrusJ88Kv3Y8jiPKwrEr1hUwhWB8vtwEG9vSYXU7Gw-b4fZRNNbUtBBWNwiK3k.IJZZ8kA_QirWQK1ax5i42zD2nV7XvKoPYRgN94en4Dc&dib_tag=se&keywords=melanie+mitchell&qid=1745436638&sprefix=melanie+mitchell%2Caps%2C206&sr=8-5#), and in [this paper](https://github.com/Alex-Linhares/FARGonautica/blob/master/Literature/Foundations-Chalmers.French.and.Hofstadter-1992-Journal%20of%20Experimental%20and%20Theoretical%20Artificial%20Intelligence.pdf). The wikipedia page has additional links for deeper reading. See also [FARGonautica](https://github.com/Alex-Linhares/Fargonautica), where a collection of Fluid Concepts projects are available. + +This implementation is a copycat of Scott Boland's [Java implementation](https://archive.org/details/JavaCopycat). +The original Java-to-Python translation work was done by J Alan Brogan (@jalanb on GitHub). +The Java version has a GUI similar to the original Lisp; this Python version has no GUI code built in but can be incorporated into a larger GUI program. + +J. Alan Brogan writes: +> In cases where I could not grok the Java implementation easily, I took ideas from the +> [LISP implementation](http://web.cecs.pdx.edu/~mm/how-to-get-copycat.html), or directly +> from [Melanie Mitchell](https://en.wikipedia.org/wiki/Melanie_Mitchell)'s book +> "[Analogy-Making as Perception](http://www.amazon.com/Analogy-Making-Perception-Computer-Melanie-Mitchell/dp/0262132893/ref=tmm_hrd_title_0?ie=UTF8&qid=1351269085&sr=1-3)". + +Running the command-line program +-------------------------------- + +To clone the repo locally, run these commands: + +``` +$ git clone https://github.com/fargonauts/copycat.git +$ cd copycat/copycat +$ python main.py abc abd ppqqrr --iterations 10 +``` + +The script takes three or four arguments. +The first two are a pair of strings with some change, for example "abc" and "abd". +The third is a string which the script should try to change analogously. +The fourth (which defaults to "1") is a number of iterations. + +This might produce output such as + +``` +ppqqss: 6 (avg time 869.0, avg temp 23.4) +ppqqrs: 4 (avg time 439.0, avg temp 37.3) +``` + +The first number indicates how many times Copycat chose that string as its answer; higher means "more obvious". +The last number indicates the average final temperature of the workspace; lower means "more elegant". + +Code structure +--------------------- + +This Copycat system consists of 4,981 lines of Python code across 40 files. Here's a breakdown. + +Core Components: +- codeletMethods.py: 1,124 lines (largest file) +- curses_reporter.py: 436 lines +- coderack.py: 310 lines +- slipnet.py: 248 lines + +Workspace Components: + +- group.py: 237 lines +- bond.py: 211 lines +- correspondence.py: 204 lines +- workspace.py: 195 lines +- workspaceObject.py: 194 lines + +Control Components: +- temperature.py: 175 lines +- conceptMapping.py: 153 lines +- rule.py: 149 lines +- copycat.py: 144 lines + +GUI Components: +- gui/gui.py: 96 lines +- gui/workspacecanvas.py: 70 lines +- gui/status.py: 66 lines +- gui/control.py: 59 lines + + +The system is well-organized with clear separation of concerns: +- Core logic (codelets, coderack, slipnet) +- Workspace management (groups, bonds, correspondences) +- Control systems (temperature, rules) +- User interface (GUI components) + +The largest file, codeletMethods.py, contains all the codelet behavior implementations, which makes sense as it's the heart of the system's analogical reasoning capabilities. + +{code.py}README.md Files +--------------------- + +We've got an LLM to document every code file, so people can look at a particular readme before delving into the work (Here's one [Example](main_README.md)). + + +Installing the module +--------------------- + +To install the Python module and get started with it, run these commands: + +``` +$ pip install -e git+git://github.com/fargonauts/copycat.git#egg=copycat +$ python +>>> from copycat import Copycat +>>> Copycat().run('abc', 'abd', 'ppqqrr', 10) +{'ppqqrs': {'count': 4, 'avgtime': 439, 'avgtemp': 37.3}, 'ppqqss': {'count': 6, 'avgtime': 869, 'avgtemp': 23.4}} +``` + +The result of `run` is a dict containing the same information as was printed by `main.py` above. diff --git a/copycat/__init__.py b/copycat/__init__.py new file mode 100644 index 0000000..732e307 --- /dev/null +++ b/copycat/__init__.py @@ -0,0 +1,4 @@ +from .copycat import Copycat, Reporter # noqa +from .problem import Problem +from .plot import plot_answers +from .io import save_answers diff --git a/copycat/bond.py b/copycat/bond.py new file mode 100644 index 0000000..bf6e004 --- /dev/null +++ b/copycat/bond.py @@ -0,0 +1,211 @@ +from .workspaceStructure import WorkspaceStructure + + +class Bond(WorkspaceStructure): + # pylint: disable=too-many-arguments + def __init__(self, ctx, source, destination, bondCategory, bondFacet, + sourceDescriptor, destinationDescriptor): + WorkspaceStructure.__init__(self, ctx) + slipnet = self.ctx.slipnet + self.source = source + self.string = self.source.string + self.destination = destination + self.leftObject = self.source + self.rightObject = self.destination + self.directionCategory = slipnet.right + if self.source.leftIndex > self.destination.rightIndex: + self.leftObject = self.destination + self.rightObject = self.source + self.directionCategory = slipnet.left + self.facet = bondFacet + self.sourceDescriptor = sourceDescriptor + self.destinationDescriptor = destinationDescriptor + self.category = bondCategory + if (self.sourceDescriptor == self.destinationDescriptor): + self.directionCategory = None + + def flippedVersion(self): + slipnet = self.ctx.slipnet + return Bond( + self.ctx, + self.destination, self.source, + self.category.getRelatedNode(slipnet.opposite), + self.facet, self.destinationDescriptor, self.sourceDescriptor + ) + + def __repr__(self): + return '' % self.__str__() + + def __str__(self): + return '%s bond between %s and %s' % ( + self.category.name, self.leftObject, self.rightObject, + ) + + def buildBond(self): + workspace = self.ctx.workspace + workspace.structures += [self] + self.string.bonds += [self] + self.category.buffer = 100.0 + if self.directionCategory: + self.directionCategory.buffer = 100.0 + self.leftObject.rightBond = self + self.rightObject.leftBond = self + self.leftObject.bonds += [self] + self.rightObject.bonds += [self] + + def break_the_structure(self): + self.breakBond() + + def breakBond(self): + workspace = self.ctx.workspace + if self in workspace.structures: + workspace.structures.remove(self) + if self in self.string.bonds: + self.string.bonds.remove(self) + self.leftObject.rightBond = None + self.rightObject.leftBond = None + if self in self.leftObject.bonds: + self.leftObject.bonds.remove(self) + if self in self.rightObject.bonds: + self.rightObject.bonds.remove(self) + + def getIncompatibleCorrespondences(self): + # returns a list of correspondences that are incompatible with + # self bond + workspace = self.ctx.workspace + incompatibles = [] + if self.leftObject.leftmost and self.leftObject.correspondence: + correspondence = self.leftObject.correspondence + if self.string == workspace.initial: + objekt = self.leftObject.correspondence.objectFromTarget + else: + objekt = self.leftObject.correspondence.objectFromInitial + if objekt.leftmost and objekt.rightBond: + if ( + objekt.rightBond.directionCategory and + objekt.rightBond.directionCategory != self.directionCategory + ): + incompatibles += [correspondence] + if self.rightObject.rightmost and self.rightObject.correspondence: + correspondence = self.rightObject.correspondence + if self.string == workspace.initial: + objekt = self.rightObject.correspondence.objectFromTarget + else: + objekt = self.rightObject.correspondence.objectFromInitial + if objekt.rightmost and objekt.leftBond: + if ( + objekt.leftBond.directionCategory and + objekt.leftBond.directionCategory != self.directionCategory + ): + incompatibles += [correspondence] + return incompatibles + + def updateInternalStrength(self): + slipnet = self.ctx.slipnet + # bonds between objects of same type(ie. letter or group) are + # stronger than bonds between different types + sourceGap = self.source.leftIndex != self.source.rightIndex + destinationGap = (self.destination.leftIndex != + self.destination.rightIndex) + if sourceGap == destinationGap: + memberCompatibility = 1.0 + else: + memberCompatibility = 0.7 + # letter category bonds are stronger + if self.facet == slipnet.letterCategory: + facetFactor = 1.0 + else: + facetFactor = 0.7 + strength = min(100.0, memberCompatibility * facetFactor * + self.category.bondDegreeOfAssociation()) + self.internalStrength = strength + + def updateExternalStrength(self): + self.externalStrength = 0.0 + supporters = self.numberOfLocalSupportingBonds() + if supporters > 0.0: + density = self.localDensity() / 100.0 + density = density ** 0.5 * 100.0 + supportFactor = 0.6 ** (1.0 / supporters ** 3) + supportFactor = max(1.0, supportFactor) + strength = supportFactor * density + self.externalStrength = strength + + def numberOfLocalSupportingBonds(self): + return sum( + 1 for b in self.string.bonds if + b.string == self.source.string and + self.leftObject.letterDistance(b.leftObject) != 0 and + self.rightObject.letterDistance(b.rightObject) != 0 and + self.category == b.category and + self.directionCategory == b.directionCategory + ) + + def sameCategories(self, other): + return (self.category == other.category and + self.directionCategory == other.directionCategory) + + def myEnds(self, object1, object2): + if self.source == object1 and self.destination == object2: + return True + return self.source == object2 and self.destination == object1 + + def localDensity(self): + # returns a rough measure of the density in the string + # of the same bond-category and the direction-category of + # the given bond + workspace = self.ctx.workspace + slotSum = 0.0 + supportSum = 0.0 + for object1 in workspace.objects: + if object1.string == self.string: + for object2 in workspace.objects: + if object1.beside(object2): + slotSum += 1.0 + for bond in self.string.bonds: + if ( + bond != self and + self.sameCategories(bond) and + self.myEnds(object1, object2) + ): + supportSum += 1.0 + try: + return 100.0 * supportSum / slotSum + except ZeroDivisionError: + return 0.0 + + def sameNeighbors(self, other): + if self.leftObject == other.leftObject: + return True + return self.rightObject == other.rightObject + + def getIncompatibleBonds(self): + return [b for b in self.string.bonds if self.sameNeighbors(b)] + + def set_source(self, value): + self.source = value + + def possibleGroupBonds(self, bonds): + result = [] + slipnet = self.ctx.slipnet + for bond in bonds: + if ( + bond.category == self.category and + bond.directionCategory == self.directionCategory + ): + result += [bond] + else: + # a modified bond might be made + if bond.category == self.category: + return [] # a different bond cannot be made here + if bond.directionCategory == self.directionCategory: + return [] # a different bond cannot be made here + if slipnet.sameness in [self.category, bond.category]: + return [] + bond = Bond( + bond.ctx, bond.destination, bond.source, self.category, + self.facet, bond.destinationDescriptor, + bond.sourceDescriptor + ) + result += [bond] + return result diff --git a/copycat/bond_README.md b/copycat/bond_README.md new file mode 100644 index 0000000..b9aa602 --- /dev/null +++ b/copycat/bond_README.md @@ -0,0 +1,54 @@ +# README_bond.md + +## Overview +`bond.py` implements the Bond system, a key component of the Copycat system that manages the relationships between objects in strings. It handles the creation, evaluation, and management of bonds that represent meaningful connections between objects based on their properties and relationships. + +## Core Components +- `Bond` class: Main class that represents a bond between objects +- Bond evaluation system +- Bond compatibility management + +## Key Features +- Manages bonds between objects in strings +- Evaluates bond strength based on multiple factors +- Handles bond direction and category +- Supports bond flipping and versioning +- Manages bond compatibility and support + +## Bond Components +- `source`: Source object of the bond +- `destination`: Destination object of the bond +- `category`: Category of the bond +- `facet`: Aspect of the bond +- `directionCategory`: Direction of the bond +- `sourceDescriptor`: Descriptor of the source object +- `destinationDescriptor`: Descriptor of the destination object + +## Main Methods +- `updateInternalStrength()`: Calculate internal bond strength +- `updateExternalStrength()`: Calculate external bond strength +- `buildBond()`: Create and establish bond +- `breakBond()`: Remove bond +- `localSupport()`: Calculate local support +- `numberOfLocalSupportingBonds()`: Count supporting bonds +- `sameCategories()`: Compare bond categories +- `localDensity()`: Calculate local bond density + +## Bond Types +- Letter category bonds +- Direction-based bonds +- Category-based bonds +- Flipped bonds +- Modified bonds + +## Dependencies +- Requires `workspaceStructure` module +- Used by the main `copycat` module + +## Notes +- Bonds are evaluated based on member compatibility and facet factors +- The system supports both same-type and different-type bonds +- Bonds can have direction categories (left, right) +- The system handles bond compatibility and support +- Bonds can be flipped to create alternative versions +- Local density and support factors influence bond strength \ No newline at end of file diff --git a/copycat/codelet.py b/copycat/codelet.py new file mode 100644 index 0000000..b467868 --- /dev/null +++ b/copycat/codelet.py @@ -0,0 +1,9 @@ +class Codelet(object): + def __init__(self, name, urgency, arguments, currentTime): + self.name = name + self.urgency = urgency + self.arguments = arguments + self.birthdate = currentTime + + def __repr__(self): + return '' % self.name diff --git a/copycat/codeletMethods.py b/copycat/codeletMethods.py new file mode 100644 index 0000000..da36dc7 --- /dev/null +++ b/copycat/codeletMethods.py @@ -0,0 +1,2012 @@ +""" +Codelet Methods Module + +This module contains all the behavior functions that codelets execute in the Copycat algorithm. +Codelets are autonomous agents that propose, test, and build structures (bonds, groups, +correspondences, rules, etc.) in the workspace. + +The main components: +1. @codelet decorator - Marks functions as codelet behaviors +2. Helper functions - Utility functions for common operations +3. Codelet behaviors - Specific algorithm implementations +""" + +import inspect +import logging + +from . import formulas +from .workspaceFormulas import chooseDirectedNeighbor +from .workspaceFormulas import chooseNeighbor +from .workspaceFormulas import chooseUnmodifiedObject +from .workspaceObject import WorkspaceObject +from .letter import Letter +from .replacement import Replacement +from .group import Group +from .bond import Bond +from .correspondence import Correspondence + + +def codelet(name): + """ + Decorator for marking functions as codelet behaviors. + + Codelets are autonomous agents in the Copycat algorithm that execute specific behaviors + to propose, test, or build structures in the workspace. This decorator validates that + the decorated function has the correct signature and marks it as a codelet method. + + Args: + name (str): The name of the codelet behavior for identification + + Returns: + function: The wrapped function with codelet metadata + + Requirements: + The decorated function must have exactly two parameters: + - ctx: The context object containing workspace, slipnet, temperature, etc. + - codelet: The codelet instance executing the behavior + + Raises: + AssertionError: If the function doesn't have the correct signature + """ + def wrap(f): + # Verify that the decorated function has exactly two parameters: + # 1. ctx - the context object containing workspace, slipnet, etc. + # 2. codelet - the codelet instance itself + # The None values in the tuple represent: no default args, no *args, no **kwargs + assert tuple(inspect.getargspec(f)) == (['ctx', 'codelet'], None, None, None) + + # Mark this function as a valid codelet method + f.is_codelet_method = True + + # Store the codelet name + f.codelet_name = name + + # Return the decorated function + return f + return wrap + +# Helper functions common to multiple codelets + +def __showWhichStringObjectIsFrom(structure): + """ + Log which string (initial or target) a workspace structure belongs to. + + This is a debugging/logging utility that helps trace which string an object + came from during codelet execution. Useful for understanding algorithm flow. + + Args: + structure: A workspace object or structure to identify + """ + if not structure: + return + workspace = structure.ctx.workspace + whence = 'other' + if isinstance(structure, WorkspaceObject): + whence = 'target' + if structure.string == workspace.initial: + whence = 'initial' + logging.info('object chosen = %s from %s string' % (structure, whence)) + + +def __getScoutSource(ctx, slipnode, relevanceMethod, typeName): + """ + Choose which string (initial or target) to scout for objects based on relevance. + + This function implements the core heuristic for scouting behaviors, where the algorithm + probabilistically chooses between the initial and target strings based on relevance + and unhappiness scores. Higher relevance and unhappiness create pressure to explore + that string. + + Args: + ctx: The context object containing random, workspace, etc. + slipnode: The slipnet node to evaluate relevance against + relevanceMethod: Function that calculates relevance between string and slipnode + typeName: String name for logging purposes (e.g., 'bond', 'group') + + Returns: + WorkspaceObject: A randomly chosen unmodified object from the selected string + + Notes: + The decision process balances: + 1. Relevance of the slipnode to each string + 2. Intra-string unhappiness (pressure to resolve problems) + 3. Random probabilistic selection + """ + random = ctx.random + workspace = ctx.workspace + initialRelevance = relevanceMethod(workspace.initial, slipnode) + targetRelevance = relevanceMethod(workspace.target, slipnode) + initialUnhappiness = workspace.initial.intraStringUnhappiness + targetUnhappiness = workspace.target.intraStringUnhappiness + logging.info('initial : relevance = %d, unhappiness=%d', + initialRelevance, int(initialUnhappiness)) + logging.info('target : relevance = %d, unhappiness=%d', + targetRelevance, int(targetUnhappiness)) + string = workspace.initial + initials = initialRelevance + initialUnhappiness + targets = targetRelevance + targetUnhappiness + if random.weighted_greater_than(targets, initials): + string = workspace.target + logging.info('target string selected: %s for %s', + workspace.target, typeName) + else: + logging.info('initial string selected: %s for %s', + workspace.initial, typeName) + source = chooseUnmodifiedObject(ctx, 'intraStringSalience', string.objects) + return source + + +def __getDescriptors(bondFacet, source, destination): + """ + Extract descriptors from source and destination objects for a given bond facet. + + Args: + bondFacet: The descriptor type/facet for the bond (e.g., letterCategory) + source: The source workspace object + destination: The destination workspace object + + Returns: + tuple: (sourceDescriptor, destinationDescriptor) describing the objects + + Raises: + AssertionError: If either object lacks the required descriptor type + """ + sourceDescriptor = source.getDescriptor(bondFacet) + destinationDescriptor = destination.getDescriptor(bondFacet) + assert sourceDescriptor and destinationDescriptor + return sourceDescriptor, destinationDescriptor + + +def __structureVsStructure(structure1, weight1, structure2, weight2): + """ + Compare two structures to determine which is stronger after temperature adjustment. + + This function implements the core competition mechanism in Copycat. Structures + compete with each other for dominance in the workspace. The comparison considers + both the inherent strength of each structure and temperature-adjusted randomness. + + Args: + structure1: First structure to compare + weight1: Weight multiplier for structure1's strength + structure2: Second structure to compare + weight2: Weight multiplier for structure2's strength + + Returns: + bool: True if structure1 wins the competition, False if structure2 wins + + Notes: + The competition process: + 1. Updates both structures' current strength values + 2. Applies temperature-adjusted weighting + 3. Uses probabilistic comparison based on weighted strengths + 4. Higher temperature = more random outcomes, lower temperature = more deterministic + """ + ctx = structure1.ctx + random = ctx.random + # TODO: use entropy + temperature = ctx.temperature + structure1.updateStrength() + structure2.updateStrength() + # TODO: use entropy + weightedStrength1 = temperature.getAdjustedValue( + structure1.totalStrength * weight1) + # TODO: use entropy + weightedStrength2 = temperature.getAdjustedValue( + structure2.totalStrength * weight2) + return random.weighted_greater_than(weightedStrength1, weightedStrength2) + + +def __fight(structure, structureWeight, incompatibles, incompatibleWeight): + """ + Execute a series of fights between a structure and incompatible structures. + + Args: + structure: The structure fighting for dominance + structureWeight: Weight multiplier for the main structure's strength + incompatibles: List of incompatible structures to fight against + incompatibleWeight: Weight multiplier for incompatible structures' strength + + Returns: + bool: True if the structure wins against all incompatibles, False otherwise + """ + if not (incompatibles and len(incompatibles)): + return True + for incompatible in incompatibles: + if not __structureVsStructure(structure, structureWeight, + incompatible, incompatibleWeight): + logging.info('lost fight with %s', incompatible) + return False + logging.info('won fight with %s', incompatible) + return True + + +def __fightIncompatibles(incompatibles, structure, name, + structureWeight, incompatibleWeight): + """ + Execute fights against incompatible structures and log the results. + + This is a wrapper around __fight() that provides additional logging about + the fight outcomes and whether structural changes ("breaking") occurred. + + Args: + incompatibles: List of incompatible structures to fight + structure: The structure fighting for dominance + name: Descriptive name for logging (e.g., 'bonds', 'groups') + structureWeight: Weight multiplier for the main structure's strength + incompatibleWeight: Weight multiplier for incompatible structures' strength + + Returns: + bool: True if fights are successful, False if the structure lost + """ + if len(incompatibles): + if __fight(structure, structureWeight, + incompatibles, incompatibleWeight): + logging.info('broke the %s', name) + return True + logging.info('failed to break %s: Fizzle', name) + return False + logging.info('no incompatible %s', name) + return True + + +def __slippability(ctx, conceptMappings): + """ + Determine if concept slippage (metaphorical thinking) is possible. + + Concept slippage allows the algorithm to make metaphorical connections + between similar concepts rather than requiring exact literal matches. + For example, seeing 'ABC' as 'first letter, second letter, third letter' + and mapping it to 'XYZ' as 'last letter, second-last letter, third-last letter'. + + Args: + ctx: The context object containing random and temperature + conceptMappings: List of possible concept mappings to evaluate + + Returns: + bool: True if at least one concept mapping allows slippage + """ + random = ctx.random + temperature = ctx.temperature + for mapping in conceptMappings: + slippiness = mapping.slippability() / 100.0 + probabilityOfSlippage = temperature.getAdjustedProbability(slippiness) + if random.coinFlip(probabilityOfSlippage): + return True + return False + + +@codelet('breaker') +def breaker(ctx, codelet): + """ + Breaker codelet that destroys existing structures based on their weakness. + + The breaker is a destructive agent that probabilistically destroys existing + structures (bonds, groups, correspondences) in the workspace. This creates + pressure for structural reorganization and prevents the algorithm from getting + stuck in local optima. + + Behavior: + 1. Probabilistically decides whether to fizzle (do nothing) based on temperature + 2. If not fizzling, randomly selects a structure to potentially break + 3. Calculates break probability based on structure's total weakness (inverse strength) + 4. Destroys structures that pass the probabilistic threshold + 5. If breaking a bond within a group, also breaks the containing group + + Args: + ctx: Context containing random, temperature, workspace components + codelet: The breaker codelet instance executing this behavior + + Notes: + Higher temperature = more likely to break structures (exploration) + Lower temperature = less likely to break structures (exploitation) + Structure weakness = 100 - structure_strength, converted to break probability + """ + # From the original LISP: + ''' + First decides probabilistically whether or not to fizzle, based on + temperature. Chooses a structure and random and decides probabilistically + whether or not to break it as a function of its total weakness. + + If the structure is a bond in a group, have to break the group in + order to break the bond. + ''' + random = ctx.random + temperature = ctx.temperature + workspace = ctx.workspace + probabilityOfFizzle = (100.0 - temperature.value()) / 100.0 + if random.coinFlip(probabilityOfFizzle): + return + # choose a structure at random + structures = [s for s in workspace.structures if + isinstance(s, (Group, Bond, Correspondence))] + assert len(structures) + structure = random.choice(structures) + __showWhichStringObjectIsFrom(structure) + breakObjects = [structure] + if isinstance(structure, Bond): + if structure.source.group: + if structure.source.group == structure.destination.group: + breakObjects += [structure.source.group] + # Break all the objects or none of them; this matches the Java + # "all objects" means a bond and its group, if it has one. + + for structure in breakObjects: + breakProbability = temperature.getAdjustedProbability( + structure.totalStrength / 100.0) + if random.coinFlip(breakProbability): + return + for structure in breakObjects: + structure.break_the_structure() + + +def chooseRelevantDescriptionByActivation(ctx, workspaceObject): + """ + Choose a relevant description based on descriptor activation levels. + + This utility function selects among all descriptions that are relevant + to a workspace object, weighting the selection by how activated each + description's descriptor is in the slipnet. + + Args: + ctx: Context containing random number generator + workspaceObject: The object to choose descriptions for + + Returns: + Description: A randomly chosen description, weighted by descriptor activation + """ + random = ctx.random + descriptions = workspaceObject.relevantDescriptions() + weights = [description.descriptor.activation for description in descriptions] + return random.weighted_choice(descriptions, weights) + + +def similarPropertyLinks(ctx, slip_node): + """ + Find property links from a slipnode that pass temperature-adjusted probability tests. + + This function implements property similarity spreading through the slipnet. + It examines all property links emanating from a given slipnode and probabilistically + activates links based on their association strength and current temperature. + + Args: + ctx: Context containing random and temperature components + slip_node: The slipnet node to evaluate property links from + + Returns: + list: List of slipnode links that passed the probability test + + Notes: + Higher association strength = higher probability of activation + Higher temperature = more random link activations (exploration) + Lower temperature = more deterministic link activations (exploitation) + """ + random = ctx.random + # TODO: use entropy + temperature = ctx.temperature + result = [] + for slip_link in slip_node.propertyLinks: + association = slip_link.degreeOfAssociation() / 100.0 + # TODO:use entropy + probability = temperature.getAdjustedProbability(association) + if random.coinFlip(probability): + result += [slip_link] + return result + + +@codelet('bottom-up-description-scout') +def bottom_up_description_scout(ctx, codelet): + """ + Bottom-up description scout that discovers new descriptions for workspace objects. + + This codelet implements data-driven description generation, starting from an object + in the workspace and working upward to discover new descriptions via property links + in the slipnet. It's called "bottom-up" because it starts with concrete objects rather + than abstract concepts. + + Behavior: + 1. Selects an object based on total salience (not yet fully described) + 2. Chooses among the object's relevant descriptions, weighted by activation + 3. Follows property links from the chosen description's descriptor + 4. Probabilistically activates property links based on association strength + 5. Proposes a new description using the destination of an activated link + + Args: + ctx: Context containing coderack, random, workspace components + codelet: The description scout codelet instance + + Notes: + This supports conceptual slippage by following property associations. + Higher activation = more likely to choose a description/link + Higher association strength = more likely to activate a property link + """ + coderack = ctx.coderack + random = ctx.random + workspace = ctx.workspace + chosenObject = chooseUnmodifiedObject(ctx, 'totalSalience', workspace.objects) + assert chosenObject + __showWhichStringObjectIsFrom(chosenObject) + # choose relevant description by activation + descriptions = chosenObject.relevantDescriptions() + weights = [d.descriptor.activation for d in descriptions] + description = random.weighted_choice(descriptions, weights) + assert description + sliplinks = similarPropertyLinks(ctx, description.descriptor) + assert sliplinks + weights = [sliplink.degreeOfAssociation() * sliplink.destination.activation + for sliplink in sliplinks] + chosen = random.weighted_choice(sliplinks, weights) + chosenProperty = chosen.destination + coderack.proposeDescription(chosenObject, chosenProperty.category(), + chosenProperty) + + +@codelet('top-down-description-scout') +def top_down_description_scout(ctx, codelet): + """ + Top-down description scout that generates descriptions guided by concept types. + + This codelet implements concept-driven description generation, starting from a + specific description type (e.g., letter category, position) and working downward + to find objects that could have that description. It's called "top-down" because + it starts with abstract concept types rather than конкретные objects. + + Behavior: + 1. Gets a description type from the codelet's arguments + 2. Selects an object based on total salience (not yet fully described) + 3. Retrieves all possible descriptions of that type for the object + 4. Chooses among possible descriptions, weighted by activation + 5. Proposes a new description using the chosen property + + Args: + ctx: Context containing coderack, random, workspace components + codelet: The description scout codelet instance with descriptionType argument + + Notes: + This enables goal-directed description generation based on concept categories + Contrasts with bottom-up scouts that generate descriptions opportunistically + Used when the system has a preference for certain types of descriptions + """ + coderack = ctx.coderack + random = ctx.random + workspace = ctx.workspace + descriptionType = codelet.arguments[0] + chosenObject = chooseUnmodifiedObject(ctx, 'totalSalience', workspace.objects) + assert chosenObject + __showWhichStringObjectIsFrom(chosenObject) + descriptions = chosenObject.getPossibleDescriptions(descriptionType) + assert descriptions and len(descriptions) + weights = [n.activation for n in descriptions] + chosenProperty = random.weighted_choice(descriptions, weights) + coderack.proposeDescription(chosenObject, chosenProperty.category(), + chosenProperty) + + +@codelet('description-strength-tester') +def description_strength_tester(ctx, codelet): + """ + Tests the strength of a proposed description to see if it's worth building. + + This codelet evaluates whether a previously proposed description is strong enough + to warrant creation in the workspace. Only descriptions that pass this probabilistic + strength test proceed to the description-builder. + + Behavior: + 1. Activates the description's descriptor (maximul buffer activation) + 2. Updates and evaluates the description's total Strength + 3. Converts strength to probability via temperature adjustment + 4. If passes probability test, queues description-builder codelet + 5. If fails, the description is abandoned (fizzles) + + Args: + ctx: Context containing coderack, random, temperature components + codelet: Codelet with argument containing the description to test + + Notes: + Higher description strength = higher probability of acceptance + Temperature affects the acceptance probability (higher temp = more lenient) + Failed descriptions are discarded, promoting quality over quantity + """ + coderack = ctx.coderack + random = ctx.random + # TODO: use entropy + temperature = ctx.temperature + description = codelet.arguments[0] + description.descriptor.buffer = 100.0 + description.updateStrength() + strength = description.totalStrength + # TODO: use entropy + probability = temperature.getAdjustedProbability(strength / 100.0) + assert random.coinFlip(probability) + coderack.newCodelet('description-builder', strength, [description]) + + +@codelet('description-builder') +def description_builder(ctx, codelet): + """ + Builds a description that has passed the strength test. + + This codelet actually constructs descriptions in the workspace. It handles two + scenarios: either the object already has this descriptor (activation only) or + the descriptor needs to be newly created. + + Behavior: + 1. Verifies the target object still exists in the workspace + 2. Checks if the object already has this specific descriptor + 3. If already described: activates concepts only + 4. If not described: builds the new descriptor relationship + 5. Updates buffer activations for relevant concepts + + Args: + ctx: Context containing workspace + codelet: Codelet with argument containing the description to build + + Notes: + This implements the final step of description creation after strength testing + Distinguishes between creating new descriptions vs. reactivating existing ones + Ensures consistency with workspace state + """ + workspace = ctx.workspace + description = codelet.arguments[0] + assert description.object in workspace.objects + if description.object.described(description.descriptor): + description.descriptionType.buffer = 100.0 + description.descriptor.buffer = 100.0 + else: + description.build() + + +def __supportForDescriptionType(ctx, descriptionType, string): + """ + Calculate support score for a description type within a specific string. + + This function measures how well-supported a description type is within a string + by examining both the activation of the concept in the slipnet and the + prevalence of that description type among objects in the string. + + Args: + ctx: Context containing workspace + descriptionType: The type of description to evaluate (e.g., letterCategory) + string: The workspace string to analyze + + Returns: + float: Support score between 0-1 (higher = better supported) + + Notes: + Support combines conceptual activation and empirical prevalence + High support indicates the description type is both conceptually active + and empirically common within the string + """ + workspace = ctx.workspace + described_count = 0 + total = 0 + for o in workspace.objects: + if o.string == string: + total += 1 + described_count += sum(1 for d in o.descriptions if d.descriptionType == descriptionType) + string_support = described_count / float(total) + return (descriptionType.activation + string_support) / 2 + + +def __chooseBondFacet(ctx, source, destination): + """ + Select the best descriptor type/facet for forming a bond between two objects. + + This function implements the bond facet selection heuristic, choosing among + available descriptor types that both objects can participate in, weighting + the choice by how well-supported each facet is in the source object's string. + + Args: + ctx: Context containing random, slipnet components + source: Source object for the bond + destination: Destination object for the bond + + Returns: + Slipnode: The chosen bond facet/slot for the bond + + Notes: + Only considers descriptor types that both objects actually have + Supports include letterCategory and length by default + Weighted selection favors facets with higher support scores + Supports probabilistic exploration of bond possibilities + """ + random = ctx.random + slipnet = ctx.slipnet + + # specify the descriptor types that bonds can form between + b = [ + slipnet.letterCategory, + slipnet.length, + ] + + sourceFacets = [d.descriptionType for d in source.descriptions if d.descriptionType in b] + bondFacets = [d.descriptionType for d in destination.descriptions if d.descriptionType in sourceFacets] + supports = [__supportForDescriptionType(ctx, f, source.string) for f in bondFacets] + return random.weighted_choice(bondFacets, supports) + + +@codelet('bottom-up-bond-scout') +def bottom_up_bond_scout(ctx, codelet): + """ + Bottom-up bond scout that discovers bonds between neighboring objects. + + This codelet implements data-driven bond formation, starting from objects + in the workspace and seeking to create bonds with their immediate neighbors. + It's called "bottom-up" because it starts with concrete objects rather than + abstract bond categories. + + Behavior: + 1. Selects a source object based on intra-string salience + 2. Chooses a neighbor of the source object as destination + 3. Determines the best bond facet/description type to use + 4. Extracts descriptors for both objects using the chosen facet + 5. Determines bond category (sameness, successor, etc.) from descriptors + 6. Proposes the bond for strength testing and potential creation + + Args: + ctx: Context containing coderack, slipnet, workspace components + codelet: The bond scout codelet instance + + Notes: + This enables opportunistic bond discovery between adjacent objects + Considers both letter-based and structural (length) relationships + Maps onto adjacent objects to form bonds within strings + Successor bonds require specific letter relationships + """ + coderack = ctx.coderack + slipnet = ctx.slipnet + workspace = ctx.workspace + source = chooseUnmodifiedObject(ctx, 'intraStringSalience', workspace.objects) + assert source is not None + __showWhichStringObjectIsFrom(source) + destination = chooseNeighbor(ctx, source) + assert destination + logging.info('destination: %s', destination) + bondFacet = __chooseBondFacet(ctx, source, destination) + assert bondFacet + logging.info('chosen bond facet: %s', bondFacet.get_name()) + logging.info('Source: %s, destination: %s', source, destination) + bond_descriptors = __getDescriptors(bondFacet, source, destination) + sourceDescriptor, destinationDescriptor = bond_descriptors + logging.info("source descriptor: %s", sourceDescriptor.name.upper()) + logging.info("destination descriptor: %s", + destinationDescriptor.name.upper()) + category = sourceDescriptor.getBondCategory(destinationDescriptor) + assert category + if category == slipnet.identity: + category = slipnet.sameness + logging.info('proposing %s bond ', category.name) + coderack.proposeBond(source, destination, category, bondFacet, + sourceDescriptor, destinationDescriptor) + + +@codelet('rule-scout') +def rule_scout(ctx, codelet): + """ + Rule scout that generates transformation rules based on letter changes. + + This codelet analyzes which letters changed between initial and modified strings + and attempts to generalize these changes into rules. Rules represent patterns + like "change leftmost letter to its successor" or "change 'b' to 'd'". + + Behavior: + 1. Assumes all letters have been replaced (no unreplaced objects) + 2. Finds objects that have changed (have replacements) + 3. If no changes, proposes empty rule + 4. For the last changed object, generates distinguishing descriptors + 5. Filters descriptors based on correspondence slippages and target objects + 6. Selects descriptor using conceptual depth and temperature weighting + 7. Selects relational transformation using conceptual depth weighting + 8. Proposes the rule for strength testing + + Args: + ctx: Context containing coderack, random, slipnet, temperature, workspace + codelet: The rule scout codelet instance + + Notes: + Distinguishing descriptors include position (leftmost, rightmost, middle) + and letter category (if unique within the string) + Conceptual depth guides selection toward more abstract, human-like rules + Temperature affects both descriptor and relation selection probabilities + Only generates rules for letter transformations, not other structure changes + """ + coderack = ctx.coderack + random = ctx.random + slipnet = ctx.slipnet + # TODO: use entropy + temperature = ctx.temperature + workspace = ctx.workspace + assert workspace.numberOfUnreplacedObjects() == 0 + changedObjects = [o for o in workspace.initial.objects if o.changed] + # assert len(changedObjects) < 2 + # if there are no changed objects, propose a rule with no changes + if not changedObjects: + return coderack.proposeRule(None, None, None, None) + + changed = changedObjects[-1] + # generate a list of distinguishing descriptions for the first object + # ie. string-position (left-,right-most,middle or whole) or letter category + # if it is the only one of its type in the string + objectList = [] + position = changed.getDescriptor(slipnet.stringPositionCategory) + if position: + objectList += [position] + letter = changed.getDescriptor(slipnet.letterCategory) + otherObjectsOfSameLetter = [o for o in workspace.initial.objects + if not o != changed + and o.getDescriptionType(letter)] + if not len(otherObjectsOfSameLetter): + objectList += [letter] + # if this object corresponds to another object in the workspace + # objectList = the union of this and the distingushing descriptors + if changed.correspondence: + targetObject = changed.correspondence.objectFromTarget + newList = [] + slippages = workspace.slippages() + for node in objectList: + node = node.applySlippages(slippages) + if targetObject.described(node): + if targetObject.distinguishingDescriptor(node): + newList += [node] + objectList = newList # surely this should be += + # "union of this and distinguishing descriptors" + assert objectList + # use conceptual depth to choose a description + # TODO: use entropy + weights = [ + temperature.getAdjustedValue(node.conceptualDepth) + for node in objectList + ] + descriptor = random.weighted_choice(objectList, weights) + # choose the relation (change the leftmost object to "successor" or "d" + objectList = [] + if changed.replacement.relation: + objectList += [changed.replacement.relation] + objectList += [changed.replacement.objectFromModified.getDescriptor( + slipnet.letterCategory)] + # TODO: use entropy + # use conceptual depth to choose a relation + weights = [ + temperature.getAdjustedValue(node.conceptualDepth) + for node in objectList + ] + relation = random.weighted_choice(objectList, weights) + coderack.proposeRule(slipnet.letterCategory, descriptor, + slipnet.letter, relation) + + +@codelet('rule-strength-tester') +def rule_strength_tester(ctx, codelet): + """ + Tests the strength of a proposed rule to determine if it should be built. + + This codelet evaluates whether a rule generated by rule-scout is strong enough + to warrant creation in the workspace. Only rules that pass this probabilistic + strength test proceed to the rule-builder. + + Behavior: + 1. Gets the rule to test from codelet arguments + 2. Updates the rule's strength based on current workspace state + 3. Converts strength to acceptance probability via temperature adjustment + 4. If passes probability test, queues rule-builder codelet + 5. If fails, the rule is discarded (fizzles) + + Args: + ctx: Context containing coderack, random, temperature components + codelet: Codelet with argument containing the rule to test + + Notes: + Rule strength depends on how well the rule describes the observed changes + Higher temperature = more lenient rule acceptance (exploration) + Lower temperature = stricter rule acceptance (exploitation) + Failed rules are discarded, ensuring only coherent patterns are adopted + """ + coderack = ctx.coderack + random = ctx.random + # TODO: use entropy + temperature = ctx.temperature + rule = codelet.arguments[0] + rule.updateStrength() + # TODO: use entropy + probability = temperature.getAdjustedProbability(rule.totalStrength / 100.0) + if random.coinFlip(probability): + coderack.newCodelet('rule-builder', rule.totalStrength, [rule]) + + +@codelet('replacement-finder') +def replacement_finder(ctx, codelet): + """ + Finds and establishes letter replacements between initial and modified strings. + + This codelet analyzes character-by-character changes between the initial string + and modified string, creating Replacement objects that represent letter transformations. + This is a prerequisite for rule generation, as rules describe patterns in letter changes. + + Behavior: + 1. Randomly selects an unmodified letter from the initial string + 2. Finds the corresponding letter at the same position in modified string + 3. Calculates the ASCII difference between the letters + 4. Maps small differences to semantic relations (sameness, successor, predecessor) + 5. Creates a Replacement object linking the letters with their relation + 6. Marks the letter as changed if transformation is not sameness + 7. Updates the workspace's changed object for rule generation + + Args: + ctx: Context containing random, slipnet, workspace components + codelet: The replacement finder codelet instance + + Notes: + Only processes ASCII differences < 2 (immediate neighbors in alphabet) + Larger differences create replacements without semantic relations + Successor/predecessor relations support rule patterns like "increment alphabet" + Changed objects trigger rule generation by rule-scout codelets + Requires exact positional correspondence between initial and modified strings + """ + random = ctx.random + slipnet = ctx.slipnet + workspace = ctx.workspace + # choose random letter in initial string + letters = [o for o in workspace.initial.objects if isinstance(o, Letter)] + assert letters + letterOfInitialString = random.choice(letters) + assert not letterOfInitialString.replacement + position = letterOfInitialString.leftIndex + moreLetters = [o for o in workspace.modified.objects + if isinstance(o, Letter) and o.leftIndex == position] + assert moreLetters + letterOfModifiedString = moreLetters[0] + initialAscii = ord(workspace.initialString[position - 1]) + modifiedAscii = ord(workspace.modifiedString[position - 1]) + diff = initialAscii - modifiedAscii + if abs(diff) < 2: + relations = { + 0: slipnet.sameness, + -1: slipnet.successor, + 1: slipnet.predecessor + } + relation = relations[diff] + else: + relation = None + letterOfInitialString.replacement = Replacement(ctx, letterOfInitialString, + letterOfModifiedString, relation) + if relation != slipnet.sameness: + letterOfInitialString.changed = True + workspace.changedObject = letterOfInitialString + + +@codelet('top-down-bond-scout--category') +def top_down_bond_scout__category(ctx, codelet): + """ + Top-down bond scout guided by specific bond category (sameness, successor, etc.). + + This codelet implements concept-driven bond formation, starting from a specific + bond category and seeking to create bonds that match that category. It's called + "top-down" because it starts with abstract relationship concepts rather than + concrete object properties. + + Behavior: + 1. Gets bond category from codelet arguments (e.g., sameness, successor) + 2. Selects source object using category relevance to initial/target strings + 3. Chooses neighbor of source object as destination + 4. Determines best bond facet/description type for both objects + 5. Extracts descriptors for source and destination objects + 6. Calculates forward bond category (source -> destination) + 7. Handles identity bonds by converting to sameness category + 8. Calculates backward bond category (destination -> source) + 9. Proposes bond in correct direction to match the desired category + + Args: + ctx: Context containing coderack, slipnet, workspace components + codelet: Codelet with bond category as argument + + Notes: + Bidirectional bond evaluation ensures optimal direction matching + Category-guided selection promotes goal-directed bond formation + Distinguishes between forward and backward bond categories + Integrates with conceptual relevance assessment for source selection + """ + coderack = ctx.coderack + slipnet = ctx.slipnet + logging.info('top_down_bond_scout__category') + category = codelet.arguments[0] + source = __getScoutSource(ctx, category, formulas.localBondCategoryRelevance, + 'bond') + destination = chooseNeighbor(ctx, source) + logging.info('source: %s, destination: %s', source, destination) + assert destination + bondFacet = __chooseBondFacet(ctx, source, destination) + assert bondFacet + sourceDescriptor, destinationDescriptor = __getDescriptors( + bondFacet, source, destination) + forwardBond = sourceDescriptor.getBondCategory(destinationDescriptor) + if forwardBond == slipnet.identity: + forwardBond = slipnet.sameness + backwardBond = slipnet.sameness + else: + backwardBond = destinationDescriptor.getBondCategory(sourceDescriptor) + assert category == forwardBond or category == backwardBond + if category == forwardBond: + coderack.proposeBond(source, destination, category, + bondFacet, sourceDescriptor, + destinationDescriptor) + else: + coderack.proposeBond(destination, source, category, + bondFacet, destinationDescriptor, + sourceDescriptor) + + +@codelet('top-down-bond-scout--direction') +def top_down_bond_scout__direction(ctx, codelet): + """ + Top-down bond scout guided by specific direction category (left, right). + + This codelet implements direction-driven bond formation, starting from a specific + direction concept (left or right) and seeking to create bonds in that direction. + It's another form of top-down processing that focuses on spatial relationships + rather than semantic bond categories. + + Behavior: + 1. Gets direction category from codelet arguments (left or right) + 2. Selects source object using direction relevance to initial/target strings + 3. Chooses neighbor in the specific direction using chooseDirectedNeighbor + 4. Determines best bond facet/description type for both objects + 5. Extracts descriptors for source and destination objects + 6. Determines bond category from the descriptor relationship + 7. Handles identity bonds by converting to sameness category + 8. Proposes bond with determined category and descriptors + + Args: + ctx: Context containing coderack, slipnet, workspace components + codelet: Codelet with direction category as argument + + Notes: + Direction-guided selection promotes spatially coherent bond formation + Unlike category scouts, this focuses on object adjacency and position + Uses direction relevance assessment for optimal source object selection + Enables goal-directed bond discovery based on directional preferences + """ + coderack = ctx.coderack + slipnet = ctx.slipnet + direction = codelet.arguments[0] + source = __getScoutSource(ctx, direction, formulas.localDirectionCategoryRelevance, + 'bond') + destination = chooseDirectedNeighbor(ctx, source, direction) + assert destination + logging.info('to object: %s', destination) + bondFacet = __chooseBondFacet(ctx, source, destination) + assert bondFacet + sourceDescriptor, destinationDescriptor = __getDescriptors( + bondFacet, source, destination) + category = sourceDescriptor.getBondCategory(destinationDescriptor) + assert category + if category == slipnet.identity: + category = slipnet.sameness + coderack.proposeBond(source, destination, category, bondFacet, + sourceDescriptor, destinationDescriptor) + + +@codelet('bond-strength-tester') +def bond_strength_tester(ctx, codelet): + """ + Tests the strength of a proposed bond to determine if it should be built. + + This codelet evaluates whether a bond generated by bond scouts is strong enough + to warrant creation in the workspace. Only bonds that pass this probabilistic + strength test proceed to the bond-builder. + + Behavior: + 1. Gets the bond to test from codelet arguments + 2. Updates the bond's strength based on current workspace state + 3. Converts strength to acceptance probability via temperature adjustment + 4. Logs bond strength for debugging purposes + 5. If passes probability test, activates bond descriptors and queues bond-builder + 6. If fails, the bond is discarded (fizzles) + + Args: + ctx: Context containing coderack, random, temperature components + codelet: Codelet with argument containing the bond to test + + Notes: + Bond strength depends on descriptor compatibility and workspace constraints + Higher temperature = more lenient bond acceptance (exploration) + Lower temperature = stricter bond acceptance (exploitation) + Activating descriptors promotes relevant concepts for future bond formation + """ + coderack = ctx.coderack + random = ctx.random + # TODO: use entropy + temperature = ctx.temperature + bond = codelet.arguments[0] + __showWhichStringObjectIsFrom(bond) + bond.updateStrength() + strength = bond.totalStrength + # TODO: use entropy + probability = temperature.getAdjustedProbability(strength / 100.0) + logging.info('bond strength = %d for %s', strength, bond) + assert random.coinFlip(probability) + bond.facet.buffer = 100.0 + bond.sourceDescriptor.buffer = 100.0 + bond.destinationDescriptor.buffer = 100.0 + logging.info("succeeded: posting bond-builder") + coderack.newCodelet('bond-builder', strength, [bond]) + + +@codelet('bond-builder') +def bond_builder(ctx, codelet): + """ + Builds a bond that has passed the strength test. + + This codelet actually constructs bonds in the workspace after they have + passed strength testing. It handles conflicts with existing structures + and ensures bond creation follows workspace constraints. + + Behavior: + 1. Verifies source and destination objects still exist in workspace + 2. Checks for existing equivalent bonds (same neighbors and categories) + 3. If equivalent exists, activates descriptors and exits early + 4. Identifies incompatible bonds that would conflict with this bond + 5. Fights incompatible bonds using strength-based competition + 6. Identifies incompatible groups that would conflict + 7. Fights incompatible groups using strength-based competition + 8. Handles special cases for end-of-string bonds with correspondences + 9. Breaks/removes all defeated incompatible structures + 10. Finally constructs the bond in the workspace + + Args: + ctx: Context containing workspace + codelet: Codelet with argument containing the bond to build + + Notes: + Bond building involves complex conflict resolution with existing structures + Incompatible structures are removed only if the new bond wins competitions + Special consideration for end-of-string bonds affects correspondence handling + Successful bond creation activates bond categories and descriptor concepts + """ + workspace = ctx.workspace + bond = codelet.arguments[0] + __showWhichStringObjectIsFrom(bond) + bond.updateStrength() + assert (bond.source in workspace.objects or + bond.destination in workspace.objects) + for stringBond in bond.string.bonds: + if bond.sameNeighbors(stringBond) and bond.sameCategories(stringBond): + if bond.directionCategory: + bond.directionCategory.buffer = 100.0 + bond.category.buffer = 100.0 + logging.info('already exists: activate descriptors & Fizzle') + return + incompatibleBonds = bond.getIncompatibleBonds() + logging.info('number of incompatibleBonds: %d', len(incompatibleBonds)) + if len(incompatibleBonds): + logging.info('%s', incompatibleBonds[0]) + assert __fightIncompatibles(incompatibleBonds, bond, 'bonds', 1.0, 1.0) + incompatibleGroups = bond.source.getCommonGroups(bond.destination) + assert __fightIncompatibles(incompatibleGroups, bond, 'groups', 1.0, 1.0) + # fight all incompatible correspondences + incompatibleCorrespondences = [] + if bond.leftObject.leftmost or bond.rightObject.rightmost: + if bond.directionCategory: + incompatibleCorrespondences = bond.getIncompatibleCorrespondences() + if incompatibleCorrespondences: + logging.info("trying to break incompatible correspondences") + assert __fight(bond, 2.0, incompatibleCorrespondences, 3.0) + # assert __fightIncompatibles(incompatibleCorrespondences, + # bond, 'correspondences', 2.0, 3.0) + for incompatible in incompatibleBonds: + incompatible.break_the_structure() + for incompatible in incompatibleGroups: + incompatible.break_the_structure() + for incompatible in incompatibleCorrespondences: + incompatible.break_the_structure() + logging.info('building bond %s', bond) + bond.buildBond() + + +# pylint: disable=too-many-branches +# pylint: disable=too-many-statements +@codelet('top-down-group-scout--category') +def top_down_group_scout__category(ctx, codelet): + """ + Top-down group scout guided by specific group category. + + This codelet implements concept-driven group formation, starting from a specific + group category (sameness group, successor group, etc.) and seeking to create + groups that match that category. It's called "top-down" because it starts with + abstract group concepts rather than concrete object relationships. + + Behavior: + 1. Gets group category from codelet arguments + 2. Maps group category to corresponding bond category + 3. Selects source object using bond category relevance to initial/target strings + 4. Determines group direction (left/right) based on source position or preference + 5. Searches leftward from source to find leftmost object with matching bonds + 6. Searchers rightward from leftmost to find rightmost object with matching bonds + 7. Collects all objects and bonds between the span + 8. Proposes group if valid span is found + + Args: + ctx: Context containing coderack, random, slipnet, workspace components + codelet: Codelet with group category as argument + + Notes: + Groups combine multiple objects linked by bonds of the same category and direction + Search strategy ensures cohesive groups with consistent bond properties + Handles singleton letter groups when no bonds match the desired category + Direction preference influences group formation toward left or right + """ + coderack = ctx.coderack + random = ctx.random + slipnet = ctx.slipnet + groupCategory = codelet.arguments[0] + category = groupCategory.getRelatedNode(slipnet.bondCategory) + assert category + source = __getScoutSource(ctx, category, formulas.localBondCategoryRelevance, + 'group') + assert source and not source.spansString() + if source.leftmost: + direction = slipnet.right + elif source.rightmost: + direction = slipnet.left + else: + direction = random.weighted_choice( + [slipnet.left, slipnet.right], + [slipnet.left.activation, slipnet.right.activation] + ) + if direction == slipnet.left: + firstBond = source.leftBond + else: + firstBond = source.rightBond + if not firstBond or firstBond.category != category: + # check the other side of object + if direction == slipnet.right: + firstBond = source.leftBond + else: + firstBond = source.rightBond + if not firstBond or firstBond.category != category: + if category == slipnet.sameness and isinstance(source, Letter): + group = Group(source.string, slipnet.samenessGroup, + None, slipnet.letterCategory, [source], []) + probability = group.singleLetterGroupProbability() + if random.coinFlip(probability): + coderack.proposeSingleLetterGroup(source) + return + direction = firstBond.directionCategory + search = True + bondFacet = None + # find leftmost object in group with these bonds + while search: + search = False + if not source.leftBond: + continue + if source.leftBond.category != category: + continue + if source.leftBond.directionCategory != direction: + if source.leftBond.directionCategory: + continue + if not bondFacet or bondFacet == source.leftBond.facet: + bondFacet = source.leftBond.facet + direction = source.leftBond.directionCategory + source = source.leftBond.leftObject + search = True + # find rightmost object in group with these bonds + search = True + destination = source + while search: + search = False + if not destination.rightBond: + continue + if destination.rightBond.category != category: + continue + if destination.rightBond.directionCategory != direction: + if destination.rightBond.directionCategory: + continue + if not bondFacet or bondFacet == destination.rightBond.facet: + bondFacet = destination.rightBond.facet + direction = source.rightBond.directionCategory + destination = destination.rightBond.rightObject + search = True + assert destination != source + objects = [source] + bonds = [] + while source != destination: + bonds += [source.rightBond] + objects += [source.rightBond.rightObject] + source = source.rightBond.rightObject + coderack.proposeGroup(objects, bonds, groupCategory, + direction, bondFacet) + + +@codelet('top-down-group-scout--direction') +def top_down_group_scout__direction(ctx, codelet): + """ + Top-down group scout guided by specific direction category. + + This codelet implements direction-driven group formation, starting from a specific + direction concept (left or right) and seeking to create groups in that direction. + It's another form of top-down processing that focuses on spatial directional + relationships rather than semantic group categories. + + Behavior: + 1. Gets direction category from codelet arguments (left or right) + 2. Selects source object using direction relevance to initial/target strings + 3. Determines preferred direction based on source position or activation + 4. Checks for existing bonds with matching direction category + 5. If no suitable bonds, searches in alternate direction + 6. Extends group by walking leftward from source with matching bonds + 7. Extends group by walking rightward from leftmost with matching bonds + 8. Collects all objects and bonds within the directional span + 9. Proposes group with determined category and direction + + Args: + ctx: Context containing coderack, random, slipnet, workspace components + codelet: Codelet with direction category as argument + + Notes: + Direction preference influences which bonds are considered for group formation + Groups formed this way maintain consistent directional flow + Handles cases where no suitable directional bonds exist initially + Enables goal-directed group discovery based on directional coherence + """ + coderack = ctx.coderack + random = ctx.random + slipnet = ctx.slipnet + direction = codelet.arguments[0] + source = __getScoutSource(ctx, direction, + formulas.localDirectionCategoryRelevance, + 'direction') + logging.info('source chosen = %s', source) + assert not source.spansString() + if source.leftmost: + mydirection = slipnet.right + elif source.rightmost: + mydirection = slipnet.left + else: + mydirection = random.weighted_choice( + [slipnet.left, slipnet.right], + [slipnet.left.activation, slipnet.right.activation] + ) + if mydirection == slipnet.left: + firstBond = source.leftBond + else: + firstBond = source.rightBond + if not firstBond: + logging.info('no firstBond') + else: + logging.info('firstBond: %s', firstBond) + if firstBond and not firstBond.directionCategory: + direction = None + if not firstBond or firstBond.directionCategory != direction: + if mydirection == slipnet.right: + firstBond = source.leftBond + else: + firstBond = source.rightBond + if not firstBond: + logging.info('no firstBond2') + else: + logging.info('firstBond2: %s', firstBond) + if firstBond and not firstBond.directionCategory: + direction = None + assert firstBond and firstBond.directionCategory == direction + logging.info('possible group: %s', firstBond) + category = firstBond.category + assert category + groupCategory = category.getRelatedNode(slipnet.groupCategory) + logging.info('trying from %s to %s', source, category.name) + bondFacet = None + # find leftmost object in group with these bonds + search = True + while search: + search = False + if not source.leftBond: + continue + if source.leftBond.category != category: + continue + if source.leftBond.directionCategory != direction: + if source.leftBond.directionCategory: + continue + if not bondFacet or bondFacet == source.leftBond.facet: + bondFacet = source.leftBond.facet + direction = source.leftBond.directionCategory + source = source.leftBond.leftObject + search = True + destination = source + search = True + while search: + search = False + if not destination.rightBond: + continue + if destination.rightBond.category != category: + continue + if destination.rightBond.directionCategory != direction: + if destination.rightBond.directionCategory: + continue + if not bondFacet or bondFacet == destination.rightBond.facet: + bondFacet = destination.rightBond.facet + direction = source.rightBond.directionCategory + destination = destination.rightBond.rightObject + search = True + assert destination != source + logging.info('proposing group from %s to %s', source, destination) + objects = [source] + bonds = [] + while source != destination: + bonds += [source.rightBond] + objects += [source.rightBond.rightObject] + source = source.rightBond.rightObject + coderack.proposeGroup(objects, bonds, groupCategory, + direction, bondFacet) + + +# noinspection PyStringFormat +@codelet('group-scout--whole-string') +def group_scout__whole_string(ctx, codelet): + """ + Whole-string group scout that attempts to group entire strings. + + This codelet implements comprehensive group formation by attempting to create + groups that span entire strings or large portions thereof. It starts from the + leftmost object and walks through all bonds to identify potential whole-string + groupings. + + Behavior: + 1. Randomly selects either initial or target string to analyze + 2. Finds the leftmost object in the selected string + 3. Walks up the group hierarchy to find highest sameness group candidate + 4. If object already spans string, proposes that existing group or singleton + 5. Otherwise, walks through all bonds from leftmost to rightmost + 6. Randomly selects one bond to determine group category and direction + 7. Filters bonds to those compatible with group formation constraints + 8. Proposes group spanning entire string with consistent properties + + Args: + ctx: Context containing coderack, random, slipnet, workspace components + codelet: The whole-string group scout codelet instance + + Notes: + This implements the most comprehensive group formation strategy + Considers existing group hierarchies to avoid redundant groupings + Random bond selection introduces probabilistic diversity in group categories + Handles both full string spanning groups and individual singleton letters + Enables discovery of patterns that encompass entire string structures + """ + coderack = ctx.coderack + random = ctx.random + slipnet = ctx.slipnet + workspace = ctx.workspace + string = random.choice([workspace.initial, workspace.target]) + # find leftmost object & the highest group to which it belongs + leftmost = next((o for o in string.objects if o.leftmost), None) + assert leftmost is not None + while leftmost.group and leftmost.group.bondCategory == slipnet.sameness: + leftmost = leftmost.group + if leftmost.spansString(): + # the object already spans the string - propose this object + if isinstance(leftmost, Group): + group = leftmost + coderack.proposeGroup(group.objectList, group.bondList, + group.groupCategory, group.directionCategory, + group.facet) + else: + coderack.proposeSingleLetterGroup(leftmost) + return + bonds = [] + objects = [leftmost] + while leftmost.rightBond: + bonds += [leftmost.rightBond] + leftmost = leftmost.rightBond.rightObject + objects += [leftmost] + assert leftmost.rightmost + # choose a random bond from list + chosenBond = random.choice(bonds) + bonds = chosenBond.possibleGroupBonds(bonds) + assert bonds + category = chosenBond.category + groupCategory = category.getRelatedNode(slipnet.groupCategory) + directionCategory = chosenBond.directionCategory + bondFacet = chosenBond.facet + coderack.proposeGroup(objects, bonds, groupCategory, directionCategory, bondFacet) + + +@codelet('group-strength-tester') +def group_strength_tester(ctx, codelet): + """ + Tests the strength of a proposed group to determine if it should be built. + + This codelet evaluates whether a group generated by group scouts is strong enough + to warrant creation in the workspace. Only groups that pass this probabilistic + strength test proceed to the group-builder. + + Behavior: + 1. Gets the group to test from codelet arguments + 2. Updates the group's strength based on current workspace state + 3. Converts strength to acceptance probability via temperature adjustment + 4. Logs group strength for debugging purposes + 5. If passes probability test, activates related concepts and queues group-builder + 6. If fails, the group is discarded (fizzles) + + Args: + ctx: Context containing coderack, random, temperature components + codelet: Codelet with argument containing the group to test + + Notes: + Group strength depends on cohesion of constituent bonds and object relationships + Higher temperature = more lenient group acceptance (exploration) + Lower temperature = stricter group acceptance (exploitation) + Activating related bond and direction concepts supports future group formation + Failed groups are discarded, ensuring only coherent patterns are adopted + """ + coderack = ctx.coderack + random = ctx.random + slipnet = ctx.slipnet + # TODO: use entropy + temperature = ctx.temperature + # update strength value of the group + group = codelet.arguments[0] + __showWhichStringObjectIsFrom(group) + group.updateStrength() + strength = group.totalStrength + # TODO: use entropy + probability = temperature.getAdjustedProbability(strength / 100.0) + if random.coinFlip(probability): + # it is strong enough - post builder & activate nodes + group.groupCategory.getRelatedNode(slipnet.bondCategory).buffer = 100.0 + if group.directionCategory: + group.directionCategory.buffer = 100.0 + coderack.newCodelet('group-builder', strength, [group]) + + +@codelet('group-builder') +def group_builder(ctx, codelet): + """ + Builds a group that has passed the strength test. + + This codelet actually constructs groups in the workspace after they have + passed strength testing. It handles conflicts with existing structures, + creates necessary bonds, and ensures group creation follows workspace + constraints. + + Behavior: + 1. Verifies the group proposal and checks for equivalent existing groups + 2. If equivalent exists, activates group descriptors and exits early + 3. Validates that all group objects still exist in workspace + 4. Identifies incompatible bonds that would conflict with group formation + 5. Fights incompatible bonds using strength-based competition + 6. Identifies incompatible groups that contain conflicting object subsets + 7. Fights incompatible groups using strength-based competition + 8. Creates missing bonds between adjacent objects in the group + 9. Builds the group structure in the workspace + 10. Activates group-related conceptual descriptions + + Args: + ctx: Context containing slipnet, workspace components + codelet: Codelet with argument containing the group to build + + Notes: + Group building involves complex conflict resolution with existing structures + Incompatible structures are removed only if the new group wins competitions + Missing bonds are automatically created between group objects + Successful group creation activates group categories and descriptor concepts + Groups provide hierarchical organization for sets of related objects + """ + slipnet = ctx.slipnet + workspace = ctx.workspace + # update strength value of the group + group = codelet.arguments[0] + __showWhichStringObjectIsFrom(group) + equivalent = group.string.equivalentGroup(group) + if equivalent: + logging.info('already exists...activate descriptors & fizzle') + group.activateDescriptions() + equivalent.addDescriptions(group.descriptions) + return + # check to see if all objects are still there + for o in group.objectList: + assert o in workspace.objects + # check to see if bonds are there of the same direction + incompatibleBonds = [] # incompatible bond list + if len(group.objectList) > 1: + previous = group.objectList[0] + for objekt in group.objectList[1:]: + leftBond = objekt.leftBond + if leftBond: + if leftBond.leftObject == previous: + continue + if leftBond.directionCategory == group.directionCategory: + continue + incompatibleBonds += [leftBond] + previous = objekt + next_object = group.objectList[-1] + for objekt in reversed(group.objectList[:-1]): + rightBond = objekt.rightBond + if rightBond: + if rightBond.rightObject == next_object: + continue + if rightBond.directionCategory == group.directionCategory: + continue + incompatibleBonds += [rightBond] + next_object = objekt + # if incompatible bonds exist - fight + group.updateStrength() + assert __fightIncompatibles(incompatibleBonds, group, 'bonds', 1.0, 1.0) + # fight incompatible groups + # fight all groups containing these objects + incompatibleGroups = group.getIncompatibleGroups() + assert __fightIncompatibles(incompatibleGroups, group, 'Groups', 1.0, 1.0) + for incompatible in incompatibleBonds: + incompatible.break_the_structure() + # create new bonds + group.bondList = [] + for i in range(1, len(group.objectList)): + object1 = group.objectList[i - 1] + object2 = group.objectList[i] + if not object1.rightBond: + if group.directionCategory == slipnet.right: + source = object1 + destination = object2 + else: + source = object2 + destination = object1 + category = group.groupCategory.getRelatedNode(slipnet.bondCategory) + facet = group.facet + newBond = Bond(ctx, source, destination, category, facet, + source.getDescriptor(facet), + destination.getDescriptor(facet)) + newBond.buildBond() + group.bondList += [object1.rightBond] + for incompatible in incompatibleGroups: + incompatible.break_the_structure() + group.buildGroup() + group.activateDescriptions() + logging.info('building group') + + +@codelet('rule-builder') +def rule_builder(ctx, codelet): + """ + Builds a rule that has passed the strength test. + + This codelet actually constructs rules in the workspace after they have + passed strength testing. It handles rule equivalence checks and implements + the competitive rule replacement mechanism. + + Behavior: + 1. Gets the rule to build from codelet arguments + 2. Checks if the rule is equivalent to the existing workspace rule + 3. If equivalent, activates rule descriptors and exits early + 4. Updates rule strength based on current workspace state + 5. If workspace already has a rule, fights for replacement + 6. Only proceeds if new rule wins the strength competition + 7. Builds the rule in the workspace, replacing any existing rule + + Args: + ctx: Context containing workspace + codelet: Codelet with argument containing the rule to build + + Notes: + Rules represent transformation patterns that map initial to target letters + Only one rule can exist at a time in the workspace + Rule replacement requires competitive strength testing + Equivalent rules activate their conceptual descriptions without replacement + Rules serve as the final abstraction layer in the Copycat algorithm + """ + workspace = ctx.workspace + rule = codelet.arguments[0] + if rule.ruleEqual(workspace.rule): + rule.activateRuleDescriptions() + return + rule.updateStrength() + assert rule.totalStrength + # fight against other rules + if workspace.rule is not None: + assert __structureVsStructure(rule, 1.0, workspace.rule, 1.0) + workspace.buildRule(rule) + + +def __getCutoffWeights(bondDensity): + """ + Get weighted probabilities for selecting answer cutoff thresholds based on bond density. + + This function implements an adaptive strategy for deciding when the system has found + sufficient structural information to translate a rule into a final answer. Higher bond + density indicates more complexity in the workspace, affecting the optimal cutoff strategy. + + Args: + bondDensity (float): Density of bonds in workspace (0.0 to 1.0+) + + Returns: + list: List of 10 probability weights for cutoff thresholds + + Notes: + Higher bond density -> earlier cutoff thresholds weighted heavily + Lower bond density -> later cutoff thresholds weighted heavily + Adaptive weighting helps system find optimal solution timing + Cutoff range typically 10-100 codelet executions + """ + if bondDensity > 0.8: + return [5.0, 150.0, 5.0, 2.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] + elif bondDensity > 0.6: + return [2.0, 5.0, 150.0, 5.0, 2.0, 1.0, 1.0, 1.0, 1.0, 1.0] + elif bondDensity > 0.4: + return [1.0, 2.0, 5.0, 150.0, 5.0, 2.0, 1.0, 1.0, 1.0, 1.0] + elif bondDensity > 0.2: + return [1.0, 1.0, 2.0, 5.0, 150.0, 5.0, 2.0, 1.0, 1.0, 1.0] + else: + return [1.0, 1.0, 1.0, 2.0, 5.0, 150.0, 5.0, 2.0, 1.0, 1.0] + + +@codelet('rule-translator') +def rule_translator(ctx, codelet): + """ + Translates a rule into a final answer when termination criteria are met. + + This codelet implements the final step of the Copycat algorithm, translating + the discovered rule pattern into a concrete answer for the letter sequence + problem. It uses adaptive cutoff strategies based on workspace complexity. + + Behavior: + 1. Verifies that a rule exists in the workspace + 2. Calculates bond density as a measure of workspace complexity + 3. Uses adaptive cutoff weights based on bond density + 4. Randomly selects termination cutoff threshold + 5. If current temperature reaches cutoff, attempts rule translation + 6. If translation succeeds, sets workspace final answer + 7. If translation fails, clamps temperature to delay termination + + Args: + ctx: Context containing coderack, random, temperature, workspace components + codelet: The rule translator codelet instance + + Notes: + Bond density indicates workspace complexity (higher = more complex) + Higher bond density leads to earlier termination cutoffs + Lower bond density leads to later termination cutoffs + Cutoff range typically spans from 10-100 codelet executions + Translation failure triggers temperature clamping to prevent early termination + Only successful translations produce final answers + """ + coderack = ctx.coderack + random = ctx.random + # TODO: use entropy + temperature = ctx.temperature + workspace = ctx.workspace + assert workspace.rule + if len(workspace.initial) + len(workspace.target) <= 2: + bondDensity = 1.0 + else: + numberOfBonds = len(workspace.initial.bonds) + len(workspace.target.bonds) + nearlyTotalLength = len(workspace.initial) + len(workspace.target) - 2 + bondDensity = numberOfBonds / nearlyTotalLength + bondDensity = min(bondDensity, 1.0) + weights = __getCutoffWeights(bondDensity) + cutoff = 10.0 * random.weighted_choice(list(range(1, 11)), weights) + # TODO: use entropy + if cutoff >= temperature.actual_value: + result = workspace.rule.buildTranslatedRule() + if result is not None: + workspace.finalAnswer = result + else: + temperature.clampUntil(coderack.codeletsRun + 100) + + +@codelet('bottom-up-correspondence-scout') +def bottom_up_correspondence_scout(ctx, codelet): + """ + Bottom-up correspondence scout that discovers mappings between strings. + + This codelet implements data-driven correspondence formation, starting from + specific objects in the workspace and seeking to create correspondences + between objects in the initial and target strings. It's called "bottom-up" + because it starts with concrete objects rather than abstract correspondence + concepts. + + Behavior: + 1. Selects object from initial string based on inter-string salience + 2. Selects object from target string based on inter-string salience + 3. Ensures both objects have same span structure (single vs multi-object) + 4. Computes possible concept mappings between object descriptions + 5. Verifies that concept slippage is possible for at least one mapping + 6. Filters mappings to only those that are distinguishing + 7. Handles string-level flipping when objects span entire strings + 8. Proposes correspondence with viable concept mappings + + Args: + ctx: Context containing coderack, slipnet, workspace components + codelet: The correspondence scout codelet instance + + Notes: + This enables opportunistic correspondence discovery between string objects + Concept mappings allow metaphorical connections beyond literal matches + Distinguishing mappings ensure correspondences are meaningful and unique + String flipping handles reverse mappings (ABC->XYZ vs XYZ->ABC) + Inter-string salience guides selection toward most relevant objects + """ + coderack = ctx.coderack + slipnet = ctx.slipnet + workspace = ctx.workspace + objectFromInitial = chooseUnmodifiedObject(ctx, 'interStringSalience', + workspace.initial.objects) + assert objectFromInitial is not None + objectFromTarget = chooseUnmodifiedObject(ctx, 'interStringSalience', + workspace.target.objects) + assert objectFromTarget is not None + assert objectFromInitial.spansString() == objectFromTarget.spansString() + # get the posible concept mappings + conceptMappings = formulas.getMappings( + objectFromInitial, objectFromTarget, + objectFromInitial.relevantDescriptions(), + objectFromTarget.relevantDescriptions()) + assert conceptMappings and __slippability(ctx, conceptMappings) + # find out if any are distinguishing + distinguishingMappings = [m for m in conceptMappings if m.distinguishing()] + assert distinguishingMappings + # if both objects span the strings, check to see if the + # string description needs to be flipped + opposites = [m for m in distinguishingMappings + if m.initialDescriptionType == slipnet.stringPositionCategory + and m.initialDescriptionType != slipnet.bondFacet] + initialDescriptionTypes = [m.initialDescriptionType for m in opposites] + flipTargetObject = False + if (objectFromInitial.spansString() and + objectFromTarget.spansString() and + slipnet.directionCategory in initialDescriptionTypes + and all(m.label == slipnet.opposite for m in opposites) # unreached? + and slipnet.opposite.activation != 100.0): + objectFromTarget = objectFromTarget.flippedVersion() + conceptMappings = formulas.getMappings( + objectFromInitial, objectFromTarget, + objectFromInitial.relevantDescriptions(), + objectFromTarget.relevantDescriptions()) + flipTargetObject = True + coderack.proposeCorrespondence(objectFromInitial, objectFromTarget, + conceptMappings, flipTargetObject) + + +@codelet('important-object-correspondence-scout') +def important_object_correspondence_scout(ctx, codelet): + """ + Important object correspondence scout guided by conceptual importance. + + This codelet implements concept-driven correspondence formation, starting from + the most important objects in the initial string and seeking to create + correspondences based on key distinguishing descriptors. It focuses on + objects that are conceptually significant rather than just salient. + + Behavior: + 1. Selects object from initial string based on relative importance + 2. Gets distinguishing descriptors from the selected object + 3. Applies concept slippages to map descriptors across strings + 4. Chooses descriptor based on temperature-adjusted conceptual depth + 5. Finds target objects that have matching descriptions + 6. Selects target object based on inter-string salience + 7. Ensures both objects have same span structure + 8. Computes concept mappings between object descriptions + 9. Verifies concept slippage possibility and distinguishing properties + 10. Handles string-level flipping for reverse mappings + 11. Proposes correspondence with viable concept mappings + + Args: + ctx: Context containing coderack, random, slipnet, temperature, workspace components + codelet: The important object correspondence scout codelet instance + + Notes: + This enables goal-directed correspondence formation based on importance + Conceptual depth weighting favors more abstract, human-like mappings + Temperature adjustment allows for probabilistic descriptor selection + Important objects drive the formation of key structural relationships + Contrasts with bottom-up scouts that work opportunistically + """ + coderack = ctx.coderack + random = ctx.random + slipnet = ctx.slipnet + # TODO: use entropy + temperature = ctx.temperature + workspace = ctx.workspace + objectFromInitial = chooseUnmodifiedObject(ctx, 'relativeImportance', + workspace.initial.objects) + assert objectFromInitial is not None + descriptors = objectFromInitial.relevantDistinguishingDescriptors() + # choose descriptor by conceptual depth + # TODO: use entropy + weights = [temperature.getAdjustedValue(n.conceptualDepth) for n in descriptors] + slipnode = random.weighted_choice(descriptors, weights) + assert slipnode + initialDescriptor = slipnode + for mapping in workspace.slippages(): + if mapping.initialDescriptor == slipnode: + initialDescriptor = mapping.targetDescriptor + targetCandidates = [] + for objekt in workspace.target.objects: + for description in objekt.relevantDescriptions(): + if description.descriptor == initialDescriptor: + targetCandidates += [objekt] + assert targetCandidates + objectFromTarget = chooseUnmodifiedObject(ctx, 'interStringSalience', + targetCandidates) + assert objectFromInitial.spansString() == objectFromTarget.spansString() + # get the posible concept mappings + conceptMappings = formulas.getMappings( + objectFromInitial, objectFromTarget, + objectFromInitial.relevantDescriptions(), + objectFromTarget.relevantDescriptions()) + assert conceptMappings and __slippability(ctx, conceptMappings) + # find out if any are distinguishing + distinguishingMappings = [m for m in conceptMappings if m.distinguishing()] + assert distinguishingMappings + # if both objects span the strings, check to see if the + # string description needs to be flipped + opposites = [m for m in distinguishingMappings + if m.initialDescriptionType == slipnet.stringPositionCategory + and m.initialDescriptionType != slipnet.bondFacet] + initialDescriptionTypes = [m.initialDescriptionType for m in opposites] + flipTargetObject = False + if (objectFromInitial.spansString() + and objectFromTarget.spansString() + and slipnet.directionCategory in initialDescriptionTypes + and all(m.label == slipnet.opposite for m in opposites) # unreached? + and slipnet.opposite.activation != 100.0): + objectFromTarget = objectFromTarget.flippedVersion() + conceptMappings = formulas.getMappings( + objectFromInitial, objectFromTarget, + objectFromInitial.relevantDescriptions(), + objectFromTarget.relevantDescriptions()) + flipTargetObject = True + coderack.proposeCorrespondence(objectFromInitial, objectFromTarget, + conceptMappings, flipTargetObject) + + +@codelet('correspondence-strength-tester') +def correspondence_strength_tester(ctx, codelet): + """ + Tests the strength of a proposed correspondence to determine if it should be built. + + This codelet evaluates whether a correspondence generated by correspondence scouts + is strong enough to warrant creation in the workspace. Only correspondences that + pass this probabilistic strength test proceed to the correspondence-builder. + + Behavior: + 1. Gets the correspondence to test from codelet arguments + 2. Validates that source and target objects still exist appropriately + 3. Updates correspondence strength based on current workspace state + 4. Converts strength to acceptance probability via temperature adjustment + 5. If passes probability test, activates concept mappings and queues correspondence-builder + 6. If fails, the correspondence is discarded (fizzles) + + Args: + ctx: Context containing coderack, random, temperature, workspace components + codelet: Codelet with argument containing the correspondence to test + + Notes: + Correspondence strength depends on mapping quality and workspace constraints + Higher temperature = more lenient correspondence acceptance (exploration) + Lower temperature = stricter correspondence acceptance (exploitation) + Activating concept mappings promotes relevant relationships for future formation + Failed correspondences are discarded, ensuring only coherent mappings are adopted + """ + coderack = ctx.coderack + random = ctx.random + # TODO: use entropy + temperature = ctx.temperature + workspace = ctx.workspace + correspondence = codelet.arguments[0] + objectFromInitial = correspondence.objectFromInitial + objectFromTarget = correspondence.objectFromTarget + assert (objectFromInitial in workspace.objects and + (objectFromTarget in workspace.objects or + correspondence.flipTargetObject and + not workspace.target.equivalentGroup( + objectFromTarget.flipped_version()))) + correspondence.updateStrength() + strength = correspondence.totalStrength + # TODO: use entropy + probability = temperature.getAdjustedProbability(strength / 100.0) + if random.coinFlip(probability): + # activate some concepts + for mapping in correspondence.conceptMappings: + mapping.initialDescriptionType.buffer = 100.0 + mapping.initialDescriptor.buffer = 100.0 + mapping.targetDescriptionType.buffer = 100.0 + mapping.targetDescriptor.buffer = 100.0 + coderack.newCodelet('correspondence-builder', + strength, [correspondence]) + + +@codelet('correspondence-builder') +def correspondence_builder(ctx, codelet): + """ + Builds a correspondence that has passed the strength test. + + This codelet actually constructs correspondences in the workspace after they have + passed strength testing. It handles complex conflict resolution with existing + structures and ensures correspondence creation follows workspace constraints. + + Behavior: + 1. Gets correspondence to build from codelet arguments + 2. Handles reflexive correspondences by adding mappings to existing ones + 3. Validates object existence and flipping relationships + 4. Fights against all incompatible correspondences using span-based strength + 5. Checks for incompatible bonds and groups at string endpoints + 6. Fights incompatible bonds with weighted strength advantages + 7. Fights incompatible groups that would conflict + 8. Checks for incompatible rules and fights with competitive criteria + 9. Breaks all defeated incompatible structures + 10. Finally constructs the correspondence in the workspace + + Args: + ctx: Context containing workspace + codelet: Codelet with argument containing the correspondence to build + + Notes: + Correspondence building involves complex conflict resolution with existing structures + Incompatible structures are removed only if the new correspondence wins competitions + Special handling for end-of-string correspondences affects bond and group conflicts + Span-based strength balancing ensures fair competition between correspondences + Successful correspondence creation establishes key structural relationships + """ + workspace = ctx.workspace + correspondence = codelet.arguments[0] + objectFromInitial = correspondence.objectFromInitial + objectFromTarget = correspondence.objectFromTarget + wantFlip = correspondence.flipTargetObject + if wantFlip: + flipper = objectFromTarget.flippedVersion() + targetNotFlipped = not workspace.target.equivalentGroup(flipper) + else: + targetNotFlipped = False + initialInObjects = objectFromInitial in workspace.objects + targetInObjects = objectFromTarget in workspace.objects + assert (initialInObjects or ( + not targetInObjects and (not (wantFlip and targetNotFlipped)))) + if correspondence.reflexive(): + # if the correspondence exists, activate concept mappings + # and add new ones to the existing corr. + existing = correspondence.objectFromInitial.correspondence + for mapping in correspondence.conceptMappings: + if mapping.label: + mapping.label.buffer = 100.0 + if not mapping.isContainedBy(existing.conceptMappings): + existing.conceptMappings += [mapping] + return + incompatibles = correspondence.getIncompatibleCorrespondences() + # fight against all correspondences + if incompatibles: + correspondenceSpans = (correspondence.objectFromInitial.letterSpan() + + correspondence.objectFromTarget.letterSpan()) + for incompatible in incompatibles: + incompatibleSpans = (incompatible.objectFromInitial.letterSpan() + + incompatible.objectFromTarget.letterSpan()) + assert __structureVsStructure(correspondence, correspondenceSpans, + incompatible, incompatibleSpans) + incompatibleBond = None + incompatibleGroup = None + # if there is an incompatible bond then fight against it + initial = correspondence.objectFromInitial + target = correspondence.objectFromTarget + if (initial.leftmost or initial.rightmost and + target.leftmost or target.rightmost): + # search for the incompatible bond + incompatibleBond = correspondence.getIncompatibleBond() + if incompatibleBond: + # bond found - fight against it + assert __structureVsStructure(correspondence, 3.0, + incompatibleBond, 2.0) + # won against incompatible bond + incompatibleGroup = target.group + if incompatibleGroup: + assert __structureVsStructure(correspondence, 1.0, + incompatibleGroup, 1.0) + # if there is an incompatible rule, fight against it + incompatibleRule = None + if workspace.rule: + if workspace.rule.incompatibleRuleCorrespondence(correspondence): + incompatibleRule = workspace.rule + assert __structureVsStructure(correspondence, 1.0, + incompatibleRule, 1.0) + for incompatible in incompatibles: + incompatible.break_the_structure() + # break incompatible group and bond if they exist + if incompatibleBond: + incompatibleBond.break_the_structure() + if incompatibleGroup: + incompatibleGroup.break_the_structure() + if incompatibleRule: + workspace.breakRule() + correspondence.buildCorrespondence() diff --git a/copycat/codeletMethods_README.md b/copycat/codeletMethods_README.md new file mode 100644 index 0000000..9fc659f --- /dev/null +++ b/copycat/codeletMethods_README.md @@ -0,0 +1,51 @@ +# Codelet Methods + +## Overview +The codelet methods system is a core component of the Copycat architecture that implements the various operations and behaviors that codelets can perform. This system defines the actual implementation of codelet behaviors that drive the analogical reasoning process. + +## Key Components +- Codelet operation implementations +- Behavior definitions +- Action handlers +- State management +- Event processing + +## Codelet Types +1. **Workspace Codelets** + - Object creation and modification + - Structure building + - Relationship formation + - Group management + +2. **Slipnet Codelets** + - Concept activation + - Node management + - Link formation + - Activation spreading + +3. **Correspondence Codelets** + - Mapping creation + - Relationship matching + - Structure alignment + - Similarity assessment + +## Usage +Codelet methods are called by the coderack system when codelets are executed: + +```python +# Example of a codelet method implementation +def some_codelet_method(workspace, slipnet, coderack): + # Perform operations + # Update state + # Create new codelets +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Coderack: Manages codelet execution +- Workspace: Provides objects to operate on +- Slipnet: Provides conceptual knowledge +- Correspondence: Manages mappings between structures \ No newline at end of file diff --git a/copycat/codelet_README.md b/copycat/codelet_README.md new file mode 100644 index 0000000..80b0a09 --- /dev/null +++ b/copycat/codelet_README.md @@ -0,0 +1,98 @@ +# Codelet System + +## Overview +The codelet system is a fundamental component of the Copycat architecture that defines the basic structure and behavior of codelets. Codelets are small, specialized agents that perform specific operations in the workspace, forming the basis of the system's parallel processing capabilities. + +## Key Features +- Codelet structure +- Behavior definition +- Priority management +- State tracking +- Execution control + +## Codelet Types +1. **Basic Codelets** + - Scout codelets + - Builder codelets + - Evaluator codelets + - Breaker codelets + +2. **Specialized Codelets** + - Group codelets + - Bond codelets + - Correspondence codelets + - Rule codelets + +3. **Control Codelets** + - Temperature codelets + - Pressure codelets + - Urgency codelets + - Cleanup codelets + +## Usage +Codelets are created and managed through the codelet system. + +The behavior parameter in codelet.set_behavior would typically be a reference to one of the **many codelet methods defined in codeletMethods.py**. +For example, a codelet might be assigned a behavior like: +- build_group - to create a new group of related elements +- evaluate_bond - to assess the strength of a bond +- scout_for_correspondence - to look for potential mappings between structures +The behavior is what gives each codelet its purpose and role in the system's parallel processing architecture. When the codelet is executed via codelet.run(), it performs this assigned behavior in the context of the current workspace state. + +```python +# Create a codelet +codelet = Codelet(name, priority) + +# Set behavior +codelet.set_behavior(behavior) + +# Execute codelet +result = codelet.run() +``` + +## Codelet Decorator +The `@codelet` decorator is defined in `codeletMethods.py` and is used to mark functions as codelet behaviors. Here's its implementation: + +```python +def codelet(name): + """Decorator for otherwise-unused functions that are in fact used as codelet behaviors""" + def wrap(f): + # Verify that the decorated function has exactly two parameters: + # 1. ctx - the context object containing workspace, slipnet, etc. + # 2. codelet - the codelet instance itself + # The None values in the tuple represent: no default args, no *args, no **kwargs + assert tuple(inspect.getargspec(f)) == (['ctx', 'codelet'], None, None, None) + + # Mark this function as a valid codelet method + f.is_codelet_method = True + + # Store the codelet type name for reference + f.codelet_name = name + return f + return wrap +``` + +The decorator: +1. Takes a `name` parameter that identifies the type of codelet +2. Wraps the decorated function with additional metadata: + - Marks the function as a codelet method with `is_codelet_method = True` + - Stores the codelet name with `codelet_name = name` +3. Verifies that the decorated function has the correct signature (must take `ctx` and `codelet` parameters) + +Example usage: +```python +@codelet('breaker') +def breaker(ctx, codelet): + # Codelet behavior implementation + pass +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Coderack: Manages codelet execution +- CodeletMethods: Provides codelet behaviors +- Workspace: Environment for codelets +- Temperature: Influences codelet behavior \ No newline at end of file diff --git a/copycat/coderack.py b/copycat/coderack.py new file mode 100644 index 0000000..33fef58 --- /dev/null +++ b/copycat/coderack.py @@ -0,0 +1,310 @@ +import math +import logging + +from . import codeletMethods +from .bond import Bond +from .codelet import Codelet +from .correspondence import Correspondence +from .description import Description +from .group import Group +from .rule import Rule + + +NUMBER_OF_BINS = 7 + + +def getUrgencyBin(urgency): + i = int(urgency) * NUMBER_OF_BINS / 100 + if i >= NUMBER_OF_BINS: + return NUMBER_OF_BINS + return i + 1 + + +class Coderack(object): + def __init__(self, ctx): + self.ctx = ctx + self.reset() + self.methods = {} + + for name in dir(codeletMethods): + method = getattr(codeletMethods, name) + if getattr(method, 'is_codelet_method', False): + self.methods[method.codelet_name] = method + + assert set(self.methods.keys()) == set([ + 'breaker', + 'bottom-up-description-scout', + 'top-down-description-scout', + 'description-strength-tester', + 'description-builder', + 'bottom-up-bond-scout', + 'top-down-bond-scout--category', + 'top-down-bond-scout--direction', + 'bond-strength-tester', + 'bond-builder', + 'top-down-group-scout--category', + 'top-down-group-scout--direction', + 'group-scout--whole-string', + 'group-strength-tester', + 'group-builder', + 'replacement-finder', + 'rule-scout', + 'rule-strength-tester', + 'rule-builder', + 'rule-translator', + 'bottom-up-correspondence-scout', + 'important-object-correspondence-scout', + 'correspondence-strength-tester', + 'correspondence-builder', + ]) + + def reset(self): + self.codelets = [] + self.codeletsRun = 0 + + def updateCodelets(self): + if self.codeletsRun > 0: + self.postTopDownCodelets() + self.postBottomUpCodelets() + + def probabilityOfPosting(self, codeletName): + # TODO: use entropy + temperature = self.ctx.temperature + workspace = self.ctx.workspace + if codeletName == 'breaker': + return 1.0 + if 'replacement' in codeletName: + if workspace.numberOfUnreplacedObjects() > 0: + return 1.0 + return 0.0 + if 'rule' in codeletName: + if not workspace.rule: + return 1.0 + return workspace.rule.totalWeakness() / 100.0 + if 'correspondence' in codeletName: + return workspace.interStringUnhappiness / 100.0 + if 'description' in codeletName: + # TODO: use entropy + return (temperature.value() / 100.0) ** 2 + return workspace.intraStringUnhappiness / 100.0 + + def howManyToPost(self, codeletName): + random = self.ctx.random + workspace = self.ctx.workspace + if codeletName == 'breaker' or 'description' in codeletName: + return 1 + if 'translator' in codeletName: + if not workspace.rule: + return 0 + return 1 + if 'rule' in codeletName: + return 2 + if 'group' in codeletName and not workspace.numberOfBonds(): + return 0 + if 'replacement' in codeletName and workspace.rule: + return 0 + number = 0 + if 'bond' in codeletName: + number = workspace.numberOfUnrelatedObjects() + if 'group' in codeletName: + number = workspace.numberOfUngroupedObjects() + if 'replacement' in codeletName: + number = workspace.numberOfUnreplacedObjects() + if 'correspondence' in codeletName: + number = workspace.numberOfUncorrespondingObjects() + if number < random.sqrtBlur(2.0): + return 1 + if number < random.sqrtBlur(4.0): + return 2 + return 3 + + def post(self, codelet): + self.codelets += [codelet] + if len(self.codelets) > 100: + oldCodelet = self.chooseOldCodelet() + self.removeCodelet(oldCodelet) + + def postTopDownCodelets(self): + random = self.ctx.random + slipnet = self.ctx.slipnet + for node in slipnet.slipnodes: + if node.activation != 100.0: + continue + for codeletName in node.codelets: + probability = self.probabilityOfPosting(codeletName) + howMany = self.howManyToPost(codeletName) + for _ in range(howMany): + if not random.coinFlip(probability): + continue + urgency = getUrgencyBin( + node.activation * node.conceptualDepth / 100.0) + codelet = Codelet(codeletName, urgency, [node], self.codeletsRun) + logging.info('Post top down: %s, with urgency: %d', + codelet.name, urgency) + self.post(codelet) + + def postBottomUpCodelets(self): + logging.info("posting bottom up codelets") + self.__postBottomUpCodelets('bottom-up-description-scout') + self.__postBottomUpCodelets('bottom-up-bond-scout') + self.__postBottomUpCodelets('group-scout--whole-string') + self.__postBottomUpCodelets('bottom-up-correspondence-scout') + self.__postBottomUpCodelets('important-object-correspondence-scout') + self.__postBottomUpCodelets('replacement-finder') + self.__postBottomUpCodelets('rule-scout') + self.__postBottomUpCodelets('rule-translator') + self.__postBottomUpCodelets('breaker') + + def __postBottomUpCodelets(self, codeletName): + random = self.ctx.random + # TODO: use entropy + temperature = self.ctx.temperature + probability = self.probabilityOfPosting(codeletName) + howMany = self.howManyToPost(codeletName) + urgency = 3 + if codeletName == 'breaker': + urgency = 1 + + # TODO: use entropy + if temperature.value() < 25.0 and 'translator' in codeletName: + urgency = 5 + for _ in range(howMany): + if random.coinFlip(probability): + codelet = Codelet(codeletName, urgency, [], self.codeletsRun) + self.post(codelet) + + def removeCodelet(self, codelet): + self.codelets.remove(codelet) + + def newCodelet(self, name, strength, arguments): + urgency = getUrgencyBin(strength) + newCodelet = Codelet(name, urgency, arguments, self.codeletsRun) + self.post(newCodelet) + + # pylint: disable=too-many-arguments + def proposeRule(self, facet, description, category, relation): + """Creates a proposed rule, and posts a rule-strength-tester codelet. + + The new codelet has urgency a function of + the degree of conceptual-depth of the descriptions in the rule + """ + rule = Rule(self.ctx, facet, description, category, relation) + rule.updateStrength() + if description and relation: + averageDepth = (description.conceptualDepth + relation.conceptualDepth) / 2.0 + urgency = 100.0 * math.sqrt(averageDepth / 100.0) + else: + urgency = 0 + self.newCodelet('rule-strength-tester', urgency, [rule]) + + def proposeCorrespondence(self, initialObject, targetObject, + conceptMappings, flipTargetObject): + correspondence = Correspondence(self.ctx, initialObject, targetObject, + conceptMappings, flipTargetObject) + for mapping in conceptMappings: + mapping.initialDescriptionType.buffer = 100.0 + mapping.initialDescriptor.buffer = 100.0 + mapping.targetDescriptionType.buffer = 100.0 + mapping.targetDescriptor.buffer = 100.0 + mappings = correspondence.distinguishingConceptMappings() + urgency = sum(mapping.strength() for mapping in mappings) + numberOfMappings = len(mappings) + if urgency: + urgency /= numberOfMappings + binn = getUrgencyBin(urgency) + logging.info('urgency: %s, number: %d, bin: %d', + urgency, numberOfMappings, binn) + self.newCodelet('correspondence-strength-tester', + urgency, [correspondence]) + + def proposeDescription(self, objekt, type_, descriptor): + description = Description(objekt, type_, descriptor) + descriptor.buffer = 100.0 + urgency = type_.activation + self.newCodelet('description-strength-tester', + urgency, [description]) + + def proposeSingleLetterGroup(self, source): + slipnet = self.ctx.slipnet + self.proposeGroup([source], [], slipnet.samenessGroup, None, + slipnet.letterCategory) + + def proposeGroup(self, objects, bondList, groupCategory, directionCategory, + bondFacet): + slipnet = self.ctx.slipnet + bondCategory = groupCategory.getRelatedNode(slipnet.bondCategory) + bondCategory.buffer = 100.0 + if directionCategory: + directionCategory.buffer = 100.0 + group = Group(objects[0].string, groupCategory, directionCategory, + bondFacet, objects, bondList) + urgency = bondCategory.bondDegreeOfAssociation() + self.newCodelet('group-strength-tester', urgency, [group]) + + def proposeBond(self, source, destination, bondCategory, bondFacet, + sourceDescriptor, destinationDescriptor): + bondFacet.buffer = 100.0 + sourceDescriptor.buffer = 100.0 + destinationDescriptor.buffer = 100.0 + bond = Bond(self.ctx, source, destination, bondCategory, bondFacet, + sourceDescriptor, destinationDescriptor) + urgency = bondCategory.bondDegreeOfAssociation() + self.newCodelet('bond-strength-tester', urgency, [bond]) + + def chooseOldCodelet(self): + # selects an old codelet to remove from the coderack + # more likely to select lower urgency codelets + urgencies = [] + for codelet in self.codelets: + urgency = ((self.codeletsRun - codelet.birthdate) * + (7.5 - codelet.urgency)) + urgencies += [urgency] + random = self.ctx.random + return random.weighted_choice(self.codelets, urgencies) + + def postInitialCodelets(self): + workspace = self.ctx.workspace + n = len(workspace.objects) + if n == 0: + # The most pathological case. + codeletsToPost = [ + ('rule-scout', 1), + ] + else: + codeletsToPost = [ + ('bottom-up-bond-scout', 2 * n), + ('replacement-finder', 2 * n), + ('bottom-up-correspondence-scout', 2 * n), + ] + for name, count in codeletsToPost: + for _ in range(count): + codelet = Codelet(name, 1, [], self.codeletsRun) + self.post(codelet) + + def chooseAndRunCodelet(self): + if not len(self.codelets): + # Indeed, this happens fairly often. + self.postInitialCodelets() + codelet = self.chooseCodeletToRun() + self.run(codelet) + + def chooseCodeletToRun(self): + random = self.ctx.random + # TODO: use entropy + temperature = self.ctx.temperature + assert self.codelets + + # TODO: use entropy + scale = (100.0 - temperature.value() + 10.0) / 15.0 + chosen = random.weighted_choice(self.codelets, [codelet.urgency ** scale for codelet in self.codelets]) + self.removeCodelet(chosen) + return chosen + + def run(self, codelet): + methodName = codelet.name + self.codeletsRun += 1 + method = self.methods[methodName] + try: + method(self.ctx, codelet) + except AssertionError: + pass diff --git a/copycat/coderack_README.md b/copycat/coderack_README.md new file mode 100644 index 0000000..19bf8f1 --- /dev/null +++ b/copycat/coderack_README.md @@ -0,0 +1,49 @@ +# README_coderack.md + +## Overview +`coderack.py` implements the Coderack, a key component of the Copycat system that manages the execution of codelets (small, focused procedures) that drive the analogical reasoning process. It handles the posting, selection, and execution of codelets based on their urgency and the current state of the system. + +## Core Components +- `Coderack` class: Main class that manages codelet execution +- Codelet management system +- Urgency-based codelet selection + +## Key Features +- Manages a collection of codelets (small procedures) +- Implements urgency-based codelet selection +- Supports both top-down and bottom-up codelet posting +- Handles codelet execution and removal +- Manages rule, correspondence, description, and group proposals + +## Codelet Types +- Breaker codelets +- Description codelets (top-down and bottom-up) +- Bond codelets (top-down and bottom-up) +- Group codelets (top-down and whole-string) +- Rule codelets (scout, strength-tester, builder, translator) +- Correspondence codelets (bottom-up and important-object) +- Replacement finder codelets + +## Main Methods +- `post(codelet)`: Add a codelet to the coderack +- `chooseAndRunCodelet()`: Select and execute a codelet +- `postTopDownCodelets()`: Post codelets from activated slipnet nodes +- `postBottomUpCodelets()`: Post codelets based on workspace state +- `proposeRule()`: Create and post a rule proposal +- `proposeCorrespondence()`: Create and post a correspondence proposal +- `proposeDescription()`: Create and post a description proposal +- `proposeGroup()`: Create and post a group proposal +- `proposeBond()`: Create and post a bond proposal + +## Dependencies +- Requires `codeletMethods` module +- Uses `bond`, `codelet`, `correspondence`, `description`, `group`, and `rule` modules +- Uses `logging` for debug output +- Uses `math` for urgency calculations + +## Notes +- Codelets are organized by urgency bins +- The system maintains a maximum of 100 codelets +- Codelet selection is influenced by temperature and workspace state +- The system supports both deterministic and probabilistic codelet posting +- Codelet urgency is calculated based on various factors including conceptual depth \ No newline at end of file diff --git a/copycat/conceptMapping.py b/copycat/conceptMapping.py new file mode 100644 index 0000000..788372b --- /dev/null +++ b/copycat/conceptMapping.py @@ -0,0 +1,153 @@ +class ConceptMapping(object): + def __init__(self, initialDescriptionType, targetDescriptionType, + initialDescriptor, targetDescriptor, + initialObject, targetObject): + self.slipnet = initialDescriptionType.slipnet + self.initialDescriptionType = initialDescriptionType + self.targetDescriptionType = targetDescriptionType + self.initialDescriptor = initialDescriptor + self.targetDescriptor = targetDescriptor + self.initialObject = initialObject + self.targetObject = targetObject + self.label = initialDescriptor.getBondCategory(targetDescriptor) + + def __repr__(self): + return '' % ( + self.__str__(), self.initialDescriptor, self.targetDescriptor) + + def __str__(self): + return self.label.name if self.label else 'anonymous' + + def slippability(self): + association = self.__degreeOfAssociation() + if association == 100.0: + return 100.0 + depth = self.__conceptualDepth() / 100.0 + return association * (1 - depth * depth) + + def __degreeOfAssociation(self): + # Assumes the 2 descriptors are connected in the slipnet by <= 1 link + if self.initialDescriptor == self.targetDescriptor: + return 100.0 + for link in self.initialDescriptor.lateralSlipLinks: + if link.destination == self.targetDescriptor: + return link.degreeOfAssociation() + return 0.0 + + def strength(self): + association = self.__degreeOfAssociation() + if association == 100.0: + return 100.0 + depth = self.__conceptualDepth() / 100.0 + return association * (1 + depth * depth) + + def __conceptualDepth(self): + return (self.initialDescriptor.conceptualDepth + + self.targetDescriptor.conceptualDepth) / 2.0 + + def distinguishing(self): + slipnet = self.slipnet + if self.initialDescriptor == slipnet.whole: + if self.targetDescriptor == slipnet.whole: + return False + if not self.initialObject.distinguishingDescriptor( + self.initialDescriptor): + return False + return self.targetObject.distinguishingDescriptor( + self.targetDescriptor) + + def sameInitialType(self, other): + return self.initialDescriptionType == other.initialDescriptionType + + def sameTargetType(self, other): + return self.targetDescriptionType == other.targetDescriptionType + + def sameTypes(self, other): + return self.sameInitialType(other) and self.sameTargetType(other) + + def sameInitialDescriptor(self, other): + return self.initialDescriptor == other.initialDescriptor + + def sameTargetDescriptor(self, other): + return self.targetDescriptor == other.targetDescriptor + + def sameDescriptors(self, other): + if self.sameInitialDescriptor(other): + return self.sameTargetDescriptor(other) + return False + + def sameKind(self, other): + return self.sameTypes(other) and self.sameDescriptors(other) + + def nearlySameKind(self, other): + return self.sameTypes(other) and self.sameInitialDescriptor(other) + + def isContainedBy(self, mappings): + return any(self.sameKind(mapping) for mapping in mappings) + + def isNearlyContainedBy(self, mappings): + return any(self.nearlySameKind(mapping) for mapping in mappings) + + def related(self, other): + if self.initialDescriptor.related(other.initialDescriptor): + return True + return self.targetDescriptor.related(other.targetDescriptor) + + def incompatible(self, other): + # Concept-mappings (a -> b) and (c -> d) are incompatible if a is + # related to c or if b is related to d, and the a -> b relationship is + # different from the c -> d relationship. E.g., rightmost -> leftmost + # is incompatible with right -> right, since rightmost is linked + # to right, but the relationships (opposite and identity) are different + # Notice that slipnet distances are not looked at, only slipnet links. + # This should be changed eventually. + if not self.related(other): + return False + if not self.label or not other.label: + return False + return self.label != other.label + + def supports(self, other): + # Concept-mappings (a -> b) and (c -> d) support each other if a is + # related to c and if b is related to d and the a -> b relationship is + # the same as the c -> d relationship. E.g., rightmost -> rightmost + # supports right -> right and leftmost -> leftmost. + # Notice that slipnet distances are not looked at, only slipnet links. + # This should be changed eventually. + + # If the two concept-mappings are the same, then return t. This + # means that letter->group supports letter->group, even though these + # concept-mappings have no label. + + if self.sameDescriptors(other): + return True + # if the descriptors are not related return false + if not self.related(other): + return False + if not self.label or not other.label: + return False + return self.label == other.label + + def relevant(self): + if self.initialDescriptionType.fully_active(): + return self.targetDescriptionType.fully_active() + return False + + def slippage(self): + slipnet = self.slipnet + return self.label not in [slipnet.sameness, slipnet.identity] + + def symmetricVersion(self): + if not self.slippage(): + return self + bond = self.targetDescriptor.getBondCategory(self.initialDescriptor) + if bond == self.label: + return self + return ConceptMapping( + self.targetDescriptionType, + self.initialDescriptionType, + self.targetDescriptor, + self.initialDescriptor1, + self.initialObject, + self.targetObject + ) diff --git a/copycat/conceptMapping_README.md b/copycat/conceptMapping_README.md new file mode 100644 index 0000000..db68b18 --- /dev/null +++ b/copycat/conceptMapping_README.md @@ -0,0 +1,54 @@ +# Concept Mapping System + +## Overview +The concept mapping system is a crucial component of the Copycat architecture that manages the mapping between concepts and their representations in the workspace. This system handles the translation between abstract concepts and concrete instances. + +## Key Features +- Concept-to-instance mapping +- Mapping validation +- Relationship tracking +- State management +- Event handling + +## Mapping Types +1. **Direct Mappings** + - Letter-to-concept mappings + - Number-to-concept mappings + - Symbol-to-concept mappings + - Pattern-to-concept mappings + +2. **Structural Mappings** + - Group-to-concept mappings + - Bond-to-concept mappings + - Hierarchy-to-concept mappings + - Pattern-to-concept mappings + +3. **Special Mappings** + - Rule-to-concept mappings + - Context-to-concept mappings + - Meta-concept mappings + - Derived mappings + +## Usage +Concept mappings are created and managed through the mapping system: + +```python +# Create a new concept mapping +mapping = ConceptMapping(source, target) + +# Access mapping properties +strength = mapping.get_strength() + +# Modify mapping state +mapping.set_strength(new_value) +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Slipnet: Provides concepts to map +- Workspace: Provides instances to map to +- Codelets: Operate on mappings +- Correspondence: Manages mapping relationships \ No newline at end of file diff --git a/copycat/copycat.py b/copycat/copycat.py new file mode 100644 index 0000000..92b9d89 --- /dev/null +++ b/copycat/copycat.py @@ -0,0 +1,144 @@ +from .coderack import Coderack +from .randomness import Randomness +from .slipnet import Slipnet +from .temperature import Temperature +from .workspace import Workspace +from .gui import GUI + +from pprint import pprint + + +class Reporter(object): + """Do-nothing base class for defining new reporter types""" + def report_answer(self, answer): + pass + + def report_coderack(self, coderack): + pass + + def report_slipnet(self, slipnet): + pass + + def report_temperature(self, temperature): #TODO: use entropy + pass + + def report_workspace(self, workspace): + pass + + +class Copycat(object): + def __init__(self, rng_seed=None, reporter=None, gui=False): + self.coderack = Coderack(self) + self.random = Randomness(rng_seed) + self.slipnet = Slipnet() + self.temperature = Temperature() # TODO: use entropy + self.workspace = Workspace(self) + self.reporter = reporter or Reporter() + if gui: + self.gui = GUI('Copycat') + self.lastUpdate = float('-inf') + + def step(self): + self.coderack.chooseAndRunCodelet() + self.reporter.report_coderack(self.coderack) + self.reporter.report_temperature(self.temperature) + self.reporter.report_workspace(self.workspace) + + def update_workspace(self, currentTime): + self.workspace.updateEverything() + self.coderack.updateCodelets() + self.slipnet.update(self.random) + self.temperature.update(self.workspace.getUpdatedTemperature()) + self.lastUpdate = currentTime + self.reporter.report_slipnet(self.slipnet) + + def check_reset(self): + if self.gui.app.primary.control.go: + initial, modified, target = self.gui.app.primary.control.get_vars() + self.gui.app.reset_with_strings(initial, modified, target) + self.workspace.resetWithStrings(initial, modified, target) + return True + else: + return False + + def mainLoop(self): + currentTime = self.coderack.codeletsRun + self.temperature.tryUnclamp(currentTime) + # Every 5 codelets, we update the workspace. + if currentTime >= self.lastUpdate + 5: + self.update_workspace(currentTime) + self.step() + + + def runTrial(self): + """Run a trial of the copycat algorithm""" + self.coderack.reset() + self.slipnet.reset() + self.temperature.reset() # TODO: use entropy + self.workspace.reset() + while self.workspace.finalAnswer is None: + self.mainLoop() + answer = { + 'answer': self.workspace.finalAnswer, + 'temp': self.temperature.last_unclamped_value, # TODO: use entropy + 'time': self.coderack.codeletsRun, + } + self.reporter.report_answer(answer) + return answer + + def runGUI(self): + while not self.check_reset(): + self.gui.update(self) + self.gui.refresh() + answers = {} + self.temperature.useAdj('pbest') + while True: + if self.check_reset(): + answers = {} + self.gui.refresh() + if not self.gui.paused(): + answer = self.runTrial() + self.gui.update(self) + d = answers.setdefault(answer['answer'], { + 'count': 0, + 'sumtemp': 0, + 'sumtime': 0 + }) + d['count'] += 1 + d['sumtemp'] += answer['temp'] + d['sumtime'] += answer['time'] + self.gui.add_answers(answers) + + for answer, d in answers.items(): + d['avgtemp'] = d.pop('sumtemp') / d['count'] + d['avgtime'] = d.pop('sumtime') / d['count'] + pprint(answers) + return answers + + def run(self, initial, modified, target, iterations): + self.workspace.resetWithStrings(initial, modified, target) + answers = {} + formula = 'pbest' + self.temperature.useAdj(formula) + for i in range(iterations): + answer = self.runTrial() + d = answers.setdefault(answer['answer'], { + 'count': 0, + 'sumtemp': 0, # TODO: use entropy + 'sumtime': 0 + }) + d['count'] += 1 + d['sumtemp'] += answer['temp'] # TODO: use entropy + d['sumtime'] += answer['time'] + + for answer, d in answers.items(): + d['avgtemp'] = d.pop('sumtemp') / d['count'] + d['avgtime'] = d.pop('sumtime') / d['count'] + print('The formula {} provided:'.format(formula)) + print('Average difference: {}'.format(self.temperature.getAverageDifference())) + return answers + + def run_forever(self, initial, modified, target): + self.workspace.resetWithStrings(initial, modified, target) + while True: + self.runTrial() diff --git a/copycat/copycat_README.md b/copycat/copycat_README.md new file mode 100644 index 0000000..a296d1a --- /dev/null +++ b/copycat/copycat_README.md @@ -0,0 +1,40 @@ +# README_copycat.md + +## Overview +`copycat.py` is the core module of the Copycat system, implementing the main analogical reasoning algorithm. It coordinates the interaction between various components like the workspace, slipnet, coderack, and temperature system. + +## Core Components +- `Copycat` class: Main class that orchestrates the analogical reasoning process +- `Reporter` class: Base class for defining different types of reporters (GUI, curses, etc.) + +## Key Features +- Implements the main Copycat algorithm for analogical reasoning +- Manages the interaction between different system components +- Supports multiple interfaces (GUI, curses, command-line) +- Provides temperature-based control of the reasoning process +- Handles multiple iterations and answer collection + +## Main Methods +- `run(initial, modified, target, iterations)`: Run the algorithm for a specified number of iterations +- `runGUI()`: Run the algorithm with graphical interface +- `run_forever(initial, modified, target)`: Run the algorithm continuously +- `runTrial()`: Run a single trial of the algorithm +- `step()`: Execute a single step of the algorithm +- `update_workspace(currentTime)`: Update all workspace components + +## Dependencies +- Requires `coderack`, `randomness`, `slipnet`, `temperature`, and `workspace` modules +- Uses `pprint` for pretty printing results +- Optional GUI support through the `gui` module + +## Usage +The module is typically used through one of the interface modules: +- `main.py` for command-line interface +- `gui.py` for graphical interface +- `curses_main.py` for terminal-based interface + +## Notes +- The system uses a temperature-based control mechanism to guide the reasoning process +- Results include answer statistics, temperature, and time metrics +- The system supports both single-run and continuous operation modes +- The reporter system allows for flexible output handling \ No newline at end of file diff --git a/copycat/correspondence.py b/copycat/correspondence.py new file mode 100644 index 0000000..0a3e2d2 --- /dev/null +++ b/copycat/correspondence.py @@ -0,0 +1,204 @@ +from .conceptMapping import ConceptMapping +from .group import Group +from .letter import Letter +from .workspaceStructure import WorkspaceStructure +from . import formulas + + +class Correspondence(WorkspaceStructure): + def __init__(self, ctx, objectFromInitial, objectFromTarget, + conceptMappings, flipTargetObject): + WorkspaceStructure.__init__(self, ctx) + self.objectFromInitial = objectFromInitial + self.objectFromTarget = objectFromTarget + self.conceptMappings = conceptMappings + self.flipTargetObject = flipTargetObject + self.accessoryConceptMappings = [] + + def __repr__(self): + return '<%s>' % self.__str__() + + def __str__(self): + return 'Correspondence between %s and %s' % ( + self.objectFromInitial, self.objectFromTarget) + + def distinguishingConceptMappings(self): + return [m for m in self.conceptMappings if m.distinguishing()] + + def relevantDistinguishingConceptMappings(self): + return [m for m in self.conceptMappings + if m.distinguishing() and m.relevant()] + + def extract_target_bond(self): + targetBond = False + if self.objectFromTarget.leftmost: + targetBond = self.objectFromTarget.rightBond + elif self.objectFromTarget.rightmost: + targetBond = self.objectFromTarget.leftBond + return targetBond + + def extract_initial_bond(self): + initialBond = False + if self.objectFromInitial.leftmost: + initialBond = self.objectFromInitial.rightBond + elif self.objectFromInitial.rightmost: + initialBond = self.objectFromInitial.leftBond + return initialBond + + def getIncompatibleBond(self): + slipnet = self.ctx.slipnet + initialBond = self.extract_initial_bond() + if not initialBond: + return None + targetBond = self.extract_target_bond() + if not targetBond: + return None + if initialBond.directionCategory and targetBond.directionCategory: + mapping = ConceptMapping( + slipnet.directionCategory, + slipnet.directionCategory, + initialBond.directionCategory, + targetBond.directionCategory, + None, + None + ) + for m in self.conceptMappings: + if m.incompatible(mapping): + return targetBond + return None + + def getIncompatibleCorrespondences(self): + workspace = self.ctx.workspace + return [o.correspondence for o in workspace.initial.objects + if o and self.incompatible(o.correspondence)] + + def incompatible(self, other): + if not other: + return False + if self.objectFromInitial == other.objectFromInitial: + return True + if self.objectFromTarget == other.objectFromTarget: + return True + for mapping in self.conceptMappings: + for otherMapping in other.conceptMappings: + if mapping.incompatible(otherMapping): + return True + return False + + def supporting(self, other): + if self == other: + return False + if self.objectFromInitial == other.objectFromInitial: + return False + if self.objectFromTarget == other.objectFromTarget: + return False + if self.incompatible(other): + return False + for mapping in self.distinguishingConceptMappings(): + for otherMapping in other.distinguishingConceptMappings(): + if mapping.supports(otherMapping): + return True + return False + + def support(self): + workspace = self.ctx.workspace + if isinstance(self.objectFromInitial, Letter): + if self.objectFromInitial.spansString(): + return 100.0 + if isinstance(self.objectFromTarget, Letter): + if self.objectFromTarget.spansString(): + return 100.0 + total = sum(c.totalStrength for c in workspace.correspondences() + if self.supporting(c)) + return min(total, 100.0) + + def updateInternalStrength(self): + """A function of how many concept mappings there are + + Also considered: their strength and how well they cohere""" + distinguishingMappings = self.relevantDistinguishingConceptMappings() + numberOfConceptMappings = len(distinguishingMappings) + if numberOfConceptMappings < 1: + self.internalStrength = 0.0 + return + totalStrength = sum(m.strength() for m in distinguishingMappings) + averageStrength = totalStrength / numberOfConceptMappings + if numberOfConceptMappings == 1.0: + numberOfConceptMappingsFactor = 0.8 + elif numberOfConceptMappings == 2.0: + numberOfConceptMappingsFactor = 1.2 + else: + numberOfConceptMappingsFactor = 1.6 + if self.internallyCoherent(): + internalCoherenceFactor = 2.5 + else: + internalCoherenceFactor = 1.0 + internalStrength = (averageStrength * internalCoherenceFactor * + numberOfConceptMappingsFactor) + self.internalStrength = min(internalStrength, 100.0) + + def updateExternalStrength(self): + self.externalStrength = self.support() + + def internallyCoherent(self): + """Whether any pair of distinguishing mappings support each other""" + mappings = self.relevantDistinguishingConceptMappings() + for i in range(len(mappings)): + for j in range(len(mappings)): + if i != j: + if mappings[i].supports(mappings[j]): + return True + return False + + def slippages(self): + mappings = [m for m in self.conceptMappings if m.slippage()] + mappings += [m for m in self.accessoryConceptMappings if m.slippage()] + return mappings + + def reflexive(self): + initial = self.objectFromInitial + if not initial.correspondence: + return False + if initial.correspondence.objectFromTarget == self.objectFromTarget: + return True + return False + + def buildCorrespondence(self): + workspace = self.ctx.workspace + workspace.structures += [self] + if self.objectFromInitial.correspondence: + self.objectFromInitial.correspondence.breakCorrespondence() + if self.objectFromTarget.correspondence: + self.objectFromTarget.correspondence.breakCorrespondence() + self.objectFromInitial.correspondence = self + self.objectFromTarget.correspondence = self + # add mappings to accessory-concept-mapping-list + relevantMappings = self.relevantDistinguishingConceptMappings() + for mapping in relevantMappings: + if mapping.slippage(): + self.accessoryConceptMappings += [mapping.symmetricVersion()] + if isinstance(self.objectFromInitial, Group): + if isinstance(self.objectFromTarget, Group): + bondMappings = formulas.getMappings( + self.objectFromInitial, + self.objectFromTarget, + self.objectFromInitial.bondDescriptions, + self.objectFromTarget.bondDescriptions + ) + for mapping in bondMappings: + self.accessoryConceptMappings += [mapping] + if mapping.slippage(): + self.accessoryConceptMappings += [ + mapping.symmetricVersion()] + for mapping in self.conceptMappings: + if mapping.label: + mapping.label.activation = 100.0 + + def break_the_structure(self): + self.breakCorrespondence() + + def breakCorrespondence(self): + workspace = self.ctx.workspace + workspace.structures.remove(self) + self.objectFromInitial.correspondence = None + self.objectFromTarget.correspondence = None diff --git a/copycat/correspondence_README.md b/copycat/correspondence_README.md new file mode 100644 index 0000000..88c78de --- /dev/null +++ b/copycat/correspondence_README.md @@ -0,0 +1,51 @@ +# README_correspondence.md + +## Overview +`correspondence.py` implements the Correspondence system, a key component of the Copycat system that manages the mapping relationships between objects in the initial and target strings. It handles the creation, evaluation, and management of correspondences that link objects based on their properties and relationships. + +## Core Components +- `Correspondence` class: Main class that represents a mapping between objects +- Concept mapping system +- Correspondence strength evaluation + +## Key Features +- Manages mappings between objects in initial and target strings +- Evaluates correspondence strength based on multiple factors +- Handles concept slippages and mappings +- Supports both direct and accessory concept mappings +- Manages correspondence compatibility and support + +## Correspondence Components +- `objectFromInitial`: Object from the initial string +- `objectFromTarget`: Object from the target string +- `conceptMappings`: List of concept mappings +- `accessoryConceptMappings`: Additional concept mappings +- `flipTargetObject`: Flag for target object flipping + +## Main Methods +- `updateInternalStrength()`: Calculate internal correspondence strength +- `updateExternalStrength()`: Calculate external correspondence strength +- `buildCorrespondence()`: Create and establish correspondence +- `breakCorrespondence()`: Remove correspondence +- `incompatible()`: Check correspondence compatibility +- `supporting()`: Check if correspondence supports another +- `internallyCoherent()`: Check internal coherence + +## Concept Mapping Types +- Distinguishing mappings +- Relevant distinguishing mappings +- Bond mappings +- Direction mappings +- Symmetric mappings + +## Dependencies +- Requires `conceptMapping`, `group`, `letter`, and `workspaceStructure` modules +- Uses `formulas` for mapping calculations +- Used by the main `copycat` module + +## Notes +- Correspondences are evaluated based on concept mapping strength and coherence +- The system supports both direct and indirect concept mappings +- Correspondences can be incompatible with each other +- The system handles both letter and group correspondences +- Concept slippages are tracked and managed \ No newline at end of file diff --git a/copycat/curses_reporter.py b/copycat/curses_reporter.py new file mode 100644 index 0000000..faa8548 --- /dev/null +++ b/copycat/curses_reporter.py @@ -0,0 +1,436 @@ +import curses +import time + +from .copycat import Reporter +from .bond import Bond +from .correspondence import Correspondence +from .description import Description +from .group import Group +from .letter import Letter +from .rule import Rule + + +class SafeSubwindow(object): + def __init__(self, window, h, w, y, x): + self.w = window.derwin(h, w, y, x) + + def addnstr(self, y, x, s, n): + self.w.addnstr(y, x, s, n) + + def addstr(self, y, x, s, attr=curses.A_NORMAL): + try: + self.w.addstr(y, x, s, attr) + except Exception as e: + if str(e) != 'addstr() returned ERR': + raise + + def border(self): + self.w.border() + + def derwin(self, h, w, y, x): + return self.w.derwin(h, w, y, x) + + def erase(self): + self.w.erase() + + def getch(self): + self.w.nodelay(True) # make getch() non-blocking + return self.w.getch() + + def getmaxyx(self): + return self.w.getmaxyx() + + def is_vacant(self, y, x): + ch_plus_attr = self.w.inch(y, x) + if ch_plus_attr == -1: + return True # it's out of bounds + return (ch_plus_attr & 0xFF) == 0x20 + + def refresh(self): + self.w.refresh() + + +class CursesReporter(Reporter): + def __init__(self, window, focus_on_slipnet=False, fps_goal=None): + curses.curs_set(0) # hide the cursor + curses.noecho() # hide keypresses + height, width = window.getmaxyx() + if focus_on_slipnet: + upperHeight = 10 + else: + upperHeight = 25 + answersHeight = 5 + coderackHeight = height - upperHeight - answersHeight + self.focusOnSlipnet = focus_on_slipnet + self.fpsGoal = fps_goal + self.temperatureWindow = SafeSubwindow(window, height, 5, 0, 0) # TODO: use entropy (entropyWindow) + self.upperWindow = SafeSubwindow(window, upperHeight, width-5, 0, 5) + self.coderackWindow = SafeSubwindow(window, coderackHeight, width-5, upperHeight, 5) + self.answersWindow = SafeSubwindow(window, answersHeight, width-5, upperHeight + coderackHeight, 5) + self.fpsWindow = SafeSubwindow(self.answersWindow, 3, 9, answersHeight - 3, width - 14) + for w in [self.temperatureWindow, self.upperWindow, self.answersWindow, self.fpsWindow]: + w.erase() + w.border() + w.refresh() + self.answers = {} + self.fpsTicks = 0 + self.fpsSince = time.time() + self.fpsMeasured = 100 # just a made-up number at first + self.fpsDelay = 0 + + def do_keyboard_shortcuts(self): + w = self.temperatureWindow # just a random window + ordch = w.getch() + if ordch in [ord('P'), ord('p')]: + w.addstr(0, 0, 'PAUSE', curses.A_STANDOUT) + w.refresh() + ordch = None + while ordch not in [ord('P'), ord('p'), 27, ord('Q'), ord('q')]: + time.sleep(0.1) + ordch = w.getch() + self.fpsTicks = 0 + self.fpsSince = time.time() + w.erase() + w.border() + w.refresh() + if ordch in [27, ord('Q'), ord('q')]: + raise KeyboardInterrupt() + if ordch in [ord('F')]: + self.fpsGoal = (self.fpsGoal or self.fpsMeasured) * 1.25 + if ordch in [ord('f')]: + self.fpsGoal = (self.fpsGoal or self.fpsMeasured) * 0.8 + + + def report_answer(self, answer): + d = self.answers.setdefault(answer['answer'], { + 'answer': answer['answer'], + 'count': 0, + 'sumtime': 0, + 'sumtemp': 0, + }) + d['count'] += 1 + d['sumtemp'] += answer['temp'] + d['sumtime'] += answer['time'] + d['avgtemp'] = d['sumtemp'] / d['count'] + d['avgtime'] = d['sumtime'] / d['count'] + + def fitness(d): + return 3 * d['count'] - d['avgtemp'] + + def represent(d): + return '%s: %d (avg time %.1f, avg temp %.1f)' % ( + d['answer'], d['count'], d['avgtime'], d['avgtemp'], + ) + + answersToPrint = sorted(iter(self.answers.values()), key=fitness, reverse=True) + + w = self.answersWindow + pageWidth = w.getmaxyx()[1] + if pageWidth >= 96: + columnWidth = (pageWidth - 6) / 2 + for i, d in enumerate(answersToPrint[:3]): + w.addnstr(i+1, 2, represent(d), columnWidth) + for i, d in enumerate(answersToPrint[3:6]): + w.addnstr(i+1, pageWidth - columnWidth - 2, represent(d), columnWidth) + else: + columnWidth = pageWidth - 4 + for i, d in enumerate(answersToPrint[:3]): + w.addnstr(i+1, 2, represent(d), columnWidth) + w.refresh() + + def depict_fps(self): + w = self.fpsWindow + now = time.time() + elapsed = now - self.fpsSince + fps = self.fpsTicks / elapsed + if self.fpsGoal is not None: + seconds_of_work_per_frame = (elapsed / self.fpsTicks) - self.fpsDelay + desired_time_working_per_second = self.fpsGoal * seconds_of_work_per_frame + if desired_time_working_per_second < 1.0: + self.fpsDelay = (1.0 - desired_time_working_per_second) / fps + else: + self.fpsDelay = 0 + w.addstr(1, 1, 'FPS:%3d' % fps, curses.A_NORMAL) + w.refresh() + self.fpsSince = now + self.fpsTicks = 0 + self.fpsMeasured = fps + + def report_coderack(self, coderack): + self.fpsTicks += 1 # for the purposes of FPS calculation + if self.fpsDelay: + time.sleep(self.fpsDelay) + if time.time() > self.fpsSince + 1.200: + self.depict_fps() + + NUMBER_OF_BINS = 7 + + # Combine duplicate codelets for printing. + counts = {} + for c in coderack.codelets: + assert 1 <= c.urgency <= NUMBER_OF_BINS + key = (c.urgency, c.name) + counts[key] = counts.get(key, 0) + 1 + + # Sort the most common and highest-urgency codelets to the top. + entries = sorted( + (count, key[0], key[1]) + for key, count in counts.items() + ) + + # Figure out how we'd like to render each codelet's name. + printable_entries = [ + (urgency, '%s (%d)' % (name, count)) + for count, urgency, name in entries + ] + + # Render each codelet in the appropriate column, + # as close to the top of the page as physically possible. + w = self.coderackWindow + pageHeight, pageWidth = w.getmaxyx() + columnWidth = (pageWidth - len('important-object-correspondence-scout (n)')) / (NUMBER_OF_BINS - 1) + + w.erase() + for u, string in printable_entries: + # Find the highest point on the page where we could place this entry. + start_column = int((u - 1) * columnWidth) + end_column = start_column + len(string) + for r in range(pageHeight): + if all(w.is_vacant(r, c) for c in range(start_column, end_column+20)): + w.addstr(r, start_column, string) + break + w.refresh() + + def slipnode_name_and_attr(self, slipnode): + if slipnode.activation == 100: + return (slipnode.name.upper(), curses.A_STANDOUT) + if slipnode.activation > 50: + return (slipnode.name.upper(), curses.A_BOLD) + else: + return (slipnode.name.lower(), curses.A_NORMAL) + + def report_slipnet(self, slipnet): + if not self.focusOnSlipnet: + return + w = self.upperWindow + pageHeight, pageWidth = w.getmaxyx() + w.erase() + w.addstr(1, 2, 'Total: %d slipnodes and %d sliplinks' % ( + len(slipnet.slipnodes), + len(slipnet.sliplinks), + )) + + for c, node in enumerate(slipnet.letters): + s, attr = self.slipnode_name_and_attr(node) + w.addstr(2, 2 * c + 2, s, attr) + for c, node in enumerate(slipnet.numbers): + s, attr = self.slipnode_name_and_attr(node) + w.addstr(3, 2 * c + 2, s, attr) + row = 4 + column = 2 + for node in slipnet.slipnodes: + if node not in slipnet.letters + slipnet.numbers: + s, attr = self.slipnode_name_and_attr(node) + if column + len(s) > pageWidth - 1: + row += 1 + column = 2 + w.addstr(row, column, s, attr) + column += len(s) + 1 + w.border() + w.refresh() + + #TODO: use entropy + def report_temperature(self, temperature): + self.do_keyboard_shortcuts() + w = self.temperatureWindow + height = w.getmaxyx()[0] + max_mercury = height - 4 + mercury = max_mercury * temperature.value() / 100.0 + for i in range(max_mercury): + ch = ' ,o%8'[int(4 * min(max(0, mercury - i), 1))] + w.addstr(max_mercury - i, 1, 3*ch) + w.addnstr(height - 2, 1, '%3d' % temperature.actual_value, 3) + w.refresh() + + def length_of_workspace_object_depiction(self, o, description_structures): + result = len(str(o)) + if o.descriptions: + result += 2 + result += 2 * (len(o.descriptions) - 1) + for d in o.descriptions: + s, _ = self.slipnode_name_and_attr(d.descriptor) + result += len(s) + if d not in description_structures: + result += 2 + result += 1 + return result + + def depict_workspace_object(self, w, row, column, o, maxImportance, description_structures): + if maxImportance != 0.0 and o.relativeImportance == maxImportance: + attr = curses.A_BOLD + else: + attr = curses.A_NORMAL + w.addstr(row, column, str(o), attr) + column += len(str(o)) + if o.descriptions: + w.addstr(row, column, ' (', curses.A_NORMAL) + column += 2 + for i, d in enumerate(o.descriptions): + if i != 0: + w.addstr(row, column, ', ', curses.A_NORMAL) + column += 2 + s, attr = self.slipnode_name_and_attr(d.descriptor) + if d not in description_structures: + s = '[%s]' % s + w.addstr(row, column, s, attr) + column += len(s) + w.addstr(row, column, ')', curses.A_NORMAL) + column += 1 + return column + + def depict_bond(self, w, row, column, bond): + slipnet = bond.ctx.slipnet + if bond.directionCategory == slipnet.right: + s = '-- %s -->' % bond.category.name + elif bond.directionCategory == slipnet.left: + s = '<-- %s --' % bond.category.name + elif bond.directionCategory is None: + s = '<-- %s -->' % bond.category.name + if isinstance(bond.leftObject, Group): + s = 'G' + s + if isinstance(bond.rightObject, Group): + s = s + 'G' + w.addstr(row, column, s, curses.A_NORMAL) + return column + len(s) + + def depict_grouping_brace(self, w, firstrow, lastrow, column): + if firstrow == lastrow: + w.addstr(firstrow, column, '}', curses.A_NORMAL) + else: + w.addstr(firstrow, column, '\\', curses.A_NORMAL) + w.addstr(lastrow, column, '/', curses.A_NORMAL) + for r in range(firstrow + 1, lastrow): + w.addstr(r, column, '|', curses.A_NORMAL) + + def report_workspace(self, workspace): + if self.focusOnSlipnet: + return + slipnet = workspace.ctx.slipnet + w = self.upperWindow + pageHeight, pageWidth = w.getmaxyx() + w.erase() + w.addstr(1, 2, '%d objects (%d letters in %d groups), %d other structures (%d bonds, %d correspondences, %d descriptions, %d rules)' % ( + len(workspace.objects), + len([o for o in workspace.objects if isinstance(o, Letter)]), + len([o for o in workspace.objects if isinstance(o, Group)]), + len(workspace.structures) - len([o for o in workspace.objects if isinstance(o, Group)]), + len([o for o in workspace.structures if isinstance(o, Bond)]), + len([o for o in workspace.structures if isinstance(o, Correspondence)]), + len([o for o in workspace.structures if isinstance(o, Description)]), + len([o for o in workspace.structures if isinstance(o, Rule)]), + )) + + group_objects = {o for o in workspace.objects if isinstance(o, Group)} + letter_objects = {o for o in workspace.objects if isinstance(o, Letter)} + group_and_letter_objects = group_objects | letter_objects + assert set(workspace.objects) == group_and_letter_objects + assert group_objects <= set(workspace.structures) + + latent_groups = {o.group for o in workspace.objects if o.group is not None} + assert latent_groups <= group_objects + assert group_objects <= latent_groups + member_groups = {o for g in group_objects for o in g.objectList if isinstance(o, Group)} + assert member_groups <= group_objects + + bond_structures = {o for o in workspace.structures if isinstance(o, Bond)} + known_bonds = {o.leftBond for o in group_and_letter_objects if o.leftBond is not None} + known_bonds |= {o.rightBond for o in group_and_letter_objects if o.rightBond is not None} + assert known_bonds == bond_structures + + description_structures = {o for o in workspace.structures if isinstance(o, Description)} + latent_descriptions = {d for o in group_and_letter_objects for d in o.descriptions} + assert description_structures <= latent_descriptions + + current_rules = set([workspace.rule]) if workspace.rule is not None else set() + + correspondences_between_initial_and_target = {o for o in workspace.structures if isinstance(o, Correspondence)} + + assert set(workspace.structures) == set.union( + group_objects, + bond_structures, + description_structures, + current_rules, + correspondences_between_initial_and_target, + ) + for g in group_objects: + assert g.string in [workspace.initial, workspace.modified, workspace.target] + + row = 2 + for o in current_rules: + w.addstr(row, 2, str(o), curses.A_BOLD) + + for string in [workspace.initial, workspace.modified, workspace.target]: + row += 1 + letters_in_string = sorted( + (o for o in letter_objects if o.string == string), + key=lambda o: o.leftIndex, + ) + groups_in_string = sorted( + (o for o in group_objects if o.string == string), + key=lambda o: o.leftIndex, + ) + if groups_in_string or letters_in_string: + maxImportance = max(o.relativeImportance for o in groups_in_string + letters_in_string) + bonds_in_string = sorted( + (b for b in bond_structures if b.string == string), + key=lambda b: b.leftObject.rightIndex, + ) + assert bonds_in_string == sorted(string.bonds, key=lambda b: b.leftObject.rightIndex) + startrow_for_group = {} + endrow_for_group = {} + + max_column = 0 + for letter in letters_in_string: + for g in groups_in_string: + if g.leftIndex == letter.leftIndex: + startrow_for_group[g] = row + if g.rightIndex == letter.rightIndex: + endrow_for_group[g] = row + column = self.depict_workspace_object(w, row, 2, letter, maxImportance, description_structures) + row += 1 + max_column = max(max_column, column) + for b in bonds_in_string: + if b.leftObject.rightIndex == letter.rightIndex: + assert b.rightObject.leftIndex == letter.rightIndex + 1 + column = self.depict_bond(w, row, 4, b) + row += 1 + max_column = max(max_column, column) + + for group in groups_in_string: + start = startrow_for_group[group] + end = endrow_for_group[group] + # Place this group's graphical depiction. + depiction_width = 3 + self.length_of_workspace_object_depiction(group, description_structures) + for firstcolumn in range(max_column, 1000): + lastcolumn = firstcolumn + depiction_width + okay = all( + w.is_vacant(r, c) + for c in range(firstcolumn, lastcolumn + 1) + for r in range(start, end + 1) + ) + if okay: + self.depict_grouping_brace(w, start, end, firstcolumn + 1) + self.depict_workspace_object(w, (start + end) / 2, firstcolumn + 3, group, maxImportance, description_structures) + break + + row += 1 + column = 2 + + for o in correspondences_between_initial_and_target: + slipnet = workspace.ctx.slipnet + w.addstr(row, column, '%s (%s)' % (str(o), str([m for m in o.conceptMappings if m.label != slipnet.identity])), curses.A_NORMAL) + row += 1 + column = 2 + + w.border() + w.refresh() diff --git a/copycat/curses_reporter_README.md b/copycat/curses_reporter_README.md new file mode 100644 index 0000000..e6c727a --- /dev/null +++ b/copycat/curses_reporter_README.md @@ -0,0 +1,55 @@ +# Curses Reporter System + +## Overview +The curses reporter system is a visualization component of the Copycat architecture that provides a terminal-based user interface for monitoring and debugging the system's operation. This system uses the curses library to create an interactive display of the system's state. + +## Key Features +- Real-time state display +- Interactive monitoring +- Debug information +- System metrics visualization +- User input handling + +## Display Components +1. **Main Display** + - Workspace visualization + - Slipnet state + - Coderack status + - Correspondence view + +2. **Debug Panels** + - Codelet execution + - Activation levels + - Mapping strengths + - Error messages + +3. **Control Interface** + - User commands + - System controls + - Display options + - Navigation + +## Usage +The curses reporter is used to monitor the system: + +```python +# Initialize the reporter +reporter = CursesReporter() + +# Update the display +reporter.update_display() + +# Handle user input +reporter.process_input() +``` + +## Dependencies +- Python 3.x +- curses library +- No other external dependencies required + +## Related Components +- Workspace: Provides state to display +- Slipnet: Provides activation information +- Coderack: Provides execution status +- Correspondence: Provides mapping information \ No newline at end of file diff --git a/copycat/description.py b/copycat/description.py new file mode 100644 index 0000000..ab81347 --- /dev/null +++ b/copycat/description.py @@ -0,0 +1,57 @@ +from .workspaceStructure import WorkspaceStructure + + +class Description(WorkspaceStructure): + def __init__(self, workspaceObject, descriptionType, descriptor): + WorkspaceStructure.__init__(self, workspaceObject.ctx) + self.object = workspaceObject + self.string = workspaceObject.string + self.descriptionType = descriptionType + self.descriptor = descriptor + + def __repr__(self): + return '' % self.__str__() + + def __str__(self): + s = 'description(%s) of %s' % (self.descriptor.get_name(), self.object) + workspace = self.ctx.workspace + if self.object.string == getattr(workspace, 'initial', None): + s += ' in initial string' + else: + s += ' in target string' + return s + + def updateInternalStrength(self): + self.internalStrength = self.descriptor.conceptualDepth + + def updateExternalStrength(self): + self.externalStrength = (self.localSupport() + + self.descriptionType.activation) / 2 + + def localSupport(self): + workspace = self.ctx.workspace + described_like_self = 0 + for other in workspace.objects: + if self.object == other: + continue + if self.object.isWithin(other) or other.isWithin(self.object): + continue + for description in other.descriptions: + if description.descriptionType == self.descriptionType: + described_like_self += 1 + results = {0: 0.0, 1: 20.0, 2: 60.0, 3: 90.0} + if described_like_self in results: + return results[described_like_self] + return 100.0 + + def build(self): + self.descriptionType.buffer = 100.0 + self.descriptor.buffer = 100.0 + if not self.object.described(self.descriptor): + self.object.descriptions += [self] + + def breakDescription(self): + workspace = self.ctx.workspace + if self in workspace.structures: + workspace.structures.remove(self) + self.object.descriptions.remove(self) diff --git a/copycat/description_README.md b/copycat/description_README.md new file mode 100644 index 0000000..8bf368e --- /dev/null +++ b/copycat/description_README.md @@ -0,0 +1,54 @@ +# Description System + +## Overview +The description system is a core component of the Copycat architecture that manages the representation and generation of descriptions for objects and structures in the workspace. This system provides detailed characterizations of elements in the analogical reasoning process. + +## Key Features +- Object description +- Structure characterization +- Property management +- Relationship description +- State representation + +## Description Types +1. **Basic Descriptions** + - Object properties + - Structure features + - Relationship details + - State information + +2. **Composite Descriptions** + - Group characteristics + - Pattern descriptions + - Hierarchical details + - Context information + +3. **Special Descriptions** + - Rule descriptions + - Mapping details + - Meta-descriptions + - Derived characteristics + +## Usage +Descriptions are created and managed through the description system: + +```python +# Create a description +desc = Description(object) + +# Add properties +desc.add_property('property_name', value) + +# Generate description +text = desc.generate() +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Workspace: Contains described objects +- WorkspaceObject: Objects to describe +- Codelets: Use descriptions +- Correspondence: Maps descriptions \ No newline at end of file diff --git a/copycat/formulas.py b/copycat/formulas.py new file mode 100644 index 0000000..195cccc --- /dev/null +++ b/copycat/formulas.py @@ -0,0 +1,59 @@ +from .conceptMapping import ConceptMapping + + +def weightedAverage(values): + total = 0.0 + totalWeights = 0.0 + for value, weight in values: + total += value * weight + totalWeights += weight + if not totalWeights: + return 0.0 + return total / totalWeights + + +def __localRelevance(string, isRelevant): + numberOfObjectsNotSpanning = 0.0 + numberOfMatches = 0.0 + for o in string.objects: + if not o.spansString(): + numberOfObjectsNotSpanning += 1.0 + if isRelevant(o): + numberOfMatches += 1.0 + if numberOfObjectsNotSpanning == 1: + return 100.0 * numberOfMatches + return 100.0 * numberOfMatches / (numberOfObjectsNotSpanning - 1.0) + + +def localBondCategoryRelevance(string, category): + def isRelevant(o): + return o.rightBond and o.rightBond.category == category + if len(string.objects) == 1: + return 0.0 + return __localRelevance(string, isRelevant) + + +def localDirectionCategoryRelevance(string, direction): + def isRelevant(o): + return o.rightBond and o.rightBond.directionCategory == direction + return __localRelevance(string, isRelevant) + + +def getMappings(objectFromInitial, objectFromTarget, + initialDescriptions, targetDescriptions): + mappings = [] + for initial in initialDescriptions: + for target in targetDescriptions: + if initial.descriptionType == target.descriptionType: + if (initial.descriptor == target.descriptor or + initial.descriptor.slipLinked(target.descriptor)): + mapping = ConceptMapping( + initial.descriptionType, + target.descriptionType, + initial.descriptor, + target.descriptor, + objectFromInitial, + objectFromTarget + ) + mappings += [mapping] + return mappings diff --git a/copycat/formulas_README.md b/copycat/formulas_README.md new file mode 100644 index 0000000..e2938c5 --- /dev/null +++ b/copycat/formulas_README.md @@ -0,0 +1,54 @@ +# Formulas System + +## Overview +The formulas system is a utility component of the Copycat architecture that provides mathematical and logical formulas for various calculations and evaluations throughout the system. This system implements core mathematical operations used in the analogical reasoning process. + +## Key Features +- Mathematical operations +- Probability calculations +- Distance metrics +- Similarity measures +- Utility functions + +## Formula Types +1. **Mathematical Formulas** + - Probability calculations + - Distance metrics + - Similarity scores + - Weight computations + +2. **Evaluation Formulas** + - Fitness functions + - Quality measures + - Comparison metrics + - Ranking formulas + +3. **Special Formulas** + - Temperature adjustments + - Activation functions + - Threshold calculations + - Normalization methods + +## Usage +Formulas are used throughout the system: + +```python +# Calculate probability +prob = formulas.calculate_probability(event) + +# Compute similarity +sim = formulas.compute_similarity(obj1, obj2) + +# Evaluate fitness +fitness = formulas.evaluate_fitness(solution) +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Temperature: Uses formulas for calculations +- Slipnet: Uses formulas for activation +- Workspace: Uses formulas for evaluation +- Statistics: Uses formulas for analysis \ No newline at end of file diff --git a/copycat/group.py b/copycat/group.py new file mode 100644 index 0000000..0d3abb4 --- /dev/null +++ b/copycat/group.py @@ -0,0 +1,237 @@ +from .description import Description +from .workspaceObject import WorkspaceObject +from . import formulas + + +class Group(WorkspaceObject): + # pylint: disable=too-many-instance-attributes + def __init__(self, string, groupCategory, directionCategory, facet, + objectList, bondList): + # pylint: disable=too-many-arguments + WorkspaceObject.__init__(self, string) + slipnet = self.ctx.slipnet + self.groupCategory = groupCategory + self.directionCategory = directionCategory + self.facet = facet + self.objectList = objectList + self.bondList = bondList + self.bondCategory = self.groupCategory.getRelatedNode( + slipnet.bondCategory) + + leftObject = objectList[0] + rightObject = objectList[-1] + self.leftIndex = leftObject.leftIndex + self.leftmost = self.leftIndex == 1 + self.rightIndex = rightObject.rightIndex + self.rightmost = self.rightIndex == len(self.string) + + self.descriptions = [] + self.bondDescriptions = [] + self.name = '' + + if self.bondList and len(self.bondList): + firstFacet = self.bondList[0].facet + self.addBondDescription( + Description(self, slipnet.bondFacet, firstFacet)) + self.addBondDescription( + Description(self, slipnet.bondCategory, self.bondCategory)) + + self.addDescription(slipnet.objectCategory, slipnet.group) + self.addDescription(slipnet.groupCategory, self.groupCategory) + if not self.directionCategory: + # sameness group - find letterCategory + letter = self.objectList[0].getDescriptor(self.facet) + self.addDescription(self.facet, letter) + if self.directionCategory: + self.addDescription(slipnet.directionCategory, self.directionCategory) + if self.spansString(): + self.addDescription(slipnet.stringPositionCategory, slipnet.whole) + elif self.leftmost: + self.addDescription(slipnet.stringPositionCategory, slipnet.leftmost) + elif self.rightmost: + self.addDescription(slipnet.stringPositionCategory, slipnet.rightmost) + elif self.middleObject(): + self.addDescription(slipnet.stringPositionCategory, slipnet.middle) + self.add_length_description_category() + + def add_length_description_category(self): + # check whether or not to add length description category + random = self.ctx.random + slipnet = self.ctx.slipnet + probability = self.lengthDescriptionProbability() + if random.coinFlip(probability): + length = len(self.objectList) + if length < 6: + self.addDescription(slipnet.length, slipnet.numbers[length - 1]) + + def __str__(self): + s = self.string.__str__() + l = self.leftIndex - 1 + r = self.rightIndex + return 'group[%d:%d] == %s' % (l, r - 1, s[l:r]) + + def getIncompatibleGroups(self): + result = [] + for objekt in self.objectList: + while objekt.group: + result += [objekt.group] + objekt = objekt.group + return result + + def addBondDescription(self, description): + self.bondDescriptions += [description] + + def singleLetterGroupProbability(self): + slipnet = self.ctx.slipnet + temperature = self.ctx.temperature + numberOfSupporters = self.numberOfLocalSupportingGroups() + if not numberOfSupporters: + return 0.0 + if numberOfSupporters == 1: + exp = 4.0 + elif numberOfSupporters == 2: + exp = 2.0 + else: + exp = 1.0 + support = self.localSupport() / 100.0 + activation = slipnet.length.activation / 100.0 + supportedActivation = (support * activation) ** exp + #TODO: use entropy + return temperature.getAdjustedProbability(supportedActivation) + + def flippedVersion(self): + slipnet = self.ctx.slipnet + flippedBonds = [b.flippedversion() for b in self.bondList] + flippedGroup = self.groupCategory.getRelatedNode(slipnet.flipped) + flippedDirection = self.directionCategory.getRelatedNode( + slipnet.flipped) + return Group(self.string, flippedGroup, flippedDirection, + self.facet, self.objectList, flippedBonds) + + def buildGroup(self): + workspace = self.ctx.workspace + workspace.objects += [self] + workspace.structures += [self] + self.string.objects += [self] + for objekt in self.objectList: + objekt.group = self + workspace.buildDescriptions(self) + self.activateDescriptions() + + def activateDescriptions(self): + for description in self.descriptions: + description.descriptor.buffer = 100.0 + + def lengthDescriptionProbability(self): + slipnet = self.ctx.slipnet + temperature = self.ctx.temperature + length = len(self.objectList) + if length > 5: + return 0.0 + cubedlength = length ** 3 + fred = cubedlength * (100.0 - slipnet.length.activation) / 100.0 + probability = 0.5 ** fred + #TODO: use entropy + value = temperature.getAdjustedProbability(probability) + if value < 0.06: + value = 0.0 + return value + + def break_the_structure(self): + self.breakGroup() + + def breakGroup(self): + workspace = self.ctx.workspace + if self.correspondence: + self.correspondence.breakCorrespondence() + if self.group: + self.group.breakGroup() + if self.leftBond: + self.leftBond.breakBond() + if self.rightBond: + self.rightBond.breakBond() + + while len(self.descriptions): + description = self.descriptions[-1] + description.breakDescription() + for o in self.objectList: + o.group = None + if self in workspace.structures: + workspace.structures.remove(self) + if self in workspace.objects: + workspace.objects.remove(self) + if self in self.string.objects: + self.string.objects.remove(self) + + def updateInternalStrength(self): + slipnet = self.ctx.slipnet + relatedBondAssociation = self.groupCategory.getRelatedNode( + slipnet.bondCategory).degreeOfAssociation() + bondWeight = relatedBondAssociation ** 0.98 + length = len(self.objectList) + if length == 1: + lengthFactor = 5.0 + elif length == 2: + lengthFactor = 20.0 + elif length == 3: + lengthFactor = 60.0 + else: + lengthFactor = 90.0 + lengthWeight = 100.0 - bondWeight + weightList = ((relatedBondAssociation, bondWeight), + (lengthFactor, lengthWeight)) + self.internalStrength = formulas.weightedAverage(weightList) + + def updateExternalStrength(self): + if self.spansString(): + self.externalStrength = 100.0 + else: + self.externalStrength = self.localSupport() + + def localSupport(self): + numberOfSupporters = self.numberOfLocalSupportingGroups() + if numberOfSupporters == 0: + return 0.0 + supportFactor = min(1.0, 0.6 ** (1 / (numberOfSupporters ** 3))) + densityFactor = 100.0 * ((self.localDensity() / 100.0) ** 0.5) + return densityFactor * supportFactor + + def numberOfLocalSupportingGroups(self): + count = 0 + for objekt in self.string.objects: + if isinstance(objekt, Group) and self.isOutsideOf(objekt): + if (objekt.groupCategory == self.groupCategory and + objekt.directionCategory == self.directionCategory): + count += 1 + return count + + def localDensity(self): + numberOfSupporters = self.numberOfLocalSupportingGroups() + halfLength = len(self.string) / 2.0 + return 100.0 * numberOfSupporters / halfLength + + def sameGroup(self, other): + if self.leftIndex != other.leftIndex: + return False + if self.rightIndex != other.rightIndex: + return False + if self.groupCategory != other.groupCategory: + return False + if self.directionCategory != other.directionCategory: + return False + if self.facet != other.facet: + return False + return True + + def distinguishingDescriptor(self, descriptor): + """Whether no other object of the same type has the same descriptor""" + if not WorkspaceObject.distinguishingDescriptor(self, descriptor): + return False + for objekt in self.string.objects: + # check to see if they are of the same type + if isinstance(objekt, Group) and objekt != self: + # check all descriptions for the descriptor + for description in objekt.descriptions: + if description.descriptor == descriptor: + return False + return True diff --git a/copycat/group_README.md b/copycat/group_README.md new file mode 100644 index 0000000..0d2fd54 --- /dev/null +++ b/copycat/group_README.md @@ -0,0 +1,53 @@ +# README_group.md + +## Overview +`group.py` implements the Group system, a key component of the Copycat system that manages the grouping of objects in strings. It handles the creation, evaluation, and management of groups that represent meaningful collections of objects based on their properties and relationships. + +## Core Components +- `Group` class: Main class that represents a group of objects +- Group evaluation system +- Group description management + +## Key Features +- Manages groups of objects in strings +- Evaluates group strength based on multiple factors +- Handles group descriptions and bond descriptions +- Supports group flipping and versioning +- Manages group compatibility and support + +## Group Components +- `groupCategory`: Category of the group +- `directionCategory`: Direction of the group +- `facet`: Aspect of the group +- `objectList`: List of objects in the group +- `bondList`: List of bonds in the group +- `descriptions`: List of group descriptions +- `bondDescriptions`: List of bond descriptions + +## Main Methods +- `updateInternalStrength()`: Calculate internal group strength +- `updateExternalStrength()`: Calculate external group strength +- `buildGroup()`: Create and establish group +- `breakGroup()`: Remove group +- `localSupport()`: Calculate local support +- `numberOfLocalSupportingGroups()`: Count supporting groups +- `sameGroup()`: Compare groups for equality + +## Group Types +- Single letter groups +- Multi-letter groups +- Direction-based groups +- Category-based groups +- Length-based groups + +## Dependencies +- Requires `description`, `workspaceObject`, and `formulas` modules +- Used by the main `copycat` module + +## Notes +- Groups are evaluated based on bond association and length +- The system supports both single and multi-object groups +- Groups can have multiple descriptions and bond descriptions +- The system handles group compatibility and support +- Groups can be flipped to create alternative versions +- Length descriptions are probabilistically added based on temperature \ No newline at end of file diff --git a/copycat/gui/__init__.py b/copycat/gui/__init__.py new file mode 100644 index 0000000..80f3ae3 --- /dev/null +++ b/copycat/gui/__init__.py @@ -0,0 +1 @@ +from .gui import GUI diff --git a/copycat/gui/control.py b/copycat/gui/control.py new file mode 100644 index 0000000..e74023d --- /dev/null +++ b/copycat/gui/control.py @@ -0,0 +1,59 @@ +import tkinter as tk +import tkinter.ttk as ttk + +from .gridframe import GridFrame +from .entry import Entry + +class Control(GridFrame): + def __init__(self, parent, *args, **kwargs): + GridFrame.__init__(self, parent, *args, **kwargs) + + self.paused = True + self.steps = 0 + self.go = False + + self.playbutton = ttk.Button(self, text='Play', command=lambda : self.toggle()) + self.add(self.playbutton, 0, 0) + + self.stepbutton = ttk.Button(self, text='Step', command=lambda : self.step()) + self.add(self.stepbutton, 1, 0) + + self.entry = Entry(self) + self.add(self.entry, 0, 1, xspan=2) + + self.gobutton = ttk.Button(self, text='Go', command=lambda : self.set_go()) + self.add(self.gobutton, 0, 2, xspan=2) + + def play(self): + self.paused = False + self.playbutton['text'] = 'Pause' + + def pause(self): + self.paused = True + self.playbutton['text'] = 'Play' + + def toggle(self): + if self.paused: + self.play() + else: + self.pause() + + def step(self): + self.steps += 1 + + def has_step(self): + if self.steps > 0: + self.steps -= 1 + return True + else: + return False + + def set_go(self): + self.go = True + self.play() + + def get_vars(self): + return self.entry.a.get(), self.entry.b.get(), self.entry.c.get() + + def reset(self): + self.go = False diff --git a/copycat/gui/entry.py b/copycat/gui/entry.py new file mode 100644 index 0000000..e4112a8 --- /dev/null +++ b/copycat/gui/entry.py @@ -0,0 +1,27 @@ + +import tkinter as tk +import tkinter.ttk as ttk + +from .gridframe import GridFrame + +class Entry(GridFrame): + def __init__(self, parent, *args, **kwargs): + GridFrame.__init__(self, parent, *args, **kwargs) + self.aLabel = ttk.Label(self, text='Initial:') + self.a = ttk.Entry(self, style='EntryStyle.TEntry') + + self.add(self.aLabel, 0, 0) + self.add(self.a, 0, 1) + + self.bLabel = ttk.Label(self, text='Final:') + self.b = ttk.Entry(self, style='EntryStyle.TEntry') + + self.add(self.bLabel, 1, 0) + self.add(self.b, 1, 1) + + self.cLabel = ttk.Label(self, text='Next:') + self.c = ttk.Entry(self, style='EntryStyle.TEntry') + + self.add(self.cLabel, 2, 0) + self.add(self.c, 2, 1) + GridFrame.configure(self) diff --git a/copycat/gui/gridframe.py b/copycat/gui/gridframe.py new file mode 100644 index 0000000..e083b42 --- /dev/null +++ b/copycat/gui/gridframe.py @@ -0,0 +1,11 @@ +import tkinter as tk +import tkinter.ttk as ttk + +class GridFrame(tk.Frame): + def __init__(self, parent, *args, **kwargs): + ttk.Frame.__init__(self, parent, *args, **kwargs) + + def add(self, element, x, y, xspan=1, yspan=1): + element.grid(column=x, row=y, columnspan=xspan, rowspan=yspan, sticky=tk.N+tk.E+tk.S+tk.W) + tk.Grid.rowconfigure(self, x, weight=1) + tk.Grid.columnconfigure(self, y, weight=1) diff --git a/copycat/gui/gui.py b/copycat/gui/gui.py new file mode 100644 index 0000000..7c603df --- /dev/null +++ b/copycat/gui/gui.py @@ -0,0 +1,96 @@ +import sys +import time + +import tkinter as tk +import tkinter.ttk as ttk + +from tkinter import scrolledtext +from tkinter import filedialog + +import matplotlib.pyplot as plt + +from .status import Status, StatusFrame +from .status import Plot +from .gridframe import GridFrame +from .primary import Primary +from .list import List +from .style import configure_style + +from .plot import plot_answers, plot_temp + +plt.style.use('dark_background') + +class MainApplication(GridFrame): + + def __init__(self, parent, *args, **kwargs): + GridFrame.__init__(self, parent, *args, **kwargs) + + self.parent = parent + self.primary = Primary(self, *args, **kwargs) + self.add(self.primary, 0, 0, xspan=2) + self.create_widgets() + GridFrame.configure(self) + + def create_widgets(self): + columns = 20 + self.slipList = List(self, columns) + self.add(self.slipList, 0, 1) + + self.codeletList = List(self, columns) + self.add(self.codeletList, 1, 1) + + self.objectList = List(self, columns) + self.add(self.objectList, 2, 1, xspan=2) + + self.graph1 = Plot(self, 'Temperature history') + self.add(self.graph1, 2, 0) + + self.graph2 = Plot(self, 'Answer Distribution') + self.add(self.graph2, 3, 0) + + def update(self, copycat): + self.primary.update(copycat) + + slipnodes = copycat.slipnet.slipnodes + codelets = copycat.coderack.codelets + objects = copycat.workspace.objects + + self.slipList.update(slipnodes, key=lambda s:s.activation, + formatter=lambda s : '{}: {}'.format(s.name, round(s.activation, 2))) + self.codeletList.update(codelets, key=lambda c:c.urgency, formatter= lambda s : '{}: {}'.format(s.name, round(s.urgency, 2))) + get_descriptors = lambda s : ', '.join('({}={})'.format(d.descriptionType.name, d.descriptor.name) for d in s.descriptions) + self.objectList.update(objects, formatter=lambda s : '{}: {}'.format(s, get_descriptors(s))) + + def modifier(status): + with plt.style.context(('dark_background')): + plot_temp(copycat.temperature, status) + self.graph1.status.modifier = modifier + + def reset_with_strings(self, initial, modified, target): + self.primary.reset_with_strings(initial, modified, target) + +class GUI(object): + def __init__(self, title): + self.root = tk.Tk() + self.root.title(title) + tk.Grid.rowconfigure(self.root, 0, weight=1) + tk.Grid.columnconfigure(self.root, 0, weight=1) + self.app = MainApplication(self.root) + self.app.grid(row=0, column=0, sticky=tk.N+tk.S+tk.E+tk.W) + configure_style(ttk.Style()) + + def add_answers(self, answers): + def modifier(status): + with plt.style.context(('dark_background')): + plot_answers(answers, status) + self.app.graph2.status.modifier = modifier + + def refresh(self): + self.root.update_idletasks() + self.root.update() + + def paused(self): + return self.app.primary.control.paused + + def update(self, copycat): + self.app.update(copycat) diff --git a/copycat/gui/list.py b/copycat/gui/list.py new file mode 100644 index 0000000..7768efd --- /dev/null +++ b/copycat/gui/list.py @@ -0,0 +1,26 @@ +import tkinter as tk +import tkinter.ttk as ttk + +import time + +from .gridframe import GridFrame + +class List(GridFrame): + + def __init__(self, parent, columns, updateInterval=.1): + GridFrame.__init__(self, parent) + self.text = ttk.Label(self, anchor='w', justify=tk.LEFT, width=30) + self.add(self.text, 0, 0) + + self.columns = columns + + self.lastUpdated = time.time() + self.updateInterval = updateInterval + + def update(self, l, key=None, reverse=False, formatter=lambda s : str(s)): + current = time.time() + if current - self.lastUpdated > self.updateInterval: + l = l[:self.columns] + if key is not None: + l = sorted(l, key=key, reverse=False) + self.text['text'] = '\n'.join(map(formatter, l)) diff --git a/copycat/gui/plot.py b/copycat/gui/plot.py new file mode 100644 index 0000000..54d900b --- /dev/null +++ b/copycat/gui/plot.py @@ -0,0 +1,24 @@ +import matplotlib.pyplot as plt; plt.rcdefaults() +import numpy as np +import matplotlib.pyplot as plt + +def plot_temp(temperature, status): + status.subplot.clear() + status.subplot.plot(temperature.history) + status.subplot.set_ylabel('Temperature') + status.subplot.set_xlabel('Time') + status.subplot.set_title('Temperature History') + +def plot_answers(answers, status): + answers = sorted(answers.items(), key=lambda kv : kv[1]['count']) + objects = [t[0] for t in answers] + yvalues = [t[1]['count'] for t in answers] + + y_pos = np.arange(len(objects)) + + status.subplot.clear() + status.subplot.bar(y_pos, yvalues, align='center', alpha=0.5) + status.subplot.set_xticks(y_pos) + status.subplot.set_xticklabels(tuple(objects)) + status.subplot.set_ylabel('Count') + status.subplot.set_title('Answers') diff --git a/copycat/gui/primary.py b/copycat/gui/primary.py new file mode 100644 index 0000000..03011eb --- /dev/null +++ b/copycat/gui/primary.py @@ -0,0 +1,30 @@ +import tkinter as tk +import tkinter.ttk as ttk + +from tkinter import scrolledtext +from tkinter import filedialog + +from .control import Control +from .gridframe import GridFrame + +from .workspacecanvas import WorkspaceCanvas + +class Primary(GridFrame): + + def __init__(self, parent, *args, **kwargs): + GridFrame.__init__(self, parent, *args, **kwargs) + + self.canvas = WorkspaceCanvas(self) + self.add(self.canvas, 0, 0, xspan=2) + + self.control = Control(self) + self.add(self.control, 0, 2) + + GridFrame.configure(self) + + def update(self, copycat): + self.canvas.update(copycat) + + def reset_with_strings(self, initial, modified, target): + self.canvas.reset_with_strings(initial, modified, target) + self.control.reset() diff --git a/copycat/gui/status.py b/copycat/gui/status.py new file mode 100644 index 0000000..56d252a --- /dev/null +++ b/copycat/gui/status.py @@ -0,0 +1,66 @@ +import matplotlib +matplotlib.use("TkAgg") +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk +from matplotlib.figure import Figure + +import tkinter as tk +import tkinter.ttk as ttk + +import time +import matplotlib.animation as animation + +import matplotlib.pyplot as plt + +plt.style.use('dark_background') + +from .gridframe import GridFrame + +class Plot(GridFrame): + def __init__(self, parent, title): + GridFrame.__init__(self, parent) + self.status = Status() + self.sframe = StatusFrame(self, self.status, title) + self.add(self.sframe, 0, 0, xspan=2) + + self.savebutton = ttk.Button(self, text='Save to path:', command=lambda : self.save()) + self.add(self.savebutton, 0, 1) + + self.pathentry = ttk.Entry(self, style='EntryStyle.TEntry', textvariable='output/dist.png') + self.add(self.pathentry, 1, 1) + + def save(self): + path = self.pathentry.get() + if len(path) > 0: + try: + self.status.figure.savefig(path) + except Exception as e: + print(e) + +class StatusFrame(ttk.Frame): + def __init__(self, parent, status, title): + ttk.Frame.__init__(self, parent) + self.status = status + + self.canvas = FigureCanvasTkAgg(status.figure, self) + self.canvas.show() + self.canvas.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True) + + self.animation = animation.FuncAnimation(status.figure, lambda i : status.update_plots(i), interval=1000) + +class Status(object): + def __init__(self): + self.figure = Figure(figsize=(5,5), dpi=100) + self.subplot = self.figure.add_subplot(111) + self.x = [] + self.y = [] + + def modifier(status): + with plt.style.context(('dark_background')): + status.subplot.plot(status.x, status.y) + + self.modifier = modifier + self.update_plots(0) + + def update_plots(self, i): + self.subplot.clear() + self.modifier(self) diff --git a/copycat/gui/style.py b/copycat/gui/style.py new file mode 100644 index 0000000..a8bca07 --- /dev/null +++ b/copycat/gui/style.py @@ -0,0 +1,33 @@ +style_dict = dict(foreground='white', + background='black') + +map_options = dict( + foreground=[('disabled', 'black'), + ('pressed', 'white'), + ('active', 'white')], + background=[('disabled', 'black'), + ('pressed', '!focus', 'black'), + ('active', 'black')], + highlightcolor=[('focus', 'black'), + ('!focus', 'black')]) + +def configure_style(style): + style.configure('TButton', **style_dict) + style.map('TButton', **map_options) + style.configure('TLabel', **style_dict) + #style.configure('TEntry', **style_dict) + #style.map('TEntry', **map_options) + + # A hack to change entry style + style.element_create("plain.field", "from", "clam") + style.layout("EntryStyle.TEntry", + [('Entry.plain.field', {'children': [( + 'Entry.background', {'children': [( + 'Entry.padding', {'children': [( + 'Entry.textarea', {'sticky': 'nswe'})], + 'sticky': 'nswe'})], 'sticky': 'nswe'})], + 'border':'2', 'sticky': 'nswe'})]) + style.configure("EntryStyle.TEntry", + background="black", + foreground="white", + fieldbackground="black") diff --git a/copycat/gui/workspacecanvas.py b/copycat/gui/workspacecanvas.py new file mode 100644 index 0000000..62ccb90 --- /dev/null +++ b/copycat/gui/workspacecanvas.py @@ -0,0 +1,70 @@ +import tkinter as tk +import tkinter.ttk as ttk + +from .gridframe import GridFrame + +font1Size = 32 +font1 = ('Helvetica', font1Size) + +class WorkspaceCanvas(GridFrame): + + def __init__(self, parent, *args, **kwargs): + GridFrame.__init__(self, parent, *args, **kwargs) + + self.chars = [] + + self.initial = '' + self.modified = '' + self.target = '' + self.answer = '' + + self.changed = False + + self.canvas = tk.Canvas(self, background='black') + #self.canvas['width'] = 1600 + self.add(self.canvas, 0, 0) + + GridFrame.configure(self) + + def update(self, copycat): + answer = '' if copycat.workspace.rule is None else copycat.workspace.rule.buildTranslatedRule() + if answer != self.answer: + self.changed = True + + if self.changed: + self.canvas.delete('all') + del self.chars[:] + self.add_text() + + def add_text(self): + padding = 100 + + def add_sequences(sequences, x, y): + for sequence in sequences: + x += padding + if sequence is None: + sequence = '' + for char in sequence: + self.chars.append((char, (x, y))) + self.canvas.create_text(x, y, text=char, anchor=tk.NW, font=font1, fill='white') + x += font1Size + return x, y + + x = 0 + y = padding + + add_sequences([self.initial, self.modified], x, y) + + x = 0 + y += padding + + add_sequences([self.target, self.answer], x, y) + + def reset_with_strings(self, initial, modified, target): + if initial != self.initial or \ + modified != self.modified or \ + target != self.target: + self.changed = True + self.initial = initial + self.modified = modified + self.target = target diff --git a/copycat/io.py b/copycat/io.py new file mode 100644 index 0000000..ae1185b --- /dev/null +++ b/copycat/io.py @@ -0,0 +1,9 @@ + +def save_answers(answers, filename): + answers = sorted(answers.items(), key=lambda kv : kv[1]['count']) + keys = [k for k, v in answers] + counts = [str(v['count']) for k, v in answers] + with open(filename, 'w') as outfile: + outfile.write(','.join(keys)) + outfile.write('\n') + outfile.write(','.join(counts)) diff --git a/copycat/io_README.md b/copycat/io_README.md new file mode 100644 index 0000000..08cc95f --- /dev/null +++ b/copycat/io_README.md @@ -0,0 +1,54 @@ +# Input/Output System + +## Overview +The input/output system is a utility component of the Copycat architecture that handles file and data input/output operations. This system provides interfaces for reading and writing data to and from various sources. + +## Key Features +- File operations +- Data parsing +- Format conversion +- Error handling +- State management + +## Operation Types +1. **File Operations** + - File reading + - File writing + - File management + - Path handling + +2. **Data Operations** + - Data parsing + - Format conversion + - Data validation + - Error handling + +3. **Special Operations** + - Configuration loading + - Logging + - Debug output + - State persistence + +## Usage +I/O operations are performed through the I/O system: + +```python +# Read from a file +data = io.read_file(file_path) + +# Write to a file +io.write_file(file_path, data) + +# Parse data +parsed = io.parse_data(data) +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Workspace: Uses I/O for data loading +- Statistics: Uses I/O for logging +- Curses Reporter: Uses I/O for display +- Problem: Uses I/O for problem loading \ No newline at end of file diff --git a/copycat/letter.py b/copycat/letter.py new file mode 100644 index 0000000..36eaaf5 --- /dev/null +++ b/copycat/letter.py @@ -0,0 +1,49 @@ +from .workspaceObject import WorkspaceObject + + +class Letter(WorkspaceObject): + def __init__(self, string, position, length): + WorkspaceObject.__init__(self, string) + workspace = self.ctx.workspace + workspace.objects += [self] + string.objects += [self] + self.leftIndex = position + self.leftmost = self.leftIndex == 1 + self.rightIndex = position + self.rightmost = self.rightIndex == length + + def describe(self, position, length): + slipnet = self.ctx.slipnet + if length == 1: + self.addDescription(slipnet.stringPositionCategory, slipnet.single) + if self.leftmost: + self.addDescription(slipnet.stringPositionCategory, slipnet.leftmost) + if self.rightmost: + self.addDescription(slipnet.stringPositionCategory, slipnet.rightmost) + if position * 2 == length + 1: + self.addDescription(slipnet.stringPositionCategory, slipnet.middle) + + def __repr__(self): + return '' % self.__str__() + + def __str__(self): + if not self.string: + return '' + i = self.leftIndex - 1 + if len(self.string) <= i: + raise ValueError('len(self.string) <= self.leftIndex :: %d <= %d', + len(self.string), self.leftIndex) + return self.string[i] + + def distinguishingDescriptor(self, descriptor): + """Whether no other object of the same type has the same descriptor""" + if not WorkspaceObject.distinguishingDescriptor(self, descriptor): + return False + for objekt in self.string.objects: + # check to see if they are of the same type + if isinstance(objekt, Letter) and objekt != self: + # check all descriptions for the descriptor + for description in objekt.descriptions: + if description.descriptor == descriptor: + return False + return True diff --git a/copycat/letter_README.md b/copycat/letter_README.md new file mode 100644 index 0000000..41592f2 --- /dev/null +++ b/copycat/letter_README.md @@ -0,0 +1,54 @@ +# Letter System + +## Overview +The letter system is a specialized component of the Copycat architecture that handles the representation and manipulation of individual letters and characters in the workspace. This system manages the properties and behaviors of letter objects in the analogical reasoning process. + +## Key Features +- Letter representation +- Character properties +- Letter manipulation +- Pattern matching +- State management + +## Letter Types +1. **Basic Letters** + - Alphabetic characters + - Numeric characters + - Special characters + - Whitespace + +2. **Structured Letters** + - Grouped letters + - Bonded letters + - Hierarchical letters + - Pattern letters + +3. **Special Letters** + - Rule letters + - Mapping letters + - Context letters + - Meta-letters + +## Usage +Letter operations are performed through the letter system: + +```python +# Create a letter +letter = Letter(char, properties) + +# Access letter properties +char = letter.get_char() + +# Modify letter state +letter.set_properties(new_properties) +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Workspace: Contains letter objects +- WorkspaceObject: Base class for letters +- Codelets: Operate on letters +- Correspondence: Maps between letters \ No newline at end of file diff --git a/copycat/plot.py b/copycat/plot.py new file mode 100644 index 0000000..772d0f1 --- /dev/null +++ b/copycat/plot.py @@ -0,0 +1,20 @@ +import matplotlib.pyplot as plt; plt.rcdefaults() +import numpy as np +import matplotlib.pyplot as plt + + +def plot_answers(answers, show=True, save=True, filename='distribution.png'): + answers = sorted(answers.items(), key=lambda kv : kv[1]['count']) + objects = [t[0] + ' (temp:{})'.format(round(t[1]['avgtemp'], 2)) for t in answers] + yvalues = [t[1]['count'] for t in answers] + + y_pos = np.arange(len(objects)) + + plt.bar(y_pos, yvalues, align='center', alpha=0.5) + plt.xticks(y_pos, objects) + plt.ylabel('Count') + plt.title('Answers') + if show: + plt.show() + if save: + plt.savefig('output/{}'.format(filename)) diff --git a/copycat/plot_README.md b/copycat/plot_README.md new file mode 100644 index 0000000..1acad14 --- /dev/null +++ b/copycat/plot_README.md @@ -0,0 +1,56 @@ +# Plot System + +## Overview +The plot system is a visualization component of the Copycat architecture that provides plotting and graphing capabilities for analyzing and displaying system data. This system helps in visualizing various metrics and relationships in the analogical reasoning process. + +## Key Features +- Data visualization +- Graph generation +- Metric plotting +- Analysis display +- State management + +## Plot Types +1. **Performance Plots** + - Activation curves + - Execution timelines + - Success rates + - Error distributions + +2. **System Plots** + - Object counts + - Link distributions + - State changes + - Memory usage + +3. **Analysis Plots** + - Pattern frequencies + - Relationship maps + - Similarity matrices + - Correlation graphs + +## Usage +Plots are generated through the plot system: + +```python +# Create a plot +plot = Plot(data, plot_type) + +# Configure plot +plot.set_title("Title") +plot.set_labels("X", "Y") + +# Display plot +plot.show() +``` + +## Dependencies +- Python 3.x +- matplotlib library +- No other external dependencies required + +## Related Components +- Statistics: Provides data for plotting +- Curses Reporter: Uses plots for display +- Workspace: Provides object data +- Slipnet: Provides activation data \ No newline at end of file diff --git a/copycat/problem.py b/copycat/problem.py new file mode 100644 index 0000000..6f23777 --- /dev/null +++ b/copycat/problem.py @@ -0,0 +1,69 @@ +from .copycat import Copycat + +from pprint import pprint + +class Problem: + def __init__(self, initial, modified, target, iterations, distributions=None, formulas=None): + self.formulas = formulas + if formulas is not None: + assert hasattr(Copycat(), 'temperature') + else: + if hasattr(Copycat(), 'temperature'): + self.formulas = set(Copycat().temperature.adj_formulas()) + print(self.formulas) + self.initial = initial + self.modified = modified + self.target = target + + self.iterations = iterations + if distributions is None: + self.distributions = self.solve() + else: + self.distributions = distributions + print(self.formulas) + + def test(self, comparison, expected=None): + print('-' * 120) + print('Testing copycat problem: {} : {} :: {} : _'.format(self.initial, + self.modified, + self.target)) + print('expected:') + if expected is None: + expected = self.distributions + pprint(expected) + + actual = self.solve() + print('actual:') + pprint(actual) + comparison(actual, expected) + print('-' * 120) + + def solve(self): + print('-' * 120) + print('Testing copycat problem: {} : {} :: {} : _'.format(self.initial, + self.modified, + self.target)) + copycat = Copycat() + answers = dict() + if self.formulas == None: + if hasattr(copycat, 'temperature'): + formula = copycat.temperature.getAdj() + else: + formula = None + answers[formula] = copycat.run(self.initial, + self.modified, + self.target, + self.iterations) + else: + print(self.formulas) + for formula in self.formulas: + copycat.temperature.useAdj(formula) + answers[formula] = copycat.run(self.initial, + self.modified, + self.target, + self.iterations) + print('Done with {}'.format(formula)) + return answers + + def generate(self): + self.distributions = self.solve() diff --git a/copycat/problem_README.md b/copycat/problem_README.md new file mode 100644 index 0000000..2a81923 --- /dev/null +++ b/copycat/problem_README.md @@ -0,0 +1,54 @@ +# Problem System + +## Overview +The problem system is a core component of the Copycat architecture that manages the representation and handling of analogical reasoning problems. This system defines the structure and properties of problems that the system attempts to solve. + +## Key Features +- Problem representation +- Problem loading +- Problem validation +- Solution tracking +- State management + +## Problem Types +1. **Basic Problems** + - String problems + - Pattern problems + - Rule problems + - Mapping problems + +2. **Composite Problems** + - Multi-step problems + - Hierarchical problems + - Network problems + - Context problems + +3. **Special Problems** + - Test problems + - Debug problems + - Meta-problems + - Derived problems + +## Usage +Problems are created and managed through the problem system: + +```python +# Create a problem +problem = Problem(initial_state, target_state) + +# Load a problem +problem = Problem.load_from_file(file_path) + +# Solve a problem +solution = problem.solve() +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Workspace: Contains problem state +- Slipnet: Provides concepts for solving +- Codelets: Operate on problems +- Correspondence: Maps problem elements \ No newline at end of file diff --git a/copycat/randomness.py b/copycat/randomness.py new file mode 100644 index 0000000..d5dd99b --- /dev/null +++ b/copycat/randomness.py @@ -0,0 +1,43 @@ +import bisect +import math +import random + + +def accumulate(iterable): + total = 0 + for v in iterable: + total += v + yield total + + +class Randomness(object): + def __init__(self, seed=None): + self.rng = random.Random(seed) + + def coinFlip(self, p=0.5): + return self.rng.random() < p + + def choice(self, seq): + return self.rng.choice(seq) + + def weighted_choice(self, seq, weights): + if not seq: + # Many callers rely on this behavior. + return None + else: + cum_weights = list(accumulate(weights)) + total = cum_weights[-1] + return seq[bisect.bisect_left(cum_weights, self.rng.random() * total)] + + def weighted_greater_than(self, first, second): + total = first + second + if total == 0: + return False + return self.coinFlip(float(first) / total) + + def sqrtBlur(self, value): + # This is exceedingly dumb, but it matches the Java code. + root = math.sqrt(value) + if self.coinFlip(): + return value + root + return value - root diff --git a/copycat/randomness_README.md b/copycat/randomness_README.md new file mode 100644 index 0000000..fa29420 --- /dev/null +++ b/copycat/randomness_README.md @@ -0,0 +1,54 @@ +# Randomness System + +## Overview +The randomness system is a utility component of the Copycat architecture that provides controlled random number generation and probabilistic operations. This system ensures consistent and reproducible random behavior across the analogical reasoning process. + +## Key Features +- Random number generation +- Probability distributions +- State management +- Seed control +- Reproducibility + +## Operation Types +1. **Basic Operations** + - Random number generation + - Probability sampling + - Distribution selection + - State initialization + +2. **Advanced Operations** + - Weighted selection + - Distribution mixing + - State transitions + - Pattern generation + +3. **Special Operations** + - Seed management + - State persistence + - Debug control + - Test reproducibility + +## Usage +Random operations are performed through the randomness system: + +```python +# Generate a random number +value = randomness.random() + +# Sample from a distribution +sample = randomness.sample(distribution) + +# Set random seed +randomness.set_seed(seed) +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Codelets: Use randomness for selection +- Temperature: Uses randomness for control +- Statistics: Uses randomness for sampling +- Workspace: Uses randomness for operations \ No newline at end of file diff --git a/copycat/replacement.py b/copycat/replacement.py new file mode 100644 index 0000000..0d4d094 --- /dev/null +++ b/copycat/replacement.py @@ -0,0 +1,9 @@ +from .workspaceStructure import WorkspaceStructure + + +class Replacement(WorkspaceStructure): + def __init__(self, ctx, objectFromInitial, objectFromModified, relation): + WorkspaceStructure.__init__(self, ctx) + self.objectFromInitial = objectFromInitial + self.objectFromModified = objectFromModified + self.relation = relation diff --git a/copycat/replacement_README.md b/copycat/replacement_README.md new file mode 100644 index 0000000..f806720 --- /dev/null +++ b/copycat/replacement_README.md @@ -0,0 +1,54 @@ +# Replacement System + +## Overview +The replacement system is a utility component of the Copycat architecture that manages the substitution and replacement of objects and structures in the workspace. This system handles the transformation of elements during the analogical reasoning process. + +## Key Features +- Object replacement +- Structure transformation +- Pattern substitution +- State management +- History tracking + +## Operation Types +1. **Basic Operations** + - Element replacement + - Pattern substitution + - Structure transformation + - State updates + +2. **Advanced Operations** + - Chain replacements + - Group transformations + - Context updates + - History tracking + +3. **Special Operations** + - Rule application + - Mapping translation + - Meta-replacements + - Derived transformations + +## Usage +Replacements are performed through the replacement system: + +```python +# Replace an object +new_obj = replacement.replace(old_obj, new_obj) + +# Apply a transformation +result = replacement.transform(obj, rule) + +# Track changes +history = replacement.get_history() +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Workspace: Contains objects to replace +- Rule: Provides replacement rules +- Correspondence: Maps replacements +- Codelets: Execute replacements \ No newline at end of file diff --git a/copycat/rule.py b/copycat/rule.py new file mode 100644 index 0000000..fcfe91a --- /dev/null +++ b/copycat/rule.py @@ -0,0 +1,149 @@ +import logging + + +from .workspaceStructure import WorkspaceStructure +from . import formulas + + +class Rule(WorkspaceStructure): + def __init__(self, ctx, facet, descriptor, category, relation): + WorkspaceStructure.__init__(self, ctx) + self.facet = facet + self.descriptor = descriptor + self.category = category + self.relation = relation + + def __str__(self): + if not self.facet: + return 'Empty rule' + return 'replace %s of %s %s by %s' % ( + self.facet.name, self.descriptor.name, + self.category.name, self.relation.name) + + def updateExternalStrength(self): + self.externalStrength = self.internalStrength + + def updateInternalStrength(self): + workspace = self.ctx.workspace + if not (self.descriptor and self.relation): + self.internalStrength = 50.0 + return + averageDepth = (self.descriptor.conceptualDepth + + self.relation.conceptualDepth) / 2.0 + averageDepth **= 1.1 # LSaldyt: This value (1.1) seems 100% contrived. + # see if the object corresponds to an object + # if so, see if the descriptor is present (modulo slippages) in the + # corresponding object + changedObjects = [o for o in workspace.initial.objects if o.changed] + changed = changedObjects[0] + sharedDescriptorTerm = 0.0 + if changed and changed.correspondence: + targetObject = changed.correspondence.objectFromTarget + slippages = workspace.slippages() + slipnode = self.descriptor.applySlippages(slippages) + if not targetObject.described(slipnode): + self.internalStrength = 0.0 + return + sharedDescriptorTerm = 100.0 + conceptual_height = (100.0 - self.descriptor.conceptualDepth) / 10.0 # LSaldyt: 10? + sharedDescriptorWeight = conceptual_height ** 1.4 # LSaldyt: 1.4 is also seemingly contrived + depthDifference = 100.0 - abs(self.descriptor.conceptualDepth - + self.relation.conceptualDepth) + weights = ((depthDifference, 12), # LSaldyt: ??? + (averageDepth, 18), # ???? + (sharedDescriptorTerm, sharedDescriptorWeight)) # 12 and 18 can be reduced to 2 and 3, depending on sharedDescriptorWeight + self.internalStrength = formulas.weightedAverage(weights) + if self.internalStrength > 100.0: # LSaldyt: A better formula wouldn't need to do this. + self.internalStrength = 100.0 + + def ruleEqual(self, other): + if not other: + return False + if self.relation != other.relation: + return False + if self.facet != other.facet: + return False + if self.category != other.category: + return False + if self.descriptor != other.descriptor: + return False + return True + + def activateRuleDescriptions(self): + if self.relation: + self.relation.buffer = 100.0 + if self.facet: + self.facet.buffer = 100.0 + if self.category: + self.category.buffer = 100.0 + if self.descriptor: + self.descriptor.buffer = 100.0 + + def incompatibleRuleCorrespondence(self, correspondence): + workspace = self.ctx.workspace + if not correspondence: + return False + # find changed object + changeds = [o for o in workspace.initial.objects if o.changed] + if not changeds: + return False + changed = changeds[0] + if correspondence.objectFromInitial != changed: + return False + # it is incompatible if the rule descriptor is not in the mapping list + return any(m.initialDescriptor == self.descriptor + for m in correspondence.conceptMappings) + + def __changeString(self, string): + slipnet = self.ctx.slipnet + # applies the changes to self string ie. successor + if self.facet == slipnet.length: + if self.relation == slipnet.predecessor: + return string[:-1] + elif self.relation == slipnet.successor: + # This seems to be happening at the wrong level of abstraction. + # "Lengthening" is not an operation that makes sense on strings; + # it makes sense only on *groups*, and here we've lost the + # "groupiness" of this string. What gives? + return string + string[0] + return string + # apply character changes + if self.relation == slipnet.predecessor: + if 'a' in string: + return None + return ''.join(chr(ord(c) - 1) for c in string) + elif self.relation == slipnet.successor: + if 'z' in string: + return None + return ''.join(chr(ord(c) + 1) for c in string) + else: + return self.relation.name.lower() + + def buildTranslatedRule(self): + workspace = self.ctx.workspace + if not (self.descriptor and self.relation): + return workspace.targetString + slippages = workspace.slippages() + self.category = self.category.applySlippages(slippages) + self.facet = self.facet.applySlippages(slippages) + self.descriptor = self.descriptor.applySlippages(slippages) + self.relation = self.relation.applySlippages(slippages) + # generate the final string + changeds = [o for o in workspace.target.objects if + o.described(self.descriptor) and + o.described(self.category)] + if len(changeds) == 0: + return workspace.targetString + elif len(changeds) > 1: + logging.info("More than one letter changed. Sorry, I can't solve problems like this right now.") + return None + else: + changed = changeds[0] + logging.debug('changed object = %s', changed) + left = changed.leftIndex - 1 + right = changed.rightIndex + s = workspace.targetString + changed_middle = self.__changeString(s[left:right]) + if changed_middle is None: + return None + return s[:left] + changed_middle + s[right:] diff --git a/copycat/rule_README.md b/copycat/rule_README.md new file mode 100644 index 0000000..19c0c3a --- /dev/null +++ b/copycat/rule_README.md @@ -0,0 +1,47 @@ +# README_rule.md + +## Overview +`rule.py` implements the Rule system, a key component of the Copycat system that manages the transformation rules used in analogical reasoning. It handles the creation, evaluation, and application of rules that describe how to transform strings based on their properties and relationships. + +## Core Components +- `Rule` class: Main class that represents a transformation rule +- Rule evaluation system +- Rule translation and application system + +## Key Features +- Defines transformation rules with facets, descriptors, categories, and relations +- Evaluates rule strength based on multiple factors +- Supports rule translation through concept slippages +- Handles string transformations based on rules +- Manages rule compatibility with correspondences + +## Rule Components +- `facet`: The aspect of the object to change (e.g., length) +- `descriptor`: The property being changed +- `category`: The type of object being changed +- `relation`: The transformation to apply + +## Main Methods +- `updateInternalStrength()`: Calculate rule strength +- `updateExternalStrength()`: Update external strength +- `activateRuleDescriptions()`: Activate rule-related concepts +- `buildTranslatedRule()`: Apply rule to target string +- `incompatibleRuleCorrespondence()`: Check rule-correspondence compatibility +- `ruleEqual()`: Compare rules for equality + +## Rule Types +- Length-based rules (predecessor, successor) +- Character-based rules (predecessor, successor) +- Category-based rules + +## Dependencies +- Requires `workspaceStructure` and `formulas` modules +- Uses `logging` for debug output +- Used by the main `copycat` module + +## Notes +- Rules are evaluated based on conceptual depth and descriptor sharing +- Rule strength is calculated using weighted averages +- Rules can be translated through concept slippages +- The system supports both single-character and length-based transformations +- Rules can be incompatible with certain correspondences \ No newline at end of file diff --git a/copycat/sampleText.txt b/copycat/sampleText.txt new file mode 100644 index 0000000..7cb721a --- /dev/null +++ b/copycat/sampleText.txt @@ -0,0 +1,4 @@ +1,2 +3,4 +7,7 +100,100 diff --git a/copycat/sliplink.py b/copycat/sliplink.py new file mode 100644 index 0000000..a21b61b --- /dev/null +++ b/copycat/sliplink.py @@ -0,0 +1,27 @@ + +class Sliplink(object): + def __init__(self, source, destination, label=None, length=0.0): + self.source = source + self.destination = destination + self.label = label + self.fixedLength = length + source.outgoingLinks += [self] + destination.incomingLinks += [self] + + def degreeOfAssociation(self): + if self.fixedLength > 0 or not self.label: + return 100.0 - self.fixedLength + return self.label.degreeOfAssociation() + + def intrinsicDegreeOfAssociation(self): + if self.fixedLength > 1: + return 100.0 - self.fixedLength + if self.label: + return 100.0 - self.label.intrinsicLinkLength + return 0.0 + + def spread_activation(self): + self.destination.buffer += self.intrinsicDegreeOfAssociation() + + def points_at(self, other): + return self.destination == other diff --git a/copycat/sliplink_README.md b/copycat/sliplink_README.md new file mode 100644 index 0000000..b302ffa --- /dev/null +++ b/copycat/sliplink_README.md @@ -0,0 +1,54 @@ +# Sliplink System + +## Overview +The sliplink system is a core component of the Copycat architecture that manages the connections between nodes in the slipnet. This system handles the creation, modification, and traversal of links between conceptual nodes in the network. + +## Key Features +- Link representation +- Connection management +- Weight handling +- State tracking +- Event processing + +## Link Types +1. **Basic Links** + - Category links + - Property links + - Instance links + - Similarity links + +2. **Structural Links** + - Hierarchy links + - Composition links + - Association links + - Pattern links + +3. **Special Links** + - Rule links + - Context links + - Meta-links + - Derived links + +## Usage +Sliplinks are created and managed through the sliplink system: + +```python +# Create a sliplink +link = Sliplink(source_node, target_node, weight) + +# Access link properties +weight = link.get_weight() + +# Modify link state +link.set_weight(new_weight) +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Slipnet: Contains sliplinks +- Slipnode: Connected by sliplinks +- Codelets: Use sliplinks for traversal +- Workspace: Uses links for relationships \ No newline at end of file diff --git a/copycat/slipnet.py b/copycat/slipnet.py new file mode 100644 index 0000000..db0b040 --- /dev/null +++ b/copycat/slipnet.py @@ -0,0 +1,248 @@ +from .slipnode import Slipnode +from .sliplink import Sliplink + + +class Slipnet(object): + # pylint: disable=too-many-instance-attributes + def __init__(self): + self.__addInitialNodes() + self.__addInitialLinks() + self.reset() + + def reset(self): + self.numberOfUpdates = 0 + for node in self.slipnodes: + node.reset() + for node in self.initiallyClampedSlipnodes: + node.clampHigh() + + def update(self, random): + self.numberOfUpdates += 1 + if self.numberOfUpdates == 50: + for node in self.initiallyClampedSlipnodes: + node.unclamp() + for node in self.slipnodes: + node.update() + for node in self.slipnodes: + node.spread_activation() + for node in self.slipnodes: + node.addBuffer() + node.jump(random) + node.buffer = 0.0 + + def isDistinguishingDescriptor(self, descriptor): + """Whether no other object of the same type has the same descriptor""" + if descriptor == self.letter: + return False + if descriptor == self.group: + return False + if descriptor in self.numbers: + return False + return True + + def __addInitialNodes(self): + # pylint: disable=too-many-statements + self.slipnodes = [] + self.letters = [] + for c in 'abcdefghijklmnopqrstuvwxyz': + slipnode = self.__addNode(c, 10.0) + self.letters += [slipnode] + self.numbers = [] + for c in '12345': + slipnode = self.__addNode(c, 30.0) + self.numbers += [slipnode] + + # string positions + self.leftmost = self.__addNode('leftmost', 40.0) + self.rightmost = self.__addNode('rightmost', 40.0) + self.middle = self.__addNode('middle', 40.0) + self.single = self.__addNode('single', 40.0) + self.whole = self.__addNode('whole', 40.0) + + # alphabetic positions + self.first = self.__addNode('first', 60.0) + self.last = self.__addNode('last', 60.0) + + # directions + self.left = self.__addNode('left', 40.0) + self.left.codelets += ['top-down-bond-scout--direction'] + self.left.codelets += ['top-down-group-scout--direction'] + self.right = self.__addNode('right', 40.0) + self.right.codelets += ['top-down-bond-scout--direction'] + self.right.codelets += ['top-down-group-scout--direction'] + + # bond types + self.predecessor = self.__addNode('predecessor', 50.0, 60.0) + self.predecessor.codelets += ['top-down-bond-scout--category'] + self.successor = self.__addNode('successor', 50.0, 60.0) + self.successor.codelets += ['top-down-bond-scout--category'] + self.sameness = self.__addNode('sameness', 80.0) + self.sameness.codelets += ['top-down-bond-scout--category'] + + # group types + self.predecessorGroup = self.__addNode('predecessorGroup', 50.0) + self.predecessorGroup.codelets += ['top-down-group-scout--category'] + self.successorGroup = self.__addNode('successorGroup', 50.0) + self.successorGroup.codelets += ['top-down-group-scout--category'] + self.samenessGroup = self.__addNode('samenessGroup', 80.0) + self.samenessGroup.codelets += ['top-down-group-scout--category'] + + # other relations + self.identity = self.__addNode('identity', 90.0) + self.opposite = self.__addNode('opposite', 90.0, 80.0) + + # objects + self.letter = self.__addNode('letter', 20.0) + self.group = self.__addNode('group', 80.0) + + # categories + self.letterCategory = self.__addNode('letterCategory', 30.0) + self.stringPositionCategory = self.__addNode('stringPositionCategory', 70.0) + self.stringPositionCategory.codelets += ['top-down-description-scout'] + self.alphabeticPositionCategory = self.__addNode('alphabeticPositionCategory', 80.0) + self.alphabeticPositionCategory.codelets += ['top-down-description-scout'] + self.directionCategory = self.__addNode('directionCategory', 70.0) + self.bondCategory = self.__addNode('bondCategory', 80.0) + self.groupCategory = self.__addNode('groupCategory', 80.0) + self.length = self.__addNode('length', 60.0) + self.objectCategory = self.__addNode('objectCategory', 90.0) + self.bondFacet = self.__addNode('bondFacet', 90.0) + + # some factors are considered "very relevant" a priori + self.initiallyClampedSlipnodes = [ + self.letterCategory, + self.stringPositionCategory, + ] + + def __addInitialLinks(self): + self.sliplinks = [] + self.__link_items_to_their_neighbors(self.letters) + self.__link_items_to_their_neighbors(self.numbers) + # letter categories + for letter in self.letters: + self.__addInstanceLink(self.letterCategory, letter, 97.0) + self.__addCategoryLink(self.samenessGroup, self.letterCategory, 50.0) + # lengths + for number in self.numbers: + self.__addInstanceLink(self.length, number) + groups = [self.predecessorGroup, self.successorGroup, self.samenessGroup] + for group in groups: + self.__addNonSlipLink(group, self.length, length=95.0) + opposites = [ + (self.first, self.last), + (self.leftmost, self.rightmost), + (self.left, self.right), + (self.successor, self.predecessor), + (self.successorGroup, self.predecessorGroup), + ] + for a, b in opposites: + self.__addOppositeLink(a, b) + # properties + self.__addPropertyLink(self.letters[0], self.first, 75.0) + self.__addPropertyLink(self.letters[-1], self.last, 75.0) + links = [ + # object categories + (self.objectCategory, self.letter), + (self.objectCategory, self.group), + # string positions, + (self.stringPositionCategory, self.leftmost), + (self.stringPositionCategory, self.rightmost), + (self.stringPositionCategory, self.middle), + (self.stringPositionCategory, self.single), + (self.stringPositionCategory, self.whole), + # alphabetic positions, + (self.alphabeticPositionCategory, self.first), + (self.alphabeticPositionCategory, self.last), + # direction categories, + (self.directionCategory, self.left), + (self.directionCategory, self.right), + # bond categories, + (self.bondCategory, self.predecessor), + (self.bondCategory, self.successor), + (self.bondCategory, self.sameness), + # group categories + (self.groupCategory, self.predecessorGroup), + (self.groupCategory, self.successorGroup), + (self.groupCategory, self.samenessGroup), + # bond facets + (self.bondFacet, self.letterCategory), + (self.bondFacet, self.length), + ] + for a, b in links: + self.__addInstanceLink(a, b) + # link bonds to their groups + self.__addNonSlipLink(self.sameness, self.samenessGroup, label=self.groupCategory, length=30.0) + self.__addNonSlipLink(self.successor, self.successorGroup, label=self.groupCategory, length=60.0) + self.__addNonSlipLink(self.predecessor, self.predecessorGroup, label=self.groupCategory, length=60.0) + # link bond groups to their bonds + self.__addNonSlipLink(self.samenessGroup, self.sameness, label=self.bondCategory, length=90.0) + self.__addNonSlipLink(self.successorGroup, self.successor, label=self.bondCategory, length=90.0) + self.__addNonSlipLink(self.predecessorGroup, self.predecessor, label=self.bondCategory, length=90.0) + # letter category to length + self.__addSlipLink(self.letterCategory, self.length, length=95.0) + self.__addSlipLink(self.length, self.letterCategory, length=95.0) + # letter to group + self.__addSlipLink(self.letter, self.group, length=90.0) + self.__addSlipLink(self.group, self.letter, length=90.0) + # direction-position, direction-neighbor, position-neighbor + self.__addBidirectionalLink(self.left, self.leftmost, 90.0) + self.__addBidirectionalLink(self.right, self.rightmost, 90.0) + self.__addBidirectionalLink(self.right, self.leftmost, 100.0) + self.__addBidirectionalLink(self.left, self.rightmost, 100.0) + self.__addBidirectionalLink(self.leftmost, self.first, 100.0) + self.__addBidirectionalLink(self.rightmost, self.first, 100.0) + self.__addBidirectionalLink(self.leftmost, self.last, 100.0) + self.__addBidirectionalLink(self.rightmost, self.last, 100.0) + # other + self.__addSlipLink(self.single, self.whole, length=90.0) + self.__addSlipLink(self.whole, self.single, length=90.0) + + def __addLink(self, source, destination, label=None, length=0.0): + link = Sliplink(source, destination, label=label, length=length) + self.sliplinks += [link] + return link + + def __addSlipLink(self, source, destination, label=None, length=0.0): + link = self.__addLink(source, destination, label, length) + source.lateralSlipLinks += [link] + + def __addNonSlipLink(self, source, destination, label=None, length=0.0): + link = self.__addLink(source, destination, label, length) + source.lateralNonSlipLinks += [link] + + def __addBidirectionalLink(self, source, destination, length): + self.__addNonSlipLink(source, destination, length=length) + self.__addNonSlipLink(destination, source, length=length) + + def __addCategoryLink(self, source, destination, length): + #noinspection PyArgumentEqualDefault + link = self.__addLink(source, destination, None, length) + source.categoryLinks += [link] + + def __addInstanceLink(self, source, destination, length=100.0): + categoryLength = source.conceptualDepth - destination.conceptualDepth + self.__addCategoryLink(destination, source, categoryLength) + #noinspection PyArgumentEqualDefault + link = self.__addLink(source, destination, None, length) + source.instanceLinks += [link] + + def __addPropertyLink(self, source, destination, length): + #noinspection PyArgumentEqualDefault + link = self.__addLink(source, destination, None, length) + source.propertyLinks += [link] + + def __addOppositeLink(self, source, destination): + self.__addSlipLink(source, destination, label=self.opposite) + self.__addSlipLink(destination, source, label=self.opposite) + + def __addNode(self, name, depth, length=0): + slipnode = Slipnode(self, name, depth, length) + self.slipnodes += [slipnode] + return slipnode + + def __link_items_to_their_neighbors(self, items): + previous = items[0] + for item in items[1:]: + self.__addNonSlipLink(previous, item, label=self.successor) + self.__addNonSlipLink(item, previous, label=self.predecessor) + previous = item diff --git a/copycat/slipnet_README.md b/copycat/slipnet_README.md new file mode 100644 index 0000000..5758cd2 --- /dev/null +++ b/copycat/slipnet_README.md @@ -0,0 +1,51 @@ +# README_slipnet.md + +## Overview +`slipnet.py` implements the Slipnet, a key component of the Copycat system that represents the conceptual network of relationships between different concepts. It manages a network of nodes (concepts) and links (relationships) that are used in analogical reasoning. + +## Core Components +- `Slipnet` class: Main class that manages the conceptual network +- Network of `Slipnode` objects representing concepts +- Network of `Sliplink` objects representing relationships + +## Key Features +- Manages a network of conceptual nodes and their relationships +- Handles activation spreading between nodes +- Supports both slip and non-slip links +- Implements various types of relationships: + - Letter categories + - String positions + - Alphabetic positions + - Directions + - Bond types + - Group types + - Other relations + +## Node Types +- Letters (a-z) +- Numbers (1-5) +- String positions (leftmost, rightmost, middle, single, whole) +- Alphabetic positions (first, last) +- Directions (left, right) +- Bond types (predecessor, successor, sameness) +- Group types (predecessorGroup, successorGroup, samenessGroup) +- Categories (letterCategory, stringPositionCategory, etc.) + +## Link Types +- Slip links (lateral connections) +- Non-slip links (fixed connections) +- Category links (hierarchical connections) +- Instance links (specific examples) +- Property links (attributes) +- Opposite links (antonyms) + +## Dependencies +- Requires `slipnode` and `sliplink` modules +- Used by the main `copycat` module + +## Notes +- The network is initialized with predefined nodes and links +- Activation spreads through the network during reasoning +- Some nodes are initially clamped to high activation +- The network supports both hierarchical and lateral relationships +- The system uses conceptual depth to determine link strengths \ No newline at end of file diff --git a/copycat/slipnode.py b/copycat/slipnode.py new file mode 100644 index 0000000..e46c688 --- /dev/null +++ b/copycat/slipnode.py @@ -0,0 +1,140 @@ +import math + + +def jump_threshold(): + return 55.0 + + +class Slipnode(object): + # pylint: disable=too-many-instance-attributes + def __init__(self, slipnet, name, depth, length=0.0): + self.slipnet = slipnet + self.name = name + self.conceptualDepth = depth + self.intrinsicLinkLength = length + self.shrunkLinkLength = length * 0.4 + + self.activation = 0.0 + self.buffer = 0.0 + self.clamped = False + self.categoryLinks = [] + self.instanceLinks = [] + self.propertyLinks = [] + self.lateralSlipLinks = [] + self.lateralNonSlipLinks = [] + self.incomingLinks = [] + self.outgoingLinks = [] + self.codelets = [] + + def __repr__(self): + return '' % self.name + + def reset(self): + self.buffer = 0.0 + self.activation = 0.0 + + def clampHigh(self): + self.clamped = True + self.activation = 100.0 + + def unclamp(self): + self.clamped = False + + def unclamped(self): + return not self.clamped + + def category(self): + if not len(self.categoryLinks): + return None + link = self.categoryLinks[0] + return link.destination + + def fully_active(self): + """Whether this node has full activation""" + float_margin = 0.00001 + return self.activation > 100.0 - float_margin + + def bondDegreeOfAssociation(self): + linkLength = self.intrinsicLinkLength + if self.fully_active(): + linkLength = self.shrunkLinkLength + result = math.sqrt(100 - linkLength) * 11.0 + return min(100.0, result) + + def degreeOfAssociation(self): + linkLength = self.intrinsicLinkLength + if self.fully_active(): + linkLength = self.shrunkLinkLength + return 100.0 - linkLength + + def linked(self, other): + """Whether the other is among the outgoing links""" + return any(l.points_at(other) for l in self.outgoingLinks) + + def slipLinked(self, other): + """Whether the other is among the lateral links""" + return any(l.points_at(other) for l in self.lateralSlipLinks) + + def related(self, other): + """Same or linked""" + return self == other or self.linked(other) + + def applySlippages(self, slippages): + for slippage in slippages: + if self == slippage.initialDescriptor: + return slippage.targetDescriptor + return self + + def getRelatedNode(self, relation): + """Return the node that is linked to this node via this relation. + + If no linked node is found, return None + """ + slipnet = self.slipnet + if relation == slipnet.identity: + return self + destinations = [l.destination + for l in self.outgoingLinks if l.label == relation] + if destinations: + return destinations[0] + return None + + def getBondCategory(self, destination): + """Return the label of the link between these nodes if it exists. + + If it does not exist return None + """ + slipnet = self.slipnet + if self == destination: + return slipnet.identity + else: + for link in self.outgoingLinks: + if link.destination == destination: + return link.label + return None + + def update(self): + self.oldActivation = self.activation + self.buffer -= self.activation * (100.0 - self.conceptualDepth) / 100.0 + + def spread_activation(self): + if self.fully_active(): + for link in self.outgoingLinks: + link.spread_activation() + + def addBuffer(self): + if self.unclamped(): + self.activation += self.buffer + self.activation = min(max(0, self.activation), 100) + + def jump(self, random): + if self.clamped or self.activation <= jump_threshold(): + return + value = (self.activation / 100.0) ** 3 + if random.coinFlip(value): + self.activation = 100.0 + + def get_name(self): + if len(self.name) == 1: + return self.name.upper() + return self.name diff --git a/copycat/slipnode_README.md b/copycat/slipnode_README.md new file mode 100644 index 0000000..e833d25 --- /dev/null +++ b/copycat/slipnode_README.md @@ -0,0 +1,54 @@ +# Slipnode System + +## Overview +The slipnode system is a fundamental component of the Copycat architecture that implements the nodes in the slipnet. Slipnodes represent concepts and their relationships in the conceptual network, forming the basis for analogical reasoning. + +## Key Features +- Concept representation +- Activation management +- Link formation +- State tracking +- Event handling + +## Node Types +1. **Concept Nodes** + - Letter concepts + - Number concepts + - Relationship concepts + - Category concepts + +2. **Structural Nodes** + - Group concepts + - Bond concepts + - Hierarchy concepts + - Pattern concepts + +3. **Special Nodes** + - Rule nodes + - Mapping nodes + - Context nodes + - Meta-concept nodes + +## Usage +Slipnodes are created and managed through the slipnet system: + +```python +# Create a new slipnode +node = Slipnode(name, initial_activation) + +# Access node properties +activation = node.get_activation() + +# Modify node state +node.set_activation(new_value) +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Slipnet: Main container for slipnodes +- Sliplinks: Connect slipnodes +- Codelets: Operate on slipnodes +- Workspace: Provides context for node activation \ No newline at end of file diff --git a/copycat/statistics.py b/copycat/statistics.py new file mode 100644 index 0000000..bd50e83 --- /dev/null +++ b/copycat/statistics.py @@ -0,0 +1,128 @@ +from collections import defaultdict +from pprint import pprint +from math import log + +# comparison values for n degrees freedom +# These values are useable for both the chi^2 and G tests + +_ptable = { + 1:3.841, + 2:5.991, + 3:7.815, + 4:9.488, + 5:11.071, + 6:12.592, + 7:14.067, + 8:15.507, + 9:16.919, + 10:18.307, + 11:19.7, + 12:21, + 13:22.4, + 14:23.7, + 15:25, + 16:26.3 + } + + +_get_count = lambda k, d : d[k]['count'] if k in d else 0 + +def g_value(actual, expected): + # G = 2 * sum(Oi * ln(Oi/Ei)) + answerKeys = set(list(actual.keys()) + list(expected.keys())) + degreesFreedom = len(answerKeys) + G = 0 + + for k in answerKeys: + E = _get_count(k, expected) + O = _get_count(k, actual) + if E == 0: + print(' Warning! Expected 0 counts of {}, but got {}'.format(k, O)) + elif O == 0: + print(' Warning! O = {}'.format(O)) + else: + G += O * log(O/E) + G *= 2 + return degreesFreedom, G + +def chi_value(actual, expected): + answerKeys = set(list(actual.keys()) + list(expected.keys())) + degreesFreedom = len(answerKeys) + chiSquared = 0 + + for k in answerKeys: + E = _get_count(k, expected) + O = _get_count(k, actual) + if E == 0: + print(' Warning! Expected 0 counts of {}, but got {}'.format(k, O)) + else: + chiSquared += (O - E) ** 2 / E + return degreesFreedom, chiSquared + +def probability_difference(actual, expected): + actualC = 0 + expectedC = 0 + + for k in set(list(actual.keys()) + list(expected.keys())): + expectedC += _get_count(k, expected) + actualC += _get_count(k, actual) + + p = 0 + + Et = 0 + Ot = 0 + + for k in set(list(actual.keys()) + list(expected.keys())): + E = _get_count(k, expected) + O = _get_count(k, actual) + Ep = E / expectedC + Op = O / actualC + p += abs(Ep - Op) + + p /= 2 # P is between 0 and 2 -> P is between 0 and 1 + + return p + +def dist_test(actual, expected, calculation): + df, p = calculation(actual, expected) + if df not in _ptable: + raise Exception('{} degrees of freedom does not have a corresponding chi squared value.' + \ + ' Please look up the value and add it to the table in copycat/statistics.py'.format(df)) + return (p < _ptable[df]) + +def cross_formula_table(actualDict, expectedDict, calculation, probs=False): + data = dict() + for ka, actual in actualDict.items(): + for ke, expected in expectedDict.items(): + if probs: + data[(ka, ke)] = probability_difference(actual, expected) + else: + data[(ka, ke)] = dist_test(actual, expected, calculation) + return data + +def cross_table(problemSets, calculation=g_value, probs=False): + table = defaultdict(dict) + for i, (a, problemSetA) in enumerate(problemSets): + for b, problemSetB in problemSets[i + 1:]: + for problemA in problemSetA: + for problemB in problemSetB: + if (problemA.initial == problemB.initial and + problemA.modified == problemB.modified and + problemA.target == problemB.target): + answersA = problemA.distributions + answersB = problemB.distributions + table[(problemA.initial, + problemA.modified, + problemA.target)][(a, b)] = ( + cross_formula_table( + answersA, answersB, calculation, probs)) + return table + +def iso_chi_squared(actualDict, expectedDict): + for key in expectedDict.keys(): + assert key in actualDict, 'The key {} was not tested'.format(key) + actual = actualDict[key] + expected = expectedDict[key] + if not dist_test(actual, expected, g_value): + raise Exception('Value of G higher than expected') + diff --git a/copycat/statistics_README.md b/copycat/statistics_README.md new file mode 100644 index 0000000..21abdbe --- /dev/null +++ b/copycat/statistics_README.md @@ -0,0 +1,54 @@ +# Statistics System + +## Overview +The statistics system is a utility component of the Copycat architecture that collects and analyzes various metrics and statistics about the system's operation. This system helps in understanding and debugging the analogical reasoning process. + +## Key Features +- Performance metrics collection +- Statistical analysis +- Data aggregation +- Reporting utilities +- Debugging support + +## Metric Types +1. **Performance Metrics** + - Codelet execution counts + - Activation levels + - Mapping strengths + - Processing times + +2. **System Metrics** + - Memory usage + - Object counts + - Link counts + - State changes + +3. **Analysis Metrics** + - Success rates + - Error rates + - Pattern frequencies + - Relationship distributions + +## Usage +Statistics are collected and analyzed through the statistics system: + +```python +# Record a statistic +statistics.record_metric('metric_name', value) + +# Get statistical analysis +analysis = statistics.analyze_metric('metric_name') + +# Generate report +report = statistics.generate_report() +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Coderack: Provides execution metrics +- Workspace: Provides object statistics +- Slipnet: Provides activation statistics +- Correspondence: Provides mapping statistics \ No newline at end of file diff --git a/copycat/temperature.py b/copycat/temperature.py new file mode 100644 index 0000000..9941e70 --- /dev/null +++ b/copycat/temperature.py @@ -0,0 +1,175 @@ +import math + +# Alternate formulas for getAdjustedProbability + +def _original(temp, prob): + if prob == 0 or prob == 0.5 or temp == 0: + return prob + if prob < 0.5: + return 1.0 - _original(temp, 1.0 - prob) + coldness = 100.0 - temp + a = math.sqrt(coldness) + c = (10 - a) / 100 + f = (c + 1) * prob + return max(f, 0.5) + +def _entropy(temp, prob): + if prob == 0 or prob == 0.5 or temp == 0: + return prob + if prob < 0.5: + return 1.0 - _original(temp, 1.0 - prob) + coldness = 100.0 - temp + a = math.sqrt(coldness) + c = (10 - a) / 100 + f = (c + 1) * prob + return -f * math.log2(f) + +def _weighted(temp, s, u): + weighted = (temp / 100) * s + ((100 - temp) / 100) * u + return weighted + +def _weighted_inverse(temp, prob): + iprob = 1 - prob + return _weighted(temp, iprob, prob) + +def _fifty_converge(temp, prob): # Uses .5 instead of 1-prob + return _weighted(temp, .5, prob) + +def _soft_curve(temp, prob): # Curves to the average of the (1-p) and .5 + return min(1, _weighted(temp, (1.5-prob)/2, prob)) + +def _weighted_soft_curve(temp, prob): # Curves to the weighted average of the (1-p) and .5 + weight = 100 + gamma = .5 # convergance value + alpha = 1 # gamma weight + beta = 3 # iprob weight + curved = min(1, (temp / weight) * ((alpha * gamma + beta * (1 - prob)) / (alpha + beta)) + ((weight - temp) / weight) * prob) + return curved + +def _alt_fifty(temp, prob): + s = .5 + u = prob ** 2 if prob < .5 else math.sqrt(prob) + return _weighted(temp, s, u) + +def _averaged_alt(temp, prob): + s = (1.5 - prob)/2 + u = prob ** 2 if prob < .5 else math.sqrt(prob) + return _weighted(temp, s, u) + + +def _working_best(temp, prob): + s = .5 # convergence + r = 1.05 # power + u = prob ** r if prob < .5 else prob ** (1/r) + return _weighted(temp, s, u) + +def _soft_best(temp, prob): + s = .5 # convergence + r = 1.05 # power + u = prob ** r if prob < .5 else prob ** (1/r) + return _weighted(temp, s, u) + +def _parameterized_best(temp, prob): + # (D$66/100)*($E$64*$B68 + $G$64*$F$64)/($E$64 + $G$64)+((100-D$66)/100)*IF($B68 > 0.5, $B68^(1/$H$64), $B68^$H$64) + # (T/100) * (alpha * p + beta * .5) / (alpha + beta) + ((100 - T)/100) * IF(p > .5, p^(1/2), p^2) + alpha = 5 + beta = 1 + s = .5 + s = (alpha * prob + beta * s) / (alpha + beta) + r = 1.05 + u = prob ** r if prob < .5 else prob ** (1/r) + return _weighted(temp, s, u) + +def _meta(temp, prob): + r = _weighted(temp, 1, 2) # Make r a function of temperature + s = .5 + u = prob ** r if prob < .5 else prob ** (1/r) + return _weighted(temp, s, u) + +def _meta_parameterized(temp, prob): + r = _weighted(temp, 1, 2) # Make r a function of temperature + + alpha = 5 + beta = 1 + s = .5 + s = (alpha * prob + beta * s) / (alpha + beta) + u = prob ** r if prob < .5 else prob ** (1/r) + + return _weighted(temp, s, u) + +def _none(temp, prob): + return prob + +class Temperature(object): + def __init__(self): + self.reset() + self.adjustmentType = 'inverse' + self._adjustmentFormulas = { + 'original' : _original, + 'entropy' : _entropy, + 'inverse' : _weighted_inverse, + 'fifty_converge' : _fifty_converge, + 'soft' : _soft_curve, + 'weighted_soft' : _weighted_soft_curve, + 'alt_fifty' : _alt_fifty, + 'average_alt' : _averaged_alt, + 'best' : _working_best, + 'sbest' : _soft_best, + 'pbest' : _parameterized_best, + 'meta' : _meta, + 'pmeta' : _meta_parameterized, + 'none' : _none} + self.diffs = 0 + self.ndiffs = 0 + + def reset(self): + self.history = [100.0] + self.actual_value = 100.0 + self.last_unclamped_value = 100.0 + self.clamped = True + self.clampTime = 30 + + def update(self, value): + self.last_unclamped_value = value + if self.clamped: + self.actual_value = 100.0 + else: + self.history.append(value) + self.actual_value = value + + def clampUntil(self, when): + self.clamped = True + self.clampTime = when + # but do not modify self.actual_value until someone calls update() + + def tryUnclamp(self, currentTime): + if self.clamped and currentTime >= self.clampTime: + self.clamped = False + + def value(self): + return 100.0 if self.clamped else self.actual_value + + def getAdjustedValue(self, value): + return value ** (((100.0 - self.value()) / 30.0) + 0.5) + + def getAdjustedProbability(self, value): + temp = self.value() + prob = value + adjusted = self._adjustmentFormulas[self.adjustmentType](temp, prob) + + self.diffs += abs(adjusted - prob) + self.ndiffs += 1 + return adjusted + + def getAverageDifference(self): + return self.diffs / self.ndiffs + + def useAdj(self, adj): + print('Changing to adjustment formula {}'.format(adj)) + self.adjustmentType = adj + + def getAdj(self): + return self.adjustmentType + + def adj_formulas(self): + return self._adjustmentFormulas.keys() diff --git a/copycat/temperature_README.md b/copycat/temperature_README.md new file mode 100644 index 0000000..6f7a749 --- /dev/null +++ b/copycat/temperature_README.md @@ -0,0 +1,51 @@ +# README_temperature.md + +## Overview +`temperature.py` implements the Temperature system, a crucial component of the Copycat system that controls the balance between exploration and exploitation during analogical reasoning. It provides various formulas for adjusting probabilities based on the current temperature, which affects how the system makes decisions. + +## Core Components +- `Temperature` class: Main class that manages the temperature system +- Multiple adjustment formulas for probability modification +- Temperature history tracking + +## Key Features +- Manages temperature-based control of the reasoning process +- Provides multiple formulas for probability adjustment +- Supports temperature clamping and unclamping +- Tracks temperature history and differences +- Allows dynamic switching between adjustment formulas + +## Adjustment Formulas +- `original`: Basic temperature-based adjustment +- `entropy`: Entropy-based adjustment +- `inverse`: Inverse weighted adjustment +- `fifty_converge`: Convergence to 0.5 +- `soft_curve`: Soft curve adjustment +- `weighted_soft_curve`: Weighted soft curve +- `alt_fifty`: Alternative fifty convergence +- `average_alt`: Averaged alternative +- `best`: Working best formula +- `sbest`: Soft best formula +- `pbest`: Parameterized best +- `meta`: Meta-level adjustment +- `pmeta`: Parameterized meta +- `none`: No adjustment + +## Main Methods +- `update(value)`: Update temperature value +- `clampUntil(when)`: Clamp temperature until specified time +- `tryUnclamp(currentTime)`: Attempt to unclamp temperature +- `value()`: Get current temperature +- `getAdjustedProbability(value)`: Get temperature-adjusted probability +- `useAdj(adj)`: Switch to different adjustment formula + +## Dependencies +- Uses `math` for mathematical operations +- Used by the main `copycat` module + +## Notes +- Temperature starts at 100.0 and can be clamped +- Lower temperatures encourage more conservative decisions +- Higher temperatures allow more exploratory behavior +- The system tracks average differences between adjusted and original probabilities +- Different adjustment formulas can be used for different reasoning strategies \ No newline at end of file diff --git a/copycat/workspace.py b/copycat/workspace.py new file mode 100644 index 0000000..2eee178 --- /dev/null +++ b/copycat/workspace.py @@ -0,0 +1,195 @@ +"""Workspace module.""" + + +from . import formulas +from .bond import Bond +from .correspondence import Correspondence +from .letter import Letter +from .workspaceString import WorkspaceString + + +def __adjustUnhappiness(values): + result = sum(values) / 2 + if result > 100.0: + result = 100.0 + return result + + +class Workspace(object): + def __init__(self, ctx): + """To initialize the workspace.""" + self.ctx = ctx + self.totalUnhappiness = 0.0 + self.intraStringUnhappiness = 0.0 + self.interStringUnhappiness = 0.0 + + # LSaldyt: default initializations for GUI entry + self.targetString = '' + self.initialString = '' + self.modifiedString = '' + self.finalAnswer = None + self.changedObject = None + self.objects = [] + self.structures = [] + self.rule = None + + def __repr__(self): + return '' % ( + self.initialString, self.modifiedString, self.targetString, + ) + + def resetWithStrings(self, initial, modified, target): + self.targetString = target + self.initialString = initial + self.modifiedString = modified + self.reset() + + def reset(self): + self.finalAnswer = None + self.changedObject = None + self.objects = [] + self.structures = [] + self.rule = None # Only one rule? : LSaldyt + self.initial = WorkspaceString(self.ctx, self.initialString) + self.modified = WorkspaceString(self.ctx, self.modifiedString) + self.target = WorkspaceString(self.ctx, self.targetString) + + ''' + # TODO: Initial part of refactoring in this method + def getAssessedUnhappiness(self, unhappiness): + o.Unhappiness = __adjustUnhappiness( + o.relativeImportance * o.Unhappiness + for o in self.objects) + pass + ''' + + # TODO: Extract method? + def assessUnhappiness(self): + self.intraStringUnhappiness = __adjustUnhappiness( + o.relativeImportance * o.intraStringUnhappiness + for o in self.objects) + self.interStringUnhappiness = __adjustUnhappiness( + o.relativeImportance * o.interStringUnhappiness + for o in self.objects) + self.totalUnhappiness = __adjustUnhappiness( + o.relativeImportance * o.totalUnhappiness + for o in self.objects) + + # TODO: these 3 methods seem to be the same... are they? If so, Extract method. + def calculateIntraStringUnhappiness(self): + value = sum( + o.relativeImportance * o.intraStringUnhappiness + for o in self.objects + ) / 2.0 + self.intraStringUnhappiness = min(value, 100.0) + + def calculateInterStringUnhappiness(self): + value = sum( + o.relativeImportance * o.interStringUnhappiness + for o in self.objects + ) / 2.0 + self.interStringUnhappiness = min(value, 100.0) + + def calculateTotalUnhappiness(self): + value = sum( + o.relativeImportance * o.totalUnhappiness + for o in self.objects + ) / 2.0 + self.totalUnhappiness = min(value, 100.0) + + def updateEverything(self): + for structure in self.structures: + structure.updateStrength() + for obj in self.objects: + obj.updateValue() + self.initial.updateRelativeImportance() + self.target.updateRelativeImportance() + self.initial.updateIntraStringUnhappiness() + self.target.updateIntraStringUnhappiness() + + # TODO: use entropy + def getUpdatedTemperature(self): + ''' + Calculation of global tolerance towards irrelevance + ''' + self.calculateIntraStringUnhappiness() + self.calculateInterStringUnhappiness() + self.calculateTotalUnhappiness() + if self.rule: + self.rule.updateStrength() + ruleWeakness = 100.0 - self.rule.totalStrength + else: + ruleWeakness = 100.0 + return formulas.weightedAverage(( + (self.totalUnhappiness, 0.8), + (ruleWeakness, 0.2) + )) + + def numberOfUnrelatedObjects(self): + """Computes the number of all objects in the workspace with >= 1 open bond slots.""" + objects = [o for o in self.objects + if o.string == self.initial or o.string == self.target] + objects = [o for o in objects if not o.spansString()] + objects = [o for o in objects + if (not o.leftBond and not o.leftmost) or + (not o.rightBond and not o.rightmost)] + return len(objects) + + def numberOfUngroupedObjects(self): + """A list of all objects in the workspace that have no group.""" + objects = [o for o in self.objects if + o.string == self.initial or o.string == self.target] + objects = [o for o in objects if not o.spansString()] + objects = [o for o in objects if not o.group] + return len(objects) + + def numberOfUnreplacedObjects(self): + """A list of all unreplaced objects in the initial string.""" + objects = [o for o in self.objects + if o.string == self.initial and isinstance(o, Letter)] + objects = [o for o in objects if not o.replacement] + return len(objects) + + def numberOfUncorrespondingObjects(self): + """A list of all uncorresponded objects in the initial string.""" + objects = [o for o in self.objects + if o.string == self.initial or o.string == self.target] + objects = [o for o in objects if not o.correspondence] + return len(objects) + + def numberOfBonds(self): + """The number of bonds in the workspace.""" + return sum(1 for o in self.structures if isinstance(o, Bond)) + + def correspondences(self): + return [s for s in self.structures if isinstance(s, Correspondence)] + + def slippages(self): + result = [] + if self.changedObject and self.changedObject.correspondence: + result += self.changedObject.correspondence.conceptMappings + for o in self.initial.objects: + if o.correspondence: + for mapping in o.correspondence.slippages(): + if not mapping.isNearlyContainedBy(result): + result += [mapping] + return result + + def buildRule(self, rule): + if self.rule is not None: + self.structures.remove(self.rule) + self.rule = rule + self.structures += [rule] + rule.activateRuleDescriptions() + + def breakRule(self): + if self.rule is not None: + self.structures.remove(self.rule) + self.rule = None + + def buildDescriptions(self, objekt): + for description in objekt.descriptions: + description.descriptionType.buffer = 100.0 + description.descriptor.buffer = 100.0 + if description not in self.structures: + self.structures += [description] diff --git a/copycat/workspaceFormulas.py b/copycat/workspaceFormulas.py new file mode 100644 index 0000000..8f97cb3 --- /dev/null +++ b/copycat/workspaceFormulas.py @@ -0,0 +1,38 @@ + +def __chooseObjectFromList(ctx, objects, attribute): + # TODO: use entropy + random = ctx.random + temperature = ctx.temperature + weights = [ + temperature.getAdjustedValue( + getattr(o, attribute) + ) + for o in objects + ] + return random.weighted_choice(objects, weights) + + +def chooseUnmodifiedObject(ctx, attribute, inObjects): + workspace = ctx.workspace + objects = [o for o in inObjects if o.string != workspace.modified] + return __chooseObjectFromList(ctx, objects, attribute) + + +def chooseNeighbor(ctx, source): + workspace = ctx.workspace + objects = [o for o in workspace.objects if o.beside(source)] + return __chooseObjectFromList(ctx, objects, "intraStringSalience") + + +def chooseDirectedNeighbor(ctx, source, direction): + slipnet = ctx.slipnet + workspace = ctx.workspace + if direction == slipnet.left: + objects = [o for o in workspace.objects + if o.string == source.string + and source.leftIndex == o.rightIndex + 1] + else: + objects = [o for o in workspace.objects + if o.string == source.string + and o.leftIndex == source.rightIndex + 1] + return __chooseObjectFromList(ctx, objects, 'intraStringSalience') diff --git a/copycat/workspaceFormulas_README.md b/copycat/workspaceFormulas_README.md new file mode 100644 index 0000000..b206323 --- /dev/null +++ b/copycat/workspaceFormulas_README.md @@ -0,0 +1,54 @@ +# Workspace Formulas System + +## Overview +The workspace formulas system is a utility component of the Copycat architecture that provides mathematical and logical formulas for workspace operations. This system implements various calculations and transformations used in the analogical reasoning process. + +## Key Features +- Mathematical formulas +- Logical operations +- Transformation rules +- Calculation utilities +- State management + +## Formula Types +1. **Mathematical Formulas** + - Distance calculations + - Similarity measures + - Probability computations + - Activation formulas + +2. **Logical Formulas** + - Boolean operations + - Condition evaluations + - Rule applications + - Constraint checks + +3. **Transformation Formulas** + - Object transformations + - State transitions + - Value mappings + - Structure modifications + +## Usage +Formulas are used throughout the workspace system: + +```python +# Apply a formula +result = workspace_formulas.calculate_distance(obj1, obj2) + +# Evaluate a condition +is_valid = workspace_formulas.check_condition(condition) + +# Transform an object +transformed = workspace_formulas.apply_transformation(obj) +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Workspace: Uses formulas for operations +- WorkspaceObject: Provides objects for calculations +- Codelets: Apply formulas in operations +- Statistics: Uses formulas for metrics \ No newline at end of file diff --git a/copycat/workspaceObject.py b/copycat/workspaceObject.py new file mode 100644 index 0000000..9096861 --- /dev/null +++ b/copycat/workspaceObject.py @@ -0,0 +1,194 @@ +from .description import Description +from .formulas import weightedAverage +from .workspaceStructure import WorkspaceStructure + +class WorkspaceObject(WorkspaceStructure): + # pylint: disable=too-many-instance-attributes + def __init__(self, workspaceString): + WorkspaceStructure.__init__(self, workspaceString.ctx) + self.string = workspaceString + self.descriptions = [] + self.bonds = [] + self.group = None + self.changed = False + self.correspondence = None + self.rawImportance = 0.0 + self.relativeImportance = 0.0 + self.leftBond = None + self.rightBond = None + self.name = '' + self.replacement = None + self.rightIndex = 0 + self.leftIndex = 0 + self.leftmost = False + self.rightmost = False + self.intraStringSalience = 0.0 + self.interStringSalience = 0.0 + self.totalSalience = 0.0 + self.intraStringUnhappiness = 0.0 + self.interStringUnhappiness = 0.0 + self.totalUnhappiness = 0.0 + + def __str__(self): + return 'object' + + def spansString(self): + return self.leftmost and self.rightmost + + def addDescription(self, descriptionType, descriptor): + description = Description(self, descriptionType, descriptor) + self.descriptions += [description] + + def addDescriptions(self, descriptions): + workspace = self.ctx.workspace + copy = descriptions[:] # in case we add to our own descriptions + for description in copy: + if not self.containsDescription(description): + self.addDescription(description.descriptionType, + description.descriptor) + workspace.buildDescriptions(self) + + def __calculateIntraStringHappiness(self): + if self.spansString(): + return 100.0 + if self.group: + return self.group.totalStrength + bondStrength = sum(bond.totalStrength for bond in self.bonds) + return bondStrength / 6.0 + + def __calculateRawImportance(self): + """Calculate the raw importance of this object. + + Which is the sum of all relevant descriptions""" + result = 0.0 + for description in self.descriptions: + if description.descriptionType.fully_active(): + result += description.descriptor.activation + else: + result += description.descriptor.activation / 20.0 + if self.group: + result *= 2.0 / 3.0 + if self.changed: + result *= 2.0 + return result + + def updateValue(self): + self.rawImportance = self.__calculateRawImportance() + intraStringHappiness = self.__calculateIntraStringHappiness() + self.intraStringUnhappiness = 100.0 - intraStringHappiness + + interStringHappiness = 0.0 + if self.correspondence: + interStringHappiness = self.correspondence.totalStrength + self.interStringUnhappiness = 100.0 - interStringHappiness + + averageHappiness = (intraStringHappiness + interStringHappiness) / 2 + self.totalUnhappiness = 100.0 - averageHappiness + + self.intraStringSalience = weightedAverage(( + (self.relativeImportance, 0.2), + (self.intraStringUnhappiness, 0.8) + )) + self.interStringSalience = weightedAverage(( + (self.relativeImportance, 0.8), + (self.interStringUnhappiness, 0.2) + )) + self.totalSalience = (self.intraStringSalience + self.interStringSalience) / 2.0 + + def isWithin(self, other): + return (self.leftIndex >= other.leftIndex and + self.rightIndex <= other.rightIndex) + + def isOutsideOf(self, other): + return (self.leftIndex > other.rightIndex or + self.rightIndex < other.leftIndex) + + def relevantDescriptions(self): + return [d for d in self.descriptions + if d.descriptionType.fully_active()] + + def getPossibleDescriptions(self, descriptionType): + from .group import Group # gross, TODO FIXME + slipnet = self.ctx.slipnet + descriptions = [] + for link in descriptionType.instanceLinks: + node = link.destination + if node == slipnet.first and self.described(slipnet.letters[0]): + descriptions += [node] + if node == slipnet.last and self.described(slipnet.letters[-1]): + descriptions += [node] + for i, number in enumerate(slipnet.numbers, 1): + if node == number and isinstance(self, Group): + if len(self.objectList) == i: + descriptions += [node] + if node == slipnet.middle and self.middleObject(): + descriptions += [node] + return descriptions + + def containsDescription(self, sought): + soughtType = sought.descriptionType + soughtDescriptor = sought.descriptor + for d in self.descriptions: + if soughtType == d.descriptionType: + if soughtDescriptor == d.descriptor: + return True + return False + + def described(self, slipnode): + return any(d.descriptor == slipnode for d in self.descriptions) + + def middleObject(self): + # only works if string is 3 chars long + # as we have access to the string, why not just " == len / 2" ? + objectOnMyRightIsRightmost = objectOnMyLeftIsLeftmost = False + for objekt in self.string.objects: + if objekt.leftmost and objekt.rightIndex == self.leftIndex - 1: + objectOnMyLeftIsLeftmost = True + if objekt.rightmost and objekt.leftIndex == self.rightIndex + 1: + objectOnMyRightIsRightmost = True + return objectOnMyRightIsRightmost and objectOnMyLeftIsLeftmost + + def distinguishingDescriptor(self, descriptor): + slipnet = self.ctx.slipnet + return slipnet.isDistinguishingDescriptor(descriptor) + + def relevantDistinguishingDescriptors(self): + slipnet = self.ctx.slipnet + return [d.descriptor + for d in self.relevantDescriptions() + if slipnet.isDistinguishingDescriptor(d.descriptor)] + + def getDescriptor(self, descriptionType): + """The description attached to this object of the description type.""" + for description in self.descriptions: + if description.descriptionType == descriptionType: + return description.descriptor + return None + + def getDescriptionType(self, sought_description): + """The description_type attached to this object of that description""" + for description in self.descriptions: + if description.descriptor == sought_description: + return description.descriptionType + return None + + def getCommonGroups(self, other): + return [o for o in self.string.objects + if self.isWithin(o) and other.isWithin(o)] + + def letterDistance(self, other): + if other.leftIndex > self.rightIndex: + return other.leftIndex - self.rightIndex + if self.leftIndex > other.rightIndex: + return self.leftIndex - other.rightIndex + return 0 + + def letterSpan(self): + return self.rightIndex - self.leftIndex + 1 + + def beside(self, other): + if self.string != other.string: + return False + if self.leftIndex == other.rightIndex + 1: + return True + return other.leftIndex == self.rightIndex + 1 diff --git a/copycat/workspaceObject_README.md b/copycat/workspaceObject_README.md new file mode 100644 index 0000000..c729b62 --- /dev/null +++ b/copycat/workspaceObject_README.md @@ -0,0 +1,62 @@ +# Workspace Object System + +## Overview +The workspace object system is a fundamental component of the Copycat architecture that manages the representation and manipulation of objects in the workspace. The system consists of several related files: + +- `workspaceObject.py`: Core implementation of workspace objects +- `workspaceString.py`: String-specific object implementations +- `workspaceStructure.py`: Structural object implementations +- `workspaceFormulas.py`: Formula and calculation utilities + +## Workspace Objects +Workspace objects are the basic building blocks of the workspace, representing: +- Letters and characters +- Groups and collections +- Bonds and relationships +- Structural elements + +## Key Features +- Object hierarchy and inheritance +- Property management and access +- Relationship tracking +- State management +- Event handling + +## Object Types +1. **Basic Objects** + - Letters and characters + - Numbers and symbols + - Special markers + +2. **Structural Objects** + - Groups and collections + - Bonds and connections + - Hierarchical structures + +3. **Composite Objects** + - Formulas and expressions + - Complex relationships + - Derived objects + +## Usage +Workspace objects are created and managed through the workspace system: + +```python +# Create a new workspace object +obj = WorkspaceObject(properties) + +# Access object properties +value = obj.get_property('property_name') + +# Modify object state +obj.set_property('property_name', new_value) +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Workspace: Main container for workspace objects +- Codelets: Operate on workspace objects +- Slipnet: Provides conceptual knowledge for object interpretation \ No newline at end of file diff --git a/copycat/workspaceString.py b/copycat/workspaceString.py new file mode 100644 index 0000000..a57c218 --- /dev/null +++ b/copycat/workspaceString.py @@ -0,0 +1,62 @@ +from .group import Group +from .letter import Letter + + +class WorkspaceString(object): + def __init__(self, ctx, s): + slipnet = ctx.slipnet + workspace = ctx.workspace + self.ctx = ctx + self.string = s + self.bonds = [] + self.objects = [] + self.letters = [] + self.length = len(s) + self.intraStringUnhappiness = 0.0 + + for position, c in enumerate(self.string.upper(), 1): + value = ord(c) - ord('A') + letter = Letter(self, position, self.length) + letter.workspaceString = self + letter.addDescription(slipnet.objectCategory, slipnet.letter) + letter.addDescription(slipnet.letterCategory, slipnet.letters[value]) + letter.describe(position, self.length) + workspace.buildDescriptions(letter) + self.letters += [letter] + + def __repr__(self): + return '' % self.string + + def __str__(self): + return '%s with %d letters, %d objects, %d bonds' % ( + self.string, len(self.letters), len(self.objects), len(self.bonds)) + + def __len__(self): + return len(self.string) + + def __getitem__(self, i): + return self.string[i] + + def updateRelativeImportance(self): + """Update the normalised importance of all objects in the string.""" + total = sum(o.rawImportance for o in self.objects) + if not total: + for o in self.objects: + o.relativeImportance = 0.0 + else: + for o in self.objects: + o.relativeImportance = o.rawImportance / total + + def updateIntraStringUnhappiness(self): + if not len(self.objects): + self.intraStringUnhappiness = 0.0 + return + total = sum(o.intraStringUnhappiness for o in self.objects) + self.intraStringUnhappiness = total / len(self.objects) + + def equivalentGroup(self, sought): + for objekt in self.objects: + if isinstance(objekt, Group): + if objekt.sameGroup(sought): + return objekt + return None diff --git a/copycat/workspaceString_README.md b/copycat/workspaceString_README.md new file mode 100644 index 0000000..4c85a3d --- /dev/null +++ b/copycat/workspaceString_README.md @@ -0,0 +1,54 @@ +# Workspace String System + +## Overview +The workspace string system is a specialized component of the Copycat architecture that handles string representations and operations in the workspace. This system manages the manipulation and analysis of string-based objects in the analogical reasoning process. + +## Key Features +- String representation +- String manipulation +- Pattern matching +- String analysis +- State management + +## String Types +1. **Basic Strings** + - Character sequences + - Word strings + - Symbol strings + - Special markers + +2. **Structured Strings** + - Grouped strings + - Bonded strings + - Hierarchical strings + - Pattern strings + +3. **Special Strings** + - Rule strings + - Mapping strings + - Context strings + - Meta-strings + +## Usage +String operations are performed through the workspace string system: + +```python +# Create a workspace string +string = WorkspaceString(content) + +# Access string properties +length = string.get_length() + +# Modify string state +string.set_content(new_content) +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Workspace: Contains string objects +- WorkspaceObject: Base class for strings +- Codelets: Operate on strings +- Correspondence: Maps between strings \ No newline at end of file diff --git a/copycat/workspaceStructure.py b/copycat/workspaceStructure.py new file mode 100644 index 0000000..2832202 --- /dev/null +++ b/copycat/workspaceStructure.py @@ -0,0 +1,35 @@ +from . import formulas + + +class WorkspaceStructure(object): + def __init__(self, ctx): + self.ctx = ctx + self.string = None + self.internalStrength = 0.0 + self.externalStrength = 0.0 + self.totalStrength = 0.0 + + def updateStrength(self): + self.updateInternalStrength() + self.updateExternalStrength() + self.updateTotalStrength() + + def updateTotalStrength(self): + """Recalculate the strength from internal and external strengths""" + weights = ((self.internalStrength, self.internalStrength), + (self.externalStrength, 100 - self.internalStrength)) + strength = formulas.weightedAverage(weights) + self.totalStrength = strength + + def totalWeakness(self): + return 100 - self.totalStrength ** 0.95 + + def updateInternalStrength(self): + raise NotImplementedError() + + def updateExternalStrength(self): + raise NotImplementedError() + + def break_the_structure(self): + """Break this workspace structure (Bond, Correspondence, or Group)""" + raise NotImplementedError() diff --git a/copycat/workspaceStructure_README.md b/copycat/workspaceStructure_README.md new file mode 100644 index 0000000..0181269 --- /dev/null +++ b/copycat/workspaceStructure_README.md @@ -0,0 +1,54 @@ +# Workspace Structure System + +## Overview +The workspace structure system is a fundamental component of the Copycat architecture that manages the structural organization of objects in the workspace. This system handles the creation and management of hierarchical and relational structures that form the basis of analogical reasoning. + +## Key Features +- Structure representation +- Hierarchy management +- Relationship tracking +- Pattern recognition +- State management + +## Structure Types +1. **Basic Structures** + - Linear sequences + - Hierarchical trees + - Network graphs + - Pattern structures + +2. **Composite Structures** + - Group structures + - Bond structures + - Rule structures + - Mapping structures + +3. **Special Structures** + - Context structures + - Meta-structures + - Derived structures + - Temporary structures + +## Usage +Structures are created and managed through the workspace structure system: + +```python +# Create a new structure +structure = WorkspaceStructure(type, properties) + +# Access structure properties +children = structure.get_children() + +# Modify structure state +structure.add_child(child) +``` + +## Dependencies +- Python 3.x +- No external dependencies required + +## Related Components +- Workspace: Contains structures +- WorkspaceObject: Base class for structures +- Codelets: Operate on structures +- Correspondence: Maps between structures \ No newline at end of file diff --git a/copycat/workspace_README.md b/copycat/workspace_README.md new file mode 100644 index 0000000..6f9ff66 --- /dev/null +++ b/copycat/workspace_README.md @@ -0,0 +1,42 @@ +# README_workspace.md + +## Overview +`workspace.py` implements the Workspace, a central component of the Copycat system that manages the current state of the analogical reasoning process. It maintains the strings being compared, their objects, structures, and the overall state of the reasoning process. + +## Core Components +- `Workspace` class: Main class that manages the reasoning workspace +- `WorkspaceString` objects for initial, modified, and target strings +- Collection of objects and structures (bonds, correspondences, etc.) + +## Key Features +- Manages the three strings involved in the analogy (initial, modified, target) +- Tracks objects and their relationships +- Maintains structures like bonds, correspondences, and rules +- Calculates various unhappiness metrics +- Updates object values and structure strengths +- Manages the temperature-based control system + +## Main Methods +- `resetWithStrings(initial, modified, target)`: Initialize workspace with new strings +- `updateEverything()`: Update all objects and structures +- `getUpdatedTemperature()`: Calculate current temperature +- `buildRule(rule)`: Add or replace the current rule +- `breakRule()`: Remove the current rule +- `buildDescriptions(objekt)`: Add descriptions to an object + +## State Management +- Tracks total, intra-string, and inter-string unhappiness +- Maintains lists of objects and structures +- Manages the current rule and final answer +- Tracks changed objects and their correspondences + +## Dependencies +- Requires `formulas`, `bond`, `correspondence`, `letter`, and `workspaceString` modules +- Used by the main `copycat` module + +## Notes +- The workspace is the central state container for the reasoning process +- Unhappiness metrics guide the temperature-based control system +- The system supports both rule-based and correspondence-based reasoning +- Objects can have multiple descriptions and relationships +- The workspace maintains the final answer when reasoning is complete \ No newline at end of file diff --git a/cross_compare.py b/cross_compare.py new file mode 100755 index 0000000..ed36bc2 --- /dev/null +++ b/cross_compare.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +import sys +import pickle + +from pprint import pprint +from collections import defaultdict + +from copycat import Problem +from copycat.statistics import cross_table + +def compare_sets(): + pass + +def main(args): + branchProblemSets = dict() + problemSets = [] + for filename in args: + with open(filename, 'rb') as infile: + pSet = pickle.load(infile) + branchProblemSets[filename] = pSet + problemSets.append((filename, pSet)) + crossTable = cross_table(problemSets, probs=True) + key_sorted_items = lambda d : sorted(d.items(), key=lambda t:t[0]) + + tableItems = key_sorted_items(crossTable) + assert len(tableItems) > 0, 'Empty table' + + headKey, headSubDict = tableItems[0] + # Create table and add headers + table = [['source', 'compare', 'source formula', 'compare formula']] + for key, _ in tableItems: + problem = '{}:{}::{}:_'.format(*key) + table[-1].append(problem) + + # Arranged results in terms of copycat variants and formulas + arranged = defaultdict(list) + for key, subdict in tableItems: + for subkey, subsubdict in key_sorted_items(subdict): + for subsubkey, result in key_sorted_items(subsubdict): + arranged[subkey + subsubkey].append((key, result)) + + # Add test results to table + for key, results in arranged.items(): + table.append(list(map(str, [*key]))) + for _, result in results: + table[-1].append(str(result)) + + with open('output/cross_compare.csv', 'w') as outfile: + outfile.write('\n'.join(','.join(row) for row in table) + '\n') + return 0 + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/cross_compare_README.md b/cross_compare_README.md new file mode 100644 index 0000000..b563b72 --- /dev/null +++ b/cross_compare_README.md @@ -0,0 +1,37 @@ +# README_cross_compare.md + +## Overview +`cross_compare.py` is a utility script that performs cross-comparison analysis between different problem sets in the Copycat system. It reads pickled problem sets and generates a comparison table showing how different variants of the Copycat system perform across various problems. + +## Usage +Run the program from the terminal with the following command: +```bash +python cross_compare.py problem_set1.pkl problem_set2.pkl ... +``` + +### Arguments +- One or more pickled problem set files (`.pkl` files) to compare + +## Output +The script generates a CSV file at `output/cross_compare.csv` containing: +- A comparison table of different Copycat variants +- Problem formulas and their results +- Cross-comparison metrics between different problem sets + +## Features +- Reads multiple pickled problem sets +- Generates a cross-comparison table +- Organizes results by Copycat variants and formulas +- Exports results to CSV format + +## Dependencies +- Requires the `copycat` module +- Uses `pickle` for reading problem sets +- Uses `collections.defaultdict` for data organization +- Uses `pprint` for pretty printing (though not actively used in the current code) + +## File Structure +The output CSV file contains: +1. Headers: source, compare, source formula, compare formula +2. Problem descriptions in the format `A:B::C:_` +3. Results for each combination of variants and formulas \ No newline at end of file diff --git a/curses_main.py b/curses_main.py new file mode 100755 index 0000000..2779c32 --- /dev/null +++ b/curses_main.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +import argparse +import curses +import logging + +from copycat import Copycat +from copycat.curses_reporter import CursesReporter + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO, format='%(message)s', filename='./copycat.log', filemode='w') + + parser = argparse.ArgumentParser() + parser.add_argument('--focus-on-slipnet', action='store_true', help='Show the slipnet and coderack, instead of the workspace.') + parser.add_argument('--fps', type=float, default=None, help='Aim for this many frames per second.') + parser.add_argument('--seed', type=int, default=None, help='Provide a deterministic seed for the RNG.') + parser.add_argument('initial', type=str, help='A...') + parser.add_argument('modified', type=str, help='...is to B...') + parser.add_argument('target', type=str, help='...as C is to... what?') + options = parser.parse_args() + + try: + window = curses.initscr() + copycat = Copycat( + reporter=CursesReporter( + window, + focus_on_slipnet=options.focus_on_slipnet, + fps_goal=options.fps, + ), + rng_seed=options.seed, + ) + copycat.run_forever(options.initial, options.modified, options.target) + except KeyboardInterrupt: + pass + finally: + curses.endwin() diff --git a/curses_main_README.md b/curses_main_README.md new file mode 100644 index 0000000..09025af --- /dev/null +++ b/curses_main_README.md @@ -0,0 +1,36 @@ +# README_curses_main.md + +## Overview +`curses_main.py` is a terminal-based interface for the Copycat program that provides a visual representation of the analogical reasoning process using the curses library. It offers a real-time view of the system's operation, including the workspace, slipnet, and coderack. + +## Usage +Run the program from the terminal with the following command: +```bash +python curses_main.py abc abd ppqqrr +``` + +### Arguments +- `initial`: The first string in the analogy (e.g., "abc") +- `modified`: The second string showing the transformation (e.g., "abd") +- `target`: The third string to be transformed (e.g., "ppqqrr") +- `--focus-on-slipnet` (optional): Show the slipnet and coderack instead of the workspace +- `--fps` (optional): Target frames per second for the display +- `--seed` (optional): Provide a deterministic seed for the random number generator + +## Features +- Interactive terminal-based interface +- Real-time visualization of the Copycat system's operation +- Option to focus on different components (workspace or slipnet/coderack) +- Configurable display speed +- Detailed logging to `./copycat.log` + +## Dependencies +- Requires the `copycat` module +- Uses `curses` for terminal-based interface +- Uses `argparse` for command-line argument parsing +- Uses `logging` for output logging + +## Notes +- The program can be interrupted with Ctrl+C +- The display is automatically cleaned up when the program exits +- The interface provides a more detailed view of the system's operation compared to the standard command-line interface \ No newline at end of file diff --git a/distributions/.adj-tests b/distributions/.adj-tests new file mode 100644 index 0000000..25f566f Binary files /dev/null and b/distributions/.adj-tests differ diff --git a/distributions/.legacy b/distributions/.legacy new file mode 100644 index 0000000..ffc2901 Binary files /dev/null and b/distributions/.legacy differ diff --git a/distributions/.no-adj b/distributions/.no-adj new file mode 100644 index 0000000..4e7beb1 Binary files /dev/null and b/distributions/.no-adj differ diff --git a/distributions/.no-fizzle b/distributions/.no-fizzle new file mode 100644 index 0000000..5d578c4 Binary files /dev/null and b/distributions/.no-fizzle differ diff --git a/distributions/.no-prob-adj b/distributions/.no-prob-adj new file mode 100644 index 0000000..c8c029f Binary files /dev/null and b/distributions/.no-prob-adj differ diff --git a/distributions/.nuke-temp b/distributions/.nuke-temp new file mode 100644 index 0000000..9a7d5f1 Binary files /dev/null and b/distributions/.nuke-temp differ diff --git a/distributions/.soft-remove b/distributions/.soft-remove new file mode 100644 index 0000000..759a923 Binary files /dev/null and b/distributions/.soft-remove differ diff --git a/gui.py b/gui.py new file mode 100755 index 0000000..a3c72a5 --- /dev/null +++ b/gui.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +import argparse +import logging + +from copycat import Copycat, Reporter + +import matplotlib.pyplot as plt + +plt.style.use('dark_background') + +class SimpleReporter(Reporter): + def report_answer(self, answer): + print('Answered %s (time %d, final temperature %.1f)' % ( + answer['answer'], answer['time'], answer['temp'], + )) + +def main(): + logging.basicConfig(level=logging.INFO, format='%(message)s', filename='./output/copycat.log', filemode='w') + + parser = argparse.ArgumentParser() + parser.add_argument('--seed', type=int, default=None, help='Provide a deterministic seed for the RNG.') + options = parser.parse_args() + + copycat = Copycat(reporter=SimpleReporter(), rng_seed=options.seed, gui=True) + copycat.runGUI() + +if __name__ == '__main__': + main() diff --git a/gui_README.md b/gui_README.md new file mode 100644 index 0000000..fdeb811 --- /dev/null +++ b/gui_README.md @@ -0,0 +1,31 @@ +# README_gui.md + +## Overview +`gui.py` is a graphical user interface implementation for the Copycat program. It provides a visual interface for running the analogical reasoning system, using matplotlib for visualization with a dark background theme. + +## Usage +Run the program from the terminal with the following command: +```bash +python gui.py +``` + +### Arguments +- `--seed` (optional): Provide a deterministic seed for the random number generator + +## Features +- Graphical user interface for the Copycat system +- Dark background theme for better visibility +- Real-time visualization of the system's operation +- Detailed logging to `./output/copycat.log` + +## Dependencies +- Requires the `copycat` module +- Uses `matplotlib` for visualization +- Uses `argparse` for command-line argument parsing +- Uses `logging` for output logging + +## Notes +- The GUI provides a more user-friendly interface compared to the command-line version +- Results are displayed both in the GUI and printed to the console +- The interface includes temperature and time information for each answer +- The dark background theme is optimized for better visibility of the visualization \ No newline at end of file diff --git a/input/.placeholder b/input/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/input/problems.csv b/input/problems.csv new file mode 100644 index 0000000..3e941fa --- /dev/null +++ b/input/problems.csv @@ -0,0 +1,9 @@ +abc,abd,ijk +aabc,aabd,ijkk +abc,abd,kji +abc,abd,mrrjjj +abc,abd,rssttt +abc,abd,xyz +abc,abd,ijjkkk +rst,rsu,xyz +abc,abd,xyyzzz diff --git a/input/reduced_problems.csv b/input/reduced_problems.csv new file mode 100644 index 0000000..f5fe027 --- /dev/null +++ b/input/reduced_problems.csv @@ -0,0 +1,4 @@ +abc,abd,ijk +aabc,aabd,ijkk +abc,abd,xyz +abc,abd,ijjkkk diff --git a/main.py b/main.py new file mode 100755 index 0000000..d1b82a8 --- /dev/null +++ b/main.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +""" +Main Copycat program. + +To run it, type at the terminal: + + > python main.py abc abd ppqqrr --interations 10 + +The script takes three to five arguments. The first two are a pair of strings +with some change, for example "abc" and "abd". The third is a string which the +script should try to change analogously. The fourth (which defaults to "1") is +a number of iterations. One can also specify a defined seed value for the +random number generator. + +This instruction produces output such as: + + iiijjjlll: 670 (avg time 1108.5, avg temp 23.6) + iiijjjd: 2 (avg time 1156.0, avg temp 35.0) + iiijjjkkl: 315 (avg time 1194.4, avg temp 35.5) + iiijjjkll: 8 (avg time 2096.8, avg temp 44.1) + iiijjjkkd: 5 (avg time 837.2, avg temp 48.0) + + wyz: 5 (avg time 2275.2, avg temp 14.9) + xyd: 982 (avg time 2794.4, avg temp 17.5) + yyz: 7 (avg time 2731.9, avg temp 25.1) + dyz: 2 (avg time 3320.0, avg temp 27.1) + xyy: 2 (avg time 4084.5, avg temp 31.1) + xyz: 2 (avg time 1873.5, avg temp 52.1) + +The first number indicates how many times Copycat chose that string as its +answer; higher means "more obvious". The last number indicates the average +final temperature of the workspace; lower means "more elegant". +""" + +import argparse +import logging + +from copycat import Copycat, Reporter, plot_answers, save_answers + +class SimpleReporter(Reporter): + """Reports results from a single run.""" + + def report_answer(self, answer): + """Self-explanatory code.""" + print('Answered %s (time %d, final temperature %.1f)' % ( + answer['answer'], answer['time'], answer['temp'], + )) + + +def main(): + """Program's main entrance point. Self-explanatory code.""" + logging.basicConfig(level=logging.INFO, format='%(message)s', filename='./output/copycat.log', filemode='w') + + parser = argparse.ArgumentParser() + parser.add_argument('--seed', type=int, default=None, help='Provide a deterministic seed for the RNG.') + parser.add_argument('--iterations', type=int, default=1, help='Run the given case this many times.') + parser.add_argument('--plot', action='store_true', help='Plot a bar graph of answer distribution') + parser.add_argument('--noshow', action='store_true', help='Don\'t display bar graph at end of run') + parser.add_argument('initial', type=str, help='A...') + parser.add_argument('modified', type=str, help='...is to B...') + parser.add_argument('target', type=str, help='...as C is to... what?') + options = parser.parse_args() + + copycat = Copycat(reporter=SimpleReporter(), rng_seed=options.seed) + answers = copycat.run(options.initial, options.modified, options.target, options.iterations) + + for answer, d in sorted(iter(answers.items()), key=lambda kv: kv[1]['avgtemp']): + print('%s: %d (avg time %.1f, avg temp %.1f)' % (answer, d['count'], d['avgtime'], d['avgtemp'])) + + if options.plot: + plot_answers(answers, show=not options.noshow) + save_answers(answers, 'output/answers.csv') + +if __name__ == '__main__': + main() diff --git a/main_README.md b/main_README.md new file mode 100644 index 0000000..9d85e28 --- /dev/null +++ b/main_README.md @@ -0,0 +1,41 @@ +# README_main.md + +## Overview +`main.py` is the primary entry point for the Copycat program, which implements an analogical reasoning system. The program takes three strings as input and attempts to find an analogous transformation between them. + +## Usage +Run the program from the terminal with the following command: +```bash +python main.py abc abd ppqqrr --iterations 10 +``` + +### Arguments +- `initial`: The first string in the analogy (e.g., "abc") +- `modified`: The second string showing the transformation (e.g., "abd") +- `target`: The third string to be transformed (e.g., "ppqqrr") +- `--iterations` (optional): Number of times to run the case (default: 1) +- `--seed` (optional): Provide a deterministic seed for the random number generator +- `--plot` (optional): Generate a bar graph of answer distribution +- `--noshow` (optional): Don't display the bar graph at the end of the run + +## Output +The program produces output in the following format: +``` +iiijjjlll: 670 (avg time 1108.5, avg temp 23.6) +iiijjjd: 2 (avg time 1156.0, avg temp 35.0) +... +``` +Where: +- The first number indicates how many times Copycat chose that string as its answer (higher means "more obvious") +- The last number indicates the average final temperature of the workspace (lower means "more elegant") + +## Features +- Implements the Copycat analogical reasoning system +- Provides detailed logging to `./output/copycat.log` +- Can generate visualizations of answer distributions +- Saves results to `output/answers.csv` + +## Dependencies +- Requires the `copycat` module +- Uses `argparse` for command-line argument parsing +- Uses `logging` for output logging \ No newline at end of file diff --git a/output/.placeholder b/output/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/papers/Makefile b/papers/Makefile new file mode 100644 index 0000000..c6e2513 --- /dev/null +++ b/papers/Makefile @@ -0,0 +1,12 @@ + +all: + make draft + make clean + +draft: + pdflatex draft.tex + biber draft + pdflatex draft.tex + +clean: + rm *.out *.log *.xml *.bbl *.bcf *.blg *.aux diff --git a/papers/draft.tex b/papers/draft.tex new file mode 100644 index 0000000..5226a12 --- /dev/null +++ b/papers/draft.tex @@ -0,0 +1,356 @@ +\documentclass[a4paper]{article} + +%% Sets page size and margins +\usepackage[a4paper,top=3cm,bottom=2cm,left=3cm,right=3cm,marginparwidth=1.75cm]{geometry} + +%% Useful packages +\usepackage{listings} +\usepackage{amsmath} +\usepackage{pdfpages} +\usepackage{graphicx} +\usepackage{indentfirst} %% Personal taste of LSaldyt + +\usepackage[utf8]{inputenc} +\usepackage[english]{babel} + +\usepackage[backend=biber]{biblatex} +\addbibresource{sources.bib} + +\usepackage[colorinlistoftodos]{todonotes} +\usepackage[colorlinks=true, allcolors=blue]{hyperref} + + +\definecolor{lightgrey}{rgb}{0.9, 0.9, 0.9} +\lstset{ % + backgroundcolor=\color{lightgrey}} + +\title{Distributed Behavior in a Fluid Analogy Architecture} +\author{Lucas Saldyt, Alexandre Linhares} + +\begin{document} +\maketitle + +\begin{abstract} + This project focuses on effectively simulating intelligent processes behind fluid analogy-making through increasingly distributed decision-making. + In the process, it discusses creating an effective scientific framework for fluid analogy architectures. + This draft assumes extensive knowledge with the Copycat software, which was pioneered by Melanie Mitchell \cite{analogyasperception}. + A humanistic search algorithm, the Parallel Terraced Scan, is altered and tested. + Originally, this search algorithm contains a centralizing variable, called \emph{temperature}. + This paper investigates the influence of this centralizing variable by modifying, testing, and eventually removing all code related to it. + In this process, several variants of the copycat software are created. + The produced answer distributions of each resulting branch of the copycat software were then cross-compared with a Pearson's $\chi^2$ distribution test. + This paper draft explores tests done on five novel copycat problems with thirty answers given per cross comparison. + [For now, it is safest to say that the results of this paper are inconclusive: See Results section] + %% Based on this cross-comparison, the original adjustment formulas have no significant effect (But these results are preliminary, see Results section for more detail). +\end{abstract} + +\section{Introduction} + +This paper stems from Melanie Mitchell's \cite{analogyasperception} and Douglas Hofstadter's \& FARG's \cite{fluidconcepts} work on the copycat program. +It is also based on work from a previous paper by Alexandre Linhares \cite{linhares}. +This project focuses on effectively simulating intelligent processes through increasingly distributed decision-making. +In the process of evaluating the distributed nature of copycat, this paper also proposes a "Normal Science" framework. +Copycat's behavior is based on the "Parallel Terraced Scan," a humanistic-inspired search algorithm. +The Parallel Terraced Scan is, roughly, a mix between a depth-first and breadth-first search. +To switch between modes of search, FARG models use the global variable \emph{temperature}. +\emph{Temperature} is ultimately a function of the workspace rule strength then the importance and happiness of each workspace structure. +Therefore, \emph{temperature} is a global metric, but is sometimes used to make local decisions. +Since copycat means to simulate intelligence in a distributed nature, it should make use of local metrics for local decisions. +This paper explores the extent to which copycat's behavior can be improved through distributing decision making. + +Specifically, the effects of temperature are first tested. +Then, once the statistically significant effects of temperature are understood, work is done to replace temperature with a distributed metric. +Initially, temperature is removed destructively, essentially removing any lines of code that mention it, simply to see what effect it has. +Then, a surgical removal of temperature is attempted, leaving in tact affected structures or replacing them by effective distributed mechanisms. + +To evaluate the distributed nature of copycat, this paper focuses on the creation of a `normal science' framework. +By `Normal science,' this paper means the term created by Thomas Kuhn--the collaborative enterprise of furthering understanding within a paradigm. +Today, "normal science" is simply not done on FARG architectures (and on most computational cognitive architectures too... see Addyman \& French \cite{compmodeling}). +Unlike mathematical theories or experiments, which can be replicated by following the materials and methods, computational models generally have dozens of particularly tuned variables, undocumented procedures, multiple assumptions about the users computational environment, etc. +It then becomes close to impossible to reproduce a result, or to test some new idea scientifically. +This paper focuses on the introduction of statistical techniques, reduction of "magic numbers", improvement and documentation of formulas, and proposals for statistical human comparison. +Each of these methods will reduce the issues with scientific inquiry in the copycat architecture. + +To evaluate two different versions of copycat, the resulting answer distributions from a problem are compared with a Pearson's $\chi^2$ test. +Using this, the degree of difference between distributions can be calculated. +Then, desirability of answer distributions can be found as well, and the following hypotheses can be tested: + +\begin{enumerate} + \item $H_i$ Centralized, global variables constrict copycat's ability. + \item $H_0$ Centralized, global variables either improve or have no effect on copycat's ability. +\end{enumerate} + +\subsection{Objective} + + The aim of this paper is to create and test a new version of the copycat software that makes effective use of a multiple level description. + Until now, copycat has made many of its decisions, even local ones, based on a global variable, \emph{temperature}. + Two approaches will be taken toward improving copycat. + First, small portions of copycat will be removed and then tested individually. + If they do not significantly change the answer distributions given by copycat, they will be collectively removed from a working version of copycat. + Then, alternate, distributed versions of copycat will be compared to the original copycat software to effectively decide on which design choices to make. + +\subsection{Theory} + + \subsubsection{Centralized Structures} + + Since computers are universal and have vastly improved in the past five decades, it is clear that computers are capable of simulating intelligent processes \cite{computerandthebrain}. + The primary obstacle blocking strong A.I. is \emph{comprehension} of intelligent processes. + Once the brain is truly understood, writing software that emulates intelligence will be a (relatively) simple engineering task when compared to understanding the brain. + + In making progress towards understanding the brain fully, models must remain true to what is already known about intelligent processes. + Outside of speed, the largest difference between the computer and the brain is the distributed nature of computation. + Specifically, our computers as they exist today have central processing units, where literally all of computation happens. + Brains have some centralized structures, but certainly no single central location where all processing happens. + Luckily, the difference in speed between brains and computers allows computers to simulate brains even when they are running serial code. + From a design perspective, however, software should take the distributed nature of the brain into consideration, because it is most likely that distributed computation plays a large role in the brain's functionality. + + For example, codelets should behave more like ants in an anthill, as described in \emph{Gödel, Escher, Bach} \cite{geb}. + Instead of querying a global structure (i.e. the queen), ants might query each other, and each carry information about what they've last seen. + In this way, distributed computation can be carried out through many truly parallel (non-blocking) agents. + + It is clear from basic classical psychology that the brain contains some centralized structures. + For example, Broca's area and Wernicke's area are specialized for linguistic input and output. + Another great example is the hippocampi. + If any of these specialized chunks of brain are surgically removed, for instance, then the ability to perform certain tasks is greatly impacted. + To some extent, the same is true for copycat. + For example, removing the ability to update the workspace would be \emph{*roughly*} equivalent to removing both hippocampi from a human. + This paper means to first test the impact of centralized structures, like \emph{temperature}, by removing or altering them and then performing tests. + Then, distributed structures will be proposed and tested in place of centralized ones. + + However: “How gullible are you? Is your gullibility located in some "gullibility center" in your brain? Could a neurosurgeon reach in and perform some delicate operation to lower your gullibility, otherwise leaving you alone? If you believe this, you are pretty gullible, and should perhaps consider such an operation.” +― Douglas R. Hofstadter, Gödel, Escher, Bach: An Eternal Golden Braid + + Outside of \emph{temperature}, other structures in copycat, like the workspace itself, or the coderack, are also centralized. + Hopefully, these centralized structures are not constraining, but it possible they are. + If they are, their unifying effect should be taken into account. + For example, the workspace is atomic, just like centralized structures in the brain, like the hippocampi, are also atomic. + If copycat can be run such that -- during the majority of the program's runtime -- codelets may actually execute at the same time (without pausing to access globals), then it will much better replicate the human brain. + A good model for this is the functional-programming \emph{map} procedure. + From this perspective, the brain would simply be carrying out the same function in many locations (i.e. \emph{map}ping neuron.process() across each of its neurons) + Note that this is more similar to the behavior of a GPU than a CPU. + This model doesn't work when code has to synchronize to access global variables. + + Notably, however, functional distributed code is Turing complete just like imperative centralized code is Turing complete. + Especially given the speed of modern computers, functional code cannot do anything that imperative code can't. + However, working in a mental framework that models the functionality of the human brain may assist in actually modeling its processes. + + \subsubsection{Local Descriptions} + + A global description of the system (\emph{temperature}) is, at times, potentially useful. + However, in summing together the values of each workspace object, information is lost regarding which workspace objects are offending. + In general, the changes that occur will eventually be object-specific. + So, it seems to me that going from object-specific descriptions to a global description back to an object-specific action is a waste of time, at least when the end action is an object-specific action. + A global description shouldn't be \emph{obliterated} (removed 100\%). + Maybe a global description should be reserved for \emph{only} when global actions are taking place. + For example, when deciding that copycat has found a satisfactory answer, a global description should be used, because deciding to stop copycat is a global action. + However, when deciding to remove a particular structure, a global description should not be used, because removing a particular offending structure is NOT a global action. + + Of course, global description has some benefits even when it is being used to change local information. + For example, the global formula for temperature converts the raw importance value for each object into a relative importance value for each object. + If a distributed metric was used, this importance value would have to be left in its raw form. + +\section{Methods} + + \subsection{Formula Documentation} + + Many of copycat's formulas use magic numbers and marginally documented formulas. + This is less of a problem in the original LISP code, and more of a problem in the twice-translated Python3 version of copycat. + However, even in copycat's LISP implementation, formulas have redundant parameters. + For example, if given two formulas: $f(x) = x^2$ and $g(x) = 2x$, a single formula can be written $h(x) = 4x^2$ (The composed and then simplified formula). + Ideally, the adjustment formulas within copycat could be reduced in the same way, so that much of copycat's behavior rested on a handful of parameters in a single location, as opposed to more than ten parameters scattered throughout the repository. + Also, often parameters in copycat have little statistically significant effect. + As will be discussed in the $\chi^2$ distribution testing section, any copycat formulas without a significant effect will be hard-removed. + + \subsection{Testing the Effect of Temperature} + + To begin with, the existing effect of the centralizing variable, temperature, will be analyzed. + As the probability adjustment formulas are used by default, very little effect is had. + To evaluate the effect of temperature-based probability adjustment formulas, a spreadsheet was created that showed a color gradient based on each formula. + View the spreadsheets \href{https://docs.google.com/spreadsheets/d/1JT2yCBUAsFzMcbKsQUcH1DhcBbuWDKTgPvUwD9EqyTY/edit?usp=sharing}{here}. + Then, to evaluate the effect of different temperature usages, separate usages of temperature were individually removed and answer distributions were compared statistically (See section: $\chi^2$ Distribution Testing). + + \subsection{Temperature Probability Adjustment} + + Once the effect of temperature was evaluated, new temperature-based probability adjustment formulas were proposed that each had a significant effect on the answer distributions produced by copycat. + Instead of representing a temperature-less, decentralized version of copycat, these formulas are meant to represent the centralized branch of copycat. + + These formulas curve probabilities, making unlikely events more likely and likely events less likely as a function of the global \emph{temperature} variable. + + The desired (LISP documented) behavior is as follows: + At high temperatures, the system should explore options that would otherwise be unlikely. + So, at temperatures above half of the maximum temperature, probabilities with a base value less than fifty percent will be curved higher, to some threshold. + At temperatures below half of the maximum temperature, probabilities with a base value above fifty percent will be curved lower, to some threshold. + + The original formulas being used to do this were overly complicated. + In summary, many formulas were tested in a spreadsheet, and an optimal one was chosen that replicated the desired behavior. + The remainder of the section discusses different formulas and their advantages/disadvantages. + Also, as a general rule, changing these formulas causes copycat to produce statistically significantly different answer distributions. + + The original formula for curving probabilities in copycat: + \lstinputlisting[language=Python]{resources/original.py} + + An alternative that seems to improve performance on the "abd:abd::xyz:\_" problem: + This formula produces probabilities that are not bounded between 0 and 1. These are generally truncated. + \lstinputlisting[language=Python]{resources/entropy.py} + + However, this formula worsens performance on non "xyz" problems. + Likely, because of how novel the "xyz" problem is, it will require more advanced architecture changes. + For instance, MetaCat claims to assist in solving the "xyz" problem. + + The entropy formula is an improvement, but other formulas are possible too. + + Below are variations on a "weighted" formula. + The general structure is: + + \[\emph{p'} = \frac{T}{100} * S + \frac{100-T}{100} * U\] + + Where: $S$ is the convergence value for when $T = 100$ and + $U$ is the convergence value for when $T = 0$. + The below formulas simply experiment with different values for $S$ and $U$ + + \lstinputlisting[language=Python]{resources/weighted.py} + + After some experimentation and reading the original copycat documentation, it was clear that $S$ should be chosen to be $0.5$ (All events are equally likely at high temperature) and that $U$ should implement the probability curving desired at low temperatures. + + The following formulas let $U = p^r$ if $p < 0.5$ and let $U = p^\frac{1}{r}$ if $p >= 0.5$. + This controls whether/when curving happens. + Now, the \emph{single} parameter $r$ simply controls the degree to which curving happens. + Different values of $r$ were experimented with (values between $10$ and $1$ were experimented with at increasingly smaller step sizes). + $2$ and $1.05$ are both good choices at opposite "extremes". + $2$ works because it is large enough to produce novel changes in behavior at extreme temperatures without totally disregarding the original probabilities. + Values above $2$ do not work because they make probabilities too uniform. + Values below $2$ (and above $1.05$) are feasible, but produce less curving and therefore less unique behavior. + $1.05$ works because it very closely replicates the original copycat formulas, providing a very smooth curving. + Values beneath $1.05$ essentially leave probabilities unaffected, producing no significant unique behavior dependent on temperature. + + \lstinputlisting[language=Python]{resources/best.py} + + All of these separate formulas will later be cross-compared to other variants of the copycat software using a Pearson's $\chi^2$ test. + + \subsection{Temperature Usage Adjustment} + + Once the behavior based on temperature was well understood, experimentation was made with hard and soft removals of temperature and features that depend on it. + For example, first probability adjustments based on temperature were removed. + Then, the new branch of copycat was $\chi^2$ compared against the original branch. + Then, breaker-fizzling, an independent temperature-related feature was removed from the original branch and another $\chi^2$ comparison was made. + The same process was repeated for non-probability temperature-based adjustments, and then for the copycat stopping decision. + Then, a temperature-less branch of the repository was created and tested. + Then, a branch of the repository was created that removed probability adjustments, value adjustments, and fizzling, and made all other temperature-related operations use a dynamic temperature calculation. + All repository branches were then cross compared using a $\chi^2$ distribution test. + + \subsection{$\chi^2$ Distribution Testing} + + To test each different branch of the repository, a scientific framework was created. + Each run of copycat on a particular problem produces a distribution of answers. + Distributions of answers can be compared against one another with a (Pearson's) $\chi^2$ distribution test. + + $$\chi^2 = \sum_{i=1}^{n} \frac{(O_i - E_i)^2}{E_i}$$ + Where: + \newline\indent + $O_i = $ The number of observations of a particular answer + \newline\indent + $E_i = $ The number of expected observations of a particular answer + \newline + \newline\indent + Then, $\chi^2$ is calculated, using one copycat variant as a source for expected observations, and another copycat variant as a source for novel observations. + If the $\chi^2$ value is above some threshold (dependent on degrees of freedom and confidence level), then the two copycat variants are significantly different. + A standard confidence level of $95\%$ is used, and degrees of freedom is calculated as the number of different answers given from the source-variant of copycat. + Because of this, comparing copycat variants like this is \emph{not} always commutative. + + \subsection{Effectiveness Definition} + + Quantitatively evaluating the effectiveness of a cognitive architecture is difficult. + However, for copycat specifically, effectiveness can be defined as a function of the frequency of desirable answers and equivalently as the inverse frequency of undesirable answers. + Since answers are desirable to the extent that they respect the original transformation of letter sequences, desirability can also be approximated by a concrete metric. + A simple metric for desirability is simply the existing temperature formula. + So, one metric for effectiveness of a copycat variant is the frequency of low-temperature answers. + $$e = \frac{\sum_{i=i}^{n} \frac{O_i}{T_i}}{N} $$ + For simplicity, only this metric will be used. + However, this metric could be extended relatively easily. + For example, the unique variants in copycat answers could be taken into account ($n$). + +\section{Results} + + \subsection{Cross $\chi^2$ Table} + + The Cross$\chi^2$ table summarizes the results of comparing each copycat-variant's distribution with each other copycat-variant and with different internal formulas. + For the table, please see \href{"https://docs.google.com/spreadsheets/d/1d4EyEbWLJpLYlE7qSPPb8e1SqCAZUvtqVCd0Ns88E-8/edit?usp=sharing"}{google sheets}. + This table contains a lot of information, but most importantly it shows which copycat variants produce novel changes and which do not. + The following variants of copycat were created: + \begin{enumerate} + \item The original copycat (legacy) + \item Copycat with no probability adjustment formulas (no-prob-adj) + \item Copycat with no fizzling (no-fizzle) + \item Copycat with no adjustment formulas at all (no-adj) + \item Copycat with several different internal adjustment formulas (adj-tests) + \begin{enumerate} + \item alt\_fifty + \item average\_alt + \item best + \item entropy + \item fifty\_converge + \item inverse + \item meta + \item none + \item original + \item pbest + \item pmeta + \item sbest + \item soft + \item weighted\_soft + \end{enumerate} + \item Copycat with temperature 100\% removed (nuke-temp) + \item Copycat with a surgically removed temperature (soft-remove) + \end{enumerate} + + Each variant was cross-compared with each other variant on this set of problems (from \cite{fluidconcepts}). + \begin{enumerate} + \item abc:abd::efg:\_ + \item abc:abd::ijk:\_ + \item abc:abd::ijkk:\_ + \item abc:abd::mrrjjj:\_ + \item abc:abd::xyz:\_ + \end{enumerate} + + On a trial run with thirty iterations each, the following cross-comparisons showed \emph{no} difference in answer distributions: + \begin{enumerate} + \item .no-adj x .adj-tests(none) + \item .no-adj x .adj-tests(original) + \item .no-adj x .no-prob-adj + \item .no-prob-adj x .adj-tests(original) + \item .no-prob-adj x .adj-tests(pbest) + \item .no-prob-adj x .adj-tests(weighted\_soft) + \item .nuke-temp x .adj-tests(entropy) + \item .soft-remove x .adj-tests(best) + \item .soft-remove x .no-prob-adj + \end{enumerate} + + There are also several variant comparisons that only vary on one or two problems. + As discussed below, it will be easier to evaluate them with more data. + + Before the final draft of this paper, a trial will be conducted with a larger number of iterations and a variant of the Pearson's $\chi^2$ test that accounts for zero-count answer frequencies. + Also, because the comparison test is non commutative, "backwards" tests will be conducted. + Additionally, more problems will be added to the problem set, even if they are reducible. + This will provide additional data points for comparison (If two copycat variants are indistinguishable on some novel problem, they should be indistinguishable on some structurally identifical variant of the novel problem). + It is also possible that additional versions of copycat will be tested (I plan on testing small features of copycat, like parameters and so on, and removing them bit by bit). + +\section{Discussion} + + \subsection{Interpretation of table} + + It is clear that the original copycat probability adjustment formula had no statistically significant effects. + Additionally, new formulas that emulate the performance of the original formula also have no significant effects. + However, novel adjustment formulas, like the "best" formula, provide the same results as soft-removing temperature. + Soft-removing temperature is also identical to running copycat with no probability adjustments. + + \subsection{Distributed Computation Accuracy} + + [Summary of introduction, elaboration based on results] + + \subsection{Prediction??} + + Even though imperative, serial, centralized code is Turing complete just like functional, parallel, distributed code, I predict that the most progressive cognitive architectures of the future will be created using functional programming languages that run distributively and are at least capable of running in true, CPU-bound parallel. + +\printbibliography + +\end{document} diff --git a/papers/legacy/draft.tex b/papers/legacy/draft.tex new file mode 100644 index 0000000..1944c9f --- /dev/null +++ b/papers/legacy/draft.tex @@ -0,0 +1,336 @@ +\documentclass[a4paper]{article} + +%% Language and font encodings +\usepackage[english]{babel} +\usepackage[utf8x]{inputenc} +\usepackage[T1]{fontenc} + +%% Sets page size and margins +\usepackage[a4paper,top=3cm,bottom=2cm,left=3cm,right=3cm,marginparwidth=1.75cm]{geometry} + +%% Useful packages +\usepackage{listings} +\usepackage{amsmath} +\usepackage{graphicx} +\usepackage[colorinlistoftodos]{todonotes} +\usepackage[colorlinks=true, allcolors=blue]{hyperref} + +\definecolor{lightgrey}{rgb}{0.9, 0.9, 0.9} +\lstset{ % + backgroundcolor=\color{lightgrey}} + +\title{Distributed Behavior in a Fluid Analogy Architecture} +\author{Lucas Saldyt, Alexandre Linhares} + +\begin{document} +\maketitle + +\begin{abstract} + We investigate the distributed nature of computation in a FARG architecture, Copycat. + One of the foundations of those models is the \emph{Parallel Terraced Scan}--a psychologically-plausible model that enables a system to fluidly move between different modes of processing. + Previous work has modeled decision-making under Parallel Terraced Scan by using a central variable of \emph{Temperature}. + However, it is unlikely that this design decision accurately replicates the processes in the human brain. + This paper proposes several changes to copycat architectures that will increase their modeling accuracy. +\end{abstract} + +\section{Introduction} + + This paper stems from Mitchell's (1993) and Hofstadter's \& FARG's (1995) work on the copycat program. + This project focuses on effectively simulating intelligent processes through increasingly distributed decision-making. + In the process of evaluating the distributed nature of copycat, this paper also proposes a "Normal Science" framework. + + First, copycat uses a "Parallel Terraced Scan" as a humanistic inspired search algorithm. + The Parallel Terraced Scan corresponds to the psychologically-plausible behavior of briefly browsing, say, a book, and delving deeper whenever something sparks one's interest. + In a way, it is a mix between a depth-first and breadth-first search. + This type of behavior seems to very fluidly change the intensity of an activity based on local, contextual cues. + Previous FARG models use centralized structures, like the global temperature value, to control the behavior of the Parallel Terraced Scan. + This paper explores how to maintain the same behavior while distributing decision-making throughout the system. + + Specifically, this paper attempts different refactors of the copycat architecture. + First, the probability adjustment formulas based on temperature are changed. + Then, we experiment with two methods for replacing temperature with a distributed metric. + Initially, temperature is removed destructively, essentially removing any lines of code that mention it, simply to see what effect it has. + Then, a surgical removal of temperature is attempted, leaving in tact affected structures or replacing them by effective distributed mechanisms. + + To evaluate the distributed nature of copycat, this paper focuses on the creation of a `normal science' framework. + By `Normal science,' this paper means the term created by Thomas Kuhn--the collaborative enterprise of furthering understanding within a paradigm. + Today, "normal science" is simply not done on FARG architectures (and on most computational cognitive architectures too... see Addyman \& French 2012). + Unlike mathematical theories or experiments, which can be replicated by following the materials and methods, computational models generally have dozens of particularly tuned variables, undocumented procedures, multiple assumptions about the users computational environment, etc. + It then becomes close to impossible to reproduce a result, or to test some new idea scientifically. + This paper focuses on the introduction of statistical techniques, reduction of "magic numbers", improvement and documentation of formulas, and proposals for statistical human comparison. + + We also discuss, in general, the nature of the brain as a distributed system. + While the removal of a single global variable may initially seem trivial, one must realize that copycat and other cognitive architectures have many central structures. + This paper explores the justification of these central structures in general. + Is it possible to model intelligence with them, or are they harmful? + +\section{Theory} + \subsection{Notes} + + According to the differences we can enumerate between brains and computers, it is clear that, since computers are universal and have vastly improved in the past five decades, that computers are capable of simulating intelligent processes. + [Cite Von Neumann]. + Primarily, the main obstacle now lies in our comprehension of intelligent processes. + Once we truly understand the brain, writing software that emulates intelligence will be a relatively simple software engineering task. + However, we must be careful to remain true to what we already know about intelligent processes so that we may come closer to learning more about them and eventually replicating them in full. + The largest difference between the computer and the brain is the distributed nature of computation. + Specifically, our computers as they exist today have central processing units, where literally all of computation happens. + On the other hand, our brains have no central location where all processing happens. + Luckily, the speed advantage and universality of computers makes it possible to simulate the distributed behavior of the brain. + However, this simulation is only possible if computers are programmed with concern for the distributed nature of the brain. + [Actually, I go back and forth on this: global variables might be plausible, but likely aren't] + Also, even though the brain is distributed, some clustered processes must take place. + In general, centralized structures should be removed from the copycat software, because they will likely improve the accuracy of simulating intelligent processes. + It isn't clear to what degree this refactor should take place. + The easiest target is the central variable, temperature, but other central structures exist. + This paper focuses primarily on temperature, and the unwanted global unification associated with it. + + Even though copycat uses simulated parallel code, if copycat were actually parallelized, the global variable of temperature would actually prevent most copycat codelets from running at the same time. + If this global variable and other constricting centralized structures were removed, copycat's code would more closely replicate intelligent processes and would be able to be run much faster. + From a function-programming like perspective (i.e. LISP, the original language of copycat), the brain should simply be carrying out the same function in many locations (i.e. mapping neuron.process() across each of its neurons, if you will...) + Note that this is more similar to the behavior of a GPU than a CPU....? + However, in violating this model with the introduction of global variables...... + + Global variables seem like a construct that people use to model the real world. + ... + + It is entirely possible that at the level of abstraction that copycat uses, global variables are perfectly acceptable. + For example, a quick grep-search of copycat shows that the workspace singleton also exists as a global variable. + Making all of copycat distributed clearly would require a full rewrite of the software.... + + If copycat can be run such that codelets may actually execute at the same time (without pausing to access globals), then it will much better replicate the human brain. + + However, I question the assumption that the human brain has absolutely no centralized processing. + For example, input and output channels (i.e. speech mechanisms) are not accessible from the entire brain. + Also, brain-region science leads me to believe that some (for example, research concerning wernicke's or broca's areas) brain regions truly are "specialized," and thus lend some support to the existence of centralized structures in a computer model of the brain. + However, these centralized structures may be emergent? + + So, to re-iterate: Two possibilities exist (hypotheses) + + A computer model of the brain can contain centralized structures and still be effective in its modeling. + A computer model cannot have any centralzied structures if it is going to be effective in its modeling. + + Another important problem is defining the word "effective". + I suppose that "effective" would mean capable of solving fluid analogy problems, producing similar answers to an identically biased human. + However, it isn't clear to me that removing temperature increases the ability to solve problems effectively. + Is this because models are allowed to have centralized structures, or because temperature isn't the only centralized structure? + + Clearly, creating a model of copycat that doesn't have centralized structures will take an excessive amount of effort. + + \break + + The calculation for temperature in the first place is extremely convoluted (in the Python version of copycat). + It lacks any documentation, is full of magic numbers, and contains seemingly arbitrary conditionals. + (If I submitted this as a homework assignment, I would probably get a C. Lol) + + Edit: Actually, the lisp version of copycat does a very good job of documenting magic numbers and procedures. + My main complaint is that this hasn't been translated into the Python version of copycat. + However, the Python version is translated from the Java version.. + Lost in translation. + + + My goal isn't to roast copycat's code, however. + Instead, what I see is that all this convolution is \emph{unnecessary}. + Ideally, a future version of copycat, or an underlying FARG architecure will remove this convolution, and make temperature calculation simpler, streamlined, documented, understandble. + How will this happen, though? + + A global description of the system is, at times, potentially useful. + However, in summing together the values of each workspace object, information is lost regarding which workspace objects are offending. + In general, the changes that occur will eventually be object-specific. + So, it seems to me that going from object-specific descriptions to a global description back to an object-specific action is a waste of time. + + I don't think that a global description should be \emph{obliterated} (removed 100\%). + I just think that a global description should be reserved for when global actions are taking place. + For example, when deciding that copycat has found a satisfactory answer, a global description should be used, because deciding to stop copycat is a global action. + However, when deciding to remove a particular structure, a global description should not be used, because removing a particular offending structure is NOT a global action. + + Summary: it is silly to use global information to make local decisions that would be better made using local information (self-evident). + + Benefits of using local information to make local decisions: + + Code can be truly distributed, running in true parallel, CPU-bound. + This means that copycat would be faster and more like a human brain. + Specific structures would be removed based on their own offenses. + This means that relvant structures would remain untouched, which would be great! + Likely, this change to copycat would produce better answer distributions testable through the normal science framework. + + On the other hand (I've never met a one-handed researcher), global description has some benefits. + For example, the global formula for temperature converts the raw importance value for each object into a relative importance value for each object. + If a distributed metric was used, this importance value would have to be left in its raw form. + + \break + + The original copycat was written in LISP, a mixed-paradigm language. + Because of LISP's preference for functional code, global variables must be explicitly marked with surrounding asterisks. + Temperature, the workspace, and final answers are all marked global variables as discussed in this paper. + These aspects of copycat are all - by definition - impure, and therefore imperative code that relies on central state changes. + It is clear that, since imperative, mutation-focused languages (like Python) are turing complete in the same way that functional, purity-focused languages (like Haskell) are turing complete, each method is clearly capable of modeling the human brain. + However, the algorithm run by the brain is more similar to distributed, parallel functional code than it is to centralized, serial imperative code. + While there is some centralization in the brain, and evidently some state changes, it is clear that 100\% centralized 100\% serial code is not a good model of the brain. + + Also, temperature is, ultimately, just a function of objects in the global workspace. + The git branch soft-temp-removal hard-removes most usages of temperature, but continues to use a functional version of the temperature calculation for certain processes, like determining if the given answer is satisfactory or not. + So, all mentions of temperature could theoretically be removed and replaced with a dynamic calculation of temperature instead. + It is clear that in this case, this change is unnecessary. + With the goal of creating a distributed model in mind, what actually bothers me more is the global nature of the workspace, coderack, and other singleton copycat structures. + Really, when temperature is removed and replaced with some distributed metric, it is clear that the true "offending" global is the workspace/coderack. + + Alternatively, codelets could be equated to ants in an anthill (see anthill analogy in GEB). + Instead of querying a global structure, codelets could query their neighbors, the same way that ants query their neighbors (rather than, say, relying on instructions from their queen). + +Biological or psychological plausibility only matters if it actually affects the presence of intelligent processes. For example, neurons don't exist in copycat because we feel that they aren't required to simulate the processes being studied. Instead, copycat uses higher-level structures to simulate the same emergent processes that neurons do. However, codelets and the control of them relies on a global function representing tolerance to irrelevant structures. Other higher level structures in copycat likely rely on globals as well. Another central variable in copycat is the "rule" structure, of which there is only one. While some global variables might be viable, others may actually obstruct the ability to model intelligent processes. For example, a distributed notion of temperature will not only increase biological and psychological plausibility, but increase copycat's effectiveness at producing acceptable answer distributions. + +We must also realize that copycat is only a model, so even if we take goals (level of abstraction) and biological plausibility into account... +It is only worth changing temperature if it affects the model. +Arguably, it does affect the model. (Or, rather, we hypothesize that it does. There is only one way to find out for sure, and that's the point of this paper) + +So, maybe this is a paper about goals, model accuracy, and an attempt to find which cognitive details matter and which don't. It also might provide some insight into making a "Normal Science" framework. + +Copycat is full of random uncommented parameters and formulas. Personally, I would advocate for removing or at least documenting as many of these as possible. In an ideal model, all of the numbers present might be either from existing mathematical formulas, or present for a very good (emergent and explainable - so that no other number would make sense in the same place) reason. However, settling on so called "magic" numbers because the authors of the program believed that their parameterizations were correct is very dangerous. If we removed random magic numbers, we would gain confidence in our model, progress towards a normal science, and gain a better understanding of cognitive processes. + +Similarly, a lot of the testing of copycat is based on human perception of answer distributions. However, I suggest that we move to a more statistical approach. For example, deciding on some arbitrary baseline answer distribution and then modifying copycat to obtain other answer distributions and then comparing distributions with a statistical significance test would actually be indicative of what effect each change had. This paper will include code changes and proposals that lead copycat (and FARG projects in general) to a more statistical and verifiable approach. +While there is a good argument about copycat representing an individual with biases and therefore being incomparable to a distributed group of individuals, I believe that additional effort should be made to test copycat against human subjects. I may include in this paper a concrete proposal on how such an experiment might be done. + +Let's simply test the hypothesis: \[H_i\] Copycat will have an improved (significantly different with increased frequencies of more desirable answers and decreased frequencies of less desirable answers: desirability will be determined by some concrete metric, such as the number of relationships that are preserved or mirrored) answer distribution if temperature is turned to a set of distributed metrics. \[H_0\] Copycat's answer distribution will be unaffected by changing temperature to a set of distributed metrics. + + \subsection{Normal Science} + \subsubsection{Scientific Style} + The Python3 version of copycat contains many undocumented formulas and magic numbers. + Also, because of the random nature of copycat, sometimes answer distributions can be affected by the computer architecture that the software is being executed on. + To avoid this, this paper suggests documentation of formulas, removal or clear justification of magic numbers, and the use of seeding to get around random processes. + Additionally, I might discuss how randomness doesn't *really* exist. + Because of this, maybe the explicit psuedo-random nature of Copycat shouldn't exist? + Instead.. The distributed nature of computation might act as a psuedo-random process in and of itself. + \subsubsection{Scientific Testing} + Previously, no statistical tests have been done with the copycat software. + Copycat can be treated like a black box, where, when given a particular problem, copycat produces a distribution of answers as output. + In this perspective, copycat can be tweaked, and then output distributions on the same problem can be compared with a statistical test, like a $\chi^2$ test. + The $\chi^2$ value indicates the degree to which a new copycat distribution differs from an old one. + So, a $\chi^2$ test is useful both as a unit test and as a form of scientific inquiry. + For example, if a new feature is added to copycat (say, the features included in the Metcat software), then the new distributions can be compared to the distributions produced by the original version of copycat. + Ideally, these distributions will differ, giving us a binary indication of whether the changes to the software actually had any effect. + Then, once we know that a distribution is statistically novel, we can decide on metrics that evaluate its effectiveness in solving the given problem. + For example, since Metacat claims to solve the "xyz" problem, and "wyz" is generally seen as the best answer to the "xyz" problem, a metric that evaluates the health of a distribution might simply be the percentage of "wyz" answers. + This can be generalized to the percentage of desirable answers given by some copycat variant in general. + Another metric might be the inverse percentage of undesirable answers. + For example, "xyd" is an undesirable answer to the "xyz" problem. + So, if Metacat produced large quantities of "xyd," it would be worse than the legacy copycat. + However, the legacy copycat produces large quantities of "xyd" and small quantities of "wyz". + Given these two discussed metrics, it would be clear that, through our normal science framework, Metacat is superior at solving the "xyz" problem. + + Ideally, this framework can be applied to other copycat variants and on other problems. + Through the lens of this framework, copycat can be evaluated scientifically. + \subsection{Distribution} + \subsubsection{Von Neumann Discussion} + + An objective, scientifically oriented framework is essential to making progress in the domain of cognitive science. + [John Von Neumann: The Computer and the Brain? + He pointed out that there were good grounds merely in terms of electrical analysis to show that the mind, the brain itself, could not be working on a digital system. It did not have enough accuracy; or... it did not have enough memory. ...And he wrote some classical sentences saying there is a statistical language in the brain... different from any other statistical language that we use... this is what we have to discover. ...I think we shall make some progress along the lines of looking for what kind of statistical language would work.] + Notion that the brain obeys statistical, entropical mathematics + + \subsubsection{Turing Completeness} + In a nutshell, because computers are turing complete, it is clear that they can simulate the human brain, given enough power/time. + \subsubsection{Simulation of Distributed Processes} + Despite the ability of computers to simulate the human brain, simulation may not always be accurate unless programmed to be accurate... + \subsubsection{Efficiency of True Distribution} + \subsubsection{Temperature in Copycat} + \subsubsection{Other Centralizers in Copycat} + \subsubsection{The Motivation for Removing Centralizers in Copycat} +\section{Methods} + \subsection{Formula Adjustments} + \subsubsection{Temperature Probability Adjustment} + + This research begin with adjustments to probability weighting formulas. + + In copycat, temperature affects the simulation in multiple ways: + + \begin{enumerate} + \item Certain codelets are probabalistically chosen to run + \item Certain structures are probabalistically chosen to be destroyed + \item ... + \end{enumerate} + + In many cases, the formulas "get-adjusted-probability" and "get-adjusted-value" are used. + Each curves a probability as a function of temperature. + The desired behavior is as follows: + At high temperatures, the system should explore options that would otherwise be unlikely. + So, at temperatures above half of the maximum temperature, probabilities with a base value less than fifty percent will be curved higher, to some threshold. + At temperatures below half of the maximum temperature, probabilities with a base value above fifty percent will be curved lower, to some threshold. + + The original formulas being used to do this were overly complicated. + In summary, many formulas were tested in a spreadsheet, and an optimal one was chosen that replicated the desired behavior. + + The original formula for curving probabilties in copycat: + \lstinputlisting[language=Python]{formulas/original.py} + + An alternative that seems to improve performance on the abd->abd xyz->? problem: + This formula produces probabilities that are not bounded between 0 and 1. These are generally truncated. + \lstinputlisting[language=Python]{formulas/entropy.py} + + Ultimately, it wasn't clear to me that the so-called "xyz" problem should even be considered. + As discussed in [the literature], the "xyz" problem is a novel example of a cognitive obstacle. + Generally, the best techniques for solving the "xyz" problem are discussed in the the publications around the "Metacat" project, which gives copycat a temporary memory and levels of reflection upon its actions. + However, it is possible that the formula changes that target improvement in other problems may produce better results for the "xyz" problem. + Focusing on the "xyz" problem, however, will likely be harmful to the improvement of performanace on other problems. + + So, the original copycat formula is overly complicated, and doesn't perform optimally on several problems. + The entropy formula is an improvement, but other formulas are possible too. + + Below are variations on a "weighted" formula. + The general structure is: + + \[\emph{p'} = \frac{T}{100} * S + \frac{100-T}{100} * U\] + + Where: $S$ is the convergence value for when $T = 0$ and + $U$ is the convergence value for when $T = 100$. + The below formulas simply experiment with different values for $S$ and $U$ + The values of $\alpha$ and $\beta$ can be used to provide additional weighting for the formula, but are not used in this section. + + \lstinputlisting[language=Python]{formulas/weighted.py} + + [Discuss inverse formula and why $S$ was chosen to be constant] + + After some experimentation and reading the original copycat documentation, it was clear that $S$ should be chosen to be $0.5$ and that $U$ should implement the probability curving desired at high temperatures. + The following formulas let $U = p^r$ if $p < 0.5$ and let $U = p^\frac{1}{r}$ if $p >= 0.5$. + This controls whether/when curving happens. + Now, the parameter $r$ simply controls the degree to which curving happens. + Different values of $r$ were experimented with (values between $10$ and $1$ were experimented with at increasingly smaller step sizes). + $2$ and $1.05$ are both good choices at opposite "extremes". + $2$ works because it is large enough to produce novel changes in behavior at extreme temperatures without totally disregarding the original probabilities. + Values above $2$ do not work because they make probabilities too uniform. + Values below $2$ (and above $1.05$) are feasible, but produce less curving and therefore less unique behavior. + $1.05$ works because it very closely replicates the original copycat formulas, providing a very smooth curving. + Values beneath $1.05$ essentially leave probabilities unaffected, producing no significant unique behavior dependent on temperature. + + \lstinputlisting[language=Python]{formulas/best.py} + + Random thought: + It would be interesting to not hardcode the value of $r$, but to instead leave it as a variable between $0$ and $2$ that changes depending on frustration. + However, this would be much like temperature in the first place....? + $r$ could itself be a function of temperature. That would be.... meta.... lol. + + \break + ... + \break + + And ten minutes later, it was done. + The "meta" formula performs as well as the "best" formula on the "ijjkkk" problem, which I consider the most novel. + Interestingly, I noticed that the paramterized formulas aren't as good on this problem. What did I parameterize them for? Was it well justified? + (Probably not) + + At this point, I plan on using the git branch "feature-normal-science-framework" to implement a system that takes in a problem set and provides several answer distributions as output. + Then, I'll do a massive cross-formula answer distribution comparison with $\chi^2$ tests. This will give me an idea about which formula and which changes are best. + I'll also be able to compare all of these answer distributions to the frequencies obtained in temperature removal branches of the repository. + + \subsubsection{Temperature Calculation Adjustment} + \subsubsection{Temperature Usage Adjustment} + \subsection{$\chi^2$ Distribution Testing} +\section{Results} + \subsection{$\chi^2$ Table} +\section{Discussion} + \subsection{Distributed Computation Accuracy} + \subsection{Prediction} + +\bibliographystyle{alpha} +\bibliography{sample} + +\end{document} diff --git a/papers/legacy/legacy.tex b/papers/legacy/legacy.tex new file mode 100644 index 0000000..d399c91 --- /dev/null +++ b/papers/legacy/legacy.tex @@ -0,0 +1,292 @@ + +\section{LSaldyt: Brainstorm, Planning, and Outline} + +\subsection{Steps/plan} + +Normal Science: +\begin{enumerate} + \item Introduce statistical techniques + \item Reduce magic number usage, document reasoning and math + \item Propose effective human subject comparison +\end{enumerate} +Temperature: +\begin{enumerate} + \item Propose formula improvements + \item Experiment with a destructive removal of temperature + \item Experiment with a "surgical" removal of temperature + \item Assess different copycat versions with/without temperature +\end{enumerate} + +\subsection{Semi-structured Notes} + +Biological or psychological plausibility only matters if it actually affects the presence of intelligent processes. For example, neurons don't exist in copycat because we feel that they aren't required to simulate the processes being studied. Instead, copycat uses higher-level structures to simulate the same emergent processes that neurons do. However, codelets and the control of them relies on a global function representing tolerance to irrelevant structures. Other higher level structures in copycat likely rely on globals as well. Another central variable in copycat is the "rule" structure, of which there is only one. While some global variables might be viable, others may actually obstruct the ability to model intelligent processes. For example, a distributed notion of temperature will not only increase biological and psychological plausibility, but increase copycat's effectiveness at producing acceptable answer distributions. + +We must also realize that copycat is only a model, so even if we take goals (level of abstraction) and biological plausibility into account... +It is only worth changing temperature if it affects the model. +Arguably, it does affect the model. (Or, rather, we hypothesize that it does. There is only one way to find out for sure, and that's the point of this paper) + +So, maybe this is a paper about goals, model accuracy, and an attempt to find which cognitive details matter and which don't. It also might provide some insight into making a "Normal Science" framework. + +Copycat is full of random uncommented parameters and formulas. Personally, I would advocate for removing or at least documenting as many of these as possible. In an ideal model, all of the numbers present might be either from existing mathematical formulas, or present for a very good (emergent and explainable - so that no other number would make sense in the same place) reason. However, settling on so called "magic" numbers because the authors of the program believed that their parameterizations were correct is very dangerous. If we removed random magic numbers, we would gain confidence in our model, progress towards a normal science, and gain a better understanding of cognitive processes. + +Similarly, a lot of the testing of copycat is based on human perception of answer distributions. However, I suggest that we move to a more statistical approach. For example, deciding on some arbitrary baseline answer distribution and then modifying copycat to obtain other answer distributions and then comparing distributions with a statistical significance test would actually be indicative of what effect each change had. This paper will include code changes and proposals that lead copycat (and FARG projects in general) to a more statistical and verifiable approach. +While there is a good argument about copycat representing an individual with biases and therefore being incomparable to a distributed group of individuals, I believe that additional effort should be made to test copycat against human subjects. I may include in this paper a concrete proposal on how such an experiment might be done. + +Let's simply test the hypothesis: \[H_i\] Copycat will have an improved (significantly different with increased frequencies of more desirable answers and decreased frequencies of less desirable answers: desirability will be determined by some concrete metric, such as the number of relationships that are preserved or mirrored) answer distribution if temperature is turned to a set of distributed metrics. \[H_0\] Copycat's answer distribution will be unaffected by changing temperature to a set of distributed metrics. + +\subsection{Random Notes} + +This is all just free-flow unstructured notes. Don't take anything too seriously :). + +Below are a list of relevant primary and secondary sources I am reviewing: + +Biological/Psychological Plausibility: +\begin{verbatim} +http://www.cell.com/trends/cognitive-sciences/abstract/S1364-6613(16)30217-0 +"There is no evidence for a single site of working memory storage." +https://ekmillerlab.mit.edu/2017/01/10/the-distributed-nature-of-working-memory/ + +Creativity as a distributed process (SECONDARY: Review primaries) +https://blogs.scientificamerican.com/beautiful-minds/the-real-neuroscience-of-creativity/ +cognition results from the dynamic interactions of distributed brain areas operating in large-scale networks +http://scottbarrykaufman.com/wp-content/uploads/2013/08/Bressler_Large-Scale_Brain_10.pdf + +MIT Encyclopedia of the Cognitive Sciences: +In reference to connectionist models: +"Advantages of distribution are generally held to include greater representational capacity, content addressability, automatic generalization, fault tolerance, and biological plausibility. Disadvantages include slow learning, catastrophic interference, and binding problems." + +Cites: +French, R. (1992). Semi-distributed representation and catastrophic forgetting in connectionist networks. +Smolensky, P. (1991). Connectionism, constituency, and the language of thought. +[...] +\end{verbatim} + +(Sure, we know that the brain is a distributed system, but citing some neuroscience makes me feel much safer.) + +Goal related sources: +\begin{verbatim} +This will all most likely be FARG related stuff +Isolating and enumerating FARG's goals will help show me what direction to take +[..] +\end{verbatim} + +Eliminating global variables might create a program that is more psychologically and biologically plausible, as according to the above. But our goals should be kept in mind. If we wanted full psychological and biological plausibility, we would just replicate a human mind atom for atom, particle for particle, or string for string. + +Levels of abstraction in modeling the human brain and its processes: +\begin{enumerate} + \item Cloning a brain at the smallest scale possible (i.e. preserving quantum states of electrons or something) + \item Simulating true neurons, abstracting away quantum mechanical detail + \item Artificial neurons that abstract away electrochemical detail + \item Simulation of higher-level brain structures and behaviors that transcends individual neurons + ... + \item Highest level of abstraction that still produces intelligent processes +\end{enumerate} + +How far do we plan to go? What are we even abstracting? Which details matter and which don't? + +One side: Abstraction from biological detail may eventually mean that global variables become plausible +Alt: Abstraction may remove some features and not others. Global variables may \emph{never} be plausible, even at the highest level of abstraction. (Of course, this extreme is probably not the case). + +Lack of a centralized structure versus lack of a global phenomena + +Since temperature, for example, is really just a function of several local phenomena, how global is it? I mean: If a centralized decision maker queried local phenomena separately, and made decisions based on that, it would be the same. Maybe centralized decision makers don't exist. Decisions, while seemingly central, have to emerge from agent processes. But what level of abstraction are we working on? + +Clearly, if we knew 100\% which details mattered, we would already have an effective architecture. + + +\section{A formalization of the model} + +Let $\Omega = \{\omega_1, \omega_2, ..., \omega_n\}$ be a finite discrete space. In FARG models $\Omega$ represents the \emph{working short-term memory} of the system and the goal is to craft a context-sensitive representation (cite FRENCH here). Hence $\Omega$ holds \emph{all possible configurations} of objects that could possibly exist in one's working memory; a large space. + +Let us define the neighborhood function $A:(\Omega,$C$) \to 2^\Omega$ as the set of \emph{perceived affordances} under \emph{context} $C$. The affordances $A$ define which state transitions $\omega_i \to \omega_j$ are possible at a particular context $C$. Another term that has been used in the complexity literature is \emph{the adjacent possible}. + +A context is defined by the high-level ideas, the concepts that are active at a particular point in time. + +The \emph{Cohesion} of the system is measured by the mutual information between the external memory, the short-term memory state $\omega_i$, and the context $C$. + +\subsection{Copycat} + +% LUCAS: this entire section is copies from my old "minds and machines" paper... so we should discuss at some point whether to re-write it or not. + +\subsubsection{The letter-analogy domain} + +Consider the following, seemingly trivial, analogy problem: $abc \to abd:ijk \to ?$, that is, if the letter string “abc” changes to the letter string “abd”, how would the letter string “ijk” change “in the same way”? This is the domain of the Copycat project, and before we attempt a full description of the system, let us discuss in more detail some of the underlying intricacies. Most people will in this case come up with a rule of transformation that looks like: “Replace the rightmost letter by its successor in the alphabet”, the application of which would lead to $ijl$. This is a simple and straightforward example. But other examples bring us the full subtlety of this domain. The reader unfamiliar with the Copycat project is invited to consider the following problems: $abc\to abd: ijjkkk?\to $, $abc\to abd: xyz\to ?$, $abc\to abd: mrrkkk\to ?$, among others (Mitchell, 2003) to have a sense of the myriad of subtle intuitions involved in solving these problems. + +To solve this type of problem, one could come up with a scheme where the computer must first find a representation that models the change and then apply that change to the new string. This natural sequence of operations is \emph{not possible}, however, because \emph{the transformation rule representing the change itself must bend to contextual cues and adapt to the particularities of the letter strings}. For example, in the problem $abc\to abd: xyz\to ?$, the system may at first find a rule like “change rightmost letter to its successor in the alphabet”. However, this explicit rule cannot be carried out in this case, simply because $z$ has no successor. This leads to an impasse, out of which the only alternative by the system is to use a flexible, context-sensitive, representation system. + +The reader may have noticed that this cognitive processing bears some similarities to the process of chess perception. Perception obviously plays a significant role in letter string analogies, as it is necessary to connect a set of individual units--in this case, letter sequences--, into a meaningful interpretation which stresses the underlying pressures of the analogy. In chess it is also necessary to connect disparate pieces into a meaningful description stressing the position’s pressures. But the most striking similarities with chess perception (in what concerns bounded rationality) seems to be the absolute lack of a single objectively correct answer, we have instead just an intuitive subjective feeling, given by the great number of simultaneous pressures arising in each problem. + +In the previous section we have made reference to some studies considering multiple, incompatible chunks that emerge in chess positions. In letter strings this same problem appears. Consider for instance the following problem: + +If $aabc\to aabd: ijkk?$ + +\begin{itemize} +\item One may chunk the initial strings as $(a)(abc)$ and $(a)(abd)$ and find a `corresponding’ chunk $(ijk)(k)$, which could lead to the following transformation rule: “change the last letter of the increasing sequence to its successor in the alphabet”. This interpretation would lead to the answer $ijlk$. + +\item Or, alternatively, one may chunk the initial strings as $(aa)(b)(c)$ and $(aa)(b)(d)$ and find a counterpart string with the chunking $(i)(j)(kk)$, and, in this case, the mapping can be inverted: The first letter group $(aa)$ maps to the last letter group $(kk)$, and this will also invert the other mappings, leading to $(b)$ mapping to $(j)$ and $(c)$ mapping to $(i)$. Because this viewpoint substantially stresses the concept `opposite’, Copycat is able to create the transformation rule “change the first letter to its predecessor in the alphabet”, leading to the solution $hjkk$, which preserves symmetry between group letter sizes and between successorship and predecessorship relations. + +\item Other potential transformation rules could lead, in this problem, to $ijkl$ (change the last letter to its successor in the alphabet), $ijll$ (change the last group of letters to its successor in the alphabet), or $jjkk$ (change the first letter to its successor in the alphabet). This problem of many incompatible (and overlapping) chunkings is of importance. The specific chunking of a problem is directly linked to its solution, because chunks stress what is important on the underlying relations. +\end{itemize} + +\subsubsection{The FARG architecture of Copycat} + +How does the Copycat system work? Before reviewing its underlying parts, let us bear in mind one of its principal philosophical points. Copycat is not intended solely as a letter-string analogy program. The intention of the project is the test of a theory; a theory of `statistically emergent active symbols’ (Hofstadter 1979; Hofstadter 1985) which is diametrically opposite to the “symbol system hypothesis” (Newell, 1980; Simon, 1980). The major idea of active symbols is that instead of being tokens passively manipulated by programs, active symbols emerge from high numbers of interdependent subcognitive processes, which swarm over the system and drive its processing by triggering a complex `chain reaction of concepts’. The system is termed `subsymbolic’ because these processes are intended to correspond to subliminal human information processes of few milliseconds, such as a subtle activation of a concept (i.e., priming), or an unconscious urge to look for a particular object. So the models are of collective (or emergent) computation, where a multitude of local processes gradually build a context-sensitive representation of the problem. These symbols are active because they drive processing, leading a chain reaction of activation spreading, in which active concepts continuously trigger related concepts, and short-term memory structures are construed to represent the symbol (in this philosophical view a token does not have any associated meaning, while a meaningful representation, a symbol, emerges from an interlocked interpretation of many subcognitive pressing urges). + +This cognitively plausible architecture has been applied to numerous domains (see for instance French 1992; Mitchell and Hofstadter 1990; Mitchell 1993; McGraw 1995; Marshall 1999; Rehling 2001 MANY ARE MISSING HERE!). It has five principal components: + +\begin{enumerate} + +\item A workspace that interacts with external memory--this is the working short-term memory of the model. The workspace is where the representations are construed, with innumerable pressing urges waiting for attention and their corresponding impulsive processes swarming over the representation, independently perceiving and creating many types of subpatterns. Common examples of such subpatterns are bonds between letters – such as group bonds between $a*a$ or successor bonds between successive letters $a*b$ –, or relations between objects, awareness of abstract roles played by objects, and so on. + +\item Pressing urges and impulsive processes – The computational processes constructing the representations on short-term memory are subcognitive impulsive processes named codelets. The system perceives a great number of subtle pressures that immediately invoke subcognitive urges to handle them. These urges will eventually become impulsive processes. Some of these impulsive processes may look for particular objects, some may look for particular relations between objects and create bonds between them, some may group objects into chunks, or associate descriptions to objects, etc. The collective computation of these impulsive processes, at any given time, stands for the working memory of the model. These processes can be described as impulsive for a number of reasons: first of all, they are involuntary, as there is no conscious decision required for their triggering. (As Daniel Dennett once put it, if I ask you “not to think of an elephant”, it is too late, you already have done so, in an involuntary way.) They are also automatic, as there is no need for conscious decisions to be taken in their internal processing; they simply know how to do their job without asking for help. They are fast, with only a few operations carried out. They accomplish direct connections between their micro-perceptions and their micro-actions. Processing is also granular and fragmented – as opposed to a linearly structured sequence of operations that cannot be interrupted (Linhares 2003). Finally, they are functional, associated with a subpattern, and operate on a subsymbolic level (but not restricted to the manipulation of internal numerical parameters as opposed to most connectionist systems). + + +\item List of parallel priorities— Each impulsive process executes a local, incremental, change to the emerging representation, but the philosophy of the system is that all pressing urges are perceived simultaneously, in parallel. So there is at any point in time a list of subcognitive urges ready to execute, fighting for the attention of the system and waiting probabilistically to fire as an impulsive process. This list of parallel priorities is named in Copycat as the coderack. + +\item A semantic associative network undergoing constant flux– The system has very limited basic knowledge: it knows the 26 letters of the alphabet, and the immediate successorship relations entailed (it does not, for instance, know that the shapes of lowercase letters p, b, q bear some resemblance). The long-term memory of the system is embedded over a network of nodes representing concepts with links between nodes associating related concepts. This network is a crucial part for the formation of the chain reaction of conceptual activation: any specific concept, when activated, propagates activation to its related concepts, which will in turn launch top-down expectation-driven urges to look for those related concepts. This mode of computation not only enforces a context-sensitive search but also is the basis of the chain reaction of activation spreading – hence the term ‘active symbols’. This network is named in Copycat as the slipnet. One of the most original features of the slipnet is the ability to “slip one concept into another”, in which analogies between concepts are made (for details see Hofstadter 1995, Mitchell 1993). + +\item A temperature measure – It should be obvious that the system does not zoom in immediately and directly into a faultless representation. The process of representation construction is gradual, tentative, and numerous impulsive processes are executed erroneously. At start, the system has no expectations of the content of letter strings, so it slowly wanders through many possibilities before converging on an specific interpretation, a process named the parallel terraced scan (Hofstadter 1995); and embedded within it is a control parameter of temperature that is similar in some aspects to that found in simulated annealing (Cagan and Kotovsky 1997; Hofstadter 1995). The temperature measures the global amount of disorder and misunderstanding contained in the situation. So at the beginning of the process, when no relevant information has been gathered, the temperature will be high, but it will gradually decrease as intricate relationships are perceived, first concepts are activated, the abstract roles played by letters and chunks are found; and meaning starts to emerge. Though other authors have proposed a relationship between temperature and understanding (Cagan and Kotovsky, 1997), there is still a crucial difference here (see Hofstadter 1985, 1995): unlike the simulated annealing process that has a forcedly monotonically decreasing temperature schedule, the construction of a representation for these letter strings does not necessarily get monotonically improved as time flows. As in the $abc\to abd : xyz\to ?$ problem, there are many instants when roadblocks are reached, when snags appear, and incompatible structures arise. At these moments, complexity (and entropy and confusion) grows, and so the temperature decrease is not monotonic. + +Finally, temperature does not act as a control parameter dictated by the user, that is, \emph{forced} to go either down or up, but it also acts \emph{as a feedback mechanism} to the system, which may reorganize itself, accepting or rejecting changes as temperature allows. As pressing urges are perceived, their corresponding impulses eventually propose changes to working memory, to construct or to destruct structures. How do these proposed changes get accepted? Through the guidance of temperature. At start $T$ is high and the vast majority of proposed structures are built, but as it decreases it becomes increasingly more important for a proposed change to be compatible with the existing interpretation. And the system may thus focus on developing a particular viewpoint. + +\end{enumerate} + +\begin{figure} +\centering +\includegraphics[width=0.9\textwidth]{fig4-copycat.png} +\caption{\label{fig:run-1}Copycat after 110 codelets have executed. This implementation was carried out by Scott Bolland from the University of Queensland, Australia (2003, available online).} +\end{figure} + +\subsubsection{An example run} + +Let us consider an example run of the Copycat system, and look at some specific steps in its processing of the problem $abc\to abd : iijjkk \to ?$ + + + + +Figure \ref{fig:run-1} presents the working memory (workspace) after 110 codelets. The system at this point has not perceived much structure. It has perceived each individual letter, it has mapped the letters $c$ and $d$ between the original and target strings, and it has perceived some initial bonds between neighboring letters. Some of these bonds are sameness bonds (such as $i*i$), some are successorship bonds (such as $i*j$), and some are predecessorship bonds (such as $b*c$). In fact, there is confusion between the competing views of successorship and predecessorship relations in the string $abc$. These incompatible interpretations will occasionally compete. The system is also mapping the leftmost letter $a$ to the leftmost letter $i$. + + + + +Notice that a first chunk has been created in the group `$jj$'. Now \emph{this chunk is an individual object on its own}, capable of bonding with (and relating to) other objects. Notice also that the system has not yet perceived---and built the corresponding bond between---the two $k$'s in succession. So perception in Copycat is granular, fragmented over large numbers of small `micro-events'. + +\begin{figure} +\centering +\includegraphics[width=0.9\textwidth]{fig5-copycat.png} +\caption{\label{fig:run-2}Copycat’s working memory after the execution of 260 codelets.} +\end{figure} + + +After an additional 150 codelets have been executed (Figure \ref{fig:run-2}), more structure is built: we now have three group chunks perceived; and there is also less confusion in the $abc$, as a `staircase' relation is perceived: that is, the system now perceives $abc$ as a successorship group, another chunked object. Finally, an initial translation rule appears: replace letter category of rightmost letter by successor. If the system were to stop processing at this stage it would apply this rule rather crudely and obtain the answer $iijjkl$. Note that temperature is dropping as more structure is created. + +\begin{figure} +\centering +\includegraphics[width=0.9\textwidth]{fig6-copycat.png} +\caption{\label{fig:run-3}Copycat’s working memory after the execution of 280 codelets. } +\end{figure} + + +Let us slow down our overview a little bit and return in Figure \ref{fig:run-3} after only 20 codelets have run, to illustrate an important phenomenon: though $c$ now will map to the group $kk$, which is an important discovery, the global temperature will still be higher than that of the previous point (Figure \ref{fig:run-2}). This occurs because there is some `confusion' arising from the predecessorship bond which was found between chunks `$ii$' and `$jj$', which does not seem to fit well with all those successorship relations already perceived and with the high activation of the successorship concept. So temperature does not always drop monotonically. + + +\begin{figure} +\centering +\includegraphics[width=0.9\textwidth]{fig7-copycat.png} +\caption{\label{fig:frog}Copycat's working memory the after execution of 415 codelets.} +\end{figure} + +On the next step we can perceive two important changes: first, the system perceives some successorship relations between the groups $ii$ and $jj$ and between the groups $jj$ and $kk$, but these relations are perceived in isolation from each other. Another important discovery is that $jj$ is interpreted as being in `the middle of' $iijjkk$, which will eventually lead to its mapping to the letter $b$ in the original string. + + +\begin{figure} +\centering +\includegraphics[width=0.9\textwidth]{fig8-copycat.png} +\caption{\label{fig:f8}Copycat’s working memory after the execution of 530 codelets.} +\end{figure} + + +\begin{figure} +\centering +\includegraphics[width=0.9\textwidth]{fig9-copycat.png} +\caption{\label{fig:f9}Final solution obtained after the execution of 695 codelets.} +\end{figure} + + +The system finally perceives that the successorship relations between the $ii$, $jj$, and $kk$ groups are not isolated and creates a single successorship group encompassing these three sameness groups. Thus two successor groups are perceived on the workspace, and a mapping between them is built. However, a still maps to the letter $i$, instead of to the group $ii$, and $c$ still maps to the letter $k$, instead of to the group $kk$. + +From this stage it still remains for the letter $a$ to map to the group $ii$ and for the letter $c$ to map to group $kk$, which will lead naturally to the translated rule ``replace letter category of rightmost group to successor'', illustrating the slipping of the concept letter to the concept group. + +After 695 codelets, the system reaches the answer $iijjll$. The workspace may seem very clean and symmetric, but it has evolved from a great deal of disorder and from many microscopic `battles' between incompatible interpretations. + +The most important concepts activated in this example were group and successor group. Once some sameness bonds were constructed, they rapidly activated the concept sameness group which re-inforced the search to find sameness groups, such as $kk$. Once the initial successorship bonds were created, the activation of the corresponding concept rapidly enabled the system to find other instances of successorship relations (between, for instance, the sameness groups $jj$ and $kk$). Different problems would activate other sets of concepts. For example, `$abc\to abd: xyz\to ?$’ would probably activate the concept \emph{opposite}. And `$abc\to abd: mrrjjj\to ?$' would probably activate the concept length (Mitchell 1993). This rapid activation of concepts (and their top-down pressing urges), with the associated propagation of activation to related concepts, creates a chain reaction of impulsive cognition, and is the key to active symbols theory. The reader is refereed to Mitchell (1993) and to Marshall (1999) to have an idea of how the answers provided by Copycat resemble human intuition. + +We may safely conclude at this point that there are many similarities between copycat and the chess perception process, including: (i) an iterative ‘locking in’ process into a representation; (ii) smaller units bond and combine to form higher level, meaningfully coherent structures; (iii) the perception process is fragmented, granular, with great levels of confusion and entropy at start, but as time progresses it is able to gradually converge into a context-sensitive representation; (iv) there is a high interaction between an external memory, a limited size short term memory, and a long term memory; and (v) this interaction is done simultaneously by bottom-up and top-down processes. + +\subsection{How to include Figures} + + +First you have to upload the image file from your computer using the upload link the project menu. Then use the includegraphics command to include it in your document. Use the figure environment and the caption command to add a number and a caption to your figure. See the code for Figure \ref{fig:frog} in this section for an example. + + + + +\subsection{How to add Comments} + +Comments can be added to your project by clicking on the comment icon in the toolbar above. % * 2016-07-03T09:54:16.211Z: +% +% Here's an example comment! +% +To reply to a comment, simply click the reply button in the lower right corner of the comment, and you can close them when you're done. + +Comments can also be added to the margins of the compiled PDF using the todo command\todo{Here's a comment in the margin!}, as shown in the example on the right. You can also add inline comments: + +\todo[inline, color=green!40]{This is an inline comment.} + +\subsection{How to add Tables} + +Use the table and tabular commands for basic tables --- see Table~\ref{tab:widgets}, for example. + +\begin{table} +\centering +\begin{tabular}{l|r} +Item & Quantity \\\hline +Widgets & 42 \\ +Gadgets & 13 +\end{tabular} +\caption{\label{tab:widgets}An example table.} +\end{table} + +\subsection{How to write Mathematics} + +\LaTeX{} is great at typesetting mathematics. Let $X_1, X_2, \ldots, X_n$ be a sequence of independent and identically distributed random variables with $\text{E}[X_i] = \mu$ and $\text{Var}[X_i] = \sigma^2 < \infty$, and let +\[S_n = \frac{X_1 + X_2 + \cdots + X_n}{n} + = \frac{1}{n}\sum_{i}^{n} X_i\] +denote their mean. Then as $n$ approaches infinity, the random variables $\sqrt{n}(S_n - \mu)$ converge in distribution to a normal $\mathcal{N}(0, \sigma^2)$. + + +\subsection{How to create Sections and Subsections} + +Use section and subsections to organize your document. Simply use the section and subsection buttons in the toolbar to create them, and we'll handle all the formatting and numbering automatically. + +\subsection{How to add Lists} + +You can make lists with automatic numbering \dots + +\begin{enumerate} +\item Like this, +\item and like this. +\end{enumerate} +\dots or bullet points \dots +\begin{itemize} +\item Like this, +\item and like this. +\end{itemize} + +\subsection{How to add Citations and a References List} + +You can upload a \verb|.bib| file containing your BibTeX entries, created with JabRef; or import your \href{https://www.overleaf.com/blog/184}{Mendeley}, CiteULike or Zotero library as a \verb|.bib| file. You can then cite entries from it, like this: \cite{greenwade93}. Just remember to specify a bibliography style, as well as the filename of the \verb|.bib|. + +You can find a \href{https://www.overleaf.com/help/97-how-to-include-a-bibliography-using-bibtex}{video tutorial here} to learn more about BibTeX. + +We hope you find Overleaf useful, and please let us know if you have any feedback using the help menu above --- or use the contact form at \url{https://www.overleaf.com/contact}! diff --git a/papers/legacy/paper.tex b/papers/legacy/paper.tex new file mode 100644 index 0000000..f822090 --- /dev/null +++ b/papers/legacy/paper.tex @@ -0,0 +1,339 @@ +\documentclass[a4paper]{article} + +%% Language and font encodings +\usepackage[english]{babel} +\usepackage[utf8x]{inputenc} +\usepackage[T1]{fontenc} + +%% Sets page size and margins +\usepackage[a4paper,top=3cm,bottom=2cm,left=3cm,right=3cm,marginparwidth=1.75cm]{geometry} + +%% Useful packages +\usepackage{listings} +\usepackage{amsmath} +\usepackage{graphicx} +\usepackage[colorinlistoftodos]{todonotes} +\usepackage[colorlinks=true, allcolors=blue]{hyperref} + +\definecolor{lightgrey}{rgb}{0.9, 0.9, 0.9} +\lstset{ % + backgroundcolor=\color{lightgrey}} + +\title{The Distributed Nature of Copycat..? (WIP)} +\author{Lucas Saldyt, Alexandre Linhares} + +\begin{document} +\maketitle + +\begin{abstract} + We investigate the distributed nature of computation in a FARG architecture, Copycat. + One of the foundations of those models is the \emph{Parallel Terraced Scan}--a psychologically-plausible model that enables a system to fluidly move between different modes of processing. + Previous work has modeled decision-making under Parallel Terraced Scan by using a central variable of \emph{Temperature}. + However, it is unlikely that this design decision accurately replicates the processes in the human brain. + + Additionally, Copycat and other FARG architectures have incredible high rates of unscientific inquiry. + Specifically, Copycat uses many undocumented formulas and magic numbers, some of which have been parameterized to fix particular problems at the expense of performing worse on others. + This paper aims to add a framework for conducting so-called "Normal" science with Copycat, in the hopes of making our findings more concrete. + +\end{abstract} + +\section{Introduction} + + This paper stems from Mitchell's (1993) and Hofstadter \& FARG (1995). The goals of this project are twofold: + + Firstly, we focus on effectively simulating intelligent processes through increasingly distributed decision-making. + ... + Written by Linhares: + The Parallel Terraced Scan is a major innovation of FARG architectures. + It corresponds to the psychologically-plausible behavior of briefly browsing, say, a book, and delving deeper whenever something sparks one's interest. + This type of behavior seems to very fluidly change the intensity of an activity based on local, contextual cues. + It is found in high-level decisions such as marriage and low-level decisions such as a foraging predator choosing whether to further explore a particular area. + Previous FARG models have used a central temperature T to implement this behavior. + We explore how to maintain the same behavior while distributing decision-making throughout the system. + ... + Specifically, we begin by attempting different refactors of the copycat architecture. + First, we experiment with different treatments of temperature, adjusting the formulas that depend on it + Then, we experiment with two methods for replacing temperature with a distributed metric, instead. + First, we remove temperature destructively, essentially removing any lines of code that mention it, simply to see what effect it has. + Then, we move toward a surgical removal of temperature, leaving in tact affected structures or replacing them by effective distributed mechanisms. + + Secondly, we focus on the creation of a `normal science' framework in FARG architectures. + By `normal science' we use the term created by Thomas Kuhn--the collaborative enterprise of furthering understanding within a paradigm. + Today, "normal science" is simply not done on FARG architectures (and on most computational cognitive architectures too... see Addyman \& French 2012). + Unlike mathematical theories or experiments, which can be replicated by following the materials and methods, computational models generally have dozens of particularly tuned variables, undocumented procedures, multiple assumptions about the users computational environment, etc. + It then becomes close to impossible to reproduce a result, or to test some new idea. + This paper focuses on the introduction of statistical techniques, reduction of "magic numbers", improvement and documentation of formulas, and proposals for effective human comparison. + + We also discuss, in general, the nature of the brain as a distributed system. + While the removal of a single global variable may initially seem trivial, one must realize that copycat and other cognitive architectures have many central structures. + This paper explores the justification of these central structures in general. + Is it possible to model intelligence with them, or are they harmful? + ... + +\section{Body: Distributed Decision Making and Normal Science} + +\subsection{Distributed Decision Making} + + The distributed nature of decision making is essential to modeling intelligent processes [..] + +\subsection{Normal Science} + + An objective, scientifically oriented framework is essential to making progress in the domain of cognitive science. + [John Von Neumann: The Computer and the Brain? + He pointed out that there were good grounds merely in terms of electrical analysis to show that the mind, the brain itself, could not be working on a digital system. It did not have enough accuracy; or... it did not have enough memory. ...And he wrote some classical sentences saying there is a statistical language in the brain... different from any other statistical language that we use... this is what we have to discover. ...I think we shall make some progress along the lines of looking for what kind of statistical language would work.] + Notion that the brain obeys statistical, entropical mathematics + +\subsection{Notes} + + According to the differences we can enumerate between brains and computers, it is clear that, since computers are universal and have vastly improved in the past five decades, that computers are capable of simulating intelligent processes. + [Cite Von Neumann]. + Primarily, the main obstacle now lies in our comprehension of intelligent processes. + Once we truly understand the brain, writing software that emulates intelligence will be a relatively simple software engineering task. + However, we must be careful to remain true to what we already know about intelligent processes so that we may come closer to learning more about them and eventually replicating them in full. + The largest difference between the computer and the brain is the distributed nature of computation. + Specifically, our computers as they exist today have central processing units, where literally all of computation happens. + On the other hand, our brains have no central location where all processing happens. + Luckily, the speed advantage and universality of computers makes it possible to simulate the distributed behavior of the brain. + However, this simulation is only possible if computers are programmed with concern for the distributed nature of the brain. + [Actually, I go back and forth on this: global variables might be plausible, but likely aren't] + Also, even though the brain is distributed, some clustered processes must take place. + In general, centralized structures should be removed from the copycat software, because they will likely improve the accuracy of simulating intelligent processes. + It isn't clear to what degree this refactor should take place. + The easiest target is the central variable, temperature, but other central structures exist. + This paper focuses primarily on temperature, and the unwanted global unification associated with it. + + Even though copycat uses simulated parallel code, if copycat were actually parallelized, the global variable of temperature would actually prevent most copycat codelets from running at the same time. + If this global variable and other constricting centralized structures were removed, copycat's code would more closely replicate intelligent processes and would be able to be run much faster. + From a function-programming like perspective (i.e. LISP, the original language of copycat), the brain should simply be carrying out the same function in many locations (i.e. mapping neuron.process() across each of its neurons, if you will...) + However, in violating this model with the introduction of global variables...... + + Global variables seem like a construct that people use to model the real world. + ... + + It is entirely possible that at the level of abstraction that copycat uses, global variables are perfectly acceptable. + For example, a quick grep-search of copycat shows that the workspace singleton also exists as a global variable. + Making all of copycat distributed clearly would require a full rewrite of the software.... + + If copycat can be run such that codelets may actually execute at the same time (without pausing to access globals), then it will much better replicate the human brain. + + However, I question the assumption that the human brain has absolutely no centralized processing. + For example, input and output channels (i.e. speech mechanisms) are not accessible from the entire brain. + Also, brain-region science leads me to believe that some (for example, research concerning wernicke's or broca's areas) brain regions truly are "specialized," and thus lend some support to the existence of centralized structures in a computer model of the brain. + However, these centralized structures may be emergent? + + So, to re-iterate: Two possibilities exist (hypotheses) + + A computer model of the brain can contain centralized structures and still be effective in its modeling. + A computer model cannot have any centralzied structures if it is going to be effective in its modeling. + + Another important problem is defining the word "effective". + I suppose that "effective" would mean capable of solving fluid analogy problems, producing similar answers to an identically biased human. + However, it isn't clear to me that removing temperature increases the ability to solve problems effectively. + Is this because models are allowed to have centralized structures, or because temperature isn't the only centralized structure? + + Clearly, creating a model of copycat that doesn't have centralized structures will take an excessive amount of effort. + + \break + ..... + \break + + The calculation for temperature in the first place is extremely convoluted (in the Python version of copycat). + It lacks any documentation, is full of magic numbers, and contains seemingly arbitrary conditionals. + (If I submitted this as a homework assignment, I would probably get a C. Lol) + + Edit: Actually, the lisp version of copycat does a very good job of documenting magic numbers and procedures. + My main complaint is that this hasn't been translated into the Python version of copycat. + However, the Python version is translated from the Java version.. + Lost in translation. + + + My goal isn't to roast copycat's code, however. + Instead, what I see is that all this convolution is \emph{unnecessary}. + Ideally, a future version of copycat, or an underlying FARG architecure will remove this convolution, and make temperature calculation simpler, streamlined, documented, understandble. + How will this happen, though? + + A global description of the system is, at times, potentially useful. + However, in summing together the values of each workspace object, information is lost regarding which workspace objects are offending. + In general, the changes that occur will eventually be object-specific. + So, it seems to me that going from object-specific descriptions to a global description back to an object-specific action is a waste of time. + + I don't think that a global description should be \emph{obliterated} (removed 100\%). + I just think that a global description should be reserved for when global actions are taking place. + For example, when deciding that copycat has found a satisfactory answer, a global description should be used, because deciding to stop copycat is a global action. + However, when deciding to remove a particular structure, a global description should not be used, because removing a particular offending structure is NOT a global action. + + Summary: it is silly to use global information to make local decisions that would be better made using local information (self-evident). + + Benefits of using local information to make local decisions: + + Code can be truly distributed, running in true parallel, CPU-bound. + This means that copycat would be faster and more like a human brain. + Specific structures would be removed based on their own offenses. + This means that relvant structures would remain untouched, which would be great! + Likely, this change to copycat would produce better answer distributions testable through the normal science framework. + + On the other hand (I've never met a one-handed researcher), global description has some benefits. + For example, the global formula for temperature converts the raw importance value for each object into a relative importance value for each object. + If a distributed metric was used, this importance value would have to be left in its raw form. + +\subsubsection{Functional Programming Languages and the Brain} + + The original copycat was written in LISP, a mixed-paradigm language. + Because of LISP's preference for functional code, global variables must be explicitly marked with surrounding asterisks. + Temperature, the workspace, and final answers are all marked global variables as discussed in this paper. + These aspects of copycat are all - by definition - impure, and therefore imperative code that relies on central state changes. + It is clear that, since imperative, mutation-focused languages (like Python) are turing complete in the same way that functional, purity-focused languages (like Haskell) are turing complete, each method is clearly capable of modeling the human brain. + However, the algorithm run by the brain is more similar to distributed, parallel functional code than it is to centralized, serial imperative code. + While there is some centralization in the brain, and evidently some state changes, it is clear that 100\% centralized 100\% serial code is not a good model of the brain. + + Also, temperature is, ultimately, just a function of objects in the global workspace. + The git branch soft-temp-removal hard-removes most usages of temperature, but continues to use a functional version of the temperature calculation for certain processes, like determining if the given answer is satisfactory or not. + So, all mentions of temperature could theoretically be removed and replaced with a dynamic calculation of temperature instead. + It is clear that in this case, this change is unnecessary. + With the goal of creating a distributed model in mind, what actually bothers me more is the global nature of the workspace, coderack, and other singleton copycat structures. + Really, when temperature is removed and replaced with some distributed metric, it is clear that the true "offending" global is the workspace/coderack. + + Alternatively, codelets could be equated to ants in an anthill (see anthill analogy in GEB). + Instead of querying a global structure, codelets could query their neighbors, the same way that ants query their neighbors (rather than, say, relying on instructions from their queen). + +\subsection{Initial Formula Adjustments} + +This research begin with adjustments to probability weighting formulas. + +In copycat, temperature affects the simulation in multiple ways: + +\begin{enumerate} + \item Certain codelets are probabalistically chosen to run + \item Certain structures are probabalistically chosen to be destroyed + \item ... +\end{enumerate} + +In many cases, the formulas "get-adjusted-probability" and "get-adjusted-value" are used. +Each curves a probability as a function of temperature. +The desired behavior is as follows: +At high temperatures, the system should explore options that would otherwise be unlikely. +So, at temperatures above half of the maximum temperature, probabilities with a base value less than fifty percent will be curved higher, to some threshold. +At temperatures below half of the maximum temperature, probabilities with a base value above fifty percent will be curved lower, to some threshold. + +The original formulas being used to do this were overly complicated. +In summary, many formulas were tested in a spreadsheet, and an optimal one was chosen that replicated the desired behavior. + +The original formula for curving probabilties in copycat: +\lstinputlisting[language=Python]{formulas/original.py} + +An alternative that seems to improve performance on the abd->abd xyz->? problem: +This formula produces probabilities that are not bounded between 0 and 1. These are generally truncated. +\lstinputlisting[language=Python]{formulas/entropy.py} + +Ultimately, it wasn't clear to me that the so-called "xyz" problem should even be considered. +As discussed in [the literature], the "xyz" problem is a novel example of a cognitive obstacle. +Generally, the best techniques for solving the "xyz" problem are discussed in the the publications around the "Metacat" project, which gives copycat a temporary memory and levels of reflection upon its actions. +However, it is possible that the formula changes that target improvement in other problems may produce better results for the "xyz" problem. +Focusing on the "xyz" problem, however, will likely be harmful to the improvement of performanace on other problems. + +So, the original copycat formula is overly complicated, and doesn't perform optimally on several problems. +The entropy formula is an improvement, but other formulas are possible too. + +Below are variations on a "weighted" formula. +The general structure is: + +\[\emph{p'} = \frac{T}{100} * S + \frac{100-T}{100} * U\] + +Where: $S$ is the convergence value for when $T = 0$ and + $U$ is the convergence value for when $T = 100$. +The below formulas simply experiment with different values for $S$ and $U$ +The values of $\alpha$ and $\beta$ can be used to provide additional weighting for the formula, but are not used in this section. + +\lstinputlisting[language=Python]{formulas/weighted.py} + +[Discuss inverse formula and why $S$ was chosen to be constant] + +After some experimentation and reading the original copycat documentation, it was clear that $S$ should be chosen to be $0.5$ and that $U$ should implement the probability curving desired at high temperatures. +The following formulas let $U = p^r$ if $p < 0.5$ and let $U = p^\frac{1}{r}$ if $p >= 0.5$. +This controls whether/when curving happens. +Now, the parameter $r$ simply controls the degree to which curving happens. +Different values of $r$ were experimented with (values between $10$ and $1$ were experimented with at increasingly smaller step sizes. +$2$ and $1.05$ are both good choices at opposite "extremes". +$2$ works because it is large enough to produce novel changes in behavior at extreme temperatures without totally disregarding the original probabilities. +Values above $2$ do not work because they make probabilities too uniform. +Values below $2$ (and above $1.05$) are feasible, but produce less curving and therefore less unique behavior. +$1.05$ works because it very closely replicates the original copycat formulas, providing a very smooth curving. +Values beneath $1.05$ essentially leave probabilities unaffected, producing no significant unique behavior dependent on temperature. + +\lstinputlisting[language=Python]{formulas/best.py} + +Random thought: +It would be interesting to not hardcode the value of $r$, but to instead leave it as a variable between $0$ and $2$ that changes depending on frustration. +However, this would be much like temperature in the first place....? +$r$ could itself be a function of temperature. That would be.... meta.... lol. + +\break +... +\break + +And ten minutes later, it was done. +The "meta" formula performs as well as the "best" formula on the "ijjkkk" problem, which I consider the most novel. +Interestingly, I noticed that the paramterized formulas aren't as good on this problem. What did I parameterize them for? Was it well justified? +(Probably not) + +At this point, I plan on using the git branch "feature-normal-science-framework" to implement a system that takes in a problem set and provides several answer distributions as output. +Then, I'll do a massive cross-formula answer distribution comparison with $\chi^2$ tests. This will give me an idea about which formula and which changes are best. +I'll also be able to compare all of these answer distributions to the frequencies obtained in temperature removal branches of the repository. + +\subsection{Steps/plan} + +Normal Science: +\begin{enumerate} + \item Introduce statistical techniques + \item Reduce magic number usage, document reasoning and math + \item Propose effective human subject comparison +\end{enumerate} +Temperature: +\begin{enumerate} + \item Propose formula improvements + \item Experiment with a destructive removal of temperature + \item Experiment with a "surgical" removal of temperature + \item Assess different copycat versions with/without temperature +\end{enumerate} + +\subsection{Semi-structured Notes} + +Biological or psychological plausibility only matters if it actually affects the presence of intelligent processes. For example, neurons don't exist in copycat because we feel that they aren't required to simulate the processes being studied. Instead, copycat uses higher-level structures to simulate the same emergent processes that neurons do. However, codelets and the control of them relies on a global function representing tolerance to irrelevant structures. Other higher level structures in copycat likely rely on globals as well. Another central variable in copycat is the "rule" structure, of which there is only one. While some global variables might be viable, others may actually obstruct the ability to model intelligent processes. For example, a distributed notion of temperature will not only increase biological and psychological plausibility, but increase copycat's effectiveness at producing acceptable answer distributions. + +We must also realize that copycat is only a model, so even if we take goals (level of abstraction) and biological plausibility into account... +It is only worth changing temperature if it affects the model. +Arguably, it does affect the model. (Or, rather, we hypothesize that it does. There is only one way to find out for sure, and that's the point of this paper) + +So, maybe this is a paper about goals, model accuracy, and an attempt to find which cognitive details matter and which don't. It also might provide some insight into making a "Normal Science" framework. + +Copycat is full of random uncommented parameters and formulas. Personally, I would advocate for removing or at least documenting as many of these as possible. In an ideal model, all of the numbers present might be either from existing mathematical formulas, or present for a very good (emergent and explainable - so that no other number would make sense in the same place) reason. However, settling on so called "magic" numbers because the authors of the program believed that their parameterizations were correct is very dangerous. If we removed random magic numbers, we would gain confidence in our model, progress towards a normal science, and gain a better understanding of cognitive processes. + +Similarly, a lot of the testing of copycat is based on human perception of answer distributions. However, I suggest that we move to a more statistical approach. For example, deciding on some arbitrary baseline answer distribution and then modifying copycat to obtain other answer distributions and then comparing distributions with a statistical significance test would actually be indicative of what effect each change had. This paper will include code changes and proposals that lead copycat (and FARG projects in general) to a more statistical and verifiable approach. +While there is a good argument about copycat representing an individual with biases and therefore being incomparable to a distributed group of individuals, I believe that additional effort should be made to test copycat against human subjects. I may include in this paper a concrete proposal on how such an experiment might be done. + +Let's simply test the hypothesis: \[H_i\] Copycat will have an improved (significantly different with increased frequencies of more desirable answers and decreased frequencies of less desirable answers: desirability will be determined by some concrete metric, such as the number of relationships that are preserved or mirrored) answer distribution if temperature is turned to a set of distributed metrics. \[H_0\] Copycat's answer distribution will be unaffected by changing temperature to a set of distributed metrics. + +\subsection{Random Notes} + +This is all just free-flow unstructured notes. Don't take anything too seriously :). + +Below are a list of relevant primary and secondary sources I am reviewing: + +Biological/Psychological Plausibility: +\begin{verbatim} +http://www.cell.com/trends/cognitive-sciences/abstract/S1364-6613(16)30217-0 +"There is no evidence for a single site of working memory storage." +https://ekmillerlab.mit.edu/2017/01/10/the-distributed-nature-of-working-memory/ + +Creativity as a distributed process (SECONDARY: Review primaries) +https://blogs.scientificamerican.com/beautiful-minds/the-real-neuroscience-of-creativity/ +cognition results from the dynamic interactions of distributed brain areas operating in large-scale networks +http://scottbarrykaufman.com/wp-content/uploads/2013/08/Bressler_Large-Scale_Brain_10.pdf + +\end{verbatim} + + +\bibliographystyle{alpha} +\bibliography{sample} + +\end{document} diff --git a/papers/resources/adj.l b/papers/resources/adj.l new file mode 100644 index 0000000..044a674 --- /dev/null +++ b/papers/resources/adj.l @@ -0,0 +1,28 @@ +(defun get-temperature-adjusted-probability (prob &aux low-prob-factor + result) +; This function is a filter: it inputs a value (from 0 to 100) and returns +; a probability (from 0 - 1) based on that value and the temperature. When +; the temperature is 0, the result is (/ value 100), but at higher +; temperatures, values below 50 get raised and values above 50 get lowered +; as a function of temperature. +; I think this whole formula could probably be simplified. + + (setq result + (cond ((= prob 0) 0) + ((<= prob .5) + (setq low-prob-factor (max 1 (truncate (abs (log prob 10))))) + (min (+ prob + (* (/ (- 10 (sqrt (fake-reciprocal *temperature*))) + 100) + (- (expt 10 (- (1- low-prob-factor))) prob))) + .5)) + + ((= prob .5) .5) + ((> prob .5) + (max (- 1 + (+ (- 1 prob) + (* (/ (- 10 (sqrt (fake-reciprocal *temperature*))) + 100) + (- 1 (- 1 prob))))) + .5)))) + result) diff --git a/papers/resources/best.py b/papers/resources/best.py new file mode 100644 index 0000000..395be83 --- /dev/null +++ b/papers/resources/best.py @@ -0,0 +1,21 @@ +def _working_best(temp, prob): + s = .5 # convergence + r = 1.05 # power + u = prob ** r if prob < .5 else prob ** (1/r) + return _weighted(temp, prob, s, u) + +def _soft_best(temp, prob): + s = .5 # convergence + r = 1.05 # power + u = prob ** r if prob < .5 else prob ** (1/r) + return _weighted(temp, prob, s, u) + +def _parameterized_best(temp, prob): + alpha = 5 + beta = 1 + s = .5 + s = (alpha * prob + beta * s) / (alpha + beta) + r = 1.05 + u = prob ** r if prob < .5 else prob ** (1/r) + return _weighted(temp, prob, s, u) + diff --git a/papers/resources/entropy.py b/papers/resources/entropy.py new file mode 100644 index 0000000..8743e1e --- /dev/null +++ b/papers/resources/entropy.py @@ -0,0 +1,12 @@ +import math + +def _entropy(temp, prob): + if prob == 0 or prob == 0.5 or temp == 0: + return prob + if prob < 0.5: + return 1.0 - _original(temp, 1.0 - prob) + coldness = 100.0 - temp + a = math.sqrt(coldness) + c = (10 - a) / 100 + f = (c + 1) * prob + return -f * math.log2(f) diff --git a/papers/resources/final.pdf b/papers/resources/final.pdf new file mode 100644 index 0000000..9e45135 Binary files /dev/null and b/papers/resources/final.pdf differ diff --git a/papers/resources/original.py b/papers/resources/original.py new file mode 100644 index 0000000..c80f482 --- /dev/null +++ b/papers/resources/original.py @@ -0,0 +1,12 @@ +import math + +def _original(temp, prob): + if prob == 0 or prob == 0.5 or temp == 0: + return prob + if prob < 0.5: + return 1.0 - _original(temp, 1.0 - prob) + coldness = 100.0 - temp + a = math.sqrt(coldness) + c = (10 - a) / 100 + f = (c + 1) * prob + return max(f, 0.5) diff --git a/papers/resources/weighted.py b/papers/resources/weighted.py new file mode 100644 index 0000000..77007f9 --- /dev/null +++ b/papers/resources/weighted.py @@ -0,0 +1,28 @@ +def _weighted(temp, prob, s, u): + weighted = (temp / 100) * s + ((100 - temp) / 100) * u + return weighted + +def _weighted_inverse(temp, prob): + iprob = 1 - prob + return _weighted(temp, prob, iprob, prob) + +# Uses .5 instead of 1-prob +def _fifty_converge(temp, prob): + return _weighted(temp, prob, .5, prob) + +# Curves to the average of the (1-p) and .5 +def _soft_curve(temp, prob): + return min(1, _weighted(temp, prob, (1.5-prob)/2, prob)) + +# Curves to the weighted average of the (1-p) and .5 +def _weighted_soft_curve(temp, prob): + weight = 100 + gamma = .5 # convergance value + alpha = 1 # gamma weight + beta = 3 # iprob weight + curved = min(1, + (temp / weight) * + ((alpha * gamma + beta * (1 - prob)) / + (alpha + beta)) + + ((weight - temp) / weight) * prob) + return curved diff --git a/papers/sources.bib b/papers/sources.bib new file mode 100644 index 0000000..5719d69 --- /dev/null +++ b/papers/sources.bib @@ -0,0 +1,55 @@ + +@article{linhares, + author = "Alexandre Linhares", + title = "The emergence of choice: Decision-making and strategic thinking through analogies", + journal = "Information Sciences", + volume = "259", + pages = "36-56", + year = "2014" +} + +@article{compmodeling, + author = "Casper Addyman , Robert M. French", + title = "Computational modeling in cognitive science: a manifesto for change.", + journal = "Topics in Cognitive Science", + year="2012" +} + +@book{analogyasperception, + title = {Analogy Making as Perception}, + author = {Melanie Mitchell}, + isbn = {0-262-13289-3}, + year = {1993}, + publisher = {Massachusetts Institute of Technology} +} + +@book{fluidconcepts, + title={Fluid Concepts and Creative Analogies}, + author={Douglas Hofstadter, FARG}, + isbn={0-465-02475-0}, + year={1995}, + publisher={Basic Books} +} + +@book{computerandthebrain, + title={The Computer \& The Brain}, + author={John Von Neumann}, + isbn={978-0-300-18111-1}, + year={1958}, + publisher={Yale University Press} +} + +@book{geb, + title={Gödel, Escher, Bach: an Eternal Golden Braid}, + author={Douglas Hofstadter}, + isbn={0-456-02656-7}, + year={1979}, + publisher={Basic Books} +} + +@online{knuthwebsite, + author = "Donald Knuth", + title = "Knuth: Computers and Typesetting", + url = "http://www-cs-faculty.stanford.edu/~uno/abcde.html", + keywords = "latex,knuth" +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..db5d81e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +matplotlib +numpy diff --git a/results.txt b/results.txt new file mode 100644 index 0000000..ae0481e --- /dev/null +++ b/results.txt @@ -0,0 +1,802 @@ +-------------------------------------------------------------------------------- + + +distributions/.legacy x distributions/.adj-tests +Problem: abc:abd::efg:_ +Comparing None with pmeta: Warning! Expected 0 counts of efg, but got 1 +Warning! Expected 0 counts of efd, but got 1 +Warning! Expected 0 counts of eff, but got 6 +Warning! Expected 0 counts of ffg, but got 1 +Warning! Expected 0 counts of dfg, but got 2 +Succeeded. +Comparing None with soft: Warning! Expected 0 counts of efg, but got 1 +Warning! Expected 0 counts of eff, but got 6 +Warning! Expected 0 counts of dfg, but got 2 +Succeeded. +Comparing None with entropy: Warning! Expected 0 counts of efg, but got 1 +Warning! Expected 0 counts of efd, but got 1 +Warning! Expected 0 counts of eff, but got 6 +Warning! Expected 0 counts of ffg, but got 1 +Warning! Expected 0 counts of dfg, but got 2 +Succeeded. +Comparing None with meta: Warning! Expected 0 counts of efg, but got 1 +Warning! Expected 0 counts of efd, but got 1 +Warning! Expected 0 counts of eff, but got 6 +Warning! Expected 0 counts of dfg, but got 2 +Succeeded. +Comparing None with fifty_converge: Warning! Expected 0 counts of efg, but got 1 +Warning! Expected 0 counts of efd, but got 1 +Warning! Expected 0 counts of eff, but got 6 +Warning! Expected 0 counts of dfg, but got 2 +Succeeded. +Comparing None with sbest: Warning! Expected 0 counts of efg, but got 1 +Warning! Expected 0 counts of eff, but got 6 +Warning! Expected 0 counts of ffg, but got 1 +Warning! Expected 0 counts of dfg, but got 2 +Succeeded. +Comparing None with average_alt: Warning! Expected 0 counts of efg, but got 1 +Warning! Expected 0 counts of efd, but got 1 +Warning! Expected 0 counts of eff, but got 6 +Warning! Expected 0 counts of dfg, but got 2 +Succeeded. +Comparing None with weighted_soft: Warning! Expected 0 counts of efg, but got 1 +Warning! Expected 0 counts of ffg, but got 1 +Warning! Expected 0 counts of dfg, but got 2 +Failed. +Comparing None with inverse: Warning! Expected 0 counts of eff, but got 6 +Warning! Expected 0 counts of dfg, but got 2 +Succeeded. +Comparing None with alt_fifty: Warning! Expected 0 counts of efg, but got 1 +Warning! Expected 0 counts of eff, but got 6 +Warning! Expected 0 counts of ffg, but got 1 +Warning! Expected 0 counts of dfg, but got 2 +Succeeded. +Comparing None with best: Warning! Expected 0 counts of efg, but got 1 +Warning! Expected 0 counts of eff, but got 6 +Warning! Expected 0 counts of ffg, but got 1 +Warning! Expected 0 counts of dfg, but got 2 +Succeeded. +Comparing None with none: Warning! Expected 0 counts of efg, but got 1 +Warning! Expected 0 counts of eff, but got 6 +Warning! Expected 0 counts of ffg, but got 1 +Warning! Expected 0 counts of dfg, but got 2 +Succeeded. +Comparing None with pbest: Warning! Expected 0 counts of efg, but got 1 +Warning! Expected 0 counts of efd, but got 1 +Warning! Expected 0 counts of eff, but got 6 +Warning! Expected 0 counts of ffg, but got 1 +Warning! Expected 0 counts of dfg, but got 2 +Succeeded. +Comparing None with original: Warning! Expected 0 counts of efg, but got 1 +Warning! Expected 0 counts of eff, but got 6 +Warning! Expected 0 counts of ffg, but got 1 +Warning! Expected 0 counts of dfg, but got 2 +Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.legacy x distributions/.adj-tests +Problem: abc:abd::ijk:_ +Comparing None with pmeta: Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of ijj, but got 4 +Warning! Expected 0 counts of jjk, but got 4 +Succeeded. +Comparing None with soft: Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of ijj, but got 4 +Failed. +Comparing None with entropy: Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of jjk, but got 4 +Failed. +Comparing None with meta: Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of ijd, but got 1 +Warning! Expected 0 counts of ijj, but got 4 +Warning! Expected 0 counts of jjk, but got 4 +Succeeded. +Comparing None with fifty_converge: Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of ijj, but got 4 +Warning! Expected 0 counts of jjk, but got 4 +Succeeded. +Comparing None with sbest: Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of ijd, but got 1 +Warning! Expected 0 counts of ijj, but got 4 +Failed. +Comparing None with average_alt: Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of ijj, but got 4 +Warning! Expected 0 counts of jjk, but got 4 +Succeeded. +Comparing None with weighted_soft: Warning! Expected 0 counts of ijj, but got 4 +Succeeded. +Comparing None with inverse: Warning! Expected 0 counts of ijd, but got 1 +Failed. +Comparing None with alt_fifty: Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of ijj, but got 4 +Warning! Expected 0 counts of jjk, but got 4 +Succeeded. +Comparing None with best: Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of ijd, but got 1 +Warning! Expected 0 counts of ijj, but got 4 +Warning! Expected 0 counts of jjk, but got 4 +Succeeded. +Comparing None with none: Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of ijj, but got 4 +Failed. +Comparing None with pbest: Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of ijj, but got 4 +Warning! Expected 0 counts of jjk, but got 4 +Succeeded. +Comparing None with original: Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of ijj, but got 4 +Warning! Expected 0 counts of jjk, but got 4 +Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.legacy x distributions/.adj-tests +Problem: abc:abd::xyz:_ +Comparing None with pmeta: Warning! Expected 0 counts of xyy, but got 5 +Warning! Expected 0 counts of wyz, but got 9 +Warning! Expected 0 counts of xd, but got 1 +Warning! Expected 0 counts of xyz, but got 5 +Failed. +Comparing None with soft: Warning! Expected 0 counts of xd, but got 1 +Failed. +Comparing None with entropy: Warning! Expected 0 counts of wyz, but got 9 +Warning! Expected 0 counts of xd, but got 1 +Warning! Expected 0 counts of dyz, but got 1 +Failed. +Comparing None with meta: Warning! Expected 0 counts of xyy, but got 5 +Warning! Expected 0 counts of xd, but got 1 +Warning! Expected 0 counts of dyz, but got 1 +Warning! Expected 0 counts of xyz, but got 5 +Failed. +Comparing None with fifty_converge: Warning! Expected 0 counts of xyy, but got 5 +Warning! Expected 0 counts of wyz, but got 9 +Warning! Expected 0 counts of xd, but got 1 +Warning! Expected 0 counts of dyz, but got 1 +Warning! Expected 0 counts of xyz, but got 5 +Failed. +Comparing None with sbest: Warning! Expected 0 counts of xyy, but got 5 +Warning! Expected 0 counts of wyz, but got 9 +Warning! Expected 0 counts of xd, but got 1 +Warning! Expected 0 counts of dyz, but got 1 +Warning! Expected 0 counts of xyz, but got 5 +Failed. +Comparing None with average_alt: Warning! Expected 0 counts of xd, but got 1 +Warning! Expected 0 counts of dyz, but got 1 +Warning! Expected 0 counts of xyz, but got 5 +Failed. +Comparing None with weighted_soft: Warning! Expected 0 counts of xd, but got 1 +Warning! Expected 0 counts of xyz, but got 5 +Failed. +Comparing None with inverse: Warning! Expected 0 counts of xd, but got 1 +Failed. +Comparing None with alt_fifty: Warning! Expected 0 counts of xyy, but got 5 +Warning! Expected 0 counts of xd, but got 1 +Warning! Expected 0 counts of dyz, but got 1 +Failed. +Comparing None with best: Warning! Expected 0 counts of xd, but got 1 +Warning! Expected 0 counts of dyz, but got 1 +Warning! Expected 0 counts of xyz, but got 5 +Failed. +Comparing None with none: Warning! Expected 0 counts of xyy, but got 5 +Warning! Expected 0 counts of wyz, but got 9 +Warning! Expected 0 counts of xd, but got 1 +Warning! Expected 0 counts of yyz, but got 9 +Warning! Expected 0 counts of dyz, but got 1 +Warning! Expected 0 counts of xyz, but got 5 +Failed. +Comparing None with pbest: Warning! Expected 0 counts of xyy, but got 5 +Warning! Expected 0 counts of wyz, but got 9 +Warning! Expected 0 counts of xd, but got 1 +Warning! Expected 0 counts of yyz, but got 9 +Warning! Expected 0 counts of dyz, but got 1 +Warning! Expected 0 counts of xyz, but got 5 +Failed. +Comparing None with original: Warning! Expected 0 counts of xyy, but got 5 +Warning! Expected 0 counts of wyz, but got 9 +Warning! Expected 0 counts of xd, but got 1 +Warning! Expected 0 counts of dyz, but got 1 +Warning! Expected 0 counts of xyz, but got 5 +Failed. + + +-------------------------------------------------------------------------------- + + +distributions/.legacy x distributions/.adj-tests +Problem: abc:abd::ijkk:_ +Comparing None with pmeta: Warning! Expected 0 counts of ijkd, but got 1 +Warning! Expected 0 counts of ijkk, but got 18 +Failed. +Comparing None with soft: Warning! Expected 0 counts of ijkd, but got 1 +Warning! Expected 0 counts of ijkk, but got 18 +Failed. +Comparing None with entropy: Warning! Expected 0 counts of ijkk, but got 18 +Failed. +Comparing None with meta: Warning! Expected 0 counts of ijkd, but got 1 +Warning! Expected 0 counts of ijkk, but got 18 +Failed. +Comparing None with fifty_converge: Warning! Expected 0 counts of ijkd, but got 1 +Warning! Expected 0 counts of ijkk, but got 18 +Failed. +Comparing None with sbest: Warning! Expected 0 counts of ijkd, but got 1 +Warning! Expected 0 counts of ijkk, but got 18 +Failed. +Comparing None with average_alt: Warning! Expected 0 counts of ijkd, but got 1 +Warning! Expected 0 counts of ijkk, but got 18 +Failed. +Comparing None with weighted_soft: Warning! Expected 0 counts of ijkk, but got 18 +Failed. +Comparing None with inverse: Warning! Expected 0 counts of ijkd, but got 1 +Warning! Expected 0 counts of ijkk, but got 18 +Failed. +Comparing None with alt_fifty: Warning! Expected 0 counts of ijkd, but got 1 +Warning! Expected 0 counts of ijkk, but got 18 +Failed. +Comparing None with best: Warning! Expected 0 counts of ijkd, but got 1 +Warning! Expected 0 counts of ijkk, but got 18 +Failed. +Comparing None with none: Warning! Expected 0 counts of ijkk, but got 18 +Failed. +Comparing None with pbest: Warning! Expected 0 counts of ijkd, but got 1 +Warning! Expected 0 counts of ijkk, but got 18 +Failed. +Comparing None with original: Warning! Expected 0 counts of ijkd, but got 1 +Warning! Expected 0 counts of ijkk, but got 18 +Failed. + + +-------------------------------------------------------------------------------- + + +distributions/.legacy x distributions/.adj-tests +Problem: abc:abd::mrrjjj:_ +Comparing None with pmeta: Warning! Expected 0 counts of nrrjjj, but got 8 +Warning! Expected 0 counts of mrrjjd, but got 1 +Failed. +Comparing None with soft: Warning! Expected 0 counts of nrrjjj, but got 8 +Warning! Expected 0 counts of mrrjjd, but got 1 +Failed. +Comparing None with entropy: Warning! Expected 0 counts of nrrjjj, but got 8 +Warning! Expected 0 counts of mrrjjd, but got 1 +Succeeded. +Comparing None with meta: Warning! Expected 0 counts of nrrjjj, but got 8 +Warning! Expected 0 counts of mrrjjd, but got 1 +Failed. +Comparing None with fifty_converge: Warning! Expected 0 counts of nrrjjj, but got 8 +Warning! Expected 0 counts of mrrjjd, but got 1 +Failed. +Comparing None with sbest: Warning! Expected 0 counts of nrrjjj, but got 8 +Failed. +Comparing None with average_alt: Warning! Expected 0 counts of nrrjjj, but got 8 +Failed. +Comparing None with weighted_soft: Warning! Expected 0 counts of mrrjjd, but got 1 +Failed. +Comparing None with inverse: Failed. +Comparing None with alt_fifty: Warning! Expected 0 counts of nrrjjj, but got 8 +Failed. +Comparing None with best: Warning! Expected 0 counts of nrrjjj, but got 8 +Warning! Expected 0 counts of mrrjjd, but got 1 +Failed. +Comparing None with none: Warning! Expected 0 counts of nrrjjj, but got 8 +Failed. +Comparing None with pbest: Warning! Expected 0 counts of nrrjjj, but got 8 +Warning! Expected 0 counts of mrrjjd, but got 1 +Failed. +Comparing None with original: Warning! Expected 0 counts of nrrjjj, but got 8 +Warning! Expected 0 counts of mrrjjd, but got 1 +Failed. + + +-------------------------------------------------------------------------------- + + +distributions/.legacy x distributions/.nuke-temp +Problem: abc:abd::efg:_ +Comparing None with None: Warning! Expected 0 counts of efg, but got 1 +Warning! Expected 0 counts of eff, but got 6 +Warning! Expected 0 counts of dfg, but got 2 +Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.legacy x distributions/.nuke-temp +Problem: abc:abd::ijk:_ +Comparing None with None: Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of ijj, but got 4 +Warning! Expected 0 counts of jjk, but got 4 +Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.legacy x distributions/.nuke-temp +Problem: abc:abd::xyz:_ +Comparing None with None: Warning! Expected 0 counts of xyy, but got 5 +Warning! Expected 0 counts of wyz, but got 9 +Warning! Expected 0 counts of xd, but got 1 +Warning! Expected 0 counts of yyz, but got 9 +Warning! Expected 0 counts of dyz, but got 1 +Warning! Expected 0 counts of xyz, but got 5 +Failed. + + +-------------------------------------------------------------------------------- + + +distributions/.legacy x distributions/.nuke-temp +Problem: abc:abd::ijkk:_ +Comparing None with None: Warning! Expected 0 counts of ijkk, but got 18 +Failed. + + +-------------------------------------------------------------------------------- + + +distributions/.legacy x distributions/.nuke-temp +Problem: abc:abd::mrrjjj:_ +Comparing None with None: Failed. + + +-------------------------------------------------------------------------------- + + +distributions/.legacy x distributions/.soft-remove +Problem: abc:abd::efg:_ +Comparing None with None: Warning! Expected 0 counts of efg, but got 1 +Warning! Expected 0 counts of eff, but got 6 +Warning! Expected 0 counts of ffg, but got 1 +Warning! Expected 0 counts of dfg, but got 2 +Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.legacy x distributions/.soft-remove +Problem: abc:abd::ijk:_ +Comparing None with None: Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of ijj, but got 4 +Warning! Expected 0 counts of jjk, but got 4 +Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.legacy x distributions/.soft-remove +Problem: abc:abd::xyz:_ +Comparing None with None: Warning! Expected 0 counts of xyy, but got 5 +Warning! Expected 0 counts of wyz, but got 9 +Warning! Expected 0 counts of xd, but got 1 +Warning! Expected 0 counts of dyz, but got 1 +Warning! Expected 0 counts of xyz, but got 5 +Failed. + + +-------------------------------------------------------------------------------- + + +distributions/.legacy x distributions/.soft-remove +Problem: abc:abd::ijkk:_ +Comparing None with None: Warning! Expected 0 counts of ijkk, but got 18 +Failed. + + +-------------------------------------------------------------------------------- + + +distributions/.legacy x distributions/.soft-remove +Problem: abc:abd::mrrjjj:_ +Comparing None with None: Warning! Expected 0 counts of nrrjjj, but got 8 +Warning! Expected 0 counts of mrrjjd, but got 1 +Failed. + + +-------------------------------------------------------------------------------- + + +distributions/.adj-tests x distributions/.nuke-temp +Problem: abc:abd::efg:_ +Comparing pmeta with None: Succeeded. +Comparing soft with None: Succeeded. +Comparing entropy with None: Succeeded. +Comparing meta with None: Succeeded. +Comparing fifty_converge with None: Succeeded. +Comparing sbest with None: Succeeded. +Comparing average_alt with None: Succeeded. +Comparing weighted_soft with None: Warning! Expected 0 counts of eff, but got 1 +Succeeded. +Comparing inverse with None: Warning! Expected 0 counts of efg, but got 1 +Succeeded. +Comparing alt_fifty with None: Succeeded. +Comparing best with None: Succeeded. +Comparing none with None: Succeeded. +Comparing pbest with None: Succeeded. +Comparing original with None: Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.adj-tests x distributions/.nuke-temp +Problem: abc:abd::ijk:_ +Comparing pmeta with None: Succeeded. +Comparing soft with None: Warning! Expected 0 counts of jjk, but got 1 +Succeeded. +Comparing entropy with None: Warning! Expected 0 counts of ijj, but got 1 +Succeeded. +Comparing meta with None: Succeeded. +Comparing fifty_converge with None: Succeeded. +Comparing sbest with None: Warning! Expected 0 counts of jjk, but got 1 +Succeeded. +Comparing average_alt with None: Succeeded. +Comparing weighted_soft with None: Warning! Expected 0 counts of djk, but got 2 +Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of jjk, but got 1 +Succeeded. +Comparing inverse with None: Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of ijj, but got 1 +Warning! Expected 0 counts of jjk, but got 2 +Succeeded. +Comparing alt_fifty with None: Succeeded. +Comparing best with None: Succeeded. +Comparing none with None: Warning! Expected 0 counts of jjk, but got 1 +Succeeded. +Comparing pbest with None: Succeeded. +Comparing original with None: Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.adj-tests x distributions/.nuke-temp +Problem: abc:abd::xyz:_ +Comparing pmeta with None: Warning! Expected 0 counts of yyz, but got 1 +Warning! Expected 0 counts of dyz, but got 1 +Succeeded. +Comparing soft with None: Warning! Expected 0 counts of xyy, but got 1 +Warning! Expected 0 counts of wyz, but got 3 +Warning! Expected 0 counts of yyz, but got 2 +Warning! Expected 0 counts of dyz, but got 2 +Warning! Expected 0 counts of xyz, but got 1 +Succeeded. +Comparing entropy with None: Warning! Expected 0 counts of yyz, but got 2 +Warning! Expected 0 counts of xyy, but got 1 +Warning! Expected 0 counts of xyz, but got 1 +Succeeded. +Comparing meta with None: Warning! Expected 0 counts of yyz, but got 2 +Warning! Expected 0 counts of wyz, but got 1 +Succeeded. +Comparing fifty_converge with None: Warning! Expected 0 counts of yyz, but got 1 +Succeeded. +Comparing sbest with None: Warning! Expected 0 counts of yyz, but got 1 +Succeeded. +Comparing average_alt with None: Warning! Expected 0 counts of yyz, but got 1 +Warning! Expected 0 counts of xyy, but got 2 +Warning! Expected 0 counts of wyz, but got 2 +Succeeded. +Comparing weighted_soft with None: Warning! Expected 0 counts of yyz, but got 4 +Warning! Expected 0 counts of xyy, but got 3 +Warning! Expected 0 counts of dyz, but got 1 +Warning! Expected 0 counts of wyz, but got 3 +Succeeded. +Comparing inverse with None: Warning! Expected 0 counts of xyy, but got 1 +Warning! Expected 0 counts of wyz, but got 4 +Warning! Expected 0 counts of yyz, but got 9 +Warning! Expected 0 counts of dyz, but got 1 +Warning! Expected 0 counts of xyz, but got 1 +Succeeded. +Comparing alt_fifty with None: Warning! Expected 0 counts of yyz, but got 2 +Warning! Expected 0 counts of wyz, but got 2 +Warning! Expected 0 counts of xyz, but got 1 +Succeeded. +Comparing best with None: Warning! Expected 0 counts of yyz, but got 1 +Warning! Expected 0 counts of xyy, but got 1 +Warning! Expected 0 counts of wyz, but got 1 +Succeeded. +Comparing none with None: Succeeded. +Comparing pbest with None: Succeeded. +Comparing original with None: Warning! Expected 0 counts of yyz, but got 1 +Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.adj-tests x distributions/.nuke-temp +Problem: abc:abd::ijkk:_ +Comparing pmeta with None: Warning! Expected 0 counts of ijll, but got 13 +Succeeded. +Comparing soft with None: Warning! Expected 0 counts of ijll, but got 17 +Failed. +Comparing entropy with None: Succeeded. +Comparing meta with None: Warning! Expected 0 counts of ijkkk, but got 1 +Warning! Expected 0 counts of ijll, but got 11 +Succeeded. +Comparing fifty_converge with None: Warning! Expected 0 counts of ijll, but got 15 +Failed. +Comparing sbest with None: Warning! Expected 0 counts of ijll, but got 19 +Failed. +Comparing average_alt with None: Warning! Expected 0 counts of ijkkk, but got 1 +Warning! Expected 0 counts of ijll, but got 11 +Succeeded. +Comparing weighted_soft with None: Warning! Expected 0 counts of djkk, but got 1 +Warning! Expected 0 counts of ijll, but got 10 +Succeeded. +Comparing inverse with None: Warning! Expected 0 counts of djkk, but got 1 +Warning! Expected 0 counts of jjkk, but got 1 +Warning! Expected 0 counts of ijll, but got 11 +Succeeded. +Comparing alt_fifty with None: Warning! Expected 0 counts of ijkkk, but got 2 +Warning! Expected 0 counts of ijll, but got 15 +Failed. +Comparing best with None: Warning! Expected 0 counts of ijll, but got 18 +Failed. +Comparing none with None: Warning! Expected 0 counts of ijll, but got 16 +Failed. +Comparing pbest with None: Warning! Expected 0 counts of ijkkk, but got 2 +Warning! Expected 0 counts of ijll, but got 17 +Failed. +Comparing original with None: Warning! Expected 0 counts of ijll, but got 13 +Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.adj-tests x distributions/.nuke-temp +Problem: abc:abd::mrrjjj:_ +Comparing pmeta with None: Warning! Expected 0 counts of mrrkkk, but got 24 +Warning! Expected 0 counts of mrrjjjj, but got 4 +Failed. +Comparing soft with None: Warning! Expected 0 counts of mrrkkk, but got 10 +Warning! Expected 0 counts of mrrjjjj, but got 5 +Succeeded. +Comparing entropy with None: Succeeded. +Comparing meta with None: Warning! Expected 0 counts of mrrjjjj, but got 7 +Warning! Expected 0 counts of mrrd, but got 1 +Warning! Expected 0 counts of mrrkkk, but got 12 +Failed. +Comparing fifty_converge with None: Warning! Expected 0 counts of mrrkkk, but got 13 +Warning! Expected 0 counts of mrrjjjj, but got 5 +Failed. +Comparing sbest with None: Warning! Expected 0 counts of mrrkkk, but got 7 +Warning! Expected 0 counts of mrrjjjj, but got 8 +Succeeded. +Comparing average_alt with None: Warning! Expected 0 counts of mrrkkk, but got 11 +Warning! Expected 0 counts of mrrjjjj, but got 2 +Succeeded. +Comparing weighted_soft with None: Warning! Expected 0 counts of mrrjjjj, but got 2 +Warning! Expected 0 counts of mrrkkk, but got 14 +Warning! Expected 0 counts of mrrjkk, but got 2 +Succeeded. +Comparing inverse with None: Warning! Expected 0 counts of mrrjjjj, but got 1 +Warning! Expected 0 counts of mrrkkk, but got 9 +Warning! Expected 0 counts of mrrjkk, but got 1 +Succeeded. +Comparing alt_fifty with None: Warning! Expected 0 counts of mrrkkk, but got 13 +Warning! Expected 0 counts of mrrjjjj, but got 7 +Failed. +Comparing best with None: Warning! Expected 0 counts of mrrkkk, but got 13 +Warning! Expected 0 counts of mrrjjjj, but got 4 +Succeeded. +Comparing none with None: Warning! Expected 0 counts of mrrkkk, but got 25 +Failed. +Comparing pbest with None: Warning! Expected 0 counts of mrrd, but got 1 +Warning! Expected 0 counts of mrrjjjj, but got 4 +Warning! Expected 0 counts of mrrkkk, but got 18 +Failed. +Comparing original with None: Warning! Expected 0 counts of mrrkkk, but got 25 +Failed. + + +-------------------------------------------------------------------------------- + + +distributions/.adj-tests x distributions/.soft-remove +Problem: abc:abd::efg:_ +Comparing pmeta with None: Succeeded. +Comparing soft with None: Warning! Expected 0 counts of ffg, but got 1 +Succeeded. +Comparing entropy with None: Succeeded. +Comparing meta with None: Warning! Expected 0 counts of ffg, but got 1 +Succeeded. +Comparing fifty_converge with None: Warning! Expected 0 counts of ffg, but got 1 +Succeeded. +Comparing sbest with None: Succeeded. +Comparing average_alt with None: Warning! Expected 0 counts of ffg, but got 1 +Succeeded. +Comparing weighted_soft with None: Warning! Expected 0 counts of eff, but got 1 +Succeeded. +Comparing inverse with None: Warning! Expected 0 counts of efg, but got 1 +Warning! Expected 0 counts of ffg, but got 2 +Succeeded. +Comparing alt_fifty with None: Succeeded. +Comparing best with None: Succeeded. +Comparing none with None: Succeeded. +Comparing pbest with None: Succeeded. +Comparing original with None: Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.adj-tests x distributions/.soft-remove +Problem: abc:abd::ijk:_ +Comparing pmeta with None: Succeeded. +Comparing soft with None: Warning! Expected 0 counts of jjk, but got 1 +Succeeded. +Comparing entropy with None: Warning! Expected 0 counts of ijj, but got 1 +Succeeded. +Comparing meta with None: Succeeded. +Comparing fifty_converge with None: Succeeded. +Comparing sbest with None: Warning! Expected 0 counts of jjk, but got 1 +Succeeded. +Comparing average_alt with None: Succeeded. +Comparing weighted_soft with None: Warning! Expected 0 counts of djk, but got 2 +Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of jjk, but got 1 +Succeeded. +Comparing inverse with None: Warning! Expected 0 counts of hjk, but got 1 +Warning! Expected 0 counts of ijj, but got 1 +Warning! Expected 0 counts of jjk, but got 2 +Succeeded. +Comparing alt_fifty with None: Succeeded. +Comparing best with None: Succeeded. +Comparing none with None: Warning! Expected 0 counts of jjk, but got 1 +Succeeded. +Comparing pbest with None: Succeeded. +Comparing original with None: Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.adj-tests x distributions/.soft-remove +Problem: abc:abd::xyz:_ +Comparing pmeta with None: Warning! Expected 0 counts of dyz, but got 1 +Succeeded. +Comparing soft with None: Warning! Expected 0 counts of xyy, but got 1 +Warning! Expected 0 counts of wyz, but got 3 +Warning! Expected 0 counts of dyz, but got 2 +Warning! Expected 0 counts of xyz, but got 1 +Succeeded. +Comparing entropy with None: Warning! Expected 0 counts of xyy, but got 1 +Warning! Expected 0 counts of xyz, but got 1 +Succeeded. +Comparing meta with None: Warning! Expected 0 counts of wyz, but got 1 +Succeeded. +Comparing fifty_converge with None: Succeeded. +Comparing sbest with None: Succeeded. +Comparing average_alt with None: Warning! Expected 0 counts of xyy, but got 2 +Warning! Expected 0 counts of wyz, but got 2 +Succeeded. +Comparing weighted_soft with None: Warning! Expected 0 counts of xyy, but got 3 +Warning! Expected 0 counts of dyz, but got 1 +Warning! Expected 0 counts of wyz, but got 3 +Failed. +Comparing inverse with None: Warning! Expected 0 counts of xyy, but got 1 +Warning! Expected 0 counts of wyz, but got 4 +Warning! Expected 0 counts of dyz, but got 1 +Warning! Expected 0 counts of xyz, but got 1 +Failed. +Comparing alt_fifty with None: Warning! Expected 0 counts of wyz, but got 2 +Warning! Expected 0 counts of xyz, but got 1 +Succeeded. +Comparing best with None: Warning! Expected 0 counts of xyy, but got 1 +Warning! Expected 0 counts of wyz, but got 1 +Succeeded. +Comparing none with None: Succeeded. +Comparing pbest with None: Succeeded. +Comparing original with None: Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.adj-tests x distributions/.soft-remove +Problem: abc:abd::ijkk:_ +Comparing pmeta with None: Succeeded. +Comparing soft with None: Failed. +Comparing entropy with None: Failed. +Comparing meta with None: Warning! Expected 0 counts of ijkkk, but got 1 +Succeeded. +Comparing fifty_converge with None: Succeeded. +Comparing sbest with None: Failed. +Comparing average_alt with None: Warning! Expected 0 counts of ijkkk, but got 1 +Succeeded. +Comparing weighted_soft with None: Warning! Expected 0 counts of djkk, but got 1 +Succeeded. +Comparing inverse with None: Warning! Expected 0 counts of djkk, but got 1 +Warning! Expected 0 counts of jjkk, but got 1 +Succeeded. +Comparing alt_fifty with None: Warning! Expected 0 counts of ijkkk, but got 2 +Succeeded. +Comparing best with None: Failed. +Comparing none with None: Succeeded. +Comparing pbest with None: Warning! Expected 0 counts of ijkkk, but got 2 +Failed. +Comparing original with None: Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.adj-tests x distributions/.soft-remove +Problem: abc:abd::mrrjjj:_ +Comparing pmeta with None: Failed. +Comparing soft with None: Failed. +Comparing entropy with None: Failed. +Comparing meta with None: Failed. +Comparing fifty_converge with None: Failed. +Comparing sbest with None: Warning! Expected 0 counts of mrrjjd, but got 1 +Failed. +Comparing average_alt with None: Warning! Expected 0 counts of mrrjjd, but got 1 +Failed. +Comparing weighted_soft with None: Warning! Expected 0 counts of nrrjjj, but got 1 +Warning! Expected 0 counts of mrrjkk, but got 2 +Succeeded. +Comparing inverse with None: Warning! Expected 0 counts of nrrjjj, but got 1 +Warning! Expected 0 counts of mrrjkk, but got 1 +Warning! Expected 0 counts of mrrjjd, but got 3 +Succeeded. +Comparing alt_fifty with None: Warning! Expected 0 counts of mrrjjd, but got 1 +Failed. +Comparing best with None: Succeeded. +Comparing none with None: Warning! Expected 0 counts of mrrjjd, but got 1 +Succeeded. +Comparing pbest with None: Succeeded. +Comparing original with None: Failed. + + +-------------------------------------------------------------------------------- + + +distributions/.nuke-temp x distributions/.soft-remove +Problem: abc:abd::efg:_ +Comparing None with None: Warning! Expected 0 counts of ffg, but got 1 +Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.nuke-temp x distributions/.soft-remove +Problem: abc:abd::ijk:_ +Comparing None with None: Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.nuke-temp x distributions/.soft-remove +Problem: abc:abd::xyz:_ +Comparing None with None: Succeeded. + + +-------------------------------------------------------------------------------- + + +distributions/.nuke-temp x distributions/.soft-remove +Problem: abc:abd::ijkk:_ +Comparing None with None: Failed. + + +-------------------------------------------------------------------------------- + + +distributions/.nuke-temp x distributions/.soft-remove +Problem: abc:abd::mrrjjj:_ +Comparing None with None: Warning! Expected 0 counts of nrrjjj, but got 1 +Warning! Expected 0 counts of mrrjjd, but got 2 +Failed. + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ac987e5 --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Self-explanatory.""" +from setuptools import setup + +setup( + name='copycat', + version='0.0.1', + packages=['copycat'], + install_requires=[ + # pip requirements go here; at the moment there are none + ], + package_data={'': ['LICENSE']}, + + # metadata for upload to PyPI + author="The Fluid Analogies Research Group, J Alan Brogan, and Arthur O'Dwyer", + author_email='arthur.j.odwyer@gmail.com', + description="Python implementation of Douglas Hofstadter's Copycat.", + license='MIT license', + long_description=open('README.md').read(), + keywords='ai analogy copycat farg fargitecture hofstadter slipnet', + url='https://github.com/Quuxplusone/co.py.cat', +) diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..7842590 --- /dev/null +++ b/tests.py @@ -0,0 +1,62 @@ +import unittest +import os.path +import pickle +import argparse +import sys + +from pprint import pprint +from copycat import Problem +from copycat.statistics import iso_chi_squared + +# TODO: update test cases to use entropy + +def generate(): + print('Generating distributions for new file') + iterations = 30 + problems = [ + Problem('abc', 'abd', 'efg', iterations), + Problem('abc', 'abd', 'ijk', iterations), + Problem('abc', 'abd', 'xyz', iterations), + Problem('abc', 'abd', 'ijkk', iterations), + Problem('abc', 'abd', 'mrrjjj', iterations)] + + with open(TestCopycat.Filename, 'wb') as outfile: + pickle.dump(problems, outfile) + return problems + +class TestCopycat(unittest.TestCase): + Filename = None + + def setUp(self): + self.longMessage = True # new in Python 2.7 + + def test(self): + print('Testing copycat with input file: {}'.format(TestCopycat.Filename)) + try: + with open(TestCopycat.Filename, 'rb') as infile: + problems = pickle.load(infile) + except Exception as e: + print('Generating due to error:') + print(e) + problems = generate() + + for problem in problems: + problem.test(iso_chi_squared) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--generate', action='store_true') + parser.add_argument('filename', default='.distributions', nargs='?') + parser.add_argument('unittest_args', default=[], nargs='?') + + args = parser.parse_args() + # TODO: Go do something with args.input and args.filename + + TestCopycat.Filename = args.filename + + if args.generate: + generate() + + # Now set the sys.argv to the unittest_args (leaving sys.argv[0] alone) + sys.argv[1:] = args.unittest_args + unittest.main() diff --git a/tests_README.md b/tests_README.md new file mode 100644 index 0000000..f53a55e --- /dev/null +++ b/tests_README.md @@ -0,0 +1,45 @@ +# README_tests.md + +## Overview +`tests.py` is a test suite for the Copycat program that verifies the system's behavior across various analogical reasoning problems. It includes functionality to generate test distributions and run statistical tests on the results. + +## Usage +Run the tests from the terminal with the following command: +```bash +python tests.py [filename] [unittest_args] +``` + +### Arguments +- `filename` (optional): Path to the test distributions file (default: '.distributions') +- `--generate`: Generate new test distributions +- `unittest_args` (optional): Additional arguments to pass to unittest + +## Test Cases +The test suite includes the following problem sets: +1. `abc` → `abd` : `efg` → ? +2. `abc` → `abd` : `ijk` → ? +3. `abc` → `abd` : `xyz` → ? +4. `abc` → `abd` : `ijkk` → ? +5. `abc` → `abd` : `mrrjjj` → ? + +Each problem is run for 30 iterations by default. + +## Features +- Generates and saves test distributions +- Runs statistical tests using chi-squared analysis +- Supports custom test distribution files +- Integrates with Python's unittest framework +- Provides detailed test output and error messages + +## Dependencies +- Requires the `copycat` module +- Uses `unittest` for test framework +- Uses `pickle` for saving/loading test distributions +- Uses `argparse` for command-line argument parsing +- Uses `copycat.statistics` for statistical analysis + +## Notes +- The test distributions are saved in a pickle file +- The test suite can be run with or without generating new distributions +- Statistical tests use the `iso_chi_squared` function from the copycat statistics module +- The test suite is designed to be extensible for adding new test cases \ No newline at end of file