feat: Add Studio UI, intake system, and extractor improvements

Dashboard:
- Add Studio page with drag-drop model upload and Claude chat
- Add intake system for study creation workflow
- Improve session manager and context builder
- Add intake API routes and frontend components

Optimization Engine:
- Add CLI module for command-line operations
- Add intake module for study preprocessing
- Add validation module with gate checks
- Improve Zernike extractor documentation
- Update spec models with better validation
- Enhance solve_simulation robustness

Documentation:
- Add ATOMIZER_STUDIO.md planning doc
- Add ATOMIZER_UX_SYSTEM.md for UX patterns
- Update extractor library docs
- Add study-readme-generator skill

Tools:
- Add test scripts for extraction validation
- Add Zernike recentering test

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-27 12:02:30 -05:00
parent 3193831340
commit a26914bbe8
56 changed files with 14173 additions and 646 deletions

View File

@@ -245,17 +245,45 @@ def _get_study_error_info(study_dir: Path, results_dir: Path) -> dict:
def _load_study_info(study_dir: Path, topic: Optional[str] = None) -> Optional[dict]:
"""Load study info from a study directory. Returns None if not a valid study."""
# Look for optimization config (check multiple locations)
config_file = study_dir / "optimization_config.json"
if not config_file.exists():
config_file = study_dir / "1_setup" / "optimization_config.json"
if not config_file.exists():
# Look for config file - prefer atomizer_spec.json (v2.0), fall back to legacy optimization_config.json
config_file = None
is_atomizer_spec = False
# Check for AtomizerSpec v2.0 first
for spec_path in [
study_dir / "atomizer_spec.json",
study_dir / "1_setup" / "atomizer_spec.json",
]:
if spec_path.exists():
config_file = spec_path
is_atomizer_spec = True
break
# Fall back to legacy optimization_config.json
if config_file is None:
for legacy_path in [
study_dir / "optimization_config.json",
study_dir / "1_setup" / "optimization_config.json",
]:
if legacy_path.exists():
config_file = legacy_path
break
if config_file is None:
return None
# Load config
with open(config_file) as f:
config = json.load(f)
# Normalize AtomizerSpec v2.0 to legacy format for compatibility
if is_atomizer_spec and "meta" in config:
# Extract study_name and description from meta
meta = config.get("meta", {})
config["study_name"] = meta.get("study_name", study_dir.name)
config["description"] = meta.get("description", "")
config["version"] = meta.get("version", "2.0")
# Check if results directory exists (support both 2_results and 3_results)
results_dir = study_dir / "2_results"
if not results_dir.exists():
@@ -311,12 +339,21 @@ def _load_study_info(study_dir: Path, topic: Optional[str] = None) -> Optional[d
best_trial = min(history, key=lambda x: x["objective"])
best_value = best_trial["objective"]
# Get total trials from config (supports both formats)
total_trials = (
config.get("optimization_settings", {}).get("n_trials")
or config.get("optimization", {}).get("n_trials")
or config.get("trials", {}).get("n_trials", 50)
)
# Get total trials from config (supports AtomizerSpec v2.0 and legacy formats)
total_trials = None
# AtomizerSpec v2.0: optimization.budget.max_trials
if is_atomizer_spec:
total_trials = config.get("optimization", {}).get("budget", {}).get("max_trials")
# Legacy formats
if total_trials is None:
total_trials = (
config.get("optimization_settings", {}).get("n_trials")
or config.get("optimization", {}).get("n_trials")
or config.get("optimization", {}).get("max_trials")
or config.get("trials", {}).get("n_trials", 100)
)
# Get accurate status using process detection
status = get_accurate_study_status(study_dir.name, trial_count, total_trials, has_db)
@@ -380,7 +417,12 @@ async def list_studies():
continue
# Check if this is a study (flat structure) or a topic folder (nested structure)
is_study = (item / "1_setup").exists() or (item / "optimization_config.json").exists()
# Support both AtomizerSpec v2.0 (atomizer_spec.json) and legacy (optimization_config.json)
is_study = (
(item / "1_setup").exists()
or (item / "atomizer_spec.json").exists()
or (item / "optimization_config.json").exists()
)
if is_study:
# Flat structure: study directly in studies/
@@ -396,10 +438,12 @@ async def list_studies():
if sub_item.name.startswith("."):
continue
# Check if this subdirectory is a study
sub_is_study = (sub_item / "1_setup").exists() or (
sub_item / "optimization_config.json"
).exists()
# Check if this subdirectory is a study (AtomizerSpec v2.0 or legacy)
sub_is_study = (
(sub_item / "1_setup").exists()
or (sub_item / "atomizer_spec.json").exists()
or (sub_item / "optimization_config.json").exists()
)
if sub_is_study:
study_info = _load_study_info(sub_item, topic=item.name)
if study_info: