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>
This commit is contained in:
192
tools/find_best_iteration.py
Normal file
192
tools/find_best_iteration.py
Normal file
@@ -0,0 +1,192 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user