F/reverse rotor - #3436
Draft
andrew-platt wants to merge 51 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
Adds 5MW_OC4Semi_WSt_WavesWN_MirrorRotor under the mirrorrotor label, extending mirrored-rotor coverage to HydroDyn, SeaState, MoorDyn and the platform degrees of freedom. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The page opened by saying the flag runs the same turbine as its mirror image. It does not. It reverses the rotor, and the reflection is applied internally to the rotor and the drivetrain quantities attached to it. The tower, nacelle, support structure, mooring and the environment are untouched. That distinction matters in use. A counter-clockwise machine sits in a real wind field, on a real support structure, with a real mooring spread and real yaw setpoints, none of which should be mirrored. The previous wording invited the opposite conclusion, that a user should reflect their environment to match the rotor. The list of inputs the flag leaves alone is therefore reframed: it is a requirement of the verification comparison, which needs the whole problem to be symmetric, not advice for ordinary use. The measured sign table gets the same qualification, since it records how two runs of a symmetric comparison relate to each other rather than promising that those channels change sign whenever the flag is set. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
SED reported rotor speed and acceleration through one slot each, reached by both the rotor-convention names and the axis-suffixed ones. For a mirrored rotor those two families disagree in sign, so a single slot cannot serve both. RotSpeed and RotAcc keep the rotor's own convention, reading positive whenever the rotor turns its design direction. LSSTipV, LSSTipVxa, LSSTipVxs and the matching acceleration names now reach new slots holding the physical component about the shaft x axis. This is the same split already applied to ElastoDyn. RotTorq and LSShftTq are deliberately left sharing a slot, matching the judgement made for ElastoDyn: a torque name with no axis suffix follows the rotor convention. For a clockwise rotor every one of these reduces to the same value, so no existing model or output file changes. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
SED handed out its rotor speed already converted to the rotor's own convention, while ElastoDyn hands out the physical value. Two consumers read it, and they disagreed about which they wanted: ServoDyn expects the clockwise convention, and AeroDisk was receiving the clockwise value from SED but the physical value from ElastoDyn. No single correction inside AeroDisk could have served both. y%RotSpeed is now the physical shaft speed, matching ElastoDyn, and the ServoDyn boundary converts it exactly as the ElastoDyn boundary already does. Both structural modules now hand AeroDisk the same thing. The write-output channels are unaffected: they are computed from the states directly and already carry the split between the rotor-convention and axis-suffixed names. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
AeroDisk had no notion of rotation direction at all. Its aero table is defined in the clockwise convention, so the rotor speed is converted to that convention before the table is read, which keeps the tip-speed ratio positive and inside the tabulated range. The coefficients that come back are reflected as they become physical loads: force is a true vector and moment a pseudovector, so different components change sign. C_F and C_M themselves are left in the table's convention, which makes the Ct and Cq outputs read in the rotor's own convention while the resulting forces and moments read physically, with no further work. Cp and the power are products of two flipped quantities and are unaffected. The skew-aligned triad also needed a correction. Its third axis is built from a cross product of two true vectors, which is a pseudovector, so on a mirrored rotor the triad came out left-handed. This is the same error already fixed in AeroDyn's disk-average frame. Note that it is not exercised by the regression case added here, which is unyawed, so the out-of-plane force and moment components are identically zero. SimplifiedElastoDyn was never handed the flag through the glue code at all, only through its standalone driver. Both it and AeroDisk are wired up here and their restrictions removed. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Gemini 3.6 Flash <gemini@google.com>
Adds 5MW_Land_DLL_WTurb_ADsk_SED_MirrorRotor under the mirrorrotor label, completing mirrored-rotor coverage of the second structural and aerodynamic path. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
The nodal outputs are a third copy of the blade output block, alongside the one in AeroDyn_IO.f90 and the free-wake copy beside it. Only the first was mirrored in phase 1, so a mirrored rotor reported nodal lift, drag, normal and tangential force, and the y-direction coefficients and induction, in a mixture of frames. The error was invisible to every test written so far. The nodal channels are named AB1N001Fd where the module channels are B1N001Fd, and no mirrored test requested the nodal form. It was caught by running one clockwise and one mirrored rotor inside a single driver run and comparing them: the angle of attack agreed exactly, so the momentum solution was right, but the lift and drag split was rotated by twice the inflow angle, with the magnitude preserved. The same sign map as AeroDyn_IO.f90 is applied. The free-wake branches are left alone deliberately: there m%Y and the inflow angle both come from the wake solution in the same frame, so those expressions are already self-consistent. Mirroring that path means converting its loads and its outputs together, along with the wake itself, and is not attempted here. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Runs a clockwise and a counter-clockwise rotor in one driver run. This is the only mirrored case whose blade outputs are in the nodal form, and the two rotors can be compared against each other directly rather than only against a stored baseline. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
A clockwise and a counter-clockwise rotor on one semisubmersible, which makes the whole system symmetric about the vertical plane. Rotor 2 mirrors rotor 1 within a single solve, so the drivetrain, substructure, moorings and two controller instances are all checked at once, with no run-to-run difference to explain away. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
BlCenBt is the chordwise offset of a blade section's centre of buoyancy, and it sits in the same in-plane direction as BlSwpAC, which is already mirrored two lines above. Leaving it alone put the buoyant load of a counter-clockwise MHK rotor on the wrong side of the blade. The neighbouring columns are correctly left alone. BlCenBn is the out-of-plane offset, which no more flips than curvature does. BlCpn, BlCpt, BlCan, BlCat and BlCam are dimensionless coefficients that end up scaling chord squared, so they carry no direction at all. Found by working through what a mirrored marine turbine would need, rather than by a failing test: buoyancy is computed only for MHK turbines, and no mirrored case is an MHK case. In the RM1 rotor BlSwpAC is zero at every station while BlCenBt is not, so the mirror that was present did nothing and the one that was missing did all the damage. Correcting it moves 116 of 184 channels, and the nodal buoyant moments by more than their own magnitude. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The only mirrored cases that reach the buoyancy calculation, and so the only regression cover for the centre-of-buoyancy mirror. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The table claimed to be measured but had been maintained by hand, and had drifted. It said the AeroDyn Ft, Cy and Vindy families change sign, which was true of the module channels and false of the nodal ones until that path was mirrored. It also predated the marine turbine work, so nothing buoyant appeared at all. It is now generated by tools/emit_sign_table.py, which runs every registered mirrored case, compares each channel against its clockwise counterpart, and reports the observed behaviour. All five pairs resolve completely, so every family listed has been seen rather than reasoned about. The families that carry a sign in the code but that no registered case requests are called out separately as inferred rather than measured, since that gap is what allowed two sign errors to survive this long. Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The skew-aligned disk triad is built so that all three basis vectors mirror as true vectors, which is exactly what the RotDir on z_hat achieves: the cross product of two true vectors is a pseudovector, and the factor turns it back. Taking components against a triad that reflects with the flow, a true vector such as force keeps every component, because the vector and the basis vector reflect together, while a pseudovector such as moment reverses every one. The loads did not follow that rule. Force(2) carried a RotDir it should not, and Moment(2) lacked one it should have had. Both are only reachable when the coefficient table supplies lateral coefficients, which the shipped 5MW table does not, so neither had ever executed. Measured against the new yawed pair, the disk-frame forces are now identical between the clockwise and mirrored runs and the disk-frame moments are all sign-flipped, and in the inertial frame ADFyi, ADMxi and ADMzi flip while ADFxi, ADFzi and ADMyi do not. Before the change ADFyi came out identical, which is what a mirror image cannot do. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Rxyz holds x_hat, y_hat and z_hat as its columns. Multiplying it on the left of a vector rebuilds an inertial vector from disk components; multiplying on the right projects an inertial vector onto the disk axes. Both call sites used the wrong one, in opposite directions. The wind and structural velocity outputs hold an inertial vector and want its components, but reconstructed instead, which reduced ADVWindx/y/z to the shaft axis scaled by the wind speed. ADVWindz should be identically zero, since z_hat is built perpendicular to the relative wind, and instead read 2.04 m/s in the case used here. It is now zero exactly. The load outputs hold disk components and want an inertial vector, and projected instead. The moment block additionally reconstructed from m%Force, so ADMxi, ADMyi and ADMzi were copies of ADFxi, ADFyi and ADFzi rather than moments at all. This predates the counter-clockwise rotor work and is not caused by it, but the mirror comparison is what exposed it: the affected channels could not be made to resolve, because the quantities themselves were not what their names claimed. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
RtSpeed, RtTSR, RtAeroPwr and BAeroPwr are written by code shared between the wake models, from a local omega that the two paths set differently. BEMT supplies m%BEMT_u%omega, which already carries RotDir because BEMT is presented a clockwise-equivalent problem. OLAF supplied Calc_Omega, which is the bare physical speed. The power channels multiply by RotDir again. For BEMT the two factors cancel and the reported power is physical, which is correct. For OLAF the single factor would not cancel, so a mirrored rotor would report negative power and a negative power coefficient, and RtSpeed and RtTSR, which do not multiply by RotDir at all, would come out with the opposite sign to the same rotor run under BEMT. OLAF now presents omega in the same clockwise-equivalent convention. RotDir is unity for every rotor that can currently reach this path, since mirrored rotors are refused with OLAF, so no existing result changes; the fix simply removes a trap from the path that work will take. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Both halves carry the mirrorrotor label, so ctest -R mirrorrotor runs them with the rest. The r-test pointer also brings in the extended AeroDyn output lists, their regenerated baselines, and the AeroDisk baselines that moved with the corrected output transforms. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The scripts that establish what the regression suite asserts have lived in a scratch directory outside version control, which meant the evidence for the sign table could not be re-created from the repository alone. They now sit in reg_tests/otherTests, alongside a README describing what each one does and the two traps that have cost time before: that the shared classifier reads text output rather than packed binary, and that a .outb written in the packed format resolves to about 1.5e-5, so no comparison drawn from one should be run tighter than that. Each script locates the repository from its own position on disk, honouring OPENFAST_REPO, rather than the absolute path of one developer's checkout. Nothing here is wired into ctest. These are run by hand when a claim needs re-establishing or when a change touches the rotor convention. run_guards.sh is new. It runs the MirrorRotor restrictions in both states and checks that each fatal error appears for a mirrored rotor and does not appear for a clockwise one. The linearisation, OLAF and AeroAcoustics guards all behave correctly. The steady-state solver guard cannot be reached this way, because the flag it tests is a parameter fixed to false on the path the openfast binary takes, and only the aero-map program passes it as true. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Three things had drifted. The restrictions table still listed SimplifiedElastoDyn and AeroDisk as unsupported, although both were enabled some commits ago and each has a registered case. The table of where the mirror is applied did not mention AeroDisk at all. The verification section said three comparisons were kept as regression cases, then listed five, while thirteen tests carry the mirrorrotor label. The measured sign table is regenerated. The caveat that the coefficient families were inferred rather than measured is gone, because they are now requested by a registered case and observed. Across the six pairs 1002 channels resolve: 677 identical, 324 sign-flipped and one mirrored angle, with 58 below the noise floor everywhere and 133 mooring channels set aside because their pairing depends on the layout. None is unresolved. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The deck it copies refers to ../5MW_Baseline for the inflow and airfoil data, and the script never created that link. Run from a fresh directory the cases aborted while reading their inputs, before reaching the guard. That failure mode is worth naming, because it is not symmetric. The mirrored variants reported the guard missing and failed loudly, but the clockwise controls check that the message is *absent*, and a run that never reaches the guard satisfies that trivially. Half the check was passing for the wrong reason and would have gone on doing so. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
OLAF is no longer refused for a mirrored rotor. The angle of attack was taken from geometry that had not been told the rotor turns the other way, so the section flew backwards: a case that gave 9.1 degrees clockwise gave 169.3 mirrored, and the two runs differed from the first time step. The rotation direction is carried **per wing**, not per simulation. OLAF holds the wings of every rotor in one shared wake and one Biot-Savart solve, so a farm may mix the two directions and both have to live in the same frame. Reflecting the world OLAF sees would have been simpler and would have broken exactly on the mixed cases this flag already supports. Carrying it per wing also leaves the wake in the physical frame, which keeps the wake visualisation, the ambient wind sampled along the filaments, and any circulation written to a checkpoint in the frame the user expects. Three things change for a mirrored wing. The angle of attack is taken in the clockwise-equivalent frame, so the airfoil tables stay verbatim as they are everywhere else in this feature. The bound circulation reverses, being a pseudovector along a span axis that is itself a true vector. And the lifting line's leading and trailing edges move to the other side of the reference point, because the section is used as the mirror image of the tabulated one; without that the bound vortex and the shed sheet sit on the wrong side of the chord and the wake is not the mirror image at all. That last one is worth the emphasis: it is the difference between agreeing to about seven per cent and agreeing exactly. The sign factor sits on the chordwise term in FVW_AeroOuts and on the normal term in Wings_ComputeCirculationPolarData. That looks inconsistent and both are required. The lifting-line panel normal is a cross product of two true vectors and so is a pseudovector, while the rows of a direction cosine matrix are not, so the two carry opposite signs under the reflection. Putting the factor on the matching term in both places was tried and measured, and is wrong. The loads leave the free-wake path in the same convention the blade-element path produces, so the two share their downstream treatment rather than diverging again. Measured on ad_B1n2_OLAF, a mirrored rotor now reproduces the clockwise one to 0.000e+00 relative difference over the whole time history. Nothing changes for a clockwise rotor, where every factor here is unity; all ten registered OLAF cases reproduce their baselines. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Each blade-node output family is written twice, once for the blade-element path and once for the free wake, and only the first carried the mirror sign. The Vindy, Cm, Cy, Ct, Fl, Fd, Fn and Ft families therefore came out of a mirrored free-wake rotor in the wrong convention, in both the module and the nodal form. Now that the free-wake quantities reach these blocks in the same clockwise-equivalent convention the blade-element ones use, the two branches carry the same factors in the same places, including the twist angle used to rotate the coefficients. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Both halves carry the mirrorrotor label. The r-test pointer also brings in the twin-rotor case's return to the free wake and its regenerated baseline. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
The pair is added to the set the table is generated from, and pairs may now name their output files directly, since the module-level driver cases do not follow the glue-code naming. Seven pairs now contribute. 1015 channels resolve: 686 identical, 328 sign-flipped and one mirrored angle, with 55 below the noise floor everywhere and 133 mooring channels set aside. None is unresolved. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
OLAF leaves the list of restrictions and gains a section of its own, since the reasoning behind it is not obvious from the code: why the direction is carried per wing rather than by reflecting the world the wake solver sees, what the three changes are, and why the sign factor sits on a different term in the two places the angle of attack is formed. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
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