<commit_before>/*
 * SuRealPMCB.cc
 *
 * Copyright (C) 2014 OpenCog Foundation
 *
 * Author: <NAME> <https://github.com/williampma>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License v3 as
 * published by the Free Software Foundation and including the exceptions
 * at http://opencog.org/wiki/Licenses
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, write to:
 * Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <opencog/nlp/types/atom_types.h>
#include <opencog/guile/SchemeSmob.h>

#include "SuRealPMCB.h"


using namespace opencog::nlp;
using namespace opencog;


/**
 * The constructor for the PatternMatcherCallback.
 *
 * @param pAS    the corresponding AtomSpace
 * @param vars   the set of nodes that should be treated as variables
 */
SuRealPMCB::SuRealPMCB(AtomSpace* pAS, std::set<Handle> vars)
    : DefaultPatternMatchCB(pAS), m_vars(vars), m_eval(new SchemeEval(pAS))
{

}

SuRealPMCB::~SuRealPMCB()
{
    delete m_eval;
}

/**
 * Override the variable_match callback.
 *
 * Do the special checking for each variable matches.  Note that variables
 * in SuReal will not just be VariableNode, but any nodes with an actual word
 * (eg. (PredicateNode "runs"), (ConceptNode "dog@1324-12213"), etc).
 *
 * For each variable and its potential solution, the corresponding WordNode
 * will be located.  Then we following the WordNode to find LgWordCset to get
 * the LG dictionary entries.  If the two words have identical LG entries,
 * then the two words matches.  Just one pair of LG entries need to match even
 * if a word has multiple entries.
 *
 * @param hPat    the variable, a node extracted from the original query
 * @param hSoln   the potential mapping
 * @return        false if solution is rejected, true if accepted
 */
bool SuRealPMCB::variable_match(Handle &hPat, Handle &hSoln)
{
    logger().debug("[SuReal] In variable_match, looking at %s", hSoln->toShortString().c_str());

    // reject if the solution is not of the same type
    if (hPat->getType() != hSoln->getType())
        return false;

    // VariableNode can be matched to any VariableNode, similarly for InterpretationNode
    if (hPat->getType() == VARIABLE_NODE || hPat->getType() == INTERPRETATION_NODE)
        return true;

    std::string sPat = _as->getName(hPat);
    std::string sPatWord = sPat.substr(0, sPat.find_first_of('@'));
    std::string sSoln = _as->getName(hSoln);

    // get the WordNode associated with the word (extracted from "word@1234" convention)
    Handle hPatWordNode = _as->getHandle(WORD_NODE, sPatWord);

    // get the corresponding WordInstanceNode for hSoln
    Handle hSolnWordInst = _as->getHandle(WORD_INSTANCE_NODE, sSoln);

    // no WordInstanceNode? reject!
    if (hSolnWordInst == Handle::UNDEFINED)
        return false;

    // get the source connectors for the solution
    std::string scmCode = "(word-inst-get-source-conn " + SchemeSmob::to_string(hSolnWordInst) + ")";
    HandleSeq qTargetConns = m_eval->eval_q(scmCode);

    HandleSeq qOr = _as->getNeighbors(hPatWordNode, false, true, LG_WORD_CSET, false);
    HandleSeq qDisjuncts;

    auto insertHelper = [&](const Handle& h)
    {
        HandleSeq q = _as->getOutgoing(h);
        qDisjuncts.insert(qDisjuncts.end(), q.begin(), q.end());
    };

    std::for_each(qOr.begin(), qOr.end(), insertHelper);

    logger().debug("[SuReal] Looking at %d disjuncts of %s", qDisjuncts.size(), hPat->toShortString().c_str());

    // for each disjunct, get its outgoing set, and match 1-to-1 with qConns
    auto matchHelper = [&](const Handle& hDisjunct)
    {
        HandleSeq qSourceConns;

        // check if hDisjunct is LgAnd or just a lone connector
        if (hDisjunct->getType() == LG_AND)
            qSourceConns = _as->getOutgoing(hDisjunct);
        else
            qSourceConns.push_back(hDisjunct);

        if (qSourceConns.size() != qTargetConns.size())
            return false;

        for (uint i = 0; i < qSourceConns.size(); i++)
        {
            scmCode = "(lg-conn-linkable? " + SchemeSmob::to_string(qSourceConns[i]) + " " + SchemeSmob::to_string(qTargetConns[i]) + ")";

            if (m_eval->eval(scmCode) == "#f")
                return false;
        }

        return true;
    };

    return std::any_of(qDisjuncts.begin(), qDisjuncts.end(), matchHelper);
}

/**
 * Override the clause_match callback.
 *
 * Reject a clause match if the matched link is not in a SetLink linked to an
 * InterpretationNode.  The reason of doing it here instead of earlier in
 * link_match() is because it is possible a SetLink is used for other purpose
 * as part of R2L's output.
 *
 * @param pattrn_link_h   the matched clause, extracted from the input SetLink<commit_msg>Added proper handling of multi-connector<commit_after>/*
 * SuRealPMCB.cc
 *
 * Copyright (C) 2014 OpenCog Foundation
 *
 * Author: <NAME> <https://github.com/williampma>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License v3 as
 * published by the Free Software Foundation and including the exceptions
 * at http://opencog.org/wiki/Licenses
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, write to:
 * Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <opencog/nlp/types/atom_types.h>
#include <opencog/guile/SchemeSmob.h>

#include "SuRealPMCB.h"


using namespace opencog::nlp;
using namespace opencog;


/**
 * The constructor for the PatternMatcherCallback.
 *
 * @param pAS    the corresponding AtomSpace
 * @param vars   the set of nodes that should be treated as variables
 */
