/*
* sections.cc
*
* Translate AtomSpace Sections to LG Exp's.
*
* Copyright (c) 2022 Linas Vepstas <linasvepstas@gmail.com>
*/
#ifdef HAVE_ATOMESE
#include <cstdlib>
#include <opencog/util/oc_assert.h>
#include <opencog/atoms/value/FloatValue.h>
#include <opencog/atomspace/AtomSpace.h>
#include <opencog/lg/types/atom_types.h>
#undef STRINGIFY
extern "C" {
#include "../link-includes.h" // For Dictionary
#include "../api-structures.h" // For Sentence_s
#include "../dict-common/dict-common.h" // for Dictionary_s
#include "../dict-common/dict-internals.h" // for dict_node_new()
#include "../dict-common/dict-utils.h" // for size_of_expression()
#include "../dict-ram/dict-ram.h"
#include "lookup-atomese.h"
};
#include "local-as.h"
using namespace opencog;
// ===============================================================
/// Count the number of times that the `germ` appears in some
/// Section.
static size_t count_sections(Local* local, const Handle& germ)
{
// Are there any Sections in the local atomspace?
size_t nsects = germ->getIncomingSetSizeByType(SECTION);
if (0 < nsects or nullptr == local->stnp) return nsects;
local->stnp->fetch_incoming_by_type(germ, SECTION);
local->stnp->barrier();
nsects = germ->getIncomingSetSizeByType(SECTION);
return nsects;
}
/// Return true if the given word can be found as the germ of some
/// Section, else return false. As a side-effect, Sections are loaded
/// from storage.
bool section_boolean_lookup(Dictionary dict, const char *s)
{
double start = 0.0;
if (D_USER_TIMES <= verbosity) start = total_usage_time();
Local* local = (Local*) (dict->as_server);
Handle wrd = local->asp->add_node(WORD_NODE, s);
// Are there any Sections for this word in the local atomspace?
size_t nwrdsects = count_sections(local, wrd);
// Does this word belong to any classes?
size_t nclass = wrd->getIncomingSetSizeByType(MEMBER_LINK);
if (0 == nclass and local->stnp)
{
local->stnp->fetch_incoming_by_type(wrd, MEMBER_LINK);
local->stnp->barrier();
}
size_t nclssects = 0;
for (const Handle& memb : wrd->getIncomingSetByType(MEMBER_LINK))
{
const Handle& wcl = memb->getOutgoingAtom(1);
if (WORD_CLASS_NODE != wcl->get_type()) continue;
nclssects += count_sections(local, wcl);
}
#define RES_COL_WIDTH 37
if (D_USER_TIMES <= verbosity)
{
double now = total_usage_time();
char buf[128] = "";
snprintf(buf, sizeof(buf),
"Cls: %lu Scts: %lu+%lu for >>%s<<",
nclass, nwrdsects, nclssects, s);
prt_error("Atomese: %-*s %6.2f seconds\n",
RES_COL_WIDTH, buf, now - start);
}
return 0 != (nwrdsects + nclssects);
}
// ===============================================================
/// Return a cached LG-compatible link string.
/// Assigns a new name, if one does not exist.
/// `germ` is the germ of the section, `ctcr` is one of the connectors.
static std::string get_linkname(Local* local, const Handle& germ,
const Handle& ctcr)
{
// The connection target is the first Atom in the connector
const Handle& tgt = ctcr->getOutgoingAtom(0);
const Handle& dir = ctcr->getOutgoingAtom(1);
const std::string& sdir = dir->get_name();
// The link is the connection of both of these.
if ('+' == sdir[0])
{
const Handle& lnk = local->asp->add_link(LIST_LINK, germ, tgt);
return cached_linkname(local, lnk);
}
// Else direction is '-' and tgt comes before germ.
const Handle& lnk = local->asp->add_link(LIST_LINK, tgt, germ);
return cached_linkname(local, lnk);
}
// Debugging utility
void print_section(Dictionary dict, const Handle& sect)
{
Local* local = (Local*) (dict->as_server);
const Handle& germ = sect->getOutgoingAtom(0);
printf("(%s: ", germ->get_name().c_str());
const Handle& conseq = sect->getOutgoingAtom(1);
for (const Handle& ctcr : conseq->getOutgoingSet())
{
// The connection target is the first Atom in the connector
const Handle& tgt = ctcr->getOutgoingAtom(0);
const Handle& dir = ctcr->getOutgoingAtom(1);
const std::string& sdir = dir->get_name();
printf("%s%c ", tgt->get_name().c_str(), sdir[0]);
}
printf(") == ");
bool first = true;
for (const Handle& ctcr : conseq->getOutgoingSet())
{
// Assign an upper-case name to the link.
std::string slnk = get_linkname(local, germ, ctcr);
const Handle& dir = ctcr->getOutgoingAtom(1);
const std::string& sdir = dir->get_name();
if (not first) printf("& ");
first = false;
printf("%s%c ", slnk.c_str(), sdir[0]);
}
printf("\n");
fflush(stdout);
}
// ===============================================================
extern thread_local Sentence sentlo;
extern thread_local HandleSeq sent_words;
/// Create expressions for sections on a word. This is unconditional;
/// no caches are used.
Exp* make_sect_exprs(Dictionary dict, const Handle& germ)
{
Local* local = (Local*) (dict->as_server);
Exp* orhead = nullptr;
// Loop over all Sections on the word.
HandleSeq sects = germ->getIncomingSetByType(SECTION);
for (const Handle& sect: sects)
{
// Apparently, some sections are missing MI's. This is
// most likey due to some data-processing bug, where the
// MI's were not recomputed. For now, we will silently
// ignore this issue, and assign a default to them.
double mi = local->cost_default;
const ValuePtr& mivp = sect->getValue(local->miks);
if (mivp)
{
// MI is the second entry in the vector.
const FloatValuePtr& fmivp = FloatValueCast(mivp);
mi = fmivp->value()[local->cost_index];
}
// If the MI is too low, just skip this.
if (mi < local->cost_cutoff)
continue;
Exp* andhead = nullptr;
Exp* andtail = nullptr;
// The connector sequence the second Atom.
// Loop over the connectors in the connector sequence.
// The atomspace stores connectors in word-order, while LG
// stores them as distance-from given word. Thus, we have
// to reverse the order when building the expression.
const Handle& conseq = sect->getOutgoingAtom(1);
for (const Handle& ctcr : conseq->getOutgoingSet())
{
// The direction is the second Atom in the connector
const Handle& dir = ctcr->getOutgoingAtom(1);
char cdir = dir->get_name().c_str()[0];
/* Assign an upper-case name to the link. */
const std::string& slnk = get_linkname(local, germ, ctcr);
std::lock_guard<std::mutex> guard(local->dict_mutex);
Exp* eee = make_connector_node(dict,
dict->Exp_pool, slnk.c_str(), cdir, false);
if ('+' == cdir)
and_enchain_right(dict->Exp_pool, andhead, andtail, eee);
else if ('-' == cdir)
and_enchain_left(dict->Exp_pool, andhead, andtail, eee);
}
// Sanity-check the Section - this should never-ever happen!
if (nullptr == andhead)
{
prt_error("Warning: Empty connector sequence for %s\n",
sect->to_short_string().c_str());
continue;
}
std::lock_guard<std::mutex> guard(local->dict_mutex);
// Optional: shorten the expression,
// if there's only one connector in it.
if (nullptr == andhead->operand_first->operand_next)
{
Exp *tmp = andhead->operand_first;
andhead->operand_first = NULL;
// pool_free(dict->Exp_pool, andhead); // not needed!?
andhead = tmp;
}
double cost = (local->cost_scale * mi) + local->cost_offset;
andhead->cost = cost;
// Save the exp-section pairing in the AtomSpace.
// XXX Why did we think this was needed ??
// What were we going to do with it?
// cache_disjunct_string(local, sect, andhead);
#if DEBUG
print_section(dict, sect);
const char* wrd = germ->get_name().c_str();
printf("Word: '%s' Exp: %s\n", wrd, lg_exp_stringify(andhead));
#endif
// Add it to the linked list.
or_enchain(dict->Exp_pool, orhead, andhead);
}
return orhead;
}
#define ENCHAIN(ORHEAD,EXP) \
if (nullptr == ORHEAD) \
ORHEAD = (EXP); \
else { \
std::lock_guard<std::mutex> guard(local->dict_mutex); \
ORHEAD = make_or_node(dict->Exp_pool, ORHEAD, (EXP)); \
}
/// Given a word, return the collection of Dict_nodes holding the
/// expressions for that word. Valid only when sections are enabled.
/// Returns the "plain" node, without any word-pair decorations.
///
/// The dictnode is always cached, and the cached node is returned.
static Dict_node * lookup_plain_section(Dictionary dict, const Handle& germ)
{
Local* local = (Local*) (dict->as_server);
const char* ssc = germ->get_name().c_str();
// Do we already have this word cached? If so, pull from
// the cache.
{
std::lock_guard<std::mutex> guard(local->dict_mutex);
Dict_node* dn = dict_node_lookup(dict, ssc);
if (dn) return dn;
}
Exp* exp = nullptr;
// Create disjuncts consisting entirely of "ANY" links.
if (local->any_disjuncts)
{
std::lock_guard<std::mutex> guard(local->dict_mutex);
Exp* any = make_any_exprs(dict, dict->Exp_pool);
or_enchain(dict->Exp_pool, exp, any);
}
// Create expressions from Sections
Exp* sects = make_sect_exprs(dict, germ);
ENCHAIN(exp, sects);
// Get expressions, where the word is in some class.
for (const Handle& memb : germ->getIncomingSetByType(MEMBER_LINK))
{
const Handle& wcl = memb->getOutgoingAtom(1);
if (WORD_CLASS_NODE != wcl->get_type()) continue;
Exp* clexp = make_sect_exprs(dict, wcl);
if (nullptr == clexp) continue;
lgdebug(+D_SPEC+5, "as_lookup_list class for >>%s<< nexpr=%d\n",
ssc, size_of_expression(clexp));
ENCHAIN(exp, clexp);
}
if (nullptr == exp)
return nullptr;
lgdebug(D_USER_INFO, "Atomese: Created %d plain exprs for >>%s<<\n",
size_of_expression(exp), ssc);
std::lock_guard<std::mutex> guard(local->dict_mutex);
make_dn(dict, exp, ssc);
return dict_node_lookup(dict, ssc);
}
/// Given a word, return a Dict_node holding the expressions for that
/// word. Valid only when sections are enabled. This is the "plain"
/// expression, wrapped with extra word-pair decorations, if requested.
Dict_node * lookup_section(Dictionary dict, const Handle& germ)
{
Local* local = (Local*) (dict->as_server);
// Get the "plain" expressions, without any additional word-pair
// decorations.
Dict_node* dn = lookup_plain_section(dict, germ);
if (0 >= local->extra_pairs) return dn;
// If we are here, then we have to tack on extra connectors,
// created from word-pairs. Doing this naively results in
// a combinatorial explosion, and so instead, we only add the
// the word-pairs for words occurring in the sentence. These
// must not be cached, and so they are rebuilt every time.
// A per-thread Sentence::Exp_pool is used.
Exp* extras = make_cart_pairs(dict, germ, sentlo->Exp_pool,
sent_words,
local->extra_pairs,
local->extra_any);
// No word-pairs found! Very unexpected, but OK.
// Return the plain expression.
if (nullptr == extras)
return dn;
Exp* andhead = nullptr;
Exp* andtail = nullptr;
// We really expect dn to not be null here, but ... perhaps it
// is, if it's just not been observed before.
Exp* eee = nullptr;
if (dn)
{
eee = dn->exp;
dict_node_free_lookup(dict, dn);
// Isolate the expression, so that it's not edited.
eee = make_and_node(sentlo->Exp_pool, eee, NULL);
}
// Place the dictionary expression in the middle;
// wrap on the left and right with extra word-pairs.
and_enchain_right(sentlo->Exp_pool, andhead, andtail, eee);
Exp* optex = make_optional_node(sentlo->Exp_pool, extras);
and_enchain_left(sentlo->Exp_pool, andhead, andtail, optex);
optex = make_optional_node(sentlo->Exp_pool, extras);
and_enchain_right(sentlo->Exp_pool, andhead, andtail, optex);
const char* ssc = germ->get_name().c_str();
lgdebug(D_USER_INFO, "Atomese: Decorated %d exprs for >>%s<<\n",
size_of_expression(andhead), ssc);
Dict_node * dsn = dict_node_new();
dsn->string = ssc;
dsn->exp = andhead;
return dsn;
}
#endif /* HAVE_ATOMESE */