333 lines
12 KiB
Python
333 lines
12 KiB
Python
|
|
"""
|
||
|
|
Step Classifier - Phase 2.6
|
||
|
|
|
||
|
|
Classifies workflow steps into:
|
||
|
|
1. Engineering Features - Complex FEA/CAE operations needing research/documentation
|
||
|
|
2. Inline Calculations - Simple math operations to generate on-the-fly
|
||
|
|
3. Post-Processing Hooks - Middleware scripts between engineering steps
|
||
|
|
|
||
|
|
Author: Atomizer Development Team
|
||
|
|
Version: 0.1.0 (Phase 2.6)
|
||
|
|
Last Updated: 2025-01-16
|
||
|
|
"""
|
||
|
|
|
||
|
|
from typing import Dict, List, Any, Optional
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from pathlib import Path
|
||
|
|
import re
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class StepClassification:
|
||
|
|
"""Classification result for a workflow step."""
|
||
|
|
step_type: str # 'engineering_feature', 'inline_calculation', 'post_processing_hook'
|
||
|
|
complexity: str # 'simple', 'moderate', 'complex'
|
||
|
|
requires_research: bool
|
||
|
|
requires_documentation: bool
|
||
|
|
auto_generate: bool
|
||
|
|
reasoning: str
|
||
|
|
|
||
|
|
|
||
|
|
class StepClassifier:
|
||
|
|
"""
|
||
|
|
Intelligently classifies workflow steps to determine if they need:
|
||
|
|
- Full feature engineering (FEA/CAE operations)
|
||
|
|
- Inline code generation (simple math)
|
||
|
|
- Post-processing hooks (middleware)
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
# Engineering operations that require research/documentation
|
||
|
|
self.engineering_operations = {
|
||
|
|
# FEA Result Extraction
|
||
|
|
'extract_result': ['displacement', 'stress', 'strain', 'reaction_force',
|
||
|
|
'element_force', 'temperature', 'modal', 'buckling'],
|
||
|
|
|
||
|
|
# FEA Property Modifications
|
||
|
|
'update_fea_property': ['cbush_stiffness', 'pcomp_layup', 'mat1_properties',
|
||
|
|
'pshell_thickness', 'pbeam_properties', 'contact_stiffness'],
|
||
|
|
|
||
|
|
# Geometry/CAD Operations
|
||
|
|
'modify_geometry': ['extrude', 'revolve', 'boolean', 'fillet', 'chamfer'],
|
||
|
|
'read_expression': ['part_expression', 'assembly_expression'],
|
||
|
|
|
||
|
|
# Simulation Setup
|
||
|
|
'run_analysis': ['sol101', 'sol103', 'sol106', 'sol111', 'sol400'],
|
||
|
|
'create_material': ['mat1', 'mat8', 'mat9', 'physical_material'],
|
||
|
|
'apply_loads': ['force', 'moment', 'pressure', 'thermal_load'],
|
||
|
|
'create_mesh': ['tetra', 'hex', 'shell', 'beam'],
|
||
|
|
}
|
||
|
|
|
||
|
|
# Simple mathematical operations (no feature needed)
|
||
|
|
self.simple_math_operations = {
|
||
|
|
'average', 'mean', 'max', 'maximum', 'min', 'minimum',
|
||
|
|
'sum', 'total', 'count', 'ratio', 'percentage',
|
||
|
|
'compare', 'difference', 'delta', 'absolute',
|
||
|
|
'normalize', 'scale', 'round', 'floor', 'ceil'
|
||
|
|
}
|
||
|
|
|
||
|
|
# Statistical operations (still simple, but slightly more complex)
|
||
|
|
self.statistical_operations = {
|
||
|
|
'std', 'stddev', 'variance', 'median', 'mode',
|
||
|
|
'percentile', 'quartile', 'range', 'iqr'
|
||
|
|
}
|
||
|
|
|
||
|
|
# Post-processing indicators
|
||
|
|
self.post_processing_indicators = {
|
||
|
|
'custom objective', 'metric', 'criteria', 'evaluation',
|
||
|
|
'transform', 'filter', 'aggregate', 'combine'
|
||
|
|
}
|
||
|
|
|
||
|
|
def classify_step(self, action: str, domain: str, params: Dict[str, Any],
|
||
|
|
request_context: str = "") -> StepClassification:
|
||
|
|
"""
|
||
|
|
Classify a workflow step into engineering feature, inline calc, or hook.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
action: The action type (e.g., 'extract_result', 'update_parameters')
|
||
|
|
domain: The domain (e.g., 'result_extraction', 'optimization')
|
||
|
|
params: Step parameters
|
||
|
|
request_context: Original user request for context
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
StepClassification with type and reasoning
|
||
|
|
"""
|
||
|
|
action_lower = action.lower()
|
||
|
|
request_lower = request_context.lower()
|
||
|
|
|
||
|
|
# Check for engineering operations
|
||
|
|
if self._is_engineering_operation(action, params):
|
||
|
|
return StepClassification(
|
||
|
|
step_type='engineering_feature',
|
||
|
|
complexity='complex',
|
||
|
|
requires_research=True,
|
||
|
|
requires_documentation=True,
|
||
|
|
auto_generate=False,
|
||
|
|
reasoning=f"FEA/CAE operation '{action}' requires specialized knowledge and documentation"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Check for simple mathematical calculations
|
||
|
|
if self._is_simple_calculation(action, params, request_lower):
|
||
|
|
return StepClassification(
|
||
|
|
step_type='inline_calculation',
|
||
|
|
complexity='simple',
|
||
|
|
requires_research=False,
|
||
|
|
requires_documentation=False,
|
||
|
|
auto_generate=True,
|
||
|
|
reasoning=f"Simple mathematical operation that can be generated inline"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Check for post-processing hooks
|
||
|
|
if self._is_post_processing_hook(action, params, request_lower):
|
||
|
|
return StepClassification(
|
||
|
|
step_type='post_processing_hook',
|
||
|
|
complexity='moderate',
|
||
|
|
requires_research=False,
|
||
|
|
requires_documentation=False,
|
||
|
|
auto_generate=True,
|
||
|
|
reasoning=f"Post-processing calculation between FEA steps"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Check if it's a known simple action
|
||
|
|
if action in ['identify_parameters', 'update_parameters', 'optimize']:
|
||
|
|
return StepClassification(
|
||
|
|
step_type='engineering_feature',
|
||
|
|
complexity='moderate',
|
||
|
|
requires_research=False, # May already exist
|
||
|
|
requires_documentation=True,
|
||
|
|
auto_generate=False,
|
||
|
|
reasoning=f"Standard optimization workflow step"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Default: treat as engineering feature to be safe
|
||
|
|
return StepClassification(
|
||
|
|
step_type='engineering_feature',
|
||
|
|
complexity='moderate',
|
||
|
|
requires_research=True,
|
||
|
|
requires_documentation=True,
|
||
|
|
auto_generate=False,
|
||
|
|
reasoning=f"Unknown action type, treating as engineering feature"
|
||
|
|
)
|
||
|
|
|
||
|
|
def _is_engineering_operation(self, action: str, params: Dict[str, Any]) -> bool:
|
||
|
|
"""Check if this is a complex engineering operation."""
|
||
|
|
# Check action type
|
||
|
|
if action in self.engineering_operations:
|
||
|
|
return True
|
||
|
|
|
||
|
|
# Check for FEA-specific parameters
|
||
|
|
fea_indicators = [
|
||
|
|
'result_type', 'solver', 'element_type', 'material_type',
|
||
|
|
'mesh_type', 'load_type', 'subcase', 'solution'
|
||
|
|
]
|
||
|
|
|
||
|
|
for indicator in fea_indicators:
|
||
|
|
if indicator in params:
|
||
|
|
return True
|
||
|
|
|
||
|
|
# Check for specific result types that need FEA extraction
|
||
|
|
if 'result_type' in params:
|
||
|
|
result_type = params['result_type']
|
||
|
|
engineering_results = ['displacement', 'stress', 'strain', 'reaction_force',
|
||
|
|
'element_force', 'temperature', 'modal', 'buckling']
|
||
|
|
if result_type in engineering_results:
|
||
|
|
return True
|
||
|
|
|
||
|
|
return False
|
||
|
|
|
||
|
|
def _is_simple_calculation(self, action: str, params: Dict[str, Any],
|
||
|
|
request_context: str) -> bool:
|
||
|
|
"""Check if this is a simple mathematical calculation."""
|
||
|
|
# Check for math keywords in action
|
||
|
|
action_words = set(action.lower().split('_'))
|
||
|
|
if action_words & self.simple_math_operations:
|
||
|
|
return True
|
||
|
|
|
||
|
|
# Check for statistical operations
|
||
|
|
if action_words & self.statistical_operations:
|
||
|
|
return True
|
||
|
|
|
||
|
|
# Check for calculation keywords in request
|
||
|
|
calc_patterns = [
|
||
|
|
r'\b(calculate|compute|find)\s+(average|mean|max|min|sum)\b',
|
||
|
|
r'\b(average|mean)\s+of\b',
|
||
|
|
r'\bfind\s+the\s+(maximum|minimum)\b',
|
||
|
|
r'\bcompare\s+.+\s+to\s+',
|
||
|
|
]
|
||
|
|
|
||
|
|
for pattern in calc_patterns:
|
||
|
|
if re.search(pattern, request_context):
|
||
|
|
return True
|
||
|
|
|
||
|
|
return False
|
||
|
|
|
||
|
|
def _is_post_processing_hook(self, action: str, params: Dict[str, Any],
|
||
|
|
request_context: str) -> bool:
|
||
|
|
"""Check if this is a post-processing hook between steps."""
|
||
|
|
# Look for custom objective/metric definitions
|
||
|
|
for indicator in self.post_processing_indicators:
|
||
|
|
if indicator in request_context:
|
||
|
|
# Check if it involves multiple inputs (sign of post-processing)
|
||
|
|
if 'average' in request_context and 'maximum' in request_context:
|
||
|
|
return True
|
||
|
|
if 'compare' in request_context:
|
||
|
|
return True
|
||
|
|
if 'assign' in request_context and 'metric' in request_context:
|
||
|
|
return True
|
||
|
|
|
||
|
|
return False
|
||
|
|
|
||
|
|
def classify_workflow(self, workflow_steps: List[Any],
|
||
|
|
request_context: str = "") -> Dict[str, List[Any]]:
|
||
|
|
"""
|
||
|
|
Classify all steps in a workflow.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
{
|
||
|
|
'engineering_features': [...],
|
||
|
|
'inline_calculations': [...],
|
||
|
|
'post_processing_hooks': [...]
|
||
|
|
}
|
||
|
|
"""
|
||
|
|
classified = {
|
||
|
|
'engineering_features': [],
|
||
|
|
'inline_calculations': [],
|
||
|
|
'post_processing_hooks': []
|
||
|
|
}
|
||
|
|
|
||
|
|
for step in workflow_steps:
|
||
|
|
classification = self.classify_step(
|
||
|
|
step.action,
|
||
|
|
step.domain,
|
||
|
|
step.params,
|
||
|
|
request_context
|
||
|
|
)
|
||
|
|
|
||
|
|
step_with_classification = {
|
||
|
|
'step': step,
|
||
|
|
'classification': classification
|
||
|
|
}
|
||
|
|
|
||
|
|
if classification.step_type == 'engineering_feature':
|
||
|
|
classified['engineering_features'].append(step_with_classification)
|
||
|
|
elif classification.step_type == 'inline_calculation':
|
||
|
|
classified['inline_calculations'].append(step_with_classification)
|
||
|
|
elif classification.step_type == 'post_processing_hook':
|
||
|
|
classified['post_processing_hooks'].append(step_with_classification)
|
||
|
|
|
||
|
|
return classified
|
||
|
|
|
||
|
|
def get_summary(self, classified_workflow: Dict[str, List[Any]]) -> str:
|
||
|
|
"""Get human-readable summary of classification."""
|
||
|
|
lines = []
|
||
|
|
lines.append("Workflow Classification Summary")
|
||
|
|
lines.append("=" * 80)
|
||
|
|
lines.append("")
|
||
|
|
|
||
|
|
# Engineering features
|
||
|
|
eng_features = classified_workflow['engineering_features']
|
||
|
|
lines.append(f"Engineering Features (Need Research): {len(eng_features)}")
|
||
|
|
for item in eng_features:
|
||
|
|
step = item['step']
|
||
|
|
classification = item['classification']
|
||
|
|
lines.append(f" - {step.action} ({step.domain})")
|
||
|
|
lines.append(f" Reason: {classification.reasoning}")
|
||
|
|
|
||
|
|
lines.append("")
|
||
|
|
|
||
|
|
# Inline calculations
|
||
|
|
inline_calcs = classified_workflow['inline_calculations']
|
||
|
|
lines.append(f"Inline Calculations (Auto-Generate): {len(inline_calcs)}")
|
||
|
|
for item in inline_calcs:
|
||
|
|
step = item['step']
|
||
|
|
lines.append(f" - {step.action}: {step.params}")
|
||
|
|
|
||
|
|
lines.append("")
|
||
|
|
|
||
|
|
# Post-processing hooks
|
||
|
|
hooks = classified_workflow['post_processing_hooks']
|
||
|
|
lines.append(f"Post-Processing Hooks (Auto-Generate): {len(hooks)}")
|
||
|
|
for item in hooks:
|
||
|
|
step = item['step']
|
||
|
|
lines.append(f" - {step.action}: {step.params}")
|
||
|
|
|
||
|
|
return "\n".join(lines)
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
"""Test the step classifier."""
|
||
|
|
from optimization_engine.workflow_decomposer import WorkflowDecomposer
|
||
|
|
|
||
|
|
print("Step Classifier Test")
|
||
|
|
print("=" * 80)
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Test with CBUSH optimization request
|
||
|
|
request = """I want to extract forces in direction Z of all the 1D elements and find the average of it,
|
||
|
|
then find the maximum value and compare it to the average, then assign it to a objective metric that needs to be minimized."""
|
||
|
|
|
||
|
|
decomposer = WorkflowDecomposer()
|
||
|
|
classifier = StepClassifier()
|
||
|
|
|
||
|
|
print("Request:")
|
||
|
|
print(request)
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Decompose workflow
|
||
|
|
steps = decomposer.decompose(request)
|
||
|
|
|
||
|
|
print("Workflow Steps:")
|
||
|
|
for i, step in enumerate(steps, 1):
|
||
|
|
print(f"{i}. {step.action} ({step.domain})")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Classify steps
|
||
|
|
classified = classifier.classify_workflow(steps, request)
|
||
|
|
|
||
|
|
# Display summary
|
||
|
|
print(classifier.get_summary(classified))
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
main()
|