diff --git a/tests/test_config_workspace.py b/tests/test_config_workspace.py new file mode 100755 index 0000000..721f100 --- /dev/null +++ b/tests/test_config_workspace.py @@ -0,0 +1,178 @@ +import autoflow.api as api +from autoflow import AutoFlowConfig, build_workspace + + +def test_autoflow_config_defaults_cover_workspace_and_rendering_contract(): + cfg = AutoFlowConfig() + + assert cfg.inputs == [] + assert cfg.output_dir == "./results" + + assert cfg.skip_derived is False + assert cfg.skip_plane_metrics is False + assert cfg.use_multithread is True + assert cfg.reuse_planes == "" + + assert cfg.use_center_plane is True + assert cfg.cross_section_dist == 5.0 + assert cfg.start_dist == 5.0 + assert cfg.end_dist == 0.0 + + assert cfg.remove_small_cc is True + assert cfg.min_cc_volume == 50.0 + + assert cfg.seed_ratio == 0.02 + assert cfg.tube_radius == 0.05 + + assert cfg.fps == 12 + assert cfg.plane_rotation_frames == 180 + assert cfg.make_plane_video is False + assert cfg.make_wss_video is False + assert cfg.make_streamlines_video is False + assert cfg.make_tke_video is False + + assert cfg.camera_view == "right" + assert cfg.camera_distance_scale == 1.5 + assert cfg.rotate_dynamic_video is True + assert cfg.dynamic_rotation_frames == 180 + assert cfg.dynamic_rotation_elevation_deg == 10.0 + assert cfg.dynamic_time_repeat == 3 + + assert cfg.add_plane_idx is False + assert cfg.add_path_idx is False + + assert cfg.wss_clim == (0.0, 10.0) + assert cfg.wss_bar_cfg == api.DEFAULT_WSS_BAR_CFG + assert cfg.wss_bar_cfg is not api.DEFAULT_WSS_BAR_CFG + + assert cfg.tke_clim == (0.0, 100.0) + assert cfg.tke_bar_cfg == api.DEFAULT_TKE_BAR_CFG + assert cfg.tke_bar_cfg is not api.DEFAULT_TKE_BAR_CFG + + assert cfg.streamline_clim == (0.0, 1) + assert cfg.streamline_bar_cfg == api.DEFAULT_STREAMLINE_BAR_CFG + assert cfg.streamline_bar_cfg is not api.DEFAULT_STREAMLINE_BAR_CFG + + +def test_build_workspace_maps_default_config_values(): + ws = build_workspace() + + assert ws.plane_gen_params.to_dict() == { + "use_center_plane": True, + "cross_section_distance": 5.0, + "start_distance": 5.0, + "end_distance": 0.0, + "smoothing_window": 15, + "smoothing_polyorder": 2, + "inter_time": 10, + } + assert ws.skeleton_params.to_dict() == { + "remove_small_cc": True, + "min_cc_volume_mm3": 50.0, + "do_closing": True, + "do_opening": False, + "gaussian_sigma": 0.5, + "gaussian_enabled": True, + } + assert ws.streamline_params.to_dict() == { + "seed_ratio": 0.02, + "max_steps": 2000, + "min_seeds": 50, + "terminal_speed": 0.01, + "rng_seed": 0, + } + assert getattr(ws.streamline_params, "tube_radius") == 0.05 + + +def test_build_workspace_maps_custom_plane_generation_parameters(): + config = AutoFlowConfig( + use_center_plane=False, + cross_section_dist=12.5, + start_dist=1.5, + end_dist=2.5, + ) + + ws = build_workspace(config) + + assert ws.plane_gen_params.to_dict() == { + "use_center_plane": False, + "cross_section_distance": 12.5, + "start_distance": 1.5, + "end_distance": 2.5, + "smoothing_window": 15, + "smoothing_polyorder": 2, + "inter_time": 10, + } + + +def test_build_workspace_maps_skeleton_and_streamline_parameters(): + config = AutoFlowConfig( + remove_small_cc=False, + min_cc_volume=42.0, + seed_ratio=0.03, + tube_radius=0.12, + ) + + ws = build_workspace(config) + + assert ws.skeleton_params.to_dict() == { + "remove_small_cc": False, + "min_cc_volume_mm3": 42.0, + "do_closing": True, + "do_opening": False, + "gaussian_sigma": 0.5, + "gaussian_enabled": True, + } + assert ws.streamline_params.to_dict() == { + "seed_ratio": 0.03, + "max_steps": 2000, + "min_seeds": 50, + "terminal_speed": 0.01, + "rng_seed": 0, + } + assert getattr(ws.streamline_params, "tube_radius") == 0.12 + + +def test_run_case_forwards_rendering_defaults_to_process_single(monkeypatch, tmp_path): + captured = {} + + def fake_process_single(input_path, case_dir, **kwargs): + captured["input_path"] = input_path + captured["case_dir"] = case_dir + captured.update(kwargs) + return {"status": "ok", "case_dir": case_dir} + + monkeypatch.setattr(api, "process_single", fake_process_single) + + cfg = AutoFlowConfig() + out_dir = tmp_path / "case" + summary = api.run_case("demo_input.h5", output_dir=str(out_dir), config=cfg) + + assert summary == {"status": "ok", "case_dir": str(out_dir)} + assert captured["input_path"] == "demo_input.h5" + assert captured["case_dir"] == str(out_dir) + + assert captured["workspace"].plane_gen_params.cross_section_distance == 5.0 + assert captured["fps"] == 12 + assert captured["plane_rotation_frames"] == 180 + assert captured["make_plane_video"] is False + assert captured["make_wss_video"] is False + assert captured["make_streamlines_video"] is False + assert captured["make_tke_video"] is False + assert captured["camera_view"] == "right" + assert captured["camera_distance_scale"] == 1.5 + assert captured["rotate_dynamic_video"] is True + assert captured["dynamic_rotation_frames"] == 180 + assert captured["dynamic_rotation_elevation_deg"] == 10.0 + assert captured["dynamic_time_repeat"] == 3 + assert captured["add_plane_idx"] is False + assert captured["add_path_idx"] is False + assert captured["wss_clim"] == (0.0, 10.0) + assert captured["tke_clim"] == (0.0, 100.0) + assert captured["streamline_clim"] == (0.0, 1) + assert captured["wss_bar_cfg"] == api.DEFAULT_WSS_BAR_CFG + assert captured["tke_bar_cfg"] == api.DEFAULT_TKE_BAR_CFG + assert captured["streamline_bar_cfg"] == api.DEFAULT_STREAMLINE_BAR_CFG + assert captured["wss_bar_cfg"] is not cfg.wss_bar_cfg + assert captured["tke_bar_cfg"] is not cfg.tke_bar_cfg + assert captured["streamline_bar_cfg"] is not cfg.streamline_bar_cfg diff --git a/tests/test_demo_workflows.py b/tests/test_demo_workflows.py new file mode 100755 index 0000000..c60f557 --- /dev/null +++ b/tests/test_demo_workflows.py @@ -0,0 +1,130 @@ +import json +import os +from pathlib import Path +import subprocess +import sys + +import numpy as np + +from autoflow import AutoFlowConfig, run_batch + + +REPO_ROOT = Path(__file__).resolve().parents[1] +DEMO_INPUT = REPO_ROOT / "data" / "demo_data.h5" +EXPECTED_PIXELWISE_KEYS = {"origin", "spacing", "tke", "tke_time", "wss"} +NOTEBOOK_VIDEO_KEYS = {"streamlines", "tke", "wss"} + + +def _run_cli(*args: str) -> subprocess.CompletedProcess[str]: + env = os.environ.copy() + existing = env.get("PYTHONPATH", "") + env["PYTHONPATH"] = str(REPO_ROOT) if not existing else os.pathsep.join((str(REPO_ROOT), existing)) + return subprocess.run( + [sys.executable, "-m", "autoflow.cli", *args], + cwd=REPO_ROOT, + env=env, + capture_output=True, + text=True, + check=False, + ) + + +def _assert_demo_outputs(output_root: Path, expected_video_keys: set[str]) -> dict: + case_dir = output_root / "demo_data" + summary_path = case_dir / "summary.json" + plane_positions_path = case_dir / "plane_positions.json" + planes_path = case_dir / "planes.json" + plane_metrics_path = case_dir / "plane_metrics.json" + plane_qc_path = case_dir / "plane_qc.json" + pixelwise_path = case_dir / "derived_metrics_pixelwise.npz" + batch_report_path = output_root / "batch_report.json" + time_summary_path = output_root / "time_summary.txt" + + for path in ( + summary_path, + plane_positions_path, + planes_path, + plane_metrics_path, + plane_qc_path, + pixelwise_path, + batch_report_path, + time_summary_path, + ): + assert path.is_file(), path + + summary = json.loads(summary_path.read_text(encoding="utf-8")) + assert summary["input"].endswith("data/demo_data.h5") + assert summary["output_dir"] == str(case_dir) + assert summary["n_planes"] >= 1 + assert summary["n_paths"] >= 1 + assert summary["n_forks"] >= 1 + assert len(summary["plane_metrics"]) == summary["n_planes"] + assert set(summary["pixelwise_export"]) == EXPECTED_PIXELWISE_KEYS + assert Path(summary["plane_positions_file"]).is_file() + assert summary["reused_planes_file"] == "" + + batch_report = json.loads(batch_report_path.read_text(encoding="utf-8")) + assert len(batch_report) == 1 + assert batch_report[0]["status"] == "ok" + assert batch_report[0]["file"].endswith("data/demo_data.h5") + + assert set(summary["videos"]) == expected_video_keys + for key in expected_video_keys: + video_path = Path(summary["videos"][key]) + assert video_path.is_file(), video_path + assert video_path.suffix in {".gif", ".mp4"} + + pixelwise = np.load(pixelwise_path) + assert set(pixelwise.files) == EXPECTED_PIXELWISE_KEYS + + return summary + + +def test_cli_demo_command_processes_demo_data(tmp_path): + output_root = tmp_path / "cli_demo" + + result = _run_cli(str(DEMO_INPUT), "--output-dir", str(output_root)) + + assert result.returncode == 0, result.stderr or result.stdout + assert "Found 1 file(s) to process." in result.stdout + assert "Done: 1/1 succeeded." in result.stdout + + summary = _assert_demo_outputs(output_root, expected_video_keys=set()) + assert summary["videos"] == {} + + +def test_library_demo_notebook_workflow_processes_demo_data(tmp_path): + output_root = tmp_path / "library_demo" + config = AutoFlowConfig( + inputs=[str(DEMO_INPUT)], + output_dir=str(output_root), + skip_derived=False, + use_multithread=True, + reuse_planes="", + use_center_plane=True, + cross_section_dist=15.0, + start_dist=5.0, + end_dist=0.0, + remove_small_cc=True, + min_cc_volume=50.0, + make_plane_video=False, + make_wss_video=True, + make_streamlines_video=True, + make_tke_video=True, + camera_view="right", + camera_distance_scale=1.5, + rotate_dynamic_video=True, + dynamic_rotation_frames=180, + dynamic_rotation_elevation_deg=10.0, + dynamic_time_repeat=3, + add_path_idx=False, + ) + + results, case_out = run_batch(config) + + assert len(results) == 1 + assert results[0]["status"] == "ok" + assert case_out == str(output_root / "demo_data") + + summary = _assert_demo_outputs(output_root, expected_video_keys=NOTEBOOK_VIDEO_KEYS) + assert set(summary["videos"]) == NOTEBOOK_VIDEO_KEYS