Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,8 @@ validation/references/
# Example/optimiser output directories (all crates)
outputs/
report/
.venv/
__pycache__/
venv/
.venv/
__pycache__/
8 changes: 4 additions & 4 deletions crates/cfd-core/src/physics/cavitation/regimes/classifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ use super::types::CavitationRegime;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CavitationRegimeClassifier<T: RealField + Copy> {
/// Rayleigh-Plesset bubble model
pub(super) bubble_model: RayleighPlesset<T>,
pub bubble_model: RayleighPlesset<T>,
/// Ambient pressure (Pa)
pub(super) ambient_pressure: T,
pub ambient_pressure: T,
/// Acoustic pressure amplitude (Pa), if applicable
pub(super) acoustic_pressure: Option<T>,
pub acoustic_pressure: Option<T>,
/// Acoustic frequency (Hz), if applicable
pub(super) acoustic_frequency: Option<T>,
pub acoustic_frequency: Option<T>,
}

impl<T: RealField + Copy + FromPrimitive> CavitationRegimeClassifier<T> {
Expand Down
66 changes: 66 additions & 0 deletions crates/cfd-python/src/cavitation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//! Cavitation regime models wrapper for `PyO3`

use cfd_core::physics::cavitation::RayleighPlesset;
use cfd_core::physics::cavitation::CavitationRegimeClassifier;
use pyo3::prelude::*;

/// Cavitation Regime Classifier for predicting cavitation behavior
#[pyclass(name = "CavitationRegimeClassifier")]
pub struct PyCavitationRegimeClassifier {
inner: CavitationRegimeClassifier<f64>,
}

#[pymethods]
impl PyCavitationRegimeClassifier {
/// Create a new Cavitation Regime Classifier
#[new]
#[pyo3(signature = (initial_radius, liquid_density, liquid_viscosity, surface_tension, vapor_pressure, polytropic_index, ambient_pressure))]
fn new(
initial_radius: f64,
liquid_density: f64,
liquid_viscosity: f64,
surface_tension: f64,
vapor_pressure: f64,
polytropic_index: f64,
ambient_pressure: f64,
) -> Self {
let bubble_model = RayleighPlesset {
initial_radius,
liquid_density,
liquid_viscosity,
surface_tension,
vapor_pressure,
polytropic_index,
};

Self {
inner: CavitationRegimeClassifier::new(
bubble_model,
ambient_pressure,
None, // no acoustic pressure by default
None, // no acoustic frequency by default
),
}
}

/// Calculate Blake threshold pressure [Pa]
fn blake_threshold(&self) -> f64 {
self.inner.blake_threshold()
}

/// Calculate critical Blake radius for unstable growth [m]
fn blake_critical_radius(&self) -> f64 {
self.inner.bubble_model.blake_critical_radius(self.inner.ambient_pressure)
}

fn __str__(&self) -> String {
format!(
"CavitationRegimeClassifier(P_ambient={:.1} Pa)",
self.inner.ambient_pressure
)
}

fn __repr__(&self) -> String {
self.__str__()
}
}
43 changes: 43 additions & 0 deletions crates/cfd-python/src/hemolysis.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//! Hemolysis model wrappers for `PyO3`

use cfd_core::physics::api::HemolysisModel;
use pyo3::prelude::*;

/// Giersiepen hemolysis power law model
#[pyclass(name = "GiersiepenModel")]
pub struct PyGiersiepenModel {
inner: HemolysisModel,
}

#[pymethods]
impl PyGiersiepenModel {
/// Create standard Giersiepen model
#[new]
fn new() -> Self {
PyGiersiepenModel {
inner: HemolysisModel::giersiepen_standard(),
}
}

/// Calculate hemolysis damage index from shear stress and exposure time
///
/// # Arguments
/// - `shear_stress`: Shear stress [Pa]
/// - `exposure_time`: Exposure time [s]
///
/// # Returns
/// - Blood damage index [-]
fn calculate_damage(&self, shear_stress: f64, exposure_time: f64) -> PyResult<f64> {
self.inner
.damage_index(shear_stress, exposure_time)
.map_err(|e: cfd_core::error::Error| pyo3::exceptions::PyValueError::new_err(e.to_string()))
}

fn __str__(&self) -> String {
"GiersiepenModel()".to_string()
}

fn __repr__(&self) -> String {
self.__str__()
}
}
8 changes: 8 additions & 0 deletions crates/cfd-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ use pyo3::prelude::*;

mod bifurcation;
mod blood;
mod cavitation;
mod hemolysis;
mod poiseuille_2d;
mod result_types;
mod solver_2d;
Expand All @@ -80,6 +82,8 @@ mod womersley;

pub use bifurcation::{PyBifurcationSolver, PyTrifurcationResult, PyTrifurcationSolver};
pub use blood::*;
pub use cavitation::PyCavitationRegimeClassifier;
pub use hemolysis::PyGiersiepenModel;
pub use poiseuille_2d::{PyPoiseuilleConfig, PyPoiseuilleResult, PyPoiseuilleSolver};
pub use result_types::PyBifurcationResult;
pub use solver_2d::*;
Expand Down Expand Up @@ -108,6 +112,10 @@ fn cfd_python(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyCrossBlood>()?;
m.add_class::<PyFahraeuasLindqvist>()?;

// Cavitation and Hemolysis
m.add_class::<PyCavitationRegimeClassifier>()?;
m.add_class::<PyGiersiepenModel>()?;

// Womersley pulsatile flow
m.add_class::<PyWomersleyNumber>()?;
m.add_class::<PyWomersleyProfile>()?;
Expand Down
83 changes: 71 additions & 12 deletions validation/cross_validate_rust_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,38 @@
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, 'CavitySolver2D'):
# Check if the required API is exposed
Comment on lines +60 to +61
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Redundant check for CavitySolver2D before CavitationRegimeClassifier.

The outer hasattr(cfd_python, 'CavitySolver2D') check appears unrelated to the Blake threshold test. This may be a copy-paste artifact. The test should only check for CavitationRegimeClassifier.

🐛 Proposed fix to remove unrelated check
 if has_cfd_python:
     print(f"\nRust implementation:")
     print(f"  Located in: crates/cfd-core/src/physics/cavitation/regimes.rs")

-    if hasattr(cfd_python, 'CavitySolver2D'):
-        # Check if the required API is exposed
-        if hasattr(cfd_python, 'CavitationRegimeClassifier'):
+    # Check if the required API is exposed
+    if hasattr(cfd_python, 'CavitationRegimeClassifier'):

And adjust the corresponding else clause indentation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@validation/cross_validate_rust_python.py` around lines 60 - 61, Remove the
unrelated outer hasattr(cfd_python, 'CavitySolver2D') guard so the Blake
threshold test only checks for CavitationRegimeClassifier; modify the block
around the CavitationRegimeClassifier check (the code that currently sits under
the CavitySolver2D if) to directly use if hasattr(cfd_python,
'CavitationRegimeClassifier') and shift the corresponding else clause
indentation to match that check (ensure the expected test/skip behavior is
preserved for the CavitationRegimeClassifier branch).

if hasattr(cfd_python, 'CavitationRegimeClassifier'):
classifier = cfd_python.CavitationRegimeClassifier(
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,
ambient_pressure=P_inf
)

R_c_rust = classifier.blake_critical_radius()
P_Blake_rust = classifier.blake_threshold()

print(f" R_c (Rust) = {R_c_rust*1e6:.4f} μm")
print(f" P_Blake (Rust) = {P_Blake_rust:.2f} Pa")

# Assert equivalence
rc_error = abs(R_c_rust - R_c_python) / R_c_python * 100
pb_error = abs(P_Blake_rust - P_Blake_python) / P_Blake_python * 100

if rc_error < 0.01 and pb_error < 0.01:
print(f" Cross-validation: ✓ MATCH (<0.01% diff)")
else:
print(f" Cross-validation: ✗ MISMATCH (Rc err: {rc_error:.4f}%, Pb err: {pb_error:.4f}%)")
else:
print(f" Skipping test: CavitationRegimeClassifier not found in cfd_python")
Comment on lines +84 to +88
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unnecessary f prefix from strings without placeholders.

Static analysis (Ruff F541) correctly identifies f-strings that contain no placeholders. These should be plain strings.

🧹 Proposed fix for lines 84 and 88
             if rc_error < 0.01 and pb_error < 0.01:
-                print(f"  Cross-validation: ✓ MATCH (<0.01% diff)")
+                print("  Cross-validation: ✓ MATCH (<0.01% diff)")
             else:
                 print(f"  Cross-validation: ✗ MISMATCH (Rc err: {rc_error:.4f}%, Pb err: {pb_error:.4f}%)")
         else:
-            print(f"  Skipping test: CavitationRegimeClassifier not found in cfd_python")
+            print("  Skipping test: CavitationRegimeClassifier not found in cfd_python")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
print(f" Cross-validation: ✓ MATCH (<0.01% diff)")
else:
print(f" Cross-validation: ✗ MISMATCH (Rc err: {rc_error:.4f}%, Pb err: {pb_error:.4f}%)")
else:
print(f" Skipping test: CavitationRegimeClassifier not found in cfd_python")
print(" Cross-validation: ✓ MATCH (<0.01% diff)")
else:
print(f" Cross-validation: ✗ MISMATCH (Rc err: {rc_error:.4f}%, Pb err: {pb_error:.4f}%)")
else:
print(" Skipping test: CavitationRegimeClassifier not found in cfd_python")
🧰 Tools
🪛 Ruff (0.15.5)

[error] 84-84: f-string without any placeholders

Remove extraneous f prefix

(F541)


[error] 88-88: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@validation/cross_validate_rust_python.py` around lines 84 - 88, The two print
statements that use f-strings but contain no placeholders should be converted to
plain strings: update the print in the branch that prints "  Cross-validation: ✓
MATCH (<0.01% diff)" and the print in the else that prints "  Skipping test:
CavitationRegimeClassifier not found in cfd_python" to remove the leading f
prefix (leave the other print that uses {rc_error} and {pb_error} unchanged);
locate these prints near the cross-validation logic in
cross_validate_rust_python.py (the prints used alongside
CavitationRegimeClassifier handling) and replace f"..." with "..." so static
analysis Ruff F541 is satisfied.

else:
print(f"\nRust verification skipped (cfd_python not available)")

Expand Down Expand Up @@ -101,12 +127,25 @@ def carreau_yasuda_python(shear_rate):
if has_cfd_python:
print(f"\nRust implementation:")
print(f" Located in: crates/cfd-core/src/physics/fluid/blood.rs")
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" Cross-validation:")

