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
6 changes: 3 additions & 3 deletions .github/workflows/performance-benchmarking.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ validation/references/
# Example/optimiser output directories (all crates)
outputs/
report/
.venv/
__pycache__/
4 changes: 2 additions & 2 deletions benches/cfd_benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,12 @@ fn benchmark_sparse_matrix_operations(c: &mut Criterion) {
}

fn benchmark_fluid_calculations(c: &mut Criterion) {
use cfd_core::physics::fluid::traits::Fluid as FluidTrait;
use cfd_core::physics::fluid::traits::Fluid;
use cfd_core::physics::fluid::newtonian::ConstantPropertyFluid;

let mut group = c.benchmark_group("fluid_calculations");

let fluid = Fluid::new("water".to_string(), 1000.0, 0.001, 4182.0, 0.6, 1482.0);
let fluid = ConstantPropertyFluid::new("water".to_string(), 1000.0, 0.001, 4182.0, 0.6, 1482.0);

let state = fluid.properties_at(0.0, 0.0).unwrap();

Expand Down
4 changes: 2 additions & 2 deletions benches/solver_performance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use cfd_2d::grid::StructuredGrid2D;
use cfd_2d::solvers::fdm::{FdmConfig, PoissonSolver};
use cfd_core::compute::solver::Solver;
use cfd_core::error::Result;
use cfd_core::physics::fluid::traits::Fluid;
use cfd_core::physics::fluid::newtonian::ConstantPropertyFluid;

use criterion::{black_box, criterion_group, criterion_main, Criterion};

Expand All @@ -16,7 +16,7 @@ struct NetworkBenchmarkContext {
}

fn build_network_benchmark_context() -> Result<NetworkBenchmarkContext> {
let fluid = Fluid::<f64>::water_20c()?;
let fluid = ConstantPropertyFluid::<f64>::water_20c()?;
let mut builder = NetworkBuilder::new();
let n0 = builder.add_inlet("0".to_string());
let n1 = builder.add_outlet("1".to_string());
Expand Down
47 changes: 36 additions & 11 deletions validation/compare_cavity_external.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,24 @@ def run_cfd_python_cavity(Re: float = 100, nx: int = 65, ny: int = 65):
# Check if cfd_python has cavity solver
if hasattr(cfd_python, 'CavitySolver2D'):
solver = cfd_python.CavitySolver2D(
reynolds=Re,
nx=nx, ny=ny,
Re=Re,
tolerance=1e-6,
max_iterations=20000
lid_velocity=1.0,
cavity_size=1.0
)
result = solver.solve()

return {
"u": np.array(result.u_field),
"v": np.array(result.v_field),
"p": np.array(result.p_field),
"u": np.array(result.u_field) if hasattr(result, 'u_field') else np.zeros((ny, nx)),
"v": np.array(result.v_field) if hasattr(result, 'v_field') else np.zeros((ny, nx)),
"p": np.array(result.p_field) if hasattr(result, 'p_field') else np.zeros((ny, nx)),
"u_centerline": np.array(result.u_centerline),
"v_centerline": np.array(result.v_centerline),
"x": np.array(result.x_coords),
"y": np.array(result.y_coords),
"converged": result.converged,
"iterations": result.iterations,
"residual": result.residual
"iterations": getattr(result, 'iterations', 0),
"residual": getattr(result, 'residual', 0.0)
Comment on lines +59 to +68
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The hasattr and getattr calls are hiding a critical issue. The CavityResult2D object returned by solver.solve() does not expose the u_field, v_field, p_field, iterations, or residual attributes. You can verify this by inspecting the PyCavityResult2D struct in crates/cfd-python/src/solver_2d/cavity.rs.

As a result:

  • The u, v, and p fields for the cfd_python solution will always be initialized as arrays of zeros.
  • iterations and residual will always be their default values (0 and 0.0).

This renders the subsequent validation comparison incorrect, as it will be comparing the external solution against zero fields.

To fix this, the PyCavityResult2D struct in the Rust code needs to be updated to include and expose these fields from the underlying Rust solver result.

Comment on lines +59 to +68
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

Potential shape mismatch when fields are missing.

The default np.zeros((ny, nx)) uses the function parameters, but if the solver returns different dimensions, the zero arrays won't match the x_coords/y_coords arrays accessed on lines 64-65. Consider deriving the shape from the coordinate arrays:

🛡️ Proposed fix to derive shape from coordinates
+            x_coords = np.array(result.x_coords)
+            y_coords = np.array(result.y_coords)
+            grid_shape = (len(y_coords), len(x_coords))
+            
             return {
-                "u": np.array(result.u_field) if hasattr(result, 'u_field') else np.zeros((ny, nx)),
-                "v": np.array(result.v_field) if hasattr(result, 'v_field') else np.zeros((ny, nx)),
-                "p": np.array(result.p_field) if hasattr(result, 'p_field') else np.zeros((ny, nx)),
+                "u": np.array(result.u_field) if hasattr(result, 'u_field') else np.zeros(grid_shape),
+                "v": np.array(result.v_field) if hasattr(result, 'v_field') else np.zeros(grid_shape),
+                "p": np.array(result.p_field) if hasattr(result, 'p_field') else np.zeros(grid_shape),
                 "u_centerline": np.array(result.u_centerline),
                 "v_centerline": np.array(result.v_centerline),
-                "x": np.array(result.x_coords),
-                "y": np.array(result.y_coords),
+                "x": x_coords,
+                "y": y_coords,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@validation/compare_cavity_external.py` around lines 59 - 68, The zeros
fallback for "u","v","p" uses (ny,nx) which can mismatch the actual coordinate
arrays; instead compute the grid shape from result.x_coords and result.y_coords
(e.g. shape = (len(result.y_coords), len(result.x_coords))) and use that to
create np.zeros when the respective field is missing, falling back to (ny,nx)
only if x_coords/y_coords are absent; update the creation of the "u","v","p"
entries in the dict around the block that references result, result.x_coords and
result.y_coords accordingly.

}
else:
print("WARN: CavitySolver2D not found in cfd_python - using placeholder")
Expand Down Expand Up @@ -103,9 +103,34 @@ def compare_solutions(cfd_python_result, external_result, Re: float):

# Ensure same grid size
if cfd_python_result["u"].shape != ext_sol["u"].shape:
print(f"WARN: Grid size mismatch: cfd_python {cfd_python_result['u'].shape} vs external {ext_sol['u'].shape}")
# TODO: Interpolate if needed
return None
print(f"WARN: Grid size mismatch: cfd_python {cfd_python_result['u'].shape} vs external {ext_sol['u'].shape}. Interpolating...")
from scipy.interpolate import RectBivariateSpline
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

According to PEP 8, imports should usually be at the top of the file. Please move this import statement to the top-level of the module to improve readability and conform to standard Python style. Since scipy is a required dependency for this validation script, there's no need for a local import.


# cfd_python_result original grid
x_old = cfd_python_result["x"]
y_old = cfd_python_result["y"]

# external solver grid
x_new = ext_sol["x"]
y_new = ext_sol["y"]

# Interpolate fields
spline_u = RectBivariateSpline(y_old, x_old, cfd_python_result["u"])
cfd_python_result["u"] = spline_u(y_new, x_new)

spline_v = RectBivariateSpline(y_old, x_old, cfd_python_result["v"])
cfd_python_result["v"] = spline_v(y_new, x_new)

spline_p = RectBivariateSpline(y_old, x_old, cfd_python_result["p"])
cfd_python_result["p"] = spline_p(y_new, x_new)
Comment on lines +117 to +125
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The interpolation logic for the u, v, and p fields is duplicated. This can be refactored into a loop to make the code more concise and easier to maintain.

Suggested change
# Interpolate fields
spline_u = RectBivariateSpline(y_old, x_old, cfd_python_result["u"])
cfd_python_result["u"] = spline_u(y_new, x_new)
spline_v = RectBivariateSpline(y_old, x_old, cfd_python_result["v"])
cfd_python_result["v"] = spline_v(y_new, x_new)
spline_p = RectBivariateSpline(y_old, x_old, cfd_python_result["p"])
cfd_python_result["p"] = spline_p(y_new, x_new)
# Interpolate fields
for field_key in ["u", "v", "p"]:
spline = RectBivariateSpline(y_old, x_old, cfd_python_result[field_key])
cfd_python_result[field_key] = spline(y_new, x_new)

Comment on lines +106 to +125
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

RectBivariateSpline requires strictly ascending coordinates.

If the solver returns grid coordinates in descending order or non-monotonic order, RectBivariateSpline will raise a ValueError. Consider validating or sorting the coordinates before interpolation.

🛡️ Proposed fix to validate coordinate ordering
         # cfd_python_result original grid
         x_old = cfd_python_result["x"]
         y_old = cfd_python_result["y"]
+        
+        # RectBivariateSpline requires strictly ascending coordinates
+        if not (np.all(np.diff(x_old) > 0) and np.all(np.diff(y_old) > 0)):
+            print("ERROR: Grid coordinates must be strictly ascending for interpolation")
+            return None

         # external solver grid
         x_new = ext_sol["x"]
         y_new = ext_sol["y"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@validation/compare_cavity_external.py` around lines 106 - 125,
RectBivariateSpline requires strictly ascending coordinates so validate and fix
x_old and y_old before creating spline_* (spline_u, spline_v, spline_p): check
if x_old or y_old are strictly increasing and if not, sort them (or reverse them
if they are strictly decreasing) and reorder the corresponding
cfd_python_result["u"], ["v"], ["p"] axes to match the sorted coordinate
ordering; then construct RectBivariateSpline with the corrected x_old/y_old and
proceed with spline(y_new, x_new); also catch ValueError from
RectBivariateSpline and raise a clear error if coordinates are non-monotonic
(not sortable) so the caller can handle it.


# Update centerlines
cfd_python_result["u_centerline"] = cfd_python_result["u"][:, len(x_new)//2]
cfd_python_result["v_centerline"] = cfd_python_result["v"][len(y_new)//2, :]

# Update coordinates
cfd_python_result["x"] = x_new
cfd_python_result["y"] = y_new

# Compute L2 errors
u_diff = cfd_python_result["u"] - ext_sol["u"]
Expand Down
Loading