""" Interview Intelligence Smart features for the interview process: - ExtractorMapper: Maps goals to appropriate extractors - InterviewIntelligence: Auto-detection, inference, complexity determination """ from dataclasses import dataclass, field from pathlib import Path from typing import Dict, List, Any, Optional, Literal, Tuple import re @dataclass class ExtractorSelection: """Result of mapping a goal to an extractor.""" extractor_id: str extractor_name: str goal_type: str # minimize, maximize, target params: Dict[str, Any] = field(default_factory=dict) fallback: Optional[str] = None confidence: float = 1.0 notes: Optional[str] = None class ExtractorMapper: """ Maps physics goals to appropriate extractors. Uses the Atomizer extractor library (SYS_12) to select the right extractor for each objective or constraint. """ # Goal to extractor mapping GOAL_MAP = { # Mass objectives "minimize_mass": ExtractorSelection( extractor_id="E4", extractor_name="BDF Mass Extraction", goal_type="minimize", fallback="E5", notes="Uses BDF parsing for accurate mass. Falls back to NX expression." ), "minimize_weight": ExtractorSelection( extractor_id="E4", extractor_name="BDF Mass Extraction", goal_type="minimize", fallback="E5" ), # Displacement/stiffness objectives "minimize_displacement": ExtractorSelection( extractor_id="E1", extractor_name="Displacement Extraction", goal_type="minimize", params={"component": "magnitude", "node_id": "auto"}, notes="Extracts displacement magnitude. Node ID auto-detected from max." ), "maximize_stiffness": ExtractorSelection( extractor_id="E1", extractor_name="Displacement Extraction", goal_type="minimize", # Stiffness = 1/displacement params={"component": "magnitude", "node_id": "auto"}, notes="Stiffness maximization = displacement minimization" ), # Frequency objectives "maximize_frequency": ExtractorSelection( extractor_id="E2", extractor_name="Frequency Extraction", goal_type="maximize", params={"mode_number": 1}, notes="First natural frequency. Mode number adjustable." ), "target_frequency": ExtractorSelection( extractor_id="E2", extractor_name="Frequency Extraction", goal_type="target", params={"mode_number": 1, "target": None}, notes="Target a specific frequency value." ), # Stress objectives "minimize_stress": ExtractorSelection( extractor_id="E3", extractor_name="Solid Stress Extraction", goal_type="minimize", params={"element_type": "auto", "stress_type": "von_mises"}, notes="Von Mises stress. Element type auto-detected." ), # Optical objectives "minimize_wavefront_error": ExtractorSelection( extractor_id="E8", extractor_name="Zernike Wavefront Fitting", goal_type="minimize", params={"n_terms": 15, "radius": "auto"}, notes="Fits surface to Zernike polynomials. Optical applications." ), # Custom "custom": ExtractorSelection( extractor_id="custom", extractor_name="Custom Extractor", goal_type="custom", confidence=0.5, notes="User will define custom extraction logic." ), } # Constraint type to extractor mapping CONSTRAINT_MAP = { "stress": ExtractorSelection( extractor_id="E3", extractor_name="Solid Stress Extraction", goal_type="max", params={"stress_type": "von_mises"} ), "max_stress": ExtractorSelection( extractor_id="E3", extractor_name="Solid Stress Extraction", goal_type="max", params={"stress_type": "von_mises"} ), "displacement": ExtractorSelection( extractor_id="E1", extractor_name="Displacement Extraction", goal_type="max", params={"component": "magnitude"} ), "max_displacement": ExtractorSelection( extractor_id="E1", extractor_name="Displacement Extraction", goal_type="max", params={"component": "magnitude"} ), "frequency": ExtractorSelection( extractor_id="E2", extractor_name="Frequency Extraction", goal_type="min", params={"mode_number": 1} ), "min_frequency": ExtractorSelection( extractor_id="E2", extractor_name="Frequency Extraction", goal_type="min", params={"mode_number": 1} ), "mass": ExtractorSelection( extractor_id="E4", extractor_name="BDF Mass Extraction", goal_type="max" ), "max_mass": ExtractorSelection( extractor_id="E4", extractor_name="BDF Mass Extraction", goal_type="max" ), } def map_goal_to_extractor( self, goal: str, introspection: Optional[Dict[str, Any]] = None ) -> ExtractorSelection: """ Map a physics goal to the appropriate extractor. Args: goal: Goal identifier (e.g., "minimize_mass") introspection: Optional introspection results for auto-detection Returns: ExtractorSelection with extractor details """ goal_lower = goal.lower().strip() # Direct match if goal_lower in self.GOAL_MAP: selection = self.GOAL_MAP[goal_lower] # Auto-detect parameters if introspection available if introspection: selection = self._refine_selection(selection, introspection) return selection # Fuzzy matching for common variations for key, selection in self.GOAL_MAP.items(): if key.replace("_", " ") in goal_lower or goal_lower in key: return selection # Default to custom return self.GOAL_MAP["custom"] def map_constraint_to_extractor( self, constraint_type: str, introspection: Optional[Dict[str, Any]] = None ) -> ExtractorSelection: """ Map a constraint type to the appropriate extractor. Args: constraint_type: Constraint type (e.g., "stress", "displacement") introspection: Optional introspection results Returns: ExtractorSelection with extractor details """ type_lower = constraint_type.lower().strip() if type_lower in self.CONSTRAINT_MAP: selection = self.CONSTRAINT_MAP[type_lower] if introspection: selection = self._refine_selection(selection, introspection) return selection # Try to infer from name if "stress" in type_lower: return self.CONSTRAINT_MAP["stress"] if "disp" in type_lower or "deflect" in type_lower: return self.CONSTRAINT_MAP["displacement"] if "freq" in type_lower or "modal" in type_lower: return self.CONSTRAINT_MAP["frequency"] if "mass" in type_lower or "weight" in type_lower: return self.CONSTRAINT_MAP["mass"] return ExtractorSelection( extractor_id="custom", extractor_name="Custom Constraint", goal_type="constraint", confidence=0.5 ) def _refine_selection( self, selection: ExtractorSelection, introspection: Dict[str, Any] ) -> ExtractorSelection: """Refine extractor selection based on introspection.""" import copy refined = copy.deepcopy(selection) # Auto-detect element type for stress extraction if refined.extractor_id == "E3" and refined.params.get("element_type") == "auto": element_types = introspection.get("element_types", []) if "solid" in element_types or any("TET" in e or "HEX" in e for e in element_types): refined.params["element_type"] = "solid" elif "shell" in element_types or any("QUAD" in e or "TRI" in e for e in element_types): refined.params["element_type"] = "shell" refined.extractor_id = "E3_shell" # Use shell stress extractor # Auto-detect node for displacement if refined.extractor_id == "E1" and refined.params.get("node_id") == "auto": # Use max displacement node from baseline if available if "max_disp_node" in introspection: refined.params["node_id"] = introspection["max_disp_node"] return refined def get_extractor_summary(self, selections: List[ExtractorSelection]) -> str: """Generate a summary of selected extractors.""" lines = ["**Selected Extractors:**", ""] for sel in selections: params_str = "" if sel.params: params_str = " (" + ", ".join(f"{k}={v}" for k, v in sel.params.items()) + ")" lines.append(f"- **{sel.extractor_id}**: {sel.extractor_name}{params_str}") if sel.notes: lines.append(f" > {sel.notes}") return "\n".join(lines) @dataclass class StudyTypeInference: """Result of inferring study type.""" study_type: str # single_objective, multi_objective, parametric protocol: str # protocol_10_single, protocol_11_multi confidence: float reasons: List[str] = field(default_factory=list) class InterviewIntelligence: """ Smart features for the interview process. Provides: - Study type inference from context - Auto-selection of extractors - History-based suggestions - Complexity determination """ def __init__(self): """Initialize intelligence module.""" self.extractor_mapper = ExtractorMapper() def infer_study_type( self, study_name: str, user_description: str, introspection: Optional[Dict[str, Any]] = None ) -> StudyTypeInference: """ Infer study type from available context. Args: study_name: Study name (may contain hints) user_description: User's problem description introspection: Optional introspection results Returns: StudyTypeInference with type and protocol """ reasons = [] score_multi = 0 score_single = 0 text = f"{study_name} {user_description}".lower() # Check for multi-objective keywords if any(kw in text for kw in ["pareto", "trade-off", "tradeoff", "multi-objective", "multiobjective"]): score_multi += 2 reasons.append("Multi-objective keywords detected") if any(kw in text for kw in ["versus", " vs ", "and minimize", "and maximize", "balance"]): score_multi += 1 reasons.append("Conflicting goals language detected") # Check for single-objective keywords if any(kw in text for kw in ["minimize", "maximize", "reduce", "increase"]): # Count occurrences count = sum(1 for kw in ["minimize", "maximize", "reduce", "increase"] if kw in text) if count == 1: score_single += 1 reasons.append("Single optimization goal language") else: score_multi += 1 reasons.append("Multiple optimization verbs detected") # Default to single objective if no strong signals if score_multi > score_single: return StudyTypeInference( study_type="multi_objective", protocol="protocol_11_multi", confidence=min(1.0, 0.5 + score_multi * 0.2), reasons=reasons ) else: return StudyTypeInference( study_type="single_objective", protocol="protocol_10_single", confidence=min(1.0, 0.5 + score_single * 0.2), reasons=reasons if reasons else ["Default to single-objective"] ) def auto_select_extractors( self, objectives: List[Dict[str, Any]], constraints: List[Dict[str, Any]], introspection: Optional[Dict[str, Any]] = None ) -> Dict[str, ExtractorSelection]: """ Automatically select appropriate extractors. Args: objectives: List of objective definitions constraints: List of constraint definitions introspection: Optional introspection results Returns: Dict mapping objective/constraint names to ExtractorSelection """ selections = {} # Map objectives for i, obj in enumerate(objectives): goal = obj.get("goal", "") if isinstance(obj, dict) else str(obj) name = obj.get("name", f"objective_{i}") if isinstance(obj, dict) else f"objective_{i}" selection = self.extractor_mapper.map_goal_to_extractor(goal, introspection) selections[name] = selection # Map constraints for i, con in enumerate(constraints): con_type = con.get("type", "") if isinstance(con, dict) else str(con) name = con.get("name", f"constraint_{i}") if isinstance(con, dict) else f"constraint_{i}" selection = self.extractor_mapper.map_constraint_to_extractor(con_type, introspection) selections[name] = selection return selections def determine_complexity( self, state: "InterviewState", introspection: Optional[Dict[str, Any]] = None ) -> Literal["simple", "moderate", "complex"]: """ Determine study complexity for adaptive questioning. Based on: - Number of objectives - Number of design variables - Analysis complexity - Custom components Args: state: Current interview state introspection: Optional introspection results Returns: Complexity level """ score = 0 answers = state.answers # Objectives n_obj = len(answers.get("objectives", [])) secondary = answers.get("objectives_secondary", []) if "none" not in secondary: n_obj += len(secondary) if n_obj == 1: score += 0 elif n_obj == 2: score += 1 else: score += 2 # Design variables n_dvs = len(answers.get("design_variables", [])) if n_dvs <= 3: score += 0 elif n_dvs <= 6: score += 1 else: score += 2 # Analysis types analysis_types = answers.get("analysis_types", []) if len(analysis_types) > 2: score += 2 elif len(analysis_types) > 1: score += 1 if "coupled_thermal_structural" in analysis_types: score += 1 if "nonlinear" in analysis_types: score += 1 # Introspection complexity if introspection: if introspection.get("multiple_solutions", False): score += 1 if len(introspection.get("expressions", [])) > 20: score += 1 # Categorize if score <= 2: return "simple" elif score <= 5: return "moderate" else: return "complex" def suggest_trial_count( self, n_design_variables: int, n_objectives: int, complexity: str ) -> int: """ Suggest appropriate number of trials. Args: n_design_variables: Number of design variables n_objectives: Number of objectives complexity: Study complexity level Returns: Suggested trial count """ # Base: 15 trials per design variable base = n_design_variables * 15 # Multi-objective needs more if n_objectives > 1: base = int(base * 1.5) # Adjust for complexity if complexity == "simple": base = max(50, base) elif complexity == "moderate": base = max(100, base) else: base = max(150, base) # Round to nice numbers if base <= 50: return 50 elif base <= 75: return 75 elif base <= 100: return 100 elif base <= 150: return 150 elif base <= 200: return 200 else: return int((base // 100) * 100) def suggest_sampler( self, n_objectives: int, n_design_variables: int ) -> str: """ Suggest appropriate sampler/optimizer. Args: n_objectives: Number of objectives n_design_variables: Number of design variables Returns: Sampler name """ if n_objectives > 1: return "NSGA-II" # Multi-objective elif n_design_variables <= 3: return "TPE" # Tree-structured Parzen Estimator elif n_design_variables <= 10: return "CMA-ES" # Covariance Matrix Adaptation else: return "TPE" # TPE handles high dimensions well def analyze_design_variable_candidates( self, expressions: List[Dict[str, Any]] ) -> List[Dict[str, Any]]: """ Analyze expressions to find design variable candidates. Args: expressions: List of expressions from introspection Returns: Sorted list of candidates with scores """ candidates = [] # High confidence patterns high_patterns = [ (r"thickness", "Thickness parameter"), (r"width", "Width parameter"), (r"height", "Height parameter"), (r"diameter", "Diameter parameter"), (r"radius", "Radius parameter"), (r"length", "Length parameter"), (r"depth", "Depth parameter"), (r"angle", "Angle parameter"), (r"fillet", "Fillet radius"), (r"chamfer", "Chamfer dimension"), (r"rib_", "Rib parameter"), (r"wall_", "Wall parameter"), (r"flange_", "Flange parameter"), ] # Medium confidence patterns medium_patterns = [ (r"dim_", "Dimension parameter"), (r"size_", "Size parameter"), (r"param_", "Named parameter"), (r"^p\d+$", "Numbered parameter"), (r"var_", "Variable"), ] # Exclusion patterns exclude_patterns = [ r"mesh_", r"count_", r"num_", r"material", r"derived_", r"calc_", r"_result$", r"_output$", r"^n\d+$", r"count$" ] for expr in expressions: name = expr.get("name", "") value = expr.get("value") formula = expr.get("formula", "") # Skip non-numeric if not isinstance(value, (int, float)): continue # Skip formulas (computed values) if formula and formula != str(value): continue # Check exclusions if any(re.search(p, name.lower()) for p in exclude_patterns): continue # Score score = 0 reason = "Named expression" for pattern, desc in high_patterns: if re.search(pattern, name.lower()): score = 3 reason = desc break if score == 0: for pattern, desc in medium_patterns: if re.search(pattern, name.lower()): score = 2 reason = desc break if score == 0 and len(name) > 2: score = 1 if score > 0: candidates.append({ "name": name, "value": value, "score": score, "reason": reason, "suggested_min": round(value * 0.5, 3) if value > 0 else round(value * 1.5, 3), "suggested_max": round(value * 1.5, 3) if value > 0 else round(value * 0.5, 3), }) # Sort by score descending candidates.sort(key=lambda x: (-x["score"], x["name"])) return candidates # Import for type hints from typing import TYPE_CHECKING if TYPE_CHECKING: from .interview_state import InterviewState