Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ All notable changes to **flypad** are documented here. The format follows
shared scale.

### Fixed
- GUI runs write `run_info.json` and `config.used.yaml` like the CLI does
([#1](https://github.com/fmi-basel/gfelsenb-flypad-analysis/issues/1)). The CLI and the
GUI each had their own copy of the pipeline sequence and the GUI's omitted the
provenance step; both now drive one `pipeline.run_experiment()`, so a results directory
is identical whichever produced it (`run_info.json` records which, as `command`). This
also fixes `flypad stats` silently falling back to a default config — instead of the
run's — on GUI-produced directories.
- Significance brackets are laid out in final axes fractions and no longer escape the
axes over the facet title; with many pairs the stack compresses so the data always
keeps at least half the axis height.
Expand Down
6 changes: 6 additions & 0 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,9 @@ Drag the recordings folder onto the drop area, point *Config YAML* at
`configs/example_experiment.yaml`, pick a metric, and press **Run analysis**. Progress
streams to the bar; when it finishes the per-condition table and an interactive dashboard
appear, and the same files are written to the output directory.

The GUI and `flypad run` drive the same pipeline, so the results directory is identical
either way — tables, figures and the `run_info.json` / `config.used.yaml` provenance pair.
`run_info.json` records which one produced it in its `command` field (`run` or `gui`), and
because `config.used.yaml` is there, a later `flypad stats` on that directory reuses the
run's own settings rather than falling back to defaults.
49 changes: 10 additions & 39 deletions src/flypad/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,53 +36,24 @@ def run(
) -> None:
"""Run the full pipeline on a folder of recordings."""
from flypad.config import load_config
from flypad.pipeline import (
absolute_onsets,
build_tables,
detect_experiment,
render_figures,
write_provenance,
write_tables,
)
from flypad.pipeline import run_experiment

cfg = load_config(config, preset=mode, overrides=set_)
out_dir = out or cfg.output.dir

detection = detect_experiment(data_dir, cfg, progress=console.log)
console.log("building tables")
tables = build_tables(detection, cfg)
written = write_tables(tables, out_dir, formats=cfg.output.formats)
if plots and cfg.plotting.enabled:
written += render_figures(
tables["per_fly"],
tables["events"],
out_dir,
cfg,
comparisons=tables["comparisons"],
n_samples=detection.n_samples,
data_dir=data_dir,
events_absolute=absolute_onsets(detection),
progress=console.log,
)

from flypad.stats import apply_qc_removal

kept = len(apply_qc_removal(tables["per_fly"]))
written += write_provenance(
out_dir,
# The same orchestration the GUI drives, so both produce identical directories.
result = run_experiment(
data_dir,
cfg,
files=detection.files,
out_dir,
make_plots=plots,
command="run",
extra={
"n_sips": len(tables["events"]),
"n_flies_kept": kept,
"n_samples_recorded": detection.n_samples,
},
progress=console.log,
)
console.print(
f"[green]done[/] {len(detection.files)} files · "
f"{len(tables['events']):,} sips · {kept} flies kept · "
f"{len(written)} files written to [bold]{out_dir}[/]"
f"[green]done[/] {result.n_files} files · "
f"{result.n_sips:,} sips · {result.n_flies_kept} flies kept · "
f"{len(result.written)} files written to [bold]{out_dir}[/]"
)


Expand Down
73 changes: 19 additions & 54 deletions src/flypad/gui/workers.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,26 @@
"""Background pipeline execution for the GUI (design §4, M8).

The heavy lifting is a plain, Qt-free function (:func:`run_pipeline_job`) so it can be
unit-tested directly; :class:`PipelineWorker` is the thin ``QObject`` that runs it on a
worker thread and re-emits progress/result/error as Qt signals (keeping the UI
The heavy lifting is :func:`flypad.pipeline.run_experiment`, shared with ``flypad run``
so the two entry points cannot drift apart; :func:`run_pipeline_job` is the Qt-free
delegation to it and :class:`PipelineWorker` the thin ``QObject`` that runs it on a
worker thread, re-emitting progress/result/error as Qt signals (keeping the UI
responsive — the coupling bug the old port had).
"""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path

import pandas as pd
from qtpy.QtCore import QObject, Signal # type: ignore[attr-defined]

from flypad.config.models import Config
from flypad.pipeline import (
absolute_onsets,
build_tables,
detect_experiment,
render_figures,
write_tables,
)
from flypad.pipeline import ExperimentResult, run_experiment

Progress = Callable[[str], None]


@dataclass
class JobResult:
"""Outcome of a full GUI pipeline run."""

out_dir: Path
n_files: int
n_sips: int
n_flies_kept: int
written: list[Path]
per_fly: pd.DataFrame
per_condition: pd.DataFrame
#: Outcome of a full GUI pipeline run — the same result the CLI gets.
JobResult = ExperimentResult


def run_pipeline_job(
Expand All @@ -48,38 +31,20 @@ def run_pipeline_job(
make_plots: bool = True,
progress: Progress | None = None,
) -> JobResult:
"""Run detect → tables → (figures) → write and summarise the result.
"""Run the pipeline for the GUI and summarise the result.

Pure orchestration over :mod:`flypad.pipeline`; no Qt here.
A thin delegation to :func:`flypad.pipeline.run_experiment` — the orchestration
itself is shared with ``flypad run`` so the two cannot drift apart. It is tagged
``command="gui"`` in ``run_info.json``; everything else about the results directory
is identical.
"""
detection = detect_experiment(data_dir, config, progress=progress)
if progress is not None:
progress("building tables")
tables = build_tables(detection, config)
written = write_tables(tables, out_dir, formats=config.output.formats)
if make_plots and config.plotting.enabled:
written += render_figures(
tables["per_fly"],
tables["events"],
out_dir,
config,
comparisons=tables["comparisons"],
n_samples=detection.n_samples,
data_dir=data_dir,
events_absolute=absolute_onsets(detection),
progress=progress,
)
from flypad.stats import apply_qc_removal

kept = len(apply_qc_removal(tables["per_fly"]))
return JobResult(
out_dir=Path(out_dir),
n_files=len(detection.files),
n_sips=len(tables["events"]),
n_flies_kept=kept,
written=written,
per_fly=tables["per_fly"],
per_condition=tables["per_condition"],
return run_experiment(
data_dir,
config,
out_dir,
make_plots=make_plots,
command="gui",
progress=progress,
)


Expand Down
4 changes: 4 additions & 0 deletions src/flypad/pipeline/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
from flypad.pipeline.context import Context
from flypad.pipeline.runner import (
DetectionResult,
ExperimentResult,
absolute_onsets,
build_tables,
config_hash,
detect_experiment,
read_table,
render_figures,
run_experiment,
write_provenance,
write_tables,
)
Expand All @@ -18,6 +20,7 @@
"REGISTRY",
"Context",
"DetectionResult",
"ExperimentResult",
"absolute_onsets",
"build_tables",
"config_hash",
Expand All @@ -27,6 +30,7 @@
"registered_stages",
"render_figures",
"run",
"run_experiment",
"write_provenance",
"write_tables",
]
84 changes: 84 additions & 0 deletions src/flypad/pipeline/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,90 @@ def write_provenance(
return [run_info, used]


@dataclass
class ExperimentResult:
"""Everything a completed run produced: tables, written paths, headline counts."""

out_dir: Path
files: list[Path]
tables: dict[str, pd.DataFrame]
written: list[Path]
n_sips: int
n_flies_kept: int
n_samples: int

@property
def n_files(self) -> int:
return len(self.files)

@property
def per_fly(self) -> pd.DataFrame:
return self.tables["per_fly"]

@property
def per_condition(self) -> pd.DataFrame:
return self.tables["per_condition"]


def run_experiment(
data_dir: str | Path,
config: Config,
out_dir: str | Path,
*,
make_plots: bool = True,
command: str = "run",
progress: Progress | None = None,
) -> ExperimentResult:
"""Run the full pipeline: detect → tables → figures → write, provenance included.

The single orchestration behind both ``flypad run`` and the GUI, so a results
directory is the same whichever produced it. ``command`` is recorded in
``run_info.json`` to say which one did (``"run"`` / ``"gui"``).

Keeping this in one place is deliberate: when the CLI and the GUI each had their own
copy of the sequence, the GUI's silently omitted :func:`write_provenance`, which also
made ``flypad stats`` fall back to a default config on those directories (#1).
"""
detection = detect_experiment(data_dir, config, progress=progress)
_emit(progress, "building tables")
tables = build_tables(detection, config)
written = write_tables(tables, out_dir, formats=config.output.formats)
if make_plots and config.plotting.enabled:
written += render_figures(
tables["per_fly"],
tables["events"],
out_dir,
config,
comparisons=tables["comparisons"],
n_samples=detection.n_samples,
data_dir=data_dir,
events_absolute=absolute_onsets(detection),
progress=progress,
)
kept = len(apply_qc_removal(tables["per_fly"]))
n_sips = len(tables["events"])
written += write_provenance(
out_dir,
config,
files=detection.files,
command=command,
extra={
"n_sips": n_sips,
"n_flies_kept": kept,
"n_samples_recorded": detection.n_samples,
},
)
return ExperimentResult(
out_dir=Path(out_dir),
files=detection.files,
tables=tables,
written=written,
n_sips=n_sips,
n_flies_kept=kept,
n_samples=detection.n_samples,
)


def read_table(results_dir: str | Path, name: str) -> pd.DataFrame:
"""Read a saved table by name, preferring parquet over csv."""
out = Path(results_dir).expanduser()
Expand Down
Loading
Loading