SuRealPMCB::SuRealPMCB(AtomSpace* pAS, std::set<Handle> vars)
    : DefaultPatternMatchCB(pAS), m_vars(vars), m_eval(new SchemeEval(pAS))
{

}

SuRealPMCB::~SuRealPMCB()
{
    delete m_eval;
}

/**
 * Override the variable_match callback.
 *
 * Do the special checking for each variable matches.  Note that variables
 * in SuReal will not just be VariableNode, but any nodes with an actual word
 * (eg. (PredicateNode "runs"), (ConceptNode "dog@1324-12213"), etc).
 *
 * For each variable and its potential solution, the corresponding WordNode
 * will be located.  Then we following the WordNode to find LgWordCset to get
 * the LG dictionary entries.  If the two words have identical LG entries,
 * then the two words matches.  Just one pair of LG entries need to match even
 * if a word has multiple entries.
 *
 * @param hPat    the variable, a node extracted from the original query
 * @param hSoln   the potential mapping
 * @return        false if solution is rejected, true if accepted
 */
bool SuRealPMCB::variable_match(Handle &hPat, Handle &hSoln)
{
    logger().debug("[SuReal] In variable_match, looking at %s", hSoln->toShortString().c_str());

    // reject if the solution is not of the same type
    if (hPat->getType() != hSoln->getType())
        return false;

    // VariableNode can be matched to any VariableNode, similarly for InterpretationNode
    if (hPat->getType() == VARIABLE_NODE || hPat->getType() == INTERPRETATION_NODE)
        return true;

    std::string sPat = _as->getName(hPat);
    std::string sPatWord = sPat.substr(0, sPat.find_first_of('@'));
    std::string sSoln = _as->getName(hSoln);

    // get the WordNode associated with the word (extracted from "word@1234" convention)
    Handle hPatWordNode = _as->getHandle(WORD_NODE, sPatWord);

    // get the corresponding WordInstanceNode for hSoln
    Handle hSolnWordInst = _as->getHandle(WORD_INSTANCE_NODE, sSoln);

    // no WordInstanceNode? reject!
    if (hSolnWordInst == Handle::UNDEFINED)
        return false;

    // get the source connectors for the solution
    std::string scmCode = "(word-inst-get-source-conn " + SchemeSmob::to_string(hSolnWordInst) + ")";
    HandleSeq qTargetConns = m_eval->eval_q(scmCode);

    HandleSeq qOr = _as->getNeighbors(hPatWordNode, false, true, LG_WORD_CSET, false);
    HandleSeq qDisjuncts;

    auto insertHelper = [&](const Handle& h)
    {
        HandleSeq q = _as->getOutgoing(h);
        qDisjuncts.insert(qDisjuncts.end(), q.begin(), q.end());
    };

    std::for_each(qOr.begin(), qOr.end(), insertHelper);

    logger().debug("[SuReal] Looking at %d disjuncts of %s", qDisjuncts.size(), hPat->toShortString().c_str());

    // for each disjunct, get its outgoing set, and match 1-to-1 with qConns
    auto matchHelper = [&](const Handle& hDisjunct)
    {
        std::list<Handle> sourceConns;
        std::list<Handle> targetConns(qTargetConns.begin(), qTargetConns.end());

        // check if hDisjunct is LgAnd or just a lone connector
        if (hDisjunct->getType() == LG_AND)
        {
            HandleSeq q = _as->getOutgoing(hDisjunct);
            sourceConns = std::list<Handle>(q.begin(), q.end());
        }
        else
        {
            sourceConns.push_back(hDisjunct);
        }

        Handle hMultiConn = Handle::UNDEFINED;

        while (not sourceConns.empty() && not targetConns.empty())
        {
            if (hMultiConn != Handle::UNDEFINED)
                scmCode = "(lg-conn-linkable? " + SchemeSmob::to_string(hMultiConn) + " " + SchemeSmob::to_string(targetConns.front()) + ")";
            else
                scmCode = "(lg-conn-linkable? " + SchemeSmob::to_string(sourceConns.front()) + " " + SchemeSmob::to_string(targetConns.front()) + ")";

            if (m_eval->eval(scmCode) == "#f\n")
            {
                if (hMultiConn != Handle::UNDEFINED)
                {
                    hMultiConn = Handle::UNDEFINED;
                    continue;
                }

                return false;
            }

            if (hMultiConn != Handle::UNDEFINED)
            {
                targetConns.pop_front();
                continue;
            }

            if (_as->getOutgoing(sourceConns.front()).size() == 3)
                hMultiConn = sourceConns.front();

            sourceConns.pop_front();
            targetConns.pop_front();
        }

        // check if both source and target are used up
        if (not sourceConns.empty() or not targetConns.empty())
            return false;

        logger().info(hDisjunct->toShortString() + " passed!");

        return true;
    };

    return std::any_of(qDisjuncts.begin(), qDisjuncts.end(), matchHelper);
}

/**
 * Override the clause_match callback.
 *
 * Reject a clause match if the matched link is not in a SetLink linked to an
 * InterpretationNode.  The reason of doing it here instead of earlier in
 * link_match() is because it is possible a SetLink is used for other purpose
 * as part of R2L's output.
 *
 * @param pattrn_link_h   the matched clause, extracted from the input SetLink
{'fraction_non_alphanumeric': 0.10815757364476207, 'fraction_numerical': 0.005926442391493812, 'mean_word_length': 3.124730409777139, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 50, 'https://': 2, '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': '4801519', 'n_tokens_mistral': 3637, 'n_tokens_neox': 3475, 'n_words': 1119}