Skip to content

Commit 770c58a

Browse files
authored
Merge pull request #9 from dmandache/fix/resume-when-finished
Fix/resume-when-finished
2 parents 6600d1e + 1323231 commit 770c58a

12 files changed

Lines changed: 201 additions & 24 deletions

File tree

src/imperandi/extract/phase.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,6 @@ def extract_phase_volumes(
192192

193193

194194
def main(args: argparse.Namespace) -> None:
195-
phase_extractor = _load_phase_extractor()
196-
197195
output_path = Path(args.csv_path_out)
198196
error_path = Path(args.error_csv_path)
199197
exclude_hash_args = {
@@ -217,8 +215,17 @@ def main(args: argparse.Namespace) -> None:
217215
paths = resume_ctx["paths"]
218216
state = resume_ctx["state"]
219217
can_resume = resume_ctx["can_resume"]
218+
already_finished = resume_ctx["already_finished"]
220219
ckpt = CheckpointManager(paths=paths, config=resume_ctx["config"])
221220

221+
if already_finished:
222+
logger.info(
223+
"Resume enabled and matching phase run already finished; skipping execution."
224+
)
225+
return
226+
227+
phase_extractor = _load_phase_extractor()
228+
222229
if can_resume and paths.main_checkpoint_path.exists():
223230
logger.info("Resuming phase from checkpoint: %s", paths.main_checkpoint_path)
224231
df = pd.read_csv(paths.main_checkpoint_path).copy()

src/imperandi/extract/radiomics.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -443,9 +443,6 @@ def extract_radiomics_from_dataframe(
443443

444444

445445
def main(args: argparse.Namespace) -> None:
446-
sitk_module, featureextractor_module = _load_radiomics_dependencies()
447-
extractor = _create_radiomics_extractor(featureextractor_module, DEFAULT_SETTINGS)
448-
449446
output_path = Path(args.csv_path_out)
450447
error_path = Path(args.error_csv_path)
451448
exclude_hash_args = {
@@ -469,8 +466,18 @@ def main(args: argparse.Namespace) -> None:
469466
paths = resume_ctx["paths"]
470467
state = resume_ctx["state"]
471468
can_resume = resume_ctx["can_resume"]
469+
already_finished = resume_ctx["already_finished"]
472470
ckpt = CheckpointManager(paths=paths, config=resume_ctx["config"])
473471

472+
if already_finished:
473+
logger.info(
474+
"Resume enabled and matching radiomics run already finished; skipping execution."
475+
)
476+
return
477+
478+
sitk_module, featureextractor_module = _load_radiomics_dependencies()
479+
extractor = _create_radiomics_extractor(featureextractor_module, DEFAULT_SETTINGS)
480+
474481
if can_resume and paths.main_checkpoint_path.exists():
475482
logger.info("Resuming radiomics from checkpoint: %s", paths.main_checkpoint_path)
476483
df = pd.read_csv(paths.main_checkpoint_path).copy()

src/imperandi/ingest/parse.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,8 +842,21 @@ def process_with_checkpoint(
842842
paths = resume_ctx["paths"]
843843
state = resume_ctx["state"]
844844
can_resume = resume_ctx["can_resume"]
845+
already_finished = resume_ctx["already_finished"]
845846
ckpt = CheckpointManager(paths=paths, config=resume_ctx["config"])
846847

848+
if already_finished:
849+
logger.info(
850+
"Resume enabled and matching parse run already finished; skipping execution."
851+
)
852+
if output_path.exists():
853+
return pd.read_csv(output_path)
854+
if paths.main_checkpoint_path.exists():
855+
return pd.read_csv(paths.main_checkpoint_path).drop(
856+
columns=["_source_idx"], errors="ignore"
857+
)
858+
return df_paths.copy()
859+
847860
if can_resume and paths.main_checkpoint_path.exists():
848861
logger.info("Resuming parse from checkpoint: %s", paths.main_checkpoint_path)
849862
df = pd.read_csv(paths.main_checkpoint_path).copy()

src/imperandi/process/convert.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,8 +536,15 @@ def main(args):
536536
paths = resume_ctx["paths"]
537537
state = resume_ctx["state"]
538538
can_resume = resume_ctx["can_resume"]
539+
already_finished = resume_ctx["already_finished"]
539540
ckpt = CheckpointManager(paths=paths, config=resume_ctx["config"])
540541

542+
if already_finished:
543+
logger.info(
544+
"Resume enabled and matching convert run already finished; skipping execution."
545+
)
546+
return
547+
541548
if args.verbose:
542549
for p in args.csv_path:
543550
check_file(p)

src/imperandi/process/segment.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@ def segment_volume(
481481
) -> List[str]:
482482
"""Run segmentation tasks and optional post‐processing."""
483483
warnings: List[str] = []
484+
ran_any_task = False
484485
tasks = tasks_config.get("tasks", [])
485486
if not tasks:
486487
raise ValueError("No tasks provided in config")
@@ -524,6 +525,7 @@ def segment_volume(
524525
task=task_name,
525526
**extra,
526527
)
528+
ran_any_task = True
527529
except Exception as exc:
528530
logger.error(
529531
"Segmentation failed on %s (%s): %s", nifti_path, task_name, exc
@@ -553,6 +555,13 @@ def segment_volume(
553555
merged_output = str(postprocess.get("output", "merged")).strip() or "merged"
554556
merged_name = _output_to_filename(merged_output)
555557
dst = output_dir / merged_name
558+
if dst.exists() and not force and not ran_any_task:
559+
if verbose:
560+
logger.info(
561+
"Skip postprocess – output exists and row already has task outputs: %s",
562+
dst,
563+
)
564+
return warnings
556565
if dst.exists():
557566
warnings.append(
558567
f"Postprocess output will overwrite existing file and continue: {dst}"
@@ -800,12 +809,6 @@ def normalize_segment_args(args: argparse.Namespace) -> argparse.Namespace:
800809

801810
def main(args: argparse.Namespace) -> None:
802811
setup_logging(verbose=getattr(args, "verbose", False))
803-
tasks_config = load_segmentation_config(
804-
getattr(args, "manifest", None),
805-
base_path=Path(__file__).resolve().parents[1],
806-
)
807-
prefetch_totalsegmentator_models(tasks_config)
808-
809812
output_path = Path(args.csv_path_out)
810813
error_path = Path(args.error_csv_path)
811814

@@ -830,8 +833,21 @@ def main(args: argparse.Namespace) -> None:
830833
paths = resume_ctx["paths"]
831834
state = resume_ctx["state"]
832835
can_resume = resume_ctx["can_resume"]
836+
already_finished = resume_ctx["already_finished"]
833837
ckpt = CheckpointManager(paths=paths, config=resume_ctx["config"])
834838

839+
if already_finished:
840+
logger.info(
841+
"Resume enabled and matching segment run already finished; skipping execution."
842+
)
843+
return
844+
845+
tasks_config = load_segmentation_config(
846+
getattr(args, "manifest", None),
847+
base_path=Path(__file__).resolve().parents[1],
848+
)
849+
prefetch_totalsegmentator_models(tasks_config)
850+
835851
from imperandi.utils.multiprocessing import (
836852
apply_strategy_env,
837853
strategy_to_log_dict,

src/imperandi/utils/run_state.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,12 @@ def prepare_resume_context(
229229
args_hash=args_hash,
230230
input_fingerprint=input_fp,
231231
)
232+
already_finished = can_resume and bool((state or {}).get("finished"))
232233
return {
233234
"paths": paths,
234235
"state": state,
235236
"can_resume": can_resume,
237+
"already_finished": already_finished,
236238
"config": CheckpointConfig(
237239
command=command,
238240
args_hash=args_hash,

tests/test_convert.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,4 +286,4 @@ def fake_convert(work_df, output_dir, verbose, num_workers, on_result):
286286

287287
args.resume = True
288288
convert_module.main(args)
289-
assert work_sizes[-1] == 0
289+
assert work_sizes == [1]

tests/test_parse.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ def update(self, amount):
534534
assert sum(recorded["updates"]) == 5
535535

536536

537-
def test_process_with_checkpoint_counts_resumed_rows_in_progress(
537+
def test_process_with_checkpoint_skips_when_matching_run_already_finished(
538538
tmp_path, monkeypatch
539539
):
540540
monkeypatch.setattr(pd.Series, "parallel_apply", pd.Series.apply, raising=False)
@@ -580,8 +580,8 @@ def update(self, amount):
580580
)
581581

582582
assert calls == []
583-
assert recorded["updates"] == [2, 1, 3]
584-
assert sum(recorded["updates"]) == 6
583+
assert recorded["updates"] == [2, 1]
584+
assert sum(recorded["updates"]) == 3
585585
assert len(out) == 3
586586

587587

tests/test_phase.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,17 @@ def test_main_resume_skips_completed_rows(tmp_path, monkeypatch):
115115
pd.DataFrame([{"nifti_path": str(nifti)}]).to_csv(csv_path, index=False)
116116

117117
calls = {"count": 0}
118+
extractor_loads = {"count": 0}
118119

119120
def fake_process_single_volume(idx, row, *, phase_extractor, verbose=False):
120121
calls["count"] += 1
121122
return idx, {"totalseg_phase": "portal"}, None
122123

123-
monkeypatch.setattr(phase_module, "_load_phase_extractor", lambda: (lambda _: {}))
124+
def fake_load_phase_extractor():
125+
extractor_loads["count"] += 1
126+
return lambda _: {}
127+
128+
monkeypatch.setattr(phase_module, "_load_phase_extractor", fake_load_phase_extractor)
124129
monkeypatch.setattr(phase_module, "process_single_volume", fake_process_single_volume)
125130

126131
args = argparse.Namespace(
@@ -140,3 +145,4 @@ def fake_process_single_volume(idx, row, *, phase_extractor, verbose=False):
140145
args.resume = True
141146
phase_module.main(args)
142147
assert calls["count"] == 0
148+
assert extractor_loads["count"] == 1

tests/test_radiomics.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,13 @@ def test_main_resume_skips_completed_rows(tmp_path, monkeypatch):
125125
).to_csv(csv_path, index=False)
126126

127127
calls = {"count": 0}
128+
dep_calls = {"count": 0}
128129

129-
monkeypatch.setattr(
130-
radiomics_module,
131-
"_load_radiomics_dependencies",
132-
lambda: (object(), object()),
133-
)
130+
def fake_load_deps():
131+
dep_calls["count"] += 1
132+
return object(), object()
133+
134+
monkeypatch.setattr(radiomics_module, "_load_radiomics_dependencies", fake_load_deps)
134135
monkeypatch.setattr(
135136
radiomics_module,
136137
"_create_radiomics_extractor",
@@ -162,3 +163,4 @@ def fake_liver(*args, **kwargs):
162163
args.resume = True
163164
radiomics_module.main(args)
164165
assert calls["count"] == 0
166+
assert dep_calls["count"] == 1

0 commit comments

Comments
 (0)