pub struct Engine { /* private fields */ }Expand description
The main engine. Owns item registry, user states, factor pipeline, selector.
§Usage
use aria_core::{Engine, EngineConfig, Signal, Scoreable};
use aria_core::item::Item;
use aria_core::factor::{ChallengeFactor, SpacingFactor, CoverageFactor};
let mut engine = Engine::new(EngineConfig::default());
engine.add_factor(Box::new(ChallengeFactor::default()));
engine.add_factor(Box::new(SpacingFactor::default()));
engine.add_factor(Box::new(CoverageFactor));
engine.add_items(vec![
Item::new("algebra_basics", 0.2, "algebra"),
Item::new("quadratic_eq", 0.6, "algebra"),
Item::new("integration", 0.9, "calculus"),
]).unwrap();
let item = engine.suggest("user_42").unwrap();
let item_id = item.id().to_string();
engine.feedback("user_42", &item_id, Signal::new(true, 0.4)).unwrap();Implementations§
Source§impl Engine
impl Engine
Sourcepub fn new(config: EngineConfig) -> Self
pub fn new(config: EngineConfig) -> Self
Create engine with config.
Examples found in repository?
12fn main() {
13 println!("=== aria-core: Learning Domain Simulation ===\n");
14
15 // --- Build engine ---
16 let mut engine = Engine::new(EngineConfig {
17 exploration_rate: 0.0, // deterministic for demo
18 alpha: 0.1, // faster skill movement so visible in 15 steps
19 });
20
21 // Register factors — caller's choice
22 engine.add_factor(Box::new(ChallengeFactor::new(0.2)));
23 engine.add_factor(Box::new(SpacingFactor::new(10))); // 10s interval for demo
24 engine.add_factor(Box::new(CoverageFactor));
25 engine.seed_rng(42);
26
27 // Register items — caller's curriculum
28 engine.add_items(vec![
29 // Math track
30 Item::new("counting", 0.05, "math"),
31 Item::new("addition", 0.15, "math"),
32 Item::new("multiplication", 0.30, "math"),
33 Item::new("fractions", 0.45, "math"),
34 Item::new("algebra_basics", 0.60, "math"),
35 Item::new("quadratics", 0.75, "math")
36 .with_prereqs(vec!["algebra_basics".into()]),
37 Item::new("calculus_intro", 0.90, "math")
38 .with_prereqs(vec!["quadratics".into()]),
39
40 // Science track (no prereqs — runs in parallel)
41 Item::new("sci_observation", 0.10, "science"),
42 Item::new("sci_hypothesis", 0.25, "science"),
43 Item::new("sci_experiment", 0.50, "science"),
44 Item::new("sci_analysis", 0.70, "science"),
45 Item::new("sci_research", 0.90, "science"),
46 ]).unwrap();
47
48 println!("Items registered: {}", engine.item_count());
49 println!("Factors registered: {}\n", engine.factor_count());
50
51 println!("{:<5} {:<22} {:<10} {:<8} {:<8} {:<10}",
52 "Step", "Item", "Category", "Success", "Effort", "Skill");
53 println!("{}", "-".repeat(70));
54
55 // --- Simulate 15 interactions ---
56 // Alternates easy successes and a few failures to show adaptation
57 let interactions: Vec<(bool, f32)> = vec![
58 (true, 0.8), // 1 hard success
59 (true, 0.3), // 2 easy success → optimism up
60 (true, 0.2), // 3 easy
61 (false, 0.9), // 4 failure → optimism eases
62 (true, 0.5), // 5
63 (true, 0.4), // 6
64 (true, 0.2), // 7 easy → optimism climbs
65 (true, 0.3), // 8
66 (false, 0.7), // 9 failure
67 (true, 0.5), // 10
68 (true, 0.4), // 11
69 (true, 0.3), // 12
70 (true, 0.2), // 13
71 (true, 0.5), // 14
72 (true, 0.3), // 15
73 ];
74
75 for (step, (success, effort)) in interactions.iter().enumerate() {
76 let item = engine.suggest("alice").unwrap();
77 let item_id = item.id().to_string();
78 let category = item.category().to_string();
79
80 engine.feedback("alice", &item_id, Signal::new(*success, *effort)).unwrap();
81
82 let state = engine.get_state("alice").unwrap();
83 println!("{:<5} {:<22} {:<10} {:<8} {:<8.2} {:<.4}",
84 step + 1,
85 item_id,
86 category,
87 if *success { "✓" } else { "✗" },
88 effort,
89 state.skill,
90 );
91 }
92
93 // --- Show final state ---
94 let state = engine.get_state("alice").unwrap();
95 println!("\n=== Final State ===");
96 println!("Skill: {:.4}", state.skill);
97 println!("Optimism bias: {:.4}", state.optimism_bias);
98 println!("Target: {:.4}", state.target());
99 println!("Interactions: {}", state.interaction_count);
100 println!("Resolved items: {:?}", state.resolved_set);
101
102 println!("\n=== Category Coverage ===");
103 let mut cats: Vec<_> = state.category_count.iter().collect();
104 cats.sort_by_key(|(k, _)| k.as_str());
105 for (cat, count) in cats {
106 println!(" {:<12} → {} interactions", cat, count);
107 }
108
109 // --- Serialise / deserialise round-trip ---
110 println!("\n=== Serialisation Round-Trip ===");
111 let encoded = Serialiser::encode(state);
112 println!("Encoded keys: {}", encoded.len());
113 let decoded = Serialiser::decode(&encoded).unwrap();
114 assert!((decoded.skill - state.skill).abs() < 1e-5, "skill mismatch after round-trip");
115 assert_eq!(decoded.interaction_count, state.interaction_count);
116 println!("Round-trip: ✓ skill={:.4} interactions={}", decoded.skill, decoded.interaction_count);
117
118 // --- Prereq demonstration ---
119 println!("\n=== Prerequisite Gating ===");
120 println!("calculus_intro requires: quadratics → algebra_basics");
121 let can_see_calculus = state.resolved_set.contains("quadratics");
122 println!("User resolved quadratics: {}", can_see_calculus);
123 println!("(calculus_intro will unlock once quadratics is resolved)");
124
125 println!("\n=== Custom Domain Example (ecommerce) ===");
126 demo_ecommerce();
127}
128
129/// Quick demo showing the same engine used for a completely different domain.
130fn demo_ecommerce() {
131 use aria_core::factor::ChallengeFactor;
132
133 struct BudgetFactor;
134 impl aria_core::factor::Factor for BudgetFactor {
135 fn name(&self) -> &str { "budget" }
136 fn score(&self, item: &dyn Scoreable, state: &aria_core::ProfileState, _now: u64) -> f32 {
137 // score_proxy = price_ratio (0=free, 1=most expensive)
138 // ProfileState.skill = budget_willingness
139 let target = state.target();
140 let diff = item.score_proxy() - target;
141 (-diff * diff / 0.08).exp() // bandwidth=0.2
142 }
143 }
144
145 let mut engine = Engine::new(EngineConfig {
146 exploration_rate: 0.0,
147 alpha: 0.1,
148 });
149 engine.add_factor(Box::new(BudgetFactor));
150 engine.add_factor(Box::new(ChallengeFactor::new(0.25))); // reused as price-fit
151 engine.seed_rng(1);
152
153 engine.add_items(vec![
154 Item::new("budget_headphones", 0.1, "audio"),
155 Item::new("mid_headphones", 0.4, "audio"),
156 Item::new("premium_headphones", 0.9, "audio"),
157 Item::new("budget_speaker", 0.15, "audio"),
158 Item::new("smartwatch_basic", 0.3, "wearables"),
159 Item::new("smartwatch_pro", 0.8, "wearables"),
160 ]).unwrap();
161
162 // User with mid budget
163 let mut state = aria_core::ProfileState::new();
164 state.skill = 0.35; // budget willingness
165 engine.load_state("shopper", state);
166
167 print!("Suggestions for mid-budget shopper: ");
168 for _ in 0..3 {
169 let item = engine.suggest("shopper").unwrap();
170 let id = item.id().to_string();
171 print!("{} ", id);
172 engine.feedback("shopper", &id, Signal::new(true, 0.5)).unwrap();
173 }
174 println!();
175}Sourcepub fn set_updater(&mut self, updater: Box<dyn StateUpdater>)
pub fn set_updater(&mut self, updater: Box<dyn StateUpdater>)
Replace the default state updater with a custom implementation. Call before first interaction.
Sourcepub fn add_items(&mut self, items: Vec<Item>) -> Result<(), AriaError>
pub fn add_items(&mut self, items: Vec<Item>) -> Result<(), AriaError>
Register items. Returns Err if prerequisites form a cycle.
Examples found in repository?
12fn main() {
13 println!("=== aria-core: Learning Domain Simulation ===\n");
14
15 // --- Build engine ---
16 let mut engine = Engine::new(EngineConfig {
17 exploration_rate: 0.0, // deterministic for demo
18 alpha: 0.1, // faster skill movement so visible in 15 steps
19 });
20
21 // Register factors — caller's choice
22 engine.add_factor(Box::new(ChallengeFactor::new(0.2)));
23 engine.add_factor(Box::new(SpacingFactor::new(10))); // 10s interval for demo
24 engine.add_factor(Box::new(CoverageFactor));
25 engine.seed_rng(42);
26
27 // Register items — caller's curriculum
28 engine.add_items(vec![
29 // Math track
30 Item::new("counting", 0.05, "math"),
31 Item::new("addition", 0.15, "math"),
32 Item::new("multiplication", 0.30, "math"),
33 Item::new("fractions", 0.45, "math"),
34 Item::new("algebra_basics", 0.60, "math"),
35 Item::new("quadratics", 0.75, "math")
36 .with_prereqs(vec!["algebra_basics".into()]),
37 Item::new("calculus_intro", 0.90, "math")
38 .with_prereqs(vec!["quadratics".into()]),
39
40 // Science track (no prereqs — runs in parallel)
41 Item::new("sci_observation", 0.10, "science"),
42 Item::new("sci_hypothesis", 0.25, "science"),
43 Item::new("sci_experiment", 0.50, "science"),
44 Item::new("sci_analysis", 0.70, "science"),
45 Item::new("sci_research", 0.90, "science"),
46 ]).unwrap();
47
48 println!("Items registered: {}", engine.item_count());
49 println!("Factors registered: {}\n", engine.factor_count());
50
51 println!("{:<5} {:<22} {:<10} {:<8} {:<8} {:<10}",
52 "Step", "Item", "Category", "Success", "Effort", "Skill");
53 println!("{}", "-".repeat(70));
54
55 // --- Simulate 15 interactions ---
56 // Alternates easy successes and a few failures to show adaptation
57 let interactions: Vec<(bool, f32)> = vec![
58 (true, 0.8), // 1 hard success
59 (true, 0.3), // 2 easy success → optimism up
60 (true, 0.2), // 3 easy
61 (false, 0.9), // 4 failure → optimism eases
62 (true, 0.5), // 5
63 (true, 0.4), // 6
64 (true, 0.2), // 7 easy → optimism climbs
65 (true, 0.3), // 8
66 (false, 0.7), // 9 failure
67 (true, 0.5), // 10
68 (true, 0.4), // 11
69 (true, 0.3), // 12
70 (true, 0.2), // 13
71 (true, 0.5), // 14
72 (true, 0.3), // 15
73 ];
74
75 for (step, (success, effort)) in interactions.iter().enumerate() {
76 let item = engine.suggest("alice").unwrap();
77 let item_id = item.id().to_string();
78 let category = item.category().to_string();
79
80 engine.feedback("alice", &item_id, Signal::new(*success, *effort)).unwrap();
81
82 let state = engine.get_state("alice").unwrap();
83 println!("{:<5} {:<22} {:<10} {:<8} {:<8.2} {:<.4}",
84 step + 1,
85 item_id,
86 category,
87 if *success { "✓" } else { "✗" },
88 effort,
89 state.skill,
90 );
91 }
92
93 // --- Show final state ---
94 let state = engine.get_state("alice").unwrap();
95 println!("\n=== Final State ===");
96 println!("Skill: {:.4}", state.skill);
97 println!("Optimism bias: {:.4}", state.optimism_bias);
98 println!("Target: {:.4}", state.target());
99 println!("Interactions: {}", state.interaction_count);
100 println!("Resolved items: {:?}", state.resolved_set);
101
102 println!("\n=== Category Coverage ===");
103 let mut cats: Vec<_> = state.category_count.iter().collect();
104 cats.sort_by_key(|(k, _)| k.as_str());
105 for (cat, count) in cats {
106 println!(" {:<12} → {} interactions", cat, count);
107 }
108
109 // --- Serialise / deserialise round-trip ---
110 println!("\n=== Serialisation Round-Trip ===");
111 let encoded = Serialiser::encode(state);
112 println!("Encoded keys: {}", encoded.len());
113 let decoded = Serialiser::decode(&encoded).unwrap();
114 assert!((decoded.skill - state.skill).abs() < 1e-5, "skill mismatch after round-trip");
115 assert_eq!(decoded.interaction_count, state.interaction_count);
116 println!("Round-trip: ✓ skill={:.4} interactions={}", decoded.skill, decoded.interaction_count);
117
118 // --- Prereq demonstration ---
119 println!("\n=== Prerequisite Gating ===");
120 println!("calculus_intro requires: quadratics → algebra_basics");
121 let can_see_calculus = state.resolved_set.contains("quadratics");
122 println!("User resolved quadratics: {}", can_see_calculus);
123 println!("(calculus_intro will unlock once quadratics is resolved)");
124
125 println!("\n=== Custom Domain Example (ecommerce) ===");
126 demo_ecommerce();
127}
128
129/// Quick demo showing the same engine used for a completely different domain.
130fn demo_ecommerce() {
131 use aria_core::factor::ChallengeFactor;
132
133 struct BudgetFactor;
134 impl aria_core::factor::Factor for BudgetFactor {
135 fn name(&self) -> &str { "budget" }
136 fn score(&self, item: &dyn Scoreable, state: &aria_core::ProfileState, _now: u64) -> f32 {
137 // score_proxy = price_ratio (0=free, 1=most expensive)
138 // ProfileState.skill = budget_willingness
139 let target = state.target();
140 let diff = item.score_proxy() - target;
141 (-diff * diff / 0.08).exp() // bandwidth=0.2
142 }
143 }
144
145 let mut engine = Engine::new(EngineConfig {
146 exploration_rate: 0.0,
147 alpha: 0.1,
148 });
149 engine.add_factor(Box::new(BudgetFactor));
150 engine.add_factor(Box::new(ChallengeFactor::new(0.25))); // reused as price-fit
151 engine.seed_rng(1);
152
153 engine.add_items(vec![
154 Item::new("budget_headphones", 0.1, "audio"),
155 Item::new("mid_headphones", 0.4, "audio"),
156 Item::new("premium_headphones", 0.9, "audio"),
157 Item::new("budget_speaker", 0.15, "audio"),
158 Item::new("smartwatch_basic", 0.3, "wearables"),
159 Item::new("smartwatch_pro", 0.8, "wearables"),
160 ]).unwrap();
161
162 // User with mid budget
163 let mut state = aria_core::ProfileState::new();
164 state.skill = 0.35; // budget willingness
165 engine.load_state("shopper", state);
166
167 print!("Suggestions for mid-budget shopper: ");
168 for _ in 0..3 {
169 let item = engine.suggest("shopper").unwrap();
170 let id = item.id().to_string();
171 print!("{} ", id);
172 engine.feedback("shopper", &id, Signal::new(true, 0.5)).unwrap();
173 }
174 println!();
175}Sourcepub fn add_factor(&mut self, factor: Box<dyn Factor>)
pub fn add_factor(&mut self, factor: Box<dyn Factor>)
Register a scoring factor. Order matters — factors are applied in registration order; all scores are multiplied together.
Examples found in repository?
12fn main() {
13 println!("=== aria-core: Learning Domain Simulation ===\n");
14
15 // --- Build engine ---
16 let mut engine = Engine::new(EngineConfig {
17 exploration_rate: 0.0, // deterministic for demo
18 alpha: 0.1, // faster skill movement so visible in 15 steps
19 });
20
21 // Register factors — caller's choice
22 engine.add_factor(Box::new(ChallengeFactor::new(0.2)));
23 engine.add_factor(Box::new(SpacingFactor::new(10))); // 10s interval for demo
24 engine.add_factor(Box::new(CoverageFactor));
25 engine.seed_rng(42);
26
27 // Register items — caller's curriculum
28 engine.add_items(vec![
29 // Math track
30 Item::new("counting", 0.05, "math"),
31 Item::new("addition", 0.15, "math"),
32 Item::new("multiplication", 0.30, "math"),
33 Item::new("fractions", 0.45, "math"),
34 Item::new("algebra_basics", 0.60, "math"),
35 Item::new("quadratics", 0.75, "math")
36 .with_prereqs(vec!["algebra_basics".into()]),
37 Item::new("calculus_intro", 0.90, "math")
38 .with_prereqs(vec!["quadratics".into()]),
39
40 // Science track (no prereqs — runs in parallel)
41 Item::new("sci_observation", 0.10, "science"),
42 Item::new("sci_hypothesis", 0.25, "science"),
43 Item::new("sci_experiment", 0.50, "science"),
44 Item::new("sci_analysis", 0.70, "science"),
45 Item::new("sci_research", 0.90, "science"),
46 ]).unwrap();
47
48 println!("Items registered: {}", engine.item_count());
49 println!("Factors registered: {}\n", engine.factor_count());
50
51 println!("{:<5} {:<22} {:<10} {:<8} {:<8} {:<10}",
52 "Step", "Item", "Category", "Success", "Effort", "Skill");
53 println!("{}", "-".repeat(70));
54
55 // --- Simulate 15 interactions ---
56 // Alternates easy successes and a few failures to show adaptation
57 let interactions: Vec<(bool, f32)> = vec![
58 (true, 0.8), // 1 hard success
59 (true, 0.3), // 2 easy success → optimism up
60 (true, 0.2), // 3 easy
61 (false, 0.9), // 4 failure → optimism eases
62 (true, 0.5), // 5
63 (true, 0.4), // 6
64 (true, 0.2), // 7 easy → optimism climbs
65 (true, 0.3), // 8
66 (false, 0.7), // 9 failure
67 (true, 0.5), // 10
68 (true, 0.4), // 11
69 (true, 0.3), // 12
70 (true, 0.2), // 13
71 (true, 0.5), // 14
72 (true, 0.3), // 15
73 ];
74
75 for (step, (success, effort)) in interactions.iter().enumerate() {
76 let item = engine.suggest("alice").unwrap();
77 let item_id = item.id().to_string();
78 let category = item.category().to_string();
79
80 engine.feedback("alice", &item_id, Signal::new(*success, *effort)).unwrap();
81
82 let state = engine.get_state("alice").unwrap();
83 println!("{:<5} {:<22} {:<10} {:<8} {:<8.2} {:<.4}",
84 step + 1,
85 item_id,
86 category,
87 if *success { "✓" } else { "✗" },
88 effort,
89 state.skill,
90 );
91 }
92
93 // --- Show final state ---
94 let state = engine.get_state("alice").unwrap();
95 println!("\n=== Final State ===");
96 println!("Skill: {:.4}", state.skill);
97 println!("Optimism bias: {:.4}", state.optimism_bias);
98 println!("Target: {:.4}", state.target());
99 println!("Interactions: {}", state.interaction_count);
100 println!("Resolved items: {:?}", state.resolved_set);
101
102 println!("\n=== Category Coverage ===");
103 let mut cats: Vec<_> = state.category_count.iter().collect();
104 cats.sort_by_key(|(k, _)| k.as_str());
105 for (cat, count) in cats {
106 println!(" {:<12} → {} interactions", cat, count);
107 }
108
109 // --- Serialise / deserialise round-trip ---
110 println!("\n=== Serialisation Round-Trip ===");
111 let encoded = Serialiser::encode(state);
112 println!("Encoded keys: {}", encoded.len());
113 let decoded = Serialiser::decode(&encoded).unwrap();
114 assert!((decoded.skill - state.skill).abs() < 1e-5, "skill mismatch after round-trip");
115 assert_eq!(decoded.interaction_count, state.interaction_count);
116 println!("Round-trip: ✓ skill={:.4} interactions={}", decoded.skill, decoded.interaction_count);
117
118 // --- Prereq demonstration ---
119 println!("\n=== Prerequisite Gating ===");
120 println!("calculus_intro requires: quadratics → algebra_basics");
121 let can_see_calculus = state.resolved_set.contains("quadratics");
122 println!("User resolved quadratics: {}", can_see_calculus);
123 println!("(calculus_intro will unlock once quadratics is resolved)");
124
125 println!("\n=== Custom Domain Example (ecommerce) ===");
126 demo_ecommerce();
127}
128
129/// Quick demo showing the same engine used for a completely different domain.
130fn demo_ecommerce() {
131 use aria_core::factor::ChallengeFactor;
132
133 struct BudgetFactor;
134 impl aria_core::factor::Factor for BudgetFactor {
135 fn name(&self) -> &str { "budget" }
136 fn score(&self, item: &dyn Scoreable, state: &aria_core::ProfileState, _now: u64) -> f32 {
137 // score_proxy = price_ratio (0=free, 1=most expensive)
138 // ProfileState.skill = budget_willingness
139 let target = state.target();
140 let diff = item.score_proxy() - target;
141 (-diff * diff / 0.08).exp() // bandwidth=0.2
142 }
143 }
144
145 let mut engine = Engine::new(EngineConfig {
146 exploration_rate: 0.0,
147 alpha: 0.1,
148 });
149 engine.add_factor(Box::new(BudgetFactor));
150 engine.add_factor(Box::new(ChallengeFactor::new(0.25))); // reused as price-fit
151 engine.seed_rng(1);
152
153 engine.add_items(vec![
154 Item::new("budget_headphones", 0.1, "audio"),
155 Item::new("mid_headphones", 0.4, "audio"),
156 Item::new("premium_headphones", 0.9, "audio"),
157 Item::new("budget_speaker", 0.15, "audio"),
158 Item::new("smartwatch_basic", 0.3, "wearables"),
159 Item::new("smartwatch_pro", 0.8, "wearables"),
160 ]).unwrap();
161
162 // User with mid budget
163 let mut state = aria_core::ProfileState::new();
164 state.skill = 0.35; // budget willingness
165 engine.load_state("shopper", state);
166
167 print!("Suggestions for mid-budget shopper: ");
168 for _ in 0..3 {
169 let item = engine.suggest("shopper").unwrap();
170 let id = item.id().to_string();
171 print!("{} ", id);
172 engine.feedback("shopper", &id, Signal::new(true, 0.5)).unwrap();
173 }
174 println!();
175}Sourcepub fn suggest(&mut self, user_id: &str) -> Result<&Item, AriaError>
pub fn suggest(&mut self, user_id: &str) -> Result<&Item, AriaError>
Suggest the best next item for a user.
Creates a fresh ProfileState for new users automatically. Returns a reference to the winning Item.
Examples found in repository?
12fn main() {
13 println!("=== aria-core: Learning Domain Simulation ===\n");
14
15 // --- Build engine ---
16 let mut engine = Engine::new(EngineConfig {
17 exploration_rate: 0.0, // deterministic for demo
18 alpha: 0.1, // faster skill movement so visible in 15 steps
19 });
20
21 // Register factors — caller's choice
22 engine.add_factor(Box::new(ChallengeFactor::new(0.2)));
23 engine.add_factor(Box::new(SpacingFactor::new(10))); // 10s interval for demo
24 engine.add_factor(Box::new(CoverageFactor));
25 engine.seed_rng(42);
26
27 // Register items — caller's curriculum
28 engine.add_items(vec![
29 // Math track
30 Item::new("counting", 0.05, "math"),
31 Item::new("addition", 0.15, "math"),
32 Item::new("multiplication", 0.30, "math"),
33 Item::new("fractions", 0.45, "math"),
34 Item::new("algebra_basics", 0.60, "math"),
35 Item::new("quadratics", 0.75, "math")
36 .with_prereqs(vec!["algebra_basics".into()]),
37 Item::new("calculus_intro", 0.90, "math")
38 .with_prereqs(vec!["quadratics".into()]),
39
40 // Science track (no prereqs — runs in parallel)
41 Item::new("sci_observation", 0.10, "science"),
42 Item::new("sci_hypothesis", 0.25, "science"),
43 Item::new("sci_experiment", 0.50, "science"),
44 Item::new("sci_analysis", 0.70, "science"),
45 Item::new("sci_research", 0.90, "science"),
46 ]).unwrap();
47
48 println!("Items registered: {}", engine.item_count());
49 println!("Factors registered: {}\n", engine.factor_count());
50
51 println!("{:<5} {:<22} {:<10} {:<8} {:<8} {:<10}",
52 "Step", "Item", "Category", "Success", "Effort", "Skill");
53 println!("{}", "-".repeat(70));
54
55 // --- Simulate 15 interactions ---
56 // Alternates easy successes and a few failures to show adaptation
57 let interactions: Vec<(bool, f32)> = vec![
58 (true, 0.8), // 1 hard success
59 (true, 0.3), // 2 easy success → optimism up
60 (true, 0.2), // 3 easy
61 (false, 0.9), // 4 failure → optimism eases
62 (true, 0.5), // 5
63 (true, 0.4), // 6
64 (true, 0.2), // 7 easy → optimism climbs
65 (true, 0.3), // 8
66 (false, 0.7), // 9 failure
67 (true, 0.5), // 10
68 (true, 0.4), // 11
69 (true, 0.3), // 12
70 (true, 0.2), // 13
71 (true, 0.5), // 14
72 (true, 0.3), // 15
73 ];
74
75 for (step, (success, effort)) in interactions.iter().enumerate() {
76 let item = engine.suggest("alice").unwrap();
77 let item_id = item.id().to_string();
78 let category = item.category().to_string();
79
80 engine.feedback("alice", &item_id, Signal::new(*success, *effort)).unwrap();
81
82 let state = engine.get_state("alice").unwrap();
83 println!("{:<5} {:<22} {:<10} {:<8} {:<8.2} {:<.4}",
84 step + 1,
85 item_id,
86 category,
87 if *success { "✓" } else { "✗" },
88 effort,
89 state.skill,
90 );
91 }
92
93 // --- Show final state ---
94 let state = engine.get_state("alice").unwrap();
95 println!("\n=== Final State ===");
96 println!("Skill: {:.4}", state.skill);
97 println!("Optimism bias: {:.4}", state.optimism_bias);
98 println!("Target: {:.4}", state.target());
99 println!("Interactions: {}", state.interaction_count);
100 println!("Resolved items: {:?}", state.resolved_set);
101
102 println!("\n=== Category Coverage ===");
103 let mut cats: Vec<_> = state.category_count.iter().collect();
104 cats.sort_by_key(|(k, _)| k.as_str());
105 for (cat, count) in cats {
106 println!(" {:<12} → {} interactions", cat, count);
107 }
108
109 // --- Serialise / deserialise round-trip ---
110 println!("\n=== Serialisation Round-Trip ===");
111 let encoded = Serialiser::encode(state);
112 println!("Encoded keys: {}", encoded.len());
113 let decoded = Serialiser::decode(&encoded).unwrap();
114 assert!((decoded.skill - state.skill).abs() < 1e-5, "skill mismatch after round-trip");
115 assert_eq!(decoded.interaction_count, state.interaction_count);
116 println!("Round-trip: ✓ skill={:.4} interactions={}", decoded.skill, decoded.interaction_count);
117
118 // --- Prereq demonstration ---
119 println!("\n=== Prerequisite Gating ===");
120 println!("calculus_intro requires: quadratics → algebra_basics");
121 let can_see_calculus = state.resolved_set.contains("quadratics");
122 println!("User resolved quadratics: {}", can_see_calculus);
123 println!("(calculus_intro will unlock once quadratics is resolved)");
124
125 println!("\n=== Custom Domain Example (ecommerce) ===");
126 demo_ecommerce();
127}
128
129/// Quick demo showing the same engine used for a completely different domain.
130fn demo_ecommerce() {
131 use aria_core::factor::ChallengeFactor;
132
133 struct BudgetFactor;
134 impl aria_core::factor::Factor for BudgetFactor {
135 fn name(&self) -> &str { "budget" }
136 fn score(&self, item: &dyn Scoreable, state: &aria_core::ProfileState, _now: u64) -> f32 {
137 // score_proxy = price_ratio (0=free, 1=most expensive)
138 // ProfileState.skill = budget_willingness
139 let target = state.target();
140 let diff = item.score_proxy() - target;
141 (-diff * diff / 0.08).exp() // bandwidth=0.2
142 }
143 }
144
145 let mut engine = Engine::new(EngineConfig {
146 exploration_rate: 0.0,
147 alpha: 0.1,
148 });
149 engine.add_factor(Box::new(BudgetFactor));
150 engine.add_factor(Box::new(ChallengeFactor::new(0.25))); // reused as price-fit
151 engine.seed_rng(1);
152
153 engine.add_items(vec![
154 Item::new("budget_headphones", 0.1, "audio"),
155 Item::new("mid_headphones", 0.4, "audio"),
156 Item::new("premium_headphones", 0.9, "audio"),
157 Item::new("budget_speaker", 0.15, "audio"),
158 Item::new("smartwatch_basic", 0.3, "wearables"),
159 Item::new("smartwatch_pro", 0.8, "wearables"),
160 ]).unwrap();
161
162 // User with mid budget
163 let mut state = aria_core::ProfileState::new();
164 state.skill = 0.35; // budget willingness
165 engine.load_state("shopper", state);
166
167 print!("Suggestions for mid-budget shopper: ");
168 for _ in 0..3 {
169 let item = engine.suggest("shopper").unwrap();
170 let id = item.id().to_string();
171 print!("{} ", id);
172 engine.feedback("shopper", &id, Signal::new(true, 0.5)).unwrap();
173 }
174 println!();
175}Sourcepub fn feedback(
&mut self,
user_id: &str,
item_id: &str,
signal: Signal,
) -> Result<(), AriaError>
pub fn feedback( &mut self, user_id: &str, item_id: &str, signal: Signal, ) -> Result<(), AriaError>
Report feedback for a user-item interaction. Updates internal ProfileState for the user.
Examples found in repository?
12fn main() {
13 println!("=== aria-core: Learning Domain Simulation ===\n");
14
15 // --- Build engine ---
16 let mut engine = Engine::new(EngineConfig {
17 exploration_rate: 0.0, // deterministic for demo
18 alpha: 0.1, // faster skill movement so visible in 15 steps
19 });
20
21 // Register factors — caller's choice
22 engine.add_factor(Box::new(ChallengeFactor::new(0.2)));
23 engine.add_factor(Box::new(SpacingFactor::new(10))); // 10s interval for demo
24 engine.add_factor(Box::new(CoverageFactor));
25 engine.seed_rng(42);
26
27 // Register items — caller's curriculum
28 engine.add_items(vec![
29 // Math track
30 Item::new("counting", 0.05, "math"),
31 Item::new("addition", 0.15, "math"),
32 Item::new("multiplication", 0.30, "math"),
33 Item::new("fractions", 0.45, "math"),
34 Item::new("algebra_basics", 0.60, "math"),
35 Item::new("quadratics", 0.75, "math")
36 .with_prereqs(vec!["algebra_basics".into()]),
37 Item::new("calculus_intro", 0.90, "math")
38 .with_prereqs(vec!["quadratics".into()]),
39
40 // Science track (no prereqs — runs in parallel)
41 Item::new("sci_observation", 0.10, "science"),
42 Item::new("sci_hypothesis", 0.25, "science"),
43 Item::new("sci_experiment", 0.50, "science"),
44 Item::new("sci_analysis", 0.70, "science"),
45 Item::new("sci_research", 0.90, "science"),
46 ]).unwrap();
47
48 println!("Items registered: {}", engine.item_count());
49 println!("Factors registered: {}\n", engine.factor_count());
50
51 println!("{:<5} {:<22} {:<10} {:<8} {:<8} {:<10}",
52 "Step", "Item", "Category", "Success", "Effort", "Skill");
53 println!("{}", "-".repeat(70));
54
55 // --- Simulate 15 interactions ---
56 // Alternates easy successes and a few failures to show adaptation
57 let interactions: Vec<(bool, f32)> = vec![
58 (true, 0.8), // 1 hard success
59 (true, 0.3), // 2 easy success → optimism up
60 (true, 0.2), // 3 easy
61 (false, 0.9), // 4 failure → optimism eases
62 (true, 0.5), // 5
63 (true, 0.4), // 6
64 (true, 0.2), // 7 easy → optimism climbs
65 (true, 0.3), // 8
66 (false, 0.7), // 9 failure
67 (true, 0.5), // 10
68 (true, 0.4), // 11
69 (true, 0.3), // 12
70 (true, 0.2), // 13
71 (true, 0.5), // 14
72 (true, 0.3), // 15
73 ];
74
75 for (step, (success, effort)) in interactions.iter().enumerate() {
76 let item = engine.suggest("alice").unwrap();
77 let item_id = item.id().to_string();
78 let category = item.category().to_string();
79
80 engine.feedback("alice", &item_id, Signal::new(*success, *effort)).unwrap();
81
82 let state = engine.get_state("alice").unwrap();
83 println!("{:<5} {:<22} {:<10} {:<8} {:<8.2} {:<.4}",
84 step + 1,
85 item_id,
86 category,
87 if *success { "✓" } else { "✗" },
88 effort,
89 state.skill,
90 );
91 }
92
93 // --- Show final state ---
94 let state = engine.get_state("alice").unwrap();
95 println!("\n=== Final State ===");
96 println!("Skill: {:.4}", state.skill);
97 println!("Optimism bias: {:.4}", state.optimism_bias);
98 println!("Target: {:.4}", state.target());
99 println!("Interactions: {}", state.interaction_count);
100 println!("Resolved items: {:?}", state.resolved_set);
101
102 println!("\n=== Category Coverage ===");
103 let mut cats: Vec<_> = state.category_count.iter().collect();
104 cats.sort_by_key(|(k, _)| k.as_str());
105 for (cat, count) in cats {
106 println!(" {:<12} → {} interactions", cat, count);
107 }
108
109 // --- Serialise / deserialise round-trip ---
110 println!("\n=== Serialisation Round-Trip ===");
111 let encoded = Serialiser::encode(state);
112 println!("Encoded keys: {}", encoded.len());
113 let decoded = Serialiser::decode(&encoded).unwrap();
114 assert!((decoded.skill - state.skill).abs() < 1e-5, "skill mismatch after round-trip");
115 assert_eq!(decoded.interaction_count, state.interaction_count);
116 println!("Round-trip: ✓ skill={:.4} interactions={}", decoded.skill, decoded.interaction_count);
117
118 // --- Prereq demonstration ---
119 println!("\n=== Prerequisite Gating ===");
120 println!("calculus_intro requires: quadratics → algebra_basics");
121 let can_see_calculus = state.resolved_set.contains("quadratics");
122 println!("User resolved quadratics: {}", can_see_calculus);
123 println!("(calculus_intro will unlock once quadratics is resolved)");
124
125 println!("\n=== Custom Domain Example (ecommerce) ===");
126 demo_ecommerce();
127}
128
129/// Quick demo showing the same engine used for a completely different domain.
130fn demo_ecommerce() {
131 use aria_core::factor::ChallengeFactor;
132
133 struct BudgetFactor;
134 impl aria_core::factor::Factor for BudgetFactor {
135 fn name(&self) -> &str { "budget" }
136 fn score(&self, item: &dyn Scoreable, state: &aria_core::ProfileState, _now: u64) -> f32 {
137 // score_proxy = price_ratio (0=free, 1=most expensive)
138 // ProfileState.skill = budget_willingness
139 let target = state.target();
140 let diff = item.score_proxy() - target;
141 (-diff * diff / 0.08).exp() // bandwidth=0.2
142 }
143 }
144
145 let mut engine = Engine::new(EngineConfig {
146 exploration_rate: 0.0,
147 alpha: 0.1,
148 });
149 engine.add_factor(Box::new(BudgetFactor));
150 engine.add_factor(Box::new(ChallengeFactor::new(0.25))); // reused as price-fit
151 engine.seed_rng(1);
152
153 engine.add_items(vec![
154 Item::new("budget_headphones", 0.1, "audio"),
155 Item::new("mid_headphones", 0.4, "audio"),
156 Item::new("premium_headphones", 0.9, "audio"),
157 Item::new("budget_speaker", 0.15, "audio"),
158 Item::new("smartwatch_basic", 0.3, "wearables"),
159 Item::new("smartwatch_pro", 0.8, "wearables"),
160 ]).unwrap();
161
162 // User with mid budget
163 let mut state = aria_core::ProfileState::new();
164 state.skill = 0.35; // budget willingness
165 engine.load_state("shopper", state);
166
167 print!("Suggestions for mid-budget shopper: ");
168 for _ in 0..3 {
169 let item = engine.suggest("shopper").unwrap();
170 let id = item.id().to_string();
171 print!("{} ", id);
172 engine.feedback("shopper", &id, Signal::new(true, 0.5)).unwrap();
173 }
174 println!();
175}Sourcepub fn get_state(&self, user_id: &str) -> Option<&ProfileState>
pub fn get_state(&self, user_id: &str) -> Option<&ProfileState>
Get current ProfileState for a user. Returns None if user has no interactions.
Examples found in repository?
12fn main() {
13 println!("=== aria-core: Learning Domain Simulation ===\n");
14
15 // --- Build engine ---
16 let mut engine = Engine::new(EngineConfig {
17 exploration_rate: 0.0, // deterministic for demo
18 alpha: 0.1, // faster skill movement so visible in 15 steps
19 });
20
21 // Register factors — caller's choice
22 engine.add_factor(Box::new(ChallengeFactor::new(0.2)));
23 engine.add_factor(Box::new(SpacingFactor::new(10))); // 10s interval for demo
24 engine.add_factor(Box::new(CoverageFactor));
25 engine.seed_rng(42);
26
27 // Register items — caller's curriculum
28 engine.add_items(vec![
29 // Math track
30 Item::new("counting", 0.05, "math"),
31 Item::new("addition", 0.15, "math"),
32 Item::new("multiplication", 0.30, "math"),
33 Item::new("fractions", 0.45, "math"),
34 Item::new("algebra_basics", 0.60, "math"),
35 Item::new("quadratics", 0.75, "math")
36 .with_prereqs(vec!["algebra_basics".into()]),
37 Item::new("calculus_intro", 0.90, "math")
38 .with_prereqs(vec!["quadratics".into()]),
39
40 // Science track (no prereqs — runs in parallel)
41 Item::new("sci_observation", 0.10, "science"),
42 Item::new("sci_hypothesis", 0.25, "science"),
43 Item::new("sci_experiment", 0.50, "science"),
44 Item::new("sci_analysis", 0.70, "science"),
45 Item::new("sci_research", 0.90, "science"),
46 ]).unwrap();
47
48 println!("Items registered: {}", engine.item_count());
49 println!("Factors registered: {}\n", engine.factor_count());
50
51 println!("{:<5} {:<22} {:<10} {:<8} {:<8} {:<10}",
52 "Step", "Item", "Category", "Success", "Effort", "Skill");
53 println!("{}", "-".repeat(70));
54
55 // --- Simulate 15 interactions ---
56 // Alternates easy successes and a few failures to show adaptation
57 let interactions: Vec<(bool, f32)> = vec![
58 (true, 0.8), // 1 hard success
59 (true, 0.3), // 2 easy success → optimism up
60 (true, 0.2), // 3 easy
61 (false, 0.9), // 4 failure → optimism eases
62 (true, 0.5), // 5
63 (true, 0.4), // 6
64 (true, 0.2), // 7 easy → optimism climbs
65 (true, 0.3), // 8
66 (false, 0.7), // 9 failure
67 (true, 0.5), // 10
68 (true, 0.4), // 11
69 (true, 0.3), // 12
70 (true, 0.2), // 13
71 (true, 0.5), // 14
72 (true, 0.3), // 15
73 ];
74
75 for (step, (success, effort)) in interactions.iter().enumerate() {
76 let item = engine.suggest("alice").unwrap();
77 let item_id = item.id().to_string();
78 let category = item.category().to_string();
79
80 engine.feedback("alice", &item_id, Signal::new(*success, *effort)).unwrap();
81
82 let state = engine.get_state("alice").unwrap();
83 println!("{:<5} {:<22} {:<10} {:<8} {:<8.2} {:<.4}",
84 step + 1,
85 item_id,
86 category,
87 if *success { "✓" } else { "✗" },
88 effort,
89 state.skill,
90 );
91 }
92
93 // --- Show final state ---
94 let state = engine.get_state("alice").unwrap();
95 println!("\n=== Final State ===");
96 println!("Skill: {:.4}", state.skill);
97 println!("Optimism bias: {:.4}", state.optimism_bias);
98 println!("Target: {:.4}", state.target());
99 println!("Interactions: {}", state.interaction_count);
100 println!("Resolved items: {:?}", state.resolved_set);
101
102 println!("\n=== Category Coverage ===");
103 let mut cats: Vec<_> = state.category_count.iter().collect();
104 cats.sort_by_key(|(k, _)| k.as_str());
105 for (cat, count) in cats {
106 println!(" {:<12} → {} interactions", cat, count);
107 }
108
109 // --- Serialise / deserialise round-trip ---
110 println!("\n=== Serialisation Round-Trip ===");
111 let encoded = Serialiser::encode(state);
112 println!("Encoded keys: {}", encoded.len());
113 let decoded = Serialiser::decode(&encoded).unwrap();
114 assert!((decoded.skill - state.skill).abs() < 1e-5, "skill mismatch after round-trip");
115 assert_eq!(decoded.interaction_count, state.interaction_count);
116 println!("Round-trip: ✓ skill={:.4} interactions={}", decoded.skill, decoded.interaction_count);
117
118 // --- Prereq demonstration ---
119 println!("\n=== Prerequisite Gating ===");
120 println!("calculus_intro requires: quadratics → algebra_basics");
121 let can_see_calculus = state.resolved_set.contains("quadratics");
122 println!("User resolved quadratics: {}", can_see_calculus);
123 println!("(calculus_intro will unlock once quadratics is resolved)");
124
125 println!("\n=== Custom Domain Example (ecommerce) ===");
126 demo_ecommerce();
127}Sourcepub fn load_state(&mut self, user_id: impl Into<String>, state: ProfileState)
pub fn load_state(&mut self, user_id: impl Into<String>, state: ProfileState)
Load a previously serialised ProfileState for a user. Use with Serialiser::decode to restore sessions across restarts.
Examples found in repository?
130fn demo_ecommerce() {
131 use aria_core::factor::ChallengeFactor;
132
133 struct BudgetFactor;
134 impl aria_core::factor::Factor for BudgetFactor {
135 fn name(&self) -> &str { "budget" }
136 fn score(&self, item: &dyn Scoreable, state: &aria_core::ProfileState, _now: u64) -> f32 {
137 // score_proxy = price_ratio (0=free, 1=most expensive)
138 // ProfileState.skill = budget_willingness
139 let target = state.target();
140 let diff = item.score_proxy() - target;
141 (-diff * diff / 0.08).exp() // bandwidth=0.2
142 }
143 }
144
145 let mut engine = Engine::new(EngineConfig {
146 exploration_rate: 0.0,
147 alpha: 0.1,
148 });
149 engine.add_factor(Box::new(BudgetFactor));
150 engine.add_factor(Box::new(ChallengeFactor::new(0.25))); // reused as price-fit
151 engine.seed_rng(1);
152
153 engine.add_items(vec![
154 Item::new("budget_headphones", 0.1, "audio"),
155 Item::new("mid_headphones", 0.4, "audio"),
156 Item::new("premium_headphones", 0.9, "audio"),
157 Item::new("budget_speaker", 0.15, "audio"),
158 Item::new("smartwatch_basic", 0.3, "wearables"),
159 Item::new("smartwatch_pro", 0.8, "wearables"),
160 ]).unwrap();
161
162 // User with mid budget
163 let mut state = aria_core::ProfileState::new();
164 state.skill = 0.35; // budget willingness
165 engine.load_state("shopper", state);
166
167 print!("Suggestions for mid-budget shopper: ");
168 for _ in 0..3 {
169 let item = engine.suggest("shopper").unwrap();
170 let id = item.id().to_string();
171 print!("{} ", id);
172 engine.feedback("shopper", &id, Signal::new(true, 0.5)).unwrap();
173 }
174 println!();
175}Sourcepub fn item_count(&self) -> usize
pub fn item_count(&self) -> usize
Number of registered items.
Examples found in repository?
12fn main() {
13 println!("=== aria-core: Learning Domain Simulation ===\n");
14
15 // --- Build engine ---
16 let mut engine = Engine::new(EngineConfig {
17 exploration_rate: 0.0, // deterministic for demo
18 alpha: 0.1, // faster skill movement so visible in 15 steps
19 });
20
21 // Register factors — caller's choice
22 engine.add_factor(Box::new(ChallengeFactor::new(0.2)));
23 engine.add_factor(Box::new(SpacingFactor::new(10))); // 10s interval for demo
24 engine.add_factor(Box::new(CoverageFactor));
25 engine.seed_rng(42);
26
27 // Register items — caller's curriculum
28 engine.add_items(vec![
29 // Math track
30 Item::new("counting", 0.05, "math"),
31 Item::new("addition", 0.15, "math"),
32 Item::new("multiplication", 0.30, "math"),
33 Item::new("fractions", 0.45, "math"),
34 Item::new("algebra_basics", 0.60, "math"),
35 Item::new("quadratics", 0.75, "math")
36 .with_prereqs(vec!["algebra_basics".into()]),
37 Item::new("calculus_intro", 0.90, "math")
38 .with_prereqs(vec!["quadratics".into()]),
39
40 // Science track (no prereqs — runs in parallel)
41 Item::new("sci_observation", 0.10, "science"),
42 Item::new("sci_hypothesis", 0.25, "science"),
43 Item::new("sci_experiment", 0.50, "science"),
44 Item::new("sci_analysis", 0.70, "science"),
45 Item::new("sci_research", 0.90, "science"),
46 ]).unwrap();
47
48 println!("Items registered: {}", engine.item_count());
49 println!("Factors registered: {}\n", engine.factor_count());
50
51 println!("{:<5} {:<22} {:<10} {:<8} {:<8} {:<10}",
52 "Step", "Item", "Category", "Success", "Effort", "Skill");
53 println!("{}", "-".repeat(70));
54
55 // --- Simulate 15 interactions ---
56 // Alternates easy successes and a few failures to show adaptation
57 let interactions: Vec<(bool, f32)> = vec![
58 (true, 0.8), // 1 hard success
59 (true, 0.3), // 2 easy success → optimism up
60 (true, 0.2), // 3 easy
61 (false, 0.9), // 4 failure → optimism eases
62 (true, 0.5), // 5
63 (true, 0.4), // 6
64 (true, 0.2), // 7 easy → optimism climbs
65 (true, 0.3), // 8
66 (false, 0.7), // 9 failure
67 (true, 0.5), // 10
68 (true, 0.4), // 11
69 (true, 0.3), // 12
70 (true, 0.2), // 13
71 (true, 0.5), // 14
72 (true, 0.3), // 15
73 ];
74
75 for (step, (success, effort)) in interactions.iter().enumerate() {
76 let item = engine.suggest("alice").unwrap();
77 let item_id = item.id().to_string();
78 let category = item.category().to_string();
79
80 engine.feedback("alice", &item_id, Signal::new(*success, *effort)).unwrap();
81
82 let state = engine.get_state("alice").unwrap();
83 println!("{:<5} {:<22} {:<10} {:<8} {:<8.2} {:<.4}",
84 step + 1,
85 item_id,
86 category,
87 if *success { "✓" } else { "✗" },
88 effort,
89 state.skill,
90 );
91 }
92
93 // --- Show final state ---
94 let state = engine.get_state("alice").unwrap();
95 println!("\n=== Final State ===");
96 println!("Skill: {:.4}", state.skill);
97 println!("Optimism bias: {:.4}", state.optimism_bias);
98 println!("Target: {:.4}", state.target());
99 println!("Interactions: {}", state.interaction_count);
100 println!("Resolved items: {:?}", state.resolved_set);
101
102 println!("\n=== Category Coverage ===");
103 let mut cats: Vec<_> = state.category_count.iter().collect();
104 cats.sort_by_key(|(k, _)| k.as_str());
105 for (cat, count) in cats {
106 println!(" {:<12} → {} interactions", cat, count);
107 }
108
109 // --- Serialise / deserialise round-trip ---
110 println!("\n=== Serialisation Round-Trip ===");
111 let encoded = Serialiser::encode(state);
112 println!("Encoded keys: {}", encoded.len());
113 let decoded = Serialiser::decode(&encoded).unwrap();
114 assert!((decoded.skill - state.skill).abs() < 1e-5, "skill mismatch after round-trip");
115 assert_eq!(decoded.interaction_count, state.interaction_count);
116 println!("Round-trip: ✓ skill={:.4} interactions={}", decoded.skill, decoded.interaction_count);
117
118 // --- Prereq demonstration ---
119 println!("\n=== Prerequisite Gating ===");
120 println!("calculus_intro requires: quadratics → algebra_basics");
121 let can_see_calculus = state.resolved_set.contains("quadratics");
122 println!("User resolved quadratics: {}", can_see_calculus);
123 println!("(calculus_intro will unlock once quadratics is resolved)");
124
125 println!("\n=== Custom Domain Example (ecommerce) ===");
126 demo_ecommerce();
127}Sourcepub fn factor_count(&self) -> usize
pub fn factor_count(&self) -> usize
Number of registered factors.
Examples found in repository?
12fn main() {
13 println!("=== aria-core: Learning Domain Simulation ===\n");
14
15 // --- Build engine ---
16 let mut engine = Engine::new(EngineConfig {
17 exploration_rate: 0.0, // deterministic for demo
18 alpha: 0.1, // faster skill movement so visible in 15 steps
19 });
20
21 // Register factors — caller's choice
22 engine.add_factor(Box::new(ChallengeFactor::new(0.2)));
23 engine.add_factor(Box::new(SpacingFactor::new(10))); // 10s interval for demo
24 engine.add_factor(Box::new(CoverageFactor));
25 engine.seed_rng(42);
26
27 // Register items — caller's curriculum
28 engine.add_items(vec![
29 // Math track
30 Item::new("counting", 0.05, "math"),
31 Item::new("addition", 0.15, "math"),
32 Item::new("multiplication", 0.30, "math"),
33 Item::new("fractions", 0.45, "math"),
34 Item::new("algebra_basics", 0.60, "math"),
35 Item::new("quadratics", 0.75, "math")
36 .with_prereqs(vec!["algebra_basics".into()]),
37 Item::new("calculus_intro", 0.90, "math")
38 .with_prereqs(vec!["quadratics".into()]),
39
40 // Science track (no prereqs — runs in parallel)
41 Item::new("sci_observation", 0.10, "science"),
42 Item::new("sci_hypothesis", 0.25, "science"),
43 Item::new("sci_experiment", 0.50, "science"),
44 Item::new("sci_analysis", 0.70, "science"),
45 Item::new("sci_research", 0.90, "science"),
46 ]).unwrap();
47
48 println!("Items registered: {}", engine.item_count());
49 println!("Factors registered: {}\n", engine.factor_count());
50
51 println!("{:<5} {:<22} {:<10} {:<8} {:<8} {:<10}",
52 "Step", "Item", "Category", "Success", "Effort", "Skill");
53 println!("{}", "-".repeat(70));
54
55 // --- Simulate 15 interactions ---
56 // Alternates easy successes and a few failures to show adaptation
57 let interactions: Vec<(bool, f32)> = vec![
58 (true, 0.8), // 1 hard success
59 (true, 0.3), // 2 easy success → optimism up
60 (true, 0.2), // 3 easy
61 (false, 0.9), // 4 failure → optimism eases
62 (true, 0.5), // 5
63 (true, 0.4), // 6
64 (true, 0.2), // 7 easy → optimism climbs
65 (true, 0.3), // 8
66 (false, 0.7), // 9 failure
67 (true, 0.5), // 10
68 (true, 0.4), // 11
69 (true, 0.3), // 12
70 (true, 0.2), // 13
71 (true, 0.5), // 14
72 (true, 0.3), // 15
73 ];
74
75 for (step, (success, effort)) in interactions.iter().enumerate() {
76 let item = engine.suggest("alice").unwrap();
77 let item_id = item.id().to_string();
78 let category = item.category().to_string();
79
80 engine.feedback("alice", &item_id, Signal::new(*success, *effort)).unwrap();
81
82 let state = engine.get_state("alice").unwrap();
83 println!("{:<5} {:<22} {:<10} {:<8} {:<8.2} {:<.4}",
84 step + 1,
85 item_id,
86 category,
87 if *success { "✓" } else { "✗" },
88 effort,
89 state.skill,
90 );
91 }
92
93 // --- Show final state ---
94 let state = engine.get_state("alice").unwrap();
95 println!("\n=== Final State ===");
96 println!("Skill: {:.4}", state.skill);
97 println!("Optimism bias: {:.4}", state.optimism_bias);
98 println!("Target: {:.4}", state.target());
99 println!("Interactions: {}", state.interaction_count);
100 println!("Resolved items: {:?}", state.resolved_set);
101
102 println!("\n=== Category Coverage ===");
103 let mut cats: Vec<_> = state.category_count.iter().collect();
104 cats.sort_by_key(|(k, _)| k.as_str());
105 for (cat, count) in cats {
106 println!(" {:<12} → {} interactions", cat, count);
107 }
108
109 // --- Serialise / deserialise round-trip ---
110 println!("\n=== Serialisation Round-Trip ===");
111 let encoded = Serialiser::encode(state);
112 println!("Encoded keys: {}", encoded.len());
113 let decoded = Serialiser::decode(&encoded).unwrap();
114 assert!((decoded.skill - state.skill).abs() < 1e-5, "skill mismatch after round-trip");
115 assert_eq!(decoded.interaction_count, state.interaction_count);
116 println!("Round-trip: ✓ skill={:.4} interactions={}", decoded.skill, decoded.interaction_count);
117
118 // --- Prereq demonstration ---
119 println!("\n=== Prerequisite Gating ===");
120 println!("calculus_intro requires: quadratics → algebra_basics");
121 let can_see_calculus = state.resolved_set.contains("quadratics");
122 println!("User resolved quadratics: {}", can_see_calculus);
123 println!("(calculus_intro will unlock once quadratics is resolved)");
124
125 println!("\n=== Custom Domain Example (ecommerce) ===");
126 demo_ecommerce();
127}Sourcepub fn seed_rng(&mut self, seed: u64)
pub fn seed_rng(&mut self, seed: u64)
Seed the selector RNG — use in tests for determinism.
Examples found in repository?
12fn main() {
13 println!("=== aria-core: Learning Domain Simulation ===\n");
14
15 // --- Build engine ---
16 let mut engine = Engine::new(EngineConfig {
17 exploration_rate: 0.0, // deterministic for demo
18 alpha: 0.1, // faster skill movement so visible in 15 steps
19 });
20
21 // Register factors — caller's choice
22 engine.add_factor(Box::new(ChallengeFactor::new(0.2)));
23 engine.add_factor(Box::new(SpacingFactor::new(10))); // 10s interval for demo
24 engine.add_factor(Box::new(CoverageFactor));
25 engine.seed_rng(42);
26
27 // Register items — caller's curriculum
28 engine.add_items(vec![
29 // Math track
30 Item::new("counting", 0.05, "math"),
31 Item::new("addition", 0.15, "math"),
32 Item::new("multiplication", 0.30, "math"),
33 Item::new("fractions", 0.45, "math"),
34 Item::new("algebra_basics", 0.60, "math"),
35 Item::new("quadratics", 0.75, "math")
36 .with_prereqs(vec!["algebra_basics".into()]),
37 Item::new("calculus_intro", 0.90, "math")
38 .with_prereqs(vec!["quadratics".into()]),
39
40 // Science track (no prereqs — runs in parallel)
41 Item::new("sci_observation", 0.10, "science"),
42 Item::new("sci_hypothesis", 0.25, "science"),
43 Item::new("sci_experiment", 0.50, "science"),
44 Item::new("sci_analysis", 0.70, "science"),
45 Item::new("sci_research", 0.90, "science"),
46 ]).unwrap();
47
48 println!("Items registered: {}", engine.item_count());
49 println!("Factors registered: {}\n", engine.factor_count());
50
51 println!("{:<5} {:<22} {:<10} {:<8} {:<8} {:<10}",
52 "Step", "Item", "Category", "Success", "Effort", "Skill");
53 println!("{}", "-".repeat(70));
54
55 // --- Simulate 15 interactions ---
56 // Alternates easy successes and a few failures to show adaptation
57 let interactions: Vec<(bool, f32)> = vec![
58 (true, 0.8), // 1 hard success
59 (true, 0.3), // 2 easy success → optimism up
60 (true, 0.2), // 3 easy
61 (false, 0.9), // 4 failure → optimism eases
62 (true, 0.5), // 5
63 (true, 0.4), // 6
64 (true, 0.2), // 7 easy → optimism climbs
65 (true, 0.3), // 8
66 (false, 0.7), // 9 failure
67 (true, 0.5), // 10
68 (true, 0.4), // 11
69 (true, 0.3), // 12
70 (true, 0.2), // 13
71 (true, 0.5), // 14
72 (true, 0.3), // 15
73 ];
74
75 for (step, (success, effort)) in interactions.iter().enumerate() {
76 let item = engine.suggest("alice").unwrap();
77 let item_id = item.id().to_string();
78 let category = item.category().to_string();
79
80 engine.feedback("alice", &item_id, Signal::new(*success, *effort)).unwrap();
81
82 let state = engine.get_state("alice").unwrap();
83 println!("{:<5} {:<22} {:<10} {:<8} {:<8.2} {:<.4}",
84 step + 1,
85 item_id,
86 category,
87 if *success { "✓" } else { "✗" },
88 effort,
89 state.skill,
90 );
91 }
92
93 // --- Show final state ---
94 let state = engine.get_state("alice").unwrap();
95 println!("\n=== Final State ===");
96 println!("Skill: {:.4}", state.skill);
97 println!("Optimism bias: {:.4}", state.optimism_bias);
98 println!("Target: {:.4}", state.target());
99 println!("Interactions: {}", state.interaction_count);
100 println!("Resolved items: {:?}", state.resolved_set);
101
102 println!("\n=== Category Coverage ===");
103 let mut cats: Vec<_> = state.category_count.iter().collect();
104 cats.sort_by_key(|(k, _)| k.as_str());
105 for (cat, count) in cats {
106 println!(" {:<12} → {} interactions", cat, count);
107 }
108
109 // --- Serialise / deserialise round-trip ---
110 println!("\n=== Serialisation Round-Trip ===");
111 let encoded = Serialiser::encode(state);
112 println!("Encoded keys: {}", encoded.len());
113 let decoded = Serialiser::decode(&encoded).unwrap();
114 assert!((decoded.skill - state.skill).abs() < 1e-5, "skill mismatch after round-trip");
115 assert_eq!(decoded.interaction_count, state.interaction_count);
116 println!("Round-trip: ✓ skill={:.4} interactions={}", decoded.skill, decoded.interaction_count);
117
118 // --- Prereq demonstration ---
119 println!("\n=== Prerequisite Gating ===");
120 println!("calculus_intro requires: quadratics → algebra_basics");
121 let can_see_calculus = state.resolved_set.contains("quadratics");
122 println!("User resolved quadratics: {}", can_see_calculus);
123 println!("(calculus_intro will unlock once quadratics is resolved)");
124
125 println!("\n=== Custom Domain Example (ecommerce) ===");
126 demo_ecommerce();
127}
128
129/// Quick demo showing the same engine used for a completely different domain.
130fn demo_ecommerce() {
131 use aria_core::factor::ChallengeFactor;
132
133 struct BudgetFactor;
134 impl aria_core::factor::Factor for BudgetFactor {
135 fn name(&self) -> &str { "budget" }
136 fn score(&self, item: &dyn Scoreable, state: &aria_core::ProfileState, _now: u64) -> f32 {
137 // score_proxy = price_ratio (0=free, 1=most expensive)
138 // ProfileState.skill = budget_willingness
139 let target = state.target();
140 let diff = item.score_proxy() - target;
141 (-diff * diff / 0.08).exp() // bandwidth=0.2
142 }
143 }
144
145 let mut engine = Engine::new(EngineConfig {
146 exploration_rate: 0.0,
147 alpha: 0.1,
148 });
149 engine.add_factor(Box::new(BudgetFactor));
150 engine.add_factor(Box::new(ChallengeFactor::new(0.25))); // reused as price-fit
151 engine.seed_rng(1);
152
153 engine.add_items(vec![
154 Item::new("budget_headphones", 0.1, "audio"),
155 Item::new("mid_headphones", 0.4, "audio"),
156 Item::new("premium_headphones", 0.9, "audio"),
157 Item::new("budget_speaker", 0.15, "audio"),
158 Item::new("smartwatch_basic", 0.3, "wearables"),
159 Item::new("smartwatch_pro", 0.8, "wearables"),
160 ]).unwrap();
161
162 // User with mid budget
163 let mut state = aria_core::ProfileState::new();
164 state.skill = 0.35; // budget willingness
165 engine.load_state("shopper", state);
166
167 print!("Suggestions for mid-budget shopper: ");
168 for _ in 0..3 {
169 let item = engine.suggest("shopper").unwrap();
170 let id = item.id().to_string();
171 print!("{} ", id);
172 engine.feedback("shopper", &id, Signal::new(true, 0.5)).unwrap();
173 }
174 println!();
175}