//! Inter-process communication types for Tauri frontend/backend //! //! This module defines all message types used for communication between //! the Tauri frontend and the `RustyTorch`++ backend. //! //! # Example //! //! ```rust //! use rtx_neural_operator_shared::config::PDEConfig; //! use rtx_neural_operator_shared::ipc::{NeuralOperatorRequest, NeuralOperatorResponse}; //! //! // Create initialization request //! let config = PDEConfig::darcy(64); //! let request = NeuralOperatorRequest::initialize(config); //! //! // Serialize for IPC //! let json = serde_json::to_string(&request).unwrap(); //! //! // Create success response //! let response = NeuralOperatorResponse::initialized(64, 64); //! ``` use serde::{Deserialize, Serialize}; use crate::config::PDEConfig; use crate::error::NeuralOperatorError; /// IPC request types from frontend to backend #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", content = "payload")] pub enum NeuralOperatorRequest { /// Initialize the neural operator with configuration Initialize { /// PDE configuration config: PDEConfig, }, /// Solve PDE with given input field Solve { /// Input field (flattened [H, W] array) input: Vec, }, /// Set boundary conditions for the domain SetBoundaryConditions { /// Boundary mask (1.0 = boundary, 0.0 = interior) mask: Vec, /// Boundary values at masked points values: Vec, }, /// Get current solution (cached from last solve) GetSolution, /// Get performance metrics GetMetrics, /// Reset to initial state Reset, /// Get model information GetModelInfo, } impl NeuralOperatorRequest { /// Creates an initialize request #[must_use] pub fn initialize(config: PDEConfig) -> Self { Self::Initialize { config } } /// Creates a solve request #[must_use] pub fn solve(input: Vec) -> Self { Self::Solve { input } } /// Creates a set boundary conditions request #[must_use] pub fn set_boundary_conditions(mask: Vec, values: Vec) -> Self { Self::SetBoundaryConditions { mask, values } } /// Creates a get solution request #[must_use] pub const fn get_solution() -> Self { Self::GetSolution } /// Creates a get metrics request #[must_use] pub const fn get_metrics() -> Self { Self::GetMetrics } /// Creates a reset request #[must_use] pub const fn reset() -> Self { Self::Reset } /// Creates a get model info request #[must_use] pub const fn get_model_info() -> Self { Self::GetModelInfo } } /// IPC response from backend to frontend #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", content = "data")] pub enum NeuralOperatorResponse { /// Model initialized successfully Initialized { /// Grid width width: u32, /// Grid height height: u32, /// Model name/description model_name: String, }, /// Solution computed successfully Solution(SolutionData), /// Performance metrics Metrics(PerformanceMetrics), /// Model information ModelInfo(ModelInfo), /// Operation succeeded with no data Ok, /// Error occurred Error { /// Error code code: String, /// Error message message: String, }, } impl NeuralOperatorResponse { /// Creates an initialized response #[must_use] pub fn initialized(width: u32, height: u32) -> Self { Self::Initialized { width, height, model_name: format!("FNO2d ({width}x{height})"), } } /// Creates a solution response #[must_use] pub fn solution(data: SolutionData) -> Self { Self::Solution(data) } /// Creates a metrics response #[must_use] pub fn metrics(metrics: PerformanceMetrics) -> Self { Self::Metrics(metrics) } /// Creates a model info response #[must_use] pub fn model_info(info: ModelInfo) -> Self { Self::ModelInfo(info) } /// Creates an OK response #[must_use] pub const fn ok() -> Self { Self::Ok } /// Creates an error response from an error #[must_use] pub fn error(err: NeuralOperatorError) -> Self { Self::Error { code: err.code().to_string(), message: err.to_string(), } } /// Creates an error response from a message #[must_use] pub fn error_message(message: impl Into) -> Self { Self::Error { code: "ERROR".to_string(), message: message.into(), } } /// Returns whether the response indicates success #[must_use] pub const fn is_success(&self) -> bool { !matches!(self, Self::Error { .. }) } } /// Solution data from neural operator inference #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SolutionData { /// Solution field (flattened [H, W] array) pub solution: Vec, /// Grid width pub width: u32, /// Grid height pub height: u32, /// Neural operator inference time in milliseconds pub inference_time_ms: f64, /// FEM baseline time in milliseconds (if computed) pub fem_time_ms: Option, /// Minimum value in solution pub min_value: f32, /// Maximum value in solution pub max_value: f32, /// Mean value in solution pub mean_value: f32, } impl SolutionData { /// Creates new solution data #[must_use] pub fn new(solution: Vec, width: u32, height: u32, inference_time_ms: f64) -> Self { let (min_value, max_value, sum) = solution.iter().fold( (f32::INFINITY, f32::NEG_INFINITY, 0.0_f64), |(min, max, sum), &v| (min.min(v), max.max(v), sum + f64::from(v)), ); let mean_value = (sum / solution.len() as f64) as f32; Self { solution, width, height, inference_time_ms, fem_time_ms: None, min_value, max_value, mean_value, } } /// Adds FEM baseline timing #[must_use] pub fn with_fem_time(mut self, fem_time_ms: f64) -> Self { self.fem_time_ms = Some(fem_time_ms); self } /// Returns the speedup factor vs FEM (if available) #[must_use] pub fn speedup_factor(&self) -> Option { self.fem_time_ms.map(|fem| fem / self.inference_time_ms) } } /// Performance metrics for monitoring #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct PerformanceMetrics { /// Average inference time in milliseconds pub avg_inference_time_ms: f64, /// Minimum inference time in milliseconds pub min_inference_time_ms: f64, /// Maximum inference time in milliseconds pub max_inference_time_ms: f64, /// Number of inferences performed pub inference_count: u64, /// Memory usage in megabytes pub memory_usage_mb: f32, /// Throughput (inferences per second) pub throughput: f64, } impl PerformanceMetrics { /// Creates new performance metrics #[must_use] pub fn new() -> Self { Self { avg_inference_time_ms: 0.0, min_inference_time_ms: f64::INFINITY, max_inference_time_ms: 0.0, inference_count: 0, memory_usage_mb: 0.0, throughput: 0.0, } } /// Records a new inference timing pub fn record_inference(&mut self, time_ms: f64) { self.inference_count += 1; self.min_inference_time_ms = self.min_inference_time_ms.min(time_ms); self.max_inference_time_ms = self.max_inference_time_ms.max(time_ms); // Running average let n = self.inference_count as f64; self.avg_inference_time_ms = self.avg_inference_time_ms * (n - 1.0) / n + time_ms / n; // Throughput based on average if self.avg_inference_time_ms > 0.0 { self.throughput = 1000.0 / self.avg_inference_time_ms; } } /// Sets memory usage #[must_use] pub const fn with_memory(mut self, memory_mb: f32) -> Self { self.memory_usage_mb = memory_mb; self } /// Returns whether performance meets real-time requirements (<33ms for 30fps) #[must_use] pub fn is_realtime(&self) -> bool { self.avg_inference_time_ms < 33.0 } } impl Default for PerformanceMetrics { fn default() -> Self { Self::new() } } /// Model information #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ModelInfo { /// Model name pub name: String, /// PDE type pub pde_type: String, /// Input resolution pub resolution: u32, /// Number of Fourier modes pub n_modes: (u32, u32), /// Model width (hidden dimension) pub model_width: u32, /// Number of layers pub n_layers: u32, /// Total parameters pub total_params: u64, /// Model size in megabytes pub size_mb: f32, } impl ModelInfo { /// Creates new model info #[must_use] pub fn new( name: impl Into, pde_type: impl Into, resolution: u32, n_modes: (u32, u32), model_width: u32, n_layers: u32, ) -> Self { // Rough parameter count estimate for FNO2d let lifting_params = (3 * 2 * model_width) + (2 * model_width * model_width); let spectral_params = n_layers * 2 * model_width * model_width * n_modes.0 * n_modes.1 * 2; let conv_params = n_layers * (model_width * model_width + model_width); let projection_params = (model_width * 128) + 128; let total_params = u64::from(lifting_params + spectral_params + conv_params + projection_params); Self { name: name.into(), pde_type: pde_type.into(), resolution, n_modes, model_width, n_layers, total_params, size_mb: (total_params * 4) as f32 / (1024.0 * 1024.0), // 4 bytes per f32 } } } /// Helper function to get current timestamp in milliseconds #[must_use] pub fn current_timestamp_ms() -> u64 { use std::time::{SystemTime, UNIX_EPOCH}; SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_millis() as u64) .unwrap_or(0) } // ============================================================================= // BENCHMARK TYPES // ============================================================================= /// Benchmark request from frontend to backend #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", content = "payload")] pub enum BenchmarkRequest { /// Run benchmark comparing solvers RunBenchmark { /// Resolutions to test (e.g., [32, 64, 128, 256]) resolutions: Vec, /// Methods to benchmark (e.g., ["FNO", "FDM", "FEM"]) methods: Vec, /// PDE type ("Poisson", "Heat", "Darcy") pde_type: String, /// Number of trials for averaging n_trials: usize, }, /// Get status of running benchmark GetBenchmarkStatus, /// Cancel running benchmark CancelBenchmark, } impl BenchmarkRequest { /// Creates a run benchmark request #[must_use] pub fn run( resolutions: Vec, methods: Vec, pde_type: String, n_trials: usize, ) -> Self { Self::RunBenchmark { resolutions, methods, pde_type, n_trials, } } /// Creates a get status request #[must_use] pub const fn get_status() -> Self { Self::GetBenchmarkStatus } /// Creates a cancel request #[must_use] pub const fn cancel() -> Self { Self::CancelBenchmark } } /// Benchmark result data (serializable version of `BenchmarkResult`) #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct BenchmarkResultData { /// Solver method name pub method: String, /// Grid resolution pub resolution: usize, /// Solve time in milliseconds pub solve_time_ms: f64, /// L2 error vs reference (if available) pub l2_error: Option, /// Maximum error vs reference (if available) pub max_error: Option, /// Memory usage in megabytes pub memory_mb: f64, /// Number of iterations (for iterative methods) pub iterations: Option, /// PDE type pub pde_type: String, } impl BenchmarkResultData { /// Creates a new benchmark result #[must_use] pub fn new(method: impl Into, resolution: usize, pde_type: impl Into) -> Self { Self { method: method.into(), resolution, solve_time_ms: 0.0, l2_error: None, max_error: None, memory_mb: 0.0, iterations: None, pde_type: pde_type.into(), } } /// Sets solve time #[must_use] pub const fn with_time(mut self, time_ms: f64) -> Self { self.solve_time_ms = time_ms; self } /// Sets L2 error #[must_use] pub const fn with_l2_error(mut self, error: f64) -> Self { self.l2_error = Some(error); self } /// Sets max error #[must_use] pub const fn with_max_error(mut self, error: f64) -> Self { self.max_error = Some(error); self } /// Sets memory usage #[must_use] pub const fn with_memory(mut self, memory_mb: f64) -> Self { self.memory_mb = memory_mb; self } /// Sets iterations #[must_use] pub const fn with_iterations(mut self, iterations: usize) -> Self { self.iterations = Some(iterations); self } /// Computes speedup vs a baseline time #[must_use] pub fn speedup_vs(&self, baseline_time_ms: f64) -> f64 { baseline_time_ms / self.solve_time_ms } } /// Summary statistics for a solver across multiple runs (serializable) #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct BenchmarkSummaryData { /// Solver method name pub method: String, /// Resolution pub resolution: usize, /// Average solve time in milliseconds pub avg_time_ms: f64, /// Standard deviation of solve time pub std_time_ms: f64, /// Minimum solve time pub min_time_ms: f64, /// Maximum solve time pub max_time_ms: f64, /// Average L2 error (if available) pub avg_l2_error: Option, /// Average memory usage pub avg_memory_mb: f64, /// Number of runs pub n_runs: usize, /// PDE type pub pde_type: String, } impl BenchmarkSummaryData { /// Computes speedup factor vs another solver #[must_use] pub fn speedup_vs(&self, other: &Self) -> f64 { other.avg_time_ms / self.avg_time_ms } } /// Benchmark response from backend to frontend #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", content = "data")] pub enum BenchmarkResponse { /// Benchmark results computed Results { /// Individual benchmark results results: Vec, /// Summary statistics (if multiple trials) summaries: Option>, /// Speedup analysis report speedup_report: Option, }, /// Benchmark status update Status { /// Current progress (0.0 to 1.0) progress: f64, /// Status message message: String, /// Current method being benchmarked current_method: Option, /// Current resolution being tested current_resolution: Option, }, /// Benchmark completed Complete { /// Final results results: Vec, }, /// Benchmark cancelled Cancelled, /// Error occurred Error { /// Error code code: String, /// Error message message: String, }, } impl BenchmarkResponse { /// Creates a results response #[must_use] pub fn results(results: Vec) -> Self { Self::Results { results, summaries: None, speedup_report: None, } } /// Creates a results response with summaries #[must_use] pub fn results_with_summaries( results: Vec, summaries: Vec, ) -> Self { Self::Results { results, summaries: Some(summaries), speedup_report: None, } } /// Creates a results response with summaries and speedup report #[must_use] pub fn results_with_analysis( results: Vec, summaries: Vec, speedup_report: String, ) -> Self { Self::Results { results, summaries: Some(summaries), speedup_report: Some(speedup_report), } } /// Creates a status response #[must_use] pub fn status(progress: f64, message: impl Into) -> Self { Self::Status { progress, message: message.into(), current_method: None, current_resolution: None, } } /// Creates a complete response #[must_use] pub fn complete(results: Vec) -> Self { Self::Complete { results } } /// Creates a cancelled response #[must_use] pub const fn cancelled() -> Self { Self::Cancelled } /// Creates an error response #[must_use] pub fn error(code: impl Into, message: impl Into) -> Self { Self::Error { code: code.into(), message: message.into(), } } /// Returns whether the response indicates success #[must_use] pub const fn is_success(&self) -> bool { !matches!(self, Self::Error { .. }) } } // ============================================================================= // TRAINING TYPES // ============================================================================= /// Training configuration from frontend #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct TrainingConfig { /// Number of training epochs pub epochs: usize, /// Batch size for training pub batch_size: usize, /// Learning rate pub learning_rate: f32, /// Number of training samples to generate pub n_train_samples: usize, /// Number of validation samples pub n_val_samples: usize, } impl Default for TrainingConfig { fn default() -> Self { Self { epochs: 50, batch_size: 16, learning_rate: 1e-3, n_train_samples: 1000, n_val_samples: 100, } } } impl TrainingConfig { /// Creates a new training config with default values #[must_use] pub fn new() -> Self { Self::default() } /// Sets the number of epochs #[must_use] pub const fn with_epochs(mut self, epochs: usize) -> Self { self.epochs = epochs; self } /// Sets the batch size #[must_use] pub const fn with_batch_size(mut self, batch_size: usize) -> Self { self.batch_size = batch_size; self } /// Sets the learning rate #[must_use] pub const fn with_learning_rate(mut self, learning_rate: f32) -> Self { self.learning_rate = learning_rate; self } /// Sets the number of training samples #[must_use] pub const fn with_train_samples(mut self, n_samples: usize) -> Self { self.n_train_samples = n_samples; self } /// Quick training preset (for testing) #[must_use] pub fn quick() -> Self { Self { epochs: 10, batch_size: 8, learning_rate: 1e-3, n_train_samples: 100, n_val_samples: 20, } } /// Standard training preset #[must_use] pub fn standard() -> Self { Self::default() } /// Extended training preset (higher quality) #[must_use] pub fn extended() -> Self { Self { epochs: 200, batch_size: 32, learning_rate: 5e-4, n_train_samples: 5000, n_val_samples: 500, } } } /// Training status enum #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "status", content = "details")] pub enum TrainingStatus { /// Training has not started NotStarted, /// Generating training data GeneratingData { /// Samples generated so far samples_generated: usize, /// Total samples to generate total_samples: usize, }, /// Training in progress Training, /// Training completed successfully Complete, /// Training was cancelled by user Cancelled, /// Training failed with error Error(String), } impl TrainingStatus { /// Returns whether training is in progress #[must_use] pub fn is_running(&self) -> bool { matches!(self, Self::GeneratingData { .. } | Self::Training) } /// Returns whether training has finished (success, cancel, or error) #[must_use] pub fn is_finished(&self) -> bool { matches!(self, Self::Complete | Self::Cancelled | Self::Error(_)) } } /// Training progress update (sent to frontend via polling) #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct TrainingProgress { /// Current epoch (1-indexed) pub epoch: usize, /// Total number of epochs pub total_epochs: usize, /// Current batch within epoch pub batch: usize, /// Total batches per epoch pub total_batches: usize, /// Current training loss pub loss: f32, /// Best loss achieved so far pub best_loss: f32, /// Validation loss (if available) pub val_loss: Option, /// Training samples processed per second pub samples_per_sec: f32, /// Estimated time remaining in seconds pub eta_seconds: f32, /// Elapsed time in seconds pub elapsed_seconds: f32, /// Current training status pub status: TrainingStatus, /// Loss history (last N epochs) pub loss_history: Vec, /// Device used for training (e.g., "CPU", "CUDA (RTX 4090)", "Metal (M2 Max)") #[serde(default)] pub device: String, /// Current learning rate (may change with LR scheduling) #[serde(default)] pub current_lr: f32, } impl TrainingProgress { /// Creates a new training progress with initial values #[must_use] pub fn new(total_epochs: usize, total_batches: usize) -> Self { Self { epoch: 0, total_epochs, batch: 0, total_batches, loss: f32::INFINITY, best_loss: f32::INFINITY, val_loss: None, samples_per_sec: 0.0, eta_seconds: 0.0, elapsed_seconds: 0.0, status: TrainingStatus::NotStarted, loss_history: Vec::new(), device: String::from("CPU"), current_lr: 0.0, } } /// Creates progress indicating data generation phase #[must_use] pub fn generating_data(samples_generated: usize, total_samples: usize) -> Self { Self { epoch: 0, total_epochs: 0, batch: 0, total_batches: 0, loss: f32::INFINITY, best_loss: f32::INFINITY, val_loss: None, samples_per_sec: 0.0, eta_seconds: 0.0, elapsed_seconds: 0.0, status: TrainingStatus::GeneratingData { samples_generated, total_samples, }, loss_history: Vec::new(), device: String::from("CPU"), current_lr: 0.0, } } /// Creates progress indicating completion #[must_use] pub fn complete(best_loss: f32, elapsed_seconds: f32, loss_history: Vec) -> Self { Self { epoch: loss_history.len(), total_epochs: loss_history.len(), batch: 0, total_batches: 0, loss: best_loss, best_loss, val_loss: None, samples_per_sec: 0.0, eta_seconds: 0.0, elapsed_seconds, status: TrainingStatus::Complete, loss_history, device: String::from("CPU"), current_lr: 0.0, } } /// Creates progress indicating cancellation #[must_use] pub fn cancelled() -> Self { Self { status: TrainingStatus::Cancelled, ..Self::new(0, 0) } } /// Creates progress indicating error #[must_use] pub fn error(message: impl Into) -> Self { Self { status: TrainingStatus::Error(message.into()), ..Self::new(0, 0) } } /// Sets the device name for training #[must_use] pub fn with_device(mut self, device: impl Into) -> Self { self.device = device.into(); self } /// Sets the current learning rate #[must_use] pub fn with_learning_rate(mut self, lr: f32) -> Self { self.current_lr = lr; self } /// Returns the progress as a percentage (0.0 to 1.0) #[must_use] pub fn progress_fraction(&self) -> f32 { if self.total_epochs == 0 { return 0.0; } let epoch_progress = self.epoch as f32 / self.total_epochs as f32; let batch_progress = if self.total_batches > 0 { self.batch as f32 / self.total_batches as f32 / self.total_epochs as f32 } else { 0.0 }; epoch_progress + batch_progress } } #[cfg(test)] mod tests { use super::*; use crate::config::PDEConfig; #[test] fn test_request_serialization() { let config = PDEConfig::darcy(64); let request = NeuralOperatorRequest::initialize(config); let json = serde_json::to_string(&request).unwrap(); let deserialized: NeuralOperatorRequest = serde_json::from_str(&json).unwrap(); assert!(matches!( deserialized, NeuralOperatorRequest::Initialize { .. } )); } #[test] fn test_response_success() { let response = NeuralOperatorResponse::initialized(64, 64); assert!(response.is_success()); } #[test] fn test_response_error() { let response = NeuralOperatorResponse::error(NeuralOperatorError::NotInitialized); assert!(!response.is_success()); } #[test] fn test_solution_data_stats() { let solution = vec![1.0, 2.0, 3.0, 4.0]; let data = SolutionData::new(solution, 2, 2, 10.0); assert!((data.min_value - 1.0).abs() < f32::EPSILON); assert!((data.max_value - 4.0).abs() < f32::EPSILON); assert!((data.mean_value - 2.5).abs() < f32::EPSILON); } #[test] fn test_speedup_factor() { let solution = vec![1.0; 4]; let data = SolutionData::new(solution, 2, 2, 10.0).with_fem_time(1000.0); assert!((data.speedup_factor().unwrap() - 100.0).abs() < f64::EPSILON); } #[test] fn test_performance_metrics_recording() { let mut metrics = PerformanceMetrics::new(); metrics.record_inference(10.0); metrics.record_inference(20.0); assert_eq!(metrics.inference_count, 2); assert!((metrics.avg_inference_time_ms - 15.0).abs() < f64::EPSILON); assert!((metrics.min_inference_time_ms - 10.0).abs() < f64::EPSILON); assert!((metrics.max_inference_time_ms - 20.0).abs() < f64::EPSILON); } #[test] fn test_model_info() { let info = ModelInfo::new("FNO2d", "darcy_flow", 64, (12, 12), 32, 4); assert!(info.total_params > 0); assert!(info.size_mb > 0.0); } // ============================================================================= // BENCHMARK IPC TESTS (RED PHASE - THESE WILL FAIL UNTIL WE IMPLEMENT) // ============================================================================= #[test] fn test_benchmark_request_creation() { let request = BenchmarkRequest::run( vec![32, 64, 128], vec!["FNO".to_string(), "FDM".to_string(), "FEM".to_string()], "Poisson".to_string(), 5, ); assert!(matches!(request, BenchmarkRequest::RunBenchmark { .. })); } #[test] fn test_benchmark_request_serialization() { let request = BenchmarkRequest::run(vec![64], vec!["FNO".to_string()], "Darcy".to_string(), 1); let json = serde_json::to_string(&request).unwrap(); let deserialized: BenchmarkRequest = serde_json::from_str(&json).unwrap(); assert_eq!(request, deserialized); } #[test] fn test_benchmark_result_data_creation() { let result = BenchmarkResultData { method: "FNO".to_string(), resolution: 64, solve_time_ms: 5.0, l2_error: Some(0.001), max_error: Some(0.01), memory_mb: 10.5, iterations: None, pde_type: "Poisson".to_string(), }; assert_eq!(result.method, "FNO"); assert_eq!(result.resolution, 64); assert!(result.solve_time_ms > 0.0); } #[test] fn test_benchmark_response_creation() { let results = vec![ BenchmarkResultData { method: "FNO".to_string(), resolution: 64, solve_time_ms: 5.0, l2_error: Some(0.001), max_error: None, memory_mb: 10.0, iterations: None, pde_type: "Poisson".to_string(), }, BenchmarkResultData { method: "FDM-SOR".to_string(), resolution: 64, solve_time_ms: 500.0, l2_error: Some(0.0001), max_error: None, memory_mb: 2.0, iterations: Some(1500), pde_type: "Poisson".to_string(), }, ]; let response = BenchmarkResponse::results(results); assert!(matches!(response, BenchmarkResponse::Results { .. })); } #[test] fn test_benchmark_response_serialization() { let results = vec![BenchmarkResultData { method: "FEM-CG".to_string(), resolution: 32, solve_time_ms: 100.0, l2_error: Some(0.0005), max_error: Some(0.005), memory_mb: 3.5, iterations: Some(800), pde_type: "Heat".to_string(), }]; let response = BenchmarkResponse::results(results); let json = serde_json::to_string(&response).unwrap(); let deserialized: BenchmarkResponse = serde_json::from_str(&json).unwrap(); assert!(matches!(deserialized, BenchmarkResponse::Results { .. })); } #[test] fn test_benchmark_speedup_computation() { let result_data = BenchmarkResultData { method: "FNO".to_string(), resolution: 128, solve_time_ms: 10.0, l2_error: Some(0.002), max_error: None, memory_mb: 15.0, iterations: None, pde_type: "Darcy".to_string(), }; let baseline_time = 1000.0; let speedup = result_data.speedup_vs(baseline_time); assert!((speedup - 100.0).abs() < f64::EPSILON); } #[test] fn test_benchmark_summary_data() { let summary = BenchmarkSummaryData { method: "FDM-SOR".to_string(), resolution: 64, avg_time_ms: 450.0, std_time_ms: 25.0, min_time_ms: 420.0, max_time_ms: 480.0, avg_l2_error: Some(0.0001), avg_memory_mb: 2.0, n_runs: 10, pde_type: "Poisson".to_string(), }; assert_eq!(summary.n_runs, 10); assert!(summary.avg_time_ms > summary.min_time_ms); assert!(summary.avg_time_ms < summary.max_time_ms); } }