use crate::state_predictor::SemanticStateMap;
use serde::{Deserialize, Serialize};
pub const DEFAULT_NUM_BRANCHES: usize = 8;
pub const DEFAULT_PRUNE_THRESHOLD: f32 = 0.10;
pub const EVOLVE_SIGMA: f32 = 0.15;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrajectoryBranch {
pub label: String,
pub state_vector: Vec<f32>,
pub composite_score: f32,
pub novelty: f32,
pub coherence: f32,
pub budget_fit: f32,
pub depth: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrajectorySampleResult {
pub branches: Vec<TrajectoryBranch>,
pub session_id: String,
pub total_explored: usize,
pub pruned: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CognitiveManifold {
pub nodes: Vec<Vec<f32>>,
pub edges: Vec<(usize, usize, f32)>,
pub labels: Vec<String>,
}
pub struct TrajectorySampler {
num_branches: usize,
prune_threshold: f32,
}
impl TrajectorySampler {
pub fn new(num_branches: usize, prune_threshold: f32) -> Self {
Self {
num_branches,
prune_threshold,
}
}
pub fn default_config() -> Self {
Self::new(DEFAULT_NUM_BRANCHES, DEFAULT_PRUNE_THRESHOLD)
}
pub fn sample(&self, map: &SemanticStateMap) -> TrajectorySampleResult {
let mut branches = Vec::with_capacity(self.num_branches);
for i in 0..self.num_branches {
let perturbation = (i as f32) * 0.1;
let state_vector = map
.context_state
.iter()
.enumerate()
.map(|(j, &v)| v + perturbation * ((j as f32).sin() * 0.05))
.collect::<Vec<f32>>();
let novelty = Self::compute_novelty(&state_vector, &map.context_state);
let coherence = Self::compute_coherence(&state_vector, &map.milestones);
let budget_fit = Self::compute_budget_fit(&map.milestones);
let composite = 0.4 * novelty + 0.3 * coherence + 0.3 * budget_fit;
let label = if map.milestones.is_empty() {
format!("branch_{}", i)
} else {
let ms = &map.milestones[i % map.milestones.len()];
format!("{} ({:.2})", ms.label, ms.confidence.mean())
};
branches.push(TrajectoryBranch {
label,
state_vector,
composite_score: composite,
novelty,
coherence,
budget_fit,
depth: 0,
});
}
let total_explored = branches.len();
let before = branches.len();
branches.retain(|b| b.composite_score >= self.prune_threshold);
let pruned = before - branches.len();
TrajectorySampleResult {
branches,
session_id: map.session_id.clone(),
total_explored,
pruned,
}
}
pub fn sample_with_manifold(
&self,
map: &SemanticStateMap,
_context: &str,
) -> (TrajectorySampleResult, CognitiveManifold) {
let result = self.sample(map);
let nodes: Vec<Vec<f32>> = result
.branches
.iter()
.map(|b| b.state_vector.clone())
.collect();
let labels: Vec<String> = result
.branches
.iter()
.map(|b| b.label.clone())
.collect();
let mut edges = Vec::new();
for i in 0..nodes.len() {
for j in (i + 1)..nodes.len() {
let sim = Self::cosine_sim(&nodes[i], &nodes[j]);
if sim > 0.5 {
edges.push((i, j, sim));
}
}
}
let manifold = CognitiveManifold {
nodes,
edges,
labels,
};
(result, manifold)
}
pub fn evolve_branches(&self, result: &mut TrajectorySampleResult, sigma: f32) {
for branch in &mut result.branches {
let perturbed: Vec<f32> = branch
.state_vector
.iter()
.enumerate()
.map(|(i, &v)| {
let noise = ((i as f32 + 1.0).sin() * sigma).abs();
v + noise * 0.1
})
.collect();
let novelty = Self::compute_novelty(&perturbed, &branch.state_vector);
let coherence = (branch.coherence + 0.05).min(1.0);
let budget_fit = branch.budget_fit;
let composite = 0.4 * novelty + 0.3 * coherence + 0.3 * budget_fit;
branch.state_vector = perturbed;
branch.composite_score = composite;
branch.novelty = novelty;
branch.coherence = coherence;
branch.depth += 1;
}
}
fn compute_novelty(candidate: &[f32], reference: &[f32]) -> f32 {
if candidate.is_empty() || reference.is_empty() {
return 0.5;
}
let dist = Self::l2_dist(candidate, reference);
let novelty = 1.0 / (1.0 + dist);
novelty.clamp(0.0, 1.0)
}
fn compute_coherence(state: &[f32], milestones: &[crate::state_predictor::SemanticMilestone]) -> f32 {
if milestones.is_empty() || state.is_empty() {
return 0.5;
}
let avg_conf: f32 = milestones.iter().map(|m| m.confidence.mean()).sum::<f32>() / milestones.len() as f32;
let norm: f32 = state.iter().map(|v| v * v).sum::<f32>().sqrt();
(avg_conf * 0.7 + (norm / 10.0).min(1.0) * 0.3).clamp(0.0, 1.0)
}
fn compute_budget_fit(milestones: &[crate::state_predictor::SemanticMilestone]) -> f32 {
if milestones.is_empty() {
return 0.5;
}
let avg_budget: f32 = milestones.iter().map(|m| m.token_budget as f32).sum::<f32>()
/ milestones.len() as f32
/ 4096.0;
avg_budget.clamp(0.0, 1.0)
}
fn l2_dist(a: &[f32], b: &[f32]) -> f32 {
let len = a.len().min(b.len());
let mut sum = 0.0;
for i in 0..len {
let d = a[i] - b[i];
sum += d * d;
}
sum.sqrt()
}
fn cosine_sim(a: &[f32], b: &[f32]) -> f32 {
let len = a.len().min(b.len());
if len == 0 {
return 0.0;
}
let dot: f32 = (0..len).map(|i| a[i] * b[i]).sum();
let norm_a: f32 = a.iter().map(|v| v * v).sum::<f32>().sqrt();
let norm_b: f32 = b.iter().map(|v| v * v).sum::<f32>().sqrt();
if norm_a < 1e-8 || norm_b < 1e-8 {
return 0.0;
}
dot / (norm_a * norm_b)
}
}
pub fn render_trajectory_result(result: &TrajectorySampleResult) -> String {
let mut s = String::new();
s.push_str(&format!(
"Trajectory Sample (session: {})\n",
result.session_id
));
s.push_str(&format!(
" Explored: {} | Pruned: {} | Surviving: {}\n\n",
result.total_explored,
result.pruned,
result.branches.len()
));
for (i, b) in result.branches.iter().enumerate() {
s.push_str(&format!(
" [{}] {} (score={:.3}, novelty={:.3}, coherence={:.3}, budget_fit={:.3}, depth={})\n",
i, b.label, b.composite_score, b.novelty, b.coherence, b.budget_fit, b.depth
));
}
s
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state_predictor::{SemanticStateMap, SemanticMilestone};
use crate::belief::BetaBelief;
fn make_state_map() -> SemanticStateMap {
let milestones = vec![
SemanticMilestone {
label: "hypothesis".to_string(),
label_idx: 0,
predicted_state: vec![1.0, 0.0, 0.0, 0.0],
token_budget: 512,
branch_hints: vec![],
confidence: BetaBelief::from_confidence(0.8, 4.0),
},
SemanticMilestone {
label: "decomposition".to_string(),
label_idx: 1,
predicted_state: vec![0.0, 1.0, 0.0, 0.0],
token_budget: 1024,
branch_hints: vec![],
confidence: BetaBelief::from_confidence(0.7, 4.0),
},
SemanticMilestone {
label: "execution".to_string(),
label_idx: 2,
predicted_state: vec![0.0, 0.0, 1.0, 0.0],
token_budget: 2048,
branch_hints: vec![],
confidence: BetaBelief::from_confidence(0.6, 4.0),
},
];
SemanticStateMap {
milestones,
confidence: BetaBelief::default(),
session_id: "test".to_string(),
context_state: vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6],
}
}
#[test]
fn sample_produces_branches() {
let sampler = TrajectorySampler::default_config();
let map = make_state_map();
let result = sampler.sample(&map);
assert!(!result.branches.is_empty(), "should produce branches");
assert!(result.total_explored > 0, "should have explored branches");
}
#[test]
fn scores_are_in_valid_range() {
let sampler = TrajectorySampler::default_config();
let map = make_state_map();
let result = sampler.sample(&map);
for b in &result.branches {
assert!(b.composite_score >= 0.0 && b.composite_score <= 1.0);
assert!(b.novelty >= 0.0 && b.novelty <= 1.0);
assert!(b.coherence >= 0.0 && b.coherence <= 1.0);
assert!(b.budget_fit >= 0.0 && b.budget_fit <= 1.0);
}
}
#[test]
fn pruning_removes_low_scoring_branches() {
let sampler = TrajectorySampler::new(8, 0.99);
let map = make_state_map();
let result = sampler.sample(&map);
assert!(result.pruned > 0, "should prune some branches");
}
#[test]
fn selection_returns_best_branch() {
let sampler = TrajectorySampler::default_config();
let map = make_state_map();
let result = sampler.sample(&map);
let best = result.branches.iter().max_by(|a, b| {
a.composite_score
.partial_cmp(&b.composite_score)
.unwrap()
});
assert!(best.is_some(), "should have a best branch");
}
#[test]
fn evolve_changes_scores() {
let sampler = TrajectorySampler::new(8, 0.001);
let map = make_state_map();
let mut result = sampler.sample(&map);
let before: Vec<f32> = result.branches.iter().map(|b| b.composite_score).collect();
sampler.evolve_branches(&mut result, 0.5);
let after: Vec<f32> = result.branches.iter().map(|b| b.composite_score).collect();
assert_eq!(before.len(), after.len(), "evolve should not change branch count");
let changed = before.iter().zip(after.iter()).any(|(a, b)| (a - b).abs() > 1e-6);
assert!(changed, "evolution should change scores");
}
#[test]
fn render_is_readable() {
let sampler = TrajectorySampler::default_config();
let map = make_state_map();
let result = sampler.sample(&map);
let rendered = render_trajectory_result(&result);
assert!(rendered.contains("Trajectory Sample"), "render should contain title");
assert!(rendered.contains("session: test"), "render should contain session id");
}
#[test]
fn empty_map_produces_valid_result() {
let sampler = TrajectorySampler::default_config();
let map = SemanticStateMap::default();
let result = sampler.sample(&map);
assert!(result.total_explored > 0, "should explore even with empty map");
}
#[test]
fn manifold_integration_works() {
let sampler = TrajectorySampler::new(8, 0.001);
let map = make_state_map();
let (result, manifold) = sampler.sample_with_manifold(&map, "test context");
assert!(!manifold.nodes.is_empty(), "manifold should be created from non-pruned branches");
assert_eq!(
manifold.nodes.len(),
result.branches.len(),
"manifold nodes should match surviving branches"
);
}
}