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
17 changes: 16 additions & 1 deletion docs/source/fedbiomed_node_part_1.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ We will be using [Fed-BioMed](https://fedbiomed.org/) to conduct the federated l
See [this page](https://fedbiomed.org/federated-network/) of the Fed-BioMed website for more information about how it works.
Essentially, each site participating in the federated experiment needs to deploy a **node**, and the person responsible for running the experiment (i.e. training the models) deploys a server (**researcher** in Fed-BioMed vocabulary) to which all nodes connect to.

## Record start time

```bash
brainflex record fedbiomed_node_part_1 start
```

## Deploy the node

Run this to download the images (if needed) and start them:
Expand Down Expand Up @@ -76,4 +82,13 @@ brainflex-vpn-default | info: wireguard public key for this peer: <PUBLIC_KEY>
```

Copy the public key (or the entire output of the previous command) and send it to us via email.
Then wait until you hear back before doing [Part 2](fedbiomed_node_part_2).

## Record end time

```bash
brainflex record fedbiomed_node_part_1 end
```

## Wait

Once all the above steps are done, wait until you hear back from the FL experiment coordinator before doing [Part 2](fedbiomed_node_part_2).
12 changes: 12 additions & 0 deletions docs/source/fedbiomed_node_part_2.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Fed-BioMed node: Part 2

## Record start time

```bash
brainflex record fedbiomed_node_part_2 start
```

## Check VPN connection

Check that the VPN server and the researcher are reachable:
Expand Down Expand Up @@ -35,3 +41,9 @@ docker compose exec -u fedbiomed node fedbiomed node -p /fbm-node training-plan
```

Once both training plans are approved, the node is ready for training!

## Record end time

```bash
brainflex record fedbiomed_node_part_2 end
```
12 changes: 12 additions & 0 deletions docs/source/fl_ready_dataset.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# FL-ready dataset

## Record start time

```bash
brainflex record fl_ready_data start
```

## Pipeline installation

Install the Nipoppy-to-Fed-BioMed conversion pipeline:
Expand Down Expand Up @@ -51,3 +57,9 @@ nipoppy_study/derivatives/nipoppy2fedbiomed
├── dataset.csv
└── dataset_info.json
```

## Record end time and answer questions

```bash
brainflex record fl_ready_data end
```
12 changes: 12 additions & 0 deletions docs/source/freesurfer_idps.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# FreeSurfer IDPs

## Record start time

```bash
brainflex record idp_extraction start
```

## Install pipelines

Install the FreeSurfer and FreeSurfer stats extraction pipelines:
Expand Down Expand Up @@ -95,3 +101,9 @@ nipoppy_study/derivatives/freesurfer/7.3.2/idp
├── fs7.3.2-aparc-thickness.tsv
└── fs7.3.2-aseg-volume.tsv
```

## Record end time and answer questions

```bash
brainflex record idp_extraction end
```
8 changes: 8 additions & 0 deletions docs/source/initial_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ The above step installs three packages/tools:
You can verify that they were installed successfully by running `<TOOL_NAME> --help`, where `<TOOL_NAME>` is one of `bagel`, `nipoppy`, and `brainflex`.
This should print information about the CLI commands and options.

## Initial survey

Answer a short survey about your prior experience with Nipoppy, Neurobagel and Fed-BioMed:

```bash
brainflex initial-survey
```

## Node configuration

```{attention}
Expand Down
12 changes: 12 additions & 0 deletions docs/source/phenotypic_files.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Phenotypic files

## Record start time

```bash
brainflex record semantic_harmonization start
```

## Annotation

Download the following files to your local machine:
Expand Down Expand Up @@ -49,3 +55,9 @@ sub-8 ses-unnamed snomed:248152002 55.0
sub-9 ses-unnamed snomed:248153007 60.0
```
<!-- markdownlint-enable MD010 -->

## Record end time and answer questions

```bash
brainflex record semantic_harmonization end
```
16 changes: 15 additions & 1 deletion src/brainflex/cli/console.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""Console output shared across CLI commands."""

import warnings
from collections.abc import Iterator
from collections.abc import Iterator, Sequence
from contextlib import contextmanager

from rich.console import Console
from rich.markup import escape
from rich.text import Text

from brainflex.utils.constants import LIKERT_CHOICES

console = Console(stderr=True)

Expand All @@ -15,6 +18,17 @@ def print_error(message: object) -> None:
console.print(f"[red]ERROR:[/red] {escape(str(message))}", soft_wrap=True)


def format_scale(labels: Sequence[str]) -> Text:
"""Format a Likert scale legend with one line per choice.

Raises ValueError if there is not exactly one label per choice.
"""
return Text("\n").join(
Text.assemble(" ", (choice, "prompt.choices"), f" = {label}")
for choice, label in zip(LIKERT_CHOICES, labels, strict=True)
)


def _show_warning(message: Warning | str, *args: object, **kwargs: object) -> None:
"""Print a warning message to stderr."""
console.print(f"[yellow]WARNING:[/yellow] {escape(str(message))}", soft_wrap=True)
Expand Down
43 changes: 43 additions & 0 deletions src/brainflex/cli/initial_survey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""CLI for ``brainflex initial-survey``."""

from pathlib import Path
from typing import Annotated

import typer
from rich import get_console
from rich.prompt import Prompt

from brainflex.cli.console import format_scale, print_error
from brainflex.cli.options import RootOption
from brainflex.core.initial_survey import check_initial_survey_path, initial_survey
from brainflex.utils.constants import (
FAMILIARITY_LABELS,
INITIAL_SURVEY_QUESTIONS,
LIKERT_CHOICES,
)


def initial_survey_cli(
overwrite: Annotated[
bool,
typer.Option("--overwrite", help="Replace existing survey answers."),
] = False,
path: RootOption = Path(),
) -> None:
"""Answer a survey on prior experience with the tools used in the experiment."""
# check before asking questions so that answers are not lost
try:
fpath = check_initial_survey_path(path, overwrite=overwrite)
except FileExistsError as e:
print_error(e)
raise typer.Exit(1) from e

typer.echo("Rate your familiarity with each tool:")
# same console as Prompt
get_console().print(format_scale(FAMILIARITY_LABELS), soft_wrap=True)
answers = {
question: int(Prompt.ask(question, choices=LIKERT_CHOICES))
for question in INITIAL_SURVEY_QUESTIONS
}
initial_survey(path, answers, overwrite=overwrite)
typer.echo(f"Saved answers to {fpath}")
2 changes: 2 additions & 0 deletions src/brainflex/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from brainflex import __version__
from brainflex.cli.console import render_warnings
from brainflex.cli.get_data import get_data_cli
from brainflex.cli.initial_survey import initial_survey_cli
from brainflex.cli.record import record_cli

app = typer.Typer(
Expand Down Expand Up @@ -39,4 +40,5 @@ def main(


app.command("get-data")(get_data_cli)
app.command("initial-survey")(initial_survey_cli)
app.command("record")(record_cli)
44 changes: 35 additions & 9 deletions src/brainflex/cli/record.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
"""CLI for ``brainflex record``."""

from datetime import datetime
from pathlib import Path
from typing import Annotated

import typer
from rich import get_console
from rich.prompt import Prompt
from rich.text import Text

from brainflex.cli.options import RootOption
from brainflex.core.record import Status, Step, record
from brainflex.core.record import (
Status,
Step,
check_status,
get_step_questions,
get_step_summary,
record,
)


def record_cli(
Expand All @@ -16,12 +26,28 @@ def record_cli(
path: RootOption = Path(),
) -> None:
"""Record the start or end of an experiment step."""
notes = ""
timestamp = datetime.now().astimezone()

# warn before asking questions
check_status(path, step, status)

answers = {}
if status == Status.END:
notes = Prompt.ask(
"Notes/feedback (press Enter to leave blank)",
default=notes,
show_default=False,
)
record(path, step, status, notes=notes)
typer.echo(f"Recorded {step} {status}")
if (summary := get_step_summary(step)) is not None:
# same console and colour as Prompt choices
get_console().print(Text(summary, style="prompt.choices"), end="\n\n")
for question, choices in get_step_questions(step).items():
if choices is None:
answers[question] = Prompt.ask(
f"{question} (press Enter to leave blank)",
default="",
show_default=False,
)
else:
answers[question] = Prompt.ask(question, choices=list(choices))
fpath_timestamps, fpath_survey = record(
path, step, status, answers=answers, timestamp=timestamp
)
typer.echo(f"Saved timestamp for ({step}, {status}) to {fpath_timestamps}")
if fpath_survey is not None:
typer.echo(f"Saved answers to {fpath_survey}")
66 changes: 66 additions & 0 deletions src/brainflex/core/initial_survey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Survey on prior experience, answered before the experiment."""

from collections.abc import Mapping
from pathlib import Path

import pandas as pd

from brainflex.utils.constants import (
BRAINFLEX_DIRNAME,
COL_ANSWER,
COL_QUESTION,
FAMILIARITY_LABELS,
INITIAL_SURVEY_FILENAME,
INITIAL_SURVEY_QUESTIONS,
LIKERT_CHOICES,
SURVEY_COLUMNS,
)
from brainflex.utils.likert import label_likert_answer


def check_initial_survey_path(root: Path, overwrite: bool = False) -> Path:
"""Return the path to the initial survey file of the project.

Raises FileExistsError if the file already exists and ``overwrite`` is False.
"""
fpath = root / BRAINFLEX_DIRNAME / INITIAL_SURVEY_FILENAME
if fpath.exists() and not overwrite:
raise FileExistsError(
f"Initial survey already completed: {fpath} exists."
" Use --overwrite to replace it."
)
return fpath


def initial_survey(
root: Path, answers: Mapping[str, int], overwrite: bool = False
) -> None:
"""Save the answers to the initial survey.

``answers`` maps each question in ``INITIAL_SURVEY_QUESTIONS`` to a rating on a
Likert scale from 1 to 5. Raises ValueError if questions are missing or unknown,
or if a rating is out of range, and FileExistsError if the survey file already
exists and ``overwrite`` is False.
"""
if set(answers) != set(INITIAL_SURVEY_QUESTIONS):
raise ValueError(
"Answers must be given for exactly these questions:"
f" {INITIAL_SURVEY_QUESTIONS}"
)
for question, answer in answers.items():
if str(answer) not in LIKERT_CHOICES:
raise ValueError(
f"Invalid answer {answer!r} to {question!r}:"
f" must be one of {', '.join(LIKERT_CHOICES)}"
)

fpath = check_initial_survey_path(root, overwrite=overwrite)

rows = [
{
COL_QUESTION: question,
COL_ANSWER: label_likert_answer(answers[question], FAMILIARITY_LABELS),
}
for question in INITIAL_SURVEY_QUESTIONS
]
pd.DataFrame(rows, columns=SURVEY_COLUMNS).to_csv(fpath, sep="\t", index=False)
Loading
Loading