300 lines
8.2 KiB
Markdown
300 lines
8.2 KiB
Markdown
|
|
# Unit Conversion Issue - Analysis and Fix
|
|||
|
|
|
|||
|
|
**Date:** November 24, 2025
|
|||
|
|
**Issue:** Stresses displaying 1000× too large
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Root Cause Identified
|
|||
|
|
|
|||
|
|
### BDF File Unit System
|
|||
|
|
The BDF file contains: **`PARAM UNITSYS MN-MM`**
|
|||
|
|
|
|||
|
|
This defines the Nastran unit system as:
|
|||
|
|
- **Length:** mm (millimeter)
|
|||
|
|
- **Force:** MN (MegaNewton) = 1,000,000 N
|
|||
|
|
- **Mass:** tonne (1000 kg)
|
|||
|
|
- **Stress:** Pa (Pascal) = N/m² *[NOT MPa!]*
|
|||
|
|
- **Energy:** MN-mm = 1,000 N-m = 1 kJ
|
|||
|
|
|
|||
|
|
### Material Properties Confirm This
|
|||
|
|
Young's modulus from BDF: **E = 200,000,000**
|
|||
|
|
- If units were MPa: E = 200 GPa (way too high for steel ~200 GPa)
|
|||
|
|
- If units are Pa: E = 200 MPa (way too low!)
|
|||
|
|
- **Actual: E = 200,000,000 Pa = 200 GPa** ✓ (correct for steel)
|
|||
|
|
|
|||
|
|
### What pyNastran Returns
|
|||
|
|
pyNastran reads the OP2 file and returns data **in the same units as the BDF**:
|
|||
|
|
- Displacement: mm ✓
|
|||
|
|
- Force/Reactions: **MN** (not N!)
|
|||
|
|
- Stress: **Pa** (not MPa!)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Current vs Actual Values
|
|||
|
|
|
|||
|
|
### Stress Values
|
|||
|
|
| What we claimed | Actual value | Correct interpretation |
|
|||
|
|
|----------------|--------------|------------------------|
|
|||
|
|
| 117,000 MPa | 117,000 Pa | **117 kPa = 0.117 MPa** ✓ |
|
|||
|
|
| 46,000 MPa (mean) | 46,000 Pa | **46 kPa = 0.046 MPa** ✓ |
|
|||
|
|
|
|||
|
|
**Correct stress values are 1000× smaller!**
|
|||
|
|
|
|||
|
|
### Force Values
|
|||
|
|
| What we claimed | Actual value | Correct interpretation |
|
|||
|
|
|----------------|--------------|------------------------|
|
|||
|
|
| 2.73 MN (applied) | 2.73 MN | **2.73 MN = 2,730,000 N** ✓ |
|
|||
|
|
| 150 MN (reaction) | 150 MN | **150 MN = 150,000,000 N** ✓ |
|
|||
|
|
|
|||
|
|
**Force values are correctly stored, but labeled as N instead of MN**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Impact
|
|||
|
|
|
|||
|
|
### What's Wrong:
|
|||
|
|
1. **Stress units incorrectly labeled as "MPa"** - should be "Pa"
|
|||
|
|
2. **Force/reaction units incorrectly labeled as "N"** - should be "MN"
|
|||
|
|
3. **Visualization shows stress 1000× too high**
|
|||
|
|
4. **Reports show unrealistic values** (117 GPa stress would destroy steel!)
|
|||
|
|
|
|||
|
|
### What's Correct:
|
|||
|
|
1. ✅ Displacement values (19.5 mm)
|
|||
|
|
2. ✅ Material properties (E = 200 GPa)
|
|||
|
|
3. ✅ Geometry (mm)
|
|||
|
|
4. ✅ Actual numerical values from pyNastran
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Solution
|
|||
|
|
|
|||
|
|
### Option 1: Convert to Standard Units (Recommended)
|
|||
|
|
Convert all data to consistent engineering units:
|
|||
|
|
- Length: mm → mm ✓
|
|||
|
|
- Force: MN → **N** (divide by 1e6)
|
|||
|
|
- Stress: Pa → **MPa** (divide by 1e6)
|
|||
|
|
- Mass: tonne → kg (multiply by 1000)
|
|||
|
|
|
|||
|
|
**Benefits:**
|
|||
|
|
- Standard engineering units (mm, N, MPa, kg)
|
|||
|
|
- Matches what users expect
|
|||
|
|
- No confusion in reports/visualization
|
|||
|
|
|
|||
|
|
**Changes Required:**
|
|||
|
|
- Parser: Convert forces (divide by 1e6)
|
|||
|
|
- Parser: Convert stress (divide by 1e6)
|
|||
|
|
- Update metadata to reflect actual units
|
|||
|
|
|
|||
|
|
### Option 2: Use Native Units (Not Recommended)
|
|||
|
|
Keep MN-MM-tonne-Pa system throughout
|
|||
|
|
|
|||
|
|
**Issues:**
|
|||
|
|
- Non-standard units confuse users
|
|||
|
|
- Harder to interpret values
|
|||
|
|
- Requires careful labeling everywhere
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Implementation Plan
|
|||
|
|
|
|||
|
|
### 1. Fix Parser ([neural_field_parser.py](neural_field_parser.py))
|
|||
|
|
|
|||
|
|
**Lines to modify:**
|
|||
|
|
|
|||
|
|
#### Stress Extraction (~line 602-648)
|
|||
|
|
```python
|
|||
|
|
# CURRENT (wrong):
|
|||
|
|
stress_data = stress.data[0, :, :]
|
|||
|
|
stress_results[f"{elem_type}_stress"] = {
|
|||
|
|
"data": stress_data.tolist(),
|
|||
|
|
"units": "MPa" # WRONG!
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# FIX:
|
|||
|
|
stress_data = stress.data[0, :, :] / 1e6 # Convert Pa → MPa
|
|||
|
|
stress_results[f"{elem_type}_stress"] = {
|
|||
|
|
"data": stress_data.tolist(),
|
|||
|
|
"units": "MPa" # Now correct!
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Force Extraction (~line 464-507)
|
|||
|
|
```python
|
|||
|
|
# CURRENT (partially wrong):
|
|||
|
|
"magnitude": float(load.mag), # This is in MN, not N!
|
|||
|
|
|
|||
|
|
# FIX:
|
|||
|
|
"magnitude": float(load.mag) * 1e6, # Convert MN → N
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Reaction Forces (~line 538-568)
|
|||
|
|
```python
|
|||
|
|
# CURRENT (wrong):
|
|||
|
|
reactions = grid_point_force.data[0] # In MN!
|
|||
|
|
|
|||
|
|
# FIX:
|
|||
|
|
reactions = grid_point_force.data[0] * 1e6 # Convert MN → N
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. Update Unit Detection
|
|||
|
|
Add UNITSYS parameter detection:
|
|||
|
|
```python
|
|||
|
|
def detect_units(self):
|
|||
|
|
"""Detect Nastran unit system from PARAM cards"""
|
|||
|
|
if hasattr(self.bdf, 'params') and 'UNITSYS' in self.bdf.params:
|
|||
|
|
unitsys = str(self.bdf.params['UNITSYS'].values[0])
|
|||
|
|
if 'MN' in unitsys:
|
|||
|
|
return {
|
|||
|
|
'length': 'mm',
|
|||
|
|
'force': 'MN',
|
|||
|
|
'stress': 'Pa',
|
|||
|
|
'mass': 'tonne',
|
|||
|
|
'needs_conversion': True
|
|||
|
|
}
|
|||
|
|
# Default units
|
|||
|
|
return {
|
|||
|
|
'length': 'mm',
|
|||
|
|
'force': 'N',
|
|||
|
|
'stress': 'MPa',
|
|||
|
|
'mass': 'kg',
|
|||
|
|
'needs_conversion': False
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. Add Unit Conversion Function
|
|||
|
|
```python
|
|||
|
|
def convert_to_standard_units(self, data, unit_system):
|
|||
|
|
"""Convert from Nastran units to standard engineering units"""
|
|||
|
|
if not unit_system['needs_conversion']:
|
|||
|
|
return data
|
|||
|
|
|
|||
|
|
# Convert forces: MN → N (multiply by 1e6)
|
|||
|
|
if 'loads' in data:
|
|||
|
|
for force in data['loads']['point_forces']:
|
|||
|
|
force['magnitude'] *= 1e6
|
|||
|
|
|
|||
|
|
# Convert stress: Pa → MPa (divide by 1e6)
|
|||
|
|
if 'results' in data and 'stress' in data['results']:
|
|||
|
|
for stress_type, stress_data in data['results']['stress'].items():
|
|||
|
|
if isinstance(stress_data, dict) and 'data' in stress_data:
|
|||
|
|
stress_data['data'] = np.array(stress_data['data']) / 1e6
|
|||
|
|
stress_data['units'] = 'MPa'
|
|||
|
|
|
|||
|
|
# Convert reactions: MN → N (multiply by 1e6)
|
|||
|
|
# (Handle in HDF5 write)
|
|||
|
|
|
|||
|
|
return data
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. Update HDF5 Writing
|
|||
|
|
Apply conversions when writing to HDF5:
|
|||
|
|
```python
|
|||
|
|
# Reactions
|
|||
|
|
if 'reactions' in self.neural_field_data['results']:
|
|||
|
|
reactions_data = np.array(self.neural_field_data['results']['reactions']['data'])
|
|||
|
|
if unit_system['force'] == 'MN':
|
|||
|
|
reactions_data *= 1e6 # MN → N
|
|||
|
|
hf.create_dataset('results/reactions', data=reactions_data)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Testing Plan
|
|||
|
|
|
|||
|
|
### 1. Create Unit Conversion Test
|
|||
|
|
```python
|
|||
|
|
def test_unit_conversion():
|
|||
|
|
"""Verify units are correctly converted"""
|
|||
|
|
parser = NastranToNeuralFieldParser('test_case_beam')
|
|||
|
|
data = parser.parse_all()
|
|||
|
|
|
|||
|
|
# Check stress units
|
|||
|
|
stress = data['results']['stress']['cquad4_stress']
|
|||
|
|
assert stress['units'] == 'MPa'
|
|||
|
|
max_stress = np.max(stress['data'][:, -1]) # Von Mises
|
|||
|
|
assert max_stress < 500, f"Stress {max_stress} MPa too high!"
|
|||
|
|
|
|||
|
|
# Check force units
|
|||
|
|
force = data['loads']['point_forces'][0]
|
|||
|
|
assert force['magnitude'] < 1e7, "Force should be in N"
|
|||
|
|
|
|||
|
|
print("[OK] Units correctly converted")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. Expected Values After Fix
|
|||
|
|
| Property | Before (wrong) | After (correct) |
|
|||
|
|
|----------|---------------|-----------------|
|
|||
|
|
| Max stress | 117,000 MPa | **117 MPa** ✓ |
|
|||
|
|
| Mean stress | 46,000 MPa | **46 MPa** ✓ |
|
|||
|
|
| Applied force | 2.73 MN | **2,730,000 N** |
|
|||
|
|
| Max reaction | 150 MN | **150,000,000 N** |
|
|||
|
|
|
|||
|
|
### 3. Validation Checks
|
|||
|
|
- ✓ Stress < 500 MPa (reasonable for steel)
|
|||
|
|
- ✓ Force magnitude matches applied loads
|
|||
|
|
- ✓ Material E = 200 GPa (correct for steel)
|
|||
|
|
- ✓ Displacement still 19.5 mm
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Risk Assessment
|
|||
|
|
|
|||
|
|
### Low Risk:
|
|||
|
|
- ✅ Only affects numerical scaling
|
|||
|
|
- ✅ No changes to data structure
|
|||
|
|
- ✅ Easy to verify with test
|
|||
|
|
- ✅ Can be fixed with multiplication/division
|
|||
|
|
|
|||
|
|
### What Could Go Wrong:
|
|||
|
|
- ⚠ Other BDF files might use different UNITSYS
|
|||
|
|
- ⚠ Some files might already be in correct units
|
|||
|
|
- ⚠ Need to handle multiple unit systems
|
|||
|
|
|
|||
|
|
### Mitigation:
|
|||
|
|
- Always check PARAM UNITSYS first
|
|||
|
|
- Add unit system detection
|
|||
|
|
- Log conversions clearly
|
|||
|
|
- Add validation checks
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Recommendations
|
|||
|
|
|
|||
|
|
### Immediate Actions:
|
|||
|
|
1. ✅ **Update parser to detect UNITSYS**
|
|||
|
|
2. ✅ **Add unit conversion for stress (Pa → MPa)**
|
|||
|
|
3. ✅ **Add unit conversion for forces (MN → N)**
|
|||
|
|
4. ✅ **Update metadata to reflect conversions**
|
|||
|
|
5. ✅ **Add validation checks**
|
|||
|
|
|
|||
|
|
### Long-term:
|
|||
|
|
- Support multiple Nastran unit systems
|
|||
|
|
- Add unit conversion utilities
|
|||
|
|
- Document unit assumptions clearly
|
|||
|
|
- Add warnings for unusual values
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Conclusion
|
|||
|
|
|
|||
|
|
**The system is working correctly** - pyNastran is reading the data accurately.
|
|||
|
|
|
|||
|
|
**The issue is labeling** - we incorrectly assumed MPa when Nastran uses Pa.
|
|||
|
|
|
|||
|
|
**The fix is simple** - divide stress by 1e6, multiply forces by 1e6, update labels.
|
|||
|
|
|
|||
|
|
**After fix:**
|
|||
|
|
- Stress: 117 MPa (reasonable for steel) ✓
|
|||
|
|
- Force: 2.73 MN = 2,730 kN (reasonable for large beam) ✓
|
|||
|
|
- All other values unchanged ✓
|
|||
|
|
|
|||
|
|
**System will be production-ready after this fix!** 🚀
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
*Unit Conversion Analysis v1.0*
|
|||
|
|
*Issue: 1000× stress error*
|
|||
|
|
*Root cause: MN-MM unit system misinterpretation*
|
|||
|
|
*Fix: Scale factors + label corrections*
|