Conversation
…ields can be edited properly
Add Theory section on Docs
There was a problem hiding this comment.
Pull request overview
This pull request merges the dev branch into main for a release. The changes include significant enhancements to VTK file processing with blade root coordinate transformations, additions to the MBC (Multi-Blade Coordinate) transformation workflow, comprehensive theory documentation, orientation visualization features, and various dependency updates.
Changes:
- Enhanced VTK processing: Added blade root orientation extraction and global-to-local coordinate conversion with orientation tracking
- MBC workflow improvements: Added storage of non-rotating A matrices for each operating point and average A matrix export
- Comprehensive theory documentation: Added detailed explanations of Campbell diagram generation workflow, MBC transformation, eigenanalysis, and mode tracking algorithms including MAC/MACXP metrics and spectral clustering
- Frontend enhancements: Added 3D node orientation visualization with controls, improved UI conditional rendering, and better chart tick formatting
- File structure updates: New BeamDynBlade file type and additional DOF fields for platform motion (PtfmSgDOF, PtfmSwDOF, etc.)
Reviewed changes
Copilot reviewed 19 out of 27 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| viz/vtk.go | Added GetBladeRootOrientation function, Global2Local transformation, and blade-specific coordinate conversion logic |
| viz/vtk_test.go | Updated test to handle LoadVTK now returning two VTKFile pointers plus error |
| viz/viz.go | Extended Point struct with orientation vectors and added orientation data parsing in ParseModeData |
| lin/mbc.go | Added ANRs field to store all non-rotating A matrices per operating point |
| lin/mat_data.go | Changed blade number iteration to hardcoded [1,2,3] array |
| lin/linfile.go | Increased scanner buffer size from 64KB to 1MB for handling larger files |
| results.go | Added CSV export for ANRs matrices and AvgA matrix |
| fio.go | Added BeamDynBlade file type and new fields (OrderElem, DampType, platform DOFs) |
| frontend/wailsjs/runtime/runtime.js | Added EventsOffAll export for Wails runtime |
| frontend/wailsjs/go/models.ts | Generated TypeScript models for new Go struct fields |
| frontend/src/components/Results.vue | Added showNodeOrientation toggle and improved chart tick formatting |
| frontend/src/components/Model.vue | Fixed conditional rendering to check for undefined instead of falsy values |
| frontend/src/components/ModeViz.vue | Implemented 3D orientation vector visualization with X/Y axes (Z commented out) |
| docs/content/docs/theory/index.md | Complete theory documentation covering workflow, MBC, eigenanalysis, mode tracking, MAC/MACXP, assignment problem, and spectral clustering |
| docs/hugo.toml | Added MathJax configuration for mathematical notation |
| docs/themes/hugo-whisper-theme/layouts/ | Added math.html partial for MathJax rendering |
| go.mod | Updated wails to v2.11.0, added go-chart v2.1.2, and bumped various dependencies |
| go.sum | Corresponding checksum updates for dependency changes |
| .github/workflows/build-packages.yml | Changed Go version to 1.25.3, removed go version debug step, formatting cleanup |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| // Lookup descriptions in map with all blade numbers | ||
| for j := 1; j <= numBlades; j++ { | ||
| for _, j := range []int{1, 2, 3} { |
There was a problem hiding this comment.
The loop now hardcodes blade numbers as [1, 2, 3] instead of using the numBlades parameter. This breaks functionality for turbines with a different number of blades (e.g., 2-bladed turbines). The original code for j := 1; j <= numBlades; j++ was correct and should be restored.
| for _, j := range []int{1, 2, 3} { | |
| for j := 1; j <= numBlades; j++ { |
| } | ||
|
|
||
| // Write AvgA data to file | ||
| lin.ToCSV(linOP.MBC.AvgA, linOP.RootPath+"_avg_a.csv", "%g") |
There was a problem hiding this comment.
Missing error handling for lin.ToCSV. Unlike the loop above which checks and returns errors from ToCSV calls, this call ignores the returned error. If writing AvgA data fails, the function will continue silently without reporting the issue.
| ov.X = da.MatrixF32 | ||
| } else if da.Name == "OrientationY" { | ||
| copy(om.X[:], da.MatrixF32[0]) | ||
| case "orientationy": | ||
| ov.Y = da.MatrixF32 | ||
| } else if da.Name == "OrientationZ" { | ||
| copy(om.Y[:], da.MatrixF32[0]) | ||
| case "orientationz": | ||
| ov.Z = da.MatrixF32 | ||
| copy(om.Z[:], da.MatrixF32[0]) |
There was a problem hiding this comment.
Potential index out of bounds error. The code assumes da.MatrixF32[0] exists when copying to om.X[:], om.Y[:], and om.Z[:]. If MatrixF32 is empty or nil, this will panic. Add validation to check that da.MatrixF32 has at least one element before accessing da.MatrixF32[0].
| if orientationIndices[0] > -1 { | ||
| copy(component.Line[j].OrientationX[:], vtk.PolyData.Piece.PointData.DataArray[orientationIndices[0]].MatrixF32[c]) | ||
| } | ||
| if orientationIndices[1] > -1 { | ||
| copy(component.Line[j].OrientationY[:], vtk.PolyData.Piece.PointData.DataArray[orientationIndices[1]].MatrixF32[c]) | ||
| } | ||
| if orientationIndices[2] > -1 { | ||
| copy(component.Line[j].OrientationZ[:], vtk.PolyData.Piece.PointData.DataArray[orientationIndices[2]].MatrixF32[c]) | ||
| } |
There was a problem hiding this comment.
Potential index out of bounds error. The code accesses vtk.PolyData.Piece.PointData.DataArray[orientationIndices[0]].MatrixF32[c] without validating that the MatrixF32 slice has enough elements. If c is out of bounds for MatrixF32, this will panic. Consider adding bounds checking before accessing these arrays.
| const indices = new Uint16Array(c.Line.map((_, i) => i * 2).flatMap(i => [i, i + 1])); | ||
|
|
||
| const pointsX = new Float32Array(c.Line.flatMap(p => [p.XYZ[0], p.XYZ[1], p.XYZ[2], p.XYZ[0] + p.OrientationX[0] * 4, p.XYZ[1] + p.OrientationX[1] * 4, p.XYZ[2] + p.OrientationX[2] * 4])); | ||
| const geometryX = new THREE.BufferGeometry(); | ||
| geometryX.setAttribute('position', new THREE.BufferAttribute(pointsX, 3)); | ||
| geometryX.setIndex(new THREE.BufferAttribute(indices, 1)); | ||
| const lineX = new THREE.LineSegments(geometryX, xMaterial); | ||
| orientationFrameGroup.add(lineX); | ||
|
|
||
| const pointsY = new Float32Array(c.Line.flatMap(p => [p.XYZ[0], p.XYZ[1], p.XYZ[2], p.XYZ[0] + p.OrientationY[0] * 4, p.XYZ[1] + p.OrientationY[1] * 4, p.XYZ[2] + p.OrientationY[2] * 4])); | ||
| const geometryY = new THREE.BufferGeometry(); | ||
| geometryY.setAttribute('position', new THREE.BufferAttribute(pointsY, 3)); | ||
| geometryY.setIndex(new THREE.BufferAttribute(indices, 1)); | ||
| const lineY = new THREE.LineSegments(geometryY, yMaterial); | ||
| orientationFrameGroup.add(lineY); | ||
|
|
There was a problem hiding this comment.
Potential runtime error if OrientationX, OrientationY, or OrientationZ arrays are missing or have fewer than 3 elements. The code accesses p.OrientationX[0], p.OrientationX[1], p.OrientationX[2] (and similarly for Y) without validation. Since orientation data is optional (based on the conditional checks in viz.go lines 317-325), add validation to ensure these arrays exist and have at least 3 elements before accessing them, or handle the case where they might be undefined.
| const indices = new Uint16Array(c.Line.map((_, i) => i * 2).flatMap(i => [i, i + 1])); | |
| const pointsX = new Float32Array(c.Line.flatMap(p => [p.XYZ[0], p.XYZ[1], p.XYZ[2], p.XYZ[0] + p.OrientationX[0] * 4, p.XYZ[1] + p.OrientationX[1] * 4, p.XYZ[2] + p.OrientationX[2] * 4])); | |
| const geometryX = new THREE.BufferGeometry(); | |
| geometryX.setAttribute('position', new THREE.BufferAttribute(pointsX, 3)); | |
| geometryX.setIndex(new THREE.BufferAttribute(indices, 1)); | |
| const lineX = new THREE.LineSegments(geometryX, xMaterial); | |
| orientationFrameGroup.add(lineX); | |
| const pointsY = new Float32Array(c.Line.flatMap(p => [p.XYZ[0], p.XYZ[1], p.XYZ[2], p.XYZ[0] + p.OrientationY[0] * 4, p.XYZ[1] + p.OrientationY[1] * 4, p.XYZ[2] + p.OrientationY[2] * 4])); | |
| const geometryY = new THREE.BufferGeometry(); | |
| geometryY.setAttribute('position', new THREE.BufferAttribute(pointsY, 3)); | |
| geometryY.setIndex(new THREE.BufferAttribute(indices, 1)); | |
| const lineY = new THREE.LineSegments(geometryY, yMaterial); | |
| orientationFrameGroup.add(lineY); | |
| // Build X-orientation lines only for points with valid OrientationX data | |
| const linePointsX = c.Line.filter(p => p.OrientationX && p.OrientationX.length >= 3); | |
| if (linePointsX.length > 0) { | |
| const indicesX = new Uint16Array( | |
| linePointsX.map((_, i) => i * 2).flatMap(i => [i, i + 1]) | |
| ); | |
| const pointsX = new Float32Array( | |
| linePointsX.flatMap(p => [ | |
| p.XYZ[0], p.XYZ[1], p.XYZ[2], | |
| p.XYZ[0] + p.OrientationX[0] * 4, | |
| p.XYZ[1] + p.OrientationX[1] * 4, | |
| p.XYZ[2] + p.OrientationX[2] * 4, | |
| ]) | |
| ); | |
| const geometryX = new THREE.BufferGeometry(); | |
| geometryX.setAttribute('position', new THREE.BufferAttribute(pointsX, 3)); | |
| geometryX.setIndex(new THREE.BufferAttribute(indicesX, 1)); | |
| const lineX = new THREE.LineSegments(geometryX, xMaterial); | |
| orientationFrameGroup.add(lineX); | |
| } | |
| // Build Y-orientation lines only for points with valid OrientationY data | |
| const linePointsY = c.Line.filter(p => p.OrientationY && p.OrientationY.length >= 3); | |
| if (linePointsY.length > 0) { | |
| const indicesY = new Uint16Array( | |
| linePointsY.map((_, i) => i * 2).flatMap(i => [i, i + 1]) | |
| ); | |
| const pointsY = new Float32Array( | |
| linePointsY.flatMap(p => [ | |
| p.XYZ[0], p.XYZ[1], p.XYZ[2], | |
| p.XYZ[0] + p.OrientationY[0] * 4, | |
| p.XYZ[1] + p.OrientationY[1] * 4, | |
| p.XYZ[2] + p.OrientationY[2] * 4, | |
| ]) | |
| ); | |
| const geometryY = new THREE.BufferGeometry(); | |
| geometryY.setAttribute('position', new THREE.BufferAttribute(pointsY, 3)); | |
| geometryY.setIndex(new THREE.BufferAttribute(indicesY, 1)); | |
| const lineY = new THREE.LineSegments(geometryY, yMaterial); | |
| orientationFrameGroup.add(lineY); | |
| } |
| // const pointsZ = new Float32Array(c.Line.flatMap(p => [p.XYZ[0], p.XYZ[1], p.XYZ[2], p.XYZ[0] + p.OrientationZ[0] * 4, p.XYZ[1] + p.OrientationZ[1] * 4, p.XYZ[2] + p.OrientationZ[2] * 4])); | ||
| // const geometryZ = new THREE.BufferGeometry(); | ||
| // geometryZ.setAttribute('position', new THREE.BufferAttribute(pointsZ, 3)); | ||
| // geometryZ.setIndex(new THREE.BufferAttribute(indices, 1)); | ||
| // const lineZ = new THREE.LineSegments(geometryZ, zMaterial); | ||
| // orientationFrameGroup.add(lineZ); |
There was a problem hiding this comment.
The Z orientation visualization code is commented out. If this is intentional for this release, consider adding a comment explaining why (e.g., "Z orientation visualization disabled - TODO: re-enable after testing"). If this was accidentally left commented, it should be uncommented to provide complete orientation visualization.
| // const pointsZ = new Float32Array(c.Line.flatMap(p => [p.XYZ[0], p.XYZ[1], p.XYZ[2], p.XYZ[0] + p.OrientationZ[0] * 4, p.XYZ[1] + p.OrientationZ[1] * 4, p.XYZ[2] + p.OrientationZ[2] * 4])); | |
| // const geometryZ = new THREE.BufferGeometry(); | |
| // geometryZ.setAttribute('position', new THREE.BufferAttribute(pointsZ, 3)); | |
| // geometryZ.setIndex(new THREE.BufferAttribute(indices, 1)); | |
| // const lineZ = new THREE.LineSegments(geometryZ, zMaterial); | |
| // orientationFrameGroup.add(lineZ); | |
| const pointsZ = new Float32Array(c.Line.flatMap(p => [p.XYZ[0], p.XYZ[1], p.XYZ[2], p.XYZ[0] + p.OrientationZ[0] * 4, p.XYZ[1] + p.OrientationZ[1] * 4, p.XYZ[2] + p.OrientationZ[2] * 4])); | |
| const geometryZ = new THREE.BufferGeometry(); | |
| geometryZ.setAttribute('position', new THREE.BufferAttribute(pointsZ, 3)); | |
| geometryZ.setIndex(new THREE.BufferAttribute(indices, 1)); | |
| const lineZ = new THREE.LineSegments(geometryZ, zMaterial); | |
| orientationFrameGroup.add(lineZ); |
|   | ||
|
|
||
|
|
||
| A simple greedy approach, solely based on pairing modes with maximum similairty, can lead to discontinuities and suboptimal global matching. A more robust approach can optimize the overall assignment by: |
There was a problem hiding this comment.
Typo: "similairty" should be "similarity".
| A simple greedy approach, solely based on pairing modes with maximum similairty, can lead to discontinuities and suboptimal global matching. A more robust approach can optimize the overall assignment by: | |
| A simple greedy approach, solely based on pairing modes with maximum similarity, can lead to discontinuities and suboptimal global matching. A more robust approach can optimize the overall assignment by: |
No description provided.