Skip to content
Open
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
88 changes: 88 additions & 0 deletions src/experiment/experiment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Core experiment execution logic.

This module contains the main experiment workflow functions that execute
the experimental procedures, collect data, and manage the experiment flow.

Best Practices:
- Keep experiment logic separate from setup and wrapup
- Organise experiment into clear phases (training, experimental, etc.)
- Handle errors gracefully and save progress when possible
- Log important events and data collection points
- Use clear function names that describe what each phase does
- Different sections should generally be written in separate scripts/modules
that are imported here (e.g., training_staircase.py, experiment_staircase.py)
rather than implementing everything directly in this file

Structure:
- experiment(): Main experiment function that orchestrates the workflow
- Import functions from separate modules for each phase (e.g., from .training import training_phase)

Example Usage:
```python
from .experiment import experiment

experiment(folder_path=Path("data/raw/participant_001"))
```
"""

import time
from pathlib import Path

from rich.console import Console

console = Console()


def experiment(folder_path: Path) -> None:
"""Execute the main experiment workflow.

This function contains the core experiment logic. It should be organised
into clear sections that can be easily understood and modified.

Note: Different sections should generally be written in separate scripts/modules
that are imported here (e.g., from .training_staircase import training_staircase)
rather than implementing everything directly in this file. This keeps the code
modular, testable, and easier to maintain.

Args:
folder_path: Path to the experiment data folder.

Example:
```python
from .training_staircase import training_staircase
from .experiment_staircase import experiment_staircase

def experiment(folder_path):
collect_participant_info(folder_path)

training_staircase(folder_path, ...)
experiment_staircase(folder_path, ...)

save_experiment_metadata(folder_path)
```
"""
start_time = time.time()

console.print("Starting experiment")
console.print(f"Data folder: {folder_path}")

try:
console.print("Collecting participant information...")

console.print("Running training phase...")

console.print("Running experimental phase...")

end_time = time.time()
duration = end_time - start_time

duration_file = folder_path / "experiment_duration.txt"
with open(duration_file, "w") as f:
f.write(f"{duration:.2f} seconds\n")

console.print(f"Experiment completed in {duration:.2f} seconds")

except Exception as e:
console.print(f"[Error] Experiment failed: {e}", style="red")
raise

47 changes: 47 additions & 0 deletions src/experiment/globals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Centralised experimental parameters and constants.

This module serves as the single source of truth for all experimental variables.
By centralising parameters here, we ensure consistency across the experiment
and enable easy tracking of what parameters were used in each session.

Best Practices:
- All experimental parameters should be defined here, not scattered across modules
- Use UPPERCASE naming for all constants (following software engineering best practices)
- Keep parameters immutable - do not modify them during runtime
- During setup, this file is copied to the experiment data folder for reproducibility
- Group related parameters together with clear section organisation

Example Usage:
```python
from .globals import BASE_PATH, EXAMPLE_DEVICE_PORT, EXAMPLE_BASELINE_VALUE

data_folder = BASE_PATH / "data" / "raw"
device = initialise_device(EXAMPLE_DEVICE_PORT)
set_value(EXAMPLE_BASELINE_VALUE)
```

Reproducibility:
The setup.py module automatically copies this file to the experiment data folder
as 'copy_globals.py', allowing you to track exactly which parameters were used
for each experimental session.
"""
from pathlib import Path
# Paths
EXAMPLE_PATH = "Users/username/Documents/data/raw"
EXAMPLE_RAW_PATH = Path(EXAMPLE_PATH) / "raw"
EXAMPLE_PROCESSED_PATH = Path(EXAMPLE_PATH) / "processed"
EXAMPLE_ANALYZED_PATH = Path(EXAMPLE_PATH) / "analyzed"
EXAMPLE_EXTERNAL_PATH = Path(EXAMPLE_PATH) / "external"
EXAMPLE_OTHER_PATH = Path(EXAMPLE_PATH) / "other"

