#!/usr/bin/env python """ Migration script to reorganize studies into topic-based subfolders. Run with --dry-run first to preview changes: python migrate_studies_to_topics.py --dry-run Then run without flag to execute: python migrate_studies_to_topics.py """ import shutil import argparse from pathlib import Path STUDIES_DIR = Path(__file__).parent / "studies" # Topic classification based on study name prefixes TOPIC_MAPPING = { 'bracket_': 'Simple_Bracket', 'drone_gimbal_': 'Drone_Gimbal', 'm1_mirror_': 'M1_Mirror', 'uav_arm_': 'UAV_Arm', 'simple_beam_': 'Simple_Beam', } # Files/folders to skip (not studies) SKIP_ITEMS = { 'm1_mirror_all_trials_export.csv', # Data export file '.gitkeep', '__pycache__', } def classify_study(study_name: str) -> str: """Determine which topic folder a study belongs to.""" for prefix, topic in TOPIC_MAPPING.items(): if study_name.startswith(prefix): return topic return '_Other' def get_studies_to_migrate(): """Get list of studies that need migration (not already in topic folders).""" studies = [] for item in STUDIES_DIR.iterdir(): # Skip non-directories and special items if not item.is_dir(): continue if item.name in SKIP_ITEMS: continue if item.name.startswith('.'): continue # Check if this is already a topic folder (contains study subdirs) # A topic folder would have subdirs with 1_setup folders is_topic_folder = any( (sub / "1_setup").exists() for sub in item.iterdir() if sub.is_dir() ) if is_topic_folder: print(f"[SKIP] {item.name} - already a topic folder") continue # Check if this is a study (has 1_setup or optimization_config.json) is_study = ( (item / "1_setup").exists() or (item / "optimization_config.json").exists() ) if is_study: topic = classify_study(item.name) studies.append({ 'name': item.name, 'source': item, 'topic': topic, 'target': STUDIES_DIR / topic / item.name }) else: print(f"[SKIP] {item.name} - not a study (no 1_setup folder)") return studies def migrate_studies(dry_run: bool = True): """Migrate studies to topic folders.""" studies = get_studies_to_migrate() if not studies: print("\nNo studies to migrate. All studies are already organized.") return # Group by topic for display by_topic = {} for s in studies: if s['topic'] not in by_topic: by_topic[s['topic']] = [] by_topic[s['topic']].append(s) print("\n" + "="*60) print("MIGRATION PLAN") print("="*60) for topic in sorted(by_topic.keys()): print(f"\n{topic}/") for s in by_topic[topic]: print(f" +-- {s['name']}/") print(f"\nTotal: {len(studies)} studies to migrate") if dry_run: print("\n[DRY RUN] No changes made. Run without --dry-run to execute.") return # Execute migration print("\n" + "="*60) print("EXECUTING MIGRATION") print("="*60) # Create topic folders created_topics = set() for s in studies: topic_dir = STUDIES_DIR / s['topic'] if s['topic'] not in created_topics: topic_dir.mkdir(exist_ok=True) created_topics.add(s['topic']) print(f"[CREATE] {s['topic']}/") # Move studies for s in studies: try: shutil.move(str(s['source']), str(s['target'])) print(f"[MOVE] {s['name']} -> {s['topic']}/{s['name']}") except Exception as e: print(f"[ERROR] Failed to move {s['name']}: {e}") print("\n" + "="*60) print("MIGRATION COMPLETE") print("="*60) def main(): parser = argparse.ArgumentParser(description="Migrate studies to topic folders") parser.add_argument('--dry-run', action='store_true', help='Preview changes without executing') args = parser.parse_args() migrate_studies(dry_run=args.dry_run) if __name__ == "__main__": main()