From a605a77272818331c1050c5045bcaab7ebd02dae Mon Sep 17 00:00:00 2001 From: Lu Wang Date: Fri, 14 Aug 2026 16:23:24 -0600 Subject: [PATCH 01/24] AD: fix OLAF treecode near-core regularization floor and TwrInfl OpenMP data race FVW treecode: the wake path used Tree%DistanceDirect = 2*mean(RegParam), averaged over an over-allocated array whose sentinel tail drove it negative during wake buildup, disabling the near-core direct-evaluation fallback. Replace it with a per-node maxRegParam (max eps over the cell's particles); each branch now uses distDirect = BranchFactor*radius + 2*maxRegParam so control points inside a regularization core fall back to direct evaluation. Applied to both the particle and segment trees. TwrInflArray: make FirstWarn_TowerStrike firstprivate and ErrStat2/ErrMsg2 private in the OpenMP loop to avoid a data race. Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus --- modules/aerodyn/src/AeroDyn.f90 | 3 ++- modules/aerodyn/src/FVW_VortexTools.f90 | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/modules/aerodyn/src/AeroDyn.f90 b/modules/aerodyn/src/AeroDyn.f90 index e6d6da99c7..e93e18a713 100644 --- a/modules/aerodyn/src/AeroDyn.f90 +++ b/modules/aerodyn/src/AeroDyn.f90 @@ -5605,8 +5605,9 @@ SUBROUTINE TwrInflArray( p, u, RotInflow, m, Positions, Inflow, ErrStat, ErrMsg ! these models are valid for only small tower deflections; check for potential division-by-zero errors: call CheckTwrInfl( u, ErrStat2, ErrMsg2 ); call SetErrStat(ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName ); if (ErrStat >= AbortErrLev) return + ! FirstWarn_TowerStrike is firstprivate so each thread starts from the .false. set above (avoids reading uninitialized memory); ErrStat2/ErrMsg2 are private to avoid a data race !$OMP PARALLEL default(shared) - !$OMP do private(i,Pos,theta_tower_trans,W_tower,xbar,ybar,zbar,TwrCd,TwrTI,TwrClrnc,FirstWarn_TowerStrike,DisturbInflow,v) schedule(runtime) + !$OMP do private(i,Pos,theta_tower_trans,W_tower,xbar,ybar,zbar,TwrCd,TwrTI,TwrClrnc,DisturbInflow,v,ErrStat2,ErrMsg2) firstprivate(FirstWarn_TowerStrike) schedule(runtime) do i = 1, size(Positions,2) Pos=Positions(1:3,i) diff --git a/modules/aerodyn/src/FVW_VortexTools.f90 b/modules/aerodyn/src/FVW_VortexTools.f90 index a4c5c6f920..d2a1b9b935 100644 --- a/modules/aerodyn/src/FVW_VortexTools.f90 +++ b/modules/aerodyn/src/FVW_VortexTools.f90 @@ -63,6 +63,7 @@ module FVW_VortexTools real(ReKi) :: radius !< Typical dimension of a cell (max of x,y,z extent) real(ReKi),dimension(3) :: center !< Used to store first the geometric center, then the vorticity center real(ReKi),dimension(3,10) :: Moments + real(ReKi) :: maxRegParam = 0.0_ReKi !< Max reg. param (eps) over all particles in this node integer,dimension(:),pointer :: iPart=>null() !< indexes of particles stored in this node integer,dimension(:),pointer :: leaves=>null() ! NOTE: leaves are introduced to save memory type(T_Node),dimension(:), pointer :: branches =>null() @@ -587,6 +588,7 @@ subroutine grow_tree_part(Tree, nPart, PartP, PartAlpha, PartRegFunction, PartRe node%radius=0 node%center(1:3)=Part%P(1:3,1) node%Moments=0.0_ReKi + node%maxRegParam = Part%RegParam(1) nullify(node%iPart) nullify(node%branches) allocate(node%leaves(1:1)) @@ -625,7 +627,8 @@ subroutine grow_tree_part(Tree, nPart, PartP, PartAlpha, PartRegFunction, PartRe endif Tree%iStep = iStep Tree%bGrown = .true. - Tree%DistanceDirect = 2*sum(PartRegParam)/size(PartRegParam) ! 2*mean(eps), below that distance eps has a strong effect + ! Regularization floor is now applied per node via node%maxRegParam in the tree traversal + Tree%DistanceDirect = 0.0_ReKi end subroutine grow_tree_part !> Recursive function to grow/setup a tree. @@ -675,6 +678,7 @@ subroutine grow_tree_part_substep(node, Part) GeomC = node%center ! NOTE: we rely on the fact that our parent has set this to the Geometric value VortC = 0.0_ReKi wTot = 0.0_ReKi + node%maxRegParam = 0.0_ReKi ! --- Barycenter of vorticity of the node do i = 1,node%nPart PartPos = Part%P(:,node%iPart(i)) @@ -682,6 +686,7 @@ subroutine grow_tree_part_substep(node, Part) wLoc = (PartAlpha(1)**2 + PartAlpha(2)**2 + PartAlpha(3)**2)**0.5_ReKi ! Vorticity norm VortC = VortC + wLoc*PartPos ! Sum coordinates weighted by vorticity wTot = wTot + wLoc ! Total vorticity + node%maxRegParam = max(node%maxRegParam, Part%RegParam(node%iPart(i))) ! Regularization floor uses max eps in this cell end do ! There is no vorticity, we make it a empty node and we exit if(EqualRealNos(abs(wTot),0.0_ReKi)) then @@ -925,6 +930,7 @@ subroutine grow_tree_segment(Tree_Seg, nSeg, SegPoints, SegConnct, SegGamma, Seg node%radius=0 node%center(1:3)=0.5_ReKi*(Seg%SP(1:3,1)+Seg%SP(1:3,2)) node%Moments=0.0_ReKi + node%maxRegParam = Seg%RegParam(1) nullify(node%iPart) nullify(node%branches) allocate(node%leaves(1:1)) @@ -973,7 +979,8 @@ subroutine grow_tree_segment(Tree_Seg, nSeg, SegPoints, SegConnct, SegGamma, Seg endif Tree_Seg%iStep = iStep Tree_Seg%bGrown = .true. - Tree_Seg%DistanceDirect = 2*sum(SegRegParam)/size(SegRegParam) ! 2*mean(eps), below that distance eps has a strong effect ! TODO REMOVE + ! Regularization floor is now applied per node via node%maxRegParam in the tree traversal + Tree_Seg%DistanceDirect = 0.0_ReKi end subroutine grow_tree_segment !> Recursive function to grow/setup a tree. @@ -1024,6 +1031,7 @@ subroutine grow_tree_segment_substep(node, Seg) GeomC = node%center ! NOTE: we rely on the fact that our parent has set this to the Geometric value VortC = 0.0_ReKi wTot = 0.0_ReKi + node%maxRegParam = 0.0_ReKi ! --- Barycenter of vorticity of the node do i = 1,node%nPart P1 = Seg%SP(1:3,Seg%SConnct(1,node%iPart(i))) @@ -1031,6 +1039,7 @@ subroutine grow_tree_segment_substep(node, Seg) SegCenter = 0.5_ReKi*(P1+P2) VortC = VortC + abs(Seg%SGamma(node%iPart(i)))*SegCenter ! Sum coordinates weighted by vorticity wTot = wTot + abs(Seg%SGamma(node%iPart(i))) ! Total vorticity + node%maxRegParam = max(node%maxRegParam, Seg%RegParam(node%iPart(i))) ! Regularization floor uses max eps in this cell end do ! There is no vorticity, we make it a empty node and we exit if(EqualRealNos(abs(wTot),0.0_ReKi)) then @@ -1501,7 +1510,7 @@ recursive subroutine ui_tree_part_11(node, CP, Uind) enddo endif else - distDirect = max(BranchFactor*node%radius, DistanceDirect) ! Under this distance-> Direct eval., Above it -> quadrupole calculation + distDirect = max(BranchFactor*node%radius + 2.0_ReKi*node%maxRegParam, DistanceDirect) ! Direct eval. below this distance; floor = branch radius + 2*max(eps) in cell DeltaP = - node%center + CP(1:3) ! Vector between the control point and the center of the branch r = sqrt( DeltaP(1)**2 + DeltaP(2)**2 + DeltaP(3)**2) ! Test if the control point is too close from the branch node so that a direct evaluation is needed @@ -1704,7 +1713,7 @@ recursive subroutine ui_tree_segment_11(node, CP, Uind) enddo endif else - distDirect = max(BranchFactor*node%radius, DistanceDirect) ! Under this distance-> Direct eval., Above it -> quadrupole calculation + distDirect = max(BranchFactor*node%radius + 2.0_ReKi*node%maxRegParam, DistanceDirect) ! Direct eval. below this distance; floor = branch radius + 2*max(eps) in cell DeltaP = - node%center + CP(1:3) ! Vector between the control point and the center of the branch r = sqrt( DeltaP(1)**2 + DeltaP(2)**2 + DeltaP(3)**2) ! Test if the control point is too close from the branch node so that a direct evaluation is needed From a20755163bb6e7d2817866bebc654f6f17e0ba9b Mon Sep 17 00:00:00 2001 From: Lu Wang Date: Fri, 14 Aug 2026 18:12:29 -0600 Subject: [PATCH 02/24] FVW/OLAF: skip particle exp mollifier beyond 2 core radii In ui_part_nograd_11 the idRegExp mollifier (1-exp(-(r/rc)^3))/r^3 differs from 1/r^3 by 2*rc, which is exactly the accuracy the far-field multipole already accepts at its BranchFactor*radius+2*maxRegParam floor. Treat the mollifier as 1 beyond that boundary (new PART_REG_NRAD/PART_REG_CUT3 params) so the near-field direct kernel and the far-field multipole share the same 2*rc cutoff. Since most near-field tree pairs have r>>rc, this skips exp() for the majority of evaluations, giving ~25-32 percent serial speedup on the treecode path with the output unchanged to ~1.8e-5 relative. Also caches r^2/r^3/rc^3 to drop redundant ** intrinsics; the compact-support branch is refactored identically (bit-for-bit). FVW_Subs: scope the DEV_VERSION NaN/sentinel checks in SegmentsToPartWrap to the active particles (1:nPart); the preallocated tail intentionally keeps its sentinel and must not trip the check. Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus --- modules/aerodyn/src/FVW_BiotSavart.f90 | 23 +++++++++++++++++------ modules/aerodyn/src/FVW_Subs.f90 | 6 +++--- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/modules/aerodyn/src/FVW_BiotSavart.f90 b/modules/aerodyn/src/FVW_BiotSavart.f90 index fcc301511d..d6db453b6f 100644 --- a/modules/aerodyn/src/FVW_BiotSavart.f90 +++ b/modules/aerodyn/src/FVW_BiotSavart.f90 @@ -10,6 +10,8 @@ module FVW_BiotSavart real(ReKi),parameter :: PRECISION_UI = epsilon(1.0_ReKi)/100 !< NOTE assuming problem of size 1 real(ReKi),parameter :: PRECISION_EPS = epsilon(1.0_ReKi) !< Machine Precision For the given ReKi for problems of scale 1! real(ReKi),parameter :: MIN_EXP_VALUE=-10.0_ReKi + real(ReKi),parameter :: PART_REG_NRAD = 2.0_ReKi !< Particle exp mollifier treated as 1 beyond this many core radii (matches 2*rc far-field multipole floor) + real(ReKi),parameter :: PART_REG_CUT3 = PART_REG_NRAD**3 !< Corresponding (r/rc)^3 cutoff real(ReKi),parameter :: MINDENOM=0.0_ReKi ! real(ReKi),parameter :: MINDENOM=1e-15_ReKi real(ReKi),parameter :: MINNORM=1e-4 @@ -374,26 +376,35 @@ subroutine ui_part_nograd_11(DeltaP, Alpha, RegFunction, RegParam, Ui) real(ReKi),dimension(3) :: C !< Cross product of Alpha and r real(ReKi) :: E !< Exponential poart for the mollifider real(ReKi) :: r3_inv !< + real(ReKi) :: r2, r3, rc3!< |r|^2, |r|^3, RegParam^3 (reused to avoid recomputing ** intrinsics) real(ReKi) :: rDeltaP !< norm , distance between point and particle real(ReKi) :: ScalarPart !< the part containing the inverse of the distance, but not 4pi, Mollifier - rDeltaP=sqrt(DeltaP(1)**2+ DeltaP(2)**2+ DeltaP(3)**2)! norm + r2 = DeltaP(1)**2+ DeltaP(2)**2+ DeltaP(3)**2 + rDeltaP = sqrt(r2)! norm if (rDeltaP PART_REG_CUT3*rc3) then ! r > 2*rc: mollifier -> 1 (skip exp), consistent with far-field multipole floor + ScalarPart = r3_inv*fourpi_inv + else + E = exp(-r3/rc3) + ScalarPart = (1._ReKi-E)*r3_inv*fourpi_inv + endif case (idRegCompact) ! Compact support - r3_inv = 1._ReKi/sqrt(RegParam**6+rDeltaP**6) + rc3 = RegParam*RegParam*RegParam + r3_inv = 1._ReKi/sqrt(rc3*rc3+r3*r3) ScalarPart = r3_inv*fourpi_inv case default print*,'[ERROR] Wrong regularization function for particles',RegFunction diff --git a/modules/aerodyn/src/FVW_Subs.f90 b/modules/aerodyn/src/FVW_Subs.f90 index 3c9061e537..7da341ad64 100644 --- a/modules/aerodyn/src/FVW_Subs.f90 +++ b/modules/aerodyn/src/FVW_Subs.f90 @@ -1223,9 +1223,9 @@ subroutine SegmentsToPartWrap(Sgmt, nSeg, PartPerSegment, RegFunction, Part, all Part%RegFunction = idRegExp ! TODO need to find a good equivalence and potentially adapt Epsilon in SegmentsToPart endif if (DEV_VERSION) then - call find_nan_2D(Part%P , 'SegmentsToPartWrap Part%P') - call find_nan_2D(Part%Alpha, 'SegmentsToPartWrap Part%Alpha') - if (any(Part%RegParam(:)<-9999.99_ReKi)) then + call find_nan_2D(Part%P(:,1:nPart) , 'SegmentsToPartWrap Part%P') + call find_nan_2D(Part%Alpha(:,1:nPart), 'SegmentsToPartWrap Part%Alpha') + if (any(Part%RegParam(1:nPart)<-9999.99_ReKi)) then ! Only the active particles are filled; the preallocated tail keeps its sentinel print*,'Error in Segment to part conversion' STOP endif From 45f011238eb933a9552a6280a82d9630278dfac5 Mon Sep 17 00:00:00 2001 From: Roger Bergua Date: Sun, 23 Aug 2026 19:57:51 -0600 Subject: [PATCH 03/24] CMake: CYGWIN or MINGW --- cmake/OpenfastFortranOptions.cmake | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmake/OpenfastFortranOptions.cmake b/cmake/OpenfastFortranOptions.cmake index b93e027a5c..667753a4e1 100644 --- a/cmake/OpenfastFortranOptions.cmake +++ b/cmake/OpenfastFortranOptions.cmake @@ -133,10 +133,10 @@ macro(set_fast_gfortran) set( CMAKE_Fortran_FLAGS_DEBUG "${CMAKE_Fortran_FLAGS_DEBUG} -fcheck=all,no-array-temps -pedantic -fbacktrace -finit-real=inf -finit-integer=9999." ) endif() - if(CYGWIN) - # increase the default 2MB stack size to 16 MB + if(CYGWIN OR MINGW) + # increase the default 1-2 MB stack size to 16 MB MATH(EXPR stack_size "16 * 1024 * 1024") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS},--stack,${stack_size}") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--stack,${stack_size}") endif() # Profiling From 409ed98e860d0584cc7d92a27fd4dd15762ef62d Mon Sep 17 00:00:00 2001 From: Roger Bergua Date: Tue, 25 Aug 2026 11:40:37 -0600 Subject: [PATCH 04/24] SrcPnlFile parsing --- modules/aerodyn/src/FVW_IO.f90 | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/aerodyn/src/FVW_IO.f90 b/modules/aerodyn/src/FVW_IO.f90 index a037c85dc3..aef35dcfcb 100644 --- a/modules/aerodyn/src/FVW_IO.f90 +++ b/modules/aerodyn/src/FVW_IO.f90 @@ -184,6 +184,7 @@ SUBROUTINE FVW_ReadInputFile( FileName, p, m, Inp, ErrStat, ErrMsg ) ! --- Validation of inputs if (PathIsRelative(Inp%CirculationFile)) Inp%CirculationFile = TRIM(PriPath)//TRIM(Inp%CirculationFile) + if (len_trim(Inp%SrcPnlFile)>0 .and. PathIsRelative(Inp%SrcPnlFile)) Inp%SrcPnlFile = TRIM(PriPath)//TRIM(Inp%SrcPnlFile) if (Check(.not.(ANY(idCircVALID ==Inp%CircSolvMethod)), 'Circulation method (CircSolvMethod) not implemented: '//trim(Num2LStr(Inp%CircSolvMethod)))) return if (Check(.not.(ANY(idIntMethodVALID==Inp%IntMethod )) , 'Time integration method (IntMethod) not yet implemented. Use Euler 1st order method for now.')) return From 460c9c0c7caf2e9e734968668bdda6c1779496d3 Mon Sep 17 00:00:00 2001 From: Roger Bergua Date: Mon, 31 Aug 2026 19:11:38 -0600 Subject: [PATCH 05/24] DTaero tolerance instead of epsilon fraction --- modules/aerodyn/src/FVW.f90 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/aerodyn/src/FVW.f90 b/modules/aerodyn/src/FVW.f90 index 6bc12eb3ef..a409c461e8 100644 --- a/modules/aerodyn/src/FVW.f90 +++ b/modules/aerodyn/src/FVW.f90 @@ -603,7 +603,7 @@ subroutine FVW_UpdateStates( t, n, u, utimes, p, x, xd, z, OtherState, AFInfo, m bReevaluation=.True. endif ! Compute Induced wake effects only if time since last compute is > DTfvw - if ( (( t - m%OldWakeTime ) >= p%DTfvw*OneMinusEpsilon) ) then + if ( (( t - m%OldWakeTime ) >= p%DTfvw - 0.25_DbKi*p%DTaero) ) then m%OldWakeTime = t m%ComputeWakeInduced = .TRUE. ! It's time to update the induced velocities from wake else @@ -1584,7 +1584,7 @@ subroutine WriteVTKOutputs(t, force, VTKstep, u, p, x, z, m, ErrStat, ErrMsg) do iW=1,p%nWings m%W(iW)%Vtot_CP = m%W(iW)%Vind_CP + m%W(iW)%Vwnd_CP - m%W(iW)%Vstr_CP enddo - if ( force .or. (( t - m%VTKlastTime ) >= p%DTvtk*OneMinusEpsilon )) then + if ( force .or. (( t - m%VTKlastTime ) >= p%DTvtk - 0.25_DbKi*p%DTaero )) then m%VTKlastTime = t if ((p%VTKCoord==2).or.(p%VTKCoord==3)) then ! Hub reference coordinates, for export only, ALL VTK Will be exported in this coordinate system! @@ -1614,7 +1614,7 @@ subroutine WriteVTKOutputs(t, force, VTKstep, u, p, x, z, m, ErrStat, ErrMsg) CALL DistributeRequestedWind_Grid(u%V_wind, p, m) do iGrid=1,p%nGridOut bWithinTime = t>=m%GridOutputs(iGrid)%tStart-p%DTaero/2. .and. t<= m%GridOutputs(iGrid)%tEnd+p%DTaero/2. - bTimeToOutput = ( t - m%GridOutputs(iGrid)%tLastOutput) >= m%GridOutputs(iGrid)%DTout * OneMinusEpsilon + bTimeToOutput = ( t - m%GridOutputs(iGrid)%tLastOutput) >= m%GridOutputs(iGrid)%DTout - 0.25_DbKi*p%DTaero if (force .or. (bWithinTime .and. bTimeToOutput) ) then ! Compute induced velocity on grid, TODO use the same Tree for all CalcOutput call InducedVelocitiesAll_OnGrid(m%GridOutputs(iGrid), p, x, m, ErrStat2, ErrMsg2); From 483931445a570d8ac54d027a9399ceb0288e39ee Mon Sep 17 00:00:00 2001 From: Roger Bergua Date: Mon, 31 Aug 2026 19:12:18 -0600 Subject: [PATCH 06/24] Remove OneMinusEpsilon declaration --- modules/aerodyn/src/FVW.f90 | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/aerodyn/src/FVW.f90 b/modules/aerodyn/src/FVW.f90 index a409c461e8..9eb690f868 100644 --- a/modules/aerodyn/src/FVW.f90 +++ b/modules/aerodyn/src/FVW.f90 @@ -30,9 +30,6 @@ module FVW public :: FVW_CalcOutput public :: FVW_UpdateStates - ! parameter for deciding if enough time has elapsed (Wake calculation, and vtk output) - real(DbKi), parameter :: OneMinusEpsilon = 1 - 10000*EPSILON(1.0_DbKi) - contains !---------------------------------------------------------------------------------------------------------------------------------- From 2248a77b0f27203ff5ac9aa30734e6852c52e21a Mon Sep 17 00:00:00 2001 From: Lu Wang Date: Wed, 2 Sep 2026 14:30:42 -0600 Subject: [PATCH 07/24] OLAF: remove unused Tree DistanceDirect field; convert velocity-method dispatch to select case Remove always-zero T_Tree%DistanceDirect field and its assignments; the two wake-path reads now pass 0.0_ReKi literally (regularization floor is applied per node via node%maxRegParam). Convert the if/elseif velocity-method dispatch in FVW_InitRegularization, InducedVelocitiesAll_Init/Calc/End and LiftingLineInducedVelocities to select case, adding case default fatal for unhandled methods. Blade-path DistanceDirect (MaxWingLength*2.2) is unchanged. Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus --- modules/aerodyn/src/FVW_Subs.f90 | 96 +++++++++++++++---------- modules/aerodyn/src/FVW_VortexTools.f90 | 9 +-- 2 files changed, 60 insertions(+), 45 deletions(-) diff --git a/modules/aerodyn/src/FVW_Subs.f90 b/modules/aerodyn/src/FVW_Subs.f90 index 7da341ad64..7f295802fd 100644 --- a/modules/aerodyn/src/FVW_Subs.f90 +++ b/modules/aerodyn/src/FVW_Subs.f90 @@ -1047,7 +1047,8 @@ subroutine FVW_InitRegularization(x, p, m, ErrStat, ErrMsg) write(*,'(A,1F8.4)') 'RegParam (Input ) : ',p%WakeRegParam endif - if (p%RegDeterMethod==idRegDeterConstant) then + select case (p%RegDeterMethod) + case (idRegDeterConstant) ! Constant reg param throughout the wake if (p%WakeRegMethod==idRegAge) then ! NOTE: age method implies a division by rc p%WingRegParam=max(0.01_ReKi, p%WingRegParam) @@ -1061,7 +1062,7 @@ subroutine FVW_InitRegularization(x, p, m, ErrStat, ErrMsg) x%W(iW)%Eps_NW(1:3,:,2) = p%WakeRegParam ! Second age is always WakeRegParam endif - else if (p%RegDeterMethod==idRegDeterAuto) then + case (idRegDeterAuto) ! TODO this is beta print*,'!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' print*,'!!! NOTE: using optimized wake regularization parameters is still a beta feature!' @@ -1078,14 +1079,14 @@ subroutine FVW_InitRegularization(x, p, m, ErrStat, ErrMsg) write(*,'(A,1F8.4)') 'WakeRegParam : ', p%WakeRegParam write(*,'(A,1F8.4)') 'WingRegParam : ', p%WingRegParam write(*,'(A,1F9.4)') 'CoreSpreadEddyVisc: ', p%CoreSpreadEddyVisc - ! Set reg param on wing and first NW - ! NOTE: setting the same in all three directions for now, TODO! - x%W(iW)%Eps_NW(1:3,:,1) = p%WingRegParam ! First age is always WingRegParam (LL) - if (p%nNWMax>1) then - x%W(iW)%Eps_NW(1:3,:,2) = p%WakeRegParam ! Second age is always WakeRegParam - endif + ! Set reg param on wing and first NW + ! NOTE: setting the same in all three directions for now, TODO! + x%W(iW)%Eps_NW(1:3,:,1) = p%WingRegParam ! First age is always WingRegParam (LL) + if (p%nNWMax>1) then + x%W(iW)%Eps_NW(1:3,:,2) = p%WakeRegParam ! Second age is always WakeRegParam + endif - else if (p%RegDeterMethod==idRegDeterChord) then + case (idRegDeterChord) ! Using chord to scale the reg param do iSpan=1,p%W(iW)%nSpan x%W(iW)%Eps_NW(1:3, iSpan, 1) = p%WingRegParam * p%W(iW)%chord_CP(iSpan) @@ -1094,7 +1095,7 @@ subroutine FVW_InitRegularization(x, p, m, ErrStat, ErrMsg) endif enddo - else if (p%RegDeterMethod==idRegDeterSpan) then + case (idRegDeterSpan) ! Using dr to scale the reg param do iSpan=1,p%W(iW)%nSpan ds = p%W(iW)%s_LL(iSpan+1)-p%W(iW)%s_LL(iSpan) @@ -1103,10 +1104,11 @@ subroutine FVW_InitRegularization(x, p, m, ErrStat, ErrMsg) x%W(iW)%Eps_NW(1:3, iSpan, 2) = p%WakeRegParam * ds endif enddo - else ! Should never happen (caught earlier) + + case default ! Should never happen (caught earlier) ErrStat = ErrID_Fatal ErrMsg ='Regularization determination method not implemented' - endif + end select if (iW==1) then call WrScr(' - OLAF regularization parameters (for wing 1):') @@ -1261,18 +1263,19 @@ subroutine InducedVelocitiesAll_Init(p, x, m, Sgmt, Part, Tree, Panl, ErrStat, E Sgmt%nAct = nSeg Sgmt%nActP = nSegP - ! --- Convert to particles if needed - if ((p%VelocityMethod(iVel)==idVelocityTreePart) .or. (p%VelocityMethod(iVel)==idVelocityPart)) then + select case (p%VelocityMethod(iVel)) + case (idVelocityPart) + ! --- Convert segments to particles call SegmentsToPartWrap(Sgmt, nSeg, p%PartPerSegment(iVel), p%RegFunction, Part, allocPart=allocPart) - endif - - ! --- Grow tree if needed - if (p%VelocityMethod(iVel)==idVelocityTreePart) then + case (idVelocityTreePart) + ! --- Convert segments to particles + call SegmentsToPartWrap(Sgmt, nSeg, p%PartPerSegment(iVel), p%RegFunction, Part, allocPart=allocPart) + ! --- Grow particle tree call grow_tree_part(Tree, Part%nAct, Part%P, Part%Alpha, Part%RegFunction, Part%RegParam, 0) - - elseif (p%VelocityMethod(iVel)==idVelocityTreeSeg) then + case (idVelocityTreeSeg) + ! --- Grow segment tree call grow_tree_segment(Tree, nSeg, Sgmt%Points, Sgmt%Connct(:,1:nSeg), Sgmt%Gamma(1:nSeg), p%RegFunction, Sgmt%Epsilon(1:nSeg), 0) - endif + end select ! --- Src Panl%p_Src => p%SrcPnl @@ -1298,20 +1301,26 @@ subroutine InducedVelocitiesAll_Calc(CPs, nCPs, Uind, p, Sgmt, Part, Tree, Panl, ErrStat= ErrID_None ErrMsg ='' - if (p%VelocityMethod(iVel)==idVelocityBasic) then + select case (p%VelocityMethod(iVel)) + case (idVelocityBasic) call ui_seg( 1, nCPs, CPs, 1, Sgmt%nAct, Sgmt%Points, Sgmt%Connct, Sgmt%Gamma, Sgmt%RegFunction, Sgmt%Epsilon, Uind) - elseif (p%VelocityMethod(iVel)==idVelocityTreePart) then + case (idVelocityTreePart) ! Tree has already been grown with InducedVelocitiesAll_Init !call print_tree(Tree) - call ui_tree_part(Tree, nCPs, CPs, p%TreeBranchFactor(iVel), Tree%DistanceDirect, Uind, ErrStat, ErrMsg) + call ui_tree_part(Tree, nCPs, CPs, p%TreeBranchFactor(iVel), 0.0_ReKi, Uind, ErrStat, ErrMsg) - elseif (p%VelocityMethod(iVel)==idVelocityPart) then + case (idVelocityPart) call ui_part_nograd(nCPs, CPs, Part%nAct, Part%P, Part%Alpha, Part%RegFunction, Part%RegParam, Uind) - elseif (p%VelocityMethod(iVel)==idVelocityTreeSeg) then - call ui_tree_segment(Tree, CPs, nCPs, p%TreeBranchFactor(iVel), Tree%DistanceDirect, Uind, ErrStat, ErrMsg) - endif + case (idVelocityTreeSeg) + call ui_tree_segment(Tree, CPs, nCPs, p%TreeBranchFactor(iVel), 0.0_ReKi, Uind, ErrStat, ErrMsg) + + case default + ErrStat = ErrID_Fatal + ErrMsg = 'Velocity method not implemented' + + end select ! --- Src Panels if (associated(Panl%p_Src)) then @@ -1338,19 +1347,25 @@ subroutine InducedVelocitiesAll_End(p, Tree, Part, Panl, ErrStat, ErrMsg, deallo ErrStat= ErrID_None ErrMsg ='' - if (p%VelocityMethod(iVel)==idVelocityBasic) then + select case (p%VelocityMethod(iVel)) + case (idVelocityBasic) ! Nothing - elseif (p%VelocityMethod(iVel)==idVelocityTreePart) then + case (idVelocityTreePart) if (deallocPart) deallocate(Part%P, Part%Alpha, Part%RegParam) call cut_tree(Tree) - elseif (p%VelocityMethod(iVel)==idVelocityPart) then + case (idVelocityPart) if (deallocPart) deallocate(Part%P, Part%Alpha, Part%RegParam) - elseif (p%VelocityMethod(iVel)==idVelocityTreeSeg) then + case (idVelocityTreeSeg) call cut_tree(Tree) ! We do not deallocate segment - endif + + case default + ErrStat = ErrID_Fatal + ErrMsg = 'Velocity method not implemented' + + end select ! Src Panels (we nullify only) nullify(Panl%p_Src) @@ -1547,26 +1562,31 @@ subroutine LiftingLineInducedVelocities(p, x, InductionAtCP, iDepthStart, m, Err ! --- Compute velocity on LL ! TreeSeg is faster but introduce some noise, so we keep this open for the user to choose - if (p%VelocityMethod(iVel) == idVelocityBasic) then + select case (p%VelocityMethod(iVel)) + case (idVelocityBasic) call ui_seg( 1, nCPs, CPs, 1, nSeg, m%Sgmt%Points, m%Sgmt%Connct, m%Sgmt%Gamma, m%Sgmt%RegFunction, m%Sgmt%Epsilon, Uind) - else if (p%VelocityMethod(iVel) == idVelocityPart) then + case (idVelocityPart) call SegmentsToPartWrap(m%Sgmt, nSeg, p%PartPerSegment(iVel), p%RegFunction, m%Part, allocPart=.false.) call ui_part_nograd(nCPs, CPs, m%Part%nAct, m%Part%P, m%Part%Alpha, m%Part%RegFunction, m%Part%RegParam, Uind) !deallocate(Part%P, Part%Alpha, Part%RegParam) - else if (p%VelocityMethod(iVel) == idVelocityTreeSeg) then + case (idVelocityTreeSeg) call grow_tree_segment(Tree, nSeg, m%Sgmt%Points, m%Sgmt%Connct(:,1:nSeg), m%Sgmt%Gamma(1:nSeg), m%Sgmt%RegFunction, m%Sgmt%Epsilon(1:nSeg), 0) call ui_tree_segment(Tree, CPs, nCPs, p%TreeBranchFactor(iVel), DistanceDirect, Uind, ErrStat, ErrMsg) call cut_tree(Tree) - else if (p%VelocityMethod(iVel) == idVelocityTreePart) then + case (idVelocityTreePart) call SegmentsToPartWrap(m%Sgmt, nSeg, p%PartPerSegment(iVel), p%RegFunction, m%Part, allocPart=.false.) call grow_tree_part(Tree, m%Part%nAct, m%Part%P, m%Part%Alpha, m%Part%RegFunction, m%Part%RegParam, 0) call ui_tree_part(Tree, nCPs, CPs, p%TreeBranchFactor(iVel), DistanceDirect, Uind, ErrStat, ErrMsg) !deallocate(Part%P, Part%Alpha, Part%RegParam) call cut_tree(Tree) - endif + + case default + ErrStat = ErrID_Fatal + ErrMsg = 'Velocity method not implemented' + end select ! --- Src Panel contribution if (p%SrcPnl%n>0) then call ui_quad_src_nn(CPs, m%SrcPnl%RHS, p%SrcPnl%xi, p%SrcPnl%eta, p%SrcPnl%Pcent, p%SrcPnl%R_g2p, Uind, nCPs, p%SrcPnl%n) diff --git a/modules/aerodyn/src/FVW_VortexTools.f90 b/modules/aerodyn/src/FVW_VortexTools.f90 index d2a1b9b935..a8589bebce 100644 --- a/modules/aerodyn/src/FVW_VortexTools.f90 +++ b/modules/aerodyn/src/FVW_VortexTools.f90 @@ -72,11 +72,10 @@ module FVW_VortexTools !> The type tree contains some basic data, a chained-list of nodes, and a pointer to the Particle data that were used type T_Tree - type(T_VPart) :: Part !< Storage for all particles - type(T_VSgmt) :: Seg !< Storage for all segments + type(T_VPart) :: Part !< Storage for all particles + type(T_VSgmt) :: Seg !< Storage for all segments integer :: iStep =-1 !< Time step at which the tree was built logical :: bGrown =.false. !< Is the tree build - real(ReKi) :: DistanceDirect type(T_Node) :: Root !< Contains the chained-list of nodes end type T_Tree @@ -627,8 +626,6 @@ subroutine grow_tree_part(Tree, nPart, PartP, PartAlpha, PartRegFunction, PartRe endif Tree%iStep = iStep Tree%bGrown = .true. - ! Regularization floor is now applied per node via node%maxRegParam in the tree traversal - Tree%DistanceDirect = 0.0_ReKi end subroutine grow_tree_part !> Recursive function to grow/setup a tree. @@ -979,8 +976,6 @@ subroutine grow_tree_segment(Tree_Seg, nSeg, SegPoints, SegConnct, SegGamma, Seg endif Tree_Seg%iStep = iStep Tree_Seg%bGrown = .true. - ! Regularization floor is now applied per node via node%maxRegParam in the tree traversal - Tree_Seg%DistanceDirect = 0.0_ReKi end subroutine grow_tree_segment !> Recursive function to grow/setup a tree. From 980d9e302ebbfd84749c610f3c27228c1ec5fd1c Mon Sep 17 00:00:00 2001 From: Lu Wang Date: Wed, 2 Sep 2026 14:34:13 -0600 Subject: [PATCH 08/24] Update r-test pointer --- reg_tests/r-test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reg_tests/r-test b/reg_tests/r-test index f8d1d131de..630015613d 160000 --- a/reg_tests/r-test +++ b/reg_tests/r-test @@ -1 +1 @@ -Subproject commit f8d1d131debad662dffa749bf37b440f8320095f +Subproject commit 630015613d3fd03f21add529180d3c3edfcbbd76 From d937bfb73e25114496151cca8a744b204f53bf2d Mon Sep 17 00:00:00 2001 From: Roger Bergua Date: Tue, 15 Sep 2026 11:17:02 -0600 Subject: [PATCH 09/24] GetBoundsT: lower bound with grid tolerance --- modules/inflowwind/src/IfW_FlowField.f90 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/inflowwind/src/IfW_FlowField.f90 b/modules/inflowwind/src/IfW_FlowField.f90 index 43f21947e0..12170b874f 100644 --- a/modules/inflowwind/src/IfW_FlowField.f90 +++ b/modules/inflowwind/src/IfW_FlowField.f90 @@ -1289,6 +1289,11 @@ subroutine GetBoundsT(PosX, DT) ! Adjust indices and interpolant if (IT_LO >= 1 .and. IT_HI <= G3D%NSteps) then ! Point is within grid + else if (IT_LO == 0 .and. T_GRID >= -GridTol) then + ! Within tolerance of the first time step + IT_LO = 1 + IT_HI = 1 + DT = -1.0_ReKi else if (IT_LO == G3D%NSteps) then if (G3D%Periodic) then ! Time wraps back to beginning From 0c5c9424179b9fc2ba3a7952133370f74eba0bfd Mon Sep 17 00:00:00 2001 From: Roger Bergua Date: Tue, 15 Sep 2026 11:19:56 -0600 Subject: [PATCH 10/24] IfW_FlowField.f90: Fix typos --- modules/inflowwind/src/IfW_FlowField.f90 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/inflowwind/src/IfW_FlowField.f90 b/modules/inflowwind/src/IfW_FlowField.f90 index 12170b874f..e4c9427e8c 100644 --- a/modules/inflowwind/src/IfW_FlowField.f90 +++ b/modules/inflowwind/src/IfW_FlowField.f90 @@ -789,7 +789,7 @@ subroutine Grid3DField_GetCell(G3D, Time, Position, CalcAccel, AllowExtrap, & if (ErrStat >= AbortErrLev) return !---------------------------------------------------------------------------- - ! Extract interpolation cells from grids based on poisiont + ! Extract interpolation cells from grids based on position !---------------------------------------------------------------------------- ! If position is inside grid @@ -1278,7 +1278,7 @@ subroutine GetBoundsT(PosX, DT) ! so take modulus again to ensure that T_GRID is less than NSteps T_GRID = MODULO(T_GRID, real(G3D%NSteps, ReKi)) end if - + ! Calculate bounding grid indices IT_LO = floor(T_GRID, IntKi) + 1 IT_HI = IT_LO + 1 @@ -1311,7 +1311,7 @@ subroutine GetBoundsT(PosX, DT) ! Time exceeds array bounds call SetErrStat(ErrID_Fatal, ' Error: GF wind array was exhausted at '// & TRIM(Num2LStr(TIME))//' seconds (trying to access data at '// & - TRIM(Num2LStr(TimeShifted))//' seconds). IT_Lo='//TRIM(Num2LStr(IT_Lo))// & + TRIM(Num2LStr(TimeShifted))//' seconds). IT_LO='//TRIM(Num2LStr(IT_Lo))// & ', IT_HI='//TRIM(Num2LStr(IT_Hi)), & ErrStat, ErrMsg, RoutineName) end if From d0e013ac339b101454b8f2587caad2997d669e0a Mon Sep 17 00:00:00 2001 From: Roger Bergua Date: Fri, 18 Sep 2026 11:42:26 -0600 Subject: [PATCH 11/24] FVW_Subs: convert InducedVelocitiesAll_OnGrid (wake tree rebuild) into a reusable Calc-only routine --- modules/aerodyn/src/FVW_Subs.f90 | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/modules/aerodyn/src/FVW_Subs.f90 b/modules/aerodyn/src/FVW_Subs.f90 index 7f295802fd..dcaf489949 100644 --- a/modules/aerodyn/src/FVW_Subs.f90 +++ b/modules/aerodyn/src/FVW_Subs.f90 @@ -1124,28 +1124,29 @@ end subroutine FVW_InitRegularization -!> Compute induced velocities from all vortex elements onto nPoints -!! In : x, x%W(iW)%r_NW, x%W(iW)%r_FW, x%W(iW)%Gamma_NW, x%W(iW)%Gamma_FW +!> Compute induced velocities from all vortex elements onto nPoints, +!! reusing a wake Tree/Panl built once by InducedVelocitiesAll_Init +!! for all grids at a given output step. +!! In : Tree, Panl (already built from current wake state) !! Out: Vind -subroutine InducedVelocitiesAll_OnGrid(g, p, x, m, ErrStat, ErrMsg) +subroutine InducedVelocitiesAll_OnGrid_Calc(g, p, Sgmt, Part, Tree, Panl, ErrStat, ErrMsg) type(GridOutType), intent(inout) :: g !< Grid on whcih to compute the velocity type(FVW_ParameterType), intent(in ) :: p !< Parameters - type(FVW_ContinuousStateType), intent(in ) :: x !< States - type(FVW_MiscVarType), intent(inout) :: m !< Initial misc/optimization variables + type(T_Sgmt), intent(in ) :: Sgmt !< Segments, already built by InducedVelocitiesAll_Init + type(T_Part), intent(in ) :: Part !< Particle storage, already built by InducedVelocitiesAll_Init + type(T_Tree), intent(inout) :: Tree !< Wake tree, already built by InducedVelocitiesAll_Init + type(T_Panl), intent(in ) :: Panl !< Panel storage, already set by InducedVelocitiesAll_Init integer(IntKi), intent( out) :: ErrStat !< Error status of the operation character(*), intent( out) :: ErrMsg !< Error message if ErrStat /= ErrID_None ! Local variables integer(IntKi) :: nCPs, iHeadP integer(IntKi) :: i,j,k real(ReKi) :: xP,yP,zP - ! TODO new options - type(T_Tree) :: Tree - type(T_Panl) :: Panl real(ReKi), dimension(:,:), allocatable :: CPs ! TODO get rid of me with dedicated functions real(ReKi), dimension(:,:), allocatable :: Uind ! TODO get rid of me with dedicated functions ErrStat= ErrID_None ErrMsg ='' - if (OLAF_PROFILING) call tic('InducedVelocitiesAll_OnGrid') + if (OLAF_PROFILING) call tic('InducedVelocitiesAll_OnGrid_Calc') ! --- Packing control points nCPs = g%nx * g%ny * g%nz @@ -1168,11 +1169,8 @@ subroutine InducedVelocitiesAll_OnGrid(g, p, x, m, ErrStat, ErrMsg) iHeadP=1 call FlattenValues(g%uGrid, Uind, iHeadP); ! NOTE: Uind contains uGrid now (Uwnd) - ! --- Compute induced velocity - ! Convert Panels to segments, segments to particles, particles to tree - call InducedVelocitiesAll_Init(p, x, m, m%Sgmt, m%Part, Tree, Panl, ErrStat, ErrMsg, allocPart=.false.) - call InducedVelocitiesAll_Calc(CPs, nCPs, Uind, p, m%Sgmt, m%Part, Tree, Panl, ErrStat, ErrMsg) - call InducedVelocitiesAll_End(p, Tree, m%Part, Panl, ErrStat, ErrMsg, deallocPart=.false.) + ! --- Compute induced velocity using the wake Sgmt/Part/Tree/Panl built once + call InducedVelocitiesAll_Calc(CPs, nCPs, Uind, p, Sgmt, Part, Tree, Panl, ErrStat, ErrMsg) ! --- Unpacking induced velocity points iHeadP=1 @@ -1183,7 +1181,7 @@ subroutine InducedVelocitiesAll_OnGrid(g, p, x, m, ErrStat, ErrMsg) if (OLAF_PROFILING) call toc() -end subroutine InducedVelocitiesAll_OnGrid +end subroutine InducedVelocitiesAll_OnGrid_Calc !> Wrapper to setup part from set of segments subroutine SegmentsToPartWrap(Sgmt, nSeg, PartPerSegment, RegFunction, Part, allocPart) From 3e163e17157471cdf1405f391d565b63c8bababd Mon Sep 17 00:00:00 2001 From: Roger Bergua Date: Fri, 18 Sep 2026 12:19:53 -0600 Subject: [PATCH 12/24] FVW: build the wake tree once per VTK and reuse it for all grids --- modules/aerodyn/src/FVW.f90 | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/modules/aerodyn/src/FVW.f90 b/modules/aerodyn/src/FVW.f90 index 9eb690f868..64925a563f 100644 --- a/modules/aerodyn/src/FVW.f90 +++ b/modules/aerodyn/src/FVW.f90 @@ -453,6 +453,7 @@ SUBROUTINE FVW_SetParametersFromInputFile( InputFileData, p, ErrStat, ErrMsg ) p%FWShedVorticity = InputFileData%FWShedVorticity p%DiffusionMethod = InputFileData%DiffusionMethod p%RegFunction = InputFileData%RegFunction + p%RegFunctionPart = InputFileData%RegFunctionPart p%RegDeterMethod = InputFileData%RegDeterMethod p%WakeRegMethod = InputFileData%WakeRegMethod p%WakeRegParam = InputFileData%WakeRegParam @@ -1567,6 +1568,9 @@ subroutine WriteVTKOutputs(t, force, VTKstep, u, p, x, z, m, ErrStat, ErrMsg) character(*), parameter :: RoutineName = 'WriteVTKOutputs' integer(IntKi) :: iW, iGrid integer(IntKi) :: nSeg, nSegP + logical :: bAnyGridOutput + type(T_Tree) :: Tree + type(T_Panl) :: Panl ErrStat = ErrID_None ErrMsg = '' if (OLAF_PROFILING) call tic('WriteVTKOutputs') @@ -1609,18 +1613,32 @@ subroutine WriteVTKOutputs(t, force, VTKstep, u, p, x, z, m, ErrStat, ErrMsg) ! Distribute the Wind we requested to Inflow wind to storage Misc arrays ! TODO ANDY: replace with direct call to inflow wind at Grid points CALL DistributeRequestedWind_Grid(u%V_wind, p, m) + ! Check ahead of time whether any grid needs output now, to avoid rebuilding the wake tree if not necessary + bAnyGridOutput = force do iGrid=1,p%nGridOut bWithinTime = t>=m%GridOutputs(iGrid)%tStart-p%DTaero/2. .and. t<= m%GridOutputs(iGrid)%tEnd+p%DTaero/2. bTimeToOutput = ( t - m%GridOutputs(iGrid)%tLastOutput) >= m%GridOutputs(iGrid)%DTout - 0.25_DbKi*p%DTaero - if (force .or. (bWithinTime .and. bTimeToOutput) ) then - ! Compute induced velocity on grid, TODO use the same Tree for all CalcOutput - call InducedVelocitiesAll_OnGrid(m%GridOutputs(iGrid), p, x, m, ErrStat2, ErrMsg2); - call SetErrStat(ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName) - m%GridOutputs(iGrid)%tLastOutput = t - call WrVTK_FVW_Grid(p, m, iGrid, trim(p%VTK_OutFileBase)//'FVW_Grid', VTKstep, 9) - m%VTKstep=VTKstep ! We save the step at which writing occurred - endif + if (bWithinTime .and. bTimeToOutput) bAnyGridOutput = .true. enddo + if (bAnyGridOutput) then + ! Build the wake segments/tree once and reuse it for all grids below + call InducedVelocitiesAll_Init(p, x, m, m%Sgmt, m%Part, Tree, Panl, ErrStat2, ErrMsg2, allocPart=.false.) + call SetErrStat(ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName) + do iGrid=1,p%nGridOut + bWithinTime = t>=m%GridOutputs(iGrid)%tStart-p%DTaero/2. .and. t<= m%GridOutputs(iGrid)%tEnd+p%DTaero/2. + bTimeToOutput = ( t - m%GridOutputs(iGrid)%tLastOutput) >= m%GridOutputs(iGrid)%DTout - 0.25_DbKi*p%DTaero + if (force .or. (bWithinTime .and. bTimeToOutput) ) then + ! Compute induced velocity on grid, reusing the wake tree built once + call InducedVelocitiesAll_OnGrid_Calc(m%GridOutputs(iGrid), p, m%Sgmt, m%Part, Tree, Panl, ErrStat2, ErrMsg2); + call SetErrStat(ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName) + m%GridOutputs(iGrid)%tLastOutput = t + call WrVTK_FVW_Grid(p, m, iGrid, trim(p%VTK_OutFileBase)//'FVW_Grid', VTKstep, 9) + m%VTKstep=VTKstep ! We save the step at which writing occurred + endif + enddo + call InducedVelocitiesAll_End(p, Tree, m%Part, Panl, ErrStat2, ErrMsg2, deallocPart=.false.) + call SetErrStat(ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName) + endif endif if (OLAF_PROFILING) call toc() end subroutine WriteVTKOutputs From b1e2954d1b9874bbad59fc1bb83f6e203b700318 Mon Sep 17 00:00:00 2001 From: Roger Bergua Date: Fri, 18 Sep 2026 12:40:06 -0600 Subject: [PATCH 13/24] FVW: Streamline VTK grid-output condition --- modules/aerodyn/src/FVW.f90 | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/modules/aerodyn/src/FVW.f90 b/modules/aerodyn/src/FVW.f90 index 64925a563f..bb04ea5f32 100644 --- a/modules/aerodyn/src/FVW.f90 +++ b/modules/aerodyn/src/FVW.f90 @@ -1568,7 +1568,7 @@ subroutine WriteVTKOutputs(t, force, VTKstep, u, p, x, z, m, ErrStat, ErrMsg) character(*), parameter :: RoutineName = 'WriteVTKOutputs' integer(IntKi) :: iW, iGrid integer(IntKi) :: nSeg, nSegP - logical :: bAnyGridOutput + logical, allocatable :: bDoGrid(:) ! Flag per output grid: .true. if it needs to be written now type(T_Tree) :: Tree type(T_Panl) :: Panl ErrStat = ErrID_None @@ -1613,23 +1613,21 @@ subroutine WriteVTKOutputs(t, force, VTKstep, u, p, x, z, m, ErrStat, ErrMsg) ! Distribute the Wind we requested to Inflow wind to storage Misc arrays ! TODO ANDY: replace with direct call to inflow wind at Grid points CALL DistributeRequestedWind_Grid(u%V_wind, p, m) - ! Check ahead of time whether any grid needs output now, to avoid rebuilding the wake tree if not necessary - bAnyGridOutput = force + ! Compute once which grids are due for output now + allocate(bDoGrid(p%nGridOut)) do iGrid=1,p%nGridOut bWithinTime = t>=m%GridOutputs(iGrid)%tStart-p%DTaero/2. .and. t<= m%GridOutputs(iGrid)%tEnd+p%DTaero/2. bTimeToOutput = ( t - m%GridOutputs(iGrid)%tLastOutput) >= m%GridOutputs(iGrid)%DTout - 0.25_DbKi*p%DTaero - if (bWithinTime .and. bTimeToOutput) bAnyGridOutput = .true. + bDoGrid(iGrid) = force .or. (bWithinTime .and. bTimeToOutput) enddo - if (bAnyGridOutput) then + if (any(bDoGrid)) then ! Build the wake segments/tree once and reuse it for all grids below call InducedVelocitiesAll_Init(p, x, m, m%Sgmt, m%Part, Tree, Panl, ErrStat2, ErrMsg2, allocPart=.false.) call SetErrStat(ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName) do iGrid=1,p%nGridOut - bWithinTime = t>=m%GridOutputs(iGrid)%tStart-p%DTaero/2. .and. t<= m%GridOutputs(iGrid)%tEnd+p%DTaero/2. - bTimeToOutput = ( t - m%GridOutputs(iGrid)%tLastOutput) >= m%GridOutputs(iGrid)%DTout - 0.25_DbKi*p%DTaero - if (force .or. (bWithinTime .and. bTimeToOutput) ) then + if (bDoGrid(iGrid)) then ! Compute induced velocity on grid, reusing the wake tree built once - call InducedVelocitiesAll_OnGrid_Calc(m%GridOutputs(iGrid), p, m%Sgmt, m%Part, Tree, Panl, ErrStat2, ErrMsg2); + call InducedVelocitiesAll_OnGrid_Calc(m%GridOutputs(iGrid), p, m%Sgmt, m%Part, Tree, Panl, ErrStat2, ErrMsg2) call SetErrStat(ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName) m%GridOutputs(iGrid)%tLastOutput = t call WrVTK_FVW_Grid(p, m, iGrid, trim(p%VTK_OutFileBase)//'FVW_Grid', VTKstep, 9) @@ -1639,6 +1637,7 @@ subroutine WriteVTKOutputs(t, force, VTKstep, u, p, x, z, m, ErrStat, ErrMsg) call InducedVelocitiesAll_End(p, Tree, m%Part, Panl, ErrStat2, ErrMsg2, deallocPart=.false.) call SetErrStat(ErrStat2, ErrMsg2, ErrStat, ErrMsg, RoutineName) endif + deallocate(bDoGrid) endif if (OLAF_PROFILING) call toc() end subroutine WriteVTKOutputs From aa22ddca92444536596889f1f2a5dfb56887026c Mon Sep 17 00:00:00 2001 From: Mohammad Salik Date: Fri, 18 Sep 2026 21:36:43 +0200 Subject: [PATCH 14/24] AD: refresh wind rotations for direction linearization --- modules/aerodyn/src/AeroDyn.f90 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/modules/aerodyn/src/AeroDyn.f90 b/modules/aerodyn/src/AeroDyn.f90 index e93e18a713..c5d3aefc09 100644 --- a/modules/aerodyn/src/AeroDyn.f90 +++ b/modules/aerodyn/src/AeroDyn.f90 @@ -6564,6 +6564,9 @@ subroutine PerturbFlowField(Var, BaseFF, PerturbSign, PerturbFF) PerturbFF%Uniform%VelH = BaseFF%Uniform%VelH PerturbFF%Uniform%ShrV = BaseFF%Uniform%ShrV PerturbFF%PropagationDir = BaseFF%PropagationDir + PerturbFF%RotToWind = BaseFF%RotToWind + PerturbFF%RotFromWind = BaseFF%RotFromWind + PerturbFF%RotateWindBox = BaseFF%RotateWindBox select case (Var%DL%Num) case (AD_u_HWindSpeed) PerturbFF%Uniform%VelH = BaseFF%Uniform%VelH + Var%Perturb*PerturbSign @@ -6571,6 +6574,15 @@ subroutine PerturbFlowField(Var, BaseFF, PerturbSign, PerturbFF) PerturbFF%Uniform%ShrV = BaseFF%Uniform%ShrV + Var%Perturb*PerturbSign case (AD_u_PropagationDir) PerturbFF%PropagationDir = BaseFF%PropagationDir + Var%Perturb*PerturbSign + PerturbFF%RotToWind(1,:) = [ & + cos(-PerturbFF%VFlowAngle)*cos(-PerturbFF%PropagationDir), & + cos(-PerturbFF%VFlowAngle)*sin(-PerturbFF%PropagationDir), -sin(-PerturbFF%VFlowAngle)] + PerturbFF%RotToWind(2,:) = [-sin(-PerturbFF%PropagationDir), cos(-PerturbFF%PropagationDir), 0.0_ReKi] + PerturbFF%RotToWind(3,:) = [ & + sin(-PerturbFF%VFlowAngle)*cos(-PerturbFF%PropagationDir), & + sin(-PerturbFF%VFlowAngle)*sin(-PerturbFF%PropagationDir), cos(-PerturbFF%VFlowAngle)] + PerturbFF%RotFromWind = transpose(PerturbFF%RotToWind) + PerturbFF%RotateWindBox = .true. end select end subroutine From 316018d320fc3bcf468c7033a8b2741d5f867c6f Mon Sep 17 00:00:00 2001 From: andrew-platt Date: Fri, 18 Sep 2026 13:53:52 -0600 Subject: [PATCH 15/24] Add GitHub action to check Fortran source for tab characters OpenFAST style requires 3 space indentation in Fortran source; tab characters are not allowed because they render inconsistently across editors and break continuation-line alignment. Add a workflow that scans every git-tracked *.f90 / *.F90 file and reports the file, line, and column of each tab found, failing the job if any are present. Findings are also emitted as GitHub annotations so they appear inline on the pull request diff. Registry-generated *_Types.f90 files are excluded from the scan, since they are produced by the OpenFAST Registry rather than edited by hand. Also remove the 7 pre-existing tab characters so the check passes on a clean tree. All were in comments or whitespace-only lines, so there is no change in behavior. Co-Authored-By: Claude Opus 5 (1M context) --- .github/scripts/check_tabs.sh | 73 +++++++++++++++++++ .github/workflows/check-tabs.yml | 34 +++++++++ modules/lindyn/src/LinDyn.f90 | 6 +- modules/moordyn/src/MoorDyn_Line.f90 | 2 +- .../Test_MeshMapping/Test_MeshMapping_Mod.f90 | 4 +- modules/nwtc-library/src/NWTC_Num.f90 | 2 +- 6 files changed, 114 insertions(+), 7 deletions(-) create mode 100755 .github/scripts/check_tabs.sh create mode 100644 .github/workflows/check-tabs.yml diff --git a/.github/scripts/check_tabs.sh b/.github/scripts/check_tabs.sh new file mode 100755 index 0000000000..cffd879cde --- /dev/null +++ b/.github/scripts/check_tabs.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# +# check_tabs.sh -- verify that no Fortran source file contains tab characters. +# +# OpenFAST style requires 3-space indentation; tab characters are not allowed +# because they render inconsistently across editors and break the alignment of +# continuation lines. +# +# Scans every git-tracked *.f90 / *.F90 file. Registry-generated *_Types.f90 +# files are excluded, since they are produced by the OpenFAST Registry rather +# than edited by hand. +# +# Exits 0 when clean, 1 when any tab character is found. +# +set -uo pipefail + +TAB=$(printf '\t') +MARK='--->' + +cd "$(git rev-parse --show-toplevel)" || exit 1 + +echo "OpenFAST Fortran source style check" +echo "===================================" +echo +echo "OpenFAST style requires 3 space indentation in all Fortran source files." +echo "Tab characters are not allowed." +echo + +mapfile -t FILES < <(git ls-files -- '*.f90' '*.F90' | grep -v '_Types\.f90$' | sort) + +if [ "${#FILES[@]}" -eq 0 ]; then + echo "ERROR: no Fortran source files found -- is this a git checkout?" + exit 1 +fi + +n_bad_files=0 +n_bad_lines=0 + +for file in "${FILES[@]}"; do + matches=$(grep -n -- "$TAB" "$file") || continue + + n_bad_files=$((n_bad_files + 1)) + echo "$file" + + while IFS= read -r match; do + lineno=${match%%:*} + content=${match#*:} + column=$(awk -v s="$content" 'BEGIN { print index(s, "\t") }') + n_bad_lines=$((n_bad_lines + 1)) + + printf ' line %s, column %s: %s\n' "$lineno" "$column" "${content//$TAB/$MARK}" + + # Inline annotation on the pull request diff. + if [ "${GITHUB_ACTIONS:-}" = "true" ]; then + echo "::error file=${file},line=${lineno},col=${column}::Tab character found. Use 3 space indentation; tabs are not allowed." + fi + done <<< "$matches" + + echo +done + +echo "Checked ${#FILES[@]} Fortran source file(s)." + +if [ "$n_bad_files" -gt 0 ]; then + echo + echo "ERROR: found ${n_bad_lines} line(s) containing tab characters in ${n_bad_files} file(s)." + echo " Replace each tab with spaces (3 space indentation) and commit the fix." + echo " Tabs above are shown as '${MARK}'." + exit 1 +fi + +echo "No tab characters found." +exit 0 diff --git a/.github/workflows/check-tabs.yml b/.github/workflows/check-tabs.yml new file mode 100644 index 0000000000..b15cc04a92 --- /dev/null +++ b/.github/workflows/check-tabs.yml @@ -0,0 +1,34 @@ + +name: 'Fortran Source Style' + +#------------------------------------------------------------------------------- +# Notes +# - OpenFAST requires 3 space indentation in Fortran source; tab characters are +# not allowed. This workflow enforces that rule on all git-tracked *.f90 and +# *.F90 files. +# - Registry-generated *_Types.f90 files are excluded by the check script. +#------------------------------------------------------------------------------- + +on: + push: + paths: + - '**.f90' + - '**.F90' + - '.github/workflows/check-tabs.yml' + - '.github/scripts/check_tabs.sh' + + pull_request: + types: [opened, synchronize, edited, reopened] + +jobs: + check-tabs: + name: Check for tab characters + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: false + + - name: Check Fortran source for tab characters + run: .github/scripts/check_tabs.sh diff --git a/modules/lindyn/src/LinDyn.f90 b/modules/lindyn/src/LinDyn.f90 index 799c0d2fd3..22d6ce6e2f 100644 --- a/modules/lindyn/src/LinDyn.f90 +++ b/modules/lindyn/src/LinDyn.f90 @@ -724,9 +724,9 @@ subroutine Init_Lin(p, InitOut, errStat, errMsg) ! LinNames_y {:} - - "Names of the outputs used in linearization" - ! LinNames_x {:} - - "Names of the continuous states used in linearization" - ! LinNames_u {:} - - "Names of the inputs used in linearization" - -! RotFrame_y {:} - - "Flag that tells FAST/MBC3 if the outputs used in linearization are in the rotating frame" - -! RotFrame_x {:} - - "Flag that tells FAST/MBC3 if the continuous states used in linearization are in the rotating frame" - -! RotFrame_u {:} - - "Flag that tells FAST/MBC3 if the inputs used in linearization are in the rotating frame" - +! RotFrame_y {:} - - "Flag that tells FAST/MBC3 if the outputs used in linearization are in the rotating frame" - +! RotFrame_x {:} - - "Flag that tells FAST/MBC3 if the continuous states used in linearization are in the rotating frame" - +! RotFrame_u {:} - - "Flag that tells FAST/MBC3 if the inputs used in linearization are in the rotating frame" - ! IsLoad_u {:} - - "Flag that tells FAST if the inputs used in linearization are loads (for preconditioning matrix)" - ! DerivOrder_x {:} - - "Integer that tells FAST/MBC3 the maximum derivative order of continuous states used in linearization" - !Appropriate Jacobian row/column names and rotating-frame flags here: diff --git a/modules/moordyn/src/MoorDyn_Line.f90 b/modules/moordyn/src/MoorDyn_Line.f90 index affb532d31..e36be23107 100644 --- a/modules/moordyn/src/MoorDyn_Line.f90 +++ b/modules/moordyn/src/MoorDyn_Line.f90 @@ -1778,7 +1778,7 @@ SUBROUTINE Line_GetStateDeriv(Line, Xd, m, p, ErrStat, ErrMsg) !, FairFtot, Fai ! DO I = N-4, N ! print*, "I =", I, "rdd_old =", Line%rdd_old(:,I) ! enddo - ! endif + ! endif ! check for NaNs DO J = 1, 6*(N-1) diff --git a/modules/nwtc-library/Old_test/Test_MeshMapping/Test_MeshMapping_Mod.f90 b/modules/nwtc-library/Old_test/Test_MeshMapping/Test_MeshMapping_Mod.f90 index c770a5ab68..3e04d66484 100644 --- a/modules/nwtc-library/Old_test/Test_MeshMapping/Test_MeshMapping_Mod.f90 +++ b/modules/nwtc-library/Old_test/Test_MeshMapping/Test_MeshMapping_Mod.f90 @@ -1257,7 +1257,7 @@ subroutine CreateOutputMeshes_Test10 Mesh1_O%TranslationVel(:,3) = (/-0.000275392638286576, 1.26055965665728e-09,-0.00660990970209241/) Mesh1_O%TranslationVel(:,4) = (/-0.000295042205834761,-5.36239895154722e-07,-0.00463324552401900/) Mesh1_O%TranslationVel(:,5) = (/ 0, 0, 0 /) - + Mesh1_O%RotationVel(:,1) = (/2.96345683636901e-06,-2.34051149163861e-05,-5.91553508400011e-08 /) Mesh1_O%RotationVel(:,2) = (/4.80349626741372e-09, 2.67697760136798e-05, 2.29704255616525e-09 /) Mesh1_O%RotationVel(:,3) = (/1.04646687759669e-07,-4.83136973343790e-05,-1.12983400413214e-09 /) @@ -1275,7 +1275,7 @@ subroutine CreateOutputMeshes_Test10 Mesh1_O%RotationAcc(:,3) = (/ 0.000204067997401580,-0.0911907851696014, -1.95350694411900e-05/) Mesh1_O%RotationAcc(:,4) = (/ 7.36967340344563e-05, 0.0997802764177322, 7.97090106061660e-05/) Mesh1_O%RotationAcc(:,5) = (/ 0, 0, 0 /) - + ! test: diff --git a/modules/nwtc-library/src/NWTC_Num.f90 b/modules/nwtc-library/src/NWTC_Num.f90 index c07c5aad35..70b84d4e5a 100644 --- a/modules/nwtc-library/src/NWTC_Num.f90 +++ b/modules/nwtc-library/src/NWTC_Num.f90 @@ -1077,7 +1077,7 @@ END SUBROUTINE CubicSplineInterpM !! 0 & \lambda_3 & -\lambda_2 \\ !! -\lambda_3 & 0 & \lambda_1 \\ !! \lambda_2 & -\lambda_1 & 0 -!! \end{bmatrix} +!! \end{bmatrix} !! \f} !! The angle of rotation for \f$\lambda\f$ is !! \f{equation}{ \theta = \sqrt{{\lambda_1}^2+{\lambda_2}^2+{\lambda_3}^2} \f} From bf06cde9058d7be2c33f32a7cce98031b81413f8 Mon Sep 17 00:00:00 2001 From: andrew-platt Date: Fri, 18 Sep 2026 14:00:23 -0600 Subject: [PATCH 16/24] StrucCtrl: remove tabs from omega_P description string The omega_P description in StrucCtrl_Registry.txt contained tab characters, which the Registry copied verbatim into the generated Doxygen comment in StrucCtrl_Types.f90. Replace them with spaces, matching the alignment of the neighboring alpha_P entry, and regenerate StrucCtrl_Types.f90. Comment text only; no change in behavior. Assisted-by: LLM [Claude Code] [Copilot] --- modules/servodyn/src/StrucCtrl_Registry.txt | 2 +- modules/servodyn/src/StrucCtrl_Types.f90 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/servodyn/src/StrucCtrl_Registry.txt b/modules/servodyn/src/StrucCtrl_Registry.txt index 9329bc07d0..f8d3245418 100644 --- a/modules/servodyn/src/StrucCtrl_Registry.txt +++ b/modules/servodyn/src/StrucCtrl_Registry.txt @@ -138,7 +138,7 @@ typedef ^ MiscVarType ReKi a_G {:}{:} - - "Gravitational acceleration vecto typedef ^ MiscVarType ReKi rdisp_P {:}{:} - - "Translational displacement vector, local coordinates for point" m typedef ^ MiscVarType ReKi rdot_P {:}{:} - - "Translational velocity vector, local coordinates for point" m/s typedef ^ MiscVarType ReKi rddot_P {:}{:} - - "Translational acceleration vector, local coordinates for point" m/s^2 -typedef ^ MiscVarType ReKi omega_P {:}{:} - - "Rotational velocity vector, local coordinates for point" rad/s +typedef ^ MiscVarType ReKi omega_P {:}{:} - - "Rotational velocity vector, local coordinates for point" rad/s typedef ^ MiscVarType ReKi alpha_P {:}{:} - - "Rotational aceeleration vector, local coordinates for point" rad/s^2 typedef ^ MiscVarType ReKi F_P {:}{:} - - "StC force vector, local coordinates for point" N typedef ^ MiscVarType ReKi M_P {:}{:} - - "StC moment vector, local coordinates for point" N-m diff --git a/modules/servodyn/src/StrucCtrl_Types.f90 b/modules/servodyn/src/StrucCtrl_Types.f90 index c25d951de6..6031746333 100644 --- a/modules/servodyn/src/StrucCtrl_Types.f90 +++ b/modules/servodyn/src/StrucCtrl_Types.f90 @@ -176,7 +176,7 @@ MODULE StrucCtrl_Types REAL(ReKi) , DIMENSION(:,:), ALLOCATABLE :: rdisp_P !< Translational displacement vector, local coordinates for point [m] REAL(ReKi) , DIMENSION(:,:), ALLOCATABLE :: rdot_P !< Translational velocity vector, local coordinates for point [m/s] REAL(ReKi) , DIMENSION(:,:), ALLOCATABLE :: rddot_P !< Translational acceleration vector, local coordinates for point [m/s^2] - REAL(ReKi) , DIMENSION(:,:), ALLOCATABLE :: omega_P !< Rotational velocity vector, local coordinates for point [rad/s] + REAL(ReKi) , DIMENSION(:,:), ALLOCATABLE :: omega_P !< Rotational velocity vector, local coordinates for point [rad/s] REAL(ReKi) , DIMENSION(:,:), ALLOCATABLE :: alpha_P !< Rotational aceeleration vector, local coordinates for point [rad/s^2] REAL(ReKi) , DIMENSION(:,:), ALLOCATABLE :: F_P !< StC force vector, local coordinates for point [N] REAL(ReKi) , DIMENSION(:,:), ALLOCATABLE :: M_P !< StC moment vector, local coordinates for point [N-m] From a42c07d8938104f630b30be39933e1730cb0dcfa Mon Sep 17 00:00:00 2001 From: andrew-platt Date: Fri, 18 Sep 2026 13:33:57 -0600 Subject: [PATCH 17/24] Replace tabs with spaces per format requirement --- modules/inflowwind/src/IfW_FlowField.f90 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/inflowwind/src/IfW_FlowField.f90 b/modules/inflowwind/src/IfW_FlowField.f90 index e4c9427e8c..4684f346f7 100644 --- a/modules/inflowwind/src/IfW_FlowField.f90 +++ b/modules/inflowwind/src/IfW_FlowField.f90 @@ -1290,9 +1290,9 @@ subroutine GetBoundsT(PosX, DT) if (IT_LO >= 1 .and. IT_HI <= G3D%NSteps) then ! Point is within grid else if (IT_LO == 0 .and. T_GRID >= -GridTol) then - ! Within tolerance of the first time step - IT_LO = 1 - IT_HI = 1 + ! Within tolerance of the first time step + IT_LO = 1 + IT_HI = 1 DT = -1.0_ReKi else if (IT_LO == G3D%NSteps) then if (G3D%Periodic) then From 6f85fe77f68e70397a2bd27030e4913b2185a916 Mon Sep 17 00:00:00 2001 From: andrew-platt Date: Fri, 18 Sep 2026 14:36:35 -0600 Subject: [PATCH 18/24] Address PR review: fail the tab check on scan errors Two issues raised in review of the tab check script: - The repository-root guard was ineffective. When "git rev-parse --show-toplevel" fails it produces no output, and "cd ''" returns 0 and stays in the current directory, so the "|| exit 1" never fired and the script ran on whatever directory it was invoked from. Capture the root first and test that command's status directly. - "grep ... || continue" treated an I/O error (exit 2) the same as "no match" (exit 1), so a file that failed to scan was silently skipped and the job still passed. Skip only on exit 1 and abort on any other non-zero status. Introduce a distinct exit status so a check that could not run never resembles a clean pass: 0 no tabs, 1 tabs found, 2 check could not run. Assisted-by: LLM [Claude Code] [Copilot] --- .github/scripts/check_tabs.sh | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/scripts/check_tabs.sh b/.github/scripts/check_tabs.sh index cffd879cde..70d7a42a3f 100755 --- a/.github/scripts/check_tabs.sh +++ b/.github/scripts/check_tabs.sh @@ -10,14 +10,21 @@ # files are excluded, since they are produced by the OpenFAST Registry rather # than edited by hand. # -# Exits 0 when clean, 1 when any tab character is found. +# Exit status: +# 0 no tab characters found +# 1 one or more tab characters found +# 2 the check could not run (not a git checkout, or a file failed to scan) # set -uo pipefail TAB=$(printf '\t') MARK='--->' -cd "$(git rev-parse --show-toplevel)" || exit 1 +repo_root=$(git rev-parse --show-toplevel) || { + echo "ERROR: not inside a git checkout; cannot determine repository root." + exit 2 +} +cd "$repo_root" || exit 2 echo "OpenFAST Fortran source style check" echo "===================================" @@ -30,14 +37,23 @@ mapfile -t FILES < <(git ls-files -- '*.f90' '*.F90' | grep -v '_Types\.f90$' | if [ "${#FILES[@]}" -eq 0 ]; then echo "ERROR: no Fortran source files found -- is this a git checkout?" - exit 1 + exit 2 fi n_bad_files=0 n_bad_lines=0 for file in "${FILES[@]}"; do - matches=$(grep -n -- "$TAB" "$file") || continue + # grep exits 0 on a match, 1 on no match, and >1 on an actual error. + # Only "no match" may be skipped; a scan failure must not look like a pass. + matches=$(grep -n -- "$TAB" "$file") + status=$? + if [ "$status" -eq 1 ]; then + continue + elif [ "$status" -ne 0 ]; then + echo "ERROR: failed to scan ${file} (grep exit ${status})." + exit 2 + fi n_bad_files=$((n_bad_files + 1)) echo "$file" From 2ac51e19ed8f4a68216983c24b32cb74da6a7759 Mon Sep 17 00:00:00 2001 From: Roger Bergua Date: Mon, 21 Sep 2026 13:26:09 -0600 Subject: [PATCH 19/24] Remove leftover assignment --- modules/aerodyn/src/FVW.f90 | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/aerodyn/src/FVW.f90 b/modules/aerodyn/src/FVW.f90 index bb04ea5f32..4df398a6d6 100644 --- a/modules/aerodyn/src/FVW.f90 +++ b/modules/aerodyn/src/FVW.f90 @@ -453,7 +453,6 @@ SUBROUTINE FVW_SetParametersFromInputFile( InputFileData, p, ErrStat, ErrMsg ) p%FWShedVorticity = InputFileData%FWShedVorticity p%DiffusionMethod = InputFileData%DiffusionMethod p%RegFunction = InputFileData%RegFunction - p%RegFunctionPart = InputFileData%RegFunctionPart p%RegDeterMethod = InputFileData%RegDeterMethod p%WakeRegMethod = InputFileData%WakeRegMethod p%WakeRegParam = InputFileData%WakeRegParam From 9dd8ea86f4e7b41eff20cf8f96ca60abbf187966 Mon Sep 17 00:00:00 2001 From: Lu Wang Date: Mon, 21 Sep 2026 13:46:24 -0600 Subject: [PATCH 20/24] Fix missing regularization core on mirrored FVW segments (ShearModel=1) In PackPanelsToSegments, the ground-mirror block reflected segment points and negated circulation but never assigned SegEpsilon for the mirrored half. The image segments therefore kept the -999999 sentinel core size from allocation, which silently disabled or corrupted their Biot-Savart contribution (segment/particle kernels collapse to ~0 for a negative core, and the treecode MAC floor degenerates), so ground effect under ShearModel=1 was under-delivered rather than crashing. Copy the regularization core from each real segment to its image (SegEpsilon(iMirror) = SegEpsilon(i)), which is physically exact since an image vortex shares the core size of its source. Co-authored-by: GitHub Copilot Co-authored-by: Claude --- modules/aerodyn/src/FVW_Subs.f90 | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/aerodyn/src/FVW_Subs.f90 b/modules/aerodyn/src/FVW_Subs.f90 index 7f295802fd..1c29dfad22 100644 --- a/modules/aerodyn/src/FVW_Subs.f90 +++ b/modules/aerodyn/src/FVW_Subs.f90 @@ -980,6 +980,7 @@ subroutine PackPanelsToSegments(p, x, iDepthStart, bMirror, nNW, nFW, SegConnct, SegConnct(1:2, iMirror) = SegConnct(1:2, i) + nSegP ! Increased point indices SegConnct(3:4, iMirror) = SegConnct(3:4, i) ! Span and age is copied SegGamma(iMirror) = -SegGamma(i) ! Vorticity needs mirroring + SegEpsilon(iMirror) = SegEpsilon(i) ! Regularization (core size) is copied enddo do i=1,nSegP iMirror = i + nSegP From 718f1df1bbe2d8ff13ffb25ffd7e3557520d04c3 Mon Sep 17 00:00:00 2001 From: andrew-platt Date: Tue, 22 Sep 2026 15:10:29 -0600 Subject: [PATCH 21/24] Address PR review: scan all Fortran sources, fix column reporting Two further issues raised in review: - Excluding files by the "*_Types.f90" name pattern was wrong in both directions. It skipped six hand-maintained sources that happen to use that name, among them TurbSim_Types.f90 and ModMesh_Types.f90, so tabs in them could never be caught; and it still scanned NWTC_Library_IncSubs.f90, which is Registry-generated but does not match the pattern. Drop the exclusion and scan every tracked *.f90 / *.F90 file. When a tab is found in a file carrying the Registry marker, the report now says so and points at the registry .txt input, since that is where such a tab has to be fixed. - The reported column could be wrong. "awk -v" decodes backslash escapes in the assigned value, so any line containing a backslash -- Doxygen markup such as \f$ or \end{}, or a format string -- shifted the computed index. Compute the column with parameter expansion instead, which does no decoding. Line numbers were never affected. Coverage goes from 267 to 329 scanned files. Assisted-by: LLM [Claude Code] [Copilot] --- .github/scripts/check_tabs.sh | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/scripts/check_tabs.sh b/.github/scripts/check_tabs.sh index 70d7a42a3f..12efaa92c5 100755 --- a/.github/scripts/check_tabs.sh +++ b/.github/scripts/check_tabs.sh @@ -6,9 +6,10 @@ # because they render inconsistently across editors and break the alignment of # continuation lines. # -# Scans every git-tracked *.f90 / *.F90 file. Registry-generated *_Types.f90 -# files are excluded, since they are produced by the OpenFAST Registry rather -# than edited by hand. +# Scans every git-tracked *.f90 / *.F90 file, with no exclusions. When a tab is +# found in a file generated by the OpenFAST Registry, the report says so and +# points at the registry input, since the fix belongs in the .txt rather than in +# the generated Fortran. # # Exit status: # 0 no tab characters found @@ -33,7 +34,7 @@ echo "OpenFAST style requires 3 space indentation in all Fortran source files." echo "Tab characters are not allowed." echo -mapfile -t FILES < <(git ls-files -- '*.f90' '*.F90' | grep -v '_Types\.f90$' | sort) +mapfile -t FILES < <(git ls-files -- '*.f90' '*.F90' | sort) if [ "${#FILES[@]}" -eq 0 ]; then echo "ERROR: no Fortran source files found -- is this a git checkout?" @@ -61,7 +62,12 @@ for file in "${FILES[@]}"; do while IFS= read -r match; do lineno=${match%%:*} content=${match#*:} - column=$(awk -v s="$content" 'BEGIN { print index(s, "\t") }') + + # Compute the column with parameter expansion rather than awk: "awk -v" + # decodes backslash escapes in the assigned value, which shifts the index + # on any line containing a backslash (Doxygen markup, format strings). + prefix=${content%%$TAB*} + column=$(( ${#prefix} + 1 )) n_bad_lines=$((n_bad_lines + 1)) printf ' line %s, column %s: %s\n' "$lineno" "$column" "${content//$TAB/$MARK}" @@ -72,6 +78,15 @@ for file in "${FILES[@]}"; do fi done <<< "$matches" + # A tab in a Registry-generated file cannot be fixed in place: it was copied + # from a description string in the registry input, so point the developer + # there instead. + if head -n 1 "$file" 2>/dev/null | grep -q 'STARTOFREGISTRYGENERATEDFILE'; then + echo " NOTE: this file is generated by the OpenFAST Registry. Fix the tab in" + echo " the corresponding registry input (.txt) in the same directory," + echo " then regenerate this file." + fi + echo done From a63ded4e01c592d5c2fe079dd69147e3ad910443 Mon Sep 17 00:00:00 2001 From: andrew-platt Date: Tue, 22 Sep 2026 15:41:33 -0600 Subject: [PATCH 22/24] Address PR review: correct stale exclusion note in the workflow The workflow header still claimed that Registry-generated *_Types.f90 files are excluded by the check script. That has not been true since 718f1df1b, which dropped the exclusion because the name pattern also skipped six hand-maintained sources. Comment only; no change to the workflow's behavior. Assisted-by: LLM [Claude Code] [Copilot] --- .github/workflows/check-tabs.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check-tabs.yml b/.github/workflows/check-tabs.yml index b15cc04a92..957d063070 100644 --- a/.github/workflows/check-tabs.yml +++ b/.github/workflows/check-tabs.yml @@ -6,7 +6,9 @@ name: 'Fortran Source Style' # - OpenFAST requires 3 space indentation in Fortran source; tab characters are # not allowed. This workflow enforces that rule on all git-tracked *.f90 and # *.F90 files. -# - Registry-generated *_Types.f90 files are excluded by the check script. +# - No files are excluded from the scan. A tab in a Registry-generated file is +# reported with a note pointing at the registry .txt input, since that is +# where it has to be fixed. #------------------------------------------------------------------------------- on: From 6e5f8942c3169706e92ecd87313a339c59fa330b Mon Sep 17 00:00:00 2001 From: andrew-platt Date: Wed, 23 Sep 2026 14:44:53 -0600 Subject: [PATCH 23/24] Update regression tests with aero after OF PR #3468 --- reg_tests/r-test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reg_tests/r-test b/reg_tests/r-test index 630015613d..2aa1664832 160000 --- a/reg_tests/r-test +++ b/reg_tests/r-test @@ -1 +1 @@ -Subproject commit 630015613d3fd03f21add529180d3c3edfcbbd76 +Subproject commit 2aa1664832f47d6c1286583b33c36bf4dfe1b80b From b21a20b6c18c9ac8d94b238800be589833bd0ccb Mon Sep 17 00:00:00 2001 From: andrew-platt Date: Thu, 24 Sep 2026 15:54:58 -0600 Subject: [PATCH 24/24] FVW: import T_Tree explicitly from FVW_VortexTools WriteVTKOutputs declares type(T_Tree), which currently resolves only indirectly through the bare `use FVW_Subs`, which in turn does a bare `use FVW_VortexTools`. That works today, but breaks at a distance if FVW_Subs ever gains a private default or narrows its own use clause. Add T_Tree to the existing only-list so the dependency is explicit. No functional change. Co-Authored-By: Claude Opus 5 (1M context) --- modules/aerodyn/src/FVW.f90 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/aerodyn/src/FVW.f90 b/modules/aerodyn/src/FVW.f90 index 9cc517d362..15da86e339 100644 --- a/modules/aerodyn/src/FVW.f90 +++ b/modules/aerodyn/src/FVW.f90 @@ -14,7 +14,7 @@ module FVW use FVW_IO use FVW_Wings use FVW_BiotSavart - use FVW_VortexTools, only: tic, toc + use FVW_VortexTools, only: tic, toc, T_Tree use FVW_Tests use AirFoilInfo