Verifying the trimmed operating point
VANE does not compute rotor performance: you supply the turbine’s steady operating
schedule (see Quick start), and OpenFAST’s CalcSteady trims the turbine about
it. Below rated it trims the generator torque at the prescribed pitch and rotor
speed; above rated it trims the blade pitch at constant generator torque. The point
OpenFAST converges to is therefore a check on your data. If it does not reproduce your
curve, something disagrees: the curve itself, the rotor radius or air density it was
normalized with, the gearbox ratio or loss, or the deck.
verify_operating_points() reads the trimmed point
back out of the .lin files and compares it with the curve:
from vane.run import find_lin_files, verify_operating_points
lin_sets = [find_lin_files(f"sweep_out/ws{ws}/Main.fst") for ws in (6, 10, 14, 20)]
report = verify_operating_points(
lin_sets,
"rotor_performance.csv", # cp / ct columns are used when present
deck="sweep_out/ws6/Main.fst",
)
print(report.to_table())
assert report.passed, [p.label for p in report.flagged]
Either deck= (an .fst file the turbine constants are read from) or
constants= (see Turbine constants) is required. Each entry of lin_sets holds
the .lin files of one linearization, as paths or parsed
LinFile objects. The CSV is read by
rotor_performance_from_csv(); its cp and ct columns may be
fractions or declared percentages (cp [%]), as described in
the linearization-sweep guide.
After run_operating_sweep(), pass the result to
verify_sweep_operating_points(). It reads the
constants from the first verified case’s deck, labels each point with its case name,
and lists the cases that did not converge or wrote no .lin files in
report.skipped rather than dropping them:
from vane.run import run_operating_sweep, verify_sweep_operating_points
sweep = run_operating_sweep(..., performance_csv="rotor_performance.csv")
report = verify_sweep_operating_points(sweep, "rotor_performance.csv")
report.passed is true only when every verified point passes and no case was
skipped. report.to_records() gives one flat record per point (for a
pandas.DataFrame).
What is compared
For every linearization the azimuth files are averaged, and for each operating point the report carries:
the trimmed wind speed and rotor speed (from the
.linheaders);the trimmed blade pitch: the mean over the blades and the azimuth files of the
ED Blade <i> pitch commandinputs (or the collective pitch-command input), plus its spread (zero for a collective trim);the generator torque (the
ED Generator torqueinput);the aerodynamic power and power coefficient \(C_p\) from the torque balance below;
the thrust coefficient \(C_t\), when the outputs carry AeroDyn’s rotor thrust (
RtFldFxh, orRtAeroFxhin older AeroDyn versions, in the AeroDynOutList);the curve values at the trimmed wind speed (the same monotone PCHIP interpolation the sweep uses, without extrapolation), the deviations, and a
pass/flag/incompletestatus.
The optional cp and ct columns of the CSV (also accepted as
power_coefficient and thrust_coefficient) are read as fractions; a blank or
non-numeric cell counts as unknown.
Below rated the rotor speed and pitch are prescribed from your curve, so their
agreement is by construction and \(C_p\) is the informative check; above rated the
trimmed pitch is. The default tolerances (TrimTolerances)
are 1 % on rotor speed, 0.5 deg on pitch, and 5 % on \(C_p\) and \(C_t\). Pass
tolerances=TrimTolerances(...) to change them.
Torque balance
At a periodic steady state the azimuth-mean rotor acceleration is zero, so the mean
aerodynamic torque on the low-speed shaft equals the torque the shaft transmits. With
gearbox ratio \(N\) and efficiency \(\eta\) (ElastoDyn GBRatio and
GBoxEff), the low-speed-shaft torque that balances the high-speed-shaft generator
torque \(Q_g\) is
that is, \(N Q_g / \eta\) when power flows from the rotor to the generator (the loss is added back) and \(N Q_g \eta\) when the generator motors the rotor. Then
Assumptions:
Steady state. The point is a converged periodic trim. A point the retry ladder could not converge should not be verified.
Gearbox losses as modelled by
GBoxEffand nothing else. Bearing friction and other drivetrain losses are not in ElastoDyn and are not added.No generator efficiency. \(Q_g\) is the mechanical torque on the high-speed shaft, so the electrical efficiency (
GenEff) does not enter. A curve’sgen_powercolumn is electrical and is not compared.No other torque on the rotor.
CalcSteady’s artificial trim damping (Twr_Kdmp/Bld_Kdmp) violates this; see Trim damping below. It is off by default, and the retry ladder adds it only on its generic escalation step (a timeout or an unrecognised failure).The thrust is AeroDyn’s rotor aerodynamic thrust along the shaft. ElastoDyn’s
RotThrustis not used: it is the low-speed-shaft thrust, which also carries the axial component of the rotor weight on a tilted shaft.
Trim damping
During CalcSteady OpenFAST can add artificial damping forces on the tower and blade
nodes (Twr_Kdmp, Bld_Kdmp) to settle the trim faster. Those forces also act on
the rotor, so at the trimmed point the generator torque balances the aerodynamic
torque minus what the damping absorbs, and the torque balance understates the
aerodynamic power.
In tests on the IEA 10 MW monopile deck (OpenFAST 4.0.5) trimmed with
Twr_Kdmp = Bld_Kdmp = 5000 N/(m/s), AeroDyn’s RtFldPwr was 436.6 kW at 4 m/s
against 396.7 kW from the torque balance, and 10.95 MW against 10.60 MW at 25 m/s.
The other possible explanations were ruled out on the data:
the
ED Generator torqueinput already contains ServoDyn’s torque-trim offset (y%GenTrq = GenTrq + CtrlOffset), and ElastoDyn’sRotTorqoutput equals it;the turbine is direct drive with
GBRatio = 1andGBoxEff = 100 %, so there is no high- versus low-speed-shaft ambiguity;AeroDyn’s
RtSpeedmatches the rotor speed;the azimuth-mean rotor acceleration is \(10^{-6}\) rad/s² or less below rated, so the rotor is not still accelerating;
RtAeroPwris \(\Omega M_{x,h}\) of the blade loads alone (no tower or nacelle term).
Rerunning the 4 m/s point with Twr_Kdmp = Bld_Kdmp = 0 raised the trimmed generator
torque from 631 to 685 kN m, left AeroDyn’s power unchanged (436.0 kW), and reduced the
difference from 10.1 % to 1.3 %. In tests on three above-rated NREL 5 MW points trimmed
with 10000 N/(m/s) under OpenFAST 5.0.0, the torque balance still matched the curve
within 0.7 %. How much torque the damping absorbs therefore depends on the model and on
the OpenFAST version.
VANE therefore reads the trim damping of the deck beside each set’s .lin files
(<root>.fst beside <root>.<k>.lin) into
TrimmedOperatingPoint.trim_damping: the larger of Twr_Kdmp and Bld_Kdmp,
0.0 when CalcSteady is off, and None when the deck cannot be read. When it
is non-zero and the outputs carry AeroDyn’s rotor power (RtFldPwr, else
RtAeroPwr), that power gives \(C_p\). point.power_source names the source
used ("torque balance" or the output channel, for example
"output: AD RtFldPwr"). Without the AeroDyn output the balance is kept and a
warning is logged: add RtFldPwr to the AeroDyn OutList for damped sweeps.
The damping also moves the trimmed operating point itself (the generator torque is trimmed lower than the undamped balance would give). Verification cannot undo that. Where the operating point matters, trim without damping, or with as little as convergence allows.
Turbine constants
With deck= the constants are read by
from_deck(): GBRatio
(GBoxRatio for Simplified-ElastoDyn), GBoxEff (percent; a lossless gearbox when
absent), TipRad and PreCone from the structural (EDFile) input, and
AeroDyn’s AirDens, or the .fst AirDens when AeroDyn’s is "default" or
absent. BeamDyn decks keep the rotor geometry in ElastoDyn, so they are read the same
way.
The rotor radius defaults to the rotor-plane (projected) radius
\(R = \text{TipRad}\cos(\text{PreCone})\), the swept-area radius blade-element
tools normally normalize with. radius_convention="tip" uses TipRad itself. A
curve normalized with some other radius or density needs explicit constants:
from vane.run import TurbineConstants, verify_operating_points
constants = TurbineConstants(
gearbox_ratio=97.0,
gearbox_efficiency=0.944, # a fraction, not percent
rotor_radius=63.0,
air_density=1.225,
)
report = verify_operating_points(lin_sets, "curve.csv", constants=constants)
Missing data
When the linearization was written without inputs (LinInputs = 0) the pitch and
generator torque are read from the ED BldPitch<i> and GenTq outputs instead. A
quantity that cannot be found at all is NaN, its check is unavailable, the
point is incomplete, and point.issues says what to add (LinInputs or an
OutList channel). A curve without cp/ct columns, or the
(wind, rpm, pitch) tuple that operating_curve_from_csv()
returns, skips those comparisons without making a point incomplete. A wind speed
outside the curve’s range is reported, not extrapolated.
Reference turbines
In tests with CalcSteady sweeps of the OpenFAST reference decks against their
published rotor-performance curves, the trimmed \(C_p\) agreed within about 2 % and
the above-rated pitch within 0.4 deg for the NREL 5 MW turbine (published curve), and
within about 2 % and 0.15 deg for the IEA 3.4 MW turbine (blade-element curve, with its
95.5 % gearbox efficiency). The IEA 10 MW turbine, trimmed with damping and so verified
on AeroDyn’s power, was 4 to 6 % above its blade-element curve below rated and within
2.5 % above rated. A larger deviation is worth investigating before the linearization
is used.