use std::collections::HashMap;
use std::fmt;
#[inline]
fn xorshift64(state: &mut u64) -> u64 {
let mut x = *state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*state = x;
x
}
#[inline]
fn xorshift_f64(state: &mut u64) -> f64 {
(xorshift64(state) >> 11) as f64 / (1u64 << 53) as f64
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TaskId(pub String);
impl TaskId {
pub fn new(id: impl Into<String>) -> Self {
TaskId(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for TaskId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct TaskExample {
pub features: Vec<f64>,
pub label: f64,
pub task_id: TaskId,
}
impl TaskExample {
pub fn new(features: Vec<f64>, label: f64, task_id: TaskId) -> Self {
TaskExample {
features,
label,
task_id,
}
}
}
#[derive(Debug, Clone)]
pub struct ModelParams {
pub weights: Vec<f64>,
pub bias: f64,
pub dim: usize,
}
impl ModelParams {
pub fn zeros(dim: usize) -> Self {
ModelParams {
weights: vec![0.0; dim],
bias: 0.0,
dim,
}
}
fn predict(&self, x: &[f64]) -> f64 {
let dot: f64 = self
.weights
.iter()
.zip(x.iter())
.map(|(w, xi)| w * xi)
.sum();
dot + self.bias
}
fn mse_and_grads(&self, examples: &[TaskExample]) -> (f64, Vec<f64>, f64) {
let n = examples.len() as f64;
let mut grad_w = vec![0.0; self.dim];
let mut grad_b = 0.0_f64;
let mut loss = 0.0_f64;
for ex in examples {
let pred = self.predict(&ex.features);
let residual = pred - ex.label;
loss += residual * residual;
let coeff = 2.0 * residual / n;
for (gw, xi) in grad_w.iter_mut().zip(ex.features.iter()) {
*gw += coeff * xi;
}
grad_b += coeff;
}
loss /= n;
(loss, grad_w, grad_b)
}
}
#[derive(Debug, Clone)]
pub struct AdaptationStep {
pub params: ModelParams,
pub loss: f64,
pub gradient: Vec<f64>,
pub step_num: usize,
}
#[derive(Debug, Clone)]
pub struct MetaTask {
pub id: TaskId,
pub support_set: Vec<TaskExample>,
pub query_set: Vec<TaskExample>,
pub adapted_params: Option<ModelParams>,
}
impl MetaTask {
pub fn new(id: TaskId, support_set: Vec<TaskExample>, query_set: Vec<TaskExample>) -> Self {
MetaTask {
id,
support_set,
query_set,
adapted_params: None,
}
}
pub fn feature_dim(&self) -> Option<usize> {
self.support_set.first().map(|ex| ex.features.len())
}
}
#[derive(Debug, Clone)]
pub enum MetaAlgorithm {
MAML {
inner_lr: f64,
inner_steps: u8,
},
ProtoNet,
Reptile {
step_size: f64,
},
FOMAML {
inner_lr: f64,
},
}
#[derive(Debug, Clone)]
pub struct OptimizerConfig {
pub algorithm: MetaAlgorithm,
pub meta_lr: f64,
pub n_tasks_per_batch: usize,
pub max_params_dim: usize,
}
impl OptimizerConfig {
pub fn default_maml(dim: usize) -> Self {
OptimizerConfig {
algorithm: MetaAlgorithm::MAML {
inner_lr: 0.01,
inner_steps: 5,
},
meta_lr: 0.001,
n_tasks_per_batch: 4,
max_params_dim: dim,
}
}
pub fn default_reptile(dim: usize) -> Self {
OptimizerConfig {
algorithm: MetaAlgorithm::Reptile { step_size: 0.1 },
meta_lr: 0.001,
n_tasks_per_batch: 4,
max_params_dim: dim,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct MetaStats {
pub tasks_trained: u64,
pub meta_updates: u64,
pub avg_adaptation_loss: f64,
pub avg_query_loss: f64,
pub convergence_delta: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum MetaError {
InsufficientTasks(usize),
DimensionMismatch {
expected: usize,
got: usize,
},
AdaptationFailed(String),
InvalidConfig(String),
}
impl fmt::Display for MetaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MetaError::InsufficientTasks(n) => {
write!(f, "insufficient tasks: need at least {n}")
}
MetaError::DimensionMismatch { expected, got } => {
write!(f, "dimension mismatch: expected {expected}, got {got}")
}
MetaError::AdaptationFailed(msg) => write!(f, "adaptation failed: {msg}"),
MetaError::InvalidConfig(msg) => write!(f, "invalid config: {msg}"),
}
}
}
impl std::error::Error for MetaError {}
pub struct MetaLearningOptimizer {
config: OptimizerConfig,
tasks: HashMap<TaskId, MetaTask>,
feature_dim: Option<usize>,
stats: MetaStats,
adaptation_loss_sum: f64,
adaptation_loss_count: u64,
query_loss_sum: f64,
query_loss_count: u64,
prev_query_loss: Option<f64>,
}
impl MetaLearningOptimizer {
pub fn new(config: OptimizerConfig) -> Self {
MetaLearningOptimizer {
config,
tasks: HashMap::new(),
feature_dim: None,
stats: MetaStats::default(),
adaptation_loss_sum: 0.0,
adaptation_loss_count: 0,
query_loss_sum: 0.0,
query_loss_count: 0,
prev_query_loss: None,
}
}
pub fn add_task(&mut self, task: MetaTask) -> Result<(), MetaError> {
if let Some(dim) = task.feature_dim() {
match self.feature_dim {
None => {
if dim > self.config.max_params_dim {
return Err(MetaError::InvalidConfig(format!(
"feature dim {dim} exceeds max_params_dim {}",
self.config.max_params_dim
)));
}
self.feature_dim = Some(dim);
}
Some(expected) => {
if dim != expected {
return Err(MetaError::DimensionMismatch { expected, got: dim });
}
}
}
}
for qex in &task.query_set {
let got = qex.features.len();
if let Some(expected) = self.feature_dim {
if got != expected {
return Err(MetaError::DimensionMismatch { expected, got });
}
}
}
self.tasks.insert(task.id.clone(), task);
self.stats.tasks_trained += 1;
Ok(())
}
pub fn adapt_to_task(
&self,
task_id: &TaskId,
init_params: &ModelParams,
steps: u8,
lr: f64,
) -> Result<Vec<AdaptationStep>, MetaError> {
let task = self
.tasks
.get(task_id)
.ok_or_else(|| MetaError::AdaptationFailed(format!("unknown task: {task_id}")))?;
if task.support_set.is_empty() {
return Err(MetaError::AdaptationFailed(
"support set is empty".to_string(),
));
}
let expected_dim = init_params.dim;
for ex in &task.support_set {
let got = ex.features.len();
if got != expected_dim {
return Err(MetaError::DimensionMismatch {
expected: expected_dim,
got,
});
}
}
let mut params = init_params.clone();
let mut history = Vec::with_capacity(steps as usize);
for step in 0..steps {
let (loss, grad_w, grad_b) = params.mse_and_grads(&task.support_set);
for (w, gw) in params.weights.iter_mut().zip(grad_w.iter()) {
*w -= lr * gw;
}
params.bias -= lr * grad_b;
history.push(AdaptationStep {
params: params.clone(),
loss,
gradient: grad_w,
step_num: step as usize,
});
}
Ok(history)
}
pub fn meta_update(
&mut self,
task_ids: &[TaskId],
current_params: &ModelParams,
) -> Result<ModelParams, MetaError> {
if task_ids.is_empty() {
return Err(MetaError::InsufficientTasks(1));
}
let dim = current_params.dim;
let result = match &self.config.algorithm.clone() {
MetaAlgorithm::MAML {
inner_lr,
inner_steps,
} => self.meta_update_maml(task_ids, current_params, *inner_lr, *inner_steps, dim)?,
MetaAlgorithm::FOMAML { inner_lr } => {
self.meta_update_fomaml(task_ids, current_params, *inner_lr, dim)?
}
MetaAlgorithm::Reptile { step_size } => {
self.meta_update_reptile(task_ids, current_params, *step_size, dim)?
}
MetaAlgorithm::ProtoNet => self.meta_update_protonet(task_ids, current_params, dim)?,
};
let avg_q = self.compute_avg_query_loss(task_ids, &result);
self.query_loss_sum += avg_q;
self.query_loss_count += 1;
let new_avg = self.query_loss_sum / self.query_loss_count as f64;
let delta = match self.prev_query_loss {
Some(prev) => (new_avg - prev).abs(),
None => 0.0,
};
self.prev_query_loss = Some(new_avg);
self.stats.avg_query_loss = new_avg;
self.stats.convergence_delta = delta;
self.stats.meta_updates += 1;
Ok(result)
}
pub fn evaluate_task(&self, task_id: &TaskId, params: &ModelParams) -> Result<f64, MetaError> {
let task = self
.tasks
.get(task_id)
.ok_or_else(|| MetaError::AdaptationFailed(format!("unknown task: {task_id}")))?;
if task.query_set.is_empty() {
return Err(MetaError::AdaptationFailed(
"query set is empty".to_string(),
));
}
let (loss, _, _) = params.mse_and_grads(&task.query_set);
Ok(loss)
}
pub fn initialize_params(dim: usize, seed: u64) -> ModelParams {
let mut state = if seed == 0 { 0xdeadbeef_cafebabe } else { seed };
let weights: Vec<f64> = (0..dim)
.map(|_| (xorshift_f64(&mut state) - 0.5) * 0.01)
.collect();
let bias = (xorshift_f64(&mut state) - 0.5) * 0.01;
ModelParams { weights, bias, dim }
}
pub fn few_shot_predict(
&self,
task: &MetaTask,
x: &[f64],
init_params: &ModelParams,
) -> Result<f64, MetaError> {
let (steps, lr) = match &self.config.algorithm {
MetaAlgorithm::MAML {
inner_lr,
inner_steps,
} => (*inner_steps, *inner_lr),
MetaAlgorithm::FOMAML { inner_lr } => (5u8, *inner_lr),
MetaAlgorithm::Reptile { step_size } => (5u8, *step_size),
MetaAlgorithm::ProtoNet => (1u8, 0.01),
};
if task.support_set.is_empty() {
return Err(MetaError::AdaptationFailed(
"support set is empty for few_shot_predict".to_string(),
));
}
let dim = init_params.dim;
if x.len() != dim {
return Err(MetaError::DimensionMismatch {
expected: dim,
got: x.len(),
});
}
for ex in &task.support_set {
let got = ex.features.len();
if got != dim {
return Err(MetaError::DimensionMismatch { expected: dim, got });
}
}
let mut params = init_params.clone();
for _ in 0..steps {
let (_, grad_w, grad_b) = params.mse_and_grads(&task.support_set);
for (w, gw) in params.weights.iter_mut().zip(grad_w.iter()) {
*w -= lr * gw;
}
params.bias -= lr * grad_b;
}
Ok(params.predict(x))
}
pub fn stats(&self) -> MetaStats {
self.stats.clone()
}
fn meta_update_maml(
&mut self,
task_ids: &[TaskId],
current_params: &ModelParams,
inner_lr: f64,
inner_steps: u8,
dim: usize,
) -> Result<ModelParams, MetaError> {
let mut meta_grad_w = vec![0.0_f64; dim];
let mut meta_grad_b = 0.0_f64;
let mut valid_count = 0usize;
for tid in task_ids {
let history = self.adapt_to_task(tid, current_params, inner_steps, inner_lr)?;
if let Some(last) = history.last() {
for (mg, (aw, iw)) in meta_grad_w.iter_mut().zip(
last.params
.weights
.iter()
.zip(current_params.weights.iter()),
) {
*mg += aw - iw;
}
meta_grad_b += last.params.bias - current_params.bias;
let adapt_loss = last.loss;
self.adaptation_loss_sum += adapt_loss;
self.adaptation_loss_count += 1;
self.stats.avg_adaptation_loss =
self.adaptation_loss_sum / self.adaptation_loss_count as f64;
valid_count += 1;
}
}
if valid_count == 0 {
return Err(MetaError::InsufficientTasks(1));
}
let inv = 1.0 / valid_count as f64;
let meta_lr = self.config.meta_lr;
let mut new_w = current_params.weights.clone();
for (w, mg) in new_w.iter_mut().zip(meta_grad_w.iter()) {
*w += meta_lr * mg * inv;
}
let new_b = current_params.bias + meta_lr * meta_grad_b * inv;
Ok(ModelParams {
weights: new_w,
bias: new_b,
dim,
})
}
fn meta_update_fomaml(
&mut self,
task_ids: &[TaskId],
current_params: &ModelParams,
inner_lr: f64,
dim: usize,
) -> Result<ModelParams, MetaError> {
let inner_steps: u8 = 1;
let mut meta_grad_w = vec![0.0_f64; dim];
let mut meta_grad_b = 0.0_f64;
let mut valid_count = 0usize;
for tid in task_ids {
let task = self
.tasks
.get(tid)
.ok_or_else(|| MetaError::AdaptationFailed(format!("unknown task: {tid}")))?;
if task.support_set.is_empty() {
continue;
}
for ex in &task.support_set {
let got = ex.features.len();
if got != dim {
return Err(MetaError::DimensionMismatch { expected: dim, got });
}
}
let history = self.adapt_to_task(tid, current_params, inner_steps, inner_lr)?;
if let Some(last) = history.last() {
let task2 = self
.tasks
.get(tid)
.ok_or_else(|| MetaError::AdaptationFailed(format!("unknown task: {tid}")))?;
let (qloss, qgrad_w, qgrad_b) = last.params.mse_and_grads(&task2.query_set);
for (mg, qg) in meta_grad_w.iter_mut().zip(qgrad_w.iter()) {
*mg += qg;
}
meta_grad_b += qgrad_b;
self.adaptation_loss_sum += qloss;
self.adaptation_loss_count += 1;
self.stats.avg_adaptation_loss =
self.adaptation_loss_sum / self.adaptation_loss_count as f64;
valid_count += 1;
}
}
if valid_count == 0 {
return Err(MetaError::InsufficientTasks(1));
}
let inv = 1.0 / valid_count as f64;
let meta_lr = self.config.meta_lr;
let mut new_w = current_params.weights.clone();
for (w, mg) in new_w.iter_mut().zip(meta_grad_w.iter()) {
*w -= meta_lr * mg * inv;
}
let new_b = current_params.bias - meta_lr * meta_grad_b * inv;
Ok(ModelParams {
weights: new_w,
bias: new_b,
dim,
})
}
fn meta_update_reptile(
&mut self,
task_ids: &[TaskId],
current_params: &ModelParams,
step_size: f64,
dim: usize,
) -> Result<ModelParams, MetaError> {
let inner_steps = 5u8;
let inner_lr = 0.01;
let mut result = current_params.clone();
let mut valid_count = 0usize;
for tid in task_ids {
let history = self.adapt_to_task(tid, current_params, inner_steps, inner_lr)?;
if let Some(last) = history.last() {
for (idx, rw) in result.weights.iter_mut().enumerate() {
let init_w = current_params.weights[idx];
let adapted_w = last.params.weights[idx];
*rw += step_size * (adapted_w - init_w);
}
result.bias += step_size * (last.params.bias - current_params.bias);
self.adaptation_loss_sum += last.loss;
self.adaptation_loss_count += 1;
self.stats.avg_adaptation_loss =
self.adaptation_loss_sum / self.adaptation_loss_count as f64;
valid_count += 1;
}
}
if valid_count == 0 {
return Err(MetaError::InsufficientTasks(1));
}
let _ = dim; Ok(result)
}
fn meta_update_protonet(
&mut self,
task_ids: &[TaskId],
current_params: &ModelParams,
dim: usize,
) -> Result<ModelParams, MetaError> {
let mut proto_w = vec![0.0_f64; dim];
let mut proto_b = 0.0_f64;
let mut valid_count = 0usize;
for tid in task_ids {
let task = self
.tasks
.get(tid)
.ok_or_else(|| MetaError::AdaptationFailed(format!("unknown task: {tid}")))?;
if task.support_set.is_empty() {
continue;
}
let n = task.support_set.len() as f64;
let mean_label: f64 = task.support_set.iter().map(|e| e.label).sum::<f64>() / n;
let mut mean_feat = vec![0.0_f64; dim];
for ex in &task.support_set {
if ex.features.len() != dim {
return Err(MetaError::DimensionMismatch {
expected: dim,
got: ex.features.len(),
});
}
for (mf, xi) in mean_feat.iter_mut().zip(ex.features.iter()) {
*mf += xi / n;
}
}
for (pw, mf) in proto_w.iter_mut().zip(mean_feat.iter()) {
*pw += mf * mean_label;
}
proto_b += mean_label;
valid_count += 1;
}
if valid_count == 0 {
return Err(MetaError::InsufficientTasks(1));
}
let inv = 1.0 / valid_count as f64;
let meta_lr = self.config.meta_lr;
let mut new_w = current_params.weights.clone();
for (w, pw) in new_w.iter_mut().zip(proto_w.iter()) {
*w += meta_lr * pw * inv;
}
let new_b = current_params.bias + meta_lr * proto_b * inv;
Ok(ModelParams {
weights: new_w,
bias: new_b,
dim,
})
}
fn compute_avg_query_loss(&self, task_ids: &[TaskId], params: &ModelParams) -> f64 {
let mut sum = 0.0;
let mut count = 0usize;
for tid in task_ids {
if let Ok(loss) = self.evaluate_task(tid, params) {
sum += loss;
count += 1;
}
}
if count == 0 {
0.0
} else {
sum / count as f64
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_regression_task(
id: &str,
slope: f64,
intercept: f64,
n_support: usize,
n_query: usize,
seed: u64,
) -> MetaTask {
let tid = TaskId::new(id);
let mut state = seed;
let mut support = Vec::with_capacity(n_support);
for _ in 0..n_support {
let x = xorshift_f64(&mut state) * 4.0 - 2.0;
let y = slope * x + intercept;
support.push(TaskExample::new(vec![x], y, tid.clone()));
}
let mut query = Vec::with_capacity(n_query);
for _ in 0..n_query {
let x = xorshift_f64(&mut state) * 4.0 - 2.0;
let y = slope * x + intercept;
query.push(TaskExample::new(vec![x], y, tid.clone()));
}
MetaTask::new(tid, support, query)
}
fn make_2d_task(
id: &str,
w0: f64,
w1: f64,
bias: f64,
n_support: usize,
n_query: usize,
seed: u64,
) -> MetaTask {
let tid = TaskId::new(id);
let mut state = seed;
let mut support = Vec::with_capacity(n_support);
for _ in 0..n_support {
let x0 = xorshift_f64(&mut state) * 2.0 - 1.0;
let x1 = xorshift_f64(&mut state) * 2.0 - 1.0;
let y = w0 * x0 + w1 * x1 + bias;
support.push(TaskExample::new(vec![x0, x1], y, tid.clone()));
}
let mut query = Vec::with_capacity(n_query);
for _ in 0..n_query {
let x0 = xorshift_f64(&mut state) * 2.0 - 1.0;
let x1 = xorshift_f64(&mut state) * 2.0 - 1.0;
let y = w0 * x0 + w1 * x1 + bias;
query.push(TaskExample::new(vec![x0, x1], y, tid.clone()));
}
MetaTask::new(tid, support, query)
}
#[test]
fn test_add_task_basic() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
let task = make_regression_task("t1", 2.0, 1.0, 5, 5, 1);
assert!(opt.add_task(task).is_ok());
assert_eq!(opt.stats().tasks_trained, 1);
}
#[test]
fn test_add_multiple_tasks() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
for i in 0..5 {
let task = make_regression_task(&format!("t{i}"), i as f64, 0.0, 4, 4, i as u64 + 1);
assert!(opt.add_task(task).is_ok());
}
assert_eq!(opt.stats().tasks_trained, 5);
}
#[test]
fn test_add_task_dimension_consistency() {
let config = OptimizerConfig::default_maml(2);
let mut opt = MetaLearningOptimizer::new(config);
let t1 = make_2d_task("t1", 1.0, 2.0, 0.5, 4, 4, 10);
assert!(opt.add_task(t1).is_ok());
let t2 = make_regression_task("t2", 1.0, 0.0, 4, 4, 20);
let err = opt.add_task(t2).unwrap_err();
assert!(matches!(
err,
MetaError::DimensionMismatch {
expected: 2,
got: 1
}
));
}
#[test]
fn test_add_task_dim_exceeds_max() {
let config = OptimizerConfig {
algorithm: MetaAlgorithm::MAML {
inner_lr: 0.01,
inner_steps: 3,
},
meta_lr: 0.001,
n_tasks_per_batch: 2,
max_params_dim: 2,
};
let mut opt = MetaLearningOptimizer::new(config);
let tid = TaskId::new("too-big");
let ex = TaskExample::new(vec![1.0, 2.0, 3.0], 0.5, tid.clone());
let task = MetaTask::new(tid, vec![ex.clone()], vec![ex]);
let err = opt.add_task(task).unwrap_err();
assert!(matches!(err, MetaError::InvalidConfig(_)));
}
#[test]
fn test_add_task_empty_support_allowed() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
let tid = TaskId::new("empty");
let qex = TaskExample::new(vec![1.0], 1.0, tid.clone());
let task = MetaTask::new(tid, vec![], vec![qex]);
assert!(opt.add_task(task).is_ok());
}
#[test]
fn test_adapt_returns_correct_step_count() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
let task = make_regression_task("t1", 3.0, 0.5, 10, 5, 42);
opt.add_task(task).expect("test: should succeed");
let init = MetaLearningOptimizer::initialize_params(1, 1);
let steps = opt
.adapt_to_task(&TaskId::new("t1"), &init, 7, 0.01)
.expect("test: should succeed");
assert_eq!(steps.len(), 7);
}
#[test]
fn test_adapt_step_numbers_sequential() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
let task = make_regression_task("t1", 1.0, 0.0, 8, 4, 5);
opt.add_task(task).expect("test: should succeed");
let init = MetaLearningOptimizer::initialize_params(1, 7);
let steps = opt
.adapt_to_task(&TaskId::new("t1"), &init, 5, 0.05)
.expect("test: should succeed");
for (i, step) in steps.iter().enumerate() {
assert_eq!(step.step_num, i);
}
}
#[test]
fn test_adapt_loss_non_negative() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
let task = make_regression_task("t1", 2.0, -1.0, 10, 5, 11);
opt.add_task(task).expect("test: should succeed");
let init = MetaLearningOptimizer::initialize_params(1, 99);
let steps = opt
.adapt_to_task(&TaskId::new("t1"), &init, 10, 0.01)
.expect("test: should succeed");
for step in &steps {
assert!(step.loss >= 0.0, "loss must be non-negative");
}
}
#[test]
fn test_adapt_loss_decreases_over_steps() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
let task = make_regression_task("t1", 1.5, 0.3, 20, 5, 7);
opt.add_task(task).expect("test: should succeed");
let init = MetaLearningOptimizer::initialize_params(1, 3);
let steps = opt
.adapt_to_task(&TaskId::new("t1"), &init, 20, 0.05)
.expect("test: should succeed");
let first_loss = steps.first().map(|s| s.loss).unwrap_or(f64::MAX);
let last_loss = steps.last().map(|s| s.loss).unwrap_or(f64::MAX);
assert!(
last_loss <= first_loss + 1e-10,
"loss should decrease: {first_loss} -> {last_loss}"
);
}
#[test]
fn test_adapt_2d_loss_decreases() {
let config = OptimizerConfig::default_maml(2);
let mut opt = MetaLearningOptimizer::new(config);
let task = make_2d_task("t1", 1.0, -1.0, 0.5, 15, 5, 42);
opt.add_task(task).expect("test: should succeed");
let init = MetaLearningOptimizer::initialize_params(2, 9);
let steps = opt
.adapt_to_task(&TaskId::new("t1"), &init, 30, 0.02)
.expect("test: should succeed");
let first = steps.first().map(|s| s.loss).expect("test: should succeed");
let last = steps.last().map(|s| s.loss).expect("test: should succeed");
assert!(last <= first + 1e-9);
}
#[test]
fn test_adapt_unknown_task_error() {
let config = OptimizerConfig::default_maml(1);
let opt = MetaLearningOptimizer::new(config);
let init = MetaLearningOptimizer::initialize_params(1, 1);
let err = opt
.adapt_to_task(&TaskId::new("no-such"), &init, 5, 0.01)
.unwrap_err();
assert!(matches!(err, MetaError::AdaptationFailed(_)));
}
#[test]
fn test_adapt_empty_support_error() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
let tid = TaskId::new("empty");
let qex = TaskExample::new(vec![1.0], 1.0, tid.clone());
let task = MetaTask::new(tid.clone(), vec![], vec![qex]);
opt.add_task(task).expect("test: should succeed");
let init = MetaLearningOptimizer::initialize_params(1, 1);
let err = opt.adapt_to_task(&tid, &init, 3, 0.01).unwrap_err();
assert!(matches!(err, MetaError::AdaptationFailed(_)));
}
#[test]
fn test_adapt_dim_mismatch_error() {
let config = OptimizerConfig::default_maml(2);
let mut opt = MetaLearningOptimizer::new(config);
let task = make_2d_task("t1", 1.0, 1.0, 0.0, 5, 5, 1);
opt.add_task(task).expect("test: should succeed");
let bad_init = MetaLearningOptimizer::initialize_params(3, 1);
let err = opt
.adapt_to_task(&TaskId::new("t1"), &bad_init, 3, 0.01)
.unwrap_err();
assert!(matches!(err, MetaError::DimensionMismatch { .. }));
}
#[test]
fn test_meta_update_maml_returns_new_params() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
for i in 0..4 {
let task = make_regression_task(
&format!("t{i}"),
(i + 1) as f64,
0.1,
8,
4,
(i * 7 + 1) as u64,
);
opt.add_task(task).expect("test: should succeed");
}
let init = MetaLearningOptimizer::initialize_params(1, 42);
let ids: Vec<TaskId> = (0..4).map(|i| TaskId::new(format!("t{i}"))).collect();
let new_params = opt.meta_update(&ids, &init).expect("test: should succeed");
assert_eq!(new_params.dim, 1);
assert_eq!(opt.stats().meta_updates, 1);
}
#[test]
fn test_meta_update_maml_params_changed() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
for i in 0..4 {
let task = make_regression_task(
&format!("t{i}"),
(i as f64 + 1.0) * 0.7,
0.3,
10,
5,
i as u64 + 11,
);
opt.add_task(task).expect("test: should succeed");
}
let init = MetaLearningOptimizer::initialize_params(1, 17);
let ids: Vec<TaskId> = (0..4).map(|i| TaskId::new(format!("t{i}"))).collect();
let new_params = opt.meta_update(&ids, &init).expect("test: should succeed");
let changed = new_params.weights[0] != init.weights[0] || new_params.bias != init.bias;
assert!(changed, "meta_update should change parameters");
}
#[test]
fn test_meta_update_maml_multiple_rounds_converge() {
let config = OptimizerConfig {
algorithm: MetaAlgorithm::MAML {
inner_lr: 0.05,
inner_steps: 5,
},
meta_lr: 0.1,
n_tasks_per_batch: 4,
max_params_dim: 1,
};
let mut opt = MetaLearningOptimizer::new(config);
for i in 0..4 {
let task = make_regression_task(&format!("t{i}"), 1.0, 0.0, 10, 5, i as u64 + 1);
opt.add_task(task).expect("test: should succeed");
}
let mut params = MetaLearningOptimizer::initialize_params(1, 5);
let ids: Vec<TaskId> = (0..4).map(|i| TaskId::new(format!("t{i}"))).collect();
for _ in 0..20 {
params = opt
.meta_update(&ids, ¶ms)
.expect("test: should succeed");
}
assert_eq!(opt.stats().meta_updates, 20);
}
#[test]
fn test_meta_update_maml_empty_task_list_error() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
let init = MetaLearningOptimizer::initialize_params(1, 1);
let err = opt.meta_update(&[], &init).unwrap_err();
assert!(matches!(err, MetaError::InsufficientTasks(_)));
}
#[test]
fn test_meta_update_reptile_basic() {
let config = OptimizerConfig::default_reptile(1);
let mut opt = MetaLearningOptimizer::new(config);
for i in 0..3 {
let task =
make_regression_task(&format!("r{i}"), (i + 1) as f64, 0.0, 8, 4, i as u64 + 5);
opt.add_task(task).expect("test: should succeed");
}
let init = MetaLearningOptimizer::initialize_params(1, 42);
let ids: Vec<TaskId> = (0..3).map(|i| TaskId::new(format!("r{i}"))).collect();
let new_p = opt.meta_update(&ids, &init).expect("test: should succeed");
assert_eq!(new_p.dim, 1);
}
#[test]
fn test_meta_update_reptile_params_change() {
let config = OptimizerConfig::default_reptile(1);
let mut opt = MetaLearningOptimizer::new(config);
for i in 0..3 {
let task = make_regression_task(&format!("r{i}"), 2.0, 1.0, 10, 5, i as u64 + 100);
opt.add_task(task).expect("test: should succeed");
}
let init = MetaLearningOptimizer::initialize_params(1, 77);
let ids: Vec<TaskId> = (0..3).map(|i| TaskId::new(format!("r{i}"))).collect();
let new_p = opt.meta_update(&ids, &init).expect("test: should succeed");
let changed = new_p.weights[0] != init.weights[0] || new_p.bias != init.bias;
assert!(changed);
}
#[test]
fn test_meta_update_reptile_multiple_rounds() {
let config = OptimizerConfig::default_reptile(1);
let mut opt = MetaLearningOptimizer::new(config);
for i in 0..3 {
let task = make_regression_task(&format!("r{i}"), 1.0, 0.5, 8, 4, i as u64 + 3);
opt.add_task(task).expect("test: should succeed");
}
let mut params = MetaLearningOptimizer::initialize_params(1, 17);
let ids: Vec<TaskId> = (0..3).map(|i| TaskId::new(format!("r{i}"))).collect();
for _ in 0..10 {
params = opt
.meta_update(&ids, ¶ms)
.expect("test: should succeed");
}
assert_eq!(opt.stats().meta_updates, 10);
}
#[test]
fn test_meta_update_fomaml_basic() {
let config = OptimizerConfig {
algorithm: MetaAlgorithm::FOMAML { inner_lr: 0.02 },
meta_lr: 0.01,
n_tasks_per_batch: 3,
max_params_dim: 1,
};
let mut opt = MetaLearningOptimizer::new(config);
for i in 0..3 {
let task = make_regression_task(
&format!("f{i}"),
i as f64 + 0.5,
0.0,
8,
4,
(i * 3 + 2) as u64,
);
opt.add_task(task).expect("test: should succeed");
}
let init = MetaLearningOptimizer::initialize_params(1, 55);
let ids: Vec<TaskId> = (0..3).map(|i| TaskId::new(format!("f{i}"))).collect();
let new_p = opt.meta_update(&ids, &init).expect("test: should succeed");
assert_eq!(new_p.dim, 1);
}
#[test]
fn test_meta_update_fomaml_params_change() {
let config = OptimizerConfig {
algorithm: MetaAlgorithm::FOMAML { inner_lr: 0.05 },
meta_lr: 0.1,
n_tasks_per_batch: 3,
max_params_dim: 1,
};
let mut opt = MetaLearningOptimizer::new(config);
for i in 0..3 {
let task = make_regression_task(&format!("f{i}"), 2.0, 0.5, 10, 5, i as u64 + 20);
opt.add_task(task).expect("test: should succeed");
}
let init = MetaLearningOptimizer::initialize_params(1, 3);
let ids: Vec<TaskId> = (0..3).map(|i| TaskId::new(format!("f{i}"))).collect();
let new_p = opt.meta_update(&ids, &init).expect("test: should succeed");
let changed = new_p.weights[0] != init.weights[0] || new_p.bias != init.bias;
assert!(changed);
}
#[test]
fn test_meta_update_protonet_basic() {
let config = OptimizerConfig {
algorithm: MetaAlgorithm::ProtoNet,
meta_lr: 0.01,
n_tasks_per_batch: 3,
max_params_dim: 2,
};
let mut opt = MetaLearningOptimizer::new(config);
for i in 0..3 {
let task = make_2d_task(&format!("p{i}"), 1.0, 1.0, 0.0, 6, 4, (i * 5 + 1) as u64);
opt.add_task(task).expect("test: should succeed");
}
let init = MetaLearningOptimizer::initialize_params(2, 11);
let ids: Vec<TaskId> = (0..3).map(|i| TaskId::new(format!("p{i}"))).collect();
let new_p = opt.meta_update(&ids, &init).expect("test: should succeed");
assert_eq!(new_p.dim, 2);
assert_eq!(opt.stats().meta_updates, 1);
}
#[test]
fn test_evaluate_task_perfect_params() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
let task = make_regression_task("t1", 2.0, 1.0, 5, 10, 13);
opt.add_task(task).expect("test: should succeed");
let params = ModelParams {
weights: vec![2.0],
bias: 1.0,
dim: 1,
};
let loss = opt
.evaluate_task(&TaskId::new("t1"), ¶ms)
.expect("test: should succeed");
assert!(
loss < 1e-20,
"perfect params should give ~0 MSE, got {loss}"
);
}
#[test]
fn test_evaluate_task_non_negative() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
let task = make_regression_task("t1", 3.0, -1.0, 5, 10, 17);
opt.add_task(task).expect("test: should succeed");
let params = MetaLearningOptimizer::initialize_params(1, 7);
let loss = opt
.evaluate_task(&TaskId::new("t1"), ¶ms)
.expect("test: should succeed");
assert!(loss >= 0.0);
}
#[test]
fn test_evaluate_task_unknown_error() {
let config = OptimizerConfig::default_maml(1);
let opt = MetaLearningOptimizer::new(config);
let params = MetaLearningOptimizer::initialize_params(1, 1);
let err = opt
.evaluate_task(&TaskId::new("no-such"), ¶ms)
.unwrap_err();
assert!(matches!(err, MetaError::AdaptationFailed(_)));
}
#[test]
fn test_evaluate_task_empty_query_error() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
let tid = TaskId::new("empty-q");
let sex = TaskExample::new(vec![1.0], 1.0, tid.clone());
let task = MetaTask::new(tid.clone(), vec![sex], vec![]);
opt.add_task(task).expect("test: should succeed");
let params = MetaLearningOptimizer::initialize_params(1, 1);
let err = opt.evaluate_task(&tid, ¶ms).unwrap_err();
assert!(matches!(err, MetaError::AdaptationFailed(_)));
}
#[test]
fn test_initialize_params_dim() {
let params = MetaLearningOptimizer::initialize_params(4, 42);
assert_eq!(params.dim, 4);
assert_eq!(params.weights.len(), 4);
}
#[test]
fn test_initialize_params_small_values() {
let params = MetaLearningOptimizer::initialize_params(100, 123);
for w in ¶ms.weights {
assert!(w.abs() <= 0.01, "weight {w} exceeds 0.01");
}
assert!(
params.bias.abs() <= 0.01,
"bias {} exceeds 0.01",
params.bias
);
}
#[test]
fn test_initialize_params_deterministic() {
let p1 = MetaLearningOptimizer::initialize_params(5, 77);
let p2 = MetaLearningOptimizer::initialize_params(5, 77);
assert_eq!(p1.weights, p2.weights);
assert_eq!(p1.bias, p2.bias);
}
#[test]
fn test_initialize_params_different_seeds() {
let p1 = MetaLearningOptimizer::initialize_params(5, 1);
let p2 = MetaLearningOptimizer::initialize_params(5, 2);
assert_ne!(
p1.weights, p2.weights,
"different seeds should give different weights"
);
}
#[test]
fn test_initialize_params_zero_seed_fallback() {
let params = MetaLearningOptimizer::initialize_params(3, 0);
assert_eq!(params.dim, 3);
}
#[test]
fn test_few_shot_predict_basic() {
let config = OptimizerConfig::default_maml(1);
let opt = MetaLearningOptimizer::new(config);
let tid = TaskId::new("fs1");
let support: Vec<TaskExample> = (0..8)
.map(|i| {
let x = i as f64;
TaskExample::new(vec![x], 2.0 * x + 1.0, tid.clone())
})
.collect();
let task = MetaTask::new(tid, support, vec![]);
let init = MetaLearningOptimizer::initialize_params(1, 42);
let pred = opt
.few_shot_predict(&task, &[3.0], &init)
.expect("test: should succeed");
assert!(pred.is_finite(), "prediction should be finite");
}
#[test]
fn test_few_shot_predict_dim_mismatch() {
let config = OptimizerConfig::default_maml(2);
let opt = MetaLearningOptimizer::new(config);
let tid = TaskId::new("fs2");
let support = vec![TaskExample::new(vec![1.0, 2.0], 3.0, tid.clone())];
let task = MetaTask::new(tid, support, vec![]);
let init = MetaLearningOptimizer::initialize_params(2, 9);
let err = opt.few_shot_predict(&task, &[1.0], &init).unwrap_err();
assert!(matches!(
err,
MetaError::DimensionMismatch {
expected: 2,
got: 1
}
));
}
#[test]
fn test_few_shot_predict_empty_support_error() {
let config = OptimizerConfig::default_maml(1);
let opt = MetaLearningOptimizer::new(config);
let tid = TaskId::new("fse");
let task = MetaTask::new(tid, vec![], vec![]);
let init = MetaLearningOptimizer::initialize_params(1, 1);
let err = opt.few_shot_predict(&task, &[1.0], &init).unwrap_err();
assert!(matches!(err, MetaError::AdaptationFailed(_)));
}
#[test]
fn test_few_shot_predict_reptile() {
let config = OptimizerConfig::default_reptile(1);
let opt = MetaLearningOptimizer::new(config);
let tid = TaskId::new("rfs");
let support: Vec<TaskExample> = (0..5)
.map(|i| {
let x = i as f64 * 0.5;
TaskExample::new(vec![x], 3.0 * x, tid.clone())
})
.collect();
let task = MetaTask::new(tid, support, vec![]);
let init = MetaLearningOptimizer::initialize_params(1, 7);
let pred = opt
.few_shot_predict(&task, &[2.0], &init)
.expect("test: should succeed");
assert!(pred.is_finite());
}
#[test]
fn test_few_shot_predict_adapts_correctly_2d() {
let config = OptimizerConfig {
algorithm: MetaAlgorithm::MAML {
inner_lr: 0.05,
inner_steps: 20,
},
meta_lr: 0.01,
n_tasks_per_batch: 2,
max_params_dim: 2,
};
let opt = MetaLearningOptimizer::new(config);
let tid = TaskId::new("2dfs");
let support: Vec<TaskExample> = (0..20)
.map(|i| {
let x0 = i as f64 * 0.1;
let x1 = (i as f64) * 0.2 - 1.0;
TaskExample::new(vec![x0, x1], 1.5 * x0 + 0.5 * x1, tid.clone())
})
.collect();
let task = MetaTask::new(tid, support, vec![]);
let init = MetaLearningOptimizer::initialize_params(2, 42);
let pred = opt
.few_shot_predict(&task, &[1.0, 0.0], &init)
.expect("test: should succeed");
assert!((pred - 1.5).abs() < 1.0, "pred {pred} should be near 1.5");
}
#[test]
fn test_stats_initial_state() {
let config = OptimizerConfig::default_maml(1);
let opt = MetaLearningOptimizer::new(config);
let stats = opt.stats();
assert_eq!(stats.tasks_trained, 0);
assert_eq!(stats.meta_updates, 0);
assert_eq!(stats.avg_adaptation_loss, 0.0);
assert_eq!(stats.avg_query_loss, 0.0);
}
#[test]
fn test_stats_tasks_trained_increments() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
for i in 0..5 {
let task = make_regression_task(&format!("t{i}"), 1.0, 0.0, 5, 5, i as u64 + 1);
opt.add_task(task).expect("test: should succeed");
}
assert_eq!(opt.stats().tasks_trained, 5);
}
#[test]
fn test_stats_meta_updates_increments() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
for i in 0..3 {
let task = make_regression_task(&format!("t{i}"), 1.0, 0.0, 5, 5, i as u64 + 1);
opt.add_task(task).expect("test: should succeed");
}
let mut params = MetaLearningOptimizer::initialize_params(1, 42);
let ids: Vec<TaskId> = (0..3).map(|i| TaskId::new(format!("t{i}"))).collect();
for n in 1..=5 {
params = opt
.meta_update(&ids, ¶ms)
.expect("test: should succeed");
assert_eq!(opt.stats().meta_updates, n);
}
}
#[test]
fn test_stats_avg_adaptation_loss_non_negative() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
for i in 0..4 {
let task =
make_regression_task(&format!("t{i}"), (i + 1) as f64, 0.5, 8, 4, i as u64 + 2);
opt.add_task(task).expect("test: should succeed");
}
let init = MetaLearningOptimizer::initialize_params(1, 9);
let ids: Vec<TaskId> = (0..4).map(|i| TaskId::new(format!("t{i}"))).collect();
opt.meta_update(&ids, &init).expect("test: should succeed");
assert!(opt.stats().avg_adaptation_loss >= 0.0);
}
#[test]
fn test_stats_query_loss_non_negative() {
let config = OptimizerConfig::default_maml(1);
let mut opt = MetaLearningOptimizer::new(config);
for i in 0..4 {
let task = make_regression_task(&format!("t{i}"), 1.0, 0.5, 8, 4, i as u64 + 3);
opt.add_task(task).expect("test: should succeed");
}
let init = MetaLearningOptimizer::initialize_params(1, 11);
let ids: Vec<TaskId> = (0..4).map(|i| TaskId::new(format!("t{i}"))).collect();
opt.meta_update(&ids, &init).expect("test: should succeed");
assert!(opt.stats().avg_query_loss >= 0.0);
}
#[test]
fn test_error_display_insufficient_tasks() {
let err = MetaError::InsufficientTasks(2);
assert!(err.to_string().contains("insufficient"));
}
#[test]
fn test_error_display_dimension_mismatch() {
let err = MetaError::DimensionMismatch {
expected: 4,
got: 3,
};
let s = err.to_string();
assert!(s.contains("4") && s.contains("3"));
}
#[test]
fn test_error_display_adaptation_failed() {
let err = MetaError::AdaptationFailed("oops".to_string());
assert!(err.to_string().contains("oops"));
}
#[test]
fn test_error_display_invalid_config() {
let err = MetaError::InvalidConfig("bad lr".to_string());
assert!(err.to_string().contains("bad lr"));
}
#[test]
fn test_error_is_clone() {
let err = MetaError::InsufficientTasks(3);
let err2 = err.clone();
assert_eq!(err, err2);
}
#[test]
fn test_xorshift64_deterministic() {
let mut s1 = 12345u64;
let mut s2 = 12345u64;
assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
}
#[test]
fn test_xorshift_f64_range() {
let mut state = 99999u64;
for _ in 0..1000 {
let v = xorshift_f64(&mut state);
assert!((0.0..1.0).contains(&v), "out of range: {v}");
}
}
#[test]
fn test_model_params_predict() {
let p = ModelParams {
weights: vec![2.0, -1.0],
bias: 0.5,
dim: 2,
};
let pred = p.predict(&[1.0, 1.0]);
assert!((pred - 1.5).abs() < 1e-12);
}
#[test]
fn test_model_params_zeros() {
let p = ModelParams::zeros(3);
assert_eq!(p.weights, vec![0.0, 0.0, 0.0]);
assert_eq!(p.bias, 0.0);
assert_eq!(p.dim, 3);
}
#[test]
fn test_mse_zero_on_perfect_fit() {
let p = ModelParams {
weights: vec![3.0],
bias: 0.0,
dim: 1,
};
let tid = TaskId::new("t");
let examples: Vec<TaskExample> = (0..5)
.map(|i| {
let x = i as f64;
TaskExample::new(vec![x], 3.0 * x, tid.clone())
})
.collect();
let (loss, _, _) = p.mse_and_grads(&examples);
assert!(loss < 1e-20, "MSE should be ~0 for perfect fit, got {loss}");
}
#[test]
fn test_end_to_end_maml_regression() {
let config = OptimizerConfig {
algorithm: MetaAlgorithm::MAML {
inner_lr: 0.05,
inner_steps: 5,
},
meta_lr: 0.1,
n_tasks_per_batch: 4,
max_params_dim: 1,
};
let mut opt = MetaLearningOptimizer::new(config);
for i in 0..4 {
let task = make_regression_task(
&format!("t{i}"),
2.0,
i as f64 * 0.5,
15,
5,
(i * 13 + 7) as u64,
);
opt.add_task(task).expect("test: should succeed");
}
let mut meta_params = MetaLearningOptimizer::initialize_params(1, 42);
let ids: Vec<TaskId> = (0..4).map(|i| TaskId::new(format!("t{i}"))).collect();
for _ in 0..30 {
meta_params = opt
.meta_update(&ids, &meta_params)
.expect("test: should succeed");
}
let tid = TaskId::new("new");
let new_support: Vec<TaskExample> = (0..5)
.map(|i| {
let x = i as f64 * 0.5;
TaskExample::new(vec![x], 2.0 * x + 0.3, tid.clone())
})
.collect();
let new_query: Vec<TaskExample> = (0..5)
.map(|i| {
let x = i as f64 * 0.5 + 0.1;
TaskExample::new(vec![x], 2.0 * x + 0.3, tid.clone())
})
.collect();
let new_task = MetaTask::new(tid.clone(), new_support, new_query);
opt.add_task(new_task).expect("test: should succeed");
let adapted = opt
.adapt_to_task(&tid, &meta_params, 10, 0.05)
.expect("test: should succeed");
let init_loss = adapted.first().map(|s| s.loss).unwrap_or(f64::MAX);
let final_loss = adapted.last().map(|s| s.loss).unwrap_or(f64::MAX);
assert!(
final_loss <= init_loss + 1e-6,
"adaptation should reduce loss: {init_loss} -> {final_loss}"
);
}
#[test]
fn test_end_to_end_reptile() {
let config = OptimizerConfig::default_reptile(1);
let mut opt = MetaLearningOptimizer::new(config);
for i in 0..4 {
let task = make_regression_task(
&format!("t{i}"),
1.5,
i as f64 * 0.2,
10,
5,
(i + 1) as u64 * 7,
);
opt.add_task(task).expect("test: should succeed");
}
let mut params = MetaLearningOptimizer::initialize_params(1, 33);
let ids: Vec<TaskId> = (0..4).map(|i| TaskId::new(format!("t{i}"))).collect();
for _ in 0..15 {
params = opt
.meta_update(&ids, ¶ms)
.expect("test: should succeed");
}
assert_eq!(opt.stats().meta_updates, 15);
assert!(opt.stats().avg_query_loss >= 0.0);
}
#[test]
fn test_task_id_display() {
let tid = TaskId::new("hello");
assert_eq!(tid.to_string(), "hello");
assert_eq!(tid.as_str(), "hello");
}
#[test]
fn test_meta_task_feature_dim() {
let tid = TaskId::new("t");
let ex = TaskExample::new(vec![1.0, 2.0, 3.0], 0.0, tid.clone());
let task = MetaTask::new(tid, vec![ex], vec![]);
assert_eq!(task.feature_dim(), Some(3));
}
#[test]
fn test_meta_task_feature_dim_empty() {
let tid = TaskId::new("t");
let task = MetaTask::new(tid, vec![], vec![]);
assert_eq!(task.feature_dim(), None);
}
}