diff --git a/README.md b/README.md index 354c56a..0a55f18 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ J Alan Brogan writes: > 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)". -Cloning the repo ----------------- +Running the command-line program +-------------------------------- To clone the repo locally, run these commands: @@ -40,6 +40,25 @@ 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". + +Running the `curses` interface +------------------------------ + +Follow the instructions to clone the repo as above, but then run `curses_main` instead of `main`: + +``` +$ git clone https://github.com/Quuxplusone/co.py.cat.git +$ cd co.py.cat/copycat +$ python curses_main.py abc abd ppqqrr +``` + +This script takes only three 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 number of iterations is always implicitly "infinite". +To kill the program, hit Ctrl+C. + + Installing the module --------------------- diff --git a/copycat/codeletMethods.py b/copycat/codeletMethods.py index a1d6254..aad3f8e 100644 --- a/copycat/codeletMethods.py +++ b/copycat/codeletMethods.py @@ -854,8 +854,9 @@ def rule_translator(ctx, codelet): weights = __getCutoffWeights(bondDensity) cutoff = 10.0 * random.weighted_choice(range(1, 11), weights) if cutoff >= temperature.actual_value: - if workspace.rule.buildTranslatedRule(): - workspace.foundAnswer = True + result = workspace.rule.buildTranslatedRule() + if result is not None: + workspace.finalAnswer = result else: temperature.clampUntil(coderack.codeletsRun + 100) diff --git a/copycat/copycat.py b/copycat/copycat.py index a344c75..bfeec9a 100644 --- a/copycat/copycat.py +++ b/copycat/copycat.py @@ -47,42 +47,43 @@ class Copycat(object): self.reporter.report_temperature(self.temperature) return lastUpdate - def runTrial(self, answers): + def runTrial(self): """Run a trial of the copycat algorithm""" self.coderack.reset() self.slipnet.reset() self.temperature.reset() self.workspace.reset() lastUpdate = float('-inf') - while not self.workspace.foundAnswer: + while self.workspace.finalAnswer is None: lastUpdate = self.mainLoop(lastUpdate) - if self.workspace.rule: - answer = self.workspace.rule.finalAnswer - else: - answer = None - finalTemperature = self.temperature.last_unclamped_value - finalTime = self.coderack.codeletsRun - logging.info('Answered %s (time %d, final temperature %.1f)' % (answer, finalTime, finalTemperature)) - self.reporter.report_answer({ - 'answer': answer, - 'temp': finalTemperature, - 'time': finalTime, - }) - d = answers.setdefault(answer, { - 'count': 0, - 'sumtemp': 0, - 'sumtime': 0 - }) - d['count'] += 1 - d['sumtemp'] += finalTemperature - d['sumtime'] += finalTime + answer = { + 'answer': self.workspace.finalAnswer, + 'temp': self.temperature.last_unclamped_value, + 'time': self.coderack.codeletsRun, + } + self.reporter.report_answer(answer) + return answer def run(self, initial, modified, target, iterations): self.workspace.resetWithStrings(initial, modified, target) answers = {} for i in xrange(iterations): - self.runTrial(answers) + answer = self.runTrial() + d = answers.setdefault(answer['answer'], { + 'count': 0, + 'sumtemp': 0, + 'sumtime': 0 + }) + d['count'] += 1 + d['sumtemp'] += answer['temp'] + d['sumtime'] += answer['time'] + for answer, d in answers.iteritems(): d['avgtemp'] = d.pop('sumtemp') / d['count'] d['avgtime'] = d.pop('sumtime') / d['count'] return answers + + def run_forever(self, initial, modified, target): + self.workspace.resetWithStrings(initial, modified, target) + while True: + self.runTrial() diff --git a/copycat/curses_main.py b/copycat/curses_main.py new file mode 100644 index 0000000..9fbbe10 --- /dev/null +++ b/copycat/curses_main.py @@ -0,0 +1,26 @@ +import curses +import logging +import sys + +from copycat import Copycat +from curses_reporter import CursesReporter + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO, format='%(message)s', filename='./copycat.log', filemode='w') + + try: + args = sys.argv[1:] + initial, modified, target = args + except ValueError: + print >>sys.stderr, 'Usage: %s initial modified target [iterations]' % sys.argv[0] + sys.exit(1) + + try: + window = curses.initscr() + copycat = Copycat(reporter=CursesReporter(window)) + copycat.run_forever(initial, modified, target) + except KeyboardInterrupt: + pass + finally: + curses.endwin() diff --git a/copycat/curses_reporter.py b/copycat/curses_reporter.py new file mode 100644 index 0000000..ab3bed0 --- /dev/null +++ b/copycat/curses_reporter.py @@ -0,0 +1,111 @@ +import curses +from copycat import Reporter + +class CursesReporter(Reporter): + def __init__(self, window): + curses.curs_set(0) # hide the cursor + height, width = window.getmaxyx() + slipnetHeight = 10 + coderackHeight = height - 15 + answersHeight = 5 + self.temperatureWindow = window.derwin(height, 5, 0, 0) + self.slipnetWindow = curses.newwin(slipnetHeight, width-5, 0, 5) + self.coderackWindow = curses.newwin(coderackHeight, width-5, slipnetHeight, 5) + self.answersWindow = curses.newwin(answersHeight, width-5, slipnetHeight + coderackHeight, 5) + for w in [self.temperatureWindow, self.slipnetWindow, self.answersWindow]: + w.clear() + w.border() + w.refresh() + self.answers = {} + + 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(self.answers.itervalues(), 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 report_coderack(self, coderack): + 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.iteritems() + ) + + # 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) + + def is_vacant(y, x): + return (w.inch(y, x) & 0xFF) == 0x20 + + w.erase() + for u, string in printable_entries: + # Find the highest point on the page where we could place this entry. + start_column = (u - 1) * columnWidth + end_column = start_column + len(string) + for r in range(pageHeight): + if all(is_vacant(r, c) for c in xrange(start_column, end_column+20)): + w.addstr(r, start_column, string) + break + w.refresh() + + def report_slipnet(self, slipnet): + pass + + def report_temperature(self, temperature): + height = self.temperatureWindow.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))] + self.temperatureWindow.addstr(max_mercury - i, 1, 3*ch) + self.temperatureWindow.addnstr(height - 2, 1, '%3d' % temperature.actual_value, 3) + self.temperatureWindow.refresh() diff --git a/copycat/description.py b/copycat/description.py index 2c7eaf9..472e819 100644 --- a/copycat/description.py +++ b/copycat/description.py @@ -16,7 +16,7 @@ class Description(WorkspaceStructure): def __str__(self): s = 'description(%s) of %s' % (self.descriptor.get_name(), self.object) workspace = self.ctx.workspace - if self.object.string == workspace.initial: + if self.object.string == getattr(workspace, 'initial', None): s += ' in initial string' else: s += ' in target string' diff --git a/copycat/main.py b/copycat/main.py index d9e8962..da92d5b 100644 --- a/copycat/main.py +++ b/copycat/main.py @@ -1,11 +1,18 @@ import logging import sys -from copycat import Copycat +from copycat import Copycat, Reporter + + +class SimpleReporter(Reporter): + def report_answer(self, answer): + print 'Answered %s (time %d, final temperature %.1f)' % ( + answer['answer'], answer['time'], answer['temp'], + ) if __name__ == '__main__': - logging.basicConfig(level=logging.WARN, format='%(message)s', filename='./copycat.log', filemode='w') + logging.basicConfig(level=logging.INFO, format='%(message)s', filename='./copycat.log', filemode='w') try: args = sys.argv[1:] @@ -19,7 +26,7 @@ if __name__ == '__main__': print >>sys.stderr, 'Usage: %s initial modified target [iterations]' % sys.argv[0] sys.exit(1) - copycat = Copycat() + copycat = Copycat(reporter=SimpleReporter()) answers = copycat.run(initial, modified, target, iterations) for answer, d in sorted(answers.iteritems(), key=lambda kv: kv[1]['avgtemp']): diff --git a/copycat/rule.py b/copycat/rule.py index 205df03..522cdbe 100644 --- a/copycat/rule.py +++ b/copycat/rule.py @@ -99,9 +99,13 @@ class Rule(WorkspaceStructure): # applies the changes to self string ie. successor if self.facet == slipnet.length: if self.relation == slipnet.predecessor: - return string[0:-1] - if self.relation == slipnet.successor: - return string + string[0:1] + 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: @@ -123,24 +127,21 @@ class Rule(WorkspaceStructure): self.descriptor = self.descriptor.applySlippages(slippages) self.relation = self.relation.applySlippages(slippages) # generate the final string - self.finalAnswer = workspace.targetString changeds = [o for o in workspace.target.objects if o.described(self.descriptor) and o.described(self.category)] - changed = changeds and changeds[0] or None - logging.debug('changed object = %s', changed) - if changed: - left = changed.leftIndex - startString = '' - if left > 1: - startString = self.finalAnswer[0: left - 1] + 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 - middleString = self.__changeString( - self.finalAnswer[left - 1: right]) - if not middleString: - return False - endString = '' - if right < len(self.finalAnswer): - endString = self.finalAnswer[right:] - self.finalAnswer = startString + middleString + endString - return True + 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/workspace.py b/copycat/workspace.py index b7cb098..df357a9 100644 --- a/copycat/workspace.py +++ b/copycat/workspace.py @@ -32,7 +32,7 @@ class Workspace(object): self.reset() def reset(self): - self.foundAnswer = False + self.finalAnswer = None self.changedObject = None self.objects = [] self.structures = []