Files
copycat/copycat/codeletMethods.py
Alex Linhares 866881be69 Rename bondFacet to bond_edge_type and clean up slipnet.json
The slipnet node previously called "bondFacet" was misleading since
"facet" is used throughout the codebase as a property name on Bond,
Group, and Rule objects. Renamed to "bond_edge_type" to better reflect
its role as a descriptor type category. Also removed precomputed
minPathToLetter data from slipnet.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 23:19:55 +00:00

2013 lines
88 KiB
Python

"""
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(bond_edge_type, source, destination):
"""
Extract descriptors from source and destination objects for a given bond facet.
Args:
bond_edge_type: 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(bond_edge_type)
destinationDescriptor = destination.getDescriptor(bond_edge_type)
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]
bond_edge_types = [d.descriptionType for d in destination.descriptions if d.descriptionType in sourceFacets]
supports = [__supportForDescriptionType(ctx, f, source.string) for f in bond_edge_types]
return random.weighted_choice(bond_edge_types, 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)
bond_edge_type = __chooseBondFacet(ctx, source, destination)
assert bond_edge_type
logging.info('chosen bond facet: %s', bond_edge_type.get_name())
logging.info('Source: %s, destination: %s', source, destination)
bond_descriptors = __getDescriptors(bond_edge_type, 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, bond_edge_type,
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
bond_edge_type = __chooseBondFacet(ctx, source, destination)
assert bond_edge_type
sourceDescriptor, destinationDescriptor = __getDescriptors(
bond_edge_type, 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,
bond_edge_type, sourceDescriptor,
destinationDescriptor)
else:
coderack.proposeBond(destination, source, category,
bond_edge_type, 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)
bond_edge_type = __chooseBondFacet(ctx, source, destination)
assert bond_edge_type
sourceDescriptor, destinationDescriptor = __getDescriptors(
bond_edge_type, source, destination)
category = sourceDescriptor.getBondCategory(destinationDescriptor)
assert category
if category == slipnet.identity:
category = slipnet.sameness
coderack.proposeBond(source, destination, category, bond_edge_type,
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
bond_edge_type = 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 bond_edge_type or bond_edge_type == source.leftBond.facet:
bond_edge_type = 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 bond_edge_type or bond_edge_type == destination.rightBond.facet:
bond_edge_type = 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, bond_edge_type)
@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)
bond_edge_type = 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 bond_edge_type or bond_edge_type == source.leftBond.facet:
bond_edge_type = 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 bond_edge_type or bond_edge_type == destination.rightBond.facet:
bond_edge_type = 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, bond_edge_type)
# 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
bond_edge_type = chosenBond.facet
coderack.proposeGroup(objects, bonds, groupCategory, directionCategory, bond_edge_type)
@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.bond_edge_type]
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.bond_edge_type]
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()