<commit_before>__author__ = 'jade'

from pln.rules.rules import Rule
from pln.logic import Logic
from pln.formulas import revisionFormula
from pln.rules import rules

from opencog.atomspace import types, Atom, AtomSpace, TruthValue

import random
from collections import defaultdict

'''There are lots of possible heuristics for choosing atoms. It also depends on the kind of rule, and will have a HUGE effect on the system. (i.e. if you choose less useful atoms, you will waste a lot of time / it will take exponentially longer to find something useful / you will find exponentially more rubbish! and since all other parts of opencog have combinatorial explosions, generating rubbish is VERY bad!)

The algorithm doesn't explicitly distinguish between depth-first and breadth-first search, but you could bias it. You can set a parameter for how much STI to give each atom that is used (or produced) by forward chaining (which should be set relative to the other AI processes!). If it's low, then PLN will tend to stay within the current "bubble of attention". If it's high, then PLN will tend to "move the bubble of attention" so it chains together strings (or rather DAGs) of inferences.

I really think we should make a reasonably fast algorithm, and then add rules one at a time, adding useful heuristics simultaneously (so that PLN will actually be useful rather than just get in the AI's way). It looks to me like this set of heuristics would be enough to make PLN not spiral crazily out of control.

* choose atoms with probability proportional to their STI
* higher confidence atoms are usually better
* higher strength atoms are often better (but obviously for finding Not(A) you don't want A to be strong)

It's possible to do a lot of learning online, and it's both efficient and a nice control strategy. The system should give high STI to things that happened recently or are near the agent

Cognitive synergy:
* After you have clustering and fishgram working, you can do reasoning on the results of those processes 
* Dimensional embedding tells you which similarity links (or inheritance links) are likely to be good (see the opencog book)
* PLN planning: you could have various special heuristics for hierarchical planning
* Use MOSES

You can (and should) also think carefully about how to represent data so that more useful inferences are shorter. This will generally help a LOT!!!!!!!!!!

Special cases for different kinds of rules:
* For deduction, it would be really useful to explicitly notice a hierarchy of more abstract/specific concepts (as described in the RealWorldReasoning book).
And any time you find Inheritance A B, also calculate the intensional and extensional parts separately
* For space/time reasoning, you should prefer things that are close in space/time (and in fact, for reasoning about objects generally. You can find nearby things using the space/time servers
* You can probably get past toddler-level AI without having to use predicate logic much. And it may be easier to convert 2+ place predicates into 1-place predicates (see EvaluationToMemberRule)
'''

class AbstractChainer(Logic):
    '''Has important utility methods for chainers.'''
    def __init__(self, atomspace):
        Logic.__init__(self, atomspace)
        self.rules = []

    def add_rule(self, rule):
        assert isinstance(rule, Rule)
        assert type(rule) != Rule

        self.rules.append(rule)

    def log_failed_inference(self,message):
        print 'Attempted invalid inference:',message

    # Finds a list of candidate atoms and then matches all of them against the template.
    # Uses the Attentional Focus where possible but will search the whole AtomSpace if necessary.
    def _select_one_matching(self, template, s = {}, allow_zero_tv = False):
        # If the template is a specific atom, just return that!
        if len(self.variables(template)) == 0:
            return template

        atoms = self.atomspace.get_atoms_in_attentional_focus()
        atom = self._select_atom(template, s, atoms)
        if not atom:
            # if it can't find anything in the attentional focus, try the whole atomspace. (it actually still uses indexes to find a subset of the links, that is more likely to be useful)
            atoms = self.lookup_atoms(template, s)
            atom = self._select_atom(template, s, atoms)

        return atom

    def _select_fromAF(self, template, substitution):
        # never allow inputs with variables. (not even for backchaining targets)
        ground_results = True

        atoms = self.find(template, substitution)

        if len(atoms) == 0:
            return None

        return self._selectOne(atoms)

    def _select_atom(self, template, substitution, atoms):
        # This method will sample atoms before doing any filtering, and will only apply the filters on as many atoms as it needs to.

        # The atomspace lookup and the shuffle are both O(N)...
        # But if you shuffle it you're guaranteed to eventually find a suitable atom if one exists.
        # The correct option would be to do it inside the atomspace.
        # Sample without replacement, or shuffle a little bit at a time.
        # Or even being able to get results N atoms at a time would help a lot!
        random.shuffle(atoms)
        #atoms = random.sample(atoms, len(atoms))

        # O(N*the percentage of atoms that are useful)
        for atom in atoms:
            if self.wanted_atom(atom, template, substitution, ground=False):
                return atom
        return None

    def _selectOne(self, atoms):
        # The score should always be an int or stuff will get weird. sti is an int but TV mean and conf are not
        def sti_score(atom):
            sti = atom.av['sti']
            return max(sti, 1)

        def mixed_score(atom):
            s = int(10*sti_score(atom) + 100*(atom.tv.mean+0.01) + 100*(atom.tv.confidence+0.01))
            return s

        score = sti_score
        #score = mixed_score

        assert type(atoms[0]) == Atom

        total = sum([score(atom) for atom in atoms])
        pick = random.randrange(0, total)
        current = 0
        for atom in atoms:
            current += score(atom)
            if current >= pick:
                return atom

        assert False

    def _select_rule(self):
        if not self.learnRuleFrequencies:
            return random.choice(self.rules)
        else:
            def score(rule):
                return self.rule_count[rule]

            max = sum([score(rule) for rule in self.rules])
            pick = random.randrange(0, max)
            current = 0
            for rule in self.rules:
                current += score(rule)
                if current >= pick:
                    self.rule_count[rule] += 1
                    return rule

    def valid_structure(self, atom):
        '''Does a kind of 'type-check' to see if an Atom's structure makes sense.
           The forward chainer is very creative and will come up with anything allowed by the Rules
           otherwise.'''
