use super::meta_learning::{MetaAction, MetaState};
use super::optimizer::AdaptationType;
use scirs2_core::numeric::Float;
#[derive(Debug, Clone)]
pub(super) struct BanditArm<A: Float + Send + Sync> {
pub(super) label: String,
pub(super) adaptation_type: AdaptationType,
pub(super) magnitude_f64: f64,
pub(super) weights: Vec<A>,
pub(super) bias: A,
pub(super) pulls: usize,
pub(super) reward_total: A,
}
impl<A: Float + Send + Sync> BanditArm<A> {
pub(super) fn new(label: String, adaptation_type: AdaptationType, magnitude_f64: f64) -> Self {
Self {
label,
adaptation_type,
magnitude_f64,
weights: Vec::new(),
bias: A::zero(),
pulls: 0,
reward_total: A::zero(),
}
}
pub(super) fn magnitude(&self) -> Option<A> {
A::from(self.magnitude_f64)
}
pub(super) fn predict(&self, features: &[A]) -> A {
let mut value = self.bias;
for (weight, &feature) in self.weights.iter().zip(features.iter()) {
value = value + *weight * feature;
}
value
}
pub(super) fn sgd_step(&mut self, features: &[A], error: A, learning_rate: A, l2_lambda: A) {
if self.weights.len() < features.len() {
self.weights.resize(features.len(), A::zero());
}
let input_energy = features
.iter()
.fold(A::one(), |acc, &feature| acc + feature * feature);
if input_energy <= A::zero() || !input_energy.is_finite() {
return;
}
let step = learning_rate * error / input_energy;
for (weight, &feature) in self.weights.iter_mut().zip(features.iter()) {
*weight = *weight - step * feature - l2_lambda * *weight;
}
self.bias = self.bias - step;
}
pub(super) fn observe(&mut self, reward: A) {
self.pulls += 1;
self.reward_total = self.reward_total + reward;
}
}
#[derive(Debug, Clone, Default)]
pub(super) struct FeatureScaler<A: Float + Send + Sync> {
pub(super) means: Vec<A>,
pub(super) m2: Vec<A>,
pub(super) count: usize,
}
impl<A: Float + Send + Sync> FeatureScaler<A> {
pub(super) fn observe(&mut self, features: &[A]) {
if self.means.len() < features.len() {
self.means.resize(features.len(), A::zero());
self.m2.resize(features.len(), A::zero());
}
self.count = self.count.saturating_add(1);
let Some(count) = A::from(self.count) else {
return;
};
for (index, &value) in features.iter().enumerate() {
if !value.is_finite() {
continue;
}
let mean = self.means[index];
let delta = value - mean;
let new_mean = mean + delta / count;
self.means[index] = new_mean;
self.m2[index] = self.m2[index] + delta * (value - new_mean);
}
}
pub(super) fn standardize(&self, features: &[A]) -> Vec<A> {
if self.count < 2 {
return features.to_vec();
}
let Some(count) = A::from(self.count) else {
return features.to_vec();
};
features
.iter()
.enumerate()
.map(|(index, &value)| {
let (Some(&mean), Some(&m2)) = (self.means.get(index), self.m2.get(index)) else {
return A::zero();
};
let variance = m2 / count;
if variance <= A::zero() || !variance.is_finite() {
return A::zero();
}
let standardized = (value - mean) / variance.sqrt();
if standardized.is_finite() {
standardized
} else {
A::zero()
}
})
.collect()
}
}
pub(super) fn state_features<A: Float + Send + Sync>(state: &MetaState<A>) -> Vec<A> {
let mut features = Vec::with_capacity(
state.performance_metrics.len()
+ state.resource_state.len()
+ state.drift_indicators.len()
+ 1,
);
features.extend(state.performance_metrics.iter().copied());
features.extend(state.resource_state.iter().copied());
features.extend(state.drift_indicators.iter().copied());
let history = (state.adaptation_history as f64 + 1.0).ln();
if let Some(value) = A::from(history) {
features.push(value);
}
features
}
pub(super) fn arm_index_for<A: Float + Send + Sync>(action: &MetaAction<A>) -> Option<usize> {
let adaptation_type = action.adaptation_types.first()?;
let magnitude = action.adaptation_magnitudes.first()?.to_f64()?;
let table = arm_table();
let mut best: Option<(usize, f64)> = None;
for (index, (_, arm_type, arm_magnitude)) in table.iter().enumerate() {
if !same_adaptation_type(arm_type, adaptation_type) {
continue;
}
let distance = (arm_magnitude - magnitude).abs();
if best.map(|(_, d)| distance < d).unwrap_or(true) {
best = Some((index, distance));
}
}
best.map(|(index, _)| index)
}
fn same_adaptation_type(left: &AdaptationType, right: &AdaptationType) -> bool {
left == right
}
pub(super) fn arm_table() -> Vec<(&'static str, AdaptationType, f64)> {
let mut table = Vec::with_capacity(10);
for magnitude in [-0.2_f64, -0.1, -0.05, 0.05, 0.1, 0.2] {
table.push(("learning_rate", AdaptationType::LearningRate, magnitude));
}
for magnitude in [-0.3_f64, -0.1, 0.1, 0.3] {
table.push(("buffer_size", AdaptationType::BufferSize, magnitude));
}
table
}