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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Versions follow [Semantic Versioning](https://semver.org) (`<major>.<minor>.<pat

## Unreleased

### Changed

- Stylistic changes to CLI help strings.

## [0.17.0] - 2026-10-12

### Added
Expand Down
21 changes: 21 additions & 0 deletions docs/how-to/imaging-with-behaviour-analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Imaging with behaviour

## Pixel-wise regression

`mesoscopy process regression` fits a ridge regression of behavioural regressors to every pixel of a ∆F/F
recording. Run it on a z-scored recording from `process zscore`.

```bash
mesoscopy process regression /path/to/example-recording_zscored.h5 /path/to/example-recording_regressors.npz
```

The regressor file is an NPZ or HDF5 file holding the `regressors` matrix, their `labels` and the `trial_idx` of the
frames they cover.

Pass `-n/--nuisance-regressors` to add regressors recorded on another clock, such as motion energy from a behaviour
camera. The nuisance file is an NPZ or HDF5 file with a `timestamps` array. Every other array in it becomes one
regressor, resampled onto the recording's timestamps, z-scored and added to the regressor matrix. Repeat `-n` to add
regressors from several files.

The command writes `example-recording_zscored_regression.npz` with the coefficients, R² and mean squared error of
every pixel. Pass `--h5` for an HDF5 file instead.
7 changes: 7 additions & 0 deletions docs/typical-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ mesoscopy report /path/to/example-recording_registered.h5
mesoscopy process regions /path/to/example-recording.nwb
```

This writes `example-recording_regions.csv` with the mean ∆F/F of each Allen atlas region in every frame.

Pass `-m/--mask` to extract custom regions instead. A mask is an NPY, NPZ or TIFF file drawn on the registered
frames. A boolean mask gives one region, named after the file or, in an NPZ file, after its key. An integer mask
gives one region per label, named `<name>_<label>`. Masks are not mirrored across hemispheres.
Repeat `-m` to add masks from several files, and add `--include-aba` to keep the atlas regions as well.

## Extract peri-event window activity

Given a recording aligned with `mesoscopy align` and the `*_trials.csv` from `visiomode-analysis session`, extract
Expand Down
50 changes: 35 additions & 15 deletions src/mesoscopy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,33 @@

__version__ = version("mesoscopy")

DOCS_URL = "https://docs.mesoscopy.org"


def docs_epilog(page: str = "") -> str:
"""Help epilog pointing at the docs.

Args:
page: Page path under the docs site, such as "how-to/metrics/". Empty for the home page.

Returns:
The epilog, marked so that click does not rewrap the URL.
"""
return f"\b\nDocumentation at {DOCS_URL}/{page}" if page else f"\b\nDocumentation at {DOCS_URL}"


# Lazy subcommands are imported only when they are looked up, to avoid importing heavy dependencies unnecessarily.
LAZY_SUBCOMMANDS = {
"align": ("mesoscopy.align:align_cmd", "Write behaviour-aligned frame timestamps to a recording."),
"align": ("mesoscopy.align:align_cmd", "Align frame timestamps to a behaviour session."),
"convert": ("mesoscopy.convert:convert_cmd", "Convert raw recordings to NWB or HDF5."),
"export": ("mesoscopy.export:export_cmd", "Export mesoscopy-generated files to other formats."),
"inspect": ("mesoscopy.inspect:inspect_cmd", "Inspect a recording session and associated preprocessing output."),
"export": ("mesoscopy.export:export_cmd", "Export mesoscopy files to other formats."),
"inspect": ("mesoscopy.inspect:inspect_cmd", "Inspect an NWB recording and its processing progress."),
"postprocess": ("mesoscopy.postprocess:postprocess_cmd", "Postprocess extracted activity."),
"preprocess": ("mesoscopy.preprocess:preprocess_cmd", "Extract haemodynamics-corrected deltaF from a recording."),
"process": ("mesoscopy.process:process_cmd", "Extract activity from preprocessed or registered recordings."),
"preprocess": ("mesoscopy.preprocess:preprocess_cmd", "Extract haemodynamics-corrected ∆F/F from a recording."),
"process": ("mesoscopy.process:process_cmd", "Analyse preprocessed or registered recordings."),
"register": ("mesoscopy.register:register_cmd", "Register recordings to an anatomical template."),
"report": ("mesoscopy.report:report_cmd", "Generate an HTML report for a mesoscopy processing step."),
"sample": ("mesoscopy:sample", "Sample an image frame from an HDF5 file and export it as a PNG."),
"report": ("mesoscopy.report:report_cmd", "Generate an HTML report for a pipeline stage."),
"sample": ("mesoscopy:sample", "Save one frame of an HDF5 recording as a PNG."),
"start-time": ("mesoscopy:start_time", "Print a recording's start time."),
}

Expand Down Expand Up @@ -115,7 +130,7 @@ def _load_lazy_command(self, cmd_name: str) -> click.Command:
return cmd


@click.group(cls=LazyGroup, lazy_subcommands=LAZY_SUBCOMMANDS)
@click.group(cls=LazyGroup, lazy_subcommands=LAZY_SUBCOMMANDS, epilog=docs_epilog())
@click.version_option(__version__)
def cli():
"""Widefield calcium imaging analysis pipeline."""
Expand All @@ -124,13 +139,17 @@ def cli():
@click.command()
@click.argument("path", type=click.Path(exists=True))
@click.argument("out_dir", type=click.Path(dir_okay=True))
@click.option("--index", default=0, show_default=True, help="Index of frame to sample.")
@click.option("--crop", default=0)
@click.option("--vmin", type=float, default=0)
@click.option("--vmax", type=float, default=255)
@click.option("--key", type=str, default=None, help="Activity column")
@click.option("--index", default=0, show_default=True, help="Index of the frame to save.")
@click.option("--crop", default=0, show_default=True, help="Pixels to crop from each edge.")
@click.option("--vmin", type=float, default=0, show_default=True, help="Lower end of the colour scale.")
@click.option("--vmax", type=float, default=255, show_default=True, help="Upper end of the colour scale.")
@click.option("--key", type=str, default=None, help="Dataset holding the frames, such as F or frames.")
def sample(path, out_dir, index, crop=0, vmin=0, vmax=255, key="frames"):
"""Sample an image frame from an HDF5 file and export it as a PNG."""
"""Save one frame of an HDF5 recording as a PNG.

PATH is an HDF5 recording and OUT_DIR the directory for the image. Pick the frame with --index and its dataset
with --key. Writes <name>_sample-<index>.png.
"""
import h5py
from matplotlib import pyplot as plt

Expand All @@ -154,7 +173,8 @@ def sample(path, out_dir, index, crop=0, vmin=0, vmax=255, key="frames"):
def start_time(path: str) -> None:
"""Print a recording's start time.

This is the first /timestamps entry of an HDF5 file, or the session_start_time of an NWB file, as stored.
PATH is an HDF5 recording or an NWB file. Prints the first frame timestamp of an HDF5 file, or the session start
time of an NWB file.
""" # noqa: DOC501
from mesoscopy import io

Expand Down
16 changes: 10 additions & 6 deletions src/mesoscopy/align/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,26 @@
"--session-start",
type=str,
default=None,
help="Behaviour session start time, ISO 8601. Mutually exclusive with --behaviour-json.",
help="Behaviour session start time in ISO format, instead of --behaviour-json.",
)
@click.option(
"--behaviour-json",
type=click.Path(exists=True, dir_okay=False),
default=None,
help="Visiomode behaviour session JSON file; its 'timestamp' key gives the session start.",
help="Visiomode behaviour session JSON file giving the session start.",
)
@click.option(
"--behaviour-id",
type=str,
default=None,
help="Behaviour session identifier. Required with --session-start, defaults to the JSON file stem otherwise.",
help="Behaviour session identifier, required with --session-start. Defaults to the --behaviour-json file name.",
)
@click.option(
"--max-offset",
type=float,
default=DEFAULT_MAX_OFFSET_S,
show_default=True,
help="Largest allowed offset, in seconds, between the first frame and the behaviour session start.",
help="Largest allowed offset between the first frame and the session start, in seconds.",
)
def align_cmd(
path: str,
Expand All @@ -71,7 +71,11 @@ def align_cmd(
behaviour_id: str | None,
max_offset: float,
) -> None:
"""Write behaviour-aligned frame timestamps to a preprocessed or registered HDF5 recording.""" # noqa: DOC501
"""Align frame timestamps to a behaviour session.

PATH is a preprocessed or registered HDF5 recording. Give the session start with --behaviour-json, or with
--session-start and --behaviour-id. Writes /timestamps_aligned, in seconds from the session start, into PATH.
""" # noqa: DOC501
if (session_start is None) == (behaviour_json is None):
msg = "Exactly one of --session-start or --behaviour-json is required."
raise click.UsageError(msg)
Expand Down Expand Up @@ -103,7 +107,7 @@ def align_recording(

Args:
h5_path (str): Path to the HDF5 recording holding an ISO 8601 `/timestamps` dataset.
session_start (str | datetime): Behaviour session start time, ISO 8601.
session_start (str | datetime): Behaviour session start time, ISO format.
behaviour_session (str): Behaviour session identifier.
max_offset_s (float, optional): Largest allowed absolute offset of the first frame from the session start,
in seconds. Defaults to 600.
Expand Down
84 changes: 48 additions & 36 deletions src/mesoscopy/convert/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@

import click

from mesoscopy import docs_epilog


@click.group("convert")
def convert_cmd(): ...
def convert_cmd() -> None:
"""Convert raw recordings to NWB or HDF5."""


@convert_cmd.command("h5")
@convert_cmd.command("h5", epilog=docs_epilog("typical-workflow/"))
@click.argument(
"input_path",
type=click.Path(exists=True),
Expand All @@ -40,45 +43,50 @@ def convert_cmd(): ...
"--out-dir",
type=click.Path(),
default="./",
help="Output directory for converted file, defaults to current working directory. Will be created if it doesn't exist.",
help="Output directory, created if missing.",
)
@click.option(
"-l",
"--link-only",
is_flag=True,
show_default=True,
default=False,
help="Create a link to the HDF5 dataset instead of copying it over. If this is enabled, you MUST keep the raw HDF5 file - if you delete it, the data will also be gone.",
help="Link the raw frames instead of copying them. The NWB file then needs the raw HDF5 file to stay in place.",
)
@click.option(
"-f", "--frames-group", type=str, default="frames", help="HDF group path under which frame data is stored."
"-f", "--frames-group", type=str, default="frames", show_default=True, help="HDF5 dataset holding the frames."
)
@click.option(
"-t",
"--timestamps-group",
type=str,
default="timestamps",
help="HDF group path under which timestamp data is stored.",
show_default=True,
help="HDF5 dataset holding the timestamps.",
)
@click.option(
"-m",
"--meta",
"meta_path",
type=click.Path(exists=True),
help="Path to animal metadata file. Must be YAML or JSON format.",
help="Subject and session metadata file in YAML or JSON format.",
)
@click.option("--subject-id", type=str, help="Metadata field - subject identifier.")
@click.option("--sex", type=click.Choice(["M", "F"], case_sensitive=False), help="Metadata field - subject sex.")
@click.option("--genotype", type=str, help="Metadata field - subject genotype.")
@click.option("--species", type=str, help="Metadata field - subject species.")
@click.option("--strain", type=str, help="Metadata field - subject strain.")
@click.option("--dob", type=str, help="Metadata field - subject date of birth in YYYY-MM-DD format (i.e. 1900-01-31).")
@click.option("--description", "session_description", type=str, help="Metadata field - session description.")
@click.option("--experimenter", type=str, help="Metadata field - experimenter name.")
@click.option("--lab", type=str, help="Metadata field - lab experiment was done in.")
@click.option("--institution", type=str, help="Metadata field - institution experiment was done in.")
@click.option("--subject-id", type=str, help="Subject identifier.")
@click.option("--sex", type=click.Choice(["M", "F"], case_sensitive=False), help="Subject sex.")
@click.option("--genotype", type=str, help="Subject genotype.")
@click.option("--species", type=str, help="Subject species.")
@click.option("--strain", type=str, help="Subject strain.")
@click.option("--dob", type=str, help="Subject date of birth as YYYY-MM-DD.")
@click.option("--description", "session_description", type=str, help="Session description.")
@click.option("--experimenter", type=str, help="Experimenter name.")
@click.option("--lab", type=str, help="Lab the session was recorded in.")
@click.option("--institution", type=str, help="Institution the session was recorded in.")
def convert_h5_cmd(**kwargs: typing.Any) -> None:
"""Convert a raw mesoscale calcium recording session from HDF5 to an NWB file compatible with mesoscopy."""
"""Convert a raw HDF5 recording to NWB.

INPUT_PATH is a raw dual-channel HDF5 recording. Fill in the subject and session metadata with --meta or the
metadata options. Writes an NWB file named after INPUT_PATH.
"""
import mesoscopy.convert.hdf5 as conv_h5

conv_h5.to_nwb(**kwargs)
Expand All @@ -94,61 +102,65 @@ def convert_h5_cmd(**kwargs: typing.Any) -> None:
"--out-dir",
type=click.Path(),
default="./",
help="Output directory for converted file, defaults to current working directory. Will be created if it doesn't exist.",
help="Output directory, created if missing.",
)
@click.option(
"-t",
"--ts-path",
type=click.Path(exists=True),
help="Path to timestamps file. Must be plain text or delimited.",
help="Frame timestamps file, plain text or delimited.",
)
@click.option(
"-d",
"--ts-delimiter",
type=click.Choice([",", "\t", " "], case_sensitive=False),
default=",",
help="Timestamp file delimiter, if file is delimited.",
help="Delimiter of the timestamps file.",
)
@click.option(
"-c",
"--ts-column",
type=int,
help="Column in timestamp file to be used as timestamps. Must be provided if file is delimited and has more than one column.",
help="Column of the timestamps file to read, needed when it has more than one.",
)
@click.option(
"--ts-has-header",
type=bool,
is_flag=True,
default=False,
help="Flag signifying whether or not the timestamps file has a header row.",
help="The timestamps file has a header row.",
)
@click.option(
"-l",
"--hdf5-only",
is_flag=True,
show_default=True,
default=False,
help="Generate an HDF5 linker file only without converting to NWB.",
help="Write the HDF5 file only, without converting it to NWB.",
)
@click.option(
"-m",
"--meta",
"meta_path",
type=click.Path(exists=True),
help="Path to animal metadata file. Must be YAML or JSON format.",
help="Subject and session metadata file in YAML or JSON format.",
)
@click.option("--subject-id", type=str, help="Metadata field - subject identifier.")
@click.option("--sex", type=click.Choice(["M", "F"], case_sensitive=False), help="Metadata field - subject sex.")
@click.option("--genotype", type=str, help="Metadata field - subject genotype.")
@click.option("--species", type=str, help="Metadata field - subject species.")
@click.option("--strain", type=str, help="Metadata field - subject strain.")
@click.option("--dob", type=str, help="Metadata field - subject date of birth in YYYY-MM-DD format (i.e. 1900-01-31).")
@click.option("--description", "session_description", type=str, help="Metadata field - session description.")
@click.option("--experimenter", type=str, help="Metadata field - experimenter name.")
@click.option("--lab", type=str, help="Metadata field - lab experiment was done in.")
@click.option("--institution", type=str, help="Metadata field - institution experiment was done in.")
@click.option("--subject-id", type=str, help="Subject identifier.")
@click.option("--sex", type=click.Choice(["M", "F"], case_sensitive=False), help="Subject sex.")
@click.option("--genotype", type=str, help="Subject genotype.")
@click.option("--species", type=str, help="Subject species.")
@click.option("--strain", type=str, help="Subject strain.")
@click.option("--dob", type=str, help="Subject date of birth as YYYY-MM-DD.")
@click.option("--description", "session_description", type=str, help="Session description.")
@click.option("--experimenter", type=str, help="Experimenter name.")
@click.option("--lab", type=str, help="Lab the session was recorded in.")
@click.option("--institution", type=str, help="Institution the session was recorded in.")
def convert_video_cmd(**kwargs: typing.Any) -> None:
"""Convert a raw mesoscale calcium recording session from video to an NWB file compatible with mesoscopy."""
"""Convert a raw video recording to NWB.

INPUT_PATH is an AVI or MP4 recording and --ts-path its frame timestamps file. Fill in the subject and session
metadata with --meta or the metadata options. Writes an HDF5 file and an NWB file named after INPUT_PATH.
"""
import mesoscopy.convert.video as conv_vid

if not kwargs.get("ts_path"):
Expand Down
Loading
Loading