all_match = True
for gamma_dot in test_shear_rates:
mu_python = carreau_yasuda_python(gamma_dot)
mu_rust = blood.apparent_viscosity(gamma_dot)
error_pct = abs(mu_rust - mu_python) / mu_python * 100

if error_pct > 0.01:
all_match = False
print(f" Mismatch at γ̇ = {gamma_dot}: Python={mu_python*1000:.4f}, Rust={mu_rust*1000:.4f} (err: {error_pct:.2f}%)")

if all_match:
print(f" ✓ MATCH for all shear rates (<0.01% diff)")
else:
print(f" Skipping test: CarreauYasudaBlood not found in cfd_python")
Comment on lines +131 to +148
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unnecessary f prefixes; gamma character is acceptable.

Lines 133, 146, and 148 have f-strings without placeholders. The ambiguous gamma character (γ̇) at line 143 is intentional scientific notation for shear rate and should be kept.

🧹 Proposed fix for f-string issues
     if hasattr(cfd_python, 'CarreauYasudaBlood'):
         blood = cfd_python.CarreauYasudaBlood()
-        print(f"  Cross-validation:")
+        print("  Cross-validation:")

         all_match = True
         for gamma_dot in test_shear_rates:
             mu_python = carreau_yasuda_python(gamma_dot)
             mu_rust = blood.apparent_viscosity(gamma_dot)
             error_pct = abs(mu_rust - mu_python) / mu_python * 100

             if error_pct > 0.01:
                 all_match = False
                 print(f"    Mismatch at γ̇ = {gamma_dot}: Python={mu_python*1000:.4f}, Rust={mu_rust*1000:.4f} (err: {error_pct:.2f}%)")

         if all_match:
