Skip to main content

Signal

Struct Signal 

Source
pub struct Signal {
    pub success: bool,
    pub effort: f32,
    pub metadata: HashMap<String, String>,
}
Expand description

Feedback signal reported by caller after a user interacts with an item.

Domain-agnostic by design:

  • In learning: success = answered correctly, effort = how hard it felt (0=easy, 1=hard)
  • In ecommerce: success = purchased/clicked, effort = price relative to budget
  • In travel: success = booked/saved, effort = distance/cost relative to preference
  • In content: success = completed/liked, effort = complexity relative to level

Callers may extend context via the metadata map for custom StateUpdater logic.

Fields§

§success: bool

Did the interaction result in a positive outcome?

§effort: f32

Normalised effort cost of this interaction. Range [0.0, 1.0]. 0.0 = effortless, 1.0 = maximum effort / friction.

§metadata: HashMap<String, String>

Optional arbitrary metadata for custom updater logic. Key-value pairs; values are caller-defined strings.

Implementations§

Source§

impl Signal

Source

pub fn new(success: bool, effort: f32) -> Self

Minimal constructor — no metadata.

Examples found in repository?
examples/learning_simulation.rs (line 80)
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}
Source

pub fn with_metadata( success: bool, effort: f32, metadata: HashMap<String, String>, ) -> Self

Constructor with metadata.

Source

pub fn performance(&self) -> f32

Composite performance score derived from success + effort. High success + low effort → score near 1.0 (too easy). High success + high effort → score near 0.5 (good challenge). Failure → score near 0.0.

Trait Implementations§

Source§

impl Clone for Signal

Source§

fn clone(&self) -> Signal

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Signal

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.