F/reverse rotor - #3436
Draft
andrew-platt wants to merge 25 commits into
Draft
Conversation
Points at r-test commit baef6e291, which sets MirrorRotor to F in the three multi-rotor cases that previously set "F T". The flag is currently a parsing-only stub (FAST_Subs.f90:3080-3083), so results are unchanged. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ment-mismatch) Debug builds with gfortran fail to compile fftpack5.1.f with 64 hard errors: Error: Type mismatch in argument 'dsum' at (1); passed REAL(4) to REAL(8) Error: Type mismatch in argument 'c' at (1); passed REAL(4) to COMPLEX(4) These mismatches are intentional: FFTPACK 5.1 uses legacy Fortran 77 type punning and is deliberately compiled with -fno-default-real-8 while the rest of the build uses -fdefault-real-8, so its callers can pass explicit SiKi/R4Ki arrays. -fallow-argument-mismatch is already applied to downgrade them to warnings. However, Debug builds add -pedantic, which promotes the argument mismatches back to errors and overrides -fallow-argument-mismatch. Appending -Wno-pedantic to this file's compile flags restores the intended behaviour. The suppression is scoped to this single legacy source; Release builds are unaffected as they do not pass -pedantic. Verified on gfortran 12.2.0 with cmake -DDOUBLE_PRECISION=On -DCMAKE_BUILD_TYPE=Debug -DGENERATE_TYPES=On .. Isolation test on the exact compile line: -pedantic -> 64 errors -pedantic -std=legacy -> 64 errors -pedantic -Wno-argument-mismatch -> 64 errors -pedantic -Wno-pedantic -> 0 errors (object builds) Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Adds the first working slice of counter-clockwise rotor support. A new RotInitInputType flag MirrorRotor selects a RotParameterType RotDir of -1, which is applied only at the AeroDyn module boundary. BEMT, unsteady aero, DBEMT and AirfoilInfo are untouched and continue to solve the equivalent clockwise problem, so airfoil polars are used verbatim and no polar transformation is needed. Applied on the way in (SetInputsForBEMT): omega, psi_s, theta, toeAngle, Vy and omega_z. The cant angle is deliberately not flipped. Blade twist and swept-AC offset are negated during init, after the setCantAngle loop, because setCantAngle derives BlCrvAng from BlTwist and the cant must not follow the mirror. Applied on the way out: the y-component of forces and the x and z components of moments, following v' = Sv for true vectors and w' = -Sw for pseudovectors with S = diag(1,-1,1). Several write-outputs mixed the two frames and are corrected here. Rotor and blade power used the clockwise omega against a mirrored Mxh, which produced negative power. BNFl, BNFd, BNFn and BNFt combined mirrored forces with clockwise phi and theta. BNVIndy multiplied two clockwise quantities. BNCy, BNCt and BNCm now carry the mirror. RtTSR keeps the clockwise omega, which is the value that stays positive. The free-wake copy of the blade output block is left alone; OLAF remains guard-railed. The AeroDyn driver gains a per-turbine MirrorRotor(i) input and mirrors the prescribed hub kinematics, standing in for ElastoDyn. Verified against a paired clockwise/mirrored NREL 5MW driver case: every one of the 150 output channels is exactly equal, exactly sign-flipped, a mirrored angle, or below the numerical noise floor, with zero residual. BEMT internals including Alpha, Phi, Vrel, Cl, Cd and the induction factors are bit-identical between the two runs. MirrorRotor defaults to false and the clockwise path is unchanged; the AeroDyn driver regression cases reproduce their baselines bit-for-bit. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A condition sweep over the AeroDyn driver (pitch, wind speed, tip-speed ratio, propeller-brake, tilt, precone, shear, yaw, both BEM models, DBEMT and four unsteady-aero models) exposed four sign errors that the original axisymmetric zero-pitch check could not reach. Blade pitch was not mirrored by the driver. Blade twist is negated when the inputs are read, so with a non-zero pitch the two ended up in opposite frames and the local angle of attack was wrong. The driver stands in for ElastoDyn here, so it now applies RotDir to the prescribed pitch and its rate, matching p%BlPitch = p%RotDir * BlPitch. The skew-aligned disk frame used to measure the azimuth fed to the skewed wake correction is built from cross(V_diskAvg, x_hat_disk). That is a pseudovector, so under the mirror the frame changed handedness and the azimuth came out as pi minus azimuth rather than negated. The correction was then applied with the wrong sign, moving the axial induction the wrong way around the disk. RotDir is now applied to that vector, which leaves the azimuth already in the clockwise-equivalent frame, so psi_s no longer needs its own factor. This was invisible without Skew_Mod = 1 because psi_s is not used elsewhere. The polar BEM path builds psiSkewOffset from a second cross product and needed the same treatment; applying RotDir to z_vec makes that triad mirror as S R S, after which theta(1) simply negates. Finally the reported skew angle is placed in the rotor's own convention. Its sign is cosmetic, since chi is formed from abs(chi0), and this makes the two BEM models agree with each other under the mirror. All 23 sweep variants now pass: every channel is identical, exactly sign-flipped, or below the numerical noise floor. The clockwise path is unchanged and the AeroDyn driver regression cases stay bit-identical. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Adds MirrorRotor support to SimplifiedElastoDyn, the drivetrain half of the counter-clockwise rotor work. As in AeroDyn the mirror lives at the module boundary: the azimuth and rotor speed states are physical, while inputs and outputs stay in the rotor's own convention so a mirrored rotor turning its design direction still reads positive. RotDir is applied to the initial azimuth and rotor speed, to the generator torque on its way in, to the commanded blade pitch where it is turned into blade root geometry, and to the reported azimuth, speed, acceleration, torque and power. The HSS brake torque is deliberately left alone: it is already signed by the direction the shaft is turning, so a blanket sign flip would make the brake drive the rotor instead of stopping it. That last point exposed a latent bug. SED_AB4 and SED_ABM4 both sign the brake with SIGN(HSSBrTrqC, qdt), but SED_RK4 assigned the commanded torque directly, so the brake always acted in one fixed direction regardless of which way the shaft was turning. A clockwise rotor spinning forwards never notices, but ABM4 uses RK4 for startup, so a mirrored rotor was accelerated by its own brake for the first two steps before the multistep scheme took over and corrected the sign. RK4 now matches the other two integrators. Verified with a brake canary: the HSS brake case run clockwise and mirrored now produces bit-identical azimuth, rotor speed, rotor acceleration, generator speed and generator acceleration, and both runs decelerate 12 rpm to rest. The clockwise path is unchanged. sed_test_freewheel is bit-identical. sed_test_HSSbrk is identical while the rotor is turning and differs only after it has stopped, where the brake sign is now taken from a rotor speed of order 1e-18; that shifts the far digits of the existing stopped-state chatter by a relative 1e-10. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
MirrorRotor already existed in the OpenFAST primary input file as a stub: it was read into p_FAST%MirrorRotor but never used. This connects it. The glue code now passes the per-rotor flag into the ElastoDyn and AeroDyn initialization inputs, and rejects the combinations that have not been worked through yet: linearization, the steady-state aero map solver, SimplifiedElastoDyn, BeamDyn, AeroDisk, ExtLoads and ServoDyn. AeroDyn adds two more of its own, for the OLAF free wake and the acoustics model. Each of these guards is removed as the corresponding module is done. ElastoDyn gets the same boundary treatment as AeroDyn and SED. The azimuth and rotor speed states are physical, so the initial azimuth, rotor speed and blade pitch are mirrored as they are read, and the structural twist is negated to match the aerodynamic twist that AeroDyn already negates. Blade one sits at QT(GeAz) + AzimB1Up + pi/2, so the azimuth initial condition keeps those offsets outside the mirror. On the drivetrain, the generator torque is converted from the ServoDyn convention onto the physical shaft and the high-speed shaft speed is reported in the rotor's own convention. As in SED the brake torque is left alone, since it is already signed by the direction the shaft is turning and mirroring the whole bracket would make it drive the rotor. MirrorRotor still defaults to false everywhere. 5MW_Land_DLL_WTurb and 5MW_Land_DLL_WTurb_SED remain bit-identical to their baselines. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The blade coordinate system is built from the commanded pitch, not from the pitch degree of freedom, so mirroring only the pitch DOF state left the blade geometry unmirrored whenever the pitch DOF is off, which is the usual case. The rotor then ran with the twist mirrored but the pitch not, which put every blade at the wrong angle of attack. With a rigid rotor the mirrored machine produced 12 percent more thrust than its clockwise twin. The blade root twist angle reported by AeroDyn made it obvious: 14.308 degrees clockwise against 12.308 mirrored, where 13.308 is the root twist and 1.0 the pitch, so the two were arriving with opposite relative signs instead of both negated. SetCoordSy now mirrors the commanded pitch, matching the pitch DOF state which is already physical. The AeroDyn summary confirmed the blade twist and swept-axis offset were already being negated correctly, so only the pitch needed changing. With a rigid rotor the two runs now agree exactly: thrust identical, torque exactly opposite. With the flexible blade and tower degrees of freedom enabled every one of the eighty output channels resolves to identical, exactly sign-flipped, a mirrored angle, or the numerical noise floor. The signs follow the mirror operator throughout, with tower base side-to-side force and fore-aft moment flipping while fore-aft force and side-to-side moment do not, and out-of-plane blade deflection matching while in-plane deflection flips. 5MW_Land_DLL_WTurb, 5MW_Land_DLL_WTurb_SED and 5MW_Land_BD_DLL_WTurb are all still bit-identical to their baselines. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The gearbox efficiency factor is selected from the sign of the low-speed shaft torque about the shaft axis. A mirrored rotor generates with a negative shaft torque, so that test classified generation as motoring and raised the gearbox factor to 1/GBoxEff instead of GBoxEff, turning the loss into a gain. The test now uses the torque in the rotor's own convention, which is the direction power actually flows. The high-speed shaft torque and power are reported in the rotor's own convention as well, so they are unchanged by the mirror. The low-speed shaft torque keeps its axis suffix and stays physical, so it flips. Verified with a free drivetrain, loose coupling and a 95 percent gearbox. Those all matter: the baseline gearbox is 100 percent efficient, which makes both branches of the factor identical, and SignLSSTrq is only reached from the loose-coupling integrators, so a tight-coupled run with a lossless gearbox cannot see this at all. With the fix reverted under those settings, 74 channels fail; with it in place every channel resolves. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Several ElastoDyn channel names share one AllOuts slot even though they mean different things once a rotor can be mirrored. RotSpeed and LSSTipVxa were the same number, as were Azimuth and LSSTipPxa, RotAccel and LSSTipAxa, and RotTorq, LSShftTq and LSShftMxa. A name carrying an explicit axis suffix should report the physical component, while a name describing a rotor or drivetrain quantity should report it as the rotor experiences it. Four AllOuts slots are added for the rotor-convention channels and the five affected names are pointed at them. The names, units and ordering of the parameter tables are untouched, so the three parallel arrays keep their length and alphabetical ordering; only the index array changes. Generator speed and acceleration move to the rotor convention too, since the generator side stays positive whichever way the rotor was built. For a clockwise rotor RotDir is +1 and every one of these is numerically identical to what it was, so no existing deck or baseline changes. The split only becomes visible when MirrorRotor is set. The three parameter tables are indexed in parallel with nothing in the build checking they agree, so a misalignment would silently report the wrong channel. tools/check_ed_outparams.py parses all three out of the source and verifies the lengths match, the names stay sorted and unique, and every index symbol is declared and within MaxOutPts. Mirrored against the clockwise pair, RotSpeed, RotAccel, Azimuth, RotTorq, LSShftTq, HSShftTq, GenSpeed, GenAccel, RotPwr and RotThrust now all read identically, while LSShftMxa, LSSTipVxa, LSSTipAxa and LSSGagMxa flip and LSSTipPxa and LSSGagPxa mirror as angles. Rigid, flexible and free-drivetrain runs all resolve completely. Four regression cases including an offshore one stay bit-identical. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Adds docs/source/user/glue-code/mirror_rotor.rst covering what MirrorRotor does, the two output conventions and why they differ, the measured sign table, the current limitations, and how the mirror is verified. The sign table is measured rather than asserted: it comes from running the same model clockwise and mirrored and comparing every output channel. The note that mirroring reverses the blade sweep order, so blade 2 of one rotor corresponds to blade 3 of the other under non-axisymmetric inflow, is there because that is an easy way to misread a comparison. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Adds 5MW_Land_noDLL_Steady_CW and 5MW_Land_noDLL_Steady_MirrorRotor to the regression suite under a new mirrorrotor label, so the pair can be run on its own while a mirrored rotor is still being worked through. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
BeamDyn describes a blade by its key-point geometry and a 6x6 sectional
mass and stiffness matrix. Mirroring the blade about its local x-z plane
reflects y, which negates the key-point y offset and the twist, and
transforms the matrices by T*M*T^T with T = diag(1,-1,1,-1,1,-1). The
theta entries pick up a sign because rotations are pseudovectors. In
practice every matrix entry flips exactly when one of its indices is in
{2,4,6}.
The transform is applied between reading the input and validating it, so
the polar-inertia check runs against the mirrored blade. That check
compares mass0(6,6) against mass0(4,4) + mass0(5,5), all diagonal
entries, which the transform leaves alone.
Note the twist stored on read is already the negative of the value in the
file, so negating it here leaves it equal to the file value rather than
doubly negated.
A unit test covers the transform directly: that applying it twice is the
identity, that entries flip exactly when one index is in {2,4,6}, that
the polar-inertia constraint survives, and that key points reflect in y
and reverse twist while the x and z offsets are untouched.
Coupled, a mirrored BeamDyn rotor now resolves cleanly against its
clockwise twin, including the BeamDyn root and tip channels: the x and z
forces, the y moment and the x and z deflections match, while the y
force, the x and z moments and the y deflection flip. This holds with
steady inflow, shear, yaw and a free drivetrain.
BeamDyn's guard rail is removed. The clockwise path is unchanged and
5MW_Land_BD_DLL_WTurb and 5MW_Land_BD_DLL_WTurb_StC stay bit-identical.
Co-authored-by: GitHub Copilot <copilot@github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Adds 5MW_Land_BD_noDLL_Steady_CW and 5MW_Land_BD_noDLL_Steady_MirrorRotor under the mirrorrotor label alongside the ElastoDyn pair. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
y%BlPitch was inconsistent: it carried the physical pitch angle when the pitch DOF was enabled, but the command it was handed when it was not. Apply RotDir to the DOF branch so the output is always in the command's convention, which is what ServoDyn is given. HSSBrTq belongs to the same generator-side family as HSShftTq and HSShftPwr, so it reads in the rotor's own convention rather than about the +x shaft axis. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A furling machine is chiral by design: the tail boom, tail fin and the rotor- and tail-furl axes are all offset to one side, and the nacelle mass centre is laterally offset with them. Reversing only the rotor leaves that geometry untouched and produces a turbine that is not the mirror of anything. Mirroring it properly means transforming the furl input file as well, which is out of scope, so fail rather than answer quietly. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ServoDyn is left entirely alone. Instead the signals crossing its boundary are converted, so an unmodified controller -- including a Bladed-style DLL such as ROSCO -- sees exactly what it would see on a clockwise rotor and needs no knowledge that the rotor has been reversed. In-plane and about-axis quantities flip; out-of-plane ones do not. HSS_Spd and BlPitch already arrive in that convention from ElastoDyn, RotPwr is a product of two flipped quantities, and ElastoDyn signs the generator and brake torques itself, so none of those are converted here. The shaft azimuth is negated and re-wrapped so it still increases clockwise. Yaw is deliberately untouched. It acts about the vertical axis in the inertial frame, so it is not a rotor-convention quantity: the yaw error stays physically correct and a yaw controller still points the nacelle into the real wind. ServoDyn also receives the yaw angle and rate through the ordinary variable mapping rather than here, so converting the yaw moment on its own would inverse the sign of its yaw spring and drive the nacelle unstable. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Adds AWT_WSt_StartUp_HighSpShutDown_MirrorRotor under the mirrorrotor label. This is the first mirrored regression case with ServoDyn in the loop, and it covers the high-speed-shaft brake through zero rotor speed. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Adds two sections. The first records that ServoDyn is left unmodified and is handed everything in the clockwise convention, and why yaw is deliberately excluded from that. The second is the one that costs time if it is not written down: mirroring the turbine does not mirror the environment or the control setpoints. Initial nacelle yaw, wind direction, horizontal shear, a turbulence box, a prescribed structural-control load and lateral geometry are all left as written, and leaving one of them unmirrored looks exactly like a sign error in the code. Also corrects the limitations table, which still listed BeamDyn and ServoDyn as unsupported, and adds the furling restriction. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Adds 5MW_Land_DLL_WTurb_MirrorRotor and 5MW_Land_BD_DLL_WTurb_MirrorRotor under the mirrorrotor label. These are the cases that drive a mirrored rotor with an unmodified Bladed-style controller through a y-reflected turbulence box, and they complete the mirrored-rotor coverage for ServoDyn. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Downgrades the furling guard from fatal to a warning and lets the run proceed. The tail is modelled as a drag force applying a moment about the yaw axis, and that calculation does not depend on which way the rotor turns, since the tail does not interact with the wake. With the aerodynamic and structural mirror already verified, there is reasonable confidence the combination behaves as intended once the whole structure is mirrored. What the flag does not do is mirror the furl geometry itself. That puts furling in the same category as an initial nacelle yaw or a wind direction: an asymmetric input the user must mirror as well if the intent is to reproduce the mirror image of the clockwise turbine. The documentation moves it accordingly, out of the unsupported table and into the list of inputs the mirror leaves alone. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
andrew-platt
marked this pull request as draft
August 19, 2026 14:11
Collaborator
Author
Where the mirror is applied
AeroDyn, in a little more detail
Airfoil polars are used verbatim; there is no polar transformation. The one exceptionElastoDyn, SimplifiedElastoDyn and AeroDyn all convert signals crossing a boundary. BeamDyn instead transforms the blade description itself as it is read — key-point That is necessary because those matrices carry bend–twist and shear–extension couplings with a handedness of their own; presenting mirrored motion to an unmirrored blade would not give the mirrored answer. The blade input file still describes the clockwise blade, and the finite-element solver is still never told anything. |
Three gaps found while auditing what phases 1 to 3 actually left behind. BeamDyn was never documented. It gained support two phases ago and the only change made here at the time was to delete the row saying it was unsupported, which left the page implying it did not work. Worse, the concept section stated the reflection is applied at module boundaries only, and BeamDyn is precisely where that is untrue: its blade description is transformed on read, because the 6x6 stiffness and mass matrices carry couplings with a handedness of their own. That claim is now qualified and the transform is written out. Adds a module map giving, for each module, whether it changes at all and where. ServoDyn, InflowWind and the aerodynamic and structural kernels are listed with the others precisely because the answer for them is "nothing". The AeroDyn driver gained a required MirrorRotor input per turbine, which the driver documentation never mentioned and its example input blocks did not show, so a reader copying one would have written a file the driver rejects. Also brings the verification section up to date: it described only the first phase, and now names the regression pairs and the controller result. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The two example driver files shipped with the documentation never gained the MirrorRotor input the driver now requires, so anyone copying one would have written a file the driver rejects. ad_driver_multiple.dvr needs it for both of its turbines. Also rewords the BasicHAWTFormat comment here and in the driver documentation to match the regression inputs: the basic inputs are named as a range rather than counted, since the count was wrong and MirrorRotor now sits between the flag and them. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
andrew-platt
force-pushed
the
f/reverseRotor
branch
from
August 19, 2026 14:59
71ac041 to
a7253c1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft
Feature or improvement description
Enabling the
MirrorRotoroption in OpenFAST. Complete description coming soon.Current development status
Phase detail
MirrorY.bts)openfast_ioround-tripVerification
ctest -L mirrorrotorOutstanding non-blockers
OutListParameters.xlsxDECISIONS.mdtoolsCreated 3 todos
Test results, if applicable