converge_analytics/packs/classification/
solver.rs1use super::types::*;
2use converge_pack::gate::GateResult as Result;
3use converge_pack::gate::{ProblemSpec, ReplayEnvelope, SolverReport};
4
5pub struct LogisticClassifier;
6
7impl LogisticClassifier {
8 pub fn solve(
9 &self,
10 input: &ClassificationInput,
11 spec: &ProblemSpec,
12 ) -> Result<(ClassificationOutput, SolverReport)> {
13 let (pos_label, neg_label) = input
14 .labels
15 .clone()
16 .unwrap_or_else(|| ("positive".to_string(), "negative".to_string()));
17
18 let mut positive_count = 0usize;
19 let mut negative_count = 0usize;
20
21 let predictions: Vec<ClassifiedRecord> = input
22 .records
23 .iter()
24 .enumerate()
25 .map(|(i, record)| {
26 let logit: f64 = record
27 .iter()
28 .zip(&input.weights)
29 .map(|(x, w)| x * w)
30 .sum::<f64>()
31 + input.bias;
32
33 let probability = sigmoid(logit);
34 let label = if probability >= input.threshold {
35 positive_count += 1;
36 pos_label.clone()
37 } else {
38 negative_count += 1;
39 neg_label.clone()
40 };
41
42 ClassifiedRecord {
43 index: i,
44 probability,
45 label,
46 }
47 })
48 .collect();
49
50 let total = predictions.len();
51 let output = ClassificationOutput {
52 predictions,
53 positive_count,
54 negative_count,
55 total,
56 };
57
58 let avg_decisiveness: f64 = output
60 .predictions
61 .iter()
62 .map(|p| (p.probability - 0.5).abs() * 2.0)
63 .sum::<f64>()
64 / total as f64;
65
66 let confidence = avg_decisiveness.clamp(0.3, 0.95);
67
68 let replay = ReplayEnvelope::minimal(spec.seed());
69 let report = SolverReport::optimal("logistic-classifier-v1", confidence, replay);
70
71 Ok((output, report))
72 }
73}
74
75fn sigmoid(x: f64) -> f64 {
76 1.0 / (1.0 + (-x).exp())
77}