Skip to main content

phago_runtime/
curriculum.rs

1//! Curriculum ordering for training data.
2//!
3//! Orders triples in a pedagogically meaningful sequence:
4//! 1. Foundation: high-weight, same-community triples (core concepts)
5//! 2. Bridges: cross-community triples (connecting knowledge)
6//! 3. Periphery: low-weight triples (specialized details)
7
8use crate::community::CommunityResult;
9use crate::export::WeightedTriple;
10use serde::Serialize;
11
12/// A curriculum-ordered sequence of triples.
13#[derive(Debug, Clone, Serialize)]
14pub struct Curriculum {
15    pub foundation: Vec<WeightedTriple>,
16    pub bridges: Vec<WeightedTriple>,
17    pub periphery: Vec<WeightedTriple>,
18}
19
20impl Curriculum {
21    /// Total number of triples.
22    pub fn total(&self) -> usize {
23        self.foundation.len() + self.bridges.len() + self.periphery.len()
24    }
25
26    /// Get all triples in curriculum order.
27    pub fn ordered(&self) -> Vec<&WeightedTriple> {
28        self.foundation.iter()
29            .chain(self.bridges.iter())
30            .chain(self.periphery.iter())
31            .collect()
32    }
33}
34
35/// Build a curriculum from triples and community assignments.
36///
37/// - Foundation: both nodes in the same community, weight > median
38/// - Bridge: nodes in different communities
39/// - Periphery: same community but weight ≤ median
40pub fn build_curriculum(
41    triples: &[WeightedTriple],
42    communities: &CommunityResult,
43) -> Curriculum {
44    if triples.is_empty() {
45        return Curriculum {
46            foundation: Vec::new(),
47            bridges: Vec::new(),
48            periphery: Vec::new(),
49        };
50    }
51
52    // Compute median weight
53    let mut weights: Vec<f64> = triples.iter().map(|t| t.weight).collect();
54    weights.sort_by(|a, b| a.partial_cmp(b).unwrap());
55    let median = weights[weights.len() / 2];
56
57    let mut foundation = Vec::new();
58    let mut bridges = Vec::new();
59    let mut periphery = Vec::new();
60
61    for triple in triples {
62        let subj_community = communities.assignments.get(&triple.subject);
63        let obj_community = communities.assignments.get(&triple.object);
64
65        match (subj_community, obj_community) {
66            (Some(sc), Some(oc)) if sc != oc => {
67                bridges.push(triple.clone());
68            }
69            _ => {
70                if triple.weight > median {
71                    foundation.push(triple.clone());
72                } else {
73                    periphery.push(triple.clone());
74                }
75            }
76        }
77    }
78
79    // Sort each section by weight descending
80    foundation.sort_by(|a, b| b.weight.partial_cmp(&a.weight).unwrap_or(std::cmp::Ordering::Equal));
81    bridges.sort_by(|a, b| b.weight.partial_cmp(&a.weight).unwrap_or(std::cmp::Ordering::Equal));
82    periphery.sort_by(|a, b| b.weight.partial_cmp(&a.weight).unwrap_or(std::cmp::Ordering::Equal));
83
84    Curriculum {
85        foundation,
86        bridges,
87        periphery,
88    }
89}