OverviewFilesCommitsIssuesDocsReleasesPeers

atomspace-gpu / tests / persist / gpu / GpuBasicSaveUTest.cxxtest

830 lines · cxxtest ref:
/*
 * 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 ================= */