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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ FBM_CONTAINER_INSTANCE_ID=default
# Set to the output of `id -u` and `id -g`. If unset, files are owned by UID/GID 2101
#CONTAINER_UID=
#CONTAINER_GID=

# Timezone of the researcher container. If unset, UTC will be used
#TZ=
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ The experiment script can be invoked via e.g.:
docker compose exec -u fedbiomed researcher python /brainflex-server/scripts/run_experiments.py --help
```

The output directory should be `fbm-researcher/results` inside the container.
The output directory should be `/fbm-researcher/results` inside the container.

### Stopping

Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ services:
- FBM_RESEARCHER_COMPONENT_ROOT=/fbm-researcher
# address of the researcher on the VPN
- FBM_SERVER_HOST=10.222.0.2
# timezone, used e.g. for the date subfolder of the experiment results
- TZ=${TZ:-UTC}
volumes:
- ./fbm-researcher:/fbm-researcher # contains fedbiomed component configuration and data
# brainflex training plans and experiment script
Expand Down
16 changes: 15 additions & 1 deletion scripts/run_experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import warnings
from collections import defaultdict
from dataclasses import dataclass, field
from datetime import date
from enum import Enum
from pathlib import Path

Expand Down Expand Up @@ -41,6 +42,7 @@
DEFAULT_AGGREGATOR = "scaffold"
DEFAULT_FEDPROX_MU = 1.0
DEFAULT_ALPHA = 0.1
DEFAULT_DATE_TAG = date.today().strftime("%Y_%m_%d")


@dataclass(frozen=True)
Expand All @@ -60,6 +62,7 @@ class TaskConfig:
@dataclass(frozen=True)
class PathHelper:
output_dir: Path
date_tag: str
n_rounds: int
batch_size: int
n_updates: int
Expand Down Expand Up @@ -89,7 +92,7 @@ def get_fpath_out(
)
if held_out_node is not None:
stem = f"{stem}-heldout_{held_out_node}"
return self.output_dir / task / f"{stem}{extension}"
return self.output_dir / self.date_tag / task / f"{stem}{extension}"


TASKS = {
Expand All @@ -116,6 +119,14 @@ def _get_parser() -> argparse.ArgumentParser:
parser.add_argument(
"output_dir", type=Path, help="Directory to save the global models to."
)
parser.add_argument(
"--date-tag",
default=DEFAULT_DATE_TAG,
help=(
"Name of the subfolder of the output directory to save results to."
f" Default: {DEFAULT_DATE_TAG}."
),
)
parser.add_argument(
"--task",
dest="tasks",
Expand Down Expand Up @@ -567,6 +578,7 @@ def _run_task(
def run_experiments(
output_dir: Path,
tasks: list[str],
date_tag: str = DEFAULT_DATE_TAG,
n_rounds: int = DEFAULT_N_ROUNDS,
batch_size: int = DEFAULT_BATCH_SIZE,
n_updates: int = DEFAULT_N_UPDATES,
Expand Down Expand Up @@ -604,6 +616,7 @@ def run_experiments(

path_helper = PathHelper(
output_dir=output_dir,
date_tag=date_tag,
n_rounds=n_rounds,
batch_size=batch_size,
n_updates=n_updates,
Expand Down Expand Up @@ -686,6 +699,7 @@ def main(argv: list[str] | None = None) -> None:
run_experiments(
args.output_dir,
args.tasks,
date_tag=args.date_tag,
n_rounds=args.n_rounds,
batch_size=args.batch_size,
n_updates=args.n_updates,
Expand Down
29 changes: 26 additions & 3 deletions tests/scripts/test_run_experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ def test_source(self, module: ModuleType, task: str):
def path_helper(module: ModuleType, tmp_path: Path) -> object:
return module.PathHelper(
output_dir=tmp_path,
date_tag="2026_01_01",
n_rounds=10,
batch_size=32,
n_updates=50,
Expand All @@ -169,13 +170,14 @@ class TestPathHelper:
def test_get_fpath_out(self, path_helper: object, tmp_path: Path):
assert path_helper.get_fpath_out("age", "results") == (
tmp_path
/ "2026_01_01"
/ "age"
/ "results-scaffold-10rounds-50updates-32-0.001lr-0.2test-0.1alpha.json"
)

def test_extension(self, path_helper: object, tmp_path: Path):
fpath = path_helper.get_fpath_out("sex", "model", ".pkl")
assert fpath.parent == tmp_path / "sex"
assert fpath.parent == tmp_path / "2026_01_01" / "sex"
assert fpath.suffix == ".pkl"

def test_held_out_node(self, path_helper: object):
Expand Down Expand Up @@ -757,10 +759,12 @@ def _write_results(
task: str,
data: dict,
held_out_node: str | None = None,
date_tag: str | None = None,
) -> None:
"""Write a results file for the default CLI arguments."""
fpath = module.PathHelper(
output_dir=dpath,
date_tag=date_tag if date_tag is not None else module.DEFAULT_DATE_TAG,
n_rounds=module.DEFAULT_N_ROUNDS,
batch_size=module.DEFAULT_BATCH_SIZE,
n_updates=module.DEFAULT_N_UPDATES,
Expand Down Expand Up @@ -795,24 +799,34 @@ def test_tasks_in_order(
]
kwargs = mock_run_task.call_args.kwargs
assert kwargs["n_rounds"] == 2
assert kwargs["fpath_results"].parent == tmp_path.resolve() / "age"
dpath_date = tmp_path.resolve() / module.DEFAULT_DATE_TAG
assert kwargs["fpath_results"].parent == dpath_date / "age"
assert kwargs["fpath_results"].name.startswith("results-scaffold-2rounds-")
assert kwargs["fpath_model"].name.startswith("model-scaffold-2rounds-")
assert kwargs["fpath_model"].suffix == ".pkl"
assert kwargs["dpath_tensorboard"].parent == tmp_path.resolve() / "age"
assert kwargs["dpath_tensorboard"].parent == dpath_date / "age"
assert kwargs["dpath_tensorboard"].name.startswith(
"tensorboard-scaffold-2rounds-"
)
settings = kwargs["settings"]
assert settings["output_dir"] == tmp_path.resolve()
assert settings["tasks"] == ["sex", "age"]
assert settings["date_tag"] == module.DEFAULT_DATE_TAG
assert settings["n_rounds"] == 2
assert settings["learning_rate"] == module.DEFAULT_LEARNING_RATE
assert settings["overwrite"] is False
assert settings["tensorboard"] is True
assert settings["secagg"] is True
assert kwargs["secagg"] is True

def test_date_tag(
self, module: ModuleType, mock_run_task: MagicMock, tmp_path: Path
):
module.main([str(tmp_path), "--task", "age", "--date-tag", "mytag", "--no-cv"])
kwargs = mock_run_task.call_args.kwargs
assert kwargs["fpath_results"].parent == tmp_path.resolve() / "mytag" / "age"
assert kwargs["settings"]["date_tag"] == "mytag"

def test_no_secagg(
self, module: ModuleType, mock_run_task: MagicMock, tmp_path: Path
):
Expand Down Expand Up @@ -867,6 +881,15 @@ def test_existing_settings_only(
module.main([str(tmp_path), "--task", "age", "--no-cv"])
mock_run_task.assert_called_once()

def test_existing_results_other_date_tag(
self, module: ModuleType, mock_run_task: MagicMock, tmp_path: Path
):
_write_results(
module, tmp_path.resolve(), "age", {"results": {}}, date_tag="old"
)
module.main([str(tmp_path), "--task", "age", "--no-cv"])
mock_run_task.assert_called_once()

def test_overwrite(
self, module: ModuleType, mock_run_task: MagicMock, tmp_path: Path
):
Expand Down
Loading