# Variables DEVICE 1 (example)
EXAMPLE_DEVICE_1_STRING = 'example_value'
EXAMPLE_DEVICE_1_FLOAT = 32.5
EXAMPLE_DEVICE_1_INTEGER = 45
EXAMPLE_DEVICE_1_BOOLEAN = True

# Variables DEVICE 2 (example)
EXAMPLE_DEVICE_2_STRING = 'example_value'
EXAMPLE_DICTIONARY = {'key1': 'value1', 'key2': 42}
EXAMPLE_LIST = [1, 2, 3, 4, 5]
EXAMPLE_NONE = None
138 changes: 138 additions & 0 deletions src/experiment/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""Experiment entry point and main execution flow.

This module serves as the main entry point for running experiments. It orchestrates
the complete experiment workflow: setup, execution, and wrapup.

Best Practices:
- This should be the only script that users run directly (e.g., python -m src.experiment.run)
- Use argparse for command-line argument parsing if needed
- Implement signal handling for graceful shutdown (Ctrl+C)
- Separate experiment logic into distinct functions that can be tested independently
- Always call setup() before experiment execution
- Always call wrapup() after experiment completion, even on errors (use try/finally)

Structure:
- main(): Entry point that parses arguments and coordinates the workflow
- signal_handler(): Handles interruption signals gracefully

Example Usage:
```bash
python -m src.experiment.run
```

Example Workflow:
```python
def main():
folder_path = create_data_folder()
hardware = setup(folder_path)

try:
experiment(folder_path, hardware)
finally:
wrapup(folder_path)
cleanup(hardware)
```
"""

import signal
import sys
from pathlib import Path

from rich.console import Console

from .experiment import experiment
from .globals import BASE_PATH
from .setup import setup
from .wrapup import wrapup

console = Console()


def copy_globals_to_experiment_folder(folder_path: Path) -> None:
"""Copy globals.py to experiment folder for parameter tracking.

This ensures that the exact experimental parameters used in each session
are preserved with the data, enabling full reproducibility.

Args:
folder_path: Path to the experiment data folder.
"""
import shutil

try:
globals_source = Path(__file__).parent / "globals.py"
globals_dest = folder_path / "copy_globals.py"

if globals_source.exists():
shutil.copy2(globals_source, globals_dest)
console.print(
f"✅ Saved experiment parameters to {globals_dest}",
style="green"
)
else:
console.print(
"[Warning] globals.py not found, skipping copy",
style="yellow"
)
except Exception as e:
console.print(
f"[Warning] Failed to copy globals.py: {e}",
style="yellow"
)


def signal_handler(sig: int, frame) -> None:
"""Handle interruption signals gracefully.

Stops any running hardware devices and software processes before exiting.
This ensures clean shutdown and prevents hardware from being left in an
unsafe state.

Args:
sig: Signal number.
frame: Current stack frame.
"""
console.print("\n[Signal] Ctrl+C detected. Forcing exit.")

console.print("Stop any running hardware devices and software processes here.")

sys.exit(0)


def main() -> None:
"""Main entry point for the experiment.

Sets up the experiment environment, runs the experiment, and handles cleanup.
"""
import datetime
current_date = datetime.datetime.now().strftime('%Y%m%d')
folder_name = f"{current_date}_experiment"
folder_path = BASE_PATH / "data" / "raw" / folder_name
folder_path.mkdir(parents=True, exist_ok=True)

copy_globals_to_experiment_folder(folder_path)

try:
setup()

signal.signal(
signal.SIGINT,
lambda sig, frame: signal_handler(sig, frame)
)

experiment(folder_path)

except KeyboardInterrupt:
console.print("\n[Interrupt] Experiment interrupted by user", style="yellow")
except Exception as e:
console.print(f"\n[Error] Experiment failed: {e}", style="red")
raise
finally:
console.print("Stop any running hardware devices and software processes here.")

wrapup(folder_path)


if __name__ == "__main__":
main()

Loading