Initial commit
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
//! Neural operator inference engine
|
||||
//!
|
||||
//! Provides the main demo functionality for FNO inference.
|
||||
|
||||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
|
||||
use rtx_backend_cpu::{CpuBackend, CpuDevice};
|
||||
use rtx_neural_operator::{FNO2d, load_fno2d_weights};
|
||||
use rtx_neural_operator_shared::{
|
||||
config::{PDEConfig, PDEType},
|
||||
error::{NeuralOperatorError, Result},
|
||||
ipc::{ModelInfo, PerformanceMetrics, SolutionData},
|
||||
};
|
||||
use rtx_nn::GenericModule4D;
|
||||
use rtx_tensor::GenericTensor;
|
||||
|
||||
/// Result from a solve operation
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SolveResult {
|
||||
/// Solution field (flattened [H, W])
|
||||
pub solution: Vec<f32>,
|
||||
/// Inference time in milliseconds
|
||||
pub inference_time_ms: f64,
|
||||
}
|
||||
|
||||
/// Neural Operator Demo engine
|
||||
///
|
||||
/// Manages FNO model loading and inference for interactive PDE solving.
|
||||
pub struct NeuralOperatorDemo {
|
||||
model: Option<FNO2d<CpuBackend>>,
|
||||
config: Option<PDEConfig>,
|
||||
metrics: PerformanceMetrics,
|
||||
last_solution: Option<Vec<f32>>,
|
||||
}
|
||||
|
||||
impl NeuralOperatorDemo {
|
||||
/// Creates a new demo instance
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
model: None,
|
||||
config: None,
|
||||
metrics: PerformanceMetrics::new(),
|
||||
last_solution: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the demo with a PDE configuration
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `config` - PDE configuration specifying type and resolution
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns error if weight file not found or model loading fails.
|
||||
pub fn initialize(&mut self, config: PDEConfig) -> Result<()> {
|
||||
tracing::info!(
|
||||
"Initializing neural operator for {:?} at {}x{} resolution",
|
||||
config.pde_type,
|
||||
config.resolution,
|
||||
config.resolution
|
||||
);
|
||||
|
||||
// For now, we'll create a model with default weights
|
||||
// In production, this would load from weight files
|
||||
self.config = Some(config);
|
||||
self.metrics = PerformanceMetrics::new();
|
||||
self.last_solution = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initializes with weights from a file path
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `config` - PDE configuration
|
||||
/// * `weights_path` - Path to `SafeTensors` weight file
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns error if weight loading fails.
|
||||
pub fn initialize_with_weights(
|
||||
&mut self,
|
||||
config: PDEConfig,
|
||||
weights_path: impl AsRef<Path>,
|
||||
) -> Result<()> {
|
||||
tracing::info!("Loading FNO weights from {:?}", weights_path.as_ref());
|
||||
|
||||
let weights = load_fno2d_weights(weights_path.as_ref())
|
||||
.map_err(|e| NeuralOperatorError::weight_load(e.to_string()))?;
|
||||
|
||||
let device = CpuDevice::default();
|
||||
let model = FNO2d::from_weights(&weights, &device)
|
||||
.map_err(|e| NeuralOperatorError::weight_load(e.to_string()))?;
|
||||
|
||||
self.model = Some(model);
|
||||
self.config = Some(config);
|
||||
self.metrics = PerformanceMetrics::new();
|
||||
self.last_solution = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Solves the PDE with the given input field
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `input` - Input field (flattened [H, W] array)
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns error if model not initialized or inference fails.
|
||||
pub fn solve(&mut self, input: &[f32]) -> Result<SolveResult> {
|
||||
let config = self
|
||||
.config
|
||||
.as_ref()
|
||||
.ok_or(NeuralOperatorError::NotInitialized)?;
|
||||
|
||||
let expected_size = (config.resolution * config.resolution) as usize;
|
||||
if input.len() != expected_size {
|
||||
return Err(NeuralOperatorError::invalid_dimensions(
|
||||
format!("[{expected_size}]"),
|
||||
format!("[{}]", input.len()),
|
||||
));
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
// If we have a loaded model, use it
|
||||
let solution = if let Some(ref model) = self.model {
|
||||
// Create input tensor [1, 1, H, W]
|
||||
let h = config.resolution as usize;
|
||||
let w = config.resolution as usize;
|
||||
let device = CpuDevice::default();
|
||||
|
||||
let tensor: GenericTensor<CpuBackend, 4> =
|
||||
GenericTensor::from_slice(input, [1, 1, h, w], &device);
|
||||
|
||||
// Run inference
|
||||
let output = model.forward_4d(&tensor);
|
||||
|
||||
// Extract result
|
||||
output.to_vec()
|
||||
} else {
|
||||
// No model loaded - return a simple demo output
|
||||
// This is a placeholder that shows the demo UI works
|
||||
generate_demo_solution(input, config)
|
||||
};
|
||||
|
||||
let inference_time_ms = start.elapsed().as_secs_f64() * 1000.0;
|
||||
|
||||
// Record metrics
|
||||
self.metrics.record_inference(inference_time_ms);
|
||||
|
||||
// Cache solution
|
||||
self.last_solution = Some(solution.clone());
|
||||
|
||||
Ok(SolveResult {
|
||||
solution,
|
||||
inference_time_ms,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates solution data from a solve result
|
||||
#[must_use]
|
||||
pub fn create_solution_data(&self, result: &SolveResult) -> Option<SolutionData> {
|
||||
let config = self.config.as_ref()?;
|
||||
Some(SolutionData::new(
|
||||
result.solution.clone(),
|
||||
config.resolution,
|
||||
config.resolution,
|
||||
result.inference_time_ms,
|
||||
))
|
||||
}
|
||||
|
||||
/// Returns the last computed solution
|
||||
#[must_use]
|
||||
pub fn last_solution(&self) -> Option<&Vec<f32>> {
|
||||
self.last_solution.as_ref()
|
||||
}
|
||||
|
||||
/// Returns current performance metrics
|
||||
#[must_use]
|
||||
pub fn metrics(&self) -> &PerformanceMetrics {
|
||||
&self.metrics
|
||||
}
|
||||
|
||||
/// Returns model information
|
||||
#[must_use]
|
||||
pub fn model_info(&self) -> Option<ModelInfo> {
|
||||
let config = self.config.as_ref()?;
|
||||
Some(ModelInfo::new(
|
||||
format!("FNO2d-{}", config.pde_type.name()),
|
||||
config.pde_type.name(),
|
||||
config.resolution,
|
||||
config.n_modes,
|
||||
config.model_width,
|
||||
config.n_layers,
|
||||
))
|
||||
}
|
||||
|
||||
/// Returns the current configuration
|
||||
#[must_use]
|
||||
pub fn config(&self) -> Option<&PDEConfig> {
|
||||
self.config.as_ref()
|
||||
}
|
||||
|
||||
/// Returns whether the model is initialized
|
||||
#[must_use]
|
||||
pub fn is_initialized(&self) -> bool {
|
||||
self.config.is_some()
|
||||
}
|
||||
|
||||
/// Returns whether a trained model is loaded
|
||||
#[must_use]
|
||||
pub fn has_model(&self) -> bool {
|
||||
self.model.is_some()
|
||||
}
|
||||
|
||||
/// Resets the demo to initial state
|
||||
pub fn reset(&mut self) {
|
||||
self.model = None;
|
||||
self.config = None;
|
||||
self.metrics = PerformanceMetrics::new();
|
||||
self.last_solution = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NeuralOperatorDemo {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a demo solution when no model is loaded
|
||||
///
|
||||
/// This creates a visually interesting output that demonstrates the UI
|
||||
/// without requiring a trained model.
|
||||
fn generate_demo_solution(input: &[f32], config: &PDEConfig) -> Vec<f32> {
|
||||
let n = config.resolution as usize;
|
||||
let mut solution = vec![0.0; n * n];
|
||||
|
||||
// Create a simple diffusion-like response to the input
|
||||
// This mimics what a PDE solver would produce
|
||||
for i in 0..n {
|
||||
for j in 0..n {
|
||||
let idx = i * n + j;
|
||||
let x = j as f32 / n as f32;
|
||||
let y = i as f32 / n as f32;
|
||||
|
||||
// Combine input with a smooth basis function
|
||||
let input_val = input[idx];
|
||||
let smooth = (std::f32::consts::PI * x).sin() * (std::f32::consts::PI * y).sin();
|
||||
|
||||
// Different responses for different PDE types
|
||||
let response = match config.pde_type {
|
||||
PDEType::DarcyFlow => {
|
||||
// Pressure-like response
|
||||
input_val * smooth * 0.5 + (1.0 - x) * 0.3
|
||||
}
|
||||
PDEType::HeatEquation => {
|
||||
// Temperature diffusion
|
||||
input_val * smooth.powi(2) * 0.8
|
||||
}
|
||||
PDEType::Poisson => {
|
||||
// Potential field
|
||||
input_val * smooth * 0.6 + smooth * 0.2
|
||||
}
|
||||
PDEType::NavierStokes => {
|
||||
// Velocity-like field
|
||||
let vortex = ((x - 0.5).powi(2) + (y - 0.5).powi(2)).sqrt();
|
||||
input_val * (1.0 - vortex).max(0.0) * 0.7
|
||||
}
|
||||
};
|
||||
|
||||
solution[idx] = response;
|
||||
}
|
||||
}
|
||||
|
||||
solution
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_demo_creation() {
|
||||
let demo = NeuralOperatorDemo::new();
|
||||
assert!(!demo.is_initialized());
|
||||
assert!(!demo.has_model());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_initialize() {
|
||||
let mut demo = NeuralOperatorDemo::new();
|
||||
let config = PDEConfig::darcy(64);
|
||||
demo.initialize(config).unwrap();
|
||||
|
||||
assert!(demo.is_initialized());
|
||||
assert!(!demo.has_model()); // No weights loaded
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_solve_without_model() {
|
||||
let mut demo = NeuralOperatorDemo::new();
|
||||
let config = PDEConfig::darcy(32);
|
||||
demo.initialize(config).unwrap();
|
||||
|
||||
let input = vec![1.0; 32 * 32];
|
||||
let result = demo.solve(&input).unwrap();
|
||||
|
||||
assert_eq!(result.solution.len(), 32 * 32);
|
||||
assert!(result.inference_time_ms > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_solve_dimension_mismatch() {
|
||||
let mut demo = NeuralOperatorDemo::new();
|
||||
let config = PDEConfig::darcy(64);
|
||||
demo.initialize(config).unwrap();
|
||||
|
||||
let input = vec![1.0; 32 * 32]; // Wrong size
|
||||
let result = demo.solve(&input);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_solve_not_initialized() {
|
||||
let mut demo = NeuralOperatorDemo::new();
|
||||
let input = vec![1.0; 64 * 64];
|
||||
let result = demo.solve(&input);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_metrics_recording() {
|
||||
let mut demo = NeuralOperatorDemo::new();
|
||||
let config = PDEConfig::darcy(16);
|
||||
demo.initialize(config).unwrap();
|
||||
|
||||
let input = vec![1.0; 16 * 16];
|
||||
demo.solve(&input).unwrap();
|
||||
demo.solve(&input).unwrap();
|
||||
|
||||
let metrics = demo.metrics();
|
||||
assert_eq!(metrics.inference_count, 2);
|
||||
assert!(metrics.avg_inference_time_ms > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_info() {
|
||||
let mut demo = NeuralOperatorDemo::new();
|
||||
let config = PDEConfig::darcy(64)
|
||||
.with_modes(12, 12)
|
||||
.with_width(32)
|
||||
.with_layers(4);
|
||||
demo.initialize(config).unwrap();
|
||||
|
||||
let info = demo.model_info().unwrap();
|
||||
assert_eq!(info.resolution, 64);
|
||||
assert_eq!(info.n_modes, (12, 12));
|
||||
assert_eq!(info.model_width, 32);
|
||||
assert_eq!(info.n_layers, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset() {
|
||||
let mut demo = NeuralOperatorDemo::new();
|
||||
let config = PDEConfig::darcy(64);
|
||||
demo.initialize(config).unwrap();
|
||||
|
||||
let input = vec![1.0; 64 * 64];
|
||||
demo.solve(&input).unwrap();
|
||||
|
||||
demo.reset();
|
||||
|
||||
assert!(!demo.is_initialized());
|
||||
assert!(demo.last_solution().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_pde_types() {
|
||||
let mut demo = NeuralOperatorDemo::new();
|
||||
let input = vec![1.0; 16 * 16];
|
||||
|
||||
for pde_type in [
|
||||
PDEType::DarcyFlow,
|
||||
PDEType::HeatEquation,
|
||||
PDEType::Poisson,
|
||||
PDEType::NavierStokes,
|
||||
] {
|
||||
let config = PDEConfig::new(pde_type, 16);
|
||||
demo.initialize(config).unwrap();
|
||||
|
||||
let result = demo.solve(&input).unwrap();
|
||||
assert_eq!(result.solution.len(), 16 * 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user