""" Study Blueprint Data structures for the study blueprint - the validated configuration ready for study generation. """ from dataclasses import dataclass, field, asdict from typing import Dict, List, Any, Optional import json @dataclass class DesignVariable: """Design variable specification.""" parameter: str current_value: float min_value: float max_value: float units: Optional[str] = None is_integer: bool = False def to_dict(self) -> Dict[str, Any]: return asdict(self) def to_config_format(self) -> Dict[str, Any]: """Convert to optimization_config.json format.""" return { "expression_name": self.parameter, "bounds": [self.min_value, self.max_value], "units": self.units or "", "is_integer": self.is_integer, } @dataclass class Objective: """Optimization objective specification.""" name: str goal: str # minimize, maximize, target extractor: str # Extractor ID (e.g., "E1", "E4") extractor_name: Optional[str] = None extractor_params: Optional[Dict[str, Any]] = None weight: float = 1.0 target_value: Optional[float] = None # For target objectives def to_dict(self) -> Dict[str, Any]: return asdict(self) def to_config_format(self) -> Dict[str, Any]: """Convert to optimization_config.json format.""" config = { "name": self.name, "type": self.goal, "extractor": self.extractor, "weight": self.weight, } if self.extractor_params: config["extractor_params"] = self.extractor_params if self.target_value is not None: config["target"] = self.target_value return config @dataclass class Constraint: """Optimization constraint specification.""" name: str constraint_type: str # max, min threshold: float extractor: str # Extractor ID extractor_name: Optional[str] = None extractor_params: Optional[Dict[str, Any]] = None is_hard: bool = True penalty_weight: float = 1000.0 # For soft constraints def to_dict(self) -> Dict[str, Any]: return asdict(self) def to_config_format(self) -> Dict[str, Any]: """Convert to optimization_config.json format.""" config = { "name": self.name, "type": self.constraint_type, "threshold": self.threshold, "extractor": self.extractor, "hard": self.is_hard, } if self.extractor_params: config["extractor_params"] = self.extractor_params if not self.is_hard: config["penalty_weight"] = self.penalty_weight return config @dataclass class StudyBlueprint: """ Complete study blueprint ready for generation. This is the validated configuration that will be used to create the study files (optimization_config.json, run_optimization.py, etc.) """ # Study metadata study_name: str study_description: str = "" interview_session_id: str = "" # Model paths model_path: str = "" sim_path: str = "" fem_path: str = "" # Design space design_variables: List[DesignVariable] = field(default_factory=list) # Optimization goals objectives: List[Objective] = field(default_factory=list) constraints: List[Constraint] = field(default_factory=list) # Optimization settings protocol: str = "protocol_10_single" # or "protocol_11_multi" n_trials: int = 100 sampler: str = "TPE" use_neural_acceleration: bool = False # Solver settings solver_config: Dict[str, Any] = field(default_factory=dict) solve_all_solutions: bool = True # Extractors configuration extractors_config: Dict[str, Any] = field(default_factory=dict) # Validation warnings_acknowledged: List[str] = field(default_factory=list) baseline_validated: bool = False baseline_results: Optional[Dict[str, Any]] = None def to_dict(self) -> Dict[str, Any]: """Convert to dictionary.""" return { "study_name": self.study_name, "study_description": self.study_description, "interview_session_id": self.interview_session_id, "model_path": self.model_path, "sim_path": self.sim_path, "fem_path": self.fem_path, "design_variables": [dv.to_dict() for dv in self.design_variables], "objectives": [obj.to_dict() for obj in self.objectives], "constraints": [con.to_dict() for con in self.constraints], "protocol": self.protocol, "n_trials": self.n_trials, "sampler": self.sampler, "use_neural_acceleration": self.use_neural_acceleration, "solver_config": self.solver_config, "solve_all_solutions": self.solve_all_solutions, "extractors_config": self.extractors_config, "warnings_acknowledged": self.warnings_acknowledged, "baseline_validated": self.baseline_validated, "baseline_results": self.baseline_results, } def to_json(self) -> str: """Serialize to JSON string.""" return json.dumps(self.to_dict(), indent=2) @classmethod def from_dict(cls, data: Dict[str, Any]) -> "StudyBlueprint": """Create from dictionary.""" design_variables = [ DesignVariable(**dv) for dv in data.get("design_variables", []) ] objectives = [ Objective(**obj) for obj in data.get("objectives", []) ] constraints = [ Constraint(**con) for con in data.get("constraints", []) ] return cls( study_name=data.get("study_name", ""), study_description=data.get("study_description", ""), interview_session_id=data.get("interview_session_id", ""), model_path=data.get("model_path", ""), sim_path=data.get("sim_path", ""), fem_path=data.get("fem_path", ""), design_variables=design_variables, objectives=objectives, constraints=constraints, protocol=data.get("protocol", "protocol_10_single"), n_trials=data.get("n_trials", 100), sampler=data.get("sampler", "TPE"), use_neural_acceleration=data.get("use_neural_acceleration", False), solver_config=data.get("solver_config", {}), solve_all_solutions=data.get("solve_all_solutions", True), extractors_config=data.get("extractors_config", {}), warnings_acknowledged=data.get("warnings_acknowledged", []), baseline_validated=data.get("baseline_validated", False), baseline_results=data.get("baseline_results"), ) def to_config_json(self) -> Dict[str, Any]: """ Convert to optimization_config.json format. This is the format expected by the optimization runner. """ config = { "study_name": self.study_name, "description": self.study_description, "version": "2.0", "model": { "part_file": self.model_path, "sim_file": self.sim_path, "fem_file": self.fem_path, }, "design_variables": [ dv.to_config_format() for dv in self.design_variables ], "objectives": [ obj.to_config_format() for obj in self.objectives ], "constraints": [ con.to_config_format() for con in self.constraints ], "optimization": { "n_trials": self.n_trials, "sampler": self.sampler, "protocol": self.protocol, "neural_acceleration": self.use_neural_acceleration, }, "solver": { "solve_all": self.solve_all_solutions, **self.solver_config, }, "extractors": self.extractors_config, "_metadata": { "interview_session_id": self.interview_session_id, "warnings_acknowledged": self.warnings_acknowledged, "baseline_validated": self.baseline_validated, } } return config def to_markdown(self) -> str: """Generate human-readable markdown summary.""" lines = [] lines.append(f"# Study Blueprint: {self.study_name}") lines.append("") if self.study_description: lines.append(f"**Description**: {self.study_description}") lines.append("") # Design Variables lines.append(f"## Design Variables ({len(self.design_variables)})") lines.append("") lines.append("| Parameter | Current | Min | Max | Units |") lines.append("|-----------|---------|-----|-----|-------|") for dv in self.design_variables: lines.append(f"| {dv.parameter} | {dv.current_value} | {dv.min_value} | {dv.max_value} | {dv.units or '-'} |") lines.append("") # Objectives lines.append(f"## Objectives ({len(self.objectives)})") lines.append("") lines.append("| Name | Goal | Extractor | Weight |") lines.append("|------|------|-----------|--------|") for obj in self.objectives: lines.append(f"| {obj.name} | {obj.goal} | {obj.extractor} | {obj.weight} |") lines.append("") # Constraints if self.constraints: lines.append(f"## Constraints ({len(self.constraints)})") lines.append("") lines.append("| Name | Type | Threshold | Extractor | Hard? |") lines.append("|------|------|-----------|-----------|-------|") for con in self.constraints: op = "<=" if con.constraint_type == "max" else ">=" lines.append(f"| {con.name} | {op} | {con.threshold} | {con.extractor} | {'Yes' if con.is_hard else 'No'} |") lines.append("") # Settings lines.append("## Optimization Settings") lines.append("") lines.append(f"- **Protocol**: {self.protocol}") lines.append(f"- **Trials**: {self.n_trials}") lines.append(f"- **Sampler**: {self.sampler}") lines.append(f"- **Neural Acceleration**: {'Enabled' if self.use_neural_acceleration else 'Disabled'}") lines.append("") # Validation lines.append("## Validation") lines.append("") lines.append(f"- **Baseline Validated**: {'Yes' if self.baseline_validated else 'No'}") if self.warnings_acknowledged: lines.append(f"- **Warnings Acknowledged**: {len(self.warnings_acknowledged)}") for w in self.warnings_acknowledged: lines.append(f" - {w}") lines.append("") return "\n".join(lines) def validate(self) -> List[str]: """ Validate blueprint completeness. Returns: List of validation errors (empty if valid) """ errors = [] if not self.study_name: errors.append("Study name is required") if not self.design_variables: errors.append("At least one design variable is required") if not self.objectives: errors.append("At least one objective is required") for dv in self.design_variables: if dv.min_value >= dv.max_value: errors.append(f"Invalid bounds for {dv.parameter}: min >= max") return errors def is_multi_objective(self) -> bool: """Check if this is a multi-objective study.""" return len(self.objectives) > 1 def get_objective_count(self) -> int: """Get number of objectives.""" return len(self.objectives) def get_constraint_count(self) -> int: """Get number of constraints.""" return len(self.constraints) def get_design_variable_count(self) -> int: """Get number of design variables.""" return len(self.design_variables) class BlueprintBuilder: """ Helper class for building StudyBlueprint from interview state. """ def __init__(self): """Initialize builder.""" from .interview_intelligence import InterviewIntelligence self.intelligence = InterviewIntelligence() def from_interview_state( self, state: "InterviewState", introspection: Optional[Dict[str, Any]] = None ) -> StudyBlueprint: """ Build StudyBlueprint from completed interview state. Args: state: Completed interview state introspection: Optional introspection results Returns: StudyBlueprint ready for generation """ answers = state.answers intro = introspection or state.introspection # Build design variables design_variables = [] for dv_data in answers.get("design_variables", []): if isinstance(dv_data, dict): dv = DesignVariable( parameter=dv_data.get("parameter", ""), current_value=dv_data.get("current_value", 0), min_value=dv_data.get("min_value", 0), max_value=dv_data.get("max_value", 1), units=dv_data.get("units"), is_integer=dv_data.get("is_integer", False), ) design_variables.append(dv) elif isinstance(dv_data, str): # Just a parameter name - look up in introspection expr = self._find_expression(dv_data, intro.get("expressions", [])) if expr: value = expr.get("value", 0) dv = DesignVariable( parameter=dv_data, current_value=value, min_value=value * 0.5 if value > 0 else value * 1.5, max_value=value * 1.5 if value > 0 else value * 0.5, ) design_variables.append(dv) # Build objectives objectives = [] primary_goal = answers.get("objectives", [{}]) if isinstance(primary_goal, list) and primary_goal: primary = primary_goal[0] if isinstance(primary_goal[0], dict) else {"goal": primary_goal[0]} else: primary = {"goal": str(primary_goal)} # Map to extractor extractor_sel = self.intelligence.extractor_mapper.map_goal_to_extractor( primary.get("goal", ""), intro ) objectives.append(Objective( name=primary.get("name", "primary_objective"), goal=self._normalize_goal(primary.get("goal", "")), extractor=extractor_sel.extractor_id, extractor_name=extractor_sel.extractor_name, extractor_params=extractor_sel.params, weight=primary.get("weight", 1.0), )) # Add secondary objectives secondary = answers.get("objectives_secondary", []) for sec_goal in secondary: if sec_goal == "none" or not sec_goal: continue sec_sel = self.intelligence.extractor_mapper.map_goal_to_extractor( sec_goal, intro ) objectives.append(Objective( name=f"secondary_{sec_goal}", goal=self._normalize_goal(sec_goal), extractor=sec_sel.extractor_id, extractor_name=sec_sel.extractor_name, extractor_params=sec_sel.params, weight=0.5, # Default lower weight for secondary )) # Build constraints constraints = [] constraint_answers = answers.get("constraints", {}) constraint_handling = answers.get("constraint_handling", "hard") if "max_stress" in constraint_answers and constraint_answers["max_stress"]: stress_sel = self.intelligence.extractor_mapper.map_constraint_to_extractor("stress", intro) constraints.append(Constraint( name="max_stress", constraint_type="max", threshold=constraint_answers["max_stress"], extractor=stress_sel.extractor_id, extractor_name=stress_sel.extractor_name, extractor_params=stress_sel.params, is_hard=constraint_handling != "soft", )) if "max_displacement" in constraint_answers and constraint_answers["max_displacement"]: disp_sel = self.intelligence.extractor_mapper.map_constraint_to_extractor("displacement", intro) constraints.append(Constraint( name="max_displacement", constraint_type="max", threshold=constraint_answers["max_displacement"], extractor=disp_sel.extractor_id, extractor_name=disp_sel.extractor_name, extractor_params=disp_sel.params, is_hard=constraint_handling != "soft", )) if "min_frequency" in constraint_answers and constraint_answers["min_frequency"]: freq_sel = self.intelligence.extractor_mapper.map_constraint_to_extractor("frequency", intro) constraints.append(Constraint( name="min_frequency", constraint_type="min", threshold=constraint_answers["min_frequency"], extractor=freq_sel.extractor_id, extractor_name=freq_sel.extractor_name, extractor_params=freq_sel.params, is_hard=constraint_handling != "soft", )) if "max_mass" in constraint_answers and constraint_answers["max_mass"]: mass_sel = self.intelligence.extractor_mapper.map_constraint_to_extractor("mass", intro) constraints.append(Constraint( name="max_mass", constraint_type="max", threshold=constraint_answers["max_mass"], extractor=mass_sel.extractor_id, extractor_name=mass_sel.extractor_name, is_hard=constraint_handling != "soft", )) # Determine protocol protocol = "protocol_11_multi" if len(objectives) > 1 else "protocol_10_single" # Get settings n_trials = answers.get("n_trials", 100) if n_trials == "custom": n_trials = 100 # Default # Build blueprint blueprint = StudyBlueprint( study_name=state.study_name, study_description=answers.get("problem_description", ""), interview_session_id=state.session_id, model_path=intro.get("part_file", ""), sim_path=intro.get("sim_file", ""), fem_path=intro.get("fem_file", ""), design_variables=design_variables, objectives=objectives, constraints=constraints, protocol=protocol, n_trials=int(n_trials) if isinstance(n_trials, (int, float)) else 100, sampler=self.intelligence.suggest_sampler(len(objectives), len(design_variables)), use_neural_acceleration=answers.get("use_neural_acceleration", False), solve_all_solutions=answers.get("solve_all_solutions", True), warnings_acknowledged=state.warnings_acknowledged, baseline_validated=answers.get("run_baseline_validation", False), ) return blueprint def _find_expression(self, name: str, expressions: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]: """Find expression by name.""" for expr in expressions: if expr.get("name") == name: return expr return None def _normalize_goal(self, goal: str) -> str: """Normalize goal string to standard format.""" goal_lower = goal.lower() if "minimize" in goal_lower or "reduce" in goal_lower: return "minimize" elif "maximize" in goal_lower or "increase" in goal_lower: return "maximize" elif "target" in goal_lower: return "target" else: return goal # Import for type hints from typing import TYPE_CHECKING if TYPE_CHECKING: from .interview_state import InterviewState