pub struct ChallengeFactor {
pub bandwidth: f32,
}Expand description
ChallengeFactor — Gaussian centred at user’s target (skill + optimism).
Items at exactly target difficulty score 1.0.
Items far from target score near 0.0.
bandwidth controls tolerance: wider = more variety, narrower = strict gating.
Domain mapping:
learning → item difficulty vs user mastery
ecommerce → price_ratio vs budget willingness
travel → remoteness vs adventurousness
content → reading level vs literacy
Fields§
§bandwidth: f32Implementations§
Source§impl ChallengeFactor
impl ChallengeFactor
Sourcepub fn new(bandwidth: f32) -> Self
pub fn new(bandwidth: f32) -> Self
Examples found in repository?
examples/learning_simulation.rs (line 22)
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}Trait Implementations§
Source§impl Default for ChallengeFactor
impl Default for ChallengeFactor
Auto Trait Implementations§
impl Freeze for ChallengeFactor
impl RefUnwindSafe for ChallengeFactor
impl Send for ChallengeFactor
impl Sync for ChallengeFactor
impl Unpin for ChallengeFactor
impl UnsafeUnpin for ChallengeFactor
impl UnwindSafe for ChallengeFactor
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more