feat: Add SIM file introspection journal and enhanced file-type specific UI
- Create introspect_sim.py NX journal to extract solutions, BCs from SIM files - Update introspect_sim_file() to optionally call NX journal for full introspection - Add FEM mesh section (nodes, elements, materials, properties) to IntrospectionPanel - Add SIM solutions and boundary conditions sections to IntrospectionPanel - Show introspection method and NX errors in panel
This commit is contained in:
@@ -5357,19 +5357,89 @@ def introspect_fem_file(fem_path: Path) -> dict:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def introspect_sim_file(sim_path: Path) -> dict:
|
def introspect_sim_file(sim_path: Path, use_nx: bool = True) -> dict:
|
||||||
"""
|
"""
|
||||||
Introspect a .sim file - extract solution info.
|
Introspect a .sim file - extract solution info.
|
||||||
|
|
||||||
Note: .sim is a binary NX format, so we extract what we can from associated files.
|
Args:
|
||||||
|
sim_path: Path to the .sim file
|
||||||
|
use_nx: If True, use NX journal for full introspection (requires NX license)
|
||||||
|
If False, use file-based heuristics only
|
||||||
|
|
||||||
|
Note: .sim is a binary NX format. Full introspection requires NX Open.
|
||||||
"""
|
"""
|
||||||
result = {
|
result = {
|
||||||
"file_type": "sim",
|
"file_type": "sim",
|
||||||
"file_name": sim_path.name,
|
"file_name": sim_path.name,
|
||||||
"success": True,
|
"success": True,
|
||||||
"note": "SIM files are binary NX format. Use NX introspection for full details.",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Try NX journal introspection if requested
|
||||||
|
if use_nx:
|
||||||
|
try:
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Find NX installation
|
||||||
|
nx_paths = [
|
||||||
|
Path("C:/Program Files/Siemens/DesigncenterNX2512/NXBIN/run_journal.exe"),
|
||||||
|
Path("C:/Program Files/Siemens/NX2412/NXBIN/run_journal.exe"),
|
||||||
|
Path("C:/Program Files/Siemens/Simcenter3D_2412/NXBIN/run_journal.exe"),
|
||||||
|
]
|
||||||
|
journal_runner = None
|
||||||
|
for p in nx_paths:
|
||||||
|
if p.exists():
|
||||||
|
journal_runner = p
|
||||||
|
break
|
||||||
|
|
||||||
|
if journal_runner:
|
||||||
|
journal_path = (
|
||||||
|
Path(__file__).parent.parent.parent.parent.parent
|
||||||
|
/ "nx_journals"
|
||||||
|
/ "introspect_sim.py"
|
||||||
|
)
|
||||||
|
if not journal_path.exists():
|
||||||
|
# Try alternate path
|
||||||
|
journal_path = Path("C:/Users/antoi/Atomizer/nx_journals/introspect_sim.py")
|
||||||
|
|
||||||
|
if journal_path.exists():
|
||||||
|
cmd = [
|
||||||
|
str(journal_runner),
|
||||||
|
str(journal_path),
|
||||||
|
str(sim_path),
|
||||||
|
str(sim_path.parent),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Run NX journal with timeout
|
||||||
|
proc = subprocess.run(
|
||||||
|
cmd, capture_output=True, text=True, timeout=120, cwd=str(sim_path.parent)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for output file
|
||||||
|
output_file = sim_path.parent / "_introspection_sim.json"
|
||||||
|
if output_file.exists():
|
||||||
|
with open(output_file, "r") as f:
|
||||||
|
nx_result = json.load(f)
|
||||||
|
result.update(nx_result)
|
||||||
|
result["introspection_method"] = "nx_journal"
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
result["nx_error"] = "Journal completed but no output file"
|
||||||
|
result["nx_stdout"] = proc.stdout[-2000:] if proc.stdout else None
|
||||||
|
result["nx_stderr"] = proc.stderr[-2000:] if proc.stderr else None
|
||||||
|
else:
|
||||||
|
result["nx_error"] = f"Journal not found: {journal_path}"
|
||||||
|
else:
|
||||||
|
result["nx_error"] = "NX installation not found"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result["nx_error"] = "NX journal timed out (120s)"
|
||||||
|
except Exception as e:
|
||||||
|
result["nx_error"] = f"NX introspection failed: {str(e)}"
|
||||||
|
|
||||||
|
# Fallback: file-based heuristics
|
||||||
|
result["introspection_method"] = "file_heuristics"
|
||||||
|
result["note"] = "SIM files are binary NX format. NX introspection unavailable."
|
||||||
|
|
||||||
# Check for associated .dat file that might have been exported
|
# Check for associated .dat file that might have been exported
|
||||||
dat_file = sim_path.parent / (sim_path.stem + ".dat")
|
dat_file = sim_path.parent / (sim_path.stem + ".dat")
|
||||||
if not dat_file.exists():
|
if not dat_file.exists():
|
||||||
@@ -5380,7 +5450,13 @@ def introspect_sim_file(sim_path: Path) -> dict:
|
|||||||
|
|
||||||
if dat_file.exists():
|
if dat_file.exists():
|
||||||
result["associated_dat"] = dat_file.name
|
result["associated_dat"] = dat_file.name
|
||||||
# Could parse the DAT file for solution info if needed
|
|
||||||
|
# Try to infer solver type from related files
|
||||||
|
parent_dir = sim_path.parent
|
||||||
|
if any(f.suffix.lower() == ".afm" for f in parent_dir.iterdir() if f.is_file()):
|
||||||
|
result["inferred"] = {"assembly_fem": True, "solver": "nastran"}
|
||||||
|
elif any(f.suffix.lower() == ".fem" for f in parent_dir.iterdir() if f.is_file()):
|
||||||
|
result["inferred"] = {"assembly_fem": False, "solver": "nastran"}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ import {
|
|||||||
File,
|
File,
|
||||||
Database,
|
Database,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
Layers,
|
||||||
|
Grid3x3,
|
||||||
|
Target,
|
||||||
|
Zap,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useCanvasStore } from '../../../hooks/useCanvasStore';
|
import { useCanvasStore } from '../../../hooks/useCanvasStore';
|
||||||
|
|
||||||
@@ -82,6 +86,45 @@ interface ExpressionsResult {
|
|||||||
user_count: number;
|
user_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FEM file introspection result (from PyNastran)
|
||||||
|
interface FemIntrospection {
|
||||||
|
node_count?: number;
|
||||||
|
element_count?: number;
|
||||||
|
element_types?: Record<string, number>;
|
||||||
|
materials?: Array<{ id: number; type: string; name?: string }>;
|
||||||
|
properties?: Array<{ id: number; type: string; material_id?: number }>;
|
||||||
|
coordinate_systems?: Array<{ id: number; type: string }>;
|
||||||
|
load_sets?: number[];
|
||||||
|
spc_sets?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// SIM file introspection result (from NX journal)
|
||||||
|
interface SimIntrospection {
|
||||||
|
solutions?: Array<{
|
||||||
|
name: string;
|
||||||
|
type?: string;
|
||||||
|
properties?: Record<string, unknown>;
|
||||||
|
}>;
|
||||||
|
boundary_conditions?: {
|
||||||
|
constraints?: Array<{ name: string; type: string }>;
|
||||||
|
loads?: Array<{ name: string; type: string }>;
|
||||||
|
total_count?: number;
|
||||||
|
};
|
||||||
|
tree_structure?: {
|
||||||
|
simulation_objects?: Array<{ pattern: string; type: string; found: boolean }>;
|
||||||
|
found_types?: string[];
|
||||||
|
};
|
||||||
|
loaded_parts?: Array<{ name: string; type: string; leaf?: string }>;
|
||||||
|
part_info?: {
|
||||||
|
name?: string;
|
||||||
|
is_assembly?: boolean;
|
||||||
|
component_count?: number;
|
||||||
|
components?: Array<{ name: string; type: string }>;
|
||||||
|
};
|
||||||
|
introspection_method?: string;
|
||||||
|
nx_error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface IntrospectionResult {
|
interface IntrospectionResult {
|
||||||
part_file?: string;
|
part_file?: string;
|
||||||
part_path?: string;
|
part_path?: string;
|
||||||
@@ -95,7 +138,7 @@ interface IntrospectionResult {
|
|||||||
dependent_files?: DependentFile[];
|
dependent_files?: DependentFile[];
|
||||||
extractors_available?: Extractor[];
|
extractors_available?: Extractor[];
|
||||||
warnings?: string[];
|
warnings?: string[];
|
||||||
// Additional fields from NX introspection
|
// Additional fields from NX introspection (PRT files)
|
||||||
mass_properties?: Record<string, unknown>;
|
mass_properties?: Record<string, unknown>;
|
||||||
materials?: Record<string, unknown>;
|
materials?: Record<string, unknown>;
|
||||||
bodies?: Record<string, unknown>;
|
bodies?: Record<string, unknown>;
|
||||||
@@ -119,6 +162,18 @@ interface IntrospectionResult {
|
|||||||
op2: string[];
|
op2: string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
// FEM file introspection (from PyNastran)
|
||||||
|
fem?: FemIntrospection;
|
||||||
|
// SIM file introspection (from NX journal)
|
||||||
|
sim?: SimIntrospection;
|
||||||
|
// Additional SIM fields that may be at top level
|
||||||
|
solutions?: SimIntrospection['solutions'];
|
||||||
|
boundary_conditions?: SimIntrospection['boundary_conditions'];
|
||||||
|
tree_structure?: SimIntrospection['tree_structure'];
|
||||||
|
loaded_parts?: SimIntrospection['loaded_parts'];
|
||||||
|
part_info?: SimIntrospection['part_info'];
|
||||||
|
introspection_method?: string;
|
||||||
|
nx_error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Baseline run result interface
|
// Baseline run result interface
|
||||||
@@ -165,7 +220,7 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(
|
const [expandedSections, setExpandedSections] = useState<Set<string>>(
|
||||||
new Set(['expressions', 'extractors', 'file_deps', 'fea_results'])
|
new Set(['expressions', 'extractors', 'file_deps', 'fea_results', 'fem_mesh', 'sim_solutions', 'sim_bcs'])
|
||||||
);
|
);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
@@ -804,6 +859,219 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* FEM Mesh Info Section (for .fem/.afm files) */}
|
||||||
|
{result.fem && (
|
||||||
|
<div className="border border-dark-700 rounded-lg overflow-hidden">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleSection('fem_mesh')}
|
||||||
|
className="w-full flex items-center justify-between px-3 py-2 bg-dark-800 hover:bg-dark-750 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Grid3x3 size={14} className="text-teal-400" />
|
||||||
|
<span className="text-sm font-medium text-white">
|
||||||
|
FEM Mesh ({result.fem.node_count?.toLocaleString() || 0} nodes)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{expandedSections.has('fem_mesh') ? (
|
||||||
|
<ChevronDown size={14} className="text-dark-400" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight size={14} className="text-dark-400" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{expandedSections.has('fem_mesh') && (
|
||||||
|
<div className="p-2 space-y-1">
|
||||||
|
<div className="flex justify-between p-2 bg-dark-850 rounded text-xs">
|
||||||
|
<span className="text-dark-400">Nodes</span>
|
||||||
|
<span className="text-white font-mono">{result.fem.node_count?.toLocaleString() || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between p-2 bg-dark-850 rounded text-xs">
|
||||||
|
<span className="text-dark-400">Elements</span>
|
||||||
|
<span className="text-white font-mono">{result.fem.element_count?.toLocaleString() || 0}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Element types breakdown */}
|
||||||
|
{result.fem.element_types && Object.keys(result.fem.element_types).length > 0 && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-xs text-dark-500 mb-1">Element Types:</p>
|
||||||
|
{Object.entries(result.fem.element_types).map(([type, count]) => (
|
||||||
|
<div key={type} className="flex justify-between p-1.5 bg-dark-800 rounded text-xs mb-0.5">
|
||||||
|
<span className="text-teal-400 font-mono">{type}</span>
|
||||||
|
<span className="text-white">{count}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Materials */}
|
||||||
|
{result.fem.materials && result.fem.materials.length > 0 && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-xs text-dark-500 mb-1">Materials ({result.fem.materials.length}):</p>
|
||||||
|
{result.fem.materials.slice(0, 5).map((mat) => (
|
||||||
|
<div key={mat.id} className="flex justify-between p-1.5 bg-dark-800 rounded text-xs mb-0.5">
|
||||||
|
<span className="text-blue-400">ID {mat.id}</span>
|
||||||
|
<span className="text-white font-mono">{mat.type}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{result.fem.materials.length > 5 && (
|
||||||
|
<p className="text-xs text-dark-500 text-center">+{result.fem.materials.length - 5} more</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Properties */}
|
||||||
|
{result.fem.properties && result.fem.properties.length > 0 && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-xs text-dark-500 mb-1">Properties ({result.fem.properties.length}):</p>
|
||||||
|
{result.fem.properties.slice(0, 5).map((prop) => (
|
||||||
|
<div key={prop.id} className="flex justify-between p-1.5 bg-dark-800 rounded text-xs mb-0.5">
|
||||||
|
<span className="text-amber-400">ID {prop.id}</span>
|
||||||
|
<span className="text-white font-mono">{prop.type}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{result.fem.properties.length > 5 && (
|
||||||
|
<p className="text-xs text-dark-500 text-center">+{result.fem.properties.length - 5} more</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Load/SPC sets */}
|
||||||
|
{(result.fem.load_sets?.length || result.fem.spc_sets?.length) && (
|
||||||
|
<div className="mt-2 flex gap-2">
|
||||||
|
{result.fem.load_sets && result.fem.load_sets.length > 0 && (
|
||||||
|
<div className="flex-1 p-1.5 bg-dark-800 rounded text-xs">
|
||||||
|
<span className="text-dark-400">Load Sets: </span>
|
||||||
|
<span className="text-green-400">{result.fem.load_sets.join(', ')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{result.fem.spc_sets && result.fem.spc_sets.length > 0 && (
|
||||||
|
<div className="flex-1 p-1.5 bg-dark-800 rounded text-xs">
|
||||||
|
<span className="text-dark-400">SPC Sets: </span>
|
||||||
|
<span className="text-red-400">{result.fem.spc_sets.join(', ')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SIM Solutions Section (for .sim files) */}
|
||||||
|
{(result.solutions || result.sim?.solutions) && (
|
||||||
|
<div className="border border-dark-700 rounded-lg overflow-hidden">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleSection('sim_solutions')}
|
||||||
|
className="w-full flex items-center justify-between px-3 py-2 bg-dark-800 hover:bg-dark-750 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Target size={14} className="text-violet-400" />
|
||||||
|
<span className="text-sm font-medium text-white">
|
||||||
|
Solutions ({(result.solutions || result.sim?.solutions)?.length || 0})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{expandedSections.has('sim_solutions') ? (
|
||||||
|
<ChevronDown size={14} className="text-dark-400" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight size={14} className="text-dark-400" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{expandedSections.has('sim_solutions') && (
|
||||||
|
<div className="p-2 space-y-1">
|
||||||
|
{((result.solutions || result.sim?.solutions) || []).map((sol, idx) => (
|
||||||
|
<div key={idx} className="p-2 bg-dark-850 rounded">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Zap size={12} className="text-violet-400" />
|
||||||
|
<span className="text-sm text-white">{sol.name}</span>
|
||||||
|
</div>
|
||||||
|
{sol.type && (
|
||||||
|
<p className="text-xs text-dark-400 mt-1">Type: {sol.type}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(result.solutions || result.sim?.solutions)?.length === 0 && (
|
||||||
|
<p className="text-xs text-dark-500 text-center py-2">No solutions found</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SIM Boundary Conditions Section */}
|
||||||
|
{(result.boundary_conditions || result.sim?.boundary_conditions) && (
|
||||||
|
<div className="border border-dark-700 rounded-lg overflow-hidden">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleSection('sim_bcs')}
|
||||||
|
className="w-full flex items-center justify-between px-3 py-2 bg-dark-800 hover:bg-dark-750 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Layers size={14} className="text-rose-400" />
|
||||||
|
<span className="text-sm font-medium text-white">
|
||||||
|
Boundary Conditions ({(result.boundary_conditions || result.sim?.boundary_conditions)?.total_count || 0})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{expandedSections.has('sim_bcs') ? (
|
||||||
|
<ChevronDown size={14} className="text-dark-400" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight size={14} className="text-dark-400" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{expandedSections.has('sim_bcs') && (
|
||||||
|
<div className="p-2 space-y-2">
|
||||||
|
{/* Constraints */}
|
||||||
|
{(result.boundary_conditions || result.sim?.boundary_conditions)?.constraints &&
|
||||||
|
(result.boundary_conditions || result.sim?.boundary_conditions)!.constraints!.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-dark-500 mb-1">Constraints:</p>
|
||||||
|
{(result.boundary_conditions || result.sim?.boundary_conditions)!.constraints!.map((bc, idx) => (
|
||||||
|
<div key={idx} className="flex justify-between p-1.5 bg-dark-800 rounded text-xs mb-0.5">
|
||||||
|
<span className="text-rose-400">{bc.name}</span>
|
||||||
|
<span className="text-dark-400">{bc.type}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Loads */}
|
||||||
|
{(result.boundary_conditions || result.sim?.boundary_conditions)?.loads &&
|
||||||
|
(result.boundary_conditions || result.sim?.boundary_conditions)!.loads!.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-dark-500 mb-1">Loads:</p>
|
||||||
|
{(result.boundary_conditions || result.sim?.boundary_conditions)!.loads!.map((load, idx) => (
|
||||||
|
<div key={idx} className="flex justify-between p-1.5 bg-dark-800 rounded text-xs mb-0.5">
|
||||||
|
<span className="text-green-400">{load.name}</span>
|
||||||
|
<span className="text-dark-400">{load.type}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(result.boundary_conditions || result.sim?.boundary_conditions)?.total_count === 0 && (
|
||||||
|
<p className="text-xs text-dark-500 text-center py-2">No boundary conditions found</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SIM Introspection Method Info */}
|
||||||
|
{(result.introspection_method || result.nx_error) && (
|
||||||
|
<div className="p-2 bg-dark-800 rounded-lg text-xs">
|
||||||
|
{result.introspection_method && (
|
||||||
|
<p className="text-dark-400">
|
||||||
|
Method: <span className="text-white">{result.introspection_method}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{result.nx_error && (
|
||||||
|
<p className="text-amber-400 mt-1">
|
||||||
|
NX Error: {result.nx_error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* File Dependencies Section (NX file chain) */}
|
{/* File Dependencies Section (NX file chain) */}
|
||||||
{result.file_dependencies && (
|
{result.file_dependencies && (
|
||||||
<div className="border border-dark-700 rounded-lg overflow-hidden">
|
<div className="border border-dark-700 rounded-lg overflow-hidden">
|
||||||
|
|||||||
380
nx_journals/introspect_sim.py
Normal file
380
nx_journals/introspect_sim.py
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
"""
|
||||||
|
NX Journal: SIM File Introspection Tool
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
This journal performs deep introspection of an NX .sim file and extracts:
|
||||||
|
- Solutions (name, type, solver)
|
||||||
|
- Boundary conditions (SPCs, loads, etc.)
|
||||||
|
- Subcases
|
||||||
|
- Linked FEM files
|
||||||
|
- Solution properties
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
run_journal.exe introspect_sim.py <sim_file_path> [output_dir]
|
||||||
|
|
||||||
|
Output:
|
||||||
|
_introspection_sim.json - JSON with all extracted data
|
||||||
|
|
||||||
|
Author: Atomizer
|
||||||
|
Created: 2026-01-20
|
||||||
|
Version: 1.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import NXOpen
|
||||||
|
import NXOpen.CAE
|
||||||
|
|
||||||
|
|
||||||
|
def get_solutions(simSimulation):
|
||||||
|
"""Extract all solutions from the simulation."""
|
||||||
|
solutions = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Iterate through all solutions in the simulation
|
||||||
|
# Solutions are accessed via FindObject with pattern "Solution[name]"
|
||||||
|
# But we can also iterate if the simulation has a solutions collection
|
||||||
|
|
||||||
|
# Try to get solution info by iterating through known solution names
|
||||||
|
# Common patterns: "Solution 1", "Solution 2", etc.
|
||||||
|
for i in range(1, 20): # Check up to 20 solutions
|
||||||
|
sol_name = f"Solution {i}"
|
||||||
|
try:
|
||||||
|
sol = simSimulation.FindObject(f"Solution[{sol_name}]")
|
||||||
|
if sol:
|
||||||
|
sol_info = {"name": sol_name, "type": str(type(sol).__name__), "properties": {}}
|
||||||
|
|
||||||
|
# Try to get common properties
|
||||||
|
try:
|
||||||
|
sol_info["properties"]["solver_type"] = (
|
||||||
|
str(sol.SolverType) if hasattr(sol, "SolverType") else None
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
sol_info["properties"]["analysis_type"] = (
|
||||||
|
str(sol.AnalysisType) if hasattr(sol, "AnalysisType") else None
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
solutions.append(sol_info)
|
||||||
|
except:
|
||||||
|
# Solution not found, stop looking
|
||||||
|
if i > 5: # Give a few tries in case there are gaps
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
solutions.append({"error": str(e)})
|
||||||
|
|
||||||
|
return solutions
|
||||||
|
|
||||||
|
|
||||||
|
def get_boundary_conditions(simSimulation, workPart):
|
||||||
|
"""Extract boundary conditions from the simulation."""
|
||||||
|
bcs = {"constraints": [], "loads": [], "total_count": 0}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to access BC collections through the simulation object
|
||||||
|
# BCs are typically stored in the simulation's children
|
||||||
|
|
||||||
|
# Look for constraint groups
|
||||||
|
constraint_names = [
|
||||||
|
"Constraint Group[1]",
|
||||||
|
"Constraint Group[2]",
|
||||||
|
"Constraint Group[3]",
|
||||||
|
"SPC[1]",
|
||||||
|
"SPC[2]",
|
||||||
|
"SPC[3]",
|
||||||
|
"Fixed Constraint[1]",
|
||||||
|
"Fixed Constraint[2]",
|
||||||
|
]
|
||||||
|
|
||||||
|
for name in constraint_names:
|
||||||
|
try:
|
||||||
|
obj = simSimulation.FindObject(name)
|
||||||
|
if obj:
|
||||||
|
bc_info = {
|
||||||
|
"name": name,
|
||||||
|
"type": str(type(obj).__name__),
|
||||||
|
}
|
||||||
|
bcs["constraints"].append(bc_info)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Look for load groups
|
||||||
|
load_names = [
|
||||||
|
"Load Group[1]",
|
||||||
|
"Load Group[2]",
|
||||||
|
"Load Group[3]",
|
||||||
|
"Force[1]",
|
||||||
|
"Force[2]",
|
||||||
|
"Pressure[1]",
|
||||||
|
"Pressure[2]",
|
||||||
|
"Enforced Displacement[1]",
|
||||||
|
"Enforced Displacement[2]",
|
||||||
|
]
|
||||||
|
|
||||||
|
for name in load_names:
|
||||||
|
try:
|
||||||
|
obj = simSimulation.FindObject(name)
|
||||||
|
if obj:
|
||||||
|
load_info = {
|
||||||
|
"name": name,
|
||||||
|
"type": str(type(obj).__name__),
|
||||||
|
}
|
||||||
|
bcs["loads"].append(load_info)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
bcs["total_count"] = len(bcs["constraints"]) + len(bcs["loads"])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
bcs["error"] = str(e)
|
||||||
|
|
||||||
|
return bcs
|
||||||
|
|
||||||
|
|
||||||
|
def get_sim_part_info(workPart):
|
||||||
|
"""Extract SIM part-level information."""
|
||||||
|
info = {"name": None, "full_path": None, "type": None, "fem_parts": [], "component_count": 0}
|
||||||
|
|
||||||
|
try:
|
||||||
|
info["name"] = workPart.Name
|
||||||
|
info["full_path"] = workPart.FullPath if hasattr(workPart, "FullPath") else None
|
||||||
|
info["type"] = str(type(workPart).__name__)
|
||||||
|
|
||||||
|
# Check for component assembly (assembly FEM)
|
||||||
|
try:
|
||||||
|
root = workPart.ComponentAssembly.RootComponent
|
||||||
|
if root:
|
||||||
|
info["is_assembly"] = True
|
||||||
|
# Count components
|
||||||
|
try:
|
||||||
|
children = root.GetChildren()
|
||||||
|
info["component_count"] = len(children) if children else 0
|
||||||
|
|
||||||
|
# Get component names
|
||||||
|
components = []
|
||||||
|
for child in children[:10]: # Limit to first 10
|
||||||
|
try:
|
||||||
|
comp_info = {
|
||||||
|
"name": child.Name if hasattr(child, "Name") else str(child),
|
||||||
|
"type": str(type(child).__name__),
|
||||||
|
}
|
||||||
|
components.append(comp_info)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
info["components"] = components
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
info["is_assembly"] = False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
info["error"] = str(e)
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def get_cae_session_info(theSession):
|
||||||
|
"""Get CAE session information."""
|
||||||
|
cae_info = {"active_sim_part": None, "active_fem_part": None, "solver_types": []}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get CAE session
|
||||||
|
caeSession = theSession.GetExportedObject("NXOpen.CAE.CaeSession")
|
||||||
|
if caeSession:
|
||||||
|
cae_info["cae_session_exists"] = True
|
||||||
|
except:
|
||||||
|
cae_info["cae_session_exists"] = False
|
||||||
|
|
||||||
|
return cae_info
|
||||||
|
|
||||||
|
|
||||||
|
def explore_simulation_tree(simSimulation, workPart):
|
||||||
|
"""Explore the simulation tree structure."""
|
||||||
|
tree_info = {"simulation_objects": [], "found_types": set()}
|
||||||
|
|
||||||
|
# Try to enumerate objects in the simulation
|
||||||
|
# This is exploratory - we don't know the exact API
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try common child object patterns
|
||||||
|
patterns = [
|
||||||
|
# Solutions
|
||||||
|
"Solution[Solution 1]",
|
||||||
|
"Solution[Solution 2]",
|
||||||
|
"Solution[SOLUTION 1]",
|
||||||
|
# Subcases
|
||||||
|
"Subcase[Subcase 1]",
|
||||||
|
"Subcase[Subcase - Static 1]",
|
||||||
|
# Loads/BCs
|
||||||
|
"LoadSet[LoadSet 1]",
|
||||||
|
"ConstraintSet[ConstraintSet 1]",
|
||||||
|
"BoundaryCondition[1]",
|
||||||
|
# FEM reference
|
||||||
|
"FemPart",
|
||||||
|
"AssyFemPart",
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
try:
|
||||||
|
obj = simSimulation.FindObject(pattern)
|
||||||
|
if obj:
|
||||||
|
obj_info = {"pattern": pattern, "type": str(type(obj).__name__), "found": True}
|
||||||
|
tree_info["simulation_objects"].append(obj_info)
|
||||||
|
tree_info["found_types"].add(str(type(obj).__name__))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
tree_info["found_types"] = list(tree_info["found_types"])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
tree_info["error"] = str(e)
|
||||||
|
|
||||||
|
return tree_info
|
||||||
|
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
"""Main entry point for NX journal."""
|
||||||
|
|
||||||
|
if len(args) < 1:
|
||||||
|
print("ERROR: No .sim file path provided")
|
||||||
|
print("Usage: run_journal.exe introspect_sim.py <sim_file_path> [output_dir]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
sim_file_path = args[0]
|
||||||
|
output_dir = args[1] if len(args) > 1 else os.path.dirname(sim_file_path)
|
||||||
|
sim_filename = os.path.basename(sim_file_path)
|
||||||
|
|
||||||
|
print(f"[INTROSPECT-SIM] " + "=" * 60)
|
||||||
|
print(f"[INTROSPECT-SIM] NX SIMULATION INTROSPECTION")
|
||||||
|
print(f"[INTROSPECT-SIM] " + "=" * 60)
|
||||||
|
print(f"[INTROSPECT-SIM] SIM File: {sim_filename}")
|
||||||
|
print(f"[INTROSPECT-SIM] Output: {output_dir}")
|
||||||
|
|
||||||
|
results = {
|
||||||
|
"sim_file": sim_filename,
|
||||||
|
"sim_path": sim_file_path,
|
||||||
|
"success": False,
|
||||||
|
"error": None,
|
||||||
|
"part_info": {},
|
||||||
|
"solutions": [],
|
||||||
|
"boundary_conditions": {},
|
||||||
|
"tree_structure": {},
|
||||||
|
"cae_info": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
theSession = NXOpen.Session.GetSession()
|
||||||
|
|
||||||
|
# Set load options
|
||||||
|
working_dir = os.path.dirname(sim_file_path)
|
||||||
|
theSession.Parts.LoadOptions.ComponentLoadMethod = (
|
||||||
|
NXOpen.LoadOptions.LoadMethod.FromDirectory
|
||||||
|
)
|
||||||
|
theSession.Parts.LoadOptions.SetSearchDirectories([working_dir], [True])
|
||||||
|
theSession.Parts.LoadOptions.ComponentsToLoad = NXOpen.LoadOptions.LoadComponents.All
|
||||||
|
theSession.Parts.LoadOptions.PartLoadOption = NXOpen.LoadOptions.LoadOption.FullyLoad
|
||||||
|
|
||||||
|
# Open the SIM file
|
||||||
|
print(f"[INTROSPECT-SIM] Opening SIM file...")
|
||||||
|
basePart, partLoadStatus = theSession.Parts.OpenActiveDisplay(
|
||||||
|
sim_file_path, NXOpen.DisplayPartOption.AllowAdditional
|
||||||
|
)
|
||||||
|
partLoadStatus.Dispose()
|
||||||
|
|
||||||
|
workPart = theSession.Parts.Work
|
||||||
|
print(f"[INTROSPECT-SIM] Loaded: {workPart.Name}")
|
||||||
|
|
||||||
|
# Switch to SFEM application
|
||||||
|
try:
|
||||||
|
theSession.ApplicationSwitchImmediate("UG_APP_SFEM")
|
||||||
|
print(f"[INTROSPECT-SIM] Switched to SFEM application")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[INTROSPECT-SIM] Note: Could not switch to SFEM: {e}")
|
||||||
|
|
||||||
|
# Get part info
|
||||||
|
print(f"[INTROSPECT-SIM] Extracting part info...")
|
||||||
|
results["part_info"] = get_sim_part_info(workPart)
|
||||||
|
print(f"[INTROSPECT-SIM] Part: {results['part_info'].get('name')}")
|
||||||
|
print(f"[INTROSPECT-SIM] Is Assembly: {results['part_info'].get('is_assembly', False)}")
|
||||||
|
|
||||||
|
# Get simulation object
|
||||||
|
print(f"[INTROSPECT-SIM] Finding Simulation object...")
|
||||||
|
try:
|
||||||
|
simSimulation = workPart.FindObject("Simulation")
|
||||||
|
print(f"[INTROSPECT-SIM] Found Simulation object: {type(simSimulation).__name__}")
|
||||||
|
|
||||||
|
# Get solutions
|
||||||
|
print(f"[INTROSPECT-SIM] Extracting solutions...")
|
||||||
|
results["solutions"] = get_solutions(simSimulation)
|
||||||
|
print(f"[INTROSPECT-SIM] Found {len(results['solutions'])} solutions")
|
||||||
|
|
||||||
|
# Get boundary conditions
|
||||||
|
print(f"[INTROSPECT-SIM] Extracting boundary conditions...")
|
||||||
|
results["boundary_conditions"] = get_boundary_conditions(simSimulation, workPart)
|
||||||
|
print(
|
||||||
|
f"[INTROSPECT-SIM] Found {results['boundary_conditions'].get('total_count', 0)} BCs"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Explore tree structure
|
||||||
|
print(f"[INTROSPECT-SIM] Exploring simulation tree...")
|
||||||
|
results["tree_structure"] = explore_simulation_tree(simSimulation, workPart)
|
||||||
|
print(
|
||||||
|
f"[INTROSPECT-SIM] Found types: {results['tree_structure'].get('found_types', [])}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[INTROSPECT-SIM] WARNING: Could not find Simulation object: {e}")
|
||||||
|
results["simulation_object_error"] = str(e)
|
||||||
|
|
||||||
|
# Get CAE session info
|
||||||
|
print(f"[INTROSPECT-SIM] Getting CAE session info...")
|
||||||
|
results["cae_info"] = get_cae_session_info(theSession)
|
||||||
|
|
||||||
|
# List all loaded parts
|
||||||
|
print(f"[INTROSPECT-SIM] Listing loaded parts...")
|
||||||
|
loaded_parts = []
|
||||||
|
for part in theSession.Parts:
|
||||||
|
try:
|
||||||
|
loaded_parts.append(
|
||||||
|
{
|
||||||
|
"name": part.Name,
|
||||||
|
"type": str(type(part).__name__),
|
||||||
|
"leaf": part.Leaf if hasattr(part, "Leaf") else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
results["loaded_parts"] = loaded_parts
|
||||||
|
print(f"[INTROSPECT-SIM] {len(loaded_parts)} parts loaded")
|
||||||
|
|
||||||
|
results["success"] = True
|
||||||
|
print(f"[INTROSPECT-SIM] ")
|
||||||
|
print(f"[INTROSPECT-SIM] INTROSPECTION COMPLETE!")
|
||||||
|
print(f"[INTROSPECT-SIM] " + "=" * 60)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
results["error"] = str(e)
|
||||||
|
results["success"] = False
|
||||||
|
print(f"[INTROSPECT-SIM] FATAL ERROR: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# Write results
|
||||||
|
output_file = os.path.join(output_dir, "_introspection_sim.json")
|
||||||
|
with open(output_file, "w") as f:
|
||||||
|
json.dump(results, f, indent=2)
|
||||||
|
print(f"[INTROSPECT-SIM] Results written to: {output_file}")
|
||||||
|
|
||||||
|
return results["success"]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main(sys.argv[1:])
|
||||||
Reference in New Issue
Block a user