""" batch_parser.py Parse multiple NX Nastran cases in batch AtomizerField Batch Parser v1.0.0 Efficiently processes multiple FEA cases for neural network training dataset creation. Usage: python batch_parser.py Example: python batch_parser.py ./training_data Directory structure expected: training_data/ ├── case_001/ │ ├── input/model.bdf │ └── output/model.op2 ├── case_002/ │ ├── input/model.bdf │ └── output/model.op2 └── ... """ import os import sys import json from pathlib import Path from datetime import datetime import traceback from neural_field_parser import NastranToNeuralFieldParser from validate_parsed_data import NeuralFieldDataValidator class BatchParser: """ Batch parser for processing multiple FEA cases This enables rapid dataset creation for neural network training. Processes each case sequentially and generates a summary report. """ def __init__(self, root_directory, validate=True, continue_on_error=True): """ Initialize batch parser Args: root_directory (str or Path): Root directory containing case subdirectories validate (bool): Run validation after parsing each case continue_on_error (bool): Continue processing if a case fails """ self.root_dir = Path(root_directory) self.validate = validate self.continue_on_error = continue_on_error self.results = [] def find_cases(self): """ Find all case directories in root directory A valid case directory contains: - input/ subdirectory with .bdf or .dat file - output/ subdirectory with .op2 file Returns: list: List of Path objects for valid case directories """ cases = [] for item in self.root_dir.iterdir(): if not item.is_dir(): continue # Check for required subdirectories and files input_dir = item / "input" output_dir = item / "output" if not input_dir.exists() or not output_dir.exists(): continue # Check for BDF file bdf_files = list(input_dir.glob("*.bdf")) + list(input_dir.glob("*.dat")) if not bdf_files: continue # Check for OP2 file op2_files = list(output_dir.glob("*.op2")) if not op2_files: continue cases.append(item) return sorted(cases) def parse_case(self, case_dir): """ Parse a single case Args: case_dir (Path): Path to case directory Returns: dict: Result dictionary with status and metadata """ result = { "case": case_dir.name, "status": "unknown", "timestamp": datetime.now().isoformat() } try: print(f"\n{'='*60}") print(f"Processing: {case_dir.name}") print(f"{'='*60}") # Parse parser = NastranToNeuralFieldParser(case_dir) data = parser.parse_all() result["status"] = "parsed" result["nodes"] = data["mesh"]["statistics"]["n_nodes"] result["elements"] = data["mesh"]["statistics"]["n_elements"] # Get max displacement and stress if available if "displacement" in data.get("results", {}): result["max_displacement"] = data["results"]["displacement"].get("max_translation") if "stress" in data.get("results", {}): for stress_type, stress_data in data["results"]["stress"].items(): if "max_von_mises" in stress_data and stress_data["max_von_mises"] is not None: result["max_stress"] = stress_data["max_von_mises"] break # Validate if requested if self.validate: print(f"\nValidating {case_dir.name}...") validator = NeuralFieldDataValidator(case_dir) validation_passed = validator.validate() result["validated"] = validation_passed if validation_passed: result["status"] = "success" else: result["status"] = "validation_failed" result["message"] = "Validation failed (see output above)" else: result["status"] = "success" except Exception as e: result["status"] = "failed" result["error"] = str(e) result["traceback"] = traceback.format_exc() print(f"\n✗ ERROR: {e}") if not self.continue_on_error: raise return result def batch_parse(self): """ Parse all cases in root directory Returns: list: List of result dictionaries """ print("\n" + "="*60) print("AtomizerField Batch Parser v1.0") print("="*60) print(f"\nRoot directory: {self.root_dir}") # Find all cases cases = self.find_cases() if not cases: print(f"\n✗ No valid cases found in {self.root_dir}") print("\nCase directories should contain:") print(" input/model.bdf (or model.dat)") print(" output/model.op2") return [] print(f"\nFound {len(cases)} case(s) to process:") for case in cases: print(f" - {case.name}") # Process each case self.results = [] start_time = datetime.now() for i, case in enumerate(cases, 1): print(f"\n[{i}/{len(cases)}] Processing {case.name}...") result = self.parse_case(case) self.results.append(result) # Show progress success_count = sum(1 for r in self.results if r["status"] == "success") print(f"\nProgress: {i}/{len(cases)} processed, {success_count} successful") end_time = datetime.now() elapsed = (end_time - start_time).total_seconds() # Print summary self._print_summary(elapsed) # Save summary to JSON self._save_summary() return self.results def _print_summary(self, elapsed_time): """Print batch processing summary""" print("\n" + "="*60) print("BATCH PROCESSING COMPLETE") print("="*60) success_count = sum(1 for r in self.results if r["status"] == "success") failed_count = sum(1 for r in self.results if r["status"] == "failed") validation_failed = sum(1 for r in self.results if r["status"] == "validation_failed") print(f"\nTotal cases: {len(self.results)}") print(f" ✓ Successful: {success_count}") if validation_failed > 0: print(f" ⚠ Validation failed: {validation_failed}") if failed_count > 0: print(f" ✗ Failed: {failed_count}") print(f"\nProcessing time: {elapsed_time:.1f} seconds") if len(self.results) > 0: print(f"Average time per case: {elapsed_time/len(self.results):.1f} seconds") # Detailed results print("\nDetailed Results:") print("-" * 60) for result in self.results: status_symbol = { "success": "✓", "failed": "✗", "validation_failed": "⚠", "parsed": "•" }.get(result["status"], "?") case_info = f"{status_symbol} {result['case']}: {result['status']}" if "nodes" in result and "elements" in result: case_info += f" ({result['nodes']:,} nodes, {result['elements']:,} elements)" if "max_stress" in result: case_info += f" | Max VM: {result['max_stress']:.2f} MPa" if result["status"] == "failed" and "error" in result: case_info += f"\n Error: {result['error']}" print(f" {case_info}") print("\n" + "="*60) if success_count == len(self.results): print("✓ ALL CASES PROCESSED SUCCESSFULLY") elif success_count > 0: print(f"⚠ {success_count}/{len(self.results)} CASES SUCCESSFUL") else: print("✗ ALL CASES FAILED") print("="*60 + "\n") def _save_summary(self): """Save batch processing summary to JSON file""" summary_file = self.root_dir / "batch_processing_summary.json" summary = { "batch_info": { "root_directory": str(self.root_dir), "timestamp": datetime.now().isoformat(), "total_cases": len(self.results), "successful_cases": sum(1 for r in self.results if r["status"] == "success"), "failed_cases": sum(1 for r in self.results if r["status"] == "failed"), "validation_enabled": self.validate }, "cases": self.results } with open(summary_file, 'w') as f: json.dump(summary, f, indent=2) print(f"Summary saved to: {summary_file}\n") def main(): """ Main entry point for batch parser """ if len(sys.argv) < 2: print("\nAtomizerField Batch Parser v1.0") print("="*60) print("\nUsage:") print(" python batch_parser.py [options]") print("\nOptions:") print(" --no-validate Skip validation step") print(" --stop-on-error Stop processing if a case fails") print("\nExample:") print(" python batch_parser.py ./training_data") print("\nDirectory structure:") print(" training_data/") print(" ├── case_001/") print(" │ ├── input/model.bdf") print(" │ └── output/model.op2") print(" ├── case_002/") print(" │ ├── input/model.bdf") print(" │ └── output/model.op2") print(" └── ...") print() sys.exit(1) root_dir = sys.argv[1] # Parse options validate = "--no-validate" not in sys.argv continue_on_error = "--stop-on-error" not in sys.argv if not Path(root_dir).exists(): print(f"ERROR: Directory not found: {root_dir}") sys.exit(1) # Create batch parser batch_parser = BatchParser( root_dir, validate=validate, continue_on_error=continue_on_error ) # Process all cases try: results = batch_parser.batch_parse() # Exit with appropriate code if not results: sys.exit(1) success_count = sum(1 for r in results if r["status"] == "success") if success_count == len(results): sys.exit(0) # All successful elif success_count > 0: sys.exit(2) # Partial success else: sys.exit(1) # All failed except KeyboardInterrupt: print("\n\nBatch processing interrupted by user") sys.exit(130) except Exception as e: print(f"\n\nFATAL ERROR: {e}") traceback.print_exc() sys.exit(1) if __name__ == "__main__": main()