aboutsummaryrefslogtreecommitdiff
path: root/src/intuition.rs
diff options
context:
space:
mode:
authortslil <tslil@posteo.de>2026-07-14 20:22:56 +0100
committertslil <tslil@posteo.de>2026-07-14 21:25:04 +0100
commitfc5789e026473ea82237be847672ecb350e64479 (patch)
tree6210659eb424124b55bd67a63397a37931e50b5f /src/intuition.rs
parent156cbfe2b0383521f540109682d98d35c88d65f8 (diff)
license, simplify code, add README, lose "learner" naming
Diffstat (limited to 'src/intuition.rs')
-rw-r--r--src/intuition.rs156
1 files changed, 156 insertions, 0 deletions
diff --git a/src/intuition.rs b/src/intuition.rs
new file mode 100644
index 0000000..f994029
--- /dev/null
+++ b/src/intuition.rs
@@ -0,0 +1,156 @@
+use std::collections::{HashMap, HashSet};
+
+use rand::{self, seq::IndexedRandom};
+use serde::{Deserialize, Serialize};
+
+use crate::{config::Config, trajectory::Action};
+
+fn compute_weight(past: &HashSet<String>, now: &HashSet<&String>) -> f32 {
+ let inter = now.iter().filter(|&&p| past.contains(p)).count();
+ if inter == 0 {
+ return 0.0;
+ }
+ (inter as f32) / ((now.len() as f32).sqrt() * (past.len() as f32).sqrt())
+}
+
+fn softmax_sample(items: &[(String, f32)], temperature: f32) -> Option<(String, f32)> {
+ if items.is_empty() {
+ return None;
+ }
+ let mut rng = rand::rng();
+
+ let max_val = items
+ .iter()
+ .map(|(_, v)| *v)
+ .fold(f32::NEG_INFINITY, f32::max);
+
+ let exp_values: Vec<_> = items
+ .iter()
+ .map(|(k, val)| (k, ((val - max_val) / temperature).exp()))
+ .collect();
+
+ let sum: f32 = exp_values.iter().map(|(_, v)| *v).sum();
+
+ let norm_values: Vec<_> = exp_values.iter().map(|&(k, v)| (k, v / sum)).collect();
+
+ norm_values
+ .choose_weighted(&mut rng, |item| item.1)
+ .map(|p| (p.0.clone(), p.1))
+ .ok()
+}
+
+#[derive(Serialize, Deserialize)]
+enum Episode {
+ Continue {
+ group: HashSet<String>,
+ avoid: String,
+ committed: String,
+ },
+ Escape {
+ from: HashSet<String>,
+ to: String,
+ committed: String,
+ },
+}
+
+#[derive(Serialize, Deserialize, Default)]
+pub struct Intuition {
+ history: Vec<Episode>,
+}
+
+pub enum Learning {
+ SkipToMore(Vec<String>, String),
+ MoreToSkip(Vec<String>, String),
+}
+
+impl Intuition {
+ pub fn learn(&mut self, learning: &Learning) {
+ let stamp = chrono::Utc::now().format("%+").to_string();
+ match learning {
+ Learning::SkipToMore(trajectory, new) => {
+ self.history.push(Episode::Escape {
+ from: trajectory.iter().map(String::clone).collect(),
+ to: new.clone(),
+ committed: stamp,
+ });
+ }
+ Learning::MoreToSkip(trajectory, new) => {
+ self.history.push(Episode::Continue {
+ group: trajectory.iter().map(String::clone).collect(),
+ avoid: new.clone(),
+ committed: stamp,
+ });
+ }
+ }
+ }
+
+ pub fn sample(
+ &self,
+ trajectory: &[String],
+ action: &Action,
+ candidates: &HashSet<String>,
+ config: &Config,
+ ) -> Option<(String, f32)> {
+ let trajectory: HashSet<_> = trajectory.iter().collect();
+
+ let candidates: Vec<_> = candidates
+ .iter()
+ .filter(|c| !trajectory.contains(c))
+ .collect();
+
+ if candidates.is_empty() {
+ return None;
+ }
+
+ let mut items: HashMap<String, f32> =
+ candidates.into_iter().map(|c| (c.clone(), 0.0)).collect();
+
+ for episode in &self.history {
+ match (episode, action) {
+ (Episode::Escape { from, to, .. }, Action::Skip) => {
+ let w = compute_weight(from, &trajectory);
+ if let Some(to_w) = items.get_mut(to) {
+ *to_w += config.max_weight * w;
+ }
+ for f in from {
+ if let Some(from_weight) = items.get_mut(f) {
+ *from_weight -= config.mid_weight * w;
+ }
+ }
+ }
+ (Episode::Escape { from, to, .. }, Action::More) => {
+ let w = compute_weight(from, &trajectory);
+ if trajectory.contains(to) {
+ for f in from {
+ if let Some(f_w) = items.get_mut(f) {
+ *f_w -= config.low_weight * w;
+ }
+ }
+ }
+ }
+ (Episode::Continue { group, avoid, .. }, Action::More) => {
+ let w = compute_weight(group, &trajectory);
+ for g in group {
+ if let Some(g_w) = items.get_mut(g) {
+ *g_w += config.max_weight * w;
+ }
+ }
+ if let Some(a_w) = items.get_mut(avoid) {
+ *a_w -= config.low_weight * w;
+ }
+ }
+ (Episode::Continue { group, .. }, Action::Skip) => {
+ let w = compute_weight(group, &trajectory);
+ for g in group {
+ if let Some(v) = items.get_mut(g) {
+ *v -= config.high_weight * w;
+ }
+ }
+ }
+ }
+ }
+
+ let pairs: Vec<_> = items.into_iter().collect();
+ softmax_sample(&pairs, config.temperature)
+ }
+}