/**
* Tests for ProbLog Integration
*
* These tests verify the ProbLog solver integration works correctly.
* Note: Requires Python 3 and ProbLog installed (pip install problog)
*/
import { describe, test, expect, beforeAll, afterAll } from '@jest/globals';
import {
ProbLogSolver,
createProbLogSolver,
probFact,
query,
evidence,
rule,
buildProgram
} from './problogIntegration.js';
// Skip tests if ProbLog is not available
const SKIP_TESTS = process.env.SKIP_PROBLOG_TESTS === 'true';
describe('ProbLog Integration', () => {
let solver: ProbLogSolver;
beforeAll(async () => {
if (SKIP_TESTS) {
console.log('⚠️ Skipping ProbLog tests (SKIP_PROBLOG_TESTS=true)');
return;
}
solver = createProbLogSolver({
timeout: 5000
});
try {
await solver.initialize();
} catch (error) {
console.error('Failed to initialize ProbLog solver:', error);
console.log('💡 Install ProbLog with: pip install problog');
throw error;
}
}, 10000);
afterAll(async () => {
if (!SKIP_TESTS && solver) {
await solver.shutdown();
}
});
describe('Connection and Setup', () => {
test('should test connection successfully', async () => {
if (SKIP_TESTS) return;
const isConnected = await solver.testConnection();
expect(isConnected).toBe(true);
});
test('should get ProbLog version', async () => {
if (SKIP_TESTS) return;
const version = await solver.getVersion();
expect(version).toBeTruthy();
expect(typeof version).toBe('string');
console.log(`ProbLog version: ${version}`);
});
});
describe('Simple Probability Queries', () => {
test('should compute simple probability (coin flip)', async () => {
if (SKIP_TESTS) return;
const program = buildProgram({
facts: [probFact(0.5, 'heads')],
queries: [query('heads')]
});
const result = await solver.query(program, ['heads']);
expect(result.probabilities).toBeDefined();
expect(result.probabilities.heads).toBeCloseTo(0.5);
expect(result.timeMs).toBeGreaterThan(0);
});
test('should compute joint probability (two coins)', async () => {
if (SKIP_TESTS) return;
const program = buildProgram({
facts: [
probFact(0.5, 'coin1'),
probFact(0.5, 'coin2')
],
rules: [
rule('both', 'coin1, coin2')
],
queries: [query('both')]
});
const result = await solver.query(program, ['both']);
expect(result.probabilities.both).toBeCloseTo(0.25);
});
test('should compute probability with different values', async () => {
if (SKIP_TESTS) return;
const program = buildProgram({
facts: [probFact(0.3, 'rain')],
queries: [query('rain')]
});
const result = await solver.query(program, ['rain']);
expect(result.probabilities.rain).toBeCloseTo(0.3);
});
});
describe('Conditional Probability', () => {
test('should compute conditional probability (medical diagnosis)', async () => {
if (SKIP_TESTS) return;
const program = `
% Disease prior
0.01::disease.
% Symptom given disease
0.8::symptom :- disease.
0.05::symptom :- \\+disease.
% Evidence: symptom is observed
evidence(symptom, true).
% Query: what's the probability of disease given symptom?
query(disease).
`.trim();
const result = await solver.query(program, ['disease']);
// P(disease|symptom) = P(symptom|disease) * P(disease) / P(symptom)
// P(symptom) = 0.8 * 0.01 + 0.05 * 0.99 = 0.0575
// P(disease|symptom) = 0.8 * 0.01 / 0.0575 ≈ 0.139
expect(result.probabilities.disease).toBeGreaterThan(0.1);
expect(result.probabilities.disease).toBeLessThan(0.2);
});
test('should handle weather prediction with evidence', async () => {
if (SKIP_TESTS) return;
const program = `
% Weather priors
0.2::rainy.
% Cloudy depends on rainy
0.8::cloudy :- rainy.
0.3::cloudy :- \\+rainy.
% Evidence: we observe cloudy sky
evidence(cloudy, true).
% Query: probability of rain given cloudy
query(rainy).
`.trim();
const result = await solver.query(program, ['rainy']);
// P(rainy|cloudy) should be higher than prior P(rainy)
expect(result.probabilities.rainy).toBeGreaterThan(0.2);
});
});
describe('Validation', () => {
test('should validate correct program', async () => {
if (SKIP_TESTS) return;
const program = buildProgram({
facts: [probFact(0.3, 'rain')],
rules: [rule('wet', 'rain')],
queries: [query('wet')]
});
const result = await solver.validate(program);
expect(result.valid).toBe(true);
expect(result.facts?.length).toBeGreaterThan(0);
expect(result.queries?.length).toBeGreaterThan(0);
});
test('should detect invalid probability', async () => {
if (SKIP_TESTS) return;
const program = '2.0::invalid.'; // Probability > 1
const result = await solver.validate(program);
expect(result.valid).toBe(false);
expect(result.errors).toBeDefined();
expect(result.errors!.length).toBeGreaterThan(0);
});
test('should validate program structure', async () => {
if (SKIP_TESTS) return;
const program = `
0.5::a.
0.5::b.
c :- a, b.
query(c).
`.trim();
const result = await solver.validate(program);
expect(result.valid).toBe(true);
expect(result.facts).toContain('0.5::a.');
expect(result.facts).toContain('0.5::b.');
expect(result.rules).toContain('c :- a, b.');
expect(result.queries).toContain('query(c).');
});
});
describe('Sampling', () => {
test('should generate samples', async () => {
if (SKIP_TESTS) return;
const program = buildProgram({
facts: [probFact(0.5, 'coin')],
queries: [query('coin')]
});
const result = await solver.sample(program, 100);
expect(result.samples).toBeDefined();
expect(result.samples.length).toBe(100);
expect(result.n).toBe(100);
expect(result.statistics).toBeDefined();
});
test('should compute sample statistics', async () => {
if (SKIP_TESTS) return;
const program = buildProgram({
facts: [probFact(0.3, 'event')],
queries: [query('event')]
});
const result = await solver.sample(program, 1000);
// With 1000 samples, frequency should be close to 0.3
expect(result.statistics?.frequencies?.event).toBeGreaterThan(0.2);
expect(result.statistics?.frequencies?.event).toBeLessThan(0.4);
});
});
describe('Parameter Learning', () => {
test('should learn simple parameter', async () => {
if (SKIP_TESTS) return;
// Program with learnable parameter
const program = `
t(_)::disease.
symptom :- disease.
`.trim();
// Training examples
const examples = [
{ symptom: true, disease: true },
{ symptom: true, disease: true },
{ symptom: false, disease: false },
{ symptom: false, disease: false }
];
const result = await solver.learn(program, examples, 10000);
expect(result.weights).toBeDefined();
expect(result.score).toBeDefined();
expect(result.iterations).toBeGreaterThan(0);
});
});
describe('Error Handling', () => {
test('should handle timeout', async () => {
if (SKIP_TESTS) return;
// Complex program that might timeout
const program = buildProgram({
facts: Array.from({ length: 50 }, (_, i) => probFact(0.5, `var${i}`)),
queries: [query('var0')]
});
await expect(
solver.query(program, ['var0'], 100)
).rejects.toThrow(/timeout/i);
});
test('should handle syntax error', async () => {
if (SKIP_TESTS) return;
const program = 'invalid syntax here!';
const result = await solver.validate(program);
expect(result.valid).toBe(false);
});
test('should handle empty program', async () => {
if (SKIP_TESTS) return;
const program = '';
const result = await solver.validate(program);
// Empty program is technically valid
expect(result).toBeDefined();
});
});
describe('Complex Scenarios', () => {
test('should handle Bayesian network', async () => {
if (SKIP_TESTS) return;
const program = `
% Classic earthquake/burglary/alarm network
0.001::earthquake.
0.01::burglary.
% Alarm probabilities
0.95::alarm :- earthquake, burglary.
0.29::alarm :- earthquake, \\+burglary.
0.94::alarm :- \\+earthquake, burglary.
0.001::alarm :- \\+earthquake, \\+burglary.
query(alarm).
query(burglary).
query(earthquake).
`.trim();
const result = await solver.query(program);
expect(result.probabilities.alarm).toBeDefined();
expect(result.probabilities.burglary).toBeDefined();
expect(result.probabilities.earthquake).toBeDefined();
// P(alarm) should be dominated by burglary case
expect(result.probabilities.alarm).toBeGreaterThan(0.009);
expect(result.probabilities.alarm).toBeLessThan(0.02);
});
test('should handle multiple queries', async () => {
if (SKIP_TESTS) return;
const program = `
0.6::flu.
0.2::covid.
0.1::cold.
0.85::fever :- flu.
0.90::fever :- covid.
0.30::fever :- cold.
0.70::cough :- flu.
0.80::cough :- covid.
0.90::cough :- cold.
query(flu).
query(covid).
query(cold).
query(fever).
query(cough).
`.trim();
const result = await solver.query(program);
expect(Object.keys(result.probabilities).length).toBeGreaterThanOrEqual(5);
expect(result.probabilities.flu).toBeCloseTo(0.6);
expect(result.probabilities.covid).toBeCloseTo(0.2);
expect(result.probabilities.cold).toBeCloseTo(0.1);
});
});
describe('Helper Functions', () => {
test('probFact should create valid fact', () => {
const fact = probFact(0.3, 'rain');
expect(fact).toBe('0.3::rain.');
});
test('query should create valid query', () => {
const q = query('test');
expect(q).toBe('query(test).');
});
test('evidence should create valid evidence', () => {
const ev = evidence('observed', true);
expect(ev).toBe('evidence(observed, true).');
});
test('rule should create valid rule', () => {
const r = rule('wet', 'rain');
expect(r).toBe('wet :- rain.');
});
test('buildProgram should combine parts', () => {
const program = buildProgram({
facts: ['0.5::a.'],
rules: ['b :- a.'],
queries: ['query(b).']
});
expect(program).toContain('0.5::a.');
expect(program).toContain('b :- a.');
expect(program).toContain('query(b).');
});
});
});