Files
Atomizer/tools/find_best_iteration.py
Antoine 1bb201e0b7 feat: Add post-optimization tools and mandatory best design archiving
New Tools (tools/):
- analyze_study.py: Generate comprehensive optimization reports
- find_best_iteration.py: Find best iteration folder, optionally copy it
- archive_best_design.py: Archive best design to 3_results/best_design_archive/<timestamp>/

Protocol Updates:
- OP_02_RUN_OPTIMIZATION.md v1.1: Add mandatory archive_best_design step
  in Post-Run Actions. This MUST be done after every optimization run.

V14 Updates:
- run_optimization.py: Auto-archive best design at end of optimization
- optimization_config.json: Expand bounds for V14 continuation
  - lateral_outer_angle: min 13->11 deg (was at 4.7%)
  - lateral_inner_pivot: min 7->5 mm (was at 8.1%)
  - lateral_middle_pivot: max 23->27 mm (was at 99.4%)
  - whiffle_min: max 60->72 mm (was at 96.3%)

Usage:
  python tools/analyze_study.py m1_mirror_adaptive_V14
  python tools/find_best_iteration.py m1_mirror_adaptive_V14
  python tools/archive_best_design.py m1_mirror_adaptive_V14

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 10:28:35 -05:00

193 lines
6.2 KiB
Python

#!/usr/bin/env python
"""
Atomizer Best Iteration Finder
Finds the iteration folder containing the best design for a study.
Useful for extracting the best model files after optimization.
Usage:
python tools/find_best_iteration.py <study_name>
python tools/find_best_iteration.py m1_mirror_adaptive_V14
python tools/find_best_iteration.py m1_mirror_adaptive_V14 --copy-to ./best_design
Author: Atomizer
Created: 2025-12-12
"""
import argparse
import json
import shutil
import sqlite3
from pathlib import Path
import sys
def find_study_path(study_name: str) -> Path:
"""Find study directory by name."""
studies_dir = Path(__file__).parent.parent / "studies"
study_path = studies_dir / study_name
if not study_path.exists():
raise FileNotFoundError(f"Study not found: {study_path}")
return study_path
def get_best_trial(study_path: Path) -> dict:
"""Get the best trial from the database."""
db_path = study_path / "3_results" / "study.db"
if not db_path.exists():
raise FileNotFoundError(f"Database not found: {db_path}")
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
# Get trial with best weighted_sum (or lowest objective 0 value)
cursor.execute("""
SELECT t.number, tua_ws.value_json, tua_src.value_json
FROM trials t
LEFT JOIN trial_user_attributes tua_ws ON t.trial_id = tua_ws.trial_id AND tua_ws.key = 'weighted_sum'
LEFT JOIN trial_user_attributes tua_src ON t.trial_id = tua_src.trial_id AND tua_src.key = 'source'
WHERE t.state = 'COMPLETE'
AND CAST(tua_ws.value_json AS REAL) < 1000
ORDER BY CAST(tua_ws.value_json AS REAL) ASC
LIMIT 1
""")
result = cursor.fetchone()
if not result:
# Fallback to trial_values
cursor.execute("""
SELECT t.number, tv.value
FROM trials t
JOIN trial_values tv ON t.trial_id = tv.trial_id AND tv.objective = 0
WHERE t.state = 'COMPLETE'
AND tv.value < 1000
ORDER BY tv.value ASC
LIMIT 1
""")
result = cursor.fetchone()
if result:
conn.close()
return {
"number": result[0],
"weighted_sum": result[1],
"source": "unknown"
}
raise ValueError("No valid trials found")
conn.close()
return {
"number": result[0],
"weighted_sum": float(result[1]) if result[1] else None,
"source": json.loads(result[2]) if result[2] else "unknown"
}
def trial_to_iteration(study_path: Path, trial_number: int) -> int:
"""Convert trial number to iteration number."""
db_path = study_path / "3_results" / "study.db"
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
# Get all FEA trial numbers in order
cursor.execute("""
SELECT t.number
FROM trials t
JOIN trial_user_attributes tua ON t.trial_id = tua.trial_id
WHERE t.state = 'COMPLETE' AND tua.key = 'source' AND tua.value_json = '"FEA"'
ORDER BY t.number
""")
fea_trials = [r[0] for r in cursor.fetchall()]
conn.close()
if trial_number not in fea_trials:
raise ValueError(f"Trial {trial_number} is not an FEA trial (seeded data has no iteration folder)")
return fea_trials.index(trial_number) + 1
def find_best_iteration(study_name: str) -> dict:
"""Find the best iteration folder for a study."""
study_path = find_study_path(study_name)
best_trial = get_best_trial(study_path)
result = {
"study_name": study_name,
"trial_number": best_trial["number"],
"weighted_sum": best_trial["weighted_sum"],
"source": best_trial["source"],
"iteration_folder": None,
"iteration_path": None
}
# Only FEA trials have iteration folders
if best_trial["source"] == "FEA":
iter_num = trial_to_iteration(study_path, best_trial["number"])
iter_folder = f"iter{iter_num}"
iter_path = study_path / "2_iterations" / iter_folder
if iter_path.exists():
result["iteration_folder"] = iter_folder
result["iteration_path"] = str(iter_path)
return result
def copy_best_iteration(study_name: str, dest_path: str):
"""Copy the best iteration folder to a destination."""
result = find_best_iteration(study_name)
if not result["iteration_path"]:
raise ValueError(f"Best trial ({result['trial_number']}) is from seeded data, no iteration folder")
src = Path(result["iteration_path"])
dst = Path(dest_path)
if dst.exists():
print(f"Removing existing destination: {dst}")
shutil.rmtree(dst)
print(f"Copying {src} to {dst}")
shutil.copytree(src, dst)
return result
def main():
parser = argparse.ArgumentParser(description="Find best iteration folder for Atomizer study")
parser.add_argument("study_name", help="Name of the study")
parser.add_argument("--copy-to", "-c", help="Copy best iteration folder to this path")
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
args = parser.parse_args()
try:
if args.copy_to:
result = copy_best_iteration(args.study_name, args.copy_to)
print(f"\nCopied best iteration to: {args.copy_to}")
else:
result = find_best_iteration(args.study_name)
if args.json:
print(json.dumps(result, indent=2))
else:
print(f"\n{'='*60}")
print(f"BEST ITERATION FOR: {result['study_name']}")
print(f"{'='*60}")
print(f" Trial Number: {result['trial_number']}")
print(f" Weighted Sum: {result['weighted_sum']:.2f}" if result['weighted_sum'] else " Weighted Sum: N/A")
print(f" Source: {result['source']}")
print(f" Iteration Folder: {result['iteration_folder'] or 'N/A (seeded data)'}")
if result['iteration_path']:
print(f" Full Path: {result['iteration_path']}")
print(f"{'='*60}")
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()