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>
193 lines
6.2 KiB
Python
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()
|