Skip to content
Merged
28 changes: 10 additions & 18 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This document outlines planned enhancements for cfd-visualization, organized by priority and release target.

**Last Updated:** 2026-03-05
**Last Updated:** 2026-03-07
**Current Version:** 0.1.0

---
Expand Down Expand Up @@ -75,26 +75,18 @@ The VTK reader had zero dedicated tests and VTKData accepted any data without va

---

## Phase 4: CLI Tooling & Entry Points
## Phase 4: CLI Tooling & Entry Points - COMPLETE

**Priority:** P1 - Registers existing orphaned code
**Target:** v0.3.0

Five scripts exist in `scripts/` but are not registered as package entry points. Users must run `python scripts/create_animation.py` instead of a proper CLI command.

### Tasks

- [ ] **4.1 Register entry points** in `pyproject.toml` - `[project.scripts]` section
- [ ] **4.2 Create unified `cfd-viz` CLI** - single entry point with argparse subcommands dispatching to existing scripts
- [ ] **4.3 Add batch processing** - `cfd-viz batch --config batch.toml` for processing multiple VTK files in one run
- [ ] **4.4 Add `cfd-viz info`** - wraps existing `print_system_info()`, shows backends and optional deps
- [ ] **4.5 Add progress indicators** - simple stderr output for batch operations and animation rendering (no extra dependency)
**Status:** Completed (2026-03-07)

### Success Criteria
Five scripts existed in `scripts/` but were not registered as package entry points. This phase added a unified `cfd-viz` CLI with argparse subcommands dispatching to existing scripts, batch processing, and progress indicators.

- `pip install -e .` makes `cfd-viz` command available
- `cfd-viz info` prints system capabilities
- `cfd-viz batch` processes multiple files from config
- [x] **4.1 Register entry points** in `pyproject.toml` - `[project.scripts]` section, `cfd-viz = "cfd_viz.cli:main"`
- [x] **4.2 Create unified `cfd-viz` CLI** (`cfd_viz/cli.py`) - single entry point with subcommands: `animate`, `dashboard`, `vorticity`, `profiles`, `monitor`, `info`, `batch`
- [x] **4.3 Add batch processing** (`cfd_viz/_batch.py`) - `cfd-viz batch --config batch.toml` for processing multiple VTK files with TOML config
- [x] **4.4 Add `cfd-viz info`** - wraps existing `print_system_info()`, shows backends and optional deps
- [x] **4.5 Add progress indicators** - simple stderr progress bar for batch operations (no extra dependency)
- [x] **4.6 Add CLI tests** (`tests/test_cli.py`) - 23 tests covering parser structure, dispatch, batch processing, progress, and entry point registration

---

Expand Down
177 changes: 177 additions & 0 deletions cfd_viz/_batch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""Batch processing for multiple VTK files from a TOML config.

Config format (``batch.toml``)::

[batch]
output_dir = "output/batch"

[[batch.jobs]]
vtk = "data/vtk_files/flow_0100.vtk"
analyses = ["vorticity", "profiles"]

[[batch.jobs]]
vtk = "data/vtk_files/flow_0200.vtk"
analyses = ["vorticity"]
output_dir = "output/batch/step200" # per-job override

[[batch.jobs]]
vtk_pattern = "data/vtk_files/flow_*.vtk"
analyses = ["animate"]
animate_type = "velocity"
fps = 10

Supported analyses: ``vorticity``, ``profiles``, ``animate``.
"""

import os
import sys


def _load_toml(path):
"""Load a TOML file, using tomllib (3.11+) or tomli."""
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib

with open(path, "rb") as f:
return tomllib.load(f)


def _progress(current, total, label=""):
"""Write a simple progress line to stderr."""
width = 30
filled = int(width * current / total) if total > 0 else width
bar = "#" * filled + "-" * (width - filled)
pct = (current / total * 100) if total > 0 else 100
sys.stderr.write(f"\r [{bar}] {pct:5.1f}% {label}")
if current >= total:
sys.stderr.write("\n")
sys.stderr.flush()


def _run_vorticity(vtk_file, output_dir):
from cfd_viz._cli_impl import create_vorticity_visualization
from cfd_viz.common import ensure_dirs, read_vtk_file

ensure_dirs()
try:
data = read_vtk_file(vtk_file)
except ValueError:
print(f" Warning: invalid VTK file {vtk_file}")
return
if data is None:
print(f" Warning: could not read {vtk_file}")
return

if data.u is None or data.v is None:
print(f" Warning: no velocity data in {vtk_file}")
return

create_vorticity_visualization(data.to_dict(), output_dir)


def _run_profiles(vtk_file, output_dir):
from cfd_viz._cli_impl import (
_vtk_to_profiles_dict,
create_cross_section_analysis,
)
from cfd_viz.common import ensure_dirs, read_vtk_file

ensure_dirs()
try:
data = read_vtk_file(vtk_file)
except ValueError:
print(f" Warning: invalid VTK file {vtk_file}")
return
if data is None:
print(f" Warning: could not read {vtk_file}")
return

data_dict = _vtk_to_profiles_dict(data)
if data_dict is None:
print(f" Warning: no velocity data in {vtk_file}")
return

create_cross_section_analysis(data_dict, output_dir)


def _run_animate(vtk_files, output_dir, animate_type="velocity", fps=5):
from cfd_viz._cli_impl import create_and_save_animation, load_vtk_files_to_frames

frames = load_vtk_files_to_frames(vtk_files)
output_path = os.path.join(output_dir, f"cfd_{animate_type}.gif")
create_and_save_animation(frames, animate_type, output_path, fps=fps)


def run_batch(config_path):
"""Execute a batch config file."""
import glob as globmod

try:
cfg = _load_toml(config_path)
except FileNotFoundError:
print(f"Error: config file not found: {config_path}", file=sys.stderr)
raise SystemExit(2) from None
except Exception as exc:
print(f"Error: failed to parse config: {exc}", file=sys.stderr)
raise SystemExit(2) from None

batch = cfg.get("batch", {})
global_output = batch.get("output_dir", "output/batch")
jobs = batch.get("jobs", [])

if not jobs:
print("No jobs defined in config.")
return

total = len(jobs)
print(f"Batch: {total} job(s) from {config_path}")

for idx, job in enumerate(jobs, 1):
analyses = job.get("analyses", [])
job_output = job.get("output_dir", global_output)
os.makedirs(job_output, exist_ok=True)

# Resolve VTK files
vtk_files = []
if "vtk" in job:
vtk_files = [job["vtk"]]
elif "vtk_pattern" in job:
vtk_files = sorted(globmod.glob(job["vtk_pattern"]))

if not vtk_files:
print(f" Job {idx}/{total}: no VTK files found, skipping")
_progress(idx, total, "skipped")
continue

label = (
os.path.basename(vtk_files[0])
if len(vtk_files) == 1
else f"{len(vtk_files)} files"
)
print(f" Job {idx}/{total}: {label} analyses={analyses}")

for analysis in analyses:
if analysis == "vorticity":
for vtk_file in vtk_files:
_run_vorticity(vtk_file, job_output)
elif analysis == "profiles":
for vtk_file in vtk_files:
_run_profiles(vtk_file, job_output)
elif analysis == "animate":
animate_type = job.get("animate_type", "velocity")
fps = job.get("fps", 5)
try:
_run_animate(vtk_files, job_output, animate_type, fps)
except Exception as exc:
print(
f" Warning: animate analysis failed for job"
f" {idx}/{total}: {exc}"
)
else:
print(f" Warning: unknown analysis '{analysis}', skipping")

_progress(idx, total, label)

print("Batch processing complete.")
Loading
Loading