diff --git a/.github/workflows/performance-benchmarking.yml b/.github/workflows/performance-benchmarking.yml index b8c7eb9b..ff1ab5e7 100644 --- a/.github/workflows/performance-benchmarking.yml +++ b/.github/workflows/performance-benchmarking.yml @@ -69,16 +69,16 @@ jobs: run: | if [ "$RUNNER_OS" == "Linux" ]; then sudo apt-get update - sudo apt-get install -y valgrind linux-tools-common + sudo apt-get install -y valgrind linux-tools-common libfontconfig1-dev fi - name: Build benchmarks - run: cargo build --release --bench comprehensive_cfd_benchmarks + run: cargo build --release --bench performance_benchmarks - name: Run comprehensive benchmarks if: github.event.inputs.benchmark_type == 'comprehensive' || github.event_name != 'workflow_dispatch' run: | - cargo bench --bench comprehensive_cfd_benchmarks | tee benchmark_results.txt + cargo bench --bench performance_benchmarks | tee benchmark_results.txt - name: Run regression detection benchmarks if: github.event.inputs.benchmark_type == 'regression' || github.event_name == 'schedule' diff --git a/.gitignore b/.gitignore index 066d60e1..0e31161e 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ report/ *.dll *.so *.dylib +.venv/ +__pycache__/ diff --git a/crates/cfd-python/src/cavitation.rs b/crates/cfd-python/src/cavitation.rs new file mode 100644 index 00000000..9b1b41e6 --- /dev/null +++ b/crates/cfd-python/src/cavitation.rs @@ -0,0 +1,116 @@ +//! Cavitation models and regime classification wrappers for `PyO3` + +use cfd_core::physics::cavitation::rayleigh_plesset::RayleighPlesset; +use cfd_core::physics::cavitation::regimes::CavitationRegimeClassifier; +use pyo3::prelude::*; + +/// Rayleigh-Plesset bubble model +#[pyclass(name = "RayleighPlesset")] +#[derive(Clone)] +pub struct PyRayleighPlesset { + pub(crate) inner: RayleighPlesset, +} + +#[pymethods] +impl PyRayleighPlesset { + /// Create new Rayleigh-Plesset bubble model + #[new] + #[pyo3(signature = (initial_radius=1e-6, liquid_density=997.0, liquid_viscosity=0.001, surface_tension=0.0728, vapor_pressure=2339.0, polytropic_index=1.4))] + fn new( + initial_radius: f64, + liquid_density: f64, + liquid_viscosity: f64, + surface_tension: f64, + vapor_pressure: f64, + polytropic_index: f64, + ) -> Self { + PyRayleighPlesset { + inner: RayleighPlesset { + initial_radius, + liquid_density, + liquid_viscosity, + surface_tension, + vapor_pressure, + polytropic_index, + }, + } + } + + /// Calculate critical Blake radius for unstable growth + fn blake_critical_radius(&self, ambient_pressure: f64) -> f64 { + self.inner.blake_critical_radius(ambient_pressure) + } + + /// Calculate bubble growth rate for inviscid case + fn growth_rate_inviscid(&self, radius: f64, ambient_pressure: f64) -> f64 { + self.inner.growth_rate_inviscid(radius, ambient_pressure) + } + + /// Calculate collapse time from Rayleigh collapse formula + fn collapse_time(&self, initial_radius: f64, pressure_difference: f64) -> f64 { + self.inner.collapse_time(initial_radius, pressure_difference) + } + + /// Calculate maximum bubble radius during growth (Rayleigh-Plesset) + fn maximum_radius(&self, pressure_ratio: f64) -> f64 { + self.inner.maximum_radius(pressure_ratio) + } + + /// Calculate bubble natural frequency + fn natural_frequency(&self, radius: f64, ambient_pressure: f64) -> f64 { + self.inner.natural_frequency(radius, ambient_pressure) + } +} + +/// Cavitation regime classifier +#[pyclass(name = "CavitationRegimeClassifier")] +pub struct PyCavitationRegimeClassifier { + inner: CavitationRegimeClassifier, +} + +#[pymethods] +impl PyCavitationRegimeClassifier { + /// Create new cavitation regime classifier + #[new] + #[pyo3(signature = (bubble_model, ambient_pressure, acoustic_pressure=None, acoustic_frequency=None))] + fn new( + bubble_model: PyRayleighPlesset, + ambient_pressure: f64, + acoustic_pressure: Option, + acoustic_frequency: Option, + ) -> Self { + PyCavitationRegimeClassifier { + inner: CavitationRegimeClassifier::new( + bubble_model.inner, + ambient_pressure, + acoustic_pressure, + acoustic_frequency, + ), + } + } + + /// Calculate Blake threshold pressure + fn blake_threshold(&self) -> f64 { + self.inner.blake_threshold() + } + + /// Calculate inertial cavitation threshold (Apfel & Holland 1991) + fn inertial_threshold(&self) -> f64 { + self.inner.inertial_threshold() + } + + /// Estimate damage potential (0-1 scale) + fn damage_potential(&self) -> f64 { + self.inner.damage_potential() + } + + /// Estimate hemolysis risk for blood flow + fn hemolysis_risk(&self) -> f64 { + self.inner.hemolysis_risk() + } + + /// Estimate sonoluminescence probability based on regime + fn sonoluminescence_probability(&self) -> f64 { + self.inner.sonoluminescence_probability() + } +} diff --git a/crates/cfd-python/src/hemolysis.rs b/crates/cfd-python/src/hemolysis.rs new file mode 100644 index 00000000..e82681f4 --- /dev/null +++ b/crates/cfd-python/src/hemolysis.rs @@ -0,0 +1,74 @@ +//! Hemolysis models wrapper for `PyO3` + +use cfd_core::physics::api::HemolysisModel; +use pyo3::prelude::*; + +/// Hemolysis model (Giersiepen) +#[pyclass(name = "HemolysisModel")] +pub struct PyHemolysisModel { + inner: HemolysisModel, +} + +#[pymethods] +impl PyHemolysisModel { + /// Create Giersiepen model with standard constants + #[staticmethod] + fn giersiepen_standard() -> Self { + PyHemolysisModel { + inner: HemolysisModel::giersiepen_standard(), + } + } + + /// Create Giersiepen model for turbulent flow + #[staticmethod] + fn giersiepen_turbulent() -> Self { + PyHemolysisModel { + inner: HemolysisModel::giersiepen_turbulent(), + } + } + + /// Create Giersiepen model for laminar flow + #[staticmethod] + fn giersiepen_laminar() -> Self { + PyHemolysisModel { + inner: HemolysisModel::giersiepen_laminar(), + } + } + + /// Create Zhang model for Couette flow + #[staticmethod] + fn zhang() -> Self { + PyHemolysisModel { + inner: HemolysisModel::zhang(), + } + } + + /// Create Heuser-Opitz threshold model + #[staticmethod] + fn heuser_opitz() -> Self { + PyHemolysisModel { + inner: HemolysisModel::heuser_opitz(), + } + } + + /// Giersiepen (1990) model validated for millifluidic and blood-processing devices + #[staticmethod] + fn giersiepen_millifluidic() -> Self { + PyHemolysisModel { + inner: HemolysisModel::giersiepen_millifluidic(), + } + } + + /// Amplify a baseline Haemolysis Index by local cavitation potential + #[staticmethod] + fn cavitation_amplified(base_hi: f64, cav_potential: f64) -> f64 { + HemolysisModel::cavitation_amplified(base_hi, cav_potential) + } + + /// Calculate blood damage index from shear stress and exposure time + fn damage_index(&self, shear_stress: f64, exposure_time: f64) -> PyResult { + self.inner + .damage_index(shear_stress, exposure_time) + .map_err(|e: cfd_core::error::Error| pyo3::exceptions::PyValueError::new_err(e.to_string())) + } +} diff --git a/crates/cfd-python/src/lib.rs b/crates/cfd-python/src/lib.rs index f21d9b2d..10fb9df9 100644 --- a/crates/cfd-python/src/lib.rs +++ b/crates/cfd-python/src/lib.rs @@ -72,6 +72,8 @@ use pyo3::prelude::*; mod bifurcation; mod blood; +mod cavitation; +mod hemolysis; mod poiseuille_2d; mod result_types; mod solver_2d; @@ -80,6 +82,8 @@ mod womersley; pub use bifurcation::{PyBifurcationSolver, PyTrifurcationResult, PyTrifurcationSolver}; pub use blood::*; +pub use cavitation::*; +pub use hemolysis::*; pub use poiseuille_2d::{PyPoiseuilleConfig, PyPoiseuilleResult, PyPoiseuilleSolver}; pub use result_types::PyBifurcationResult; pub use solver_2d::*; @@ -113,6 +117,11 @@ fn cfd_python(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + // Cavitation & Hemolysis + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + // 2D solvers (extended) m.add_class::()?; m.add_class::()?; diff --git a/validation/cross_validate_rust_python.py b/validation/cross_validate_rust_python.py index 8ebbebe2..1e2ce0bd 100644 --- a/validation/cross_validate_rust_python.py +++ b/validation/cross_validate_rust_python.py @@ -54,12 +54,39 @@ print(f" P_Blake = {P_Blake_python:.2f} Pa = {P_Blake_python/1000:.2f} kPa") if has_cfd_python: - # TODO: Check if cfd_python exposes Blake threshold calculation - # For now, document that Rust implementation is in regimes.rs print(f"\nRust implementation:") print(f" Located in: crates/cfd-core/src/physics/cavitation/regimes.rs") print(f" Method: blake_threshold() and blake_critical_radius()") - print(f" Formula matches Python implementation ✓") + + if hasattr(cfd_python, 'RayleighPlesset') and hasattr(cfd_python, 'CavitationRegimeClassifier'): + rp = cfd_python.RayleighPlesset( + initial_radius=R_0, + liquid_density=WATER_DENSITY, + liquid_viscosity=WATER_VISCOSITY, + surface_tension=WATER_SURFACE_TENSION, + vapor_pressure=WATER_VAPOR_PRESSURE, + polytropic_index=1.4 + ) + rc_rust = rp.blake_critical_radius(P_inf) + + classifier = cfd_python.CavitationRegimeClassifier( + bubble_model=rp, + ambient_pressure=P_inf + ) + p_blake_rust = classifier.blake_threshold() + + print(f" R_c (Rust) = {rc_rust*1e6:.4f} μm") + print(f" P_Blake (Rust) = {p_blake_rust:.2f} Pa = {p_blake_rust/1000:.2f} kPa") + + err_rc = abs(R_c_python - rc_rust) / rc_rust * 100 + err_p = abs(P_Blake_python - p_blake_rust) / p_blake_rust * 100 + print(f"\n Error: R_c: {err_rc:.4e}%, P_Blake: {err_p:.4e}%") + if err_rc < 0.01 and err_p < 0.01: + print(f" ✓ Cross-validation PASSED") + else: + print(f" ✗ Cross-validation FAILED") + else: + print(f" ⚠ Rust bindings for Cavitation not exposed") else: print(f"\nRust verification skipped (cfd_python not available)") @@ -104,9 +131,27 @@ def carreau_yasuda_python(shear_rate): print(f" Type: CarreauYasudaBlood") print(f" Method: apparent_viscosity(shear_rate)") - # Try to test if we can create a blood model - # Note: This depends on cfd_python API structure - print(f"\n TODO: Add cfd_python API test if blood model is exposed") + if hasattr(cfd_python, 'CarreauYasudaBlood'): + blood = cfd_python.CarreauYasudaBlood() + + print(f"\n{'Shear Rate (s⁻¹)':>20} {'μ Rust (mPa·s)':>15} {'Error':>15}") + print("-" * 55) + + all_passed = True + for gamma_dot in test_shear_rates: + mu_python = carreau_yasuda_python(gamma_dot) + mu_rust = blood.apparent_viscosity(gamma_dot) + err = abs(mu_python - mu_rust) / mu_python * 100 + print(f"{gamma_dot:20.0f} {mu_rust*1000:15.4f} {err:14.4e}%") + if err > 0.01: + all_passed = False + + if all_passed: + print(f"\n ✓ Cross-validation PASSED") + else: + print(f"\n ✗ Cross-validation FAILED") + else: + print(f"\n ⚠ Rust bindings for Blood not exposed") else: print(f"\nRust verification skipped (cfd_python not available)") @@ -145,9 +190,46 @@ def giersiepen_python(shear_stress, exposure_time): if has_cfd_python: print(f"\nRust implementation:") - print(f" Located in: crates/cfd-core/src/physics/hemolysis/giersiepen.rs") - print(f" Method: calculate_damage(shear_stress, exposure_time)") - print(f"\n TODO: Add cfd_python API test if hemolysis model is exposed") + print(f" Located in: crates/cfd-core/src/physics/hemolysis/models.rs") + print(f" Method: damage_index(shear_stress, exposure_time)") + + if hasattr(cfd_python, 'HemolysisModel'): + model = cfd_python.HemolysisModel.giersiepen_millifluidic() + + print(f"\n{'Stress (Pa)':>12} {'Time (s)':>12} {'Damage Rust':>15} {'Error':>15}") + print("-" * 55) + + all_passed = True + for tau, t in test_cases: + damage_py = giersiepen_python(tau, t) + damage_rs = model.damage_index(tau, t) + err = abs(damage_py - damage_rs) / (damage_py + 1e-15) * 100 + print(f"{tau:12.1f} {t:12.2f} {damage_rs:15.6f} {err:14.4e}%") + + if err > 1.0: # The py implementation uses beta for time and alpha for stress (different letters, same values roughly, need to allow slight differences or exactly match constants. Actually let's use the exact constants if possible) + pass # just print + + # Compare with the Giersiepen Standard which matches the python script's arbitrary C, alpha, beta + print(f"\nUsing Python's exact constants for validation:") + # The python script used C=3.62e-5, alpha=2.416, beta=0.785 + # The rust code for standard uses: coefficient=3.62e-5, stress_exponent=2.416, time_exponent=0.785 + std_model = cfd_python.HemolysisModel.giersiepen_standard() + + all_passed = True + for tau, t in test_cases: + damage_py = giersiepen_python(tau, t) + damage_rs = std_model.damage_index(tau, t) + err = abs(damage_py - damage_rs) / (damage_py + 1e-15) * 100 + print(f"{tau:12.1f} {t:12.2f} {damage_rs:15.6f} {err:14.4e}%") + if err > 0.01: + all_passed = False + + if all_passed: + print(f"\n ✓ Cross-validation PASSED") + else: + print(f"\n ✗ Cross-validation FAILED") + else: + print(f"\n ⚠ Rust bindings for Hemolysis not exposed") else: print(f"\nRust verification skipped (cfd_python not available)") @@ -174,10 +256,7 @@ def giersiepen_python(shear_stress, exposure_time): - Rust: crates/cfd-core/src/physics/hemolysis/giersiepen.rs - Formula: D = C×τ^α×t^β -NEXT STEP: Add explicit cross-validation tests that: - - Call Rust via cfd_python with same inputs - - Compare outputs to Python calculations - - Assert < 0.01% difference +Cross-validation tests implemented and executed. """) else: print(f"""