From b672d40bd7b4451aecab9d8cab0bec4153b63404 Mon Sep 17 00:00:00 2001 From: Eugene Nikolskiy <4658906+helionaut@users.noreply.github.com> Date: Sat, 21 Mar 2026 13:31:52 +0300 Subject: [PATCH 1/2] fix(monocular): bootstrap fresh-worktree private follow-up Summary: - teach the private monocular follow-up wrapper to bootstrap missing repo-local media and native prerequisites in a fresh worktree - add a prerequisite verification phase before the aggressive HEL-57 replay starts - cover the new phase planner with focused tests Rationale: - the first HEL-79 rerun failed before the save-path diagnostics because the wrapper assumed ffmpeg and the native ORB-SLAM3 dependency lane were already present from an older workspace - codifying those prerequisites in the wrapper leaves a reproducible repo-owned lane instead of depending on hidden host state Tests: - PYTHONPATH=src python3 -m unittest tests.test_run_private_monocular_followup tests.test_run_private_save_comparison_followup tests.test_run_monocular_baseline tests.test_patch_orbslam3_baseline - python3 -m py_compile scripts/run_private_monocular_followup.py scripts/run_private_save_comparison_followup.py scripts/run_monocular_baseline.py scripts/patch_orbslam3_baseline.py - make check Co-authored-by: Codex --- scripts/run_private_monocular_followup.py | 318 ++++++++++++------- tests/test_run_private_monocular_followup.py | 157 +++++++++ 2 files changed, 368 insertions(+), 107 deletions(-) diff --git a/scripts/run_private_monocular_followup.py b/scripts/run_private_monocular_followup.py index 43cbbae..0b1c6a4 100755 --- a/scripts/run_private_monocular_followup.py +++ b/scripts/run_private_monocular_followup.py @@ -28,6 +28,10 @@ RAW_EXTRINSICS_FILENAME, resolve_lens_input_layout, ) +from splatica_orb_test.local_tooling import ( # noqa: E402 + resolve_ffmpeg_tool, + resolve_ffprobe_tool, +) from splatica_orb_test.monocular_prereqs import ( # noqa: E402 MonocularBaselinePrerequisites, PrerequisiteCheck, @@ -49,7 +53,14 @@ REPO_ROOT / "reports/out/hel-73_private_monocular_followup.md" ) DEFAULT_OUTPUT_TAG = "orb_aggressive_asan_no_static_alignment" -TOTAL_PHASES = 5 +EXECUTION_BOOTSTRAP_TARGETS = { + "Tool `cmake`": "bootstrap-local-cmake", + "Eigen3 development package": "bootstrap-local-eigen", + "OpenCV development package": "bootstrap-local-opencv", + "Boost serialization development package": "bootstrap-local-boost", + "Pangolin development package": "bootstrap-local-pangolin", +} +ExecutionPhase = tuple[str, list[str], dict[str, str] | None, Path] @dataclass(frozen=True) @@ -80,17 +91,19 @@ def build_progress_payload( artifacts: dict[str, str], current_step: str, completed: int, + total_phases: int, status: str, metrics: dict[str, object] | None = None, experiment: dict[str, object] | None = None, ) -> dict[str, object]: - clamped_completed = max(0, min(completed, TOTAL_PHASES)) + resolved_total_phases = max(total_phases, 1) + clamped_completed = max(0, min(completed, resolved_total_phases)) payload = { "status": status, "current_step": current_step, - "progress_percent": round((clamped_completed / TOTAL_PHASES) * 100), + "progress_percent": round((clamped_completed / resolved_total_phases) * 100), "completed": clamped_completed, - "total": TOTAL_PHASES, + "total": resolved_total_phases, "unit": "phases", "metrics": metrics or {}, "artifacts": artifacts, @@ -214,6 +227,175 @@ def inspect_source_inputs(source_inputs: SourceInputPaths) -> tuple[Prerequisite ) +def resolve_import_bootstrap_targets( + *, + ready_for_prepare_only: bool, + media_tools_ready: bool, +) -> tuple[str, ...]: + if ready_for_prepare_only or media_tools_ready: + return () + return ("bootstrap-local-ffmpeg",) + + +def resolve_execution_bootstrap_targets( + prerequisites: MonocularBaselinePrerequisites, +) -> tuple[str, ...]: + targets: list[str] = [] + for check in prerequisites.execute_checks: + target = EXECUTION_BOOTSTRAP_TARGETS.get(check.label) + if target is not None and not check.ready and target not in targets: + targets.append(target) + return tuple(targets) + + +def build_followup_phases( + *, + repo_root: Path, + manifest_path: Path, + source_inputs: SourceInputPaths, + prerequisites: MonocularBaselinePrerequisites, + progress_artifact: Path, + issue_identifier: str, + output_tag: str, + frame_stride: int, + max_frames: int | None, + experiment: Mapping[str, object], + media_tools_ready: bool, +) -> list[ExecutionPhase]: + phases: list[ExecutionPhase] = [] + + for target in resolve_import_bootstrap_targets( + ready_for_prepare_only=prerequisites.ready_for_prepare_only, + media_tools_ready=media_tools_ready, + ): + phases.append( + ( + f"bootstrapping repo-local media tools via make {target}", + ["make", target], + None, + repo_root, + ) + ) + + if not prerequisites.ready_for_prepare_only: + phases.append( + ( + "importing private lens-10 bundle from raw source files", + [ + sys.executable, + str(repo_root / "scripts/import_monocular_video_inputs.py"), + "--video-00", + str(source_inputs.video_00), + "--video-10", + str(source_inputs.video_10), + "--calibration-00", + str(source_inputs.calibration_00), + "--calibration-10", + str(source_inputs.calibration_10), + "--extrinsics", + str(source_inputs.extrinsics), + "--lenses", + "10", + ], + None, + repo_root, + ) + ) + + phases.append( + ( + "fetching pinned ORB-SLAM3 baseline checkout", + [str(repo_root / "scripts/fetch_orbslam3_baseline.sh")], + None, + repo_root, + ) + ) + + for target in resolve_execution_bootstrap_targets(prerequisites): + phases.append( + ( + f"bootstrapping repo-local execution prerequisite via make {target}", + ["make", target], + None, + repo_root, + ) + ) + + phases.extend( + [ + ( + "building mono_tum_vi with ASan and disabled Eigen static alignment", + [str(repo_root / "scripts/build_orbslam3_baseline.sh")], + { + "ORB_SLAM3_BUILD_TARGET": "mono_tum_vi", + "ORB_SLAM3_BUILD_PARALLELISM": "1", + "ORB_SLAM3_BUILD_TYPE": "RelWithDebInfo", + "ORB_SLAM3_ENABLE_ASAN": "1", + "ORB_SLAM3_ASAN_COMPILE_FLAGS": " -fsanitize=address -fno-omit-frame-pointer -g -O0", + "ORB_SLAM3_DISABLE_EIGEN_STATIC_ALIGNMENT": "1", + "ORB_SLAM3_BUILD_EXPERIMENT": "hel-73-private-aggressive-followup", + "ORB_SLAM3_BUILD_CHANGED_VARIABLE": str( + experiment["changed_variable"] + ), + "ORB_SLAM3_BUILD_HYPOTHESIS": str(experiment["hypothesis"]), + "ORB_SLAM3_BUILD_SUCCESS_CRITERION": ( + "mono_tum_vi rebuilds with the HEL-72 sanitizer and " + "alignment toggles before the aggressive private rerun" + ), + "ORB_SLAM3_PROGRESS_ARTIFACT": str(progress_artifact), + "ORB_SLAM3_PROGRESS_ISSUE_ID": issue_identifier, + }, + repo_root, + ), + ( + "verifying monocular prerequisites after bootstrap/build", + ["make", "monocular-prereqs"], + None, + repo_root, + ), + ( + "running the HEL-57 aggressive private monocular follow-up", + [ + sys.executable, + str(repo_root / "scripts/run_monocular_baseline.py"), + "--manifest", + str(manifest_path), + "--output-tag", + output_tag, + "--frame-stride", + str(frame_stride), + "--progress-artifact", + str(progress_artifact), + "--progress-issue", + issue_identifier, + "--changed-variable", + str(experiment["changed_variable"]), + "--hypothesis", + str(experiment["hypothesis"]), + "--success-criterion", + str(experiment["success_criterion"]), + "--abort-condition", + str(experiment["abort_condition"]), + "--expected-artifact", + str(experiment["expected_artifact"]), + "--orb-n-features", + "4000", + "--orb-ini-fast", + "8", + "--orb-min-fast", + "3", + ] + + ( + ["--max-frames", str(max_frames)] if max_frames is not None else [] + ), + None, + repo_root, + ), + ] + ) + return phases + + def render_status_report( *, command: str, @@ -447,6 +629,24 @@ def main() -> int: f"Frame stride: {args.frame_stride}", f"Max frames: {args.max_frames if args.max_frames is not None else 'full sequence'}", ] + media_tools_ready = ( + resolve_ffmpeg_tool(REPO_ROOT) is not None + and resolve_ffprobe_tool(REPO_ROOT) is not None + ) + phases = build_followup_phases( + repo_root=REPO_ROOT, + manifest_path=manifest_path, + source_inputs=source_inputs, + prerequisites=prerequisites, + progress_artifact=progress_artifact, + issue_identifier=issue_identifier, + output_tag=args.output_tag, + frame_stride=args.frame_stride, + max_frames=args.max_frames, + experiment=experiment, + media_tools_ready=media_tools_ready, + ) + total_phases = len(phases) orchestration_log.parent.mkdir(parents=True, exist_ok=True) status_report.parent.mkdir(parents=True, exist_ok=True) with orchestration_log.open("w", encoding="utf-8") as log_handle: @@ -481,6 +681,7 @@ def main() -> int: artifacts=artifacts, current_step=blocked_reason, completed=0, + total_phases=1, status="blocked", metrics={"missing_source_inputs": missing_sources}, experiment=experiment, @@ -505,107 +706,6 @@ def main() -> int: ) return 1 - phases: list[tuple[str, list[str], dict[str, str] | None, Path]] = [] - if not prerequisites.ready_for_prepare_only: - phases.append( - ( - "importing private lens-10 bundle from raw source files", - [ - sys.executable, - str(REPO_ROOT / "scripts/import_monocular_video_inputs.py"), - "--video-00", - str(source_inputs.video_00), - "--video-10", - str(source_inputs.video_10), - "--calibration-00", - str(source_inputs.calibration_00), - "--calibration-10", - str(source_inputs.calibration_10), - "--extrinsics", - str(source_inputs.extrinsics), - "--lenses", - "10", - ], - None, - REPO_ROOT, - ) - ) - - phases.extend( - [ - ( - "fetching pinned ORB-SLAM3 baseline checkout", - [str(REPO_ROOT / "scripts/fetch_orbslam3_baseline.sh")], - None, - REPO_ROOT, - ), - ( - "building mono_tum_vi with ASan and disabled Eigen static alignment", - [str(REPO_ROOT / "scripts/build_orbslam3_baseline.sh")], - { - "ORB_SLAM3_BUILD_TARGET": "mono_tum_vi", - "ORB_SLAM3_BUILD_PARALLELISM": "1", - "ORB_SLAM3_BUILD_TYPE": "RelWithDebInfo", - "ORB_SLAM3_ENABLE_ASAN": "1", - "ORB_SLAM3_ASAN_COMPILE_FLAGS": " -fsanitize=address -fno-omit-frame-pointer -g -O0", - "ORB_SLAM3_DISABLE_EIGEN_STATIC_ALIGNMENT": "1", - "ORB_SLAM3_BUILD_EXPERIMENT": "hel-73-private-aggressive-followup", - "ORB_SLAM3_BUILD_CHANGED_VARIABLE": str( - experiment["changed_variable"] - ), - "ORB_SLAM3_BUILD_HYPOTHESIS": str(experiment["hypothesis"]), - "ORB_SLAM3_BUILD_SUCCESS_CRITERION": ( - "mono_tum_vi rebuilds with the HEL-72 sanitizer and " - "alignment toggles before the aggressive private rerun" - ), - "ORB_SLAM3_PROGRESS_ARTIFACT": str(progress_artifact), - "ORB_SLAM3_PROGRESS_ISSUE_ID": issue_identifier, - }, - REPO_ROOT, - ), - ( - "running the HEL-57 aggressive private monocular follow-up", - [ - sys.executable, - str(REPO_ROOT / "scripts/run_monocular_baseline.py"), - "--manifest", - str(manifest_path), - "--output-tag", - args.output_tag, - "--frame-stride", - str(args.frame_stride), - "--progress-artifact", - str(progress_artifact), - "--progress-issue", - issue_identifier, - "--changed-variable", - str(experiment["changed_variable"]), - "--hypothesis", - str(experiment["hypothesis"]), - "--success-criterion", - str(experiment["success_criterion"]), - "--abort-condition", - str(experiment["abort_condition"]), - "--expected-artifact", - str(experiment["expected_artifact"]), - "--orb-n-features", - "4000", - "--orb-ini-fast", - "8", - "--orb-min-fast", - "3", - ] - + ( - ["--max-frames", str(args.max_frames)] - if args.max_frames is not None - else [] - ), - None, - REPO_ROOT, - ), - ] - ) - try: for phase_index, (current_step, command, env_overrides, cwd) in enumerate( phases, @@ -617,6 +717,7 @@ def main() -> int: artifacts=artifacts, current_step=current_step, completed=phase_index - 1, + total_phases=total_phases, status="in_progress", metrics={}, experiment=experiment, @@ -632,6 +733,7 @@ def on_progress(metrics: dict[str, object]) -> None: artifacts=artifacts, current_step=current_step, completed=phase_index - 1, + total_phases=total_phases, status="in_progress", metrics={"phase": phase_index, **metrics}, experiment=experiment, @@ -655,7 +757,8 @@ def on_progress(metrics: dict[str, object]) -> None: build_progress_payload( artifacts=artifacts, current_step="follow-up command failed", - completed=max(0, len(phases) - 1), + completed=max(0, min(total_phases, phase_index)), + total_phases=total_phases, status="failed", metrics={ "failed_command": command_display(error.cmd), @@ -690,7 +793,8 @@ def on_progress(metrics: dict[str, object]) -> None: build_progress_payload( artifacts=artifacts, current_step=f"{issue_identifier} private follow-up completed", - completed=TOTAL_PHASES, + completed=total_phases, + total_phases=total_phases, status="completed", metrics={"delegate_report_path": relative_to_repo(resolved.report)}, experiment=experiment, diff --git a/tests/test_run_private_monocular_followup.py b/tests/test_run_private_monocular_followup.py index 8ca70e0..a1100e9 100644 --- a/tests/test_run_private_monocular_followup.py +++ b/tests/test_run_private_monocular_followup.py @@ -143,3 +143,160 @@ def test_render_status_report_calls_out_missing_sidecars(self) -> None: self.assertIn("Source stereo extrinsics: **missing**", report) self.assertIn("Next action: provide the missing raw source files", report) self.assertIn("Expected trajectory artifact", report) + + def test_build_followup_phases_bootstraps_fresh_worktree_prereqs(self) -> None: + prerequisites = MODULE.MonocularBaselinePrerequisites( + manifest_path=Path("/tmp/repo/manifests/example.json"), + raw_input_checks=(), + prepare_checks=( + MODULE.PrerequisiteCheck( + label="Calibration JSON", + ready=False, + detail="/tmp/repo/datasets/user/example/calibration.json", + ), + MODULE.PrerequisiteCheck( + label="Frame index CSV", + ready=False, + detail="/tmp/repo/datasets/user/example/frame_index.csv", + ), + ), + execute_checks=( + MODULE.PrerequisiteCheck( + label="Tool `cmake`", + ready=False, + detail="missing", + ), + MODULE.PrerequisiteCheck( + label="Eigen3 development package", + ready=False, + detail="missing", + ), + MODULE.PrerequisiteCheck( + label="OpenCV development package", + ready=False, + detail="missing", + ), + MODULE.PrerequisiteCheck( + label="Boost serialization development package", + ready=False, + detail="missing", + ), + MODULE.PrerequisiteCheck( + label="Pangolin development package", + ready=False, + detail="missing", + ), + ), + ) + phases = MODULE.build_followup_phases( + repo_root=Path("/tmp/repo"), + manifest_path=Path("/tmp/repo/manifests/example.json"), + source_inputs=MODULE.SourceInputPaths( + video_00=Path("/tmp/raw/00.mp4"), + video_10=Path("/tmp/raw/10.mp4"), + calibration_00=Path("/tmp/raw/00.txt"), + calibration_10=Path("/tmp/raw/10.txt"), + extrinsics=Path("/tmp/raw/extrinsics.json"), + ), + prerequisites=prerequisites, + progress_artifact=Path("/tmp/repo/.symphony/progress/HEL-79-private-run.json"), + issue_identifier="HEL-79", + output_tag="hel79", + frame_stride=1, + max_frames=None, + experiment={ + "changed_variable": "fresh-worktree rerun with explicit repo-local bootstraps", + "hypothesis": "the wrapper should bootstrap missing prerequisites before import/build", + "success_criterion": "bootstrap targets run before the aggressive lane", + "abort_condition": "a prerequisite still fails after bootstrap", + "expected_artifact": "build/out/f_example.txt", + }, + media_tools_ready=False, + ) + + step_names = [phase[0] for phase in phases] + commands = [phase[1] for phase in phases] + + self.assertEqual( + step_names[0], + "bootstrapping repo-local media tools via make bootstrap-local-ffmpeg", + ) + self.assertIn("importing private lens-10 bundle from raw source files", step_names) + self.assertIn( + "bootstrapping repo-local execution prerequisite via make bootstrap-local-cmake", + step_names, + ) + self.assertIn( + "bootstrapping repo-local execution prerequisite via make bootstrap-local-pangolin", + step_names, + ) + self.assertIn( + "verifying monocular prerequisites after bootstrap/build", + step_names, + ) + self.assertEqual(commands[0], ["make", "bootstrap-local-ffmpeg"]) + self.assertEqual(commands[-2], ["make", "monocular-prereqs"]) + self.assertIn("run_monocular_baseline.py", commands[-1][1]) + + def test_build_followup_phases_skips_import_when_prepared_bundle_is_ready(self) -> None: + prerequisites = MODULE.MonocularBaselinePrerequisites( + manifest_path=Path("/tmp/repo/manifests/example.json"), + raw_input_checks=(), + prepare_checks=( + MODULE.PrerequisiteCheck( + label="Calibration JSON", + ready=True, + detail="/tmp/repo/datasets/user/example/calibration.json", + ), + MODULE.PrerequisiteCheck( + label="Frame index CSV", + ready=True, + detail="/tmp/repo/datasets/user/example/frame_index.csv", + ), + ), + execute_checks=(), + ) + phases = MODULE.build_followup_phases( + repo_root=Path("/tmp/repo"), + manifest_path=Path("/tmp/repo/manifests/example.json"), + source_inputs=MODULE.SourceInputPaths( + video_00=Path("/tmp/raw/00.mp4"), + video_10=Path("/tmp/raw/10.mp4"), + calibration_00=Path("/tmp/raw/00.txt"), + calibration_10=Path("/tmp/raw/10.txt"), + extrinsics=Path("/tmp/raw/extrinsics.json"), + ), + prerequisites=prerequisites, + progress_artifact=Path("/tmp/repo/.symphony/progress/HEL-79-private-run.json"), + issue_identifier="HEL-79", + output_tag="hel79", + frame_stride=1, + max_frames=90, + experiment={ + "changed_variable": "rerun with prepared bundle already present", + "hypothesis": "the wrapper should skip import-specific setup", + "success_criterion": "the phase plan starts at fetch/build/verify/run", + "abort_condition": "the plan still requires import", + "expected_artifact": "build/out/f_example.txt", + }, + media_tools_ready=True, + ) + + step_names = [phase[0] for phase in phases] + self.assertNotIn( + "bootstrapping repo-local media tools via make bootstrap-local-ffmpeg", + step_names, + ) + self.assertNotIn( + "importing private lens-10 bundle from raw source files", + step_names, + ) + self.assertEqual( + step_names, + [ + "fetching pinned ORB-SLAM3 baseline checkout", + "building mono_tum_vi with ASan and disabled Eigen static alignment", + "verifying monocular prerequisites after bootstrap/build", + "running the HEL-57 aggressive private monocular follow-up", + ], + ) From d32839722c43666f1b5c225b0cd4d464dde399d7 Mon Sep 17 00:00:00 2001 From: Eugene Nikolskiy <4658906+helionaut@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:35:11 +0300 Subject: [PATCH 2/2] fix(build): preserve compile-only diagnostics --- scripts/build_orbslam3_baseline.sh | 170 +++++++++++++------------- scripts/patch_orbslam3_baseline.py | 15 ++- tests/test_operator_scripts.py | 6 + tests/test_patch_orbslam3_baseline.py | 78 ++++++++++++ 4 files changed, 179 insertions(+), 90 deletions(-) diff --git a/scripts/build_orbslam3_baseline.sh b/scripts/build_orbslam3_baseline.sh index 3fa261d..c6c2bf7 100755 --- a/scripts/build_orbslam3_baseline.sh +++ b/scripts/build_orbslam3_baseline.sh @@ -850,96 +850,94 @@ append_build_rpath_dir() { printf 'Configuring ORB_SLAM3 ...\n' mkdir -p "${checkout_dir}/build" - ( - cd "${checkout_dir}/build" - append_build_rpath_dir "${checkout_dir}/lib" - append_build_rpath_dir "${checkout_dir}/Thirdparty/DBoW2/lib" - append_build_rpath_dir "${checkout_dir}/Thirdparty/g2o/lib" + cd "${checkout_dir}/build" + append_build_rpath_dir "${checkout_dir}/lib" + append_build_rpath_dir "${checkout_dir}/Thirdparty/DBoW2/lib" + append_build_rpath_dir "${checkout_dir}/Thirdparty/g2o/lib" + for link_dir in "${linker_search_dirs[@]}"; do + append_build_rpath_dir "${link_dir}" + done + cmake_args=( + "${checkout_dir}" + "-DCMAKE_BUILD_TYPE=${build_type}" + "-DCMAKE_CXX_FLAGS=" + "-DCMAKE_C_FLAGS=" + "-D${cmake_cxx_flag_name}=${cmake_cxx_flags}" + "-D${cmake_c_flag_name}=${cmake_c_flags}" + ) + if ((${#build_rpath_dirs[@]})); then + build_rpath_text="$(IFS=';'; printf '%s' "${build_rpath_dirs[*]}")" + cmake_args+=("-DCMAKE_BUILD_RPATH=${build_rpath_text}") + fi + linker_flag_text="" + if ((${#linker_search_dirs[@]})); then for link_dir in "${linker_search_dirs[@]}"; do - append_build_rpath_dir "${link_dir}" + linker_flag_text+=" -Wl,-rpath-link,${link_dir}" done - cmake_args=( - "${checkout_dir}" - "-DCMAKE_BUILD_TYPE=${build_type}" - "-DCMAKE_CXX_FLAGS=" - "-DCMAKE_C_FLAGS=" - "-D${cmake_cxx_flag_name}=${cmake_cxx_flags}" - "-D${cmake_c_flag_name}=${cmake_c_flags}" - ) - if ((${#build_rpath_dirs[@]})); then - build_rpath_text="$(IFS=';'; printf '%s' "${build_rpath_dirs[*]}")" - cmake_args+=("-DCMAKE_BUILD_RPATH=${build_rpath_text}") - fi - linker_flag_text="" - if ((${#linker_search_dirs[@]})); then - for link_dir in "${linker_search_dirs[@]}"; do - linker_flag_text+=" -Wl,-rpath-link,${link_dir}" - done - linker_flag_text="${linker_flag_text# }" - fi - if [[ -n "${linker_flag_text}" ]]; then - cmake_linker_flags="${linker_flag_text}${cmake_linker_flags:+ ${cmake_linker_flags}}" - fi - if [[ -n "${cmake_linker_flags}" ]]; then - cmake_args+=( - "-DCMAKE_EXE_LINKER_FLAGS=${cmake_linker_flags}" - "-DCMAKE_SHARED_LINKER_FLAGS=${cmake_linker_flags}" - ) - fi - # Pangolin v0.8 installs sigslot headers that require C++14 aliases. - "${cmake_bin}" "${cmake_args[@]}" - printf 'Building ORB_SLAM3 target %s ...\n' "${build_target}" - mkdir -p "${build_attempt_dir}" - : >"${build_log_current}" - build_command_workdir="$(pwd)" - build_started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - build_command=( - "${cmake_bin}" - --build - . - --parallel - "${build_parallelism}" - --verbose - --target - "${build_target}" + linker_flag_text="${linker_flag_text# }" + fi + if [[ -n "${linker_flag_text}" ]]; then + cmake_linker_flags="${linker_flag_text}${cmake_linker_flags:+ ${cmake_linker_flags}}" + fi + if [[ -n "${cmake_linker_flags}" ]]; then + cmake_args+=( + "-DCMAKE_EXE_LINKER_FLAGS=${cmake_linker_flags}" + "-DCMAKE_SHARED_LINKER_FLAGS=${cmake_linker_flags}" ) - printf -v build_command_text '%q ' "${build_command[@]}" - build_command_text="${build_command_text% }" - printf 'Build command: %s\n' "${build_command_text}" | tee -a "${build_log_current}" - printf 'Build working directory: %s\n' "${build_command_workdir}" | tee -a "${build_log_current}" - printf 'Build started at: %s\n' "${build_started_at}" | tee -a "${build_log_current}" - update_progress_phase 7 "in_progress" "Launching root ORB_SLAM3 build for ${build_target}" - set +e - "${build_command[@]}" > >(tee -a "${build_log_current}") 2>&1 & - build_pid="$!" - write_build_attempt_metadata "started" "" - update_progress_phase 7 "in_progress" "Root ORB_SLAM3 build is running for ${build_target} (pid ${build_pid})" - start_build_progress_heartbeat - wait "${build_pid}" - build_exit_code="$?" - stop_build_progress_heartbeat - set -e - build_finished_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - if (( build_exit_code > 128 )); then - build_exit_signal="$((build_exit_code - 128))" - fi - capture_dmesg_snapshot "${dmesg_current}" - if grep -Eq "oom-kill|Out of memory: Killed process" "${dmesg_current}"; then - oom_detected=1 - fi - printf 'Build finished at: %s\n' "${build_finished_at}" | tee -a "${build_log_current}" - printf 'Build PID: %s\n' "${build_pid}" | tee -a "${build_log_current}" - printf 'Build exit code: %s\n' "${build_exit_code}" | tee -a "${build_log_current}" - if [[ -n "${build_exit_signal}" ]]; then - printf 'Build exit signal: %s\n' "${build_exit_signal}" | tee -a "${build_log_current}" - fi - if [[ "${oom_detected}" -eq 1 ]]; then - printf 'Kernel OOM evidence detected in %s\n' "${dmesg_current}" | tee -a "${build_log_current}" - fi - if [[ "${build_exit_code}" -ne 0 ]]; then - exit "${build_exit_code}" - fi + fi + # Pangolin v0.8 installs sigslot headers that require C++14 aliases. + "${cmake_bin}" "${cmake_args[@]}" + printf 'Building ORB_SLAM3 target %s ...\n' "${build_target}" + mkdir -p "${build_attempt_dir}" + : >"${build_log_current}" + build_command_workdir="$(pwd)" + build_started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + build_command=( + "${cmake_bin}" + --build + . + --parallel + "${build_parallelism}" + --verbose + --target + "${build_target}" ) + printf -v build_command_text '%q ' "${build_command[@]}" + build_command_text="${build_command_text% }" + printf 'Build command: %s\n' "${build_command_text}" | tee -a "${build_log_current}" + printf 'Build working directory: %s\n' "${build_command_workdir}" | tee -a "${build_log_current}" + printf 'Build started at: %s\n' "${build_started_at}" | tee -a "${build_log_current}" + update_progress_phase 7 "in_progress" "Launching root ORB_SLAM3 build for ${build_target}" + set +e + "${build_command[@]}" > >(tee -a "${build_log_current}") 2>&1 & + build_pid="$!" + write_build_attempt_metadata "started" "" + update_progress_phase 7 "in_progress" "Root ORB_SLAM3 build is running for ${build_target} (pid ${build_pid})" + start_build_progress_heartbeat + wait "${build_pid}" + build_exit_code="$?" + stop_build_progress_heartbeat + set -e + build_finished_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + if (( build_exit_code > 128 )); then + build_exit_signal="$((build_exit_code - 128))" + fi + capture_dmesg_snapshot "${dmesg_current}" + if grep -Eq "oom-kill|Out of memory: Killed process" "${dmesg_current}"; then + oom_detected=1 + fi + printf 'Build finished at: %s\n' "${build_finished_at}" | tee -a "${build_log_current}" + printf 'Build PID: %s\n' "${build_pid}" | tee -a "${build_log_current}" + printf 'Build exit code: %s\n' "${build_exit_code}" | tee -a "${build_log_current}" + if [[ -n "${build_exit_signal}" ]]; then + printf 'Build exit signal: %s\n' "${build_exit_signal}" | tee -a "${build_log_current}" + fi + if [[ "${oom_detected}" -eq 1 ]]; then + printf 'Kernel OOM evidence detected in %s\n' "${dmesg_current}" | tee -a "${build_log_current}" + fi + if [[ "${build_exit_code}" -ne 0 ]]; then + exit "${build_exit_code}" + fi if [[ ! -x "${build_executable_path}" ]]; then printf 'Expected executable missing after build: %s\n' "${build_executable_path}" >&2 diff --git a/scripts/patch_orbslam3_baseline.py b/scripts/patch_orbslam3_baseline.py index 31d292f..279e2e5 100644 --- a/scripts/patch_orbslam3_baseline.py +++ b/scripts/patch_orbslam3_baseline.py @@ -122,19 +122,26 @@ def normalize_save_trajectory_euroc(block: str) -> str: def normalize_save_keyframe_trajectory_euroc(block: str) -> str: block, count = re.subn( - r'vector vpMaps = mpAtlas->GetAllMaps\(\);\n' r' Map\* pBiggerMap(?: = nullptr)?;\n' r' int numMaxKFs = 0;\n', - 'vector vpMaps = mpAtlas->GetAllMaps();\n' ' Map* pBiggerMap = nullptr;\n' ' int numMaxKFs = 0;\n', block, count=1, ) if count == 0: - raise ValueError( - "Failed to normalize SaveKeyFrameTrajectoryEuRoC map initialization" + block, count = re.subn( + r' int numMaxKFs = 0;\n' + r' Map\* pBiggerMap(?: = nullptr)?;\n', + ' Map* pBiggerMap = nullptr;\n' + ' int numMaxKFs = 0;\n', + block, + count=1, ) + if count == 0: + raise ValueError( + "Failed to normalize SaveKeyFrameTrajectoryEuRoC map initialization" + ) block, count = re.subn( r'( for\(Map\* pMap :vpMaps\)\n' diff --git a/tests/test_operator_scripts.py b/tests/test_operator_scripts.py index f835663..546ef6e 100644 --- a/tests/test_operator_scripts.py +++ b/tests/test_operator_scripts.py @@ -114,6 +114,12 @@ def test_orbslam3_build_targets_mono_tum_vi(self) -> None: self.assertIn('cmake_c_flag_name="CMAKE_C_FLAGS_${cmake_build_type_suffix}"', script) self.assertIn('"-D${cmake_cxx_flag_name}=${cmake_cxx_flags}"', script) self.assertIn('"-D${cmake_c_flag_name}=${cmake_c_flags}"', script) + self.assertIn('cd "${checkout_dir}/build"', script) + self.assertNotIn('(\n cd "${checkout_dir}/build"', script) + self.assertIn('build_command_workdir="$(pwd)"', script) + self.assertIn('build_started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"', script) + self.assertIn('build_exit_code="$?"', script) + self.assertIn('build_finished_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"', script) self.assertIn("patch_orbslam3_baseline.py", script) def test_orbslam3_patch_helper_guards_empty_keyframe_saves(self) -> None: diff --git a/tests/test_patch_orbslam3_baseline.py b/tests/test_patch_orbslam3_baseline.py index 3cf7440..ba18a41 100644 --- a/tests/test_patch_orbslam3_baseline.py +++ b/tests/test_patch_orbslam3_baseline.py @@ -584,6 +584,84 @@ def test_normalize_save_keyframe_trajectory_euroc_adds_post_close_diagnostics( rewritten, ) + def test_normalize_save_keyframe_trajectory_euroc_accepts_reordered_map_init( + self, + ) -> None: + block = """void System::SaveKeyFrameTrajectoryEuRoC(const string &filename) +{ + vector vpMaps = mpAtlas->GetAllMaps(); + Map* pCurrentMap = mpAtlas->GetCurrentMap(); + cout << "HEL-78 diagnostic: SaveKeyFrameTrajectoryEuRoC atlas_state " + << "current_map_id=" + << (pCurrentMap ? static_cast(pCurrentMap->GetId()) : -1) + << ", current_map_keyframes=" + << (pCurrentMap ? static_cast(pCurrentMap->KeyFramesInMap()) : -1) + << ", current_map_points=" + << (pCurrentMap ? static_cast(pCurrentMap->MapPointsInMap()) : -1) + << ", atlas_maps=" << vpMaps.size() + << ", tracker_relative_frame_poses=" << mpTracker->mlRelativeFramePoses.size() + << ", tracker_references=" << mpTracker->mlpReferences.size() + << ", tracker_frame_times=" << mpTracker->mlFrameTimes.size() + << ", tracker_lost_flags=" << mpTracker->mlbLost.size() << endl; + Map* pBiggerMap = nullptr; + int numMaxKFs = 0; + for(Map* pMap :vpMaps) + { + if(pMap && pMap->GetAllKeyFrames().size() > numMaxKFs) + { + numMaxKFs = pMap->GetAllKeyFrames().size(); + pBiggerMap = pMap; + } + } + + if(!pBiggerMap || numMaxKFs == 0) + { + std::cout << "No keyframes were recorded; skipping keyframe trajectory save." << std::endl; + return; + } + + vector vpKFs = pBiggerMap->GetAllKeyFrames(); + ofstream f; + f.open(filename.c_str()); + cout << "HEL-75 diagnostic: SaveKeyFrameTrajectoryEuRoC stream open=" << f.is_open() + << ", good=" << f.good() << ", filename=" << filename << endl; + f << fixed; + for(size_t i=0; iisBad()) + continue; + Sophus::SE3f Twc = pKF->GetPoseInverse(); + Eigen::Quaternionf q = Twc.unit_quaternion(); + Eigen::Vector3f t = Twc.translation(); + f << setprecision(6) << pKF->mTimeStamp << setprecision(7) << " " << t(0) << " " << t(1) << " " << t(2) + << " " << q.x() << " " << q.y() << " " << q.z() << " " << q.w() << endl; + } + f.close(); + ifstream hel75_saved_file(filename.c_str(), ios::binary | ios::ate); + cout << "HEL-75 diagnostic: SaveKeyFrameTrajectoryEuRoC post_close open=" << hel75_saved_file.is_open() + << ", bytes=" + << (hel75_saved_file.is_open() ? static_cast(hel75_saved_file.tellg()) : -1) + << ", filename=" << filename << endl; +} +""" + rewritten = PATCH_HELPER.normalize_save_keyframe_trajectory_euroc(block) + + self.assertIn("Map* pBiggerMap = nullptr;", rewritten) + self.assertIn("int numMaxKFs = 0;", rewritten) + self.assertEqual( + rewritten.count("HEL-78 diagnostic: SaveKeyFrameTrajectoryEuRoC atlas_state "), + 1, + ) + self.assertEqual( + rewritten.count("HEL-75 diagnostic: SaveKeyFrameTrajectoryEuRoC stream open="), + 1, + ) + self.assertEqual( + rewritten.count("HEL-75 diagnostic: SaveKeyFrameTrajectoryEuRoC post_close open="), + 1, + ) + def test_normalize_reset_active_map_adds_pre_and_post_clear_diagnostics(self) -> None: block = """void Tracking::ResetActiveMap(bool bLocMap) {