Skip to content
Merged
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
27 changes: 9 additions & 18 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

---

Expand Down
7 changes: 6 additions & 1 deletion cfd_viz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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+unknown" # Fallback for uninstalled development usage

# Re-export commonly used items for convenience
# Import fields module for easy access
Expand Down
3 changes: 2 additions & 1 deletion cfd_viz/common/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
95 changes: 89 additions & 6 deletions cfd_viz/common/vtk_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,44 @@
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

# 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 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",
"velocity_y": "v",
"x_velocity": "u",
"y_velocity": "v",
}


class VTKData:
"""Container for VTK file data with convenient access patterns."""
Expand All @@ -30,6 +63,7 @@ def __init__(
ny: int,
dx: float,
dy: float,
validate: bool = True,
):
self.x = x
self.y = y
Expand All @@ -41,6 +75,41 @@ 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 (only for floating-point fields)
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()))
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."""
Expand All @@ -52,12 +121,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."""
Expand Down Expand Up @@ -182,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()])
Expand All @@ -191,7 +273,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 = FIELD_ALIASES.get(field_name, field_name)
fields[canonical] = np.array(values).reshape((ny, nx))
continue

i += 1
Expand Down
14 changes: 14 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Empty file added tests/common/__init__.py
Empty file.
Loading