use anyhow::{Context, Result};
use burn::module::Module;
use burn::record::{BinFileRecorder, FullPrecisionSettings};
use burn::tensor::backend::Backend;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use super::step_3_gru_model_arch::TimeSeriesGru;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ModelMetadata {
pub input_size: usize,
pub hidden_size: usize,
pub output_size: usize,
pub num_layers: usize,
pub bidirectional: bool,
pub dropout: f64,
pub learning_rate: f64,
pub timestamp: u64,
pub description: String,
}
pub fn save_model<B: Backend>(
model: &TimeSeriesGru<B>,
metadata: ModelMetadata,
path: PathBuf,
) -> Result<PathBuf> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let timestamp = metadata.timestamp;
let datetime = DateTime::<Utc>::from_timestamp(timestamp as i64, 0)
.unwrap_or_else(|| DateTime::<Utc>::from_timestamp(0, 0).unwrap());
let date_str = datetime.format("%Y%m%d_%H%M%S").to_string();
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("gru_model");
let filename = format!("{}_{}.bin", stem, date_str);
let metadata_filename = format!("{}_{}_meta.json", stem, date_str);
let parent = path.parent().unwrap_or_else(|| Path::new(""));
let full_path = parent.join(&filename);
let metadata_path = parent.join(&metadata_filename);
model
.clone()
.save_file::<BinFileRecorder<FullPrecisionSettings>, _>(&full_path, &Default::default())
.context(format!(
"Failed to save GRU model to {}",
full_path.display()
))?;
let metadata_bytes = serde_json::to_vec(&metadata)?;
let mut metadata_file = fs::File::create(&metadata_path)?;
metadata_file.write_all(&metadata_bytes)?;
println!(
"Saved GRU model to {} with metadata at {}",
full_path.display(),
metadata_path.display()
);
Ok(full_path)
}
pub fn load_model<B: Backend>(
path: &Path,
device: &B::Device,
) -> Result<(TimeSeriesGru<B>, ModelMetadata)> {
let model_path = if path.extension().map_or(false, |ext| ext == "bin") {
path.to_path_buf()
} else {
path.with_extension("bin")
};
if !model_path.exists() {
if let Some(parent) = model_path.parent() {
if let Some(stem) = model_path.file_stem().and_then(|s| s.to_str()) {
if let Ok(entries) = fs::read_dir(parent) {
let model_files: Vec<_> = entries
.filter_map(Result::ok)
.filter(|entry| {
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
file_name_str.starts_with(stem) && file_name_str.ends_with(".bin")
})
.collect();
if !model_files.is_empty() {
let most_recent = model_files
.into_iter()
.max_by_key(|entry| {
entry
.metadata()
.map(|m| m.modified())
.unwrap_or_else(|_| Ok(std::time::SystemTime::UNIX_EPOCH))
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
})
.unwrap();
println!(
"Using most recent model file: {}",
most_recent.path().display()
);
return load_model(&most_recent.path(), device);
}
}
}
}
return Err(anyhow::anyhow!(
"Model file not found: {}",
model_path.display()
));
}
let model_stem = model_path
.file_stem()
.and_then(|s| s.to_str())
.context("Invalid path")?;
let parent = model_path.parent().unwrap_or_else(|| Path::new(""));
let mut metadata_path = parent.join(format!("{}_meta.json", model_stem));
if !metadata_path.exists() {
if let Ok(entries) = fs::read_dir(parent) {
let meta_files: Vec<_> = entries
.filter_map(Result::ok)
.filter(|entry| {
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
file_name_str.contains(model_stem) && file_name_str.ends_with("_meta.json")
})
.collect();
if !meta_files.is_empty() {
let most_recent = meta_files
.into_iter()
.max_by_key(|entry| {
entry
.metadata()
.map(|m| m.modified())
.unwrap_or_else(|_| Ok(std::time::SystemTime::UNIX_EPOCH))
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
})
.unwrap();
metadata_path = most_recent.path();
println!("Using metadata file: {}", metadata_path.display());
}
}
}
let metadata: ModelMetadata = if metadata_path.exists() {
let metadata_bytes = fs::read(&metadata_path)?;
serde_json::from_slice(&metadata_bytes)?
} else {
println!("Warning: No metadata file found. Using default model architecture.");
ModelMetadata {
input_size: 12, hidden_size: 64, output_size: 1, num_layers: 1, bidirectional: false, dropout: 0.15, learning_rate: 0.001, timestamp: 0, description: "Default model architecture (no metadata found)".to_string(),
}
};
println!("Creating a new GRU model with the saved architecture from metadata");
let model = TimeSeriesGru::new(
metadata.input_size,
metadata.hidden_size,
metadata.output_size,
metadata.num_layers,
metadata.bidirectional,
metadata.dropout,
device,
);
Ok((model, metadata))
}