use scirs2_core::ndarray::{s, Array1, Array3};
use scirs2_core::numeric::Float;
use scirs2_core::random::Random;
use scirs2_core::RngExt;
use std::collections::{HashMap, VecDeque};
use std::fmt::Debug;
use crate::architecture::{ComponentPosition, ComponentType};
use crate::error::Result;
use crate::nas_engine::{OptimizerArchitecture, SearchResult, SearchSpaceConfig};
use crate::EvaluationMetric;
use super::{SearchStrategy, SearchStrategyStatistics};
pub struct DifferentiableSearch<T: Float + Debug + Send + Sync + 'static> {
architecture_weights: Array3<T>,
weight_optimizer: WeightOptimizer<T>,
temperature: T,
gumbel_softmax: bool,
continuous_relaxation: bool,
statistics: SearchStrategyStatistics<T>,
discretization_strategy: DiscretizationStrategy,
reward_baseline: T,
}
#[derive(Debug)]
pub struct WeightOptimizer<T: Float + Debug + Send + Sync + 'static> {
learning_rate: T,
momentum: T,
weight_decay: T,
velocity: Array3<T>,
}
#[derive(Debug, Clone, Copy)]
pub enum DiscretizationStrategy {
Greedy,
Sampling,
Progressive,
Threshold,
}
#[derive(Debug, Clone, Copy)]
pub enum TemperatureSchedule {
Constant,
Linear { initial: f64, final_temp: f64 },
Exponential { initial: f64, decay_rate: f64 },
Cosine { initial: f64, final_temp: f64 },
}
pub struct MemoryEfficientDARTS<T: Float + Debug + Send + Sync + 'static> {
architecture_weights: Array3<T>,
weight_optimizer: WeightOptimizer<T>,
temperature: T,
gumbel_softmax: bool,
partial_channel_ratio: T,
edge_normalization: bool,
edge_weights: Array1<T>,
discretization_strategy: DiscretizationStrategy,
statistics: SearchStrategyStatistics<T>,
reward_baseline: T,
}
pub struct RobustDARTS<T: Float + Debug + Send + Sync + 'static> {
architecture_weights: Array3<T>,
weight_optimizer: WeightOptimizer<T>,
temperature: T,
gumbel_softmax: bool,
perturbation_strength: T,
early_stopping_patience: usize,
regularization_weight: T,
discretization_strategy: DiscretizationStrategy,
statistics: SearchStrategyStatistics<T>,
eigenvalue_history: Vec<T>,
steps_without_improvement: usize,
best_validation_performance: T,
early_stopped: bool,
reward_baseline: T,
}
#[derive(Debug, Clone)]
pub struct DARTSConfig<T: Float + Debug + Send + Sync + 'static> {
pub num_operations: usize,
pub num_edges: usize,
pub initial_temperature: f64,
pub use_gumbel: bool,
pub temperature_schedule: TemperatureSchedule,
pub discretization_strategy: DiscretizationStrategy,
pub architecture_lr: f64,
pub weight_decay: f64,
pub momentum: f64,
_marker: std::marker::PhantomData<T>,
}
impl<
T: Float
+ Default
+ Clone
+ Send
+ Sync
+ std::fmt::Debug
+ scirs2_core::ndarray::ScalarOperand
+ std::iter::Sum,
> DifferentiableSearch<T>
{
pub fn new(
num_operations: usize,
num_edges: usize,
temperature: f64,
use_gumbel: bool,
) -> Self {
Self {
architecture_weights: Array3::zeros((num_edges, num_operations, 1)),
weight_optimizer: WeightOptimizer::new(
scirs2_core::numeric::NumCast::from(0.025).unwrap_or_else(|| T::zero()),
),
temperature: scirs2_core::numeric::NumCast::from(temperature)
.unwrap_or_else(|| T::zero()),
gumbel_softmax: use_gumbel,
continuous_relaxation: true,
statistics: SearchStrategyStatistics::default(),
discretization_strategy: DiscretizationStrategy::Progressive,
reward_baseline: T::zero(),
}
}
fn gumbel_softmax_sample(&self, logits: &Array1<T>) -> Array1<T> {
if !self.gumbel_softmax {
return softmax(logits);
}
let gumbel_noise: Array1<T> = Array1::from_shape_fn(logits.len(), |_| {
let u = scirs2_core::random::Random::default().random::<f64>();
T::from(gumbel_noise_from_uniform(u)).unwrap_or_else(|| T::zero())
});
let gumbel_logits = logits + &gumbel_noise;
let scaled_logits = gumbel_logits / self.temperature;
softmax(&scaled_logits)
}
}
impl<
T: Float
+ Default
+ Clone
+ Send
+ Sync
+ std::fmt::Debug
+ std::iter::Sum
+ scirs2_core::ndarray::ScalarOperand,
> SearchStrategy<T> for DifferentiableSearch<T>
{
fn initialize(&mut self, _searchspace: &SearchSpaceConfig) -> Result<()> {
self.architecture_weights =
Array3::from_shape_fn(self.architecture_weights.raw_dim(), |_| {
T::from(scirs2_core::random::Random::default().random::<f64>() * 0.1 - 0.05)
.unwrap_or_else(|| T::zero())
});
Ok(())
}
fn generate_architecture(
&mut self,
_search_space: &SearchSpaceConfig,
_history: &VecDeque<SearchResult<T>>,
) -> Result<OptimizerArchitecture<T>> {
if self.continuous_relaxation {
let mut sampled_weights = Array3::zeros(self.architecture_weights.raw_dim());
for edge_idx in 0..self.architecture_weights.dim().0 {
let edge_weights = self.architecture_weights.slice(s![edge_idx, .., 0]);
let sampled = self.gumbel_softmax_sample(&edge_weights.to_owned());
for (op_idx, &weight) in sampled.iter().enumerate() {
sampled_weights[[edge_idx, op_idx, 0]] = weight;
}
}
self.statistics.total_architectures_generated += 1;
Ok(discretize_architecture(
&sampled_weights,
&self.discretization_strategy,
))
} else {
self.statistics.total_architectures_generated += 1;
Ok(discretize_architecture(
&self.architecture_weights,
&self.discretization_strategy,
))
}
}
fn update_with_results(&mut self, results: &[SearchResult<T>]) -> Result<()> {
if results.is_empty() {
return Ok(());
}
let scored = collect_scored(results);
if scored.is_empty() {
return Ok(());
}
let performances: Vec<T> = scored.iter().map(|(_, p)| *p).collect();
self.statistics.best_performance = performances
.iter()
.cloned()
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(T::zero());
let sum: T = performances.iter().cloned().sum();
let count = T::from(performances.len()).unwrap_or_else(|| T::one());
let batch_mean = sum / count;
self.statistics.average_performance = batch_mean;
let baseline = self.reward_baseline;
let advantages: Vec<T> = performances.iter().map(|&p| p - baseline).collect();
let grad = reinforce_logit_gradient(&self.architecture_weights, &scored, &advantages);
self.weight_optimizer
.step(&mut self.architecture_weights, &grad);
let beta: T = scirs2_core::numeric::NumCast::from(0.1).unwrap_or_else(|| T::zero());
self.reward_baseline = self.reward_baseline * (T::one() - beta) + batch_mean * beta;
self.temperature = self.temperature
* scirs2_core::numeric::NumCast::from(0.999).unwrap_or_else(|| T::zero());
Ok(())
}
fn name(&self) -> &str {
"DifferentiableSearch"
}
fn get_statistics(&self) -> SearchStrategyStatistics<T> {
let mut stats = self.statistics.clone();
stats.exploration_rate = self.temperature;
stats.exploitation_rate = T::one() - self.temperature;
stats
}
}
impl<
T: Float
+ Default
+ Clone
+ Send
+ Sync
+ std::fmt::Debug
+ scirs2_core::ndarray::ScalarOperand
+ std::iter::Sum,
> MemoryEfficientDARTS<T>
{
pub fn new(
num_operations: usize,
num_edges: usize,
temperature: f64,
use_gumbel: bool,
partial_channel_ratio: f64,
edge_normalization: bool,
) -> Self {
let ratio = partial_channel_ratio.clamp(0.01, 1.0);
Self {
architecture_weights: Array3::zeros((num_edges, num_operations, 1)),
weight_optimizer: WeightOptimizer::new(
scirs2_core::numeric::NumCast::from(0.025).unwrap_or_else(|| T::zero()),
),
temperature: scirs2_core::numeric::NumCast::from(temperature)
.unwrap_or_else(|| T::zero()),
gumbel_softmax: use_gumbel,
partial_channel_ratio: scirs2_core::numeric::NumCast::from(ratio)
.unwrap_or_else(|| T::one()),
edge_normalization,
edge_weights: Array1::ones(num_edges),
discretization_strategy: DiscretizationStrategy::Progressive,
statistics: SearchStrategyStatistics::default(),
reward_baseline: T::zero(),
}
}
fn partial_channel_mask(&self, weights: &Array3<T>) -> Array3<T> {
let num_edges = weights.dim().0;
let num_ops = weights.dim().1;
let mut mask = Array3::zeros(weights.raw_dim());
let active_ops =
((num_ops as f64) * self.partial_channel_ratio.to_f64().unwrap_or(1.0)).ceil() as usize;
let active_ops = active_ops.max(1).min(num_ops);
for edge_idx in 0..num_edges {
let edge_slice = weights.slice(s![edge_idx, .., 0]);
let mut indexed: Vec<(usize, T)> = edge_slice.iter().cloned().enumerate().collect();
indexed.sort_by(|(_, a), (_, b)| {
b.abs()
.partial_cmp(&a.abs())
.unwrap_or(std::cmp::Ordering::Equal)
});
for (rank, (op_idx, _)) in indexed.iter().enumerate() {
if rank < active_ops {
mask[[edge_idx, *op_idx, 0]] = T::one();
}
}
}
mask
}
fn apply_partial_channels(&self, weights: &Array3<T>) -> Array3<T> {
let mask = self.partial_channel_mask(weights);
let mut masked = Array3::zeros(weights.raw_dim());
let (num_edges, num_ops, depth) = weights.dim();
for e in 0..num_edges {
for o in 0..num_ops {
for d in 0..depth {
masked[[e, o, d]] = mask[[e, o, d]] * weights[[e, o, d]];
}
}
}
masked
}
fn apply_edge_normalization(&self, weights: &Array3<T>) -> Array3<T> {
if !self.edge_normalization {
return weights.clone();
}
let num_edges = weights.dim().0;
let mut normalized = weights.clone();
let edge_probs = softmax(&self.edge_weights);
for edge_idx in 0..num_edges {
let edge_weight = edge_probs[edge_idx];
for op_idx in 0..weights.dim().1 {
normalized[[edge_idx, op_idx, 0]] = normalized[[edge_idx, op_idx, 0]] * edge_weight;
}
}
normalized
}
fn gumbel_softmax_sample(&self, logits: &Array1<T>) -> Array1<T> {
if !self.gumbel_softmax {
return softmax(logits);
}
let gumbel_noise: Array1<T> = Array1::from_shape_fn(logits.len(), |_| {
let u = scirs2_core::random::Random::default().random::<f64>();
T::from(gumbel_noise_from_uniform(u)).unwrap_or_else(|| T::zero())
});
let gumbel_logits = logits + &gumbel_noise;
let scaled_logits = gumbel_logits / self.temperature;
softmax(&scaled_logits)
}
}
impl<
T: Float
+ Default
+ Clone
+ Send
+ Sync
+ std::fmt::Debug
+ std::iter::Sum
+ scirs2_core::ndarray::ScalarOperand,
> SearchStrategy<T> for MemoryEfficientDARTS<T>
{
fn initialize(&mut self, _searchspace: &SearchSpaceConfig) -> Result<()> {
self.architecture_weights =
Array3::from_shape_fn(self.architecture_weights.raw_dim(), |_| {
T::from(scirs2_core::random::Random::default().random::<f64>() * 0.1 - 0.05)
.unwrap_or_else(|| T::zero())
});
self.edge_weights = Array1::ones(self.architecture_weights.dim().0);
Ok(())
}
fn generate_architecture(
&mut self,
_search_space: &SearchSpaceConfig,
_history: &VecDeque<SearchResult<T>>,
) -> Result<OptimizerArchitecture<T>> {
let partial_weights = self.apply_partial_channels(&self.architecture_weights);
let normalized_weights = self.apply_edge_normalization(&partial_weights);
let mut sampled_weights = Array3::zeros(normalized_weights.raw_dim());
for edge_idx in 0..normalized_weights.dim().0 {
let edge_weights = normalized_weights.slice(s![edge_idx, .., 0]);
let sampled = self.gumbel_softmax_sample(&edge_weights.to_owned());
for (op_idx, &weight) in sampled.iter().enumerate() {
sampled_weights[[edge_idx, op_idx, 0]] = weight;
}
}
self.statistics.total_architectures_generated += 1;
Ok(discretize_architecture(
&sampled_weights,
&self.discretization_strategy,
))
}
fn update_with_results(&mut self, results: &[SearchResult<T>]) -> Result<()> {
if results.is_empty() {
return Ok(());
}
let scored = collect_scored(results);
if scored.is_empty() {
return Ok(());
}
let performances: Vec<T> = scored.iter().map(|(_, p)| *p).collect();
self.statistics.best_performance = performances
.iter()
.cloned()
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(T::zero());
let sum: T = performances.iter().cloned().sum();
let count = T::from(performances.len()).unwrap_or_else(|| T::one());
let batch_mean = sum / count;
self.statistics.average_performance = batch_mean;
let baseline = self.reward_baseline;
let advantages: Vec<T> = performances.iter().map(|&p| p - baseline).collect();
let mask = self.partial_channel_mask(&self.architecture_weights);
let partial = self.apply_partial_channels(&self.architecture_weights);
let effective = self.apply_edge_normalization(&partial);
let grad_effective = reinforce_logit_gradient(&effective, &scored, &advantages);
let edge_probs = softmax(&self.edge_weights);
let (num_edges, num_ops, _depth) = self.architecture_weights.dim();
let mut grad_weights = Array3::zeros(self.architecture_weights.raw_dim());
for e in 0..num_edges {
let edge_scale = if self.edge_normalization {
edge_probs.get(e).copied().unwrap_or_else(T::one)
} else {
T::one()
};
for o in 0..num_ops {
grad_weights[[e, o, 0]] = grad_effective[[e, o, 0]] * edge_scale * mask[[e, o, 0]];
}
}
self.weight_optimizer
.step(&mut self.architecture_weights, &grad_weights);
if self.edge_normalization && num_edges > 0 {
let edge_scores: Vec<T> = (0..num_edges)
.map(|e| {
(0..num_ops)
.map(|o| grad_effective[[e, o, 0]] * partial[[e, o, 0]])
.fold(T::zero(), |acc, v| acc + v)
})
.collect();
let mean_score = edge_scores
.iter()
.enumerate()
.fold(T::zero(), |acc, (e, &s)| {
acc + s * edge_probs.get(e).copied().unwrap_or_else(T::zero)
});
let edge_lr: T = scirs2_core::numeric::NumCast::from(0.01).unwrap_or_else(|| T::zero());
let updatable = num_edges.min(self.edge_weights.len());
for (e, &score) in edge_scores.iter().enumerate().take(updatable) {
let beta = edge_probs.get(e).copied().unwrap_or_else(T::zero);
self.edge_weights[e] = self.edge_weights[e] + edge_lr * beta * (score - mean_score);
}
}
let beta: T = scirs2_core::numeric::NumCast::from(0.1).unwrap_or_else(|| T::zero());
self.reward_baseline = self.reward_baseline * (T::one() - beta) + batch_mean * beta;
self.temperature = self.temperature
* scirs2_core::numeric::NumCast::from(0.999).unwrap_or_else(|| T::zero());
Ok(())
}
fn name(&self) -> &str {
"MemoryEfficientDARTS"
}
fn get_statistics(&self) -> SearchStrategyStatistics<T> {
let mut stats = self.statistics.clone();
stats.exploration_rate = self.temperature;
stats.exploitation_rate = T::one() - self.temperature;
stats
}
}
impl<
T: Float
+ Default
+ Clone
+ Send
+ Sync
+ std::fmt::Debug
+ scirs2_core::ndarray::ScalarOperand
+ std::iter::Sum,
> RobustDARTS<T>
{
pub fn new(
num_operations: usize,
num_edges: usize,
temperature: f64,
use_gumbel: bool,
perturbation_strength: f64,
early_stopping_patience: usize,
regularization_weight: f64,
) -> Self {
Self {
architecture_weights: Array3::zeros((num_edges, num_operations, 1)),
weight_optimizer: WeightOptimizer::new(
scirs2_core::numeric::NumCast::from(0.025).unwrap_or_else(|| T::zero()),
),
temperature: scirs2_core::numeric::NumCast::from(temperature)
.unwrap_or_else(|| T::zero()),
gumbel_softmax: use_gumbel,
perturbation_strength: scirs2_core::numeric::NumCast::from(perturbation_strength)
.unwrap_or_else(|| T::zero()),
early_stopping_patience,
regularization_weight: scirs2_core::numeric::NumCast::from(regularization_weight)
.unwrap_or_else(|| T::zero()),
discretization_strategy: DiscretizationStrategy::Greedy,
statistics: SearchStrategyStatistics::default(),
eigenvalue_history: Vec::new(),
steps_without_improvement: 0,
best_validation_performance: T::neg_infinity(),
early_stopped: false,
reward_baseline: T::zero(),
}
}
fn compute_perturbation_loss(&self, weights: &Array3<T>) -> T {
let perturbation = Array3::from_shape_fn(weights.raw_dim(), |_| {
let noise = Random::default().random::<f64>();
self.perturbation_strength * T::from(noise * 2.0 - 1.0).unwrap_or_else(|| T::zero())
});
let perturbed_weights = weights + &perturbation;
let mut total_kl = T::zero();
for edge_idx in 0..weights.dim().0 {
let original = softmax(&weights.slice(s![edge_idx, .., 0]).to_owned());
let perturbed = softmax(&perturbed_weights.slice(s![edge_idx, .., 0]).to_owned());
for (p, q) in original.iter().zip(perturbed.iter()) {
let epsilon: T =
scirs2_core::numeric::NumCast::from(1e-10).unwrap_or_else(|| T::zero());
if *p > epsilon && *q > epsilon {
total_kl = total_kl + *p * (*p / *q).ln();
}
}
}
total_kl
}
fn estimate_dominant_eigenvalue(&self) -> T {
let num_elements = self.architecture_weights.len();
if num_elements == 0 {
return T::zero();
}
let mean =
self.architecture_weights.sum() / T::from(num_elements).unwrap_or_else(|| T::one());
let variance = self
.architecture_weights
.mapv(|x| (x - mean) * (x - mean))
.sum()
/ T::from(num_elements).unwrap_or_else(|| T::one());
variance.sqrt()
}
fn should_early_stop(&self) -> bool {
self.early_stopped || self.steps_without_improvement >= self.early_stopping_patience
}
fn gumbel_softmax_sample(&self, logits: &Array1<T>) -> Array1<T> {
if !self.gumbel_softmax {
return softmax(logits);
}
let gumbel_noise: Array1<T> = Array1::from_shape_fn(logits.len(), |_| {
let u = scirs2_core::random::Random::default().random::<f64>();
T::from(gumbel_noise_from_uniform(u)).unwrap_or_else(|| T::zero())
});
let gumbel_logits = logits + &gumbel_noise;
let scaled_logits = gumbel_logits / self.temperature;
softmax(&scaled_logits)
}
}
impl<
T: Float
+ Default
+ Clone
+ Send
+ Sync
+ std::fmt::Debug
+ std::iter::Sum
+ scirs2_core::ndarray::ScalarOperand,
> SearchStrategy<T> for RobustDARTS<T>
{
fn initialize(&mut self, _searchspace: &SearchSpaceConfig) -> Result<()> {
self.architecture_weights =
Array3::from_shape_fn(self.architecture_weights.raw_dim(), |_| {
T::from(scirs2_core::random::Random::default().random::<f64>() * 0.1 - 0.05)
.unwrap_or_else(|| T::zero())
});
self.eigenvalue_history.clear();
self.steps_without_improvement = 0;
self.best_validation_performance = T::neg_infinity();
self.early_stopped = false;
Ok(())
}
fn generate_architecture(
&mut self,
_search_space: &SearchSpaceConfig,
_history: &VecDeque<SearchResult<T>>,
) -> Result<OptimizerArchitecture<T>> {
if self.should_early_stop() {
self.statistics.total_architectures_generated += 1;
return Ok(discretize_architecture(
&self.architecture_weights,
&DiscretizationStrategy::Greedy,
));
}
let mut sampled_weights = Array3::zeros(self.architecture_weights.raw_dim());
for edge_idx in 0..self.architecture_weights.dim().0 {
let edge_weights = self.architecture_weights.slice(s![edge_idx, .., 0]);
let sampled = self.gumbel_softmax_sample(&edge_weights.to_owned());
for (op_idx, &weight) in sampled.iter().enumerate() {
sampled_weights[[edge_idx, op_idx, 0]] = weight;
}
}
self.statistics.total_architectures_generated += 1;
Ok(discretize_architecture(
&sampled_weights,
&self.discretization_strategy,
))
}
fn update_with_results(&mut self, results: &[SearchResult<T>]) -> Result<()> {
if results.is_empty() || self.early_stopped {
return Ok(());
}
let scored = collect_scored(results);
let performances: Vec<T> = scored.iter().map(|(_, p)| *p).collect();
if !performances.is_empty() {
let current_best = performances
.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.cloned()
.unwrap_or(T::zero());
self.statistics.best_performance = current_best;
let sum: T = performances.iter().cloned().sum();
let count = T::from(performances.len()).unwrap_or_else(|| T::one());
let batch_mean = sum / count;
self.statistics.average_performance = batch_mean;
if current_best > self.best_validation_performance {
self.best_validation_performance = current_best;
self.steps_without_improvement = 0;
} else {
self.steps_without_improvement += 1;
}
let eigenvalue = self.estimate_dominant_eigenvalue();
self.eigenvalue_history.push(eigenvalue);
if self.eigenvalue_history.len() >= 10 {
let recent_len = self.eigenvalue_history.len();
let recent_eigenvalue = self.eigenvalue_history[recent_len - 1];
let earlier_eigenvalue = self.eigenvalue_history[recent_len - 10];
let growth_ratio = if earlier_eigenvalue
> scirs2_core::numeric::NumCast::from(1e-10).unwrap_or_else(|| T::zero())
{
recent_eigenvalue / earlier_eigenvalue
} else {
T::one()
};
let collapse_threshold: T =
scirs2_core::numeric::NumCast::from(5.0).unwrap_or_else(|| T::zero());
if growth_ratio > collapse_threshold {
self.early_stopped = true;
return Ok(());
}
}
if self.should_early_stop() {
self.early_stopped = true;
return Ok(());
}
let perturbation_loss = self.compute_perturbation_loss(&self.architecture_weights);
let penalty = self.regularization_weight * perturbation_loss;
let baseline = self.reward_baseline;
let advantages: Vec<T> = performances
.iter()
.map(|&p| p - baseline - penalty)
.collect();
let grad = reinforce_logit_gradient(&self.architecture_weights, &scored, &advantages);
self.weight_optimizer
.step(&mut self.architecture_weights, &grad);
let beta: T = scirs2_core::numeric::NumCast::from(0.1).unwrap_or_else(|| T::zero());
self.reward_baseline = self.reward_baseline * (T::one() - beta) + batch_mean * beta;
self.temperature = self.temperature
* scirs2_core::numeric::NumCast::from(0.999).unwrap_or_else(|| T::zero());
}
Ok(())
}
fn name(&self) -> &str {
"RobustDARTS"
}
fn get_statistics(&self) -> SearchStrategyStatistics<T> {
let mut stats = self.statistics.clone();
stats.exploration_rate = self.temperature;
stats.exploitation_rate = T::one() - self.temperature;
stats
}
}
impl<T: Float + Debug + Default + Send + Sync + 'static> Default for DARTSConfig<T> {
fn default() -> Self {
Self {
num_operations: 4,
num_edges: 8,
initial_temperature: 1.0,
use_gumbel: true,
temperature_schedule: TemperatureSchedule::Exponential {
initial: 1.0,
decay_rate: 0.999,
},
discretization_strategy: DiscretizationStrategy::Progressive,
architecture_lr: 0.025,
weight_decay: 1e-4,
momentum: 0.9,
_marker: std::marker::PhantomData,
}
}
}
impl<
T: Float
+ Debug
+ Default
+ Clone
+ Send
+ Sync
+ 'static
+ scirs2_core::ndarray::ScalarOperand
+ std::iter::Sum,
> DARTSConfig<T>
{
pub fn new() -> Self {
Self::default()
}
pub fn num_operations(mut self, n: usize) -> Self {
self.num_operations = n;
self
}
pub fn num_edges(mut self, n: usize) -> Self {
self.num_edges = n;
self
}
pub fn temperature(mut self, t: f64) -> Self {
self.initial_temperature = t;
self
}
pub fn gumbel_softmax(mut self, use_gumbel: bool) -> Self {
self.use_gumbel = use_gumbel;
self
}
pub fn temperature_schedule(mut self, schedule: TemperatureSchedule) -> Self {
self.temperature_schedule = schedule;
self
}
pub fn discretization_strategy(mut self, strategy: DiscretizationStrategy) -> Self {
self.discretization_strategy = strategy;
self
}
pub fn architecture_lr(mut self, lr: f64) -> Self {
self.architecture_lr = lr;
self
}
pub fn weight_decay(mut self, wd: f64) -> Self {
self.weight_decay = wd;
self
}
pub fn momentum(mut self, m: f64) -> Self {
self.momentum = m;
self
}
pub fn build(self) -> DifferentiableSearch<T> {
DifferentiableSearch::new(
self.num_operations,
self.num_edges,
self.initial_temperature,
self.use_gumbel,
)
}
pub fn build_memory_efficient(
self,
partial_channel_ratio: f64,
edge_normalization: bool,
) -> MemoryEfficientDARTS<T> {
MemoryEfficientDARTS::new(
self.num_operations,
self.num_edges,
self.initial_temperature,
self.use_gumbel,
partial_channel_ratio,
edge_normalization,
)
}
pub fn build_robust(
self,
perturbation_strength: f64,
early_stopping_patience: usize,
regularization_weight: f64,
) -> RobustDARTS<T> {
RobustDARTS::new(
self.num_operations,
self.num_edges,
self.initial_temperature,
self.use_gumbel,
perturbation_strength,
early_stopping_patience,
regularization_weight,
)
}
}
impl<T: Float + Debug + Default + Send + Sync> WeightOptimizer<T> {
fn new(learningrate: T) -> Self {
Self {
learning_rate: learningrate,
momentum: scirs2_core::numeric::NumCast::from(0.9).unwrap_or_else(|| T::zero()),
weight_decay: scirs2_core::numeric::NumCast::from(1e-4).unwrap_or_else(|| T::zero()),
velocity: Array3::zeros((0, 0, 0)),
}
}
fn step(&mut self, weights: &mut Array3<T>, grad: &Array3<T>) {
if self.velocity.raw_dim() != weights.raw_dim() {
self.velocity = Array3::zeros(weights.raw_dim());
}
let (edges, ops, depth) = weights.dim();
let lr = self.learning_rate;
let momentum = self.momentum;
let weight_decay = self.weight_decay;
for e in 0..edges {
for o in 0..ops {
for d in 0..depth {
let g = grad[[e, o, d]];
let v = momentum * self.velocity[[e, o, d]] + g;
self.velocity[[e, o, d]] = v;
let w = weights[[e, o, d]];
weights[[e, o, d]] = w - lr * weight_decay * w + lr * v;
}
}
}
}
}
fn softmax<T: Float + Debug + Send + Sync + 'static + scirs2_core::ndarray::ScalarOperand>(
x: &Array1<T>,
) -> Array1<T> {
let max_val = x
.iter()
.cloned()
.fold(T::neg_infinity(), |a, b| if a > b { a } else { b });
let exp_x = x.mapv(|xi| (xi - max_val).exp());
let sum_exp = exp_x.sum();
exp_x / sum_exp
}
fn gumbel_noise_from_uniform(u: f64) -> f64 {
let u = u.clamp(1e-12, 1.0 - 1e-12);
-(-u.ln()).ln()
}
const DARTS_OP_INDEX_PREFIX: &str = "darts_op_index_";
const DARTS_NUM_OPS_KEY: &str = "darts_num_ops";
fn op_index_to_component_type(idx: usize) -> ComponentType {
match idx {
0 => ComponentType::SGD,
1 => ComponentType::Adam,
2 => ComponentType::AdaGrad,
3 => ComponentType::RMSprop,
4 => ComponentType::AdamW,
5 => ComponentType::LAMB,
6 => ComponentType::LARS,
7 => ComponentType::Lion,
8 => ComponentType::RAdam,
9 => ComponentType::Lookahead,
10 => ComponentType::SAM,
11 => ComponentType::LBFGS,
12 => ComponentType::SparseAdam,
13 => ComponentType::GroupedAdam,
14 => ComponentType::MAML,
15 => ComponentType::Reptile,
16 => ComponentType::MetaSGD,
17 => ComponentType::ConstantLR,
18 => ComponentType::ExponentialLR,
19 => ComponentType::StepLR,
20 => ComponentType::CosineAnnealingLR,
21 => ComponentType::OneCycleLR,
22 => ComponentType::CyclicLR,
23 => ComponentType::L1Regularizer,
24 => ComponentType::L2Regularizer,
25 => ComponentType::ElasticNetRegularizer,
26 => ComponentType::DropoutRegularizer,
27 => ComponentType::GradientClipping,
28 => ComponentType::WeightDecay,
29 => ComponentType::AdaptiveLR,
30 => ComponentType::AdaptiveMomentum,
31 => ComponentType::AdaptiveRegularization,
32 => ComponentType::LSTMOptimizer,
33 => ComponentType::TransformerOptimizer,
34 => ComponentType::AttentionOptimizer,
35 => ComponentType::AdaDelta,
36 => ComponentType::Momentum,
37 => ComponentType::Nesterov,
38 => ComponentType::LRScheduler,
39 => ComponentType::BatchNorm,
40 => ComponentType::Dropout,
_ => ComponentType::Custom,
}
}
fn collect_scored<T: Float + Debug + Send + Sync + 'static>(
results: &[SearchResult<T>],
) -> Vec<(&SearchResult<T>, T)> {
results
.iter()
.filter_map(|r| {
r.evaluation_results
.metric_scores
.get(&EvaluationMetric::FinalPerformance)
.map(|p| (r, *p))
})
.collect()
}
fn argmax_index<T: Float>(v: &Array1<T>) -> usize {
v.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx)
.unwrap_or(0)
}
fn recover_chosen_op<T: Float + Debug + Send + Sync + 'static>(
result: &SearchResult<T>,
edge: usize,
num_ops: usize,
) -> Option<usize> {
let architecture = &result.architecture;
let comp = architecture.components.get(edge)?;
let encoded_num_ops = architecture
.metadata
.get(DARTS_NUM_OPS_KEY)
.and_then(|value| value.parse::<usize>().ok());
if encoded_num_ops.is_none_or(|encoded| encoded == num_ops) {
let encoded_index = architecture
.metadata
.get(&format!("{}{}", DARTS_OP_INDEX_PREFIX, edge))
.and_then(|value| value.parse::<usize>().ok())
.filter(|&idx| idx < num_ops)
.filter(|&idx| &op_index_to_component_type(idx).to_string() == comp);
if let Some(idx) = encoded_index {
return Some(idx);
}
}
let mut matches =
(0..num_ops).filter(|&op| &op_index_to_component_type(op).to_string() == comp);
let first = matches.next()?;
if matches.next().is_some() {
return None;
}
Some(first)
}
fn reinforce_logit_gradient<T>(
weights: &Array3<T>,
scored: &[(&SearchResult<T>, T)],
advantages: &[T],
) -> Array3<T>
where
T: Float + Debug + Send + Sync + 'static + scirs2_core::ndarray::ScalarOperand,
{
let mut grad = Array3::zeros(weights.raw_dim());
let (num_edges, num_ops, _depth) = weights.dim();
if num_edges == 0 || num_ops == 0 || scored.is_empty() {
return grad;
}
let probs: Vec<Array1<T>> = (0..num_edges)
.map(|e| softmax(&weights.slice(s![e, .., 0]).to_owned()))
.collect();
let count = T::from(scored.len()).unwrap_or_else(|| T::one());
for (i, (result, _)) in scored.iter().enumerate() {
let advantage = advantages.get(i).copied().unwrap_or_else(|| T::zero());
for (e, p) in probs.iter().enumerate() {
let chosen = recover_chosen_op(result, e, num_ops).unwrap_or_else(|| argmax_index(p));
for o in 0..num_ops {
let indicator = if o == chosen { T::one() } else { T::zero() };
grad[[e, o, 0]] = grad[[e, o, 0]] + advantage * (indicator - p[o]) / count;
}
}
}
grad
}
fn discretize_architecture<
T: Float + Debug + Default + Send + Sync + 'static + scirs2_core::ndarray::ScalarOperand,
>(
weights: &Array3<T>,
strategy: &DiscretizationStrategy,
) -> OptimizerArchitecture<T> {
use crate::architecture::OptimizerComponent;
let num_ops = weights.dim().1;
let mut components = Vec::new();
let mut selected_ops: Vec<usize> = Vec::with_capacity(weights.dim().0);
for edge_idx in 0..weights.dim().0 {
let edge_weights = weights.slice(s![edge_idx, .., 0]);
let selected_op_idx = match strategy {
DiscretizationStrategy::Greedy => edge_weights
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx)
.unwrap_or(0),
DiscretizationStrategy::Sampling => {
let probs = softmax(&edge_weights.to_owned());
let rand_val = scirs2_core::random::Random::default().random::<f64>();
let mut cumsum = 0.0;
let mut selected_idx = 0;
for (idx, prob) in probs.iter().enumerate() {
cumsum += prob.to_f64().unwrap_or(0.0);
if cumsum >= rand_val {
selected_idx = idx;
break;
}
}
selected_idx
}
DiscretizationStrategy::Threshold => {
let threshold: T =
scirs2_core::numeric::NumCast::from(0.5).unwrap_or_else(|| T::zero());
edge_weights
.iter()
.enumerate()
.find(|(_, &weight)| weight > threshold)
.map(|(idx, _)| idx)
.unwrap_or(0)
}
DiscretizationStrategy::Progressive => {
edge_weights
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx)
.unwrap_or(0)
}
};
let component_type = op_index_to_component_type(selected_op_idx);
selected_ops.push(selected_op_idx);
let mut hyperparameters = HashMap::new();
hyperparameters.insert("_learningrate".to_string(), 0.001f64);
components.push(OptimizerComponent {
id: format!("comp_{}", edge_idx),
component_type,
hyperparameters,
enabled: true,
position: ComponentPosition {
layer: 0,
index: edge_idx as u32,
x: 0.0,
y: 0.0,
},
});
}
OptimizerArchitecture {
components: components
.iter()
.map(|c| c.component_type.to_string())
.collect(),
parameters: components
.iter()
.enumerate()
.flat_map(|(i, c)| {
c.hyperparameters.iter().map(move |(k, v)| {
(
format!("{}_{}", i, k),
scirs2_core::numeric::NumCast::from(*v).unwrap_or_else(|| T::zero()),
)
})
})
.collect(),
hyperparameters: components
.iter()
.enumerate()
.flat_map(|(i, c)| {
c.hyperparameters.iter().map(move |(k, v)| {
(
format!("{}_{}", i, k),
scirs2_core::numeric::NumCast::from(*v).unwrap_or_else(|| T::zero()),
)
})
})
.collect(),
connections: Vec::new(),
metadata: {
let mut metadata = HashMap::with_capacity(selected_ops.len() + 1);
metadata.insert(DARTS_NUM_OPS_KEY.to_string(), num_ops.to_string());
for (edge_idx, op_idx) in selected_ops.iter().enumerate() {
metadata.insert(
format!("{}{}", DARTS_OP_INDEX_PREFIX, edge_idx),
op_idx.to_string(),
);
}
metadata
},
architecture_id: format!("arch_{}", Random::default().random::<u64>()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nas_engine::results::{
ArchitectureEncoding, EvaluationResults, ResourceUsage, SearchResultMetadata,
};
use std::time::Duration;
#[test]
fn progressive_discretization_picks_the_most_positive_raw_logit() {
let mut weights = Array3::<f64>::zeros((1, 3, 1));
weights[[0, 0, 0]] = -3.0;
weights[[0, 1, 0]] = -0.5;
weights[[0, 2, 0]] = -5.0;
let progressive = discretize_architecture(&weights, &DiscretizationStrategy::Progressive);
let greedy = discretize_architecture(&weights, &DiscretizationStrategy::Greedy);
let progressive_idx = recorded_op_index(&progressive, 0).expect("op index recorded");
let greedy_idx = recorded_op_index(&greedy, 0).expect("op index recorded");
assert_eq!(
progressive_idx, 1,
"Progressive must choose the largest logit (index 1); squaring the \
signed logits selects index 2, the most negative one"
);
assert_eq!(
progressive_idx, greedy_idx,
"argmax is invariant to monotone sharpening, so Progressive and \
Greedy must agree"
);
assert_ne!(progressive_idx, 2, "the most negative logit must never win");
}
#[test]
fn progressive_discretization_agrees_with_greedy_on_every_edge() {
let mut weights = Array3::<f64>::zeros((4, 5, 1));
let table = [
[-1.0, -2.0, 0.5, -9.0, -0.25],
[3.0, 0.1, -7.0, 2.9, -0.5],
[-0.2, -0.1, -0.3, -0.4, -0.5],
[0.05, 0.10, 0.60, 0.20, 0.05],
];
for (edge, row) in table.iter().enumerate() {
for (op, value) in row.iter().enumerate() {
weights[[edge, op, 0]] = *value;
}
}
let progressive = discretize_architecture(&weights, &DiscretizationStrategy::Progressive);
for (edge, row) in table.iter().enumerate() {
let expected = row
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx)
.expect("non-empty row");
let selected = recorded_op_index(&progressive, edge).expect("op index recorded");
assert_eq!(
selected, expected,
"edge {edge}: expected op {expected}, got {selected}"
);
}
}
fn recorded_op_index(architecture: &OptimizerArchitecture<f64>, edge: usize) -> Option<usize> {
architecture
.metadata
.get(&format!("darts_op_index_{}", edge))
.and_then(|raw| raw.parse::<usize>().ok())
}
fn search_result_for(
architecture: OptimizerArchitecture<f64>,
performance: f64,
) -> SearchResult<f64> {
let mut metric_scores = HashMap::new();
metric_scores.insert(EvaluationMetric::FinalPerformance, performance);
SearchResult {
architecture,
evaluation_results: EvaluationResults {
metric_scores,
overall_score: performance,
confidence_intervals: HashMap::new(),
evaluation_time: Duration::from_secs(0),
success: true,
error_message: None,
cv_results: None,
benchmark_results: HashMap::new(),
training_trajectory: Vec::new(),
},
generation: 0,
search_time: 0.0,
resource_usage: ResourceUsage::default(),
encoding: ArchitectureEncoding::default(),
metadata: SearchResultMetadata::default(),
}
}
#[test]
fn test_darts_creation() {
let search = DifferentiableSearch::<f64>::new(4, 8, 1.0, true);
assert_eq!(search.name(), "DifferentiableSearch");
}
#[test]
fn test_memory_efficient_darts_creation() {
let search = MemoryEfficientDARTS::<f64>::new(4, 8, 1.0, true, 0.25, true);
assert_eq!(search.name(), "MemoryEfficientDARTS");
}
#[test]
fn test_robust_darts_creation() {
let search = RobustDARTS::<f64>::new(4, 8, 1.0, true, 0.1, 20, 0.01);
assert_eq!(search.name(), "RobustDARTS");
assert!(!search.should_early_stop());
}
#[test]
fn test_darts_config_builder() {
let config = DARTSConfig::<f64>::new()
.num_operations(6)
.num_edges(12)
.temperature(0.5)
.gumbel_softmax(false)
.discretization_strategy(DiscretizationStrategy::Greedy);
assert_eq!(config.num_operations, 6);
assert_eq!(config.num_edges, 12);
let search = config.build();
assert_eq!(search.name(), "DifferentiableSearch");
}
#[test]
fn test_darts_config_build_variants() {
let config = DARTSConfig::<f64>::new();
let _me = config.clone().build_memory_efficient(0.25, true);
let _robust = config.build_robust(0.1, 20, 0.01);
}
#[test]
fn test_softmax() {
let logits = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let probs = softmax(&logits);
let sum: f64 = probs.sum();
assert!((sum - 1.0).abs() < 1e-6);
assert!(probs[2] > probs[1]);
assert!(probs[1] > probs[0]);
}
#[test]
fn test_op_index_component_type_round_trip_is_a_bijection() {
use crate::search_strategies::component_type_to_u8;
let mut seen = std::collections::HashSet::new();
for idx in 0..=40usize {
let component = op_index_to_component_type(idx);
assert_eq!(
component_type_to_u8(&component) as usize,
idx,
"op index {idx} does not round-trip through the canonical table"
);
assert!(
seen.insert(component.to_string()),
"op index {idx} collides with an earlier operation name"
);
}
assert_eq!(op_index_to_component_type(41), ComponentType::Custom);
}
#[test]
fn test_reinforce_credit_lands_on_chosen_op_with_six_operations() {
const NUM_OPS: usize = 6;
const CHOSEN: usize = 5;
let logits: Array3<f64> = Array3::zeros((1, NUM_OPS, 1));
let mut sampled: Array3<f64> = Array3::zeros((1, NUM_OPS, 1));
for op in 0..NUM_OPS {
sampled[[0, op, 0]] = 0.01 * op as f64;
}
let architecture = discretize_architecture(&sampled, &DiscretizationStrategy::Greedy);
assert_eq!(
architecture.metadata.get("darts_op_index_0"),
Some(&CHOSEN.to_string()),
"discretization must record the raw operation index"
);
let result = search_result_for(architecture, 1.0);
assert_eq!(
recover_chosen_op(&result, 0, NUM_OPS),
Some(CHOSEN),
"the chosen op must be recovered exactly, not via the lossy name map"
);
let scored = vec![(&result, 1.0f64)];
let grad = reinforce_logit_gradient(&logits, &scored, &[1.0]);
let credited = argmax_index(&grad.slice(s![0, .., 0]).to_owned());
assert_eq!(credited, CHOSEN, "credit must land on the chosen operation");
assert!(
grad[[0, CHOSEN, 0]] > 0.0,
"the chosen op must receive positive credit"
);
assert!(
grad[[0, 1, 0]] < 0.0,
"op 1 must lose probability mass, not gain it"
);
let column_sum: f64 = (0..NUM_OPS).map(|o| grad[[0, o, 0]]).sum();
assert!(column_sum.abs() < 1e-12);
}
#[test]
fn test_recover_chosen_op_name_fallback_is_exact_for_four_operations() {
let mut architecture: OptimizerArchitecture<f64> =
discretize_architecture(&Array3::zeros((1, 4, 1)), &DiscretizationStrategy::Greedy);
architecture.components = vec!["AdaGrad".to_string()];
architecture.metadata.clear();
let result = search_result_for(architecture, 1.0);
assert_eq!(recover_chosen_op(&result, 0, 4), Some(2));
}
#[test]
fn test_recover_chosen_op_declines_when_name_is_ambiguous() {
let mut architecture: OptimizerArchitecture<f64> =
discretize_architecture(&Array3::zeros((1, 50, 1)), &DiscretizationStrategy::Greedy);
architecture.components = vec!["Custom".to_string()];
architecture.metadata.clear();
let result = search_result_for(architecture, 1.0);
assert_eq!(recover_chosen_op(&result, 0, 50), None);
let mut logits: Array3<f64> = Array3::zeros((1, 50, 1));
logits[[0, 7, 0]] = 5.0;
let scored = vec![(&result, 1.0f64)];
let grad = reinforce_logit_gradient(&logits, &scored, &[1.0]);
assert_eq!(argmax_index(&grad.slice(s![0, .., 0]).to_owned()), 7);
}
#[test]
fn test_recover_chosen_op_rejects_stale_encoding() {
let mut architecture: OptimizerArchitecture<f64> =
discretize_architecture(&Array3::zeros((1, 6, 1)), &DiscretizationStrategy::Greedy);
architecture
.metadata
.insert("darts_op_index_0".to_string(), "4".to_string());
architecture.components = vec!["Lion".to_string()];
let result = search_result_for(architecture, 1.0);
assert_eq!(recover_chosen_op(&result, 0, 6), None);
assert_eq!(recover_chosen_op(&result, 0, 8), Some(7));
}
#[test]
fn test_recover_chosen_op_rejects_out_of_range_encoding() {
let mut architecture: OptimizerArchitecture<f64> =
discretize_architecture(&Array3::zeros((1, 6, 1)), &DiscretizationStrategy::Greedy);
architecture
.metadata
.insert("darts_op_index_0".to_string(), "99".to_string());
architecture.components = vec!["SGD".to_string()];
let result = search_result_for(architecture, 1.0);
assert_eq!(recover_chosen_op(&result, 0, 6), Some(0));
}
#[test]
fn test_recover_chosen_op_ignores_encoding_from_another_search_space() {
let mut architecture: OptimizerArchitecture<f64> =
discretize_architecture(&Array3::zeros((1, 6, 1)), &DiscretizationStrategy::Greedy);
architecture
.metadata
.insert("darts_num_ops".to_string(), "12".to_string());
architecture
.metadata
.insert("darts_op_index_0".to_string(), "5".to_string());
architecture.components = vec!["LAMB".to_string()];
let result = search_result_for(architecture, 1.0);
assert_eq!(recover_chosen_op(&result, 0, 6), Some(5));
}
#[test]
fn test_encoding_round_trips_every_edge() {
const NUM_EDGES: usize = 4;
const NUM_OPS: usize = 9;
let mut sampled: Array3<f64> = Array3::zeros((NUM_EDGES, NUM_OPS, 1));
let expected = [8usize, 0, 5, 3];
for (edge, &op) in expected.iter().enumerate() {
sampled[[edge, op, 0]] = 1.0;
}
let architecture = discretize_architecture(&sampled, &DiscretizationStrategy::Greedy);
let result = search_result_for(architecture, 0.5);
for (edge, &op) in expected.iter().enumerate() {
assert_eq!(recover_chosen_op(&result, edge, NUM_OPS), Some(op));
}
}
#[test]
fn test_variant_updates_change_the_sampling_distribution() {
fn edge_probabilities(weights: &Array3<f64>) -> Vec<f64> {
softmax(&weights.slice(s![0, .., 0]).to_owned()).to_vec()
}
let mut sampled: Array3<f64> = Array3::zeros((1, 6, 1));
sampled[[0, 4, 0]] = 1.0;
let architecture = discretize_architecture(&sampled, &DiscretizationStrategy::Greedy);
let results = vec![search_result_for(architecture, 1.0)];
let mut plain = DifferentiableSearch::<f64>::new(6, 1, 1.0, false);
let before = edge_probabilities(&plain.architecture_weights);
plain.update_with_results(&results).expect("plain update");
let after = edge_probabilities(&plain.architecture_weights);
assert!(after[4] > before[4], "chosen op must gain probability mass");
let mut memory_efficient = MemoryEfficientDARTS::<f64>::new(6, 1, 1.0, false, 1.0, true);
let before = edge_probabilities(&memory_efficient.architecture_weights);
let edge_weights_before = memory_efficient.edge_weights.to_vec();
memory_efficient
.update_with_results(&results)
.expect("memory-efficient update");
let after = edge_probabilities(&memory_efficient.architecture_weights);
assert!(
after[4] > before[4],
"PC-DARTS chosen op must gain probability mass"
);
assert!(memory_efficient
.edge_weights
.iter()
.zip(edge_weights_before.iter())
.all(|(a, b)| a.is_finite() && b.is_finite()));
let mut robust = RobustDARTS::<f64>::new(6, 1, 1.0, false, 0.1, 20, 0.0);
let before = edge_probabilities(&robust.architecture_weights);
robust.update_with_results(&results).expect("robust update");
let after = edge_probabilities(&robust.architecture_weights);
assert!(
after[4] > before[4],
"RobustDARTS chosen op must gain probability mass"
);
}
#[test]
fn test_edge_normalization_weights_become_non_uniform() {
let mut sampled: Array3<f64> = Array3::zeros((3, 5, 1));
sampled[[0, 1, 0]] = 1.0;
sampled[[1, 3, 0]] = 1.0;
sampled[[2, 0, 0]] = 1.0;
let architecture = discretize_architecture(&sampled, &DiscretizationStrategy::Greedy);
let results = vec![search_result_for(architecture, 2.0)];
let mut search = MemoryEfficientDARTS::<f64>::new(5, 3, 1.0, false, 1.0, true);
search
.initialize(&SearchSpaceConfig::default())
.expect("initialize");
search.update_with_results(&results).expect("update");
let weights = search.edge_weights.to_vec();
assert!(weights.iter().all(|w| w.is_finite()));
let spread = weights.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
- weights.iter().cloned().fold(f64::INFINITY, f64::min);
assert!(
spread > 0.0,
"edge-normalization weights must differentiate between edges"
);
}
}