875 lines
32 KiB
Markdown
875 lines
32 KiB
Markdown
|
|
# Model Introspection Master Plan — v1.0
|
|||
|
|
|
|||
|
|
**Authors:** Technical Lead 🔧 (plan owner), NX Expert 🖥️ (initial research)
|
|||
|
|
**Date:** 2026-02-15
|
|||
|
|
**Status:** Approved for R&D implementation
|
|||
|
|
**Location:** `docs/plans/MODEL_INTROSPECTION_MASTER_PLAN.md`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Executive Summary
|
|||
|
|
|
|||
|
|
Atomizer currently executes optimization studies that users manually configure. It has **basic introspection** — expression extraction, mass properties, partial solver config — but lacks the deep model knowledge needed to *understand* what it's optimizing.
|
|||
|
|
|
|||
|
|
This plan defines a **four-layer introspection framework** that captures the complete data picture of any NX CAD/FEA model before optimization. The output is a single structured JSON file (`model_introspection.json`) containing everything an engineer or agent needs to design a sound optimization study:
|
|||
|
|
|
|||
|
|
- **What can change** — design variables, expressions, parametric geometry
|
|||
|
|
- **What the FEA model looks like** — mesh quality, element types, materials, properties
|
|||
|
|
- **What physics governs the problem** — boundary conditions, loads, solver config, subcases
|
|||
|
|
- **Where we're starting from** — baseline displacement, stress, frequency, mass
|
|||
|
|
|
|||
|
|
A fifth layer (dependency mapping / expression graphs) is **deferred to v2** due to NXOpen API limitations that make reliable extraction impractical today.
|
|||
|
|
|
|||
|
|
**Timeline:** 12–17 working days for production-quality v1.
|
|||
|
|
**Primary tools:** NXOpen Python API (Layer 1), pyNastran BDF (Layers 2–3), pyNastran OP2 (Layer 4).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Current State
|
|||
|
|
|
|||
|
|
### 2.1 What Exists
|
|||
|
|
|
|||
|
|
| Script | Location | Extracts | Output |
|
|||
|
|
|--------|----------|----------|--------|
|
|||
|
|
| `introspect_part.py` | `nx_journals/` | Expressions (user/internal), mass, materials, bodies, features, datums, units | `_temp_introspection.json` |
|
|||
|
|
| `introspect_sim.py` | `nx_journals/` | Solutions (partial), BCs (partial), subcases (exploratory) | `_introspection_sim.json` |
|
|||
|
|
| `discover_model.py` | `nx_journals/` | Quick scan of expressions + solutions | JSON to stdout |
|
|||
|
|
| `extract_displacement.py` | `optimization_engine/extractors/` | Max displacement from OP2 | Per-trial result |
|
|||
|
|
| `extract_von_mises_stress.py` | `optimization_engine/extractors/` | Max von Mises from OP2 | Per-trial result |
|
|||
|
|
| `extract_part_mass_material.py` | `nx_journals/` | Mass + material from part | JSON |
|
|||
|
|
|
|||
|
|
### 2.2 What's Missing
|
|||
|
|
|
|||
|
|
- ❌ **Mesh quality metrics** — no aspect ratio, jacobian, warpage, skew
|
|||
|
|
- ❌ **BC/load details** — no magnitudes, DOF specifications, target node/element sets
|
|||
|
|
- ❌ **Solver configuration** — no output requests, convergence settings, solution sequence details
|
|||
|
|
- ❌ **Property cards** — no PSHELL thickness, PSOLID assignments, property-element mapping
|
|||
|
|
- ❌ **Baseline results in one place** — extractors exist but aren't aggregated pre-optimization
|
|||
|
|
- ❌ **Unified output** — no single JSON capturing the full model state
|
|||
|
|
- ❌ **Validation** — no cross-checking between data sources
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Framework Architecture
|
|||
|
|
|
|||
|
|
### 3.1 Four-Layer Model (v1)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ Layer 1: GEOMETRIC PARAMETERS [NXOpen API] │
|
|||
|
|
│ Expressions, features, mass, materials, units │
|
|||
|
|
│ → What can be optimized? │
|
|||
|
|
├─────────────────────────────────────────────────────────────────┤
|
|||
|
|
│ Layer 2: FEA MODEL STRUCTURE [pyNastran BDF] │
|
|||
|
|
│ Mesh quality, element types, materials, properties │
|
|||
|
|
│ → What's the baseline mesh health? │
|
|||
|
|
├─────────────────────────────────────────────────────────────────┤
|
|||
|
|
│ Layer 3: SOLVER CONFIGURATION [pyNastran BDF] │
|
|||
|
|
│ Solutions, subcases, BCs, loads, output requests │
|
|||
|
|
│ → What physics governs the problem? │
|
|||
|
|
├─────────────────────────────────────────────────────────────────┤
|
|||
|
|
│ Layer 4: BASELINE RESULTS [pyNastran OP2] │
|
|||
|
|
│ Pre-optimization stress, displacement, frequency, mass │
|
|||
|
|
│ → Where are we starting from? │
|
|||
|
|
└─────────────────────────────────────────────────────────────────┘
|
|||
|
|
|
|||
|
|
DEFERRED TO v2:
|
|||
|
|
┌───────────────────────────────────────────────────────────────┐
|
|||
|
|
│ Layer 5: DEPENDENCIES & RELATIONSHIPS │
|
|||
|
|
│ Expression graph, feature tree, parametric sensitivities │
|
|||
|
|
└───────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 Design Principles
|
|||
|
|
|
|||
|
|
1. **One source per data type.** Don't mix NXOpen and pyNastran for the same data. NXOpen owns geometry/expressions; pyNastran owns FEA/solver data.
|
|||
|
|
2. **BDF export is a prerequisite.** Layer 2–3 extraction requires a current BDF file. The orchestrator must trigger a BDF export (or verify freshness) before parsing.
|
|||
|
|
3. **Report, don't recommend.** v1 reports what exists in the model. It does NOT auto-suggest bounds, objectives, or study types. That's the engineer's job.
|
|||
|
|
4. **Fail gracefully.** If one layer fails, the others still produce output. Partial introspection is better than no introspection.
|
|||
|
|
5. **Validate across sources.** Where data overlaps (element count, mass, materials), cross-check and flag discrepancies.
|
|||
|
|
|
|||
|
|
### 3.3 Data Flow
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌──────────┐
|
|||
|
|
│ .prt │
|
|||
|
|
│ file │
|
|||
|
|
└────┬─────┘
|
|||
|
|
│
|
|||
|
|
NXOpen API
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌──────────────┐
|
|||
|
|
│ Layer 1 │
|
|||
|
|
│ Geometric │
|
|||
|
|
│ Parameters │
|
|||
|
|
└──────┬───────┘
|
|||
|
|
│
|
|||
|
|
┌────────────────────┼────────────────────┐
|
|||
|
|
│ │ │
|
|||
|
|
▼ ▼ ▼
|
|||
|
|
┌────────┐ ┌──────────┐ ┌──────────┐
|
|||
|
|
│ .bdf │ │ .sim │ │ .op2 │
|
|||
|
|
│ file │ │ file │ │ file │
|
|||
|
|
└───┬────┘ └──────────┘ └────┬─────┘
|
|||
|
|
│ (metadata only │
|
|||
|
|
│ via NXOpen) │
|
|||
|
|
pyNastran BDF pyNastran OP2
|
|||
|
|
│ │
|
|||
|
|
▼ ▼
|
|||
|
|
┌──────────────┐ ┌──────────────┐
|
|||
|
|
│ Layer 2+3 │ │ Layer 4 │
|
|||
|
|
│ FEA Model │ │ Baseline │
|
|||
|
|
│ + Solver │ │ Results │
|
|||
|
|
└──────┬───────┘ └──────┬───────┘
|
|||
|
|
│ │
|
|||
|
|
└──────────┬───────────────────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌────────────────┐
|
|||
|
|
│ Orchestrator │
|
|||
|
|
│ Merge + JSON │
|
|||
|
|
│ + Validate │
|
|||
|
|
└────────┬───────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌──────────────────────────┐
|
|||
|
|
│ model_introspection.json │
|
|||
|
|
│ introspection_summary.md │
|
|||
|
|
└──────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Master JSON Schema
|
|||
|
|
|
|||
|
|
### 4.1 Top-Level Structure
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"introspection_version": "1.0.0",
|
|||
|
|
"timestamp": "ISO-8601",
|
|||
|
|
"model_id": "string — derived from part filename",
|
|||
|
|
"files": {
|
|||
|
|
"part": "path/to/model.prt",
|
|||
|
|
"sim": "path/to/model_sim1.sim",
|
|||
|
|
"fem": "path/to/model_fem1.fem",
|
|||
|
|
"bdf": "path/to/exported.bdf",
|
|||
|
|
"op2": "path/to/results.op2"
|
|||
|
|
},
|
|||
|
|
"geometric_parameters": { "..." },
|
|||
|
|
"fea_model": { "..." },
|
|||
|
|
"solver_configuration": { "..." },
|
|||
|
|
"baseline_results": { "..." },
|
|||
|
|
"candidate_design_variables": [ "..." ],
|
|||
|
|
"validation": { "..." },
|
|||
|
|
"metadata": {
|
|||
|
|
"extraction_time_seconds": 0.0,
|
|||
|
|
"layers_completed": ["geometric", "fea_model", "solver", "baseline"],
|
|||
|
|
"layers_failed": [],
|
|||
|
|
"warnings": []
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 Layer 1 — `geometric_parameters`
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"expressions": {
|
|||
|
|
"user_defined": [
|
|||
|
|
{
|
|||
|
|
"name": "thickness",
|
|||
|
|
"value": 3.0,
|
|||
|
|
"units": "mm",
|
|||
|
|
"formula": "3.0",
|
|||
|
|
"is_constant": false,
|
|||
|
|
"part": "bracket.prt"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"internal": [
|
|||
|
|
{
|
|||
|
|
"name": "p47",
|
|||
|
|
"value": 6.0,
|
|||
|
|
"units": "mm",
|
|||
|
|
"formula": "thickness * 2"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"total_user": 3,
|
|||
|
|
"total_internal": 52
|
|||
|
|
},
|
|||
|
|
"mass_properties": {
|
|||
|
|
"mass_kg": 0.234,
|
|||
|
|
"volume_mm3": 85000.0,
|
|||
|
|
"surface_area_mm2": 15000.0,
|
|||
|
|
"center_of_gravity_mm": [12.3, 45.6, 78.9],
|
|||
|
|
"num_solid_bodies": 1
|
|||
|
|
},
|
|||
|
|
"materials": [
|
|||
|
|
{
|
|||
|
|
"name": "Aluminum 6061-T6",
|
|||
|
|
"assigned_to_bodies": ["Body(1)"],
|
|||
|
|
"properties": {
|
|||
|
|
"density_kg_m3": 2700.0,
|
|||
|
|
"youngs_modulus_MPa": 68900.0,
|
|||
|
|
"poisson_ratio": 0.33,
|
|||
|
|
"yield_strength_MPa": 276.0,
|
|||
|
|
"ultimate_strength_MPa": 310.0
|
|||
|
|
},
|
|||
|
|
"source": "NXOpen part material"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"features": {
|
|||
|
|
"total_count": 12,
|
|||
|
|
"by_type": {
|
|||
|
|
"Extrude": 3,
|
|||
|
|
"Shell": 1,
|
|||
|
|
"Sketch": 2,
|
|||
|
|
"Datum Plane": 2,
|
|||
|
|
"Fillet": 4
|
|||
|
|
},
|
|||
|
|
"suppressed_count": 0
|
|||
|
|
},
|
|||
|
|
"units": {
|
|||
|
|
"length": "Millimeter",
|
|||
|
|
"mass": "Kilogram",
|
|||
|
|
"force": "Newton",
|
|||
|
|
"temperature": "Celsius",
|
|||
|
|
"system": "Metric (mm, kg, N, °C)"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.3 Layer 2 — `fea_model`
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"mesh": {
|
|||
|
|
"total_nodes": 12450,
|
|||
|
|
"total_elements": 8234,
|
|||
|
|
"element_types": {
|
|||
|
|
"CTETRA": { "count": 7800, "order": "linear" },
|
|||
|
|
"CQUAD4": { "count": 434, "order": "linear" }
|
|||
|
|
},
|
|||
|
|
"quality_metrics": {
|
|||
|
|
"aspect_ratio": {
|
|||
|
|
"min": 1.02,
|
|||
|
|
"max": 8.34,
|
|||
|
|
"mean": 2.45,
|
|||
|
|
"std": 1.23,
|
|||
|
|
"p95": 5.12,
|
|||
|
|
"threshold": 10.0,
|
|||
|
|
"elements_exceeding": 0
|
|||
|
|
},
|
|||
|
|
"jacobian": {
|
|||
|
|
"min": 0.62,
|
|||
|
|
"max": 1.0,
|
|||
|
|
"mean": 0.91,
|
|||
|
|
"threshold": 0.5,
|
|||
|
|
"elements_below": 0
|
|||
|
|
},
|
|||
|
|
"warpage_deg": {
|
|||
|
|
"max": 5.2,
|
|||
|
|
"threshold": 10.0,
|
|||
|
|
"elements_exceeding": 0
|
|||
|
|
},
|
|||
|
|
"skew_deg": {
|
|||
|
|
"max": 45.2,
|
|||
|
|
"threshold": 60.0,
|
|||
|
|
"elements_exceeding": 0
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"quality_verdict": "PASS"
|
|||
|
|
},
|
|||
|
|
"materials": [
|
|||
|
|
{
|
|||
|
|
"mat_id": 1,
|
|||
|
|
"card_type": "MAT1",
|
|||
|
|
"name": "Aluminum 6061-T6",
|
|||
|
|
"E_MPa": 68900.0,
|
|||
|
|
"G_MPa": 25900.0,
|
|||
|
|
"nu": 0.33,
|
|||
|
|
"rho": 2.7e-6,
|
|||
|
|
"alpha": 2.36e-5
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"properties": [
|
|||
|
|
{
|
|||
|
|
"prop_id": 1,
|
|||
|
|
"card_type": "PSHELL",
|
|||
|
|
"thickness_mm": 3.0,
|
|||
|
|
"mat_id": 1,
|
|||
|
|
"element_count": 434
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"prop_id": 2,
|
|||
|
|
"card_type": "PSOLID",
|
|||
|
|
"mat_id": 1,
|
|||
|
|
"element_count": 7800
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Mesh quality computation:** All quality metrics are computed from element node coordinates using pyNastran's geometry data. For each element, aspect ratio = longest edge / shortest edge. Jacobian is computed at element integration points. This is more reliable than NXOpen's `QualityAuditBuilder` which has limited documentation.
|
|||
|
|
|
|||
|
|
### 4.4 Layer 3 — `solver_configuration`
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"solutions": [
|
|||
|
|
{
|
|||
|
|
"name": "Solution 1",
|
|||
|
|
"sol_sequence": 101,
|
|||
|
|
"sol_type": "Static Linear",
|
|||
|
|
"solver": "NX Nastran"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"subcases": [
|
|||
|
|
{
|
|||
|
|
"id": 1,
|
|||
|
|
"label": "Subcase - Static 1",
|
|||
|
|
"load_set_id": 1,
|
|||
|
|
"spc_set_id": 1,
|
|||
|
|
"output_requests": {
|
|||
|
|
"displacement": { "format": "OP2", "scope": "ALL" },
|
|||
|
|
"stress": { "format": "OP2", "scope": "ALL" },
|
|||
|
|
"strain": null,
|
|||
|
|
"force": null
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"constraints": [
|
|||
|
|
{
|
|||
|
|
"spc_id": 1,
|
|||
|
|
"type": "SPC1",
|
|||
|
|
"dofs_constrained": [1, 2, 3, 4, 5, 6],
|
|||
|
|
"dof_labels": ["Tx", "Ty", "Tz", "Rx", "Ry", "Rz"],
|
|||
|
|
"node_count": 145,
|
|||
|
|
"node_ids_sample": [1, 2, 3, 4, 5],
|
|||
|
|
"description": "Fixed support — all 6 DOF"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"loads": [
|
|||
|
|
{
|
|||
|
|
"load_id": 1,
|
|||
|
|
"type": "FORCE",
|
|||
|
|
"node_id": 456,
|
|||
|
|
"magnitude_N": 1000.0,
|
|||
|
|
"direction": [0.0, -1.0, 0.0],
|
|||
|
|
"components_N": { "Fx": 0.0, "Fy": -1000.0, "Fz": 0.0 }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"load_id": 2,
|
|||
|
|
"type": "PLOAD4",
|
|||
|
|
"element_count": 25,
|
|||
|
|
"pressure_MPa": 5.0,
|
|||
|
|
"direction": "element normal"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"bulk_data_stats": {
|
|||
|
|
"total_cards": 15234,
|
|||
|
|
"card_types": {
|
|||
|
|
"GRID": 12450,
|
|||
|
|
"CTETRA": 7800,
|
|||
|
|
"CQUAD4": 434,
|
|||
|
|
"MAT1": 1,
|
|||
|
|
"PSHELL": 1,
|
|||
|
|
"PSOLID": 1,
|
|||
|
|
"SPC1": 1,
|
|||
|
|
"FORCE": 1,
|
|||
|
|
"PLOAD4": 1
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**BDF export requirement:** The orchestrator must ensure a current BDF file exists before Layer 2–3 extraction. Options:
|
|||
|
|
1. Export BDF via NXOpen journal (`sim.ExportNastranDeck()`) as the first orchestrator step
|
|||
|
|
2. Accept a user-provided BDF path
|
|||
|
|
3. Find the most recent BDF in the sim output directory and verify its timestamp
|
|||
|
|
|
|||
|
|
Option 1 is preferred — it guarantees freshness.
|
|||
|
|
|
|||
|
|
### 4.5 Layer 4 — `baseline_results`
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"source_op2": "bracket_sim1-solution_1.op2",
|
|||
|
|
"solution": "Solution 1",
|
|||
|
|
"subcase_id": 1,
|
|||
|
|
"converged": true,
|
|||
|
|
"displacement": {
|
|||
|
|
"max_magnitude_mm": 2.34,
|
|||
|
|
"max_node_id": 4567,
|
|||
|
|
"max_component": "Tz",
|
|||
|
|
"max_component_value_mm": -2.31,
|
|||
|
|
"mean_magnitude_mm": 0.45
|
|||
|
|
},
|
|||
|
|
"stress": {
|
|||
|
|
"von_mises": {
|
|||
|
|
"max_MPa": 145.6,
|
|||
|
|
"max_element_id": 2345,
|
|||
|
|
"mean_MPa": 45.2,
|
|||
|
|
"p95_MPa": 112.0
|
|||
|
|
},
|
|||
|
|
"margin_of_safety": {
|
|||
|
|
"yield": 0.89,
|
|||
|
|
"ultimate": 1.13,
|
|||
|
|
"yield_strength_MPa": 276.0,
|
|||
|
|
"ultimate_strength_MPa": 310.0,
|
|||
|
|
"note": "MoS = (allowable / actual) - 1"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"modal": null,
|
|||
|
|
"mass_from_solver_kg": 0.234
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Note:** `modal` is populated only if a SOL 103 result exists. Fields would include `modes: [{number, frequency_hz, effective_mass_fraction}]`.
|
|||
|
|
|
|||
|
|
### 4.6 `candidate_design_variables`
|
|||
|
|
|
|||
|
|
This section **reports** expressions that are likely design variable candidates. It does NOT suggest bounds or objectives — that's the engineer's job.
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
[
|
|||
|
|
{
|
|||
|
|
"name": "thickness",
|
|||
|
|
"current_value": 3.0,
|
|||
|
|
"units": "mm",
|
|||
|
|
"reason": "User-defined expression, non-constant, drives geometry"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"name": "width",
|
|||
|
|
"current_value": 50.0,
|
|||
|
|
"units": "mm",
|
|||
|
|
"reason": "User-defined expression, non-constant, drives geometry"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Selection criteria:** An expression is a candidate DV if:
|
|||
|
|
1. It is user-defined (not internal `p###`)
|
|||
|
|
2. It is not constant (formula is not just a literal used in a single non-geometric context)
|
|||
|
|
3. It has dimensional units (mm, deg, etc.) or is clearly a count
|
|||
|
|
4. Its name is not a known system expression (e.g., `PI`, `TRUE`, unit names)
|
|||
|
|
|
|||
|
|
### 4.7 `validation`
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"cross_checks": [
|
|||
|
|
{
|
|||
|
|
"metric": "element_count",
|
|||
|
|
"nxopen_value": 8234,
|
|||
|
|
"pynastran_value": 8234,
|
|||
|
|
"match": true
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metric": "mass_kg",
|
|||
|
|
"nxopen_value": 0.234,
|
|||
|
|
"pynastran_value": 0.2338,
|
|||
|
|
"match": false,
|
|||
|
|
"delta_percent": 0.09,
|
|||
|
|
"tolerance_percent": 1.0,
|
|||
|
|
"within_tolerance": true,
|
|||
|
|
"note": "Small delta due to mesh discretization vs. CAD geometry"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"metric": "material_E_MPa",
|
|||
|
|
"nxopen_value": 68900.0,
|
|||
|
|
"pynastran_value": 68900.0,
|
|||
|
|
"match": true
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"overall_status": "PASS",
|
|||
|
|
"discrepancies": []
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Extraction Methods
|
|||
|
|
|
|||
|
|
### 5.1 Layer 1 — NXOpen Python API
|
|||
|
|
|
|||
|
|
**Base script:** `nx_journals/introspect_part.py` (enhance, don't rewrite)
|
|||
|
|
|
|||
|
|
| Data | API | Notes |
|
|||
|
|
|------|-----|-------|
|
|||
|
|
| Expressions | `part.Expressions` iterator | Filter user vs internal via name pattern (`p###` = internal) |
|
|||
|
|
| Expression values | `expr.Value`, `expr.RightHandSide`, `expr.Units.Name` | |
|
|||
|
|
| Mass properties | `part.MeasureManager.NewMassProperties()` | Requires solid body list |
|
|||
|
|
| Materials | `body.GetPhysicalMaterial()` | Per-body; extract property values via `GetPropertyValue()` |
|
|||
|
|
| Features | `part.Features` iterator | Type via `type(feature).__name__`; count suppressed |
|
|||
|
|
| Units | `part.UnitCollection`, `part.PartUnits` | System-level unit identification |
|
|||
|
|
|
|||
|
|
**Enhancement needed:**
|
|||
|
|
- Add candidate DV identification logic
|
|||
|
|
- Structure output to match schema §4.2
|
|||
|
|
- Add error handling for each extraction block (fail gracefully)
|
|||
|
|
|
|||
|
|
### 5.2 Layers 2–3 — pyNastran BDF
|
|||
|
|
|
|||
|
|
**New script:** `nx_journals/introspect_bdf.py` (or `optimization_engine/extractors/introspect_bdf.py`)
|
|||
|
|
|
|||
|
|
| Data | pyNastran API | Notes |
|
|||
|
|
|------|---------------|-------|
|
|||
|
|
| Elements | `bdf.elements` dict | Key = EID, value has `.type` attribute |
|
|||
|
|
| Nodes | `bdf.nodes` dict | Key = NID |
|
|||
|
|
| Materials | `bdf.materials` dict | MAT1: `.E`, `.G`, `.nu`, `.rho` |
|
|||
|
|
| Properties | `bdf.properties` dict | PSHELL: `.t`, `.mid1`; PSOLID: `.mid` |
|
|||
|
|
| SPCs | `bdf.spcs` dict | SPC1: `.components` (DOF string), `.node_ids` |
|
|||
|
|
| Forces | `bdf.loads` dict | FORCE: `.mag`, `.xyz` (direction vector) |
|
|||
|
|
| Pressures | `bdf.loads` dict | PLOAD4: `.pressures`, element IDs |
|
|||
|
|
| Subcases | `bdf.subcases` dict | `.params` for LOAD, SPC, output requests |
|
|||
|
|
| Bulk stats | `bdf.card_count` | Card type → count |
|
|||
|
|
|
|||
|
|
**Mesh quality computation** (pyNastran element geometry):
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import numpy as np
|
|||
|
|
from pyNastran.bdf.bdf import BDF
|
|||
|
|
|
|||
|
|
def compute_mesh_quality(bdf_model):
|
|||
|
|
"""Compute mesh quality metrics from element geometry."""
|
|||
|
|
aspect_ratios = []
|
|||
|
|
|
|||
|
|
for eid, elem in bdf_model.elements.items():
|
|||
|
|
if elem.type in ('CQUAD4', 'CTRIA3'):
|
|||
|
|
# Get node positions
|
|||
|
|
nodes = [bdf_model.nodes[nid].get_position() for nid in elem.node_ids]
|
|||
|
|
|
|||
|
|
# Compute edge lengths
|
|||
|
|
edges = []
|
|||
|
|
n = len(nodes)
|
|||
|
|
for i in range(n):
|
|||
|
|
edge = np.linalg.norm(nodes[(i+1) % n] - nodes[i])
|
|||
|
|
edges.append(edge)
|
|||
|
|
|
|||
|
|
ar = max(edges) / max(min(edges), 1e-12)
|
|||
|
|
aspect_ratios.append(ar)
|
|||
|
|
|
|||
|
|
elif elem.type == 'CTETRA':
|
|||
|
|
nodes = [bdf_model.nodes[nid].get_position() for nid in elem.node_ids[:4]]
|
|||
|
|
edges = []
|
|||
|
|
for i in range(4):
|
|||
|
|
for j in range(i+1, 4):
|
|||
|
|
edges.append(np.linalg.norm(nodes[j] - nodes[i]))
|
|||
|
|
ar = max(edges) / max(min(edges), 1e-12)
|
|||
|
|
aspect_ratios.append(ar)
|
|||
|
|
|
|||
|
|
if not aspect_ratios:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
ar = np.array(aspect_ratios)
|
|||
|
|
return {
|
|||
|
|
"min": float(ar.min()),
|
|||
|
|
"max": float(ar.max()),
|
|||
|
|
"mean": float(ar.mean()),
|
|||
|
|
"std": float(ar.std()),
|
|||
|
|
"p95": float(np.percentile(ar, 95))
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.3 Layer 4 — pyNastran OP2
|
|||
|
|
|
|||
|
|
**Leverage existing extractors:**
|
|||
|
|
- `optimization_engine/extractors/extract_displacement.py`
|
|||
|
|
- `optimization_engine/extractors/extract_von_mises_stress.py`
|
|||
|
|
|
|||
|
|
**New aggregation script:** `nx_journals/introspect_baseline.py`
|
|||
|
|
|
|||
|
|
| Data | pyNastran API | Notes |
|
|||
|
|
|------|---------------|-------|
|
|||
|
|
| Displacement | `op2.displacements[subcase_id].data` | Magnitude = sqrt(Tx² + Ty² + Tz²) |
|
|||
|
|
| Stress | `op2.ctetra_stress[sc]` or `op2.cquad4_stress[sc]` | Von Mises column varies by element type |
|
|||
|
|
| Eigenvalues | `op2.eigenvalues` | SOL 103 only |
|
|||
|
|
| Grid point weight | `op2.grid_point_weight` | Solver-computed mass |
|
|||
|
|
|
|||
|
|
### 5.4 BDF Export (Prerequisite Step)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# NXOpen journal to export BDF from .sim
|
|||
|
|
def export_bdf(sim_path, output_bdf_path):
|
|||
|
|
"""Export Nastran input deck from simulation."""
|
|||
|
|
theSession = NXOpen.Session.GetSession()
|
|||
|
|
# Open sim, find solution, export deck
|
|||
|
|
# Details depend on NX version — see introspect_sim.py patterns
|
|||
|
|
pass
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Alternative:** If a solve has already been run, the BDF exists in the solver output directory. The orchestrator should check for it before triggering a fresh export.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. Implementation Phases
|
|||
|
|
|
|||
|
|
### Phase 1: Enhanced Part Introspection (3–4 days)
|
|||
|
|
|
|||
|
|
**Goal:** Complete Layer 1 extraction with structured JSON output.
|
|||
|
|
|
|||
|
|
**Tasks:**
|
|||
|
|
1. Refactor `introspect_part.py` output to match schema §4.2
|
|||
|
|
2. Add candidate DV identification logic (§4.6 criteria)
|
|||
|
|
3. Add feature type counting and suppression detection
|
|||
|
|
4. Add material property extraction via `GetPropertyValue()`
|
|||
|
|
5. Structured error handling — each block in try/except, log failures
|
|||
|
|
6. Unit tests with known bracket/beam models
|
|||
|
|
|
|||
|
|
**Output:** `layer1_geometric.json`
|
|||
|
|
**Owner:** NX Expert
|
|||
|
|
|
|||
|
|
### Phase 2: BDF-Based FEA Model Introspection (3–4 days)
|
|||
|
|
|
|||
|
|
**Goal:** Complete Layer 2 extraction — mesh, materials, properties, quality.
|
|||
|
|
|
|||
|
|
**Tasks:**
|
|||
|
|
1. Create `introspect_bdf.py` with pyNastran BDF parsing
|
|||
|
|
2. Implement mesh quality computation (aspect ratio, jacobian, warpage, skew)
|
|||
|
|
3. Extract material cards (MAT1, MAT2 logged but not parsed in v1)
|
|||
|
|
4. Extract property cards (PSHELL, PSOLID) with element assignments
|
|||
|
|
5. Compute quality verdict (PASS/WARN/FAIL based on thresholds)
|
|||
|
|
6. Test on Hydrotech beam BDF and M1 mirror BDF
|
|||
|
|
|
|||
|
|
**Output:** `layer2_fea_model.json`
|
|||
|
|
**Owner:** NX Expert
|
|||
|
|
|
|||
|
|
### Phase 3: BDF-Based Solver Configuration (3–4 days)
|
|||
|
|
|
|||
|
|
**Goal:** Complete Layer 3 extraction — subcases, BCs, loads, output requests.
|
|||
|
|
|
|||
|
|
**Tasks:**
|
|||
|
|
1. Extend `introspect_bdf.py` with subcase parsing
|
|||
|
|
2. Extract SPC constraints with DOF details and node counts
|
|||
|
|
3. Extract loads (FORCE, PLOAD4, MOMENT, GRAV) with magnitudes and directions
|
|||
|
|
4. Extract output requests from case control
|
|||
|
|
5. Add bulk data card statistics
|
|||
|
|
6. Integrate BDF export step (NXOpen journal or path detection)
|
|||
|
|
|
|||
|
|
**Output:** `layer3_solver.json`
|
|||
|
|
**Owner:** NX Expert
|
|||
|
|
|
|||
|
|
### Phase 4: Baseline Results Aggregation (1–2 days)
|
|||
|
|
|
|||
|
|
**Goal:** Complete Layer 4 — aggregate existing extractors into baseline JSON.
|
|||
|
|
|
|||
|
|
**Tasks:**
|
|||
|
|
1. Create `introspect_baseline.py` using existing OP2 extractors
|
|||
|
|
2. Compute displacement max/mean with node identification
|
|||
|
|
3. Compute stress max with element identification and MoS
|
|||
|
|
4. Optionally extract modal frequencies if SOL 103 results exist
|
|||
|
|
5. Extract solver-computed mass from grid point weight generator
|
|||
|
|
|
|||
|
|
**Output:** `layer4_baseline.json`
|
|||
|
|
**Owner:** NX Expert or Technical Lead
|
|||
|
|
|
|||
|
|
### Phase 5: Orchestrator + Validation (2–3 days)
|
|||
|
|
|
|||
|
|
**Goal:** Single-command full introspection with cross-validation and summary.
|
|||
|
|
|
|||
|
|
**Tasks:**
|
|||
|
|
1. Create `run_introspection.py` orchestrator
|
|||
|
|
2. Sequence: BDF export → Layer 1 → Layer 2 → Layer 3 → Layer 4
|
|||
|
|
3. Merge all layer JSONs into master `model_introspection.json`
|
|||
|
|
4. Implement cross-validation checks (§4.7)
|
|||
|
|
5. Generate `introspection_summary.md` — human-readable report
|
|||
|
|
6. Add CLI interface: `python run_introspection.py model.prt model_sim1.sim [--op2 path]`
|
|||
|
|
|
|||
|
|
**Output:** `model_introspection.json` + `introspection_summary.md`
|
|||
|
|
**Owner:** NX Expert + Technical Lead (review)
|
|||
|
|
|
|||
|
|
### Timeline Summary
|
|||
|
|
|
|||
|
|
| Phase | Days | Cumulative | Depends On |
|
|||
|
|
|-------|------|-----------|------------|
|
|||
|
|
| Phase 1 — Part introspection | 3–4 | 3–4 | — |
|
|||
|
|
| Phase 2 — FEA model (BDF) | 3–4 | 6–8 | — (parallel with Phase 1) |
|
|||
|
|
| Phase 3 — Solver config (BDF) | 3–4 | 9–12 | Phase 2 (shared script) |
|
|||
|
|
| Phase 4 — Baseline (OP2) | 1–2 | 10–14 | — (parallel with Phase 3) |
|
|||
|
|
| Phase 5 — Orchestrator | 2–3 | 12–17 | All prior phases |
|
|||
|
|
|
|||
|
|
**Phases 1 and 2 can run in parallel** (different APIs, different scripts). Phase 4 can run in parallel with Phase 3. Critical path: Phase 2 → Phase 3 → Phase 5.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. Validation Strategy
|
|||
|
|
|
|||
|
|
### 7.1 Cross-Source Checks
|
|||
|
|
|
|||
|
|
Where data is available from multiple sources, cross-validate:
|
|||
|
|
|
|||
|
|
| Metric | Source A | Source B | Tolerance |
|
|||
|
|
|--------|----------|----------|-----------|
|
|||
|
|
| Element count | pyNastran `len(bdf.elements)` | NXOpen `FeelementLabelMap.Size` | Exact match |
|
|||
|
|
| Node count | pyNastran `len(bdf.nodes)` | NXOpen `FenodeLabelMap.Size` | Exact match |
|
|||
|
|
| Mass | NXOpen `MeasureManager` | OP2 grid point weight | 1% (mesh vs. CAD geometry) |
|
|||
|
|
| Material E | NXOpen `GetPropertyValue('YoungModulus')` | pyNastran `bdf.materials[id].E` | Exact match |
|
|||
|
|
| Material ρ | NXOpen `GetPropertyValue('Density')` | pyNastran `bdf.materials[id].rho` | Unit conversion tolerance |
|
|||
|
|
|
|||
|
|
### 7.2 Self-Consistency Checks
|
|||
|
|
|
|||
|
|
- Total element count = sum of per-type counts
|
|||
|
|
- Every property ID referenced by elements exists in properties list
|
|||
|
|
- Every material ID referenced by properties exists in materials list
|
|||
|
|
- SPC/LOAD set IDs in subcases exist in constraints/loads lists
|
|||
|
|
- OP2 subcase IDs match BDF subcase IDs
|
|||
|
|
|
|||
|
|
### 7.3 Sanity Checks
|
|||
|
|
|
|||
|
|
- Mass > 0
|
|||
|
|
- Max displacement > 0 (model is loaded and responding)
|
|||
|
|
- Max stress > 0
|
|||
|
|
- No element type with 0 elements in the count
|
|||
|
|
- At least 1 constraint and 1 load in every subcase
|
|||
|
|
|
|||
|
|
### 7.4 Validation Verdict
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
PASS — All checks pass
|
|||
|
|
WARN — Non-critical discrepancies (mass within tolerance but not exact)
|
|||
|
|
FAIL — Critical mismatch (element count differs, missing materials)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. Integration with Atomizer HQ
|
|||
|
|
|
|||
|
|
### 8.1 How Agents Consume Introspection Data
|
|||
|
|
|
|||
|
|
| Agent | Uses | For |
|
|||
|
|
|-------|------|-----|
|
|||
|
|
| **Study Builder** | `candidate_design_variables`, `expressions`, `units` | Study configuration, DV setup |
|
|||
|
|
| **Technical Lead** | `mesh.quality_metrics`, `baseline_results`, `validation` | Technical review, go/no-go |
|
|||
|
|
| **Optimizer** | `fea_model.mesh.total_elements`, `baseline_results` | Runtime estimation, convergence criteria |
|
|||
|
|
| **Manager** | `metadata.layers_completed`, `validation.overall_status` | Status tracking, resource planning |
|
|||
|
|
|
|||
|
|
### 8.2 Usage Workflow
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. Antoine opens new study
|
|||
|
|
2. Run introspection: python run_introspection.py model.prt model_sim1.sim
|
|||
|
|
3. Review introspection_summary.md
|
|||
|
|
4. Study Builder reads model_introspection.json → proposes study config
|
|||
|
|
5. Technical Lead reviews → approves or flags issues
|
|||
|
|
6. Optimization proceeds with full model context
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 8.3 Knowledge Base Integration
|
|||
|
|
|
|||
|
|
Every introspection JSON is stored in the study directory:
|
|||
|
|
```
|
|||
|
|
studies/<project>/<study>/
|
|||
|
|
1_setup/
|
|||
|
|
model/
|
|||
|
|
model_introspection.json ← NEW
|
|||
|
|
introspection_summary.md ← NEW
|
|||
|
|
model.prt
|
|||
|
|
model_sim1.sim
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This becomes the canonical reference for all agents working on the study.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. Scope Exclusions (v1)
|
|||
|
|
|
|||
|
|
The following are **explicitly NOT handled** in v1. Attempting to introspect models with these features will produce partial results with appropriate warnings.
|
|||
|
|
|
|||
|
|
| Feature | Why Excluded | v2 Priority |
|
|||
|
|
|---------|-------------|-------------|
|
|||
|
|
| **Assemblies** | Multi-part expression scoping, component transforms, assembly-level materials — significant complexity | High |
|
|||
|
|
| **Composite materials** (MAT2, MAT8) | Ply stack introspection, layup sequences, orientation angles — specialized domain | Medium |
|
|||
|
|
| **Nonlinear analysis** (SOL 106) | Contact definitions, convergence settings, load stepping, large deformation flags | Medium |
|
|||
|
|
| **Topology optimization** regions | Design vs. non-design space, manufacturing constraints, density filters | Low |
|
|||
|
|
| **Expression dependency graphs** | NXOpen `RightHandSide` parsing is fragile: breaks on built-in functions (`if()`, `min()`), unit qualifiers, substring expression names. Feature-to-expression links require rebuilding builder objects — massive effort | Medium |
|
|||
|
|
| **Parametric sensitivity estimation** | Labeling `thickness → mass` as "linear" is textbook, not model-specific. Real sensitivity requires perturbation studies (separate effort) | Low |
|
|||
|
|
| **Multi-FEM models** | Multiple FEM files linked to one part — need to handle collector mapping across files | Medium |
|
|||
|
|
| **Thermal-structural coupling** | Multi-physics BC detection, thermal load application | Low |
|
|||
|
|
| **Contact pairs** | Contact detection, friction coefficients, contact algorithms | Medium |
|
|||
|
|
| **Dynamic loads** | PSD, time-history, random vibration, frequency-dependent loads | Low |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 10. v2 Roadmap
|
|||
|
|
|
|||
|
|
### 10.1 Expression Dependency Graph (Layer 5)
|
|||
|
|
|
|||
|
|
**Challenge:** NXOpen's `RightHandSide` returns the formula as a string, but parsing it reliably requires handling:
|
|||
|
|
- NX built-in math functions
|
|||
|
|
- Unit-qualified references
|
|||
|
|
- Expression names that are substrings of other names
|
|||
|
|
- Conditional expressions (`if(expr > val, a, b)`)
|
|||
|
|
|
|||
|
|
**Approach for v2:** Build a proper tokenizer/parser for NX expression syntax. Consider exporting expressions to a file and parsing offline rather than relying on API string manipulation.
|
|||
|
|
|
|||
|
|
### 10.2 Assembly Support
|
|||
|
|
|
|||
|
|
**Challenge:** Expressions live at component scope. Design variables may be in sub-components. Assembly-level mass is different from component mass.
|
|||
|
|
|
|||
|
|
**Approach:** Recursive introspection — introspect each component, then merge with assembly context (transforms, overrides).
|
|||
|
|
|
|||
|
|
### 10.3 Composite Introspection
|
|||
|
|
|
|||
|
|
**Challenge:** MAT2/MAT8 cards, PCOMP/PCOMPG properties, ply orientations, stacking sequences.
|
|||
|
|
|
|||
|
|
**Approach:** Extend pyNastran parsing for composite cards. Map plies to design variables (thickness, angle).
|
|||
|
|
|
|||
|
|
### 10.4 AI-Powered Analysis (Future)
|
|||
|
|
|
|||
|
|
- Sensitivity prediction from geometry/BC features without running FEA
|
|||
|
|
- Design variable clustering (correlated parameters)
|
|||
|
|
- Failure mode prediction from stress distributions
|
|||
|
|
- Automated study type recommendation (with engineer approval)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 11. Appendix: Key Reference Paths
|
|||
|
|
|
|||
|
|
### Existing Scripts
|
|||
|
|
| Script | Path | Status |
|
|||
|
|
|--------|------|--------|
|
|||
|
|
| `introspect_part.py` | `nx_journals/introspect_part.py` | Enhance for v1 |
|
|||
|
|
| `introspect_sim.py` | `nx_journals/introspect_sim.py` | Reference only (BDF replaces most functionality) |
|
|||
|
|
| `discover_model.py` | `nx_journals/discover_model.py` | Reference only |
|
|||
|
|
| `extract_displacement.py` | `optimization_engine/extractors/extract_displacement.py` | Use in Layer 4 |
|
|||
|
|
| `extract_von_mises_stress.py` | `optimization_engine/extractors/extract_von_mises_stress.py` | Use in Layer 4 |
|
|||
|
|
|
|||
|
|
### New Scripts (to create)
|
|||
|
|
| Script | Purpose |
|
|||
|
|
|--------|---------|
|
|||
|
|
| `introspect_bdf.py` | Layers 2–3: BDF parsing for mesh, materials, properties, solver config |
|
|||
|
|
| `introspect_baseline.py` | Layer 4: OP2 result aggregation |
|
|||
|
|
| `run_introspection.py` | Master orchestrator — runs all layers, merges, validates |
|
|||
|
|
| `export_bdf.py` | NXOpen journal to export Nastran input deck |
|
|||
|
|
|
|||
|
|
### NXOpen API Reference
|
|||
|
|
- Expressions: `NXOpen.Part.Expressions`
|
|||
|
|
- Mass: `NXOpen.Part.MeasureManager.NewMassProperties()`
|
|||
|
|
- Materials: `body.GetPhysicalMaterial()`, `mat.GetPropertyValue()`
|
|||
|
|
- Features: `NXOpen.Part.Features`
|
|||
|
|
- FEM: `NXOpen.CAE.FemPart.BaseFEModel`
|
|||
|
|
- SIM: `NXOpen.CAE.SimPart.Simulation`
|
|||
|
|
|
|||
|
|
### pyNastran API Reference
|
|||
|
|
- BDF: `pyNastran.bdf.bdf.BDF` — `.elements`, `.nodes`, `.materials`, `.properties`, `.spcs`, `.loads`, `.subcases`
|
|||
|
|
- OP2: `pyNastran.op2.op2.OP2` — `.displacements`, `.ctetra_stress`, `.cquad4_stress`, `.eigenvalues`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 12. Sign-Off
|
|||
|
|
|
|||
|
|
| Role | Status | Notes |
|
|||
|
|
|------|--------|-------|
|
|||
|
|
| Technical Lead 🔧 | ✅ Approved | Plan owner, technical review complete |
|
|||
|
|
| NX Expert 🖥️ | 🔲 Pending | Initial research author, implementation owner |
|
|||
|
|
| Manager 🎯 | 🔲 Pending | Resource allocation, timeline approval |
|
|||
|
|
| CEO (Antoine) | 🔲 Pending | Strategic direction approval |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
*The physics is the boss. Introspection just makes sure we understand what the physics is doing before we start changing things.*
|
|||
|
|
|
|||
|
|
— Technical Lead 🔧 | Atomizer Engineering Co.
|