-            print(f"  ✓ MATCH for all shear rates (<0.01% diff)")
+            print("  ✓ MATCH for all shear rates (<0.01% diff)")
     else:
-        print(f"  Skipping test: CarreauYasudaBlood not found in cfd_python")
+        print("  Skipping test: CarreauYasudaBlood not found in cfd_python")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if hasattr(cfd_python, 'CarreauYasudaBlood'):
blood = cfd_python.CarreauYasudaBlood()
print(f" Cross-validation:")
all_match = True
for gamma_dot in test_shear_rates:
mu_python = carreau_yasuda_python(gamma_dot)
mu_rust = blood.apparent_viscosity(gamma_dot)
error_pct = abs(mu_rust - mu_python) / mu_python * 100
if error_pct > 0.01:
all_match = False
print(f" Mismatch at γ̇ = {gamma_dot}: Python={mu_python*1000:.4f}, Rust={mu_rust*1000:.4f} (err: {error_pct:.2f}%)")
if all_match:
print(f" ✓ MATCH for all shear rates (<0.01% diff)")
else:
print(f" Skipping test: CarreauYasudaBlood not found in cfd_python")
if hasattr(cfd_python, 'CarreauYasudaBlood'):
blood = cfd_python.CarreauYasudaBlood()
print(" Cross-validation:")
all_match = True
for gamma_dot in test_shear_rates:
mu_python = carreau_yasuda_python(gamma_dot)
mu_rust = blood.apparent_viscosity(gamma_dot)
error_pct = abs(mu_rust - mu_python) / mu_python * 100
if error_pct > 0.01:
all_match = False
print(f" Mismatch at γ̇ = {gamma_dot}: Python={mu_python*1000:.4f}, Rust={mu_rust*1000:.4f} (err: {error_pct:.2f}%)")
if all_match:
print(" ✓ MATCH for all shear rates (<0.01% diff)")
else:
print(" Skipping test: CarreauYasudaBlood not found in cfd_python")
🧰 Tools
🪛 Ruff (0.15.5)

