use crate::error::{OptimError, Result};
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::numeric::Float;
use std::collections::HashMap;
use std::fmt::Debug;
pub mod actor_critic;
pub mod linear_models;
pub mod natural_gradients;
pub mod policy_gradient;
pub mod trust_region;
pub use actor_critic::{ActorCriticConfig, ActorCriticMethod, ActorCriticOptimizer};
pub use linear_models::{
LinearGaussianPolicy, LinearQFunction, LinearSoftmaxPolicy, LinearValueFunction,
};
pub use natural_gradients::{NaturalGradientConfig, NaturalPolicyGradient};
pub use policy_gradient::{PolicyGradientConfig, PolicyGradientMethod, PolicyGradientOptimizer};
pub use trust_region::{TrustRegionConfig, TrustRegionMethod, TrustRegionOptimizer};
#[derive(Debug, Clone)]
pub struct RLOptimizerConfig<T: Float + Debug + Send + Sync + 'static> {
pub policy_lr: T,
pub value_lr: T,
pub discount_factor: T,
pub gae_lambda: T,
pub clip_epsilon: T,
pub entropy_coeff: T,
pub value_loss_coeff: T,
pub max_grad_norm: T,
pub n_epochs: usize,
pub mini_batchsize: usize,
pub trust_region_config: Option<TrustRegionConfig<T>>,
pub use_natural_gradients: bool,
pub fisher_approximation: FisherApproximationMethod,
}
#[derive(Debug, Clone, Copy)]
pub enum FisherApproximationMethod {
Empirical,
KroneckerFactored,
Diagonal,
BlockDiagonal,
LowRank,
}
impl<T: Float + Debug + Send + Sync + 'static> Default for RLOptimizerConfig<T> {
fn default() -> Self {
Self {
policy_lr: T::from(3e-4).unwrap_or_else(|| T::zero()),
value_lr: T::from(1e-3).unwrap_or_else(|| T::zero()),
discount_factor: T::from(0.99).unwrap_or_else(|| T::zero()),
gae_lambda: T::from(0.95).unwrap_or_else(|| T::zero()),
clip_epsilon: T::from(0.2).unwrap_or_else(|| T::zero()),
entropy_coeff: T::from(0.01).unwrap_or_else(|| T::zero()),
value_loss_coeff: T::from(0.5).unwrap_or_else(|| T::zero()),
max_grad_norm: T::from(0.5).unwrap_or_else(|| T::zero()),
n_epochs: 4,
mini_batchsize: 64,
trust_region_config: None,
use_natural_gradients: false,
fisher_approximation: FisherApproximationMethod::Diagonal,
}
}
}
#[derive(Debug, Clone)]
pub struct TrajectoryBatch<T: Float + Debug + Send + Sync + 'static> {
pub observations: Array2<T>,
pub actions: Array2<T>,
pub log_probs: Array1<T>,
pub rewards: Array1<T>,
pub values: Array1<T>,
pub dones: Array1<bool>,
pub advantages: Array1<T>,
pub returns: Array1<T>,
pub final_observation: Option<Array1<T>>,
}
impl<T: Float + Debug + Send + Sync + 'static + scirs2_core::numeric::FromPrimitive>
TrajectoryBatch<T>
{
pub fn new(
observations: Array2<T>,
actions: Array2<T>,
log_probs: Array1<T>,
rewards: Array1<T>,
values: Array1<T>,
dones: Array1<bool>,
) -> Result<Self> {
let batch_size = observations.nrows();
if actions.nrows() != batch_size
|| log_probs.len() != batch_size
|| rewards.len() != batch_size
|| values.len() != batch_size
|| dones.len() != batch_size
{
return Err(OptimError::InvalidConfig(
"Inconsistent batch dimensions".to_string(),
));
}
let advantages = Array1::zeros(batch_size);
let returns = Array1::zeros(batch_size);
Ok(Self {
observations,
actions,
log_probs,
rewards,
values,
dones,
advantages,
returns,
final_observation: None,
})
}
pub fn with_final_observation(mut self, final_observation: Array1<T>) -> Result<Self> {
let expected = self.observations.ncols();
if final_observation.len() != expected {
return Err(OptimError::DimensionMismatch(format!(
"final observation length ({}) does not match observation dimension ({})",
final_observation.len(),
expected
)));
}
self.final_observation = Some(final_observation);
Ok(self)
}
pub fn compute_gae(&mut self, gamma: T, lambda: T, nextvalue: T) -> Result<()> {
let batch_size = self.rewards.len();
if batch_size == 0 {
return Ok(());
}
let mut gae = T::zero();
for t in (0..batch_size).rev() {
let nonterminal = if self.dones[t] { T::zero() } else { T::one() };
let next_val = if t == batch_size - 1 {
nextvalue
} else {
self.values[t + 1]
};
let delta = self.rewards[t] + gamma * next_val * nonterminal - self.values[t];
gae = delta + gamma * lambda * nonterminal * gae;
self.advantages[t] = gae;
self.returns[t] = gae + self.values[t];
}
Ok(())
}
pub fn compute_advantages(&mut self, gamma: T, lambda: T, nextvalue: T) -> Result<()> {
self.compute_gae(gamma, lambda, nextvalue)?;
if self.advantages.len() < 2 {
return Ok(());
}
let mean = self.advantages.mean().unwrap_or(T::zero());
let std = self
.advantages
.mapv(|x| (x - mean) * (x - mean))
.mean()
.unwrap_or(T::one())
.sqrt();
if std > T::from(1e-8).unwrap_or_else(|| T::zero()) {
self.advantages.mapv_inplace(|x| (x - mean) / std);
}
Ok(())
}
pub fn compute_discounted_returns(&mut self, gamma: T, nextvalue: T) -> Result<()> {
let batch_size = self.rewards.len();
if batch_size == 0 {
return Ok(());
}
let mut running = nextvalue;
for t in (0..batch_size).rev() {
let nonterminal = if self.dones[t] { T::zero() } else { T::one() };
running = self.rewards[t] + gamma * nonterminal * running;
self.returns[t] = running;
self.advantages[t] = running - self.values[t];
}
Ok(())
}
pub fn get_mini_batches(&self, mini_batchsize: usize) -> Vec<TrajectoryBatch<T>> {
let batch_size = self.observations.nrows();
let n_mini_batches = batch_size.div_ceil(mini_batchsize);
let mut mini_batches = Vec::new();
for i in 0..n_mini_batches {
let start = i * mini_batchsize;
let end = ((i + 1) * mini_batchsize).min(batch_size);
if start >= end {
break;
}
let obs = self.observations.slice(s![start..end, ..]).to_owned();
let acts = self.actions.slice(s![start..end, ..]).to_owned();
let log_probs = self.log_probs.slice(s![start..end]).to_owned();
let rewards = self.rewards.slice(s![start..end]).to_owned();
let values = self.values.slice(s![start..end]).to_owned();
let dones = self.dones.slice(s![start..end]).to_owned().to_vec();
let advantages = self.advantages.slice(s![start..end]).to_owned();
let returns = self.returns.slice(s![start..end]).to_owned();
let dones_array = Array1::from_vec(dones);
let final_observation = if end < batch_size {
Some(self.observations.row(end).to_owned())
} else {
self.final_observation.clone()
};
let mini_batch = TrajectoryBatch {
observations: obs,
actions: acts,
log_probs,
rewards,
values,
dones: dones_array,
advantages,
returns,
final_observation,
};
mini_batches.push(mini_batch);
}
mini_batches
}
}
#[derive(Debug, Clone)]
pub struct KroneckerBlock<T: Float + Debug + Send + Sync + 'static> {
pub name: String,
pub inputs: Array2<T>,
pub outputs: Array2<T>,
}
pub fn parameter_count<T: Float + Debug + Send + Sync + 'static>(
params: &HashMap<String, Array1<T>>,
) -> usize {
params.values().map(|p| p.len()).sum()
}
pub fn parameter_keys<T: Float + Debug + Send + Sync + 'static>(
params: &HashMap<String, Array1<T>>,
) -> Vec<String> {
let mut keys: Vec<String> = params.keys().cloned().collect();
keys.sort();
keys
}
pub fn flatten_named<T: Float + Debug + Send + Sync + 'static>(
params: &HashMap<String, Array1<T>>,
) -> Array1<T> {
let mut flat = Array1::zeros(parameter_count(params));
let mut offset = 0usize;
for key in parameter_keys(params) {
let value = ¶ms[&key];
for (i, &v) in value.iter().enumerate() {
flat[offset + i] = v;
}
offset += value.len();
}
flat
}
pub fn unflatten_named<T: Float + Debug + Send + Sync + 'static>(
template: &HashMap<String, Array1<T>>,
flat: &Array1<T>,
) -> Result<HashMap<String, Array1<T>>> {
let total = parameter_count(template);
if total != flat.len() {
return Err(OptimError::DimensionMismatch(format!(
"Flat vector length ({}) does not match total parameter count ({})",
flat.len(),
total
)));
}
let keys = parameter_keys(template);
let mut out: HashMap<String, Array1<T>> = HashMap::with_capacity(keys.len());
let mut offset = 0usize;
for key in keys {
let len = template[&key].len();
let mut chunk = Array1::zeros(len);
for i in 0..len {
chunk[i] = flat[offset + i];
}
out.insert(key, chunk);
offset += len;
}
Ok(out)
}
pub fn clip_named_gradients<T: Float + Debug + Send + Sync + 'static>(
gradients: &HashMap<String, Array1<T>>,
max_norm: T,
) -> (HashMap<String, Array1<T>>, T) {
let mut total = T::zero();
for grad in gradients.values() {
for &g in grad.iter() {
total = total + g * g;
}
}
let norm = total.sqrt();
let factor = if max_norm > T::zero() && norm > max_norm && norm > T::zero() {
max_norm / norm
} else {
T::one()
};
let clipped = gradients
.iter()
.map(|(name, grad)| (name.clone(), grad.mapv(|g| g * factor)))
.collect();
(clipped, norm)
}
pub fn scale_named_gradients<T: Float + Debug + Send + Sync + 'static>(
gradients: &HashMap<String, Array1<T>>,
factor: T,
) -> HashMap<String, Array1<T>> {
gradients
.iter()
.map(|(name, grad)| (name.clone(), grad.mapv(|g| g * factor)))
.collect()
}
pub fn add_named_gradients<T: Float + Debug + Send + Sync + 'static>(
base: &mut HashMap<String, Array1<T>>,
addend: HashMap<String, Array1<T>>,
) -> Result<()> {
for (name, grad) in addend {
match base.get_mut(&name) {
Some(target) => {
if target.len() != grad.len() {
return Err(OptimError::DimensionMismatch(format!(
"gradient '{name}' has length {} in one term and {} in the other",
target.len(),
grad.len()
)));
}
for i in 0..target.len() {
target[i] = target[i] + grad[i];
}
}
None => {
base.insert(name, grad);
}
}
}
Ok(())
}
pub trait PolicyNetwork<T: Float + Debug + Send + Sync + 'static> {
fn evaluate_actions(
&self,
observations: &Array2<T>,
actions: &Array2<T>,
) -> Result<PolicyEvaluation<T>>;
fn get_action_distribution(&self, observations: &Array2<T>) -> Result<ActionDistribution<T>>;
fn update_parameters(&mut self, deltas: &HashMap<String, Array1<T>>) -> Result<()>;
fn get_parameters(&self) -> HashMap<String, Array1<T>>;
fn log_prob_gradient(
&self,
observations: &Array2<T>,
actions: &Array2<T>,
coefficients: &Array1<T>,
) -> Result<HashMap<String, Array1<T>>> {
let _ = (observations, actions, coefficients);
Err(OptimError::UnsupportedOperation(
"PolicyNetwork::log_prob_gradient is not implemented for this policy; \
policy-gradient updates require an analytic (or autodiff) score function"
.to_string(),
))
}
fn entropy_gradient(&self, observations: &Array2<T>) -> Result<HashMap<String, Array1<T>>> {
let _ = observations;
Err(OptimError::UnsupportedOperation(
"PolicyNetwork::entropy_gradient is not implemented for this policy; \
set entropy_coeff = 0 or provide an analytic entropy gradient"
.to_string(),
))
}
fn mean_action_gradient(
&self,
observations: &Array2<T>,
weights: &Array2<T>,
) -> Result<HashMap<String, Array1<T>>> {
let _ = (observations, weights);
Err(OptimError::UnsupportedOperation(
"PolicyNetwork::mean_action_gradient is not implemented for this policy; \
deterministic-policy-gradient updates (DDPG/TD3/SAC actor) require it"
.to_string(),
))
}
fn score_matrix(&self, observations: &Array2<T>, actions: &Array2<T>) -> Result<Array2<T>> {
let n = observations.nrows();
let dim = parameter_count(&self.get_parameters());
let mut scores = Array2::zeros((n, dim));
let one = Array1::from_elem(1, T::one());
for i in 0..n {
let obs_i = observations.slice(s![i..i + 1, ..]).to_owned();
let act_i = actions.slice(s![i..i + 1, ..]).to_owned();
let grad = self.log_prob_gradient(&obs_i, &act_i, &one)?;
let flat = flatten_named(&grad);
if flat.len() != dim {
return Err(OptimError::DimensionMismatch(format!(
"score vector length ({}) does not match parameter count ({})",
flat.len(),
dim
)));
}
for j in 0..dim {
scores[[i, j]] = flat[j];
}
}
Ok(scores)
}
fn kronecker_factors(
&self,
observations: &Array2<T>,
actions: &Array2<T>,
) -> Result<Vec<KroneckerBlock<T>>> {
let _ = (observations, actions);
Err(OptimError::UnsupportedOperation(
"PolicyNetwork::kronecker_factors is not implemented for this policy; \
Kronecker-factored Fisher estimation requires per-layer factors"
.to_string(),
))
}
}
impl<T: Float + Debug + Send + Sync + 'static, P: PolicyNetwork<T> + ?Sized> PolicyNetwork<T>
for &mut P
{
fn evaluate_actions(
&self,
observations: &Array2<T>,
actions: &Array2<T>,
) -> Result<PolicyEvaluation<T>> {
(**self).evaluate_actions(observations, actions)
}
fn get_action_distribution(&self, observations: &Array2<T>) -> Result<ActionDistribution<T>> {
(**self).get_action_distribution(observations)
}
fn update_parameters(&mut self, deltas: &HashMap<String, Array1<T>>) -> Result<()> {
(**self).update_parameters(deltas)
}
fn get_parameters(&self) -> HashMap<String, Array1<T>> {
(**self).get_parameters()
}
fn log_prob_gradient(
&self,
observations: &Array2<T>,
actions: &Array2<T>,
coefficients: &Array1<T>,
) -> Result<HashMap<String, Array1<T>>> {
(**self).log_prob_gradient(observations, actions, coefficients)
}
fn entropy_gradient(&self, observations: &Array2<T>) -> Result<HashMap<String, Array1<T>>> {
(**self).entropy_gradient(observations)
}
fn mean_action_gradient(
&self,
observations: &Array2<T>,
weights: &Array2<T>,
) -> Result<HashMap<String, Array1<T>>> {
(**self).mean_action_gradient(observations, weights)
}
fn score_matrix(&self, observations: &Array2<T>, actions: &Array2<T>) -> Result<Array2<T>> {
(**self).score_matrix(observations, actions)
}
fn kronecker_factors(
&self,
observations: &Array2<T>,
actions: &Array2<T>,
) -> Result<Vec<KroneckerBlock<T>>> {
(**self).kronecker_factors(observations, actions)
}
}
pub trait ValueNetwork<T: Float + Debug + Send + Sync + 'static> {
fn evaluate_value(&self, observations: &Array2<T>) -> Result<Array1<T>>;
fn update_parameters(&mut self, deltas: &HashMap<String, Array1<T>>) -> Result<()>;
fn get_parameters(&self) -> HashMap<String, Array1<T>>;
fn value_gradient(
&self,
observations: &Array2<T>,
residuals: &Array1<T>,
) -> Result<HashMap<String, Array1<T>>> {
let _ = (observations, residuals);
Err(OptimError::UnsupportedOperation(
"ValueNetwork::value_gradient is not implemented for this network; \
value-function updates require an analytic (or autodiff) gradient"
.to_string(),
))
}
}
pub trait QNetwork<T: Float + Debug + Send + Sync + 'static>: ValueNetwork<T> {
fn evaluate_q(&self, states: &Array2<T>, actions: &Array2<T>) -> Result<Array1<T>>;
fn q_gradient(
&self,
states: &Array2<T>,
actions: &Array2<T>,
residuals: &Array1<T>,
) -> Result<HashMap<String, Array1<T>>> {
let _ = (states, actions, residuals);
Err(OptimError::UnsupportedOperation(
"QNetwork::q_gradient is not implemented for this critic".to_string(),
))
}
fn action_gradient(&self, states: &Array2<T>, actions: &Array2<T>) -> Result<Array2<T>> {
let _ = (states, actions);
Err(OptimError::UnsupportedOperation(
"QNetwork::action_gradient is not implemented for this critic; \
the deterministic policy gradient requires ∇_a Q(s, a)"
.to_string(),
))
}
}
#[derive(Debug, Clone)]
pub struct PolicyEvaluation<T: Float + Debug + Send + Sync + 'static> {
pub log_probs: Array1<T>,
pub entropy: Array1<T>,
pub metrics: HashMap<String, T>,
}
#[derive(Debug, Clone)]
pub struct ActionDistribution<T: Float + Debug + Send + Sync + 'static> {
pub mean: Option<Array2<T>>,
pub std: Option<Array2<T>>,
pub logits: Option<Array2<T>>,
pub distribution_type: DistributionType,
}
#[derive(Debug, Clone, Copy)]
pub enum DistributionType {
Gaussian,
Categorical,
Beta,
Mixed,
}
#[derive(Debug, Clone)]
pub struct RLScheduler<T: Float + Debug + Send + Sync + 'static> {
pub initiallr: T,
pub current_lr: T,
pub decay_factor: T,
pub schedule: ScheduleType,
pub update_count: usize,
pub schedule_params: HashMap<String, T>,
}
#[derive(Debug, Clone, Copy)]
pub enum ScheduleType {
Constant,
Linear,
Exponential,
Cosine,
Step,
Adaptive,
}
impl<T: Float + Debug + Send + Sync + 'static> RLScheduler<T> {
pub fn new(initiallr: T, schedule: ScheduleType) -> Self {
Self {
initiallr,
current_lr: initiallr,
decay_factor: T::from(0.99).unwrap_or_else(|| T::zero()),
schedule,
update_count: 0,
schedule_params: HashMap::new(),
}
}
pub fn step(&mut self) -> T {
self.update_count += 1;
match self.schedule {
ScheduleType::Constant => {
}
ScheduleType::Linear => {
let decay_steps = self
.schedule_params
.get("decay_steps")
.copied()
.unwrap_or(T::from(10000).unwrap_or_else(|| T::zero()));
let progress =
T::from(self.update_count).unwrap_or_else(|| T::zero()) / decay_steps;
self.current_lr = self.initiallr * (T::one() - progress).max(T::zero());
}
ScheduleType::Exponential => {
self.current_lr = self.current_lr * self.decay_factor;
}
ScheduleType::Step => {
let step_size = self
.schedule_params
.get("step_size")
.copied()
.unwrap_or(T::from(1000).unwrap_or_else(|| T::zero()));
if T::from(self.update_count).unwrap_or_else(|| T::zero()) % step_size == T::zero()
{
self.current_lr = self.current_lr * self.decay_factor;
}
}
ScheduleType::Cosine => {
let max_steps = self
.schedule_params
.get("max_steps")
.copied()
.unwrap_or(T::from(10000).unwrap_or_else(|| T::zero()));
let progress = T::from(self.update_count).unwrap_or_else(|| T::zero()) / max_steps;
let pi = T::from(std::f64::consts::PI).unwrap_or_else(|| T::zero());
self.current_lr = self.initiallr * (T::one() + (pi * progress).cos())
/ T::from(2).unwrap_or_else(|| T::zero());
}
ScheduleType::Adaptive => {
}
}
self.current_lr
}
pub fn get_lr(&self) -> T {
self.current_lr
}
pub fn set_param(&mut self, key: &str, value: T) {
self.schedule_params.insert(key.to_string(), value);
}
}
#[derive(Debug, Clone)]
pub struct RLOptimizationMetrics<T: Float + Debug + Send + Sync + 'static> {
pub policy_loss: T,
pub value_loss: T,
pub entropy_loss: T,
pub total_loss: T,
pub kl_divergence: Option<T>,
pub explained_variance: T,
pub clip_fraction: Option<T>,
pub policy_lr: T,
pub value_lr: T,
pub policy_grad_norm: T,
pub value_grad_norm: T,
pub custom_metrics: HashMap<String, T>,
}
impl<T: Float + Debug + Send + Sync + 'static> Default for RLOptimizationMetrics<T> {
fn default() -> Self {
Self {
policy_loss: T::zero(),
value_loss: T::zero(),
entropy_loss: T::zero(),
total_loss: T::zero(),
kl_divergence: None,
explained_variance: T::zero(),
clip_fraction: None,
policy_lr: T::from(3e-4).unwrap_or_else(|| T::zero()),
value_lr: T::from(1e-3).unwrap_or_else(|| T::zero()),
policy_grad_norm: T::zero(),
value_grad_norm: T::zero(),
custom_metrics: HashMap::new(),
}
}
}
use scirs2_core::ndarray::s;