/**
* Probabilistic Visualizer - ASCII Art Visualizations
* Creates visual representations of probabilistic programs and results
*/
import {
ProbabilisticProgram,
ProbabilisticResult,
BayesianInference,
DecisionRecommendation
} from '../../../types.js';
export class ProbabilisticVisualizer {
/**
* Visualize probabilistic program structure
*/
visualizeProgram(program: ProbabilisticProgram): string {
const lines: string[] = [];
lines.push('Probabilistic Program Structure');
lines.push('===============================\n');
// Create dependency graph
const dependencies = this.buildDependencyGraph(program);
// Visualize as ASCII tree
lines.push(this.createDependencyTree(dependencies));
return lines.join('\n');
}
/**
* Visualize Bayesian network
*/
visualizeBayesianNetwork(program: ProbabilisticProgram): string {
const lines: string[] = [];
lines.push('Bayesian Network');
lines.push('================\n');
const graph = this.buildDependencyGraph(program);
// Create nodes with probabilities
for (const [node, deps] of Object.entries(graph.nodes)) {
const prob = graph.probabilities[node];
if (prob !== undefined) {
lines.push(`┌─────────────────┐`);
lines.push(`│ ${node.padEnd(15)} │`);
lines.push(`│ P=${prob.toFixed(3).padEnd(12)} │`);
lines.push(`└─────────────────┘`);
if (deps.length > 0) {
lines.push(` ↑ depends on`);
for (const dep of deps) {
lines.push(` ├─ ${dep}`);
}
}
lines.push('');
}
}
return lines.join('\n');
}
/**
* Visualize probability distribution
*/
visualizeDistribution(dist: Record<string, number>, title: string = 'Probability Distribution'): string {
const lines: string[] = [];
lines.push(title);
lines.push('='.repeat(title.length));
lines.push('');
const maxProb = Math.max(...Object.values(dist));
const maxWidth = 40;
for (const [label, prob] of Object.entries(dist)) {
const barLength = Math.round((prob / maxProb) * maxWidth);
const bar = '█'.repeat(barLength);
const percent = `${(prob * 100).toFixed(1)}%`;
lines.push(`${label.padEnd(15)} │${bar.padEnd(maxWidth)}│ ${percent.padStart(6)}`);
}
return lines.join('\n');
}
/**
* Visualize derivation tree
*/
visualizeDerivation(result: ProbabilisticResult): string {
const lines: string[] = [];
lines.push(`Derivation Tree for ${result.query}`);
lines.push('='.repeat(25 + result.query.length));
lines.push('');
if (!result.derivationTrace || result.derivationTrace.length === 0) {
lines.push('No derivation trace available.');
return lines.join('\n');
}
lines.push(` ${result.query} (${result.probability.toFixed(3)})`);
lines.push(' │');
for (const step of result.derivationTrace) {
const indentLevel = step.dependencies.length;
const indent = ' '.repeat(indentLevel);
if (step.dependencies.length > 0) {
lines.push(`${indent}├─ ${step.rule}`);
lines.push(`${indent}│ P=${step.probability.toFixed(3)}`);
for (let i = 0; i < step.dependencies.length; i++) {
const dep = step.dependencies[i];
const isLast = i === step.dependencies.length - 1;
const branch = isLast ? '└─' : '├─';
lines.push(`${indent}${branch} ${dep}`);
}
} else {
lines.push(` ├─ ${step.rule} (base fact)`);
}
}
return lines.join('\n');
}
/**
* Visualize inference comparison
*/
visualizeInferenceComparison(inference: BayesianInference): string {
const lines: string[] = [];
lines.push('Prior vs Posterior Probabilities');
lines.push('=================================\n');
const maxWidth = 30;
for (const query of Object.keys(inference.prior)) {
const priorProb = inference.prior[query];
const postProb = inference.posterior[query];
lines.push(`${query}:`);
// Prior bar
const priorBar = '█'.repeat(Math.round(priorProb * maxWidth));
lines.push(` Prior: │${priorBar.padEnd(maxWidth)}│ ${(priorProb * 100).toFixed(1)}%`);
// Posterior bar
const postBar = '█'.repeat(Math.round(postProb * maxWidth));
lines.push(` Posterior: │${postBar.padEnd(maxWidth)}│ ${(postProb * 100).toFixed(1)}%`);
// Change indicator
const change = postProb - priorProb;
if (Math.abs(change) > 0.01) {
const arrow = change > 0 ? '↑' : '↓';
lines.push(` Change: ${arrow} ${Math.abs(change * 100).toFixed(1)}%`);
}
lines.push('');
}
return lines.join('\n');
}
/**
* Visualize decision tree
*/
visualizeDecision(decision: DecisionRecommendation): string {
const lines: string[] = [];
lines.push('Decision Tree');
lines.push('=============\n');
// Sort alternatives by expected utility
const sorted = [...decision.alternatives].sort((a, b) => b.expectedUtility - a.expectedUtility);
for (let i = 0; i < sorted.length; i++) {
const alt = sorted[i];
const isRecommended = alt.action === decision.recommendedAction;
const marker = isRecommended ? '✓' : ' ';
const label = isRecommended ? ' (RECOMMENDED)' : '';
lines.push(`${marker} ${alt.action}${label}`);
lines.push(` ├─ Expected Utility: ${alt.expectedUtility.toFixed(2)}`);
// Draw utility bar
const maxEU = sorted[0].expectedUtility;
const minEU = sorted[sorted.length - 1].expectedUtility;
const range = maxEU - minEU;
const barLength = range > 0 ? Math.round(((alt.expectedUtility - minEU) / range) * 30) : 15;
const bar = '█'.repeat(barLength);
lines.push(` └─ │${bar.padEnd(30)}│`);
lines.push('');
}
// Risk analysis visualization
lines.push('Risk Profile:');
lines.push(` Best Case: ↑ ${decision.riskAnalysis.bestCase.utility.toFixed(2)} (P=${(decision.riskAnalysis.bestCase.probability * 100).toFixed(1)}%)`);
lines.push(` Worst Case: ↓ ${decision.riskAnalysis.worstCase.utility.toFixed(2)} (P=${(decision.riskAnalysis.worstCase.probability * 100).toFixed(1)}%)`);
lines.push(` Variance: σ²=${decision.riskAnalysis.variance.toFixed(2)}`);
return lines.join('\n');
}
/**
* Visualize probability over time/iterations
*/
visualizeConvergence(iterations: Array<{ iteration: number; value: number }>): string {
const lines: string[] = [];
lines.push('Convergence Plot');
lines.push('================\n');
const height = 15;
const width = 50;
const values = iterations.map(i => i.value);
const minVal = Math.min(...values);
const maxVal = Math.max(...values);
const range = maxVal - minVal;
// Create grid
const grid: string[][] = [];
for (let y = 0; y < height; y++) {
grid[y] = [];
for (let x = 0; x < width; x++) {
grid[y][x] = ' ';
}
}
// Plot points
for (let i = 0; i < iterations.length; i++) {
const x = Math.round((i / (iterations.length - 1)) * (width - 1));
const normalizedVal = range > 0 ? (iterations[i].value - minVal) / range : 0.5;
const y = height - 1 - Math.round(normalizedVal * (height - 1));
if (y >= 0 && y < height && x >= 0 && x < width) {
grid[y][x] = '•';
}
}
// Draw grid
lines.push(`${maxVal.toFixed(3)} ┤` + grid[0].join(''));
for (let y = 1; y < height - 1; y++) {
lines.push(` │` + grid[y].join(''));
}
lines.push(`${minVal.toFixed(3)} └${'─'.repeat(width)}`);
lines.push(` 0${' '.repeat(width - 10)}${iterations.length}`);
lines.push(' Iteration');
return lines.join('\n');
}
/**
* Build dependency graph from program
*/
private buildDependencyGraph(program: ProbabilisticProgram): {
nodes: Record<string, string[]>;
probabilities: Record<string, number>;
} {
const nodes: Record<string, string[]> = {};
const probabilities: Record<string, number> = {};
// Add facts as leaf nodes
for (const fact of program.facts) {
nodes[fact.fact] = [];
probabilities[fact.fact] = fact.probability;
}
// Add rules
for (const rule of program.rules) {
nodes[rule.head] = rule.body;
if (rule.probability !== undefined) {
probabilities[rule.head] = rule.probability;
}
}
return { nodes, probabilities };
}
/**
* Create dependency tree visualization
*/
private createDependencyTree(graph: {
nodes: Record<string, string[]>;
probabilities: Record<string, number>;
}): string {
const lines: string[] = [];
// Find root nodes (nodes with no dependencies or not depended on by others)
const dependedOn = new Set<string>();
for (const deps of Object.values(graph.nodes)) {
for (const dep of deps) {
dependedOn.add(dep);
}
}
const roots = Object.keys(graph.nodes).filter(n => graph.nodes[n].length === 0 || !dependedOn.has(n));
if (roots.length === 0 && Object.keys(graph.nodes).length > 0) {
// If no clear roots, use all nodes
roots.push(...Object.keys(graph.nodes));
}
for (const root of roots) {
this.drawTree(root, graph, lines, 0, new Set());
}
return lines.join('\n');
}
/**
* Recursively draw tree structure
*/
private drawTree(
node: string,
graph: { nodes: Record<string, string[]>; probabilities: Record<string, number> },
lines: string[],
depth: number,
visited: Set<string>
): void {
if (visited.has(node)) {
lines.push(`${' '.repeat(depth)}├─ ${node} (cyclic)`);
return;
}
visited.add(node);
const prob = graph.probabilities[node];
const probStr = prob !== undefined ? ` [P=${prob.toFixed(3)}]` : '';
const indent = ' '.repeat(depth);
lines.push(`${indent}${depth > 0 ? '├─ ' : ''}${node}${probStr}`);
const deps = graph.nodes[node] || [];
for (const dep of deps) {
this.drawTree(dep, graph, lines, depth + 1, new Set(visited));
}
}
}