Skip to content

Commit d351361

Browse files
authored
feat(stage): republish upstream outputs so the chain survives (#26)
Nextflow hands each process only the previous one's /results, so a stage that does not republish what it received removes it from the chain. The packaging stage needs every stage's data_process.json and the refined reconstructions, and the resample stage needs refinement/final-voxel and final-world for the specimen pass. The old capsules each had their own carry-forward helpers, which had already drifted apart; this is one tested implementation. Files are hardlinked with a copy fallback, so passing a multi-gigabyte tree along costs little. Co-authored-by: Peter Grotz <199680633+peter-grotz@users.noreply.github.com>
1 parent 0977096 commit d351361

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

‎src/exaspim_swc_processing/stage.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,17 @@
2626
from aind_data_schema.core.processing import DataProcess, ProcessStage, ResourceUsage
2727
from aind_data_schema_models.process_names import ProcessName
2828

29+
from exaspim_swc_processing.layout import place_artifact
30+
2931
CAPSULE_URL_TEMPLATE = "https://codeocean.allenneuraldynamics.org/capsule/{capsule_id}"
3032
"""Code Ocean capsule URL, used when no repository URL is given."""
3133

3234
DATA_PROCESS_FILENAME = "data_process.json"
3335
"""Name a stage writes its record under."""
3436

37+
UPSTREAM_STAGES = ("dispatch", "refinement", "alignment", "final")
38+
"""Stage directories that are passed along the chain, in pipeline order."""
39+
3540

3641
def _resources() -> ResourceUsage:
3742
"""Describe the machine the stage ran on.
@@ -224,3 +229,44 @@ def write_stage_process(process: DataProcess, output_dir: Path) -> Path:
224229
path = output_dir / DATA_PROCESS_FILENAME
225230
path.write_text(process.model_dump_json(indent=2), encoding="utf-8")
226231
return path
232+
233+
234+
def carry_forward(
235+
data_dir: Path,
236+
results_dir: Path,
237+
stages: Sequence[str],
238+
) -> list[str]:
239+
"""Republish upstream stage outputs so later stages can still see them.
240+
241+
Nextflow hands each process only the previous one's ``/results``, so a stage that does
242+
not republish what it received removes it from the chain. The terminal packaging stage
243+
needs every stage's ``data_process.json`` and the refined reconstructions, so each
244+
intermediate stage has to pass them along.
245+
246+
Files are hardlinked where the filesystem allows and copied otherwise, so passing a
247+
multi-gigabyte tree through costs little beyond directory entries.
248+
249+
Parameters
250+
----------
251+
data_dir : Path
252+
Directory the upstream outputs are mounted at.
253+
results_dir : Path
254+
Directory this stage writes to.
255+
stages : Sequence[str]
256+
Stage directory names to republish, if present.
257+
258+
Returns
259+
-------
260+
list[str]
261+
The stages that were found and republished, in the order given.
262+
"""
263+
carried: list[str] = []
264+
for stage in stages:
265+
source = data_dir / stage
266+
if not source.is_dir():
267+
continue
268+
for path in sorted(source.rglob("*")):
269+
if path.is_file():
270+
place_artifact(path, results_dir / stage / path.relative_to(source))
271+
carried.append(stage)
272+
return carried

‎tests/test_stage.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99

1010
from exaspim_swc_processing.stage import (
1111
DATA_PROCESS_FILENAME,
12+
UPSTREAM_STAGES,
1213
build_stage_process,
14+
carry_forward,
1315
installed_version,
1416
resolve_code,
1517
write_stage_process,
@@ -151,3 +153,35 @@ def test_write_round_trips_through_the_schema(tmp_path: Path) -> None:
151153
assert path.name == DATA_PROCESS_FILENAME
152154
reloaded = DataProcess.model_validate_json(path.read_text(encoding="utf-8"))
153155
assert reloaded.name == "s"
156+
157+
158+
def test_carry_forward_republishes_upstream_stages(tmp_path: Path) -> None:
159+
"""Nextflow hands on only the previous stage's results, so each stage must republish."""
160+
data = tmp_path / "data"
161+
(data / "dispatch").mkdir(parents=True)
162+
(data / "dispatch" / "data_process.json").write_text("{}", encoding="utf-8")
163+
(data / "refinement" / "final-voxel").mkdir(parents=True)
164+
(data / "refinement" / "final-voxel" / "N001-794492-HP.swc").write_text("x", encoding="utf-8")
165+
166+
results = tmp_path / "results"
167+
carried = carry_forward(data, results, UPSTREAM_STAGES)
168+
169+
assert carried == ["dispatch", "refinement"]
170+
assert (results / "dispatch" / "data_process.json").is_file()
171+
assert (results / "refinement" / "final-voxel" / "N001-794492-HP.swc").is_file()
172+
173+
174+
def test_carry_forward_skips_absent_stages(tmp_path: Path) -> None:
175+
"""The first stage has no upstream outputs; that is not an error."""
176+
assert carry_forward(tmp_path / "data", tmp_path / "results", UPSTREAM_STAGES) == []
177+
178+
179+
def test_carry_forward_preserves_nested_structure(tmp_path: Path) -> None:
180+
"""Deeply nested outputs survive the hop."""
181+
data = tmp_path / "data"
182+
nested = data / "final" / "ccf_space_reconstructions" / "jsons"
183+
nested.mkdir(parents=True)
184+
(nested / "N001-794492-HP.json").write_text("{}", encoding="utf-8")
185+
results = tmp_path / "results"
186+
carry_forward(data, results, ("final",))
187+
assert (results / "final/ccf_space_reconstructions/jsons/N001-794492-HP.json").is_file()

0 commit comments

Comments
 (0)