Consistent formatting pass: line wrapping, import sorting, trailing whitespace removal, let-chain indentation, merged derive attributes, and unsafe block reformatting. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
287 lines
9.1 KiB
Rust
287 lines
9.1 KiB
Rust
//! Sampling module for PIDDM demo.
|
|
|
|
use tokio::sync::mpsc;
|
|
|
|
use rtx_piddm::DDPMScheduler;
|
|
use rtx_piddm_shared::{
|
|
GeneratedSample, PdeType, PiddmError, PiddmResult, PiddmSamplingConfig, SamplingProgress,
|
|
SamplingResult,
|
|
};
|
|
|
|
/// PIDDM sampler for the demo.
|
|
pub struct PiddmSampler {
|
|
config: PiddmSamplingConfig,
|
|
pde_type: PdeType,
|
|
scheduler: Option<DDPMScheduler>,
|
|
}
|
|
|
|
impl PiddmSampler {
|
|
/// Create a new sampler.
|
|
#[must_use]
|
|
pub fn new(config: PiddmSamplingConfig, pde_type: PdeType) -> Self {
|
|
Self {
|
|
config,
|
|
pde_type,
|
|
scheduler: None,
|
|
}
|
|
}
|
|
|
|
/// Load model weights.
|
|
pub fn load_weights(&mut self, _path: Option<&str>) -> PiddmResult<()> {
|
|
// Initialize scheduler
|
|
let scheduler = DDPMScheduler::new(
|
|
1000, // num_timesteps
|
|
1e-4, // beta_start
|
|
0.02, // beta_end
|
|
);
|
|
self.scheduler = Some(scheduler);
|
|
|
|
// TODO: Load actual weights from SafeTensors file
|
|
Ok(())
|
|
}
|
|
|
|
/// Generate samples with progress reporting.
|
|
pub async fn sample(
|
|
&self,
|
|
progress_tx: mpsc::Sender<SamplingProgress>,
|
|
) -> PiddmResult<SamplingResult> {
|
|
if self.scheduler.is_none() {
|
|
return Err(PiddmError::ModelNotLoaded);
|
|
}
|
|
|
|
let start_time = std::time::Instant::now();
|
|
let num_samples = self.config.num_samples;
|
|
let resolution = self.config.resolution as usize;
|
|
let total_steps = self.config.sampling_steps;
|
|
|
|
let mut samples = Vec::with_capacity(num_samples as usize);
|
|
let mut total_physics_residual = 0.0;
|
|
|
|
for sample_idx in 0..num_samples {
|
|
// Simulate denoising process
|
|
let mut field = vec![0.0f32; resolution * resolution];
|
|
|
|
// Initialize with noise
|
|
for v in &mut field {
|
|
*v = rand::random::<f32>() * 2.0 - 1.0;
|
|
}
|
|
|
|
for step in 0..total_steps {
|
|
// Calculate noise level (decreasing)
|
|
let t = 1.0 - (f64::from(step) + 1.0) / f64::from(total_steps);
|
|
let noise_level = t * 0.5; // Simplified noise schedule
|
|
|
|
// Simulate denoising step
|
|
let scale = 1.0 - 0.01 * (step as f32 + 1.0) / total_steps as f32;
|
|
for v in &mut field {
|
|
*v *= scale;
|
|
// Add structure based on PDE type
|
|
*v += 0.01 * rand::random::<f32>();
|
|
}
|
|
|
|
// Calculate physics residual
|
|
let physics_residual = self.compute_physics_residual(&field, resolution);
|
|
|
|
let progress = SamplingProgress {
|
|
step: step + 1,
|
|
total_steps,
|
|
sample: sample_idx + 1,
|
|
total_samples: num_samples,
|
|
noise_level,
|
|
physics_residual,
|
|
};
|
|
|
|
if progress_tx.send(progress).await.is_err() {
|
|
return Err(PiddmError::SamplingError(
|
|
"Progress channel closed".to_string(),
|
|
));
|
|
}
|
|
|
|
// Small delay
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
|
|
}
|
|
|
|
// Generate final sample based on PDE type
|
|
let field = self.generate_pde_solution(resolution);
|
|
let (min_val, max_val) = field.iter().fold((f32::MAX, f32::MIN), |(min, max), &v| {
|
|
(min.min(v), max.max(v))
|
|
});
|
|
|
|
let physics_residual = self.compute_physics_residual(&field, resolution);
|
|
total_physics_residual += physics_residual;
|
|
|
|
samples.push(GeneratedSample {
|
|
index: sample_idx,
|
|
field,
|
|
resolution: resolution as u32,
|
|
physics_residual,
|
|
max_value: f64::from(max_val),
|
|
min_value: f64::from(min_val),
|
|
});
|
|
}
|
|
|
|
let sampling_time = start_time.elapsed().as_secs_f64();
|
|
let avg_physics_residual = total_physics_residual / f64::from(num_samples);
|
|
|
|
Ok(SamplingResult {
|
|
samples,
|
|
sampling_time_seconds: sampling_time,
|
|
avg_physics_residual,
|
|
pde_type: self.pde_type,
|
|
})
|
|
}
|
|
|
|
/// Generate a PDE solution based on type.
|
|
fn generate_pde_solution(&self, resolution: usize) -> Vec<f32> {
|
|
let mut field = vec![0.0f32; resolution * resolution];
|
|
let dx = 1.0 / (resolution as f32 - 1.0);
|
|
|
|
match self.pde_type {
|
|
PdeType::Poisson => {
|
|
// Sinusoidal solution
|
|
let kx = 2.0;
|
|
let ky = 2.0;
|
|
let pi = std::f32::consts::PI;
|
|
for j in 0..resolution {
|
|
for i in 0..resolution {
|
|
let x = i as f32 * dx;
|
|
let y = j as f32 * dx;
|
|
field[j * resolution + i] = (pi * kx * x).sin() * (pi * ky * y).sin();
|
|
}
|
|
}
|
|
}
|
|
PdeType::Heat => {
|
|
// Linear temperature gradient
|
|
for j in 0..resolution {
|
|
for i in 0..resolution {
|
|
let y = j as f32 * dx;
|
|
field[j * resolution + i] = y;
|
|
}
|
|
}
|
|
}
|
|
PdeType::Darcy => {
|
|
// Gaussian pressure field
|
|
let cx = 0.5;
|
|
let cy = 0.5;
|
|
let sigma = 0.2;
|
|
for j in 0..resolution {
|
|
for i in 0..resolution {
|
|
let x = i as f32 * dx;
|
|
let y = j as f32 * dx;
|
|
let r2 = (x - cx).powi(2) + (y - cy).powi(2);
|
|
field[j * resolution + i] = (-r2 / (2.0 * sigma * sigma)).exp();
|
|
}
|
|
}
|
|
}
|
|
PdeType::Burgers => {
|
|
// Shock solution
|
|
let shock_x = 0.5;
|
|
let steepness = 20.0;
|
|
for j in 0..resolution {
|
|
for i in 0..resolution {
|
|
let x = i as f32 * dx;
|
|
field[j * resolution + i] =
|
|
0.5 * (1.0 - (steepness * (x - shock_x)).tanh());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
field
|
|
}
|
|
|
|
/// Compute physics residual (L2 norm of PDE residual).
|
|
fn compute_physics_residual(&self, field: &[f32], resolution: usize) -> f64 {
|
|
let dx = 1.0 / (resolution as f32 - 1.0);
|
|
let mut residual_sum = 0.0f64;
|
|
let mut count = 0;
|
|
|
|
for j in 1..resolution - 1 {
|
|
for i in 1..resolution - 1 {
|
|
let idx = j * resolution + i;
|
|
let u = field[idx];
|
|
let u_xm = field[idx - 1];
|
|
let u_xp = field[idx + 1];
|
|
let u_ym = field[idx - resolution];
|
|
let u_yp = field[idx + resolution];
|
|
|
|
// Laplacian
|
|
let laplacian =
|
|
(u_xm - 2.0 * u + u_xp) / (dx * dx) + (u_ym - 2.0 * u + u_yp) / (dx * dx);
|
|
|
|
// For Poisson, residual is laplacian (assuming zero source for simplicity)
|
|
residual_sum += f64::from(laplacian).powi(2);
|
|
count += 1;
|
|
}
|
|
}
|
|
|
|
if count > 0 {
|
|
(residual_sum / f64::from(count)).sqrt()
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_sampler_creation() {
|
|
let config = PiddmSamplingConfig::default();
|
|
let sampler = PiddmSampler::new(config, PdeType::Poisson);
|
|
assert!(sampler.scheduler.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_load_weights() {
|
|
let config = PiddmSamplingConfig::default();
|
|
let mut sampler = PiddmSampler::new(config, PdeType::Poisson);
|
|
sampler.load_weights(None).unwrap();
|
|
assert!(sampler.scheduler.is_some());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_sampling() {
|
|
let config = PiddmSamplingConfig {
|
|
num_samples: 2,
|
|
resolution: 16,
|
|
sampling_steps: 10,
|
|
..Default::default()
|
|
};
|
|
|
|
let mut sampler = PiddmSampler::new(config, PdeType::Poisson);
|
|
sampler.load_weights(None).unwrap();
|
|
|
|
let (tx, mut rx) = mpsc::channel(100);
|
|
let result = sampler.sample(tx).await.unwrap();
|
|
|
|
assert_eq!(result.samples.len(), 2);
|
|
assert_eq!(result.samples[0].resolution, 16);
|
|
|
|
// Check we received progress updates
|
|
let mut count = 0;
|
|
while rx.try_recv().is_ok() {
|
|
count += 1;
|
|
}
|
|
assert!(count > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_physics_residual() {
|
|
let config = PiddmSamplingConfig {
|
|
resolution: 32,
|
|
..Default::default()
|
|
};
|
|
let sampler = PiddmSampler::new(config, PdeType::Poisson);
|
|
|
|
// Generate a smooth field
|
|
let field = sampler.generate_pde_solution(32);
|
|
let residual = sampler.compute_physics_residual(&field, 32);
|
|
|
|
// Residual should be finite and reasonable
|
|
assert!(residual.is_finite());
|
|
}
|
|
}
|