#        if atom.type in [types.InheritanceLink, types.SubsetLink, types.SimilarityLink]:
#            #is_between_nodes = atom.out[0].is_node() and atom.out[1].is_node()
#            not_self_link    = atom.out[0] != atom.out[1]
#            return not_self_link
        if atom.arity == 2:
            # heuristically assume that all selflinks are invalid!
            self_link = atom.out[0] == atom.out[1]
            # don't allow inheritancelinks (or anything else?) with two variables. (These are rapidly created as backchaining targets with DeductionRule)
            both_variables = self.is_variable(atom.out[0]) and self.is_variable(atom.out[1])
            return not self_link and not both_variables
        elif atom.type in rules.BOOLEAN_LINKS:
            # see if it has semantically sensible arguments (e.g you don't want MemberLinks inside AndLinks)
            suitable = all(self.is_variable(arg) or arg.is_a(types.ConceptNode) or arg.is_a(types.EvaluationLink) for arg in atom.out)
            return suitable
        elif atom.type in rules.FIRST_ORDER_LINKS:
            suitable = all(self.is_variable(arg) or arg.is_a(types.ConceptNode) or arg.is_a(types.ObjectNode) or arg.type in rules.BOOLEAN_LINKS)
            return suitable
        elif atom.type in rules.HIGHER_ORDER_LINKS:
            suitable = all(self.is_variable(arg) or arg.is_a(types.EvaluationLink) or arg.type in rules.BOOLEAN_LINKS)
        elif atom.is_a(types.MemberLink):
            # Assume the domain of all predicates is objects or concepts or variables
            element = atom.out[0]
            return element.is_a(types.ObjectNode) or element.is_a(types.ConceptNode) or element.is_a(types.VariableNode)
        else:
            return True

    def count_objects(self):
        all_object_nodes = self.atomspace.get_atoms_by_type(types.ObjectNode)
        return len(all_object_nodes)

    def count_members(self, conceptnode):
        var = self.new_variable()
        template = self.link(types.MemberLink, [var, conceptnode])
        members = self.find(template)
        self.atomspace.remove(var)
        return len(members)
<commit_msg>Allow higher-order logic<commit_after>__author__ = 'jade'

from pln.rules.rules import Rule
from pln.logic import Logic
from pln.formulas import revisionFormula
from pln.rules import rules

from opencog.atomspace import types, Atom, AtomSpace, TruthValue

import random
from collections import defaultdict

'''There are lots of possible heuristics for choosing atoms. It also depends on the kind of rule, and will have a HUGE effect on the system. (i.e. if you choose less useful atoms, you will waste a lot of time / it will take exponentially longer to find something useful / you will find exponentially more rubbish! and since all other parts of opencog have combinatorial explosions, generating rubbish is VERY bad!)

The algorithm doesn't explicitly distinguish between depth-first and breadth-first search, but you could bias it. You can set a parameter for how much STI to give each atom that is used (or produced) by forward chaining (which should be set relative to the other AI processes!). If it's low, then PLN will tend to stay within the current "bubble of attention". If it's high, then PLN will tend to "move the bubble of attention" so it chains together strings (or rather DAGs) of inferences.

I really think we should make a reasonably fast algorithm, and then add rules one at a time, adding useful heuristics simultaneously (so that PLN will actually be useful rather than just get in the AI's way). It looks to me like this set of heuristics would be enough to make PLN not spiral crazily out of control.

* choose atoms with probability proportional to their STI
* higher confidence atoms are usually better
* higher strength atoms are often better (but obviously for finding Not(A) you don't want A to be strong)

It's possible to do a lot of learning online, and it's both efficient and a nice control strategy. The system should give high STI to things that happened recently or are near the agent

Cognitive synergy:
* After you have clustering and fishgram working, you can do reasoning on the results of those processes 
* Dimensional embedding tells you which similarity links (or inheritance links) are likely to be good (see the opencog book)
* PLN planning: you could have various special heuristics for hierarchical planning
* Use MOSES

You can (and should) also think carefully about how to represent data so that more useful inferences are shorter. This will generally help a LOT!!!!!!!!!!

Special cases for different kinds of rules:
* For deduction, it would be really useful to explicitly notice a hierarchy of more abstract/specific concepts (as described in the RealWorldReasoning book).
And any time you find Inheritance A B, also calculate the intensional and extensional parts separately
* For space/time reasoning, you should prefer things that are close in space/time (and in fact, for reasoning about objects generally. You can find nearby things using the space/time servers
* You can probably get past toddler-level AI without having to use predicate logic much. And it may be easier to convert 2+ place predicates into 1-place predicates (see EvaluationToMemberRule)
'''

