""" neural_field_parser.py Parses NX Nastran files into Neural Field training data AtomizerField Data Parser v1.0.0 Converts NX Nastran BDF/OP2 files into standardized neural field training format. This format is designed to be future-proof for years of neural network training. Usage: python neural_field_parser.py Example: python neural_field_parser.py training_case_001 """ import json import numpy as np import h5py from pathlib import Path from datetime import datetime import hashlib import warnings # pyNastran imports try: from pyNastran.bdf.bdf import BDF from pyNastran.op2.op2 import OP2 except ImportError: print("ERROR: pyNastran is required. Install with: pip install pyNastran") raise class NastranToNeuralFieldParser: """ Parses Nastran BDF/OP2 files into Neural Field data structure v1.0 This parser extracts complete field data (stress, displacement, strain at every node/element) rather than just scalar maximum values. This enables neural networks to learn complete physics fields for 1000x faster structural optimization. Data Structure v1.0: ------------------- - metadata: Version, timestamps, analysis info, units - mesh: Complete node coordinates, element connectivity - materials: Full material properties (E, nu, rho, etc.) - boundary_conditions: All constraints (SPC, MPC) - loads: All loading conditions (forces, pressures, gravity, thermal) - results: Complete field results (displacement, stress, strain at ALL points) Attributes: case_dir (Path): Directory containing input/output subdirectories bdf_file (Path): Path to Nastran input deck (.bdf or .dat) op2_file (Path): Path to Nastran binary results (.op2) bdf (BDF): pyNastran BDF reader object op2 (OP2): pyNastran OP2 reader object neural_field_data (dict): Complete parsed data structure """ def __init__(self, case_directory): """ Initialize parser with case directory Args: case_directory (str or Path): Path to case directory containing: - input/model.bdf (or model.dat) - output/model.op2 """ self.case_dir = Path(case_directory) # Find BDF file (try both .bdf and .dat extensions) bdf_candidates = list((self.case_dir / "input").glob("model.bdf")) + \ list((self.case_dir / "input").glob("model.dat")) if not bdf_candidates: raise FileNotFoundError( f"No model.bdf or model.dat found in {self.case_dir / 'input'}/" ) self.bdf_file = bdf_candidates[0] # Find OP2 file op2_candidates = list((self.case_dir / "output").glob("model.op2")) if not op2_candidates: raise FileNotFoundError( f"No model.op2 found in {self.case_dir / 'output'}/" ) self.op2_file = op2_candidates[0] print(f"Found BDF: {self.bdf_file.name}") print(f"Found OP2: {self.op2_file.name}") # Initialize readers with minimal debug output self.bdf = BDF(debug=False) self.op2 = OP2(debug=False) # Initialize data structure v1.0 self.neural_field_data = { "metadata": {}, "geometry": {}, "mesh": {}, "materials": {}, "boundary_conditions": {}, "loads": {}, "results": {} } def parse_all(self): """ Main parsing function - extracts all data from BDF/OP2 files Returns: dict: Complete neural field data structure """ print("\n" + "="*60) print("Starting AtomizerField Neural Field Parser v1.0") print("="*60) # Parse input deck print("\n[1/6] Reading BDF file...") self.bdf.read_bdf(str(self.bdf_file)) print(f" Loaded {len(self.bdf.nodes)} nodes, {len(self.bdf.elements)} elements") # Parse results print("\n[2/6] Reading OP2 file...") self.op2.read_op2(str(self.op2_file)) # Check for sol attribute (may not exist in all pyNastran versions) sol_num = getattr(self.op2, 'sol', 'Unknown') print(f" Loaded solution: SOL {sol_num}") # Extract all data print("\n[3/6] Extracting metadata...") self.extract_metadata() print("\n[4/6] Extracting mesh data...") self.extract_mesh() print("\n[5/6] Extracting materials, BCs, and loads...") self.extract_materials() self.extract_boundary_conditions() self.extract_loads() print("\n[6/6] Extracting field results...") self.extract_results() # Save to file print("\nSaving data to disk...") self.save_data() print("\n" + "="*60) print("Parsing complete! [OK]") print("="*60) return self.neural_field_data def extract_metadata(self): """ Extract metadata and analysis information This includes: - Data format version (v1.0.0) - Timestamps - Analysis type (SOL 101, 103, etc.) - Units system - File hashes for provenance """ # Generate file hash for data provenance with open(self.bdf_file, 'rb') as f: bdf_hash = hashlib.sha256(f.read()).hexdigest() with open(self.op2_file, 'rb') as f: op2_hash = hashlib.sha256(f.read()).hexdigest() # Extract title if available title = "" if hasattr(self.bdf, 'case_control_deck') and self.bdf.case_control_deck: if hasattr(self.bdf.case_control_deck, 'title'): title = str(self.bdf.case_control_deck.title) # Get solution type if available sol_num = getattr(self.op2, 'sol', 'Unknown') self.neural_field_data["metadata"] = { "version": "1.0.0", "created_at": datetime.now().isoformat(), "source": "NX_Nastran", "case_directory": str(self.case_dir), "case_name": self.case_dir.name, "analysis_type": f"SOL_{sol_num}", "title": title, "file_hashes": { "bdf": bdf_hash, "op2": op2_hash }, "units": { "length": "mm", # Standard NX units "force": "N", "stress": "MPa", "mass": "kg", "temperature": "C" }, "parser_version": "1.0.0", "notes": "Complete field data for neural network training" } print(f" Analysis: {self.neural_field_data['metadata']['analysis_type']}") def extract_mesh(self): """ Extract complete mesh data from BDF This preserves: - All node coordinates (global coordinate system) - All element connectivity - Element types (solid, shell, beam, rigid) - Material and property IDs for each element """ print(" Extracting nodes...") # Nodes - store in sorted order for consistent indexing nodes = [] node_ids = [] for nid, node in sorted(self.bdf.nodes.items()): node_ids.append(nid) # Get position in global coordinate system pos = node.get_position() nodes.append([pos[0], pos[1], pos[2]]) nodes_array = np.array(nodes, dtype=np.float64) print(f" Extracted {len(nodes)} nodes") print(f" Extracting elements...") # Elements - organize by type for efficient neural network processing element_data = { "solid": [], "shell": [], "beam": [], "rigid": [] } element_type_counts = {} for eid, elem in sorted(self.bdf.elements.items()): elem_type = elem.type element_type_counts[elem_type] = element_type_counts.get(elem_type, 0) + 1 # Solid elements (3D stress states) if elem_type in ['CTETRA', 'CHEXA', 'CPENTA', 'CTETRA10', 'CHEXA20', 'CPENTA15']: element_data["solid"].append({ "id": eid, "type": elem_type, "nodes": list(elem.node_ids), "material_id": elem.pid, # Property ID which links to material "property_id": elem.pid if hasattr(elem, 'pid') else None }) # Shell elements (2D plane stress) elif elem_type in ['CQUAD4', 'CTRIA3', 'CQUAD8', 'CTRIA6', 'CQUAD', 'CTRIA']: thickness = None try: # Get thickness from property if hasattr(elem, 'pid') and elem.pid in self.bdf.properties: prop = self.bdf.properties[elem.pid] if hasattr(prop, 't'): thickness = prop.t except: pass element_data["shell"].append({ "id": eid, "type": elem_type, "nodes": list(elem.node_ids), "material_id": elem.pid, "property_id": elem.pid, "thickness": thickness }) # Beam elements (1D elements) elif elem_type in ['CBAR', 'CBEAM', 'CROD', 'CONROD']: element_data["beam"].append({ "id": eid, "type": elem_type, "nodes": list(elem.node_ids), "material_id": elem.pid if hasattr(elem, 'pid') else None, "property_id": elem.pid if hasattr(elem, 'pid') else None }) # Rigid elements (kinematic constraints) elif elem_type in ['RBE2', 'RBE3', 'RBAR', 'RROD']: element_data["rigid"].append({ "id": eid, "type": elem_type, "nodes": list(elem.node_ids) }) print(f" Extracted {len(self.bdf.elements)} elements:") for etype, count in element_type_counts.items(): print(f" {etype}: {count}") # Calculate mesh bounding box for reference bbox_min = nodes_array.min(axis=0) bbox_max = nodes_array.max(axis=0) bbox_size = bbox_max - bbox_min # Store mesh data self.neural_field_data["mesh"] = { "statistics": { "n_nodes": len(nodes), "n_elements": len(self.bdf.elements), "element_types": { "solid": len(element_data["solid"]), "shell": len(element_data["shell"]), "beam": len(element_data["beam"]), "rigid": len(element_data["rigid"]) }, "element_type_breakdown": element_type_counts }, "bounding_box": { "min": bbox_min.tolist(), "max": bbox_max.tolist(), "size": bbox_size.tolist() }, "nodes": { "ids": node_ids, "coordinates": nodes_array.tolist(), # Will be stored in HDF5 "shape": list(nodes_array.shape), "dtype": str(nodes_array.dtype) }, "elements": element_data } def extract_materials(self): """ Extract all material properties Captures complete material definitions including: - Isotropic (MAT1): E, nu, rho, G, alpha - Orthotropic (MAT8, MAT9): directional properties - Stress limits for validation """ print(" Extracting materials...") materials = [] for mid, mat in sorted(self.bdf.materials.items()): mat_data = { "id": mid, "type": mat.type } if mat.type == 'MAT1': # Isotropic material mat_data.update({ "E": float(mat.e) if mat.e is not None else None, # Young's modulus "nu": float(mat.nu) if mat.nu is not None else None, # Poisson's ratio "rho": float(mat.rho) if mat.rho is not None else None,# Density "G": float(mat.g) if mat.g is not None else None, # Shear modulus "alpha": float(mat.a) if hasattr(mat, 'a') and mat.a is not None else None, # Thermal expansion "tref": float(mat.tref) if hasattr(mat, 'tref') and mat.tref is not None else None, }) # Stress limits (if defined) try: if hasattr(mat, 'St') and callable(mat.St): mat_data["ST"] = float(mat.St()) if mat.St() is not None else None if hasattr(mat, 'Sc') and callable(mat.Sc): mat_data["SC"] = float(mat.Sc()) if mat.Sc() is not None else None if hasattr(mat, 'Ss') and callable(mat.Ss): mat_data["SS"] = float(mat.Ss()) if mat.Ss() is not None else None except: pass elif mat.type == 'MAT8': # Orthotropic shell material mat_data.update({ "E1": float(mat.e11) if hasattr(mat, 'e11') and mat.e11 is not None else None, "E2": float(mat.e22) if hasattr(mat, 'e22') and mat.e22 is not None else None, "nu12": float(mat.nu12) if hasattr(mat, 'nu12') and mat.nu12 is not None else None, "G12": float(mat.g12) if hasattr(mat, 'g12') and mat.g12 is not None else None, "G1z": float(mat.g1z) if hasattr(mat, 'g1z') and mat.g1z is not None else None, "G2z": float(mat.g2z) if hasattr(mat, 'g2z') and mat.g2z is not None else None, "rho": float(mat.rho) if hasattr(mat, 'rho') and mat.rho is not None else None, }) materials.append(mat_data) self.neural_field_data["materials"] = materials print(f" Extracted {len(materials)} materials") def extract_boundary_conditions(self): """ Extract all boundary conditions Includes: - SPC: Single point constraints (fixed DOFs) - MPC: Multi-point constraints (equations) - SUPORT: Free body supports """ print(" Extracting boundary conditions...") bcs = { "spc": [], # Single point constraints "mpc": [], # Multi-point constraints "suport": [] # Free body supports } # SPC (fixed DOFs) - critical for neural network to understand support conditions spc_count = 0 for spc_id, spc_list in self.bdf.spcs.items(): for spc in spc_list: try: # Handle different SPC types if hasattr(spc, 'node_ids'): nodes = spc.node_ids elif hasattr(spc, 'node'): nodes = [spc.node] else: continue for node in nodes: bcs["spc"].append({ "id": spc_id, "node": int(node), "dofs": str(spc.components) if hasattr(spc, 'components') else "123456", "enforced_motion": float(spc.enforced) if hasattr(spc, 'enforced') and spc.enforced is not None else 0.0 }) spc_count += 1 except Exception as e: warnings.warn(f"Could not parse SPC {spc_id}: {e}") # MPC equations mpc_count = 0 for mpc_id, mpc_list in self.bdf.mpcs.items(): for mpc in mpc_list: try: bcs["mpc"].append({ "id": mpc_id, "nodes": list(mpc.node_ids) if hasattr(mpc, 'node_ids') else [], "coefficients": list(mpc.coefficients) if hasattr(mpc, 'coefficients') else [], "components": list(mpc.components) if hasattr(mpc, 'components') else [] }) mpc_count += 1 except Exception as e: warnings.warn(f"Could not parse MPC {mpc_id}: {e}") self.neural_field_data["boundary_conditions"] = bcs print(f" Extracted {spc_count} SPCs, {mpc_count} MPCs") def extract_loads(self): """ Extract all loading conditions Includes: - Point forces and moments - Distributed pressures - Gravity loads - Thermal loads """ print(" Extracting loads...") loads = { "point_forces": [], "pressure": [], "gravity": [], "thermal": [] } force_count = 0 pressure_count = 0 gravity_count = 0 # Point forces, moments, and pressures for load_id, load_list in self.bdf.loads.items(): for load in load_list: try: if load.type == 'FORCE': loads["point_forces"].append({ "id": load_id, "type": "force", "node": int(load.node), "magnitude": float(load.mag), "direction": [float(load.xyz[0]), float(load.xyz[1]), float(load.xyz[2])], "coord_system": int(load.cid) if hasattr(load, 'cid') else 0 }) force_count += 1 elif load.type == 'MOMENT': loads["point_forces"].append({ "id": load_id, "type": "moment", "node": int(load.node), "magnitude": float(load.mag), "direction": [float(load.xyz[0]), float(load.xyz[1]), float(load.xyz[2])], "coord_system": int(load.cid) if hasattr(load, 'cid') else 0 }) force_count += 1 elif load.type in ['PLOAD', 'PLOAD2', 'PLOAD4']: pressure_data = { "id": load_id, "type": load.type } if hasattr(load, 'eids'): pressure_data["elements"] = list(load.eids) elif hasattr(load, 'eid'): pressure_data["elements"] = [int(load.eid)] if hasattr(load, 'pressures'): pressure_data["pressure"] = [float(p) for p in load.pressures] elif hasattr(load, 'pressure'): pressure_data["pressure"] = float(load.pressure) loads["pressure"].append(pressure_data) pressure_count += 1 elif load.type == 'GRAV': loads["gravity"].append({ "id": load_id, "acceleration": float(load.scale), "direction": [float(load.N[0]), float(load.N[1]), float(load.N[2])], "coord_system": int(load.cid) if hasattr(load, 'cid') else 0 }) gravity_count += 1 except Exception as e: warnings.warn(f"Could not parse load {load_id} type {load.type}: {e}") # Temperature loads (if available) thermal_count = 0 if hasattr(self.bdf, 'temps'): for temp_id, temp_list in self.bdf.temps.items(): for temp in temp_list: try: loads["thermal"].append({ "id": temp_id, "node": int(temp.node), "temperature": float(temp.temperature) }) thermal_count += 1 except Exception as e: warnings.warn(f"Could not parse thermal load {temp_id}: {e}") self.neural_field_data["loads"] = loads print(f" Extracted {force_count} forces, {pressure_count} pressures, {gravity_count} gravity, {thermal_count} thermal") def extract_results(self): """ Extract complete field results from OP2 This is the CRITICAL function for neural field learning. We extract COMPLETE fields, not just maximum values: - Displacement at every node (6 DOF) - Stress at every element (full tensor) - Strain at every element (full tensor) - Reaction forces at constrained nodes This complete field data enables the neural network to learn the physics of how structures respond to loads. """ print(" Extracting field results...") results = {} # Determine subcase ID (usually 1 for linear static) subcase_id = 1 if hasattr(self.op2, 'isubcase_name_map'): available_subcases = list(self.op2.isubcase_name_map.keys()) if available_subcases: subcase_id = available_subcases[0] print(f" Using subcase ID: {subcase_id}") # Displacement - complete field at all nodes if hasattr(self.op2, 'displacements') and subcase_id in self.op2.displacements: print(" Processing displacement field...") disp = self.op2.displacements[subcase_id] disp_data = disp.data[0, :, :] # [itime=0, all_nodes, 6_dofs] # Extract node IDs node_ids = disp.node_gridtype[:, 0].tolist() # Calculate magnitudes for quick reference translation_mag = np.linalg.norm(disp_data[:, :3], axis=1) rotation_mag = np.linalg.norm(disp_data[:, 3:], axis=1) results["displacement"] = { "node_ids": node_ids, "data": disp_data.tolist(), # Will be stored in HDF5 "shape": list(disp_data.shape), "dtype": str(disp_data.dtype), "max_translation": float(np.max(translation_mag)), "max_rotation": float(np.max(rotation_mag)), "units": "mm and radians" } print(f" Displacement: {len(node_ids)} nodes, max={results['displacement']['max_translation']:.6f} mm") # Stress - handle different element types stress_results = {} # Solid element stress (CTETRA, CHEXA, etc.) stress_attrs = ['ctetra_stress', 'chexa_stress', 'cpenta_stress'] for attr in stress_attrs: if hasattr(self.op2, attr): stress_obj = getattr(self.op2, attr) if subcase_id in stress_obj: elem_type = attr.replace('_stress', '') print(f" Processing {elem_type} stress...") stress = stress_obj[subcase_id] stress_data = stress.data[0, :, :] # Extract element IDs element_ids = stress.element_node[:, 0].tolist() # Von Mises stress is usually the last column von_mises = None if stress_data.shape[1] >= 7: # Has von Mises von_mises = stress_data[:, -1] max_vm = float(np.max(von_mises)) von_mises = von_mises.tolist() else: max_vm = None stress_results[f"{elem_type}_stress"] = { "element_ids": element_ids, "data": stress_data.tolist(), # Full stress tensor "shape": list(stress_data.shape), "dtype": str(stress_data.dtype), "von_mises": von_mises, "max_von_mises": max_vm, "units": "MPa" } print(f" {elem_type}: {len(element_ids)} elements, max VM={max_vm:.2f} MPa" if max_vm else f" {elem_type}: {len(element_ids)} elements") # Shell element stress shell_stress_attrs = ['cquad4_stress', 'ctria3_stress', 'cquad8_stress', 'ctria6_stress'] for attr in shell_stress_attrs: if hasattr(self.op2, attr): stress_obj = getattr(self.op2, attr) if subcase_id in stress_obj: elem_type = attr.replace('_stress', '') print(f" Processing {elem_type} stress...") stress = stress_obj[subcase_id] stress_data = stress.data[0, :, :] element_ids = stress.element_node[:, 0].tolist() stress_results[f"{elem_type}_stress"] = { "element_ids": element_ids, "data": stress_data.tolist(), "shape": list(stress_data.shape), "dtype": str(stress_data.dtype), "units": "MPa" } print(f" {elem_type}: {len(element_ids)} elements") results["stress"] = stress_results # Strain - similar to stress strain_results = {} strain_attrs = ['ctetra_strain', 'chexa_strain', 'cpenta_strain', 'cquad4_strain', 'ctria3_strain'] for attr in strain_attrs: if hasattr(self.op2, attr): strain_obj = getattr(self.op2, attr) if subcase_id in strain_obj: elem_type = attr.replace('_strain', '') strain = strain_obj[subcase_id] strain_data = strain.data[0, :, :] element_ids = strain.element_node[:, 0].tolist() strain_results[f"{elem_type}_strain"] = { "element_ids": element_ids, "data": strain_data.tolist(), "shape": list(strain_data.shape), "dtype": str(strain_data.dtype), "units": "mm/mm" } if strain_results: results["strain"] = strain_results print(f" Extracted strain for {len(strain_results)} element types") # SPC Forces (reaction forces at constraints) if hasattr(self.op2, 'spc_forces') and subcase_id in self.op2.spc_forces: print(" Processing reaction forces...") spc = self.op2.spc_forces[subcase_id] spc_data = spc.data[0, :, :] node_ids = spc.node_gridtype[:, 0].tolist() # Calculate total reaction force magnitude force_mag = np.linalg.norm(spc_data[:, :3], axis=1) moment_mag = np.linalg.norm(spc_data[:, 3:], axis=1) results["reactions"] = { "node_ids": node_ids, "forces": spc_data.tolist(), "shape": list(spc_data.shape), "dtype": str(spc_data.dtype), "max_force": float(np.max(force_mag)), "max_moment": float(np.max(moment_mag)), "units": "N and N-mm" } print(f" Reactions: {len(node_ids)} nodes, max force={results['reactions']['max_force']:.2f} N") self.neural_field_data["results"] = results def save_data(self): """ Save parsed data to JSON and HDF5 files Data structure: - neural_field_data.json: Metadata, structure, small arrays - neural_field_data.h5: Large arrays (node coordinates, field results) HDF5 is used for efficient storage and loading of large numerical arrays. JSON provides human-readable metadata and structure. """ # Save JSON metadata json_file = self.case_dir / "neural_field_data.json" # Create a copy for JSON (will remove large arrays) json_data = self._prepare_json_data() with open(json_file, 'w') as f: json.dump(json_data, f, indent=2, default=str) print(f" [OK] Saved metadata to: {json_file.name}") # Save HDF5 for large arrays h5_file = self.case_dir / "neural_field_data.h5" with h5py.File(h5_file, 'w') as f: # Metadata attributes f.attrs['version'] = '1.0.0' f.attrs['created_at'] = self.neural_field_data['metadata']['created_at'] f.attrs['case_name'] = self.neural_field_data['metadata']['case_name'] # Save mesh data mesh_grp = f.create_group('mesh') node_coords = np.array(self.neural_field_data["mesh"]["nodes"]["coordinates"]) mesh_grp.create_dataset('node_coordinates', data=node_coords, compression='gzip', compression_opts=4) mesh_grp.create_dataset('node_ids', data=np.array(self.neural_field_data["mesh"]["nodes"]["ids"])) # Save results if "results" in self.neural_field_data: results_grp = f.create_group('results') # Displacement if "displacement" in self.neural_field_data["results"]: disp_data = np.array(self.neural_field_data["results"]["displacement"]["data"]) results_grp.create_dataset('displacement', data=disp_data, compression='gzip', compression_opts=4) results_grp.create_dataset('displacement_node_ids', data=np.array(self.neural_field_data["results"]["displacement"]["node_ids"])) # Stress fields if "stress" in self.neural_field_data["results"]: stress_grp = results_grp.create_group('stress') for stress_type, stress_data in self.neural_field_data["results"]["stress"].items(): type_grp = stress_grp.create_group(stress_type) type_grp.create_dataset('data', data=np.array(stress_data["data"]), compression='gzip', compression_opts=4) type_grp.create_dataset('element_ids', data=np.array(stress_data["element_ids"])) # Strain fields if "strain" in self.neural_field_data["results"]: strain_grp = results_grp.create_group('strain') for strain_type, strain_data in self.neural_field_data["results"]["strain"].items(): type_grp = strain_grp.create_group(strain_type) type_grp.create_dataset('data', data=np.array(strain_data["data"]), compression='gzip', compression_opts=4) type_grp.create_dataset('element_ids', data=np.array(strain_data["element_ids"])) # Reactions if "reactions" in self.neural_field_data["results"]: reactions_data = np.array(self.neural_field_data["results"]["reactions"]["forces"]) results_grp.create_dataset('reactions', data=reactions_data, compression='gzip', compression_opts=4) results_grp.create_dataset('reaction_node_ids', data=np.array(self.neural_field_data["results"]["reactions"]["node_ids"])) print(f" [OK] Saved field data to: {h5_file.name}") # Calculate and display file sizes json_size = json_file.stat().st_size / 1024 # KB h5_size = h5_file.stat().st_size / 1024 # KB print(f"\n File sizes:") print(f" JSON: {json_size:.1f} KB") print(f" HDF5: {h5_size:.1f} KB") print(f" Total: {json_size + h5_size:.1f} KB") def _prepare_json_data(self): """ Prepare data for JSON export by removing large arrays (they go to HDF5 instead) """ import copy json_data = copy.deepcopy(self.neural_field_data) # Remove large arrays from nodes (keep metadata) if "mesh" in json_data and "nodes" in json_data["mesh"]: json_data["mesh"]["nodes"]["coordinates"] = f"" # Remove large result arrays if "results" in json_data: if "displacement" in json_data["results"]: shape = json_data["results"]["displacement"]["shape"] json_data["results"]["displacement"]["data"] = f"" if "stress" in json_data["results"]: for stress_type in json_data["results"]["stress"]: shape = json_data["results"]["stress"][stress_type]["shape"] json_data["results"]["stress"][stress_type]["data"] = f"" if "strain" in json_data["results"]: for strain_type in json_data["results"]["strain"]: shape = json_data["results"]["strain"][strain_type]["shape"] json_data["results"]["strain"][strain_type]["data"] = f"" if "reactions" in json_data["results"]: shape = json_data["results"]["reactions"]["shape"] json_data["results"]["reactions"]["forces"] = f"" return json_data # ============================================================================ # MAIN ENTRY POINT # ============================================================================ def main(): """ Main function to run the parser from command line Usage: python neural_field_parser.py """ import sys if len(sys.argv) < 2: print("\nAtomizerField Neural Field Parser v1.0") print("="*60) print("\nUsage:") print(" python neural_field_parser.py ") print("\nExample:") print(" python neural_field_parser.py training_case_001") print("\nCase directory should contain:") print(" input/model.bdf (or model.dat)") print(" output/model.op2") print("\n") sys.exit(1) case_dir = sys.argv[1] # Verify directory exists if not Path(case_dir).exists(): print(f"ERROR: Directory not found: {case_dir}") sys.exit(1) # Create parser try: parser = NastranToNeuralFieldParser(case_dir) except FileNotFoundError as e: print(f"\nERROR: {e}") print("\nPlease ensure your case directory contains:") print(" input/model.bdf (or model.dat)") print(" output/model.op2") sys.exit(1) # Parse all data try: data = parser.parse_all() # Print summary print("\n" + "="*60) print("PARSING SUMMARY") print("="*60) print(f"Case: {data['metadata']['case_name']}") print(f"Analysis: {data['metadata']['analysis_type']}") print(f"\nMesh:") print(f" Nodes: {data['mesh']['statistics']['n_nodes']:,}") print(f" Elements: {data['mesh']['statistics']['n_elements']:,}") for elem_type, count in data['mesh']['statistics']['element_types'].items(): if count > 0: print(f" {elem_type}: {count:,}") print(f"\nMaterials: {len(data['materials'])}") print(f"Boundary Conditions: {len(data['boundary_conditions']['spc'])} SPCs") print(f"Loads: {len(data['loads']['point_forces'])} forces, {len(data['loads']['pressure'])} pressures") if "displacement" in data['results']: print(f"\nResults:") print(f" Displacement: {len(data['results']['displacement']['node_ids'])} nodes") print(f" Max: {data['results']['displacement']['max_translation']:.6f} mm") if "stress" in data['results']: for stress_type in data['results']['stress']: if 'max_von_mises' in data['results']['stress'][stress_type]: max_vm = data['results']['stress'][stress_type]['max_von_mises'] if max_vm is not None: print(f" {stress_type}: Max VM = {max_vm:.2f} MPa") print("\n[OK] Data ready for neural network training!") print("="*60 + "\n") except Exception as e: print(f"\n" + "="*60) print("ERROR DURING PARSING") print("="*60) print(f"{e}\n") import traceback traceback.print_exc() sys.exit(1) if __name__ == "__main__": main()