Files
Atomizer/optimization_engine/utils/nx_file_discovery.py
Antoine 8cbdbcad78 feat: Add Protocol 13 adaptive optimization, Plotly charts, and dashboard improvements
## Protocol 13: Adaptive Multi-Objective Optimization
- Iterative FEA + Neural Network surrogate workflow
- Initial FEA sampling, NN training, NN-accelerated search
- FEA validation of top NN predictions, retraining loop
- adaptive_state.json tracks iteration history and best values
- M1 mirror study (V11) with 103 FEA, 3000 NN trials

## Dashboard Visualization Enhancements
- Added Plotly.js interactive charts (parallel coords, Pareto, convergence)
- Lazy loading with React.lazy() for performance
- Code splitting: plotly.js-basic-dist (~1MB vs 3.5MB)
- Chart library toggle (Recharts default, Plotly on-demand)
- ExpandableChart component for full-screen modal views
- ConsoleOutput component for real-time log viewing

## Documentation
- Protocol 13 detailed documentation
- Dashboard visualization guide
- Plotly components README
- Updated run-optimization skill with Mode 5 (adaptive)

## Bug Fixes
- Fixed TypeScript errors in dashboard components
- Fixed Card component to accept ReactNode title
- Removed unused imports across components

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 07:41:54 -05:00

349 lines
11 KiB
Python