[error] 133-133: f-string without any placeholders

Remove extraneous f prefix

(F541)


[warning] 143-143: String contains ambiguous γ (GREEK SMALL LETTER GAMMA). Did you mean y (LATIN SMALL LETTER Y)?

(RUF001)


[error] 146-146: f-string without any placeholders

Remove extraneous f prefix

(F541)


[error] 148-148: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@validation/cross_validate_rust_python.py` around lines 131 - 148, The print
statements that use f-strings but contain no placeholders should be normal
strings: update the prints in the CarreauYasudaBlood cross-validation block (the
"  Cross-validation:", the success message "  ✓ MATCH for all shear rates
(<0.01% diff)" and the skip message "  Skipping test: CarreauYasudaBlood not
found in cfd_python") to remove the leading f prefix while keeping the gamma
character in the mismatch message intact; locate the block guarded by
hasattr(cfd_python, 'CarreauYasudaBlood') and adjust the print calls so only the
mismatch line that formats mu_python/mu_rust remains an f-string and other
static messages use plain string literals.

else:
print(f"\nRust verification skipped (cfd_python not available)")

Expand Down Expand Up @@ -145,9 +184,29 @@ 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")

if hasattr(cfd_python, 'GiersiepenModel'):
model = cfd_python.GiersiepenModel()
print(f" Cross-validation:")

all_match = True
for tau, t in test_cases:
damage_python = giersiepen_python(tau, t)
damage_rust = model.calculate_damage(tau, t)

# Note: Rust model might return slightly different results based on float precision or slight variations in constants
# so we use a reasonable tolerance
error_pct = abs(damage_rust - damage_python) / damage_python * 100

if error_pct > 0.01:
all_match = False
print(f" Mismatch at τ={tau}, t={t}: Python={damage_python:.6f}, Rust={damage_rust:.6f} (err: {error_pct:.2f}%)")

if all_match:
print(f" ✓ MATCH for all test cases (<0.01% diff)")
else:
print(f" Skipping test: GiersiepenModel not found in cfd_python")
Comment on lines +187 to +209
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unnecessary f prefixes from strings without placeholders.

🧹 Proposed fix for f-string issues in TEST 3
     print(f"\nRust implementation:")
-    print(f"  Located in: crates/cfd-core/src/physics/hemolysis/models.rs")
+    print("  Located in: crates/cfd-core/src/physics/hemolysis/models.rs")

     if hasattr(cfd_python, 'GiersiepenModel'):
         model = cfd_python.GiersiepenModel()
-        print(f"  Cross-validation:")
+        print("  Cross-validation:")

         all_match = True
         for tau, t in test_cases:
             damage_python = giersiepen_python(tau, t)
             damage_rust = model.calculate_damage(tau, t)

             # Note: Rust model might return slightly different results based on float precision or slight variations in constants
             # so we use a reasonable tolerance
             error_pct = abs(damage_rust - damage_python) / damage_python * 100

             if error_pct > 0.01:
                 all_match = False
                 print(f"    Mismatch at τ={tau}, t={t}: Python={damage_python:.6f}, Rust={damage_rust:.6f} (err: {error_pct:.2f}%)")

         if all_match:
-            print(f"  ✓ MATCH for all test cases (<0.01% diff)")
+            print("  ✓ MATCH for all test cases (<0.01% diff)")
     else:
-        print(f"  Skipping test: GiersiepenModel not found in cfd_python")
+        print("  Skipping test: GiersiepenModel not found in cfd_python")
🧰 Tools
🪛 Ruff (0.15.5)

[error] 187-187: f-string without any placeholders

Remove extraneous f prefix

(F541)


[error] 191-191: f-string without any placeholders

Remove extraneous f prefix

(F541)


[error] 207-207: f-string without any placeholders

Remove extraneous f prefix

(F541)


[error] 209-209: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@validation/cross_validate_rust_python.py` around lines 187 - 209, Remove
unnecessary f-string prefixes on print statements that contain no placeholders:
change prints like print(f"  Located in: crates/..."), print(f" 
Cross-validation:"), print(f"  ✓ MATCH for all test cases (<0.01% diff)"), and
print(f"  Skipping test: GiersiepenModel not found in cfd_python") to plain
string prints. Locate the block guarded by hasattr(cfd_python,
'GiersiepenModel') around the GiersiepenModel usage and the loop over test_cases
(which calls giersiepen_python and model.calculate_damage) and update only the
string literals to remove the leading f for consistency and clarity. Ensure any
print that still needs interpolation (e.g., the mismatch message using {tau},
{t}, etc.) remains an f-string.

else:
print(f"\nRust verification skipped (cfd_python not available)")

Expand Down
Loading