//! 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_hemodynamics_shared::geometry::VesselGeometry; //! use rtx_hemodynamics_shared::physics::SimulationConfig; //! use rtx_hemodynamics_shared::ipc::{IpcRequest, IpcResponse}; //! //! // Create initialization request //! let geometry = VesselGeometry::straight(0.1, 0.005).unwrap(); //! let config = SimulationConfig::default(); //! let request = IpcRequest::initialize(geometry, config); //! //! // Serialize for IPC //! let json = serde_json::to_string(&request).unwrap(); //! //! // Create success response //! let response = IpcResponse::success(serde_json::json!({"status": "initialized"})); //! ``` use crate::fields::{FieldQuery, FieldResponse}; use crate::geometry::{GeometryModification, VesselGeometry}; use crate::physics::SimulationConfig; use serde::{Deserialize, Serialize}; /// IPC request types from frontend to backend #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", content = "payload")] pub enum IpcRequest { /// Initialize simulation with geometry and config Initialize { /// Vessel geometry geometry: VesselGeometry, /// Simulation configuration config: SimulationConfig, }, /// Query field values at specific points QueryFields(FieldQuery), /// Modify vessel geometry ModifyGeometry(GeometryModification), /// Start training/optimization StartTraining { /// Number of training epochs epochs: usize, }, /// Stop training StopTraining, /// Get current simulation state GetState, /// Get performance metrics GetMetrics, /// Reset simulation to initial state Reset, /// Export simulation results Export { /// Output format (json, vtk, csv) format: ExportFormat, /// Output path path: String, }, } impl IpcRequest { /// Creates an initialize request #[must_use] pub fn initialize(geometry: VesselGeometry, config: SimulationConfig) -> Self { Self::Initialize { geometry, config } } /// Creates a query fields request #[must_use] pub fn query_fields(query: FieldQuery) -> Self { Self::QueryFields(query) } /// Creates a modify geometry request #[must_use] pub fn modify_geometry(modification: GeometryModification) -> Self { Self::ModifyGeometry(modification) } /// Creates a start training request #[must_use] pub const fn start_training(epochs: usize) -> Self { Self::StartTraining { epochs } } /// Creates a get state request #[must_use] pub const fn get_state() -> Self { Self::GetState } /// 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 an export request #[must_use] pub fn export(format: ExportFormat, path: String) -> Self { Self::Export { format, path } } } /// Export format options #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ExportFormat { /// JSON format Json, /// VTK format for visualization Vtk, /// CSV format for data analysis Csv, } /// IPC response from backend to frontend #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct IpcResponse { /// Whether the request succeeded success: bool, /// Response data (if success) data: Option, /// Error message (if failure) error: Option, /// Response timestamp timestamp_ms: u64, } impl IpcResponse { /// Creates a success response with data #[must_use] pub fn success(data: serde_json::Value) -> Self { Self { success: true, data: Some(data), error: None, timestamp_ms: current_timestamp_ms(), } } /// Creates an error response #[must_use] pub fn error(message: impl Into) -> Self { Self { success: false, data: None, error: Some(message.into()), timestamp_ms: current_timestamp_ms(), } } /// Creates a success response without data #[must_use] pub fn ok() -> Self { Self { success: true, data: None, error: None, timestamp_ms: current_timestamp_ms(), } } /// Creates a response with field data #[must_use] pub fn with_fields(fields: FieldResponse) -> Self { Self::success(serde_json::to_value(fields).unwrap_or_default()) } /// Creates a response with state data #[must_use] pub fn with_state(state: SimulationState) -> Self { Self::success(serde_json::to_value(state).unwrap_or_default()) } /// Creates a response with metrics data #[must_use] pub fn with_metrics(metrics: PerformanceMetrics) -> Self { Self::success(serde_json::to_value(metrics).unwrap_or_default()) } /// Returns whether the response indicates success #[must_use] pub const fn is_success(&self) -> bool { self.success } /// Returns the response data #[must_use] pub const fn data(&self) -> &Option { &self.data } /// Returns the error message if present #[must_use] pub fn error_message(&self) -> Option<&str> { self.error.as_deref() } /// Returns the timestamp in milliseconds #[must_use] pub const fn timestamp_ms(&self) -> u64 { self.timestamp_ms } } /// Current simulation state #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SimulationState { /// Whether simulation is initialized initialized: bool, /// Whether training is in progress training: bool, /// Current training epoch (if training) current_epoch: usize, /// Total training epochs total_epochs: usize, /// Current loss value loss: f64, /// Best loss achieved best_loss: f64, /// Whether the model is converged converged: bool, } impl SimulationState { /// Creates a new simulation state #[must_use] pub fn new() -> Self { Self { initialized: false, training: false, current_epoch: 0, total_epochs: 0, loss: f64::INFINITY, best_loss: f64::INFINITY, converged: false, } } /// Returns whether simulation is initialized #[must_use] pub const fn is_initialized(&self) -> bool { self.initialized } /// Returns whether training is in progress #[must_use] pub const fn is_training(&self) -> bool { self.training } /// Returns the current epoch #[must_use] pub const fn current_epoch(&self) -> usize { self.current_epoch } /// Returns the total epochs #[must_use] pub const fn total_epochs(&self) -> usize { self.total_epochs } /// Returns the current loss #[must_use] pub const fn loss(&self) -> f64 { self.loss } /// Returns the best loss #[must_use] pub const fn best_loss(&self) -> f64 { self.best_loss } /// Returns whether converged #[must_use] pub const fn is_converged(&self) -> bool { self.converged } /// Returns training progress as percentage #[must_use] pub fn progress_percent(&self) -> f64 { if self.total_epochs == 0 { 0.0 } else { 100.0 * self.current_epoch as f64 / self.total_epochs as f64 } } /// Sets the initialized flag #[must_use] pub const fn with_initialized(mut self, initialized: bool) -> Self { self.initialized = initialized; self } /// Sets the training flag #[must_use] pub const fn with_training(mut self, training: bool) -> Self { self.training = training; self } /// Sets the current epoch #[must_use] pub const fn with_epoch(mut self, current: usize, total: usize) -> Self { self.current_epoch = current; self.total_epochs = total; self } /// Sets the loss values #[must_use] pub const fn with_loss(mut self, loss: f64, best: f64) -> Self { self.loss = loss; self.best_loss = best; self } /// Sets the converged flag #[must_use] pub const fn with_converged(mut self, converged: bool) -> Self { self.converged = converged; self } } impl Default for SimulationState { fn default() -> Self { Self::new() } } /// Performance metrics for monitoring #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct PerformanceMetrics { /// Inference time in milliseconds inference_time_ms: f64, /// GPU utilization (0.0 to 1.0) gpu_utilization: f64, /// Memory usage in megabytes memory_usage_mb: usize, /// Training throughput (samples/second) throughput: f64, /// Average loss over recent iterations avg_loss: f64, } impl PerformanceMetrics { /// Creates new performance metrics #[must_use] pub fn new(inference_time_ms: f64, gpu_utilization: f64, memory_usage_mb: usize) -> Self { Self { inference_time_ms, gpu_utilization, memory_usage_mb, throughput: 0.0, avg_loss: 0.0, } } /// Creates metrics with full data #[must_use] pub fn full( inference_time_ms: f64, gpu_utilization: f64, memory_usage_mb: usize, throughput: f64, avg_loss: f64, ) -> Self { Self { inference_time_ms, gpu_utilization, memory_usage_mb, throughput, avg_loss, } } /// Returns inference time in milliseconds #[must_use] pub const fn inference_time_ms(&self) -> f64 { self.inference_time_ms } /// Returns GPU utilization (0.0 to 1.0) #[must_use] pub const fn gpu_utilization(&self) -> f64 { self.gpu_utilization } /// Returns memory usage in megabytes #[must_use] pub const fn memory_usage_mb(&self) -> usize { self.memory_usage_mb } /// Returns training throughput #[must_use] pub const fn throughput(&self) -> f64 { self.throughput } /// Returns average loss #[must_use] pub const fn avg_loss(&self) -> f64 { self.avg_loss } /// Estimates FPS from inference time #[must_use] pub fn estimated_fps(&self) -> f64 { if self.inference_time_ms <= 0.0 { 0.0 } else { 1000.0 / self.inference_time_ms } } /// Returns whether performance meets real-time requirements (<33ms for 30fps) #[must_use] pub fn is_realtime(&self) -> bool { self.inference_time_ms < 33.0 } } impl Default for PerformanceMetrics { fn default() -> Self { Self::new(0.0, 0.0, 0) } } /// Training progress update message #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct TrainingProgress { /// Current epoch pub epoch: usize, /// Total epochs pub total_epochs: usize, /// Current loss pub loss: f64, /// Physics loss component pub physics_loss: f64, /// Data loss component pub data_loss: f64, /// Learning rate pub learning_rate: f64, /// Elapsed time in seconds pub elapsed_secs: f64, } impl TrainingProgress { /// Creates a new training progress update #[must_use] pub fn new( epoch: usize, total_epochs: usize, loss: f64, physics_loss: f64, data_loss: f64, learning_rate: f64, elapsed_secs: f64, ) -> Self { Self { epoch, total_epochs, loss, physics_loss, data_loss, learning_rate, elapsed_secs, } } /// Returns progress as percentage #[must_use] pub fn progress_percent(&self) -> f64 { if self.total_epochs == 0 { 0.0 } else { 100.0 * self.epoch as f64 / self.total_epochs as f64 } } /// Estimates remaining time in seconds #[must_use] pub fn estimated_remaining_secs(&self) -> f64 { if self.epoch == 0 { return 0.0; } let time_per_epoch = self.elapsed_secs / self.epoch as f64; time_per_epoch * (self.total_epochs - self.epoch) as f64 } } /// Helper function to get current timestamp in milliseconds 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) } #[cfg(test)] mod tests { use super::*; use crate::geometry::Point2D; #[test] fn test_ipc_request_serialization() { let geometry = VesselGeometry::straight(0.1, 0.005).unwrap(); let config = SimulationConfig::default(); let request = IpcRequest::initialize(geometry, config); let json = serde_json::to_string(&request).unwrap(); let deserialized: IpcRequest = serde_json::from_str(&json).unwrap(); assert!(matches!(deserialized, IpcRequest::Initialize { .. })); } #[test] fn test_ipc_response_success() { let response = IpcResponse::success(serde_json::json!({"key": "value"})); assert!(response.is_success()); assert!(response.data().is_some()); assert!(response.error_message().is_none()); } #[test] fn test_ipc_response_error() { let response = IpcResponse::error("test error"); assert!(!response.is_success()); assert!(response.data().is_none()); assert_eq!(response.error_message(), Some("test error")); } #[test] fn test_simulation_state_progress() { let state = SimulationState::new() .with_initialized(true) .with_training(true) .with_epoch(50, 100); assert!(state.is_initialized()); assert!(state.is_training()); assert!((state.progress_percent() - 50.0).abs() < f64::EPSILON); } #[test] fn test_performance_metrics_fps() { let metrics = PerformanceMetrics::new(20.0, 0.8, 1024); assert!((metrics.estimated_fps() - 50.0).abs() < f64::EPSILON); assert!(metrics.is_realtime()); } #[test] fn test_training_progress_remaining_time() { let progress = TrainingProgress::new(50, 100, 0.01, 0.005, 0.005, 0.001, 100.0); // 100 seconds for 50 epochs = 2 sec/epoch // 50 epochs remaining = 100 seconds assert!((progress.estimated_remaining_secs() - 100.0).abs() < f64::EPSILON); } }