"""
NX File Discovery Utility
=========================
Discovers all files required by an NX simulation (.sim) file and provides
functions to copy them to a target directory.
For assembly FEM models, this includes:
- The .sim file itself
- The .afm (Assembly FEM) file
- The assembly .prt file
- All component .prt files
- All idealized part files (*_i.prt)
- All component .fem files
Usage:
from optimization_engine.utils.nx_file_discovery import (
discover_sim_dependencies,
copy_sim_with_dependencies
)
# Discover all required files
deps = discover_sim_dependencies("/path/to/source/model.sim")
print(f"Found {len(deps.all_files)} required files")
# Copy all files to target directory
result = copy_sim_with_dependencies(
sim_file="/path/to/source/model.sim",
target_dir="/path/to/target/model/"
)
"""
import os
import re
import shutil
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Set, Optional, Dict
@dataclass
class SimDependencies:
"""Represents all files required by a .sim file."""
sim_file: Path
source_dir: Path
# Core files
afm_file: Optional[Path] = None
assembly_prt: Optional[Path] = None
# Component files
component_prts: List[Path] = field(default_factory=list)
idealized_prts: List[Path] = field(default_factory=list)
fem_files: List[Path] = field(default_factory=list)
# Additional output files (optional, not always needed)
output_files: List[Path] = field(default_factory=list)
@property
def all_files(self) -> List[Path]:
"""Get list of all required files."""
files = [self.sim_file]
if self.afm_file:
files.append(self.afm_file)
if self.assembly_prt:
files.append(self.assembly_prt)
files.extend(self.component_prts)
files.extend(self.idealized_prts)
files.extend(self.fem_files)
return files
@property
def all_files_with_outputs(self) -> List[Path]:
"""Get all files including output files."""
return self.all_files + self.output_files
def __str__(self):
lines = [
f"Simulation Dependencies for: {self.sim_file.name}",
f"Source directory: {self.source_dir}",
"",
"Required files:"
]
if self.afm_file:
lines.append(f" [AFM] {self.afm_file.name}")
if self.assembly_prt:
lines.append(f" [ASSY] {self.assembly_prt.name}")
for prt in self.component_prts:
lines.append(f" [PRT] {prt.name}")
for prt in self.idealized_prts:
lines.append(f" [PRT_i] {prt.name}")
for fem in self.fem_files:
lines.append(f" [FEM] {fem.name}")
lines.append(f"\nTotal: {len(self.all_files)} required files")
return "\n".join(lines)
def discover_sim_dependencies(sim_file: Path,
include_outputs: bool = False) -> SimDependencies:
"""
Discover all files required by an NX simulation file.
Args:
sim_file: Path to the .sim file
include_outputs: Whether to include output files (.op2, .f06, etc.)
Returns:
SimDependencies object with all discovered files
"""
sim_file = Path(sim_file)
if not sim_file.exists():
raise FileNotFoundError(f"Simulation file not found: {sim_file}")
source_dir = sim_file.parent
sim_stem = sim_file.stem # e.g., "ASSY_M1_assyfem1_sim1"
deps = SimDependencies(sim_file=sim_file, source_dir=source_dir)
# Detect if this is an assembly FEM
is_assembly = '_assyfem' in sim_stem.lower()
if is_assembly:
_discover_assembly_deps(deps, sim_stem, source_dir)
else:
_discover_single_part_deps(deps, sim_stem, source_dir)
if include_outputs:
_discover_output_files(deps, sim_stem, source_dir)
return deps
def _discover_assembly_deps(deps: SimDependencies, sim_stem: str, source_dir: Path):
"""Discover dependencies for an assembly FEM."""
# Extract assembly name: ASSY_M1_assyfem1_sim1 -> ASSY_M1
match = re.match(r'^(.+?)_assyfem(\d+)', sim_stem, re.IGNORECASE)
if not match:
return
assembly_name = match.group(1) # e.g., "ASSY_M1"
afm_num = match.group(2) # e.g., "1"
# Look for the .afm file
afm_pattern = f"{assembly_name}_assyfem{afm_num}.afm"
afm_files = list(source_dir.glob(afm_pattern)) or \
[f for f in source_dir.glob("*.afm") if assembly_name.lower() in f.stem.lower()]
if afm_files:
deps.afm_file = afm_files[0]
# Look for the assembly .prt file
assy_prt = source_dir / f"{assembly_name}.prt"
if assy_prt.exists():
deps.assembly_prt = assy_prt
else:
# Try case-insensitive search
for prt in source_dir.glob("*.prt"):
if prt.stem.lower() == assembly_name.lower():
deps.assembly_prt = prt
break
# Discover component files based on existing .fem files in the directory
# Pattern: Component FEM files are typically {ComponentName}_fem{N}.fem
for fem_file in source_dir.glob("*.fem"):
deps.fem_files.append(fem_file)
# Extract component name from FEM: M1_Blank_fem1.fem -> M1_Blank
fem_match = re.match(r'^(.+?)_fem\d+$', fem_file.stem, re.IGNORECASE)
if fem_match:
component_name = fem_match.group(1)
# Look for component .prt
comp_prt = source_dir / f"{component_name}.prt"
if comp_prt.exists() and comp_prt not in deps.component_prts:
deps.component_prts.append(comp_prt)
# Look for idealized part (*_fem{N}_i.prt)
idealized_prt = source_dir / f"{fem_file.stem}_i.prt"
if idealized_prt.exists() and idealized_prt not in deps.idealized_prts:
deps.idealized_prts.append(idealized_prt)
# Also scan for any _i.prt files that might have been missed
for prt in source_dir.glob("*_i.prt"):
if prt not in deps.idealized_prts:
deps.idealized_prts.append(prt)
def _discover_single_part_deps(deps: SimDependencies, sim_stem: str, source_dir: Path):
"""Discover dependencies for a single-part simulation."""
# Pattern: Model_sim1.sim -> Model.prt, Model_fem1.fem
match = re.match(r'^(.+?)_sim\d+$', sim_stem, re.IGNORECASE)
if not match:
# Try without _sim suffix
base_name = sim_stem
else:
base_name = match.group(1)
# Look for main .prt file
main_prt = source_dir / f"{base_name}.prt"
if main_prt.exists():
deps.component_prts.append(main_prt)
else:
# Try to find any matching prt
for prt in source_dir.glob("*.prt"):
if base_name.lower() in prt.stem.lower():
deps.component_prts.append(prt)
# Look for .fem file
for fem in source_dir.glob(f"{base_name}_fem*.fem"):
deps.fem_files.append(fem)
# Look for idealized parts
for prt in source_dir.glob(f"{base_name}_fem*_i.prt"):
deps.idealized_prts.append(prt)
def _discover_output_files(deps: SimDependencies, sim_stem: str, source_dir: Path):
"""Discover output files (.op2, .f06, etc.)."""
# Common output patterns
output_extensions = ['.op2', '.f06', '.f04', '.log', '.dat', '.diag', '.csv']
for ext in output_extensions:
for f in source_dir.glob(f"*{ext}"):
if sim_stem.lower() in f.stem.lower() or \
deps.sim_file.stem.lower().replace('_sim', '') in f.stem.lower():
deps.output_files.append(f)
@dataclass
class CopyResult:
"""Result of copying sim dependencies."""
success: bool
copied_files: List[Path] = field(default_factory=list)
failed_files: List[tuple] = field(default_factory=list) # (Path, error message)
target_dir: Optional[Path] = None
def __str__(self):
lines = [f"Copy Result: {'SUCCESS' if self.success else 'FAILED'}"]
if self.target_dir:
lines.append(f"Target: {self.target_dir}")
lines.append(f"Copied: {len(self.copied_files)} files")
if self.failed_files:
lines.append(f"Failed: {len(self.failed_files)} files")
for path, err in self.failed_files:
lines.append(f" - {path.name}: {err}")
return "\n".join(lines)
def copy_sim_with_dependencies(sim_file: Path,
target_dir: Path,
include_outputs: bool = False,
overwrite: bool = True) -> CopyResult:
"""
Copy a simulation file and all its dependencies to a target directory.
Args:
sim_file: Path to the .sim file
target_dir: Destination directory
include_outputs: Whether to include output files (.op2, etc.)
overwrite: Whether to overwrite existing files
Returns:
CopyResult with status and details
"""
sim_file = Path(sim_file)
target_dir = Path(target_dir)
# Discover dependencies
deps = discover_sim_dependencies(sim_file, include_outputs=include_outputs)
# Create target directory if needed
target_dir.mkdir(parents=True, exist_ok=True)
result = CopyResult(success=True, target_dir=target_dir)
# Get files to copy
files_to_copy = deps.all_files_with_outputs if include_outputs else deps.all_files
for src_file in files_to_copy:
dst_file = target_dir / src_file.name
try:
if dst_file.exists():
if not overwrite:
result.copied_files.append(dst_file) # Count as success
continue
# Remove read-only flag if needed
if not os.access(dst_file, os.W_OK):
os.chmod(dst_file, 0o666)
shutil.copy2(src_file, dst_file)
# Make sure the copy is writable
if not os.access(dst_file, os.W_OK):
os.chmod(dst_file, 0o666)
result.copied_files.append(dst_file)
except Exception as e:
result.failed_files.append((src_file, str(e)))
result.success = False
return result
def get_required_files_from_sim(sim_file: Path) -> List[str]:
"""
Get a simple list of required file names for a simulation.
Useful for validation and reporting.
Args:
sim_file: Path to the .sim file
Returns:
List of required file names (not full paths)
"""
deps = discover_sim_dependencies(sim_file)
return [f.name for f in deps.all_files]
# CLI interface
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python nx_file_discovery.py <sim_file> [target_dir]")
print(" python nx_file_discovery.py --discover <sim_file>")
sys.exit(1)
if sys.argv[1] == "--discover":
if len(sys.argv) < 3:
print("Usage: python nx_file_discovery.py --discover <sim_file>")
sys.exit(1)
sim_file = Path(sys.argv[2])
deps = discover_sim_dependencies(sim_file, include_outputs=True)
print(deps)
else:
sim_file = Path(sys.argv[1])
if len(sys.argv) >= 3:
target_dir = Path(sys.argv[2])
result = copy_sim_with_dependencies(sim_file, target_dir)
print(result)
else:
# Just discover
deps = discover_sim_dependencies(sim_file)
print(deps)