Files
rustytorch/demos/shared/src/ipc.rs
T
2026-03-04 00:08:42 +00:00

589 lines
15 KiB
Rust

//! 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<serde_json::Value>,
/// Error message (if failure)
error: Option<String>,
/// 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<String>) -> 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<serde_json::Value> {
&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);
}
}