1use scirs2_core::ndarray::{Array1, Array2};
21use scirs2_core::numeric::Float;
22use std::fmt::Debug;
23
24use super::{ActivationFunction, EncoderLayer, EncoderNetwork, LayerType};
25use crate::error::{OptimError, Result};
26
27pub fn xavier_encoder_layer<T>(
36 input_dim: usize,
37 output_dim: usize,
38 layer_type: LayerType,
39) -> Result<EncoderLayer<T>>
40where
41 T: Float + Debug + Send + Sync + 'static,
42{
43 if input_dim == 0 || output_dim == 0 {
44 return Err(OptimError::InvalidConfig(format!(
45 "encoder layer dimensions must be positive, got {input_dim}x{output_dim}"
46 )));
47 }
48 let bound = (6.0 / (input_dim + output_dim) as f64).sqrt();
49 let mut rng = scirs2_core::random::thread_rng();
50 let weights = Array2::from_shape_fn((input_dim, output_dim), |_| {
51 let sample = rng.random::<f64>() * 2.0 - 1.0;
52 scirs2_core::numeric::NumCast::from(sample * bound).unwrap_or_else(|| T::zero())
53 });
54 Ok(EncoderLayer {
55 weights,
56 bias: Array1::zeros(output_dim),
57 layer_type,
58 })
59}
60
61pub fn activate<T>(value: T, activation: ActivationFunction) -> T
67where
68 T: Float + Debug + Send + Sync + 'static,
69{
70 let sigmoid = |x: T| T::one() / (T::one() + (-x).exp());
71 match activation {
72 ActivationFunction::ReLU => {
73 if value > T::zero() {
74 value
75 } else {
76 T::zero()
77 }
78 }
79 ActivationFunction::Tanh => value.tanh(),
80 ActivationFunction::Sigmoid => sigmoid(value),
81 ActivationFunction::Swish => value * sigmoid(value),
82 ActivationFunction::GELU => {
83 let c: T =
85 scirs2_core::numeric::NumCast::from(0.797_884_560_802_865_4).unwrap_or_else(T::one);
86 let k: T = scirs2_core::numeric::NumCast::from(0.044_715).unwrap_or_else(T::zero);
87 let half: T = scirs2_core::numeric::NumCast::from(0.5).unwrap_or_else(T::one);
88 half * value * (T::one() + (c * (value + k * value * value * value)).tanh())
89 }
90 ActivationFunction::Mish => {
91 let threshold: T = scirs2_core::numeric::NumCast::from(20.0).unwrap_or_else(T::one);
93 let softplus = if value > threshold {
94 value
95 } else {
96 (T::one() + value.exp()).ln()
97 };
98 value * softplus.tanh()
99 }
100 }
101}
102
103impl<T: Float + Debug + Send + Sync + 'static> EncoderNetwork<T> {
104 pub fn forward(&self, features: &Array1<T>) -> Result<Array1<T>> {
120 let first = self.layers.first().ok_or_else(|| {
121 OptimError::InvalidConfig("encoder network has no layers".to_string())
122 })?;
123
124 let mut activation = Array1::<T>::zeros(first.weights.nrows());
125 let copy = features.len().min(activation.len());
126 for i in 0..copy {
127 activation[i] = features[i];
128 }
129
130 let last_index = self.layers.len() - 1;
131 for (index, layer) in self.layers.iter().enumerate() {
132 if layer.weights.nrows() != activation.len() {
133 return Err(OptimError::InvalidConfig(format!(
134 "encoder layer {index} expects {} inputs but received {}",
135 layer.weights.nrows(),
136 activation.len()
137 )));
138 }
139 let out_dim = layer.weights.ncols();
140 if layer.bias.len() != out_dim {
141 return Err(OptimError::InvalidConfig(format!(
142 "encoder layer {index} has {out_dim} outputs but {} biases",
143 layer.bias.len()
144 )));
145 }
146 let mut next = Array1::<T>::zeros(out_dim);
147 for j in 0..out_dim {
148 let mut sum = layer.bias[j];
149 for i in 0..activation.len() {
150 sum = sum + layer.weights[[i, j]] * activation[i];
151 }
152 next[j] = if index == last_index {
153 sum
154 } else {
155 activate(sum, self.activation)
156 };
157 }
158 activation = next;
159 }
160
161 Ok(activation)
162 }
163
164 pub fn embedding_width(&self) -> Result<usize> {
169 self.layers
170 .last()
171 .map(|l| l.weights.ncols())
172 .ok_or_else(|| OptimError::InvalidConfig("encoder network has no layers".to_string()))
173 }
174}
175
176#[cfg(test)]
177impl<T: Float + Debug + Send + Sync + 'static> super::PrototypicalNetwork<T> {
178 pub(crate) fn perturb_encoder_weight_for_test(&mut self, row: usize, col: usize, value: T) {
181 if let Some(layer) = self.encoder.layers.first_mut() {
182 if row < layer.weights.nrows() && col < layer.weights.ncols() {
183 layer.weights[[row, col]] = value;
184 }
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 fn network(dims: &[(usize, usize)]) -> EncoderNetwork<f64> {
194 EncoderNetwork {
195 layers: dims
196 .iter()
197 .map(|&(i, o)| xavier_encoder_layer::<f64>(i, o, LayerType::Linear).expect("layer"))
198 .collect(),
199 activation: ActivationFunction::ReLU,
200 }
201 }
202
203 #[test]
204 fn xavier_layers_are_not_zero() {
205 let layer = xavier_encoder_layer::<f64>(6, 4, LayerType::Linear).expect("layer");
206 let magnitude = layer.weights.iter().fold(0.0_f64, |a, &w| a.max(w.abs()));
207 assert!(magnitude > 0.0, "encoder weights are all zero");
208 let bound = (6.0_f64 / 10.0).sqrt();
209 assert!(
210 layer.weights.iter().all(|w| w.abs() <= bound + 1e-12),
211 "a weight escaped the Xavier bound {bound}"
212 );
213 }
214
215 #[test]
216 fn rejects_zero_dimensions() {
217 assert!(xavier_encoder_layer::<f64>(0, 4, LayerType::Linear).is_err());
218 assert!(xavier_encoder_layer::<f64>(4, 0, LayerType::Linear).is_err());
219 }
220
221 #[test]
222 fn forward_produces_the_embedding_width() {
223 let net = network(&[(6, 5), (5, 3)]);
224 assert_eq!(net.embedding_width().expect("width"), 3);
225 let embedded = net
226 .forward(&Array1::from_vec(vec![1.0, -0.5, 0.25, 2.0, -1.0, 0.1]))
227 .expect("forward");
228 assert_eq!(embedded.len(), 3);
229 assert!(embedded.iter().all(|v| v.is_finite()));
230 }
231
232 #[test]
233 fn forward_depends_on_its_input() {
234 let net = network(&[(4, 3)]);
235 let a = net
236 .forward(&Array1::from_vec(vec![1.0, 0.0, 0.0, 0.0]))
237 .expect("forward on the first one-hot input");
238 let b = net
239 .forward(&Array1::from_vec(vec![0.0, 1.0, 0.0, 0.0]))
240 .expect("forward on the second one-hot input");
241 let delta = a
242 .iter()
243 .zip(b.iter())
244 .map(|(x, y)| (x - y).abs())
245 .fold(0.0, f64::max);
246 assert!(
247 delta > 1e-9,
248 "the encoder ignores its input (delta {delta})"
249 );
250 }
251
252 #[test]
253 fn short_and_long_features_are_padded_and_truncated() {
254 let net = network(&[(4, 2)]);
255 assert!(net.forward(&Array1::from_vec(vec![1.0, 2.0])).is_ok());
256 assert!(net
257 .forward(&Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]))
258 .is_ok());
259 let short = net
262 .forward(&Array1::from_vec(vec![1.0, 2.0]))
263 .expect("short");
264 let padded = net
265 .forward(&Array1::from_vec(vec![1.0, 2.0, 0.0, 0.0]))
266 .expect("padded");
267 assert_eq!(short, padded);
268 }
269
270 #[test]
271 fn an_empty_network_is_an_error() {
272 let empty = EncoderNetwork::<f64> {
273 layers: Vec::new(),
274 activation: ActivationFunction::ReLU,
275 };
276 assert!(empty.forward(&Array1::from_vec(vec![1.0])).is_err());
277 assert!(empty.embedding_width().is_err());
278 }
279
280 #[test]
286 fn encode_task_goes_through_the_encoder() {
287 use crate::few_shot::{
288 DifficultyLevel, DomainCharacteristics, DomainInfo, DomainType, ExampleMetadata,
289 PrototypicalNetwork, PrototypicalNetworkConfig, QuerySet, QuerySetStatistics,
290 SupportExample, SupportSet, SupportSetStatistics, TaskData, TaskMetadata,
291 };
292 use std::collections::HashMap;
293
294 let features = [
295 Array1::from_vec(vec![1.0_f64, 2.0, 3.0, 4.0]),
296 Array1::from_vec(vec![-1.0_f64, 0.5, 2.0, -3.0]),
297 ];
298 let examples: Vec<SupportExample<f64>> = features
299 .iter()
300 .map(|f| SupportExample {
301 features: f.clone(),
302 target: 0.0,
303 weight: 1.0,
304 context: HashMap::new(),
305 metadata: ExampleMetadata {
306 source: "test".to_string(),
307 quality_score: 1.0,
308 created_at: std::time::SystemTime::now(),
309 },
310 })
311 .collect();
312
313 let task = TaskData::<f64> {
314 task_id: "f16".to_string(),
315 support_set: SupportSet {
316 examples,
317 task_metadata: TaskMetadata {
318 task_name: "f16".to_string(),
319 domain: DomainType::Optimization,
320 difficulty: DifficultyLevel::Easy,
321 created_at: std::time::SystemTime::now(),
322 },
323 statistics: SupportSetStatistics {
324 mean: Array1::zeros(4),
325 variance: Array1::zeros(4),
326 size: 2,
327 diversity_score: 1.0,
328 },
329 temporal_order: None,
330 },
331 query_set: QuerySet {
332 examples: Vec::new(),
333 statistics: QuerySetStatistics {
334 mean: Array1::zeros(4),
335 variance: Array1::zeros(4),
336 size: 0,
337 },
338 eval_metrics: Vec::new(),
339 },
340 task_params: HashMap::new(),
341 domain_info: DomainInfo {
342 domain_type: DomainType::Optimization,
343 characteristics: DomainCharacteristics {
344 input_dim: 4,
345 output_dim: 1,
346 temporal: false,
347 stochasticity: 0.0,
348 noise_level: 0.0,
349 sparsity: 0.0,
350 },
351 difficulty_level: DifficultyLevel::Easy,
352 constraints: Vec::new(),
353 },
354 };
355
356 let mut network = PrototypicalNetwork::<f64>::new(PrototypicalNetworkConfig {
357 embedding_dim: 3,
358 learning_rate: 0.01,
359 num_layers: 1,
360 hidden_dim: 4,
361 })
362 .expect("network");
363
364 let encoded = network.encode_task(&task).expect("encode_task");
365 assert_eq!(
366 encoded.len(),
367 3,
368 "embedding width must come from the encoder"
369 );
370
371 let raw_mean = [
373 (features[0][0] + features[1][0]) / 2.0,
374 (features[0][1] + features[1][1]) / 2.0,
375 (features[0][2] + features[1][2]) / 2.0,
376 ];
377 let to_raw = encoded
378 .iter()
379 .zip(raw_mean.iter())
380 .map(|(a, b)| (a - b).abs())
381 .fold(0.0_f64, f64::max);
382 assert!(
383 to_raw > 1e-9,
384 "encode_task still returns the raw feature mean (delta {to_raw})"
385 );
386
387 network.perturb_encoder_weight_for_test(1, 0, 5.0);
391 let after = network.encode_task(&task).expect("encode_task again");
392 let delta = encoded
393 .iter()
394 .zip(after.iter())
395 .map(|(a, b)| (a - b).abs())
396 .fold(0.0_f64, f64::max);
397 assert!(
398 delta > 1e-9,
399 "the encoder weights do not affect the embedding (delta {delta})"
400 );
401 }
402
403 #[test]
404 fn swish_and_mish_stay_finite_for_large_inputs() {
405 for a in [ActivationFunction::Swish, ActivationFunction::Mish] {
406 for x in [-1e3_f64, -10.0, 0.0, 10.0, 1e3] {
407 let y = activate(x, a);
408 assert!(y.is_finite(), "{a:?} at {x} produced {y}");
409 }
410 }
411 }
412}