/*
* tests/persist/gpu/GpuBasicSaveUTest.cxxtest
*
* Basic save/restore test for GpuStorageNode (Step 3).
* Verifies that the BackingStore wrapper correctly delegates
* atom storage to the GPU atom table.
*
* Tests:
* 1. Store individual nodes and links, verify via GPU fetch.
* 2. Round-trip: storeAtomSpace → loadAtomSpace into fresh AS.
*
* Copyright (C) 2026 OpenCog Foundation
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <cstdio>
#include <cstring>
#include <opencog/atoms/base/Atom.h>
#include <opencog/atoms/base/Link.h>
#include <opencog/atoms/base/Node.h>
#include <opencog/atoms/atom_types/atom_types.h>
#include <opencog/atomspace/AtomSpace.h>
#include <opencog/persist/gpu/GpuStorageNode.h>
#include <opencog/persist/gpu/GpuAtomTable.h>
#include <opencog/persist/gpu-types/atom_types.h>
#include <opencog/atoms/value/LinkValue.h>
#include <set>
#include <string>
#include <opencog/util/Logger.h>
using namespace opencog;
class GpuBasicSaveUTest : public CxxTest::TestSuite
{
public:
GpuBasicSaveUTest(void)
{
logger().set_level(Logger::INFO);
logger().set_print_to_stdout_flag(true);
}
~GpuBasicSaveUTest()
{
if (!CxxTest::TestTracker::tracker().suiteFailed())
std::remove(logger().get_filename().c_str());
}
void test_open_close(void);
void test_store_nodes(void);
void test_store_link(void);
void test_roundtrip(void);
void test_fetch_incoming(void);
void test_query(void);
void test_v1_multihop(void);
void test_v4_neurosymbolic(void);
void test_agent_loop(void);
void test_runquery_gpu(void);
void test_runquery_multiclause(void);
};
// Execute a 3-variable MeetLink; return (X,Z) pairs = (outgoing[0], outgoing[2]).
static std::vector<std::pair<Handle,Handle>> run_xz(AtomSpace* as, Handle vd, Handle body)
{
Handle meet = as->add_link(MEET_LINK, vd, body);
ValuePtr sv = meet->execute(as);
std::vector<std::pair<Handle,Handle>> out;
LinkValuePtr lv = LinkValueCast(sv);
if (lv)
for (const Handle& g : lv->to_handle_seq())
if (g->is_link() and g->get_arity() >= 3)
out.push_back({g->getOutgoingAtom(0), g->getOutgoingAtom(2)});
return out;
}
// Execute a multi-variable MeetLink; return the set of FIRST-variable ($X)
// groundings (results are ListLinks of the bound vars in order).
static std::set<std::string> run_query_firstvar(AtomSpace* as, Handle vardecl, Handle body)
{
Handle meet = as->add_link(MEET_LINK, vardecl, body);
ValuePtr sv = meet->execute(as);
std::set<std::string> names;
LinkValuePtr lv = LinkValueCast(sv);
if (lv)
for (const Handle& g : lv->to_handle_seq())
{
if (g->is_link() and g->get_arity() >= 1)
names.insert(g->getOutgoingAtom(0)->get_name());
else if (g->is_node())
names.insert(g->get_name());
}
return names;
}
// Execute a MeetLink (vardecl, body) in `as`; return grounded node names.
static std::set<std::string> run_query(AtomSpace* as, Handle vardecl, Handle body)
{
Handle meet = as->add_link(MEET_LINK, vardecl, body);
ValuePtr sv = meet->execute(as);
std::set<std::string> names;
LinkValuePtr lv = LinkValueCast(sv);
if (lv)
for (const Handle& h : lv->to_handle_seq())
if (h->is_node()) names.insert(h->get_name());
return names;
}
// Run (Meet $X:Concept (Inheritance $X animal)) in `as`; return the set
// of grounded node-names. Used to compare RAM-baseline vs GPU-fetched data.
static std::set<std::string> run_meet(AtomSpace* as, Handle animal)
{
Handle var = as->add_node(VARIABLE_NODE, "$X");
Handle tv = as->add_link(TYPED_VARIABLE_LINK, var,
as->add_node(TYPE_NODE, "ConceptNode"));
Handle body = as->add_link(INHERITANCE_LINK, var, animal);
Handle meet = as->add_link(MEET_LINK, tv, body);
ValuePtr sv = meet->execute(as);
std::set<std::string> names;
LinkValuePtr lv = LinkValueCast(sv);
if (lv)
for (const Handle& h : lv->to_handle_seq())
if (h->is_node()) names.insert(h->get_name());
return names;
}
// ============================================================
// Test that open/close/connected work correctly.
void GpuBasicSaveUTest::test_open_close(void)
{
logger().debug("BEGIN TEST: %s", __FUNCTION__);
GpuStorageNode* store = new GpuStorageNode(GPU_STORAGE_NODE, "gpu://test");
TSM_ASSERT("Should not be connected before open", !store->connected());
store->open();
TSM_ASSERT("Should be connected after open", store->connected());
// Double-open should be safe.
store->open();
TSM_ASSERT("Still connected after double open", store->connected());
store->close();
TSM_ASSERT("Should not be connected after close", !store->connected());
delete store;
printf("test_open_close: PASSED\n");
logger().debug("END TEST: %s", __FUNCTION__);
}
// ============================================================
// Store nodes through BackingStore, verify via GPU fetch.
void GpuBasicSaveUTest::test_store_nodes(void)
{
logger().debug("BEGIN TEST: %s", __FUNCTION__);
GpuStorageNode* store = new GpuStorageNode(GPU_STORAGE_NODE, "gpu://test");
store->open();
TSM_ASSERT("Not connected", store->connected());
// Create atoms (not in any AtomSpace — raw handles).
Handle cat(createNode(CONCEPT_NODE, "cat"));
Handle dog(createNode(CONCEPT_NODE, "dog"));
Handle isa(createNode(PREDICATE_NODE, "is-a"));
// Store through BackingStore API.
store->storeAtom(cat, true);
store->storeAtom(dog, true);
store->storeAtom(isa, true);
store->barrier();
// Verify GPU table has 3 atoms.
const GpuAtomTable& t = store->gpu_table();
TSM_ASSERT_EQUALS("Expected 3 atoms", t.atom_count, 3u);
// Fetch slot 0 from GPU, verify it's "cat".
{
uint16_t type;
char name_buf[256];
uint16_t name_len = sizeof(name_buf);
int rc = gpu_fetch_node(&t, 0, &type, name_buf, &name_len);
TSM_ASSERT_EQUALS("Fetch node 0 failed", rc, 0);
TSM_ASSERT_EQUALS("Type mismatch slot 0", type, (uint16_t)CONCEPT_NODE);
TSM_ASSERT_EQUALS("Name length mismatch", name_len, 3u);
TSM_ASSERT("Name mismatch slot 0", memcmp(name_buf, "cat", 3) == 0);
}
// Fetch slot 1 — "dog".
{
uint16_t type;
char name_buf[256];
uint16_t name_len = sizeof(name_buf);
int rc = gpu_fetch_node(&t, 1, &type, name_buf, &name_len);
TSM_ASSERT_EQUALS("Fetch node 1 failed", rc, 0);
TSM_ASSERT_EQUALS("Type mismatch slot 1", type, (uint16_t)CONCEPT_NODE);
TSM_ASSERT_EQUALS("Name length mismatch", name_len, 3u);
TSM_ASSERT("Name mismatch slot 1", memcmp(name_buf, "dog", 3) == 0);
}
// Fetch slot 2 — "is-a" (PredicateNode).
{
uint16_t type;
char name_buf[256];
uint16_t name_len = sizeof(name_buf);
int rc = gpu_fetch_node(&t, 2, &type, name_buf, &name_len);
TSM_ASSERT_EQUALS("Fetch node 2 failed", rc, 0);
TSM_ASSERT_EQUALS("Type mismatch slot 2", type, (uint16_t)PREDICATE_NODE);
TSM_ASSERT_EQUALS("Name length mismatch", name_len, 4u);
TSM_ASSERT("Name mismatch slot 2", memcmp(name_buf, "is-a", 4) == 0);
}
// Storing the same atom again should be a no-op.
store->storeAtom(cat, true);
TSM_ASSERT_EQUALS("Duplicate store changed count", t.atom_count, 3u);
store->close();
delete store;
printf("test_store_nodes: PASSED\n");
logger().debug("END TEST: %s", __FUNCTION__);
}
// ============================================================
// Store a link (with outgoing atoms), verify GPU data.
void GpuBasicSaveUTest::test_store_link(void)
{
logger().debug("BEGIN TEST: %s", __FUNCTION__);
GpuStorageNode* store = new GpuStorageNode(GPU_STORAGE_NODE, "gpu://test");
store->open();
Handle cat(createNode(CONCEPT_NODE, "cat"));
Handle dog(createNode(CONCEPT_NODE, "dog"));
Handle lnk(createLink(LIST_LINK, cat, dog));
// storeAtom on the link should recursively store cat and dog first.
store->storeAtom(lnk, true);
store->barrier();
const GpuAtomTable& t = store->gpu_table();
TSM_ASSERT_EQUALS("Expected 3 atoms (2 nodes + 1 link)", t.atom_count, 3u);
// Verify the link at slot 2.
{
uint16_t type;
uint32_t out_buf[16];
uint16_t arity = 16;
int rc = gpu_fetch_link(&t, 2, &type, out_buf, &arity);
TSM_ASSERT_EQUALS("Fetch link failed", rc, 0);
TSM_ASSERT_EQUALS("Link type mismatch", type, (uint16_t)LIST_LINK);
TSM_ASSERT_EQUALS("Arity mismatch", arity, 2u);
TSM_ASSERT_EQUALS("Outgoing[0] should be slot 0", out_buf[0], 0u);
TSM_ASSERT_EQUALS("Outgoing[1] should be slot 1", out_buf[1], 1u);
}
// Test link-of-links.
Handle isa(createNode(PREDICATE_NODE, "is-a"));
Handle evl(createLink(EVALUATION_LINK, isa, lnk));
store->storeAtom(evl, true);
store->barrier();
// isa → slot 3, evl → slot 4
TSM_ASSERT_EQUALS("Expected 5 atoms", t.atom_count, 5u);
{
uint16_t type;
uint32_t out_buf[16];
uint16_t arity = 16;
int rc = gpu_fetch_link(&t, 4, &type, out_buf, &arity);
TSM_ASSERT_EQUALS("Fetch eval link failed", rc, 0);
TSM_ASSERT_EQUALS("EvalLink type mismatch", type, (uint16_t)EVALUATION_LINK);
TSM_ASSERT_EQUALS("EvalLink arity mismatch", arity, 2u);
TSM_ASSERT_EQUALS("EvalLink out[0] = isa (slot 3)", out_buf[0], 3u);
TSM_ASSERT_EQUALS("EvalLink out[1] = lnk (slot 2)", out_buf[1], 2u);
}
store->close();
delete store;
printf("test_store_link: PASSED\n");
logger().debug("END TEST: %s", __FUNCTION__);
}
// ============================================================
// Full round-trip: store from one AtomSpace, load into another.
void GpuBasicSaveUTest::test_roundtrip(void)
{
logger().debug("BEGIN TEST: %s", __FUNCTION__);
GpuStorageNode* store = new GpuStorageNode(GPU_STORAGE_NODE, "gpu://test");
store->open();
// Build an AtomSpace with some atoms.
AtomSpace* as1 = new AtomSpace();
Handle cat = as1->add_node(CONCEPT_NODE, "cat");
Handle dog = as1->add_node(CONCEPT_NODE, "dog");
Handle mammal = as1->add_node(CONCEPT_NODE, "mammal");
Handle lnk = as1->add_link(LIST_LINK, cat, dog);
Handle inh = as1->add_link(INHERITANCE_LINK, cat, mammal);
// Store entire AtomSpace to GPU.
store->storeAtomSpace(as1);
store->barrier();
TSM_ASSERT_EQUALS("Expected 5 atoms on GPU",
store->gpu_table().atom_count, 5u);
// Load into a fresh AtomSpace.
AtomSpace* as2 = new AtomSpace();
store->loadAtomSpace(as2);
// Verify all atoms were reconstructed.
size_t sz1 = as1->get_size();
size_t sz2 = as2->get_size();
printf("Round-trip: stored %lu, loaded %lu atoms\n", sz1, sz2);
TSM_ASSERT_EQUALS("Atom count mismatch after round-trip", sz1, sz2);
// Verify specific atoms exist in as2 (use get_atom with prototype Handle).
Handle cat2 = as2->get_atom(createNode(CONCEPT_NODE, "cat"));
TSM_ASSERT("cat not found in loaded AS", cat2 != nullptr);
Handle dog2 = as2->get_atom(createNode(CONCEPT_NODE, "dog"));
TSM_ASSERT("dog not found in loaded AS", dog2 != nullptr);
Handle mammal2 = as2->get_atom(createNode(CONCEPT_NODE, "mammal"));
TSM_ASSERT("mammal not found in loaded AS", mammal2 != nullptr);
// Verify link structure.
Handle lnk2 = as2->get_atom(createLink(LIST_LINK, cat2, dog2));
TSM_ASSERT("ListLink(cat,dog) not found", lnk2 != nullptr);
if (lnk2)
{
TSM_ASSERT_EQUALS("ListLink arity mismatch",
lnk2->get_arity(), lnk->get_arity());
}
Handle inh2 = as2->get_atom(createLink(INHERITANCE_LINK, cat2, mammal2));
TSM_ASSERT("InheritanceLink(cat,mammal) not found", inh2 != nullptr);
store->close();
delete store;
delete as1;
delete as2;
printf("test_roundtrip: PASSED\n");
logger().debug("END TEST: %s", __FUNCTION__);
}
// ============================================================
// Phase 2b: fetch incoming-set from the GPU into a fresh AtomSpace.
void GpuBasicSaveUTest::test_fetch_incoming(void)
{
logger().debug("BEGIN TEST: %s", __FUNCTION__);
GpuStorageNode* store = new GpuStorageNode(GPU_STORAGE_NODE, "gpu://test");
store->open();
// Knowledge graph: cat is-a animal, dog is-a animal, salmon is-a fish.
AtomSpace* as1 = new AtomSpace();
Handle cat = as1->add_node(CONCEPT_NODE, "cat");
Handle dog = as1->add_node(CONCEPT_NODE, "dog");
Handle animal = as1->add_node(CONCEPT_NODE, "animal");
Handle fish = as1->add_node(CONCEPT_NODE, "fish");
Handle salmon = as1->add_node(CONCEPT_NODE, "salmon");
as1->add_link(INHERITANCE_LINK, cat, animal);
as1->add_link(INHERITANCE_LINK, dog, animal);
as1->add_link(INHERITANCE_LINK, salmon, fish);
store->storeAtomSpace(as1);
store->barrier();
// Fresh AtomSpace that only knows 'animal'. Pull its incoming
// InheritanceLinks back from the GPU.
AtomSpace* as2 = new AtomSpace();
Handle animal2 = as2->add_node(CONCEPT_NODE, "animal");
store->fetchIncomingByType(as2, animal2, INHERITANCE_LINK);
Handle inh_cat = as2->get_atom(createLink(INHERITANCE_LINK,
createNode(CONCEPT_NODE, "cat"), createNode(CONCEPT_NODE, "animal")));
Handle inh_dog = as2->get_atom(createLink(INHERITANCE_LINK,
createNode(CONCEPT_NODE, "dog"), createNode(CONCEPT_NODE, "animal")));
Handle inh_salmon = as2->get_atom(createLink(INHERITANCE_LINK,
createNode(CONCEPT_NODE, "salmon"), createNode(CONCEPT_NODE, "fish")));
TSM_ASSERT("cat is-a animal should be fetched", inh_cat != nullptr);
TSM_ASSERT("dog is-a animal should be fetched", inh_dog != nullptr);
TSM_ASSERT("salmon is-a fish must NOT be fetched (not in animal's incoming)",
inh_salmon == nullptr);
// animal's incoming-set has exactly 2 InheritanceLinks.
TSM_ASSERT_EQUALS("animal incoming-by-type count",
animal2->getIncomingSetSizeByType(INHERITANCE_LINK), (size_t)2);
store->close();
delete store;
delete as1;
delete as2;
printf("test_fetch_incoming: PASSED\n");
logger().debug("END TEST: %s", __FUNCTION__);
}
// ============================================================
// Phase 2c: GOLD CHECK. Run the SAME query (a) in RAM (baseline) and
// (b) against data fetched from the GPU. Results must be identical.
void GpuBasicSaveUTest::test_query(void)
{
logger().debug("BEGIN TEST: %s", __FUNCTION__);
GpuStorageNode* store = new GpuStorageNode(GPU_STORAGE_NODE, "gpu://test");
store->open();
// KG: cat/dog/bear is-a animal; salmon is-a fish.
// Use createAtomSpace() (shared_ptr) — MeetLink::execute needs shared_from_this.
AtomSpacePtr as1 = createAtomSpace();
Handle cat = as1->add_node(CONCEPT_NODE, "cat");
Handle dog = as1->add_node(CONCEPT_NODE, "dog");
Handle bear = as1->add_node(CONCEPT_NODE, "bear");
Handle animal = as1->add_node(CONCEPT_NODE, "animal");
Handle fish = as1->add_node(CONCEPT_NODE, "fish");
Handle salmon = as1->add_node(CONCEPT_NODE, "salmon");
as1->add_link(INHERITANCE_LINK, cat, animal);
as1->add_link(INHERITANCE_LINK, dog, animal);
as1->add_link(INHERITANCE_LINK, bear, animal);
as1->add_link(INHERITANCE_LINK, salmon, fish);
store->storeAtomSpace(as1.get());
store->barrier();
// (a) Baseline: query the full AtomSpace in RAM.
std::set<std::string> baseline = run_meet(as1.get(), animal);
// (b) GPU path: fresh AtomSpace, pull animal's incoming from the GPU,
// then run the identical query against the fetched data.
AtomSpacePtr as2 = createAtomSpace();
Handle animal2 = as2->add_node(CONCEPT_NODE, "animal");
store->fetchIncomingByType(as2.get(), animal2, INHERITANCE_LINK);
std::set<std::string> gpu = run_meet(as2.get(), animal2);
std::set<std::string> expect = {"cat", "dog", "bear"};
printf("test_query: baseline={");
for (auto& s : baseline) printf("%s ", s.c_str());
printf("} gpu={");
for (auto& s : gpu) printf("%s ", s.c_str());
printf("}\n");
TSM_ASSERT_EQUALS("baseline size should be 3", baseline.size(), (size_t)3);
TSM_ASSERT("baseline == {cat,dog,bear}", baseline == expect);
TSM_ASSERT("GOLD: GPU-path query result == RAM baseline", gpu == baseline);
TSM_ASSERT("GPU-path == {cat,dog,bear}", gpu == expect);
store->close();
delete store;
printf("test_query: PASSED (gold check: GPU query == in-RAM baseline)\n");
logger().debug("END TEST: %s", __FUNCTION__);
}
// ============================================================
// V1 VALUE TEST: crisp multi-hop reasoning, no hallucination.
// A multi-relation KG round-tripped through the GPU; multi-clause
// queries with deliberate LLM-traps. Exact + deterministic.
void GpuBasicSaveUTest::test_v1_multihop(void)
{
logger().debug("BEGIN TEST: %s", __FUNCTION__);
GpuStorageNode* store = new GpuStorageNode(GPU_STORAGE_NODE, "gpu://test");
store->open();
AtomSpacePtr as1 = createAtomSpace();
// is-a (InheritanceLink)
auto isa = [&](const char* x, const char* t) {
as1->add_link(INHERITANCE_LINK,
as1->add_node(CONCEPT_NODE, x), as1->add_node(CONCEPT_NODE, t)); };
// lives-in (EvaluationLink (Predicate "lives-in") (List x place))
auto lives = [&](const char* x, const char* p) {
as1->add_link(EVALUATION_LINK, as1->add_node(PREDICATE_NODE, "lives-in"),
as1->add_link(LIST_LINK,
as1->add_node(CONCEPT_NODE, x), as1->add_node(CONCEPT_NODE, p))); };
isa("whale","mammal"); isa("dolphin","mammal"); isa("seal","mammal");
isa("bat","mammal"); isa("cat","mammal");
isa("shark","fish"); isa("salmon","fish"); isa("eagle","bird");
lives("whale","water"); lives("dolphin","water"); lives("seal","water");
lives("shark","water"); lives("salmon","water");
lives("bat","air"); lives("eagle","air"); lives("cat","land");
// Store the whole KG to the GPU, then load it back from the GPU.
store->storeAtomSpace(as1.get());
store->barrier();
AtomSpacePtr gas = createAtomSpace(); // GPU-sourced AtomSpace
store->loadAtomSpace(gas.get());
// Multi-hop query builder: (is-a $X type) AND (lives-in $X place)
auto query = [](AtomSpace* as, const char* type, const char* place) {
Handle X = as->add_node(VARIABLE_NODE, "$X");
Handle vd = as->add_link(TYPED_VARIABLE_LINK, X,
as->add_node(TYPE_NODE, "ConceptNode"));
Handle c1 = as->add_link(INHERITANCE_LINK, X, as->add_node(CONCEPT_NODE, type));
Handle c2 = as->add_link(EVALUATION_LINK, as->add_node(PREDICATE_NODE, "lives-in"),
as->add_link(LIST_LINK, X, as->add_node(CONCEPT_NODE, place)));
Handle body = as->add_link(AND_LINK, c1, c2);
return run_query(as, vd, body);
};
// Q1: mammals that live in water. Traps: shark/salmon (water, but fish),
// bat/cat (mammal, but not water).
std::set<std::string> q1_base = query(as1.get(), "mammal", "water");
std::set<std::string> q1_gpu = query(gas.get(), "mammal", "water");
std::set<std::string> q1_exp = {"whale", "dolphin", "seal"};
printf("\nV1 Q1 'mammals that live in water': {");
for (auto& s : q1_gpu) printf("%s ", s.c_str());
printf("} (LLM trap: shark/salmon live in water but are fish)\n");
TSM_ASSERT("Q1 GPU == in-RAM baseline", q1_gpu == q1_base);
TSM_ASSERT("Q1 == {whale,dolphin,seal}", q1_gpu == q1_exp);
// Q2: fish that live in water. Trap: whale/dolphin (water, but mammals).
std::set<std::string> q2_gpu = query(gas.get(), "fish", "water");
std::set<std::string> q2_exp = {"shark", "salmon"};
printf("V1 Q2 'fish that live in water': {");
for (auto& s : q2_gpu) printf("%s ", s.c_str());
printf("} (LLM trap: whale/dolphin live in water but are mammals)\n");
TSM_ASSERT("Q2 == {shark,salmon}", q2_gpu == q2_exp);
// Determinism: same query, run 5x, identical every time.
bool stable = true;
for (int i = 0; i < 5; i++)
if (query(gas.get(), "mammal", "water") != q1_exp) stable = false;
TSM_ASSERT("Q1 deterministic over 5 runs", stable);
printf("V1 determinism: 5/5 identical runs -> %s\n", stable ? "PASS" : "FAIL");
store->close();
delete store;
printf("test_v1_multihop: PASSED (crisp multi-hop, grounded, deterministic)\n");
logger().debug("END TEST: %s", __FUNCTION__);
}
// ============================================================
// V4 NEUROSYMBOLIC LOOP: facts extracted by an LLM from text are stored
// in the GPU AtomSpace; a 3-fact composition query (which an LLM-alone
// is prone to get wrong) is answered exactly by the symbolic engine.
//
// Source text (the LLM extraction is done outside this test):
// "Alice manages Bob, who manages Carol; Carol in turn manages Dave.
// Alice and Bob are based in the Boston office, while Carol and Dave
// work out of Seattle."
// Question: who manages someone in Seattle but is themselves in Boston?
// = (manages $X $Y) AND (works-in $Y Seattle) AND (works-in $X Boston)
// Answer: Bob (manages Carol@Seattle; Bob@Boston). Needs 3 facts about
// TWO different people — the cross-referencing LLMs bungle.
void GpuBasicSaveUTest::test_v4_neurosymbolic(void)
{
logger().debug("BEGIN TEST: %s", __FUNCTION__);
GpuStorageNode* store = new GpuStorageNode(GPU_STORAGE_NODE, "gpu://test");
store->open();
AtomSpacePtr as1 = createAtomSpace();
auto rel = [&](const char* pred, const char* a, const char* b) {
as1->add_link(EVALUATION_LINK, as1->add_node(PREDICATE_NODE, pred),
as1->add_link(LIST_LINK,
as1->add_node(CONCEPT_NODE, a), as1->add_node(CONCEPT_NODE, b))); };
// LLM-extracted facts:
rel("manages","Alice","Bob"); rel("manages","Bob","Carol"); rel("manages","Carol","Dave");
rel("works-in","Alice","Boston"); rel("works-in","Bob","Boston");
rel("works-in","Carol","Seattle"); rel("works-in","Dave","Seattle");
// Round-trip through the GPU.
store->storeAtomSpace(as1.get());
store->barrier();
AtomSpacePtr gas = createAtomSpace();
store->loadAtomSpace(gas.get());
auto query = [](AtomSpace* as) {
Handle X = as->add_node(VARIABLE_NODE, "$X");
Handle Y = as->add_node(VARIABLE_NODE, "$Y");
Handle vd = as->add_link(VARIABLE_LIST,
as->add_link(TYPED_VARIABLE_LINK, X, as->add_node(TYPE_NODE, "ConceptNode")),
as->add_link(TYPED_VARIABLE_LINK, Y, as->add_node(TYPE_NODE, "ConceptNode")));
Handle mgr = as->add_node(PREDICATE_NODE, "manages");
Handle win = as->add_node(PREDICATE_NODE, "works-in");
Handle c1 = as->add_link(EVALUATION_LINK, mgr, as->add_link(LIST_LINK, X, Y));
Handle c2 = as->add_link(EVALUATION_LINK, win,
as->add_link(LIST_LINK, Y, as->add_node(CONCEPT_NODE, "Seattle")));
Handle c3 = as->add_link(EVALUATION_LINK, win,
as->add_link(LIST_LINK, X, as->add_node(CONCEPT_NODE, "Boston")));
Handle body = as->add_link(AND_LINK, c1, c2, c3);
return run_query_firstvar(as, vd, body);
};
std::set<std::string> ans = query(gas.get());
std::set<std::string> exp = {"Bob"};
printf("\nV4 neurosymbolic: 'who manages someone in Seattle but works in Boston?'\n");
printf(" AtomSpace answer: {");
for (auto& s : ans) printf("%s ", s.c_str());
printf("} (exact 3-fact composition; LLM-alone often says Alice or Carol)\n");
TSM_ASSERT("V4 answer == {Bob}", ans == exp);
TSM_ASSERT("V4 GPU == baseline", ans == query(as1.get()));
store->close();
delete store;
printf("test_v4_neurosymbolic: PASSED (text->facts->GPU AtomSpace->exact 3-hop answer)\n");
logger().debug("END TEST: %s", __FUNCTION__);
}
// ============================================================
// Phase 3 AGENT LOOP: a minimal Linas 5-bullet agent over a GPU world-model.
// world-model = GpuStorageNode AtomSpace (inside) ; sense = ingest facts ;
// decide = query the world-model ; act = derive a new fact ;
// update = store it back to the GPU. Loop to fixpoint.
// Task: forward-chain the transitive closure of 'manages' (A->B->C->D).
void GpuBasicSaveUTest::test_agent_loop(void)
{
logger().debug("BEGIN TEST: %s", __FUNCTION__);
GpuStorageNode* store = new GpuStorageNode(GPU_STORAGE_NODE, "gpu://test");
store->open();
AtomSpacePtr as = createAtomSpace(); // the agent's world-model
Handle mgr = as->add_node(PREDICATE_NODE, "manages");
Handle rch = as->add_node(PREDICATE_NODE, "reaches");
auto mk = [&](Handle pred, const char* a, const char* b) {
return as->add_link(EVALUATION_LINK, pred,
as->add_link(LIST_LINK,
as->add_node(CONCEPT_NODE, a), as->add_node(CONCEPT_NODE, b))); };
// SENSE: a chain of management facts. Base case: reaches = manages.
const char* chain[3][2] = {{"A","B"},{"B","C"},{"C","D"}};
std::set<std::pair<std::string,std::string>> known;
for (auto& e : chain) { mk(mgr, e[0], e[1]); mk(rch, e[0], e[1]);
known.insert({e[0], e[1]}); }
store->storeAtomSpace(as.get());
store->barrier();
// DECIDE pattern: reaches($X,$Y) AND manages($Y,$Z) -> derive reaches($X,$Z)
Handle X = as->add_node(VARIABLE_NODE, "$X");
Handle Y = as->add_node(VARIABLE_NODE, "$Y");
Handle Z = as->add_node(VARIABLE_NODE, "$Z");
Handle ct = as->add_node(TYPE_NODE, "ConceptNode");
Handle vd = as->add_link(VARIABLE_LIST,
as->add_link(TYPED_VARIABLE_LINK, X, ct),
as->add_link(TYPED_VARIABLE_LINK, Y, ct),
as->add_link(TYPED_VARIABLE_LINK, Z, ct));
Handle body = as->add_link(AND_LINK,
as->add_link(EVALUATION_LINK, rch, as->add_link(LIST_LINK, X, Y)),
as->add_link(EVALUATION_LINK, mgr, as->add_link(LIST_LINK, Y, Z)));
int cycles = 0, total = 0;
while (cycles <= 10)
{
auto xz = run_xz(as.get(), vd, body); // DECIDE (query)
int added = 0;
for (auto& p : xz)
{
std::string xn = p.first->get_name(), zn = p.second->get_name();
if (known.count({xn, zn})) continue;
Handle nf = as->add_link(EVALUATION_LINK, rch, // ACT (derive)
as->add_link(LIST_LINK, p.first, p.second));
store->storeAtom(nf, true); // UPDATE (persist to GPU)
known.insert({xn, zn});
added++;
}
cycles++; total += added;
if (0 == added) break;
}
store->barrier();
printf("\nPhase 3 agent loop: %d cycles, derived %d new 'reaches' facts\n", cycles, total);
printf(" transitive closure: ");
for (auto& k : known) printf("%s->%s ", k.first.c_str(), k.second.c_str());
printf("\n");
// Transitive closure of A->B->C->D has 6 pairs; A->D is the 3-hop derivation.
TSM_ASSERT_EQUALS("closure size == 6", known.size(), (size_t)6);
TSM_ASSERT("derived A->D (3-hop)", known.count({"A","D"}) == 1);
// Persistence: derived facts live on the GPU. Load fresh, count 'reaches'.
AtomSpacePtr fresh = createAtomSpace();
store->loadAtomSpace(fresh.get());
HandleSeq evals;
fresh->get_handles_by_type(evals, EVALUATION_LINK, false);
int rcount = 0;
for (auto& h : evals)
if (h->getOutgoingAtom(0)->get_name() == "reaches") rcount++;
printf(" 'reaches' facts persisted on GPU: %d\n", rcount);
TSM_ASSERT_EQUALS("6 reaches persisted on GPU", rcount, 6);
store->close();
delete store;
printf("test_agent_loop: PASSED (sense->query->derive->update closure on GPU world-model)\n");
logger().debug("END TEST: %s", __FUNCTION__);
}
// ============================================================
// runQuery GPU override: matching runs on the GPU (not the CPU crawl).
// (Meet $X:Concept (Inheritance $X animal)) answered via the GPU fast-path.
void GpuBasicSaveUTest::test_runquery_gpu(void)
{
logger().debug("BEGIN TEST: %s", __FUNCTION__);
GpuStorageNode* store = new GpuStorageNode(GPU_STORAGE_NODE, "gpu://test");
store->open();
AtomSpacePtr as1 = createAtomSpace();
Handle cat = as1->add_node(CONCEPT_NODE, "cat");
Handle dog = as1->add_node(CONCEPT_NODE, "dog");
Handle bear = as1->add_node(CONCEPT_NODE, "bear");
Handle animal = as1->add_node(CONCEPT_NODE, "animal");
Handle fish = as1->add_node(CONCEPT_NODE, "fish");
Handle salmon = as1->add_node(CONCEPT_NODE, "salmon");
as1->add_link(INHERITANCE_LINK, cat, animal);
as1->add_link(INHERITANCE_LINK, dog, animal);
as1->add_link(INHERITANCE_LINK, bear, animal);
as1->add_link(INHERITANCE_LINK, salmon, fish);
store->storeAtomSpace(as1.get());
store->barrier();
// Query through the real runQuery() API — exercises the GPU fast-path.
AtomSpacePtr as = createAtomSpace();
Handle animal2 = as->add_node(CONCEPT_NODE, "animal");
Handle X = as->add_node(VARIABLE_NODE, "$X");
Handle vd = as->add_link(TYPED_VARIABLE_LINK, X, as->add_node(TYPE_NODE, "ConceptNode"));
Handle meet = as->add_link(MEET_LINK, vd, as->add_link(INHERITANCE_LINK, X, animal2));
Handle qkey = as->add_node(PREDICATE_NODE, "*gpu-query-key*");
store->runQuery(meet, qkey);
std::set<std::string> got;
LinkValuePtr lv = LinkValueCast(meet->getValue(qkey));
if (lv)
for (const Handle& h : lv->to_handle_seq())
if (h->is_node()) got.insert(h->get_name());
std::set<std::string> exp = {"cat", "dog", "bear"};
printf("\nrunQuery (GPU override) -> {");
for (auto& s : got) printf("%s ", s.c_str());
printf("}\n");
TSM_ASSERT_EQUALS("runQuery result size 3", got.size(), (size_t)3);
TSM_ASSERT("runQuery GPU result == {cat,dog,bear}", got == exp);
store->close();
delete store;
printf("test_runquery_gpu: PASSED (matching on GPU via runQuery override)\n");
logger().debug("END TEST: %s", __FUNCTION__);
}
// ============================================================
// General multi-clause GPU matcher via runQuery: a conjunction mixing a
// direct InheritanceLink clause + an Eval-relation clause, single variable.
void GpuBasicSaveUTest::test_runquery_multiclause(void)
{
logger().debug("BEGIN TEST: %s", __FUNCTION__);
GpuStorageNode* store = new GpuStorageNode(GPU_STORAGE_NODE, "gpu://test");
store->open();
AtomSpacePtr as1 = createAtomSpace();
auto isa = [&](const char* x, const char* t) {
as1->add_link(INHERITANCE_LINK,
as1->add_node(CONCEPT_NODE, x), as1->add_node(CONCEPT_NODE, t)); };
auto lives = [&](const char* x, const char* p) {
as1->add_link(EVALUATION_LINK, as1->add_node(PREDICATE_NODE, "lives-in"),
as1->add_link(LIST_LINK,
as1->add_node(CONCEPT_NODE, x), as1->add_node(CONCEPT_NODE, p))); };
isa("whale","mammal"); isa("dolphin","mammal"); isa("seal","mammal");
isa("bat","mammal"); isa("cat","mammal");
isa("shark","fish"); isa("salmon","fish");
lives("whale","water"); lives("dolphin","water"); lives("seal","water");
lives("shark","water"); lives("salmon","water");
lives("bat","air"); lives("cat","land");
store->storeAtomSpace(as1.get());
store->barrier();
// (Meet $X (And (Inheritance $X mammal) (Eval (Pred lives-in)(List $X water))))
AtomSpacePtr as = createAtomSpace();
Handle X = as->add_node(VARIABLE_NODE, "$X");
Handle vd = as->add_link(TYPED_VARIABLE_LINK, X, as->add_node(TYPE_NODE, "ConceptNode"));
Handle c1 = as->add_link(INHERITANCE_LINK, X, as->add_node(CONCEPT_NODE, "mammal"));
Handle c2 = as->add_link(EVALUATION_LINK, as->add_node(PREDICATE_NODE, "lives-in"),
as->add_link(LIST_LINK, X, as->add_node(CONCEPT_NODE, "water")));
Handle meet = as->add_link(MEET_LINK, vd, as->add_link(AND_LINK, c1, c2));
Handle qkey = as->add_node(PREDICATE_NODE, "*mc-key*");
store->runQuery(meet, qkey);
std::set<std::string> got;
LinkValuePtr lv = LinkValueCast(meet->getValue(qkey));
if (lv)
for (const Handle& h : lv->to_handle_seq())
if (h->is_node()) got.insert(h->get_name());
std::set<std::string> exp = {"whale", "dolphin", "seal"};
printf("\nrunQuery multi-clause (is-a mammal AND lives-in water) -> {");
for (auto& s : got) printf("%s ", s.c_str());
printf("}\n");
TSM_ASSERT_EQUALS("multi-clause result size 3", got.size(), (size_t)3);
TSM_ASSERT("multi-clause GPU result == {whale,dolphin,seal}", got == exp);
store->close();
delete store;
printf("test_runquery_multiclause: PASSED (general multi-clause matcher on GPU)\n");
logger().debug("END TEST: %s", __FUNCTION__);
}
/* ============================= END OF FILE ================= */