diff --git a/src/experiment/experiment.py b/src/experiment/experiment.py new file mode 100644 index 0000000..ff7a850 --- /dev/null +++ b/src/experiment/experiment.py @@ -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 + diff --git a/src/experiment/globals.py b/src/experiment/globals.py new file mode 100644 index 0000000..2e2e784 --- /dev/null +++ b/src/experiment/globals.py @@ -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 \ No newline at end of file diff --git a/src/experiment/run.py b/src/experiment/run.py new file mode 100644 index 0000000..69de378 --- /dev/null +++ b/src/experiment/run.py @@ -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() + diff --git a/src/experiment/setup.py b/src/experiment/setup.py new file mode 100644 index 0000000..7023dcf --- /dev/null +++ b/src/experiment/setup.py @@ -0,0 +1,216 @@ +"""Pre-experiment initialisation and validation checks. + +This module handles all pre-experiment setup tasks including hardware checks, +software validation, system resource verification, and experimenter reminders. + +Best Practices: + - Always run setup() at the beginning of every experiment + - Check hardware connections and functionality before starting + - Verify software dependencies and versions + - Check disk space and system resources + - Provide clear reminders and prompts for the experimenter + - Return initialised hardware objects for use in the experiment + - Implement retry logic using while loops - if a check fails or user says no, + provide the opportunity to address the issue and try again + - Use try/except blocks around hardware checks to handle errors gracefully + and allow retries when hardware connections fail + +Structure: + - setup(): Main setup function that orchestrates all checks + - check_hardware_*(): Individual hardware validation functions + - check_software_*(): Software and dependency checks + - check_system_*(): System resource checks + +Below is an example of how to use the setup function to check the hardware. +""" + +from typing import Dict, Optional, Any + +from rich.console import Console +from rich.panel import Panel +from rich.prompt import Confirm + +from .globals import LOCAL_THRESHOLD_GB + +console = Console() + + +def check_disk_space(path: str = ".") -> None: + """Check available disk space before starting experiment. + + Args: + path: Path to check disk space (default: current directory). + """ + import shutil + _, _, free = shutil.disk_usage(path) + free_gb = free / (1024 ** 3) + if free_gb < LOCAL_THRESHOLD_GB: + console.print( + f"[bold yellow]WARNING:[/bold yellow] " + f"Only {free_gb:.2f} GB free. Please ensure sufficient space.", + style="yellow" + ) + + +def check_hardware_example() -> bool: + """Example hardware check function. + + This is a template for implementing hardware checks. Replace with + actual hardware validation logic for your specific equipment. + Use try/except blocks to handle connection errors gracefully. + + Returns: + True if hardware check passes, False otherwise. + """ + console.rule("[bold cyan]Hardware Check Example[/]") + console.print( + Panel( + "This is an example hardware check.\n" + "Replace this function with actual hardware validation.", + border_style="yellow", + ) + ) + + try: + response = Confirm.ask("\nIs the hardware ready?", default=True) + return response + except Exception as e: + console.print(f"[Error] Hardware check failed: {e}", style="red") + return False + + +def check_software_example() -> bool: + """Example software check function. + + This is a template for implementing software checks. Replace with + actual software validation logic for your specific software. + Use try/except blocks to handle errors gracefully. + + Returns: + True if software check passes, False otherwise. + """ + console.rule("[bold cyan]Software Check Example[/]") + console.print( + Panel( + "This is an example software check.\n" + "Replace this function with actual software validation.", + border_style="yellow", + ) + ) + + try: + response = Confirm.ask("\nIs the software ready?", default=True) + return response + except Exception as e: + console.print(f"[Error] Software check failed: {e}", style="red") + return False + + +def check_system_example() -> bool: + """Example system check function. + + This is a template for implementing system checks. Replace with + actual system validation logic for your specific system. + Use try/except blocks to handle errors gracefully. + + Returns: + True if system check passes, False otherwise. + """ + console.rule("[bold cyan]System Check Example[/]") + console.print( + Panel( + "This is an example system check.\n" + "Replace this function with actual system validation.", + border_style="yellow", + ) + ) + + try: + response = Confirm.ask("\nIs the system ready?", default=True) + return response + except Exception as e: + console.print(f"[Error] System check failed: {e}", style="red") + return False + + +def setup() -> Optional[Dict[str, Any]]: + """Run all pre-experiment setup checks and initialisation. + + This function orchestrates all setup tasks: + 1. System and dependency checks + 2. Hardware validation + 3. Disk space verification + 4. Initialising hardware objects + + All checks are wrapped in while loops with try/except blocks to allow + retries if checks fail or user indicates issues need to be addressed. + + Returns: + Dictionary of initialised hardware objects, or None if setup fails. + + Example: + ```python + hardware = setup() + if hardware: + chiller = hardware.get('chiller') + camera = hardware.get('camera') + ``` + """ + console.rule("[bold cyan]Experiment Setup[/]") + + check_disk_space() + + console.print( + Panel( + "Please verify the following before continuing:\n" + "1. All hardware is properly connected\n" + "2. Software is running correctly\n" + "3. Environment conditions are appropriate", + border_style="cyan", + title="Pre-Experiment Checklist" + ) + ) + + ready = Confirm.ask("\nAre you ready to continue?", default=True) + if not ready: + console.print("Setup cancelled by user.", style="yellow") + return None + + while True: + try: + hardware_ready = check_hardware_example() + if hardware_ready: + break + else: + console.print( + "[Warning] Hardware check did not pass.", + style="yellow" + ) + retry = Confirm.ask( + "Would you like to run the hardware check again?", + default=True + ) + if not retry: + console.print( + "[Error] Hardware check failed. Please verify connections.", + style="red" + ) + return None + except Exception as e: + console.print( + f"[Error] Hardware check encountered an error: {e}", + style="red" + ) + retry = Confirm.ask( + "Would you like to try the hardware check again?", + default=True + ) + if not retry: + return None + + hardware = {} + + console.rule("[bold green]Setup Complete[/]") + console.print("✅ All setup checks passed", style="green") + + return hardware if hardware else None diff --git a/src/experiment/utils.py b/src/experiment/utils.py new file mode 100644 index 0000000..2b1c88e --- /dev/null +++ b/src/experiment/utils.py @@ -0,0 +1,73 @@ +"""Utility functions for experiment management. + +This module should contain common utility functions used throughout the experiment +for file operations, console output, path handling, and system information. + +Best Practices: + - Keep utility functions pure and reusable + - Use type hints for all function parameters and return values + - Provide clear docstrings explaining purpose and usage + - Handle errors gracefully with informative messages + - Use rich library for formatted console output + +Common Utilities You Should Implement: + - File and folder operations (create folders, save files, read files) + - Path manipulation and validation + - System information gathering (device name, username, disk space) + - Data validation helpers + - Date/time formatting + +Example Functions Provided: + - example_get_device_name(): Get hostname/device name + - example_get_username(): Get current system username + +Example Usage: + ```python + from rich.console import Console + from .utils import example_get_device_name, example_get_username + + console = Console() + console.rule("[bold cyan]Section Title[/]") + console.print("Message", style="green") + + device = example_get_device_name() + username = example_get_username() + ``` +""" + +from rich.console import Console + +console = Console() + + +def example_get_device_name() -> str: + """Example: Get the hostname/device name of the current machine. + + This is an example function. Replace with your own implementation. + + Returns: + Hostname string, or 'unknown' if unavailable. + """ + import socket + + try: + hostname = socket.gethostname() or socket.getfqdn() + return hostname if hostname else "unknown" + except Exception: + return "unknown" + + +def example_get_username() -> str: + """Example: Get the current system username. + + This is an example function. Replace with your own implementation. + + Returns: + Username string, or 'unknown' if unavailable. + """ + import getpass + + try: + return getpass.getuser() or "unknown" + except Exception: + return "unknown" diff --git a/src/experiment/wrapup.py b/src/experiment/wrapup.py new file mode 100644 index 0000000..5e69409 --- /dev/null +++ b/src/experiment/wrapup.py @@ -0,0 +1,167 @@ +"""Post-experiment cleanup and finalisation. + +This module handles all tasks that should be performed after experiment completion, +including data validation, final data collection, result generation, and cleanup. + +Best Practices: + - Always call wrapup() at the end of the experiment (use try/finally) + - Validate data completeness and quality before finishing + - Collect any final measurements or metadata + - Generate summary plots or reports + - Copy data to backup locations (cloud, research drive, etc.) + - Clean up hardware connections and resources + - Provide clear feedback to the experimenter about completion status + +Structure: + - wrapup(): Main wrapup function that orchestrates all cleanup tasks + - validate_data_files(): Check that all required data files exist + - collect_final_metadata(): Gather any remaining experiment information + - run_sanity_checks(): Automatic code to do sanity checks and summarise results + +Example Usage: + ```python + from .wrapup import wrapup + + try: + run_experiment() + finally: + wrapup(folder_path=Path("data/raw/participant_001")) + ``` +""" + +from pathlib import Path +from typing import Dict, List + +from rich.console import Console + +console = Console() + + +def validate_data_files(folder_path: Path, required_files: List[str]) -> Dict[str, bool]: + """Validate that all required data files exist. + + This function should check that all necessary data files are present + in the experiment folder. Return a dictionary mapping filenames to + their existence status. + + Args: + folder_path: Path to experiment data folder. + required_files: List of required filenames to check. + + Returns: + Dictionary mapping filenames to existence status (True/False). + + Example: + ```python + required = ['trials.csv', 'metadata.json', 'copy_globals.py'] + validation = validate_data_files(folder_path, required) + all_present = all(validation.values()) + ``` + """ + console.print(f"Validating data files in {folder_path}") + + results = {} + for filename in required_files: + file_path = folder_path / filename + results[filename] = file_path.exists() + + return results + + +def collect_final_metadata(folder_path: Path) -> None: + """Collect any final metadata or measurements. + + This function should prompt for any remaining information that needs + to be recorded at the end of the experiment. + + Args: + folder_path: Path to experiment data folder. + """ + console.rule("[bold cyan]Final Metadata Collection[/]") + + console.print(f"Collecting final metadata for {folder_path}") + + console.print( + "Please provide any final information about the experiment session.", + style="cyan" + ) + + input("\nPress Enter to continue...") + + +def run_sanity_checks(folder_path: Path) -> None: + """Run automatic sanity checks and summarise results. + + This function should perform automatic validation of data quality, + completeness, and consistency. Use your functions or code to perform + sanity checks and summarise the results. + + Args: + folder_path: Path to experiment data folder. + """ + console.rule("[bold cyan]Running Sanity Checks[/]") + + console.print(f"Running sanity checks on data in {folder_path}") + + console.print("Use your functions or code to perform sanity checks here.") + + +def backup_data(folder_path: Path) -> None: + """Back up experiment data to research drive or cloud storage. + + This function should copy the experiment data folder to a backup location. + Use your functions or code to back up your data. + + Args: + folder_path: Path to experiment data folder. + """ + console.rule("[bold cyan]Backing Up Data[/]") + + console.print(f"Copying data from {folder_path}") + + console.print("Use your functions or code to back up your data here.") + + +def wrapup(folder_path: Path) -> None: + """Handle end-of-experiment wrap-up tasks. + + This function orchestrates all post-experiment tasks: + 1. Validate that all required data files exist + 2. Collect final metadata + 3. Run sanity checks and summarise results + 4. Back up data to research drive or cloud storage + + Args: + folder_path: Path to experiment data folder. + + Example: + ```python + try: + run_experiment() + finally: + wrapup(folder_path=experiment_folder) + ``` + """ + console.rule("[bold cyan]Experiment Wrap-Up[/]") + + if not folder_path.exists(): + console.print( + f"[Warning] Experiment folder not found: {folder_path}", + style="yellow" + ) + return + + required_files = ['copy_globals.py'] + validate_data_files(folder_path, required_files) + + collect_final_metadata(folder_path) + + run_sanity_checks(folder_path) + + backup_data(folder_path) + + console.rule("[bold green]Wrap-Up Complete[/]") + console.print( + f"✅ Experiment wrap-up completed for {folder_path.name}", + style="green" + )