Source code for vane.run.trim_tuning

# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2026 Jae Hoon Seo, SMI Lab, Inha University
"""Proactive per-operating-point trim auto-tuning for the linearization sweep.

The ``CalcSteady`` trim that finds a turbine's periodic steady state before linearizing
is hardest at and above rated wind speed: thrust is high and the platform/tower response
is lightly damped, so a tolerance and trim damping that converge an easy below-rated
point often stall a near-/above-rated one. :class:`~vane.run.LinearizationRetryPolicy`
recovers from that *reactively* — it adapts the tolerance, ``TMax``, trim gains and
damping to the classified failure after a run has already failed, costing one or more
wasted OpenFAST runs per hard point.

This module sets those same knobs *proactively*. Given an operating point and the
campaign-base :class:`~vane.run.LinearizationSettings`, :func:`tune_trim_settings`
returns the settings to start that point from, applying the documented above-rated trim
floors — a looser ``TrimTol`` and a trim-damping floor on ``Twr_Kdmp``/``Bld_Kdmp`` — to
points at or above rated so they are likely to converge on the first attempt. It only
ever *raises* tolerance/damping toward those floors, never tightens below the campaign
base, and below-rated and standstill points are returned unchanged, so the tuner can
only help. The reactive retry ladder still escalates from the tuned starting point for
any residual hard case.

The tuner adjusts only the three fields the retry ladder already treats as per-case
(``trim_tol``, ``twr_kdmp``, ``bld_kdmp``), so a sweep's run provenance — which records
those per case and compares the rest campaign-wide — is unaffected. The
below/above-rated boundary is the settings' own ``rated_wind_speed``, the same split
:func:`~vane.run.configure_linearization` uses to choose torque vs pitch trim.
"""

from __future__ import annotations

import math
from dataclasses import dataclass, replace
from typing import TYPE_CHECKING

from vane.run.lin_config import MAX_TRIM_TOL

if TYPE_CHECKING:
    from vane.run.lin_config import LinearizationSettings
    from vane.run.operating_points import OperatingPoint

__all__ = ["TrimTuningPolicy", "tune_trim_settings"]


[docs] @dataclass class TrimTuningPolicy: """Floors applied to the trim settings of an at/above-rated operating point. Parameters ---------- above_rated_trim_tol : float Minimum rotor-speed trim tolerance (``TrimTol``) to use at or above rated wind speed. A point's tolerance is raised to this floor (never lowered below the campaign base); below-rated points keep the base tolerance. The default ``1e-2`` is the loosened tolerance the above-rated trim needs (the base default is ``1e-3``). It must not exceed ``MAX_TRIM_TOL`` (``0.1``), and the retry ladder that escalates from it clamps there too. above_rated_damping : float Minimum artificial trim damping (``Twr_Kdmp`` and ``Bld_Kdmp``) to use at or above rated wind speed; the point's damping is raised to this floor. The damping acts only during the steady-state trim and is stripped from the linearized model, but it can bias the trimmed operating point slightly, so use the smallest floor that converges. The default ``5000`` matches the offshore preset's trim damping. """ above_rated_trim_tol: float = 1e-2 above_rated_damping: float = 5000.0 def __post_init__(self) -> None: """Reject a non-finite or non-positive floor. Raises ------ ValueError If ``above_rated_trim_tol`` or ``above_rated_damping`` is not finite and positive, or ``above_rated_trim_tol`` exceeds ``MAX_TRIM_TOL``. """ for name, value in ( ("above_rated_trim_tol", self.above_rated_trim_tol), ("above_rated_damping", self.above_rated_damping), ): if ( not isinstance(value, (int, float)) or not math.isfinite(value) or value <= 0.0 ): msg = f"{name} must be finite and positive, got {value!r}" raise ValueError(msg) if self.above_rated_trim_tol > MAX_TRIM_TOL: msg = ( f"above_rated_trim_tol must not exceed {MAX_TRIM_TOL}, got " f"{self.above_rated_trim_tol!r}" ) raise ValueError(msg)
[docs] def tune_trim_settings( operating_point: OperatingPoint, base: LinearizationSettings, policy: TrimTuningPolicy, ) -> LinearizationSettings: """Return the trim settings to start one operating point from. For a rotating point that trims (``CalcSteady``) at or above the base settings' ``rated_wind_speed``, the trim tolerance and tower/blade trim damping are raised to the policy's floors (whichever is larger — the campaign base is never tightened). A below-rated point, or a standstill/parked point that takes a single linearization without a trim, is returned unchanged. Parameters ---------- operating_point : OperatingPoint The operating point whose starting trim settings to compute. base : LinearizationSettings The campaign-base settings. policy : TrimTuningPolicy The above-rated trim floors to apply. Returns ------- LinearizationSettings A copy of ``base`` with the above-rated trim floors applied when they raise the knobs, or an unchanged copy otherwise. """ # A point with no periodic trim (standstill/parked, or CalcSteady disabled) takes a # single linearization, so there is no trim to tune. if not (operating_point.is_rotating and base.calc_steady): return replace(base) # Below rated the trim is easy (torque trim, lower thrust); keep the base. if base.is_below_rated(operating_point.wind_speed): return replace(base) return replace( base, trim_tol=max(base.trim_tol, policy.above_rated_trim_tol), twr_kdmp=max(base.twr_kdmp, policy.above_rated_damping), bld_kdmp=max(base.bld_kdmp, policy.above_rated_damping), )