use serde::{Deserialize, Serialize};
use std::path::Path;
use crate::error::{CliError, CliResult};
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ProjectConfig {
pub project: ProjectMeta,
#[serde(default)]
pub training: TrainingConfig,
#[serde(default)]
pub model: ModelConfig,
#[serde(default)]
pub data: DataConfig,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ProjectMeta {
pub name: String,
#[serde(default = "default_version")]
pub version: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub authors: Vec<String>,
}
fn default_version() -> String {
"0.1.0".to_string()
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TrainingConfig {
#[serde(default = "default_epochs")]
pub epochs: usize,
#[serde(default = "default_batch_size")]
pub batch_size: usize,
#[serde(default = "default_lr")]
pub learning_rate: f64,
#[serde(default)]
pub optimizer: OptimizerConfig,
#[serde(default)]
pub scheduler: Option<SchedulerConfig>,
#[serde(default = "default_device")]
pub device: String,
#[serde(default)]
pub seed: Option<u64>,
#[serde(default = "default_checkpoint_freq")]
pub checkpoint_frequency: usize,
#[serde(default = "default_output_dir")]
pub output_dir: String,
#[serde(default = "default_workers")]
pub num_workers: usize,
#[serde(default)]
pub gradient_clip: Option<f64>,
#[serde(default)]
pub mixed_precision: bool,
}
impl Default for TrainingConfig {
fn default() -> Self {
Self {
epochs: default_epochs(),
batch_size: default_batch_size(),
learning_rate: default_lr(),
optimizer: OptimizerConfig::default(),
scheduler: None,
device: default_device(),
seed: None,
checkpoint_frequency: default_checkpoint_freq(),
output_dir: default_output_dir(),
num_workers: default_workers(),
gradient_clip: None,
mixed_precision: false,
}
}
}
fn default_epochs() -> usize {
10
}
fn default_batch_size() -> usize {
32
}
fn default_lr() -> f64 {
0.001
}
fn default_device() -> String {
"cpu".to_string()
}
fn default_checkpoint_freq() -> usize {
1
}
fn default_output_dir() -> String {
"./output".to_string()
}
fn default_workers() -> usize {
4
}
#[derive(Debug, Serialize, Deserialize)]
pub struct OptimizerConfig {
#[serde(default = "default_optimizer")]
pub name: String,
#[serde(default)]
pub weight_decay: f64,
#[serde(default)]
pub momentum: f64,
#[serde(default = "default_beta1")]
pub beta1: f64,
#[serde(default = "default_beta2")]
pub beta2: f64,
#[serde(default = "default_eps")]
pub eps: f64,
#[serde(default)]
pub nesterov: bool,
}
impl Default for OptimizerConfig {
fn default() -> Self {
Self {
name: default_optimizer(),
weight_decay: 0.0,
momentum: 0.0,
beta1: default_beta1(),
beta2: default_beta2(),
eps: default_eps(),
nesterov: false,
}
}
}
fn default_optimizer() -> String {
"adam".to_string()
}
fn default_beta1() -> f64 {
0.9
}
fn default_beta2() -> f64 {
0.999
}
fn default_eps() -> f64 {
1e-8
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SchedulerConfig {
pub name: String,
#[serde(default)]
pub step_size: Option<usize>,
#[serde(default = "default_gamma")]
pub gamma: f64,
#[serde(default)]
pub milestones: Vec<usize>,
#[serde(default)]
pub t_max: Option<usize>,
#[serde(default)]
pub eta_min: f64,
#[serde(default)]
pub warmup_epochs: usize,
}
fn default_gamma() -> f64 {
0.1
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ModelConfig {
#[serde(default)]
pub architecture: String,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub input_size: Option<usize>,
#[serde(default)]
pub num_classes: Option<usize>,
#[serde(default)]
pub hidden_sizes: Vec<usize>,
#[serde(default)]
pub dropout: f64,
#[serde(default)]
pub pretrained: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DataConfig {
#[serde(default)]
pub train_path: Option<String>,
#[serde(default)]
pub val_path: Option<String>,
#[serde(default)]
pub test_path: Option<String>,
#[serde(default)]
pub format: String,
#[serde(default = "default_val_split")]
pub val_split: f64,
#[serde(default)]
pub augmentation: bool,
#[serde(default = "default_shuffle")]
pub shuffle: bool,
#[serde(default)]
pub normalize: bool,
#[serde(default)]
pub mean: Vec<f64>,
#[serde(default)]
pub std: Vec<f64>,
}
fn default_val_split() -> f64 {
0.1
}
fn default_shuffle() -> bool {
true
}
impl ProjectConfig {
pub fn load<P: AsRef<Path>>(path: P) -> CliResult<Self> {
let content = std::fs::read_to_string(path.as_ref())?;
let config: ProjectConfig = toml::from_str(&content)?;
Ok(config)
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> CliResult<()> {
let content =
toml::to_string_pretty(self).map_err(|e| CliError::Serialization(e.to_string()))?;
std::fs::write(path, content)?;
Ok(())
}
pub fn new(name: &str) -> Self {
Self {
project: ProjectMeta {
name: name.to_string(),
version: "0.1.0".to_string(),
description: String::new(),
authors: vec![],
},
training: TrainingConfig::default(),
model: ModelConfig::default(),
data: DataConfig::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = ProjectConfig::new("test-project");
assert_eq!(config.project.name, "test-project");
assert_eq!(config.training.epochs, 10);
assert_eq!(config.training.batch_size, 32);
}
#[test]
fn test_config_serialization() {
let config = ProjectConfig::new("test-project");
let toml_str = toml::to_string_pretty(&config).unwrap();
let parsed: ProjectConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(parsed.project.name, "test-project");
}
}