class AbstractChainer(Logic):
    '''Has important utility methods for chainers.'''
    def __init__(self, atomspace):
        Logic.__init__(self, atomspace)
        self.rules = []

    def add_rule(self, rule):
        assert isinstance(rule, Rule)
        assert type(rule) != Rule

        self.rules.append(rule)

    def log_failed_inference(self,message):
        print 'Attempted invalid inference:',message

    # Finds a list of candidate atoms and then matches all of them against the template.
    # Uses the Attentional Focus where possible but will search the whole AtomSpace if necessary.
    def _select_one_matching(self, template, s = {}, allow_zero_tv = False):
        # If the template is a specific atom, just return that!
        if len(self.variables(template)) == 0:
            return template

        atoms = self.atomspace.get_atoms_in_attentional_focus()
        atom = self._select_atom(template, s, atoms)
        if not atom:
            # if it can't find anything in the attentional focus, try the whole atomspace. (it actually still uses indexes to find a subset of the links, that is more likely to be useful)
            atoms = self.lookup_atoms(template, s)
            atom = self._select_atom(template, s, atoms)

        return atom

    def _select_fromAF(self, template, substitution):
        # never allow inputs with variables. (not even for backchaining targets)
        ground_results = True

        atoms = self.find(template, substitution)

        if len(atoms) == 0:
            return None

        return self._selectOne(atoms)

    def _select_atom(self, template, substitution, atoms):
        # This method will sample atoms before doing any filtering, and will only apply the filters on as many atoms as it needs to.

        # The atomspace lookup and the shuffle are both O(N)...
        # But if you shuffle it you're guaranteed to eventually find a suitable atom if one exists.
        # The correct option would be to do it inside the atomspace.
        # Sample without replacement, or shuffle a little bit at a time.
        # Or even being able to get results N atoms at a time would help a lot!
        random.shuffle(atoms)
        #atoms = random.sample(atoms, len(atoms))

        # O(N*the percentage of atoms that are useful)
        for atom in atoms:
            if self.wanted_atom(atom, template, substitution, ground=False):
                return atom
        return None

    def _selectOne(self, atoms):
        # The score should always be an int or stuff will get weird. sti is an int but TV mean and conf are not
        def sti_score(atom):
            sti = atom.av['sti']
            return max(sti, 1)

        def mixed_score(atom):
            s = int(10*sti_score(atom) + 100*(atom.tv.mean+0.01) + 100*(atom.tv.confidence+0.01))
            return s

        score = sti_score
        #score = mixed_score

        assert type(atoms[0]) == Atom

        total = sum([score(atom) for atom in atoms])
        pick = random.randrange(0, total)
        current = 0
        for atom in atoms:
            current += score(atom)
            if current >= pick:
                return atom

        assert False

    def _select_rule(self):
        if not self.learnRuleFrequencies:
            return random.choice(self.rules)
        else:
            def score(rule):
                return self.rule_count[rule]

            max = sum([score(rule) for rule in self.rules])
            pick = random.randrange(0, max)
            current = 0
            for rule in self.rules:
                current += score(rule)
                if current >= pick:
                    self.rule_count[rule] += 1
                    return rule

    def valid_structure(self, atom):
        '''Does a kind of 'type-check' to see if an Atom's structure makes sense.
           The forward chainer is very creative and will come up with anything allowed by the Rules
           otherwise.'''
        if atom.arity == 2:
            # heuristically assume that all selflinks are invalid!
            self_link = atom.out[0] == atom.out[1]
            # don't allow inheritancelinks (or anything else?) with two variables. (These are rapidly created as backchaining targets with DeductionRule)
            both_variables = self.is_variable(atom.out[0]) and self.is_variable(atom.out[1])
            return not self_link and not both_variables
        else:
            return True

    def count_objects(self):
        all_object_nodes = self.atomspace.get_atoms_by_type(types.ObjectNode)
        return len(all_object_nodes)

    def count_members(self, conceptnode):
        var = self.new_variable()
        template = self.link(types.MemberLink, [var, conceptnode])
        members = self.find(template)
        self.atomspace.remove(var)
        return len(members)

{'fraction_non_alphanumeric': 0.06527430333352725, 'fraction_numerical': 0.003781488161033219, 'mean_word_length': 2.8551244673693654, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5977326', 'n_tokens_mistral': 4606, 'n_tokens_neox': 4376, 'n_words': 2025}