From c49049f7734c5bde45921848fda69d3f87d61d45 Mon Sep 17 00:00:00 2001 From: shaia Date: Fri, 6 Mar 2026 20:16:36 +0200 Subject: [PATCH 01/12] foundation: Add pytest config, single-source version, VTKData validation and field aliases Phase 2 (Foundation & Code Quality) source changes: - Add [tool.pytest.ini_options] with testpaths, markers, and filterwarnings - Replace hardcoded __version__ with importlib.metadata lookup - Add VTKData field shape validation (ValueError on mismatch) - Add NaN/inf warnings and __repr__ to VTKData - Add FIELD_ALIASES and CANONICAL_NAMES for field name normalization - Add __contains__ and has_field() to VTKData for alias-aware lookup - Normalize SCALARS field names at VTK read time (e.g. "pressure" -> "p") --- cfd_viz/__init__.py | 7 +++- cfd_viz/common/__init__.py | 3 +- cfd_viz/common/vtk_reader.py | 71 +++++++++++++++++++++++++++++++++--- pyproject.toml | 14 +++++++ 4 files changed, 88 insertions(+), 7 deletions(-) diff --git a/cfd_viz/__init__.py b/cfd_viz/__init__.py index d08070d..0d5e520 100644 --- a/cfd_viz/__init__.py +++ b/cfd_viz/__init__.py @@ -36,7 +36,12 @@ For more examples, see the examples/ directory. """ -__version__ = "0.1.0" +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("cfd-visualization") +except PackageNotFoundError: + __version__ = "0.1.0" # Fallback for uninstalled development usage # Re-export commonly used items for convenience # Import fields module for easy access diff --git a/cfd_viz/common/__init__.py b/cfd_viz/common/__init__.py index de8d5d1..993c8f8 100644 --- a/cfd_viz/common/__init__.py +++ b/cfd_viz/common/__init__.py @@ -1,10 +1,11 @@ # Common utilities package from .config import ANIMATIONS_DIR, DATA_DIR, PLOTS_DIR, ensure_dirs, find_vtk_files -from .vtk_reader import VTKData, read_vtk_file +from .vtk_reader import FIELD_ALIASES, VTKData, read_vtk_file __all__ = [ "ANIMATIONS_DIR", "DATA_DIR", + "FIELD_ALIASES", "PLOTS_DIR", "VTKData", "ensure_dirs", diff --git a/cfd_viz/common/vtk_reader.py b/cfd_viz/common/vtk_reader.py index 6e91ee1..cc2a9fe 100644 --- a/cfd_viz/common/vtk_reader.py +++ b/cfd_viz/common/vtk_reader.py @@ -10,11 +10,26 @@ across all visualization scripts in the project. """ +import warnings from typing import Any, Dict, Optional, Tuple import numpy as np from numpy.typing import NDArray +# Maps VTK file field names to canonical short names used in VTKData.fields. +CANONICAL_NAMES: Dict[str, str] = { + "pressure": "p", +} + +# Maps alternative user-facing names to canonical names for lookup. +FIELD_ALIASES: Dict[str, str] = { + "pressure": "p", + "velocity_x": "u", + "velocity_y": "v", + "x_velocity": "u", + "y_velocity": "v", +} + class VTKData: """Container for VTK file data with convenient access patterns.""" @@ -41,6 +56,38 @@ def __init__( self.dx = dx self.dy = dy + # Validate field shapes + expected_shape = (ny, nx) + for name, field in self.fields.items(): + if field.shape != expected_shape: + raise ValueError( + f"Field '{name}' has shape {field.shape}, " + f"expected {expected_shape} (ny, nx)" + ) + + # Warn on NaN/inf values + for name, field in self.fields.items(): + if np.any(np.isnan(field)): + warnings.warn( + f"Field '{name}' contains NaN values", + UserWarning, + stacklevel=2, + ) + if np.any(np.isinf(field)): + warnings.warn( + f"Field '{name}' contains infinite values", + UserWarning, + stacklevel=2, + ) + + def __repr__(self) -> str: + field_names = ", ".join(sorted(self.fields.keys())) + return ( + f"VTKData(nx={self.nx}, ny={self.ny}, " + f"dx={self.dx:.6g}, dy={self.dy:.6g}, " + f"fields=[{field_names}])" + ) + @property def u(self) -> Optional[NDArray]: """X-velocity component.""" @@ -52,12 +99,25 @@ def v(self) -> Optional[NDArray]: return self.fields.get("v") def __getitem__(self, key: str) -> NDArray: - """Access fields by name.""" - return self.fields[key] + """Access fields by name, with alias support.""" + canonical = FIELD_ALIASES.get(key, key) + if canonical in self.fields: + return self.fields[canonical] + raise KeyError(key) def get(self, key: str, default: Any = None) -> Any: - """Get field with default value.""" - return self.fields.get(key, default) + """Get field with default value, with alias support.""" + canonical = FIELD_ALIASES.get(key, key) + return self.fields.get(canonical, default) + + def __contains__(self, key: str) -> bool: + """Support 'key in data' syntax, with alias support.""" + canonical = FIELD_ALIASES.get(key, key) + return canonical in self.fields + + def has_field(self, key: str) -> bool: + """Check if a field exists (supports aliases).""" + return key in self def keys(self): """Return field names.""" @@ -191,7 +251,8 @@ def read_vtk_file(filename: str) -> Optional[VTKData]: i += 1 if len(values) == nx * ny: - fields[field_name] = np.array(values).reshape((ny, nx)) + canonical = CANONICAL_NAMES.get(field_name, field_name) + fields[canonical] = np.array(values).reshape((ny, nx)) continue i += 1 diff --git a/pyproject.toml b/pyproject.toml index b24d5a8..35a2f2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,6 +122,20 @@ indent-style = "space" # Unix-style line endings for cross-platform consistency line-ending = "lf" +[tool.pytest.ini_options] +testpaths = ["tests"] +markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", + "integration: marks end-to-end integration tests", +] +filterwarnings = [ + "error", + "ignore::DeprecationWarning:matplotlib.*", + "ignore::DeprecationWarning:numpy.*", + "ignore::matplotlib._api.deprecation.MatplotlibDeprecationWarning", + "ignore::pytest.PytestUnraisableExceptionWarning", +] + [dependency-groups] dev = [ "cfd-python>=0.1.6", From d8e0e1012418b76a1fa497b03e8d6165b85612aa Mon Sep 17 00:00:00 2001 From: shaia Date: Fri, 6 Mar 2026 20:16:51 +0200 Subject: [PATCH 02/12] test: Add VTK reader tests and end-to-end integration tests 35 tests for VTK reader covering construction, validation, field aliases, STRUCTURED_POINTS/RECTILINEAR_GRID parsing, malformed files, and the read_vtk_velocity convenience function. 5 integration tests verifying the full pipeline: VTK read -> compute derived fields -> plot, cfd-python conversion, field alias resolution, version accessibility, and VTKData repr. --- tests/common/__init__.py | 0 tests/common/test_vtk_reader.py | 432 ++++++++++++++++++++++++++++++++ tests/test_integration.py | 79 ++++++ 3 files changed, 511 insertions(+) create mode 100644 tests/common/__init__.py create mode 100644 tests/common/test_vtk_reader.py create mode 100644 tests/test_integration.py diff --git a/tests/common/__init__.py b/tests/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/common/test_vtk_reader.py b/tests/common/test_vtk_reader.py new file mode 100644 index 0000000..d1ccc00 --- /dev/null +++ b/tests/common/test_vtk_reader.py @@ -0,0 +1,432 @@ +"""Tests for cfd_viz.common.vtk_reader module.""" + +from pathlib import Path + +import numpy as np +import pytest + +from cfd_viz.common.vtk_reader import ( + FIELD_ALIASES, + VTKData, + read_vtk_file, + read_vtk_velocity, +) + +SAMPLE_VTK_DIR = Path(__file__).parent.parent.parent / "data" / "vtk_files" + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_vtk_data(nx=4, ny=3, fields=None): + """Create a minimal VTKData for testing.""" + x = np.linspace(0, 1, nx) + y = np.linspace(0, 1, ny) + X, Y = np.meshgrid(x, y) + dx = x[1] - x[0] if nx > 1 else 1.0 + dy = y[1] - y[0] if ny > 1 else 1.0 + if fields is None: + fields = { + "u": np.ones((ny, nx)), + "v": np.zeros((ny, nx)), + } + return VTKData(x=x, y=y, X=X, Y=Y, fields=fields, nx=nx, ny=ny, dx=dx, dy=dy) + + +def _write_structured_points( + path, nx, ny, vectors=None, scalars=None, origin=(0, 0, 0), spacing=(1, 1, 1) +): + """Write a minimal STRUCTURED_POINTS VTK file.""" + lines = [ + "# vtk DataFile Version 3.0", + "Test", + "ASCII", + "DATASET STRUCTURED_POINTS", + f"DIMENSIONS {nx} {ny} 1", + f"ORIGIN {origin[0]} {origin[1]} {origin[2]}", + f"SPACING {spacing[0]} {spacing[1]} {spacing[2]}", + "", + f"POINT_DATA {nx * ny}", + ] + if vectors is not None: + lines.append("VECTORS velocity float") + for u, v in zip(vectors[0].ravel(), vectors[1].ravel()): + lines.append(f"{u} {v} 0.0") + if scalars is not None: + for name, data in scalars.items(): + lines.append(f"SCALARS {name} float 1") + lines.append("LOOKUP_TABLE default") + for val in data.ravel(): + lines.append(str(val)) + path.write_text("\n".join(lines) + "\n") + + +# --------------------------------------------------------------------------- +# VTKData construction +# --------------------------------------------------------------------------- + + +class TestVTKDataConstruction: + def test_basic_construction(self): + data = _make_vtk_data() + assert data.nx == 4 + assert data.ny == 3 + assert data.X.shape == (3, 4) + + def test_u_v_properties(self): + data = _make_vtk_data() + assert data.u is not None + np.testing.assert_array_equal(data.u, np.ones((3, 4))) + np.testing.assert_array_equal(data.v, np.zeros((3, 4))) + + def test_u_v_properties_missing(self): + data = _make_vtk_data(fields={"p": np.zeros((3, 4))}) + assert data.u is None + assert data.v is None + + def test_field_access_by_name(self): + data = _make_vtk_data() + np.testing.assert_array_equal(data["u"], np.ones((3, 4))) + + def test_field_access_by_alias(self): + data = _make_vtk_data(fields={"p": np.ones((3, 4))}) + np.testing.assert_array_equal(data["pressure"], np.ones((3, 4))) + + def test_get_with_default(self): + data = _make_vtk_data() + assert data.get("nonexistent") is None + assert data.get("nonexistent", 42) == 42 + + def test_get_alias(self): + data = _make_vtk_data(fields={"p": np.ones((3, 4))}) + result = data.get("pressure") + assert result is not None + np.testing.assert_array_equal(result, np.ones((3, 4))) + + def test_has_field(self): + data = _make_vtk_data() + assert data.has_field("u") + assert not data.has_field("nonexistent") + + def test_has_field_alias(self): + data = _make_vtk_data(fields={"p": np.zeros((3, 4))}) + assert data.has_field("pressure") + + def test_keys_returns_canonical_names(self): + data = _make_vtk_data(fields={"u": np.zeros((3, 4)), "p": np.ones((3, 4))}) + assert set(data.keys()) == {"u", "p"} + + def test_to_dict(self): + data = _make_vtk_data() + d = data.to_dict() + assert "u" in d + assert "X" in d + assert d["nx"] == 4 + + def test_repr(self): + data = _make_vtk_data() + r = repr(data) + assert "VTKData" in r + assert "nx=4" in r + assert "ny=3" in r + assert "u" in r + + def test_invalid_field_shape_raises(self): + with pytest.raises(ValueError, match="Field 'u' has shape"): + _make_vtk_data(fields={"u": np.ones((5, 5))}) + + def test_nan_warning(self): + fields = {"u": np.array([[1, float("nan")], [3, 4]])} + with pytest.warns(UserWarning, match="NaN"): + _make_vtk_data(nx=2, ny=2, fields=fields) + + def test_inf_warning(self): + fields = {"u": np.array([[1, float("inf")], [3, 4]])} + with pytest.warns(UserWarning, match="infinite"): + _make_vtk_data(nx=2, ny=2, fields=fields) + + def test_getitem_missing_raises_keyerror(self): + data = _make_vtk_data() + with pytest.raises(KeyError): + data["nonexistent"] + + +# --------------------------------------------------------------------------- +# Reading STRUCTURED_POINTS +# --------------------------------------------------------------------------- + + +class TestReadVTKFileStructuredPoints: + def test_read_real_sample_file(self): + vtk_file = SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk" + data = read_vtk_file(str(vtk_file)) + assert data is not None + assert data.nx == 50 + assert data.ny == 50 + assert data.u is not None + assert data.v is not None + assert data.u.shape == (50, 50) + + def test_real_file_with_scalars(self): + vtk_file = SAMPLE_VTK_DIR / "animated_flow_0050.vtk" + data = read_vtk_file(str(vtk_file)) + assert data is not None + # "pressure" should be normalized to "p" + assert "p" in data + assert data["p"].shape == (data.ny, data.nx) + + def test_origin_and_spacing(self): + vtk_file = SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk" + data = read_vtk_file(str(vtk_file)) + assert data.dx == pytest.approx(0.020408, rel=1e-3) + assert data.x[0] == pytest.approx(0.0) + + def test_synthetic_minimal(self, tmp_path): + u = np.array([[1.0, 2.0], [3.0, 4.0]]) + v = np.array([[0.1, 0.2], [0.3, 0.4]]) + vtk_file = tmp_path / "test.vtk" + _write_structured_points(vtk_file, nx=2, ny=2, vectors=(u, v)) + data = read_vtk_file(str(vtk_file)) + assert data is not None + assert data.nx == 2 + assert data.ny == 2 + np.testing.assert_allclose(data.u, u) + np.testing.assert_allclose(data.v, v) + + def test_scalars_with_lookup_table(self, tmp_path): + u = np.ones((3, 3)) + v = np.zeros((3, 3)) + p = np.arange(9, dtype=float).reshape(3, 3) + vtk_file = tmp_path / "test.vtk" + _write_structured_points( + vtk_file, + nx=3, + ny=3, + vectors=(u, v), + scalars={"pressure": p}, + ) + data = read_vtk_file(str(vtk_file)) + assert data is not None + # "pressure" normalized to "p" + assert "p" in data + np.testing.assert_allclose(data["p"], p) + + def test_custom_origin_and_spacing(self, tmp_path): + u = np.ones((2, 3)) + v = np.zeros((2, 3)) + vtk_file = tmp_path / "test.vtk" + _write_structured_points( + vtk_file, + nx=3, + ny=2, + vectors=(u, v), + origin=(1.0, 2.0, 0.0), + spacing=(0.5, 0.25, 1.0), + ) + data = read_vtk_file(str(vtk_file)) + assert data.x[0] == pytest.approx(1.0) + assert data.x[-1] == pytest.approx(2.0) + assert data.y[0] == pytest.approx(2.0) + assert data.dx == pytest.approx(0.5) + assert data.dy == pytest.approx(0.25) + + +# --------------------------------------------------------------------------- +# Reading RECTILINEAR_GRID +# --------------------------------------------------------------------------- + + +class TestReadVTKFileRectilinearGrid: + def test_synthetic_rectilinear(self, tmp_path): + nx, ny = 3, 3 + content = ( + "# vtk DataFile Version 3.0\n" + "Rectilinear Test\n" + "ASCII\n" + "DATASET RECTILINEAR_GRID\n" + f"DIMENSIONS {nx} {ny} 1\n" + "X_COORDINATES 3 float\n" + "0.0 0.5 1.0\n" + "Y_COORDINATES 3 float\n" + "0.0 0.5 1.0\n" + "Z_COORDINATES 1 float\n" + "0.0\n" + "\n" + f"POINT_DATA {nx * ny}\n" + "VECTORS velocity float\n" + ) + u_vals = np.arange(1, 10, dtype=float) + v_vals = np.zeros(9) + vec_lines = "\n".join(f"{u} {v} 0.0" for u, v in zip(u_vals, v_vals)) + vtk_file = tmp_path / "rect.vtk" + vtk_file.write_text(content + vec_lines + "\n") + + data = read_vtk_file(str(vtk_file)) + assert data is not None + assert data.nx == 3 + assert data.ny == 3 + assert data.x[0] == pytest.approx(0.0) + assert data.x[-1] == pytest.approx(1.0) + np.testing.assert_allclose(data.u.ravel(), u_vals) + + def test_non_uniform_spacing(self, tmp_path): + nx, ny = 3, 2 + content = ( + "# vtk DataFile Version 3.0\n" + "Non-uniform\n" + "ASCII\n" + "DATASET RECTILINEAR_GRID\n" + f"DIMENSIONS {nx} {ny} 1\n" + "X_COORDINATES 3 float\n" + "0.0 0.3 1.0\n" + "Y_COORDINATES 2 float\n" + "0.0 1.0\n" + "Z_COORDINATES 1 float\n" + "0.0\n" + "\n" + f"POINT_DATA {nx * ny}\n" + "VECTORS velocity float\n" + ) + vec_lines = "\n".join("1.0 0.0 0.0" for _ in range(nx * ny)) + vtk_file = tmp_path / "nonuniform.vtk" + vtk_file.write_text(content + vec_lines + "\n") + + data = read_vtk_file(str(vtk_file)) + assert data is not None + # Non-uniform: dx is computed from first two coordinates + assert data.x[1] == pytest.approx(0.3) + + +# --------------------------------------------------------------------------- +# Malformed files +# --------------------------------------------------------------------------- + + +class TestReadVTKFileMalformed: + def test_missing_dimensions(self, tmp_path): + content = ( + "# vtk DataFile Version 3.0\n" + "No dims\n" + "ASCII\n" + "DATASET STRUCTURED_POINTS\n" + "POINT_DATA 4\n" + "VECTORS velocity float\n" + "1.0 0.0 0.0\n" + "1.0 0.0 0.0\n" + "1.0 0.0 0.0\n" + "1.0 0.0 0.0\n" + ) + vtk_file = tmp_path / "nodims.vtk" + vtk_file.write_text(content) + with pytest.raises(ValueError, match="dimensions not found"): + read_vtk_file(str(vtk_file)) + + def test_file_not_found(self): + result = read_vtk_file("/nonexistent/path/to/file.vtk") + assert result is None + + def test_empty_file(self, tmp_path): + vtk_file = tmp_path / "empty.vtk" + vtk_file.write_text("") + with pytest.raises(ValueError, match="dimensions not found"): + read_vtk_file(str(vtk_file)) + + def test_truncated_vector_data(self, tmp_path): + content = ( + "# vtk DataFile Version 3.0\n" + "Truncated\n" + "ASCII\n" + "DATASET STRUCTURED_POINTS\n" + "DIMENSIONS 3 3 1\n" + "ORIGIN 0 0 0\n" + "SPACING 1 1 1\n" + "\n" + "POINT_DATA 9\n" + "VECTORS velocity float\n" + "1.0 0.0 0.0\n" + "1.0 0.0 0.0\n" + ) + vtk_file = tmp_path / "truncated.vtk" + vtk_file.write_text(content) + # Truncated data causes reshape error + with pytest.raises(ValueError, match="cannot reshape"): + read_vtk_file(str(vtk_file)) + + +# --------------------------------------------------------------------------- +# read_vtk_velocity convenience function +# --------------------------------------------------------------------------- + + +class TestReadVTKVelocity: + def test_returns_tuple(self): + vtk_file = SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk" + result = read_vtk_velocity(str(vtk_file)) + X, Y, u, v = result + assert X is not None + assert X.shape == (50, 50) + assert u.shape == (50, 50) + + def test_file_not_found_returns_nones(self): + result = read_vtk_velocity("/nonexistent/file.vtk") + assert all(r is None for r in result) + + +# --------------------------------------------------------------------------- +# Field aliases +# --------------------------------------------------------------------------- + + +class TestFieldAliases: + def test_pressure_alias_maps_to_p(self): + assert FIELD_ALIASES["pressure"] == "p" + + def test_velocity_aliases(self): + assert FIELD_ALIASES["velocity_x"] == "u" + assert FIELD_ALIASES["velocity_y"] == "v" + + def test_canonical_names_not_in_aliases_as_values(self): + # Canonical names should not map to themselves + assert "u" not in FIELD_ALIASES + assert "v" not in FIELD_ALIASES + assert "p" not in FIELD_ALIASES + + def test_canonical_name_normalization_at_read_time(self, tmp_path): + """SCALARS 'pressure' should be stored as 'p' in fields.""" + u = np.ones((2, 2)) + v = np.zeros((2, 2)) + p = np.array([[1.0, 2.0], [3.0, 4.0]]) + vtk_file = tmp_path / "test.vtk" + _write_structured_points( + vtk_file, + nx=2, + ny=2, + vectors=(u, v), + scalars={"pressure": p}, + ) + data = read_vtk_file(str(vtk_file)) + # Stored under canonical name "p", not "pressure" + assert "p" in data.fields + assert "pressure" not in data.fields + # But accessible via alias through __contains__ and __getitem__ + assert "pressure" in data + np.testing.assert_allclose(data["pressure"], p) + + def test_unknown_scalar_name_preserved(self, tmp_path): + """SCALARS with unknown names are stored as-is.""" + u = np.ones((2, 2)) + v = np.zeros((2, 2)) + temp = np.array([[300.0, 301.0], [302.0, 303.0]]) + vtk_file = tmp_path / "test.vtk" + _write_structured_points( + vtk_file, + nx=2, + ny=2, + vectors=(u, v), + scalars={"temperature": temp}, + ) + data = read_vtk_file(str(vtk_file)) + assert "temperature" in data + np.testing.assert_allclose(data["temperature"], temp) diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..67a9341 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,79 @@ +"""End-to-end integration tests for cfd-visualization pipeline.""" + +from pathlib import Path + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np +import pytest + +from cfd_viz.common import read_vtk_file +from cfd_viz.convert import from_cfd_python +from cfd_viz.fields import magnitude, vorticity +from cfd_viz.plotting import plot_contour_field + +SAMPLE_VTK_DIR = Path(__file__).parent.parent / "data" / "vtk_files" + + +@pytest.mark.integration +class TestEndToEnd: + def test_vtk_read_compute_plot(self): + """Read VTK file -> compute derived fields -> plot -> no exceptions.""" + data = read_vtk_file(str(SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk")) + assert data is not None + + speed = magnitude(data.u, data.v) + omega = vorticity(data.u, data.v, data.dx, data.dy) + + assert speed.shape == (data.ny, data.nx) + assert omega.shape == (data.ny, data.nx) + + fig, axes = plt.subplots(1, 2, figsize=(12, 5)) + plot_contour_field(data.X, data.Y, speed, ax=axes[0], title="Speed") + plot_contour_field(data.X, data.Y, omega, ax=axes[1], title="Vorticity") + plt.close(fig) + + def test_cfd_python_convert_plot(self): + """from_cfd_python -> compute -> plot -> no exceptions.""" + nx, ny = 20, 20 + rng = np.random.default_rng(42) + u_flat = rng.standard_normal(nx * ny).tolist() + v_flat = rng.standard_normal(nx * ny).tolist() + p_flat = rng.standard_normal(nx * ny).tolist() + + data = from_cfd_python(u_flat, v_flat, nx=nx, ny=ny, p=p_flat) + + speed = magnitude(data.u, data.v) + fig, ax = plt.subplots() + plot_contour_field(data.X, data.Y, speed, ax=ax) + plt.close(fig) + + def test_field_alias_in_pipeline(self): + """Verify aliases work through the full pipeline.""" + data = read_vtk_file(str(SAMPLE_VTK_DIR / "animated_flow_0050.vtk")) + assert data is not None + + # "pressure" was normalized to "p" at read time + p = data.get("pressure") + assert p is not None + + fig, ax = plt.subplots() + plot_contour_field(data.X, data.Y, p, ax=ax) + plt.close(fig) + + def test_version_accessible(self): + """__version__ should be a non-empty string.""" + from cfd_viz import __version__ + + assert isinstance(__version__, str) + assert len(__version__) > 0 + + def test_vtk_data_repr(self): + """VTKData repr should be informative.""" + data = read_vtk_file(str(SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk")) + r = repr(data) + assert "VTKData" in r + assert "50" in r + assert "u" in r From 56f17aa504086349db81f59ebdeabe3cfcd5ce49 Mon Sep 17 00:00:00 2001 From: shaia Date: Fri, 6 Mar 2026 20:17:38 +0200 Subject: [PATCH 03/12] docs: Mark Phase 2 as complete in roadmap --- ROADMAP.md | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 817f59c..4b2a403 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -48,27 +48,18 @@ The cfd-python integration had inline `if/else` branching scattered across `stat --- -## Phase 2: Foundation & Code Quality +## Phase 2: Foundation & Code Quality - COMPLETE -**Priority:** P0 - Must-have before adding features -**Target:** v0.2.0 - -The VTK reader (`cfd_viz/common/vtk_reader.py`) is 240 lines with zero dedicated tests. VTKData accepts any data without validation. The version string is duplicated between `pyproject.toml` and `__init__.py`. These are trust issues for a library used in research. - -### Tasks - -- [ ] **2.1 Add pytest configuration** to `pyproject.toml` - testpaths, markers, filterwarnings -- [ ] **2.2 Write VTK reader tests** - STRUCTURED_POINTS/RECTILINEAR_GRID parsing, malformed files, edge cases (empty fields, single-row grids) -- [ ] **2.3 Add VTKData validation** - verify field shapes match `(ny, nx)` on construction, warn on NaN/inf, add `__repr__` for debugging -- [ ] **2.4 Normalize field naming** - add `FIELD_ALIASES` mapping so `data["pressure"]` and `data["p"]` both work; standardize across all modules -- [ ] **2.5 Single-source version** - use hatchling dynamic version or `importlib.metadata`; remove the duplication -- [ ] **2.6 Add integration test** - end-to-end: create VTK data in memory, read it, compute vorticity, plot to figure, assert no exceptions +**Status:** Completed (2026-03-06) -### Success Criteria +The VTK reader had zero dedicated tests and VTKData accepted any data without validation. This phase added validation, field aliases, single-source versioning, and comprehensive test coverage. -- VTK reader has dedicated test coverage -- VTKData raises `ValueError` for mismatched field shapes -- Version appears in exactly one source file +- [x] **2.1 Add pytest configuration** to `pyproject.toml` - testpaths, markers (`slow`, `integration`), filterwarnings +- [x] **2.2 Write VTK reader tests** - 35 tests covering STRUCTURED_POINTS/RECTILINEAR_GRID parsing, malformed files, edge cases, field aliases +- [x] **2.3 Add VTKData validation** - field shape validation (`ValueError`), NaN/inf warnings, `__repr__` for debugging +- [x] **2.4 Normalize field naming** - `FIELD_ALIASES` and `CANONICAL_NAMES` mappings; `data["pressure"]` and `data["p"]` both work; `__contains__`, `has_field()` methods +- [x] **2.5 Single-source version** - `importlib.metadata` in `__init__.py`; version defined only in `pyproject.toml` +- [x] **2.6 Add integration test** - 5 end-to-end tests: VTK read → compute → plot, cfd-python conversion, field aliases, version, repr --- From 4879ae2bad535e83a5640743d418f624a30d1f25 Mon Sep 17 00:00:00 2001 From: shaia Date: Sat, 7 Mar 2026 06:53:45 +0200 Subject: [PATCH 04/12] test: Skip tests requiring sample VTK files when files are unavailable VTK sample files are gitignored (*.vtk) so they don't exist in CI. Tests that depend on real VTK files now skip gracefully instead of failing. --- tests/common/test_vtk_reader.py | 8 ++++++++ tests/test_integration.py | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/tests/common/test_vtk_reader.py b/tests/common/test_vtk_reader.py index d1ccc00..db9911d 100644 --- a/tests/common/test_vtk_reader.py +++ b/tests/common/test_vtk_reader.py @@ -13,6 +13,10 @@ ) SAMPLE_VTK_DIR = Path(__file__).parent.parent.parent / "data" / "vtk_files" +_sample_file_missing = not (SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk").exists() +_skip_no_samples = pytest.mark.skipif( + _sample_file_missing, reason="Sample VTK files not available" +) # --------------------------------------------------------------------------- @@ -159,6 +163,7 @@ def test_getitem_missing_raises_keyerror(self): class TestReadVTKFileStructuredPoints: + @_skip_no_samples def test_read_real_sample_file(self): vtk_file = SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk" data = read_vtk_file(str(vtk_file)) @@ -169,6 +174,7 @@ def test_read_real_sample_file(self): assert data.v is not None assert data.u.shape == (50, 50) + @_skip_no_samples def test_real_file_with_scalars(self): vtk_file = SAMPLE_VTK_DIR / "animated_flow_0050.vtk" data = read_vtk_file(str(vtk_file)) @@ -177,6 +183,7 @@ def test_real_file_with_scalars(self): assert "p" in data assert data["p"].shape == (data.ny, data.nx) + @_skip_no_samples def test_origin_and_spacing(self): vtk_file = SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk" data = read_vtk_file(str(vtk_file)) @@ -361,6 +368,7 @@ def test_truncated_vector_data(self, tmp_path): class TestReadVTKVelocity: + @_skip_no_samples def test_returns_tuple(self): vtk_file = SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk" result = read_vtk_velocity(str(vtk_file)) diff --git a/tests/test_integration.py b/tests/test_integration.py index 67a9341..d7a6913 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -15,10 +15,15 @@ from cfd_viz.plotting import plot_contour_field SAMPLE_VTK_DIR = Path(__file__).parent.parent / "data" / "vtk_files" +_sample_file_missing = not (SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk").exists() +_skip_no_samples = pytest.mark.skipif( + _sample_file_missing, reason="Sample VTK files not available" +) @pytest.mark.integration class TestEndToEnd: + @_skip_no_samples def test_vtk_read_compute_plot(self): """Read VTK file -> compute derived fields -> plot -> no exceptions.""" data = read_vtk_file(str(SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk")) @@ -50,6 +55,7 @@ def test_cfd_python_convert_plot(self): plot_contour_field(data.X, data.Y, speed, ax=ax) plt.close(fig) + @_skip_no_samples def test_field_alias_in_pipeline(self): """Verify aliases work through the full pipeline.""" data = read_vtk_file(str(SAMPLE_VTK_DIR / "animated_flow_0050.vtk")) @@ -70,6 +76,7 @@ def test_version_accessible(self): assert isinstance(__version__, str) assert len(__version__) > 0 + @_skip_no_samples def test_vtk_data_repr(self): """VTKData repr should be informative.""" data = read_vtk_file(str(SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk")) From ff82585700ff3de25b46afeafa7a05c88fc0b02d Mon Sep 17 00:00:00 2001 From: shaia Date: Sat, 7 Mar 2026 07:12:40 +0200 Subject: [PATCH 05/12] fix: Replace isalpha() heuristic with VTK section header check in SCALARS parser The isalpha() check treated valid float tokens like "nan" and "inf" as VTK keywords, prematurely terminating scalar field parsing. Check against known VTK section headers instead. --- cfd_viz/common/vtk_reader.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/cfd_viz/common/vtk_reader.py b/cfd_viz/common/vtk_reader.py index cc2a9fe..db03a3d 100644 --- a/cfd_viz/common/vtk_reader.py +++ b/cfd_viz/common/vtk_reader.py @@ -16,6 +16,28 @@ import numpy as np from numpy.typing import NDArray +# VTK section headers that signal the end of a data block. +_VTK_SECTION_HEADERS = frozenset( + { + "SCALARS", + "VECTORS", + "NORMALS", + "TENSORS", + "TEXTURE_COORDINATES", + "FIELD", + "LOOKUP_TABLE", + "POINT_DATA", + "CELL_DATA", + "DATASET", + "DIMENSIONS", + "ORIGIN", + "SPACING", + "X_COORDINATES", + "Y_COORDINATES", + "Z_COORDINATES", + } +) + # Maps VTK file field names to canonical short names used in VTKData.fields. CANONICAL_NAMES: Dict[str, str] = { "pressure": "p", @@ -242,7 +264,7 @@ def read_vtk_file(filename: str) -> Optional[VTKData]: if not line_content: i += 1 continue - if line_content.split()[0].isalpha(): + if line_content.split()[0] in _VTK_SECTION_HEADERS: break try: values.extend([float(x) for x in line_content.split()]) From c3826e574182b2b0372efeb79ef2e51919cccf14 Mon Sep 17 00:00:00 2001 From: shaia Date: Sat, 7 Mar 2026 07:47:13 +0200 Subject: [PATCH 06/12] fix: Use non-authoritative version fallback to avoid drift from pyproject.toml Replace hard-coded "0.1.0" fallback with "0+unknown" so it cannot silently drift from the canonical version in pyproject.toml. --- cfd_viz/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cfd_viz/__init__.py b/cfd_viz/__init__.py index 0d5e520..53a9ebd 100644 --- a/cfd_viz/__init__.py +++ b/cfd_viz/__init__.py @@ -41,7 +41,7 @@ try: __version__ = version("cfd-visualization") except PackageNotFoundError: - __version__ = "0.1.0" # Fallback for uninstalled development usage + __version__ = "0+unknown" # Fallback for uninstalled development usage # Re-export commonly used items for convenience # Import fields module for easy access From f7cd550fcbd0df2070f700ba312ccb1eb0b3e441 Mon Sep 17 00:00:00 2001 From: shaia Date: Sat, 7 Mar 2026 08:05:40 +0200 Subject: [PATCH 07/12] test: Replace sample-file-dependent tests with synthetic VTK fixtures Tests that previously required gitignored VTK sample files now generate their own test data via _write_structured_points in tmp_path, so they work in CI without needing real data files. --- tests/common/test_vtk_reader.py | 74 ++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/tests/common/test_vtk_reader.py b/tests/common/test_vtk_reader.py index db9911d..6e376ef 100644 --- a/tests/common/test_vtk_reader.py +++ b/tests/common/test_vtk_reader.py @@ -1,7 +1,5 @@ """Tests for cfd_viz.common.vtk_reader module.""" -from pathlib import Path - import numpy as np import pytest @@ -12,13 +10,6 @@ read_vtk_velocity, ) -SAMPLE_VTK_DIR = Path(__file__).parent.parent.parent / "data" / "vtk_files" -_sample_file_missing = not (SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk").exists() -_skip_no_samples = pytest.mark.skipif( - _sample_file_missing, reason="Sample VTK files not available" -) - - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -163,31 +154,55 @@ def test_getitem_missing_raises_keyerror(self): class TestReadVTKFileStructuredPoints: - @_skip_no_samples - def test_read_real_sample_file(self): - vtk_file = SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk" + def test_read_real_sample_file(self, tmp_path): + nx, ny = 50, 50 + u = np.ones((ny, nx)) + v = np.zeros((ny, nx)) + vtk_file = tmp_path / "flow_field_50x50_Re100.vtk" + _write_structured_points(vtk_file, nx=nx, ny=ny, vectors=(u, v)) data = read_vtk_file(str(vtk_file)) assert data is not None - assert data.nx == 50 - assert data.ny == 50 + assert data.nx == nx + assert data.ny == ny assert data.u is not None assert data.v is not None - assert data.u.shape == (50, 50) - - @_skip_no_samples - def test_real_file_with_scalars(self): - vtk_file = SAMPLE_VTK_DIR / "animated_flow_0050.vtk" + assert data.u.shape == (ny, nx) + + def test_real_file_with_scalars(self, tmp_path): + nx, ny = 10, 8 + u = np.ones((ny, nx)) + v = np.zeros((ny, nx)) + p = np.arange(nx * ny, dtype=float).reshape(ny, nx) + vtk_file = tmp_path / "animated_flow_0050.vtk" + _write_structured_points( + vtk_file, + nx=nx, + ny=ny, + vectors=(u, v), + scalars={"pressure": p}, + ) data = read_vtk_file(str(vtk_file)) assert data is not None # "pressure" should be normalized to "p" assert "p" in data assert data["p"].shape == (data.ny, data.nx) - @_skip_no_samples - def test_origin_and_spacing(self): - vtk_file = SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk" + def test_origin_and_spacing(self, tmp_path): + nx, ny = 50, 50 + dx_expected = 0.020408 + u = np.ones((ny, nx)) + v = np.zeros((ny, nx)) + vtk_file = tmp_path / "flow_field_50x50_Re100.vtk" + _write_structured_points( + vtk_file, + nx=nx, + ny=ny, + vectors=(u, v), + origin=(0.0, 0.0, 0.0), + spacing=(dx_expected, dx_expected, 1.0), + ) data = read_vtk_file(str(vtk_file)) - assert data.dx == pytest.approx(0.020408, rel=1e-3) + assert data.dx == pytest.approx(dx_expected, rel=1e-3) assert data.x[0] == pytest.approx(0.0) def test_synthetic_minimal(self, tmp_path): @@ -368,14 +383,17 @@ def test_truncated_vector_data(self, tmp_path): class TestReadVTKVelocity: - @_skip_no_samples - def test_returns_tuple(self): - vtk_file = SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk" + def test_returns_tuple(self, tmp_path): + nx, ny = 50, 50 + u_in = np.ones((ny, nx)) + v_in = np.zeros((ny, nx)) + vtk_file = tmp_path / "flow_field.vtk" + _write_structured_points(vtk_file, nx=nx, ny=ny, vectors=(u_in, v_in)) result = read_vtk_velocity(str(vtk_file)) X, Y, u, v = result assert X is not None - assert X.shape == (50, 50) - assert u.shape == (50, 50) + assert X.shape == (ny, nx) + assert u.shape == (ny, nx) def test_file_not_found_returns_nones(self): result = read_vtk_velocity("/nonexistent/file.vtk") From 1a21427517bcde36676139aea3d0ec07993ced2a Mon Sep 17 00:00:00 2001 From: shaia Date: Sat, 7 Mar 2026 08:07:45 +0200 Subject: [PATCH 08/12] test: Replace sample-file-dependent integration tests with synthetic VTK fixtures Integration tests now generate their own VTK data in tmp_path instead of depending on gitignored sample files, ensuring they work in CI. --- tests/test_integration.py | 77 +++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index d7a6913..8ab10ac 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,7 +1,5 @@ """End-to-end integration tests for cfd-visualization pipeline.""" -from pathlib import Path - import matplotlib matplotlib.use("Agg") @@ -14,19 +12,47 @@ from cfd_viz.fields import magnitude, vorticity from cfd_viz.plotting import plot_contour_field -SAMPLE_VTK_DIR = Path(__file__).parent.parent / "data" / "vtk_files" -_sample_file_missing = not (SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk").exists() -_skip_no_samples = pytest.mark.skipif( - _sample_file_missing, reason="Sample VTK files not available" -) + +def _write_structured_points( + path, nx, ny, vectors=None, scalars=None, origin=(0, 0, 0), spacing=(1, 1, 1) +): + """Write a minimal STRUCTURED_POINTS VTK file.""" + lines = [ + "# vtk DataFile Version 3.0", + "Test", + "ASCII", + "DATASET STRUCTURED_POINTS", + f"DIMENSIONS {nx} {ny} 1", + f"ORIGIN {origin[0]} {origin[1]} {origin[2]}", + f"SPACING {spacing[0]} {spacing[1]} {spacing[2]}", + "", + f"POINT_DATA {nx * ny}", + ] + if vectors is not None: + lines.append("VECTORS velocity float") + for u, v in zip(vectors[0].ravel(), vectors[1].ravel()): + lines.append(f"{u} {v} 0.0") + if scalars is not None: + for name, data in scalars.items(): + lines.append(f"SCALARS {name} float 1") + lines.append("LOOKUP_TABLE default") + for val in data.ravel(): + lines.append(str(val)) + path.write_text("\n".join(lines) + "\n") @pytest.mark.integration class TestEndToEnd: - @_skip_no_samples - def test_vtk_read_compute_plot(self): + def test_vtk_read_compute_plot(self, tmp_path): """Read VTK file -> compute derived fields -> plot -> no exceptions.""" - data = read_vtk_file(str(SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk")) + nx, ny = 50, 50 + rng = np.random.default_rng(0) + u = rng.standard_normal((ny, nx)) + v = rng.standard_normal((ny, nx)) + vtk_file = tmp_path / "flow_field.vtk" + _write_structured_points(vtk_file, nx=nx, ny=ny, vectors=(u, v)) + + data = read_vtk_file(str(vtk_file)) assert data is not None speed = magnitude(data.u, data.v) @@ -55,18 +81,26 @@ def test_cfd_python_convert_plot(self): plot_contour_field(data.X, data.Y, speed, ax=ax) plt.close(fig) - @_skip_no_samples - def test_field_alias_in_pipeline(self): + def test_field_alias_in_pipeline(self, tmp_path): """Verify aliases work through the full pipeline.""" - data = read_vtk_file(str(SAMPLE_VTK_DIR / "animated_flow_0050.vtk")) + nx, ny = 10, 8 + u = np.ones((ny, nx)) + v = np.zeros((ny, nx)) + p = np.arange(nx * ny, dtype=float).reshape(ny, nx) + vtk_file = tmp_path / "animated_flow.vtk" + _write_structured_points( + vtk_file, nx=nx, ny=ny, vectors=(u, v), scalars={"pressure": p} + ) + + data = read_vtk_file(str(vtk_file)) assert data is not None # "pressure" was normalized to "p" at read time - p = data.get("pressure") - assert p is not None + p_field = data.get("pressure") + assert p_field is not None fig, ax = plt.subplots() - plot_contour_field(data.X, data.Y, p, ax=ax) + plot_contour_field(data.X, data.Y, p_field, ax=ax) plt.close(fig) def test_version_accessible(self): @@ -76,10 +110,15 @@ def test_version_accessible(self): assert isinstance(__version__, str) assert len(__version__) > 0 - @_skip_no_samples - def test_vtk_data_repr(self): + def test_vtk_data_repr(self, tmp_path): """VTKData repr should be informative.""" - data = read_vtk_file(str(SAMPLE_VTK_DIR / "flow_field_50x50_Re100.vtk")) + nx, ny = 50, 50 + u = np.ones((ny, nx)) + v = np.zeros((ny, nx)) + vtk_file = tmp_path / "flow_field.vtk" + _write_structured_points(vtk_file, nx=nx, ny=ny, vectors=(u, v)) + + data = read_vtk_file(str(vtk_file)) r = repr(data) assert "VTKData" in r assert "50" in r From 3ce6ebcba863ab415fc47dd2a4b381888702618c Mon Sep 17 00:00:00 2001 From: shaia Date: Sat, 7 Mar 2026 08:44:44 +0200 Subject: [PATCH 09/12] test: Rename test to match assertion (checks keys, not values) Renamed test_canonical_names_not_in_aliases_as_values to test_canonical_names_not_aliased_to_themselves since it asserts canonical names are not present as keys in FIELD_ALIASES. --- tests/common/test_vtk_reader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/common/test_vtk_reader.py b/tests/common/test_vtk_reader.py index 6e376ef..d3a7d87 100644 --- a/tests/common/test_vtk_reader.py +++ b/tests/common/test_vtk_reader.py @@ -413,8 +413,8 @@ def test_velocity_aliases(self): assert FIELD_ALIASES["velocity_x"] == "u" assert FIELD_ALIASES["velocity_y"] == "v" - def test_canonical_names_not_in_aliases_as_values(self): - # Canonical names should not map to themselves + def test_canonical_names_not_aliased_to_themselves(self): + # Canonical names should not appear as keys in FIELD_ALIASES assert "u" not in FIELD_ALIASES assert "v" not in FIELD_ALIASES assert "p" not in FIELD_ALIASES From 60afe40a8a535d381b571298465022021bc540b4 Mon Sep 17 00:00:00 2001 From: shaia Date: Sat, 7 Mar 2026 08:45:56 +0200 Subject: [PATCH 10/12] fix: Guard NaN/inf validation against non-floating dtype fields np.isnan/np.isinf raise TypeError on integer arrays. Skip the check for non-floating dtypes since they cannot contain NaN or inf. --- cfd_viz/common/vtk_reader.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cfd_viz/common/vtk_reader.py b/cfd_viz/common/vtk_reader.py index db03a3d..cb04a1b 100644 --- a/cfd_viz/common/vtk_reader.py +++ b/cfd_viz/common/vtk_reader.py @@ -87,8 +87,10 @@ def __init__( f"expected {expected_shape} (ny, nx)" ) - # Warn on NaN/inf values + # Warn on NaN/inf values (only for floating-point fields) for name, field in self.fields.items(): + if not np.issubdtype(field.dtype, np.floating): + continue if np.any(np.isnan(field)): warnings.warn( f"Field '{name}' contains NaN values", From 5de948db4cdc0fa1f3abb22b42fa4ff351c26c29 Mon Sep 17 00:00:00 2001 From: shaia Date: Sat, 7 Mar 2026 12:30:09 +0200 Subject: [PATCH 11/12] refactor: Remove CANONICAL_NAMES, reuse FIELD_ALIASES for read-time normalization Eliminates duplicate pressure->p mapping by using FIELD_ALIASES as the single source of truth for both read-time normalization and lookup-time aliasing. --- cfd_viz/common/vtk_reader.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/cfd_viz/common/vtk_reader.py b/cfd_viz/common/vtk_reader.py index cb04a1b..f17afe1 100644 --- a/cfd_viz/common/vtk_reader.py +++ b/cfd_viz/common/vtk_reader.py @@ -38,12 +38,8 @@ } ) -# Maps VTK file field names to canonical short names used in VTKData.fields. -CANONICAL_NAMES: Dict[str, str] = { - "pressure": "p", -} - # Maps alternative user-facing names to canonical names for lookup. +# Also used at read time to normalize VTK field names to canonical short names. FIELD_ALIASES: Dict[str, str] = { "pressure": "p", "velocity_x": "u", @@ -275,7 +271,7 @@ def read_vtk_file(filename: str) -> Optional[VTKData]: i += 1 if len(values) == nx * ny: - canonical = CANONICAL_NAMES.get(field_name, field_name) + canonical = FIELD_ALIASES.get(field_name, field_name) fields[canonical] = np.array(values).reshape((ny, nx)) continue From 1ce4d22cb0352d7ffec98357c094b0e5e4f9a7bf Mon Sep 17 00:00:00 2001 From: shaia Date: Sat, 7 Mar 2026 12:31:20 +0200 Subject: [PATCH 12/12] refactor: Make NaN/inf validation optional via validate parameter Add validate=True parameter to VTKData constructor so callers can skip the O(N) per-field nan/inf scans for large grids when not needed. --- cfd_viz/common/vtk_reader.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/cfd_viz/common/vtk_reader.py b/cfd_viz/common/vtk_reader.py index f17afe1..27302cc 100644 --- a/cfd_viz/common/vtk_reader.py +++ b/cfd_viz/common/vtk_reader.py @@ -63,6 +63,7 @@ def __init__( ny: int, dx: float, dy: float, + validate: bool = True, ): self.x = x self.y = y @@ -84,21 +85,22 @@ def __init__( ) # Warn on NaN/inf values (only for floating-point fields) - for name, field in self.fields.items(): - if not np.issubdtype(field.dtype, np.floating): - continue - if np.any(np.isnan(field)): - warnings.warn( - f"Field '{name}' contains NaN values", - UserWarning, - stacklevel=2, - ) - if np.any(np.isinf(field)): - warnings.warn( - f"Field '{name}' contains infinite values", - UserWarning, - stacklevel=2, - ) + if validate: + for name, field in self.fields.items(): + if not np.issubdtype(field.dtype, np.floating): + continue + if np.any(np.isnan(field)): + warnings.warn( + f"Field '{name}' contains NaN values", + UserWarning, + stacklevel=2, + ) + if np.any(np.isinf(field)): + warnings.warn( + f"Field '{name}' contains infinite values", + UserWarning, + stacklevel=2, + ) def __repr__(self) -> str: field_names = ", ".join(sorted(self.fields.keys()))