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>
248 lines
8.3 KiB
Python
248 lines
8.3 KiB
Python
#!/usr/bin/env python
|
|
"""
|
|
Atomizer Best Design Archiver
|
|
|
|
Archives the best design iteration folder to 3_results/best_design_archive/
|
|
with timestamp. Should be called at the end of every optimization run.
|
|
|
|
Usage:
|
|
python tools/archive_best_design.py <study_name>
|
|
python tools/archive_best_design.py m1_mirror_adaptive_V14
|
|
|
|
# Or call from run_optimization.py at the end:
|
|
from tools.archive_best_design import archive_best_design
|
|
archive_best_design("my_study")
|
|
|
|
Author: Atomizer
|
|
Created: 2025-12-12
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import shutil
|
|
import sqlite3
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
# Add parent to path for imports
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
|
|
def find_study_path(study_name: str) -> Path:
|
|
"""Find study directory by name."""
|
|
# Check if already a path
|
|
if Path(study_name).exists():
|
|
return Path(study_name)
|
|
|
|
# Check relative to tools dir
|
|
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_info(study_path: Path) -> dict:
|
|
"""Get the best trial information 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
|
|
cursor.execute("""
|
|
SELECT t.number,
|
|
tua_ws.value_json as weighted_sum,
|
|
tua_40.value_json as obj_40,
|
|
tua_60.value_json as obj_60,
|
|
tua_mfg.value_json as obj_mfg,
|
|
tua_src.value_json as source
|
|
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_40 ON t.trial_id = tua_40.trial_id AND tua_40.key = 'rel_filtered_rms_40_vs_20'
|
|
LEFT JOIN trial_user_attributes tua_60 ON t.trial_id = tua_60.trial_id AND tua_60.key = 'rel_filtered_rms_60_vs_20'
|
|
LEFT JOIN trial_user_attributes tua_mfg ON t.trial_id = tua_mfg.trial_id AND tua_mfg.key = 'mfg_90_optician_workload'
|
|
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 objective 0
|
|
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 {
|
|
"trial_number": result[0],
|
|
"weighted_sum": result[1],
|
|
"objectives": {},
|
|
"source": "unknown"
|
|
}
|
|
raise ValueError("No valid trials found")
|
|
|
|
conn.close()
|
|
|
|
return {
|
|
"trial_number": result[0],
|
|
"weighted_sum": float(result[1]) if result[1] else None,
|
|
"objectives": {
|
|
"rel_filtered_rms_40_vs_20": float(result[2]) if result[2] else None,
|
|
"rel_filtered_rms_60_vs_20": float(result[3]) if result[3] else None,
|
|
"mfg_90_optician_workload": float(result[4]) if result[4] else None,
|
|
},
|
|
"source": json.loads(result[5]) if result[5] 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()
|
|
|
|
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 archive_best_design(study_name: str, verbose: bool = True) -> dict:
|
|
"""
|
|
Archive the best design to 3_results/best_design_archive/<timestamp>/
|
|
|
|
Args:
|
|
study_name: Name of the study or path to study directory
|
|
verbose: Print progress messages
|
|
|
|
Returns:
|
|
Dictionary with archive info
|
|
"""
|
|
study_path = find_study_path(study_name)
|
|
|
|
# Get best trial info
|
|
best_trial = get_best_trial_info(study_path)
|
|
|
|
if best_trial["source"] != "FEA":
|
|
if verbose:
|
|
print(f"[WARN] Best trial #{best_trial['trial_number']} is from seeded data ({best_trial['source']})")
|
|
print("[WARN] No iteration folder to archive. Skipping.")
|
|
return {
|
|
"success": False,
|
|
"reason": "Best trial is from seeded data, no iteration folder",
|
|
"trial_number": best_trial["trial_number"],
|
|
"source": best_trial["source"]
|
|
}
|
|
|
|
# Find iteration folder
|
|
iter_num = trial_to_iteration(study_path, best_trial["trial_number"])
|
|
iter_folder = f"iter{iter_num}"
|
|
iter_path = study_path / "2_iterations" / iter_folder
|
|
|
|
if not iter_path.exists():
|
|
raise FileNotFoundError(f"Iteration folder not found: {iter_path}")
|
|
|
|
# Create archive directory with timestamp
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
archive_dir = study_path / "3_results" / "best_design_archive" / timestamp
|
|
archive_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
if verbose:
|
|
print(f"[INFO] Archiving best design...")
|
|
print(f" Trial: #{best_trial['trial_number']}")
|
|
print(f" Iteration: {iter_folder}")
|
|
print(f" Weighted Sum: {best_trial['weighted_sum']:.2f}")
|
|
print(f" Archive: {archive_dir}")
|
|
|
|
# Copy all files from iteration folder
|
|
files_copied = 0
|
|
for src_file in iter_path.iterdir():
|
|
dst_file = archive_dir / src_file.name
|
|
if src_file.is_file():
|
|
shutil.copy2(src_file, dst_file)
|
|
files_copied += 1
|
|
elif src_file.is_dir():
|
|
shutil.copytree(src_file, dst_file)
|
|
files_copied += 1
|
|
|
|
# Write metadata file
|
|
metadata = {
|
|
"study_name": study_path.name,
|
|
"trial_number": best_trial["trial_number"],
|
|
"iteration_folder": iter_folder,
|
|
"weighted_sum": best_trial["weighted_sum"],
|
|
"objectives": best_trial["objectives"],
|
|
"source": best_trial["source"],
|
|
"archived_at": datetime.now().isoformat(),
|
|
"files_copied": files_copied
|
|
}
|
|
|
|
metadata_path = archive_dir / "_archive_info.json"
|
|
with open(metadata_path, "w") as f:
|
|
json.dump(metadata, f, indent=2)
|
|
|
|
if verbose:
|
|
print(f"[OK] Archived {files_copied} files to {archive_dir.name}/")
|
|
|
|
return {
|
|
"success": True,
|
|
"archive_path": str(archive_dir),
|
|
"trial_number": best_trial["trial_number"],
|
|
"iteration_folder": iter_folder,
|
|
"weighted_sum": best_trial["weighted_sum"],
|
|
"files_copied": files_copied
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Archive best design iteration folder",
|
|
epilog="This should be run at the end of every optimization."
|
|
)
|
|
parser.add_argument("study_name", help="Name of the study to archive")
|
|
parser.add_argument("--quiet", "-q", action="store_true", help="Suppress output")
|
|
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
|
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
result = archive_best_design(args.study_name, verbose=not args.quiet)
|
|
|
|
if args.json:
|
|
print(json.dumps(result, indent=2))
|
|
elif result["success"]:
|
|
print(f"\n[SUCCESS] Best design archived to: {result['archive_path']}")
|
|
|
|
except Exception as e:
|
|
print(f"[ERROR] {e}")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|