API reference

The reference is generated from the NumPy-style docstrings in the source. Every public module is listed once, grouped by subpackage. Modules and functions whose names start with an underscore are private and are not shown.

Each subpackage __init__ re-exports the principal public names of its modules, so from vane.modal import compute_modes and from vane.modal.eigensolver import compute_modes refer to the same object. vane.run re-exports every public name of its modules. Elsewhere, a helper or constant that a subpackage does not re-export is imported from its defining module (for example from vane.modal.averaging import azimuth_mean). The entries below are listed under the defining module.

Package root

VANE — Visualization and Automated Natural-frequency Extraction.

Automated OpenFAST linearization post-processing, physics-aware mode identification, and state-space export for digital twin development of fixed-bottom and floating offshore wind turbines.

The package root re-exports the most common entry points, so they can be imported as from vane import ...: ModalPipeline, PipelineResult, run_study(), discover_operating_points(), StudyResult, Provenance, and the extension protocols Tracker and Labeler. Each is documented in its own module below.

Pipeline

High-level modal-analysis pipeline.

ModalPipeline runs the full workflow end to end: for each operating point it applies the MBC3 transform, solves for modes, then links the modes across operating points, assembles the Campbell diagram, and flags resonance crossings. The PipelineResult exposes every intermediate product and convenience exports to a state-space system, so a complete analysis is a few lines of code.

DEFAULT_PARAMETER_RTOL = 0.0001

Default relative tolerance below which two operating points’ sweep-axis values are treated as the same point (a plateau), scaled by the largest-magnitude value on the axis. It sits well above the round-off that separates nominally equal trimmed/settled values (.lin headers carry a handful of significant digits, and azimuth averages of the same schedule differ only in the trailing digits) and well below any physically meaningful sweep step (typically 1e-2 or more of the axis magnitude).

class ModalPipeline(*, frequency_weight=0.5, mac_threshold=0.5, harmonics=(1, 3, 6, 9), tracking_strategy='single_path', branch_review=False, resolve=False, tracker=None, heavily_damped_threshold=0.99)[source]

Bases: object

End-to-end modal analysis from linearization files to a Campbell diagram.

Parameters:
  • frequency_weight (float)

  • mac_threshold (float)

  • harmonics (Sequence[int])

  • tracking_strategy (TrackingStrategy)

  • branch_review (bool)

  • resolve (bool)

  • tracker (Tracker | None)

  • heavily_damped_threshold (float | None)

run(operating_points, *, parameter_name='rotor_speed_rpm', parameter_rtol=0.0001)[source]

Run the full pipeline over a set of operating points.

Parameters:
  • operating_points (Sequence[Sequence[LinFile]]) – One entry per operating point, each a sequence of linearization files spanning that point’s azimuth sweep.

  • parameter_name (str, optional) – The operating parameter to plot against: "rotor_speed_rpm" (the default) or "wind_speed". Above rated the trimmed rotor speed is constant, so a sweep that extends past rated must use "wind_speed".

  • parameter_rtol (float, optional) – Relative tolerance, scaled by the largest-magnitude parameter value, below which two operating points count as the same point on the sweep axis. Such a plateau is rejected because ordering by nearly equal values is arbitrary and would link non-adjacent operating points during tracking. Default DEFAULT_PARAMETER_RTOL; must be a finite number in [0, 1).

Returns:

PipelineResult – Every intermediate product of the analysis. Operating points are ordered by ascending operating parameter regardless of input order, so tracking and resonance detection see a monotonic sweep.

Raises:

ValueError – If operating_points is empty, parameter_name is unknown, parameter_rtol is invalid, or the operating-parameter values are non-finite or not distinct (beyond parameter_rtol) across points.

Return type:

PipelineResult

class PipelineResult(mbc_results, solutions, identification, campbell, resonances, parameter_name)[source]

Bases: object

Outputs of a full modal-analysis run.

Parameters:
  • mbc_results (list[MBCResult]) – The MBC transform result for each operating point.

  • solutions (list[ModalSolution]) – The modal solution for each operating point.

  • identification (IdentificationResult) – The cross-operating-point mode tracks.

  • campbell (CampbellDiagram) – The assembled Campbell diagram.

  • resonances (list[ResonanceCrossing]) – Detected resonance crossings (empty for non-rotor-speed runs).

  • parameter_name (str) – Name of the operating parameter used.

mbc_results: list[MBCResult]
solutions: list[ModalSolution]
identification: IdentificationResult
campbell: CampbellDiagram
resonances: list[ResonanceCrossing]
parameter_name: str
property tracks: list[ModeTrack]

Return the identified mode tracks.

state_space(operating_point=0)[source]

Export the state-space system at one operating point.

Parameters:

operating_point (int, optional) – Index of the operating point to export (default the first).

Returns:

StateSpace – The continuous-time system from that operating point’s MBC result.

Raises:

IndexError – If operating_point is negative or out of range.

Return type:

StateSpace

Study and provenance

High-level study orchestration with reproducibility provenance.

A study discovers the linearization files of a campaign, runs the modal pipeline, and records the provenance of the result — the exact source files (with content hashes) grouped by operating point, each point’s case name, full azimuth list, and operating-parameter value, the tuning thresholds, and the library and environment versions — so a Campbell diagram can be tied back to precisely the inputs and assumptions that produced it. write_bundle serializes a reproducibility package (a JSON manifest plus the result tables).

class DiscoveredOperatingPoint(name, files)[source]

Bases: Sequence[LinFile]

A discovered operating point: its case name and its azimuth files.

Behaves as a sequence of its LinFile objects, so it can be passed wherever a sequence of linearization files is expected, while also carrying the <case> grouping name that discovery would otherwise lose.

Parameters:
  • name (str) – The <case> root the files were grouped under.

  • files (tuple[LinFile, ...]) – The parsed azimuth files of this operating point.

name: str
files: tuple[LinFile, ...]
class Environment(python_version, platform, dependencies)[source]

Bases: object

The software environment a study ran in.

Parameters:
  • python_version (str) – The CPython version (e.g. "3.11.9").

  • platform (str) – The platform identification string.

  • dependencies (dict[str, str]) – Version of each recorded scientific dependency.

python_version: str
platform: str
dependencies: dict[str, str]
class OperatingPointProvenance(name, n_azimuths, azimuths_deg, azimuth_min_deg, azimuth_max_deg, rotor_speed_rpm, wind_speed, parameter_value, source_files)[source]

Bases: object

Provenance of one operating point.

Parameters:
  • name (str) – The <case> grouping name (empty if the point was not discovered by name).

  • n_azimuths (int) – Number of azimuth samples in the operating point.

  • azimuths_deg (tuple[float, ...]) – Every sampled azimuth, in degrees.

  • azimuth_min_deg (float) – Smallest and largest sampled azimuth, in degrees.

  • azimuth_max_deg (float) – Smallest and largest sampled azimuth, in degrees.

  • rotor_speed_rpm (float) – Azimuth-averaged rotor speed, in rev/min.

  • wind_speed (float) – Azimuth-averaged hub-height wind speed, in m/s.

  • parameter_value (float) – The operating-parameter value used to order the sweep.

  • source_files (tuple[SourceFile, ...]) – The files of this operating point, each with its content hash.

name: str
n_azimuths: int
azimuths_deg: tuple[float, ...]
azimuth_min_deg: float
azimuth_max_deg: float
rotor_speed_rpm: float
wind_speed: float
parameter_value: float
source_files: tuple[SourceFile, ...]
class Provenance(vane_version, created_at, parameter_name, frequency_weight, mac_threshold, tracking_strategy, branch_review, resolve, harmonics, environment, operating_points, n_tracks, n_resonances, n_branch_groups, parameter_rtol=0.0001)[source]

Bases: object

A reproducibility record for a study run.

Parameters:
  • vane_version (str) – Version of the library that produced the result.

  • created_at (str) – ISO-8601 timestamp of when the provenance was recorded.

  • parameter_name (str) – Operating parameter the sweep was run against.

  • frequency_weight (float) – Tracking tuning thresholds used.

  • mac_threshold (float) – Tracking tuning thresholds used.

  • tracking_strategy (str) – Cross-operating-point tracking strategy used ("single_path" or "simultaneous").

  • branch_review (bool) – Whether the branch-review pass was run.

  • resolve (bool) – Whether the smooth (minimum-curvature) alternative was computed for eligible branch groups.

  • harmonics (tuple[int, ...]) – Excitation harmonics used for resonance detection.

  • environment (Environment) – The Python, platform, and dependency versions.

  • operating_points (tuple[OperatingPointProvenance, ...]) – Per-operating-point provenance, in the (sorted) order of the result.

  • n_tracks (int) – Number of identified mode tracks, detected resonance crossings, and contested branch groups (the last is 0 unless branch_review was run).

  • n_resonances (int) – Number of identified mode tracks, detected resonance crossings, and contested branch groups (the last is 0 unless branch_review was run).

  • n_branch_groups (int) – Number of identified mode tracks, detected resonance crossings, and contested branch groups (the last is 0 unless branch_review was run).

  • parameter_rtol (float, optional) – Relative plateau tolerance the sweep axis was validated with, so the run can be reproduced with the same acceptance (default vane.pipeline.DEFAULT_PARAMETER_RTOL).

vane_version: str
created_at: str
parameter_name: str
frequency_weight: float
mac_threshold: float
tracking_strategy: str
branch_review: bool
resolve: bool
harmonics: tuple[int, ...]
environment: Environment
operating_points: tuple[OperatingPointProvenance, ...]
n_tracks: int
n_resonances: int
n_branch_groups: int
parameter_rtol: float = 0.0001
property source_files: tuple[SourceFile, ...]

Return every source file across all operating points (flattened).

to_dict()[source]

Return the provenance as a JSON-serializable dictionary.

Return type:

dict[str, object]

class SourceFile(path, sha256)[source]

Bases: object

A source linearization file and the hash of its contents.

Parameters:
  • path (str) – Path the file was read from.

  • sha256 (str) – Hex-encoded SHA-256 digest of the file’s bytes.

path: str
sha256: str
class StudyResult(pipeline, provenance)[source]

Bases: object

The pipeline result of a study together with its provenance.

Parameters:
  • pipeline (PipelineResult) – Every intermediate product of the analysis.

  • provenance (Provenance) – The reproducibility record.

pipeline: PipelineResult
provenance: Provenance
write_bundle(output_dir, *, report=True)[source]

Write a reproducibility bundle to output_dir.

The bundle contains provenance.json (the manifest of inputs and assumptions), campbell.csv (the tracked-mode result table), and, unless disabled, a self-contained report.html audit dashboard.

Parameters:
  • output_dir (str or pathlib.Path) – Destination directory; created if it does not exist.

  • report (bool, optional) – Also write the HTML audit report (report.html). Default True.

Return type:

None

discover_operating_points(directory)[source]

Discover and group a directory’s .lin files into operating points.

Files are named <case>.<index>.lin by OpenFAST; those sharing a <case> root are one operating point’s azimuth sweep, and the case name is retained on the returned DiscoveredOperatingPoint. Per-module files written under LinOutMod = True (OpenFAST names them <case>.<index>.<Module>.lin, e.g. case.1.ED.lin, case.1.SEA.lin, case.1.BD1.lin) are always excluded, whatever the alphabetic module token. Files named <case>.<Module>.<index>.lin (e.g. case.ED.1.lin) are excluded when every index of the <case>.<Module> group has a full-system twin <case>.<index>.lin: they describe a single module’s sub-system, not the coupled turbine, so they must not be treated as operating points. A module-like group without a full set of twins is kept, since the case root then genuinely ends in that token (e.g. a separate case.ED.fst run). See _module_sidecar_files() for the residual ambiguity, excluded with a warning.

Parameters:

directory (str or pathlib.Path) – Directory containing .lin linearization files.

Returns:

list[DiscoveredOperatingPoint] – One named operating point per <case> root, ordered by case.

Raises:

FileNotFoundError – If the directory contains no full-system .lin files.

Return type:

list[DiscoveredOperatingPoint]

run_study(operating_points, *, parameter_name='rotor_speed_rpm', parameter_rtol=0.0001, frequency_weight=0.5, mac_threshold=0.5, harmonics=(1, 3, 6, 9), tracking_strategy='single_path', branch_review=False, resolve=False, timestamp=None)[source]

Run the modal pipeline and record the provenance of the result.

Parameters:
  • operating_points (Sequence[Sequence[LinFile]]) – One azimuth sweep per operating point (e.g. from discover_operating_points()).

  • parameter_name (str, optional) – Operating parameter to run against ("rotor_speed_rpm" or "wind_speed"). A sweep that extends past rated, where the trimmed rotor speed is constant, must use "wind_speed".

  • parameter_rtol (float, optional) – Relative plateau tolerance of the sweep axis (see vane.pipeline.ModalPipeline.run()). Default vane.pipeline.DEFAULT_PARAMETER_RTOL.

  • frequency_weight (float, optional) – Tracking tuning thresholds.

  • mac_threshold (float, optional) – Tracking tuning thresholds.

  • harmonics (Sequence[int], optional) – Excitation harmonics for resonance detection.

  • tracking_strategy ({"single_path", "simultaneous"}, optional) – Cross-operating-point tracking strategy; recorded in the provenance. See vane.modal.identifier.identify_modes().

  • branch_review (bool, optional) – When True, flag crossing/veering hotspots for audit; recorded in the provenance and rendered in the report. Default False.

  • resolve (bool, optional) – When True, also compute the smooth (minimum-curvature) alternative for eligible hotspots (implies branch_review). Default False.

  • timestamp (datetime or None, optional) – Timestamp to record (defaults to the current UTC time); injectable for reproducible tests.

Returns:

StudyResult – The pipeline result and its provenance.

Raises:

ValueError – If parameter_name is not a known operating parameter, parameter_rtol is invalid, or the operating-parameter values are non-finite or not distinct beyond parameter_rtol (a plateau, such as the constant rotor speed above rated; use "wind_speed" there).

Return type:

StudyResult

Extension protocols

Extension protocols for VANE’s pluggable analysis stages.

VANE’s two model-dependent stages, cross-operating-point tracking and physical labelling, are each defined here as a typing.Protocol, so an alternative implementation can be used wherever the built-in is used, without subclassing anything. These are static (type-checker) contracts; the consumers accept the protocol type, so a static type checker verifies that a custom implementation fits.

Tracker and Labeler are callable-only protocols and are deliberately not runtime_checkable: an isinstance check against a callable Protocol only confirms that the object is callable (not its signature or return type), so it would accept a non-tracker and fail later. Use static typing, or simply the injection points, for them.

The built-ins satisfy these contracts structurally:

  • vane.modal.identify_modes() is a Tracker;

  • vane.modal.label_solution() is a Labeler.

The consumers accept the protocol type, not the concrete class: vane.ModalPipeline takes an optional tracker. An LTP/Floquet tracker or an alternative rule-based labeller can therefore be used without changing the pipeline.

class Labeler(*args, **kwargs)[source]

Bases: Protocol

Assign a physical label to every mode of a solution.

Any callable taking a ModalSolution and returning one ModeLabel per mode (in mode order) is a labeler. vane.modal.label_solution() is the built-in. A static (callable) contract: not runtime_checkable (see the module docstring).

class Tracker(*args, **kwargs)[source]

Bases: Protocol

Link modes across operating points into labeled tracks.

Any callable taking the per-operating-point solutions (with the optional sweep axis) and returning an IdentificationResult is a tracker. vane.modal.identify_modes() is the built-in; a Floquet/LTP tracker conforms by presenting the same call. A static (callable) contract: not runtime_checkable (see the module docstring).

Command-line interface

The vane console command wraps the Python API. Run vane --help or vane <command> --help for the options of each command.

Command-line interface for VANE.

Exposes the high-level analysis as a vane console command so a linearization sweep can be post-processed without writing Python:

  • vane modes — the modal solution for a single operating point’s .lin files;

  • vane campbell — the tracked Campbell diagram across a directory of operating points;

  • vane study — a full provenance-tracked study written as a reproducibility bundle (provenance.json + campbell.csv + an HTML audit report).

Each command is a thin wrapper over the same public API used from Python (ModalPipeline, run_study(), discover_operating_points(), and vane.export), so the CLI adds convenience without a second code path. Known input errors are reported as a concise message on stderr with a non-zero exit code rather than a traceback.

class Parameter(*values)[source]

Bases: str, Enum

Operating parameter to analyse a sweep against.

rpm = 'rpm'
wind = 'wind'
class Tracking(*values)[source]

Bases: str, Enum

Cross-operating-point mode-tracking strategy.

single_path = 'single_path'
simultaneous = 'simultaneous'
main(version=False)[source]

Automated OpenFAST linearization post-processing and mode identification.

Parameters:

version (Annotated[bool, <typer.models.OptionInfo object at 0x736e29a4d8b0>])

Return type:

None

modes(lin_files, output=None)[source]

Compute the modal solution for one operating point’s linearization files.

Parameters:
  • lin_files (Annotated[list[Path], <typer.models.ArgumentInfo object at 0x736e29a4fa10>])

  • output (Annotated[Path | None, <typer.models.OptionInfo object at 0x736e29a4ec90>])

Return type:

None

campbell(directory, output=None, parameter=Parameter.rpm)[source]

Build the tracked Campbell diagram across a directory of operating points.

Parameters:
  • directory (Annotated[Path, <typer.models.ArgumentInfo object at 0x736e29a4c920>])

  • output (Annotated[Path | None, <typer.models.OptionInfo object at 0x736e29a4cce0>])

  • parameter (Annotated[Parameter, <typer.models.OptionInfo object at 0x736e29a4ce30>])

Return type:

None

study(directory, output_dir, parameter=Parameter.rpm, tracking=Tracking.single_path, branch_review=False, resolve=False, report=True)[source]

Run a provenance-tracked study and write a reproducibility bundle.

Parameters:
  • directory (Annotated[Path, <typer.models.ArgumentInfo object at 0x736e29ab48f0>])

  • output_dir (Annotated[Path, <typer.models.OptionInfo object at 0x736e29ab4b00>])

  • parameter (Annotated[Parameter, <typer.models.OptionInfo object at 0x736e29ab4b90>])

  • tracking (Annotated[Tracking, <typer.models.OptionInfo object at 0x736e29ab4d40>])

  • branch_review (Annotated[bool, <typer.models.OptionInfo object at 0x736e29ab4e60>])

  • resolve (Annotated[bool, <typer.models.OptionInfo object at 0x736e29ab4f80>])

  • report (Annotated[bool, <typer.models.OptionInfo object at 0x736e29ab50d0>])

Return type:

None

I/O

I/O layer — OpenFAST linearization and model file parsers, MBC3 transform.

Reader for OpenFAST linearization (.lin) files.

This module parses the modern linearization output produced by the OpenFAST ModGlue engine into structured, in-memory objects. The modern format is identified by the presence of a Derivative Order column in the operating-point tables; the legacy format (which lacks that column and instead emits StateRotation / mass-matrix blocks) is intentionally not supported and raises LinFileFormatError.

The on-disk layout consumed here is, in order:

  1. A header block beginning at Simulation information: holding scalar metadata (simulation time, rotor speed in rad/s, azimuth in rad, wind speed, and channel counts) and terminated by Jacobians included in this file?.

  2. Operating-point tables (Order of continuous states: and the analogous blocks for state derivatives, inputs, and outputs). Each row carries an index, an operating-point value, a rotating-frame flag, a derivative order, and a free-text description.

  3. Matrix blocks (A, B, C, D under Linearized state matrices: and, when Jacobians are written, dUdu/dUdy/dXdy/J under Jacobian matrices:).

class LinFile(path, sim_time, rotor_speed, azimuth, wind_speed, jacobians_included, x, xdot, u, y, a, b, c, d, jacobians)[source]

Bases: object

Parsed contents of a single OpenFAST .lin linearization file.

Parameters:
  • path (pathlib.Path) – Source file the data was read from.

  • sim_time (float) – Simulation time of the linearization, in seconds.

  • rotor_speed (float) – Rotor speed at the operating point, in rad/s (as written by OpenFAST).

  • azimuth (float) – Rotor azimuth at the operating point, in rad.

  • wind_speed (float) – Mean hub-height wind speed, in m/s. Zero when InflowWind is absent.

  • jacobians_included (bool) – Whether the file contains the Jacobian matrices block.

  • x (OperatingPointTable) – Operating-point tables for the continuous states, continuous-state derivatives, inputs, and outputs, respectively.

  • xdot (OperatingPointTable) – Operating-point tables for the continuous states, continuous-state derivatives, inputs, and outputs, respectively.

  • u (OperatingPointTable) – Operating-point tables for the continuous states, continuous-state derivatives, inputs, and outputs, respectively.

  • y (OperatingPointTable) – Operating-point tables for the continuous states, continuous-state derivatives, inputs, and outputs, respectively.

  • a (npt.NDArray[np.float64] or None) – State-space matrices \(A\) (n_x, n_x), \(B\) (n_x, n_u), \(C\) (n_y, n_x), and \(D\) (n_y, n_u). None when the corresponding block is absent (e.g. B/D with zero inputs).

  • b (npt.NDArray[np.float64] or None) – State-space matrices \(A\) (n_x, n_x), \(B\) (n_x, n_u), \(C\) (n_y, n_x), and \(D\) (n_y, n_u). None when the corresponding block is absent (e.g. B/D with zero inputs).

  • c (npt.NDArray[np.float64] or None) – State-space matrices \(A\) (n_x, n_x), \(B\) (n_x, n_u), \(C\) (n_y, n_x), and \(D\) (n_y, n_u). None when the corresponding block is absent (e.g. B/D with zero inputs).

  • d (npt.NDArray[np.float64] or None) – State-space matrices \(A\) (n_x, n_x), \(B\) (n_x, n_u), \(C\) (n_y, n_x), and \(D\) (n_y, n_u). None when the corresponding block is absent (e.g. B/D with zero inputs).

  • jacobians (dict[str, npt.NDArray[np.float64]]) – Any additional matrices from the Jacobian matrices block, keyed by their label (e.g. "dUdu", "dUdy", "dXdy").

path: Path
sim_time: float
rotor_speed: float
azimuth: float
wind_speed: float
jacobians_included: bool
x: OperatingPointTable
xdot: OperatingPointTable
u: OperatingPointTable
y: OperatingPointTable
a: npt.NDArray[np.float64] | None
b: npt.NDArray[np.float64] | None
c: npt.NDArray[np.float64] | None
d: npt.NDArray[np.float64] | None
jacobians: dict[str, npt.NDArray[np.float64]]
property n_x: int

Return the number of continuous states.

property n_u: int

Return the number of inputs.

property n_y: int

Return the number of outputs.

exception LinFileFormatError[source]

Bases: ValueError

Raised when a .lin file cannot be parsed as the modern format.

class NonFiniteLinValue(path, section, variable, token, line_number)[source]

Bases: object

The first non-finite number found in a .lin file.

Parameters:
  • path (pathlib.Path) – The .lin file scanned.

  • section (str) – Where the value sits: "header" for a header scalar, "x"/"xdot"/ "u"/"y" for an operating-point table, or the matrix label ("A", "B", "C", "D" or a Jacobian such as "dUdu").

  • variable (str) – What the value belongs to: the header field ("Rotor Speed"), the channel description of an operating-point row ("AD x3 blade 1, node 59, -"), or the matrix entry with its row and column channels ("A[247, 1] (row: ...; column: ...)", 1-based).

  • token (str) – The value as written in the file ("NaN", "Infinity", "*****", …).

  • line_number (int) – The 1-based line of the file holding the value.

path: Path
section: str
variable: str
token: str
line_number: int
property summary: str

Return the location, variable and token as one line.

class OperatingPointTable(values, rotating_frame, derivative_order, descriptions)[source]

Bases: object

Operating-point metadata for one channel group of a .lin file.

A channel group is the set of continuous states, continuous-state derivatives, inputs, or outputs. All array attributes are parallel and share the channel ordering written in the file.

Parameters:
  • values (npt.NDArray[np.float64]) – Operating-point value of each channel, shape (n_channels,). For orientation channels that are written with multiple comma-separated components, only the first component is retained here.

  • rotating_frame (npt.NDArray[np.bool]) – Per-channel flag, shape (n_channels,). True marks a channel expressed in the blade-rotating frame (the hook used by the MBC3 transform).

  • derivative_order (npt.NDArray[np.int]) – Per-channel derivative order (0, 1, or 2), shape (n_channels,).

  • descriptions (list[str]) – Per-channel free-text description, length n_channels. Each begins with the originating module prefix (e.g. "ED ...").

values: npt.NDArray[np.float64]
rotating_frame: npt.NDArray[np.bool_]
derivative_order: npt.NDArray[np.int_]
descriptions: list[str]
property modules: list[str]

Return the originating module prefix of each channel.

Returns:

list[str] – The leading module token of every description (e.g. "ED" or "BD_1"); an empty string is used when no prefix is present.

find_nonfinite_lin_value(path)[source]

Return the first non-finite number in a .lin file, or None.

The scan covers the header scalars, the four operating-point tables (every component of an orientation channel) and every matrix block. read_lin_file() rejects a non-finite header scalar or operating-point value but accepts a non-finite matrix entry as nan. This scan finds both, and names the channel the value belongs to.

Parameters:

path (str or os.PathLike) – The .lin file to scan.

Returns:

NonFiniteLinValue or None – The first non-finite value in file order, or None when every number in the file is finite.

Raises:

OSError – If the file cannot be read.

Return type:

NonFiniteLinValue | None

Notes

The file is first searched as a whole for a token that spells a non-finite number (NaN, Inf, Infinity, a Fortran * overflow run or an Intel/MSVC 1.#QNAN-style marker), which is fast even for a file of many megabytes. Only a file with such a token is then walked line by line, so that a non-numeric word (in a channel description, say) is never reported. Lines that do not follow the modern layout are skipped rather than rejected: the scan reports non-finite numbers and does not validate the format.

read_lin_file(path)[source]

Read and parse a modern OpenFAST .lin file.

Parameters:

path (str or os.PathLike) – Path to the .lin file to read.

Returns:

LinFile – The parsed linearization data.

Raises:

LinFileFormatError – If the file is empty, truncated, in the unsupported legacy format, or otherwise does not conform to the modern ModGlue layout.

Return type:

LinFile

Notes

Numeric fields containing Fortran overflow markers (runs of *) are parsed as numpy.nan and a warning is logged; the parse does not fail. Empty B/C/D blocks (no inputs/outputs) are not written by OpenFAST, so the corresponding LinFile attribute is None; the A matrix, however, is required whenever the file declares continuous states.

Readers for OpenFAST model input decks (.fst and ElastoDyn).

These parsers extract only the information VANE needs to drive linearization post-processing: the linearization configuration block from the primary .fst file (so the expected set of .lin files is known), the active module switches and referenced sub-file paths, and the rotor/tower geometry from ElastoDyn (used to scale mode shapes).

OpenFAST input files use the convention <value(s)> <Keyword> - <description>; the value(s) precede the keyword on each line. Quoted string values and multi-value array lines (e.g. LinTimes) are both handled.

class FastModel(path, comp, files, lin)[source]

Bases: object

Subset of a primary .fst model relevant to linearization.

Parameters:
  • path (pathlib.Path) – Path to the primary .fst file.

  • comp (dict[str, int]) – Active-module switches keyed by name (e.g. "CompHydro").

  • files (dict[str, pathlib.Path]) – Referenced sub-file paths resolved relative to path’s directory, keyed by their keyword (e.g. "EDFile"). Entries marked "unused" in the deck are omitted.

  • lin (LinearizationConfig) – Parsed linearization configuration.

path: Path
comp: dict[str, int]
files: dict[str, Path]
lin: LinearizationConfig
property ed_file: Path | None

Return the resolved ElastoDyn input path, or None if unset.

property is_offshore: bool

Return whether the model includes sea-state or hydrodynamic modules.

property is_floating: bool

Return a heuristic for a floating platform (has mooring, no SubDyn).

Notes

This is an approximation based on module switches: floating platforms use a mooring system (CompMooring >= 1) and no SubDyn substructure (CompSub == 0). Detailed system classification (spar/semi/TLP) is the responsibility of the configuration layer, not this reader.

exception FstFileError[source]

Bases: ValueError

Raised when an OpenFAST input file is malformed or missing a field.

class LinearizationConfig(linearize, calc_steady, trim_case, trim_tol, trim_gain, n_lin_times, lin_times, lin_inputs, lin_outputs, lin_out_jac, lin_out_mod)[source]

Bases: object

Linearization settings parsed from a primary .fst file.

Parameters:
  • linearize (bool) – Master linearization switch (Linearize).

  • calc_steady (bool) – Whether a periodic steady-state trim precedes linearization (CalcSteady); when True, n_lin_times azimuth snapshots are produced per operating point.

  • trim_case (int) – Trim target {1: yaw, 2: torque, 3: pitch} (TrimCase).

  • trim_tol (float) – Rotor-speed convergence tolerance (TrimTol).

  • trim_gain (float) – Proportional trim gain (TrimGain).

  • n_lin_times (int) – Number of linearization snapshots (NLinTimes); the number of .lin files written per operating point.

  • lin_times (list[float]) – Absolute linearization times in seconds (LinTimes); meaningful only when calc_steady is False.

  • lin_inputs (int) – Inputs included {0: none, 1: standard, 2: all} (LinInputs).

  • lin_outputs (int) – Outputs included {0: none, 1: from OutList, 2: all} (LinOutputs).

  • lin_out_jac (bool) – Whether full Jacobians are written (LinOutJac).

  • lin_out_mod (bool) – Whether per-module .lin files are written (LinOutMod).

linearize: bool
calc_steady: bool
trim_case: int
trim_tol: float
trim_gain: float
n_lin_times: int
lin_times: list[float]
lin_inputs: int
lin_outputs: int
lin_out_jac: bool
lin_out_mod: bool
class TurbineGeometry(num_blades, tip_radius, hub_radius, tower_height, tower_base_height)[source]

Bases: object

Rotor and tower geometry parsed from an ElastoDyn input file.

Parameters:
  • num_blades (int) – Number of blades (NumBl).

  • tip_radius (float) – Rotor apex-to-tip distance in metres (TipRad).

  • hub_radius (float) – Rotor apex-to-blade-root distance in metres (HubRad).

  • tower_height (float) – Tower-top height above its reference datum in metres (TowerHt).

  • tower_base_height (float) – Tower-base height above the same datum in metres (TowerBsHt); 0.0 when the ElastoDyn file does not define it.

num_blades: int
tip_radius: float
hub_radius: float
tower_height: float
tower_base_height: float
property blade_length: float

Return the flexible blade length TipRad - HubRad in metres.

property tower_length: float

Return the flexible tower length TowerHt - TowerBsHt in metres.

fortran_float(value)[source]

Return a deck value read as a Fortran real.

The token is cut at its first comma (a list-directed separator, as in 1.21D+02, TipRad), quotes are stripped, and a double-precision exponent (1.21D+02, 5d-3) is read like E, as Fortran list-directed input does.

Parameters:

value (str) – The value token.

Returns:

float – The number.

Raises:

ValueError – If the token is not a number.

Return type:

float

read_elastodyn_geometry(path)[source]

Read rotor and tower geometry from an ElastoDyn input file.

Parameters:

path (pathlib.Path) – Path to the ElastoDyn .dat file.

Returns:

TurbineGeometry – The parsed rotor/tower geometry.

Raises:

FstFileError – If the file cannot be read or a required field is missing.

Return type:

TurbineGeometry

Notes

NumBl, TipRad, HubRad, and TowerHt are required. TowerBsHt is optional and defaults to 0.0 (tower base at the datum) when absent, as in ElastoDyn files that omit it.

read_fst_file(path)[source]

Read a primary OpenFAST .fst file.

Parameters:

path (pathlib.Path) – Path to the .fst file.

Returns:

FastModel – Parsed module switches, referenced file paths, and linearization config.

Raises:

FstFileError – If the file cannot be read. Missing module switches and linearization settings are tolerated and fall back to their defaults.

Return type:

FastModel

Format-preserving editor for OpenFAST input files.

OpenFAST input decks use the convention <value(s)> <Keyword> - <description>, where the value(s) precede the keyword on each line. To drive a linearization, VANE must change a handful of these values — enable Linearize, set NLinTimes, set the operating point’s RotSpeed and HWindSpeed, and so on — without disturbing the rest of the deck. This module provides FastInputFile, a small editor that reads a deck, looks up a keyword, replaces its value while preserving the keyword spelling, the trailing comment, and the column layout, and writes the deck back. It is the write-side counterpart of vane.io.fst_reader.

The editor operates on a single file. A deck spans several files (the primary .fst references ElastoDyn, InflowWind, ServoDyn, …); FastInputFile. resolve() returns a referenced sub-file’s path so the caller can open and edit it the same way.

exception FastInputError[source]

Bases: ValueError

Raised when an OpenFAST input file keyword cannot be found or edited.

class FastInputFile(path, lines)[source]

Bases: object

An editable OpenFAST input deck file.

The file is held as its original lines; set() rewrites only the value of a single keyword’s line, leaving every other character — including the keyword, the - description comment, and the surrounding whitespace — intact.

Parameters:
  • path (pathlib.Path) – Path the deck was read from (used to resolve referenced sub-files and as the default write destination).

  • lines (list[str]) – The file’s lines, without trailing newlines.

classmethod read(path)[source]

Read an OpenFAST input file.

Parameters:

path (str or pathlib.Path) – Path to the input file.

Returns:

FastInputFile – The editable deck.

Raises:

FastInputError – If the file cannot be read.

Return type:

FastInputFile

Notes

The file is decoded as UTF-8; a byte that is not UTF-8 (a Windows-1252 ° in a comment, say) is kept as is and written back unchanged by write(), as OpenFAST itself reads the file byte by byte.

tokens(keyword)[source]

Return the value tokens preceding keyword on its line.

Parameters:

keyword (str) – The exact keyword (field name) to locate.

Returns:

list[str] – The whitespace-delimited value tokens before the keyword.

Raises:

FastInputError – If the keyword is not present.

Return type:

list[str]

get(keyword)[source]

Return the first value token of keyword.

Parameters:

keyword (str) – The keyword to read.

Returns:

str – The first value token (quotes preserved).

Raises:

FastInputError – If the keyword is not present or has no value.

Return type:

str

set(keyword, value)[source]

Replace the value of keyword in place.

The keyword, its column, and the trailing - description comment are preserved; only the leading value field is rewritten. Booleans render as True/False; ints and floats use str; a str is written verbatim (so quoted paths and format codes such as '"ES20.11E3"' are the caller’s responsibility).

Parameters:
  • keyword (str) – The keyword whose value to change.

  • value (str or int or float or bool) – The new value.

Raises:

FastInputError – If the keyword is not present.

Return type:

None

set_array(keyword, values)[source]

Replace keyword with a space-separated list of values.

Used for array fields such as LinTimes.

Parameters:
  • keyword (str) – The array keyword.

  • values (Sequence[float]) – The values to write, joined with two spaces.

Raises:

FastInputError – If the keyword is not present.

Return type:

None

resolve(keyword)[source]

Return the path of a referenced sub-file given its keyword.

The value is treated as a path (quoted with " or ', as Fortran accepts, or bare) relative to this deck’s directory.

Parameters:

keyword (str) – The keyword whose value is a sub-file path (e.g. "EDFile").

Returns:

pathlib.Path – The resolved sub-file path.

Raises:

FastInputError – If the keyword is absent or its value is empty/"unused".

Return type:

Path

text()[source]

Return the current deck as text with a trailing newline.

Return type:

str

write(path=None)[source]

Write the deck to disk.

Parameters:

path (str or pathlib.Path or None, optional) – Destination path; defaults to the path the deck was read from. Parent directories are created.

Returns:

pathlib.Path – The path written to.

Return type:

Path

Notes

The write is atomic: the text is written to a freshly created, uniquely named temporary file in the same directory and then Path.replace()-d into place. This avoids leaving a partially written deck on failure and — because the replace swaps the directory entry to a new inode — editing a hard-linked copy never modifies the shared (e.g. template) inode, which lets a sweep hard-link its heavy read-only inputs while still editing the small deck files safely. The temporary file is created exclusively (a unique name), so it never collides with or mutates a pre-existing .tmp hard link, and an existing deck’s permission bits are preserved across the replace.

On Windows the temporary-file creation and the replace can fail transiently with an access-denied or sharing-violation error while another process (an antivirus scan, the search indexer, or an OpenFAST process still releasing its handles) briefly holds the deck or its directory. The whole write is then retried a few times with a short backoff (see vane.utils.fs_retry.retry_transient()) before the error is raised.

Multi-blade coordinate (MBC3 / Coleman) transformation.

OpenFAST writes linearized state matrices in a mixed frame in which the blade degrees of freedom rotate with the rotor, making the system matrices azimuth-dependent. The MBC3 transform projects the three per-blade rotating coordinates onto non-rotating multi-blade coordinates (collective, cosine, and sine), and averaging the transformed matrices over one revolution yields the azimuth-invariant linear time-invariant system used for modal analysis.

This module implements the transform for three-bladed rotors directly from the parsed LinFile objects of a single operating point (one file per azimuth). Continuous states are reordered into the canonical [q2, q2dot, q1] layout (second-order displacements, their velocities, then first-order states) and blade triplets are detected from the channel descriptions. The transformed A, B, C, and D matrices are averaged over azimuth.

Notes

At zero rotor speed the transform reduces to a similarity transform of the state matrix and therefore preserves its eigenvalues; this property is used to verify the implementation.

class MBCResult(avg_a, avg_b, avg_c, avg_d, state_descriptions, ndof2, ndof1, n_blades, performed_transformation, rotor_speed_rpm, wind_speed, azimuths_deg, mbc_coordinates=<factory>, per_azimuth_a=None, output_descriptions=<factory>, input_descriptions=<factory>, output_mbc_coordinates=<factory>, input_mbc_coordinates=<factory>, output_operating_point=None, input_operating_point=None, per_azimuth_b=None, per_azimuth_c=None, per_azimuth_d=None, per_azimuth_output_operating_point=None, per_azimuth_input_operating_point=None)[source]

Bases: object

Result of an MBC3 transform over one operating point’s azimuth sweep.

Parameters:
  • avg_a (npt.NDArray[np.float64] or None) – Azimuth-averaged state-space matrices in the non-rotating frame. Each is None when the corresponding input matrix was absent. When the transform is performed, rows/columns follow the reordered [q2, q2dot, q1] state layout; otherwise the original order is kept.

  • avg_b (npt.NDArray[np.float64] or None) – Azimuth-averaged state-space matrices in the non-rotating frame. Each is None when the corresponding input matrix was absent. When the transform is performed, rows/columns follow the reordered [q2, q2dot, q1] state layout; otherwise the original order is kept.

  • avg_c (npt.NDArray[np.float64] or None) – Azimuth-averaged state-space matrices in the non-rotating frame. Each is None when the corresponding input matrix was absent. When the transform is performed, rows/columns follow the reordered [q2, q2dot, q1] state layout; otherwise the original order is kept.

  • avg_d (npt.NDArray[np.float64] or None) – Azimuth-averaged state-space matrices in the non-rotating frame. Each is None when the corresponding input matrix was absent. When the transform is performed, rows/columns follow the reordered [q2, q2dot, q1] state layout; otherwise the original order is kept.

  • state_descriptions (list[str]) – State descriptions in the same order as the rows/columns of avg_a.

  • ndof2 (int) – Number of second-order degrees of freedom.

  • ndof1 (int) – Number of first-order states.

  • n_blades (int) – Number of blades inferred from the rotating triplets (3, or 0 when no rotating triplets were found).

  • performed_transformation (bool) – Whether the MBC3 transform was applied. False when no three-blade triplets were detected, in which case the matrices are plain azimuth averages in the original state order.

  • rotor_speed_rpm (float) – Mean rotor speed over the azimuth sweep, in rev/min.

  • wind_speed (float) – Mean wind speed over the azimuth sweep, in m/s.

  • azimuths_deg (npt.NDArray[np.float64]) – Sorted rotor azimuths used, in degrees.

  • mbc_coordinates (list[str]) – Multi-blade coordinate of each state, aligned with state_descriptions: "collective", "cosine", or "sine" for a transformed blade-triplet state, and "" for non-rotating states or when the transform was not performed.

  • output_descriptions (list[str]) – Output channel descriptions aligned with the rows of avg_c/avg_d (in the transformed output order). Empty when no output matrix is present.

  • input_descriptions (list[str]) – Input channel descriptions aligned with the columns of avg_b/avg_d (in the transformed input order). Empty when no input matrix is present.

  • output_mbc_coordinates (list[str]) – Multi-blade coordinate of each output ("collective"/"cosine"/ "sine" for a transformed blade-triplet output, else ""), aligned with output_descriptions. A transformed output row is a multi-blade combination, not the per-blade channel its description names.

  • input_mbc_coordinates (list[str]) – Multi-blade coordinate of each input, aligned with input_descriptions.

  • output_operating_point (npt.NDArray[np.float64] or None) – Azimuth-averaged output operating point in the transformed output order. This is the baseline that must be subtracted from absolute measurements before a perturbation-state estimator is run.

  • input_operating_point (npt.NDArray[np.float64] or None) – Azimuth-averaged input operating point in the transformed input order.

  • per_azimuth_a (list of array or None) – The transformed (non-rotating) state-space matrices at each azimuth, in the same state/input/output order as their averaged counterparts. They are retained only when retain_per_azimuth=True and the corresponding matrix exists. The complete ensemble supports azimuth-periodic input/output simulation; the A stack also underpins Floquet and azimuth-spread analyses.

  • per_azimuth_b (list of array or None) – The transformed (non-rotating) state-space matrices at each azimuth, in the same state/input/output order as their averaged counterparts. They are retained only when retain_per_azimuth=True and the corresponding matrix exists. The complete ensemble supports azimuth-periodic input/output simulation; the A stack also underpins Floquet and azimuth-spread analyses.

  • per_azimuth_c (list of array or None) – The transformed (non-rotating) state-space matrices at each azimuth, in the same state/input/output order as their averaged counterparts. They are retained only when retain_per_azimuth=True and the corresponding matrix exists. The complete ensemble supports azimuth-periodic input/output simulation; the A stack also underpins Floquet and azimuth-spread analyses.

  • per_azimuth_d (list of array or None) – The transformed (non-rotating) state-space matrices at each azimuth, in the same state/input/output order as their averaged counterparts. They are retained only when retain_per_azimuth=True and the corresponding matrix exists. The complete ensemble supports azimuth-periodic input/output simulation; the A stack also underpins Floquet and azimuth-spread analyses.

  • per_azimuth_output_operating_point (list of array or None) – Transformed output operating-point vectors at each azimuth, aligned with the output descriptions. These preserve deterministic periodic loads that an azimuth average would otherwise erase.

  • per_azimuth_input_operating_point (list of array or None) – Transformed input operating-point vectors at each azimuth, aligned with the input descriptions. These preserve deterministic periodic controls.

avg_a: npt.NDArray[np.float64] | None
avg_b: npt.NDArray[np.float64] | None
avg_c: npt.NDArray[np.float64] | None
avg_d: npt.NDArray[np.float64] | None
state_descriptions: list[str]
ndof2: int
ndof1: int
n_blades: int
performed_transformation: bool
rotor_speed_rpm: float
wind_speed: float
azimuths_deg: npt.NDArray[np.float64]
mbc_coordinates: list[str]
per_azimuth_a: list[npt.NDArray[np.float64]] | None = None
output_descriptions: list[str]
input_descriptions: list[str]
output_mbc_coordinates: list[str]
input_mbc_coordinates: list[str]
output_operating_point: npt.NDArray[np.float64] | None = None
input_operating_point: npt.NDArray[np.float64] | None = None
per_azimuth_b: list[npt.NDArray[np.float64]] | None = None
per_azimuth_c: list[npt.NDArray[np.float64]] | None = None
per_azimuth_d: list[npt.NDArray[np.float64]] | None = None
per_azimuth_output_operating_point: list[npt.NDArray[np.float64]] | None = None
per_azimuth_input_operating_point: list[npt.NDArray[np.float64]] | None = None
find_blade_triplets(descriptions, rotating_frame)[source]

Group rotating channels into per-blade triplets by description.

For each rotating channel, every blade-index token is located (prose blade 1, BeamDyn BD_1, blade-coordinate/AeroDyn b1/B1, PitchBearing1, or a fallback single digit). A sibling matcher is built in which all blade digits are tied to one captured value, so a triplet is confirmed only when the other rotating channels are identical except for a consistent blade number — node, mode, and span indices stay literal and are never mistaken for the blade index, even when a channel carries more than one blade token. The (internal DOF index = ...) parenthetical, which holds a second blade-numbered token, is removed before matching.

Parameters:
  • descriptions (Sequence[str]) – Channel descriptions for one channel group.

  • rotating_frame (npt.NDArray[np.bool]) – Per-channel rotating-frame flags, parallel to descriptions.

Returns:

list[tuple[int, …]] – Triplets of indices into descriptions, each ordered by blade number; channels that do not form a complete blade triplet are omitted.

Return type:

list[Triplet]

mbc3_transform(lin_files, *, omega_dot=0.0, retain_per_azimuth=False, rotor_speed_drift_rtol=0.01, wind_speed_drift_rtol=0.01)[source]

Apply the MBC3 transform and azimuth-average a set of linearizations.

Parameters:
  • lin_files (Sequence[LinFile]) – Linearization files for a single operating point, one per azimuth. They must share the same channel layout; they are sorted by azimuth here.

  • omega_dot (float, optional) – Rotor angular acceleration in rad/s^2, assumed zero (appropriate for a steady-state periodic operating point).

  • retain_per_azimuth (bool, optional) – Keep the transformed A/B/C/D matrices at each azimuth for Floquet, azimuth-spread, and periodic input/output analyses. Off by default to save memory.

  • rotor_speed_drift_rtol (float, optional) – Relative rotor-speed spread allowed across the azimuth sweep before the files are rejected as not one operating point. Defaults to a tight 1%; raise it to accept a looser definition of “the same operating point”.

  • wind_speed_drift_rtol (float, optional) – Relative wind-speed spread allowed across the azimuth sweep. Defaults to 1% so wind-dependent Jacobian changes cannot be interpreted as rotor-periodic content. Both drift tolerances must be finite and nonnegative.

Warns:
  • For a *rotating* operating point with blade triplets, a single/too-few or

  • non-uniform azimuth sweep is logged as a warning (the MBC azimuth average only)

  • removes the rotor’s periodic content when it spans a full revolution

  • near-uniformly, so a sparse sweep yields a misleading averaged model. A rotating

  • state that forms no blade triplet (e.g. of a one- or two-bladed rotor) is logged

  • as a warning too, since it stays in the rotating frame.

Returns:

MBCResult – The azimuth-averaged non-rotating matrices and associated metadata.

Raises:

ValueError – If lin_files is empty, the files have inconsistent state counts, a file is missing the A matrix, two files share an azimuth, a drift tolerance is invalid, rotor/wind speed drifts across the sweep (the files are not a single operating point), or a rotating state names a blade numbered above three (the transform is the three-blade one).

Return type:

MBCResult

Keep or remove linearization states by description pattern or DOF class.

select_states() returns a copy of a linearized model with a subset of its continuous states: the matching rows and columns of A, the rows of B and the columns of C follow the selection, and D is unchanged. It works on a raw LinFile (before the MBC3 transform) and on an MBCResult (after it).

A second-order degree of freedom has two states, its displacement and its velocity (First time derivative of ...). The two are always selected together: a pattern or class that matches either one keeps, or removes, both, so the [q2, q2dot, q1] layout the MBC3 transform and the eigensolver rely on stays consistent.

select_states(model: LinFile, *, keep: str | Sequence[str] | None = None, drop: str | Sequence[str] | None = None, keep_categories: Iterable[DofCategory | str] | None = None, drop_categories: Iterable[DofCategory | str] | None = None) → LinFile[source]
select_states(model: MBCResult, *, keep: str | Sequence[str] | None = None, drop: str | Sequence[str] | None = None, keep_categories: Iterable[DofCategory | str] | None = None, drop_categories: Iterable[DofCategory | str] | None = None) → MBCResult

Return a copy of a linearized model with a subset of its states.

Parameters:
  • model (LinFile or MBCResult) – A parsed .lin file (before the MBC3 transform) or the result of mbc3_transform() (after it).

  • keep (str or Sequence[str] or None, optional) – Regular expressions (case-insensitive, matched anywhere in the state description). When keep or keep_categories is given, only the states that match one of them are retained.

  • drop (str or Sequence[str] or None, optional) – Regular expressions of states to remove. Removal is applied after keep.

  • keep_categories (Iterable[DofCategory or str] or None, optional) – Degree-of-freedom classes (DofCategory members or their string values, as returned by classify_dof()) to retain or remove.

  • drop_categories (Iterable[DofCategory or str] or None, optional) – Degree-of-freedom classes (DofCategory members or their string values, as returned by classify_dof()) to retain or remove.

Returns:

LinFile or MBCResult – A new object of the same type. A keeps the selected rows and columns, B the selected rows and C the selected columns; D, the inputs and the outputs are unchanged. For an MBCResult the per-azimuth matrices, ndof2, ndof1 and the MBC coordinate labels follow the selection. For a LinFile the module Jacobians, whose dimensions involve the states, are not carried over.

Raises:
  • ValueError – If no selector is given, a pattern or category matches no state, a pattern is not a valid regular expression, a category is unknown, or the selection removes every state.

  • TypeError – If model is neither a LinFile nor an MBCResult, or a pattern is not a string.

Return type:

LinFile | MBCResult

Notes

The displacement and velocity of a second-order degree of freedom are selected together: if either description matches, both states are kept (or removed). Use the function before the MBC3 transform to remove a module’s states from every azimuth, or after it to reduce the averaged model; in both cases the result can be passed on unchanged (for example to mbc3_transform() or modes_from_mbc()).

Examples

>>> reduced = select_states(lin, drop=r"\bHD\b")
>>> tower = select_states(
...     result, keep_categories=["tower_fore_aft_1", "tower_side_side_1"]
... )

Guarded compatibility corrections for known OpenFAST linearization defects.

The corrections in this module are deliberately signature based. They do not infer physics from arbitrary non-zero columns: a correction is applied only when the complete matrix/name pattern produced by the corresponding OpenFAST implementation is present.

class HydroDynWaveDofSignCorrection(wave_input_index, states_per_dof, dof_signs, corrected_rows)[source]

Bases: object

Audit record for an explicit .ssexctn DOF convention correction.

Parameters:
wave_input_index: int
states_per_dof: tuple[int, int, int, int, int, int]
dof_signs: tuple[int, int, int, int, int, int]
corrected_rows: tuple[int, ...]
class HydroDynWaveInputRemap(source_wave_index, source_carrier_index, wave_b_l2_before, carrier_b_l2_before, carrier_nonzero_rows, state_space_signature)[source]

Bases: object

Audit record for the OpenFAST HydroDyn state-space wave-input correction.

source_wave_index is the zero column carrying the intended wave label; source_carrier_index is the column into which OpenFAST placed the state-space excitation B vector. The corrected model swaps only these two descriptions, leaving every numeric matrix and operating-point value unchanged.

Parameters:
  • source_wave_index (int)

  • source_carrier_index (int)

  • wave_b_l2_before (float)

  • carrier_b_l2_before (float)

  • carrier_nonzero_rows (tuple[int, ...])

  • state_space_signature (str)

source_wave_index: int
source_carrier_index: int
wave_b_l2_before: float
carrier_b_l2_before: float
carrier_nonzero_rows: tuple[int, ...]
state_space_signature: str
class StateSpaceExcitationMetadata(format_header, wave_heading_deg, time_offset_s, state_count, states_per_dof)[source]

Bases: object

Header metadata required to drive a HydroDyn .ssexctn realization.

HydroDyn evaluates the incident wave at t + time_offset_s. The offset is part of the fitted realization and is therefore a required runtime input convention, not a tunable phase correction.

Parameters:
format_header: str
wave_heading_deg: float
time_offset_s: float
state_count: int
states_per_dof: tuple[int, int, int, int, int, int]
apply_hydrodyn_wave_dof_signs(result, *, states_per_dof, dof_signs, nonzero_atol=0.0)[source]

Apply an explicit wave-excitation DOF sign convention to retained B data.

This operation is intentionally not automatic. Callers must derive dof_signs from an independent hydrodynamic convention audit (for example the phase of the WAMIT .3 coefficients versus the preview-corrected .ssexctn transfer). The function enforces that the averaged and every retained azimuth-resolved wave column have the same contiguous, HydroDyn-only state block matching the six-DOF state counts in the fitted file.

Parameters:
Return type:

tuple[MBCResult, HydroDynWaveDofSignCorrection]

read_state_space_excitation_metadata(path)[source]

Read and validate the five-line header of a HydroDyn .ssexctn file.

Parameters:

path (str | Path)

Return type:

StateSpaceExcitationMetadata

remap_hydrodyn_state_space_wave_input(result, *, zero_atol=0.0, nonzero_atol=0.0)[source]

Return a copy with OpenFAST’s HydroDyn wave-input label defect corrected.

OpenFAST’s HydroDyn state-space excitation Jacobian places SS_Exctn%B in m%Jac%Nu (the final HydroDyn extended-input column), while the module registers wave elevation before current speed, shear, and propagation direction. The result is a non-zero wave-excitation column labelled propagation direction and a zero column labelled wave elevation.

This function fails closed unless all four HydroDyn extended inputs occur exactly once, the wave/current/shear columns are zero, the direction-labelled column is non-zero only in HydroDyn states, both affected direct-feedthrough columns are zero, and both operating-point inputs are zero. On success it swaps the two descriptions and their MBC-coordinate labels; no matrix coefficient is changed.

Parameters:
  • result (MBCResult) – Azimuth-averaged OpenFAST model.

  • zero_atol (float, optional) – Absolute tolerance used for columns that must be zero. The default is exact.

  • nonzero_atol (float, optional) – Minimum absolute coefficient required in the carrier column.

Returns:

corrected, audit (tuple[MBCResult, HydroDynWaveInputRemap]) – Corrected copy and an immutable provenance record.

Raises:

ValueError – If the complete known-defect signature is not present.

Return type:

tuple[MBCResult, HydroDynWaveInputRemap]

WAMIT diagonal-QTF parsing and Newman’s slow-drift reconstruction.

The implementation follows OpenFAST HydroDyn’s NewmanApp_InitCalc formulation: the positive and negative parts of each real diagonal difference-frequency QTF are square-root filtered separately, transformed into analytic time histories, squared, and subtracted. It is intentionally limited to a single wave heading so unsupported directional assumptions fail closed.

class DiagonalDifferenceQTF(angular_frequency_rad_s, values_si, wave_heading_deg)[source]

Bases: object

Dimensional diagonal difference-frequency QTF for one wave heading.

Parameters:
  • angular_frequency_rad_s (npt.NDArray[np.float64]) – Strictly increasing angular-frequency grid, shape (n_frequency,).

  • values_si (npt.NDArray[np.float64]) – Real diagonal QTF values, shape (n_frequency, 6). Translational columns have units N/m² and rotational columns N-m/m².

  • wave_heading_deg (float) – Common incident-wave heading of both QTF wave components.

angular_frequency_rad_s: npt.NDArray[np.float64]
values_si: npt.NDArray[np.float64]
wave_heading_deg: float
newman_slow_drift_load(wave_elevation_m, *, sample_interval_s, qtf, wave_low_cutoff_rad_s=0.0, wave_high_cutoff_rad_s=inf)[source]

Evaluate Newman’s mean- and slow-drift six-axis load on a periodic record.

Parameters:
  • wave_elevation_m (array-like) – Equally sampled, periodic incident-wave elevation. The record should span the sea-state realization’s period; using a cropped non-periodic record causes spectral-edge error and must be disclosed by the caller.

  • sample_interval_s (float) – Wave-elevation sample interval in seconds.

  • qtf (DiagonalDifferenceQTF) – Dimensional real diagonal QTF.

  • wave_low_cutoff_rad_s (float, optional) – Incident-wave frequency cutoffs. These match SeaState’s WvLowCOff and WvHiCOff rather than the resulting difference-frequency cutoffs.

  • wave_high_cutoff_rad_s (float, optional) – Incident-wave frequency cutoffs. These match SeaState’s WvLowCOff and WvHiCOff rather than the resulting difference-frequency cutoffs.

Returns:

npt.NDArray[np.float64] – Six-axis force/moment history, shape (n_time, 6).

Return type:

npt.NDArray[np.float64]

read_wamit_diagonal_difference_qtf(path, *, water_density_kg_m3, gravity_m_s2, characteristic_length_m, wave_heading_deg=0.0, tolerance=1e-08, imaginary_tolerance=1e-05)[source]

Read and dimensionalize one heading’s diagonal from a WAMIT .12d file.

WAMIT stores period pairs, direction pairs, a one-based load component, and the complex QTF. Newman’s approximation uses only rows where both periods and both headings are equal, and discards the negligible imaginary diagonal component. HydroDyn dimensionalizes forces by rho*g*L and moments by rho*g*L**2; this function applies the same convention.

tolerance controls equality checks on periods/headings. The separate imaginary_tolerance admits only the small diagonal roundoff that HydroDyn explicitly discards before applying Newman’s approximation.

Raises:

ValueError – If physical scales are invalid, the requested diagonal is incomplete, or its nominally real values have a material imaginary component.

Parameters:
Return type:

DiagonalDifferenceQTF

Configuration

Configuration — DOF/output index mapping and system type definitions.

Classification of OpenFAST degree-of-freedom descriptions.

OpenFAST embeds an internal degree-of-freedom (DOF) token in each ElastoDyn state description (e.g. DOF_TFA1 for the first tower fore-aft mode or DOF_BF(1,1) for the first flapwise mode of blade 1). These tokens are stable across versions and are used here to map a state description onto a physical DOF category, the originating module, the blade index for rotating DOFs, and whether the state is a velocity. This classification underpins physics-aware mode labeling.

BeamDyn states carry no such token: they are finite-element nodal displacements and rotations, e.g. BD_1 finite element node 2 (number of elements = 1; element order = 5) translational displacement in X, m and its velocity twin (BD_1 First time derivative of finite element node 2 ..., also spelled with the derivative marker before the BD_<n> prefix). BD_<n> names the blade, and the node index runs from the root (node 1, a constraint that is never a state) to the tip.

In the linearization the nodal states are perturbations expressed in the BeamDyn blade reference frame frozen at the operating point: the blade-root orientation that ElastoDyn passes to BeamDyn, i.e. ElastoDyn’s pitched blade coordinate system. Z runs along the pitch axis toward the tip, X is normal to the rotor plane at zero pitch (the flapwise direction) and Y lies in the rotor plane at zero pitch (the edgewise direction). Because the frame follows the blade pitch, X/Y stay flapwise/edgewise at any pitch angle; only the local structural twist (largest at the root) mixes them, so the split is the pitch-axis flap/edge split ElastoDyn uses for its own tip deflections. A translation along X and a rotation about Y are therefore DofCategory.BLADE_FLAP, a translation along Y and a rotation about X DofCategory.BLADE_EDGE, a rotation about Z DofCategory.BLADE_TORSION, and a translation along Z DofCategory.BLADE_AXIAL.

The root boundary condition of the linearized beam is the root-motion input measured from the same frozen frame, so a nodal state is the total nodal perturbation: it includes the rigid motion the blade root carries from the rest of the structure (a rotor-azimuth perturbation appears as a rigid rotation of every node), and the nodal velocity states are total velocities as well. These categories therefore say which blade direction a state measures, not which bending mode it belongs to: the mode order (1st/2nd/3rd) is resolved from the mode shape along the span by vane.modal.labeler.label_solution(), which also separates near-rigid blade motion from elastic bending.

class DofCategory(*values)[source]

Bases: Enum

Physical category of a degree of freedom (string-valued, 3.10 compatible).

PLATFORM_SURGE = 'platform_surge'
PLATFORM_SWAY = 'platform_sway'
PLATFORM_HEAVE = 'platform_heave'
PLATFORM_ROLL = 'platform_roll'
PLATFORM_PITCH = 'platform_pitch'
PLATFORM_YAW = 'platform_yaw'
TOWER_FORE_AFT_1 = 'tower_fore_aft_1'
TOWER_SIDE_SIDE_1 = 'tower_side_side_1'
TOWER_FORE_AFT_2 = 'tower_fore_aft_2'
TOWER_SIDE_SIDE_2 = 'tower_side_side_2'
NACELLE_YAW = 'nacelle_yaw'
GENERATOR_AZIMUTH = 'generator_azimuth'
DRIVETRAIN_TORSION = 'drivetrain_torsion'
ROTOR_FURL = 'rotor_furl'
TAIL_FURL = 'tail_furl'
TEETER = 'teeter'
BLADE_FLAP_1 = 'blade_flap_1'
BLADE_FLAP_2 = 'blade_flap_2'
BLADE_EDGE_1 = 'blade_edge_1'
BLADE_PITCH = 'blade_pitch'
UNKNOWN = 'unknown'
BLADE_FLAP = 'blade_flap'
BLADE_EDGE = 'blade_edge'
BLADE_TORSION = 'blade_torsion'
BLADE_AXIAL = 'blade_axial'
class DofInfo(category, module, blade, is_velocity, node=None, component=None)[source]

Bases: object

Classification of a single state description.

Parameters:
  • category (DofCategory) – The physical DOF category (UNKNOWN if not recognized).

  • module (str) – The originating module prefix (e.g. "ED" or "BD_1"), or an empty string if absent.

  • blade (int or None) – The 1-based blade index for rotating blade DOFs, otherwise None.

  • is_velocity (bool) – Whether the state is the time derivative (velocity) of a DOF.

  • node (int or None, optional) – For a BeamDyn nodal state, the 1-based finite-element node index (node 1 is the blade root, so state nodes start at 2); otherwise None.

  • component (str or None, optional) – For a BeamDyn nodal state, the measured component in the blade reference frame: "TX", "TY", "TZ" for a translation along X, Y, Z and "RX", "RY", "RZ" for a rotation about it; otherwise None.

category: DofCategory
module: str
blade: int | None
is_velocity: bool
node: int | None = None
component: str | None = None
classify_dof(description)[source]

Classify a state description into a DofInfo.

Parameters:

description (str) – A state description from a linearization file (e.g. "ED 1st tower fore-aft ... (internal DOF index = DOF_TFA1), m").

Returns:

DofInfo – The classification; category is DofCategory.UNKNOWN when the description carries neither a known ElastoDyn DOF_ token nor a recognized BeamDyn nodal quantity. For a BeamDyn state, module and blade come from its BD_<n> prefix (blade is None for a bare BD prefix) and node/component locate the state on the blade.

Return type:

DofInfo

Classification of OpenFAST linearization output channels.

The output equation y = C x + D u of a linearization maps the system state and inputs onto OpenFAST output channels. Many of those channels are loads — mooring fairlead and anchor tensions, tower-base and tower-top (yaw-bearing) moments and forces, blade-root moments, shaft torque — which are the quantities of interest for structural load and health monitoring. This module classifies an output channel description (e.g. "MAP T[1], [N]" or "ED YawBrMzp, (kN-m)") into a physical OutputCategory, recording the originating module, the load type, the component index (mooring line or blade number), the measured quantity, the axis, and the unit. It is the output-side counterpart of vane.config.classify_dof().

The catalog covers the common MAP++/MoorDyn mooring, ElastoDyn, and BeamDyn load channels; unrecognized channels are returned as OutputCategory.UNKNOWN and are simply not treated as loads.

LOAD_CATEGORIES: frozenset[OutputCategory] = frozenset({OutputCategory.BLADE_ROOT_FORCE, OutputCategory.BLADE_ROOT_MOMENT, OutputCategory.MOORING_ANCHOR_TENSION, OutputCategory.MOORING_FAIRLEAD_TENSION, OutputCategory.MOORING_LINE_TENSION, OutputCategory.SHAFT_TORQUE, OutputCategory.TOWER_BASE_FORCE, OutputCategory.TOWER_BASE_MOMENT, OutputCategory.TOWER_TOP_FORCE, OutputCategory.TOWER_TOP_MOMENT})

Categories that represent a structural load (a force, moment, or tension).

class OutputCategory(*values)[source]

Bases: Enum

Physical category of a linearization output channel (string-valued).

MOORING_FAIRLEAD_TENSION = 'mooring_fairlead_tension'
MOORING_ANCHOR_TENSION = 'mooring_anchor_tension'
MOORING_LINE_TENSION = 'mooring_line_tension'
TOWER_BASE_MOMENT = 'tower_base_moment'
TOWER_BASE_FORCE = 'tower_base_force'
TOWER_TOP_MOMENT = 'tower_top_moment'
TOWER_TOP_FORCE = 'tower_top_force'
BLADE_ROOT_MOMENT = 'blade_root_moment'
BLADE_ROOT_FORCE = 'blade_root_force'
SHAFT_TORQUE = 'shaft_torque'
PLATFORM_MOTION = 'platform_motion'
DEFLECTION = 'deflection'
WAVE_ELEVATION = 'wave_elevation'
UNKNOWN = 'unknown'
class OutputInfo(category, module, is_load, component, quantity, axis, unit)[source]

Bases: object

Classification of a single output channel description.

Parameters:
  • category (OutputCategory) – The physical category (UNKNOWN if not recognized).

  • module (str) – The originating module prefix (e.g. "ED", "MAP", "MD"), or an empty string if absent.

  • is_load (bool) – Whether the channel is a structural load (a member of LOAD_CATEGORIES).

  • component (int or None) – The 1-based mooring-line or blade index for component-indexed channels, otherwise None.

  • quantity (str) – The measured quantity ("tension", "force", "moment", "displacement", "rotation", "elevation", or "").

  • axis (str or None) – The axis label ("x", "y", "z") for directional forces and moments, otherwise None.

  • unit (str) – The unit token parsed from the trailing (...) or [...] of the description (e.g. "kN-m", "N", "m"), or an empty string.

category: OutputCategory
module: str
is_load: bool
component: int | None
quantity: str
axis: str | None
unit: str
classify_output(description)[source]

Classify a linearization output channel description.

Parameters:

description (str) – An output channel description from a .lin file (e.g. "MAP T[1], [N]" or "ED YawBrMzp, (kN-m)").

Returns:

OutputInfo – The classification; category is OutputCategory.UNKNOWN when the channel name matches no known pattern.

Return type:

OutputInfo

Reference geometry of a wind-turbine system for mode-shape visualisation.

A linearization file carries modal degrees of freedom — generalized coordinates such as platform surge, tower fore-aft bending, or a blade flap mode — not nodal positions. To draw a mode on the physical structure, a reference (undeformed) geometry is required: where the tower, nacelle, rotor, floating platform, and mooring lines sit in space. This module provides that geometry as a small, parametric, fully auditable model.

The geometry is a schematic assembled from a turbine’s principal dimensions (tower height, blade length, hub overhang, platform draft and radius, mooring fairlead and anchor positions). It is deliberately not a high-fidelity hull mesh: every node position follows from the documented dimensions, so the drawing can be audited line by line. The deformation that animates it (see vane.viz.modeshape_fowt) is driven by the actual complex mode shape, so the relative motion across the structure is physically meaningful even though the hull outline is stylised.

Coordinate convention

A right-handed inertial frame with origin at the intersection of the undeflected tower centreline and the still-water level (SWL), matching OpenFAST’s platform reference point: +x downwind, +y to the left looking downwind, +z up. Onshore systems place the origin at the tower base instead; the convention is otherwise identical.

class FowtGeometry(system_type, members, reference_point, tower_top, tower_length, n_blades, characteristic_length, metadata=<factory>)[source]

Bases: object

Reference geometry of a complete wind-turbine system.

Parameters:
  • system_type (SystemType) – The support-structure class this geometry represents.

  • members (list[Member]) – All polyline components (tower, blades, nacelle, hub, platform members, mooring lines).

  • reference_point (npt.NDArray[np.float64]) – The platform reference point about which the rigid-body platform degrees of freedom (surge/sway/heave/roll/pitch/yaw) act, shape (3,).

  • tower_top (npt.NDArray[np.float64]) – Position of the tower top (yaw bearing), shape (3,); the origin of the tower-top rotation that carries the rotor-nacelle assembly.

  • tower_length (float) – Undeformed tower length, used to convert a bending slope into a tower-top rotation.

  • n_blades (int) – Number of rotor blades.

  • characteristic_length (float) – A representative length (typically the hub height) used to scale the animated deflection to a visible, dimensionless fraction.

  • metadata (dict[str, float])

Notes

The geometry is immutable; presets are produced by the builder functions in this module. members is stored as a tuple-backed list to keep the dataclass hashable-friendly while remaining ergonomic to iterate.

system_type: SystemType
members: list[Member]
reference_point: NDArray[float64]
tower_top: NDArray[float64]
tower_length: float
n_blades: int
characteristic_length: float
metadata: dict[str, float]
members_of_kind(kind)[source]

Return the members whose kind equals kind.

Parameters:

kind (str) – The component class to select.

Returns:

list[Member] – The matching members, in their stored order.

Return type:

list[Member]

class Member(name, kind, nodes, span_fraction=None, blade_index=None, flap_dir=None, edge_dir=None, rides_tower_top=False)[source]

Bases: object

One polyline component of the reference structure.

A member is an ordered list of node positions forming a polyline (the tower centreline, one blade, a pontoon, a mooring line, …). The optional fields tell the deformation engine how the member responds to the modal degrees of freedom; members that only ride the platform rigid-body motion (a pontoon, a column) need none of them.

Parameters:
  • name (str) – Human-readable label, shown in the figure legend.

  • kind (str) – Component class, one of "tower", "blade", "platform", "mooring", "nacelle", or "hub". Governs the deformation rule.

  • nodes (npt.NDArray[np.float64]) – Undeformed node positions, shape (n_nodes, 3).

  • span_fraction (npt.NDArray[np.float64] or None) – For "tower" and "blade" members, the normalized arc length \(\xi \in [0, 1]\) of each node from the clamped end (tower base / blade root), shape (n_nodes,). The bending shape functions are evaluated at these values.

  • blade_index (int or None) – For "blade" members, the 1-based blade number; its reference azimuth is 2*pi*(blade_index - 1)/n_blades, measured as a right-hand rotation about the downwind shaft axis from vertical (the OpenFAST rotor azimuth used by the MBC3 transform), so blade 2 of a three-bladed rotor points to -y. None otherwise.

  • flap_dir (npt.NDArray[np.float64] or None) – For "blade" members, the unit flapwise (out-of-rotor-plane) and edgewise (in-rotor-plane) directions in the inertial frame, shape (3,). Blade bending displaces nodes along these. They follow the ElastoDyn blade-frame signs: positive flap points nominally downwind and positive edge points against the direction of rotation (toward the trailing edge).

  • edge_dir (npt.NDArray[np.float64] or None) – For "blade" members, the unit flapwise (out-of-rotor-plane) and edgewise (in-rotor-plane) directions in the inertial frame, shape (3,). Blade bending displaces nodes along these. They follow the ElastoDyn blade-frame signs: positive flap points nominally downwind and positive edge points against the direction of rotation (toward the trailing edge).

  • rides_tower_top (bool) – Whether the member is rigidly attached to the tower top (nacelle, hub, and blades) and therefore inherits the tower-top translation and bending rotation in addition to the platform rigid-body motion.

name: str
kind: str
nodes: NDArray[float64]
span_fraction: NDArray[float64] | None = None
blade_index: int | None = None
flap_dir: NDArray[float64] | None = None
edge_dir: NDArray[float64] | None = None
rides_tower_top: bool = False
class SystemType(*values)[source]

Bases: Enum

Support structure of a wind-turbine system (string-valued, 3.10 safe).

FIXED = 'fixed'
SPAR = 'spar'
SEMI = 'semi'
TLP = 'tlp'
LAND = 'land'
MONOPILE = 'monopile'
JACKET = 'jacket'
default_geometry(system_type)[source]

Return a default reference geometry for a system type.

Parameters:

system_type (SystemType) – The support-structure class. TLP uses the tension-leg schematic of tlp_geometry().

Returns:

FowtGeometry – A representative geometry built with the preset’s default dimensions.

Return type:

FowtGeometry

jacket_geometry(*, hub_height=110.0, tower_base_z=20.0, blade_length=60.0, base_radius=15.0, top_radius=7.0, water_depth=50.0, embedment=0.0, n_legs=4, n_bays=4, n_blades=3, shaft_tilt_deg=-5.0, precone_deg=-2.5, overhang=7.0)[source]

Build a bottom-fixed lattice-jacket reference geometry.

n_legs battered legs run from the mudline up to the transition piece, tied by n_bays levels of X-braces; a transition-piece deck carries the tower. The legs taper from base_radius at the seabed to top_radius at the transition piece.

Returns:

FowtGeometry – The assembled jacket geometry.

Parameters:
Return type:

FowtGeometry

monopile_geometry(*, hub_height=90.0, tower_base_z=20.0, blade_length=60.0, monopile_radius=3.0, water_depth=20.0, embedment=25.0, mudline_z=0.0, n_blades=3, shaft_tilt_deg=-5.0, precone_deg=-2.5, overhang=7.0)[source]

Build a bottom-fixed monopile reference geometry (no mooring lines).

The monopile runs from below the mudline up to the tower base above the waterline. There are no mooring lines; the platform reference point sits at the mudline, so platform degrees of freedom (if any) act there.

Parameters:
  • mudline_z (float, optional) – Seabed elevation. The default 0.0 keeps the plain bottom-fixed convention (seabed at the reference plane, no water column). Offshore presets pass -water_depth so the still-water plane at z = 0 sits above the seabed and the pile crosses the waterline.

  • hub_height (float)

  • tower_base_z (float)

  • blade_length (float)

  • monopile_radius (float)

  • water_depth (float)

  • embedment (float)

  • n_blades (int)

  • shaft_tilt_deg (float)

  • precone_deg (float)

  • overhang (float)

Returns:

FowtGeometry – The assembled monopile geometry.

Return type:

FowtGeometry

onshore_geometry(*, hub_height=90.0, blade_length=60.0, foundation_radius=8.0, n_blades=3, shaft_tilt_deg=-5.0, precone_deg=-2.5, overhang=7.0)[source]

Build a land-based (onshore) reference geometry: tower + rotor on the ground.

The tower rises from the ground plane (z = 0) to the hub; there is no substructure, water, or mooring. A foundation ring marks the base.

Returns:

FowtGeometry – The assembled onshore geometry.

Parameters:
Return type:

FowtGeometry

semisub_geometry(*, hub_height=90.0, tower_base_z=10.0, blade_length=60.0, column_radius=6.0, offset_radius=28.0, draft=20.0, freeboard=10.0, fairlead_radius=28.0, fairlead_depth=14.0, anchor_radius=420.0, water_depth=200.0, n_blades=3, shaft_tilt_deg=-5.0, precone_deg=-2.5, overhang=7.0)[source]

Build a three-column semi-submersible FOWT reference geometry.

A central column carries the tower; three outer columns on an offset radius are joined to it by pontoons and braces, and three catenary mooring lines run from the outer-column fairleads to seabed anchors. Defaults are representative of a 5-MW-class semi-submersible and may be overridden per turbine.

Parameters:
  • hub_height (float, optional) – Hub height above SWL.

  • tower_base_z (float, optional) – Tower-base height above SWL (top of the central column / freeboard).

  • blade_length (float, optional) – Blade length from root to tip.

  • column_radius (float, optional) – Radius of each buoyancy column.

  • offset_radius (float, optional) – Radial distance of the outer columns from the centreline.

  • draft (float, optional) – Column draft below SWL.

  • freeboard (float, optional) – Column height above SWL.

  • fairlead_radius (float, optional) – Radius of the mooring fairleads.

  • fairlead_depth (float, optional) – Depth of the fairleads below SWL.

  • anchor_radius (float, optional) – Radius of the seabed anchors.

  • water_depth (float, optional) – Water depth (anchors sit on the seabed at -water_depth).

  • n_blades (int, optional) – Number of rotor blades.

  • shaft_tilt_deg (float, optional) – Shaft tilt angle in degrees, with the OpenFAST ShftTilt sign (positive raises the downwind shaft end). The default -5 tilts the upwind rotor nose-up, as for the NREL 5-MW turbine.

  • precone_deg (float, optional) – Blade precone angle in degrees, with the OpenFAST PreCone sign (positive cones the blades downwind). The default -2.5 cones the blades upwind, away from the tower.

  • overhang (float, optional) – Upwind shaft overhang of the hub from the tower top.

Returns:

FowtGeometry – The assembled semi-submersible geometry.

Return type:

FowtGeometry

spar_geometry(*, hub_height=90.0, tower_base_z=10.0, blade_length=60.0, spar_radius=4.7, draft=120.0, fairlead_radius=5.2, fairlead_depth=70.0, anchor_radius=850.0, water_depth=320.0, n_blades=3, shaft_tilt_deg=-5.0, precone_deg=-2.5, overhang=7.0)[source]

Build a single-column spar-buoy FOWT reference geometry (OC3-Hywind class).

A deep slender cylinder provides buoyancy and ballast stability; three catenary mooring lines spread from fairleads near the waterline. Parameters mirror semisub_geometry().

Returns:

FowtGeometry – The assembled spar geometry.

Parameters:
Return type:

FowtGeometry

tlp_geometry(*, hub_height=90.0, tower_base_z=10.0, blade_length=60.0, column_radius=6.0, pontoon_radius=20.0, draft=20.0, freeboard=10.0, water_depth=200.0, n_tendons=3, n_blades=3, shaft_tilt_deg=-5.0, precone_deg=-2.5, overhang=7.0)[source]

Build a tension-leg-platform (TLP) reference geometry.

A central buoyancy column carries the tower; spoke pontoons reach out to the tendon-attachment corners, and vertical taut tendons run straight down from each corner to a seabed anchor directly below it — the defining TLP feature, unlike a semi-submersible’s spread catenary mooring.

Returns:

FowtGeometry – The assembled TLP geometry.

Parameters:
Return type:

FowtGeometry

Modal analysis

Modal analysis — eigensolver, MAC, participation factors, mode identification.

Eigenvalue analysis of the azimuth-averaged state matrix.

Given the non-rotating (MBC-averaged) state matrix \(A\) in the canonical [q2, q2dot, q1] layout, an eigenvalue analysis yields the full-system modes: their natural frequencies, damping ratios, and (complex) mode shapes.

Each under-damped mode appears as a complex-conjugate eigenvalue pair; only the representative with positive imaginary part is retained. For an eigenvalue \(\lambda\), the modal quantities are

\[\omega_n = |\lambda|, \qquad \zeta = \frac{-\operatorname{Re}\lambda}{|\lambda|}, \qquad \omega_d = \operatorname{Im}\lambda,\]

so the natural and damped frequencies in hertz are \(\omega_n / 2\pi\) and \(\omega_d / 2\pi\). The velocity (q2dot) rows are dropped from the returned mode shapes, leaving the second-order displacements and first-order states; the full eigenvectors are also retained.

Notes

OpenFAST exposes no full-system mass or stiffness matrix, so modal frequencies are obtained directly from \(A\) and vary with operating point.

Numerically split rigid-body pairs. A degree of freedom without restoring stiffness – the free rotor-azimuth (generator) DOF of a variable-speed turbine – contributes a zero eigenvalue (the azimuth integrator) next to a small real one (the aerodynamic damping of the rotor speed). Where the damping is small, the finite-difference noise in the azimuth column of \(A\) is enough to merge the two into a complex pair of negligible frequency (periods of tens of minutes) instead of two real eigenvalues. Such a pair is not an oscillation, and reporting it as one makes a spurious mode appear at isolated operating points. A complex pair is therefore classified as a split rigid-body pair – dropped from the oscillatory modes and counted in n_rigid_body_modes – when both

  1. its natural frequency is below rigid_body_frequency_hz (default RIGID_BODY_FREQUENCY_HZ, a period of 333 s: well below every restoring-force mode of a wind turbine, including the 100-200 s surge and sway of moored floating platforms), and

  2. its imaginary part is not resolved by the data: by first-order perturbation theory a relative error \(\delta\) in \(A\) moves the eigenvalue by up to \(\kappa\,\delta\,\rho(A)\), so a pair with

    \[|\operatorname{Im}\lambda| \le \delta\,\kappa(\lambda)\,\rho(A)\]

    is indistinguishable from two real eigenvalues. \(\kappa\) is evaluated on the diagonally balanced matrix (the problem the eigenvalue solver actually solves, and independent of the state units), \(\rho\) is the spectral radius and \(\delta\) is split_pair_rtol (default SPLIT_PAIR_RTOL).

Both conditions are needed. The first alone would drop a well-resolved slow oscillation of an unusually soft model. The second alone is not enough because the spectral radius of a model with stiff first-order states (MoorDyn line nodes) is large, so well-resolved surge and sway modes of such a model can fall below the bound; the frequency floor keeps them.

RIGID_BODY_FREQUENCY_HZ = 0.003

Natural frequency (Hz) below which a numerically split pair may be rigid-body.

A period of 333 s. Moored floating platforms have their slowest restoring-force modes (surge, sway) at periods of roughly 100-200 s (0.005-0.01 Hz), so no physical oscillation of a wind turbine is this slow.

SPLIT_PAIR_RTOL = 0.0001

Relative accuracy \(\delta\) of the state matrix assumed for split pairs.

OpenFAST computes its Jacobians by finite differences with perturbations of order \(10^{-3}\), and the MBC transform averages them over azimuth, so the entries of \(A\) are not reliable to better than about this relative level. On the reference-turbine sweeps the split rigid-body pairs sit 10-200 times inside the resulting bound, and the platform modes of floating models without MoorDyn 2-50 times outside it.

class ModalSolution(eigenvalues, natural_frequencies_hz, damping_ratios, damped_frequencies_hz, mode_shapes, full_eigenvectors, n_rigid_body_modes, dof_descriptions, condition_numbers=<factory>, is_degenerate=<factory>, n_unstable=0, n_overdamped=0, dof_mbc_coordinates=<factory>, full_left_eigenvectors=<factory>, state_descriptions=<factory>, n_split_rigid_body_pairs=0, rotor_speed_rpm=nan)[source]

Bases: object

Modal properties extracted from a state matrix.

All per-mode arrays share the same ordering (ascending natural frequency when the solver sorts). Only under-damped modes (positive imaginary eigenvalue) are represented.

Parameters:
  • eigenvalues (npt.NDArray[np.complex128]) – Retained eigenvalues \(\lambda\), shape (n_modes,).

  • natural_frequencies_hz (npt.NDArray[np.float64]) – Natural frequencies \(|\lambda| / 2\pi\), shape (n_modes,).

  • damping_ratios (npt.NDArray[np.float64]) – Damping ratios \(-\operatorname{Re}\lambda / |\lambda|\).

  • damped_frequencies_hz (npt.NDArray[np.float64]) – Damped frequencies \(\operatorname{Im}\lambda / 2\pi\).

  • mode_shapes (npt.NDArray[np.complex128]) – Reduced complex mode shapes (second-order displacements then first-order states), shape (ndof2 + ndof1, n_modes), phase-normalized so each mode’s dominant component is real and positive.

  • full_eigenvectors (npt.NDArray[np.complex128]) – Full eigenvectors over all states, shape (n_states, n_modes), sharing the same phase normalization.

  • full_left_eigenvectors (npt.NDArray[np.complex128]) – Full left eigenvectors over all states, shape (n_states, n_modes). Together with full_eigenvectors these support scale-invariant biorthogonal participation and first-order eigenvalue sensitivity.

  • n_rigid_body_modes (int) – Number of degrees of freedom without an under-damped representative (rigid-body or over-damped modes, including split rigid-body pairs).

  • dof_descriptions (list[str]) – Descriptions of the degrees of freedom that index the rows of mode_shapes (the second-order displacements then first-order states). Empty when descriptions were not supplied to the solver.

  • state_descriptions (list[str]) – Descriptions of every state indexing the full left and right eigenvectors. Empty when descriptions were not supplied to the solver.

  • dof_mbc_coordinates (list[str]) – Multi-blade coordinate ("collective"/"cosine"/"sine"/"") of each mode_shapes row, aligned with dof_descriptions. Empty when not supplied.

  • condition_numbers (npt.NDArray[np.float64]) – Per-mode eigenvalue condition number \(\kappa = 1 / |y^H x|\) (the reciprocal cosine of the angle between the left and right eigenvectors), shape (n_modes,). 1 is perfectly conditioned; large values flag eigenvalues that are numerically sensitive (highly non-normal system).

  • is_degenerate (npt.NDArray[np.bool]) – Per-mode flag marking modes whose eigenvalue is (near-)repeated by another retained mode, so the individual mode shape is defined only up to a rotation within the shared invariant subspace.

  • n_unstable (int) – Number of eigenvalues with positive real part (unstable). Non-zero is a red flag for the operating point. Split rigid-body pairs are not counted: the sign of their real part is not resolved by the data.

  • n_overdamped (int) – Number of non-oscillatory stable real eigenvalues (over-damped modes), which are not returned as oscillatory modes.

  • n_split_rigid_body_pairs (int) – Number of complex-conjugate pairs classified as numerically split rigid-body eigenvalues (see the module notes) and therefore left out of the oscillatory modes; each also counts in n_rigid_body_modes.

  • rotor_speed_rpm (float) – Rotor speed (rev/min) of the operating point the state matrix was linearized at, or nan when unknown (e.g. a matrix given directly to compute_modes()). modes_from_mbc() copies it from the MBC result; the labeller uses it to read a blade whirl mode’s damping in the rotating frame.

eigenvalues: npt.NDArray[np.complex128]
natural_frequencies_hz: npt.NDArray[np.float64]
damping_ratios: npt.NDArray[np.float64]
damped_frequencies_hz: npt.NDArray[np.float64]
mode_shapes: npt.NDArray[np.complex128]
full_eigenvectors: npt.NDArray[np.complex128]
n_rigid_body_modes: int
dof_descriptions: list[str]
condition_numbers: npt.NDArray[np.float64]
is_degenerate: npt.NDArray[np.bool_]
n_unstable: int = 0
n_overdamped: int = 0
dof_mbc_coordinates: list[str]
full_left_eigenvectors: npt.NDArray[np.complex128]
state_descriptions: list[str]
n_split_rigid_body_pairs: int = 0
rotor_speed_rpm: float = nan
property n_modes: int

Return the number of retained modes.

property natural_frequencies_rad_s: npt.NDArray[np.float64]

Return natural frequencies in rad/s.

property max_condition_number: float

Return the worst (largest) eigenvalue condition number, or 1 if none.

compute_modes(a, ndof2, ndof1, *, sort_by_frequency=True, descriptions=None, mbc_coordinates=None, rigid_body_frequency_hz=0.003, split_pair_rtol=0.0001)[source]

Compute modal properties from a state matrix.

Parameters:
  • a (npt.NDArray[np.float64]) – Square state matrix in [q2, q2dot, q1] order, shape (2 * ndof2 + ndof1, 2 * ndof2 + ndof1).

  • ndof2 (int) – Number of second-order degrees of freedom (the q2 block size).

  • ndof1 (int) – Number of first-order states (the q1 block size).

  • sort_by_frequency (bool, optional) – Sort the returned modes by ascending natural frequency (the default).

  • descriptions (list[str] or None, optional) – Descriptions of all 2 * ndof2 + ndof1 states, in the matrix order. When given, the subset that indexes the mode-shape rows is stored on the result as dof_descriptions. A list of only the ndof2 + ndof1 mode-shape rows (displacements then first-order states) is also accepted and stored as is; state_descriptions is then left empty.

  • mbc_coordinates (list[str] or None, optional) – Multi-blade coordinate of all states (or of the mode-shape rows), in the matrix order; the mode-shape subset is stored on the result as dof_mbc_coordinates.

  • rigid_body_frequency_hz (float, optional) – Natural frequency below which a complex pair whose imaginary part is not resolved by the data is classified as a split rigid-body pair (see the module notes). Default RIGID_BODY_FREQUENCY_HZ; 0.0 keeps every complex pair as an oscillatory mode.

  • split_pair_rtol (float, optional) – Assumed relative accuracy of a in the split-pair test. Default SPLIT_PAIR_RTOL; 0.0 keeps every complex pair.

Returns:

ModalSolution – The modal frequencies, damping ratios, and mode shapes.

Raises:

ValueError – If a is not square, if ndof2/ndof1 are not non-negative integers, if its size does not match 2 * ndof2 + ndof1, if descriptions or mbc_coordinates has neither one entry per state nor one per mode-shape row, or if rigid_body_frequency_hz or split_pair_rtol is negative or non-finite.

Return type:

ModalSolution

modes_from_mbc(result, *, rigid_body_frequency_hz=0.003, split_pair_rtol=0.0001)[source]

Compute modal properties from an MBC transform result.

Parameters:
Returns:

ModalSolution – The modal solution for the averaged state matrix.

Raises:

ValueError – If result.avg_a is None.

Return type:

ModalSolution

Modal Assurance Criterion and mode-to-mode matching.

The Modal Assurance Criterion (MAC) measures the correlation between two mode shapes; a value of 1 indicates perfect correlation and 0 indicates orthogonality [1]. For complex (state-space) modes the pole-weighted MACXP criterion [2] is also provided. match_modes performs a one-to-one Hungarian assignment over a MAC (optionally frequency-penalized) matrix between two operating points; this is a pairwise building block. Cross-operating-point tracking over a whole sweep is done separately by vane.modal.identify_modes(), which extracts globally optimal continuity paths rather than chaining pairwise matches.

References

compute_mac(phi_ref, phi_test)[source]

Compute the Modal Assurance Criterion (MAC) matrix.

Parameters:
  • phi_ref (npt.NDArray[np.complex128]) – Reference mode-shape matrix, shape (n_dof, n_modes_ref).

  • phi_test (npt.NDArray[np.complex128]) – Test mode-shape matrix, shape (n_dof, n_modes_test).

Returns:

npt.NDArray[np.float64] – MAC matrix of shape (n_modes_ref, n_modes_test) with values in [0, 1].

Raises:

ValueError – If the two mode-shape matrices have incompatible first dimensions.

Return type:

npt.NDArray[np.float64]

Notes

The MAC between modes \(i\) and \(j\) is

\[\text{MAC}_{ij} = \frac{|\phi_i^H \phi_j|^2} {(\phi_i^H \phi_i)(\phi_j^H \phi_j)}.\]
compute_macxp(phi_ref, lambda_ref, phi_test, lambda_test)[source]

Compute the pole-weighted MACXP matrix for complex modes.

MACXP augments the MAC with the transpose product and weights both by the modal poles, making it more discriminating than MAC for complex state-space modes.

Parameters:
  • phi_ref (npt.NDArray[np.complex128]) – Reference and test mode-shape matrices, shape (n_dof, n_modes).

  • phi_test (npt.NDArray[np.complex128]) – Reference and test mode-shape matrices, shape (n_dof, n_modes).

  • lambda_ref (npt.NDArray[np.complex128]) – Corresponding modal poles (eigenvalues), shape (n_modes,).

  • lambda_test (npt.NDArray[np.complex128]) – Corresponding modal poles (eigenvalues), shape (n_modes,).

Returns:

npt.NDArray[np.float64] – MACXP matrix of shape (n_modes_ref, n_modes_test); a mode correlated with itself yields 1.

Raises:

ValueError – If shapes are inconsistent between mode matrices and pole vectors.

Return type:

npt.NDArray[np.float64]

Notes

The criterion is defined for damped modes (poles with a non-zero real part); an undamped pole makes its self-correlation term infinite, so the whole row (reference pole) or column (test pole) of every undamped pole is returned as NaN rather than raising – including the cross entries against damped modes, which would otherwise collapse to a misleading 0.0.

match_modes(mac, ref_frequencies=None, test_frequencies=None, frequency_weight=0.0, *, forbidden=None)[source]

Match reference modes to test modes by maximum-weight assignment.

A Hungarian (optimal) assignment maximizes the total correlation between the two mode sets. When frequencies are supplied, the MAC of each pair is scaled by 1 - frequency_weight * |df| / span so that frequency proximity breaks ties between similar shapes (the strategy used for Campbell-diagram tracking).

Parameters:
  • mac (npt.NDArray[np.float64]) – Correlation matrix, shape (n_ref, n_test) (e.g. from compute_mac()).

  • ref_frequencies (npt.NDArray[np.float64] or None, optional) – Per-mode natural frequencies for the reference and test sets. Both must be given to apply the frequency penalty.

  • test_frequencies (npt.NDArray[np.float64] or None, optional) – Per-mode natural frequencies for the reference and test sets. Both must be given to apply the frequency penalty.

  • frequency_weight (float, optional) – Strength of the frequency penalty in [0, 1]; 0 (default) uses MAC alone.

  • forbidden (npt.NDArray[np.bool] or None, optional) – Pairs that must not be matched, shape (n_ref, n_test). This is a hard constraint: whenever an assignment avoiding every forbidden pair exists, it is chosen.

Returns:

list[tuple[int, int]] – (ref_index, test_index) pairs, one per matched mode (min(n_ref, n_test) pairs), ordered by reference index.

Raises:

ValueError – If forbidden does not match the shape of mac, or no assignment avoids every forbidden pair.

Return type:

list[tuple[int, int]]

Per-degree-of-freedom participation in each mode.

OpenFAST exposes no full-system mass matrix, so a classical mass-normalized modal participation factor is unavailable. Instead, participation is quantified from the right eigenvector: the magnitude of each state’s component in a mode measures how strongly that degree of freedom participates. Each mode is normalized so its dominant component has magnitude one, and a sign is assigned from the phase of each component relative to the dominant one (components more than 90 degrees out of phase are negated), giving the signed contribution used for physical mode identification.

class ParticipationResult(magnitude, signed_magnitude, phase_deg, dominant_state)[source]

Bases: object

Per-state participation in each mode.

Parameters:
  • magnitude (npt.NDArray[np.float64]) – Normalized participation magnitude, shape (n_dof, n_modes); each column’s maximum is 1.

  • signed_magnitude (npt.NDArray[np.float64]) – magnitude with the sign of components more than 90 degrees out of phase with the dominant component flipped negative.

  • phase_deg (npt.NDArray[np.float64]) – Phase of each component in degrees, shape (n_dof, n_modes).

  • dominant_state (npt.NDArray[np.int]) – Index of the largest-magnitude state in each mode, shape (n_modes,).

magnitude: npt.NDArray[np.float64]
signed_magnitude: npt.NDArray[np.float64]
phase_deg: npt.NDArray[np.float64]
dominant_state: npt.NDArray[np.int_]
compute_participation(mode_shapes, scale_factors=None)[source]

Compute per-state participation from complex mode shapes.

Parameters:
  • mode_shapes (npt.NDArray[np.complex128]) – Complex mode-shape matrix, shape (n_dof, n_modes).

  • scale_factors (npt.NDArray[np.float64] or None, optional) – Per-DOF multipliers applied before normalization (e.g. 1/blade_length for translational blade DOFs) so heterogeneous DOFs are comparable. When None, no scaling is applied.

Returns:

ParticipationResult – Normalized magnitudes, signed magnitudes, phases, and dominant states.

Raises:

ValueError – If scale_factors length does not match the number of DOFs.

Return type:

ParticipationResult

participation_from_modes(solution, scale_factors=None)[source]

Compute participation from a ModalSolution’s mode shapes.

Parameters:
  • solution (ModalSolution) – Modal solution whose mode_shapes are analysed.

  • scale_factors (npt.NDArray[np.float64] or None, optional) – Per-DOF multipliers, as in compute_participation().

Returns:

ParticipationResult – The participation result for the solution’s mode shapes.

Return type:

ParticipationResult

participation_scale_factors(descriptions, *, blade_length, tower_length)[source]

Derive per-DOF participation scale factors from the state descriptions.

A turbine’s states are a mix of units: blade and tower deflections are in metres while the other degrees of freedom are rotations (radians) or generalized coordinates. Comparing their raw eigenvector components over-weights whichever has the larger numeric units. Scaling each translational deflection by its characteristic length — blade flap/edge by the flexible blade length, tower fore-aft/side-side by the flexible tower length — turns it into a dimensionless tip-deflection ratio comparable to the rest. A BeamDyn blade’s nodal translations (along the blade-frame X, Y and Z axes, in metres) are scaled by the blade length too, so they become spanwise deflection ratios comparable with the nodal rotations, which are in radians. It builds the scale_factors array that compute_participation() accepts (replacing a hand-made one), by classifying each state with classify_dof().

Every other category — the platform rigid-body DOFs (surge/sway/heave and roll/pitch/yaw), blade pitch, drivetrain torsion, generator azimuth, furl/teeter, BeamDyn nodal rotations, and any unrecognized state — is left at 1.0 (no length scaling). Velocity states scale by the same factor as their position DOF, since they share its classification.

Only the raw-magnitude participation (compute_participation()) depends on these factors. The physical labels of vane.modal.label_solution() use the biorthogonal participation, which is invariant to any rescaling of the states and needs none.

Parameters:
  • descriptions (Sequence[str]) – The state descriptions, one per mode-shape row and in the same order (e.g. from the linearization file / MBC result), as classified by classify_dof().

  • blade_length (float) – Flexible blade length in metres (e.g. TurbineGeometry.blade_length from read_elastodyn_geometry(), TipRad - HubRad, which the ElastoDyn file defines for BeamDyn blades too). Must be finite and positive.

  • tower_length (float) – Flexible tower length in metres (e.g. TurbineGeometry.tower_length). Must be finite and positive.

Returns:

npt.NDArray[np.float64] – The per-DOF scale factors, shape (len(descriptions),): 1/blade_length for ElastoDyn blade flap/edge deflections and BeamDyn nodal translations, 1/tower_length for tower deflections, 1.0 otherwise. Pass it straight to compute_participation() / participation_from_modes().

Raises:

ValueError – If blade_length or tower_length is not finite and positive.

Return type:

npt.NDArray[np.float64]

Scale-invariant left–right modal participation.

Raw right-eigenvector amplitudes depend on the units chosen for the states. A metre, a radian, and an internal aerodynamic or hydrodynamic state therefore cannot be compared by magnitude alone. For a simple eigenvalue, the element-wise left–right product

\[p_{ki}=\frac{\overline{w}_{ki}v_{ki}} {\mathbf{w}_i^H\mathbf{v}_i}\]

is invariant to any nonsingular diagonal rescaling of the states. Its complex components sum to one. This module reports those factors and a non-negative share formed by normalising their magnitudes, both per state and aggregated into physical subsystems.

Individual factors are not physically unique for a repeated eigenvalue because the eigenvectors may rotate within its invariant subspace. The caller should therefore use is_degenerate as an acceptance check before interpreting an individual degenerate mode.

class BiorthogonalParticipation(complex_factors, magnitude_fractions, closure_error, valid, subsystem_names, subsystem_fractions, state_subsystems)[source]

Bases: object

Left–right participation factors for every retained oscillatory mode.

Parameters:
  • complex_factors (npt.NDArray[np.complex128]) – Complex state factors, shape (n_states, n_modes). Each valid column sums to one.

  • magnitude_fractions (npt.NDArray[np.float64]) – Absolute factors normalised to sum to one per valid mode.

  • closure_error (npt.NDArray[np.float64]) – Absolute difference between each complex column sum and one.

  • valid (npt.NDArray[np.bool]) – Whether the left–right inner product is numerically resolvable. Invalid columns contain NaN and must not be interpreted.

  • subsystem_names (tuple[str, ...]) – Ordered subsystem labels indexing subsystem_fractions.

  • subsystem_fractions (npt.NDArray[np.float64]) – Magnitude fractions summed by subsystem, shape (n_subsystems, n_modes).

  • state_subsystems (tuple[str, ...]) – Subsystem assigned to each state.

complex_factors: npt.NDArray[np.complex128]
magnitude_fractions: npt.NDArray[np.float64]
closure_error: npt.NDArray[np.float64]
valid: npt.NDArray[np.bool_]
subsystem_names: tuple[str, ...]
subsystem_fractions: npt.NDArray[np.float64]
state_subsystems: tuple[str, ...]
biorthogonal_participation(solution, *, subsystem_labels=None)[source]

Compute scale-invariant participation from a modal solution.

Parameters:
  • solution (ModalSolution) – Modal solution retaining matching full left and right eigenvectors.

  • subsystem_labels (sequence of str or None, optional) – Explicit subsystem label for every state. When omitted, labels are inferred from solution.state_descriptions.

Returns:

BiorthogonalParticipation – State and subsystem participation factors.

Raises:

ValueError – If left eigenvectors are unavailable, shapes disagree, or supplied labels do not match the state count.

Return type:

BiorthogonalParticipation

classify_state_subsystem(description)[source]

Classify an OpenFAST state description into an engineering subsystem.

Parameters:

description (str)

Return type:

str

Cross-operating-point mode identification (global tracking).

Modes computed independently at successive operating points (rotor speeds or wind speeds) are linked into tracks — the lines of a Campbell diagram. Both tracking strategies score a candidate link by the same affinity (the modal-assurance criterion tempered by a frequency-continuity penalty) and forbid links below the MAC threshold; they differ only in how that affinity is resolved into tracks:

"single_path" (the default) extracts each mode line as the globally optimal continuity path through the whole sweep: it repeatedly finds the single best-continuity path over the not-yet-assigned modes (a deterministic dynamic program over the affinity) and removes it, until every mode is assigned. It reads the strongest line off first.

"simultaneous" instead resolves each operating-point transition at once: all modes on one side are matched to all modes on the next by a single maximum-weight assignment (the Hungarian algorithm, with both sides free to stay unmatched). No early high-affinity link can then claim a mode that a different pairing would use to reach a higher overall total — the failure mode of a per-line extraction in dense spectra or at near-degenerate crossings, where an earlier high-scoring path can strand a node a later path would have preferred.

Both are deterministic — no random restarts or preset cluster counts — and each track carries a confidence (its weakest link’s correlation) and an ambiguity flag (raised where a mode had two near-equally correlated continuations, i.e. a crossing or veering hotspot). Every track is given a physical label.

DEFAULT_RIGID_BODY_FREQUENCY_HZ = 0.001

Natural frequency (Hz) below which a mode is rigid-body and is not tracked.

A period of 1000 s is far longer than any restrained mode of a turbine: the softest moored floating-platform surge and sway modes sit near 0.005-0.01 Hz. Below it lie only modes without restoring stiffness, such as the free rotor azimuth.

LABEL_MISMATCH_PENALTY = 0.5

Factor applied to the affinity of a link between two differently labelled modes.

When both ends of a candidate link carry an unambiguous physical label (see vane.modal.labeler.ModeLabel.is_ambiguous) and the labels differ – another family, mode number or multi-blade type – the link’s affinity is multiplied by this factor. A confident label change between neighbouring operating points is strong evidence that the link joins two different physical modes; ambiguous (hybrid) modes, as at a veering, are not penalised.

NEAR_COINCIDENT_RTOL = 0.01

Relative eigenvalue distance within which modes of one point are near-coincident.

Two modes whose eigenvalues satisfy \(|\lambda_i - \lambda_j| \le \mathrm{rtol} \max(|\lambda_i|, |\lambda_j|)\) are mixed by any coupling of comparable size, so their individual eigenvectors are an arbitrary basis of the pair’s subspace (e.g. the collective and regressive second flap of the IEA 22-MW turbine, 0.07 % apart at 10 m/s). Their MAC with a neighbouring point’s mode then measures the mixing, not the identity. For a link between two modes carrying the same unambiguous label, the subspace MAC of each end against the other end’s near-coincident cluster is used when it is higher; the labels (assigned one per multi-blade type by vane.modal.label_solution()) then decide which cluster member continues which line.

class IdentificationResult(tracks, n_operating_points, branch_groups=<factory>, heavily_damped_tracks=<factory>, heavily_damped_modes=<factory>)[source]

Bases: object

Result of cross-operating-point mode identification.

Parameters:
  • tracks (list[ModeTrack]) – The identified mode tracks, ordered by their first natural frequency.

  • n_operating_points (int) – Number of operating points processed.

  • branch_groups (list[BranchGroup]) – Crossing/veering hotspots flagged by the branch-review pass (empty unless branch_review was requested). See vane.modal.branch.

  • heavily_damped_tracks (list[ModeTrack]) – Tracks of the heavily damped (near-critical) modes, linked among themselves only, ordered by first natural frequency. Empty when the heavily damped tier is disabled or no mode qualifies.

  • heavily_damped_modes (list[list[int]]) – For each operating point, the indices of the modes classified heavily damped, so they are reported even where they form no track. Empty when the tier is disabled.

tracks: list[ModeTrack]
n_operating_points: int
branch_groups: list[BranchGroup]
heavily_damped_tracks: list[ModeTrack]
heavily_damped_modes: list[list[int]]
property n_heavily_damped: int

Return the number of heavily damped modes over all operating points.

class ModeTrack(operating_points, mode_indices, natural_frequencies_hz, damping_ratios, label, confidence=1.0, is_ambiguous=False, is_heavily_damped=False, label_agreement=1.0, point_labels=<factory>)[source]

Bases: object

A mode followed across operating points.

Parameters:
  • operating_points (list[int]) – Indices of the operating points this track spans, in order.

  • mode_indices (list[int]) – The mode index within each corresponding operating point.

  • natural_frequencies_hz (list[float]) – Natural frequency at each operating point along the track.

  • damping_ratios (list[float]) – Damping ratio at each operating point along the track.

  • label (ModeLabel) – Representative physical label: the label carrying the largest summed confidence over the track’s operating points (its most confident instance), counting only unambiguous point labels when there are any.

  • confidence (float) – The weakest link correlation along the track (minimum MAC between consecutive points, or the subspace MAC across a near-coincident pair), in [0, 1]; 1.0 for a single-point track.

  • is_ambiguous (bool) – Whether the track’s identity is in doubt: a link was a near-tie (a mode with two near-equally correlated continuations, i.e. a crossing or veering hotspot), an unambiguous point label disagrees with the track label, every point label is ambiguous, or the track is a single operating point of a multi-point sweep (no continuity confirms it).

  • is_heavily_damped (bool) – Whether the track links heavily damped (near-critical) modes, which are tracked in their own tier (see identify_modes()).

  • label_agreement (float) – Fraction of the track’s labelled operating points whose own label equals the track label, in [0, 1].

  • point_labels (list[ModeLabel]) – The label of the track’s mode at each of its operating points, aligned with operating_points; empty when any of those solutions carries no DOF descriptions (the agreement and ambiguity still use the labelled ones).

operating_points: list[int]
mode_indices: list[int]
natural_frequencies_hz: list[float]
damping_ratios: list[float]
label: ModeLabel
confidence: float = 1.0
is_ambiguous: bool = False
is_heavily_damped: bool = False
label_agreement: float = 1.0
point_labels: list[ModeLabel]
heavily_damped_modes(solution, *, threshold=0.99, rotor_speed_rpm=None)[source]

Return which modes of one operating point are heavily damped.

A mode is heavily damped (near-critical) when its damping ratio, measured in the frame in which the mode is defined (rotor_frame_damping_ratios()), is at least threshold. Such a mode barely oscillates: its damped frequency is a small fraction of its natural frequency, which mostly measures the decay rate, and its shape correlates weakly with its neighbours across operating points. These are oscillatory (complex) modes; the eigensolver’s n_overdamped instead counts the real, non-oscillatory eigenvalues, which are never returned as modes.

Parameters:
  • solution (ModalSolution) – The modal solution of one operating point.

  • threshold (float, optional) – Damping ratio in (0, 1] at or above which a mode is heavily damped. Default DEFAULT_HEAVILY_DAMPED_THRESHOLD.

  • rotor_speed_rpm (float or None, optional) – The rotor speed (rev/min) at the operating point. None (the default) uses solution.rotor_speed_rpm, as vane.modal.label_solution() does. When neither is known, regressive and progressive blade whirl modes are never classified, since their fixed-frame damping ratio is shifted by the whirl.

Returns:

npt.NDArray[np.bool_] – Shape (n_modes,); True for each heavily damped mode.

Raises:

ValueError – If threshold is not in (0, 1] or rotor_speed_rpm is not finite.

Return type:

npt.NDArray[np.bool_]

identify_modes(solutions, *, frequency_weight=0.5, mac_threshold=0.5, ambiguity_margin=0.2, strategy='single_path', branch_review=False, resolve=False, parameter_values=None, rigid_body_frequency_hz=0.001, heavily_damped_threshold=0.99, rotor_speeds_rpm=None, continuity_metric='natural_frequency')[source]

Link modes across operating points into labeled tracks, globally.

Parameters:
  • solutions (Sequence[ModalSolution]) – Modal solutions, one per operating point, in operating-point order. All must share the same number of mode-shape degrees of freedom.

  • frequency_weight (float, optional) – Weight of the frequency-continuity penalty in the link affinity, [0, 1].

  • mac_threshold (float, optional) – Minimum modal assurance criterion for two modes to be linkable.

  • ambiguity_margin (float, optional) – A link is flagged ambiguous when the best continuation’s MAC exceeds the second best by less than this margin.

  • strategy ({"single_path", "simultaneous"}, optional) – How links are resolved into tracks. "single_path" (the default) extracts the globally optimal continuity path one line at a time; "simultaneous" resolves each operating-point transition with one maximum-weight assignment, which is more reliable where lines cross or veer in a dense spectrum. The two agree on well-separated spectra and differ only at contested transitions; see the module docstring.

  • branch_review (bool, optional) – When True, run the branch-review pass and attach the detected crossing/veering hotspots to the result’s branch_groups (the tracks themselves are unchanged). Default False. See vane.modal.branch.

  • resolve (bool, optional) – When True, also attach the smooth (minimum-curvature) alternative to each eligible branch group (implies branch_review; the tracks are still not modified). Default False.

  • parameter_values (Sequence[float] or None, optional) – The operating-parameter value at each operating point, used only by resolve as the curvature abscissa so smoothness is measured against the real (possibly non-uniform) sweep spacing. Defaults to the operating-point index (uniform spacing).

  • rigid_body_frequency_hz (float, optional) – Modes whose undamped natural frequency is below this value (Hz) are rigid-body modes with no restoring stiffness and are left out of every track. Default DEFAULT_RIGID_BODY_FREQUENCY_HZ; 0.0 tracks every mode.

  • heavily_damped_threshold (float or None, optional) – Damping ratio, in (0, 1], at or above which a mode is heavily damped (see heavily_damped_modes()). Heavily damped modes are kept out of the main tracks and linked among themselves only, into heavily_damped_tracks, so they can neither take over nor split a lightly damped line; every one is listed per operating point in heavily_damped_modes. Default DEFAULT_HEAVILY_DAMPED_THRESHOLD. None disables the tier and tracks every mode together.

  • rotor_speeds_rpm (Sequence[float] or None, optional) – Rotor speed (rev/min) at each operating point. Needed to measure the damping of a regressive or progressive blade whirl mode in the rotating frame (see rotor_frame_damping_ratios()). None (the default) uses each solution’s own rotor_speed_rpm (set by vane.modal.eigensolver.modes_from_mbc()), as vane.modal.label_solution() does; at a point whose rotor speed is unknown (nan) such modes are never classified heavily damped, because their fixed-frame damping ratio is shifted by the whirl and is not a property of the mode.

  • continuity_metric ({"natural_frequency", "eigenvalue"}, optional) – The distance the continuity penalty measures between two modes of the main tracks. "natural_frequency" (the default) is the difference of undamped natural frequencies; "eigenvalue" is the distance \(|\lambda_i - \lambda_j| / 2\pi\) in the complex plane, which equals it for lightly damped modes and also weighs a change of decay rate. The heavily damped tier always uses "eigenvalue", since a near-critical mode’s natural frequency mostly measures its decay rate.

Returns:

IdentificationResult – The identified tracks and the number of operating points.

Raises:

ValueError – If frequency_weight, mac_threshold, or ambiguity_margin is not in [0, 1], rigid_body_frequency_hz is negative or non-finite, heavily_damped_threshold is not in (0, 1], rotor_speeds_rpm has the wrong length or non-finite values, strategy or continuity_metric is unknown, or the solutions have inconsistent mode-shape DOF layout.

Return type:

IdentificationResult

Notes

The free rotor-azimuth (generator) DOF has no stiffness: its azimuth and speed states give a zero and an aerodynamic-damping eigenvalue, which the eigensolver normally returns as real and drops. Where the two nearly coincide they can come back as a complex pair of negligible frequency (periods of tens of minutes), so the mode would appear at isolated operating points only. The rigid_body_frequency_hz floor removes it everywhere.

A blade mode whose aerodynamic damping exceeds critical in the rotating frame has two real rotating-frame eigenvalues \(-\sigma_1, -\sigma_2\). The MBC transform maps each cyclic pair of them to a fixed-frame pair \(-\sigma_k \pm i\Omega\): an oscillation at the rotor frequency whose natural frequency \(\sqrt{\sigma_k^2 + \Omega^2}\) measures the decay rate rather than any vibration, while the collective pair is real and is dropped by the eigensolver. Tracking those images by natural frequency against the lightly damped whirl lines either side of the transition draws spurious lines; the heavily damped tier keeps them apart.

rotor_frame_damping_ratios(solution, rotor_speed_rpm, multiblade)[source]

Return each mode’s damping ratio in the frame in which the mode is defined.

Parameters:
  • solution (ModalSolution) – The modal solution of one operating point (in multi-blade coordinates).

  • rotor_speed_rpm (float) – The rotor speed (rev/min) at that operating point.

  • multiblade (Sequence[str or None]) – The multi-blade type of each mode ("collective", "regressive", "progressive", "cyclic" or None), e.g. the multiblade of vane.modal.label_solution().

Returns:

npt.NDArray[np.float64] – Shape (n_modes,). For a regressive or progressive whirl mode, the damping ratio of the rotating-frame eigenvalue it is the image of; for every other mode (collective, tower, platform, drivetrain, unlabelled), the fixed-frame damping ratio. A "cyclic" mode of undetermined whirl takes the smaller of the two whirl readings.

Raises:

ValueError – If multiblade does not have one entry per mode or rotor_speed_rpm is not finite.

Return type:

npt.NDArray[np.float64]

Notes

The MBC transform shifts a cyclic blade mode’s rotating-frame eigenvalue \(\lambda_r = -\sigma + i\omega_r\) by the rotor frequency \(\Omega\): the progressive whirl appears at \(\omega_r + \Omega\) and the regressive at \(|\omega_r - \Omega|\), with the same decay rate \(\sigma\). The fixed-frame damping ratio \(\sigma / |\lambda|\) therefore differs between the two whirls of one blade mode and rises towards 1 on the regressive branch as \(\omega_r\) approaches \(\Omega\), without the blade mode itself being any more damped. Undoing the shift, \(\omega_r = |\omega \mp \Omega|\), gives the damping ratio \(\sigma / \sqrt{\sigma^2 + \omega_r^2}\) of the blade mode, which is the same for its regressive, collective and progressive members.

Physics-aware labeling of system modes.

Each mode is labeled by the physical degree-of-freedom category that dominates it. Participation is aggregated per DofCategory (via vane.config.dof_map.classify_dof()), the dominant category names the mode (e.g. “1st Tower Fore-Aft”), and the fraction of participation it carries is reported as a confidence — a low value flags an ambiguous, mixed mode.

label_solution() votes with the scale-invariant left–right (biorthogonal) participation of vane.modal.biorthogonal, so the many unclassified module states of a coupled model (mooring line nodes, hydrodynamic radiation states, aerodynamic states) cannot out-vote the structural degrees of freedom merely by having large raw eigenvector amplitudes in their own units.

BeamDyn blades have no modal DOFs: their nodal states vote as flapwise, edgewise, torsional, or axial blade motion (see vane.config.dof_map), and label_solution() numbers the resulting flap, edge, and torsion modes from the spanwise shape of their section rotation and their frequency order.

Ambiguity is reported, never resolved silently. A mode whose runner-up category carries at least LABEL_AMBIGUITY_RATIO of the winner’s weight (a hybrid, e.g. a monopile’s second tower fore-aft mode mixed with the progressive second flap), or whose multi-blade type is not decided by its collective/cyclic split and whirl, is returned with is_ambiguous=True and the runner-up as alternative. Within one operating point each blade mode family (e.g. the second flap) has exactly one collective, one regressive and one progressive member; label_solution() enforces this (see label_solution(), Notes).

BEAMDYN_RIGID_RANGE = 0.1

Spanwise rotation spread below which a BeamDyn blade mode counts as rigid.

The linearized BeamDyn nodal states carry the rigid motion of the blade root. When the section rotation varies along the span by less than this fraction of its largest value, the blade turns as a rigid body and the mode is not an elastic bending mode; a rigid collective edgewise rotation (all blades turning together in the rotor plane) is the rotor-azimuth mode.

BEAMDYN_SHAPE_TOLERANCE = 0.05

Relative size below which a spanwise rotation increment is ignored.

The order of a BeamDyn bending or torsion mode is one plus the number of sign changes of the increments of its nodal section rotation along the span; increments smaller than this fraction of the largest one are treated as numerical noise and skipped.

DEFAULT_HEAVILY_DAMPED_THRESHOLD = 0.99

Damping ratio at or above which a mode is heavily damped (near-critical).

The ratio is measured in the frame in which the mode is defined: the rotating frame for a regressive or progressive blade whirl mode (see vane.modal.identifier.rotor_frame_damping_ratios()), the fixed frame otherwise. 0.99 means within 1 % of critical damping, where the damped frequency is below 15 % of the natural frequency and the natural frequency \(|\lambda|\) is essentially a decay rate. It is set from the reference-turbine sweeps (NREL 5-MW land and OC3 monopile, IEA 3.4-MW, IEA 10-MW and IEA 15-MW monopile, CalcSteady with quasi-steady aero): every first-flap Campbell line that tracks cleanly stays at or below 0.95 in this frame (its fixed-frame value reaches 0.998 on the regressive branch), while the modes that broke the IEA 15-MW lines above rated sit at 1.000, i.e. past critical damping in the rotating frame. Lightly damped lines that the community still draws (first flap at 0.25-0.78 in published IEA Campbell diagrams) are far below it.

DEFAULT_MIN_KNOWN_PARTICIPATION = 0.5

Default minimum share of participation that classified DOFs must carry.

A mode is named after a physical DOF category only when the classified (structural) degrees of freedom carry at least half of its total participation, i.e. when the structure – not unclassified module states – is the majority participant. Below this the mode is reported as "Unidentified".

DRIVETRAIN_ROOT_SHARE = 0.25

Minimum in-phase root rotation of a BeamDyn drivetrain-torsion mode.

With the drivetrain flexible (DrTrDOF) and the generator locked (no GenDOF), the lowest collective edgewise BeamDyn mode is the drivetrain torsion: the whole rotor swings against the shaft spring, so the blade roots rotate in phase with the tips by a large fraction of the tip rotation (one half on the NREL 5-MW). In a collective edgewise bending mode the hub instead stays put or counter-rotates. A collective edgewise mode whose root section rotation is in phase with its peak rotation and at least this fraction of it is named "Drivetrain Torsion".

LABEL_AMBIGUITY_RATIO = 0.75

Runner-up to winner weight ratio at or above which a label is ambiguous.

The category vote (and, for a blade mode, the collective-versus-cyclic split) names a mode after its largest share. When the runner-up carries at least this fraction of the winner’s weight – e.g. 0.40 against 0.53 – the mode is a hybrid of the two, and which name it gets is decided by a few percent of participation that move with the operating point. The label is then reported as ambiguous with the runner-up as its alternative. 0.75 is set from the reference-turbine sweeps: the clean modes of the NREL 5-MW, IEA 3.4-, 10-, 15- and 22-MW turbines have runner-up ratios below 0.4, while the hybrids that flip labels between neighbouring operating points (monopile tower/flap, flap/edge veering, flap/rotor-speed, aero-state/flap) sit between 0.8 and 1.0.

TOWER_SUPPORT_SHARE = 0.3333333333333333

Minimum share of a support plane’s participation that tower bending must carry.

A support plane groups the platform translation and tilt with the tower bending DOFs of one plane (fore-aft: surge, pitch and the tower fore-aft modes; side-side: sway, roll and the tower side-side modes). On a fixed-bottom support whose substructure flexibility is carried by the platform DOFs (e.g. a monopile), the global tower bending modes put a large share of their participation on platform translation, so a per-category vote would call them platform modes. When tower bending carries at least this share of the plane’s participation and the plane as a whole out-votes every other category, the mode is a tower bending mode of that plane. A rigid-body platform mode of a floating turbine has (near-)zero tower participation and is never regrouped.

WHIRL_CIRCULARITY_THRESHOLD = 0.2

Circularity below which a cyclic blade mode’s whirl direction is undetermined.

The circularity \(2\operatorname{Im}(q_s \overline{q_c}) / (|q_c|^2 + |q_s|^2)\) is +1/-1 for a pure backward/forward whirl and 0 for a standing pattern; below this magnitude the mode is a standing ("cyclic") pattern, e.g. a mixture of the regressive and progressive members of a near-coincident pair.

class ModeLabel(category, label, confidence, dominant_dofs, multiblade=None, is_ambiguous=False, alternative=None, alternative_category=None, alternative_confidence=0.0)[source]

Bases: object

Physical label for a single mode.

Parameters:
  • category (DofCategory) – The dominant DOF category.

  • label (str) – Human-readable name of the dominant category.

  • confidence (float) – Fraction of total participation carried by the dominant category, in [0, 1]; low values indicate a mixed or ambiguous mode. For an "Unidentified" mode it is the share carried by unclassified states.

  • dominant_dofs (list[str]) – Descriptions of the highest-participation degrees of freedom (up to three).

  • multiblade (str or None) – For a blade mode after the MBC transform, the multi-blade type "collective", "regressive", "progressive", or "cyclic" (a standing cyclic mode with no whirl direction); None for non-blade modes or when no multi-blade information is available.

  • is_ambiguous (bool, optional) – Whether the label is not decided by the mode’s participation: the runner-up category carries at least LABEL_AMBIGUITY_RATIO of the winner’s weight, the multi-blade type is not decided (collective and cyclic shares within that ratio, or a standing cyclic pattern), or the per-point multi-blade assignment of label_solution() overrode the mode’s own type. Default False.

  • alternative (str or None, optional) – The strongest competing label: the runner-up category of the vote, the competing multi-blade type of an undecided blade mode, or the label the mode had before a multi-blade reassignment; None when nothing competes. It is given for unambiguous labels too (as the runner-up); is_ambiguous says whether it is a close competitor.

  • alternative_category (DofCategory or None, optional) – The category of alternative.

  • alternative_confidence (float, optional) – The share of the participation carried by alternative (0.0 when there is none).

category: DofCategory
label: str
confidence: float
dominant_dofs: list[str]
multiblade: str | None = None
is_ambiguous: bool = False
alternative: str | None = None
alternative_category: DofCategory | None = None
alternative_confidence: float = 0.0
category_to_label(category)[source]

Return the human-readable label for a DOF category.

Parameters:

category (DofCategory) – The category to name.

Returns:

str – The display label ("Unidentified" for unknown categories).

Return type:

str

label_mode(magnitudes, descriptions, *, multiblade=None, min_known_fraction=None)[source]

Label one mode from its per-DOF participation weights.

Parameters:
  • magnitudes (npt.NDArray[np.float64]) – Non-negative participation weight of each DOF, shape (n_dof,): raw mode-shape magnitudes, or (preferably) the scale-invariant participation fractions used by label_solution().

  • descriptions (Sequence[str]) – DOF descriptions parallel to magnitudes.

  • multiblade (str or None, optional) – Multi-blade type ("collective"/"regressive"/"progressive") for a blade mode; appended to the label and stored on the result.

  • min_known_fraction (float or None, optional) – When None (the default), the plain plurality vote: the category with the largest summed weight wins, DofCategory.UNKNOWN included. When a number in [0, 1] is given, the dominant category is the largest classified (non-UNKNOWN) category, accepted only if the classified categories together carry at least this fraction of the total weight; otherwise the mode is "Unidentified". On this (participation) path a support plane – the platform translation and tilt plus the tower bending DOFs of one plane – votes as one group when tower bending carries at least TOWER_SUPPORT_SHARE of it; if the group out-votes every other category the mode is named after its dominant tower DOF.

Returns:

ModeLabel – The dominant category, its label, the confidence, and the top DOFs. The confidence is the dominant category’s share of the total weight (the whole support plane’s share for a regrouped tower mode); for an "Unidentified" mode it is the share carried by unclassified states.

Raises:
  • ValueError – If magnitudes and descriptions have different lengths, or if min_known_fraction is outside [0, 1] or non-finite.

  • TypeError – If min_known_fraction is not a real number.

Return type:

ModeLabel

label_modes(participation, descriptions, *, min_known_fraction=None)[source]

Label every mode of a participation result.

Parameters:
  • participation (ParticipationResult) – Per-DOF participation (its magnitude columns are one mode each).

  • descriptions (Sequence[str]) – DOF descriptions parallel to the participation rows.

  • min_known_fraction (float or None, optional) – Forwarded to label_mode(); None (the default) keeps the plain plurality vote.

Returns:

list[ModeLabel] – One label per mode (column).

Return type:

list[ModeLabel]

label_solution(solution, *, min_known_participation=0.5, rotor_speed_rpm=None)[source]

Label every mode of a modal solution by its biorthogonal participation.

For mode \(k\) with right and left eigenvectors \(\mathbf{x}_k\) and \(\mathbf{y}_k\) of the full state matrix, the participation of state \(i\) is \(p_{ik} \propto |\overline{y}_{ik} x_{ik}|\), normalised so that \(\sum_i p_{ik} = 1\) (see vane.modal.biorthogonal.biorthogonal_participation()). The displacement and velocity states of a second-order DOF are summed onto that DOF, and the shares are voted per DofCategory by label_mode() with min_known_fraction=min_known_participation: the largest classified category names the mode if the classified DOFs carry at least min_known_participation of it, otherwise the mode is "Unidentified". The confidence is the dominant category’s share of the mode’s participation.

Platform translation/tilt and tower bending of one plane vote together when tower bending carries at least TOWER_SUPPORT_SHARE of that plane’s participation, so the global tower modes of a fixed-bottom support (whose substructure flexibility sits in the platform DOFs) are named tower modes, not platform modes. The tower modes of each plane are then numbered in order of frequency ("3rd Tower Fore-Aft" for the third), never below the number of their dominant tower DOF.

A non-degenerate rotor mode carries its multi-blade type (collective, regressive, progressive, or a standing cyclic pattern), decided from the collective/cyclic split of its participation and the whirl of its dominant cosine/sine pair.

A BeamDyn blade mode (one voted BLADE_FLAP, BLADE_EDGE, or BLADE_TORSION) is then numbered, e.g. "2nd Blade Flap (Collective)". Its shape order is one plus the number of sign changes of the spanwise increments of its section rotation (about Y for flap, X for edge, Z for torsion), which is blind to the rigid root rotation the nodal states carry. Within a family the modes of one multi-blade type are numbered in frequency order, each at least its shape order. A mode whose section rotation barely varies along the span (BEAMDYN_RIGID_RANGE) is rigid blade motion, not bending: a rigid collective edgewise mode is the rotor-azimuth mode ("Rotor/Generator"), and any other rigid mode keeps its unnumbered family label.

Parameters:
  • solution (ModalSolution) – A solution whose dof_descriptions are populated (i.e. produced with descriptions, as by vane.modal.eigensolver.modes_from_mbc()).

  • min_known_participation (float, optional) – Minimum share of the participation, in [0, 1], that classified DOFs must carry for a mode to receive a physical label (default DEFAULT_MIN_KNOWN_PARTICIPATION, 0.5: a majority).

  • rotor_speed_rpm (float or None, optional) – Rotor speed (rev/min) of the operating point, used to read a whirl mode’s damping in the rotating frame so heavily damped modes stay out of the per-point multi-blade assignment. Default None uses solution.rotor_speed_rpm (set by vane.modal.eigensolver.modes_from_mbc()).

Returns:

list[ModeLabel] – One label per mode.

Raises:
  • ValueError – If the solution carries no DOF descriptions, or min_known_participation is outside [0, 1] or non-finite.

  • TypeError – If min_known_participation is not a real number.

Return type:

list[ModeLabel]

Notes

A solution without usable left eigenvectors (e.g. constructed by hand), or an individual mode whose left–right product is numerically unresolvable, falls back to the plurality vote over raw mode-shape magnitudes. The multi-blade type is decided from the mode’s MBC coordinates – the collective/cyclic split from its participation (or, without it, from the blade energy of the mode shape) and the whirl from the shape’s cosine/sine phase; on the participation path it is attached only to a blade or "Unidentified" mode, never to a mode the vote assigns to the tower, drivetrain, or platform.

Ambiguity. A label whose runner-up category carries at least LABEL_AMBIGUITY_RATIO of the winner’s weight is returned with is_ambiguous=True and the runner-up as alternative; so is a blade mode whose collective and cyclic shares are within that ratio, or whose whirl is a standing pattern ("cyclic").

One mode per multi-blade type. Each blade mode family (e.g. the second flap) of a three-bladed rotor has one collective, one regressive and one progressive member, the regressive below the progressive. Where near-coincident members mix and break this at one point, the family’s lightly damped members are assigned to the three types jointly (best summed fit subject to the whirl frequency order); a reassigned member is marked ambiguous, and a member left over is renamed after its non-blade alternative (e.g. a tower or platform mode it is mixed with) or keeps its unsuffixed family name, ambiguous either way. Heavily damped modes (DEFAULT_HEAVILY_DAMPED_THRESHOLD, read in the rotating frame for a whirl mode) are left out of the assignment: the fixed-frame images of an overdamped blade mode whirl at the rotor speed and are not members of a whirl pair.

BeamDyn drivetrain. With a flexible drivetrain, a BeamDyn model’s drivetrain-torsion mode votes for the generator azimuth (generator free) or for collective edgewise motion (generator locked); it is renamed "Drivetrain Torsion" from its blade-root rotation (see DRIVETRAIN_ROOT_SHARE).

The tower modes of each plane are numbered after the multi-blade assignment, so a tower/blade hybrid that loses its blade type to a purer family member (and is renamed after its tower alternative) is counted among the tower modes.

Branch review — detect crossing and veering hotspots among Campbell tracks.

Cross-operating-point tracking (vane.modal.identifier) links modes into the lines of a Campbell diagram. Where two lines cross or veer (approach, exchange character, then repel), a per-transition tracker is least certain which mode continues which line. This module detects those contested clusters and surfaces them as auditable BranchGroup records — a branch-review view — rather than quietly trusting the automatic assignment.

Detection is deterministic and conservative. A pair of tracks is flagged only where both their frequencies approach (within a scale-adaptive tolerance) and their mode shapes genuinely compete (an ambiguous link or elevated cross-correlation across a transition). Frequency proximity alone is not enough: two modes with orthogonal shapes that merely pass close in frequency are tracked unambiguously and are not a hazard, so they are not flagged.

The pass adds no risk to the tracks themselves — it only reports. Re-drawing contested clusters is a separate, opt-in step (see the resolve option of vane.modal.identify_modes()).

class BranchGroup(track_indices, op_lo, op_hi, frequency_band_hz, kind, min_separation_hz, min_separation_op, sub_windows, is_dense, resolved=False, smooth_branches=None)[source]

Bases: object

A cluster of tracks contested at a crossing or veering hotspot.

Parameters:
  • track_indices (tuple[int, ...]) – Indices (ascending) into IdentificationResult.tracks of the tracks in the cluster.

  • op_lo (int) – Hull operating-point window spanning all of the cluster’s contested windows.

  • op_hi (int) – Hull operating-point window spanning all of the cluster’s contested windows.

  • frequency_band_hz (tuple[float, float]) – Minimum and maximum natural frequency of the member tracks within the hull window.

  • kind (BranchKind) – Group-level classification — BranchKind.CROSSING if any member pair crosses, otherwise BranchKind.VEERING.

  • min_separation_hz (float) – Closest frequency approach between any member pair within the window.

  • min_separation_op (int) – Operating-point index at which the closest approach occurs.

  • sub_windows (tuple[BranchSubWindow, ...]) – The distinct contested windows that make up the group, ordered canonically.

  • is_dense (bool) – Whether the group has more than two tracks. Dense groups are still surfaced for review; resolution handles them up to a small branch cap.

  • resolved (bool) – Whether a resolution pass computed a smooth alternative for this group (always False for detection-only runs).

  • smooth_branches (tuple[tuple[float, ...], ...] or None) – The minimum-curvature (“smooth”, non-crossing) frequency alternative for the contested cluster, one tuple per branch aligned with the window operating points op_lo..op_hi. Set by the resolution pass for an eligible group, else None. The actual tracks are never modified — this is review-only metadata contrasting the smooth convention with the shape-following base tracks.

track_indices: tuple[int, ...]
op_lo: int
op_hi: int
frequency_band_hz: tuple[float, float]
kind: BranchKind
min_separation_hz: float
min_separation_op: int
sub_windows: tuple[BranchSubWindow, ...]
is_dense: bool
resolved: bool = False
smooth_branches: tuple[tuple[float, ...], ...] | None = None
class BranchKind(*values)[source]

Bases: Enum

Whether a contested window is a frequency crossing or a veering.

CROSSING = 'crossing'
VEERING = 'veering'
class BranchSubWindow(op_lo, op_hi, kind, truncated)[source]

Bases: object

A contiguous operating-point window where two tracks are contested.

Parameters:
  • op_lo (int) – First and last operating-point index of the window (op_hi > op_lo).

  • op_hi (int) – First and last operating-point index of the window (op_hi > op_lo).

  • kind (BranchKind) – Whether the two tracks cross (their frequency order swaps) or veer (approach and separate without swapping) over the window.

  • truncated (bool) – Whether the closest approach is at a window endpoint, so the hotspot may extend beyond the swept range.

op_lo: int
op_hi: int
kind: BranchKind
truncated: bool
attach_branch_groups(result, solutions, *, resolve=False, parameter_values=None, **kwargs)[source]

Return a copy of result with its branch_groups populated.

Detection does not alter the tracks; the returned result shares the same track objects and only carries the additional branch groups.

Parameters:
  • result (IdentificationResult) – The tracking result to review.

  • solutions (Sequence[ModalSolution]) – The per-operating-point modal solutions, in operating-point order.

  • resolve (bool, optional) – When True, also compute the smooth (minimum-curvature) alternative for each eligible group (see resolve_branch_groups()); the tracks are still not modified. Default False.

  • parameter_values (Sequence[float] or None, optional) – The operating-parameter value at each operating point, forwarded to the resolution curvature (see resolve_branch_groups()).

  • **kwargs – Forwarded to detect_branch_groups() (veering_tol_hz, mac_competition, ambiguity_margin).

Returns:

IdentificationResult – A new result with the detected (and optionally resolved) branch groups.

Return type:

IdentificationResult

detect_branch_groups(result, solutions, *, veering_tol_hz=None, mac_competition=0.25, ambiguity_margin=0.2)[source]

Find crossing/veering hotspots among a result’s tracks.

Parameters:
  • result (IdentificationResult) – The tracking result whose tracks are reviewed. Not modified.

  • solutions (Sequence[ModalSolution]) – The per-operating-point modal solutions the tracks were built from, in operating-point order (used for the mode-shape correlations).

  • veering_tol_hz (float or None, optional) – Frequency proximity below which two tracks may be contested. None (the default) derives it deterministically as 0.25 times the median adjacent natural-frequency spacing across the sweep, so it scales with the spectrum.

  • mac_competition (float, optional) – Cross-MAC at or above which two tracks are deemed to compete across a transition.

  • ambiguity_margin (float, optional) – Two non-negligible continuations whose MAC differs by less than this are a near-tie (an ambiguous link).

Returns:

list[BranchGroup] – The contested clusters, ordered canonically by window then frequency. Empty when no tracks are contested.

Raises:

ValueError – If mac_competition or ambiguity_margin is not in [0, 1], or veering_tol_hz is negative.

Return type:

list[BranchGroup]

resolve_branch_groups(groups, result, *, parameter_values=None)[source]

Annotate each eligible group with its smooth (minimum-curvature) alternative.

For a group of two to four tracks spanning at least three operating points where every track is present throughout the window, computes the assignment of the group’s modes to branches that minimizes the total squared frequency curvature — the smooth, non-crossing convention — and attaches it as smooth_branches with resolved=True. The tracks are never modified; this is review-only metadata. Larger groups, short windows, and ragged membership are left unresolved.

Parameters:
  • groups (list[BranchGroup]) – The detected branch groups.

  • result (IdentificationResult) – The tracking result the groups index (for the member-track frequencies).

  • parameter_values (Sequence[float] or None, optional) – The operating-parameter value at each operating point, used as the abscissa for the curvature so smoothness is measured against the real (possibly non-uniform) sweep spacing. None (the default) falls back to the operating-point index (uniform spacing). When given, it must have one value per operating point and be strictly increasing.

Returns:

list[BranchGroup] – New groups, with the smooth alternative attached where eligible.

Return type:

list[BranchGroup]

Uncertainty quantification for modal estimates.

Two physically grounded uncertainty sources are combined into one per-mode confidence:

  • Azimuth spread. The MBC transform produces one non-rotating state matrix per azimuth; their average is the linear-time-invariant model. For an isotropic rotor every per-azimuth matrix is identical (azimuth-invariant to machine precision), so the eigenvalue spread across azimuths is ~0; for an anisotropic rotor the spread grows, quantifying how well the averaged model represents the periodic system.

  • Numerical conditioning and degeneracy from the eigen-analysis.

The unified confidence multiplies independent reliability factors (each in [0, 1]) so that a mode is trusted only when it is numerically well posed, non-degenerate, azimuth-stable, and (optionally) confidently tracked. It feeds the covariance initialization of the state-space export.

class AzimuthSpread(natural_frequency_std, damping_ratio_std, n_azimuths)[source]

Bases: object

Per-mode variation of the modal estimates across azimuth.

Parameters:
  • natural_frequency_std (npt.NDArray[np.float64]) – Standard deviation of each averaged mode’s natural frequency (Hz) across the per-azimuth perturbations of the averaged model, one entry per mode of vane.modal.eigensolver.modes_from_mbc() of the same result, in its order.

  • damping_ratio_std (npt.NDArray[np.float64]) – Standard deviation of each averaged mode’s damping ratio across azimuths.

  • n_azimuths (int) – Number of azimuth samples used.

natural_frequency_std: npt.NDArray[np.float64]
damping_ratio_std: npt.NDArray[np.float64]
n_azimuths: int
azimuth_spread(result)[source]

Compute the per-mode azimuth spread of a retained MBC result.

Rather than comparing per-azimuth spectra directly, this measures how each frozen-time matrix A(psi) perturbs the averaged model: for an isotropic rotor every A(psi) == avg_a and the spread is zero, while anisotropy makes the per-azimuth matrices (and their spectra) differ by an amount the spread captures. For each averaged mode the first-order eigenvalue perturbation dlambda = y^H (A(psi) - avg_a) x / (y^H x) (left eigenvector y, right x) is evaluated at every azimuth, and the standard deviation of the resulting natural frequency and damping is returned. It is zero for an isotropic rotor (A(psi) == avg_a) and grows with anisotropy.

Parameters:

result (MBCResult) – An MBC result computed with retain_per_azimuth=True.

Returns:

AzimuthSpread – The per-mode frequency and damping spread.

Raises:

ValueError – If the per-azimuth matrices were not retained or avg_a is absent.

Return type:

AzimuthSpread

unified_mode_confidence(solution, *, frequency_spread=None, track_confidence=None, spread_scale=0.05)[source]

Combine the uncertainty sources into one per-mode confidence in [0, 1].

The confidence is the product of independent reliability factors: numerical conditioning (1 / kappa), a degeneracy penalty, an azimuth-stability factor exp(-relative_spread / spread_scale), and an optional tracking confidence.

Parameters:
  • solution (ModalSolution) – The modal solution (provides condition numbers and degeneracy flags).

  • frequency_spread (npt.NDArray[np.float64] or None, optional) – Per-mode natural-frequency standard deviation (e.g. from azimuth_spread()); nan entries are treated as zero spread.

  • track_confidence (npt.NDArray[np.float64] or None, optional) – Per-mode tracking confidence in [0, 1] from the cross-operating-point tracker.

  • spread_scale (float, optional) – Relative-spread scale at which the azimuth-stability factor falls to 1/e.

Returns:

npt.NDArray[np.float64] – The per-mode confidence.

Return type:

npt.NDArray[np.float64]

Calibration of the per-mode confidence into a reliability metric.

vane.modal.unified_mode_confidence() returns a per-mode confidence in [0, 1] built from physically grounded factors (numerical conditioning, degeneracy, azimuth spread, tracking ambiguity). That confidence is only useful if it is calibrated — among the modes assigned confidence ~c, about a fraction c should be correct. A raw confidence that is systematically over- or under-confident misleads any downstream decision (which modes to trust, how to weight the Kalman covariance).

This module measures that calibration against a ground-truth correctness label and, when the raw confidence is miscalibrated, learns a monotone recalibration map so the reported number can be read as a probability of being correct:

  • calibrate_confidence() bins (confidence, correct) pairs into a reliability diagram and reports the expected/maximum calibration error and the Brier score, plus a pool-adjacent-violators (isotonic) map confidence -> P(correct).

  • recalibrate() applies that map to new confidences.

  • confidence_threshold() reads off the smallest confidence at which the calibrated probability of being correct reaches a target — the evidence behind a claim like “confidence >= 0.85 implies >= 95 % correct”.

The correct label is supplied by the caller and is domain-specific: for mode identification it is predicted_label == true_label against a synthetic ground truth; for cross-operating-point tracking it is whether a track was assigned to the right mode. The calibration itself is agnostic to how correctness was decided.

class ConfidenceCalibration(bin_edges, bin_confidence, bin_accuracy, bin_count, expected_calibration_error, max_calibration_error, brier_score, map_confidence, map_accuracy, n_samples)[source]

Bases: object

The calibration of a confidence score against a correctness label.

Parameters:
  • bin_edges (npt.NDArray[np.float64]) – The n_bins + 1 equal-width bin edges spanning [0, 1].

  • bin_confidence (npt.NDArray[np.float64]) – Mean confidence within each bin, shape (n_bins,); nan for an empty bin.

  • bin_accuracy (npt.NDArray[np.float64]) – Empirical fraction correct within each bin, shape (n_bins,); nan for an empty bin. The reliability diagram plots bin_confidence vs bin_accuracy.

  • bin_count (npt.NDArray[np.int]) – Number of samples in each bin, shape (n_bins,).

  • expected_calibration_error (float) – Sample-weighted mean absolute gap between confidence and accuracy across non-empty bins (ECE); 0 is perfectly calibrated.

  • max_calibration_error (float) – Largest absolute confidence-accuracy gap over non-empty bins (MCE).

  • brier_score (float) – Mean squared error of confidence against the correctness label; lower is better.

  • map_confidence (npt.NDArray[np.float64]) – Strictly increasing confidence values of the isotonic recalibration map.

  • map_accuracy (npt.NDArray[np.float64]) – Non-decreasing calibrated P(correct) at each map_confidence point.

  • n_samples (int) – Number of (confidence, correct) pairs the calibration was built from.

bin_edges: npt.NDArray[np.float64]
bin_confidence: npt.NDArray[np.float64]
bin_accuracy: npt.NDArray[np.float64]
bin_count: npt.NDArray[np.int_]
expected_calibration_error: float
max_calibration_error: float
brier_score: float
map_confidence: npt.NDArray[np.float64]
map_accuracy: npt.NDArray[np.float64]
n_samples: int
calibrate_confidence(confidence, correct, *, n_bins=10)[source]

Calibrate a confidence score against a ground-truth correctness label.

Builds an equal-width reliability diagram over [0, 1], the expected (ECE) and maximum (MCE) calibration errors, the Brier score, and an isotonic (monotone) recalibration map confidence -> P(correct) fitted by pool-adjacent-violators.

Parameters:
  • confidence (npt.ArrayLike) – Per-sample confidence in [0, 1] (e.g. from vane.modal.unified_mode_confidence()).

  • correct (npt.ArrayLike) – Per-sample correctness label, boolean or 0/1, aligned with confidence.

  • n_bins (int, optional) – Number of equal-width reliability bins over [0, 1]. Default 10.

Returns:

ConfidenceCalibration – The reliability diagram, calibration-error metrics, Brier score, and isotonic recalibration map.

Raises:

ValueError – If the inputs are not equal-length non-empty 1-D arrays, confidence is non-finite or outside [0, 1], correct is not boolean/0/1, or n_bins is not a positive integer.

Return type:

ConfidenceCalibration

confidence_threshold(calibration, target_accuracy)[source]

Return the smallest confidence whose calibrated P(correct) reaches a target.

Inverts the same piecewise-linear curve recalibrate() evaluates, so the returned confidence c is the lowest value with recalibrate(c) >= target_accuracy: when the target falls between two map points the crossing confidence is linearly interpolated, rather than rounded up to the next sampled point. The answer is reported on the observed confidence span; a target at or below the lowest mapped accuracy returns the lowest mapped confidence.

Parameters:
Returns:

float – The smallest confidence whose calibrated probability meets the target.

Raises:

ValueError – If target_accuracy is not in (0, 1], or the calibrated probability never reaches it (the confidence is not discriminative enough for this target).

Return type:

float

recalibrate(calibration, confidence)[source]

Map raw confidence to a calibrated probability of being correct.

Linearly interpolates confidence onto the isotonic recalibration map; values below or above the fitted range are clamped to the map’s end values.

Parameters:
Returns:

npt.NDArray[np.float64] – The calibrated probability of being correct, same shape as confidence.

Raises:

ValueError – If confidence is non-finite or outside [0, 1].

Return type:

npt.NDArray[np.float64]

Azimuth-density calibration of the averaged-model modal frequencies.

The azimuth-averaged (Coleman/MBC) model is only as good as its azimuth sampling: too few azimuths and the average does not remove the rotor’s periodic content, so the Campbell frequencies are biased. VANE’s “thin azimuth coverage” warning is otherwise an empirical judgement; this module replaces it with a calibration curve — how the averaged natural frequencies converge as the azimuth count grows — measured against the densest available sampling as the reference.

Given a dense per-azimuth set, azimuth_density_sweep() sub-samples it to a series of azimuth counts (1, 3, 6, 12, 36, …), re-averages, and reports the natural-frequency error of each coarse model relative to the full-density reference. recommend_azimuth_count() then reads off the smallest azimuth count that brings that error under a tolerance — a data-grounded answer to “how many azimuths are enough” for a given turbine, in place of a hard-coded rule.

class AzimuthConvergence(densities, frequency_error_hz, natural_frequencies_hz, reference_frequencies_hz)[source]

Bases: object

How the averaged-model natural frequencies converge with azimuth count.

Parameters:
  • densities (npt.NDArray[np.int]) – The azimuth counts evaluated, ascending.

  • frequency_error_hz (npt.NDArray[np.float64]) – For each density, the maximum natural-frequency difference (Hz) between that coarse averaged model and the full-density reference.

  • natural_frequencies_hz (npt.NDArray[np.float64]) – The sorted natural frequencies (Hz) at each density, shape (n_density, n_frequencies).

  • reference_frequencies_hz (npt.NDArray[np.float64]) – The reference (full-density) sorted natural frequencies (Hz).

densities: npt.NDArray[np.int_]
frequency_error_hz: npt.NDArray[np.float64]
natural_frequencies_hz: npt.NDArray[np.float64]
reference_frequencies_hz: npt.NDArray[np.float64]
azimuth_density_sweep(per_azimuth_a, azimuths, *, densities=(1, 3, 6, 12, 36))[source]

Measure how the averaged model’s frequencies converge with azimuth count.

Sub-samples the (azimuth-sorted) per-azimuth matrices to each requested count, takes the mean as the averaged model, and compares its natural frequencies to those of the full-density average (the reference).

Parameters:
  • per_azimuth_a (npt.ArrayLike) – The frozen-time state matrices, shape (n_azimuth, n, n) (e.g. MBCResult.per_azimuth_a).

  • azimuths (npt.ArrayLike) – The rotor azimuth of each matrix; used only to order the samples, so any consistent unit works (degrees or radians, e.g. MBCResult.azimuths_deg).

  • densities (Sequence[int], optional) – Azimuth counts to evaluate; counts above the available number are skipped. Default (1, 3, 6, 12, 36).

Returns:

AzimuthConvergence – The per-density frequencies and their error versus the full-density reference.

Raises:

ValueError – If the matrices are not a consistent (n_azimuth, n, n) stack, the matrices or azimuths are non-finite, the azimuths do not match, a requested density is non-finite or not a whole number, or no requested density is achievable.

Return type:

AzimuthConvergence

recommend_azimuth_count(convergence, *, tolerance_hz)[source]

Return the smallest evaluated azimuth count whose frequency error is within tol.

Reads the convergence curve for the fewest azimuths at which the averaged model’s natural-frequency error (and that of every denser model evaluated) stays at or below tolerance_hz — the data-grounded minimum azimuth coverage.

Parameters:
Returns:

int – The recommended azimuth count.

Raises:

ValueError – If tolerance_hz is not positive, or no evaluated density meets it (the sampling does not converge within tolerance — use more azimuths).

Return type:

int

Floquet / Hill modal analysis of a linear time-periodic (LTP) system.

A spinning rotor makes the linearized dynamics \(\dot{\mathbf x} = \mathbf A(\psi) \mathbf x\) periodic in the azimuth \(\psi = \Omega t\) with period \(T = 2\pi/\Omega\), not time-invariant. The multi-blade (Coleman) transform removes that periodicity only for an isotropic rotor; for an anisotropic system — a two-bladed rotor, or a floating turbine whose mooring/hydrodynamics break the rotational symmetry — the azimuth-averaged model is an approximation whose error this module quantifies and replaces.

Floquet theory writes each solution as \(\mathbf x(t) = e^{\lambda t}\mathbf p(t)\) with \(\mathbf p\) periodic; the Floquet exponents \(\lambda\) are the true modal eigenvalues of the periodic system and \(\mathbf p(t) = \sum_m \mathbf p_m e^{i m \Omega t}\) carries the harmonic mode-shape components. Two independent routes are provided:

  • Hill’s method (floquet_modes()): expand \(\mathbf A(\psi) = \sum_n \mathbf A_n e^{i n\psi}\) and solve the eigenproblem of the Hill matrix with blocks \(\mathbf H_{kj} = \mathbf A_{k-j} - i k\Omega\,\delta_{kj}\mathbf I\); the principal exponents are the eigenvalues whose eigenvectors concentrate in the fundamental (\(m=0\)) harmonic, and the blocks give \(\mathbf p_m\).

  • The monodromy matrix (monodromy_exponents()): integrate the state-transition matrix over one period (piecewise-constant / matrix-exponential product); its eigenvalues are the Floquet multipliers \(\mu\), and \(\lambda = \ln\mu / T\). An independent cross-check of Hill.

For an isotropic rotor (every \(\mathbf A(\psi)\) equal) both reduce to the ordinary eigenvalues of that constant matrix, so the Floquet result degrades gracefully to the averaged-model answer; the gap between them is the anisotropy error (averaging_error()).

HILL_UNIFORM_RTOL = 0.001

Relative tolerance on the azimuth intervals for Hill’s method. The Fourier coefficients of A(psi) come from the equal-weight discrete transform, which is exact only on a uniform grid, so every interval must equal the mean interval to this fraction of it (plus a 1e-9 rad absolute floor).

class FloquetSolution(exponents, natural_frequencies_hz, damping_ratios, harmonics, harmonic_mode_shapes, rotor_speed)[source]

Bases: object

The Floquet modes of a linear time-periodic system.

Parameters:
  • exponents (npt.NDArray[np.complex128]) – The principal Floquet exponents \(\lambda\) (one per state), sorted by ascending natural frequency. Selected as the Hill eigenvalues that concentrate in the fundamental (\(m=0\)) harmonic, one per distinct Floquet solution (harmonic-shifted copies \(\lambda + ik\Omega\) of a kept exponent are skipped), so a near-time-invariant system recovers the ordinary eigenvalues unfolded (constant \(A\) gives eig(A) exactly) — unlike monodromy_exponents(), whose imaginary part is known only modulo \(\Omega\).

  • natural_frequencies_hz (npt.NDArray[np.float64]) – |lambda| / 2pi for each exponent.

  • damping_ratios (npt.NDArray[np.float64]) – -Re(lambda) / |lambda| for each exponent.

  • harmonics (npt.NDArray[np.int]) – The harmonic indices m of the mode-shape blocks, from -n_harmonics to +n_harmonics.

  • harmonic_mode_shapes (npt.NDArray[np.complex128]) – The periodic mode-shape components p_m, shape (n_harmonics_total, n_dof, n_modes); [harmonics == 0] is the fundamental (mean) mode shape.

  • rotor_speed (float) – The rotor speed \(\Omega\) (rad/s) used.

exponents: npt.NDArray[np.complex128]
natural_frequencies_hz: npt.NDArray[np.float64]
damping_ratios: npt.NDArray[np.float64]
harmonics: npt.NDArray[np.int_]
harmonic_mode_shapes: npt.NDArray[np.complex128]
rotor_speed: float
averaging_error(per_azimuth_a, azimuths, rotor_speed, *, n_harmonics=4)[source]

Return the azimuth-averaging error: max frequency gap (Hz) Floquet vs averaged.

Compares the Floquet natural frequencies to those of the azimuth-averaged model (the eigenvalues of the mean of per_azimuth_a, which is what the averaged-LTI / MBC path uses). Zero for an isotropic rotor; it grows with anisotropy — the quantity that says when the averaged Campbell diagram is untrustworthy.

Parameters:
Returns:

float – The maximum absolute difference (Hz) between matched Floquet and averaged natural frequencies.

Raises:

ValueError – As floquet_modes(); in particular the azimuths must be near-uniform (relative HILL_UNIFORM_RTOL), because the Floquet frequencies come from Hill’s method.

Return type:

float

floquet_modes(per_azimuth_a, azimuths, rotor_speed, *, n_harmonics=4)[source]

Compute the Floquet modes of an LTP system by Hill’s harmonic-balance method.

Parameters:
  • per_azimuth_a (Sequence[npt.NDArray[np.float64]]) – The frozen-time state matrices \(\mathbf A(\psi_k)\), one per azimuth (e.g. MBCResult.per_azimuth_a with retain_per_azimuth=True).

  • azimuths (Sequence[float]) – The rotor azimuth (rad) of each matrix, spanning one revolution.

  • rotor_speed (float) – The rotor speed \(\Omega\) (rad/s); must be positive.

  • n_harmonics (int, optional) – Harmonic truncation order M; the Hill blocks need Fourier coefficients up to \(2M\), so the near-uniform azimuth sweep (see Notes) must have len(per_azimuth_a) >= 4*n_harmonics + 1 samples so those coefficients are resolved without aliasing.

Returns:

FloquetSolution – The principal Floquet exponents, their frequencies/damping, and the harmonic mode-shape components.

Raises:

ValueError – If rotor_speed is not positive, the matrices are not square and consistent, the azimuth sweep is not near-uniform (see Notes; the message names the worst interval and its relative deviation), or it has too few samples to resolve the Hill coefficients for n_harmonics without aliasing.

Warns:

RuntimeWarning – If the selected exponents do not reproduce the monodromy multipliers (\(e^{\lambda T}\), compared modulo \(i\Omega\)) to within \(10^{-2}\Omega\) — the harmonic truncation or azimuth sampling is too coarse for this system.

Return type:

FloquetSolution

Notes

Limitation: near-uniform azimuths only. The Fourier coefficients of \(\mathbf A(\psi)\) are computed with the equal-weight discrete transform, which is exact only on a uniform grid. Every azimuth interval, including the one that closes the revolution, must equal the mean interval within a relative HILL_UNIFORM_RTOL (1e-3); otherwise ValueError is raised. OpenFAST linearizes at the time steps nearest the requested azimuths, so a delivered grid is uneven unless the time step is commensurate with the azimuth spacing (commensurate_time_step()) and the rotor runs at its trimmed speed. Analyse an uneven delivered grid with monodromy_exponents(), which weights each matrix by its own interval and accepts any distinct azimuths (its frequencies are known only modulo \(\Omega\)). averaging_error() calls this function and has the same restriction.

Every Floquet solution appears in the Hill spectrum as a family of copies \(\lambda + ik\Omega\). Exactly one representative per distinct solution is kept: candidates are walked in descending fundamental (\(m=0\)) energy and a candidate that is a harmonic-shifted copy of an already-kept exponent (same exponent modulo \(i\Omega\) and the same eigenvector shifted by \(k\) blocks) is skipped. Inside a parametric-resonance tongue (real negative multipliers, \(\operatorname{Im}\lambda \equiv \Omega/2\)) the conjugate of an exponent is such a copy, so both tongue families — including an unstable one — are kept; distinct solutions sharing a multiplier (an isotropic mode at exactly \(\Omega/2\)) are both kept.

monodromy_exponents(per_azimuth_a, azimuths, rotor_speed)[source]

Return the Floquet exponents via the monodromy matrix (independent of Hill).

Integrates the state-transition matrix over one revolution as a product of matrix exponentials over the (piecewise-constant) azimuth intervals; the eigenvalues of that monodromy matrix are the Floquet multipliers \(\mu\), and the exponents are \(\ln\mu / T\).

Note

The multiplier \(\mu = e^{\lambda T}\) is invariant under \(\lambda \mapsto \lambda + i k\Omega\), so this matrix determines the exponent’s real part (damping/growth rate) exactly but its imaginary part (frequency) only modulo \(\Omega\): the principal branch of the logarithm folds it into the fundamental strip \((-\Omega/2, \Omega/2]\). A mode whose damped frequency exceeds \(\Omega/2\) is therefore reported aliased (e.g. a 0.8 Hz mode with a 1 Hz rotor comes back at 0.2 Hz). Use floquet_modes() (Hill) for the unfolded frequencies; this helper is an exact cross-check of the damping and of frequencies within the strip.

Parameters:
  • per_azimuth_a (Sequence[npt.NDArray[np.float64]]) – The frozen-time state matrices, one per azimuth.

  • azimuths (Sequence[float]) – The rotor azimuth (rad) of each matrix.

  • rotor_speed (float) – The rotor speed \(\Omega\) (rad/s); must be positive.

Returns:

npt.NDArray[np.complex128] – The Floquet exponents (real part exact; imaginary part folded into the principal strip \((-\Omega/2, \Omega/2]\)), sorted by natural frequency.

Raises:

ValueError – If rotor_speed is not positive and finite, the matrices are non-finite or inconsistently shaped, or the azimuths are non-finite or not distinct (after wrapping into [0, 2pi)).

Return type:

npt.NDArray[np.complex128]

Azimuth averaging

See Azimuth averaging for the workflow.

Azimuth averaging on delivered grids: quadrature, harmonics and the azimuth state.

The azimuth-averaged (Coleman/MBC) model of a rotating turbine is a weighted mean of the fixed-frame state matrices \(\mathbf A(\psi_\ell)\) sampled at the azimuths a linearization actually delivered. Those azimuths are rarely uniform: each snapshot is taken at the first time step at or past its target azimuth. This module provides the building blocks for treating that mean exactly and for auditing it.

  • Quadrature. interval_weights() gives the weights of the periodic piecewise-linear interpolant of the samples (the exact mean of that interpolant), and weight_kernel() their discrete Fourier transform \(W(h)=\sum_\ell w_\ell e^{\,jh\psi_\ell}\), the aliasing kernel of the grid.

  • Harmonics. fit_harmonics() represents the samples by a harmonic model \(\mathbf A(\psi)=\sum_h \mathbf A_h e^{\,jh\psi}\). With the default interpolating trigonometric fit the error of any weighted mean of the samples is exactly \(\mathbf E=\sum_h[W_S(h)-W_{\mathrm{ref}}(h)]\mathbf A_h\) (grid_error_matrix()).

  • The rotor-azimuth state. locate_azimuth_state() finds the ElastoDyn generator-azimuth position (DOF_GeAz) and rotor-speed states from the state descriptions. For a linearization about a periodic orbit the time derivative of that orbit (the orbit tangent, whose azimuth component is one) solves the linearized equations, so one Floquet exponent is exactly zero. azimuth_consistency() tests this from the averaged model alone: the averaged azimuth eigenvalue plus its second-order sideband correction (sideband_correction()) must vanish.

  • Azimuth treatments. pin_azimuth() replaces each sample by \(\mathbf A(\psi)(\mathbf I-\boldsymbol\xi\mathbf e_{\mathrm{az}}^T)\), which makes the tangent \(\boldsymbol\xi\) an exact null vector of every sample and, when \(\boldsymbol\xi\) is the right eigenvector of the reference mean (azimuth_tangent()), leaves every other reference eigenvalue unchanged. The simpler zero_azimuth_column() and delete_azimuth_state() are kept for comparison; they also remove the zero-eigenvalue artefact but bias the reference, because the mean of the azimuth column is physical (the rotation of the gravity-sagged rotor, cancelled in an exact linearization by the cyclic blade coupling).

The per-branch error budget built on these pieces is in vane.modal.averaging_error_estimate. Everything here is pure NumPy/SciPy and takes azimuths in radians.

class AzimuthConsistency(eigenvalue, correction, exponent, index, azimuth_share, condition, stiffness, speed_damping, tolerance, consistent, pair_indices, pair_eigenvalues, pair_exponents, residual_exponent, residual_stiffness, absolute_consistent, scale_tolerance, branch_risks, assessment)[source]

Bases: object

Result of the azimuth (periodic-orbit) consistency test.

The azimuth position and rotor-speed states form a pair of modes, condensed \(s^2-bs-\kappa=0\). Both modes are tested: their sideband-corrected exponents \(\gamma_{1,2}\) estimate the two Floquet exponents, one of which is exactly zero for a linearization about a periodic orbit.

Parameters:
  • eigenvalue (complex) – The averaged eigenvalue \(\lambda\) of the growing mode of the pair, the one whose exponent has the larger real part (1/s).

  • correction (complex) – Its sideband correction \(\widehat{\Delta\lambda}\).

  • exponent (complex) – \(\gamma_1=\lambda+\widehat{\Delta\lambda}\), the growing exponent of the pair. In a saddle (\(\kappa>0\)) this is the unstable root, not the stable one, whatever their azimuth shares.

  • index (int) – Index of that mode in the eigenbasis of the reference mean.

  • azimuth_share (float) – Participation share of the azimuth position state in that mode.

  • condition (float) – Eigenvalue condition number of that mode (large for the near-Jordan pair).

  • stiffness (float) – \(\kappa\), the azimuth stiffness of the static condensation of the reference mean onto (azimuth, rotor speed); nan without the speed state.

  • speed_damping (float) – \(b\), the rotor-speed damping of that condensation (1/s); nan without the speed state.

  • tolerance (float) – Absolute tolerance on the residual exponent (1/s), kept for comparison.

  • consistent (bool) – assessment == "consistent".

  • pair_indices (tuple[int, int]) – Indices of the two modes of the pair, growing first.

  • pair_eigenvalues (tuple[complex, complex]) – Their averaged eigenvalues.

  • pair_exponents (tuple[complex, complex]) – Their sideband-corrected exponents \((\gamma_1,\gamma_2)\), \(\operatorname{Re}\gamma_1\ge\operatorname{Re}\gamma_2\).

  • residual_exponent (float) – \(r=\min(|\gamma_1|,|\gamma_2|)\), the distance of the pair from an exact zero exponent (1/s).

  • residual_stiffness (float) – \(\kappa_r=-\operatorname{Re}(\gamma_1\gamma_2)\), the azimuth stiffness implied by the exponents (1/s^2); zero for a consistent linearization.

  • absolute_consistent (bool) – residual_exponent <= tolerance: a fixed absolute-tolerance test.

  • scale_tolerance (float) – The dimensionless tolerance :math:` au` of the scaled test.

  • branch_risks (tuple[BranchAzimuthRisk, ...]) – The decay and coalescence ratios of every assessed branch.

  • assessment (str) – "consistent", "inconsistent-but-harmless" or "inconsistent-and-risky" (see azimuth_consistency()).

eigenvalue: complex
correction: complex
exponent: complex
index: int
azimuth_share: float
condition: float
stiffness: float
speed_damping: float
tolerance: float
consistent: bool
pair_indices: tuple[int, int]
pair_eigenvalues: tuple[complex, complex]
pair_exponents: tuple[complex, complex]
residual_exponent: float
residual_stiffness: float
absolute_consistent: bool
scale_tolerance: float
branch_risks: tuple[BranchAzimuthRisk, ...]
assessment: Literal['consistent', 'inconsistent-but-harmless', 'inconsistent-and-risky']
property growth_rate: float

Return \(\operatorname{Re}\gamma_1\), the largest real part of the pair.

With an open-loop rotor whose aerodynamic torque rises with speed (\(b>0\)) this is the physical rotor-speed divergence, not a defect; the assessment therefore uses the residual, not the growth rate.

property speed_damping_exponent: float

Return \(b_\gamma=\operatorname{Re}(\gamma_1+\gamma_2)\) (1/s).

property pendulum_frequency: float

Return the azimuth pendulum frequency \(\sqrt{-\kappa-b^2/4}\) (rad/s).

A negative stiffness, for example created by aliasing on a coarse grid, makes the azimuth an oscillator that can hybridise with a platform branch near this frequency. Zero for a real pair or an unknown condensation.

property residual_pendulum_frequency: float

Return \(|\operatorname{Im}\gamma_1|\), the residual pair frequency.

Non-zero only when the residual makes the azimuth pair oscillatory.

property worst_branch: BranchAzimuthRisk | None

Return the assessed branch with the largest coalescence ratio.

Returns:

BranchAzimuthRisk or None – None when no branch was assessed.

class AzimuthStates(position, speed=None)[source]

Bases: object

Positions of the rotor-azimuth position and rotor-speed states.

Parameters:
  • position (int) – 0-based index of the rotor-azimuth position state (ElastoDyn DOF_GeAz).

  • speed (int or None, optional) – 0-based index of its rate, the rotor-speed state, when known.

position: int
speed: int | None = None
class BranchAzimuthRisk(index, eigenvalue, decay_ratio, coalescence_ratio, risky)[source]

Bases: object

How strongly the azimuth residual can contaminate one branch.

Parameters:
  • index (int) – Index of the branch in the eigenbasis of the reference mean.

  • eigenvalue (complex) – Its reference eigenvalue \(\lambda_j=-\sigma_j+j\omega_j\) (1/s).

  • decay_ratio (float) – \(\nu_j=r/\sigma_j\), the residual exponent over the branch decay rate (inf for a branch that does not decay).

  • coalescence_ratio (float) – \(\mu_j=|\kappa_r|/(\omega_j^2+b_\gamma^2/4)\), the residual azimuth stiffness over the coalescence threshold of the branch.

  • risky (bool) – Both ratios exceed the scale tolerance.

index: int
eigenvalue: complex
decay_ratio: float
coalescence_ratio: float
risky: bool
class EigenBasis(values, left, right, condition)[source]

Bases: object

Biorthonormal eigentriples of a state matrix.

Parameters:
  • values (npt.NDArray[np.complex128]) – Every eigenvalue \(\lambda_k\) (conjugates included), in solver order.

  • left (npt.NDArray[np.complex128]) – Left eigenvectors as columns, scaled so that \(\mathbf u_k^H\mathbf v_k=1\).

  • right (npt.NDArray[np.complex128]) – Unit-norm right eigenvectors as columns.

  • condition (npt.NDArray[np.float64]) – Eigenvalue condition numbers \(1/|\hat{\mathbf u}_k^H\mathbf v_k|\) of the unit-norm pair.

values: npt.NDArray[np.complex128]
left: npt.NDArray[np.complex128]
right: npt.NDArray[np.complex128]
condition: npt.NDArray[np.float64]
participation()[source]

Return the participation magnitudes \(|\bar u_{sk}v_{sk}|\).

Returns:

npt.NDArray[np.float64] – Shape (n_states, n_modes).

Return type:

npt.NDArray[np.float64]

nearest(eigenvalue)[source]

Return the index of the eigenvalue closest to eigenvalue.

Parameters:

eigenvalue (complex) – Target eigenvalue (1/s).

Returns:

int – Index into values.

Return type:

int

class HarmonicModel(azimuths, reference_weights, reference_mean, coefficients, method, interpolating, nyquist_order=None, blades=None)[source]

Bases: object

Harmonic representation \(\mathbf A(\psi)=\sum_h\mathbf A_h e^{jh\psi}\).

Parameters:
  • azimuths (npt.NDArray[np.float64]) – The delivered azimuths (rad, wrapped into [0, 2pi)) the model was fitted to.

  • reference_weights (npt.NDArray[np.float64]) – The interval weights of those azimuths (the reference quadrature rule).

  • reference_mean (npt.NDArray[np.float64]) – The reference mean \(\sum_\ell w_\ell^{\mathrm{ref}}\mathbf A(\psi_\ell)\) computed from the samples.

  • coefficients (dict[int, npt.NDArray[np.complex128]]) – \(\mathbf A_h\) for every order present, with \(\mathbf A_{-h}=\overline{\mathbf A_h}\).

  • method (str) – "trigonometric" (least-squares trigonometric series) or "piecewise_linear" (exact Fourier coefficients of the periodic piecewise-linear interpolant).

  • interpolating (bool) – Whether the model reproduces every sample exactly (a square trigonometric design). Only an interpolating model makes grid_error_matrix() exact.

  • nyquist_order (int or None) – The order represented by its cosine only (half the sample count of an interpolating fit to an even number of samples), else None.

  • blades (int or None) – When set, only orders that are multiples of blades were fitted.

azimuths: npt.NDArray[np.float64]
reference_weights: npt.NDArray[np.float64]
reference_mean: npt.NDArray[np.float64]
coefficients: dict[int, npt.NDArray[np.complex128]]
method: str
interpolating: bool
nyquist_order: int | None = None
blades: int | None = None
property orders: npt.NDArray[np.int_]

Return every order present (negative and positive), ascending.

property max_order: int

Return the highest order present (including a cosine-only Nyquist order).

property resolved_order: int

Return the highest order whose cosine and sine are both resolved.

property mean: npt.NDArray[np.float64]

Return the zeroth harmonic \(\mathbf A_0\) (real).

evaluate(azimuths)[source]

Evaluate the model at the given azimuths.

Parameters:

azimuths (npt.ArrayLike) – Azimuths in radians (any finite values).

Returns:

npt.NDArray[np.float64] – The model matrices, shape (n, *sample_shape).

Return type:

npt.NDArray[np.float64]

uniform_mean(count, start=0.0)[source]

Return the model mean over a uniform count-grid starting at start.

Parameters:
  • count (int) – Number of uniform azimuths.

  • start (float, optional) – First azimuth in radians.

Returns:

npt.NDArray[np.float64] – \(\mathbf A_0+\sum_{h\neq0,\ N\mid h}\mathbf A_h e^{jh\psi_0}\), the exact equal-weight mean of the model on that grid.

Return type:

npt.NDArray[np.float64]

class TowerPassageFit(orders, magnitudes, decay, amplitude, log_r2, condensed)[source]

Bases: object

Fit of the tower-passage law to the azimuth-column harmonics.

Parameters:
  • orders (npt.NDArray[np.int]) – The fitted orders (resolved multiples of the blade count).

  • magnitudes (npt.NDArray[np.float64]) – \(|c_h|\) at those orders.

  • decay (float) – \(\rho\), the fitted decay per harmonic order.

  • amplitude (float) – \(A_B\), the fitted magnitude at the blade order.

  • log_r2 (float) – Coefficient of determination of the fit in \(\log|c_h|\).

  • condensed (bool) – Whether \(c_h\) is the condensed azimuth stiffness harmonic \(\boldsymbol\ell^T\mathbf A_h\boldsymbol\xi\) (rotor-speed state known) or the azimuth-column norm \(\|\mathbf A_h\mathbf e_{\mathrm{az}}\|\).

orders: npt.NDArray[np.int_]
magnitudes: npt.NDArray[np.float64]
decay: float
amplitude: float
log_r2: float
condensed: bool
azimuth_consistency(model, rotor_speed, azimuth, *, tolerance=0.0001, basis=None, max_order=None, branches=None, scale_tolerance=0.01)[source]

Test whether the samples are a linearization about a periodic orbit.

Parameters:
  • model (HarmonicModel) – Harmonic model of the samples ("piecewise_linear" recommended).

  • rotor_speed (float) – Rotor speed \(\Omega\) in rad/s.

  • azimuth (AzimuthStates or int) – The rotor-azimuth states (see locate_azimuth_state()). The rotor-speed index is needed for the condensed stiffness and damping.

  • tolerance (float, optional) – Absolute tolerance on the residual exponent in 1/s, reported as absolute_consistent. Default 1e-4. It decides the assessment only when no branch is assessed.

  • basis (EigenBasis or None, optional) – Eigentriples of model.reference_mean if already computed.

  • max_order (int or None, optional) – Highest harmonic order in the sideband sum; default every order.

  • branches (Sequence[int] or None, optional) – Indices (into the eigenbasis of the reference mean) of the branches the residual is assessed against. Default every oscillatory mode (positive imaginary part) outside the pair.

  • scale_tolerance (float, optional) – Dimensionless tolerance \(\tau\) of the decay and coalescence ratios. Default 0.01. estimate_averaging_error() uses its damping tolerance, as a fraction.

Returns:

AzimuthConsistency – Both exponents of the azimuth/speed pair (the growing one first), the residual exponent and stiffness, the per-branch ratios and the graded assessment.

Raises:

ValueError – If the model is not of square matrices, rotor_speed, tolerance or scale_tolerance is invalid, or an index is out of range.

Return type:

AzimuthConsistency

Notes

The orbit tangent of an autonomous system linearized about a periodic orbit is a periodic solution of the linear time-periodic model, so one Floquet exponent is exactly zero. The averaged model then has an eigenvalue of second order in the harmonics whose sideband correction cancels it. Both modes of the azimuth/rotor-speed pair are corrected, giving \(\gamma_{1,2}\). In the condensation \(s^2-bs-\kappa=0\) their sum is the speed damping \(b_\gamma\) and their product is \(-\kappa_r\), so the defect is the residual stiffness \(\kappa_r=-\gamma_1\gamma_2\) (zero when one root vanishes) and the residual exponent \(r=\min|\gamma_{1,2}|\). A positive \(b\) with \(\kappa_r=0\) is the physical open-loop rotor-speed divergence and is not a defect.

The residual matters only for the branches it can reach. For a branch \(\lambda_j=-\sigma_j+j\omega_j\) two ratios are formed,

\[\nu_j=\frac{r}{\sigma_j},\qquad \mu_j=\frac{|\kappa_r|}{\omega_j^2+b_\gamma^2/4}.\]

\(\nu_j\le\tau\) means the residual root is slower than the branch decay by \(1/\tau\): even fully mixed into the branch it would change the decay rate by at most \(\tau\sigma_j\). \(\mu_j\) is the coalescence condition of the two-state azimuth model: the azimuth pendulum, of decay rate \(|b|/2\), can be tuned to the branch frequency only when the azimuth stiffness reaches \(\omega_j^2+b^2/4\). \(\mu_j\) is the share of that threshold the residual consumes on its own, and \(\sqrt{\mu_j}\) is the natural frequency of the residual pair relative to it. The assessment is "consistent" when \(\nu_j\le\tau\) for every branch; "inconsistent-and-risky" when some branch has both \(\nu_j>\tau\) and \(\mu_j>\tau\) (the residual is neither slow against the branch decay nor a decade below it in frequency, for \(\tau=0.01\)); "inconsistent-but-harmless" otherwise. Without branches the absolute test decides between the first and the last. The test detects but does not locate the defect (for example a trim controller still acting during the linearization revolution).

azimuth_mean(azimuths, matrices, *, rule='interval')[source]

Return the azimuth mean of a stack of sampled matrices.

Parameters:
  • azimuths (npt.ArrayLike) – Delivered azimuths in radians, one per sample.

  • matrices (npt.ArrayLike) – Stack of samples, shape (n_azimuth, ...) (state, input or output matrices).

  • rule ({"interval", "arithmetic"}, optional) – "interval" (default) uses interval_weights(); "arithmetic" the plain sample mean that the MBC transform applies.

Returns:

npt.NDArray[np.float64] – The weighted mean, with the shape of one sample.

Raises:

ValueError – If the inputs are inconsistent or the rule is unknown.

Return type:

npt.NDArray[np.float64]

azimuth_tangent(mean_matrix, azimuth, *, n_samples=None)[source]

Return the orbit-tangent estimate \(\boldsymbol\xi\) from an averaged model.

Parameters:
  • mean_matrix (npt.ArrayLike) – The reference (azimuth-averaged) state matrix of a dense grid.

  • azimuth (AzimuthStates or int) – The rotor-azimuth position state.

  • n_samples (int or None, optional) – Number of azimuths that formed the mean. Below 18 a warning is issued: the neutral direction of a coarse mean is itself contaminated by aliasing.

Returns:

npt.NDArray[np.float64] – The real right eigenvector of the mode with the largest azimuth-state share, normalised to \(\xi_{\mathrm{az}}=1\).

Raises:

ValueError – If the matrix is invalid or the azimuth index is out of range.

Warns:

RuntimeWarning – If n_samples is below 18, or the azimuth eigenvalue is complex (an oscillatory azimuth pair; the real part of its eigenvector is returned and pinning then preserves the reference only to the size of its imaginary part).

Return type:

npt.NDArray[np.float64]

delete_azimuth_state(matrices, azimuth)[source]

Delete the rotor-azimuth position state (row and column).

Parameters:
  • matrices (npt.ArrayLike) – One state matrix (n, n) or a stack (n_azimuth, n, n).

  • azimuth (AzimuthStates or int) – The rotor-azimuth position state.

Returns:

npt.NDArray[np.float64] – Matrices of size n - 1. The rotor-speed state is kept as a first-order state, so the [q2, q2dot, q1] pairing no longer holds.

Raises:

ValueError – If the matrices are not square or the index is out of range.

Return type:

npt.NDArray[np.float64]

Notes

This is the conventional Campbell practice of dropping the zero-frequency generator mode. Its eigenvalues (and Floquet multipliers) are exactly those of zero_azimuth_column() without the zero, because a zero column makes the matrix block-triangular. It therefore biases the reference in the same way; see zero_azimuth_column().

eigen_basis(matrix)[source]

Return the biorthonormal eigentriples of a square state matrix.

Parameters:

matrix (npt.ArrayLike) – Square, finite state matrix.

Returns:

EigenBasis – Eigenvalues with left and right eigenvectors, u_k^H v_k = 1.

Raises:

ValueError – If the matrix is not square and finite, or an eigenvalue is defective (its left and right eigenvectors are orthogonal to working precision).

Return type:

EigenBasis

fit_harmonics(azimuths, matrices, *, method='trigonometric', max_order=None, blades=None)[source]

Fit a harmonic model \(\mathbf A(\psi)=\sum_h\mathbf A_h e^{jh\psi}\).

Parameters:
  • azimuths (npt.ArrayLike) – Delivered azimuths in radians, one per sample (any order).

  • matrices (npt.ArrayLike) – Sample stack, shape (n_azimuth, ...).

  • method ({"trigonometric", "piecewise_linear"}, optional) – "trigonometric" (default) solves the trigonometric design by least squares. With the default max_order it is square, orders 0..(M-1)//2 plus the cosine of order M/2 for an even sample count M, so the model interpolates every sample and the sampling-error identity of grid_error_matrix() holds exactly. "piecewise_linear" returns the exact Fourier coefficients of the periodic piecewise-linear interpolant of the samples; its \(\mathbf A_0\) equals the interval-weighted mean, which makes it the natural model for the sideband (periodicity) estimate.

  • max_order (int or None, optional) – Highest harmonic order. Default (M-1)//2 (with the Nyquist cosine for the trigonometric fit to an even M). A trigonometric fit with 2*max_order + 1 > M is rejected.

  • blades (int or None, optional) – Keep only orders that are multiples of blades (the content an isotropic blades-bladed rotor leaves after MBC). The trigonometric fit is then a least-squares fit and does not interpolate.

Returns:

HarmonicModel – The fitted model, with the reference (interval) weights and mean.

Raises:

ValueError – If the inputs are inconsistent, method is unknown, max_order or blades is not a positive integer, the design has more unknowns than samples, or it is ill-conditioned.

Return type:

HarmonicModel

grid_error_matrix(model, azimuths, weights=None)[source]

Return the error of a grid’s mean relative to the reference mean.

Parameters:
  • model (HarmonicModel) – Harmonic model of the delivered samples.

  • azimuths (npt.ArrayLike) – The azimuths (rad) of the grid whose mean is assessed: a subset of the delivered azimuths (a measured sub-grid) or any hypothetical grid (a design).

  • weights (npt.ArrayLike or None, optional) – The grid’s quadrature weights; default its interval_weights().

Returns:

npt.NDArray[np.float64] – \(\mathbf E=\sum_h[W_{\mathrm{grid}}(h)-W_{\mathrm{ref}}(h)]\mathbf A_h\).

Return type:

npt.NDArray[np.float64]

Notes

Any weighted mean of the samples is \(\sum_h W(h)\mathbf A_h\) exactly when the model interpolates the samples, so for a sub-grid of the delivered azimuths and an interpolating model this equals \(\sum_\ell(w_\ell^{S}-w_\ell^{\mathrm{ref}}) \mathbf A(\psi_\ell)\) to rounding. The zeroth harmonic cancels because \(W(0)=1\) for every consistent rule. On a uniform \(N\)-grid \(W(h)\) vanishes unless \(N\mid h\) (the familiar aliasing rule); on a non-uniform grid it leaks at every order, which this form keeps. For a grid that is not a subset of the samples the result is a model-based prediction that ignores orders above HarmonicModel.max_order.

interval_weights(azimuths)[source]

Return the interval (periodic trapezoidal) weights of an azimuth grid.

Parameters:

azimuths (npt.ArrayLike) – Delivered azimuths in radians, any order, distinct modulo \(2\pi\).

Returns:

npt.NDArray[np.float64] – One weight per azimuth, in the input order, summing to one. Applied to the samples they give the exact revolution mean of the periodic piecewise-linear interpolant.

Raises:

ValueError – If the azimuths are empty, non-finite, duplicated after wrapping, or leave a gap larger than half a revolution (a single azimuth always does).

Return type:

npt.NDArray[np.float64]

Notes

With the azimuths sorted, \(g_\ell=\psi_{\ell+1}-\psi_\ell\) the periodic gaps and \(g_0 \equiv g_M\),

\[w_\ell = \frac{g_{\ell-1}+g_\ell}{4\pi},\]

which reduces to \(1/M\) on a uniform grid.

locate_azimuth_state(state_descriptions, *, ndof2=None)[source]

Locate the rotor-azimuth position and rotor-speed states from descriptions.

Parameters:
  • state_descriptions (Sequence[str]) – State descriptions in matrix order (e.g. MBCResult.state_descriptions).

  • ndof2 (int or None, optional) – Number of second-order DOFs of the [q2, q2dot, q1] layout. Used to place the rotor-speed state at position + ndof2 when no velocity description of the generator DOF is present.

Returns:

AzimuthStates – The position index and, when found, the speed index.

Raises:

ValueError – If no state, or more than one non-velocity state, carries the generator azimuth token DOF_GeAz, or ndof2 is inconsistent with the layout.

Return type:

AzimuthStates

pin_azimuth(matrices, tangent, azimuth)[source]

Apply tangent-pinned averaging to one matrix or a stack of samples.

Each matrix is replaced by \(\mathbf A'=\mathbf A(\mathbf I-\boldsymbol\xi\mathbf e_{\mathrm{az}}^T)\).

Parameters:
  • matrices (npt.ArrayLike) – One state matrix (n, n) or a stack (n_azimuth, n, n).

  • tangent (npt.ArrayLike) – Tangent \(\boldsymbol\xi\) of length n; it is rescaled so that its azimuth component is one.

  • azimuth (AzimuthStates or int) – The rotor-azimuth position state.

Returns:

npt.NDArray[np.float64] – The pinned matrices, same shape as matrices. Only the azimuth column changes, to \(\mathbf A\mathbf e_{\mathrm{az}}-\mathbf A\boldsymbol\xi\).

Raises:

ValueError – If shapes disagree, the inputs are non-finite, or the tangent’s azimuth component is zero.

Return type:

npt.NDArray[np.float64]

Notes

\(\mathbf I-\boldsymbol\xi\mathbf e_{\mathrm{az}}^T\) is an oblique projector, so \(\mathbf A'(\psi)\boldsymbol\xi=\mathbf 0\) for every sample: every sub-grid mean has an exact zero eigenvalue and the periodic model an exact unit Floquet multiplier. If \(\boldsymbol\xi\) is a right eigenvector of the reference mean with eigenvalue \(\lambda_{\mathrm{az}}\), the pinned mean is \(\bar{\mathbf A}-\lambda_{\mathrm{az}}\boldsymbol\xi\mathbf e_{\mathrm{az}}^T\) and, by Brauer’s theorem, keeps every other reference eigenvalue unchanged.

sideband_correction(model, basis, index, rotor_speed, *, max_order=None)[source]

Return the second-order sideband (periodicity) correction of one mode.

Parameters:
  • model (HarmonicModel) – Harmonic model of the samples (the "piecewise_linear" fit is consistent with the interval-weighted reference).

  • basis (EigenBasis) – Eigentriples of the reference mean.

  • index (int) – Mode index into basis.values.

  • rotor_speed (float) – Rotor speed \(\Omega\) in rad/s.

  • max_order (int or None, optional) – Highest harmonic order included; default every order of the model.

Returns:

complex – \(\widehat{\Delta\lambda}_j=\sum_{h\neq0}\sum_k \frac{(\mathbf u_j^H\mathbf A_{-h}\mathbf v_k) (\mathbf u_k^H\mathbf A_h\mathbf v_j)}{\lambda_j+jh\Omega-\lambda_k}\), the estimate of the Floquet exponent minus the averaged eigenvalue. Every mode is retained in the sum: truncating it to the modes nearest in detuning is not admissible.

Raises:

ValueError – If rotor_speed is not positive or index is out of range.

Return type:

complex

subgrid_weights(azimuths, indices, *, rule='interval')[source]

Return full-length weights of a sub-grid of the delivered azimuths.

Parameters:
  • azimuths (npt.ArrayLike) – All delivered azimuths in radians.

  • indices (Sequence[int] or npt.ArrayLike) – Positions (into azimuths) of the samples that form the sub-grid.

  • rule ({"interval", "arithmetic"}, optional) – "interval" weights the sub-grid by its own interval_weights(); "arithmetic" gives each of its samples 1/len(indices).

Returns:

npt.NDArray[np.float64] – A weight vector aligned with azimuths, zero off the sub-grid, summing to one.

Raises:

ValueError – If indices is empty, out of range or repeated, or rule is unknown, or the sub-grid is invalid for interval_weights().

Return type:

npt.NDArray[np.float64]

tower_passage_decay(model, azimuth, *, blades=3)[source]

Fit the tower-passage law to the harmonics of the azimuth column.

Parameters:
  • model (HarmonicModel) – Harmonic model of the delivered samples (the azimuth column as delivered, not pinned or zeroed).

  • azimuth (AzimuthStates or int) – The rotor-azimuth states. With the rotor-speed state the condensed harmonics \(c_h=\boldsymbol\ell^T\mathbf A_h\boldsymbol\xi\) are fitted, otherwise the column norms \(\|\mathbf A_h\mathbf e_{\mathrm{az}}\|\).

  • blades (int, optional) – Blade count \(B\); only its multiples are fitted. Default 3.

Returns:

TowerPassageFit – \(|c_h|=A_B\,(h/B)\,\rho^{\,h-B}\) fitted by least squares in \(\log\) over the resolved multiples of \(B\).

Raises:

ValueError – If the model is not of square matrices, blades is not a positive integer, the condensation is singular, or fewer than two blade multiples are resolved with non-zero content.

Return type:

TowerPassageFit

Notes

A tower passage of peak deficit \(\delta\) and harmonic decay \(\rho\) makes the frozen-azimuth derivative of the rotor loads, which is the azimuth column, carry \(|c_h|\propto h\rho^{h-1}\): the derivative of a localised bump whose Fourier coefficients fall as \(\rho^h\). Every other load harmonic of the rotor comes from the same localised inflow, so \(\rho\) sets the decay above the resolved orders that azimuth_count_table() uses when a branch’s own sensitivity does not decay.

uniform_azimuths(count, start=0.0)[source]

Return a uniform grid of count azimuths (rad) starting at start.

Parameters:
  • count (int) – Number of azimuths; must be at least one.

  • start (float, optional) – First azimuth in radians.

Returns:

npt.NDArray[np.float64] – start + 2 pi k / count for k = 0 .. count - 1, wrapped into [0, 2pi).

Raises:

ValueError – If count is not a positive integer or start is not finite.

Return type:

npt.NDArray[np.float64]

weight_kernel(azimuths, weights, orders)[source]

Return the quadrature-weight DFT kernel of an azimuth grid.

The kernel is \(W(h)=\sum_\ell w_\ell e^{jh\psi_\ell}\), the aliasing kernel of the grid.

Parameters:
  • azimuths (npt.ArrayLike) – Azimuths in radians.

  • weights (npt.ArrayLike) – One weight per azimuth.

  • orders (Sequence[int] or npt.ArrayLike) – Harmonic orders \(h\) at which to evaluate the kernel.

Returns:

npt.NDArray[np.complex128] – \(W(h)\) for each requested order. \(W(0)=\sum_\ell w_\ell\). On a uniform \(N\)-grid starting at \(\psi_0\) with equal weights, \(W(h)=e^{jh\psi_0}\) when \(N\mid h\) and zero otherwise.

Raises:

ValueError – If the weights do not match the azimuths or are non-finite.

Return type:

npt.NDArray[np.complex128]

zero_azimuth_column(matrices, azimuth)[source]

Zero the rotor-azimuth column (naive neutralisation, kept for comparison).

Parameters:
  • matrices (npt.ArrayLike) – One state matrix (n, n) or a stack (n_azimuth, n, n).

  • azimuth (AzimuthStates or int) – The rotor-azimuth position state.

Returns:

npt.NDArray[np.float64] – A copy with the azimuth column set to zero.

Raises:

ValueError – If the matrices are not square or the index is out of range.

Return type:

npt.NDArray[np.float64]

Notes

This is pin_azimuth() with \(\boldsymbol\xi=\mathbf e_{\mathrm{az}}\). It restores an exact zero eigenvalue and the perturbative regime of the sampling error, but \(\mathbf e_{\mathrm{az}}\) is not an eigenvector of the mean, so it changes the reference spectrum: the mean of the azimuth column carries the rotation of the steady cyclic blade deflection, which is physical. On archived floating-turbine linearizations it moved platform damping by up to 7 % of its value. Prefer pin_azimuth() with azimuth_tangent() of a dense grid.

Per-branch error estimates of the azimuth-averaged model.

The averaged (Coleman/MBC) analysis of a rotating turbine carries three errors that are usually only guessed at. Each is predicted here from the averaged model and the harmonic content of the samples, without a periodic (Floquet) solution:

  • Sampling. The error of a coarse or hypothetical azimuth grid. Its matrix error is exactly a quadrature-weight error times the harmonic content (vane.modal.averaging.grid_error_matrix()), and its effect on each branch follows from eigenvalue perturbation of the averaged eigensolution: first or second order, or, when the rotor-azimuth state is present, a structured predictor that keeps the azimuth column as an exact rank-one term and the near-Jordan azimuth/rotor-speed modes exactly. The structured predictor also yields a predicted modal assurance against the reference branch, the hybridisation flag.

  • Quadrature. The difference between the arithmetic sample mean (what the MBC transform applies) and the interval-weighted mean on the delivered grid.

  • Periodicity. The second-order sideband estimate of the Floquet exponent minus the averaged eigenvalue (vane.modal.averaging.sideband_correction()).

All terms are reported in the same units: the relative change of the damped frequency \(|\operatorname{Im}\lambda|\) and of the damping ratio \(\zeta=-\operatorname{Re}\lambda/|\lambda|\), in percent of the reference value. estimate_averaging_error() assembles them per branch, together with the azimuth consistency test. azimuth_count_table() designs the azimuth count from the \(\operatorname{lcm}(N, B)\) aliasing rule and the fitted harmonics, and commensurate_time_step() suggests a time step that puts every target azimuth on a whole number of steps.

class AliasTail(model, own_decay, tower_decay, own_level, peak_level, anchor_order, peak_order)[source]

Bases: object

Model of one branch’s harmonic sensitivity above the resolved orders.

Parameters:
  • model (str) – "own decay" (the branch’s own sensitivity peaks inside the resolved multiples of \(B\) and its fitted trend at least halves after), "tower law" (it does not, and the tower-passage decay of the azimuth column is used), "no content" (the sensitivity is rounding, the tail is zero) or "unresolved" (no tail can be justified from the samples).

  • own_decay (float) – Fitted decay per order of the branch’s own sensitivity (nan if unused).

  • tower_decay (float) – Tower-passage decay per order (nan if unused).

  • own_level (float) – Level of the smallest own-decay envelope through every fitted point, at anchor_order (1/s).

  • peak_level (float) – Largest observed sensitivity \(\max_h s_j(h)\) (1/s), the anchor of the tower-law envelope.

  • anchor_order (int) – The highest resolved multiple of \(B\).

  • peak_order (int) – The resolved order of the largest observed sensitivity.

model: str
own_decay: float
tower_decay: float
own_level: float
peak_level: float
anchor_order: int
peak_order: int
envelope(order)[source]

Return the tail estimate of \(s_j(h)\) at an order above the anchor.

Parameters:

order (int) – Harmonic order \(h\).

Returns:

float – The envelope of the tail model, nan when unresolved.

Return type:

float

class AveragingErrorEstimate(branches, azimuth, grid, reference_mean, rotor_speed, method, damping_tolerance_pct, frequency_tolerance_pct, max_condition=1000.0, sampling_status=None, sampling_first_alias=None, reference_resolved_order=None)[source]

Bases: object

Per-branch error estimates of an azimuth-averaged model.

Parameters:
  • branches (dict[str, BranchErrorEstimate]) – The assessed branches by label.

  • azimuth (AzimuthConsistency or None) – The azimuth consistency test, when the rotor-azimuth state was given.

  • grid (GridReport) – Quadrature properties of the delivered grid.

  • reference_mean (npt.NDArray[np.float64]) – The interval-weighted reference mean.

  • rotor_speed (float) – Rotor speed (rad/s).

  • method (str) – Branch predictor used for the sampling and quadrature terms.

  • damping_tolerance_pct (float) – Damping-ratio tolerance, percent of the reference value.

  • frequency_tolerance_pct (float) – Frequency tolerance, percent of the reference value.

  • max_condition (float, optional) – Eigenvalue condition number above which a branch is ill-conditioned.

  • sampling_status (str or None, optional) – "measured" (a sub-grid of the delivered samples), "resolved" (a predicted grid whose first alias order \(\operatorname{lcm}(N,B)\) lies within the resolved orders of the reference), "unresolved: needs denser reference ..." otherwise, None without a grid.

  • sampling_first_alias (int or None, optional) – \(\operatorname{lcm}(N,B)\) of a predicted grid of \(N\) azimuths.

  • reference_resolved_order (int or None, optional) – The highest harmonic order the reference samples resolve.

branches: dict[str, BranchErrorEstimate]
azimuth: AzimuthConsistency | None
grid: GridReport
reference_mean: npt.NDArray[np.float64]
rotor_speed: float
method: str
damping_tolerance_pct: float
frequency_tolerance_pct: float
max_condition: float = 1000.0
sampling_status: str | None = None
sampling_first_alias: int | None = None
reference_resolved_order: int | None = None
property azimuth_assessment: str | None

Return the graded azimuth assessment, None without the azimuth state.

Returns:

str or None – "consistent", "inconsistent-but-harmless" or "inconsistent-and-risky".

property within_tolerance: bool

Return whether every branch is within tolerance and the azimuth is not risky.

A harmless azimuth inconsistency (see azimuth_consistency()) does not void the branch budget; a risky one does.

class AzimuthCountRow(count, first_alias_order, multiple_of_blades, resolved_damping_error_pct, resolved_frequency_error_pct, unresolved_damping_bound_pct, total_damping_bound_pct, min_tracking_mac, hybridised, within_tolerance, resolved_damping_by_branch, unresolved_damping_by_branch, alias_resolved=True, resolved_within_tolerance=False)[source]

Bases: object

Predicted averaging error of one uniform azimuth count.

Parameters:
  • count (int) – Number of uniform azimuths \(N\).

  • first_alias_order (int) – \(\operatorname{lcm}(N, B)\).

  • multiple_of_blades (bool) – \(B \mid N\).

  • resolved_damping_error_pct (float) – Worst damping-ratio error over branches and start azimuths from the resolved harmonics (exact eigenvalues of the model’s uniform mean).

  • resolved_frequency_error_pct (float) – The same for the damped frequency.

  • unresolved_damping_bound_pct (float) – Worst first-order, worst-phase estimate of the aliases above the model’s highest order, from the documented tail model (nan when a branch’s tail is unresolved or extrapolation is off). It is a model-based estimate, not a bound.

  • total_damping_bound_pct (float) – Worst over branches of resolved plus tail estimate (nan when any tail is unresolved).

  • min_tracking_mac (float) – Lowest MAC with which a branch was tracked.

  • hybridised (bool) – Some branch was tracked with a MAC below the hybridisation threshold.

  • within_tolerance (bool) – total_damping_bound_pct is finite and within the tolerance: the extrapolated decision.

  • resolved_damping_by_branch (dict[str, float]) – Worst resolved damping-ratio error per branch.

  • unresolved_damping_by_branch (dict[str, float]) – Tail estimate per branch (nan when unresolved).

  • alias_resolved (bool, optional) – \(\operatorname{lcm}(N,B)\) lies within the orders the model resolves, so the resolved error contains the first alias of the count.

  • resolved_within_tolerance (bool, optional) – alias_resolved, not hybridised and the resolved error within the tolerance: the decision supported by the samples.

count: int
first_alias_order: int
multiple_of_blades: bool
resolved_damping_error_pct: float
resolved_frequency_error_pct: float
unresolved_damping_bound_pct: float
total_damping_bound_pct: float
min_tracking_mac: float
hybridised: bool
within_tolerance: bool
resolved_damping_by_branch: dict[str, float]
unresolved_damping_by_branch: dict[str, float]
alias_resolved: bool = True
resolved_within_tolerance: bool = False
class AzimuthCountTable(rows, recommended, blades, damping_tolerance_pct, extrapolated, recommended_resolved=None, recommended_extrapolated=None, extrapolation_status='not requested', tails=<factory>, resolved_order=0)[source]

Bases: object

Azimuth-count design table and its two recommendations.

Parameters:
  • rows (tuple[AzimuthCountRow, ...]) – One row per evaluated count, ascending.

  • recommended (int or None) – The recommendation supported by the samples, equal to recommended_resolved.

  • blades (int) – Blade count used for the lcm rule.

  • damping_tolerance_pct (float) – The damping-ratio tolerance, percent.

  • extrapolated (bool) – Whether a tail estimate was formed for at least one branch.

  • recommended_resolved (int or None, optional) – The smallest count whose first alias the model resolves and whose resolved error meets the tolerance. The resolved error is exact for the content the samples resolve, so this is the recommended count.

  • recommended_extrapolated (int or None, optional) – The smallest count whose resolved error plus tail estimate meets the tolerance. A model-based estimate, not a bound; None when a tail is unresolved.

  • extrapolation_status (str, optional) – "extrapolated", "not requested" or "unresolved: needs denser linearization ..." naming the branches whose tail cannot be modelled.

  • tails (dict[str, AliasTail], optional) – The tail model of each branch.

  • resolved_order (int, optional) – The highest harmonic order the model resolves.

rows: tuple[AzimuthCountRow, ...]
recommended: int | None
blades: int
damping_tolerance_pct: float
extrapolated: bool
recommended_resolved: int | None = None
recommended_extrapolated: int | None = None
extrapolation_status: str = 'not requested'
tails: dict[str, AliasTail]
resolved_order: int = 0
row(count)[source]

Return the row of one azimuth count.

Parameters:

count (int) – An evaluated azimuth count.

Returns:

AzimuthCountRow – Its row.

Raises:

KeyError – If count was not evaluated.

Return type:

AzimuthCountRow

class BranchErrorEstimate(label, index, eigenvalue, frequency_hz, damping_ratio, sampling, sampling_dominant_order, quadrature, periodicity, hybridised, within_tolerance, condition=nan, well_conditioned=True, sampling_exact=None, mixed=False, sampling_resolved=True)[source]

Bases: object

Error budget of one branch of the averaged model.

Parameters:
  • label (str) – Branch label.

  • index (int) – Index of the branch in the eigenbasis of the reference mean.

  • eigenvalue (complex) – Reference eigenvalue (1/s).

  • frequency_hz (float) – Reference damped frequency \(|\operatorname{Im}\lambda|/2\pi\).

  • damping_ratio (float) – Reference damping ratio.

  • sampling (ErrorEstimate or None) – Predicted error of the requested azimuth grid, None if no grid was given.

  • sampling_dominant_order (int or None) – Harmonic order contributing most to the first-order sampling error.

  • quadrature (ErrorEstimate) – Arithmetic mean versus the interval-weighted mean on the delivered grid.

  • periodicity (ErrorEstimate) – Second-order sideband (Floquet) estimate.

  • hybridised (bool) – The sampling term (exact when available) or the quadrature term has hybridised.

  • within_tolerance (bool) – The branch is well conditioned, its sampling term is resolved, every term is within the tolerances and none has hybridised or mixed.

  • condition (float) – Eigenvalue condition number \(\kappa_j=1/|\hat{\mathbf u}_j^H \hat{\mathbf v}_j|\) of the unit-norm left and right eigenvectors.

  • well_conditioned (bool) – \(\kappa_j\) does not exceed max_condition. Above it the perturbative terms (sampling prediction, quadrature, periodicity) cannot be trusted, and the branch is reported as ill-conditioned.

  • sampling_exact (ErrorEstimate or None) – The sampling error from the exact trial eigenproblem of the grid mean (the delivered samples for a measured sub-grid, the harmonic model otherwise), None when not computed.

  • mixed (bool) – The general mode-mixing check flagged the sampling term (exact when available) or the quadrature term.

  • sampling_resolved (bool) – The sampling term is a measurement (a sub-grid of the delivered samples) or a prediction whose first alias order the reference resolves. False means the term is only the resolved part of the error and the branch cannot be accepted for that grid (see AveragingErrorEstimate.sampling_status).

label: str
index: int
eigenvalue: complex
frequency_hz: float
damping_ratio: float
sampling: ErrorEstimate | None
sampling_dominant_order: int | None
quadrature: ErrorEstimate
periodicity: ErrorEstimate
hybridised: bool
within_tolerance: bool
condition: float = nan
well_conditioned: bool = True
sampling_exact: ErrorEstimate | None = None
mixed: bool = False
sampling_resolved: bool = True
property sampling_term: ErrorEstimate | None

exact when available.

Returns:

ErrorEstimate or None – sampling_exact if computed, else sampling.

Type:

Return the sampling term used for the assessment

class CommensurateStep(time_step, steps_per_azimuth, requested_time_step, relative_change, azimuth_spacing_deg, azimuth_per_step_deg)[source]

Bases: object

A time step that places every target azimuth on a whole number of steps.

Parameters:
  • time_step (float) – Suggested time step (s), never larger than the requested one.

  • steps_per_azimuth (int) – Whole number of steps between consecutive target azimuths.

  • requested_time_step (float) – The requested time step (s).

  • relative_change (float) – (requested - suggested) / requested.

  • azimuth_spacing_deg (float) – Target azimuth spacing, 360 / count degrees.

  • azimuth_per_step_deg (float) – Rotor rotation per suggested step, degrees.

time_step: float
steps_per_azimuth: int
requested_time_step: float
relative_change: float
azimuth_spacing_deg: float
azimuth_per_step_deg: float
class ErrorEstimate(shift, frequency_error_pct, damping_error_pct, mac, hybridised, method, mixing_mac=nan, mixed=False, shift_bound=nan)[source]

Bases: object

One predicted error term of a branch.

Parameters:
  • shift (complex) – Predicted eigenvalue change (1/s) relative to the reference branch.

  • frequency_error_pct (float) – Relative change of the damped frequency, percent (nan for a real reference eigenvalue).

  • damping_error_pct (float) – Relative change of the damping ratio, percent (nan for an undamped reference).

  • mac (float) – Predicted modal assurance of the perturbed eigenvector with the reference one (nan when the term predicts no eigenvector).

  • hybridised (bool) – mac is below the hybridisation threshold.

  • method (str) – "first", "second", "structured", "exact" or "sideband".

  • mixing_mac (float, optional) – Modal assurance of the general mode-mixing check: the reference eigenvector against its counterpart in the perturbed model, from the exact trial eigenproblem or from an exact solve over the modes strongly coupled to the branch, independent of the azimuth subspace (nan when not evaluated).

  • mixed (bool, optional) – mixing_mac is below the hybridisation threshold: the branch has mixed with another mode, whatever that mode is.

  • shift_bound (float, optional) – First-order conditioning bound \(\kappa_j\|\mathbf E\|_2\) on \(|\delta\lambda|\) (1/s), nan when not evaluated.

shift: complex
frequency_error_pct: float
damping_error_pct: float
mac: float
hybridised: bool
method: str
mixing_mac: float = nan
mixed: bool = False
shift_bound: float = nan
within(damping_tolerance_pct, frequency_tolerance_pct)[source]

Return whether both errors are within the tolerances and nothing mixed.

Parameters:
  • damping_tolerance_pct (float) – Damping-ratio tolerance, percent of the reference value.

  • frequency_tolerance_pct (float) – Frequency tolerance, percent of the reference value.

Returns:

bool – True when neither error exceeds its tolerance (an undefined, NaN error is not checked; an infinite one exceeds it) and the branch has neither hybridised nor mixed.

Return type:

bool

class GridReport(n_azimuths, max_gap_deviation_pct, kernel_orders, kernel_magnitude)[source]

Bases: object

Quadrature properties of the delivered azimuth grid.

Parameters:
  • n_azimuths (int) – Number of delivered azimuths.

  • max_gap_deviation_pct (float) – Largest deviation of a gap from the uniform spacing, percent.

  • kernel_orders (npt.NDArray[np.int]) – Orders 1 .. n_azimuths - 1.

  • kernel_magnitude (npt.NDArray[np.float64]) – \(|W_{\mathrm{ref}}(h)|\) of the interval weights at those orders: the fraction of each harmonic that leaks into the reference mean (zero to rounding on a uniform grid).

n_azimuths: int
max_gap_deviation_pct: float
kernel_orders: npt.NDArray[np.int_]
kernel_magnitude: npt.NDArray[np.float64]
alias_free_azimuth_count(blades, highest_order)[source]

Return the smallest uniform azimuth count free of aliases up to an order.

Parameters:
  • blades (int) – Blade count \(B\) (1 when the content is at every order).

  • highest_order (int) – Highest harmonic order with significant content.

Returns:

int – The smallest \(N\) with \(\operatorname{lcm}(N, B)\) greater than highest_order.

Raises:

ValueError – If blades or highest_order is not a positive integer.

Return type:

int

azimuth_count_table(model, branches=None, *, blades=3, counts=range(5, 37), starts=24, damping_tolerance_pct=1.0, extrapolate=True, max_alias_order=None, mac_threshold=0.9, tail_decay=None)[source]

Predict the averaging error of uniform azimuth counts and recommend one.

Parameters:
  • model (HarmonicModel) – Harmonic model of a dense set of samples (the interpolating trigonometric fit of fit_harmonics()). Branch errors are measured against its zeroth harmonic.

  • branches (Mapping[str, int | complex] or None, optional) – Branches to assess (as for estimate_averaging_error(), resolved against the eigenbasis of model.mean). Default every oscillatory mode.

  • blades (int, optional) – Blade count \(B\) for the \(\operatorname{lcm}(N, B)\) rule. Default 3.

  • counts (Iterable[int], optional) – Uniform azimuth counts to evaluate. Default 5 to 36.

  • starts (int, optional) – Start azimuths per count, spread over one grid spacing; the worst is kept.

  • damping_tolerance_pct (float, optional) – Damping-ratio tolerance, percent of the reference value.

  • extrapolate (bool, optional) – Form the tail estimate of the aliases above the model’s highest order.

  • max_alias_order (int or None, optional) – Highest alias order summed in the tail estimate; default None sums the geometric envelope to convergence, so the estimate does not depend on it.

  • mac_threshold (float, optional) – Tracking MAC below which a count is flagged hybridised.

  • tail_decay (float or TowerPassageFit or None, optional) – Tower-passage decay per order for branches whose own sensitivity does not decay within the resolved orders, either a number in \((0,1)\) or the fit of tower_passage_decay() to the delivered samples (it is ignored when its \(R^2\) in log is below 0.5). None leaves such branches unresolved.

Returns:

AzimuthCountTable – One row per count, the resolved and the extrapolated recommendation, the tail model of every branch and the extrapolation status.

Raises:

ValueError – If an argument is invalid or the model is not of square matrices.

Return type:

AzimuthCountTable

Notes

The resolved error is exact for the content the samples resolve: the exact eigenvalues of the uniform \(N\)-mean of the fitted model, worst over start azimuths. It contains the first alias of \(N\) only when \(\operatorname{lcm}(N,B)\) lies within the resolved orders (alias_resolved); only such counts are accepted, and recommended_resolved is the smallest of them meeting the tolerance. A 36-azimuth reference therefore confirms 5, 6, 9, 12 and 15 but not 7, 8, 10 or 13, whose first aliases (21 to 39) it cannot observe: for those the resolved error is only a lower estimate, and a denser reference is needed to confirm them.

The tail estimate is first order and worst phase, \(\sum_{m}\hat s_j(m\,\operatorname{lcm}(N,B))/\sigma_j\) over the aliases above the model’s highest order, with an envelope \(\hat s_j\) of the sensitivity \(s_j(h)=|\mathbf u_j^H\mathbf A_h\mathbf v_j|+|\mathbf u_j^H\mathbf A_{-h} \mathbf v_j|\):

  • own decay: when \(s_j\) peaks inside the resolved multiples of \(B\) and its log-linear fit from \(2B\) at least halves over the fitted orders, the smallest geometric envelope of that decay through every fitted point;

  • tower law: otherwise, \(\max_h s_j(h)\,(h/H)\,\rho^{h-H}\) from the highest resolved multiple \(H\), with the tower-passage decay \(\rho\) of the azimuth column (tail_decay), the law of the frozen-azimuth derivative of a localised inflow deficit;

  • neither: the branch is "unresolved", its tail and every total are nan, and the table reports "unresolved: needs denser linearization" instead of a recommendation.

Both envelopes are summable, so the estimate is independent of max_alias_order. It is a model-based extrapolation of orders the samples cannot observe, not a bound: on new OpenFAST linearizations of two floating turbines this estimate from 36 azimuths was exceeded at 13 azimuths (and, on the delivered column, at 18) and was up to 30 times conservative elsewhere. recommended_extrapolated is therefore an estimate, and recommended is the resolved count. With the delivered azimuth column the first-order tail is not valid (the near-Jordan azimuth/speed pair); evaluate the table on a pinned or neutralised model.

commensurate_time_step(rotor_speed, count, requested_time_step)[source]

Suggest a time step close to a requested one that is commensurate with the grid.

Parameters:
  • rotor_speed (float) – Rotor speed \(\Omega\) in rad/s.

  • count (int) – Number of uniform target azimuths per revolution (e.g. NLinTimes).

  • requested_time_step (float) – The time step the analyst would use (s).

Returns:

CommensurateStep – \(\Delta t=T/(Nk)\) with \(T=2\pi/\Omega\) and \(k\) the smallest whole number of steps per azimuth interval for which \(\Delta t\le\) requested_time_step.

Raises:

ValueError – If an argument is not positive and finite.

Return type:

CommensurateStep

Notes

A linearization snapshot is taken at the first time step at or past its target azimuth, so on a time step that does not divide the azimuth interval the delivered grid is non-uniform by up to \(\Omega\Delta t\). With a commensurate step and the rotor at the trimmed speed every snapshot falls on its target and the interval and arithmetic means coincide.

estimate_averaging_error(azimuths, matrices, rotor_speed, *, branches=None, azimuth=None, grid=None, method='auto', damping_tolerance_pct=1.0, frequency_tolerance_pct=0.1, periodicity_order=None, consistency_tolerance=0.0001, mac_threshold=0.9, subspace_share=0.05, exact=True, max_condition=1000.0, mixing_coupling=0.005, blades=3)[source]

Estimate the error of each branch of an azimuth-averaged model from the samples.

Parameters:
  • azimuths (npt.ArrayLike) – Delivered azimuths in radians (e.g. np.deg2rad(result.azimuths_deg)).

  • matrices (npt.ArrayLike) – Fixed-frame state matrices, shape (n_azimuth, n, n) (e.g. result.per_azimuth_a of an MBC result kept with retain_per_azimuth=True).

  • rotor_speed (float) – Rotor speed \(\Omega\) in rad/s.

  • branches (Mapping[str, int | complex] or None, optional) – Branches to assess: label to mode index of the reference mean or to an approximate eigenvalue (matched to the nearest one, on the same side of the real axis). Default every oscillatory mode (mode_00, … by frequency).

  • azimuth (AzimuthStates or None, optional) – The rotor-azimuth states (locate_azimuth_state()). Enables the consistency test and the structured predictor.

  • grid (int or npt.ArrayLike or None, optional) – The azimuth grid whose sampling error is predicted: explicit azimuths (a sub-grid of the delivered ones is measured exactly; any other grid is predicted from the interpolating harmonic model) or a count N for a hypothetical uniform grid starting at the first delivered azimuth. For the worst phase over start azimuths use azimuth_count_table(). None skips the sampling term.

  • method ({"auto", "first", "second", "structured"}, optional) – Branch predictor for the sampling and quadrature terms. "auto" uses "structured" when azimuth is given and "second" otherwise.

  • damping_tolerance_pct (float, optional) – Tolerances (percent of the reference value) for within_tolerance. The damping tolerance, as a fraction, is also the scale tolerance of the azimuth consistency test.

  • frequency_tolerance_pct (float, optional) – Tolerances (percent of the reference value) for within_tolerance. The damping tolerance, as a fraction, is also the scale tolerance of the azimuth consistency test.

  • periodicity_order (int or None, optional) – Highest harmonic order of the sideband estimate; default (M - 1) // 2.

  • consistency_tolerance (float, optional) – Absolute tolerance on the azimuth residual exponent, 1/s, reported as azimuth.absolute_consistent; the assessment uses the scaled test.

  • mac_threshold (float, optional) – MAC below which a branch is flagged hybridised or mixed.

  • subspace_share (float, optional) – Azimuth/speed participation share retained exactly by the structured predictor.

  • exact (bool, optional) – Also solve the trial eigenproblem of the grid mean (one dense eigen-solution) for the exact sampling term and the trial form of the mode-mixing check. Default True.

  • max_condition (float, optional) – Eigenvalue condition number above which a branch is ill-conditioned. Default 1e3.

  • mixing_coupling (float, optional) – Coupling-to-detuning ratio of the predicted mode-mixing check (see predict_branch_shift()).

  • blades (int, optional) – Blade count \(B\) of the \(\operatorname{lcm}(N,B)\) aliasing rule that decides whether a predicted grid is resolved. Default 3; use 1 for an anisotropic rotor.

Returns:

AveragingErrorEstimate – The per-branch budget, the azimuth consistency test and the grid report.

Raises:

ValueError – If the inputs are inconsistent or a tolerance is not positive.

Return type:

AveragingErrorEstimate

Notes

The sampling term of the analyst’s own full grid cannot be measured from that grid; assess interleaved sub-grids (grid=azimuths[::2]) or extrapolate the harmonic model with azimuth_count_table(). The quadrature and periodicity terms are fully a priori.

A grid that is not a sub-grid of the delivered samples is predicted from the interpolating harmonic model, which holds the orders up to the Nyquist order of the reference and nothing above. An \(N\)-azimuth grid of a \(B\)-bladed rotor is first aliased at \(\operatorname{lcm}(N,B)\). When that order lies above the resolved orders of the reference, the predicted term is only the resolved part of the error: the branch is reported with sampling_resolved=False and is not accepted, and sampling_status asks for a denser reference. On OpenFAST linearizations of two floating turbines, predictions from a 36-azimuth reference were exact for 12 azimuths (first alias 12) but recovered only 7 to 133 % of the error of 8, 10, 13 and 18 azimuths (first aliases 24 to 39 and the order-18 sine), while the same estimate built from a 72-azimuth reference recovered them all.

The predicted terms are perturbative and assume a well-conditioned branch: the first-order sensitivity of \(\lambda_j\) to a matrix error is \(\kappa_j\) (the pseudospectral bound \(|\delta\lambda_j|\le\kappa_j\|\mathbf E\|_2\), reported as shift_bound). A branch with \(\kappa_j>\) max_condition is reported as ill-conditioned; with exact=True its sampling term is still exact, but its quadrature and periodicity terms are not trusted. On the IEA 22 MW semi-submersible at 8 m/s the platform sway branch (\(\kappa\approx1.2\times 10^3\)) is under-predicted six- to eight-fold by every predictor on some sub-grids, which this check flags and the exact term measures.

first_alias_order(count, blades)[source]

Return the lowest harmonic order that aliases into a uniform count-mean.

Parameters:
  • count (int) – Number of uniform azimuths \(N\).

  • blades (int) – Blade count \(B\) (use 1 for content at every order).

Returns:

int – \(\operatorname{lcm}(N, B)\). When the periodic content after MBC lies only at multiples of \(B\) (an isotropic rotor), the \(N\)-sample mean is aliased only by common multiples of \(N\) and \(B\), so an \(N\) that is not a multiple of \(B\) pushes the first alias from \(N\) to \(BN\).

Raises:

ValueError – If either argument is not a positive integer.

Return type:

int

predict_branch_shift(basis, error, branches, *, method='second', azimuth=None, subspace_share=0.05, mac_threshold=0.9, mixing_coupling=0.005)[source]

Predict the change of reference branches caused by a matrix error.

Parameters:
  • basis (EigenBasis) – Eigentriples of the reference mean \(\mathbf A_0\).

  • error (npt.ArrayLike) – Matrix error \(\mathbf E\) (the perturbed model is \(\mathbf A_0+\mathbf E\)).

  • branches (Sequence[int]) – Mode indices into basis.values.

  • method ({"first", "second", "structured"}, optional) – "first": \(P_{jj}\); "second" (default): adds \(\sum_{k\neq j}P_{jk}P_{kj}/(\lambda_j-\lambda_k)\), with \(P=\mathbf U^H\mathbf E\mathbf V\); "structured": the azimuth column of \(\mathbf E\) is kept as an exact rank-one term and the modes with azimuth or rotor-speed participation above subspace_share are coupled exactly; every other mode keeps its first-order shift. Required when the rotor-azimuth state is present with its delivered column, where the azimuth/speed pair is nearly defective and ordinary perturbation breaks down.

  • azimuth (AzimuthStates or int or None, optional) – The rotor-azimuth states; required for "structured".

  • subspace_share (float, optional) – Participation share above which a mode is retained exactly. Default 0.05.

  • mac_threshold (float, optional) – Predicted MAC below which a branch is flagged hybridised or mixed. Default 0.9.

  • mixing_coupling (float, optional) – Coupling-to-detuning ratio above which a mode joins the branch’s cluster in the general mode-mixing check. Default 0.005.

Returns:

list[ErrorEstimate] – One estimate per requested branch, in order. For the first- and second-order methods the MAC is that of the first-order eigenvector correction. Every estimate also carries the general mode-mixing MAC (mixing_mac, mixed) and the first-order conditioning bound shift_bound.

Raises:

ValueError – If the error matrix does not match the basis, an index is out of range, the method is unknown, mixing_coupling is not positive, or "structured" is requested without azimuth.

Return type:

list[ErrorEstimate]

Notes

The hybridisation flag of "structured" sees only mixing through the azimuth subspace. The mixing check is independent of it: two modes are linked when their coupling \(\sqrt{|P_{ik}P_{ki}|}\) exceeds mixing_coupling times their detuning, and every mode connected to the branch by a chain of links is coupled exactly, so two branches that alias into each other (for example two heavily damped flap modes on a three-azimuth grid) are caught as well. On a coarse grid of a non-normal model the cluster can approach the whole model, and the cost that of the trial eigenproblem. The conditioning bound uses the eigenvalue condition number \(\kappa_j=\|\mathbf u_j\|\|\mathbf v_j\|/|\mathbf u_j^H\mathbf v_j|\): \(|\delta\lambda_j|\le\kappa_j\|\mathbf E\|_2\) to first order (the pseudospectral radius). It is rigorous to first order but on multi-scale models it is far from tight; the branch-level condition number is what estimate_averaging_error() tests.

trial_branch_shift(basis, trial, branches, *, mac_threshold=0.9)[source]

Solve a trial eigenproblem and track the reference branches in it.

Parameters:
  • basis (EigenBasis) – Eigentriples of the reference mean \(\mathbf A_0\).

  • trial (npt.ArrayLike) – The trial (perturbed) state matrix \(\mathbf A_0+\mathbf E\), for example a coarse sub-grid mean.

  • branches (Sequence[int]) – Mode indices into basis.values.

  • mac_threshold (float, optional) – Tracking MAC below which a branch is flagged mixed (and hybridised). Default 0.9.

Returns:

list[ErrorEstimate] – One "exact" estimate per branch: the eigenvalue of the trial matrix whose eigenvector has the largest MAC with the reference eigenvector (on the same side of the real axis), with that MAC as both mac and mixing_mac.

Raises:

ValueError – If the trial matrix does not match the basis or an index is out of range.

Return type:

list[ErrorEstimate]

Notes

This is the trial-eigenproblem form of the mode-mixing check: the exact answer for the given matrix, at the cost of one dense eigen-solution, and the reference for the predicted terms.

Campbell diagram

Campbell diagram assembly and nP excitation / resonance flagging.

Assembly of Campbell-diagram data from tracked modes.

A Campbell diagram plots modal natural frequency (and damping) against an operating parameter — rotor speed or wind speed — with one curve per tracked mode. This module pairs the mode tracks from vane.modal.identify_modes() with the operating-parameter value of each operating point, producing a structure ready for plotting or resonance analysis.

Heavily damped (near-critical) modes, which vane.modal.identify_modes() tracks in a tier of their own, are carried separately in CampbellDiagram.heavily_damped_tracks: they are drawn distinctly (or left out) and never enter the resonance analysis, since a mode damped near critically cannot resonate.

class CampbellDiagram(parameter_values, parameter_name, tracks, n_operating_points, heavily_damped_tracks=<factory>)[source]

Bases: object

Campbell-diagram data: tracked modes against an operating parameter.

Parameters:
  • parameter_values (npt.NDArray[np.float64]) – Operating-parameter value (e.g. rotor speed in rev/min) at each operating point, indexed by operating-point number, shape (n_operating_points,).

  • parameter_name (str) – Name of the operating parameter (e.g. "rotor_speed_rpm").

  • tracks (list[ModeTrack]) – The tracked modes, ordered by first natural frequency.

  • n_operating_points (int) – Number of operating points.

  • heavily_damped_tracks (list[ModeTrack]) – Tracks of heavily damped (near-critical) modes, kept apart from tracks: plotted distinctly and left out of resonance detection. Empty when the identification had none or they were not included.

parameter_values: npt.NDArray[np.float64]
parameter_name: str
tracks: list[ModeTrack]
n_operating_points: int
heavily_damped_tracks: list[ModeTrack]
track_curve(track)[source]

Return the operating-parameter, frequency, and damping arrays of a track.

Parameters:

track (ModeTrack) – A track belonging to this diagram.

Returns:

tuple of npt.NDArray[np.float64] – (parameter_values, natural_frequencies_hz, damping_ratios) along the track, each of length equal to the track’s span.

Return type:

tuple[npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.float64]]

build_campbell(result, parameter_values, *, parameter_name='rotor_speed_rpm', include_heavily_damped=True)[source]

Build a Campbell diagram from identified mode tracks.

Parameters:
  • result (IdentificationResult) – The cross-operating-point mode tracks.

  • parameter_values (Sequence[float]) – Operating-parameter value at each operating point, in operating-point order; its length must equal result.n_operating_points.

  • parameter_name (str, optional) – Name of the operating parameter (default "rotor_speed_rpm").

  • include_heavily_damped (bool, optional) – Carry the identification’s heavily damped tracks into the diagram’s heavily_damped_tracks (default True); False omits them.

Returns:

CampbellDiagram – The assembled diagram.

Raises:

ValueError – If parameter_values length does not match the number of operating points, or the values are not a finite, one-dimensional, strictly increasing sweep axis (the excitation/resonance analysis interpolates against this axis, so duplicate, non-monotone, or non-finite values would give wrong crossings or raw errors).

Return type:

CampbellDiagram

campbell_from_solutions(solutions, parameter_values, *, parameter_name='rotor_speed_rpm', frequency_weight=0.5, mac_threshold=0.5, heavily_damped_threshold=0.99, rotor_speeds_rpm=None)[source]

Identify mode tracks from solutions and build a Campbell diagram.

Parameters:
  • solutions (Sequence[ModalSolution]) – Modal solutions, one per operating point, in operating-point order.

  • parameter_values (Sequence[float]) – Operating-parameter value at each operating point.

  • parameter_name (str, optional) – Name of the operating parameter.

  • frequency_weight (float, optional) – Passed through to vane.modal.identify_modes().

  • mac_threshold (float, optional) – Passed through to vane.modal.identify_modes().

  • heavily_damped_threshold (float or None, optional) – Passed through to vane.modal.identify_modes(); None tracks every mode together.

  • rotor_speeds_rpm (Sequence[float] or None, optional) – Rotor speed (rev/min) at each operating point, so blade whirl modes are classified by their rotating-frame damping. Defaults to parameter_values when parameter_name is "rotor_speed_rpm".

Returns:

CampbellDiagram – The assembled diagram.

Return type:

CampbellDiagram

Per-rev excitation lines and resonance detection.

A rotor turning at Omega excites the structure at integer multiples of the rotational frequency: the nP lines, with frequency n * rpm / 60 Hz. A potential resonance occurs where a mode’s natural-frequency curve crosses an nP line. For a three-bladed rotor the dominant excitations are 1P (rotor imbalance) and the blade-passing 3P, 6P, and 9P lines.

class ResonanceCrossing(track_index, harmonic, rotor_speed_rpm, frequency_hz, damping_ratio, severity)[source]

Bases: object

A crossing of a mode curve with an nP excitation line.

Parameters:
  • track_index (int) – Index of the crossing mode track within the diagram.

  • harmonic (int) – The excitation harmonic n (the nP line).

  • rotor_speed_rpm (float) – Rotor speed at the crossing, interpolated between operating points.

  • frequency_hz (float) – Frequency at the crossing, in hertz.

  • damping_ratio (float) – Damping ratio of the resonating mode at the crossing, interpolated between operating points.

  • severity (ResonanceSeverity) – Damping-aware severity of the crossing.

track_index: int
harmonic: int
rotor_speed_rpm: float
frequency_hz: float
damping_ratio: float
severity: ResonanceSeverity
class ResonanceSeverity(*values)[source]

Bases: Enum

Damping-aware severity of a resonance crossing.

A crossing’s danger depends on how lightly damped the resonating mode is: a near-undamped mode at a crossing can build large response, while a well-damped one is benign.

HIGH = 'high'
MEDIUM = 'medium'
LOW = 'low'
excitation_frequencies(rotor_speed_rpm, harmonics)[source]

Return the nP excitation-line frequencies at each rotor speed.

Parameters:
  • rotor_speed_rpm (npt.NDArray[np.float64]) – Rotor speed in rev/min, shape (n_points,).

  • harmonics (Sequence[int]) – The harmonics n to evaluate.

Returns:

dict[int, npt.NDArray[np.float64]] – Mapping of each harmonic n to its nP frequency (Hz) at every rotor speed, n * rpm / 60.

Raises:

ValueError – If harmonics contains a non-positive, fractional, non-finite, or duplicate value.

Return type:

dict[int, npt.NDArray[np.float64]]

find_resonances(diagram, harmonics=(1, 3, 6, 9), *, operating_range=None)[source]

Find where mode curves cross the nP excitation lines.

A crossing is detected wherever natural_frequency - n * rpm / 60 changes sign (or is zero) between consecutive operating points along a track; the rotor speed, frequency, and damping ratio at the crossing are linearly interpolated, and each crossing is assigned a damping-aware ResonanceSeverity.

The rotor-speed samples need not be strictly increasing: each track’s curve is sorted by rotor speed and duplicate rotor speeds are collapsed internally before the crossings are found, so a non-monotone or tied sweep is handled gracefully rather than producing spurious or zero-width crossings.

Parameters:
  • diagram (CampbellDiagram) – A Campbell diagram whose operating parameter is rotor speed in rev/min.

  • harmonics (Sequence[int], optional) – Excitation harmonics to test (default 1P, 3P, 6P, 9P). Any positive-integer family may be supplied.

  • operating_range (tuple[float, float] or None, optional) – If given, only crossings whose rotor speed falls within this (min_rpm, max_rpm) operating window are returned.

Returns:

list[ResonanceCrossing] – The detected crossings, in track-then-harmonic order.

Raises:

ValueError – If the diagram’s operating parameter is not rotor speed in rev/min, harmonics contains a non-positive, fractional, non-finite, or duplicate value, or operating_range is given with min_rpm > max_rpm.

Return type:

list[ResonanceCrossing]

Driving OpenFAST

Driving OpenFAST — execute linearizations and collect the .lin output.

This subpackage produces linearization data: it runs the OpenFAST executable on an input deck and gathers the resulting .lin files, ready for the parsing and modal pipeline.

The vane.run subpackage produces linearization data: it configures a deck for each operating point, runs the OpenFAST executable, and collects the .lin output. run_operating_sweep() runs a complete sweep from an operating curve (see The OpenFAST linearization sweep); the lower-level driver, operating-point, trim and steady-state functions are public as well. Every public name of the modules below, including helpers such as curve_power_coefficient() and rotor_radius() and constants such as FLOATING_TRIM_PERIODS, can also be imported from vane.run directly (from vane.run import curve_power_coefficient).

Linearization sweep

Automated operating-point sweep from rotor-performance data or settle runs.

FLOATING_TRIM_PERIODS = 20

Periods of the slowest platform mode a floating linearization trim runs for.

class OperatingSweepResult(operating_points, cases, settle_structural_dofs=None, settle_failures=(), floating_plan=None, auto_configuration=None)[source]

Bases: object

Result of a fully automated settle/linearize operating sweep.

Parameters:
  • operating_points (list[OperatingPoint]) – The linearized operating points, in wind_speeds order. Settle-derived points carry their settle_convergence and, when the settle runs were seeded from an operating curve, their settle_curve_check. A settle point that was not linearized is absent here and listed in settle_failures.

  • cases (list[SweepCase]) – The linearization cases, one per operating point.

  • settle_structural_dofs (dict[str, bool] or None, optional) – For a settle sweep, the template ElastoDyn flags the settle runs ran with for every structural DOF the linearization sets (the settle copy keeps the deck’s flags; see run_operating_sweep()). None without settle runs.

  • settle_failures (tuple of SettleFailure, optional) – The settle points that did not reach a confirmed equilibrium, in wind_speeds order, with their diagnostics. Empty when every point settled.

  • floating_plan (FloatingSettlePlan or None, optional) – For a settle sweep, how the settle runs were set up automatically: the controller library used (or that none was loadable, so the rotor was prescribed from the operating curve), the settle damping and the platform probe it was sized from (see plan_floating_settle()). None without settle runs.

  • auto_configuration (AutoConfiguration or None, optional) – Every value the sweep inferred from the deck and curve (template root, OpenFAST executable, rated wind speed and torque, pitch trim gain, module settings), each with its source, and the warnings raised while inferring them. None with auto_configure=False.

operating_points: list[OperatingPoint]
cases: list[SweepCase]
settle_structural_dofs: dict[str, bool] | None = None
settle_failures: tuple[SettleFailure, ...] = ()
floating_plan: FloatingSettlePlan | None = None
auto_configuration: AutoConfiguration | None = None
class SettleFailure(wind_speed, reason, message, linearized=False, operating_point=None, convergence=None)[source]

Bases: object

A settle point of a sweep that did not reach a confirmed equilibrium.

A sweep never aborts because one wind does not settle: the point is recorded here with its diagnostics, the other winds carry on, and only the points that settled are linearized (unless require_settled=False; see run_operating_sweep()).

Parameters:
  • wind_speed (float) – The wind speed (m/s) of the point.

  • reason ({"not_settled", "failed", "error"}) – "not_settled": the settle ran to completion but its criterion was not met (or could not be assessed). "failed": the settle could not complete (SteadyStateError, e.g. OpenFAST failed or its output could not be read). "error": an unexpected exception, which run_operating_sweep() re-raises once the sweep is complete.

  • message (str) – A one-line description of what went wrong.

  • linearized (bool) – Whether the point was linearized anyway ("not_settled" with require_settled=False, at the settle’s last mean).

  • operating_point (OperatingPoint or None, optional) – The settle’s last mean operating point (with its settle_convergence) when the settle ran to completion; None otherwise.

  • convergence (SettleConvergence or None, optional) – The settle evidence: the last RMS, every attempt with its horizon and window, the criterion, and for a two-stage settle the confirmation. None when the settle failed before any run was judged.

wind_speed: float
reason: SettleFailureReason
message: str
linearized: bool = False
operating_point: OperatingPoint | None = None
convergence: SettleConvergence | None = None
to_dict()[source]

Return the failure as a strict-JSON dictionary (unmeasured RMS as null).

Returns:

dict[str, object] – wind_speed, reason, message, linearized, the settled rotor_speed_rpm/blade_pitch_deg/platform when measured, and the convergence evidence.

Return type:

dict[str, object]

SettleFailureReason

Why a settle point is listed in OperatingSweepResult.settle_failures.

alias of Literal[‘not_settled’, ‘failed’, ‘error’]

exception SweepSettleError(message, *, result)[source]

Bases: SteadyStateError

Raised at the end of a sweep whose settle stage did not fully succeed.

Raised only after every wind has been settled and every usable point linearized, so no completed work is lost: the full OperatingSweepResult is attached as result. Raised when strict_settle=True and any point did not settle, or whenever a settle worker hit an unexpected exception (the first is chained as __cause__).

Parameters:
  • message (str) – The error message.

  • result (OperatingSweepResult) – The completed sweep result, including its settle_failures.

Return type:

None

floating_trim_time(period, max_sim_time)[source]

Return the CalcSteady run time of a floating linearization case.

The trim must outlast the slow platform motion the settled initial conditions leave, so the run spans FLOATING_TRIM_PERIODS periods of the slowest platform mode, within [MIN_CALC_STEADY_TMAX, max_sim_time].

Parameters:
  • period (float) – Slowest platform natural period (s), from the standstill platform probe.

  • max_sim_time (float) – Upper bound (s): the retry ladder’s max_sim_time.

Returns:

float – The run time TMax (s).

Raises:

ValueError – If period or max_sim_time is not finite and positive.

Return type:

float

Notes

On the IEA 15-MW semi-submersible (slowest standstill period 125.5 s), settled cases linearized at the rotor-scaled TrimTol (7.1e-3) trimmed at 6, 8 and 12 m/s within 2400 s (about 19 periods; rotor-speed spread 0.08–0.09 %), but only at 8 m/s within 1200 s and at none within 600 s. Loosening TrimTol to 0.021 instead let the 6 m/s trim stop with a 6.6 % rotor-speed spread.

run_operating_sweep(fst_path, output_dir, wind_speeds, *, curve_wind=None, curve_rotor_speed=None, curve_pitch=None, performance_csv=None, settle=False, settle_time=1200.0, discard_time=600.0, max_settle_time=None, controller_library=None, settle_initial_platform=None, settle_curve_tolerances=None, settle_criterion='auto', settle_platform_damping=None, settle_confirm_time=None, settle_equilibrium_tolerances=None, settle_auto_damping=True, controller_search_paths=(), settings=None, structural_dofs=None, trim_tuning=None, retry_policy=None, executable=None, template_root=None, template_copy='referenced', timeout=None, max_workers=None, reserve_cpus=1, require_settled=True, strict_settle=False, strict_cases=False, validate=True, auto_configure=True)[source]

Derive operating points and run an OpenFAST linearization sweep.

The function refuses to guess the rotor speed and blade pitch. It needs a rotor-performance source: an operating curve (curve_wind/curve_rotor_speed/ curve_pitch) or a rotor-performance CSV (performance_csv) – not both – and/or settle=True to run the fully coupled controller first and read the operating point from the settled time series. Without any source, or with both a curve and a CSV, the sweep fails before any OpenFAST process is launched.

Everything else is configured automatically by default (auto_configure): run_operating_sweep(fst_path, output_dir, wind_speeds, performance_csv=csv) is a complete call. The copy root, the OpenFAST executable, the module settings and the turbine constants (rated wind speed, rated generator torque, pitch trim gain) are read from the deck and the curve, each logged and recorded with its source; anything that still cannot be made linearizable fails before launch with the keyword and its remedy.

The combinations mean:

  • curve or CSV alone – the curve, interpolated at each wind speed, is the linearization point.

  • settle=True alone – each settle run starts from 0 rpm and 0 deg collective pitch (written to the settle copy’s ElastoDyn RotSpeed/BlPitch) and the deck’s platform position; the settled point is linearized.

  • settle=True with a curve or CSV – the curve is used only as the settle runs’ initial conditions (RotSpeed and BlPitch(1..3) at each wind speed), so the rotor does not have to spin up from rest and start-up transients are smaller. The controller still decides where the turbine settles and the settled point is linearized, never the curve value. After each settle the settled rotor speed and pitch are compared with the curve (settle_curve_tolerances); a deviation outside tolerance is logged as a warning and recorded on the point’s settle_curve_check (and in the run provenance). It means the controller and the curve disagree and is not an error. Every requested wind speed must lie inside the curve’s range.

The platform offset is never estimated from the curve (VANE computes no thrust); it starts at the deck’s values unless settle_initial_platform supplies it.

Parameters:
  • controller_library (str or pathlib.Path or None, optional) – Bladed-style controller library (e.g. a libdiscon.dll built for this host) for the settle=True runs. Each settle case’s ServoDyn DLL_FileName is rewritten to it in the case copy only; the template deck is never edited. None (the default) finds one automatically (see find_controller_library()): the deck’s own DLL_FileName when it loads on this host, else a host build of the same library next to it or in controller_search_paths, VANE_CONTROLLER_LIBRARIES or the configuration file. When none exists the sweep does not fail: each settle runs with the rotor prescribed at the operating curve’s speed and pitch (no controller; prescribed_rotor of solve_steady_state()) and the point is linearized by CalcSteady from the platform equilibrium it finds – which requires an operating curve or performance_csv. The decision is recorded in OperatingSweepResult.floating_plan. Only valid with settle=True (the linearization replaces the controller by the trim and never loads the library).

  • settle_initial_platform (PlatformPosition or Mapping or None, optional) – Initial platform position for the settle runs: one PlatformPosition for every wind speed, or a mapping from wind speed to position (winds absent from it keep the deck’s position). Only the DOFs given are written. It is an initial condition only: a rough estimate (e.g. the mean surge and pitch of an earlier run) shortens the platform’s drift to its offset without changing where it settles. None (the default) keeps the deck’s platform position. Only valid with settle=True; every mapping key must be a requested wind speed.

  • settle_curve_tolerances (TrimTolerances or None, optional) – Tolerances of the settled-versus-curve comparison (rotor speed relative, pitch absolute; see compare_settled_to_curve()). None uses the TrimTolerances defaults (1 %, 0.5 deg). Only valid with settle=True and a curve or CSV.

  • settle_criterion ({"auto", "velocity", "mean_drift"}, optional) – How each settle run is judged settled (see solve_steady_state()): "auto" (the default) starts with the velocity criterion and switches to the mean-drift criterion when the platform carries a sustained, non-decaying oscillation; "velocity" is platform-velocity RMS within tolerance; "mean_drift" is operating-point means that no longer drift between the halves of the analysis window. The criterion that judged each point is recorded in its settle_convergence and in the run provenance. Only valid with settle=True.

  • settle_platform_damping (SettlePlatformDamping or None, optional) – Settle accelerator for floating platforms. None (the default) sizes it automatically for a floating deck from a standstill platform probe (see plan_floating_settle(); settle_auto_damping=False opts out). Each settle then runs in two stages, first with this temporary linear damping added to HydroDyn’s AddBLin in the settle copy, then undamped from the stage-1 means to confirm the equilibrium is unchanged (see solve_steady_state() and SettlePlatformDamping). The damping is only ever written to the settle case; every linearization case is copied afresh from the template and keeps the deck’s own AddBLin. The template must run HydroDyn with an AddBLin matrix; this is checked before any case is copied. Only valid with settle=True.

  • settle_confirm_time (float or None, optional) – TMax (s) of the undamped confirmation run of a two-stage settle. None (the default) sizes it automatically so its analysis window holds eight periods of the slowest platform mode – the platform probe’s longest period when the probe ran, else the damped stage’s dominant period – never shorter than 1200 s (the length without any period) and never longer than max_settle_time when given, else 3600 s (see confirm_time of solve_steady_state()). A decaying confirmation whose means have not yet returned to the damped equilibrium is rerun once twice as long. Used only with a settle damping.

  • settle_equilibrium_tolerances (EquilibriumTolerances or None, optional) – Tolerances on a change of the settled means (damped versus undamped stage, and window halves for "mean_drift"). None uses the EquilibriumTolerances defaults. Only valid with settle=True.

  • settle_auto_damping (bool, optional) – For a floating deck without settle_platform_damping, size the settle damping at half of critical from a standstill platform probe (two short linearizations in output_dir; see probe_platform()). Default True; False settles undamped.

  • controller_search_paths (Sequence of str or pathlib.Path, optional) – Extra controller-library files or directories searched when controller_library is not given.

  • performance_csv (str or pathlib.Path or None, optional) – Path to a rotor-performance CSV (see operating_curve_from_csv() for the column spec). When given it is parsed into the operating curve; it is mutually exclusive with the curve_* arrays. With settle=True it seeds the settle runs (see above).

  • structural_dofs (dict[str, bool] or None, optional) – ElastoDyn structural-DOF flags to set in each case’s ED sub-file. None (the default) keeps the settings object’s own value, whose default disables YawDOF and DrTrDOF for a reliable trim (dropping the spurious free-yaw mode and the ~0.62 Hz drivetrain-torsion mode, the latter a deliberate trade-off for trim convergence). Pass a mapping to override — it is merged on top of the defaults, so {"DrTrDOF": True} re-enables the drivetrain-torsion mode while still disabling YawDOF. A flag listed here but absent from the ED deck is an error. These flags apply to the linearization cases only: the settle copies keep the template deck’s own ElastoDyn DOF flags, because a settle runs the deck’s real controller on the model it was tuned for (a controller’s filters assume the deck’s drivetrain; the yaw drive holds the nacelle through ServoDyn). The settled mean rotor speed, pitch and platform position – all that is carried to the linearization – do not depend on the drivetrain or yaw flexibility. A difference between the two sets is logged, and the settle flags are recorded in OperatingSweepResult.settle_structural_dofs.

  • retry_policy (LinearizationRetryPolicy or None, optional) – The convergence retry ladder for the linearization cases (see LinearizationRetryPolicy), e.g. a larger max_sim_time for a slowly settling floating platform. None (the default) uses the default policy.

  • trim_tuning (TrimTuningPolicy or None, optional) – When set, each operating point’s starting trim tolerance and damping are tuned for its wind-speed region before the convergence retry ladder runs (see tune_trim_settings()): at/above-rated points start from the looser-tolerance, higher-damping floors those harder trims need, so they tend to converge on the first attempt rather than after a reactive retry. None (the default) starts every point from settings unchanged. Tuning only raises the per-case trim_tol/twr_kdmp/bld_kdmp toward the floors, so the sweep’s provenance is unaffected.

  • settings (LinearizationSettings or None, optional) – The linearization settings. None (the default) configures the deck automatically (with auto_configure): the offshore() module set, adapted to the deck – a HydroDyn radiation model whose .ss file is missing keeps RdtnMod 2 when the file can be fitted from the WAMIT .1 data (it is then generated in each case copy, hydro_radiation_fit), a radiation or excitation model whose state-space file cannot be had falls back to RdtnMod/ExctnMod 0 with a recorded warning, a deck’s own linearizable unsteady-aerodynamics model (UA_Mod 4-6) is kept with UAEndRad 0.99 where it would reach the tip node under tip loss, and a module the deck does not use is left untouched. Settings passed explicitly are used as given, except that the turbine constants they leave unset are still inferred (see auto_configure).

  • executable (str or pathlib.Path or None, optional) – The OpenFAST executable. None (the default) chooses one whose release reads the deck’s input-format generation, from the VANE_OPENFAST_EXECUTABLES environment variable, the file named by VANE_OPENFAST_CONFIG (default ~/.vane/openfast_executables.txt) and PATH (see select_openfast_executable()); the sweep fails before any case is copied, naming the release the deck needs, when none can. An explicit executable is used as given, but is rejected up front when its version is known not to read the deck.

  • template_root (str or pathlib.Path or None, optional) – Directory the deck’s files are copied from per case. None (the default) uses the deck’s own directory, or – when the deck references files outside it (../5MW_Baseline/...) and auto_configure is on – the deepest directory that holds the deck and every file it references by a relative path (infer_template_root()). An explicit value always wins.

  • template_copy ({"referenced", "tree"}, optional) – "referenced" (the default) copies only the files the deck references into each case (settle cases included); "tree" copies the whole template_root. See LinearizationDriver.

  • require_settled (bool, optional) – What to do with a settle point that does not reach a confirmed equilibrium (its criterion not met, or not assessable). True (the default): do not linearize it. False: linearize it at the settle’s last mean. Either way the sweep carries on with the other winds – one unsettled point never aborts it – and the point is listed in OperatingSweepResult.settle_failures with its diagnostics (and, when linearized, its settle evidence is in the case provenance). A settle that cannot complete (OpenFAST fails, unreadable output) is likewise recorded and never linearized.

  • strict_settle (bool, optional) – When True, raise SweepSettleError once the sweep is complete if any settle point is in settle_failures; the full result is attached as its result. Default False (the failures are logged and returned). An unexpected exception in a settle worker is always re-raised this way, after the other winds have been settled and linearized.

  • strict_cases (bool, optional) – How a linearization case that raises an unexpected exception (a file-system error while its case is copied or configured, say) is reported. Such a case never aborts the sweep: it is recorded in OperatingSweepResult.cases as failed (converged=False, failure_mode == "case_error") and the other points run to completion (see vane.run.LinearizationDriver.run_sweep()). Default False: the failure is logged and the result returned. True: raise vane.run.SweepCaseError once the sweep is complete, with the full result attached as its result.

  • validate (bool, optional) – When True (the default) the template deck is checked under the effective settings (validate_deck()) and every rotating curve point’s trim configuration is checked before any case is copied, and each configured case again before its OpenFAST run.

  • auto_configure (bool, optional) – When True (the default) the sweep completes its configuration from the user’s own deck and curve, logging every value and recording it with its source in OperatingSweepResult.auto_configuration, in the driver’s run provenance and in output_dir/auto_configuration.json (auto_configure_sweep()): the template root and executable (above), the module settings when settings is None, and the turbine constants left unset – rated_wind_speed (where the curve’s rotor speed and power reach their rated plateau), rated_generator_torque (controller DISCON.IN VS_RtTq, else ServoDyn VS_RtTq with VSContrl = 1, else the curve’s cp at rated) and a trim_gain_pitch left at its class default (scaled as 2e-4 * (63 m / TipRad)**3). VANE computes no rotor performance in doing so. False restores the explicit behaviour: nothing is inferred, settings=None means offshore(), and executable=None means "openfast" on PATH.

  • fst_path (str | Path)

  • output_dir (str | Path)

  • wind_speeds (Sequence[float])

  • curve_wind (Sequence[float] | None)

  • curve_rotor_speed (Sequence[float] | None)

  • curve_pitch (Sequence[float] | None)

  • settle (bool)

  • settle_time (float)

  • discard_time (float)

  • max_settle_time (float | None)

  • timeout (float | None)

  • max_workers (int | None)

  • reserve_cpus (int)

Raises:
  • SweepSettleError – After the sweep completes, with strict_settle=True and a settle failure, or when a settle worker raised an unexpected exception.

  • SweepCaseError – After the sweep completes, with strict_cases=True and a linearization case that raised an unexpected exception.

  • ValueError – Before any launch, for a malformed request or a rotating sweep whose rated wind speed is neither given nor inferable.

  • vane.run.DeckValidationError – Before any launch, when validate is on and the template cannot be linearized under the effective settings; every problem is listed with the keyword and its remedy.

  • vane.io.FastInputError – Before any launch, when a rotating curve point’s trim cannot be configured (e.g. no rated generator torque for an above-rated point).

  • vane.run.OpenFastError – Before any launch, when no OpenFAST executable can read the deck.

Return type:

OperatingSweepResult

Infer a linearization sweep’s turbine constants from the user’s deck and curve.

run_operating_sweep() needs a few turbine constants that a deck does not state in one place: the rated wind speed that separates torque trim from pitch trim, the rated generator torque the trim holds above rated and derives its torque gain from, a pitch-trim gain suited to the rotor’s size, and a module configuration OpenFAST can linearize. This module reads them from the user’s own inputs – the operating curve they supplied and the deck’s ElastoDyn, ServoDyn, AeroDyn, HydroDyn and controller input files – so a sweep runs with no other arguments. VANE still computes no rotor performance: every value is a constant read from, or arithmetic on, the user’s data, and each one is logged and recorded with its source (InferredValue, collected in AutoConfiguration) so the run provenance says where it came from. A value the user sets explicitly is never replaced.

The rules (see infer_rated_wind_speed(), infer_rated_generator_torque() and scaled_trim_gain_pitch() for the evidence behind each):

  • rated wind speed – the first curve wind at which the rotor speed has reached its plateau and, when the curve has a cp column, the aerodynamic power cp * V**3 has reached its plateau too. Cross-checked against the controller’s rated speed and power (DISCON.IN) when present; a disagreement is logged.

  • rated generator torque – the DISCON.IN VS_RtTq (the file named by ServoDyn DLL_InFile) when the deck runs a controller library; else ServoDyn VS_RtTq when VSContrl = 1; else the curve’s aerodynamic torque at rated from its cp column, ElastoDyn GBRatio/GBoxEff and the deck’s air density.

  • pitch trim gain – 1e-3 * (63 m / R)**3 with R the ElastoDyn TipRad.

  • module configuration (only when no settings are passed) – the offshore() preset adapted to the deck: a missing .ss radiation file is fitted from the WAMIT .1 data in each case copy when the fit is accurate enough (hydro_radiation_fit), other missing state-space hydrodynamics files fall back to RdtnMod/ExctnMod 0, a deck’s own linearizable unsteady-aerodynamics model is kept (with UAEndRad 0.99 where the tip node would otherwise carry it), and modules the deck does not use are left untouched.

DEFAULT_TRIM_GAIN_PITCH = 0.0002

The class-default pitch trim gain (rad/(rad/s)); a value left here is scaled.

DEFAULT_TRIM_TOL = 0.001

The class-default trim tolerance; a value left here is scaled.

MAX_SCALED_TRIM_TOL = 0.01

Upper bound of the scaled trim tolerance. The MBC3 transform accepts a rotor-speed spread of 1 % across the azimuth sweep, which a looser trim can exceed.

REFERENCE_ROTOR_RADIUS = 63.0

Rotor radius (m) at which the default trim gain and tolerance apply (NREL 5-MW).

class AutoConfiguration(inferred=(), warnings=())[source]

Bases: object

Everything a sweep inferred or adjusted on its own, for the provenance.

Parameters:
  • inferred (tuple[InferredValue, ...]) – The inferred values, in the order they were decided.

  • warnings (tuple[str, ...]) – Disagreements and fallbacks the user should know about (also logged).

inferred: tuple[InferredValue, ...] = ()
warnings: tuple[str, ...] = ()
get(name)[source]

Return the last record named name, or None.

Parameters:

name (str) – The record name.

Returns:

InferredValue or None – The record, when one was made.

Return type:

InferredValue | None

extended(inferred=(), warnings=())[source]

Return a copy with more records and warnings appended.

Parameters:
  • inferred (Sequence[InferredValue], optional) – Records to append.

  • warnings (Sequence[str], optional) – Warnings to append.

Returns:

AutoConfiguration – The extended configuration.

Return type:

AutoConfiguration

to_dict()[source]

Return the configuration as a JSON-serializable dictionary.

Returns:

dict[str, object] – inferred (a list of records) and warnings.

Return type:

dict[str, object]

class ControllerConstants(path, rated_power=None, rated_torque=None, rated_generator_speed=None, generator_efficiency=None, blade_radius=None)[source]

Bases: object

Rated constants read from a DISCON.IN-style controller input file.

Parameters:
  • path (pathlib.Path) – The controller input file (ServoDyn DLL_InFile).

  • rated_power (float or None) – VS_RtPwr (W, electrical).

  • rated_torque (float or None) – VS_RtTq (N·m, generator/HSS side).

  • rated_generator_speed (float or None) – VS_RefSpd (else PC_RefSpd), rad/s on the HSS.

  • generator_efficiency (float or None) – VS_GenEff as a fraction (the file holds percent).

  • blade_radius (float or None) – WE_BladeRadius (m).

path: Path
rated_power: float | None = None
rated_torque: float | None = None
rated_generator_speed: float | None = None
generator_efficiency: float | None = None
blade_radius: float | None = None
class InferredValue(name, value, source, detail='')[source]

Bases: object

One value VANE inferred or adjusted, with where it came from.

Parameters:
  • name (str) – What was inferred: a LinearizationSettings field name (rated_wind_speed, hydro_radiation_mode, …) or template_root / executable.

  • value (float or int or str or None) – The value used.

  • source (str) – The file and keyword, or the rule, it was taken from.

  • detail (str, optional) – Further evidence (cross-checks, the numbers combined).

name: str
value: float | int | str | None
source: str
detail: str = ''
to_dict()[source]

Return the record as a JSON-serializable dictionary.

Returns:

dict[str, object] – name, value, source and detail.

Return type:

dict[str, object]

auto_configure_sweep(fst_path, settings, *, curve_wind=None, curve_rotor_speed=None, curve_cp=None)[source]

Return sweep settings completed from the deck and curve, with the record.

With settings=None the module configuration is chosen for the deck (the offshore() preset, adapted as described in the module docstring). In every case the turbine constants the settings leave unset are inferred: rated_wind_speed and rated_generator_torque when None, and trim_gain_pitch and trim_tol when they were left at their class defaults (2e-4 and 1e-3, which apply to a 63 m rotor; see is_size_scaled_default()). Values set explicitly are kept, even when they equal the defaults.

Parameters:
  • fst_path (str or pathlib.Path) – The primary .fst deck.

  • settings (LinearizationSettings or None) – The user’s settings, or None for a fully automatic configuration.

  • curve_wind (Sequence[float] or None, optional) – The operating curve (m/s, rpm), when one was supplied.

  • curve_rotor_speed (Sequence[float] or None, optional) – The operating curve (m/s, rpm), when one was supplied.

  • curve_cp (Sequence[float] or None, optional) – The curve’s power coefficients, when its CSV has a cp column.

Returns:

tuple[LinearizationSettings, AutoConfiguration] – The completed settings and every inferred value with its source.

Return type:

tuple[LinearizationSettings, AutoConfiguration]

Notes

A constant that cannot be inferred is left unset; the sweep’s boundary checks then fail before launch with the keyword to supply.

curve_power_coefficient(csv_path, curve_wind)[source]

Return a performance CSV’s cp column aligned with curve_wind.

Parameters:
  • csv_path (str or pathlib.Path) – The rotor-performance CSV (as given to run_operating_sweep()).

  • curve_wind (Sequence[float]) – The curve’s wind speeds as operating_curve_from_csv() returned them (sorted ascending).

Returns:

list[float] or None – The power coefficient (a fraction) at each curve wind, or None when the file cannot be read, has no cp column, its rows do not match curve_wind, or any cp value is missing, non-numeric or negative.

Raises:

ValueError – If the cp column’s unit or scale is wrong or ambiguous, as for rotor_performance_from_csv(): the header accepts the same column names (cp, power_coefficient, RtAeroCp, RtFldCp, only one of them) and units (none, [-], [1] or [fraction] for a fraction; [%], [percent] or [pct] for a percentage, which is converted), a fraction above the Betz limit 16/27 is rejected as a percentage written without its unit, and a column declared in percent whose values are all at most 1 is rejected as fractions labelled as percent.

Return type:

list[float] | None

infer_rated_generator_torque(fst_path, *, curve_wind=None, curve_rotor_speed=None, curve_cp=None, controller=None)[source]

Return the rated generator torque (N·m, HSS side) from the deck or the curve.

Sources, in order:

  1. the controller input file’s VS_RtTq (DISCON.IN, the file named by ServoDyn DLL_InFile) when the deck runs a controller library;

  2. ServoDyn VS_RtTq when VSContrl = 1 (the simple variable-speed law actually uses it; under a library it is a placeholder);

  3. the curve’s aerodynamic torque at rated, Q = cp * rho/2 * pi * R**2 * V**3 / Omega averaged over the rated region, moved to the generator side with ElastoDyn GBoxEff and GBRatio (Q * GBoxEff / GBRatio), when the curve has a cp column.

Parameters:
  • fst_path (str or pathlib.Path) – The primary .fst deck.

  • curve_wind (Sequence[float] or None, optional) – The operating curve and its power coefficients, for source 3.

  • curve_rotor_speed (Sequence[float] or None, optional) – The operating curve and its power coefficients, for source 3.

  • curve_cp (Sequence[float] or None, optional) – The operating curve and its power coefficients, for source 3.

  • controller (ControllerConstants or None, optional) – The deck’s controller constants, when already read (read_controller_constants() is called otherwise).

Returns:

InferredValue or None – The torque with its source, or None when no source has it.

Return type:

InferredValue | None

Notes

The torque trim gain is derived from this value as a tenth of it (Jonkman’s rule of thumb). On the low-speed shaft that loop’s proportional gain is 1000 * TrimGain * GBRatio = 100 * Q_rated, so its time constant J / (100 * Q_rated) (J the ElastoDyn rotor and drivetrain inertia) is 0.10 s for the NREL 5-MW, 0.14 s for the IEA 3.4-MW, 0.19 s for the IEA 10-MW, 0.18 s for the IEA 15-MW and 0.23 s for the IEA 22-MW: the rule scales with size by construction, since the rated torque grows with the rotor. Source 3 reproduces the published rated torques within 0.3 % (NREL 5-MW), 1.1 % (IEA 15-MW) and 3.9 % (IEA 22-MW).

infer_rated_wind_speed(curve_wind, curve_rotor_speed, curve_cp=None)[source]

Return the rated wind speed of an operating curve, and how it was found.

Rated is the first curve wind at which the rotor speed has reached its plateau (within 0.5 % of the curve’s maximum, held for at least two samples) and, when curve_cp is given, the aerodynamic power cp * V**3 has also reached its plateau: from that wind on, every curve sample is within 0.5 % of the curve’s final power (at least two samples). A power still rising through the last samples has no plateau.

Parameters:
  • curve_wind (Sequence[float]) – The operating curve (m/s, rpm).

  • curve_rotor_speed (Sequence[float]) – The operating curve (m/s, rpm).

  • curve_cp (Sequence[float] or None, optional) – The curve’s power coefficients, aligned with curve_wind.

Returns:

tuple[float, str] – The rated wind speed (m/s) and a description of the evidence.

Raises:

ValueError – If the curve has no rotor-speed plateau (e.g. it stops below rated), or its sequences differ in length.

Return type:

tuple[float, str]

Notes

The rated wind speed selects the CalcSteady trim: below it the generator torque is trimmed at the curve’s pitch, at and above it the pitch is trimmed against a constant rated torque. The boundary that matters is therefore where the generator torque reaches rated, i.e. where the rotor speed is at its rated value and the power has stopped rising. The point where the pitch starts to rise is not used: a peak-shaving schedule pitches before rated (the published NREL 5-MW curve from 11.1 m/s, while its rotor speed and power level off at 12.12 m/s), and a fine-pitch schedule pitches below rated too. On the published reference curves the rule gives 12.12 m/s (NREL 5-MW published curve), 9.81 m/s (IEA 3.4-MW curve; published 9.8), 10.76 m/s (IEA 10-MW curve, whose rotor speed levels off at 10.27 m/s while the power still rises to 10.76 m/s), 10.66 m/s (IEA 15-MW curve; published 10.66) and 11.13 m/s (IEA 22-MW curve; published 11.13). On a coarse curve without cp (an IEA 22-MW operating table on a 1 m/s grid) the first plateau sample (12 m/s) is returned, which errs to the safe side: a point between the true rated speed and it is torque-trimmed at the curve’s pitch, which converges like any below-rated point.

read_controller_constants(fst_path)[source]

Return the rated constants of the deck’s controller input file, if any.

The file is ServoDyn’s DLL_InFile (resolved relative to the ServoDyn file), read only when ServoDyn is enabled and runs a control channel through the Bladed-style library (PCMode, VSContrl, HSSBrMode or YCMode = 5). The value ! Keyword - description layout of DISCON.IN is parsed; VS_RtPwr, VS_RtTq, VS_RefSpd (or PC_RefSpd), VS_GenEff and WE_BladeRadius are read.

Parameters:

fst_path (str or pathlib.Path) – The primary .fst deck.

Returns:

ControllerConstants or None – The constants (each None when absent or not positive), or None when the deck has no readable controller input file or it holds none of them.

Return type:

ControllerConstants | None

rotor_radius(fst_path)[source]

Return the rotor radius (ElastoDyn TipRad, m) of a deck, if readable.

Parameters:

fst_path (str or pathlib.Path) – The primary .fst deck.

Returns:

float or None – TipRad, or None when the deck has no readable positive value.

Return type:

float | None

scaled_trim_gain_pitch(radius)[source]

Return the default CalcSteady pitch trim gain for a rotor of radius.

The rule is 2e-4 * (63 m / R)**3 rad/(rad/s).

Parameters:

radius (float) – Rotor radius R (m), ElastoDyn TipRad.

Returns:

float – The pitch trim gain (TrimGain for TrimCase = 3).

Raises:

ValueError – If radius is not finite and positive.

Return type:

float

Notes

ServoDyn’s pitch trim is a PI law on the rotor-speed error e (rad/s): each step adds TrimGain * e to an integrator and the pitch offset adds 1000 * TrimGain * e on top, so the proportional gain is 1000 * TrimGain and the integral gain TrimGain / DT. The aerodynamic torque a pitch change produces, dQ/dtheta, scales with rho * R**3 * V**2 at the similar rated wind speeds of utility turbines, so holding the trim’s torque correction per unit speed error (1000 * TrimGain * dQ/dtheta) at its NREL 5-MW value gives TrimGain proportional to R**-3. It is anchored at the class default 2e-4 for R = 63 m. The ElastoDyn rotor inertia about the low-speed shaft (blades, hub and generator) grows nearly as fast, as R**3.4 over the NREL 5-MW and IEA 3.4/10/15/22-MW turbines (4.4e7 to 7.1e8 kg·m²), so the rule also keeps TrimGain times that inertia within a factor of two of the NREL 5-MW value on all five.

Single-attempt CalcSteady trims give the evidence. The anchor was 1e-3 until the reference-turbine campaign showed that the r-test NREL 5-MW land deck (63 m, DT 6.25 ms) oscillates or motors above rated with it: at 16 m/s 1e-3 keeps oscillating while 6.3e-4, 5e-4 and 2e-4 converge, and at 13 m/s, just above rated, everything above 2e-4 motors. With the 2e-4 anchor every NREL 5-MW point, the IEA 3.4-MW (64.9 m) from 9 to 25 m/s and every IEA 15-MW (121.0 m) and 22-MW (141.0 m) point above rated trim on the first attempt. Before that, the 1e-3 anchor diverged on the IEA 10-, 15- and 22-MW, and its scaled 2.6e-4 converged the IEA 10-MW (99.2 m) at 12 m/s.

scaled_trim_tol(radius)[source]

Return the default CalcSteady trim tolerance for a rotor of radius.

The rule is 1e-3 * (R / 63 m)**3, kept within [1e-3, 1e-2].

Parameters:

radius (float) – Rotor radius R (m), ElastoDyn TipRad.

Returns:

float – The trim tolerance (TrimTol).

Raises:

ValueError – If radius is not finite and positive.

Return type:

float

Notes

TrimTol bounds the change of the trimmed outputs from one rotor revolution to the next. Without trim damping, the lightly damped tower and blade motion of a large, flexible turbine decays too slowly for a tight tolerance, while the rotor speed of a small, light rotor needs one: the MBC3 transform accepts a rotor-speed spread of at most 1 % across the azimuth sweep.

Single-attempt trims on the reference-turbine campaign bracket the rule. At 1e-3 the IEA 3.4-MW (64.9 m) and the NREL 5-MW (63 m) converge every point, but the IEA 15-MW (121.0 m) converges only 4 of 9 and the IEA 22-MW (141.0 m) 2 of 9. At 1e-2 the IEA 15-MW converges 8 of 9 and the IEA 22-MW 9 of 9, while the IEA 3.4-MW’s below-rated points stop with a rotor-speed spread of 1.02–2.57 % that the MBC3 check rejects. Against the points that converge at both tolerances, 1e-2 changes the frequencies below 3 Hz by at most 0.1 %, the damping ratios by at most 0.1 percentage point and the trimmed generator torque by at most 0.8 %. The rule gives 1e-3 to 1.1e-3 for the 63–65 m rotors, 3.9e-3 for the IEA 10-MW (99 m), 7.1e-3 for the IEA 15-MW and the 1e-2 bound for the IEA 22-MW. A trim that still does not converge is retried with a looser tolerance and a longer run.

Operating points for a linearization sweep.

A linearization is taken at a single operating point — a wind speed, a rotor speed, and a blade-pitch angle. A Campbell diagram or a load/QoI sweep needs a series of them across the operating envelope. This module defines OperatingPoint and builds a sweep either from an explicit list or by interpolating a turbine’s steady-state operating curve (the rotor speed and pitch a controller would hold at each wind speed), so a user need only choose the wind speeds to cover.

class OperatingPoint(wind_speed, rotor_speed_rpm, blade_pitch_deg, name='', platform=None, settle_convergence=None, settle_curve_check=None)[source]

Bases: object

A single steady operating point for a linearization.

Parameters:
  • wind_speed (float) – Hub-height wind speed in m/s. 0 denotes a no-wind (standstill or still-air) point.

  • rotor_speed_rpm (float) – Rotor speed in rev/min. 0 denotes a parked/standstill rotor.

  • blade_pitch_deg (float) – Collective blade-pitch angle in degrees.

  • name (str) – Optional label for the point (used to name a generated case directory).

  • platform (PlatformPosition or None) – Initial floating-platform position to set in ElastoDyn. None (the default) leaves the platform degrees of freedom at the deck’s values — appropriate for a bottom-fixed turbine or when the deck is already at equilibrium.

  • settle_convergence (SettleConvergence or None) – Audited platform-velocity convergence from solve_steady_state(), when the point came from a settle run. Hand-authored curve points leave it None.

  • settle_curve_check (SettleCurveCheck or None) – For a settle run seeded from an operating curve, the settled rotor speed and pitch compared with the curve at this wind speed (see compare_settled_to_curve()); None otherwise.

wind_speed: float
rotor_speed_rpm: float
blade_pitch_deg: float
name: str = ''
platform: PlatformPosition | None = None
settle_convergence: SettleConvergence | None = None
settle_curve_check: SettleCurveCheck | None = None
property is_rotating: bool

Return whether the rotor turns under wind (a periodic operating point).

class PlatformPosition(surge=None, sway=None, heave=None, roll=None, pitch=None, yaw=None)[source]

Bases: object

Rigid-body floating-platform position (ElastoDyn platform DOFs).

Up to six platform degrees of freedom used to start a floating linearization at its thrust-loaded equilibrium, so the steady-state trim does not have to drag the slow platform modes there from rest (which it cannot reliably do near peak thrust). Translations are in metres and rotations in degrees, matching ElastoDyn’s PtfmSurge/PtfmSway/PtfmHeave/PtfmRoll/PtfmPitch/PtfmYaw.

Each field defaults to None, meaning leave that DOF at the deck’s value; only the DOFs given are written. This keeps a partially-known position (e.g. only the surge and pitch a deck happens to output) from overwriting the deck’s other platform DOFs with fabricated zeros.

Parameters:
  • surge (float or None) – Platform translational displacements in metres, or None to leave unchanged.

  • sway (float or None) – Platform translational displacements in metres, or None to leave unchanged.

  • heave (float or None) – Platform translational displacements in metres, or None to leave unchanged.

  • roll (float or None) – Platform rotational displacements in degrees, or None to leave unchanged.

  • pitch (float or None) – Platform rotational displacements in degrees, or None to leave unchanged.

  • yaw (float or None) – Platform rotational displacements in degrees, or None to leave unchanged.

surge: float | None = None
sway: float | None = None
heave: float | None = None
roll: float | None = None
pitch: float | None = None
yaw: float | None = None
operating_curve_from_csv(path)[source]

Parse a rotor-performance CSV into a steady operating curve.

Reads a header-row CSV of the turbine’s steady operating schedule and returns the (curve_wind, curve_rotor_speed, curve_pitch) arrays run_operating_sweep() and operating_points_from_curve() consume, sorted by ascending wind speed.

The CSV must have a header row naming its columns. Three columns are required (canonical names, with tolerated variants — header matching ignores case, whitespace, and separators):

  • wind_speed (m/s) – also WindSpeed, wind, V [m/s], U;

  • rotor_speed_rpm (rpm) – also RotSpeed, Omega [rpm], rpm;

  • blade_pitch_deg (deg) – also BldPitch, pitch [deg], theta.

A unit written in square brackets or parentheses after a required column’s name is parsed (case- and whitespace-insensitive) and the column converted to the canonical unit; a column without a unit is taken to be in the canonical unit:

  • wind speed accepts m/s (also mps, m s^-1) and converts km/h (also kmh, kph) by 1/3.6;

  • rotor speed accepts rpm (also rev/min) and converts rad/s by 60/(2*pi);

  • blade pitch accepts deg (also degree, degrees) and converts rad by 180/pi.

Any other unit on a required column raises ValueError rather than being silently misread, as does a converting unit on a header whose name states the canonical unit (rotor_speed_rpm [rad/s]). Each conversion is logged at INFO. Units are read from the header only; a separate units row is not supported and fails as a non-numeric value. Any other columns (cp, ct, cq, gen_power, …) are ignored here, units included, so a standard rotor-performance export drops in unchanged; rotor_performance_from_csv() reads the cp and ct columns with their units.

Parameters:

path (str or pathlib.Path) – Path to the rotor-performance CSV file.

Returns:

tuple of three list[float] – (curve_wind, curve_rotor_speed, curve_pitch), sorted by ascending wind speed.

Raises:
  • FileNotFoundError – If path does not exist.

  • ValueError – If the file is unreadable or empty, a required column is missing or carries an unsupported (or name-contradicting) unit, a value is non-numeric, NaN, or infinite, a wind speed or rotor speed is negative (negative collective blade pitch is allowed), there are fewer than two data rows (at least two operating points are needed to interpolate), or the wind speeds are duplicated or not strictly increasing once sorted.

Return type:

tuple[list[float], list[float], list[float]]

operating_curve_from_performance(*, performance_table=None)[source]

Derive a steady operating curve from a tabulated rotor-performance schedule.

Returns the (curve_wind, curve_rotor_speed, curve_pitch) arrays that operating_points_from_curve() and run_operating_sweep() consume, from an explicit performance_table – the turbine’s tabulated steady operating schedule (wind speed, rotor speed, collective pitch), validated and returned sorted by ascending wind speed. The rotor-performance data is always supplied by the caller (e.g. read from a CSV with operating_curve_from_csv()); VANE does not compute it from blade geometry.

Parameters:

performance_table (tuple of three Sequence[float] or None, optional) – (wind_speed, rotor_speed_rpm, blade_pitch_deg) arrays of the turbine’s steady operating schedule, of equal length (>= 2).

Returns:

tuple of three list[float] – (curve_wind, curve_rotor_speed, curve_pitch), sorted by ascending wind speed, ready to pass to operating_points_from_curve().

Raises:
  • ValueError – If performance_table arrays differ in length, have fewer than two samples, contain a non-finite value, have a negative wind speed or rotor speed (negative collective blade pitch is allowed), or have a non-increasing wind-speed axis.

  • RuntimeError – If no performance_table is given, so there is no rotor-performance source to derive the curve from.

Return type:

tuple[list[float], list[float], list[float]]

operating_points_from_curve(wind_speeds, *, curve_wind, curve_rotor_speed, curve_pitch)[source]

Build operating points by interpolating a steady-state operating curve.

The turbine’s steady operating curve (rotor speed and collective pitch versus wind speed) is interpolated with a shape-preserving monotone cubic (PCHIP, which does not overshoot between samples) and evaluated at each requested wind speed.

Parameters:
  • wind_speeds (Sequence[float]) – Wind speeds (m/s) to produce operating points at.

  • curve_wind (Sequence[float]) – Strictly increasing wind-speed samples of the operating curve.

  • curve_rotor_speed (Sequence[float]) – Rotor speed (rpm) at each curve_wind sample.

  • curve_pitch (Sequence[float]) – Collective blade pitch (deg) at each curve_wind sample.

Returns:

list[OperatingPoint] – One operating point per requested wind speed, named "ws<speed>".

Raises:

ValueError – If the curve arrays differ in length or have fewer than two points, the curve wind speeds are not strictly increasing, any curve value is non-finite, or a requested wind speed is non-finite or falls outside the curve’s range (interpolation only — extrapolation is not performed).

Return type:

list[OperatingPoint]

End-to-end linearization driver: a turbine model and a sweep to .lin files.

The LinearizationDriver ties the pieces together. Given a template OpenFAST case and a list of operating points, it copies the case to a fresh directory per point, configures each copy for linearization at that point (vane.run.configure_linearization()), runs OpenFAST (vane.run.run_openfast()), and collects the resulting .lin files — which feed straight into the parsing and modal pipeline. This closes the loop from a turbine model to a Campbell diagram / load (QoI) sweep / mode shapes, all from VANE.

A case is copied per operating point so that shared sub-files (a common InflowWind or ElastoDyn deck) are never mutated across points. By default the case’s own directory is the copy root; pass template_root to a higher directory when the deck references files outside it (e.g. ../5MW_Baseline/...). Only the files the deck references are copied (vane.run.deck_files.copy_template_files()), at their paths relative to template_root, so a broad root such as a whole regression-test suite costs only the deck’s own inputs per case.

class CaseProvenance(name, wind_speed, rotor_speed_rpm, blade_pitch_deg, case_dir, returncode, succeeded, n_lin, attempts=1, converged=True, trim_tol=0.0, twr_kdmp=0.0, bld_kdmp=0.0, trim_gain_torque=0.0, trim_gain_pitch=0.0, settle_assessed=None, settle_converged=None, settle_translational_velocity_rms=None, settle_angular_velocity_rms=None, settle_time=None, settle_iterations=None, settle_attempts=None, settle_criterion=None, settle_confirmation=None, settle_curve_rotor_speed_rpm=None, settle_curve_pitch_deg=None, settle_curve_status=None, operating_point_residual=None, sim_time=None, failure_mode=None, attempt_history=(), fallback_overrides=None, radiation_fit=None)[source]

Bases: object

Per-operating-point record within a sweep’s provenance.

Parameters:
  • name (str) – The case-directory name for the operating point.

  • wind_speed (float) – Hub-height wind speed (m/s).

  • rotor_speed_rpm (float) – Rotor speed (rpm).

  • blade_pitch_deg (float) – Collective blade pitch (deg).

  • case_dir (str) – The case directory the run was written to.

  • returncode (int) – OpenFAST exit status (0 on success).

  • succeeded (bool) – Whether OpenFAST exited successfully.

  • n_lin (int) – Number of full-system .lin files produced.

  • attempts (int) – How many OpenFAST runs the retry ladder used for this point.

  • converged (bool) – Whether the final attempt converged to a trustworthy linearization.

  • trim_tol (float) – The as-run trim tolerance for this case (escalated above the campaign base when the case was retried).

  • twr_kdmp (float) – The as-run tower/blade trim damping for this case (escalated on retry).

  • bld_kdmp (float) – The as-run tower/blade trim damping for this case (escalated on retry).

  • trim_gain_torque (float) – The as-run torque/pitch trim gains for this case. trim_gain_torque is the effective torque gain (the escalated override on a retry, else rated_generator_torque / 10 when derived, else the explicit value). Recorded per case because the gain is a per-case escalation ignored by the campaign-wide settings record.

  • trim_gain_pitch (float) – The as-run torque/pitch trim gains for this case. trim_gain_torque is the effective torque gain (the escalated override on a retry, else rated_generator_torque / 10 when derived, else the explicit value). Recorded per case because the gain is a per-case escalation ignored by the campaign-wide settings record.

  • settle_* (optional) – Platform-velocity settle evidence carried by a point produced through vane.run.solve_steady_state(); None for hand-authored curve points.

  • settle_criterion (str or None) – The criterion that accepted (or rejected) the settle: "velocity" or "mean_drift" (see vane.run.solve_steady_state()).

  • settle_confirmation (SettleConfirmation or None) – For a two-stage settle with temporary platform damping, the damped stage’s evidence and the undamped confirmation’s outcome; None otherwise.

  • settle_curve_rotor_speed_rpm (optional) – For a settle seeded from an operating curve, the curve’s rotor speed (rpm) and pitch (deg) at this wind speed and the settled-versus-curve status ("pass", "flag" or "unavailable"; see vane.run.SettleCurveCheck); None otherwise.

  • settle_curve_pitch_deg (optional) – For a settle seeded from an operating curve, the curve’s rotor speed (rpm) and pitch (deg) at this wind speed and the settled-versus-curve status ("pass", "flag" or "unavailable"; see vane.run.SettleCurveCheck); None otherwise.

  • settle_curve_status (optional) – For a settle seeded from an operating curve, the curve’s rotor speed (rpm) and pitch (deg) at this wind speed and the settled-versus-curve status ("pass", "flag" or "unavailable"; see vane.run.SettleCurveCheck); None otherwise.

  • operating_point_residual (OperatingPointResidualAudit or None) – The physical acceptance audit, when the check was enabled.

  • sim_time (float or None) – The as-run sim_time (TMax) setting, extended above the campaign base when a trim that ran out of time was retried; None keeps the deck’s value.

  • failure_mode (str or None) – The final attempt’s classified failure; None when it converged.

  • attempt_history (tuple[LinearizationAttempt, ...]) – The per-attempt records of the retry ladder (see LinearizationAttempt).

  • fallback_overrides (dict[str, object] or None) – For a case whose final attempt ran the retry policy’s final_fallback, the as-run values of its fallback_fields; None otherwise.

  • radiation_fit (dict[str, object] or None) – The state-space radiation file generated in the case copy (vane.run.RadiationFitRecord.to_dict(): the .ss path, the WAMIT .1 source, the state count and the per-entry fit errors); None when none was generated.

  • settle_assessed (bool | None)

  • settle_converged (bool | None)

  • settle_translational_velocity_rms (float | None)

  • settle_angular_velocity_rms (float | None)

  • settle_time (float | None)

  • settle_iterations (int | None)

  • settle_attempts (tuple[SettleAttempt, ...] | None)

name: str
wind_speed: float
rotor_speed_rpm: float
blade_pitch_deg: float
case_dir: str
returncode: int
succeeded: bool
n_lin: int
attempts: int = 1
converged: bool = True
trim_tol: float = 0.0
twr_kdmp: float = 0.0
bld_kdmp: float = 0.0
trim_gain_torque: float = 0.0
trim_gain_pitch: float = 0.0
settle_assessed: bool | None = None
settle_converged: bool | None = None
settle_translational_velocity_rms: float | None = None
settle_angular_velocity_rms: float | None = None
settle_time: float | None = None
settle_iterations: int | None = None
settle_attempts: tuple[SettleAttempt, ...] | None = None
settle_criterion: str | None = None
settle_confirmation: SettleConfirmation | None = None
settle_curve_rotor_speed_rpm: float | None = None
settle_curve_pitch_deg: float | None = None
settle_curve_status: str | None = None
operating_point_residual: OperatingPointResidualAudit | None = None
sim_time: float | None = None
failure_mode: str | None = None
attempt_history: tuple[LinearizationAttempt, ...] = ()
fallback_overrides: dict[str, object] | None = None
radiation_fit: dict[str, object] | None = None
class LinearizationAcceptancePolicy(max_mean_translational_speed=0.001, max_mean_angular_speed=0.0001, min_rotating_azimuths=12)[source]

Bases: object

Physical residual limits for a floating operating point.

Parameters:
  • max_mean_translational_speed (float) – Maximum norm of the azimuth-mean platform translational velocity, in m/s.

  • max_mean_angular_speed (float) – Maximum norm of the azimuth-mean platform angular velocity, in rad/s.

  • min_rotating_azimuths (int) – Minimum number of distinct azimuth samples needed to distinguish periodic motion from secular drift for a rotating point. The default is 12 and values below four are prohibited so blade-passing motion cannot alias into the mean. A non-rotating point needs only one linearization.

max_mean_translational_speed: float = 0.001
max_mean_angular_speed: float = 0.0001
min_rotating_azimuths: int = 12
class LinearizationAttempt(attempt, failure_mode, reason, openfast_message, failure_time, returncode, n_lin, log_file, trim_tol, twr_kdmp, bld_kdmp, trim_gain_torque, trim_gain_pitch, sim_time, tmax, fallback=False)[source]

Bases: object

The record of one OpenFAST run of an operating point’s retry ladder.

Parameters:
  • attempt (int) – The 1-based attempt number.

  • failure_mode (str or None) – The classified failure (an OpenFastFailureMode value, or "physical_residual_rejected" when the acceptance check rejected a completed run). None when the attempt converged and was accepted.

  • reason (str or None) – A one-line description of the failure; None on success.

  • openfast_message (str) – The relevant excerpt of OpenFAST’s output (its fatal error message, or its last lines); empty when none applies.

  • failure_time (float or None) – The simulation time (s) of OpenFAST’s fatal error, when it reported one.

  • returncode (int) – The OpenFAST exit status (124 for a run killed by the timeout).

  • n_lin (int) – The number of full-system .lin files the attempt wrote.

  • log_file (str or None) – The file in the case directory holding the attempt’s full OpenFAST standard output and error, <deck stem>.attempt<k>.log; None if it could not be written.

  • trim_tol (float) – The attempt’s trim tolerance, trim damping and effective trim gains.

  • twr_kdmp (float) – The attempt’s trim tolerance, trim damping and effective trim gains.

  • bld_kdmp (float) – The attempt’s trim tolerance, trim damping and effective trim gains.

  • trim_gain_torque (float) – The attempt’s trim tolerance, trim damping and effective trim gains.

  • trim_gain_pitch (float) – The attempt’s trim tolerance, trim damping and effective trim gains.

  • sim_time (float or None) – The attempt’s sim_time setting (None keeps the deck’s TMax).

  • tmax (float or None) – The TMax the attempt’s configured deck actually carried, when readable.

  • fallback (bool) – Whether the attempt ran with the retry policy’s final_fallback settings.

attempt: int
failure_mode: str | None
reason: str | None
openfast_message: str
failure_time: float | None
returncode: int
n_lin: int
log_file: str | None
trim_tol: float
twr_kdmp: float
bld_kdmp: float
trim_gain_torque: float
trim_gain_pitch: float
sim_time: float | None
tmax: float | None
fallback: bool = False
class LinearizationDriver(fst_path, output_dir, settings=<factory>, executable='openfast', template_root=None, link_inputs=True, template_copy='referenced', validate=True, retry_policy=<factory>, trim_tuning=None, acceptance_policy=None, auto_configuration=None)[source]

Bases: object

Generate, run, and collect linearizations across operating points.

Parameters:
  • fst_path (pathlib.Path) – The template case’s primary .fst deck.

  • output_dir (pathlib.Path) – Directory under which per-operating-point case directories are created. Must not be inside template_root (nor contain it), to avoid a recursive copy.

  • settings (LinearizationSettings) – Linearization tuning applied to every case.

  • executable (str or pathlib.Path) – OpenFAST executable name or path.

  • template_root (pathlib.Path or None) – Directory the case is copied from per operating point; defaults to fst_path’s parent. Set to a higher directory when the deck references files outside its own folder so those are copied too. File paths relative to it are preserved in each case directory.

  • link_inputs (bool) – When True (the default), each case is created by hard-linking the template files rather than copying their bytes, so a sweep’s heavy read-only inputs (e.g. WAMIT hydrodynamic data, airfoil tables) are shared at the inode level and a many-point sweep does not duplicate hundreds of megabytes per point. Each case still gets its own complete tree and its own outputs; the small deck files are edited via FastInputFile.write()’s atomic replace, which breaks the link so the template is never modified. Falls back to a full byte copy automatically when hard-linking is unsupported (e.g. the output directory is on a different filesystem from the template). Set to False to always copy.

  • template_copy ({"referenced", "tree"}) – What each case receives from template_root. "referenced" (the default) copies only the files the deck references, found recursively from the primary deck (deck_file_closure()); unrelated cases and stale outputs elsewhere under the root are left behind. When that walk cannot vouch for completeness (a relative reference outside template_root, an unreadable input) the whole tree is copied instead, with a logged warning. "tree" always copies the whole template tree.

  • validate (bool) – When True (the default), each configured case is checked with validate_deck() before OpenFAST is launched, so a deck that cannot linearize as configured (a DLL controller, active waves, missing state-space files, …) is rejected up front instead of failing late inside a long settle/trim run. Set to False to skip the pre-flight check.

  • trim_tuning (TrimTuningPolicy or None) – When set, each operating point’s starting trim settings are tuned for its wind-speed region (tune_trim_settings()) before the retry ladder runs: an at/above-rated point starts from the looser-tolerance, higher-damping floors that such points need, so it is more likely to converge on the first attempt instead of after a reactive retry. None (the default) starts every point from settings unchanged. Tuning only raises the per-case trim_tol/twr_kdmp/bld_kdmp toward the floors, so provenance is unaffected.

  • acceptance_policy (LinearizationAcceptancePolicy or None) – Optional post-run physical check for a floating operating point. When set, VANE averages the ElastoDyn platform-rate states over the azimuth sweep and rejects a nominally successful trimmed or explicit-time result with excessive secular translational or angular drift. This rejection is not sent through the normal retry ladder because that ladder loosens TrimTol and would make a residual drift worse; the caller must tighten the base trim or improve the warm start.

  • auto_configuration (dict[str, object] or None) – The record of values inferred from the deck and curve (set by vane.run.run_operating_sweep()), copied into provenance().

  • retry_policy (LinearizationRetryPolicy)

Raises:

ValueError – If fst_path is not within template_root, or output_dir and template_root are nested in one another.

fst_path: Path
output_dir: Path
settings: LinearizationSettings
executable: str | Path = 'openfast'
template_root: Path | None = None
template_copy: TemplateCopyMode = 'referenced'
validate: bool = True
retry_policy: LinearizationRetryPolicy
trim_tuning: TrimTuningPolicy | None = None
acceptance_policy: LinearizationAcceptancePolicy | None = None
auto_configuration: dict[str, object] | None = None
case_dir(operating_point)[source]

Return the case directory for an operating point.

Parameters:

operating_point (OperatingPoint)

Return type:

Path

validated_workspace_dir(name)[source]

Return output_dir/name, rejecting one that overlaps the template tree.

A workspace directory (a linearization case or an auxiliary settle case) that is, contains, or sits inside the template root would have the template deleted by the clean-copy step, which removes the destination first. This is possible when the template lives under the output workspace and a generated name collides with a template directory. Reject any such overlap up front with a clear error, so directory creation can never destroy the source tree.

Parameters:

name (str) – The directory name to create under output_dir.

Returns:

pathlib.Path – The resolved, validated destination directory.

Raises:

ValueError – If the destination is, contains, or is inside the template root, or name is a device name Windows reserves (CON, NUL, COM1, …, with or without an extension).

Return type:

Path

generate_case(operating_point)[source]

Copy the template case and configure it for an operating point.

Parameters:

operating_point (OperatingPoint) – The operating point to set up.

Returns:

pathlib.Path – The configured primary .fst deck inside the new case directory.

Return type:

Path

run_case(operating_point, *, timeout=None)[source]

Generate, run, and collect one operating point.

Parameters:
  • operating_point (OperatingPoint) – The operating point to run.

  • timeout (float or None, optional) – Per-run timeout in seconds.

Returns:

SweepCase – The generated case and its run outcome.

Raises:

vane.run.DeckValidationError – If validate is set and the configured deck cannot be linearized (raised before OpenFAST is launched).

Return type:

SweepCase

Notes

Called directly, an unexpected exception (a file-system error while the case is copied or configured, say) propagates. Within run_sweep() the same exception is instead recorded as a failed case (see there).

run_sweep(operating_points, *, timeout=None, max_workers=None, strict=False)[source]

Run a whole sweep of operating points.

Each operating point is an independent OpenFAST run in its own case directory, so a sweep can run several points concurrently. OpenFAST itself is single-threaded here, so max_workers processes use max_workers cores; on a multi-core workstation the sweep wall-time approaches that of a single operating point rather than their sum.

Parameters:
  • operating_points (Sequence[OperatingPoint]) – The operating points to run, in order.

  • timeout (float or None, optional) – Per-run timeout in seconds.

  • max_workers (int or None, optional) – Number of operating points to run concurrently. None (the default) uses default_max_workers(), reserving one CPU for the machine. Results are returned in the input order regardless.

  • strict (bool, optional) – When True, raise SweepCaseError once every case has finished if any case raised an unexpected exception; all the cases are attached as its cases. Default False: such a case is only recorded and logged.

Returns:

list[SweepCase] – One result per operating point, in the input order.

Raises:
  • SweepCaseError – With strict=True, after all the cases have finished, when a case raised an unexpected exception.

  • ValueError – If two operating points map to the same case directory, a case directory would overlap the template root or use a reserved device name, or max_workers is less than 1. Names are compared case-insensitively (casefold), because the default Windows and macOS filesystems are case insensitive and two such cases would otherwise share – and delete – one directory. All such checks run up front, before any case is launched.

  • vane.run.DeckValidationError – If validate is set and a configured deck cannot be linearized: an input error found before that case’s OpenFAST run is launched.

Return type:

list[SweepCase]

Notes

One operating point never discards the others. An OpenFAST failure is classified and retried (see LinearizationRetryPolicy); an unexpected exception while a case is generated, run or collected – a file-system error that outlasted the transient-error retry, an unreadable sub-file – ends that point’s retry ladder and is recorded as a failed case: converged is False, failure_mode is "case_error" (CASE_ERROR), the final LinearizationAttempt carries the exception type and message as its reason (and so does the provenance record), its attempt log holds the traceback, and the result’s return code is -1. The earlier attempts’ records are kept. The argument checks listed under Raises are boundary errors and are still raised, the ValueError ones before any case is copied or launched.

provenance(cases, *, timestamp=None)[source]

Build a reproducibility record for a completed sweep.

Records the LinearizationSettings every case was configured with, the library version and executable, and each case’s outcome, so the conditions a set of .lin files was produced under travel with them.

The recorded settings and executable are taken from the cases’ own snapshots (frozen when each case ran), not from the driver’s current attributes, so the record stays faithful even if the driver was reconfigured after the sweep.

Parameters:
Returns:

RunProvenance – The settings, environment, and per-case record for the sweep.

Raises:

ValueError – If the cases differ on a campaign-wide setting (anything other than the per-case retry-escalated trim tolerance/damping) or on the executable, so no single run configuration describes them.

Return type:

RunProvenance

class LinearizationRetryPolicy(max_attempts=4, trim_tol_factor=3.0, damping_factor=2.0, min_retry_damping=5000.0, trim_gain_factor=4.0, pitch_trim_gain_factor=0.2, tmax_factor=2.0, max_sim_time=3600.0, early_failure_time=1.0, damping_backoff_factor=0.5, adaptive=True, final_fallback=<function above_rated_torque_trim>, fallback_modes=frozenset({OpenFastFailureMode.DIVERGENCE, OpenFastFailureMode.MOTORING, OpenFastFailureMode.ROTOR_OVERSPEED}), fallback_fields=('above_rated_trim', ))[source]

Bases: object

Adaptive retry ladder for OpenFAST linearization failures.

A case is retried when OpenFAST exits non-zero, when a rotating CalcSteady run returns fewer full-system .lin files than NLinTimes or logs that it forced the linearization, or when the run times out. Each failed attempt is classified (classify_openfast_output()) and the next attempt’s settings are chosen from that failure, starting from the failed attempt’s own settings so that successive remedies compose (next_attempt_settings()):

  • trim not converged (the forced single .lin): the trim ran out of time. TMax is extended to min(TMax * tmax_factor, max_sim_time) and TrimTol is loosened by trim_tol_factor, but not beyond PERIODIC_TRIM_TOL (1e-2) unless it already was: a looser trim stops on a rotor speed that is not periodic enough for MBC3. The pitch trim gain is lowered by pitch_trim_gain_factor: near rated, the IEA 15- and 22-MW pitch trims oscillate without settling at the default gain and converge at about 1e-4 rad/(rad/s). The torque gain and the trim damping are left alone: raising the gain makes a slow trim diverge, and the damping biases the trimmed point and, on every reference deck it was tried on (IEA 3.4-MW, IEA 15-MW semi), made the rotor motor within 0.3 s.

  • non-periodic sweep: every .lin was written but the rotor speed varies across the azimuth files by more than MBC3 accepts (a loosened TrimTol let a drifting response through). TrimTol is tightened back to the base value and TMax is extended as for a trim that ran out of time.

  • motoring (u%HSS_Spd is negative): the trim overshot. The pitch gain is multiplied by pitch_trim_gain_factor and the torque gain divided by trim_gain_factor. When the failure happens within early_failure_time of the start, the trim damping itself destabilised the (light) blades, so it is removed (twr_kdmp = bld_kdmp = 0); otherwise it is kept.

  • rotor overspeed and divergence: the gains are backed off as for motoring and the trim damping is multiplied by damping_backoff_factor (removed for an early failure).

  • initialisation error: an input problem. The case is not retried.

  • timeout, an incomplete explicit-time sweep, or an unrecognised non-zero exit: the generic escalation, which loosens TrimTol by trim_tol_factor, raises the trim damping to max(previous, min_retry_damping) * damping_factor, multiplies the torque trim gain by trim_gain_factor and the pitch trim gain by pitch_trim_gain_factor.

A too-low torque gain is a common cause of a below-rated trim failure (per Jonkman), which is why the generic step raises it. The pitch trim is a PI law whose proportional gain is 1000 * TrimGain, so an above-rated failure is usually overshoot into a growing oscillation, and every remedy except the time extension lowers that gain. Exhaustion returns the final failed result so callers still get the captured diagnostics.

Parameters:
  • max_attempts (int) – Total OpenFAST runs per operating point, including the first (default 4). Most failed attempts are cheap: a divergence fails within seconds, and only a trim that runs out of time uses its whole TMax.

  • trim_tol_factor (float) – Factor that loosens TrimTol on a trim-not-converged or generic retry.

  • damping_factor (float) – Factor that raises the trim damping on a generic retry.

  • min_retry_damping (float) – Floor the trim damping is raised to before damping_factor applies on a generic retry.

  • trim_gain_factor (float) – Factor that raises the torque trim gain on a generic retry, and divides it on a motoring, overspeed or divergence retry.

  • pitch_trim_gain_factor (float) – Factor (below one by default) that multiplies the pitch trim gain on every retry except a trim-not-converged one.

  • tmax_factor (float) – Factor (at least one) that extends TMax on a trim-not-converged retry. The extended value is written as sim_time; 1 disables the extension.

  • max_sim_time (float) – Cap (s) on the extended TMax. A deck already longer than the cap is never shortened.

  • early_failure_time (float) – A motoring, overspeed or divergence failure at or before this simulation time (s) is attributed to the trim damping, which the next attempt removes.

  • damping_backoff_factor (float) – Factor in [0, 1] that lowers the trim damping after a later overspeed or divergence.

  • adaptive (bool) – True (the default) chooses each retry from the classified failure. False uses the fixed schedule of settings_for_attempt(), which applies the generic escalation on every retry whatever the failure. An initialisation error is never retried in either mode.

  • final_fallback (callable or None) – A last resort for a trim that the gain and damping remedies cannot stabilise. The default, above_rated_torque_trim(), switches an at/above-rated point from pitch to torque trim at the curve’s pitch; None disables it. When set, the last attempt (max_attempts) that follows a failure in fallback_modes runs with final_fallback(settings), where settings is what the ladder would otherwise have chosen (so removed damping or an extended TMax carry over). It must return a LinearizationSettings that differs from its argument only in retry-escalated fields and fallback_fields. The attempt is flagged fallback=True in SweepCase.attempt_history.

  • fallback_modes (frozenset[OpenFastFailureMode]) – The failures that trigger final_fallback; by default the unstable-trim modes (motoring, rotor overspeed and divergence).

  • fallback_fields (tuple[str, ...]) – The LinearizationSettings fields final_fallback may change beyond the retry-escalated ones. They are treated as per-case values, excluded from the campaign-wide provenance comparison and recorded per case in CaseProvenance.fallback_overrides.

max_attempts: int = 4
trim_tol_factor: float = 3.0
damping_factor: float = 2.0
min_retry_damping: float = 5000.0
trim_gain_factor: float = 4.0
pitch_trim_gain_factor: float = 0.2
tmax_factor: float = 2.0
max_sim_time: float = 3600.0
early_failure_time: float = 1.0
damping_backoff_factor: float = 0.5
adaptive: bool = True
final_fallback()

Return settings with at/above-rated points trimmed by generator torque.

The default LinearizationRetryPolicy.final_fallback. A pitch trim that the gain and damping remedies cannot stabilise (large rotors near rated, where the trim’s PI loop oscillates until the rotor motors) is replaced by a torque trim at the operating point’s pitch, which converges like a below-rated point. A below-rated point already uses torque trim, so the switch leaves it unchanged.

Parameters:

settings (LinearizationSettings) – The settings the retry ladder chose for the final attempt.

Returns:

LinearizationSettings – A copy with above_rated_trim="torque".

Return type:

LinearizationSettings

fallback_modes: frozenset[OpenFastFailureMode] = frozenset({OpenFastFailureMode.DIVERGENCE, OpenFastFailureMode.MOTORING, OpenFastFailureMode.ROTOR_OVERSPEED})
fallback_fields: tuple[str, ...] = ('above_rated_trim',)
settings_for_attempt(base, attempt)[source]

Return the fixed (non-adaptive) settings for a 1-based attempt number.

Attempt 1 is base unchanged. Attempt k > 1 is k - 1 generic escalation steps from base:

  • trim_tol = min(base * trim_tol_factor**(k-1), MAX_TRIM_TOL); the clamp is logged and the as-run value is recorded per case in the provenance;

  • twr_kdmp/bld_kdmp = max(base, min_retry_damping) * damping_factor**(k-1), so every retry raises the damping above both the base and the floor;

  • torque trim gain = base * trim_gain_factor**(k-1);

  • pitch trim gain = base * pitch_trim_gain_factor**(k-1).

The driver follows this schedule when adaptive is False.

Parameters:
  • base (LinearizationSettings) – The operating point’s starting settings.

  • attempt (int) – The 1-based attempt number, at most max_attempts.

Returns:

LinearizationSettings – The settings for that attempt.

Raises:

ValueError – If attempt is not an integer in [1, max_attempts] or the escalation leaves the finite positive range.

Return type:

LinearizationSettings

next_attempt_settings(base, previous, diagnosis, *, attempt, previous_tmax=None)[source]

Return the settings for the retry that follows a classified failure.

Parameters:
  • base (LinearizationSettings) – The operating point’s starting settings (used by the fixed schedule).

  • previous (LinearizationSettings) – The settings of the attempt that failed; adaptive remedies start here.

  • diagnosis (OpenFastDiagnosis) – The classified failure of that attempt.

  • attempt (int) – The 1-based number of the attempt being configured, in [2, max_attempts].

  • previous_tmax (float or None, optional) – The TMax the failed attempt actually ran with (read from its deck). The extension starts from it when previous.sim_time is unset, and from MIN_CALC_STEADY_TMAX when neither is known.

Returns:

LinearizationSettings or None – The next attempt’s settings, or None when the failure is not retriable (an initialisation error or a non-finite linearization).

Raises:

ValueError – If attempt is not an integer in [2, max_attempts].

Return type:

LinearizationSettings | None

uses_fallback(diagnosis, *, attempt)[source]

Return whether attempt runs with the final_fallback settings.

Parameters:
  • diagnosis (OpenFastDiagnosis) – The classified failure of the previous attempt.

  • attempt (int) – The 1-based number of the attempt being configured.

Returns:

bool – True when a fallback is configured, attempt is the last one (max_attempts, at least 2) and the failure is retriable and one of fallback_modes.

Return type:

bool

class RunProvenance(vane_version, created_at, executable, settings, cases, auto_configuration=None)[source]

Bases: object

Reproducibility record for a linearization sweep.

Captures the exact LinearizationSettings applied to every case — so the settings a set of .lin files was produced under are recorded alongside them, not just remembered — together with the library version, the executable, and the per-case outcome.

Parameters:
  • vane_version (str) – Version of the library that drove the sweep.

  • created_at (str) – ISO-8601 timestamp of when the record was made.

  • executable (str) – The OpenFAST executable name or path used.

  • settings (dict[str, object]) – Every LinearizationSettings field and its value.

  • cases (tuple[CaseProvenance, ...]) – Per-operating-point provenance, in run order.

  • auto_configuration (dict[str, object] or None, optional) – The values vane.run.run_operating_sweep() inferred from the deck and curve, each with its source, and the warnings raised while inferring them (see vane.run.AutoConfiguration); None when nothing was inferred.

vane_version: str
created_at: str
executable: str
settings: dict[str, object]
cases: tuple[CaseProvenance, ...]
auto_configuration: dict[str, object] | None = None
to_dict()[source]

Return the record as a JSON-serializable dictionary.

Return type:

dict[str, object]

write(path)[source]

Write the record to path as JSON and return the path written.

Parameters:

path (str | Path)

Return type:

Path

class SweepCase(operating_point, case_dir, fst_path, result, settings=<factory>, base_settings=<factory>, executable='openfast', attempts=1, converged=True, operating_point_residual=None, attempt_history=(), fallback_fields=(), radiation_fit=None)[source]

Bases: object

The generated case and run outcome for one operating point.

Parameters:
  • operating_point (OperatingPoint) – The operating point this case was generated for.

  • case_dir (pathlib.Path) – The per-operating-point directory the case was copied into.

  • fst_path (pathlib.Path) – The configured primary .fst deck inside case_dir.

  • result (OpenFastResult) – The OpenFAST run outcome (return code, .lin files, output).

  • settings (LinearizationSettings) – A snapshot of the as-run settings this case was configured with (the final retry attempt’s, so a retried case carries its escalated trim/damping), frozen at run time so provenance reflects what actually produced the case even if the driver is later mutated.

  • base_settings (LinearizationSettings) – A snapshot of the campaign base settings this case escalated from (the driver’s settings at run time, before any per-case retry escalation). Frozen at run time so the campaign-wide configuration provenance records is independent of any later driver mutation. For an unretried case this equals settings.

  • executable (str) – The OpenFAST executable used for this case, captured at run time.

  • attempts (int) – How many OpenFAST runs the retry ladder used to produce this result (1 when the first run converged). The case carries the final attempt’s outcome.

  • converged (bool) – Whether the final attempt converged: OpenFAST exited successfully, produced the requested number of full-system .lin files, did not force the linearization without finding the steady state, and every .lin file holds only finite numbers. False means the ladder was exhausted without a trustworthy linearization for this point.

  • operating_point_residual (OperatingPointResidualAudit or None) – Optional independent six-DOF platform-drift audit. None when the driver’s physical acceptance check was disabled.

  • attempt_history (tuple[LinearizationAttempt, ...]) – One record per OpenFAST run, in order: the classified failure, the settings it ran with and the log file holding its full output. Empty for a case built by hand rather than by LinearizationDriver.run_case().

  • fallback_fields (tuple[str, ...]) – The retry policy’s fallback_fields when the case ran, snapshotted so provenance reads the case with the policy that produced it even if the driver’s policy is replaced or mutated afterwards.

  • radiation_fit (RadiationFitRecord or None) – With hydro_radiation_fit, the state-space radiation file the final attempt generated in the case copy (vane.run.ensure_radiation_state_space()); None otherwise, or when the copy already had the file.

operating_point: OperatingPoint
case_dir: Path
fst_path: Path
result: OpenFastResult
settings: LinearizationSettings
base_settings: LinearizationSettings
executable: str = 'openfast'
attempts: int = 1
converged: bool = True
operating_point_residual: OperatingPointResidualAudit | None = None
attempt_history: tuple[LinearizationAttempt, ...] = ()
fallback_fields: tuple[str, ...] = ()
radiation_fit: RadiationFitRecord | None = None
property failure_mode: str | None

Return the final attempt’s classified failure, or None on success.

property succeeded: bool

Return whether OpenFAST exited successfully for this case.

property lin_files: list[Path]

Return the .lin files produced for this case.

property n_lin: int

Return the number of full-system .lin files produced.

exception SweepCaseError(message, *, cases, result=None)[source]

Bases: RuntimeError

Raised by a strict sweep after a case raised an unexpected exception.

A linearization sweep records an unexpected exception in one operating point (a file-system error while its case is copied or configured, say) as a failed SweepCase whose failure_mode is "case_error" and carries on with the other points. With strict=True (LinearizationDriver.run_sweep()) or strict_cases=True (vane.run.run_operating_sweep()) it then raises this error – but only after every other case has finished, with all of them attached, so no completed work is lost. The first case’s exception is chained as __cause__.

Parameters:
  • message (str) – The error message.

  • cases (list[SweepCase]) – Every case of the sweep, in input order, the failed ones included.

  • result (OperatingSweepResult or None, optional) – The full sweep result, when raised by vane.run.run_operating_sweep().

Return type:

None

cases

As given.

Type:

list[SweepCase]

result

As given.

Type:

OperatingSweepResult or None

default_max_workers(*, reserve_cpus=1)[source]

Return a CPU-aware default worker count for independent OpenFAST cases.

Parameters:

reserve_cpus (int)

Return type:

int

Configure an OpenFAST deck for a linearization at one operating point.

Given a primary .fst deck and an OperatingPoint, this sets the linearization controls in the primary file and the operating point in the ElastoDyn and InflowWind sub-files, following the OpenFAST recommended practice:

  • enable Linearize and write outputs/inputs at high precision (OutFmt);

  • for a rotating point, trim to a periodic steady state (CalcSteady = True) with NLinTimes azimuth snapshots, choosing torque trim below rated wind speed and pitch trim at/above it (TrimCase 2 vs 3) — the standard region split;

  • when explicit absolute times are supplied, disable CalcSteady and linearize at those times; otherwise a standstill / parked point takes one snapshot at time zero;

  • set the rotor speed and blade pitch in ElastoDyn and the steady wind speed in InflowWind, with uniform inflow (power-law shear exponent PLExp = 0) by default – the same steady uniform wind the settle run (solve_steady_state()) uses, so a settled operating point is linearized in the inflow it was settled in.

For a full floating (aero-hydro-servo-elastic) deck, LinearizationSettings.offshore() additionally makes the offshore modules linearization-compatible — still water in SeaState, state-space hydrodynamics in HydroDyn with the 2nd-order forces off, quasi-steady aero with no dynamic wake in AeroDyn, and a trim-compatible ServoDyn (the Bladed-style DLL controller is not linearizable, so CalcSteady’s trim replaces it, with region-aware generator torque — zero below rated, constant above). For a trimmed rotating point the generator DOF (GenDOF) is also ensured on, since the trim regulates rotor speed through it. These edits are skipped for any module a deck does not have, so an onshore deck is unaffected.

The deck and its sub-files are edited in place with the format-preserving FastInputFile. For a multi-point sweep, copy the case to a per-operating-point directory first so that shared sub-files (e.g. a common InflowWind deck) are not mutated across points — the linearization driver does this.

class LinearizationSettings(n_lin_times=36, calc_steady=True, trim_tol=0.001, trim_gain_torque=100.0, trim_gain_pitch=0.0002, rated_wind_speed=None, lin_inputs=1, lin_outputs=1, module_linearizations=False, full_jacobians=False, out_fmt='ES20.11E3', twr_kdmp=0.0, bld_kdmp=0.0, sim_time=None, linearization_times=None, wave_mod=None, hydro_radiation_mode=None, hydro_excitation_mode=None, disable_second_order_hydro=False, unsteady_aero_mode=None, dynamic_wake_mode=None, trim_controller=False, wind_shear_exponent=0.0, structural_dofs=None, rated_generator_torque=None, trim_gain_torque_override=None, above_rated_trim='pitch', ua_end_radius=None, hydro_radiation_fit=False)[source]

Bases: object

Tunable settings for configuring a linearization deck.

Parameters:
  • n_lin_times (int) – Number of linearizations per rotor revolution for a rotating point (NLinTimes); 36 (every 10 degrees of azimuth) is the OpenFAST recommendation.

  • calc_steady (bool) – Whether a rotating point trims to a periodic steady state before linearizing (CalcSteady). A standstill/parked point always uses a single linearization regardless of this flag.

  • trim_tol (float) – Rotor-speed convergence tolerance for the trim (TrimTol). Must be positive and at most MAX_TRIM_TOL (0.1): a larger tolerance no longer distinguishes a periodic steady state from a drifting transient. The default 1e-3 applies to a 63 m rotor; the automatic sweep configuration scales a value left at this default with the rotor radius (vane.run.auto_config.scaled_trim_tol()).

  • trim_gain_torque (float) – Proportional trim gain used below rated wind speed (torque trim, Nm/(rad/s)). Used only when rated_generator_torque is not given; must be positive (as must trim_gain_pitch). The default 100.0 is far too low for a utility-scale turbine — per Jonkman the torque trim gain should be ~a tenth of the rated generator torque (e.g. ~4000 for the NREL 5-MW), so supplying rated_generator_torque (which derives the gain) is strongly preferred over relying on this default.

  • trim_gain_pitch (float) – Trim gain used at/above rated wind speed (pitch trim, rad/(rad/s)). ServoDyn’s CalcSteady trim is a PI law: each time step adds TrimGain times the rotor-speed error to an integrator, and the pitch offset adds 1000 times that error on top, so the effective proportional gain is 1000 * TrimGain and the integral gain TrimGain / DT. The default 2e-4 applies to a 63 m rotor (the NREL 5-MW, which oscillates or motors above rated at 1e-3); the automatic sweep configuration scales a value left at this default with the rotor radius (vane.run.auto_config.scaled_trim_gain_pitch()).

  • above_rated_trim ({"pitch", "torque"}) – How CalcSteady trims an at/above-rated point. "pitch" (default, the documented OpenFAST practice) holds the generator torque constant and trims blade pitch. "torque" holds the blade pitch at the operating point’s value (the user’s rotor-performance curve) and trims generator torque with the torque gain, exactly as below rated. Torque trim converges where a pitch trim oscillates (large rotors near rated, e.g. the IEA 15- and 22-MW); the linearized plant is the same open-loop model at the curve’s pitch and rotor speed, and the trimmed torque’s departure from rated measures how far the curve and the deck disagree (see verify_operating_points()). The retry ladder switches to "torque" as its final fallback. ServoDyn’s flattened law stays engaged: in its region 3 the torque is the constant VS_RtTq with zero speed derivative, and the trim adds its offset on top, so the generator torque is one constant (VS_RtTq + offset) exactly as below rated (0 + offset), the linearized plant is unchanged, and the trim starts from rated torque rather than from zero.

  • rated_generator_torque (float or None) – Rated generator torque in N·m. When given, the torque trim gain is derived from it as rated_generator_torque / 10 (Jonkman’s rule of thumb), overriding trim_gain_torque; this is the dependable way to get a torque trim that converges below rated. It also supplies the constant generator torque (VS_RtTq) the above-rated pitch trim needs when trim_controller is on. None (default) falls back to trim_gain_torque for the gain and to the deck’s VS_RtTq for the torque – but only when the deck’s original VSContrl is 1 (the simple variable-speed law actually in use); a DLL or other controller deck carries a placeholder VS_RtTq, so an above-rated trimmed point on it requires this value. This value is preserved across retry attempts.

  • trim_gain_torque_override (float or None) – An explicit, already-resolved torque trim gain (Nm/(rad/s)) that takes precedence over both rated_generator_torque and trim_gain_torque in resolved_trim_gain_torque(). Set by the retry ladder (LinearizationRetryPolicy) to pin an escalated gain without discarding rated_generator_torque (which still drives VS_RtTq); normally left None.

  • rated_wind_speed (float or None) – Wind speed (m/s) separating the below-rated (torque-trim) and above-rated (pitch-trim) control regions of the turbine being linearized (e.g. 11.4 for the NREL 5-MW, about 10.6 for the IEA 15-MW). There is no turbine-independent default: it is required for any rotating point whose trim or controller handling depends on the region (calc_steady or trim_controller), and a clear error is raised when it is missing.

  • lin_inputs (int) – LinInputs switch (0=none, 1=standard, 2=all).

  • lin_outputs (int) – LinOutputs switch (0=none, 1=from OutList, 2=all). 1 is required for the load/QoI channels to appear in C/D.

  • module_linearizations (bool) – Set LinOutMod so OpenFAST writes per-module linearization files alongside the full-system files. This is required for module-specific physics models, including MoorDyn driven directly by measured fairlead kinematics.

  • full_jacobians (bool) – Set LinOutJac so OpenFAST also writes the unassembled module Jacobians. OpenFAST requires lin_inputs=lin_outputs=2 when this debug export is enabled. It is useful for auditing the exact module coupling paths used by a subsystem digital twin.

  • out_fmt (str) – Output number format (OutFmt); a high precision such as "ES20.11E3" is important for accurate eigenvalues.

  • twr_kdmp (float) – Tower and blade artificial damping used only during the steady-state trim.

  • bld_kdmp (float) – Tower and blade artificial damping used only during the steady-state trim.

  • sim_time (float or None) – TMax to set. None leaves the deck’s value unchanged, except that a CalcSteady trim is given at least MIN_CALC_STEADY_TMAX (600 s): a template deck’s placeholder TMax would end the trim before it converges. OpenFAST stops as soon as the trim converges, so the floor costs nothing.

  • linearization_times (tuple[float, ...] or None) – Absolute OpenFAST times in seconds at which to linearize when calc_steady=False. The values must be finite, non-negative, strictly increasing, no later than sim_time when it is supplied, and their count must equal n_lin_times. None retains the single-snapshot untrimmed behaviour. This path is intended for a rotating plant that has already been demonstrated to be at periodic equilibrium by an independent physical check.

  • wave_mod (int or None) – SeaState WaveMod to set (0 = still water, required for linearization). None leaves the deck unchanged. Setting still water also neutralises the wave options it leaves meaningless but that OpenFAST would reject: WaveDirMod and ConstWaveMod are set to 0 (required for any linearization), a wave-stretching model other than 0/1 (WaveStMod) is set to 0, MacCamy-Fuchs diffraction (MCFD) to 0, and a DEFAULT sub-surface current heading (CurrSSDir) is replaced by the deck’s WaveDir so the current keeps its direction.

  • hydro_radiation_mode (int or None) – HydroDyn RdtnMod / ExctnMod to set. Linearization rejects the convolution/DFT models, so use 0 (none) or 2 (state-space, which needs the .ss / .ssexctn files). None leaves the deck unchanged. When either is set and HydroDyn then requires RdtnDT to equal the glue-code DT (WAMIT potential flow with a radiation or state-space excitation model) but the deck holds another numeric step, RdtnDT is set to DEFAULT.

  • hydro_excitation_mode (int or None) – HydroDyn RdtnMod / ExctnMod to set. Linearization rejects the convolution/DFT models, so use 0 (none) or 2 (state-space, which needs the .ss / .ssexctn files). None leaves the deck unchanged. When either is set and HydroDyn then requires RdtnDT to equal the glue-code DT (WAMIT potential flow with a radiation or state-space excitation model) but the deck holds another numeric step, RdtnDT is set to DEFAULT.

  • hydro_radiation_fit (bool) – Generate a missing <PotFile>.ss radiation file in each linearization case copy from the WAMIT .1 data (vane.run.ensure_radiation_state_space()) before the case is validated and run; the template is never written, and the fit is recorded per case in the run provenance. Requires hydro_radiation_mode = 2. Default False. run_operating_sweep() sets it when its automatic configuration finds the file missing but the WAMIT data fittable within the acceptance limit.

  • disable_second_order_hydro (bool) – Zero HydroDyn’s 2nd-order force switches (MnDrift/NewmanApp/ DiffQTF/SumQTF) and switch off SeaState’s 2nd-order wave kinematics (WvDiffQTF/WvSumQTF), which linearization does not support.

  • unsteady_aero_mode (int or None) – AeroDyn UA_Mod to set; linearization requires 0 (quasi-steady) or 4/5/6. None leaves the deck unchanged. On a legacy-format AeroDyn deck (AFAeroMod/UAMod/WakeMod keywords), 0 sets AFAeroMod = 1 (steady airfoil aerodynamics), the only value that format can linearize; a linearizable unsteady model (4–6) cannot be expressed there and is reported by validate_deck().

  • dynamic_wake_mode (int or None) – AeroDyn DBEMT_Mod to set; linearization requires -1 (frozen), 0 (none), or 3. None leaves the deck unchanged. On a legacy-format AeroDyn deck, -1/0 replace a dynamic-BEMT wake (WakeMod = 2, not linearizable in that format) with plain BEMT (WakeMod = 1) and set the FrozenWake flag to True/False respectively; 3 cannot be expressed there and is reported by validate_deck().

  • ua_end_radius (float or None) – AeroDyn UAEndRad to set: the blade-radius fraction beyond which unsteady aerodynamics is switched off, in (0, 1]. None (the default) leaves the deck unchanged. AeroDyn keeps unsteady aerodynamics on every node whose radius fraction is not greater than UAEndRad, so UAEndRad = 1 keeps it on at the blade-tip node. With the Prandtl tip loss model that node’s inflow is fixed (full axial induction), and its unsteady-aerodynamics states can turn NaN or infinite in the linearization. A value below 1 (e.g. 0.99) ends unsteady aerodynamics inboard of the tip. The AeroDyn file must contain a UAEndRad line; a modern-format file without one is rejected before any file is written. Legacy-format files get the value only when they have the line.

  • trim_controller (bool) – Make ServoDyn trim-compatible (PCMode = 0, VSContrl = 1, HSSBrMode = 0, YCMode = 0, GenTiStr = GenTiStp = True, and a GenModel other than 1/2 reset to 1): the Bladed-style DLL controller is not linearizable, so CalcSteady’s own trim replaces it. For a trimmed rotating point the generator torque is then set up per region: below rated the generator is kept off (GenTiStr/TimGenOn beyond TMax) so the torque trim owns it; at/above rated the simple variable-speed law is flattened to a constant torque (VS_RtGnSp/VS_Rgn2K/VS_SlPc tiny, VS_RtTq set from rated_generator_torque when given, else the deck’s existing VS_RtTq) so the pitch trim converges. Flattening requires a positive constant torque; an above-rated point with neither rated_generator_torque nor a positive deck VS_RtTq is rejected (it would otherwise trim against zero torque). The deck VS_RtTq is trusted only when the deck’s original VSContrl is 1: a DLL-controlled deck (VSContrl = 5) or any other non-simple law carries a placeholder (e.g. 9999.9), so an above-rated point on such a deck without rated_generator_torque is rejected rather than trimmed at a wrong torque.

  • wind_shear_exponent (float or None) – InflowWind power-law shear exponent (PLExp) to write. The default 0.0 is steady uniform inflow, the convention of the settle run (solve_steady_state()), so a settled point is linearized in the same inflow it settled in; run_operating_sweep() passes this value to its settle runs too. None leaves the deck’s PLExp unchanged. Recorded with the other settings in the run provenance.

  • structural_dofs (dict[str, bool] or None) – ElastoDyn structural-DOF flags to set in the ED sub-file (e.g. {"YawDOF": False, "DrTrDOF": False}). None (the default) applies the linearization defaults: the blade and tower flexibility DOFs (FLEXIBILITY_DOFS: FlapDOF1, FlapDOF2, EdgeDOF, TwFADOF1, TwFADOF2, TwSSDOF1, TwSSDOF2) are enabled, so a deck shipped with rigid blades or tower still linearizes its blade and tower modes, and YawDOF and DrTrDOF are disabled. Disabling YawDOF removes the spurious free nacelle-yaw rigid-body mode (there is no yaw controller in a linearization); disabling DrTrDOF drops the drivetrain-torsion mode (~0.62 Hz) but improves CalcSteady trim convergence — a deliberate tradeoff. Supply a mapping to override: it is merged on top of the defaults, so {"DrTrDOF": True} re-enables the drivetrain-torsion mode while still disabling YawDOF, and {} keeps only the defaults. A flag the user explicitly lists but the ED deck lacks is an error; a default flag the deck lacks is silently skipped (the deck variant simply has no such DOF).

Notes

The module fields default to leave the deck unchanged (None / False), so an onshore or already-prepared deck is unaffected — except structural_dofs, whose None default disables YawDOF/DrTrDOF for a reliable linearization. For a full floating (aero-hydro-servo-elastic) deck, build the linearization-compatible set in one call with offshore().

n_lin_times: int = 36
calc_steady: bool = True
trim_tol: float = 0.001
trim_gain_torque: float = 100.0
trim_gain_pitch: float = 0.0002
rated_wind_speed: float | None = None
lin_inputs: int = 1
lin_outputs: int = 1
module_linearizations: bool = False
full_jacobians: bool = False
out_fmt: str = 'ES20.11E3'
twr_kdmp: float = 0.0
bld_kdmp: float = 0.0
sim_time: float | None = None
linearization_times: tuple[float, ...] | None = None
wave_mod: int | None = None
hydro_radiation_mode: int | None = None
hydro_excitation_mode: int | None = None
disable_second_order_hydro: bool = False
unsteady_aero_mode: int | None = None
dynamic_wake_mode: int | None = None
trim_controller: bool = False
wind_shear_exponent: float | None = 0.0
structural_dofs: dict[str, bool] | None = None
rated_generator_torque: float | None = None
trim_gain_torque_override: float | None = None
above_rated_trim: str = 'pitch'
ua_end_radius: float | None = None
hydro_radiation_fit: bool = False
is_below_rated(wind_speed)[source]

Return whether wind_speed lies in the below-rated (torque-trim) region.

Parameters:

wind_speed (float) – Operating-point wind speed (m/s).

Returns:

bool – True below rated_wind_speed, False at or above it.

Raises:

ValueError – If rated_wind_speed is not set.

Return type:

bool

resolved_trim_gain_torque()[source]

Return the torque trim gain (Nm/(rad/s)) to write into TrimGain.

An explicit trim_gain_torque_override (pinned by the retry ladder) takes precedence. Otherwise, when rated_generator_torque is given, the gain is derived from it as rated_generator_torque * 0.1 (Jonkman’s rule of thumb — a too-low torque trim gain is the most common cause of CalcSteady non-convergence below rated); otherwise the explicit trim_gain_torque is used.

Returns:

float – The torque trim gain to apply below rated wind speed.

Return type:

float

resolved_structural_dofs()[source]

Return the ElastoDyn DOF flags to apply, defaults merged with overrides.

The linearization defaults (blade and tower flexibility enabled, YawDOF and DrTrDOF disabled) are the base; a user-supplied structural_dofs mapping is merged on top, so an override re-enables or adds flags while the un-overridden defaults remain.

Returns:

dict[str, bool] – The effective ElastoDyn DOF-flag map (flag name -> enabled).

Return type:

dict[str, bool]

explicit_structural_dofs()[source]

Return the DOF-flag names the user explicitly requested (not just defaults).

A user-listed flag must exist in the ED deck (its absence is an error); a flag present only via the defaults is skipped when the deck lacks it.

Returns:

frozenset[str] – The flag names supplied in structural_dofs (empty when None).

Return type:

frozenset[str]

snapshot()[source]

Return a fully independent copy, deep-copying the mutable fields.

dataclasses.replace shallow-copies, so a plain replace snapshot would share the same structural_dofs dict object as the source. A later mutation of the source’s (or the originally-supplied) dict would then also mutate the snapshot, breaking snapshot independence. This copies the mutable structural_dofs mapping so the returned settings are decoupled from any later mutation.

Returns:

LinearizationSettings – A copy whose mutable fields share no object with this instance.

Return type:

LinearizationSettings

classmethod offshore(**overrides)[source]

Return settings prepared for a floating aero-hydro-servo-elastic deck.

Enables the full linearization-compatible module set — still water, state-space hydrodynamics, quasi-steady aero with a frozen wake (DBEMT_Mod = -1, valid for linearization across OpenFAST versions), the 2nd-order forces off and the controller replaced by the trim — that lets a real FOWT deck linearize. No trim damping is set: Twr_Kdmp/Bld_Kdmp do work on the periodic blade and tower motion and bias the trimmed operating point (the IEA 10-MW’s trimmed generator torque came out about 8 % low at 4 m/s with 5000 N/(m/s)), and on light blades they can destabilise the start of the trim. The retry ladder adds damping only when a trim keeps running out of time. Keyword overrides replace any of these fields (e.g. twr_kdmp=5000.0, bld_kdmp=5000.0 to trim with damping from the first attempt).

Parameters:

**overrides (object) – Field values to override on the returned settings (e.g. hydro_radiation_mode=0 when no .ss file is available).

Returns:

LinearizationSettings – Settings with the offshore module configuration enabled.

Return type:

LinearizationSettings

configure_linearization(deck_path, operating_point, settings=None, *, confine_to=None)[source]

Configure an OpenFAST deck for linearization at an operating point.

Edits the primary .fst and its sub-files in place.

Parameters:
  • deck_path (str or pathlib.Path) – Path to the primary .fst deck (its sub-files are edited too).

  • operating_point (OperatingPoint) – The operating point to set up.

  • settings (LinearizationSettings or None, optional) – Linearization tuning; defaults are used when None.

  • confine_to (str or pathlib.Path or None, optional) – When given, every input these settings edit must resolve inside this directory (see require_inputs_within()); the sweep driver passes each case copy so a shared sub-file is never modified, while files that are only read may stay shared.

Raises:

vane.io.FastInputError – If a required keyword (e.g. Linearize) is absent from the deck, the operating point requests a positive wind speed but the deck has no usable InflowWind file to set it in (which would silently linearize in still air), or an edited input lies outside confine_to.

Return type:

None

Notes

For a multi-point sweep, copy the case to a per-operating-point directory first so shared sub-files are not mutated across points. The template deck is assumed to already enable the structural/aero/control modules appropriate for the case (CompElast/CompAero/CompServo); this sets the linearization controls, the operating point, and — for a non-zero wind point — CompInflow so the configured wind is actually used.

is_size_scaled_default(value)[source]

Return whether a trim setting is still the class default (not user-given).

Parameters:

value (float) – LinearizationSettings.trim_tol or trim_gain_pitch.

Returns:

bool – True when the value is the untouched class default, which the automatic sweep configuration scales with the rotor radius; False for any value passed explicitly, even one equal to the default.

Return type:

bool

needs_rated_wind_speed(settings, deck_path)[source]

Return whether a rotating point on this deck needs rated_wind_speed.

The region split selects the CalcSteady trim case, and the ServoDyn generator handling under trim_controller. It is therefore needed when calc_steady is on, or when trim_controller is on and the deck’s ServoDyn is enabled; a disabled ServoDyn (CompServo = 0) is never configured, so it needs no region.

Parameters:
Returns:

bool – True when a rotating operating point requires the rated wind speed.

Return type:

bool

require_inputs_within(deck_path, root, *, keys=None)[source]

Reject a deck whose edited sub-files resolve outside root.

Linearization and settle runs edit the primary deck and some of its sub-files. A sub-file referenced outside the per-case copy (e.g. ../ElastoDyn.dat when the copy root is the deck’s own directory) would be edited where it lives – a file shared by every case and the template – so such a deck is rejected before anything is written. A sub-file that is only read may stay shared.

Parameters:
  • deck_path (str or pathlib.Path) – The primary .fst deck.

  • root (str or pathlib.Path) – Directory every edited input must resolve inside (the case copy).

  • keys (Sequence[str] or None, optional) – The sub-file keywords that will be edited. None checks every file a linearization may edit (ElastoDyn, InflowWind, AeroDyn, ServoDyn, HydroDyn, SeaState), except the file of a module the deck switches off (CompAero, CompServo, CompHydro or CompSeaSt = 0).

Raises:

vane.io.FastInputError – If the deck or an edited sub-file resolves outside root.

Return type:

None

Pre-flight validation that a deck will actually linearize, before running it.

An OpenFAST linearization fails late and obscurely when the deck holds a module configuration linearization does not support — a Bladed-style DLL controller (which has no Jacobian), wave excitation, convolution radiation, or a state-space hydrodynamics model whose .ss / .ssexctn coefficient files are missing. A long settle and trim run then aborts with a terse solver error.

validate_deck() checks the effective configuration — the deck as it will be after the LinearizationSettings edits are applied — and reports every blocking problem at once, before any OpenFAST process is launched. It is a guard, not a fixer: it never edits the deck. Settings that neutralise a problem (e.g. trim_controller replacing the DLL, or wave_mod=0 forcing still water) clear the corresponding check, so a raw FOWT template paired with LinearizationSettings.offshore() passes, while the same template run with default settings is rejected with an actionable message.

The checks mirror the linearization restrictions OpenFAST enforces in its glue code and modules (see the Linearization restrictions reference page). A module the primary deck switches off (CompServo, CompSeaSt, CompHydro, CompAero, CompMooring, CompElast, CompInflow = 0 or another module choice) leaves its input file inert, so that file is not inspected.

OPENFAST_MAX_PATH_LENGTH = 258

Longest full path, in characters, the Windows OpenFAST build opens a file by.

Windows’ classic MAX_PATH is 260 characters including the terminating null. The Intel-Fortran OpenFAST build is not long-path aware and does not collapse ..: on OpenFAST v5.0.0 an input opened as <working dir>\.\..\5MW_Baseline\<file> of 258 characters was read and one of 259 characters failed with The input file ... was not found.

exception DeckPathLengthError[source]

Bases: DeckValidationError

Raised when OpenFAST would open a deck file by a path that is too long.

See check_deck_path_lengths().

exception DeckValidationError[source]

Bases: ValueError

Raised when a deck cannot be linearized as configured.

The message lists every blocking problem found, one per line, so the deck can be fixed in a single pass rather than one error per run.

check_deck_path_lengths(deck_path, *, max_length=258)[source]

Reject a deck whose sub-files OpenFAST would open by an over-long path.

OpenFAST builds every sub-file path by joining the referencing file’s directory, as it spelled it, to the reference – without collapsing ... Started from the deck’s directory on the deck’s bare name (as run_openfast() does), a deck that keeps shared files next to it in ../Shared/ therefore opens them as <deck dir>\.\..\Shared\<file>, and a nested reference inherits the whole chain. The operating system receives that string appended to the working directory. When it exceeds the Windows path limit the file cannot be opened and OpenFAST aborts at initialisation with The input file ... was not found, although the file exists.

This check walks the deck’s references (deck_file_closure()) and spells each one as OpenFAST will, before any process is launched.

Parameters:
  • deck_path (str or pathlib.Path) – The primary .fst deck, in the directory OpenFAST will be run from.

  • max_length (int, optional) – Longest permitted full path in characters. Default OPENFAST_MAX_PATH_LENGTH.

Raises:
  • DeckPathLengthError – If any file would be opened by a path longer than max_length; the message names the longest such path and its length.

  • FileNotFoundError – If deck_path is not an existing file.

Return type:

None

Notes

The limit applies to Windows builds that are not long-path aware, which is why run_openfast() runs this check only on Windows. A reference the walk cannot follow (an unreadable input) is skipped with a debug log rather than failing the check; OpenFAST will report it itself.

validate_deck(deck_path, settings=None)[source]

Reject a deck that will not linearize as configured, before running it.

Validates the configuration that will be in force once settings is applied: a problem the settings neutralise (the DLL controller replaced by the trim, waves forced to still water) does not fail the check, but a genuinely non-linearizable state that nothing fixes does. The checks cover the module choices OpenFAST cannot linearize (SED, AeroDisk, ExtLoads, FEAMooring, OrcaFlex, ice and soil modules), the CalcSteady azimuth step, InflowWind (lidar, upflow), ServoDyn (control modes, generator model, timed generator start/stop), SeaState (waves, spreading, constrained waves, 2nd-order kinematics, still-water consistency), HydroDyn (potential-flow model, radiation/excitation models and their files, 2nd-order forces, additional body DOFs, radiation time step), AeroDyn (wake, unsteady-aero, and dynamic-wake models, unsteady aerodynamics reaching the blade-tip node under tip loss, nacelle drag), BeamDyn (pitch actuator), and MoorDyn (VIV and viscoelastic lines).

Parameters:
  • deck_path (str or pathlib.Path) – Path to the primary .fst deck. Its sub-files are inspected when present and their module is enabled; an onshore deck without them is fine.

  • settings (LinearizationSettings or None, optional) – The settings that will be applied before running. Defaults to LinearizationSettings (all module fields “leave unchanged”), so a deck that is already linearization-ready passes and a raw one is judged as-is.

Raises:
  • DeckValidationError – If one or more blocking problems are found (all are reported together).

  • vane.io.FastInputError – If deck_path cannot be read at all.

Return type:

None

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. 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 LinearizationSettings, 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 configure_linearization() uses to choose torque vs pitch trim.

class TrimTuningPolicy(above_rated_trim_tol=0.01, above_rated_damping=5000.0)[source]

Bases: object

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 = 0.01
above_rated_damping: float = 5000.0
tune_trim_settings(operating_point, base, policy)[source]

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:
Returns:

LinearizationSettings – A copy of base with the above-rated trim floors applied when they raise the knobs, or an unchanged copy otherwise.

Return type:

LinearizationSettings

Verify a linearization’s trimmed operating point against rotor-performance data.

VANE does not compute rotor performance: the user supplies the steady operating schedule (wind speed, rotor speed, blade pitch and, optionally, the power and thrust coefficients) 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. Whether the trimmed point agrees with the supplied curve is a direct consistency check on the curve and the deck: a large mismatch points to a wrong curve, rotor radius, air density, gearbox ratio or drivetrain loss, or a deck/curve inconsistency.

This module reads the trimmed operating point back out of the .lin files of each linearization (inputs first, outputs as a fallback), derives the aerodynamic power from the steady low-speed-shaft torque balance and compares rotor speed, pitch, power coefficient and (when the outputs carry the rotor thrust) thrust coefficient with the curve, each against a documented tolerance.

Torque balance

At a periodic steady state the azimuth-mean rotor acceleration vanishes, so the mean aerodynamic torque on the low-speed shaft equals the shaft torque transmitted to the gearbox. ElastoDyn relates the low-speed-shaft torque \(Q_{LSS}\) to the high-speed-shaft generator torque \(Q_g\) through the gearbox ratio \(N\) and efficiency \(\eta\),

\[Q_{LSS} = N\,Q_g\,\eta^{-\operatorname{sgn}(Q_g \Omega)},\]

so that losses reduce the generator torque when power flows rotor to generator and increase the required shaft torque when the generator motors the rotor. The aerodynamic power and power coefficient are then

\[P = Q_{LSS}\,\Omega, \qquad C_p = \frac{P}{\tfrac12 \rho \pi R^2 U^3}.\]

The generator (electrical) efficiency does not enter: Q_g is the mechanical torque on the high-speed shaft. Bearing friction and any other drivetrain loss not modelled by GBoxEff are neglected.

The balance also assumes nothing else acts on the rotor. CalcSteady’s artificial trim damping (Twr_Kdmp/Bld_Kdmp) does: on the IEA 10-MW sweep, trimmed with 5000 N/(m/s), it absorbed 8 % of the rotor torque at 4 m/s (OpenFAST v4.0.5: AeroDyn power 436.6 kW against 396.7 kW from the balance; 1.3 % apart when the same point is trimmed without damping, AeroDyn’s power unchanged). When the deck beside the .lin files has non-zero trim damping, the aerodynamic power is therefore AeroDyn’s own rotor power (RtFldPwr/RtAeroPwr) when the outputs carry it.

class OperatingPointCheck(label, trimmed, aero_power, rotor_speed, pitch, cp, ct, issues=(), power_source='torque balance')[source]

Bases: object

The verification of one trimmed operating point.

Parameters:
  • label (str) – Identifies the operating point (a sweep case name or the set index).

  • trimmed (TrimmedOperatingPoint) – The trimmed point read from the .lin files.

  • aero_power (float) – Aerodynamic rotor power, in W, from the low-speed-shaft torque balance, or AeroDyn’s own rotor power when trim damping makes the balance invalid (see power_source).

  • rotor_speed (QuantityCheck) – Rotor speed (rpm) against the curve.

  • pitch (QuantityCheck) – Blade pitch (deg) against the curve.

  • cp (QuantityCheck) – Power coefficient against the curve.

  • ct (QuantityCheck) – Thrust coefficient against the curve (unavailable without a rotor-thrust output or a curve ct).

  • issues (tuple[str, ...]) – Notes on missing data, including the trimmed point’s own issues.

  • power_source (str) – Where aero_power comes from: "torque balance" or the AeroDyn output channel ("output: AD RtFldPwr").

label: str
trimmed: TrimmedOperatingPoint
aero_power: float
rotor_speed: QuantityCheck
pitch: QuantityCheck
cp: QuantityCheck
ct: QuantityCheck
issues: tuple[str, ...] = ()
power_source: str = 'torque balance'
property checks: dict[str, QuantityCheck]

Return the four checks keyed by quantity name.

property status: Literal['pass', 'flag', 'incomplete']

Return "flag" if any check fails, "incomplete" if data is missing.

A point is "incomplete" when no check fails but its rotor speed or pitch could not be compared, or the trimmed data carries an issue. A curve that simply lacks cp/ct does not make a point incomplete.

property flagged_quantities: tuple[str, ...]

Return the names of the quantities outside tolerance.

class OperatingPointVerification(points, constants, tolerances, skipped=<factory>)[source]

Bases: object

The trimmed-versus-curve verification of a set of linearizations.

Parameters:
  • points (tuple[OperatingPointCheck, ...]) – One check per verified operating point, in input order.

  • constants (TurbineConstants) – The turbine constants used.

  • tolerances (TrimTolerances) – The tolerances applied.

  • skipped (tuple[str, ...], optional) – Operating points that could not be verified (e.g. a non-converged sweep case), each with its reason.

points: tuple[OperatingPointCheck, ...]
constants: TurbineConstants
tolerances: TrimTolerances
skipped: tuple[str, ...]
property passed: bool

Return whether every verified point passes and none was skipped.

property flagged: tuple[OperatingPointCheck, ...]

Return the points with at least one quantity outside tolerance.

to_records()[source]

Return one flat record per point, e.g. for a pandas.DataFrame.

Returns:

list[dict[str, float or str or None]] – Keys: label, wind_speed, generator_torque, aero_power, power_source, aerodyn_power, trim_damping, rotor_thrust, status, and for each of rotor_speed, pitch, cp and ct the <q>, <q>_curve, <q>_deviation and <q>_status entries.

Return type:

list[dict[str, float | str | None]]

to_table()[source]

Return a plain-text table of the verification.

Returns:

str – One row per point: wind, rotor speed (trimmed/curve), pitch (trimmed/curve/deviation), generator torque, aerodynamic power, Cp (trimmed/curve/relative deviation), Ct (likewise) and the status.

Return type:

str

class QuantityCheck(trimmed, curve, tolerance, relative)[source]

Bases: object

One trimmed quantity compared with the curve.

Parameters:
  • trimmed (float) – The value at the trimmed operating point (NaN if unavailable).

  • curve (float) – The curve value at the trimmed wind speed (NaN if unavailable).

  • tolerance (float) – The tolerance applied.

  • relative (bool) – Whether tolerance bounds the relative (True) or absolute deviation.

trimmed: float
curve: float
tolerance: float
relative: bool
property deviation: float

Return trimmed - curve.

property relative_deviation: float

Return (trimmed - curve) / |curve| (NaN when the curve is zero).

property status: Literal['pass', 'flag', 'unavailable']

Return "pass", "flag" or "unavailable".

class RotorPerformanceCurve(wind_speed, rotor_speed_rpm, blade_pitch_deg, cp=None, ct=None)[source]

Bases: object

A user-supplied steady rotor-performance schedule.

Parameters:
  • wind_speed (tuple[float, ...]) – Strictly increasing hub-height wind speeds, in m/s.

  • rotor_speed_rpm (tuple[float, ...]) – Rotor speed at each wind speed, in rpm.

  • blade_pitch_deg (tuple[float, ...]) – Collective blade pitch at each wind speed, in degrees.

  • cp (tuple[float, ...] or None, optional) – Aerodynamic power coefficient at each wind speed (NaN where unknown), or None when the curve carries no power coefficient.

  • ct (tuple[float, ...] or None, optional) – Rotor thrust coefficient at each wind speed (NaN where unknown), or None when the curve carries no thrust coefficient.

Raises:

ValueError – If the arrays differ in length, have fewer than two samples, contain a non-finite wind, rotor speed or pitch, or the wind speeds are not strictly increasing.

wind_speed: tuple[float, ...]
rotor_speed_rpm: tuple[float, ...]
blade_pitch_deg: tuple[float, ...]
cp: tuple[float, ...] | None = None
ct: tuple[float, ...] | None = None
classmethod from_arrays(wind_speed, rotor_speed_rpm, blade_pitch_deg, *, cp=None, ct=None)[source]

Build a curve from array-likes, e.g. operating_curve_from_csv output.

Parameters:
  • wind_speed (Sequence[float]) – The required schedule, in m/s, rpm and degrees.

  • rotor_speed_rpm (Sequence[float]) – The required schedule, in m/s, rpm and degrees.

  • blade_pitch_deg (Sequence[float]) – The required schedule, in m/s, rpm and degrees.

  • cp (Sequence[float] or None, optional) – Optional power and thrust coefficients.

  • ct (Sequence[float] or None, optional) – Optional power and thrust coefficients.

Returns:

RotorPerformanceCurve – The validated curve.

Return type:

RotorPerformanceCurve

at(wind_speed)[source]

Interpolate the curve at one wind speed.

The same shape-preserving monotone cubic (PCHIP) used to build sweep operating points from a curve is used here, without extrapolation.

Parameters:

wind_speed (float) – Hub-height wind speed, in m/s.

Returns:

dict[str, float] – rotor_speed_rpm, blade_pitch_deg, cp and ct; a value is NaN outside the curve’s range or when the curve lacks that quantity (a coefficient needs at least two finite samples).

Return type:

dict[str, float]

class TrimTolerances(rotor_speed_rel=0.01, pitch_deg=0.5, cp_rel=0.05, ct_rel=0.05)[source]

Bases: object

Tolerances of the trimmed-versus-curve comparison.

The defaults reflect what a correct deck/curve pair achieves on the reference turbines (rotor speed exact, pitch within a few tenths of a degree, power coefficient within a few percent): a looser agreement is worth investigating.

Parameters:
  • rotor_speed_rel (float, optional) – Relative rotor-speed tolerance (default 0.01, i.e. 1 %).

  • pitch_deg (float, optional) – Absolute blade-pitch tolerance, in degrees (default 0.5).

  • cp_rel (float, optional) – Relative power-coefficient tolerance (default 0.05).

  • ct_rel (float, optional) – Relative thrust-coefficient tolerance (default 0.05).

Raises:

ValueError – If a tolerance is negative or non-finite.

rotor_speed_rel: float = 0.01
pitch_deg: float = 0.5
cp_rel: float = 0.05
ct_rel: float = 0.05
class TrimmedOperatingPoint(wind_speed, rotor_speed_rpm, blade_pitch_deg, blade_pitch_spread_deg, generator_torque, n_lin, pitch_source=None, generator_torque_source=None, rotor_thrust=None, rotor_thrust_source=None, aerodyn_power=None, issues=(), aerodyn_power_source=None, trim_damping=None)[source]

Bases: object

The operating point OpenFAST trimmed to, read from one linearization’s files.

Every quantity is the mean over the azimuth files of the set (and, for pitch, over the blades).

Parameters:
  • wind_speed (float) – Hub-height wind speed, in m/s.

  • rotor_speed_rpm (float) – Rotor speed, in rpm.

  • blade_pitch_deg (float) – Blade pitch, mean over blades and azimuths, in degrees (NaN if absent).

  • blade_pitch_spread_deg (float) – Largest minus smallest pitch over blades and azimuths, in degrees (NaN if absent). A trimmed collective pitch has zero spread.

  • generator_torque (float) – High-speed-shaft generator torque, in N m (NaN if absent).

  • n_lin (int) – Number of .lin files in the set.

  • pitch_source (str or None) – The channel the pitch was read from, or None.

  • generator_torque_source (str or None) – The channel the generator torque was read from, or None.

  • rotor_thrust (float or None) – AeroDyn rotor aerodynamic thrust along the shaft, in N, when the outputs carry it; else None.

  • rotor_thrust_source (str or None) – The output channel the thrust was read from.

  • aerodyn_power (float or None) – AeroDyn rotor aerodynamic power, in W, when the outputs carry it; an independent check on the torque-balance power.

  • issues (tuple[str, ...]) – Human-readable notes on anything missing or inconsistent.

  • aerodyn_power_source (str or None) – The output channel aerodyn_power was read from.

  • trim_damping (float or None) – The larger of the Twr_Kdmp and Bld_Kdmp trim-damping factors, in N/(m/s), of the <root>.fst deck beside the files (0.0 when CalcSteady is off); None when that deck cannot be read. Non-zero trim damping absorbs rotor torque, so the torque balance no longer gives the aerodynamic power (see verify_operating_points()).

wind_speed: float
rotor_speed_rpm: float
blade_pitch_deg: float
blade_pitch_spread_deg: float
generator_torque: float
n_lin: int
pitch_source: str | None = None
generator_torque_source: str | None = None
rotor_thrust: float | None = None
rotor_thrust_source: str | None = None
aerodyn_power: float | None = None
issues: tuple[str, ...] = ()
aerodyn_power_source: str | None = None
trim_damping: float | None = None
class TurbineConstants(gearbox_ratio, gearbox_efficiency, rotor_radius, air_density)[source]

Bases: object

Turbine constants the torque balance and coefficients need.

Parameters:
  • gearbox_ratio (float) – Gearbox ratio \(N\) (high-speed over low-speed shaft speed), positive.

  • gearbox_efficiency (float) – Gearbox efficiency \(\eta\) as a fraction in (0, 1] (ElastoDyn’s GBoxEff is in percent; from_deck() converts it).

  • rotor_radius (float) – Rotor radius \(R\) used to normalize the coefficients, in m.

  • air_density (float) – Air density \(\rho\), in kg/m^3.

Raises:

ValueError – If any constant is non-finite or non-positive, or the efficiency exceeds one.

gearbox_ratio: float
gearbox_efficiency: float
rotor_radius: float
air_density: float
property rotor_area: float

Return the swept area \(\pi R^2\), in m^2.

classmethod from_deck(fst_path, *, radius_convention='projected')[source]

Read the constants from an OpenFAST deck.

The gearbox ratio (GBRatio, or GBoxRatio for Simplified-ElastoDyn), gearbox efficiency (GBoxEff, percent; 100 % when absent), tip radius (TipRad) and precone (PreCone(i) or PreCone) are read from the structural (EDFile) input; this holds for BeamDyn decks too, whose rotor geometry still lives in ElastoDyn. The air density is AeroDyn’s AirDens, or the .fst AirDens when AeroDyn’s is "default" or absent.

Parameters:
  • fst_path (str or pathlib.Path) – The primary .fst deck.

  • radius_convention ({"projected", "tip"}, optional) – "projected" (default) normalizes with the rotor-plane radius TipRad * cos(PreCone), the swept-area radius most blade-element tools use; "tip" uses TipRad itself. Pass explicit constants when a curve was normalized with some other radius.

Returns:

TurbineConstants – The constants.

Raises:

ValueError – If a required value is missing or non-numeric, or radius_convention is unknown.

Return type:

TurbineConstants

aerodynamic_power(generator_torque, rotor_speed_rpm, constants)[source]

Return the aerodynamic rotor power from the steady torque balance.

Parameters:
  • generator_torque (float) – High-speed-shaft generator torque, in N m.

  • rotor_speed_rpm (float) – Rotor speed, in rpm.

  • constants (TurbineConstants) – Supplies the gearbox ratio and efficiency.

Returns:

float – \(N Q_g \Omega \eta^{-\operatorname{sgn}(Q_g\Omega)}\), in W: the gearbox loss is added back when power flows rotor to generator and subtracted when the generator motors the rotor.

Return type:

float

rotor_performance_from_csv(path)[source]

Read a rotor-performance CSV, including its optional coefficient columns.

The required wind_speed, rotor_speed_rpm and blade_pitch_deg columns are parsed, unit-converted and validated exactly as by operating_curve_from_csv() (unit-annotated headers such as wind_speed [m/s] are accepted). The optional power-coefficient column (cp, power_coefficient, RtAeroCp, RtFldCp) and thrust-coefficient column (ct, thrust_coefficient, RtAeroCt, RtFldCt) are read as well; a blank or non-numeric coefficient cell becomes NaN (unknown) rather than an error.

The coefficients are returned as fractions. A coefficient column’s unit is read from its header bracket, as for the required columns: no unit, [-], [1] or [fraction] means a fraction, and [%], [percent] or [pct] a percentage, which is divided by 100. Any other unit is rejected. So is a scale that contradicts the declared unit: a fraction above the physical bound (the Betz limit 16/27 for cp, 2 for ct), which is what a percentage written without its unit looks like, and a column declared in percent whose non-zero values are all at most 1 in magnitude, which reads as fractions labelled as percent. A separate units row is not supported.

Parameters:

path (str or pathlib.Path) – Path to the rotor-performance CSV.

Returns:

RotorPerformanceCurve – The curve, sorted by ascending wind speed.

Raises:
  • FileNotFoundError – If path does not exist.

  • ValueError – As operating_curve_from_csv(); if more than one column matches the power or thrust coefficient; or if a coefficient column has an unsupported unit or a scale that contradicts its unit (see above).

Return type:

RotorPerformanceCurve

trimmed_operating_point(lin_files)[source]

Read the trimmed operating point from one linearization’s .lin files.

The blade pitch is read from the ElastoDyn pitch-command inputs (ED Blade <i> pitch command, the same for ElastoDyn and BeamDyn blades), or the collective pitch-command input; the generator torque from the ED Generator torque input. When the inputs are absent (LinInputs = 0) the ED BldPitch<i> and SrvD GenTq outputs are used instead. Wind and rotor speed come from each file’s header. Every quantity is averaged over the azimuth files.

Parameters:

lin_files (Sequence[LinFile or str or os.PathLike]) – The full-system .lin files of one linearization (one per azimuth), parsed or as paths.

Returns:

TrimmedOperatingPoint – The trimmed point; a quantity that cannot be found is NaN and explained in TrimmedOperatingPoint.issues.

Raises:

ValueError – If lin_files is empty.

Return type:

TrimmedOperatingPoint

verify_operating_points(lin_sets, curve, *, deck=None, constants=None, radius_convention='projected', tolerances=None, labels=None)[source]

Verify the trimmed operating points of linearizations against a curve.

For each linearization (its set of azimuth .lin files) the trimmed wind, rotor speed, blade pitch and generator torque are read back (trimmed_operating_point()), the aerodynamic power is derived from the steady low-speed-shaft torque balance (aerodynamic_power()) and the power coefficient \(C_p = P / (\tfrac12\rho\pi R^2U^3)\) formed. The thrust coefficient is formed only when the outputs carry AeroDyn’s rotor thrust (RtFldFxh or RtAeroFxh in the AeroDyn OutList). Each quantity is compared with the curve interpolated at the trimmed wind speed.

Below rated, CalcSteady prescribes the rotor speed and pitch from the curve, so their agreement is by construction and \(C_p\) (from the trimmed torque) is the informative check; above rated the trimmed pitch is.

When the deck beside a set’s files trimmed with artificial damping (non-zero Twr_Kdmp/Bld_Kdmp with CalcSteady), the damping absorbs part of the rotor torque and the generator torque no longer balances the aerodynamic torque. The power is then AeroDyn’s rotor power output when present (OperatingPointCheck.power_source says which was used); without it the balance is kept and a warning logged.

Parameters:
Returns:

OperatingPointVerification – The per-point checks and overall outcome.

Raises:

ValueError – If neither deck nor constants is given, labels has the wrong length, a set is empty, or the deck/curve cannot be read.

Return type:

OperatingPointVerification

verify_sweep_operating_points(sweep, curve, *, constants=None, radius_convention='projected', tolerances=None)[source]

Verify the trimmed points of a run_operating_sweep() result.

Every converged case with .lin files is verified, labelled by its operating point’s name; a case that did not converge or produced no .lin files is listed in OperatingPointVerification.skipped. Unless constants is given, the turbine constants are read from the first verified case’s configured deck.

Parameters:
Returns:

OperatingPointVerification – The per-point checks.

Raises:

ValueError – If no case of the sweep can be verified.

Return type:

OperatingPointVerification

Post-run acceptance checks for a linearized operating point.

CalcSteady establishes periodicity according to OpenFAST’s normalized output criterion. That criterion is necessary, but a deliberately loose TrimTol can still stop while a floating platform carries a secular six-DOF drift. Such a point is parseable and can produce a finite MBC model, yet it is a poor equilibrium about which to build a small-perturbation model.

This module provides an independent physical check. It reads the ElastoDyn platform rate states from every full-system .lin file and estimates the azimuth mean of each rate component with quadrature weights that exactly annihilate the 1P, 2P and 3P harmonics at the actual sample azimuths. Periodic motion therefore cancels; a secular drift does not. The resulting translational and angular residuals can reject a nominally successful trim without mistaking ordinary once-per-revolution motion for non-convergence.

The weights are the minimum-norm solution of the harmonic moment conditions, which for seven or more samples is exactly the constant term of a least-squares fit of a mean plus 1P–3P harmonics. On an exactly uniform sweep they reduce to the equal-weight average. They remain valid for the non-uniform azimuths CalcSteady actually produces: OpenFAST takes each snapshot at the first time step at or past its target azimuth, so real sweeps are uniform only to within RotSpeed * DT.

class LinearizationAcceptancePolicy(max_mean_translational_speed=0.001, max_mean_angular_speed=0.0001, min_rotating_azimuths=12)[source]

Bases: object

Physical residual limits for a floating operating point.

Parameters:
  • max_mean_translational_speed (float) – Maximum norm of the azimuth-mean platform translational velocity, in m/s.

  • max_mean_angular_speed (float) – Maximum norm of the azimuth-mean platform angular velocity, in rad/s.

  • min_rotating_azimuths (int) – Minimum number of distinct azimuth samples needed to distinguish periodic motion from secular drift for a rotating point. The default is 12 and values below four are prohibited so blade-passing motion cannot alias into the mean. A non-rotating point needs only one linearization.

max_mean_translational_speed: float = 0.001
max_mean_angular_speed: float = 0.0001
min_rotating_azimuths: int = 12
class OperatingPointResidualAudit(assessed, accepted, reason, file_count, distinct_azimuths, rotating, translational_channels, angular_channels, mean_translational_velocity, mean_angular_velocity, mean_translational_speed, mean_angular_speed, max_instantaneous_translational_speed, max_instantaneous_angular_speed, max_mean_translational_speed, max_mean_angular_speed)[source]

Bases: object

Physical residual evidence extracted from one linearization sweep.

Parameters:
  • assessed (bool)

  • accepted (bool)

  • reason (str | None)

  • file_count (int)

  • distinct_azimuths (int)

  • rotating (bool)

  • translational_channels (tuple[str, ...])

  • angular_channels (tuple[str, ...])

  • mean_translational_velocity (tuple[float, ...] | None)

  • mean_angular_velocity (tuple[float, ...] | None)

  • mean_translational_speed (float | None)

  • mean_angular_speed (float | None)

  • max_instantaneous_translational_speed (float | None)

  • max_instantaneous_angular_speed (float | None)

  • max_mean_translational_speed (float)

  • max_mean_angular_speed (float)

assessed: bool
accepted: bool
reason: str | None
file_count: int
distinct_azimuths: int
rotating: bool
translational_channels: tuple[str, ...]
angular_channels: tuple[str, ...]
mean_translational_velocity: tuple[float, ...] | None
mean_angular_velocity: tuple[float, ...] | None
mean_translational_speed: float | None
mean_angular_speed: float | None
max_instantaneous_translational_speed: float | None
max_instantaneous_angular_speed: float | None
max_mean_translational_speed: float
max_mean_angular_speed: float
class PeriodicStateResidualAudit(assessed, reason, file_count, distinct_azimuths, rotating, min_rotating_azimuths, channels, excluded_rotating_channels, excluded_cyclic_channels, max_mean_to_rms_ratio)[source]

Bases: object

State-derivative residuals for a periodic OpenFAST operating point.

Rotating-frame states are excluded because their native derivatives contain the intended rotor-periodic coordinate transport. The generator-azimuth coordinate is also excluded because its derivative is the intended rotor speed; its angular acceleration state remains in the audit. No heterogeneous state units are combined into one physical norm.

Parameters:
assessed: bool
reason: str | None
file_count: int
distinct_azimuths: int
rotating: bool
min_rotating_azimuths: int
channels: tuple[StateDerivativeResidual, ...]
excluded_rotating_channels: tuple[str, ...]
excluded_cyclic_channels: tuple[str, ...]
max_mean_to_rms_ratio: float | None
class StateDerivativeResidual(state_description, derivative_description, module, unit, mean_derivative, rms_derivative, oscillatory_rms_derivative, mean_to_rms_ratio, active)[source]

Bases: object

Azimuth statistics for one non-rotating continuous-state derivative.

mean_to_rms_ratio is dimensionless and invariant to a change of state units. It approaches zero for a resolved zero-mean periodic derivative and one for a constant secular derivative. It is None when the derivative is inactive at floating-point resolution; absolute mean_derivative and rms_derivative values are retained so this ratio is never mistaken for an engineering limit.

Parameters:
  • state_description (str)

  • derivative_description (str)

  • module (str)

  • unit (str)

  • mean_derivative (float)

  • rms_derivative (float)

  • oscillatory_rms_derivative (float)

  • mean_to_rms_ratio (float | None)

  • active (bool)

state_description: str
derivative_description: str
module: str
unit: str
mean_derivative: float
rms_derivative: float
oscillatory_rms_derivative: float
mean_to_rms_ratio: float | None
active: bool
audit_operating_point_residual(lin_files, policy=None)[source]

Audit secular platform drift across an OpenFAST azimuth sweep.

Paths are parsed with VANE’s strict .lin reader; already parsed LinFile objects are accepted too. If the model has no ElastoDyn platform-rate states (for example a fixed-bottom turbine), the check is not applicable and returns assessed=False, accepted=True. A rotating point with too few distinct azimuths is not accepted because an instantaneous periodic rate cannot be separated from secular drift.

Parameters:
Returns:

OperatingPointResidualAudit – The component means, vector norms, thresholds, and acceptance decision.

Raises:

ValueError – If no files are supplied or their state descriptions are inconsistent.

Return type:

OperatingPointResidualAudit

audit_periodic_state_residual(lin_files, *, min_rotating_azimuths=12)[source]

Audit mean derivatives without mixing state units into a scalar norm.

The derivative operating point in each .lin file is sampled over azimuth. For every non-rotating, non-cyclic continuous state, this function reports the azimuth mean, total RMS, zero-mean oscillatory RMS, and the scale-invariant ratio abs(mean) / RMS. A resolved periodic equilibrium has a small ratio, whereas a secular derivative survives the azimuth mean. Absolute residuals must still be interpreted in their stated engineering units.

Parameters:
  • lin_files (Sequence[path-like or LinFile]) – Full-system files from one operating point.

  • min_rotating_azimuths (int, optional) – Minimum number of distinct azimuths required before the periodic statistics are assessed. The default is 12; publication campaigns should normally use 36. The denser requirement prevents a 3P component from aliasing into a three-azimuth mean.

Returns:

PeriodicStateResidualAudit – Per-state residual statistics and the explicit exclusions.

Raises:

ValueError – If no files are supplied, the requested sample count is smaller than four, or the state/derivative channel layouts disagree.

Return type:

PeriodicStateResidualAudit

Solve a floating turbine’s steady operating point by a fully-coupled settle run.

A CalcSteady linearization of a floating turbine needs good initial conditions — in particular the platform’s thrust-loaded equilibrium position — or the open-loop trim diverges near peak thrust (the rotor decelerates into a stalled, motoring state). The dependable way to obtain them is to run the fully-coupled model with its real controller at a steady wind until it settles, then read the operating point off the time series.

solve_steady_state() does exactly that: it configures the deck for a constant-wind time-domain run (still water, uniform inflow with no shear by default – the same inflow the linearization then uses – and the controller left active), runs OpenFAST, discards the initial transient, and averages the steady window to return an OperatingPoint carrying the settled rotor speed, blade pitch, and PlatformPosition. Feeding that point back into configure_linearization() (with LinearizationSettings.offshore()) starts the linearization at the true equilibrium.

The deck is edited in place, so pass a writable working copy (the linearization driver copies the case per operating point).

The settle run uses exactly the still water the linearization uses: SeaState is set to WaveMod = 0 and the wave options OpenFAST rejects in still water are neutralised by the same helper configure_linearization() applies.

Unlike the linearization, the settle run keeps the deck’s Bladed-style controller library (ServoDyn DLL_FileName) active. Published decks often point it at a library built for another platform (a Linux libdiscon.so path on Windows), so the library is checked before OpenFAST launches and a missing one fails fast; pass controller_library to point the settle copy at a library built for this host.

ConfirmTimeSource

How the length of a two-stage settle’s confirmation run was chosen.

alias of Literal[‘user’, ‘platform_period’, ‘damped_stage’, ‘fallback’]

class EquilibriumTolerances(translation_m=0.1, rotation_deg=0.05, rotor_speed_rel=0.005, pitch_deg=0.1, residual_damping_ratio=0.005, growing_damping_ratio=0.02, oscillation_velocity_rms=0.2)[source]

Bases: object

Tolerances on a change of the settled operating-point means.

Used to judge whether the undamped confirmation of a two-stage settle reproduces the damped stage’s equilibrium, and – with criterion="mean_drift" – whether a run’s means still drift between the halves of its analysis window.

Parameters:
  • translation_m (float, optional) – Platform surge/sway/heave, m. Default 0.1.

  • rotation_deg (float, optional) – Platform roll/pitch/yaw, deg. Default 0.05.

  • rotor_speed_rel (float, optional) – Rotor speed, relative to max(|reference rotor speed|, 1 rpm). Default 0.005.

  • pitch_deg (float, optional) – Collective blade pitch, deg. Default 0.1.

  • residual_damping_ratio (float, optional) – Smallest damping ratio at which the residual platform oscillation of an undamped confirmation counts as decaying (see SettleConfirmation.residual_damping_ratio). Default 0.005.

  • growing_damping_ratio (float, optional) – Classification threshold: an undamped confirmation whose residual damping ratio is below -growing_damping_ratio is clearly growing (its equilibrium is unstable under the deck’s controller), not merely sustained. Default 0.02: the amplitude then grows by exp(2 pi 0.02) - 1, about 13 %, per cycle and by a factor of about 2.7 over the eight cycles an automatically sized confirmation analyses – unmistakable growth, whereas a limit cycle, a beating of two slow modes or a controller re-exciting the platform after the restart stays above it (the IEA 15-MW semi at 12-14 m/s gives -0.007 to -0.009). It does not reject anything: with the "auto" criterion a growing oscillation about a confirmed equilibrium is accepted like a sustained one, but flagged (SettleConfirmation.growing_oscillation) and logged with its own warning (see solve_steady_state()).

  • oscillation_velocity_rms (float, optional) – Largest platform-velocity RMS (m/s for the translational channels, deg/s for the angular ones) of a sustained or growing oscillation the "auto" criterion accepts about a confirmed equilibrium. Default 0.2, twenty times the default settle tolerance: at a 100 s surge period about +/-4.5 m of surge, at a 30 s pitch period about +/-1.4 deg of pitch – small enough for its whole-period means to be a well-defined measurement of the equilibrium. A larger one is reported, not accepted.

Raises:

ValueError – If any tolerance is not positive and finite.

translation_m: float = 0.1
rotation_deg: float = 0.05
rotor_speed_rel: float = 0.005
pitch_deg: float = 0.1
residual_damping_ratio: float = 0.005
growing_damping_ratio: float = 0.02
oscillation_velocity_rms: float = 0.2
exceeded(changes, reference_rotor_speed_rpm)[source]

Return the quantities whose change exceeds its tolerance.

Parameters:
  • changes (Mapping[str, float]) – Change of each mean, keyed as SettleAttempt.mean_drift.

  • reference_rotor_speed_rpm (float) – The rotor speed the relative rotor-speed tolerance refers to.

Returns:

tuple of str – The names (in the order of changes) outside tolerance.

Return type:

tuple[str, …]

class SettleAttempt(translational_velocity_rms, angular_velocity_rms, assessed, converged, settle_time, iteration, window_start=None, window_end=None, stage='settle', mean_drift=None, oscillation_period=None, criterion=None, residual_damping_ratio=None)[source]

Bases: object

Convergence evidence from one adaptive settle attempt.

Each entry preserves the horizon and measured platform-velocity RMS values before a later adaptive run overwrites the OpenFAST output file.

Parameters:
  • translational_velocity_rms (float) – Largest RMS platform translational velocity (m/s) over the analysis window; nan when no translational-velocity channel was output.

  • angular_velocity_rms (float) – Largest RMS platform angular velocity (deg/s) over the analysis window; nan when no angular-velocity channel was output.

  • assessed (bool) – Whether any platform-velocity channel was available to judge settling.

  • converged (bool) – Whether every assessed velocity RMS was within tolerance.

  • settle_time (float) – The TMax (s) of this attempt.

  • iteration (int) – One-based attempt number.

  • window_start (float or None, optional) – Bounds (s) of the trailing analysis window this attempt was judged over, [settle_time - (initial settle_time - discard_time), settle_time]. None when not recorded.

  • window_end (float or None, optional) – Bounds (s) of the trailing analysis window this attempt was judged over, [settle_time - (initial settle_time - discard_time), settle_time]. None when not recorded.

  • stage ({"settle", "damped", "confirm"}, optional) – Which run this was: an ordinary settle run ("settle"), a run of the temporarily damped first stage of a two-stage settle ("damped"), or its undamped confirmation run ("confirm"). See solve_steady_state().

  • mean_drift (dict[str, float] or None, optional) – Change of each measured operating-point mean between the first and second halves of the analysis window (second minus first): surge/sway/ heave (m), roll/pitch/yaw (deg), rotor_speed_rpm and blade_pitch_deg. None when not computed.

  • oscillation_period (float or None, optional) – Dominant period (s) of the platform-velocity channel with the largest RMS over the analysis window – the period of whatever is still moving. None when it could not be estimated (too few samples, or no motion).

  • criterion ({"velocity", "mean_drift"} or None, optional) – The criterion that judged this attempt (with criterion="auto" it may differ between attempts). None when not recorded.

  • residual_damping_ratio (float or None, optional) – Damping ratio of the platform oscillation over the analysis window, from the cycle-by-cycle decay of the most active platform-velocity channel (positive: decaying; near zero: sustained; negative: growing). None when fewer than two whole cycles were output.

translational_velocity_rms: float
angular_velocity_rms: float
assessed: bool
converged: bool
settle_time: float
iteration: int
window_start: float | None = None
window_end: float | None = None
stage: Literal['settle', 'damped', 'confirm'] = 'settle'
mean_drift: dict[str, float] | None = None
oscillation_period: float | None = None
criterion: Literal['velocity', 'mean_drift', 'auto'] | None = None
residual_damping_ratio: float | None = None
class SettleConfirmation(damping, damped_converged, damped_translational_velocity_rms, damped_angular_velocity_rms, damped_settle_time, damped_means, confirm_means, shift, shift_flagged, confirm_converged, residual_damping_ratio, stable, confirmed, tolerances, confirm_time=None, confirm_time_source=None, extended=False, sustained_oscillation=False)[source]

Bases: object

Evidence of a two-stage settle: damped stage, then undamped confirmation.

Parameters:
  • damping (SettlePlatformDamping) – The temporary platform damping of the first stage.

  • damped_converged (bool) – Whether the damped stage met the settle criterion.

  • damped_translational_velocity_rms (float) – The damped stage’s final platform-velocity RMS (m/s, deg/s; nan when not measured).

  • damped_angular_velocity_rms (float) – The damped stage’s final platform-velocity RMS (m/s, deg/s; nan when not measured).

  • damped_settle_time (float) – TMax (s) of the damped stage’s final run.

  • damped_means (dict[str, float]) – The damped stage’s settled means (keys as SettleAttempt.mean_drift); the undamped confirmation starts from them.

  • confirm_means (dict[str, float]) – The undamped confirmation run’s means over its analysis window (averaged over whole periods of any residual oscillation).

  • shift (dict[str, float]) – confirm_means - damped_means for every quantity measured in both.

  • shift_flagged (tuple of str) – Quantities whose shift exceeds tolerances.

  • confirm_converged (bool) – Whether the undamped confirmation run met the settle criterion by itself.

  • residual_damping_ratio (float or None) – Damping ratio of the undamped run’s residual platform oscillation, from the decay of its cycle-by-cycle RMS: positive when it dies out, zero or negative when it is sustained or growing. None when fewer than two whole cycles were output.

  • stable (bool) – Whether the undamped run is stable about the equilibrium: it met the criterion by itself, or (velocity criterion) its residual oscillation decays with a damping ratio of at least tolerances.residual_damping_ratio.

  • confirmed (bool) – True when the undamped run reproduced the damped equilibrium within tolerances and is stable, or – sustained_oscillation – when it reproduced it while carrying a measurable sustained or growing oscillation ("auto"). Restarting an undamped model, even at its equilibrium, rings its slow modes (the controller’s start-up transient alone does), and a lightly damped mode cannot ring down within a short confirmation; a decaying residual is accepted as confirmation of a stable equilibrium. False means the damping changed the answer, or the undamped model’s oscillation could not be accepted (too large to measure its mean, too few cycles, or a criterion other than "auto").

  • tolerances (EquilibriumTolerances) – The tolerances applied.

  • confirm_time (float or None, optional) – TMax (s) of the confirmation run that was judged (the extended one when extended). None when not recorded.

  • confirm_time_source ({"user", "platform_period", "damped_stage", "fallback"} or None, optional) – How the first confirmation’s length was chosen: given by the caller, or sized automatically from the slowest platform period (platform_period of solve_steady_state(), e.g. the standstill platform probe’s), from the damped stage’s dominant period, or the fallback length when neither was known. None when not recorded.

  • extended (bool, optional) – True when the confirmation was decaying but its means had not yet returned to the damped equilibrium, so it was rerun once twice as long and that longer run was judged.

  • sustained_oscillation (bool, optional) – True when the point was accepted although the undamped run keeps a non-decaying oscillation: its whole-period means reproduce the damped equilibrium within tolerances, and either the mean-drift criterion accepted the run or – "auto" criterion only – the oscillation is measurable (residual damping ratio known, velocity RMS within tolerances.oscillation_velocity_rms), whether sustained or growing. Linear damping cannot move an equilibrium (it acts on the velocity, zero there), so the damped stage’s equilibrium is the undamped model’s, stable or not; it is the operating point. A growing oscillation is additionally flagged by growing_oscillation.

damping: SettlePlatformDamping
damped_converged: bool
damped_translational_velocity_rms: float
damped_angular_velocity_rms: float
damped_settle_time: float
damped_means: dict[str, float]
confirm_means: dict[str, float]
shift: dict[str, float]
shift_flagged: tuple[str, ...]
confirm_converged: bool
residual_damping_ratio: float | None
stable: bool
confirmed: bool
tolerances: EquilibriumTolerances
confirm_time: float | None = None
confirm_time_source: Literal['user', 'platform_period', 'damped_stage', 'fallback'] | None = None
extended: bool = False
sustained_oscillation: bool = False
property growing_oscillation: bool

Return whether the point was accepted with a growing oscillation.

True when sustained_oscillation holds and the residual damping ratio is below -tolerances.growing_damping_ratio: the equilibrium is unstable under the deck’s controller (the closed-loop oscillation grows without the added damping). Linear damping does not move an equilibrium, and the shift test reproduced it, so it is linearized; the linearization trims without the controller, so the controller-in-the-loop instability is not part of the linear model.

Returns:

bool – Whether the accepted confirmation’s oscillation is clearly growing.

to_dict()[source]

Return the confirmation as a dictionary, with growing_oscillation.

Returns:

dict[str, object] – Every field (as dataclasses.asdict()) plus growing_oscillation.

Return type:

dict[str, object]

class SettleConvergence(translational_velocity_rms, angular_velocity_rms, assessed, converged, settle_time, iterations, attempts=(), window_start=None, window_end=None, criterion='velocity', mean_drift=None, oscillation_period=None, confirmation=None, requested_criterion='velocity', criterion_switched=False, prescribed_rotor=False, residual_damping_ratio=None, sustained_oscillation=False)[source]

Bases: object

Whether a settle run reached equilibrium, judged by platform-velocity RMS.

Parameters:
  • translational_velocity_rms (float) – Largest root-mean-square platform translational velocity (m/s) over the trailing analysis window, across the output velocity channels; nan when none were output.

  • angular_velocity_rms (float) – Largest RMS platform angular velocity (deg/s) over the trailing analysis window; nan when no angular-velocity channel was output.

  • assessed (bool) – Whether any platform-velocity channel was available to judge settling. When False the run cannot be confirmed settled (the deck output no velocity).

  • converged (bool) – Whether every assessed velocity RMS is within tolerance (True when not assessed — nothing measured cannot fail, but assessed records that).

  • settle_time (float) – The TMax (s) of the run this convergence describes.

  • iterations (int) – How many settle runs were performed (more than one only when adaptive).

  • attempts (tuple of SettleAttempt) – Ordered convergence evidence for every attempted horizon. This makes an adaptive accept/reject decision reconstructible after intermediate OpenFAST output files have been overwritten.

  • window_start (float or None, optional) – Bounds (s) of the trailing analysis window of the final run, over which both the convergence outcome and the returned operating-point means were computed. None when not recorded.

  • window_end (float or None, optional) – Bounds (s) of the trailing analysis window of the final run, over which both the convergence outcome and the returned operating-point means were computed. None when not recorded.

  • criterion ({"velocity", "mean_drift"}, optional) – The settle criterion that judged the run: platform-velocity RMS within tolerance ("velocity", the default) or operating-point means that no longer drift between the two halves of the analysis window ("mean_drift"). With criterion="auto" this is the criterion the automatic choice settled on. See solve_steady_state().

  • mean_drift (dict[str, float] or None, optional) – The final run’s half-window mean drift (see SettleAttempt.mean_drift).

  • oscillation_period (float or None, optional) – The final run’s dominant platform-velocity period (s); see SettleAttempt.oscillation_period.

  • confirmation (SettleConfirmation or None, optional) – For a two-stage settle (platform_damping given), the damped stage’s evidence and the undamped confirmation’s outcome; None for an ordinary settle.

  • requested_criterion ({"velocity", "mean_drift", "auto"}, optional) – The criterion the caller asked for. Differs from criterion only for "auto".

  • criterion_switched (bool, optional) – True when criterion="auto" switched from the velocity to the mean-drift criterion because the platform carried a sustained, non-decaying oscillation the velocity criterion can never accept.

  • prescribed_rotor (bool, optional) – True when the settle ran without a controller, with the rotor speed and blade pitch held at their initial values (see prescribed_rotor of solve_steady_state()); the settled rotor speed and pitch are then the prescribed ones and only the platform equilibrium was found.

  • residual_damping_ratio (float or None, optional) – The final run’s platform-oscillation damping ratio (see SettleAttempt.residual_damping_ratio).

  • sustained_oscillation (bool, optional) – True when a two-stage settle was accepted with a non-decaying (sustained or growing) oscillation about its confirmed equilibrium (see SettleConfirmation.sustained_oscillation); a warning is logged.

translational_velocity_rms: float
angular_velocity_rms: float
assessed: bool
converged: bool
settle_time: float
iterations: int
attempts: tuple[SettleAttempt, ...] = ()
window_start: float | None = None
window_end: float | None = None
criterion: Literal['velocity', 'mean_drift', 'auto'] = 'velocity'
mean_drift: dict[str, float] | None = None
oscillation_period: float | None = None
confirmation: SettleConfirmation | None = None
requested_criterion: Literal['velocity', 'mean_drift', 'auto'] = 'velocity'
criterion_switched: bool = False
prescribed_rotor: bool = False
residual_damping_ratio: float | None = None
sustained_oscillation: bool = False
property growing_oscillation: bool

Return whether the point was accepted with a growing oscillation.

Returns:

bool – SettleConfirmation.growing_oscillation of the confirmation; False for a single-stage settle.

to_dict()[source]

Return the record as a dictionary, with growing_oscillation added.

Returns:

dict[str, object] – Every field (as dataclasses.asdict()), with confirmation from SettleConfirmation.to_dict(), plus growing_oscillation.

Return type:

dict[str, object]

SettleCriterion

How a settle run is judged settled; see solve_steady_state(). "auto" starts with the velocity criterion and switches to the mean-drift criterion when a sustained, non-decaying oscillation is detected; a convergence record always holds the criterion that actually judged the run ("velocity" or "mean_drift").

alias of Literal[‘velocity’, ‘mean_drift’, ‘auto’]

class SettleCurveCheck(rotor_speed, pitch, tolerances)[source]

Bases: object

A settled operating point compared with the user’s operating curve.

When a settle run is seeded from a rotor-performance curve, the controller – not the curve – decides where the turbine settles, and that settled point is what is linearized. Comparing the two surfaces a disagreement between the controller and the curve (a mistuned controller, a curve for a different model or a different air density, a settle that has not reached its steady state). A deviation outside tolerance is reported, never treated as a failure.

Parameters:
  • rotor_speed (QuantityCheck) – Settled rotor speed (rpm; trimmed holds the settled value) against the curve, with a relative tolerance.

  • pitch (QuantityCheck) – Settled collective blade pitch (deg) against the curve, with an absolute tolerance.

  • tolerances (TrimTolerances) – The tolerances applied (only rotor_speed_rel and pitch_deg are used).

rotor_speed: QuantityCheck
pitch: QuantityCheck
tolerances: TrimTolerances
property status: CheckStatus

Return "flag" if either quantity is outside tolerance.

Returns:

{“pass”, “flag”, “unavailable”} – "flag" when the rotor speed or pitch deviates beyond tolerance, "unavailable" when either could not be compared, else "pass".

property flagged_quantities: tuple[str, ...]

Return the names of the quantities outside tolerance.

class SettlePlatformDamping(surge=0.0, sway=0.0, heave=0.0, roll=0.0, pitch=0.0, yaw=0.0)[source]

Bases: object

Temporary linear platform damping that speeds up a floating settle run.

The damping is added to the diagonal of HydroDyn’s AddBLin matrix (first body) in the settle copy only, for the first stage of a two-stage settle (see solve_steady_state()); it is removed again before the undamped confirmation run and never reaches a linearization case.

Why the settled mean is unchanged: the added load is F = -B q_dot. At a static equilibrium the platform velocity q_dot is zero, so the force is zero and the equilibrium equations – rotor thrust against mooring, hydrostatic and gravity restoring loads – are exactly those of the undamped model: the fixed point does not move. Over a stationary periodic response the mean of B q_dot is B (q(t+T) - q(t)) / T = 0 as well. Quadratic drag (AddBQuad, Morison drag) also vanishes at zero velocity, and the settle runs in still water, so there is no wave mean-drift force to change. Only the transient – the slow, lightly damped surge/sway/yaw ringing – decays faster. Caveats the undamped confirmation stage exists for: a model with more than one equilibrium (e.g. a controller region boundary) may be steered to a different one, and an oscillation the damping suppresses may be self-excited, in which case the undamped model has no stable equilibrium and its oscillation reappears.

Parameters:
  • surge (float, optional) – Added linear damping for the translational DOFs, N/(m/s). Default 0.

  • sway (float, optional) – Added linear damping for the translational DOFs, N/(m/s). Default 0.

  • heave (float, optional) – Added linear damping for the translational DOFs, N/(m/s). Default 0.

  • roll (float, optional) – Added linear damping for the rotational DOFs, N m/(rad/s). Default 0.

  • pitch (float, optional) – Added linear damping for the rotational DOFs, N m/(rad/s). Default 0.

  • yaw (float, optional) – Added linear damping for the rotational DOFs, N m/(rad/s). Default 0.

Raises:

ValueError – If any value is negative or non-finite, or if all are zero.

See also

SettlePlatformDamping.from_critical_fraction

size it as a fraction of critical.

surge: float = 0.0
sway: float = 0.0
heave: float = 0.0
roll: float = 0.0
pitch: float = 0.0
yaw: float = 0.0
classmethod from_critical_fraction(fraction, *, mass, natural_period)[source]

Size the damping as a fraction of critical damping per DOF.

For a single DOF with effective mass (or inertia) m and stiffness k, critical damping is 2 sqrt(k m) = 2 m omega_n = 4 pi m / T_n, so the damping for a fraction zeta is 4 pi zeta m / T_n. The natural period stands in for the stiffness because it is what is usually known (published for reference platforms, or read off an earlier settle as its SettleConvergence.oscillation_period); the mooring stiffness alone is not. Precision is not needed: any positive damping leaves the equilibrium unchanged, and a fraction of about 0.3–0.7 decays the transient in one to two cycles while staying clear of the slow creep of an overdamped DOF.

Parameters:
  • fraction (float or Mapping[str, float]) – Fraction of critical damping, the same for every DOF given in mass or one per DOF.

  • mass (Mapping[str, float]) – Effective mass (kg) for surge/sway/heave or inertia (kg m^2) for roll/pitch/yaw, including the hydrodynamic added mass, for each DOF to damp. DOFs absent from it are not damped.

  • natural_period (Mapping[str, float]) – Natural period (s) of each DOF in mass.

Returns:

SettlePlatformDamping – The absolute damping per DOF.

Raises:

ValueError – If a DOF name is unknown, a DOF in mass has no natural period (or fraction), or a mass, period or fraction is not positive and finite.

Return type:

SettlePlatformDamping

Examples

IEA 15-MW on the VolturnUS-S semi-submersible: surge mass about 3.3e7 kg (2.0e7 kg structure plus 1.3e7 kg added mass) with a surge period near 100 s under thrust; at half of critical damping

>>> damping = SettlePlatformDamping.from_critical_fraction(
...     0.5, mass={"surge": 3.3e7}, natural_period={"surge": 100.0}
... )
>>> round(damping.surge / 1e6, 2)
2.07
diagonal()[source]

Return the damping in HydroDyn DOF order (surge … yaw).

Returns:

tuple of float – (surge, sway, heave, roll, pitch, yaw).

Return type:

tuple[float, float, float, float, float, float]

SettleStage

Which run of a (two-stage) settle an attempt belongs to.

alias of Literal[‘settle’, ‘damped’, ‘confirm’]

exception SteadyStateError(message, *, convergence=None)[source]

Bases: RuntimeError

Raised when a settle run fails, optionally carrying convergence evidence.

Parameters:
Return type:

None

check_controller_library(fst_path, controller_library=None)[source]

Check that a settle run can load the deck’s Bladed-style controller library.

A settle run keeps the deck’s controller active, so when ServoDyn routes any control (PCMode, VSContrl, HSSBrMode, YCMode, AfCmode or CCmode = 5) through the controller library, OpenFAST loads DLL_FileName (relative to the ServoDyn file) at initialization. Published decks often carry a path valid only on the machine that generated them (e.g. a Linux libdiscon.so on Windows); this check turns that into a clear error before OpenFAST is launched. Nothing is written.

Parameters:
  • fst_path (str or pathlib.Path) – The primary .fst deck.

  • controller_library (str or pathlib.Path or None, optional) – A replacement controller library for the settle run. When given it must be an existing file; the deck’s own DLL_FileName is then not checked.

Returns:

pathlib.Path or None – The resolved controller_library to write into the settle copy’s DLL_FileName, or None when nothing is to be rewritten (no override given, or ServoDyn does not run the controller library).

Raises:
  • FileNotFoundError – If controller_library is given but does not exist.

  • SteadyStateError – If no override is given, ServoDyn runs the controller library, and its DLL_FileName does not name an existing file; or if the library to be used (the override, or the deck’s own) exists but cannot be loaded on this host (wrong architecture, corrupt, or missing a dependency), checked by loading it in a child process.

Return type:

Path | None

compare_settled_to_curve(settled, curve_point, tolerances=None)[source]

Compare a settled operating point with the curve value at its wind speed.

Parameters:
  • settled (OperatingPoint) – The operating point a settle run returned (the controller’s steady state).

  • curve_point (OperatingPoint) – The operating curve evaluated at the same wind speed (e.g. the seed the settle run started from).

  • tolerances (TrimTolerances or None, optional) – Rotor-speed (relative) and pitch (absolute) tolerances; None uses the TrimTolerances defaults (1 % and 0.5 deg).

Returns:

SettleCurveCheck – The rotor-speed and pitch comparisons and their combined status.

Raises:

ValueError – If the two points are at different wind speeds.

Return type:

SettleCurveCheck

solve_steady_state(fst_path, wind_speed, *, settle_time=1200.0, discard_time=600.0, rotor_speed_rpm=0.0, blade_pitch_deg=0.0, platform_position=None, executable='openfast', timeout=None, name='', velocity_tol=0.01, angular_velocity_tol=0.01, adaptive=False, max_settle_time=None, settle_growth=1.5, require_settled=False, wind_shear_exponent=0.0, controller_library=None, criterion='auto', platform_damping=None, confirm_time=None, equilibrium_tolerances=None, prescribed_rotor=False, platform_period=None)[source]

Run the fully-coupled model to steady state and read the operating point.

Rather than trust a fixed settle_time, the run is checked for genuine equilibrium: the platform-velocity channels’ RMS over the steady window must fall within tolerance (at equilibrium the platform is no longer moving). With adaptive, TMax is grown and the run repeated until that criterion is met or max_settle_time is reached. The channels this needs (the platform velocities, plus RotSpeed and BldPitch1) are appended to ElastoDyn’s OutList in the working copy when the deck does not already output them.

Every run is judged over a trailing window of fixed length settle_time - discard_time ending at that run’s TMax, i.e. [TMax - (settle_time - discard_time), TMax] (the first run’s window is [discard_time, settle_time]). A longer adaptive run therefore discards the early transient rather than diluting it, and the returned rotor speed, blade pitch and platform position are the means over that same trailing window. TStart is set to the window start, so only the window is written to the .out file.

Parameters:
  • fst_path (str or pathlib.Path) – Primary .fst deck (a writable working copy; edited in place).

  • wind_speed (float) – Steady hub-height wind speed (m/s) to settle at.

  • settle_time (float, optional) – Initial TMax for the settle run (s); should be long enough for the slow platform modes to settle past discard_time.

  • discard_time (float, optional) – Initial transient to discard (s) in the first run. Together with settle_time it fixes the analysis-window length settle_time - discard_time used by every (adaptive) run; output is written from the window start (TStart) and the window is averaged.

  • rotor_speed_rpm (float, optional) – Initial rotor speed (rpm) and collective blade pitch (deg) for the run; the controller drives them to their steady values, so these only need to be reasonable starting guesses.

  • blade_pitch_deg (float, optional) – Initial rotor speed (rpm) and collective blade pitch (deg) for the run; the controller drives them to their steady values, so these only need to be reasonable starting guesses.

  • platform_position (PlatformPosition or None, optional) – Initial six-DOF floating-platform position written to ElastoDyn. Fields left None retain the deck’s values. This changes only initial conditions; the active controller and physical modules remain untouched.

  • executable (str or pathlib.Path, optional) – OpenFAST executable name or path.

  • timeout (float or None, optional) – Run timeout in seconds (per settle run).

  • name (str, optional) – Label for the returned operating point.

  • velocity_tol (float, optional) – Tolerance on the RMS platform translational velocity (m/s) for declaring the platform settled. Default 1e-2.

  • angular_velocity_tol (float, optional) – Tolerance on the RMS platform angular velocity (deg/s). Default 1e-2.

  • adaptive (bool, optional) – When True, grow TMax by settle_growth and re-run until the platform settles within tolerance or max_settle_time is reached. Requires max_settle_time. Default False (a single run at settle_time).

  • max_settle_time (float or None, optional) – The largest TMax (s) the adaptive search may use; required when adaptive is True.

  • settle_growth (float, optional) – Factor by which TMax grows between adaptive iterations (> 1). Default 1.5.

  • require_settled (bool, optional) – When True, raise SteadyStateError if the platform is not confirmed settled (either the velocity RMS exceeds tolerance, or no platform-velocity channel was output to judge it). Default False (warn only).

  • wind_shear_exponent (float or None, optional) – InflowWind power-law shear exponent (PLExp) for the settle run. The default 0.0 is steady uniform inflow, the same default LinearizationSettings writes for the linearization, so the settled point is linearized in the inflow it settled in. None leaves the deck’s PLExp unchanged. Keep it equal to the linearization’s wind_shear_exponent; run_operating_sweep() does.

  • controller_library (str or pathlib.Path or None, optional) – Bladed-style controller library (e.g. a libdiscon.dll built for this host) to use for the settle run. When set, ServoDyn’s DLL_FileName in the deck being settled (a working copy) is rewritten to its absolute path. None (the default) keeps the deck’s own DLL_FileName, which must then exist. See check_controller_library().

  • criterion ({"auto", "velocity", "mean_drift"}, optional) – How a run is judged settled. "auto" (the default) applies the velocity criterion and switches to the mean-drift criterion only when the velocity criterion fails and the platform carries a sustained oscillation: one whose cycle-by-cycle RMS neither decays nor grows (its damping ratio, from the decay of the most active platform-velocity channel over the analysis window, lies within equilibrium_tolerances.residual_damping_ratio of zero). A still-decaying transient keeps the velocity criterion (an adaptive settle then runs longer), and a growing oscillation never switches it (a two-stage confirmation is judged as described under platform_damping). The criterion that judged the run is recorded in SettleConvergence.criterion (and per attempt), and a switch in SettleConvergence.criterion_switched. "velocity": every platform-velocity RMS over the analysis window is within velocity_tol/angular_velocity_tol – the platform has stopped moving. "mean_drift": the means of the platform position, rotor speed and blade pitch over the first and second halves of the analysis window agree within equilibrium_tolerances – the mean no longer moves, even if a stationary oscillation about it persists. Use it for a point that carries a sustained (e.g. controller-platform) oscillation the velocity criterion can never accept; the operating point is then the mean about which it oscillates, and the oscillation is still reported (its velocity RMS and SettleConvergence.oscillation_period). The criterion used is recorded in SettleConvergence.criterion. Each half, and the returned mean, is averaged over its trailing whole number of oscillation periods so the phase of a partial cycle does not bias it; make each half of the window span at least a couple of periods.

  • platform_damping (SettlePlatformDamping or None, optional) – Opt-in settle accelerator for floating platforms. When given, the settle runs in two stages. Stage 1 adds this linear damping to HydroDyn’s AddBLin in the working copy and settles (adaptively, as configured) until the criterion is met; the slow, lightly damped platform modes then decay in a cycle or two instead of ringing for many minutes. Stage 2 restores the deck’s own AddBLin byte for byte, restarts from the stage-1 means (rotor speed, blade pitch, platform position, and the tower-top and, when output, blade-tip deflections), and runs an undamped confirmation of confirm_time seconds, judged over its last three quarters. Every mean of the confirmation – and of any settle run – is averaged over the trailing whole number of the run’s dominant oscillation period (aligned to the window end; the plain window mean when no period is detected or less than one fits), so a partial cycle does not bias it. The point is accepted when the undamped run’s means reproduce the stage-1 means within equilibrium_tolerances (the shift test) and the undamped run is stable about them: it meets the criterion by itself or, for the velocity criterion, its residual oscillation decays with at least equilibrium_tolerances.residual_damping_ratio. Restarting even at the equilibrium rings the slow modes (the controller’s start-up transient does), and a lightly damped mode cannot ring down within a short run, so a decaying residual is accepted. A residual that decays but whose means have not yet returned (the shift test fails while the restart transient still biases them) gets one more confirmation run, twice as long (capped at max_settle_time when given), and that run is judged instead (SettleConfirmation.extended). With criterion="auto" a confirmation that keeps a non-decaying oscillation – sustained or growing – while passing the shift test is accepted at the stage-1 equilibrium when the oscillation is measurable: its residual damping ratio is known and its velocity RMS is within equilibrium_tolerances.oscillation_velocity_rms. A load linear in the platform velocity is zero at an equilibrium, so the damped stage’s equilibrium is the undamped model’s, stable or not, and the shift test has just reproduced it. SettleConfirmation.sustained_oscillation is set and a warning logged. When the residual is below -equilibrium_tolerances.growing_damping_ratio the point is also flagged SettleConfirmation.growing_oscillation with a distinct warning: the equilibrium is unstable under the deck’s controller, but the linearization trims without the controller, so it is linearized and the controller-in-the-loop instability is not part of the linear model. A failed shift test, a too large oscillation, too few cycles to measure it, or a non-decaying oscillation under the "velocity" or "mean_drift" criterion is reported as not converged with its reason (and raises under require_settled), never silently accepted. A single-stage settle never accepts a growing oscillation. A confirmed point is the stage-1 equilibrium; an unconfirmed one reports the undamped run’s means. Both stages are recorded in SettleConvergence.confirmation and attempts. See SettlePlatformDamping for why a load proportional to velocity cannot move the equilibrium. Requires an active HydroDyn with an AddBLin matrix.

  • confirm_time (float or None, optional) – TMax (s) of the undamped confirmation run of a two-stage settle; its analysis window is the last three quarters. None (the default) sizes it automatically so the analysis window holds eight periods of the slowest platform mode: 8 T / 0.75 rounded up to a whole second, with T the platform_period when given, else the damped stage’s dominant period. The length is never shorter than 1200 s (also used when no period is known) and never longer than max_settle_time when that is given, else 3600 s. The length and how it was chosen are recorded in SettleConfirmation.confirm_time and SettleConfirmation.confirm_time_source. An explicit value is used as given.

  • platform_period (float or None, optional) – The slowest platform natural period (s), e.g. the longest period of the standstill platform probe (slowest_period; run_operating_sweep() passes it), used to size the automatic confirm_time. None (the default) uses the damped stage’s dominant period instead. Ignored without platform_damping or with an explicit confirm_time.

  • equilibrium_tolerances (EquilibriumTolerances or None, optional) – Tolerances on a change of the settled means: between the damped and the undamped stage of a two-stage settle, and between window halves for criterion="mean_drift". None uses the EquilibriumTolerances defaults.

  • prescribed_rotor (bool, optional) – Settle without a controller. ServoDyn is switched off in the working copy (CompServo = 0, so no controller library is loaded and none is checked), and ElastoDyn’s generator and drivetrain DOFs are locked (GenDOF = DrTrDOF = False), so the rotor turns at exactly rotor_speed_rpm with the blades held at blade_pitch_deg while the platform finds its equilibrium under the resulting aerodynamic load. This is the fallback for a floating deck whose controller library is not available on this host: with a rotor-performance curve supplying the rotor speed and pitch, it yields the thrust-loaded platform position a CalcSteady linearization at that curve point starts from. The returned rotor speed and pitch are the prescribed ones; the record carries SettleConvergence.prescribed_rotor. controller_library must not be given with it. Default False.

Returns:

OperatingPoint – The settled operating point: the averaged rotor speed and blade pitch, and a PlatformPosition when the deck outputs the platform channels (otherwise platform is None).

Raises:
  • SteadyStateError – If the settle run fails, its output file cannot be read, or require_settled is set and equilibrium could not be confirmed; or, before OpenFAST launches, if ServoDyn runs the controller library, no controller_library is given, and the deck’s DLL_FileName does not exist.

  • FileNotFoundError – If controller_library is given but is not an existing file.

  • ValueError – If adaptive is set without max_settle_time or settle_growth <= 1; if any deck-written numeric input (wind_speed, rotor_speed_rpm, blade_pitch_deg, settle_time, discard_time, and max_settle_time when adaptive, and wind_shear_exponent when set) is non-finite; if wind_speed or rotor_speed_rpm is negative; if criterion is unknown; if prescribed_rotor is combined with controller_library; if platform_period is given but not positive and finite; or, with platform_damping, if confirm_time is given but not positive and finite or the deck has no active HydroDyn file with an AddBLin matrix (checked before OpenFAST launches).

Return type:

OperatingPoint

validate_settle_schedule(settle_time, discard_time, *, adaptive=False, max_settle_time=None, settle_growth=1.5, criterion='auto', confirm_time=None)[source]

Check a settle’s schedule and criterion before anything is launched.

solve_steady_state() runs these checks itself; a sweep calls them once up front so a bad schedule fails before its platform probe launches OpenFAST.

Parameters:
  • settle_time (float) – The settle run length and the discarded start-up (s).

  • discard_time (float) – The settle run length and the discarded start-up (s).

  • adaptive (bool, optional) – Whether the run is grown until it settles.

  • max_settle_time (float or None, optional) – The adaptive ceiling (s); required when adaptive.

  • settle_growth (float, optional) – The adaptive growth factor.

  • criterion (str, optional) – "auto", "velocity" or "mean_drift".

  • confirm_time (float or None, optional) – The confirmation length (s) of a two-stage settle; None is automatic.

Raises:

ValueError – If any value is out of range.

Return type:

None

Set up a floating-turbine settle automatically: controller library and damping.

A floating operating point is found by settling the fully coupled model and then linearizing at the settled mean (see solve_steady_state()). Two inputs of that settle used to come from the user; this module derives both from the deck itself.

The controller library. Published decks name the controller library of the machine that generated them (often a Linux libdiscon.so). find_controller_library() looks for a library this host can load: the deck’s own DLL_FileName; the same library with another platform or bitness suffix (.dll, _x64.dll, .so, …) next to it, next to the ServoDyn file and next to the primary deck; then in the directories or files listed by the caller, by the VANE_CONTROLLER_LIBRARIES environment variable (os.pathsep-separated) and by the configuration file controller_libraries.txt (one entry per line, # comments) in VANE’s configuration directory (VANE_CONFIG_DIR, else %APPDATA%\vane on Windows and $XDG_CONFIG_HOME/vane or ~/.config/vane elsewhere). Candidates are matched by the library’s name with the lib prefix, the bitness tag and the suffix ignored, so libdiscon.so matches libdiscon.dll, discon_x64.dll or DISCON.dll. When none is found the settle is not abandoned: plan_floating_settle() falls back to a prescribed-rotor settle (no controller; the rotor turns at the operating curve’s speed with the blades at its pitch while the platform finds its equilibrium), and the point is then linearized by CalcSteady from that equilibrium.

The settle damping. The two-stage settle accelerator (SettlePlatformDamping) needs, per platform DOF, the effective mass and the natural period. probe_platform() measures both from the model: it runs two single-snapshot standstill linearizations of the platform (still air and still water, rotor parked, aerodynamics and controller off, radiation memory off so no state-space hydrodynamic fit is needed), the second with a known damping added to HydroDyn’s AddBLin. The difference of the two state matrices is -M^-1 Delta_B – the platform columns of the exact inverse mass matrix, including the hydrodynamic added mass – and first-order eigenvalue perturbation of the undamped state matrix gives, for every mode, how fast a damper on each platform DOF damps it. For each DOF the mode it damps the most is its platform mode; that mode’s natural period, and the mass m = 1 / (2 dsigma/dB) for which a single-DOF oscillator would be damped at the same rate, size the damping as a fraction of critical (PlatformProbe.settle_damping()). The probe costs two OpenFAST runs of a fraction of a second of simulated time and needs no user data.

CONTROLLER_LIBRARIES_ENV = 'VANE_CONTROLLER_LIBRARIES'

Environment variable listing controller libraries or directories to search.

class ControllerLibraryResolution(status, library=None, deck_library='', source='', searched=())[source]

Bases: object

Where a settle run’s controller library comes from.

Parameters:
  • status ({"not_used", "deck", "found", "user", "missing"}) – "not_used": ServoDyn runs no controller library. "deck": the deck’s own DLL_FileName is loadable here. "found": a host build was found (library). "user": the caller supplied it. "missing": none could be found; a controller settle is impossible on this host.

  • library (pathlib.Path or None) – The library to write into the settle copy’s DLL_FileName; None unless status is "found" or "user".

  • deck_library (str) – The deck’s DLL_FileName as written (empty when not used).

  • source (str) – Where library was found (e.g. "next to the deck's library", "VANE_CONTROLLER_LIBRARIES"); empty when not searched.

  • searched (tuple of pathlib.Path) – The directories and files that were searched, in order.

status: Literal['not_used', 'deck', 'found', 'user', 'missing']
library: Path | None = None
deck_library: str = ''
source: str = ''
searched: tuple[Path, ...] = ()
property available: bool

Return whether a settle can run the deck’s controller on this host.

to_dict()[source]

Return the resolution as a JSON-ready dictionary.

Returns:

dict[str, object] – status, library, deck_library, source and searched (paths as strings).

Return type:

dict[str, object]

ControllerLibraryStatus

How find_controller_library() resolved the settle’s controller library.

alias of Literal[‘not_used’, ‘deck’, ‘found’, ‘user’, ‘missing’]

class FloatingSettlePlan(controller, floating, platform_damping=None, damping_source='none', probe=None, damping_fraction=0.5, notes=<factory>)[source]

Bases: object

How a sweep’s settle runs are set up, decided from the deck and this host.

Parameters:
  • controller (ControllerLibraryResolution) – How the controller library was resolved.

  • floating (bool) – Whether the deck models a floating platform (is_floating_deck()).

  • platform_damping (SettlePlatformDamping or None) – The two-stage settle damping to use (None: a single-stage settle).

  • damping_source ({"user", "probe", "none"}) – Whether the damping was given, sized from the platform probe, or is off.

  • probe (PlatformProbe or None) – The platform probe, when one was run.

  • damping_fraction (float) – Fraction of critical damping the probe-sized damping uses.

  • notes (tuple of str) – Human-readable notes on every automatic decision (also logged).

controller: ControllerLibraryResolution
floating: bool
platform_damping: SettlePlatformDamping | None = None
damping_source: Literal['user', 'probe', 'none'] = 'none'
probe: PlatformProbe | None = None
damping_fraction: float = 0.5
notes: tuple[str, ...]
property controller_library: Path | None

keep).

Type:

Return the library to write into the settle copies (None

property prescribed_rotor: bool

Return whether the settle must run without a controller (the fallback).

to_dict()[source]

Return the plan as a JSON-ready dictionary.

Returns:

dict[str, object] – The controller resolution, floating flag, damping and its source, the probe and the notes.

Return type:

dict[str, object]

class PlatformProbe(effective_mass, natural_period, damping_ratio, mode_frequency, probe_damping, n_states)[source]

Bases: object

Platform dynamics measured by two standstill linearizations.

Parameters:
  • effective_mass (dict[str, float]) – Per platform DOF, the mass (kg; kg m^2 for rotations) of a single-DOF oscillator that a damper on that DOF would damp at the same rate as it damps the DOF’s platform mode: 1 / (2 dsigma/dB). It includes the hydrodynamic added mass and the coupling to the other DOFs.

  • natural_period (dict[str, float]) – Per DOF, the undamped natural period (s) of its platform mode, 2 pi / |lambda|.

  • damping_ratio (dict[str, float]) – Per DOF, the damping ratio the platform mode already has at standstill.

  • mode_frequency (dict[str, float]) – Per DOF, the damped frequency (Hz) of its platform mode.

  • probe_damping (SettlePlatformDamping) – The damping added in the second linearization.

  • n_states (int) – Number of continuous states of the standstill linearization.

effective_mass: dict[str, float]
natural_period: dict[str, float]
damping_ratio: dict[str, float]
mode_frequency: dict[str, float]
probe_damping: SettlePlatformDamping
n_states: int
property slowest_period: float | None

Return the longest finite natural period (s) of the measured modes.

It sizes the undamped confirmation of a two-stage settle (platform_period of solve_steady_state()): the confirmation must span several periods of the slowest platform mode. Standstill periods are the right scale; under thrust the mooring stiffens and the periods shorten slightly (about 125 s at standstill against about 106 s at rated for the IEA 15-MW semi-submersible’s surge), so sizing from them errs long.

Returns:

float or None – The largest positive, finite period; None when there is none.

settle_damping(fraction=0.5)[source]

Return the settle damping as a fraction of critical per measured DOF.

Parameters:

fraction (float, optional) – Fraction of critical damping to add to every measured DOF (on top of the damping the mode already has). Default 0.5.

Returns:

SettlePlatformDamping – 4 pi fraction m / T per DOF (see SettlePlatformDamping.from_critical_fraction()).

Return type:

SettlePlatformDamping

to_dict()[source]

Return the probe as a JSON-ready dictionary.

Returns:

dict[str, object] – The per-DOF measurements, the probe damping, the state count and the slowest period.

Return type:

dict[str, object]

exception PlatformProbeError[source]

Bases: RuntimeError

Raised when the standstill platform probe cannot measure the platform.

controller_library_search_paths(environ=None)[source]

Return the configured controller-library locations and where each is from.

Parameters:

environ (Mapping[str, str] or None, optional) – The environment to read; None uses os.environ.

Returns:

tuple of (pathlib.Path, str) – Each configured file or directory with its origin: the VANE_CONTROLLER_LIBRARIES entries (os.pathsep-separated) first, then the lines of controller_libraries.txt in VANE’s configuration directory (blank lines and # comments ignored; relative entries are relative to that file).

Return type:

tuple[tuple[Path, str], …]

find_controller_library(fst_path, *, search_paths=(), environ=None)[source]

Find a controller library this host can load for a settle run.

Nothing is written; the result says what the settle should use.

Parameters:
  • fst_path (str or pathlib.Path) – The primary .fst deck.

  • search_paths (Sequence of str or pathlib.Path, optional) – Extra library files or directories to search, after the deck’s own locations and before the configured ones.

  • environ (Mapping[str, str] or None, optional) – The environment holding VANE_CONTROLLER_LIBRARIES / VANE_CONFIG_DIR; None uses os.environ.

Returns:

ControllerLibraryResolution – "not_used" when ServoDyn runs no controller library, "deck" when the deck’s own library loads here, "found" with the host build to use, or "missing".

Return type:

ControllerLibraryResolution

Notes

The search order is: the deck’s DLL_FileName; then, for the same library name (the lib prefix, a bitness tag and the suffix ignored, case folded), the directory of the deck’s library, the ServoDyn file’s directory and the primary deck’s directory; then search_paths; then VANE_CONTROLLER_LIBRARIES and the configuration file (see controller_library_search_paths()). Only libraries with this host’s suffix (.dll on Windows, .so on Linux, .dylib on macOS) are accepted; within a directory a plain or 64-bit build is preferred to a 32-bit one. A found library must still be compatible with the deck’s controller input file (DLL_InFile); VANE cannot check that.

is_floating_deck(fst_path)[source]

Return whether a deck models a floating platform.

A deck is floating when ElastoDyn frees at least one platform DOF and the platform is moored, or is carried by HydroDyn without a substructure module. A substructure (CompSub != 0) uses the platform DOFs too: without mooring it is a fixed-bottom monopile or jacket, with mooring a flexible floater modelled in SubDyn.

Parameters:

fst_path (str or pathlib.Path) – The primary .fst deck.

Returns:

bool – Whether the platform floats.

Return type:

bool

plan_floating_settle(fst_path, work_dir, *, controller_library=None, platform_damping=None, auto_damping=True, damping_fraction=0.5, controller_search_paths=(), template_root=None, template_copy='referenced', executable='openfast', timeout=None)[source]

Decide, without user input, how a deck’s settle runs are set up.

  1. Controller library. A given controller_library is checked and used. Otherwise find_controller_library() looks for a host build; when none exists the plan’s prescribed_rotor is True: the settle runs without a controller at the operating curve’s rotor speed and pitch (see prescribed_rotor of solve_steady_state()), and the point is then linearized by CalcSteady from the platform equilibrium it finds. That needs an operating curve; the caller enforces it.

  2. Settle damping. A given platform_damping is used as is. Otherwise, for a floating deck with auto_damping, probe_platform() measures the platform and the damping is damping_fraction of critical on every measured DOF. A probe that fails is logged and the settle runs undamped.

Parameters:
  • fst_path (str or pathlib.Path) – The template primary .fst deck.

  • work_dir (str or pathlib.Path) – Directory for the platform probe cases.

  • controller_library (str or pathlib.Path or None, optional) – A host controller library to use instead of searching.

  • platform_damping (SettlePlatformDamping or None, optional) – A settle damping to use instead of probing.

  • auto_damping (bool, optional) – Size the damping from the platform probe for a floating deck. Default True.

  • damping_fraction (float, optional) – Fraction of critical damping for the probe-sized damping (0 < f). Default 0.5.

  • controller_search_paths (Sequence of str or pathlib.Path, optional) – Extra library locations (see find_controller_library()).

  • template_root (str or pathlib.Path or None, optional) – The template copy root (see probe_platform()).

  • template_copy ({"referenced", "tree"}, optional) – How the template is copied for the probe.

  • executable (str or pathlib.Path, optional) – OpenFAST executable for the probe.

  • timeout (float or None, optional) – Per-run timeout (s) of the probe.

Returns:

FloatingSettlePlan – The resolved controller library, damping and the evidence behind them.

Raises:
  • FileNotFoundError – If controller_library is given but does not exist.

  • ValueError – If damping_fraction is not positive and finite.

Return type:

FloatingSettlePlan

platform_probe_from_jacobians(a_base, a_damped, descriptions, probe_damping=SettlePlatformDamping(surge=1000000.0, sway=1000000.0, heave=1000000.0, roll=1000000000.0, pitch=1000000000.0, yaw=1000000000.0))[source]

Measure the platform from two state matrices differing by a platform damping.

Parameters:
  • a_base (npt.NDArray[np.float64]) – Full-system state matrix A of the standstill linearization.

  • a_damped (npt.NDArray[np.float64]) – The same with probe_damping added to HydroDyn’s AddBLin.

  • descriptions (Sequence[str]) – The continuous-state descriptions (.lin “Order of continuous states”).

  • probe_damping (SettlePlatformDamping, optional) – The damping that was added.

Returns:

PlatformProbe – The per-DOF effective mass, natural period and damping ratio of each platform DOF that is free in the model and has an oscillatory mode.

Raises:

PlatformProbeError – If the matrices disagree in shape with the descriptions, no ElastoDyn platform DOF is free, or the probe damping did not change A.

Return type:

PlatformProbe

Notes

Adding a damping b_j on platform DOF j changes only the columns of the platform velocities: Delta A[:, v_j] = -M^{-1}[:, v_j] b_j. For each mode k with right and left eigenvectors u_k and w_k of a_base, the first-order sensitivity of its eigenvalue to b_j is

\[\frac{\partial \lambda_k}{\partial b_j} = -\frac{(w_k^H M^{-1}_{:,v_j}) \, u_{k, v_j}}{w_k^H u_k}\]

The platform mode of DOF j is the oscillatory mode whose decay rate -Re lambda the damper raises the most; a single-DOF oscillator of mass m has d(-Re lambda)/db = 1/(2m), which defines the effective mass.

probe_platform(fst_path, work_dir, *, template_root=None, template_copy='referenced', executable='openfast', timeout=None, probe_damping=SettlePlatformDamping(surge=1000000.0, sway=1000000.0, heave=1000000.0, roll=1000000000.0, pitch=1000000000.0, yaw=1000000000.0))[source]

Measure a floating platform’s effective masses and natural periods.

Two single-snapshot standstill linearizations of a copy of the deck are run in work_dir (see the module notes): still air and water, rotor parked, AeroDyn and ServoDyn off, HydroDyn radiation memory and wave excitation off; the second with probe_damping added to HydroDyn’s AddBLin. The template deck is never edited.

Parameters:
  • fst_path (str or pathlib.Path) – The template primary .fst deck.

  • work_dir (str or pathlib.Path) – Directory for the two probe cases (base/damped; existing ones are replaced).

  • template_root (str or pathlib.Path or None, optional) – Directory the deck’s files are copied from (default: the deck’s directory).

  • template_copy ({"referenced", "tree"}, optional) – How the template is copied (see copy_template_files()).

  • executable (str or pathlib.Path, optional) – OpenFAST executable.

  • timeout (float or None, optional) – Per-run timeout (s).

  • probe_damping (SettlePlatformDamping, optional) – The known damping added in the second run.

Returns:

PlatformProbe – The measured platform dynamics.

Raises:
  • PlatformProbeError – If a probe run fails, writes no .lin file, or cannot measure the platform.

  • ValueError – If the deck has no active HydroDyn with an AddBLin matrix.

Return type:

PlatformProbe

State-space radiation fits generated from WAMIT first-order radiation data.

A linearization of a floating platform needs HydroDyn’s state-space radiation model (RdtnMod = 2): the convolution model is not linearizable. That model reads a <PotFile>.ss file, which some published decks do not ship (e.g. the IEA 22-MW semi-submersible ships only the WAMIT .1/.3/.hst data). Without it the radiation memory must be switched off, leaving only the infinite-frequency added mass and no wave-radiation damping. This module generates the missing file from the WAMIT .1 data itself.

The fit. The radiation impulse-response function K_ij(t) relates the radiation force on DOF i to the velocity of DOF j; its transfer function is

\[K_{ij}(j\omega) = B_{ij}(\omega) + j\omega \left[A_{ij}(\omega) - A_{ij}(\infty)\right]\]

with A and B the frequency-dependent added mass and radiation damping. Each significant entry (every diagonal entry, and an off-diagonal one when its peak exceeds coupling_threshold of the geometric mean of the two diagonal peaks) is fitted in the frequency domain by a strictly proper rational function with stable poles, by vector fitting [Gustavsen1999]: the poles are relocated iteratively (unstable ones reflected into the left half-plane), and the residues of the final fit satisfy K_ij(0) = 0 exactly (the radiation force has no static part) by a constrained least-squares solve. The order grows until the relative error over the WAMIT frequencies, ||K_fit - K|| / ||K||, falls below tolerance. Each entry is realised in real modal form, the blocks grouped by input DOF as HydroDyn expects, and the output sign is that of the radiation force (F = -K * v).

Validation. The IEA 15-MW VolturnUS-S ships both WAMIT data and an .ss file. The generated fit matches the WAMIT data within 3.5–5 % per entry (the shipped fit within 10–21 %), its impulse responses deviate from the WAMIT cosine transform of the damping by 2–9 % of the peak (the shipped fit by 9–25 %), and a standstill linearization with it reproduces the platform frequencies of one with the shipped file within 1 % and their damping ratios within 0.003.

Wave-excitation fits (.ssexctn) are not generated. A linearization runs in still water, where the excitation states are driven by nothing and leave the platform dynamics unchanged, so ExctnMod = 0 loses only the wave-elevation input channel of the linear model.

References

[Gustavsen1999]

Gustavsen, B., & Semlyen, A. (1999). Rational approximation of frequency domain responses by vector fitting. IEEE Transactions on Power Delivery, 14(3), 1052-1061.

exception RadiationFitError[source]

Bases: ValueError

Raised when radiation data cannot be read or fitted.

class RadiationFitRecord(path, source, n_states, max_relative_error, entry_errors, written)[source]

Bases: object

Provenance of a generated .ss radiation file.

Parameters:
  • path (pathlib.Path) – The .ss file (written only when written).

  • source (pathlib.Path) – The WAMIT .1 file it was fitted to.

  • n_states (int) – Radiation states of the model.

  • max_relative_error (float) – Largest per-entry relative fit error.

  • entry_errors (dict[str, float]) – Relative fit error per fitted entry ("K11", "K15", …).

  • written (bool) – Whether the file was written (the fit met the acceptance limit).

path: Path
source: Path
n_states: int
max_relative_error: float
entry_errors: dict[str, float]
written: bool
property accepted: bool

Return whether every fitted entry is within the acceptance limit (0.2).

Returns:

bool – True when the fit is accurate enough to be written.

to_dict()[source]

Return the record as a JSON-ready dictionary.

Returns:

dict[str, object] – The fields, with the paths as strings.

Return type:

dict[str, object]

class RadiationPairFit(row, column, poles, coefficients, n_states, relative_error)[source]

Bases: object

The rational fit of one radiation-kernel entry K_ij.

Parameters:
  • row (int) – The 0-based force DOF i and velocity DOF j.

  • column (int) – The 0-based force DOF i and velocity DOF j.

  • poles (tuple of complex) – The stable poles (rad/s); one of each complex-conjugate pair.

  • coefficients (tuple of float) – Real partial-fraction coefficients: one per real pole, two (real and imaginary part of the residue) per complex pair.

  • n_states (int) – The order of the fit (states in its realisation).

  • relative_error (float) – ||K_fit - K|| / ||K|| over the WAMIT frequencies.

row: int
column: int
poles: tuple[complex, ...]
coefficients: tuple[float, ...]
n_states: int
relative_error: float
evaluate(frequencies)[source]

Return the fitted K_ij(j omega) at frequencies (rad/s).

Parameters:

frequencies (npt.NDArray[np.float64]) – Angular frequencies (rad/s).

Returns:

npt.NDArray[np.complex128] – The fitted kernel transfer.

Return type:

npt.NDArray[np.complex128]

realization()[source]

Return a real modal realisation (A, b, c) of the fitted K_ij.

Returns:

tuple of npt.NDArray[np.float64] – A (n x n), b (n) and c (n) with K_ij(s) = c (sI - A)^-1 b.

Return type:

tuple[npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.float64]]

class RadiationStateSpace(a, b, c, states_per_dof, pairs=())[source]

Bases: object

A HydroDyn state-space radiation model (the content of a .ss file).

The model maps the six platform velocities to the six radiation forces: x' = A x + B v, F = C x.

Parameters:
  • a (npt.NDArray[np.float64]) – State matrix, (n, n).

  • b (npt.NDArray[np.float64]) – Input matrix, (n, 6).

  • c (npt.NDArray[np.float64]) – Output matrix, (6, n).

  • states_per_dof (tuple of int) – States belonging to each input DOF (the blocks are grouped by input DOF).

  • pairs (tuple of RadiationPairFit, optional) – The per-entry fits (empty for a model read from a file).

a: npt.NDArray[np.float64]
b: npt.NDArray[np.float64]
c: npt.NDArray[np.float64]
states_per_dof: tuple[int, ...]
pairs: tuple[RadiationPairFit, ...] = ()
property n_states: int

Return the number of radiation states.

property max_relative_error: float

Return the largest per-entry relative fit error (nan if unknown).

force_response(frequencies)[source]

Return the velocity-to-radiation-force transfer C (j omega I - A)^-1 B.

For an exact model this equals -K(j omega).

Parameters:

frequencies (npt.NDArray[np.float64]) – Angular frequencies (rad/s).

Returns:

npt.NDArray[np.complex128] – Shape (n_frequencies, 6, 6).

Return type:

npt.NDArray[np.complex128]

class WamitRadiation(frequencies, added_mass, damping, added_mass_infinite)[source]

Bases: object

Dimensional frequency-dependent radiation coefficients of one body.

Parameters:
  • frequencies (npt.NDArray[np.float64]) – Angular frequencies (rad/s), positive and ascending, shape (n,).

  • added_mass (npt.NDArray[np.float64]) – Added mass A(omega), shape (n, 6, 6) (kg, kg m, kg m^2).

  • damping (npt.NDArray[np.float64]) – Radiation damping B(omega), shape (n, 6, 6).

  • added_mass_infinite (npt.NDArray[np.float64]) – Infinite-frequency added mass A(inf), shape (6, 6).

frequencies: npt.NDArray[np.float64]
added_mass: npt.NDArray[np.float64]
damping: npt.NDArray[np.float64]
added_mass_infinite: npt.NDArray[np.float64]
kernel()[source]

Return the radiation kernel transfer B + j omega (A - A_inf).

Returns:

npt.NDArray[np.complex128] – Shape (n, 6, 6).

Return type:

npt.NDArray[np.complex128]

ensure_radiation_state_space(fst_path, *, tolerance=0.05, overwrite=False, confine_to=None)[source]

Generate the <PotFile>.ss radiation file a deck lacks, from its .1.

Call it on a working copy before a linearization with RdtnMod = 2. Nothing is done when HydroDyn is off, uses no WAMIT potential flow (PotMod != 1), or the .ss file already exists (unless overwrite). The file is written only when every fitted entry is within 0.2 relative error; otherwise the record says so, nothing is written, and the caller should linearize with the radiation memory off (hydro_radiation_mode=0).

Parameters:
  • fst_path (str or pathlib.Path) – The primary .fst deck (a working copy; the file is written next to the WAMIT data it references).

  • tolerance (float, optional) – Per-entry fit tolerance (see fit_radiation_state_space()).

  • overwrite (bool, optional) – Refit and replace an existing .ss file. Default False.

  • confine_to (str or pathlib.Path or None, optional) – A directory the .ss file must lie in (a case copy): a PotFile that resolves outside it, e.g. into a shared template, is rejected before anything is fitted or written. None (the default) does not check.

Returns:

RadiationFitRecord or None – The fit’s provenance, or None when nothing needed to be generated.

Raises:

RadiationFitError – If the deck uses more than one WAMIT body, the .1 file is missing or cannot be fitted, or the .ss file would lie outside confine_to.

Return type:

RadiationFitRecord | None

fit_radiation_state_space(radiation, *, tolerance=0.05, max_states_per_entry=20, coupling_threshold=0.05, iterations=20)[source]

Fit a state-space radiation model to WAMIT radiation coefficients.

See the module notes for the method.

Parameters:
  • radiation (WamitRadiation) – The dimensional radiation coefficients.

  • tolerance (float, optional) – Target relative error of each fitted entry over the WAMIT frequencies; the order grows (two states at a time) until it is met. Default 0.05.

  • max_states_per_entry (int, optional) – Largest order tried per entry. Default 20.

  • coupling_threshold (float, optional) – An off-diagonal entry is fitted when its peak magnitude is at least this fraction of the geometric mean of the two diagonal peaks. Default 0.05.

  • iterations (int, optional) – Pole-relocation iterations per order. Default 20.

Returns:

RadiationStateSpace – The assembled model, its blocks grouped by input DOF, with the per-entry fits.

Raises:

RadiationFitError – If an argument is out of range or an entry cannot be fitted.

Return type:

RadiationStateSpace

preview_radiation_state_space(fst_path, *, tolerance=0.05)[source]

Fit a deck’s WAMIT .1 radiation data without writing anything.

The fit ensure_radiation_state_space() would write, for deciding before any case is copied whether the .ss file can be generated (RadiationFitRecord.accepted). An existing .ss file is ignored.

Parameters:
Returns:

RadiationFitRecord or None – The fit’s record (written is False), or None when HydroDyn is off or uses no WAMIT potential flow.

Raises:

RadiationFitError – As ensure_radiation_state_space().

Return type:

RadiationFitRecord | None

read_radiation_state_space(path)[source]

Read a HydroDyn .ss radiation file.

Parameters:

path (str or pathlib.Path) – The .ss file.

Returns:

RadiationStateSpace – The model (with no per-entry fits).

Raises:

RadiationFitError – If the file is malformed.

Return type:

RadiationStateSpace

read_wamit_radiation(path, *, water_density, length_scale=1.0)[source]

Read a WAMIT .1 file (single body) into dimensional coefficients.

Each row is PER I J Abar [Bbar]. PER = 0 is the infinite-frequency (zero-period) added mass and PER < 0 the zero-frequency one; other rows are at omega = 2 pi / PER. The non-dimensional values are scaled by rho L^k (added mass) and rho omega L^k (damping), k = 3 for two translations, 4 for mixed and 5 for two rotations.

Parameters:
  • path (str or pathlib.Path) – The .1 file.

  • water_density (float) – Water density rho (kg/m^3).

  • length_scale (float, optional) – WAMIT’s characteristic length L (HydroDyn WAMITULEN). Default 1.

Returns:

WamitRadiation – The coefficients at the finite positive frequencies, and A(inf).

Raises:

RadiationFitError – If the file has DOF indices beyond 6 (more than one body), no infinite-frequency rows, fewer than four finite frequencies, or a malformed row; or if water_density/length_scale is not positive and finite.

Return type:

WamitRadiation

write_radiation_state_space(model, path, *, header='')[source]

Write a model in HydroDyn’s .ss format.

The file holds a header line, the six enabled-DOF flags, the state count, the states per DOF, then the rows of A, B and C. It is written through an atomic replace, so a hard-linked copy never writes through to a template.

Parameters:
Returns:

pathlib.Path – The path written.

Return type:

Path

Run the OpenFAST executable on an input deck and collect its .lin files.

OpenFAST is invoked as openfast <primary>.fst from the deck’s directory (so the relative sub-file references resolve), and on success writes one full-system linearization file per linearization time, named <RootName>.<i>.lin for i = 1 .. NLinTimes. This module wraps that invocation: it locates the executable, runs it (capturing output and the return code), and discovers the produced .lin files — which feed straight into vane.io.read_lin_file() and the modal pipeline, closing the loop from model deck to identified modes.

class OpenFastDiagnosis(mode, detail, openfast_message='', failure_time=None)[source]

Bases: object

A classified OpenFAST failure.

Parameters:
  • mode (OpenFastFailureMode) – The failure class.

  • detail (str) – A one-line description of the failure.

  • openfast_message (str) – The relevant excerpt of OpenFAST’s own output (its fatal error message, or the last lines it printed), empty when none applies.

  • failure_time (float or None) – The simulation time (s) at which OpenFAST stopped with a fatal error, parsed from encountered an error at simulation time <t> of <TMax> seconds. 0.0 for a failure during initialisation; None when OpenFAST did not report it.

mode: OpenFastFailureMode
detail: str
openfast_message: str = ''
failure_time: float | None = None
property retriable: bool

Return whether other trim settings could plausibly fix the failure.

An initialisation error is an input problem and a non-finite linearization a modelling degeneracy, so neither is retriable.

property summary: str

Return the failure mode and detail as one line.

exception OpenFastError[source]

Bases: RuntimeError

Raised when the OpenFAST executable cannot be found or fails to run.

class OpenFastFailureMode(*values)[source]

Bases: str, Enum

Why an OpenFAST linearization run failed, as read from its output.

The value of each member is the stable string recorded in provenance.

TRIM_NOT_CONVERGED

The CalcSteady trim did not reach a periodic steady state before TMax: OpenFAST logs Steady state not found before end of simulation and forces a single linearization, or fewer .lin files than NLinTimes are written.

MOTORING

The generator speed went negative (u%HSS_Spd is negative), which the simple variable-speed controller rejects. Usually an overshooting trim, or trim damping destabilising light blades when it happens near t = 0.

ROTOR_OVERSPEED

CalcSteady saw the rotor advance more than one azimuth step in a time step (The rotor is spinning too fast); in practice a divergence.

DIVERGENCE

The run ended on a fatal error after signs of a diverging response (small-angle violations, NaNs, a structural solver that stopped converging).

INITIALIZATION_ERROR

OpenFAST failed while initialising its modules: an input problem that no trim setting fixes.

TIMEOUT

The run exceeded the caller’s wall-clock timeout and was killed.

INCOMPLETE_LINEARIZATION

An untrimmed explicit-time sweep exited normally but wrote fewer .lin files than requested.

NON_PERIODIC_SWEEP

A CalcSteady run wrote every requested .lin file, but the rotor speed in their headers varies across the azimuth sweep by more than MBC3 accepts (vane.io.mbc_transform.ROTOR_SPEED_DRIFT_RTOL): the trim stopped on a response that is not periodic, typically because TrimTol was loosened. The linearization driver detects it from the files; classify_openfast_output() never returns it.

NON_FINITE_LINEARIZATION

The run exited normally and wrote every requested .lin file, but a file holds a NaN or infinite value (an operating-point value or a matrix entry). This is a modelling degeneracy, such as AeroDyn unsteady aerodynamics left on at the blade-tip node, that other trim settings cannot cure. The linearization driver detects it by scanning the files; classify_openfast_output() never returns it.

CASE_ERROR

Not an OpenFAST failure: the linearization driver raised an unexpected exception while it generated, ran or collected the case (a file-system error while the case was copied or configured, say). A sweep records it per case rather than aborting (see vane.run.LinearizationDriver.run_sweep()); it is never retried, and classify_openfast_output() never returns it.

UNKNOWN

A non-zero exit with no recognised signature.

class OpenFastResult(fst_path, returncode, lin_files=<factory>, output_file=None, summary_file=None, stdout='', stderr='')[source]

Bases: object

Outcome of an OpenFAST run.

Parameters:
  • fst_path (pathlib.Path) – The primary .fst deck that was run.

  • returncode (int) – The executable’s exit status (0 on success).

  • lin_files (list[pathlib.Path]) – Full-system .lin files produced, ordered by linearization index.

  • output_file (pathlib.Path or None) – The time-series output file (.out/.outb) if one was written.

  • summary_file (pathlib.Path or None) – The .sum summary file if one was written.

  • stdout (str) – Captured standard output of the run.

  • stderr (str) – Captured standard error of the run (OpenFAST may write diagnostics here).

fst_path: Path
returncode: int
lin_files: list[Path]
output_file: Path | None = None
summary_file: Path | None = None
stdout: str = ''
stderr: str = ''
property succeeded: bool

Return whether OpenFAST exited successfully.

property output: str

Return the combined standard output and error of the run.

property n_lin: int

Return the number of full-system .lin files produced.

property diagnosis: OpenFastDiagnosis | None

Return the classified failure of this run, or None when none is seen.

A shorthand for classify_openfast_output() on the captured output and return code. It does not check the .lin count against the requested NLinTimes; the linearization driver does that.

exception OpenFastTimeoutError[source]

Bases: OpenFastError

Raised when an OpenFAST run exceeds its timeout.

A distinct subclass so callers can treat a timeout (a slow but retriable hard operating point) differently from a configuration failure (a missing deck or executable), which retrying would not fix.

classify_openfast_output(output, returncode)[source]

Classify an OpenFAST run’s failure from its console output.

Parameters:
  • output (str) – The run’s combined standard output and error.

  • returncode (int) – The executable’s exit status.

Returns:

OpenFastDiagnosis or None – The classified failure, or None for a successful exit without the forced-linearization warning. A successful exit with that warning is OpenFastFailureMode.TRIM_NOT_CONVERGED.

Return type:

OpenFastDiagnosis | None

Notes

A non-zero exit is classified in this order: an error during module initialisation, generator motoring, rotor overspeed, a divergence signature, the forced-linearization warning, and otherwise OpenFastFailureMode.UNKNOWN. Initialisation comes first because a deck that cannot be read fails there whatever else is printed.

OpenFAST wraps its console messages at about 100 columns, so a signature can be split across two lines (u%HSS_Spd is / negative.). Matching is therefore done on the output with every run of whitespace, line breaks included, collapsed to a single space, and the quoted message joins the lines it spans.

find_lin_files(fst_path)[source]

Return the full-system .lin files for a deck, ordered by index.

Parameters:

fst_path (str or pathlib.Path) – The primary .fst deck (its stem and directory locate the outputs).

Returns:

list[pathlib.Path] – The <root>.<i>.lin files sorted by i; per-module files (<root>.<Module>.<i>.lin) are excluded.

Return type:

list[Path]

find_openfast(executable='openfast')[source]

Locate the OpenFAST executable.

Parameters:

executable (str or pathlib.Path, optional) – Executable name to look up on PATH, or an explicit path to it.

Returns:

pathlib.Path – The resolved executable path.

Raises:

OpenFastError – If the executable is neither an existing file nor found on PATH.

Return type:

Path

run_openfast(fst_path, *, executable='openfast', timeout=None, extra_args=(), check_path_lengths=None)[source]

Run OpenFAST on a deck and collect the linearization output.

The executable is invoked from the deck’s directory so its relative sub-file references resolve. Pre-existing generated .lin, time-series, and summary files for this deck are removed first so the returned paths reflect only this run.

Parameters:
  • fst_path (str or pathlib.Path) – The primary .fst deck to run. Should already be configured for linearization (Linearize = True).

  • executable (str or pathlib.Path, optional) – OpenFAST executable name or path (default "openfast" on PATH).

  • timeout (float or None, optional) – Maximum seconds to allow the run; None waits indefinitely.

  • extra_args (tuple[str, ...], optional) – Additional command-line arguments inserted before the deck path.

  • check_path_lengths (bool or None, optional) – Run check_deck_path_lengths() before launching. None (the default) checks on Windows only, where the path limit applies.

Returns:

OpenFastResult – The return code, captured output, and the produced .lin files.

Raises:
  • OpenFastError – If the executable cannot be found, the deck does not exist, or the deck’s directory or the executable can be named only with the Windows \\?\ extended-length prefix (longer than the Windows path limit). A shorter path given with the prefix is passed without it.

  • vane.run.DeckPathLengthError – If OpenFAST would open one of the deck’s files by a path longer than the Windows limit (checked before the process is launched).

  • OpenFastTimeoutError – If the run exceeds timeout (a subclass of OpenFastError).

  • ValueError – If timeout is not positive and finite.

Return type:

OpenFastResult

Notes

The executable is started in its own process group (Windows) or session (POSIX). On a timeout the whole process tree is killed, not just the direct child, so a wrapper executable (a .bat or shell script that launches OpenFAST) cannot leave an orphaned solver running and holding the output pipes open.

Match an OpenFAST deck to an OpenFAST executable that can read it.

OpenFAST changes its input-file format between releases, and a deck written for one release is rejected by another: a v4 primary file fails on v5 at ConvTol (v5 added the tight-coupling lines), and a v4.0 deck fails on v4.2 (no ElastoDyn HubIner_Teeter) and on v4.1 if it has HydroDyn members (the old NPropSets table). This module

  • recognises a deck’s input-format generation from the keywords its files carry (detect_deck_generation());

  • reads an executable’s release from its version banner (openfast -v, openfast_version()), cached per executable file;

  • chooses a compatible executable from the explicit argument, the VANE_OPENFAST_EXECUTABLES environment variable, a user configuration file and PATH (select_openfast_executable()), and fails before any case is copied when none can read the deck, naming the release the deck needs.

The keyword signatures follow the OpenFAST API-change notes for each release.

OPENFAST_CONFIG_ENV = 'VANE_OPENFAST_CONFIG'

Environment variable naming a configuration file of OpenFAST executables.

OPENFAST_EXECUTABLES_ENV = 'VANE_OPENFAST_EXECUTABLES'

Environment variable listing OpenFAST executables, separated by os.pathsep.

class DeckGeneration(label, min_version, max_version, evidence=())[source]

Bases: object

The OpenFAST release range whose input format a deck is written in.

Parameters:
  • label (str) – A short name of the format generation, e.g. "v5", "v4.2" or "v4.0-v4.1".

  • min_version (tuple[int, int, int]) – The oldest release that reads the deck (inclusive).

  • max_version (tuple[int, int, int] or None) – The first release that no longer reads it (exclusive); None when no upper bound is known.

  • evidence (tuple[str, ...]) – The keyword findings the range was inferred from, for the log and the run provenance.

label: str
min_version: tuple[int, int, int]
max_version: tuple[int, int, int] | None
evidence: tuple[str, ...] = ()
accepts(version)[source]

Return whether an executable of version reads this deck’s format.

Parameters:

version (tuple[int, int, int]) – The executable’s release.

Returns:

bool – True when min_version <= version < max_version.

Return type:

bool

describe()[source]

Return the accepted release range as text, e.g. >=4.2.0, <5.0.0.

Returns:

str – The range in requirement notation.

Return type:

str

to_dict()[source]

Return the generation as a JSON-serializable dictionary.

Returns:

dict[str, object] – label, requires (the release range) and evidence.

Return type:

dict[str, object]

class OpenFastSelection(executable, version, generation, source, considered=<factory>)[source]

Bases: object

The OpenFAST executable chosen for a deck, with what was considered.

Parameters:
  • executable (pathlib.Path) – The executable to run.

  • version (tuple[int, int, int] or None) – Its release, or None when its banner could not be read.

  • generation (DeckGeneration) – The deck’s input-format generation.

  • source (str) – Where the executable came from: "argument", the environment variable, the configuration file, or "PATH".

  • considered (tuple[tuple[str, str], ...]) – Every candidate examined, as (path, version or "unknown") pairs.

executable: Path
version: tuple[int, int, int] | None
generation: DeckGeneration
source: str
considered: tuple[tuple[str, str], ...]
to_dict()[source]

Return the selection as a JSON-serializable dictionary.

Returns:

dict[str, object] – The executable, its version, the source, the deck generation and the candidates considered.

Return type:

dict[str, object]

candidate_executables(executable=None)[source]

Return the OpenFAST executables to consider, in order of preference.

The order is: the explicit executable argument; the entries of the VANE_OPENFAST_EXECUTABLES environment variable (separated by os.pathsep); the lines of the configuration file named by VANE_OPENFAST_CONFIG (default ~/.vane/openfast_executables.txt; one path per line, # comments); and openfast, openfast_x64 and OpenFAST on PATH. A leading ~ in a listed path (or in VANE_OPENFAST_CONFIG) is the user’s home directory. Duplicates (the same resolved file) are dropped; entries that do not exist are kept, so the caller can report them.

Parameters:

executable (str or pathlib.Path or None, optional) – An explicitly requested executable, listed first.

Returns:

list[tuple[str, str]] – (executable, source) pairs, where source says where it came from.

Return type:

list[tuple[str, str]]

detect_deck_generation(fst_path)[source]

Return the OpenFAST release range whose input format a deck uses.

The decision reads keyword signatures the releases introduced or removed:

  • v5 (>=5.0): the primary file has the tight-coupling or multi-rotor lines (ModCoupling, RhoInf, ConvTol, MaxConvIter, NRotors, MirrorRotor), or ElastoDyn has PitchDOF/BlPIner(1)/PBrIner(1).

  • unknown (any release): the primary file has no CompHydro line or no readable ElastoDyn file, so it carries no format signal.

  • v3 (<4.0): the primary file has CompHydro but no CompSeaSt, which v4.0 added.

  • v4.2 (>=4.2, <5.0): ElastoDyn has HubIner_Teeter.

  • v4.1 (>=4.1, <4.2): HydroDyn has the cylindrical/rectangular member tables (NPropSetsCyl) but ElastoDyn lacks HubIner_Teeter.

  • v4.0 (>=4.0, <4.1): HydroDyn has the single member table (NPropSets).

  • otherwise v4.0-v4.1 (>=4.0, <4.2): a v4 deck without HydroDyn members and without HubIner_Teeter.

Parameters:

fst_path (str or pathlib.Path) – The primary .fst deck.

Returns:

DeckGeneration – The release range and the keyword evidence for it.

Raises:

vane.io.FastInputError – If the primary deck cannot be read.

Return type:

DeckGeneration

openfast_version(executable)[source]

Return an OpenFAST executable’s release from its version banner.

Runs <executable> -v once and parses OpenFAST-v<major>.<minor>.<patch>. The result is cached per executable file (path, modification time and size), so repeated selections cost nothing.

Parameters:

executable (str or pathlib.Path) – The executable’s path or a name on PATH.

Returns:

tuple[int, int, int] or None – The release, or None when the executable cannot be found or run, or its output carries no version banner.

Return type:

tuple[int, int, int] | None

select_openfast_executable(fst_path, executable=None)[source]

Choose an OpenFAST executable that can read a deck’s input format.

The deck’s generation is detected (detect_deck_generation()) and each candidate (candidate_executables()) is probed for its release (openfast_version()).

  • An explicit executable is always the one used when it can read the deck or its version cannot be read (logged); if its version is known and outside the deck’s range, the selection fails rather than silently running another one.

  • Otherwise the first candidate whose release the deck accepts is used; failing that, the first candidate whose version could not be read (with a warning).

Parameters:
  • fst_path (str or pathlib.Path) – The primary .fst deck.

  • executable (str or pathlib.Path or None, optional) – An explicitly requested executable. None (the default) chooses among the configured ones.

Returns:

OpenFastSelection – The executable, its version, and the evidence of the choice.

Raises:

vane.run.OpenFastError – If no candidate can read the deck: the message names the release range the deck needs, the keyword evidence, and every executable considered with its version. Raised before any case is copied or launched.

Return type:

OpenFastSelection

Find and copy only the files an OpenFAST deck references.

A sweep copies the template case once per operating point so that each case can be edited independently. When the template’s sub-files live outside the deck’s own folder (../5MW_Baseline/...), template_root must be a higher directory – for the OpenFAST regression-test suite, a directory holding dozens of unrelated cases and their outputs. Copying that whole tree per point costs hundreds of megabytes per case and carries unrelated .lin files into the case directory.

deck_file_closure() instead walks the deck’s references: starting from the primary .fst it reads every referenced text input (ElastoDyn, its blade and tower files, AeroDyn and its airfoil polars and @-prefixed coordinate files, ServoDyn and its controller DLL and controller input file, the controller’s own performance file, InflowWind wind files, SeaState, HydroDyn and its potential-flow data, SubDyn, MoorDyn, structural-control files, …) and follows them recursively. The walk does not rely on a keyword list: every value token that looks like a path is resolved, relative to the referencing file’s directory (the OpenFAST convention) and to the deck’s directory (OpenFAST’s working directory), and kept only if it names an existing file under template_root. Tokens that are numbers, "unused", "default" or do not name a file are ignored, so an unknown keyword never stops the walk. Two kinds of reference name more than one file and are expanded:

  • a root name (HydroDyn PotFile, SeaState WvKinFile, InflowWind FilenameRoot and any other keyword ending in Root) brings every file <root>.<ext> in its directory (.1, .3, .hst, .ss, .ssexctn, .12d, .12s, .Elev, …), except OpenFAST output files;

  • a shared library (.dll, .so, .dylib) brings its platform and bitness variants of the same stem (DISCON.dll, DISCON.so, DISCON_x64.dll, …). Other files next to the library are copied only when they are referenced.

copy_template_files() copies the closure into a case directory, preserving each file’s path relative to template_root so every relative reference still resolves. When the walk cannot vouch for completeness – a relative reference that escapes template_root or an input that cannot be read – it logs a warning and copies the whole tree instead, exactly as before; mode="tree" selects that conservative copy outright. Correctness takes precedence over size. A reference given as an absolute path outside template_root keeps working from the case copy and is left in place (an edited input outside the case is still rejected by require_inputs_within()).

class DeckFileClosure(root, files, external=(), open_paths=())[source]

Bases: object

The files a deck references, found by deck_file_closure().

Parameters:
  • root (pathlib.Path) – The template root every file in files lies under.

  • files (tuple[pathlib.Path, ...]) – Absolute paths of the referenced files under root, the primary deck included, sorted.

  • external (tuple[pathlib.Path, ...]) – Absolute references to existing files outside root. They are not copied; an absolute path keeps resolving from the case copy.

  • open_paths (tuple[tuple[pathlib.Path, str], ...]) – For each referenced file (the deck included), the path string OpenFAST opens it by when run from the deck’s directory on the deck’s bare file name: the referencing file’s directory as OpenFAST spelled it, joined to the reference but not normalized (<deck dir>\.\..\Shared\Blade.dat). Relative strings are prefixed with the deck’s directory, so each is the full path the operating system receives. When a file is reached by several references, the longest spelling is kept.

root: Path
files: tuple[Path, ...]
external: tuple[Path, ...] = ()
open_paths: tuple[tuple[Path, str], ...] = ()
property relative_files: tuple[Path, ...]

Return the referenced files relative to root.

Returns:

tuple[pathlib.Path, …] – Each file’s path relative to the template root.

property total_bytes: int

Return the summed size of the referenced files in bytes.

Returns:

int – The total size of files on disk.

exception DeckReferenceError[source]

Bases: ValueError

Raised when the referenced-file closure of a deck cannot be trusted.

The walk raises it for a relative reference that resolves outside template_root (the case copy could not reproduce it) or for an input file that cannot be read. copy_template_files() catches it and falls back to copying the whole template tree.

TemplateCopyMode

its referenced files, or the whole tree.

Type:

How a template is copied into a case

alias of Literal[‘referenced’, ‘tree’]

copy_template_files(fst_path, template_root, destination, *, mode='referenced', link_inputs=True)[source]

Copy a template case into destination.

Parameters:
  • fst_path (str or pathlib.Path) – The primary .fst deck inside template_root.

  • template_root (str or pathlib.Path) – The directory copied from; file paths relative to it are preserved.

  • destination (str or pathlib.Path) – The case directory to create. It must not exist yet.

  • mode ({"referenced", "tree"}, optional) – "referenced" (the default) copies only the deck’s referenced files (deck_file_closure()); "tree" copies the whole template tree.

  • link_inputs (bool, optional) – When True (the default), input files are hard-linked rather than copied, so heavy read-only inputs share an inode with the template; OpenFAST output files (.out, .lin, …) are always copied. Linking falls back to a byte copy where it is unsupported (e.g. across filesystems).

Returns:

{“referenced”, “tree”} – The copy that was made: "tree" when requested, or when the referenced-file walk could not vouch for completeness and fell back (logged as a warning).

Raises:
Return type:

Literal[‘referenced’, ‘tree’]

deck_file_closure(fst_path, template_root)[source]

Return every file under template_root that a deck references, recursively.

Parameters:
  • fst_path (str or pathlib.Path) – The primary .fst deck.

  • template_root (str or pathlib.Path) – The directory the referenced files are collected from; must contain the deck.

Returns:

DeckFileClosure – The referenced files (the deck included) and any absolute references outside the root.

Raises:
  • FileNotFoundError – If fst_path is not an existing file.

  • ValueError – If fst_path is not inside template_root.

  • DeckReferenceError – If a relative reference resolves to an existing file outside template_root, or a referenced input cannot be read.

Return type:

DeckFileClosure

Notes

A value token is treated as a path when it contains ., / or \ and is not a number; root-name keywords are expanded whatever their spelling. A token is kept only if it names an existing file, so prose in a description or a stale keyword cannot add a missing file or stop the walk. Including an unneeded file is harmless; the walk errs on that side.

infer_template_root(fst_path)[source]

Return the smallest directory holding every file a deck references relatively.

A deck that references shared inputs outside its own folder (../5MW_Baseline/..., ../IEA-15-240-RWT/...) must be copied per case from a directory that contains them, so that every relative reference still resolves in the copy and every edited sub-file lies inside it. This walks the deck’s references (as deck_file_closure() does, without a root boundary) and returns the deepest common ancestor of the deck’s directory and every file reached through a relative reference. References given as absolute paths keep working from any copy and do not widen the root.

Parameters:

fst_path (str or pathlib.Path) – The primary .fst deck.

Returns:

pathlib.Path – The inferred template root; the deck’s own directory when every relative reference lies under it.

Raises:

FileNotFoundError – If fst_path is not an existing file.

Return type:

Path

Notes

A referenced input that cannot be read stops the walk; the files found up to that point still widen the root, and the reason is logged.

System identification

System identification — state-space export and Kalman-ready matrices.

State space and reduced-order models

Clean linear-time-invariant state-space representation.

A StateSpace holds the validated A, B, C, D matrices of a continuous- or discrete-time system together with the state, input, and output names. The averaged matrices of an MBC transform are exported directly into this form, and a zero-order-hold discretization is provided for use in a digital twin.

class StateSpace(a, b=None, c=None, d=None, state_names=<factory>, input_names=<factory>, output_names=<factory>, dt=None, output_mbc_coordinates=<factory>)[source]

Bases: object

A linear time-invariant state-space system.

Parameters:
  • a (npt.NDArray[np.float64]) – State matrix, shape (n_states, n_states).

  • b (npt.NDArray[np.float64] or None) – Input, output, and feedthrough matrices, shapes (n_states, n_inputs), (n_outputs, n_states), (n_outputs, n_inputs); any may be None.

  • c (npt.NDArray[np.float64] or None) – Input, output, and feedthrough matrices, shapes (n_states, n_inputs), (n_outputs, n_states), (n_outputs, n_inputs); any may be None.

  • d (npt.NDArray[np.float64] or None) – Input, output, and feedthrough matrices, shapes (n_states, n_inputs), (n_outputs, n_states), (n_outputs, n_inputs); any may be None.

  • state_names (list[str]) – Names of the states, inputs, and outputs; each is either empty or matches the corresponding dimension.

  • input_names (list[str]) – Names of the states, inputs, and outputs; each is either empty or matches the corresponding dimension.

  • output_names (list[str]) – Names of the states, inputs, and outputs; each is either empty or matches the corresponding dimension.

  • dt (float or None) – Sample time; None for a continuous-time system, otherwise the discrete sample time in seconds.

  • output_mbc_coordinates (list[str]) – Multi-blade coordinate of each output ("collective"/"cosine"/ "sine" for a transformed blade-triplet output, else ""), aligned with output_names. Empty when unknown. Carried from an MBC transform so a load map can tell a transformed multi-blade output apart from a literal per-blade channel.

Raises:

ValueError – If the matrix shapes or name lengths are inconsistent.

a: npt.NDArray[np.float64]
b: npt.NDArray[np.float64] | None = None
c: npt.NDArray[np.float64] | None = None
d: npt.NDArray[np.float64] | None = None
state_names: list[str]
input_names: list[str]
output_names: list[str]
dt: float | None = None
output_mbc_coordinates: list[str]
property n_states: int

Return the number of states.

property n_inputs: int

Return the number of inputs (from B, else D, else 0).

A feedthrough-only system (B is None but D present) still has D.shape[1] inputs; the feedthrough columns define them.

property n_outputs: int

Return the number of outputs (from C, else D, else 0).

A feedthrough-only system (C is None but D present) still has D.shape[0] outputs; the feedthrough rows define them.

property is_discrete: bool

Return whether the system is discrete-time.

is_stable()[source]

Return whether the system is asymptotically stable.

Returns:

bool – For a continuous system, whether every eigenvalue of A has negative real part; for a discrete system, whether every eigenvalue lies strictly inside the unit circle.

Return type:

bool

discretized(dt)[source]

Return a zero-order-hold discretization at sample time dt.

Parameters:

dt (float) – Sample time in seconds; must be positive.

Returns:

StateSpace – The discrete-time system; C and D are unchanged.

Raises:

ValueError – If dt is not a positive, finite number, or the system is already discrete.

Return type:

StateSpace

state_space_from_mbc(result)[source]

Export the averaged MBC matrices as a continuous-time state-space system.

Parameters:

result (MBCResult) – An MBC transform result whose avg_a is present.

Returns:

StateSpace – The continuous-time system with state names from the result.

Raises:

ValueError – If result.avg_a is None.

Return type:

StateSpace

static_gain(system)[source]

Return the zero-frequency input-output gain D - C A^-1 B.

This quantity is especially important for module-level OpenFAST models: a very large direct stiffness can be cancelled by the equilibrium response of internal states. Preserving D alone during reduction then gives the wrong static physics even when the retained modes are otherwise accurate.

Raises:

ValueError – If the system is discrete, lacks B or C, or A is singular.

Parameters:

system (StateSpace)

Return type:

npt.NDArray[np.float64]

Real modal state-space realization from modal parameters.

A set of under-damped modes is realized as a real, block-diagonal state-space system. Each mode with eigenvalue \(\lambda = \sigma + j\omega\) contributes a 2x2 block

\[\begin{split}\begin{bmatrix} \sigma & -\omega \\ \omega & \sigma \end{bmatrix},\end{split}\]

whose eigenvalues are \(\sigma \pm j\omega\), acting on the real and imaginary parts of the modal coordinate. When mode shapes are supplied, the output matrix maps the modal states to physical responses via \([\,2\operatorname{Re}\phi, \,-2\operatorname{Im}\phi\,]\). The result is a compact reduced-order model of the identified modes.

modal_state_space(eigenvalues, mode_shapes=None, *, output_names=None)[source]

Build a real modal state-space realization.

Parameters:
  • eigenvalues (npt.NDArray[np.complex128]) – Modal eigenvalues, shape (n_modes,).

  • mode_shapes (npt.NDArray[np.complex128] or None, optional) – Complex mode shapes, shape (n_outputs, n_modes); when given, the output matrix maps modal states to physical responses.

  • output_names (list[str] or None, optional) – Names of the outputs (the rows of mode_shapes).

Returns:

StateSpace – A continuous-time system with 2 * n_modes states.

Raises:

ValueError – If mode_shapes column count does not match the number of eigenvalues.

Return type:

StateSpace

modal_state_space_from_solution(solution)[source]

Build a modal realization from a ModalSolution.

Parameters:

solution (ModalSolution) – The modal solution whose eigenvalues and mode shapes are realized.

Returns:

StateSpace – The real modal state-space system, with outputs named by the solution’s DOF descriptions when available.

Return type:

StateSpace

Reduced-order modal models from a linearized state space.

A full OpenFAST linearization carries hundreds of states, many of them fast, weakly observable, or numerically marginal aero/actuator modes that a downstream estimator neither needs nor can stabilise. This module reduces the system to the few structural modes that actually drive a quantity of interest, in a real, block-diagonal modal realization that preserves the inputs and outputs.

Each retained underdamped mode \(\lambda = \sigma + j\omega\) (with right eigenvector \(\mathbf v\) and left eigenvector \(\mathbf w\), \(\mathbf w^{\mathsf H}\mathbf v = 1\)) becomes a real \(2\times2\) block

\[\begin{split}\mathbf A_k = \begin{bmatrix}\sigma & -\omega\\ \omega & \sigma\end{bmatrix},\quad \mathbf B_k = \begin{bmatrix}\operatorname{Re}(\mathbf w^{\mathsf H}\mathbf B)\\ \operatorname{Im}(\mathbf w^{\mathsf H}\mathbf B)\end{bmatrix},\quad \mathbf C_k = \big[\,2\operatorname{Re}(\mathbf C\mathbf v)\ \; -2\operatorname{Im}(\mathbf C\mathbf v)\,\big],\end{split}\]

with the feed-through \(\mathbf D\) unchanged. The block’s eigenvalues are exactly \(\sigma\pm j\omega\), so the retained modal frequencies and damping are preserved to machine precision. Modes are ranked by their residue in a chosen output (the load-observability ranking): the residue of mode \(k\) in output \(i\) is \(|\mathbf C_i\mathbf v_k|\), i.e. how strongly that mode appears in that load.

This is the reduction engine the digital-twin / reduced-estimator path depends on. It is the modal counterpart to balanced truncation; unlike balanced truncation it tolerates the unstable aero poles a turbine linearization contains (they are simply not selected, and any retained pole can optionally be reflected into the stable half-plane).

class ModalReduction(system, frequencies_hz, damping_ratios, residues, energy_retained, n_full_states, n_stabilized, state_projection=None, state_reconstruction=None, stabilized=None)[source]

Bases: object

A modally reduced state space and the modes it retained.

Parameters:
  • system (StateSpace) – The reduced, real, block-diagonal continuous state space (two states per retained mode), with the original inputs and outputs.

  • frequencies_hz (npt.NDArray[np.float64]) – Natural frequency of each retained mode (Hz), in the reduced state order.

  • damping_ratios (npt.NDArray[np.float64]) – Damping ratio of each retained mode, consistent with the realized reduced A block: a mode reflected into the stable half-plane (see n_stabilized) reports its post-stabilisation (positive) damping, not the original value.

  • residues (npt.NDArray[np.float64]) – The ranking residue of each retained mode (its magnitude in the ranked output, or its observability norm when no output was specified).

  • energy_retained (float) – Fraction of the candidate modes’ total squared residue captured by the retained modes, in [0, 1] — a cheap measure of how much of the ranked output’s modal content the reduction keeps.

  • n_full_states (int) – Number of states in the original system.

  • n_stabilized (int) – How many retained modes had an unstable eigenvalue reflected into the stable half-plane (0 unless the system had unstable poles among those kept).

  • state_projection (npt.NDArray[np.float64] or None) – Real map from a full physical state into the retained modal coordinates.

  • state_reconstruction (npt.NDArray[np.float64] or None) – Real map from retained modal coordinates back to the selected full-state subspace. Together these maps permit a consistently based periodic/LPV projection without independently reducing and re-phasing every azimuth.

  • stabilized (npt.NDArray[np.bool] or None) – Per retained mode, True where an unstable eigenvalue was reflected into the stable half-plane (n_stabilized is its count). The physical (pre-reflection) damping of a reflected mode is -damping_ratios[k]; its natural frequency is unchanged. None for a hand-built reduction that does not record it.

system: StateSpace
frequencies_hz: npt.NDArray[np.float64]
damping_ratios: npt.NDArray[np.float64]
residues: npt.NDArray[np.float64]
energy_retained: float
n_full_states: int
n_stabilized: int
state_projection: npt.NDArray[np.float64] | None = None
state_reconstruction: npt.NDArray[np.float64] | None = None
stabilized: npt.NDArray[np.bool_] | None = None
modal_reduction(system, *, n_modes=None, frequency_band_hz=None, rank_output=None, rank_outputs=None, required_transfer_pairs=None, n_required_transfer_modes=0, stabilize=True, match_dc_gain=False)[source]

Reduce a state space to a real modal realization of its dominant modes.

Keeps the underdamped modes (optionally within a frequency band) with the largest residue, ranked in a chosen output (rank_output, the load-observability ranking) by an equal-weight, unit-normalized joint residue over several outputs (rank_outputs), or by the overall observability norm, as a real block-diagonal model with the inputs and outputs preserved. The retained modal frequencies and damping are exact; the full model’s many marginal/unstable aero modes are simply dropped.

Parameters:
  • system (StateSpace) – A continuous-time system (dt is None) with a C matrix.

  • n_modes (int or None, optional) – Number of modes to retain (the highest-residue ones). None keeps every candidate mode (every underdamped mode in the band).

  • frequency_band_hz (tuple[float, float] or None, optional) – Restrict candidates to modes whose natural frequency is in [low, high] (Hz), e.g. the structural band. None imposes no band.

  • rank_output (int or str or None, optional) – Rank modes by their residue in this output (index or name) — keep the modes that drive this load. None ranks by the observability norm over all outputs unless rank_outputs is given.

  • rank_outputs (Sequence[int or str] or None, optional) – Rank modes jointly for these output rows. Each row with numerically resolved candidate-mode content has its modal-residue vector normalized to unit Euclidean norm before the rows are combined, so quantities with different physical units (for example platform motion and line tension) receive equal weight. Mutually exclusive with rank_output.

  • required_transfer_pairs (sequence of tuple(output, input) or None, optional) – Input-output pairs whose strongest transfer-function modes must be retained before the remaining modal budget is allocated by the output ranking. Each pair is ranked by the true modal transfer residue \(|(C_i v_k)(w_k^H B_j)|\), normalized so pairs in different units receive equal weight. This is intended for reduced observers that must preserve a measured response to an estimated unknown input.

  • n_required_transfer_modes (int, optional) – Number of modes reserved for required_transfer_pairs. Must be zero when no pairs are supplied, positive when they are supplied, and no greater than n_modes when that budget is finite. Default 0.

  • stabilize (bool, optional) – Reflect any retained unstable eigenvalue into the stable half-plane (sigma -> -|sigma|), so the reduced model is a stable estimator basis. Default True.

  • match_dc_gain (bool, optional) – Correct the reduced feedthrough so its zero-frequency gain exactly matches the full model. This preserves equilibrium stiffness when a large direct term is cancelled by omitted internal-state response. Both models must have B and nonsingular A matrices. Default False.

Returns:

ModalReduction – The reduced system and the retained-mode metadata.

Raises:

ValueError – If the system is discrete, has no C matrix, n_modes is not positive, frequency_band_hz is invalid, the ranking selections conflict or are empty, no modes remain after filtering, or a retained mode belongs to a defective (non-diagonalizable) repeated-eigenvalue cluster — a Jordan chain a block-diagonal modal realization cannot represent.

Return type:

ModalReduction

Notes

The realization is exact for diagonalizable systems, including genuinely repeated (but non-defective) eigenvalues. A defective repeated mode (a Jordan chain) cannot be written as independent modal blocks and is rejected rather than silently reduced to a model with a different transfer function.

output_mode_residues(system, output)[source]

Rank the underdamped modes by their residue in one output (load).

The residue of mode \(k\) in the output is \(|\\mathbf C_i\\mathbf v_k|\) — how strongly the mode appears in that load. This is the load-observability ranking that drives modal_reduction() and is the basis for choosing which modes a load estimator must retain.

Parameters:
  • system (StateSpace) – A continuous system with a C matrix.

  • output (int or str) – The output row index, or an output name (exact match, else substring).

Returns:

tuple of npt.NDArray[np.float64] – (frequencies_hz, damping_ratios, residues) for the underdamped modes, sorted by descending residue.

Raises:

ValueError – If the system is discrete, has no C matrix, or the output cannot be resolved.

Return type:

tuple[npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.float64]]

Balanced truncation — gramian-based reduction with an a-priori error bound.

modal_reduction() keeps whole modes chosen by their residue in a load. Balanced truncation is the complementary, input-output-optimal reduction: it balances the controllability and observability Gramians so that each state is equally hard to reach and to observe, then drops the states with the smallest Hankel singular values — the ones that contribute least to the transfer function. Unlike modal reduction it comes with a guarantee: the truncated model’s worst-case frequency-response error is bounded a priori by twice the sum of the discarded Hankel singular values.

For a stable system the Gramians solve the Lyapunov equations \(\mathbf A\mathbf W_c+\mathbf W_c\mathbf A^{\mathsf T}+\mathbf B\mathbf B^{\mathsf T}=\mathbf 0\) and \(\mathbf A^{\mathsf T}\mathbf W_o+\mathbf W_o\mathbf A+ \mathbf C^{\mathsf T}\mathbf C=\mathbf 0\) (discrete analogues for a sampled system). The Hankel singular values are \(\sigma_i=\sqrt{\lambda_i(\mathbf W_c\mathbf W_o)}\), and in the balanced realization both Gramians equal \(\operatorname{diag}(\sigma)\). This reduction requires asymptotic stability (the Gramians exist) and that every retained Hankel singular value be resolved above rounding level (the retained order is controllable and observable). The Gramians are factored from their symmetric eigendecomposition with rounding-level negative eigenvalues clipped, so the numerically indefinite Gramians of a lightly damped minimal system do not cause a spurious rejection. It suits the structural ROM, while the unstable aero poles of a raw turbine linearization are handled by modal_reduction() instead.

class BalancedReduction(system, hankel_singular_values, error_bound, n_full_states)[source]

Bases: object

A balanced-truncation reduction and its Hankel-singular-value spectrum.

Parameters:
  • system (StateSpace) – The reduced (or, when no order is given, balanced full-order) system, with the original inputs and outputs and the feed-through D unchanged.

  • hankel_singular_values (npt.NDArray[np.float64]) – All Hankel singular values of the full system, in descending order. The retained states correspond to the largest ones; the tail is what the error bound sums.

  • error_bound (float) – The a-priori worst-case frequency-response error 2 * sum(discarded Hankel singular values) — an upper bound on the \(\mathcal H_\infty\) error \(\lVert\mathbf G-\mathbf G_r\rVert_\infty\). Zero when nothing is dropped.

  • n_full_states (int) – Number of states in the original system.

system: StateSpace
hankel_singular_values: npt.NDArray[np.float64]
error_bound: float
n_full_states: int
balanced_truncation(system, *, n_states=None, tol=None)[source]

Reduce a system by balanced truncation, with an a-priori error bound.

Balances the controllability and observability Gramians, then keeps the states with the largest Hankel singular values. The order is set by n_states (an explicit count) or tol (drop singular values at or below tol times the largest); with neither, the system is balanced but not truncated.

Parameters:
  • system (StateSpace) – A stable system with both B and C matrices. May be continuous (dt is None) or discrete.

  • n_states (int or None, optional) – Number of states to retain (the largest Hankel singular values). Takes precedence over tol. Must be in [1, n_states_full].

  • tol (float or None, optional) – Relative threshold: retain the states whose Hankel singular value exceeds tol * max(sigma). Must be non-negative.

Returns:

BalancedReduction – The reduced system, the full Hankel-singular-value spectrum, and the bound.

Raises:

ValueError – If the system lacks B or C, is not asymptotically stable, the retained order includes a Hankel singular value at rounding level (the system is not minimal at that order), or n_states/tol is out of range.

Return type:

BalancedReduction

Operating-point ROM family with consistently-tracked modes (LPV packaging).

A turbine is linearized at many operating points (wind speed, rotor speed, …), and the digital twin wants one reduced model per point so it can schedule the estimator on the operating condition. Reducing each point independently is not enough: the reduction ranks modes by their residue, so the order — and the meaning of “mode 2” — changes from point to point as modes veer and cross. This module reduces every point and then tracks the modes across the family, so a given index is the same physical mode at every operating point: a proper linear-parameter-varying (LPV) packaging of the ROM.

Modes are matched between adjacent operating points by the Modal Assurance Criterion of their output residues (how each mode appears at the outputs — the same quantity the reduction ranks on), with a frequency-proximity tie-breaker, via an optimal (Hungarian) assignment. Each tracked mode carries the MAC value against the previous point, so a low value flags where the tracking is uncertain (a crossing or veering region) — the hook the operating-envelope-consistency analysis needs.

class RomFamily(operating_points, systems, frequencies_hz, damping_ratios, tracking_mac, stabilized=<factory>)[source]

Bases: object

A parameter-indexed family of reduced models with tracked modes.

Parameters:
  • operating_points (npt.NDArray[np.float64]) – The scheduling-parameter value of each point, shape (n_points,), in the order given.

  • systems (list[StateSpace]) – The reduced model at each operating point, all with the same mode (and state) ordering: index k is the same physical mode across the family.

  • frequencies_hz (npt.NDArray[np.float64]) – Natural frequency of each tracked mode, shape (n_points, n_modes).

  • damping_ratios (npt.NDArray[np.float64]) – Physical damping ratio of each tracked mode, shape (n_points, n_modes): the damping of the linearization’s own eigenvalue, negative for an unstable mode, even where the stored realization reflected that pole (see stabilized).

  • tracking_mac (npt.NDArray[np.float64]) – The MAC of each tracked mode against the previous operating point, shape (n_points, n_modes); the first row is all ones (the reference). A low value marks a mode whose tracking across that step is uncertain.

  • stabilized (npt.NDArray[np.bool], optional) – True where the stored realization in systems reflected that mode’s unstable pole into the stable half-plane, shape (n_points, n_modes). For such a mode the realized A block has damping -damping_ratios rather than the physical value. Defaults to all False (for hand-built families).

operating_points: npt.NDArray[np.float64]
systems: list[StateSpace]
frequencies_hz: npt.NDArray[np.float64]
damping_ratios: npt.NDArray[np.float64]
tracking_mac: npt.NDArray[np.float64]
stabilized: npt.NDArray[np.bool_]
build_rom_family(systems, operating_points, *, n_modes, frequency_band_hz=None, rank_output=None, rank_outputs=None, required_transfer_pairs=None, n_required_transfer_modes=0, frequency_weight=0.5, match_dc_gain=False, stabilize=True)[source]

Reduce a system at each operating point and track the modes across the family.

The reference point’s top-n_modes modes (by residue) define the tracked set. At each later point the system is reduced to all candidate modes and the tracked modes are matched against that full set by output-residue MAC (with a frequency tie-breaker), so a tracked mode is recovered even when it drops below the top-n_modes residue rank at that point; the matched modes are then selected and reordered to keep mode identity consistent.

Parameters:
  • systems (Sequence[StateSpace]) – One continuous-time linearization per operating point, with a C matrix and the same outputs throughout.

  • operating_points (Sequence[float]) – The scheduling-parameter value of each system (same length as systems).

  • n_modes (int) – Number of modes to retain and track at every point. Each point must yield at least this many underdamped modes.

  • frequency_band_hz (tuple[float, float] or None, optional) – Restrict the retained modes to this natural-frequency band (Hz).

  • rank_output (int or str or None, optional) – Rank modes by their residue in this output when reducing (see modal_reduction()).

  • rank_outputs (Sequence[int or str] or None, optional) – Rank modes by the equal-weight, unit-normalized joint residue of these outputs (see modal_reduction()). Mutually exclusive with rank_output.

  • required_transfer_pairs (sequence of tuple(output, input) or None, optional) – Observer input-output pairs whose strongest transfer-residue modes are reserved in the reference modal set (see modal_reduction()).

  • n_required_transfer_modes (int, optional) – Number of the n_modes slots reserved for those transfer pairs. The reserved set is fixed at the reference point and then tracked by mode identity (joint MAC/frequency matching over all candidates), not re-selected per point, so a reshuffled transfer ranking cannot swap physical modes; a reserved mode is only barred from matching a candidate with negligible transfer residue.

  • frequency_weight (float, optional) – Strength of the frequency-proximity tie-breaker in [0, 1] used when matching modes between points (default 0.5).

  • match_dc_gain (bool, optional) – After the tracked modal subset is selected, correct each reduced feedthrough so its zero-frequency gain exactly equals its corresponding full system. This is applied after truncation (not to the all-candidate intermediate model), thereby preserving equilibrium stiffness for the actual stored family. Default False.

  • stabilize (bool, optional) – Reflect a retained unstable pole into the stable half-plane in the stored realizations (see modal_reduction()), so each member is a stable estimator basis. Default True. The reported RomFamily.damping_ratios are always the physical (pre-reflection) values, RomFamily.stabilized marks every reflected mode, and a warning is logged when any mode was reflected. False stores the unstable poles.

Returns:

RomFamily – The reduced models in a consistent mode order, with per-point frequencies, physical damping ratios, the per-step tracking MAC, and the reflection mask.

Raises:

ValueError – If systems is empty, its length differs from operating_points, n_modes is not positive, the systems do not share the same output layout, input dimension, and time domain, operating_points are not finite and strictly monotonic, or a point yields fewer than n_modes modes.

Return type:

RomFamily

combine_rom_families(base, enrichment, *, duplicate_frequency_tolerance_hz=1e-08, enrichment_outputs=None)[source]

Compose two disjoint tracked modal families without re-reducing either.

The base and enrichment must use the same operating-point schedule and I/O layout. Their modal state blocks are concatenated, their output contributions are summed through a horizontal C concatenation, and the shared feedthrough is retained once. A frequency-separation check rejects a mode present in both families, which would otherwise double-count its transfer residue.

Parameters:
  • base (RomFamily) – Independently tracked, continuous- or discrete-time modal families with the same schedule, input/output names, MBC labels, feedthrough, and sample time.

  • enrichment (RomFamily) – Independently tracked, continuous- or discrete-time modal families with the same schedule, input/output names, MBC labels, feedthrough, and sample time.

  • duplicate_frequency_tolerance_hz (float, optional) – Maximum absolute per-point frequency difference considered a duplicate. Default 1e-8 Hz.

  • enrichment_outputs (sequence of int or str or None, optional) – Output rows to which the enrichment is allowed to contribute. None keeps every enrichment output. A declared subset zeros all other enrichment C rows while leaving the base family untouched; this supports separated observer blocks, such as drivetrain modes used only by a rotor-speed measurement.

Returns:

RomFamily – A block-composed family with base modes first and enrichment modes second.

Raises:

ValueError – If the tolerance is invalid, schedules or I/O layouts differ, feedthroughs do not match exactly, or a duplicate modal branch is detected.

Return type:

RomFamily

Physical-state LPV family export: named operating-point plants from .lin files.

build_rom_family() packages reduced, mode-tracked models for modal/Campbell work. This module is the complementary export for control design: it reads a directory of OpenFAST linearization files and assembles, per operating point, a state-space model whose states/inputs/outputs are the named physical DOFs the caller selects (e.g. "ED PtfmPitch", "GenTq", "BldPitch1"), with a consistent ordering across the operating-point sweep – the linear-parameter-varying plant a gain-scheduled controller schedules on.

The matrices are azimuth-averaged in the physical (rotating) frame, preserving the original per-blade channel names; no MBC/Coleman transform is applied (for the multi-blade coordinate representation use build_rom_family() / state_space_from_mbc()). The result is one LTI model per operating point – the standard plant for LQR/LPV control synthesis. This is the single VANE entry point a downstream controller needs: it never touches raw .lin files itself.

class LPVFamily(scheduling_points, systems, state_names=<factory>, input_names=<factory>, output_names=<factory>, scheduling_variable='WindSpeed', scheduling_unit='m/s', auto_added_states=<factory>, dropped_coupling_a=<factory>, dropped_coupling_c=<factory>)[source]

Bases: object

A parameter-indexed family of named physical-state plants for control design.

Parameters:
  • scheduling_points (npt.NDArray[np.float64]) – The scheduling-variable value of each operating point, shape (n_points,), sorted strictly ascending. In the raw linearization unit: WindSpeed in m/s, RotorSpeed in rad/s (not rpm).

  • systems (list[StateSpace]) – The physical-state plant at each operating point, in the same order as scheduling_points. Every system has the same selected states, inputs, and outputs, in the same order – the consistency a scheduled controller relies on.

  • state_names (list[str]) – The selected physical state, input, and output names, shared by every system.

  • input_names (list[str]) – The selected physical state, input, and output names, shared by every system.

  • output_names (list[str]) – The selected physical state, input, and output names, shared by every system.

  • scheduling_variable (str) – The scheduling variable the family is indexed on (e.g. "WindSpeed").

  • scheduling_unit (str) – The physical unit of scheduling_points in the raw linearization convention: "m/s" for WindSpeed, "rad/s" for RotorSpeed (not rpm, unlike the modal/Campbell path). Carried explicitly so a scheduled controller cannot mis-schedule by assuming the wrong unit.

  • auto_added_states (list[str]) – The states the builder added to the request to complete a position/velocity pair (see pair_derivatives in build_physical_lpv_family()), named by their unit-stripped linearization descriptions. Each also appears in state_names. Empty when nothing was added.

  • dropped_coupling_a (npt.NDArray[np.float64]) – Per operating point, the relative state-matrix coupling the state selection discards, ||A[s, ~s]||_F / ||A[s, :]||_F (s the selected states), shape (n_points,), in the order of scheduling_points. Zero when every state is selected.

  • dropped_coupling_c (npt.NDArray[np.float64]) – Per operating point, the relative output coupling the selection discards, ||C[o, ~s]||_F / ||C[o, :]||_F (o the selected outputs), shape (n_points,). Zero when no outputs are selected or every state is selected.

scheduling_points: npt.NDArray[np.float64]
systems: list[StateSpace]
state_names: list[str]
input_names: list[str]
output_names: list[str]
scheduling_variable: str = 'WindSpeed'
scheduling_unit: str = 'm/s'
auto_added_states: list[str]
dropped_coupling_a: npt.NDArray[np.float64]
dropped_coupling_c: npt.NDArray[np.float64]
property dropped_coupling: npt.NDArray[np.float64]

Per-point relative dropped coupling, the larger of the A and C terms.

Returns:

npt.NDArray[np.float64] – max(dropped_coupling_a, dropped_coupling_c) element-wise, shape (n_points,). This is the quantity max_dropped_coupling checks.

build_physical_lpv_family(lin_dir, *, states, inputs=(), outputs=(), scheduling_var='WindSpeed', pair_derivatives=True, max_dropped_coupling=0.05)[source]

Build a named physical-state LPV plant family from a directory of .lin files.

Discovers the operating points in lin_dir, averages each point’s azimuth files in the physical frame, and selects the named states/inputs/outputs into a state-space model – one per operating point, ordered by scheduling_var, with the same channel layout everywhere. No MBC transform is applied, so per-blade channel names are preserved (use build_rom_family() for the multi-blade coordinate representation).

Selecting a state subset truncates the plant: rows and columns of the unselected states are removed, discarding their coupling into the selected ones. Two safeguards keep that truncation honest. Second-order DOFs are completed with their position/velocity twin (pair_derivatives), and the relative coupling the selection still drops is measured at every point and checked (max_dropped_coupling).

Parameters:
  • lin_dir (str or pathlib.Path) – Directory of OpenFAST .lin files, searched recursively, so a campaign laid out with one sub-directory per operating point is read as distinct points (grouped into operating points by case name).

  • states (Sequence[str]) – Physical state names to select, e.g. ["ED PtfmPitch", "ED RotSpeed"]. Matched against the linearization’s state descriptions (units stripped): an exact match is preferred, else a unique substring. When a name matches both a DOF and its "First time derivative of <DOF>" twin, the position DOF is chosen (and, with pair_derivatives, its twin is then added); a name matching two genuinely different channels raises, listing them. The selected order is the state order, with any auto-added twin inserted next to its partner.

  • inputs (Sequence[str], optional) – Input and output names to select (e.g. ["GenTq", "BldPitch1"]); empty selects none (no B / C).

  • outputs (Sequence[str], optional) – Input and output names to select (e.g. ["GenTq", "BldPitch1"]); empty selects none (no B / C).

  • scheduling_var (str, optional) – The scheduling variable: "WindSpeed" (default, m/s) or "RotorSpeed". RotorSpeed is the raw linearization rotor speed in rad/s (the unit OpenFAST writes), not rpm — unlike the modal/Campbell path, which reports rotor speed in rpm. Schedule a controller built against rad/s accordingly.

  • pair_derivatives (bool, optional) – If true (default), every selected second-order DOF whose twin – the "First time derivative of <DOF>" velocity of a selected position, or the position of a selected velocity – exists in the linearization but was not requested gets that twin added, immediately after a position or immediately before a velocity, so each pair reads position then velocity. The added names (unit-stripped descriptions) are logged at INFO and recorded in LPVFamily.auto_added_states. A position without its velocity is a pure truncation artefact (d(position)/dt is exactly the dropped velocity), so disable this only to reproduce a deliberate truncation.

  • max_dropped_coupling (float or None, optional) – Ceiling on the relative coupling the state selection discards, default 0.05. At each operating point VANE computes ||A[s, ~s]||_F / ||A[s, :]||_F over the selected states s and, when outputs are selected, ||C[o, ~s]||_F / ||C[o, :]||_F over the selected outputs o; the per-point value is the larger of the two (LPVFamily.dropped_coupling). If the largest value over the family exceeds the ceiling, ValueError is raised naming the worst point and the dropped states contributing most. The norms are taken in the linearization’s own units, so the measure is a screening check, not a unit-invariant error bound. None disables the check (the norms are still recorded). Must be a finite number >= 0.

Returns:

LPVFamily – The physical-state plant family, with scheduling_points sorted ascending and a StateSpace per point sharing the selected channel layout. Its state_names is the final (paired) state list; auto_added_states, dropped_coupling_a and dropped_coupling_c record the pairing and the per-point truncation norms.

Raises:
  • FileNotFoundError – If lin_dir does not exist or contains no .lin files.

  • ValueError – If scheduling_var is unknown; max_dropped_coupling is not None or a finite number >= 0; a name selection is invalid (a bare string, an empty states, a duplicate, or a non-string entry); a requested name is missing/ambiguous; a name resolves to a different channel (or different units) across operating points; an operating point’s azimuth files disagree on the channel layout or the operating condition (wind or rotor speed) or contain a duplicate azimuth; the schedule is non-finite or not increasing; or the state selection drops more coupling than max_dropped_coupling allows.

Return type:

LPVFamily

Safe, provenance-stamped save/reload for estimator results and model families.

An estimator’s outputs are evidence: a filtered trajectory or a scheduled run must be reproducible and archivable, not recomputed from memory. This module serializes the vane.sysid result dataclasses, the tracked RomFamily and the physical-state LPVFamily to a single .npz file each and loads them back exactly.

The format is deliberately safe and transparent: the numeric arrays are stored as plain NumPy arrays and every other field as a JSON metadata blob, so a result is loaded with allow_pickle=False – it can carry no executable payload, unlike a pickle or joblib artifact. Each file also records the result type and the VANE version that produced it, so an archived result is traceable to the code that made it.

LPV_FAMILY_SCHEMA_VERSION = 1

Schema version of the LPV-family archive written by save_lpv_family(). load_lpv_family() reads only this version and names both versions when an archive records another one.

load_lpv_family(path)[source]

Load a physical-state LPV family saved by save_lpv_family().

The archive is opened with allow_pickle=False. Its format tag and schema version are checked first, then every array and metadata field, and the rebuilt family is validated as on saving, so a file that loads is a family that save_lpv_family() could have written. The arrays, names, scheduling variable and unit, auto-added states and dropped-coupling norms come back exactly as saved.

Parameters:

path (str or os.PathLike) – The .npz file written by save_lpv_family() (the suffix is added if absent).

Returns:

vane.sysid.physical_lpv.LPVFamily – The reconstructed family.

Raises:

ValueError – If the file is not a VANE LPV-family archive, records no schema version or a schema other than LPV_FAMILY_SCHEMA_VERSION (the message names both versions and the VANE version that wrote the file), or is incomplete, has unexpected entries or holds an invalid family.

Return type:

LPVFamily

load_result(path)[source]

Load an estimator result saved by save_result().

Reads the archive with allow_pickle=False (so a tampered file can carry no executable payload), reconstructs the recorded result type, and returns it exactly as saved – arrays, names, flags, and the nested consistency report.

Parameters:

path (str or os.PathLike) – The .npz file written by save_result().

Returns:

FilterResult or ScheduledFilterResult – The reconstructed result.

Raises:

ValueError – If the file is not a VANE estimator-result archive or records an unknown type.

Return type:

FilterResult | ScheduledFilterResult

load_rom_family(path)[source]

Load a RomFamily written by save_rom_family() safely.

An archive without the RomFamily.stabilized mask loads with an all-False one.

Parameters:

path (str | PathLike[str])

Return type:

RomFamily

save_lpv_family(family, path)[source]

Save a physical-state LPVFamily to .npz.

The archive holds the scheduling points, the per-point dropped-coupling norms and each system’s matrices as plain arrays under explicit indexed keys, and every other field (channel names, the scheduling variable and its unit, the auto-added states, each system’s names, sample time and matrix presence) as JSON metadata. It is stamped with the format tag, LPV_FAMILY_SCHEMA_VERSION and the producing VANE version. No object arrays are used, so load_lpv_family() opens it with allow_pickle=False.

Parameters:
Raises:
  • TypeError – If family is not exactly an LPVFamily, a system is not exactly a StateSpace, or a field has the wrong type.

  • ValueError – If the family is empty, its arrays are non-finite or of the wrong length, the scheduling points are not strictly ascending, or the systems do not share one layout (matrix shapes, names and sample time).

Return type:

None

save_result(result, path)[source]

Save an estimator result to a .npz file.

Writes the result’s NumPy arrays as plain arrays and the remaining fields (names, flags, the nested consistency report) as a JSON metadata blob, stamped with the result type and the producing VANE version. The file is self-describing and loads back with load_result() without executing any embedded code.

Parameters:
Raises:

TypeError – If result is not a registered estimator-result type, or carries a field that cannot be serialized.

Return type:

None

save_rom_family(family, path)[source]

Save a tracked RomFamily as a transparent pickle-free archive.

Each state-space matrix is stored under an explicit indexed key; names, sample times, and optional-matrix presence are JSON metadata. No object arrays are used, so load_rom_family() always opens the archive with allow_pickle=False. The per-mode reflection mask RomFamily.stabilized is stored alongside the modal metadata.

Parameters:
Return type:

None

Periodic systems

Rotor-azimuth-periodic ROMs in one fixed modal coordinate system.

Reducing every azimuth independently produces arbitrarily re-phased modal states that cannot be scheduled safely. This module instead takes one accepted averaged modal reduction and projects every retained per-azimuth MBC Jacobian through that same left/ right basis. The resulting matrices form a linear time-periodic (LTP) family whose state has a consistent meaning around the full revolution.

class PeriodicStateSpace(azimuths_deg, systems)[source]

Bases: object

A full-revolution matrix family in a fixed reduced-state basis.

Parameters:
azimuths_deg: npt.NDArray[np.float64]
systems: tuple[StateSpace, ...]
at(azimuth_deg)[source]

Linearly interpolate the periodic family, including the 360/0 seam.

Parameters:

azimuth_deg (float)

Return type:

StateSpace

discretized(dt)[source]

Exactly discretize every tabulated member before online interpolation.

Parameters:

dt (float)

Return type:

PeriodicStateSpace

periodic_state_space_from_mbc(result, reduction)[source]

Project retained per-azimuth MBC matrices through one modal basis.

The averaged reduced A is retained as the stable reference and projected per-azimuth deviations are added to it. Thus any intentionally reflected unstable poles remain reflected while the measured cyclic Jacobian variation is preserved.

The B/C/D deviations are mapped onto the reduction’s channels by input name and by output name plus MBC coordinate, so a reduction whose outputs or inputs are a reordering of the MBC result’s is remapped rather than corrupted. When either side is unnamed the channels are matched by position and only the counts are checked.

Parameters:
  • result (MBCResult) – An MBC result built with per-azimuth matrices retained.

  • reduction (ModalReduction) – A modal reduction of the result’s averaged system, with its projection maps.

Returns:

PeriodicStateSpace – One reduced member per azimuth in the reduction’s fixed basis and channel order.

Raises:

ValueError – If required averaged or per-azimuth matrices are missing, dimensions disagree, or the reduction’s input/output layout is not a permutation of the MBC result’s input/output descriptions (and output MBC coordinates).

Return type:

PeriodicStateSpace

Loads and frequency response

Quantity-of-interest (load) map from a linearized output equation.

A linearization’s output equation \(y = C x + D u\) already contains the sensitivity of every requested output — including structural loads such as mooring fairlead/anchor tensions, tower-base and yaw-bearing moments, and blade-root moments — to the system state and inputs. This module turns that into an explicit, auditable quantity-of-interest (QoI) map: for each load output it exposes

  • the state sensitivity (its row of \(C\)): how each state moves the load;

  • the input sensitivity (its row of \(D\)): the direct feed-through from the inputs (wind, wave, control) to the load, bypassing the dynamics; and

  • the modal residues \(C_i \Phi\): how each identified mode contributes to the load. Writing the state in modal coordinates \(x = \Phi q\) gives \(y_i = (C_i \Phi)\, q\), so the residue of mode \(k\) is the complex gain with which that mode appears in load \(i\). This is the bridge from a modal identification to load monitoring: it answers which modes drive this load.

The modal residues are taken over the retained (under-damped) modes of a ModalSolution; over-damped/rigid-body modes and the direct \(D u\) feed-through are not part of that modal sum and are reported separately (the input sensitivity). The map is a static, deterministic property of the linear model — the reduced-order foundation a downstream estimator consumes.

class QoiMap(sensitivities, state_names, input_names, mode_frequencies_hz=None)[source]

Bases: object

A map of output channels to their state/input/modal sensitivities.

Parameters:
  • sensitivities (list[QoiSensitivity]) – One entry per included output channel.

  • state_names (list[str]) – State descriptions indexing state_sensitivity.

  • input_names (list[str]) – Input descriptions indexing input_sensitivity.

  • mode_frequencies_hz (npt.NDArray[np.float64] or None) – Natural frequencies of the modes indexing modal_residues, or None when no modal solution was supplied.

sensitivities: list[QoiSensitivity]
state_names: list[str]
input_names: list[str]
mode_frequencies_hz: npt.NDArray[np.float64] | None = None
loads()[source]

Return only the structural-load sensitivities.

Return type:

list[QoiSensitivity]

by_category(category)[source]

Return the sensitivities of a given output category.

Parameters:

category (OutputCategory)

Return type:

list[QoiSensitivity]

get(name)[source]

Return the sensitivity whose channel name equals name.

Raises:

KeyError – If no channel has that exact name.

Parameters:

name (str)

Return type:

QoiSensitivity

modal_participation_table()[source]

Return a long-format table of modal contributions to each load.

Each row is one (load, mode) pair with the residue magnitude and phase and the per-load normalized magnitude (the fraction relative to that load’s strongest mode). Empty when no modal residues are available.

Returns:

pandas.DataFrame – Columns load, category, unit, mode, frequency_hz, residue_magnitude, residue_phase_deg, normalized.

Return type:

pd.DataFrame

class QoiSensitivity(name, info, output_index, state_sensitivity, input_sensitivity, modal_residues, mbc_coordinate='')[source]

Bases: object

Sensitivity of one output channel to the states, inputs, and modes.

Parameters:
  • name (str) – The output channel description.

  • info (OutputInfo) – Its classification (category, load flag, component, unit, …).

  • output_index (int) – Row index of this channel in C/D.

  • state_sensitivity (npt.NDArray[np.float64]) – The channel’s row of C, shape (n_states,): the linear sensitivity of the output to each state.

  • input_sensitivity (npt.NDArray[np.float64] or None) – The channel’s row of D, shape (n_inputs,), or None when the system has no feed-through matrix. The direct input-to-output gain.

  • modal_residues (npt.NDArray[np.complex128] or None) – The complex modal residues \(C_i \Phi\), shape (n_modes,), or None when no modal solution was supplied. The contribution of each retained mode to this output.

  • mbc_coordinate (str) – The multi-blade coordinate of the channel ("collective", "cosine", or "sine") when it is a transformed blade-triplet output, else "". A non-empty value means this row is a multi-blade combination rather than the literal per-blade channel its name suggests, so info.component is cleared to None.

name: str
info: OutputInfo
output_index: int
state_sensitivity: npt.NDArray[np.float64]
input_sensitivity: npt.NDArray[np.float64] | None
modal_residues: npt.NDArray[np.complex128] | None
mbc_coordinate: str = ''
property is_load: bool

Return whether this output is a structural load.

dominant_modes(top_n=3)[source]

Return the indices of the modes that contribute most to this output.

Parameters:

top_n (int) – Number of leading modes to return.

Returns:

npt.NDArray[np.int_] – Mode indices ordered by descending residue magnitude (empty when no modal residues are available).

Return type:

npt.NDArray[np.int_]

build_qoi_map(system, solution=None, *, output_mbc_coordinates=None, loads_only=True)[source]

Build the quantity-of-interest map from a state-space system.

Note

This is a modal-residue screening map — which modes drive which load channels in the linearized model — not a validated load ranking. The residues are in the linearization’s output units (and an MBC-averaged frame for rotating loads); they are not fatigue/DEL, design-load, or extreme-load magnitudes and must not be used as a substitute for a load-analysis or assessment calculation. Use it to decide which modes a downstream estimator should retain, then validate the estimator against time-domain or measured loads.

Parameters:
  • system (StateSpace) – A system carrying an output matrix C and named outputs. The optional feed-through D supplies the input sensitivities.

  • solution (ModalSolution or None, optional) – A modal solution of the same system; when given, the modal residues \(C_i \Phi\) are computed from its full eigenvectors so each load’s modal composition is available.

  • output_mbc_coordinates (list[str] or None, optional) – The multi-blade coordinate of each output, aligned with the system outputs. When None (the default) the system’s own output_mbc_coordinates are used, which state_space_from_mbc() populates — so an MBC-transformed system is handled correctly without any extra argument. A rotating blade-load output is a collective/cyclic combination, so when a channel’s coordinate is multi-blade its per-blade component index is cleared and the coordinate is recorded on the sensitivity. Pass an explicit list to override. Non-rotating loads (mooring, tower, platform) are unaffected.

  • loads_only (bool, optional) – When True (the default) include only channels classified as structural loads; when False include every output channel.

Returns:

QoiMap – The per-output sensitivities.

Raises:

ValueError – If the system has no C matrix or no output names, the output-name count disagrees with C, output_mbc_coordinates has the wrong length, or the modal solution’s eigenvectors do not span the system states.

Return type:

QoiMap

qoi_map_from_mbc(result, solution=None, *, loads_only=True)[source]

Build a QoI map directly from an MBC transform result.

A thin convenience over build_qoi_map(state_space_from_mbc(result), ...). The exported state-space carries the output multi-blade coordinates, so a rotating blade-load output is correctly identified as a multi-blade coordinate rather than mislabeled as a literal per-blade channel.

Parameters:
  • result (MBCResult) – An MBC transform result with an averaged output matrix.

  • solution (ModalSolution or None, optional) – A modal solution of the same averaged system, for the modal residues.

  • loads_only (bool, optional) – Restrict to structural-load channels (the default).

Returns:

QoiMap – The per-output sensitivities, with transformed blade outputs tagged by their multi-blade coordinate.

Return type:

QoiMap

Frequency-domain transfer and stochastic input-to-QoI propagation.

For a linear state-space model, the complex transfer matrix is

\[H(f) = C\left(sI-A\right)^{-1}B + D,\]

with \(s=j2\pi f\) for a continuous model and \(s=\exp(j2\pi f\Delta t)\) for a discrete model. If the input cross-spectral density is \(S_u(f)\), the model-implied output spectrum is

\[S_y(f) = H(f)S_u(f)H(f)^{\mathsf H}.\]

This is the auditable bridge between an input-estimation error spectrum and the motion/load quantities affected by that error. It does not consult output truth and does not claim nonlinear or out-of-support validity; those remain validation tasks.

class CrossSpectralDensity(frequencies_hz, density, channel_names)[source]

Bases: object

Named one-sided cross-spectral-density matrix.

density has shape (n_frequencies, n_channels, n_channels) and follows the covariance convention S[i, j] = E[X_i conj(X_j)].

Parameters:
  • frequencies_hz (npt.NDArray[np.float64])

  • density (npt.NDArray[np.complex128])

  • channel_names (tuple[str, ...])

frequencies_hz: npt.NDArray[np.float64]
density: npt.NDArray[np.complex128]
channel_names: tuple[str, ...]
class FrequencyResponse(frequencies_hz, response, input_names, output_names)[source]

Bases: object

Named complex input-to-output response on a frequency grid.

response has shape (n_frequencies, n_outputs, n_inputs).

Parameters:
  • frequencies_hz (npt.NDArray[np.float64])

  • response (npt.NDArray[np.complex128])

  • input_names (tuple[str, ...])

  • output_names (tuple[str, ...])

frequencies_hz: npt.NDArray[np.float64]
response: npt.NDArray[np.complex128]
input_names: tuple[str, ...]
output_names: tuple[str, ...]
frequency_response(system, frequencies_hz, *, input_channels=None, output_channels=None)[source]

Evaluate selected complex state-space transfers.

Parameters:
  • system (StateSpace) – Continuous or discrete state-space model with B and C matrices.

  • frequencies_hz (npt.ArrayLike) – Strictly increasing nonnegative physical frequencies in Hz. A discrete model is restricted to its Nyquist interval.

  • input_channels (Sequence[int | str] | None) – Optional unique indices or unambiguous names. By default all channels are retained.

  • output_channels (Sequence[int | str] | None) – Optional unique indices or unambiguous names. By default all channels are retained.

Return type:

FrequencyResponse

integrated_spectral_variance(spectrum, *, band_hz=None)[source]

Integrate each autospectrum over a declared frequency band.

Returns one nonnegative variance per channel. The trapezoidal integral runs exactly from band_hz[0] to band_hz[1]: the autospectra are linearly interpolated at band limits that fall between spectrum bins, so a band edge is not truncated to the nearest interior bin. The band is clipped to the spectrum’s frequency range, because the density is undefined outside it, and the clipped band must retain a positive width; this avoids silently reporting a zero-width integral. Without band_hz the whole spectrum grid is integrated.

Parameters:
Return type:

npt.NDArray[np.float64]

propagate_cross_spectral_density(response, spectrum)[source]

Propagate S_u through H as S_y = H S_u H^H.

Parameters:
Return type:

CrossSpectralDensity

welch_cross_spectral_density(signals, *, sample_interval_s, segment_samples=None, overlap_samples=None, channel_names=None)[source]

Estimate a one-sided cross-spectral-density matrix using Welch averaging.

Samples are rows and channels are columns. A one-dimensional input is treated as one channel. Cross terms are retained, so correlated torque/thrust errors are propagated without the independence assumption.

Parameters:
  • signals (npt.ArrayLike)

  • sample_interval_s (float)

  • segment_samples (int | None)

  • overlap_samples (int | None)

  • channel_names (Sequence[str] | None)

Return type:

CrossSpectralDensity

Estimation

Ready-to-use Kalman-filter model assembly.

A KalmanModel bundles a state-space system with its process, measurement, and initial covariances into the exact matrices a Kalman filter consumes. The system can be discretized at a chosen sample time during assembly, and the covariance dimensions are validated against the system so the resulting model is internally consistent.

class KalmanModel(a, b, c, d, q, r, p0, dt=None, state_names=<factory>, output_names=<factory>)[source]

Bases: object

A state-space system with Kalman covariances, ready for filtering.

Parameters:
  • a (npt.NDArray[np.float64]) – State matrix, shape (n_states, n_states).

  • b (npt.NDArray[np.float64] or None) – Input, output, and feedthrough matrices.

  • c (npt.NDArray[np.float64] or None) – Input, output, and feedthrough matrices.

  • d (npt.NDArray[np.float64] or None) – Input, output, and feedthrough matrices.

  • q (npt.NDArray[np.float64]) – Process-noise, measurement-noise, and initial state covariances.

  • r (npt.NDArray[np.float64]) – Process-noise, measurement-noise, and initial state covariances.

  • p0 (npt.NDArray[np.float64]) – Process-noise, measurement-noise, and initial state covariances.

  • dt (float or None) – Sample time; None for a continuous-time model.

  • state_names (list[str]) – State and output names carried from the source system.

  • output_names (list[str]) – State and output names carried from the source system.

a: npt.NDArray[np.float64]
b: npt.NDArray[np.float64] | None
c: npt.NDArray[np.float64] | None
d: npt.NDArray[np.float64] | None
q: npt.NDArray[np.float64]
r: npt.NDArray[np.float64]
p0: npt.NDArray[np.float64]
dt: float | None = None
state_names: list[str]
output_names: list[str]
build_kalman_model(state_space, covariances, *, dt=None)[source]

Assemble a Kalman model from a state-space system and covariances.

Parameters:
  • state_space (StateSpace) – The continuous- or discrete-time system.

  • covariances (CovarianceSet) – The process, measurement, and initial covariances. Q is used exactly as given and must already be the discrete per-step covariance Q_d for the model’s sample time.

  • dt (float or None, optional) – When given and the system is continuous, discretize A/B at this sample time before assembly. Q is not discretized: convert a continuous-time Q_c (e.g. from vane.sysid.modal_process_noise()) with vane.sysid.discretize_process_noise() at the same dt first.

Returns:

KalmanModel – The assembled, dimension-checked model.

Raises:

ValueError – If dt is given for an already-discrete system, the covariance dimensions do not match the system, or a covariance is not a valid covariance matrix (Q/P0 must be finite, symmetric, and positive semidefinite; R must be finite, symmetric, and positive definite).

Return type:

KalmanModel

Runnable, auditable Kalman estimator over a KalmanModel.

The rest of vane.sysid builds and confirms an estimator-ready model; this module runs it. Given the discrete model and its covariances, KalmanFilter performs the recursive predict/update, with the properties a load-estimation digital twin needs to be trusted in the field:

  • Numerically stable – the gain is formed by a Cholesky solve (never an explicit inverse) and the covariance by the Joseph form, so the error covariance stays symmetric positive-semidefinite even for ill-conditioned innovations.

  • Tolerant of real measurement streams – each step accepts an available mask, so a dropped or missing sensor updates on the channels that reported; an optional NIS gate rejects outliers (a measurement whose normalized innovation squared exceeds a chi-square threshold) and the step is flagged rather than corrupting the estimate.

  • Zero-order-hold control timing – consistent with discretized(), the prediction into step k uses the previous sample’s control and the control logged with the measurement enters only the feedthrough: \(x_k = A x_{k-1} + B u_{k-1}\), \(y_k = C x_k + D u_k\).

  • Self-diagnosing – every step records the innovation and its normalized innovation squared (NIS); filter_consistency() checks the NIS sequence against its chi-square expectation so a mis-tuned Q/R is visible, not silent.

  • Auditable offline – rts_smoother() refines a recorded run with the Rauch-Tung-Striebel backward pass, and reconstruct_load() reads a linear load functional off the filtered state with its propagated standard deviation.

SteadyStateKalmanFilter runs the same recursion with the constant steady-state gain (from steady_state_estimator()), trading per-step Riccati updates for a fixed-gain filter once the covariance has converged.

class ConsistencyReport(mean_nis, expected_nis, lower, upper, consistent, n_accepted, n_gated=0)[source]

Bases: object

Whether a run’s innovations are statistically consistent with the model.

Parameters:
  • mean_nis (float) – Average normalized innovation squared over the tested steps – every step with a finite NIS, including steps the outlier gate rejected (their NIS is computed before the gate decision).

  • expected_nis (float) – The expected mean NIS under a correctly tuned filter – the average number of measurement channels per tested step (dof / (n_accepted + n_gated)).

  • lower (float) – The two-sided chi-square acceptance bounds for mean_nis at the chosen level.

  • upper (float) – The two-sided chi-square acceptance bounds for mean_nis at the chosen level.

  • consistent (bool) – Whether mean_nis lies within [lower, upper]; False flags a mis-tuned Q/R (over- or under-confident filter) or a record with more outliers than the model’s noise explains.

  • n_accepted (int) – Number of accepted (applied) steps included in the statistic.

  • n_gated (int, optional) – Number of gated-out (rejected) steps included in the statistic (default 0).

mean_nis: float
expected_nis: float
lower: float
upper: float
consistent: bool
n_accepted: int
n_gated: int = 0
class FilterResult(states, covariances, predicted_states, predicted_covariances, nis, accepted, n_active, state_names=<factory>)[source]

Bases: object

The recorded trajectory of a Kalman run, for diagnostics and smoothing.

Parameters:
  • states (npt.NDArray[np.float64]) – Filtered (a-posteriori) state means, shape (n_steps, n_states).

  • covariances (npt.NDArray[np.float64]) – Filtered error covariances, shape (n_steps, n_states, n_states).

  • predicted_states (npt.NDArray[np.float64]) – A-priori (predicted) state means before each update, same shape as states; retained so the RTS smoother can run without re-filtering.

  • predicted_covariances (npt.NDArray[np.float64]) – A-priori error covariances, shape (n_steps, n_states, n_states).

  • nis (npt.NDArray[np.float64]) – Normalized innovation squared at each step (nan where no measurement was available; a step rejected by the NIS gate keeps its finite value).

  • accepted (npt.NDArray[np.bool]) – Whether each step’s measurement was applied (False when fully masked or rejected by the NIS gate).

  • n_active (npt.NDArray[np.int]) – Number of measurement channels in each step’s innovation (its NIS degrees of freedom), recorded for a gated-out step too; 0 only where no channel was available. Equals n_outputs unless a partial available mask reduced it.

  • state_names (list[str]) – Names of the states, aligned with the last axis of states.

states: npt.NDArray[np.float64]
covariances: npt.NDArray[np.float64]
predicted_states: npt.NDArray[np.float64]
predicted_covariances: npt.NDArray[np.float64]
nis: npt.NDArray[np.float64]
accepted: npt.NDArray[np.bool_]
n_active: npt.NDArray[np.int_]
state_names: list[str]
class KalmanFilter(model, *, initial_state=None, initial_control=None)[source]

Bases: object

A recursive discrete Kalman filter over a KalmanModel.

Holds the current state estimate x and error covariance P and advances them with predict() and update() (or step() for both). The model must be discrete (dt set) and carry a C matrix.

Control timing (zero-order hold). The discrete model produced by discretized() holds each input constant over the sample interval that follows it, so the sampled plant is

\[\mathbf x_k = \mathbf A\mathbf x_{k-1} + \mathbf B\mathbf u_{k-1} + \mathbf w_{k-1}, \qquad \mathbf y_k = \mathbf C\mathbf x_k + \mathbf D\mathbf u_k + \mathbf v_k,\]

with u_k the input logged alongside the measurement y_k. step() therefore predicts with the previous sample’s control and uses the current one only in the feedthrough D u_k. The initial (x0, P0) is the prior one step before the first measurement; the control driving that first prediction is initial_control when given, and otherwise the first control passed to step() (the input is taken as held at its first logged value before the record, so a constant input is filtered exactly).

Parameters:
  • model (KalmanModel) – The discrete model and its Q, R, P0 covariances.

  • initial_state (npt.NDArray[np.float64] or None, optional) – Initial state mean x0 (the prior one step before the first measurement); defaults to zeros.

  • initial_control (npt.NDArray[np.float64] or None, optional) – The input u_{-1} held over the interval before the first measurement, which drives the first step()’s prediction. None (default) uses the first control passed to step() (pass zeros for a plant at rest before the record).

Raises:

ValueError – If the model is continuous (no dt), has no C matrix, initial_state does not match the state dimension, or initial_control is mis-sized, non-finite, or given for a model with neither B nor D.

x: npt.NDArray[np.float64]
predict(control=None)[source]

Advance one step with the control held over this interval.

\(x \leftarrow A x (+ B u)\), \(P \leftarrow A P A^{\mathsf T} + Q\).

This is the raw transition: control is the input held (zero-order hold) over the interval being predicted – for the interval ending at measurement k that is the previous sample’s input u_{k-1}, not u_k. step() and run_filter() apply this timing automatically.

Parameters:

control (npt.NDArray[np.float64] or None, optional) – The held input; None for no input over the interval.

Raises:

ValueError – If control is mis-sized, non-finite, or given for a model with neither B nor D.

Return type:

None

update(measurement, *, control=None, available=None, gate=None, gate_probability=None)[source]

Correct the state with a measurement; return its NIS, acceptance, and dof.

The predicted measurement is \(C x (+ D u_k)\), where u_k is the control logged with this measurement (not the one that drove the prediction), so a model with a nonzero feedthrough D requires it. Uses only the available output channels (a dropped sensor is simply omitted), a Cholesky solve for the gain, and the Joseph-form covariance update. With a gate set, a measurement whose normalized innovation squared exceeds the threshold is rejected (the state is left at its prediction) and reported as not accepted.

Parameters:
  • measurement (npt.NDArray[np.float64]) – The measured outputs, length n_outputs; entries at masked-out channels are ignored.

  • control (npt.NDArray[np.float64] or None, optional) – The control input logged with this measurement, used to subtract the feedthrough D u from the predicted measurement. Required (with a nonzero D matrix) for a correct innovation; ignored if the model has no D.

  • available (npt.NDArray[np.bool] or None, optional) – Boolean mask of reporting channels (length n_outputs); None uses every channel.

  • gate (float or None, optional) – A fixed chi-square NIS threshold for outlier rejection, applied as is whatever the number of reporting channels. Prefer gate_probability when channels can drop out.

  • gate_probability (float or None, optional) – Outlier-gate acceptance probability p in (0, 1): the threshold is chi2.ppf(p, n_used) for the n_used channels that reported, so a correctly tuned filter rejects a fraction 1 - p of steps regardless of dropout. Mutually exclusive with gate; neither applies every update.

Returns:

tuple[float, bool, int] – The normalized innovation squared (nan if no channel was available), whether the update was applied, and the number of channels in the innovation – the NIS degrees of freedom, reported for a gated-out step too; 0 only when no channel was available.

Raises:

ValueError – If both gates are given, gate is not a positive finite threshold, gate_probability is not in (0, 1), measurement is not length n_outputs or has non-finite available channels, or (with a nonzero D) control is missing, mis-sized, or non-finite.

Return type:

tuple[float, bool, int]

step(measurement, control=None, *, available=None, gate=None, gate_probability=None)[source]

Predict then update; return the update’s NIS, acceptance, and dof.

control is the input u_k logged with measurement. Under the zero-order-hold convention (see the class docstring) the prediction is driven by the previous step’s control u_{k-1} (on the first step, initial_control or else u_k itself), and u_k enters only the feedthrough D u_k; it is then held for the next step’s prediction.

Parameters:
  • measurement (npt.NDArray[np.float64]) – The measured outputs at this step.

  • control (npt.NDArray[np.float64] or None, optional) – The input logged with this measurement; None for no input.

  • available (optional) – As for update().

  • gate (optional) – As for update().

  • gate_probability (optional) – As for update().

Returns:

tuple[float, bool, int] – As for update().

Return type:

tuple[float, bool, int]

class SteadyStateKalmanFilter(a, c, gain, *, b=None, d=None, initial_state=None, state_names=None, initial_control=None)[source]

Bases: object

A constant-gain Kalman filter using the precomputed steady-state gain.

Once the error covariance has converged (the steady-state DARE solution from steady_state_estimator()), the gain is constant, so each step is a matrix-vector predict/correct with no per-step Riccati update – cheaper and ideal for an embedded twin. The covariance is fixed at the steady-state value.

It follows the same zero-order-hold control timing as KalmanFilter: the prediction into step k uses the previous step’s control u_{k-1} (on the first step, initial_control or else that step’s own control), and the control logged with the measurement enters only the feedthrough D u_k.

Parameters:
  • a (npt.NDArray[np.float64]) – Discrete state matrix.

  • c (npt.NDArray[np.float64]) – Output matrix.

  • gain (npt.NDArray[np.float64]) – The steady-state Kalman gain K (EstimatorAssessment.gain).

  • b (npt.NDArray[np.float64] or None, optional) – Input matrix, for control inputs into the state.

  • d (npt.NDArray[np.float64] or None, optional) – Feedthrough matrix; when given, D u is subtracted from the measurement so a controlled model with y = C x + D u is filtered correctly.

  • initial_state (npt.NDArray[np.float64] or None, optional) – Initial state mean (the prior one step before the first measurement); defaults to zeros.

  • state_names (list[str] or None, optional) – Names of the states.

  • initial_control (npt.NDArray[np.float64] or None, optional) – The input held over the interval before the first measurement (drives the first prediction); None (default) uses the first step’s own control.

Raises:

ValueError – If the gain shape is inconsistent with a/c, initial_state does not match the state dimension, or initial_control is mis-sized, non-finite, or given for a model with neither b nor d.

x: npt.NDArray[np.float64]
step(measurement, control=None)[source]

Predict then correct with the constant gain; return the new state estimate.

\(x^- = A x (+ B u_{k-1})\), then \(x = x^- + K (z - C x^- (- D u_k))\), with u_k = control the input logged with measurement and u_{k-1} the previous step’s control (the zero-order-hold timing of KalmanFilter).

Parameters:
  • measurement (npt.NDArray[np.float64])

  • control (npt.NDArray[np.float64] | None)

Return type:

npt.NDArray[np.float64]

run(measurements, *, controls=None)[source]

Filter a measurement sequence; return state estimates (n_steps, n).

Row k of controls is the input logged with measurements[k]; the zero-order-hold timing of step() applies (continuing from any previous step() calls on this filter).

Raises:

ValueError – If measurements is not 2-D with n_outputs columns, or controls has a different number of rows.

Parameters:
  • measurements (npt.NDArray[np.float64])

  • controls (npt.NDArray[np.float64] | None)

Return type:

npt.NDArray[np.float64]

filter_consistency(result, *, confidence=0.95)[source]

Check a run’s NIS sequence against its chi-square expectation (NIS test).

For a correctly tuned filter each step’s NIS, computed from the predicted covariance before any gate decision, is chi-square distributed with as many degrees of freedom as channels reported, so the sum over the tested steps is chi-square with dof = the total number of channels in those innovations (result.n_active summed) – a partial available mask, which lowers a step’s dof, is accounted for. Steps rejected by an outlier gate are included: keeping only the accepted ones would test a chi-square truncated at the gate, whose mean is lower, so a correctly tuned, gated filter would be reported as under-confident. A mean NIS far outside the acceptance band means the filter is over-confident (R/Q too small) or under-confident (too large) – a tuning fault that would otherwise be invisible – or that the record holds more outliers than the model’s noise explains.

Parameters:
  • result (FilterResult) – A trajectory from run_filter().

  • confidence (float, optional) – Two-sided confidence level for the acceptance band (default 0.95).

Returns:

ConsistencyReport – The mean NIS, its expectation, the acceptance band, and the outcome.

Raises:

ValueError – If no step has a finite NIS to test, or confidence is not in (0, 1).

Return type:

ConsistencyReport

Notes

A step with a finite NIS but n_active == 0 (a gated step recorded without the degrees of freedom of its rejected innovation) carries no degrees of freedom and is left out.

reconstruct_load(result, load_map, *, smoothed=None)[source]

Read a linear load off the estimated state with its propagated uncertainty.

For a load functional \(z = L x\), returns the reconstructed load \(\hat z_k = L \hat x_k\) and its standard deviation \(\sqrt{\operatorname{diag}(L P_k L^{\mathsf T})}\) at each step – the virtual sensor and its error bar.

Parameters:
  • result (FilterResult) – A trajectory from run_filter().

  • load_map (npt.NDArray[np.float64]) – The load functionals L, shape (n_loads, n_states).

  • smoothed (tuple or None, optional) – A (states, covariances) pair from rts_smoother() to read the load from the smoothed estimate instead of the filtered one.

Returns:

tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]] – The reconstructed loads (n_steps, n_loads) and their standard deviations (n_steps, n_loads).

Raises:

ValueError – If load_map does not match the state dimension or is non-finite.

Return type:

tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]

rts_smoother(model, result)[source]

Refine a filtered run with the Rauch-Tung-Striebel backward pass.

The smoother conditions each state on the whole record (not just the past), which an offline audit can afford: it lowers the variance and removes the filter’s lag. The control inputs enter only through the run’s recorded predictions (result.predicted_states, formed with the zero-order-hold timing of run_filter()), so the backward pass needs no controls of its own.

Parameters:
Returns:

tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]] – The smoothed state means (n_steps, n_states) and covariances (n_steps, n_states, n_states).

Return type:

tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]

run_filter(model, measurements, *, controls=None, available=None, gate=None, gate_probability=None, initial_state=None, initial_control=None)[source]

Run a Kalman filter over a measurement sequence and record the trajectory.

Control timing (zero-order hold). Row k of controls is the input u_k logged alongside measurements[k]. Consistent with the ZOH discretization, the recursion is

\[\mathbf x_k^- = \mathbf A\mathbf x_{k-1}^+ + \mathbf B\mathbf u_{k-1}, \qquad \nu_k = \mathbf z_k - (\mathbf C\mathbf x_k^- + \mathbf D\mathbf u_k),\]

i.e. the prediction into step k uses the previous sample’s control and the current control enters only the feedthrough. initial_state (with the model’s P0) is the prior at step -1, one step before measurements[0]; the first prediction is driven by initial_control (u_{-1}), which defaults to controls[0] – the input held at its first logged value before the record, so a constant input gives exactly the same result as holding it throughout.

Parameters:
  • model (KalmanModel) – The discrete model and covariances.

  • measurements (npt.NDArray[np.float64]) – Measured outputs, shape (n_steps, n_outputs).

  • controls (npt.NDArray[np.float64] or None, optional) – Control inputs logged with the measurements, shape (n_steps, n_inputs); None for an autonomous model.

  • available (npt.NDArray[np.bool] or None, optional) – Per-step channel mask, shape (n_steps, n_outputs); None uses all channels at every step. Use this to encode sensor dropout.

  • gate (float or None, optional) – A fixed chi-square NIS threshold for per-step outlier rejection (not scaled by the number of reporting channels).

  • gate_probability (float or None, optional) – Outlier-gate acceptance probability in (0, 1); each step’s threshold is the chi-square quantile for its number of reporting channels. Mutually exclusive with gate.

  • initial_state (npt.NDArray[np.float64] or None, optional) – Prior state mean one step before measurements[0]; defaults to zeros.

  • initial_control (npt.NDArray[np.float64] or None, optional) – The input u_{-1} driving the first prediction; defaults to controls[0]. Pass zeros for a plant at rest (no input) before the record. Requires controls.

Returns:

FilterResult – The filtered and predicted state means/covariances, per-step NIS, and acceptance flags.

Raises:

ValueError – If measurements is not 2-D with n_outputs columns, controls / available lengths disagree with it, initial_control is given without controls, or the gate arguments are invalid.

Return type:

FilterResult

Kalman covariance initialization, informed by mode-identification uncertainty.

A Kalman filter needs a process-noise covariance Q, a measurement-noise covariance R, and an initial state covariance P0. When the modes were classified with uncertainty quantification, that uncertainty is propagated into P0: a mode identified with low confidence is given a larger initial variance, so the filter trusts well-identified modes more than ambiguous ones.

class CovarianceSet(q, r, p0)[source]

Bases: object

Kalman-filter covariance matrices.

Parameters:
  • q (npt.NDArray[np.float64]) – Process-noise covariance, shape (n_states, n_states).

  • r (npt.NDArray[np.float64]) – Measurement-noise covariance, shape (n_outputs, n_outputs).

  • p0 (npt.NDArray[np.float64]) – Initial state covariance, shape (n_states, n_states).

q: npt.NDArray[np.float64]
r: npt.NDArray[np.float64]
p0: npt.NDArray[np.float64]
covariances_from_confidence(confidences, n_outputs, *, process_noise=0.0001, measurement_noise=0.01, base_variance=1.0, uncertainty_scale=10.0)[source]

Build covariances whose P0 grows with mode-identification uncertainty.

Each mode maps to two modal states (real and imaginary parts); the initial variance of those states is base_variance * (1 + uncertainty_scale * (1 - confidence)), so a low-confidence mode starts with a larger covariance.

Parameters:
  • confidences (Sequence[float]) – Per-mode identification confidence in [0, 1]; values are clipped to that range.

  • n_outputs (int) – Number of measured outputs.

  • process_noise (float, optional) – Diagonal values for Q and R; must be non-negative.

  • measurement_noise (float, optional) – Diagonal values for Q and R; must be non-negative.

  • base_variance (float, optional) – Initial variance of a fully confident mode; must be non-negative.

  • uncertainty_scale (float, optional) – How strongly uncertainty inflates the initial variance; must be non-negative.

Returns:

CovarianceSet – The covariance matrices, with 2 * len(confidences) states.

Raises:

ValueError – If a noise value, base_variance, uncertainty_scale, or n_outputs is negative or non-finite, or a confidence is non-finite.

Return type:

CovarianceSet

initialize_covariances(n_states, n_outputs, *, process_noise=0.0001, measurement_noise=0.01, initial_variance=1.0)[source]

Build uniform diagonal covariance matrices.

Parameters:
  • n_states (int) – Number of states.

  • n_outputs (int) – Number of measured outputs.

  • process_noise (float, optional) – Diagonal values for Q, R, and P0 respectively; all must be non-negative.

  • measurement_noise (float, optional) – Diagonal values for Q, R, and P0 respectively; all must be non-negative.

  • initial_variance (float, optional) – Diagonal values for Q, R, and P0 respectively; all must be non-negative.

Returns:

CovarianceSet – The diagonal covariance matrices.

Raises:

ValueError – If a dimension is negative, or a noise value or initial_variance is negative or non-finite.

Return type:

CovarianceSet

Process- and measurement-noise covariances for the discrete estimator.

A Kalman filter on a discretized model needs a discrete process-noise covariance \(\mathbf Q_d\) consistent with the continuous model it came from, not a hand-picked diagonal. This module builds the covariances the reduced estimator consumes:

  • discretize_process_noise() converts a continuous process-noise spectral density \(\mathbf Q_c\) to the exact discrete covariance \(\mathbf Q_d=\int_0^{\Delta t}e^{\mathbf A s}\mathbf Q_c e^{\mathbf A^{\mathsf T} s}\,\mathrm ds\) over one step, by the Van Loan block-exponential identity — the discretization that matches the zero-order-hold A_d the state space already uses.

  • modal_process_noise() assembles a block-diagonal continuous \(\mathbf Q_c\) for the real modal realization (two states per mode), one intensity per mode, so the per-mode process noise can be set from each mode’s damping and identification uncertainty rather than a single global value.

  • measurement_noise_from_specs() turns per-channel sensor noise standard deviations into the diagonal measurement-noise covariance \(\mathbf R\).

Together they replace the placeholder diagonal Q/R with covariances traceable to the continuous model and the sensor specifications.

discretize_process_noise(a, spectral_density, dt)[source]

Discretize a continuous process-noise spectral density over one step.

Computes the discrete process-noise covariance

\[\mathbf Q_d = \int_0^{\Delta t} e^{\mathbf A s}\,\mathbf Q_c\, e^{\mathbf A^{\mathsf T} s}\,\mathrm d s\]

exactly, by the Van Loan block-exponential identity: for \(\mathbf F = \begin{bmatrix}-\mathbf A & \mathbf Q_c\\ \mathbf 0 & \mathbf A^{\mathsf T}\end{bmatrix}\), with \(e^{\mathbf F\Delta t}=\begin{bmatrix}\mathbf G_{11}&\mathbf G_{12}\\ \mathbf 0&\mathbf G_{22}\end{bmatrix}\), the discrete-time transition is \(\mathbf A_d=\mathbf G_{22}^{\mathsf T}\) (equal to the zero-order-hold \(e^{\mathbf A\Delta t}\)) and \(\mathbf Q_d=\mathbf A_d\mathbf G_{12}\). This is the process-noise companion to the ZOH discretization of (A, B).

The block exponentiates \(-\mathbf A\), which grows for stable modes; for a stiff system that intermediate overflows before the cancellation. To stay finite and well conditioned for stiff, near-marginal, and mixed systems alike, the step is split into \(2^k\) equal sub-steps small enough that the block is well behaved, and the sub-step covariance is recombined by doubling, \(\mathbf Q_d(2\tau)=\mathbf Q_d(\tau)+\boldsymbol\Phi(\tau)\mathbf Q_d(\tau) \boldsymbol\Phi(\tau)^{\mathsf T}\) with \(\boldsymbol\Phi=e^{\mathbf A\tau}\). No stability is required.

Parameters:
  • a (npt.NDArray[np.float64]) – Continuous-time system matrix, shape (n_states, n_states).

  • spectral_density (npt.NDArray[np.float64]) – Continuous process-noise spectral density Q_c, a symmetric positive semidefinite matrix of shape (n_states, n_states).

  • dt (float) – Sample step Δt (must be positive and finite).

Returns:

npt.NDArray[np.float64] – The discrete process-noise covariance Q_d, a symmetric (n_states, n_states) matrix.

Raises:

ValueError – If a is not square, spectral_density does not match a, is not a valid (symmetric positive-semidefinite) covariance, or dt is not positive and finite.

Return type:

npt.NDArray[np.float64]

measurement_noise_from_specs(noise_std)[source]

Build the measurement-noise covariance from per-channel noise specs.

Each sensor’s noise standard deviation maps to a variance on the diagonal of R, so the measurement-noise covariance is traceable to the instrument specifications rather than a single global guess.

Parameters:

noise_std (Sequence[float]) – Per-channel noise standard deviation (one per sensor), all non-negative, in the sensor’s measurement units.

Returns:

npt.NDArray[np.float64] – The diagonal measurement-noise covariance R = diag(noise_std**2) of shape (n_sensors, n_sensors).

Raises:

ValueError – If any standard deviation is negative or non-finite.

Return type:

npt.NDArray[np.float64]

modal_process_noise(intensities)[source]

Build a block-diagonal continuous process-noise density over modal states.

The real modal realization carries two states per mode; this places one intensity on each mode’s two states, giving the continuous spectral density Q_c for discretize_process_noise(). Setting a larger intensity on a less-trusted or more lightly damped mode lets the filter admit more drift there.

Parameters:

intensities (Sequence[float]) – Per-mode process-noise intensity (one value per mode), all non-negative.

Returns:

npt.NDArray[np.float64] – The block-diagonal spectral density Q_c of shape (2 * n_modes, 2 * n_modes), with intensities[k] on the two states of mode k.

Raises:

ValueError – If any intensity is negative or non-finite.

Return type:

npt.NDArray[np.float64]

Unknown-input (disturbance) augmentation for load virtual sensing.

A reduced model reconstructs states from sensors, but the quantity a digital twin ultimately wants is often an unknown input — the wave/wind load or mooring force that drives the structure and is never measured. The standard way to estimate it is to append a model of the disturbance to the state space and let the filter estimate the augmented state; the disturbance estimate is then read straight off it.

Given a physical continuous model

\[\dot{\mathbf x} = \mathbf A\mathbf x + \mathbf B\mathbf u + \mathbf E\mathbf d, \qquad \mathbf y = \mathbf C\mathbf x + \mathbf D\mathbf u + \mathbf F\mathbf d,\]

and a disturbance shaping model \(\dot{\mathbf z} = \mathbf A_d\mathbf z + \mathbf w\), \(\mathbf d = \mathbf C_d\mathbf z\) (white \(\mathbf w\)), the augmented system on \([\mathbf x; \mathbf z]\) is

\[\begin{split}\mathbf A_{\text{aug}} = \begin{bmatrix}\mathbf A & \mathbf E\mathbf C_d\\ \mathbf 0 & \mathbf A_d\end{bmatrix},\quad \mathbf B_{\text{aug}} = \begin{bmatrix}\mathbf B\\ \mathbf 0\end{bmatrix},\quad \mathbf C_{\text{aug}} = \begin{bmatrix}\mathbf C & \mathbf F\mathbf C_d\end{bmatrix},\end{split}\]

with the disturbance recovered from the augmented state by \(\hat{\mathbf d} = \begin{bmatrix}\mathbf 0 & \mathbf C_d\end{bmatrix} [\mathbf x; \mathbf z]\).

random_walk_disturbance() models a slowly-varying (quasi-constant) unknown load; harmonic_disturbance() models a narrowband load at a known frequency — the natural choice for wave-frequency mooring and platform loads. augment_with_disturbance() assembles the augmented state space, the disturbance-recovery map, and the augmented process-noise spectral density (ready for discretize_process_noise()). This construction is deterministic and auditable, so it lives in the model factory and not ad hoc in the estimator runtime.

class AugmentedModel(system, recovery, spectral_density, n_disturbance_states)[source]

Bases: object

A state space augmented with a disturbance model and the recovery map.

Parameters:
  • system (StateSpace) – The augmented continuous state space on [x; z] (physical states first, then disturbance states), with the physical inputs and outputs preserved.

  • recovery (npt.NDArray[np.float64]) – The disturbance-recovery map, shape (n_signals, n_aug_states); the disturbance estimate is d_hat = recovery @ x_aug.

  • spectral_density (npt.NDArray[np.float64]) – The augmented continuous process-noise spectral density, shape (n_aug_states, n_aug_states) — the physical process noise (or zero) on the physical block and the disturbance intensity on the disturbance block.

  • n_disturbance_states (int) – Number of appended disturbance states.

system: StateSpace
recovery: npt.NDArray[np.float64]
spectral_density: npt.NDArray[np.float64]
n_disturbance_states: int
class DisturbanceModel(a, c, intensity, state_names)[source]

Bases: object

A linear shaping model of an unknown disturbance/load.

The disturbance d is the output of a small linear system driven by white noise, z' = a z + w, d = c z, with w a continuous white process of spectral density intensity. Different a/c give different prior shapes (a random walk, a harmonic oscillator, …).

Parameters:
  • a (npt.NDArray[np.float64]) – Disturbance internal dynamics, shape (n_dist_states, n_dist_states).

  • c (npt.NDArray[np.float64]) – Disturbance output map, shape (n_signals, n_dist_states); d = c z has n_signals channels.

  • intensity (npt.NDArray[np.float64]) – Continuous process-noise spectral density of the disturbance states, a symmetric positive-semidefinite (n_dist_states, n_dist_states) matrix.

  • state_names (list[str]) – Names of the disturbance states (length n_dist_states; may be empty).

a: npt.NDArray[np.float64]
c: npt.NDArray[np.float64]
intensity: npt.NDArray[np.float64]
state_names: list[str]
augment_with_disturbance(system, disturbance, *, entry, output_entry=None, process_noise=None)[source]

Append a disturbance model to a state space for unknown-input estimation.

Builds the augmented continuous system on [x; z] with the disturbance entering the physical dynamics through entry (x' += entry @ d). An optional output_entry preserves direct disturbance-to-output feedthrough (y += output_entry @ d). The physical inputs and outputs are otherwise preserved, and the disturbance is recovered from the augmented state.

Parameters:
  • system (StateSpace) – A continuous-time physical model (dt is None).

  • disturbance (DisturbanceModel) – The disturbance shaping model (e.g. from random_walk_disturbance() or harmonic_disturbance()).

  • entry (npt.NDArray[np.float64]) – Input map of the disturbance into the physical state derivative, shape (n_states, n_signals) where n_signals is disturbance.c’s row count. Often a column block of B (the load enters where a known input does).

  • output_entry (npt.NDArray[np.float64] or None, optional) – Direct disturbance-to-output map, shape (n_outputs, n_signals). This is the disturbance column block removed from the physical system’s D matrix when an originally known input is promoted to an augmented state. None means there is no direct disturbance feedthrough.

  • process_noise (npt.NDArray[np.float64] or None, optional) – Continuous process-noise spectral density of the physical states, shape (n_states, n_states). None uses zero physical process noise.

Returns:

AugmentedModel – The augmented system, the disturbance-recovery map, the augmented process-noise spectral density, and the disturbance-state count.

Raises:

ValueError – If system is discrete, entry/output_entry do not match the physical system and disturbance signals, or process_noise has the wrong shape.

Return type:

AugmentedModel

combine_disturbance_models(disturbances)[source]

Combine independent disturbance models without changing their signals.

The returned shaping model is the block-diagonal union of the supplied dynamics and process-noise intensities. Its output matrix is block diagonal as well, so the physical disturbance signals remain ordered by model and are not summed. This is useful for mixed priors such as aerodynamic torque as a random walk and incident wave elevation as a narrowband oscillator.

Parameters:

disturbances (Sequence[DisturbanceModel]) – Non-empty sequence of validated shaping models.

Returns:

DisturbanceModel – Independent block-diagonal union of every state and signal channel.

Return type:

DisturbanceModel

gauss_markov_disturbance(time_constants_s, intensity, *, n_signals=1, name='load')[source]

Model bounded load perturbations as independent first-order processes.

Each signal follows z' = -z / tau + w and d = z. The operating-point baseline is supplied by the surrounding linear model, so the prior mean remains zero in perturbation coordinates while tau prevents an unconstrained random walk from drifting indefinitely. This is suitable for scheduled controller-regulated loads such as above-rated aerodynamic torque.

Parameters:
  • time_constants_s (float or Sequence[float]) – Positive finite mean-reversion time constant per signal in seconds.

  • intensity (float or Sequence[float]) – Non-negative continuous process-noise intensity per signal.

  • n_signals (int, optional) – Number of independent signals; default one.

  • name (str, optional) – Base name for the disturbance states.

Return type:

DisturbanceModel

harmonic_disturbance(frequencies_hz, intensity, *, name='wave')[source]

Model unknown loads as narrowband oscillators at known frequencies.

Each frequency contributes a two-state undamped oscillator [[0, ω], [-ω, 0]] (ω = 2π f) driven by white noise, with the disturbance read from its first state. This is the natural prior for a wave-frequency load whose frequency is known but whose amplitude and phase are not.

Parameters:
  • frequencies_hz (Sequence[float]) – Oscillator frequencies in Hz, all finite and positive. One disturbance channel (and two states) per frequency.

  • intensity (float or Sequence[float]) – Continuous process-noise intensity driving each oscillator (applied to both of its states); a scalar applies to all, or one value per frequency. Non-negative.

  • name (str, optional) – Base name for the disturbance states ("{name}_{i}_{re,im}").

Returns:

DisturbanceModel – A harmonic disturbance model with 2 * len(frequencies_hz) states and one output channel per frequency.

Raises:

ValueError – If no frequencies are given, any frequency is non-finite or not positive, intensity has the wrong length, or any intensity is negative.

Return type:

DisturbanceModel

random_walk_disturbance(intensity, *, n_signals=1, name='load')[source]

Model unknown loads as independent random walks (quasi-constant inputs).

Each disturbance channel is integrated white noise (z' = w, d = z), the standard prior for a slowly-varying or constant unknown input. One disturbance state per channel.

Parameters:
  • intensity (float or Sequence[float]) – Continuous process-noise intensity of each random walk; a scalar applies to all channels, or one value per channel. All values must be non-negative.

  • n_signals (int, optional) – Number of independent disturbance channels (default 1).

  • name (str, optional) – Base name for the disturbance states ("{name}_{i}").

Returns:

DisturbanceModel – A random-walk disturbance model with n_signals states.

Raises:

ValueError – If n_signals is not positive, intensity has the wrong length, or any intensity is negative.

Return type:

DisturbanceModel

random_walk_harmonic_disturbance(frequencies_hz, random_walk_intensity, harmonic_intensity, *, name='load')[source]

Model one unknown load as a DC random walk plus scheduled harmonics.

Unlike harmonic_disturbance(), whose frequencies are separate disturbance channels, this construction sums every component into one physical signal: d = z_dc + sum(z_h,re). It is intended for a rotor load containing a slowly varying component together with known 1P/3P orders. Amplitudes and phases remain estimated states; only the physics-derived frequencies are prescribed.

Parameters:
  • frequencies_hz (Sequence[float]) – Positive harmonic frequencies in hertz.

  • random_walk_intensity (float) – Continuous process-noise intensity of the DC random-walk state.

  • harmonic_intensity (float or Sequence[float]) – Continuous process-noise intensity per harmonic oscillator. A scalar broadcasts; each value is applied to both quadrature states.

  • name (str, optional) – Base name for the DC and harmonic states.

Return type:

DisturbanceModel

Observability analysis, sensor placement, and the UQ-initialized reduced estimator.

The reduced-order model (vane.sysid.rom) keeps the modes that drive a load; this module answers the two questions a load estimator then poses: which sensors observe those modes, and how to assemble the estimator with its uncertainty baked in.

  • observability_gramian() is the (infinite-horizon) observability Gramian \(\mathbf W_o\) of a stable system — the Lyapunov solution of \(\mathbf A^{\mathsf T}\mathbf W_o + \mathbf W_o\mathbf A + \mathbf C^{\mathsf T} \mathbf C = 0\) (continuous) — whose spectrum measures how strongly each state direction is seen at the outputs. With a measurement-noise covariance it becomes the noise-weighted (Fisher) information Gramian using \(\mathbf C^{\mathsf T}\mathbf R^{-1}\mathbf C\).

  • finite_horizon_observability_gramian() is the observability Gramian accumulated over a finite sensing window \([0, T]\) — the relevant measure for short-window estimation and the only one defined when the system carries marginal or unstable modes (the infinite-horizon Gramian does not exist there). It is computed exactly: by the Van Loan block-matrix-exponential identity in continuous time, and by direct summation in discrete time.

  • observability_scores() breaks the Gramian down into per-output scores (how much each candidate sensor sees) and per-mode scores (how strongly each identified mode is observed), tying the identified modal content to the sensor layout.

  • load_reconstructability() answers the load-estimator question directly: given a named load output (not necessarily a sensor) and a sensor set, what fraction of that load lies in the observable subspace, and what is the a-priori (Cramér-Rao-type) variance of reconstructing it — the assessment a virtual-sensing twin needs before it runs a single time step.

  • recommend_sensors() chooses, from a set of candidate output channels, the subset that maximises observability, by greedy D-optimal (log-determinant of \(\mathbf W_o\)) selection — the standard sensor-placement objective, which rewards covering all observable directions rather than piling sensors on the same one.

  • reduced_estimator() assembles a Kalman-ready model on the reduced system whose initial covariance is inflated by each mode’s confidence — a less-trusted mode starts with larger variance — tying the reduction, the per-mode uncertainty, and the Kalman export into one auditable estimator.

class ConstrainedPlacement(selected, names, scores, total_cost)[source]

Bases: object

A sensor selection that honours deployment constraints, and its cost.

Parameters:
  • selected (list[int]) – The chosen output (sensor) row indices: the required sensors first (in the order given), then the greedily added ones.

  • names (list[str]) – The output names of the chosen sensors (empty strings if unnamed).

  • scores (list[float]) – The observability metric (log-determinant of the Gramian) after each selection, aligned with selected.

  • total_cost (float) – The summed cost of the selected sensors.

selected: list[int]
names: list[str]
scores: list[float]
total_cost: float
class GroupConstraint(members, max_count)[source]

Bases: object

A cap on how many sensors may be chosen from one group of channels.

Parameters:
  • members (Sequence[int]) – Output row indices that belong to the group (e.g. all channels on one component, or mutually redundant sensors).

  • max_count (int) – The most sensors that may be selected from the group (>= 0).

members: Sequence[int]
max_count: int
class LoadReconstructability(load_name, reconstructable_fraction, blind_fraction, observability, noise_amplification, n_observable_directions)[source]

Bases: object

Whether and how well a target load can be reconstructed from a sensor set.

Parameters:
  • load_name (str) – The name (or "output[i]") of the target load output.

  • reconstructable_fraction (float) – Fraction in [0, 1] of the load output direction that lies in the observable subspace of the sensor set, measured in the Euclidean metric of the model’s state coordinates. 1 means the load is fully reconstructable; a value below 1 means a part of the load lives in the sensors’ blind subspace and cannot be recovered at any noise level. Only that outcome (1 or not) is independent of the state basis: the value of a partial fraction depends on the state coordinates and units (a similarity transform or rescaling of the states changes it – a purely blind load can read close to 1 in skewed coordinates), because splitting the load between the observable and blind subspaces requires a state inner product. Do not rank or threshold on a partial value; use noise_amplification (basis-independent) instead.

  • blind_fraction (float) – 1 - reconstructable_fraction — the irrecoverable share of the load, with the same coordinate dependence.

  • observability (float) – The observed energy per unit load, c_load Wo c_load^T / (c_load c_load^T); larger means the load aligns with strongly observed directions. Like the fraction, it is normalised in the state coordinates and so is basis-dependent.

  • noise_amplification (float) – The a-priori (Cramér-Rao-type) variance scale of reconstructing the load from the sensor information over the window, c_load Wo^+ c_load^T over the observable subspace. With a measurement-noise covariance supplied this is the best achievable static reconstruction variance; smaller is better. It is inf whenever reconstructable_fraction < 1 — any blind component is irrecoverable at any noise level, so the variance is undefined (reported as infinite so a smaller-is-better ranking never prefers a blind load). It is invariant under any invertible change of state coordinates (c_load Wo^+ c_load^T is unchanged when x = T x'), so it is the measure to compare sensor sets or models with.

  • n_observable_directions (int) – The rank of the sensor observability Gramian (the dimension of the observable subspace).

load_name: str
reconstructable_fraction: float
blind_fraction: float
observability: float
noise_amplification: float
n_observable_directions: int
class ObservabilityScores(per_output, output_names, per_mode)[source]

Bases: object

Per-output and per-mode breakdown of how strongly a system is observed.

Parameters:
  • per_output (npt.NDArray[np.float64]) – For each output row, the observability energy that single sensor provides (the trace of the Gramian built from that row alone), in the output order. A near-zero entry marks a sensor that sees almost nothing.

  • output_names (list[str]) – The output names aligned with per_output (empty strings if unnamed).

  • per_mode (npt.NDArray[np.float64]) – For each mode group in mode_states, the observability energy of that mode (the trace of the Gramian’s diagonal block over the mode’s states), in the group order. Empty when no mode grouping was supplied.

per_output: npt.NDArray[np.float64]
output_names: list[str]
per_mode: npt.NDArray[np.float64]
class SensorPlacement(selected, names, scores)[source]

Bases: object

A recommended set of sensors and the observability gained at each step.

Parameters:
  • selected (list[int]) – The chosen output (sensor) row indices, in the order they were selected.

  • names (list[str]) – The output names of the chosen sensors (empty strings if unnamed).

  • scores (list[float]) – The observability metric (log-determinant of the Gramian) after each selection — a non-decreasing record of the observability gained.

selected: list[int]
names: list[str]
scores: list[float]
constrained_sensor_selection(system, *, candidates=None, required=(), forbidden=(), costs=None, budget=None, max_sensors=None, groups=None, measurement_noise=None, horizon=None)[source]

Greedy D-optimal sensor selection under deployment constraints.

Extends recommend_sensors() with the constraints a real instrumentation plan faces: sensors that must or must not be used, a per-sensor cost and a total budget, a cap on the total count, and caps on how many sensors may come from each group (e.g. one component, or a set of redundant channels). Required sensors are taken first; the rest are added greedily by the largest log-determinant gain among the channels that keep every constraint satisfied, until none remain feasible.

Parameters:
  • system (StateSpace) – A system with a C matrix. Must be asymptotically stable unless a finite horizon is given.

  • candidates (Sequence[int] or None, optional) – Output row indices that may be instrumented. None considers every output.

  • required (Sequence[int], optional) – Output rows that must be selected (taken first, even if outside candidates).

  • forbidden (Sequence[int], optional) – Output rows that must never be selected.

  • costs (Sequence[float] or None, optional) – Per-output cost (length n_outputs), all non-negative. None treats every sensor as free (cost 0).

  • budget (float or None, optional) – Maximum total cost of the selection. None imposes no budget.

  • max_sensors (int or None, optional) – Maximum number of sensors. None imposes no count cap (the greedy loop then runs until no feasible candidate remains).

  • groups (Sequence[GroupConstraint] or None, optional) – Per-group caps on the number of selected sensors.

  • measurement_noise (npt.NDArray[np.float64] or None, optional) – Full (n_outputs, n_outputs) measurement-noise covariance R; each selected sensor is weighted by its inverse noise so a noisy channel ranks below a calibrated one. None weights every channel equally.

  • horizon (float or None, optional) – Finite sensing window T. None uses the infinite-horizon Gramian (stable systems only); a finite horizon also admits marginal/augmented models.

Returns:

ConstrainedPlacement – The selected sensors (required first, then greedily added), their names, the observability metric after each selection, and the total cost.

Raises:

ValueError – If the system has no C matrix, is unstable with no finite horizon, an index is out of range, required and forbidden overlap, a cost/budget/cap is negative, measurement_noise is not a symmetric positive-definite (n_outputs, n_outputs) matrix, or the required sensors alone already violate the budget, count cap, or a group cap.

Return type:

ConstrainedPlacement

finite_horizon_observability_gramian(system, horizon, *, measurement_noise=None)[source]

Return the observability Gramian accumulated over a finite window [0, T].

The finite-horizon observability Gramian is

\[\mathbf W_o(T) = \int_0^T e^{\mathbf A^{\mathsf T}\tau}\,\mathbf M\, e^{\mathbf A\tau}\,\mathrm d\tau \quad\text{(continuous)}, \qquad \mathbf W_o(N) = \sum_{k=0}^{N-1}(\mathbf A^{\mathsf T})^k\,\mathbf M\, \mathbf A^k \quad\text{(discrete)},\]

with source \(\mathbf M = \mathbf C^{\mathsf T}\mathbf C\) (or its noise-weighted form \(\mathbf C^{\mathsf T}\mathbf R^{-1}\mathbf C\)). Unlike the infinite-horizon Gramian it requires no stability assumption — it is the right observability measure for a short sensing window and for systems that retain marginal or unstable (e.g. aero) modes. The continuous integral is evaluated exactly by the Van Loan block-matrix-exponential identity; the discrete sum, directly.

Parameters:
  • system (StateSpace) – A system with a C matrix (need not be stable).

  • horizon (float) – The sensing window length T in seconds (must be positive and finite). For a discrete system (dt set) it is converted to N = round(T / dt) steps (at least 1).

  • measurement_noise (npt.NDArray[np.float64] or None, optional) – Measurement-noise covariance R; see observability_gramian().

Returns:

npt.NDArray[np.float64] – The (n_states, n_states) finite-horizon observability Gramian.

Raises:

ValueError – If the system has no C matrix, horizon is not positive and finite, measurement_noise has the wrong shape, or the Gramian is not finite (an unstable mode growing beyond floating-point range over the window).

Return type:

npt.NDArray[np.float64]

Notes

The continuous block [[-A^T, M], [0, A]] exponentiates -A, which grows for every stable mode, so over a whole window it is the fastest mode (largest |Re(lambda)| T) that overflows the intermediate exponential, not the slowest. The window is therefore split into 2^k equal sub-intervals tau short enough that max |Re(lambda)| tau <= 1; the sub-interval Gramian is recombined by doubling, W(2 tau) = W(tau) + Phi(tau)^T W(tau) Phi(tau) with Phi = expm(A tau), which stays finite for any stable mode and any window. A genuinely unstable mode can still make the Gramian itself overflow over a long window; that raises ValueError rather than returning inf/nan. For a stable system over an effectively infinite horizon use observability_gramian(), which the finite-horizon Gramian converges to.

load_reconstructability(system, load_output, *, sensor_outputs=None, measurement_noise=None, horizon=None)[source]

Assess whether a target load is reconstructable from a sensor set.

A load estimator measures a few sensor channels and reports a different output — the load. That load is recoverable only if its output direction \(\mathbf c_{\text{load}}\) lies in the observable subspace of the sensors, the range of the sensor observability Gramian \(\mathbf W_o\) (the orthogonal complement of the unobservable subspace, \(\ker\mathbf W_o\)). This function reports that projection and the a-priori reconstruction variance — an assessment the downstream twin can check before running any filter.

Whether the load is reconstructable, and its noise_amplification, are independent of the state basis. The size of a partial projection (reconstructable_fraction below 1) and observability are measured in the Euclidean metric of the state coordinates and change under a similarity transform or state rescaling; compare sensor sets by noise_amplification.

Parameters:
  • system (StateSpace) – A system with a C matrix whose rows include both the sensors and the load. Must be asymptotically stable unless a finite horizon is given.

  • load_output (int or str) – The target load output (row index, or a name matched exactly then by substring).

  • sensor_outputs (Sequence[int] or None, optional) – The output rows used as measurements. None uses every output row except the load row (so the load is never measured to reconstruct itself). An explicit set must not contain load_output.

  • measurement_noise (npt.NDArray[np.float64] or None, optional) – Measurement-noise covariance R of the sensor rows (shape (n_sensors, n_sensors)). When given, the Gramian is the Fisher information and noise_amplification is the best achievable static reconstruction error.

  • horizon (float or None, optional) – Finite sensing window T; None uses the infinite-horizon Gramian, which requires a stable system.

Returns:

LoadReconstructability – The reconstructable fraction, blind fraction, observed energy, a-priori variance scale, and the observable-subspace dimension.

Raises:

ValueError – If the system has no C matrix, the load row is zero, an index is out of range, sensor_outputs includes the load output or leaves no sensor rows, horizon is non-positive, the system is unstable with no finite horizon, or measurement_noise is the wrong shape.

Return type:

LoadReconstructability

observability_gramian(system, *, measurement_noise=None)[source]

Return the infinite-horizon observability Gramian \(\mathbf W_o\).

Solves the Lyapunov equation \(\mathbf A^{\mathsf T}\mathbf W_o + \mathbf W_o\mathbf A = -\mathbf M\) (continuous) or \(\mathbf A^{\mathsf T}\mathbf W_o\mathbf A - \mathbf W_o = -\mathbf M\) (discrete), where the source is \(\mathbf M = \mathbf C^{\mathsf T}\mathbf C\) or, when measurement_noise is given, the noise-weighted information \(\mathbf M = \mathbf C^{\mathsf T}\mathbf R^{-1}\mathbf C\). The Gramian is symmetric positive semidefinite; a near-singular Gramian signals weakly- or un-observable state directions.

Parameters:
  • system (StateSpace) – A stable system with a C matrix.

  • measurement_noise (npt.NDArray[np.float64] or None, optional) – Measurement-noise covariance R (shape (n_outputs, n_outputs)). When given, each output is inverse-noise weighted, so the Gramian becomes the Fisher information available to an estimator. None uses unit weighting.

Returns:

npt.NDArray[np.float64] – The (n_states, n_states) observability Gramian.

Raises:

ValueError – If the system has no C matrix, is not asymptotically stable, or measurement_noise has the wrong shape.

Return type:

npt.NDArray[np.float64]

See also

finite_horizon_observability_gramian

the finite-window Gramian, defined even when the system has marginal or unstable modes.

observability_scores(system, *, mode_states=None, measurement_noise=None, horizon=None)[source]

Break the observability Gramian down per output and per mode.

Two diagnostics drawn from one Gramian:

  • per output — for each output row \(\mathbf c_i\), the trace of the Gramian built from that row alone, i.e. the observability energy that single sensor provides. This ranks candidate sensors by how much they see.

  • per mode — for each state group in mode_states, the trace of the Gramian’s diagonal block over those states. For the real modal realization of modal_reduction() (two states per mode, groups [[0, 1], [2, 3], ...]) this diagonal block equals that mode’s own single-mode Gramian exactly, so it measures how strongly each identified mode is observed — connecting the modal content to the sensor layout.

Parameters:
  • system (StateSpace) – A system with a C matrix. Must be asymptotically stable unless a finite horizon is given.

  • mode_states (Sequence[Sequence[int]] or None, optional) – State-index groups, one per mode. None skips the per-mode breakdown.

  • measurement_noise (npt.NDArray[np.float64] or None, optional) – Measurement-noise covariance R; per-output scores use each channel’s own variance R[i, i]; per-mode scores use the full C^T R^-1 C information.

  • horizon (float or None, optional) – Finite sensing window T; None uses the infinite-horizon Gramian (which requires a stable system). See finite_horizon_observability_gramian().

Returns:

ObservabilityScores – The per-output and (optional) per-mode observability energies.

Raises:

ValueError – If the system has no C matrix, horizon is non-positive, the system is unstable with no finite horizon, measurement_noise is the wrong shape, or a mode-state index is out of range.

Return type:

ObservabilityScores

recommend_sensors(system, *, n_sensors, candidates=None, measurement_noise=None, horizon=None)[source]

Recommend sensors that best observe a system, by greedy D-optimal selection.

Greedily adds, from the candidate output channels, the one that most increases the log-determinant of the observability Gramian built from the selected rows of C. D-optimality rewards covering all observable directions, so it prefers complementary sensors over redundant ones. Intended for the reduced model (see modal_reduction()), where the candidates are the instrumentable channels and the modes are those that drive the load of interest.

Parameters:
  • system (StateSpace) – A system with a C matrix. Must be asymptotically stable unless a finite horizon is given.

  • n_sensors (int) – Number of sensors to recommend (capped at the number of candidates).

  • candidates (Sequence[int] or None, optional) – Output row indices that may be instrumented. None considers every output.

  • measurement_noise (npt.NDArray[np.float64] or None, optional) – Full (n_outputs, n_outputs) measurement-noise covariance R. When given, each selected sensor is weighted by its inverse noise (the marginal block R[rows, rows]), so a noisy channel ranks below a calibrated one instead of scoring identically. None weights every channel equally.

  • horizon (float or None, optional) – Finite sensing window T. None uses the infinite-horizon Gramian (which requires a stable system); a finite horizon also admits marginal/augmented models whose infinite-horizon Gramian does not exist.

Returns:

SensorPlacement – The selected sensors (in order), their names, and the observability metric after each selection.

Raises:

ValueError – If the system has no C matrix, is unstable with no finite horizon, n_sensors is not positive, a candidate index is out of range, or measurement_noise is not a symmetric positive-definite (n_outputs,     n_outputs) matrix.

Return type:

SensorPlacement

reduced_estimator(reduction, *, sensor_outputs, dt, confidences=None, process_noise=0.0001, measurement_noise=0.01, base_variance=1.0, uncertainty_scale=10.0)[source]

Assemble a UQ-initialized Kalman model on a reduced modal system.

Builds a discrete Kalman model whose measurement matrix is the chosen sensor rows of the reduced system and whose initial covariance is inflated by each retained mode’s confidence (a less-trusted mode starts with larger variance, two states per mode). This is the reduced load estimator: reduction + per-mode uncertainty + Kalman export, in one object ready for a filter.

Note

process_noise here is a hand-set discrete Q diagonal (a sensible starting placeholder), not a Van Loan discretization of a continuous spectral density: unlike the steady-state assessment path (discretize_process_noise()), it is used directly as Q_d, so it is not scaled to dt or coupled through A. For a model-consistent Q (a modal spectral density discretized against A), build the model from discretize_process_noise() instead and tune from there.

Parameters:
  • reduction (ModalReduction) – A reduced modal model from modal_reduction().

  • sensor_outputs (Sequence[int]) – Output row indices of the reduced system used as measurements (the sensors; e.g. from recommend_sensors()).

  • dt (float) – Sample time for the discrete estimator.

  • confidences (Sequence[float] or None, optional) – Per-mode confidence in [0, 1] (one per retained mode); a lower value inflates that mode’s initial covariance. None trusts every mode (all ones).

  • process_noise (float, optional) – Passed to covariances_from_confidence().

  • measurement_noise (float, optional) – Passed to covariances_from_confidence().

  • base_variance (float, optional) – Passed to covariances_from_confidence().

  • uncertainty_scale (float, optional) – Passed to covariances_from_confidence().

Returns:

KalmanModel – The dimension-checked, discretized Kalman model.

Raises:

ValueError – If a sensor index is out of range, or confidences has the wrong length.

Return type:

KalmanModel

Online scheduled (LPV) estimator over a RomFamily.

build_rom_family() packages a turbine’s linearizations into one mode-tracked, phase-aligned reduced model per operating point – the static half of a linear-parameter-varying (LPV) estimator. This module is the runtime half: given a scheduling parameter (wind speed, rotor speed, …) it selects and interpolates the family into a single model at that operating point, and runs a Kalman filter over a record whose operating point drifts, carrying the state and covariance across the sweep.

Two things make the interpolation legitimate. The family’s realizations are tracked into a consistent state order (index k is the same physical mode everywhere), phase-aligned and scale-normalized across points, so a model’s state vector means the same thing at adjacent operating points and the matrices may be blended entry-by-entry; the running estimate’s state and covariance therefore stay valid as the schedule moves – no discontinuous re-basing at a switch. And each tracked mode carries a tracking MAC against its neighbour, so where the tracking is uncertain (a crossing or veering region, or an operating point outside the family’s range) the scheduler surfaces a low-confidence flag rather than interpolating two unrelated modes silently.

This is the “run the estimator over a wind-speed sweep” product layer the ROM family was built for.

class ScheduledFilterResult(states, covariances, predicted_states, predicted_covariances, nis, accepted, n_active, min_tracking_mac, extrapolated, state_names=<factory>)[source]

Bases: object

The trajectory of a scheduled (LPV) Kalman run, with per-step confidence.

Mirrors FilterResult and adds the scheduling diagnostics.

Parameters:
  • states (npt.NDArray[np.float64]) – Filtered state means (n_steps, n_states) and covariances (n_steps, n_states, n_states).

  • covariances (npt.NDArray[np.float64]) – Filtered state means (n_steps, n_states) and covariances (n_steps, n_states, n_states).

  • predicted_states (npt.NDArray[np.float64]) – A-priori means/covariances before each update.

  • predicted_covariances (npt.NDArray[np.float64]) – A-priori means/covariances before each update.

  • nis (npt.NDArray[np.float64]) – Normalized innovation squared per step (nan where no channel was available).

  • accepted (npt.NDArray[np.bool]) – Whether each step’s measurement was applied.

  • n_active (npt.NDArray[np.int]) – Number of channels used at each accepted step.

  • min_tracking_mac (npt.NDArray[np.float64]) – The scheduled model’s lowest tracking MAC at each step (per-step confidence).

  • extrapolated (npt.NDArray[np.bool]) – Whether each step’s operating point fell outside the family range.

  • state_names (list[str]) – Names of the states.

states: npt.NDArray[np.float64]
covariances: npt.NDArray[np.float64]
predicted_states: npt.NDArray[np.float64]
predicted_covariances: npt.NDArray[np.float64]
nis: npt.NDArray[np.float64]
accepted: npt.NDArray[np.bool_]
n_active: npt.NDArray[np.int_]
min_tracking_mac: npt.NDArray[np.float64]
extrapolated: npt.NDArray[np.bool_]
state_names: list[str]
class ScheduledModel(system, operating_point, min_tracking_mac, extrapolated)[source]

Bases: object

The family interpolated to a single operating point, with a confidence flag.

Parameters:
  • system (StateSpace) – The model at operating_point – a linear blend of the bracketing family members (or the nearest member when the point is outside the family range). Same time domain (continuous/discrete) and I/O layout as the family.

  • operating_point (float) – The scheduling-parameter value this model was interpolated to.

  • min_tracking_mac (float) – The lowest tracking MAC among the modes across the interpolated step – a measure of how trustworthy the blend is. A low value means a crossing/veering region where the modes may not correspond.

  • extrapolated (bool) – True when operating_point fell outside the family’s range and the model is the clamped nearest member (an extrapolation, not an interpolation).

system: StateSpace
operating_point: float
min_tracking_mac: float
extrapolated: bool
run_scheduled_filter(family, covariances, measurements, operating_points, *, dt, controls=None, available=None, gate=None, gate_probability=None, initial_state=None, initial_control=None, mac_warn_threshold=0.8)[source]

Run a Kalman filter whose model is scheduled on a drifting operating point.

At each step the family is interpolated to that step’s operating_points value (schedule_model()), discretized with dt, and used for one predict/update; the state and covariance carry across steps in the family’s consistent, phase-aligned basis, so a moving operating point needs no discontinuous re-basing. Control timing is zero-order hold, as in run_filter(): the prediction into step k is driven by controls[k-1] (the first by initial_control, defaulting to controls[0]), and controls[k] enters only the feedthrough of the measurement logged with it. The filter tuning covariances (Q, R, P0) is held constant across the sweep – only the plant model is scheduled.

Parameters:
  • family (RomFamily) – A continuous-time mode-tracked family (members are discretized per step).

  • covariances (CovarianceSet) – The discrete process/measurement/initial covariances (Q, R, P0).

  • measurements (npt.NDArray[np.float64]) – Measured outputs, shape (n_steps, n_outputs).

  • operating_points (npt.NDArray[np.float64]) – The scheduling-parameter value at each step, shape (n_steps,) – the time-varying operating condition (need not be monotonic).

  • dt (float) – Discretization step for the scheduled model.

  • controls (npt.NDArray[np.float64] or None, optional) – Control inputs, shape (n_steps, n_inputs); row k is the input logged with measurements[k]. None for an autonomous model.

  • available (npt.NDArray[np.bool] or None, optional) – Per-step channel mask (sensor dropout), shape (n_steps, n_outputs).

  • gate (float or None, optional) – Fixed chi-square NIS threshold for per-step outlier rejection.

  • gate_probability (float or None, optional) – Gate probability p; each step is gated at chi2.ppf(p, n_used) for the channels that reported. Mutually exclusive with gate.

  • initial_state (npt.NDArray[np.float64] or None, optional) – Initial state mean; defaults to zeros.

  • initial_control (npt.NDArray[np.float64] or None, optional) – The input held before measurements[0] (u_{-1}), which drives the first prediction; defaults to controls[0]. Requires controls.

  • mac_warn_threshold (float, optional) – Tracking-MAC level below which a step is counted low-confidence (default 0.8).

Returns:

ScheduledFilterResult – The filtered trajectory plus the per-step scheduling confidence.

Raises:

ValueError – If the family is empty or discrete, dt is not positive, measurements is not (n_steps, n_outputs), or operating_points / controls / available lengths disagree with the step count, initial_control is given without controls, or both gate and gate_probability are set.

Return type:

ScheduledFilterResult

schedule_model(family, operating_point, *, mac_warn_threshold=0.8)[source]

Interpolate a ROM family to a single operating point.

Brackets operating_point between two family members and linearly blends their A/B/C/D matrices (clamping to the nearest member outside the range). Because the family is mode-tracked and phase-aligned, the blended model is a valid state-space realization in the same state basis. The returned ScheduledModel.min_tracking_mac reports the lowest tracking MAC across the interpolated step; a value below mac_warn_threshold (or an extrapolation) logs a warning, because there the interpolation may mix modes that do not correspond.

Parameters:
  • family (RomFamily) – The mode-tracked operating-point family from build_rom_family().

  • operating_point (float) – The scheduling-parameter value to interpolate to.

  • mac_warn_threshold (float, optional) – Tracking-MAC level below which a low-confidence warning is logged (default 0.8).

Returns:

ScheduledModel – The interpolated model and its confidence flags.

Raises:

ValueError – If the family is empty, or operating_point / mac_warn_threshold is not finite.

Return type:

ScheduledModel

Steady-state estimator assessment: model-conditional reconstruction-error bounds. Given the discrete model, the process-noise covariance and the measurement-noise covariance, steady_state_estimator() solves the discrete algebraic Riccati equation for the steady-state Kalman filter and returns the gain, the predicted and filtered error covariances, and per-state and per-load error standard deviations. assess_load() adds a pass or flag result against a tolerance for each load.

Each reported standard deviation is exact for the supplied model and noise covariances. It is not a guarantee about the physical turbine: it holds only to the extent that the linearized model is accurate at the operating point, the supplied covariances are the true ones, the noises are zero-mean, white, Gaussian and mutually independent, and the sensors are calibrated, unbiased and make the pair detectable. Validate against nonlinear time-domain simulation or measured data before relying on it in operation.

class EstimatorAssessment(gain, error_covariance, predicted_covariance, state_std)[source]

Bases: object

The steady-state Kalman filter and its predicted and filtered error covariances.

Parameters:
  • gain (npt.NDArray[np.float64]) – The steady-state Kalman gain K, shape (n_states, n_outputs).

  • error_covariance (npt.NDArray[np.float64]) – The a-posteriori (filtered) steady-state estimation-error covariance P+, shape (n_states, n_states) — the accuracy after each measurement update.

  • predicted_covariance (npt.NDArray[np.float64]) – The a-priori (predicted) steady-state error covariance P- (the DARE solution), shape (n_states, n_states).

  • state_std (npt.NDArray[np.float64]) – Per-state a-posteriori error standard deviation, sqrt(diag(P+)).

gain: npt.NDArray[np.float64]
error_covariance: npt.NDArray[np.float64]
predicted_covariance: npt.NDArray[np.float64]
state_std: npt.NDArray[np.float64]
class LoadAssessment(load_name, error_std, tolerance, within_tolerance)[source]

Bases: object

The reconstruction-error bound for one target load.

Parameters:
  • load_name (str) – The load’s name (or "load[i]").

  • error_std (float) – The steady-state filtered (a-posteriori) reconstruction-error standard deviation, sqrt(L P+ L^T) — the load is reconstructable to ±error_std.

  • tolerance (float or None) – The acceptance tolerance the load was checked against (None if only reported, not checked).

  • within_tolerance (bool) – Whether the load met an explicit tolerance: True only when a tolerance was given and error_std <= tolerance. False when the error exceeds the tolerance, and also False when no tolerance was given (the error is reported but not accepted). Use tolerance is None to distinguish “reported, unchecked” from “checked and failed”.

load_name: str
error_std: float
tolerance: float | None
within_tolerance: bool
steady_state_estimator(system, *, process_noise, measurement_noise)[source]

Solve the steady-state Kalman filter and its predicted/filtered covariances.

For the discrete model \(\mathbf x_{k+1}=\mathbf A_d\mathbf x_k+\mathbf w_k\), \(\mathbf y_k=\mathbf C\mathbf x_k+\mathbf v_k\) with \(\mathbf w\sim\mathcal N(0,\mathbf Q_d)\), \(\mathbf v\sim\mathcal N(0,\mathbf R)\), solves the discrete algebraic Riccati equation for the predicted error covariance \(\mathbf P^-=\mathbf A_d\mathbf P^+\mathbf A_d^{\mathsf T}+\mathbf Q_d\), then forms the gain \(\mathbf K=\mathbf P^-\mathbf C^{\mathsf T} (\mathbf C\mathbf P^-\mathbf C^{\mathsf T}+\mathbf R)^{-1}\) and the filtered covariance \(\mathbf P^+=(\mathbf I-\mathbf K\mathbf C)\mathbf P^-\).

Parameters:
  • system (StateSpace) – A discrete-time system (dt set) with a C matrix.

  • process_noise (npt.NDArray[np.float64]) – The discrete process-noise covariance Q_d, shape (n_states, n_states), symmetric positive-semidefinite.

  • measurement_noise (npt.NDArray[np.float64]) – The measurement-noise covariance R, shape (n_outputs, n_outputs), symmetric positive-definite.

Returns:

EstimatorAssessment – The steady Kalman gain and the steady-state error covariances.

Raises:

ValueError – If the system is continuous, has no C matrix, the covariances have the wrong shape or are not valid (Q_d PSD, R PD), or the filter has no steady-state solution ((A, C) not detectable from the sensors, or (A, Q^1/2) not stabilisable – a marginal mode with no process noise).

Return type:

EstimatorAssessment

assess_load(assessment, load_map, *, tolerance=None, names=None)[source]

Bound the reconstruction error of each target load from the assessment.

For each load row \(\mathbf L_i\) (a linear functional of the state, e.g. a row of C for a named output or the recovery map of augment_with_disturbance()), reports the steady-state filtered (a-posteriori) error standard deviation \(\sigma_i=\sqrt{\mathbf L_i\mathbf P^+\mathbf L_i^{\mathsf T}}\) and, if a tolerance is given, whether the load is reconstructable to within it.

Parameters:
  • assessment (EstimatorAssessment) – An assessment from steady_state_estimator().

  • load_map (npt.NDArray[np.float64]) – The load functionals L, shape (n_loads, n_states); z = L x.

  • tolerance (float or Sequence[float] or None, optional) – Acceptance tolerance on the error standard deviation; a scalar applies to every load, or one value per load. None reports the error without checking.

  • names (Sequence[str] or None, optional) – Per-load names (length n_loads). None uses "load[i]".

Returns:

list[LoadAssessment] – One assessment per load row.

Raises:

ValueError – If load_map does not match the state dimension or is non-finite, or tolerance/names have the wrong length.

Return type:

list[LoadAssessment]

Visualization

Visualization — Campbell plots, mode shape plots, damping plots.

Every figure follows one style, the plotting conventions of the Marine Structural Mechanics and Integrity Lab (SMI Lab), Inha University: Matplotlib figures are drawn inside plot_style(), Plotly figures with the registered "vane" template. Static figures take a size= ("single", "double" or a width in cm), and save_figure() writes them for publication.

Campbell-diagram plotting, interactive (Plotly) and static (Matplotlib).

Renders a CampbellDiagram: one line per tracked mode (natural frequency versus the operating parameter), the nP excitation lines, and markers at the detected resonance crossings. plot_campbell() returns an interactive Plotly figure, plot_campbell_static() a Matplotlib figure for publication, and plot_campbell_panels() a two-panel Matplotlib figure with the frequencies and the damping ratios side by side.

The figures follow the lab’s Campbell conventions. Each track is coloured by its mode family (blades green, tower black, platform red, drivetrain magenta, other modes cyan) and the line style cycles within a family; the marker shows the multi-blade type. The nP lines (“Blade Passing”) are thin and blue, tagged inline (3P) and kept out of the legend. An optional operating window is shaded grey and marked with a labelled double arrow. The static figures name each mode at its line end, in a column at the right edge with a collision-free vertical placement, and keep the legend to a compact key of the families and overlays. VANE adds resonance crossings (filled crosses coloured by severity), confidence (reduced opacity), ambiguity (open diamonds) and heavily damped tracks (dotted grey), each explained in the key. The interactive figure has a legend grouped by family and a hover label on every point (mode, operating point, frequency, damping, confidence).

HEAVILY_DAMPED_COLOR = '#7f7f7f'

Line colour of the heavily damped (near-critical) tracks.

SEVERITY_COLORS: dict[ResonanceSeverity, str] = {ResonanceSeverity.HIGH: '#d90000', ResonanceSeverity.LOW: '#8c8c8c', ResonanceSeverity.MEDIUM: '#ff8000'}

red, orange and grey.

Type:

Marker colour for each resonance severity (more damped, less alarming)

plot_campbell(diagram, *, harmonics=(1, 3, 6, 9), show_excitation=True, show_resonances=True, show_confidence=True, show_ambiguity=True, show_heavily_damped=True, operating_range=None, show_operating_range=True, figure=None, template=None, title=None)[source]

Plot a Campbell diagram as an interactive figure.

Parameters:
  • diagram (CampbellDiagram) – The diagram to plot.

  • harmonics (Sequence[int], optional) – Excitation harmonics for the nP lines and resonance markers (only used for rotor-speed diagrams).

  • show_excitation (bool, optional) – Draw the nP excitation lines, tagged inline 1P, 3P, … (rotor-speed diagrams only). The legend has one “Blade Passing” entry for them.

  • show_resonances (bool, optional) – Mark detected resonance crossings, coloured by severity (rotor-speed diagrams only).

  • show_confidence (bool, optional) – Scale each track’s line opacity by its identification confidence.

  • show_ambiguity (bool, optional) – Flag ambiguous tracks (open-diamond markers and an annotated legend name) and the ambiguous points of otherwise clear tracks (open-diamond markers at those points). The hover names each point’s own label, with its alternative when ambiguous.

  • show_heavily_damped (bool, optional) – Draw the diagram’s heavily damped tracks: dotted grey lines with open markers, named “(heavily damped)”, whose hover adds the damped frequency. Default True; False leaves them out.

  • operating_range (tuple[float, float] or None, optional) – The (min, max) operating window of the parameter. Resonance detection is restricted to it, and it is shaded grey (see show_operating_range).

  • show_operating_range (bool, optional) – Shade the operating window and name it (“Operating Speed Range” or “Operating Wind Range”). Default True; it has no effect without operating_range.

  • figure (plotly.graph_objects.Figure or None, optional) – Draw into this figure instead of creating a new one (for composing subplots or overlays). The figure is returned.

  • template (str, plotly.graph_objects.layout.Template or None, optional) – Plotly layout template to apply. None (the default) applies the VANE template "vane"; pass another name (for example "plotly_white") or a template object to override it.

  • title (str or None, optional) – Figure title. None (the default) draws none, leaving the caption to the document.

Returns:

plotly.graph_objects.Figure – The Campbell-diagram figure, with the legend grouped by mode family.

Return type:

go.Figure

plot_campbell_panels(diagram, *, harmonics=(1, 3, 6, 9), layout='auto', as_percent=True, show_excitation=True, show_resonances=True, show_confidence=True, show_ambiguity=True, show_heavily_damped=True, show_stability_bands=True, caution_damping=0.01, operating_range=None, show_operating_range=True, style='vane', size=None, direct_labels=True)[source]

Plot frequencies and damping ratios of a Campbell diagram as two panels.

Panel (a) is the Campbell diagram (plot_campbell_static()) and panel (b) the damping ratio (vane.viz.damping_plot.plot_damping_static()) against the same operating parameter. The panels share the operating-parameter axis and its label and carry (a) and (b) outside their top-left corners; one compact key in panel (a) explains the encoding of both. The mode names stand at the right edge: of panel (b) in the "row" layout and of both panels in the "column" layout.

Parameters:
  • diagram (CampbellDiagram) – The diagram to plot.

  • harmonics (Sequence[int], optional) – Excitation harmonics for panel (a).

  • layout ({"auto", "row", "column"}, optional) – "row" places the panels side by side; "column" stacks them, frequency on top. "auto" (the default) uses "row" for figures at least 12 cm wide and "column" for narrower ones.

  • as_percent (bool, optional) – Plot damping in percent (the default) rather than as a fraction.

  • show_excitation (bool, optional) – As for plot_campbell_static().

  • show_resonances (bool, optional) – As for plot_campbell_static().

  • show_confidence (bool, optional) – As for plot_campbell_static().

  • show_ambiguity (bool, optional) – As for plot_campbell_static().

  • show_heavily_damped (bool, optional) – Draw the heavily damped tracks in panel (a).

  • show_stability_bands (bool, optional) – Shade the unstable and caution bands in panel (b).

  • caution_damping (float, optional) – Upper edge of the caution band as a damping fraction (default 0.01).

  • operating_range (tuple[float, float] or None, optional) – The operating window: restricts resonance detection and is shaded in both panels (see show_operating_range).

  • show_operating_range (bool, optional) – Shade the operating window and mark it in panel (a). Default True.

  • style (str, dict, pathlib.Path or None, optional) – Matplotlib style, as for plot_campbell_static().

  • size (str, float, FigureSize or None, optional) – Figure size and fonts; None uses "double" (17 cm wide), with a height suited to the layout.

  • direct_labels (bool, optional) – Name the modes at their line ends (the default); False draws a per-track legend instead.

Returns:

matplotlib.figure.Figure – The two-panel figure.

Raises:

ValueError – If layout is not "auto", "row" or "column", or caution_damping is negative.

Return type:

Figure

plot_campbell_static(diagram, *, harmonics=(1, 3, 6, 9), show_excitation=True, show_resonances=True, show_confidence=True, show_ambiguity=True, show_heavily_damped=True, operating_range=None, show_operating_range=True, ax=None, style='vane', size=None, direct_labels=True, title=None)[source]

Plot a Campbell diagram as a static Matplotlib figure for publication.

The static counterpart of plot_campbell(), with the same encoding. Each mode is named at its line end, in a column just right of the axes (inside the right edge when drawing into the caller’s axes), moved vertically only as far as needed to keep the names apart, with a thin leader back to the line. The frequency axis ends 30 % above the highest mode, so the steep nP lines run off the top; they are tagged inline at staggered heights. A compact key inside the axes explains the families, “Blade Passing” and any overlays used.

Parameters:
  • diagram (CampbellDiagram) – The diagram to plot.

  • harmonics (Sequence[int], optional) – Excitation harmonics for the nP lines and resonance markers (only used for rotor-speed diagrams).

  • show_excitation (bool, optional) – Draw the nP excitation lines (rotor-speed diagrams only).

  • show_resonances (bool, optional) – Mark detected resonance crossings, coloured by severity (rotor-speed diagrams only).

  • show_confidence (bool, optional) – Scale each track’s line opacity by its identification confidence.

  • show_ambiguity (bool, optional) – Draw ambiguous tracks, and the ambiguous points of clear tracks, with open diamonds, and mark ambiguous tracks with ? in their labels.

  • show_heavily_damped (bool, optional) – Draw the heavily damped tracks as dotted grey lines with open markers.

  • operating_range (tuple[float, float] or None, optional) – The (min, max) operating window: restricts resonance detection and is shaded grey with a labelled double arrow (see show_operating_range).

  • show_operating_range (bool, optional) – Shade and mark the operating window. Default True.

  • ax (matplotlib.axes.Axes or None, optional) – Draw into these axes instead of creating a new figure. The axes keep their own appearance (use vane.viz.apply_style() to restyle them); the parent figure is returned.

  • style (str, dict, pathlib.Path or None, optional) – Matplotlib style in effect while drawing. "vane" (the default) is the VANE style (vane.viz.plot_style()); None keeps the current rcParams (for example a plt.style.context opened by the caller); any other value is passed to matplotlib.style.context().

  • size (str, float, FigureSize or None, optional) – Figure size and fonts (see vane.viz.figure_size()): "single" (8.5 cm wide), "double" (17 cm) or a width in cm. None uses "double" for a new figure. In a figure narrower than 12 cm the direct labels use short mode names ("1st flap reg.").

  • direct_labels (bool, optional) – Name the modes at their line ends with a compact key (the default). False draws a legend with one entry per track instead, grouped and titled by family, to the right of the axes.

  • title (str or None, optional) – Axes title; None (the default) draws none.

Returns:

matplotlib.figure.Figure – The Campbell-diagram figure.

Return type:

Figure

Damping-ratio plotting against the operating parameter, with stability signals.

Renders the damping ratio of each tracked mode as a function of the operating parameter (rotor speed or wind speed), the companion to the Campbell diagram for assessing stability margins across the operating envelope. plot_damping() returns an interactive Plotly figure and plot_damping_static() a Matplotlib figure for publication.

The plot makes stability legible at a glance: the negative-damping region is shaded as unstable and a thin band above zero as a low-margin caution zone, and any point that crosses into negative damping is flagged. Tracks use the same styling as the Campbell diagram: colour, dash and marker by mode family (vane.viz.mode_line_style()), line opacity scaled by identification confidence, and open-diamond markers on ambiguous tracks and points. The static figure names each track at its line end and explains the encoding in a compact key; the interactive one has a legend grouped by family.

plot_damping(diagram, *, as_percent=True, show_stability_bands=True, show_confidence=True, caution_damping=0.01, template=None, title=None)[source]

Plot damping ratio versus the operating parameter, with stability signals.

Parameters:
  • diagram (CampbellDiagram) – The diagram whose tracks are plotted.

  • as_percent (bool, optional) – Plot damping as a percentage (the default) rather than a fraction.

  • show_stability_bands (bool, optional) – Shade the unstable (damping < 0) region and the low-margin caution band above it. Default True.

  • show_confidence (bool, optional) – Scale each track’s line opacity by its identification confidence and mark an ambiguous track (and the ambiguous points of a clear track) with open diamonds. Default True.

  • caution_damping (float, optional) – Upper edge of the caution band as a damping fraction (default 0.01, i.e. 1%); modes below it are flagged as lightly damped.

  • template (str, plotly.graph_objects.layout.Template or None, optional) – Plotly layout template to apply. None (the default) applies the VANE template "vane".

  • title (str or None, optional) – Figure title. None (the default) draws a title only as a warning, naming the modes that go unstable.

Returns:

plotly.graph_objects.Figure – The damping figure: one line per tracked mode (styled by mode family, with the legend grouped by family), unstable points flagged, and, when enabled, the unstable and caution stability bands.

Raises:

ValueError – If caution_damping is negative.

Return type:

go.Figure

plot_damping_static(diagram, *, as_percent=True, show_stability_bands=True, show_confidence=True, caution_damping=0.01, ax=None, style='vane', size=None, direct_labels=True, title=None)[source]

Plot damping ratio versus the operating parameter as a Matplotlib figure.

The static counterpart of plot_damping(): the same tracks, family styling, stability bands and instability flags (red filled crosses). Each mode is named at its line end; a compact key inside the axes explains the encoding.

Parameters:
  • diagram (CampbellDiagram) – The diagram whose tracks are plotted.

  • as_percent (bool, optional) – Plot damping as a percentage (the default) rather than a fraction.

  • show_stability_bands (bool, optional) – Shade the unstable (damping < 0) region and the low-margin caution band above it. Default True.

  • show_confidence (bool, optional) – Scale each track’s line opacity by its identification confidence and mark ambiguous tracks and points with open diamonds. Default True.

  • caution_damping (float, optional) – Upper edge of the caution band as a damping fraction (default 0.01).

  • ax (matplotlib.axes.Axes or None, optional) – Draw into these axes instead of creating a new figure; the parent figure is returned.

  • style (str, dict, pathlib.Path or None, optional) – Matplotlib style in effect while drawing: "vane" (the default), None for the current rcParams, or any value accepted by matplotlib.style.context().

  • size (str, float, FigureSize or None, optional) – Figure size and fonts (see vane.viz.figure_size()); None uses "double" for a new figure.

  • direct_labels (bool, optional) – Name the tracks at their line ends with a compact key (the default); False draws a per-track legend grouped by family instead.

  • title (str or None, optional) – Axes title; None (the default) draws none.

Returns:

matplotlib.figure.Figure – The damping figure.

Raises:

ValueError – If caution_damping is negative.

Return type:

Figure

Static mode-shape (participation) plotting.

Since OpenFAST mode shapes are expressed over modal degrees of freedom rather than spatial coordinates, a mode is visualized as a horizontal bar chart of the signed participation of its most active degrees of freedom: bar length is the relative contribution and the sign encodes the in-/out-of-phase relationship to the dominant degree of freedom. The plot is therefore modal participation, not spatial geometry — hence the primary name plot_mode_participation(). Following the lab’s mode-shape conventions, the mode is named in the legend with its natural frequency, a thin grey line marks zero, and short_names=True names the degrees of freedom briefly ("Tower FA1", "Flap 1 coll.").

plot_mode_participation(solution, mode_index, *, top_n=10, ax=None, color='#0000d9', style='vane', size=None, short_names=False, title=None)[source]

Plot the signed participation of a mode’s most active degrees of freedom.

Parameters:
  • solution (ModalSolution) – A solution whose dof_descriptions are populated.

  • mode_index (int) – Index of the mode to plot.

  • top_n (int, optional) – Number of highest-participation degrees of freedom to show.

  • ax (matplotlib.axes.Axes or None, optional) – Draw into these axes instead of creating a new figure (for composing a multi-panel report). The axes keep their own appearance; the parent figure is returned.

  • color (str, optional) – Bar colour; blue, the third standard line colour, by default.

  • style (str, dict, pathlib.Path or None, optional) – Matplotlib style in effect while drawing. "vane" (the default) is the VANE style (vane.viz.plot_style()); None keeps the current rcParams; any other value is passed to matplotlib.style.context().

  • size (str, float, FigureSize or None, optional) – Figure size and fonts (see vane.viz.figure_size()); None uses "single" for a new figure.

  • short_names (bool, optional) – Label the bars with short DOF names ("Tower FA1", "Flap 1 coll.") instead of the full state descriptions (the default). Short names suit a printed figure.

  • title (str or None, optional) – Axes title; None (the default) draws none. The legend names the mode and its natural frequency.

Returns:

matplotlib.figure.Figure – The bar-chart figure.

Raises:

ValueError – If the solution has no DOF descriptions, mode_index is out of range, or top_n is not positive.

Return type:

Figure

plot_mode_shape(solution, mode_index, *, top_n=10, ax=None, color='#0000d9', style='vane', size=None, short_names=False, title=None)[source]

Compatibility alias for plot_mode_participation().

The plot shows signed modal participation, not spatial mode-shape geometry; plot_mode_participation() is the accurate name. This alias forwards all arguments unchanged.

Parameters:
Return type:

Figure

Three-dimensional complex mode-shape plotting.

A complex mode shape carries both magnitude and phase per degree of freedom. This view renders the most active degrees of freedom as phasors in three dimensions: the horizontal axis orders the degrees of freedom by participation while the other two axes are the real and imaginary parts of each component, so the spread out of the real plane shows how non-proportional (complex) the mode is. plot_mode_3d() is interactive (Plotly) and plot_mode_3d_static() static (Matplotlib).

plot_mode_3d(solution, mode_index, *, top_n=12, template=None, title=None)[source]

Plot a mode’s most active degrees of freedom as 3-D complex phasors.

Parameters:
  • solution (ModalSolution) – A solution whose dof_descriptions are populated.

  • mode_index (int) – Index of the mode to plot.

  • top_n (int, optional) – Number of highest-magnitude degrees of freedom to show.

  • template (str, plotly.graph_objects.layout.Template or None, optional) – Plotly layout template to apply. None (the default) applies the VANE template "vane".

  • title (str or None, optional) – Figure title. None (the default) names the mode and its natural frequency; "" draws none.

Returns:

plotly.graph_objects.Figure – The 3-D phasor figure.

Raises:

ValueError – If the solution has no DOF descriptions, mode_index is out of range, or top_n is not positive.

Return type:

go.Figure

plot_mode_3d_static(solution, mode_index, *, top_n=6, style='vane', size=None, title=None)[source]

Plot a mode’s most active degrees of freedom as static 3-D complex phasors.

Each degree of freedom is a stem from the axis of DOF ranks to its complex component (real and imaginary parts), in its own colour; the legend names the DOFs and its title the mode and its natural frequency.

Parameters:
  • solution (ModalSolution) – A solution whose dof_descriptions are populated.

  • mode_index (int) – Index of the mode to plot.

  • top_n (int, optional) – Number of highest-magnitude degrees of freedom to show (default 6).

  • style (str, dict, pathlib.Path or None, optional) – Matplotlib style in effect while drawing ("vane" by default).

  • size (str, float, FigureSize or None, optional) – Figure size and fonts; None uses "single".

  • title (str or None, optional) – Axes title; None (the default) draws none.

Returns:

matplotlib.figure.Figure – The 3-D figure.

Raises:

ValueError – If the solution has no DOF descriptions, mode_index is out of range, or top_n is not positive.

Return type:

Figure

Complex mode-shape composition views.

A mode shape is complex, and after the multi-blade-coordinate transform the blade degrees of freedom split into collective, cosine, and sine coordinates. These views expose that structure directly, without needing turbine geometry: a phasor wheel plots each dominant DOF as a vector (length = participation magnitude, angle = phase), its animated variant rotates the phasors through one modal period so the relative phasing reads as motion, and the blade-component bar chart shows how a mode’s participation divides among the collective/cosine/sine multi-blade coordinates. The phasor wheel and the bar chart also have static (Matplotlib) forms for publication.

plot_blade_components(solution, mode_index, *, template=None, title=None)[source]

Plot how a mode’s participation divides among the multi-blade coordinates.

After the MBC transform each blade degree of freedom is a collective, cosine, or sine coordinate. This bar chart shows the participation magnitude of each such DOF, coloured by coordinate: a collective-dominated bar set marks a collective mode, while comparable cosine and sine sets mark a cyclic (whirling) mode.

Parameters:
  • solution (ModalSolution) – A solution carrying dof_mbc_coordinates (i.e. produced from an MBC- transformed model) and dof_descriptions.

  • mode_index (int) – Index of the mode to plot.

  • template (str, plotly.graph_objects.layout.Template or None, optional) – Plotly layout template to apply. None (the default) applies the VANE template "vane".

  • title (str or None, optional) – Figure title. None (the default) names the mode and its natural frequency; "" draws none.

Returns:

plotly.graph_objects.Figure – One bar per multi-blade DOF, grouped and coloured by coordinate.

Raises:

ValueError – If the solution has no DOF descriptions, mode_index is out of range, or the solution carries no multi-blade coordinates.

Return type:

go.Figure

plot_blade_components_static(solution, mode_index, *, style='vane', size=None, title=None)[source]

Plot how a mode’s participation divides among the multi-blade coordinates.

The static counterpart of plot_blade_components(): one bar per multi-blade DOF, coloured by coordinate (collective black, cosine red, sine blue), with the legend titled by the mode and its natural frequency.

Parameters:
  • solution (ModalSolution) – A solution carrying dof_mbc_coordinates and dof_descriptions.

  • mode_index (int) – Index of the mode to plot.

  • style (str, dict, pathlib.Path or None, optional) – Matplotlib style in effect while drawing ("vane" by default).

  • size (str, float, FigureSize or None, optional) – Figure size and fonts; None uses "single".

  • title (str or None, optional) – Axes title; None (the default) draws none.

Returns:

matplotlib.figure.Figure – The bar chart.

Raises:

ValueError – If the solution has no DOF descriptions, mode_index is out of range, or the solution carries no multi-blade coordinates.

Return type:

Figure

plot_mode_phasor(solution, mode_index, *, top_n=10, template=None, title=None)[source]

Plot a mode’s dominant DOFs as phasors on a polar wheel.

Each degree of freedom is drawn as a vector whose length is its participation magnitude (normalized to the most active DOF) and whose angle is its phase. Two DOFs that are in phase point the same way; a 180-degree spread is anti-phase; a spread of intermediate angles indicates a travelling (whirling) mode.

Parameters:
  • solution (ModalSolution) – A solution whose dof_descriptions are populated.

  • mode_index (int) – Index of the mode to plot.

  • top_n (int, optional) – Number of highest-participation degrees of freedom to show.

  • template (str, plotly.graph_objects.layout.Template or None, optional) – Plotly layout template to apply. None (the default) applies the VANE template "vane".

  • title (str or None, optional) – Figure title. None (the default) names the mode and its natural frequency; "" draws none.

Returns:

plotly.graph_objects.Figure – The polar phasor figure.

Raises:

ValueError – If the solution has no DOF descriptions, mode_index is out of range, or top_n is not positive.

Return type:

go.Figure

plot_mode_phasor_animation(solution, mode_index, *, top_n=10, n_frames=24, template=None, title=None)[source]

Animate a mode’s phasors through one modal period.

The phasors rotate rigidly (all DOFs share the modal frequency), so the animation makes the relative phasing visible as motion: an in-phase mode sweeps as a rigid fan, while a whirling mode shows a rotating spread.

Parameters:
  • solution (ModalSolution) – A solution whose dof_descriptions are populated.

  • mode_index (int) – Index of the mode to plot.

  • top_n (int, optional) – Number of highest-participation degrees of freedom to show.

  • n_frames (int, optional) – Number of animation frames over one period (must be positive).

  • template (str, plotly.graph_objects.layout.Template or None, optional) – Plotly layout template to apply. None (the default) applies the VANE template "vane".

  • title (str or None, optional) – Figure title. None (the default) names the mode and its natural frequency; "" draws none.

Returns:

plotly.graph_objects.Figure – The animated polar phasor figure, with a play button.

Raises:

ValueError – If the solution has no DOF descriptions, mode_index is out of range, or top_n or n_frames is not positive.

Return type:

go.Figure

plot_mode_phasor_static(solution, mode_index, *, top_n=6, style='vane', size=None, title=None)[source]

Plot a mode’s dominant DOFs as phasors on a static polar wheel.

Each degree of freedom is an arrow from the centre, its length the normalized participation magnitude and its angle the phase relative to the dominant DOF (counter-clockwise, in degrees). Each arrow has its own colour (the standard line colours, in order of participation), and the legend names the DOFs with their magnitudes.

Parameters:
  • solution (ModalSolution) – A solution whose dof_descriptions are populated.

  • mode_index (int) – Index of the mode to plot.

  • top_n (int, optional) – Number of highest-participation degrees of freedom to show (default 6).

  • style (str, dict, pathlib.Path or None, optional) – Matplotlib style in effect while drawing ("vane" by default).

  • size (str, float, FigureSize or None, optional) – Figure size and fonts; None uses "single".

  • title (str or None, optional) – Axes title; None (the default) draws none (the legend title names the mode and its frequency).

Returns:

matplotlib.figure.Figure – The polar figure.

Raises:

ValueError – If the solution has no DOF descriptions, mode_index is out of range, or top_n is not positive.

Return type:

Figure

Physical full-system mode-shape visualization of a wind-turbine.

The other mode-shape views in vane.viz are deliberately abstract — phasor wheels and participation bars — because an OpenFAST mode shape is expressed over modal degrees of freedom, not spatial coordinates. This module closes the gap to a picture an engineer can read at a glance: it draws the whole structure — floating platform, mooring lines, tower, nacelle, and the three blades — and deforms it with the actual complex mode shape, then animates it through one modal cycle.

How the modal coordinates become motion

The reference geometry (vane.config.system_config) supplies undeformed node positions and, for the tower and blades, a normalized arc length per node. Each modal degree of freedom is then mapped to a physical displacement field:

  • Platform surge/sway/heave/roll/pitch/yaw move the entire structure as a rigid body about the platform reference point.

  • Tower fore-aft and side-side bending modes add a lateral deflection along the tower via clamped-free (cantilever) bending shape functions; the resulting tower-top translation and slope carry the rotor-nacelle assembly.

  • Blade flap and edge modes deflect each blade along its flapwise and edgewise directions. After the multi-blade-coordinate transform the blade modes are collective/cosine/sine; the inverse transform recovers the per-blade amplitude at each blade’s azimuth.

  • Mooring lines are dragged kinematically by the fairlead motion, fading to the fixed anchor.

Honesty

The hull and mooring outlines are a schematic (see vane.config.system_config); the bending shape functions are the standard Euler-Bernoulli cantilever modes, not the turbine’s exact polynomial shapes. What is faithful is the relative motion across the structure, which is set by the computed complex mode shape. The deflection magnitude is exaggerated by a scale factor for visibility (universal practice for mode-shape plots) — it is not a physical amplitude.

class FowtModeDeformation(geometry, displacements, scale, mode_index, frequency_hz, damping_ratio)[source]

Bases: object

A mode’s scaled complex displacement field over a reference geometry.

Parameters:
  • geometry (FowtGeometry) – The reference geometry that was deformed.

  • displacements (list[npt.NDArray[np.complex128]]) – Per-member complex displacement fields, aligned with geometry.members; element m has shape (n_nodes_m, 3) and is already multiplied by the visualization scale. The instantaneous real displacement at modal phase \(\theta\) is (displacements[m] * exp(1j * theta)).real.

  • scale (float) – The length multiplier applied so the peak nodal deflection equals the requested fraction of the geometry’s characteristic length.

  • mode_index (int) – The mode that was visualized.

  • frequency_hz (float) – Natural frequency of the mode, for labelling.

  • damping_ratio (float) – Damping ratio of the mode, for labelling.

geometry: FowtGeometry
displacements: list[NDArray[complex128]]
scale: float
mode_index: int
frequency_hz: float
damping_ratio: float
peak_phase_rad()[source]

Return the modal phase of maximum total deflection.

A complex mode passes through different deformed configurations over a cycle; the instantaneous field at phase \(\theta\) is \(\operatorname{Re}(d\,e^{i\theta})\). This returns the \(\theta\) that maximizes the field’s total squared norm — the most legible single snapshot, and the displacement extreme for a real (in-phase) mode.

Returns:

float – The peak-deflection phase in radians, in [-pi/2, pi/2] (a half-angle from the field’s double-angle norm, so it is defined modulo pi).

Return type:

float

deformed_nodes(member_index, phase_rad)[source]

Return a member’s node positions at a modal phase.

Parameters:
  • member_index (int) – Index into geometry.members.

  • phase_rad (float) – Modal phase \(\theta\) in radians.

Returns:

npt.NDArray[np.float64] – The deformed node positions, shape (n_nodes, 3).

Return type:

NDArray[float64]

deform_mode(solution, mode_index, geometry=None, *, deflection_ratio=0.12, scale=1.0)[source]

Map a mode shape onto a reference geometry as a complex displacement field.

Parameters:
  • solution (ModalSolution) – A solution whose dof_descriptions are populated.

  • mode_index (int) – Index of the mode to visualize.

  • geometry (FowtGeometry or None, optional) – The reference geometry. When None a schematic is inferred from the solution’s degrees of freedom (see infer_geometry()).

  • deflection_ratio (float, optional) – Target peak nodal deflection as a fraction of the geometry’s characteristic length. The displacement field is scaled so its largest node reaches this.

  • scale (float, optional) – Additional user exaggeration multiplier applied on top of deflection_ratio.

Returns:

FowtModeDeformation – The scaled per-member complex displacement field.

Raises:

ValueError – If the solution has no DOF descriptions, mode_index is out of range, or deflection_ratio is not positive.

Return type:

FowtModeDeformation

infer_geometry(solution)[source]

Infer a schematic reference geometry from a solution’s degrees of freedom.

A floating (semi-submersible) schematic is chosen when platform degrees of freedom are present, otherwise a bottom-fixed monopile. The number of blades is taken from the highest blade index seen, defaulting to three. Principal dimensions use the preset defaults — pass an explicit FowtGeometry to match a specific turbine.

Parameters:

solution (ModalSolution) – The solution whose dof_descriptions are inspected.

Returns:

FowtGeometry – A default geometry matching the detected support structure.

Return type:

FowtGeometry

plot_mode_fowt(solution, mode_index, geometry=None, *, deflection_ratio=0.12, scale=1.0, phase_deg=None, template=None, title=None)[source]

Plot a mode on the full wind-turbine structure at a single phase.

The undeformed structure is drawn as a faint grey outline; the deformed structure — platform, mooring lines, tower, nacelle, and blades — is drawn in colour at the requested modal phase.

Parameters:
  • solution (ModalSolution) – A solution whose dof_descriptions are populated.

  • mode_index (int) – Index of the mode to plot.

  • geometry (FowtGeometry or None, optional) – Reference geometry; inferred from the solution when None.

  • deflection_ratio (float, optional) – Peak deflection as a fraction of the characteristic length.

  • scale (float, optional) – Extra exaggeration multiplier.

  • phase_deg (float or None, optional) – Modal phase in degrees at which to freeze the deformation. When None (the default) the phase of maximum total deflection is used, which is the displacement extreme for a real mode and the most legible snapshot for a complex one.

  • template (str, plotly.graph_objects.layout.Template or None, optional) – Plotly layout template to apply. None (the default) applies the VANE template "vane".

  • title (str or None, optional) – Figure title. None (the default) names the mode, its frequency and damping and the deflection scale; "" draws none.

Returns:

plotly.graph_objects.Figure – The 3-D mode-shape figure.

Raises:

ValueError – Propagated from deform_mode() for an empty, out-of-range, or non-positively-scaled request.

Return type:

go.Figure

plot_mode_fowt_animation(solution, mode_index, geometry=None, *, motion='cycle', deflection_ratio=0.12, scale=1.0, n_frames=36, template=None, title=None)[source]

Animate a mode on the full wind-turbine structure (Play, scrub, swing/orbit).

The faint undeformed outline and the still-water plane stay fixed while the coloured structure oscillates, so the relative phasing across platform, tower, and blades reads as physical motion. The figure carries a Play/Pause control and a frame slider to scrub or replay any instant; the deformation magnification is set manually with deflection_ratio x scale (an engineering deformation-scale factor, as in an FE post-processor).

Two motions are offered. "cycle" sweeps the full modal phase, so a whirling (complex) mode traces its true orbit. "swing" oscillates the real peak shape symmetrically about the undeformed state (cos amplitude from +peak through 0 to -peak) — the standing-wave animation a modal analyst expects for a single mode.

Parameters:
  • solution (ModalSolution) – A solution whose dof_descriptions are populated.

  • mode_index (int) – Index of the mode to animate.

  • geometry (FowtGeometry or None, optional) – Reference geometry; inferred from the solution when None.

  • motion ({"cycle", "swing"}, optional) – "cycle" sweeps the full complex phase (orbit); "swing" oscillates the real peak shape about the undeformed state. Default "cycle".

  • deflection_ratio (float, optional) – Peak deflection as a fraction of the characteristic length.

  • scale (float, optional) – Manual magnification multiplier on top of deflection_ratio.

  • n_frames (int, optional) – Number of frames over one period (must be positive).

  • template (str, plotly.graph_objects.layout.Template or None, optional) – Plotly layout template to apply. None (the default) applies the VANE template "vane".

  • title (str or None, optional) – Figure title. None (the default) names the mode, its frequency and damping and the deflection scale; "" draws none.

Returns:

plotly.graph_objects.Figure – The animated 3-D mode-shape figure with a Play/Pause control and a scrub slider.

Raises:

ValueError – If n_frames is not positive, or as propagated from deform_mode().

Return type:

go.Figure

plot_mode_fowt_static(solution, mode_index, geometry=None, *, deflection_ratio=0.12, scale=1.0, phase_deg=None, elevation_deg=15.0, azimuth_deg=-50.0, style='vane', size=None, title=None)[source]

Plot a mode on the full wind-turbine structure as a static 3-D figure.

The static counterpart of plot_mode_fowt(): the undeformed structure as a light grey outline and the deformed one coloured by member (tower black, blades green, platform red, as the mode families of the Campbell diagram; nacelle, hub and mooring grey), on equal axes, with a translucent still-water plane for offshore systems. The legend title notes that the deflection is exaggerated.

Parameters:
  • solution (ModalSolution) – A solution whose dof_descriptions are populated.

  • mode_index (int) – Index of the mode to plot.

  • geometry (FowtGeometry or None, optional) – Reference geometry; inferred from the solution when None.

  • deflection_ratio (float, optional) – Peak deflection as a fraction of the characteristic length.

  • scale (float, optional) – Extra exaggeration multiplier.

  • phase_deg (float or None, optional) – Modal phase in degrees; None uses the phase of maximum total deflection.

  • elevation_deg (float, optional) – Viewing angles of the 3-D axes, in degrees.

  • azimuth_deg (float, optional) – Viewing angles of the 3-D axes, in degrees.

  • style (str, dict, pathlib.Path or None, optional) – Matplotlib style in effect while drawing ("vane" by default).

  • size (str, float, FigureSize or None, optional) – Figure size and fonts; None uses "single", nearly square.

  • title (str or None, optional) – Axes title. None (the default) draws none; the legend title names the mode and its natural frequency.

Returns:

matplotlib.figure.Figure – The 3-D mode-shape figure.

Raises:

ValueError – Propagated from deform_mode() for an empty, out-of-range, or non-positively-scaled request.

Return type:

Figure

Visualization of the load quantity-of-interest map.

A QoiMap records, for each structural load, how strongly each identified mode contributes to it (the modal residue \(C_i \Phi\)). This view renders that as a load-versus-mode heatmap: rows are the load channels, columns are the modes (labelled by natural frequency), and the colour is the per-load normalized residue magnitude. It reads as a load-monitoring map — at a glance, which modes drive which loads (e.g. the low-frequency platform modes that dominate mooring tension versus the structural modes that dominate the yaw-bearing moment). plot_load_mode_heatmap() is interactive (Plotly) and plot_load_mode_heatmap_static() static (Matplotlib).

plot_load_mode_heatmap(qoi_map, *, min_normalized=0.05, template=None, title=None)[source]

Plot a load-versus-mode modal-contribution heatmap.

Parameters:
  • qoi_map (QoiMap) – A map built with a modal solution, so each load carries modal residues.

  • min_normalized (float, optional) – Keep only the modes whose per-load normalized contribution exceeds this threshold for at least one load, so dormant modes are dropped and the map stays legible. Set to 0 to keep every mode.

  • template (str, plotly.graph_objects.layout.Template or None, optional) – Plotly layout template to apply. None (the default) applies the VANE template "vane".

  • title (str or None, optional) – Figure title; None (the default) draws none.

Returns:

plotly.graph_objects.Figure – The heatmap (loads on the y-axis, contributing modes on the x-axis, labelled by natural frequency).

Raises:

ValueError – If the map has no load channels or was built without a modal solution.

Return type:

go.Figure

plot_load_mode_heatmap_static(qoi_map, *, min_normalized=0.05, ax=None, style='vane', size=None, title=None)[source]

Plot a load-versus-mode modal-contribution heatmap as a static figure.

Parameters:
  • qoi_map (QoiMap) – A map built with a modal solution, so each load carries modal residues.

  • min_normalized (float, optional) – Keep only the modes whose per-load normalized contribution exceeds this threshold for at least one load. Set to 0 to keep every mode.

  • ax (matplotlib.axes.Axes or None, optional) – Draw into these axes instead of creating a new figure.

  • style (str, dict, pathlib.Path or None, optional) – Matplotlib style in effect while drawing ("vane" by default).

  • size (str, float, FigureSize or None, optional) – Figure size and fonts; None uses "single" for a new figure.

  • title (str or None, optional) – Axes title; None (the default) draws none.

Returns:

matplotlib.figure.Figure – The heatmap with a colour bar; modes on the x-axis labelled by natural frequency, load channels on the y-axis.

Raises:

ValueError – If the map has no load channels or was built without a modal solution.

Return type:

Figure

Diagnostic plots for the identification and tracking quality.

These are the “show your work” visuals that turn the pipeline’s internal quality signals into inspectable figures: the Modal Assurance Criterion (MAC) correlation between adjacent operating points (the basis of mode tracking), the per-track identification confidence, the zoomed review of contested crossings and veerings, and the azimuth coverage behind each operating point’s averaged model. Each plot has an interactive (Plotly) form and a static (Matplotlib) form for publication, the latter named *_static. They complement the reference plots and feed the audit report.

plot_azimuth_coverage(provenance, *, template=None, title=None)[source]

Plot the azimuth samples behind each operating point’s averaged model.

Sparse or clustered azimuths weaken the azimuth average; this shows, per operating point, exactly which rotor azimuths were sampled.

Parameters:
  • provenance (Provenance) – A study’s provenance, carrying each operating point’s azimuth list.

  • template (str, plotly.graph_objects.layout.Template or None, optional) – Plotly layout template to apply. None (the default) applies the VANE template "vane".

  • title (str or None, optional) – Figure title; None (the default) draws none.

Returns:

plotly.graph_objects.Figure – Azimuth (degrees) versus operating-parameter value, one marker per sample.

Return type:

go.Figure

plot_azimuth_coverage_static(provenance, *, ax=None, style='vane', size=None, title=None)[source]

Plot the sampled azimuths of each operating point as a static figure.

Parameters:
  • provenance (Provenance) – A study’s provenance, carrying each operating point’s azimuth list.

  • ax (matplotlib.axes.Axes or None, optional) – Draw into these axes instead of creating a new figure.

  • style (str, dict, pathlib.Path or None, optional) – Matplotlib style in effect while drawing ("vane" by default).

  • size (str, float, FigureSize or None, optional) – Figure size and fonts; None uses "single" for a new figure.

  • title (str or None, optional) – Axes title; None (the default) draws none.

Returns:

matplotlib.figure.Figure – Azimuth (0 to 360 degrees, ticks every 90 degrees) versus the operating parameter, one black marker per sample.

Return type:

Figure

plot_branch_review(diagram, branch_groups, *, template=None, title=None)[source]

Plot each contested branch group as a zoomed Campbell panel.

One small-multiple panel per crossing/veering hotspot shows the competing tracks over the group’s operating-point window, with a guide at the closest approach, so an analyst can review where the automatic tracking is least certain.

Parameters:
  • diagram (CampbellDiagram) – The diagram the branch groups index (its tracks and parameter axis).

  • branch_groups (Sequence[BranchGroup]) – The contested groups to render (e.g. result.identification.branch_groups).

  • template (str, plotly.graph_objects.layout.Template or None, optional) – Plotly layout template to apply. None (the default) applies the VANE template "vane".

  • title (str or None, optional) – Figure title. None (the default) draws one only to say how many groups were left out when there are more than nine.

Returns:

plotly.graph_objects.Figure – One panel per group (capped at nine, with the remainder noted), or a single “none detected” panel when branch_groups is empty.

Return type:

go.Figure

plot_branch_review_static(diagram, branch_groups, *, style='vane', size=None)[source]

Plot each contested branch group as a zoomed static Campbell panel.

The static counterpart of plot_branch_review(): up to nine panels, three per row, labelled (a), (b), … when there are several, each with a legend above it that names the kind of hotspot, the closest approach and the competing tracks (drawn in their family colours and line styles), the smooth alternative dashed when resolution computed it, and a dotted guide at the closest approach.

Parameters:
  • diagram (CampbellDiagram) – The diagram the branch groups index.

  • branch_groups (Sequence[BranchGroup]) – The contested groups to render.

  • style (str, dict, pathlib.Path or None, optional) – Matplotlib style in effect while drawing ("vane" by default).

  • size (str, float, FigureSize or None, optional) – Figure size and fonts; None uses "double".

Returns:

matplotlib.figure.Figure – The panels, or a single panel saying that no contested branches were found.

Return type:

Figure

plot_mac_matrix(reference, test, *, template=None, title=None)[source]

Plot the MAC matrix between two operating points as a heatmap.

The MAC between mode i of reference and mode j of test is the squared correlation of their shapes; a near-diagonal pattern means the modes track cleanly, while off-diagonal energy flags crossings or veering.

Parameters:
  • reference (ModalSolution) – The two solutions to correlate (e.g. adjacent operating points). Their mode shapes must share the same degrees of freedom.

  • test (ModalSolution) – The two solutions to correlate (e.g. adjacent operating points). Their mode shapes must share the same degrees of freedom.

  • template (str, plotly.graph_objects.layout.Template or None, optional) – Plotly layout template to apply. None (the default) applies the VANE template "vane".

  • title (str or None, optional) – Figure title; None (the default) draws none.

Returns:

plotly.graph_objects.Figure – The MAC heatmap, with values in [0, 1]; the axes are labelled with the natural frequencies of the modes.

Return type:

go.Figure

plot_mac_matrix_static(reference, test, *, ax=None, style='vane', size=None, annotate=None, title=None)[source]

Plot the MAC matrix between two operating points as a static heatmap.

Parameters:
  • reference (ModalSolution) – The two solutions to correlate; their mode shapes must share the same degrees of freedom.

  • test (ModalSolution) – The two solutions to correlate; their mode shapes must share the same degrees of freedom.

  • ax (matplotlib.axes.Axes or None, optional) – Draw into these axes instead of creating a new figure.

  • style (str, dict, pathlib.Path or None, optional) – Matplotlib style in effect while drawing ("vane" by default; None keeps the current rcParams).

  • size (str, float, FigureSize or None, optional) – Figure size and fonts (see vane.viz.figure_size()); None uses "single" for a new figure, square.

  • annotate (bool or None, optional) – Write the MAC value in each cell. None (the default) does so for matrices of up to 10 by 10 modes.

  • title (str or None, optional) – Axes title; None (the default) draws none.

Returns:

matplotlib.figure.Figure – The heatmap on a grey scale from white (0) to black (1), with a colour bar; the axes are labelled with the natural frequencies of the modes.

Return type:

Figure

plot_tracking_confidence(diagram, *, template=None, title=None)[source]

Plot each tracked mode’s identification confidence as a bar chart.

Ambiguous tracks are coloured distinctly, and a reference line marks the level below which the audit report flags a track for review.

Parameters:
  • diagram (CampbellDiagram) – The diagram whose tracks are shown.

  • template (str, plotly.graph_objects.layout.Template or None, optional) – Plotly layout template to apply. None (the default) applies the VANE template "vane".

  • title (str or None, optional) – Figure title; None (the default) draws none.

Returns:

plotly.graph_objects.Figure – One bar per track, confidence on [0, 1].

Return type:

go.Figure

plot_tracking_confidence_static(diagram, *, ax=None, style='vane', size=None, title=None)[source]

Plot each tracked mode’s identification confidence as a static bar chart.

Parameters:
  • diagram (CampbellDiagram) – The diagram whose tracks are shown.

  • ax (matplotlib.axes.Axes or None, optional) – Draw into these axes instead of creating a new figure.

  • style (str, dict, pathlib.Path or None, optional) – Matplotlib style in effect while drawing ("vane" by default).

  • size (str, float, FigureSize or None, optional) – Figure size and fonts; None uses "single" for a new figure.

  • title (str or None, optional) – Axes title; None (the default) draws none.

Returns:

matplotlib.figure.Figure – Horizontal bars, one per track in diagram order (top to bottom), blue for a clear track and orange for an ambiguous one, with the review threshold dashed.

Return type:

Figure

Self-contained HTML audit report for a study result.

Turns a StudyResult into a single, shareable HTML file that pairs the analysis with its provenance: the environment and tuning the run used, the operating points and their hashed source files, the interactive Campbell diagram, and tables of the tracked modes (with identification confidence) and detected resonance crossings (with damping-aware severity). It is the auditable artifact a digital-twin or assessment workflow can attach to a result.

study_report_html(result, *, title='VANE Study Report', include_plotlyjs=True)[source]

Render a study result as a self-contained HTML report.

Parameters:
  • result (StudyResult) – The analysis and its provenance.

  • title (str, optional) – Report heading.

  • include_plotlyjs (bool or str, optional) – How to embed Plotly.js, passed to Figure.to_html. The default True inlines the library so the report is fully self-contained and works offline (the intended audit artifact); pass "cdn" for a much smaller file that instead loads Plotly.js from a CDN at view time.

Returns:

str – A complete HTML document.

Return type:

str

write_report(result, output_path, *, title='VANE Study Report', include_plotlyjs=True)[source]

Write a study report to output_path as HTML.

Parameters:
  • result (StudyResult) – The analysis and its provenance.

  • output_path (str or pathlib.Path) – Destination .html file; parent directories are created.

  • title (str, optional) – Report heading.

  • include_plotlyjs (bool or str, optional) – Passed through to study_report_html(); defaults to True (inline, so the written file is fully self-contained).

Return type:

None

write_report_pdf(result, output_path, *, title='VANE Study Report')[source]

Write the study report to output_path as PDF (requires WeasyPrint).

Renders the report’s auditable content — the heading and run metadata, the pass/review audit findings, the provenance (environment, operating points and their hashed source files), and the tracked-mode and resonance tables — to a paginated PDF via the optional WeasyPrint engine. This is the print-ready, archivable companion to the interactive HTML report; because WeasyPrint renders HTML/CSS and does not execute JavaScript, the interactive Campbell figure is available in the HTML report (write_report()), not the PDF.

Parameters:
  • result (StudyResult) – The analysis and its provenance.

  • output_path (str or pathlib.Path) – Destination .pdf file; parent directories are created.

  • title (str, optional) – Report heading.

Returns:

pathlib.Path – The PDF file written.

Raises:

ValueError – If WeasyPrint is not installed (pip install "vane[pdf]").

Return type:

Path

Export a mode shape as a deformed mesh in the legacy VTK format.

A mode shape is most legible in 3-D over the real structure. This writer turns a mode’s nodal displacements and the undeformed node coordinates into a legacy ASCII VTK POLYDATA file — the deformed node positions, the displacement vector field, and an optional line connectivity — that opens directly in a VTK viewer and can be compared against OpenFAST’s own -VTKLin linearization output.

write_mode_animation_vtk() extends this to a VTK-native animation: it sweeps a (real or complex) mode shape through one oscillation period into one VTK file per frame plus a .pvd collection, so a VTK viewer plays the mode’s motion directly rather than the deformed shape being a single still frame. Both write the format by hand, so they need no vtk package.

write_mode_animation_vtk(points, displacement, path, *, n_frames=24, scale=1.0, lines=None, title='VANE mode shape')[source]

Write a mode’s oscillation as a VTK animation (frames + .pvd).

A mode oscillates: the physical motion at phase theta is the real part of the complex mode shape rotated by theta, Re(displacement) * cos(theta) - Im(displacement) * sin(theta). This sweeps theta over one full period in n_frames equal steps, writing each frame’s deformed mesh as a legacy VTK POLYDATA file (via the same serializer as write_mode_shape_vtk()) and a a .pvd collection at path that ties the frames into one time series. Opening the .pvd in a VTK viewer plays the mode’s motion directly — the VTK-native counterpart of the interactive Plotly animation.

A real displacement animates as a pure cos(theta) standing-wave swing; a complex one animates its travelling/orbiting motion (non-zero imaginary part).

Parameters:
  • points (npt.ArrayLike) – Undeformed node coordinates, shape (n_nodes, 3).

  • displacement (npt.ArrayLike) – Nodal mode-shape displacement vectors, shape (n_nodes, 3); real or complex.

  • path (str or pathlib.Path) – Destination .pvd collection path. One VTK file per frame is written next to it, named <stem>.<frame>.vtk.

  • n_frames (int, optional) – Number of frames over one oscillation period. Default 24.

  • scale (float, optional) – Visual exaggeration applied to the displacement. Default 1.0.

  • lines (Sequence[Sequence[int]] or None, optional) – Polyline connectivity (e.g. one polyline per beam), written into every frame.

  • title (str, optional) – The VTK header title line written into every frame, sanitized to a single line of at most 255 characters as in write_mode_shape_vtk().

Returns:

list[pathlib.Path] – The per-frame VTK files in time order. The .pvd collection is written at path (open that in a VTK viewer).

Raises:

ValueError – If points / displacement are not (n_nodes, 3) and equal in shape, non-finite, scale is non-finite, n_frames is not a whole number >= 1, or a line index is out of range.

Return type:

list[Path]

write_mode_shape_vtk(points, displacement, path, *, scale=1.0, lines=None, title='VANE mode shape')[source]

Write a mode’s deformed mesh to a legacy ASCII VTK POLYDATA file.

Parameters:
  • points (npt.ArrayLike) – Undeformed node coordinates, shape (n_nodes, 3).

  • displacement (npt.ArrayLike) – Real nodal displacement vectors of the mode, shape (n_nodes, 3). A complex array is accepted only when its imaginary part is round-off (below 1e-9 of the largest displacement magnitude); a genuinely complex mode is rejected rather than silently reduced to its real part. To write the motion at phase phase, pass np.real(displacement * np.exp(1j * phase)); to show the whole oscillation, use write_mode_animation_vtk().

  • path (str or pathlib.Path) – Destination .vtk file path.

  • scale (float, optional) – Factor applied to displacement before adding it to points to form the deformed geometry (visual exaggeration). Must be finite. Default 1.0.

  • lines (Sequence[Sequence[int]] or None, optional) – Polyline connectivity as sequences of node indices (e.g. one polyline per beam). Written as the VTK LINES cells; None writes points only.

  • title (str, optional) – The VTK header title line. Line breaks and other non-printable characters are replaced by spaces and the title is truncated to 255 characters, so the header always stays a single valid line.

Returns:

pathlib.Path – The file written: deformed POINTS, a displacement point-vector field, and the LINES cells when lines is given.

Raises:

ValueError – If points / displacement are not (n_nodes, 3) and equal in shape, non-finite, displacement has a non-negligible imaginary part, scale is non-finite, or a line index is out of range.

Return type:

Path

Colour palettes for the figures. VANE’s figures use the standard line colours defined in vane.viz (black, red, blue, green, magenta, orange, cyan; see Visualization); this module re-exports them and also provides, as an option for colour-vision deficiency, the Okabe-Ito qualitative palette (Okabe and Ito, 2008) and a helper that applies it to a figure.

vane.viz.palette.OKABE_ITO: tuple[str, ...]

The eight Okabe-Ito colours as hex strings, with black last so that the chromatic hues cycle first.

colorblind_colorway()[source]

Return the Okabe-Ito palette as a Plotly colorway list.

Returns:

list[str] – The eight colours of OKABE_ITO.

Return type:

list[str]

apply_colorblind_palette(figure)[source]

Recolour a figure with the colourblind-safe Okabe-Ito palette.

The figure’s layout.colorway becomes OKABE_ITO, and every explicit line or marker colour that is one of STANDARD_COLORS (such as the mode-family colours of the Campbell diagram) is replaced by the Okabe-Ito colour of the nearest hue. Other explicit colours (greys, colour scales) are left unchanged.

Parameters:

figure (plotly.graph_objects.Figure) – The figure to recolour, in place.

Returns:

plotly.graph_objects.Figure – The same figure, for chaining.

Return type:

go.Figure

apply_standard_palette(figure)[source]

Set a figure’s trace colour cycle to the standard line colours.

Parameters:

figure (plotly.graph_objects.Figure) – The figure; its layout.colorway is set so that traces added without an explicit colour cycle through STANDARD_COLORS.

Returns:

plotly.graph_objects.Figure – The same figure, for chaining.

Return type:

go.Figure

Export

Tabular and reduced-order-model export of the analysis products.

The analysis products — a per-operating-point modal solution, its mode shapes and participation factors, a Campbell diagram of tracked modes, and detected resonance crossings — are flattened into pandas.DataFrame tables and written to CSV, JSON, Excel, or .mat for downstream use (spreadsheets, reports, notebooks and numerical environments that read .mat files). The tables optionally carry the physical labels, per-mode confidence, and azimuth-spread uncertainty produced by the rest of the library.

write_rom() packages a reduced-order model (the reduced state space plus its modal metadata) as a self-contained, auditable bundle ready for a digital-twin / state-estimation workflow. build_physical_lpv_family() exports the named physical-state operating-point plant family a gain-scheduled controller schedules on; save_lpv_family() and load_lpv_family() archive it.

class LPVFamily(scheduling_points, systems, state_names=<factory>, input_names=<factory>, output_names=<factory>, scheduling_variable='WindSpeed', scheduling_unit='m/s', auto_added_states=<factory>, dropped_coupling_a=<factory>, dropped_coupling_c=<factory>)[source]

Bases: object

A parameter-indexed family of named physical-state plants for control design.

Parameters:
  • scheduling_points (npt.NDArray[np.float64]) – The scheduling-variable value of each operating point, shape (n_points,), sorted strictly ascending. In the raw linearization unit: WindSpeed in m/s, RotorSpeed in rad/s (not rpm).

  • systems (list[StateSpace]) – The physical-state plant at each operating point, in the same order as scheduling_points. Every system has the same selected states, inputs, and outputs, in the same order – the consistency a scheduled controller relies on.

  • state_names (list[str]) – The selected physical state, input, and output names, shared by every system.

  • input_names (list[str]) – The selected physical state, input, and output names, shared by every system.

  • output_names (list[str]) – The selected physical state, input, and output names, shared by every system.

  • scheduling_variable (str) – The scheduling variable the family is indexed on (e.g. "WindSpeed").

  • scheduling_unit (str) – The physical unit of scheduling_points in the raw linearization convention: "m/s" for WindSpeed, "rad/s" for RotorSpeed (not rpm, unlike the modal/Campbell path). Carried explicitly so a scheduled controller cannot mis-schedule by assuming the wrong unit.

  • auto_added_states (list[str]) – The states the builder added to the request to complete a position/velocity pair (see pair_derivatives in build_physical_lpv_family()), named by their unit-stripped linearization descriptions. Each also appears in state_names. Empty when nothing was added.

  • dropped_coupling_a (npt.NDArray[np.float64]) – Per operating point, the relative state-matrix coupling the state selection discards, ||A[s, ~s]||_F / ||A[s, :]||_F (s the selected states), shape (n_points,), in the order of scheduling_points. Zero when every state is selected.

  • dropped_coupling_c (npt.NDArray[np.float64]) – Per operating point, the relative output coupling the selection discards, ||C[o, ~s]||_F / ||C[o, :]||_F (o the selected outputs), shape (n_points,). Zero when no outputs are selected or every state is selected.

scheduling_points: npt.NDArray[np.float64]
systems: list[StateSpace]
state_names: list[str]
input_names: list[str]
output_names: list[str]
scheduling_variable: str = 'WindSpeed'
scheduling_unit: str = 'm/s'
auto_added_states: list[str]
dropped_coupling_a: npt.NDArray[np.float64]
dropped_coupling_c: npt.NDArray[np.float64]
property dropped_coupling: npt.NDArray[np.float64]

Per-point relative dropped coupling, the larger of the A and C terms.

Returns:

npt.NDArray[np.float64] – max(dropped_coupling_a, dropped_coupling_c) element-wise, shape (n_points,). This is the quantity max_dropped_coupling checks.

build_physical_lpv_family(lin_dir, *, states, inputs=(), outputs=(), scheduling_var='WindSpeed', pair_derivatives=True, max_dropped_coupling=0.05)[source]

Build a named physical-state LPV plant family from a directory of .lin files.

Discovers the operating points in lin_dir, averages each point’s azimuth files in the physical frame, and selects the named states/inputs/outputs into a state-space model – one per operating point, ordered by scheduling_var, with the same channel layout everywhere. No MBC transform is applied, so per-blade channel names are preserved (use build_rom_family() for the multi-blade coordinate representation).

Selecting a state subset truncates the plant: rows and columns of the unselected states are removed, discarding their coupling into the selected ones. Two safeguards keep that truncation honest. Second-order DOFs are completed with their position/velocity twin (pair_derivatives), and the relative coupling the selection still drops is measured at every point and checked (max_dropped_coupling).

Parameters:
  • lin_dir (str or pathlib.Path) – Directory of OpenFAST .lin files, searched recursively, so a campaign laid out with one sub-directory per operating point is read as distinct points (grouped into operating points by case name).

  • states (Sequence[str]) – Physical state names to select, e.g. ["ED PtfmPitch", "ED RotSpeed"]. Matched against the linearization’s state descriptions (units stripped): an exact match is preferred, else a unique substring. When a name matches both a DOF and its "First time derivative of <DOF>" twin, the position DOF is chosen (and, with pair_derivatives, its twin is then added); a name matching two genuinely different channels raises, listing them. The selected order is the state order, with any auto-added twin inserted next to its partner.

  • inputs (Sequence[str], optional) – Input and output names to select (e.g. ["GenTq", "BldPitch1"]); empty selects none (no B / C).

  • outputs (Sequence[str], optional) – Input and output names to select (e.g. ["GenTq", "BldPitch1"]); empty selects none (no B / C).

  • scheduling_var (str, optional) – The scheduling variable: "WindSpeed" (default, m/s) or "RotorSpeed". RotorSpeed is the raw linearization rotor speed in rad/s (the unit OpenFAST writes), not rpm — unlike the modal/Campbell path, which reports rotor speed in rpm. Schedule a controller built against rad/s accordingly.

  • pair_derivatives (bool, optional) – If true (default), every selected second-order DOF whose twin – the "First time derivative of <DOF>" velocity of a selected position, or the position of a selected velocity – exists in the linearization but was not requested gets that twin added, immediately after a position or immediately before a velocity, so each pair reads position then velocity. The added names (unit-stripped descriptions) are logged at INFO and recorded in LPVFamily.auto_added_states. A position without its velocity is a pure truncation artefact (d(position)/dt is exactly the dropped velocity), so disable this only to reproduce a deliberate truncation.

  • max_dropped_coupling (float or None, optional) – Ceiling on the relative coupling the state selection discards, default 0.05. At each operating point VANE computes ||A[s, ~s]||_F / ||A[s, :]||_F over the selected states s and, when outputs are selected, ||C[o, ~s]||_F / ||C[o, :]||_F over the selected outputs o; the per-point value is the larger of the two (LPVFamily.dropped_coupling). If the largest value over the family exceeds the ceiling, ValueError is raised naming the worst point and the dropped states contributing most. The norms are taken in the linearization’s own units, so the measure is a screening check, not a unit-invariant error bound. None disables the check (the norms are still recorded). Must be a finite number >= 0.

Returns:

LPVFamily – The physical-state plant family, with scheduling_points sorted ascending and a StateSpace per point sharing the selected channel layout. Its state_names is the final (paired) state list; auto_added_states, dropped_coupling_a and dropped_coupling_c record the pairing and the per-point truncation norms.

Raises:
  • FileNotFoundError – If lin_dir does not exist or contains no .lin files.

  • ValueError – If scheduling_var is unknown; max_dropped_coupling is not None or a finite number >= 0; a name selection is invalid (a bare string, an empty states, a duplicate, or a non-string entry); a requested name is missing/ambiguous; a name resolves to a different channel (or different units) across operating points; an operating point’s azimuth files disagree on the channel layout or the operating condition (wind or rotor speed) or contain a duplicate azimuth; the schedule is non-finite or not increasing; or the state selection drops more coupling than max_dropped_coupling allows.

Return type:

LPVFamily

campbell_table(diagram)[source]

Flatten a Campbell diagram into a long-format table of tracked modes.

Parameters:

diagram (CampbellDiagram) – The Campbell diagram to export.

Returns:

pandas.DataFrame – One row per (track, operating point): the track index and label, the operating-parameter value, natural frequency, damping ratio, track confidence, and ambiguity flag. The operating-parameter column is named after diagram.parameter_name.

Raises:

ValueError – If diagram.parameter_name collides with a fixed export column.

Return type:

pd.DataFrame

load_lpv_family(path)[source]

Load a physical-state LPV family saved by save_lpv_family().

The archive is opened with allow_pickle=False. Its format tag and schema version are checked first, then every array and metadata field, and the rebuilt family is validated as on saving, so a file that loads is a family that save_lpv_family() could have written. The arrays, names, scheduling variable and unit, auto-added states and dropped-coupling norms come back exactly as saved.

Parameters:

path (str or os.PathLike) – The .npz file written by save_lpv_family() (the suffix is added if absent).

Returns:

vane.sysid.physical_lpv.LPVFamily – The reconstructed family.

Raises:

ValueError – If the file is not a VANE LPV-family archive, records no schema version or a schema other than LPV_FAMILY_SCHEMA_VERSION (the message names both versions and the VANE version that wrote the file), or is incomplete, has unexpected entries or holds an invalid family.

Return type:

LPVFamily

mode_shapes_table(solution)[source]

Flatten a solution’s complex mode shapes into a long-format table.

Parameters:

solution (ModalSolution) – The modal solution whose mode_shapes (one column per mode) are exported.

Returns:

pandas.DataFrame – One row per (mode, degree of freedom): the mode index, DOF index and description, and the mode-shape component’s real part, imaginary part, magnitude, and phase (degrees).

Return type:

pd.DataFrame

modes_table(solution, *, labels=None, confidence=None, spread=None)[source]

Flatten a modal solution into a per-mode table.

Parameters:
  • solution (ModalSolution) – The modal solution to export.

  • labels (Sequence[ModeLabel] or None, optional) – Per-mode physical labels; their name, category, and multi-blade type are added as columns.

  • confidence (npt.NDArray[np.float64] or Sequence[float] or None, optional) – Per-mode unified confidence, added as a column.

  • spread (AzimuthSpread or None, optional) – Per-mode azimuth spread; its frequency and damping standard deviations are added as columns.

Returns:

pandas.DataFrame – One row per mode, ordered as in solution.

Raises:

ValueError – If any optional argument’s length does not match the number of modes.

Return type:

pd.DataFrame

participation_table(participation, *, descriptions=None)[source]

Flatten participation factors into a long-format per-(mode, DOF) table.

Parameters:
  • participation (ParticipationResult) – The per-DOF participation to export (magnitude has one column per mode).

  • descriptions (Sequence[str] or None, optional) – DOF descriptions parallel to the participation rows; generic dof_i names are used when omitted or mismatched in length.

Returns:

pandas.DataFrame – One row per (mode, DOF): the magnitude, signed magnitude, phase (degrees), and whether the DOF is the mode’s dominant participant.

Return type:

pd.DataFrame

resonance_table(resonances)[source]

Flatten detected resonance crossings into a table.

Parameters:

resonances (Sequence[ResonanceCrossing]) – The crossings to export (e.g. PipelineResult.resonances).

Returns:

pandas.DataFrame – One row per crossing: the track index, excitation harmonic, rotor speed (rpm), crossing frequency (Hz), damping ratio, and severity. Empty (with the columns present) when there are no crossings.

Return type:

pd.DataFrame

save_lpv_family(family, path)[source]

Save a physical-state LPVFamily to .npz.

The archive holds the scheduling points, the per-point dropped-coupling norms and each system’s matrices as plain arrays under explicit indexed keys, and every other field (channel names, the scheduling variable and its unit, the auto-added states, each system’s names, sample time and matrix presence) as JSON metadata. It is stamped with the format tag, LPV_FAMILY_SCHEMA_VERSION and the producing VANE version. No object arrays are used, so load_lpv_family() opens it with allow_pickle=False.

Parameters:
Raises:
  • TypeError – If family is not exactly an LPVFamily, a system is not exactly a StateSpace, or a field has the wrong type.

  • ValueError – If the family is empty, its arrays are non-finite or of the wrong length, the scheduling points are not strictly ascending, or the systems do not share one layout (matrix shapes, names and sample time).

Return type:

None

write_rom(reduction, output_dir, *, matrix_format='mat')[source]

Write a reduced-order model as a self-contained, auditable bundle.

The bundle is a directory holding the reduced state-space matrices, a per-mode modal table, and a JSON manifest, so the reduced model a digital-twin / state estimator consumes travels with the provenance of how it was reduced.

Parameters:
  • reduction (ModalReduction) – A reduced-order model from vane.sysid.modal_reduction().

  • output_dir (str or pathlib.Path) – Destination directory; created if it does not exist.

  • matrix_format ({"mat", "npz"}, optional) – Format for the state-space matrices — .mat (the default) or NumPy .npz.

Returns:

pathlib.Path – The bundle directory written: state_space.{mat,npz} (the A/B/C/ D matrices, dt, the state/input/output names and each output’s multi-blade coordinate, empty when unknown – MATLAB cell arrays of strings in .mat, unicode arrays loadable without allow_pickle in .npz; plus state_projection (full state to modal coordinates) and state_reconstruction (modal coordinates to full state) when the reduction carries them, listed under state_maps in the manifest), modes.csv (per retained mode: natural frequency, damping ratio, ranking residue), and manifest.json (the reduction metadata and library version).

Raises:

ValueError – If matrix_format is not "mat" or "npz".

Return type:

Path

write_table(table, path)[source]

Write a table to CSV, JSON, Excel, or MATLAB, inferred from the file suffix.

Parameters:
  • table (pandas.DataFrame) – The table to write.

  • path (str or pathlib.Path) – Destination path; the suffix selects the format (.csv, .json, .xlsx, or .mat). A .mat file stores each column as a MATLAB variable of the same name.

Raises:

ValueError – If the suffix is not a supported format, Excel is requested without the optional openpyxl dependency installed, or a .mat table holds a value MATLAB cannot store. Missing values (None/NaN) in object/string columns are written to .mat as empty strings in a cell array.

Return type:

None

Validation and benchmarking

Plausibility comparison of extracted frequencies against published references.

VANE is a post-processor of an OpenFAST linearization: its correctness question is whether it faithfully recovers the modal content of the matrices OpenFAST produced, which is settled against version-unambiguous references (analytical solutions, exact MBC invariants, prescribed-mode systems, and a second independent implementation on the same matrices). Published reference-turbine frequencies are deliberately not used as a pass/fail check: they are typically produced by other tools (isolated-beam modal codes or design-stage aero-structural tools) at a particular model version, and they routinely disagree with a coupled OpenFAST linearization because they answer a different question and drift with model/tool version. Requiring agreement with them would test the model, not VANE.

This module offers the published comparison in its correct role instead — a plausibility check and reporting feature. compare_to_reference() matches the frequencies VANE extracted to a set of literature values, reports the per-mode differences, and leaves the outcome to the analyst, so a discrepancy can be attributed to its cause (isolated-component vs full-system coupled modes, design-stage vs as-built, tool/version drift) rather than silently failing a tolerance. It raises no error on large deltas: it quantifies them.

class PlausibilityComparison(labels, reference_hz, matched_hz, absolute_delta_hz, relative_delta, source)[source]

Bases: object

A non-binding comparison of extracted frequencies to published references.

Each published reference frequency is paired with the closest extracted frequency (a unique assignment), and the signed differences are recorded. No tolerance is applied; the arrays simply quantify how the extracted model sits relative to the literature so the analyst can attribute the deltas.

Parameters:
  • labels (list[str]) – A label for each reference mode, parallel to reference_hz.

  • reference_hz (npt.NDArray[np.float64]) – The published reference natural frequencies (Hz), shape (n_ref,).

  • matched_hz (npt.NDArray[np.float64]) – The extracted natural frequency (Hz) paired with each reference, same shape.

  • absolute_delta_hz (npt.NDArray[np.float64]) – Signed difference matched_hz - reference_hz (Hz), same shape.

  • relative_delta (npt.NDArray[np.float64]) – Signed relative difference (matched_hz - reference_hz) / reference_hz, same shape (a fraction, not a percentage).

  • source (str or None) – An optional name for the reference set (e.g. the publication), for reports.

labels: list[str]
reference_hz: npt.NDArray[np.float64]
matched_hz: npt.NDArray[np.float64]
absolute_delta_hz: npt.NDArray[np.float64]
relative_delta: npt.NDArray[np.float64]
source: str | None
property max_abs_relative_delta: float

Return the largest absolute relative difference over the matched modes.

report()[source]

Return a human-readable plausibility table.

Returns:

str – A multi-line table of reference vs extracted frequency and the per-mode delta, headed by a note that this is a non-binding plausibility comparison.

Return type:

str

compare_to_reference(computed_hz, reference_hz, *, labels=None, source=None)[source]

Compare extracted natural frequencies to published references (non-binding).

Each reference frequency is paired with the closest extracted frequency under a unique assignment that minimizes the total absolute frequency distance (so two references never claim the same extracted mode), and the signed per-mode differences are reported. This is a plausibility check, not a correctness check: no tolerance is applied and no error is raised on large differences — they are quantified so they can be attributed to their cause (isolated-component vs coupled model, design-stage vs as-built, tool/version drift).

Parameters:
  • computed_hz (npt.ArrayLike) – The natural frequencies VANE extracted (Hz), shape (n_computed,); must be at least as many as the reference frequencies so every reference can be paired.

  • reference_hz (npt.ArrayLike) – The published reference natural frequencies (Hz), shape (n_ref,).

  • labels (Sequence[str] or None, optional) – A label for each reference mode, parallel to reference_hz (e.g. "1st tower fore-aft"). Defaults to "mode 1", "mode 2", …

  • source (str or None, optional) – An optional name for the reference set, carried into report().

Returns:

PlausibilityComparison – The per-mode reference/extracted frequencies and their differences.

Raises:

ValueError – If either input is not a 1-D array of finite, strictly positive frequencies, if labels does not match reference_hz in length, or if fewer frequencies were extracted than references (so a pairing is impossible).

Return type:

PlausibilityComparison

Ground-truth benchmarking of modal extraction accuracy and robustness.

A synthetic system is built from prescribed natural frequencies, damping ratios, and mode shapes, so the exact modal answer is known. Running the extraction on it and scoring the result against that ground truth measures accuracy directly; adding controlled noise to the system matrix and re-scoring measures robustness. This is the quantitative evidence that the extraction is accurate and degrades gracefully, which the reference tools do not provide.

A classically damped system x'' + C x' + K x = 0 with M = I is assembled from a modal matrix Phi and modal parameters via K = Phi diag(wn^2) Phi^-1 and C = Phi diag(2 zeta wn) Phi^-1; its state matrix [[0, I], [-K, -C]] then has exactly the prescribed eigenvalues and (displacement) mode shapes Phi.

class GroundTruthSystem(a, natural_frequencies_hz, damping_ratios, mode_shapes, ndof2)[source]

Bases: object

A synthetic system with known modal parameters.

Parameters:
  • a (npt.NDArray[np.float64]) – State matrix of shape (2 n, 2 n) in [displacement, velocity] order.

  • natural_frequencies_hz (npt.NDArray[np.float64]) – The prescribed natural frequencies (Hz), shape (n,).

  • damping_ratios (npt.NDArray[np.float64]) – The prescribed damping ratios, shape (n,).

  • mode_shapes (npt.NDArray[np.float64]) – The prescribed displacement mode shapes as columns, shape (n, n).

  • ndof2 (int) – The number of second-order degrees of freedom n.

a: npt.NDArray[np.float64]
natural_frequencies_hz: npt.NDArray[np.float64]
damping_ratios: npt.NDArray[np.float64]
mode_shapes: npt.NDArray[np.float64]
ndof2: int
class ModeRecoveryScore(max_frequency_error, max_damping_error, min_mac, mean_mac, n_matched)[source]

Bases: object

Accuracy of a recovered modal solution against a ground truth.

Parameters:
  • max_frequency_error (float) – Largest relative natural-frequency error over the matched modes.

  • max_damping_error (float) – Largest absolute damping-ratio error over the matched modes.

  • min_mac (float) – Smallest mode-shape MAC over the matched modes (1 = exact); 0 when any ground-truth mode was not recovered.

  • mean_mac (float) – Mean mode-shape MAC over all ground-truth modes (unrecovered modes count as 0).

  • n_matched (int) – Number of ground-truth modes matched to a recovered mode (less than the prescribed count when modes are missing).

max_frequency_error: float
max_damping_error: float
min_mac: float
mean_mac: float
n_matched: int
robustness_curve(truth, noise_levels, *, seed=0)[source]

Score recovery as increasing noise is added to the system dynamics.

One fixed seeded perturbation direction is drawn and, at each level sigma, scaled by sigma * ||dynamics|| and added to the stiffness/damping block of the state matrix (the kinematic [0, I] block is left exact). Reusing a single direction makes the sweep a pure amplitude sweep, so the error grows monotonically with sigma (to first order) rather than varying with an arbitrary fresh direction at each level.

Parameters:
  • truth (GroundTruthSystem) – The ground-truth system.

  • noise_levels (Sequence[float]) – Relative noise amplitudes, each >= 0.

  • seed (int, optional) – Seed for the deterministic perturbation direction.

Returns:

list[tuple[float, ModeRecoveryScore]] – Each noise level paired with its recovery score.

Raises:

ValueError – If any noise level is negative or non-finite.

Return type:

list[tuple[float, ModeRecoveryScore]]

score_recovery(solution, truth)[source]

Score a recovered solution against the ground truth.

Recovered modes are matched to ground-truth modes by maximum total MAC (Hungarian assignment), and the worst-case frequency and damping errors and the mode-shape correlations are reported over the matched pairs.

Parameters:
Returns:

ModeRecoveryScore – The accuracy metrics.

Raises:

ValueError – If the solution has no modes to score.

Return type:

ModeRecoveryScore

synthetic_system(natural_frequencies_hz, damping_ratios, *, mode_shapes=None)[source]

Build a classically damped system with the prescribed modal parameters.

Parameters:
  • natural_frequencies_hz (Sequence[float]) – Natural frequencies (Hz), all strictly positive.

  • damping_ratios (Sequence[float]) – Damping ratios in [0, 1), parallel to natural_frequencies_hz.

  • mode_shapes (npt.NDArray[np.float64] or None, optional) – Invertible modal matrix Phi (columns are mode shapes), shape (n, n); the identity is used when omitted.

Returns:

GroundTruthSystem – The state matrix and the exact modal parameters it realizes.

Raises:

ValueError – If the inputs have mismatched lengths, non-positive frequencies, damping outside [0, 1), or a non-square / singular mode_shapes.

Return type:

GroundTruthSystem

Utilities

Shared utility subpackage for cross-cutting helpers.

vane.utils.paths resolves Windows extended-length paths safely, and vane.utils.fs_retry retries file operations that fail transiently on Windows.

Path resolution that is safe against Windows extended-length (\\?\) paths.

On Windows, pathlib.Path.resolve() can return a path with the extended-length prefix \\?\ (CPython keeps it when a transient error, such as a directory another process is creating or deleting, stops it confirming the plain form). Under that prefix Windows does not collapse .. segments, so an OpenFAST deck that references a shared sub-file as ../Shared/File.dat no longer resolves. These helpers drop the prefix when the plain path fits within MAX_PATH and otherwise collapse .. explicitly.

has_extended_prefix(path)[source]

Return whether path carries the Windows extended-length \\?\ prefix.

Parameters:

path (str or pathlib.Path) – The path to test.

Returns:

bool – True when the path text starts with \\?\.

Return type:

bool

normalize_extended_path(path)[source]

Return path without a needless extended-length prefix.

A path carrying the Windows \\?\ prefix has its .. segments collapsed and loses the prefix when the plain form fits within MAX_PATH; a longer path keeps the prefix (it is needed there) but with .. collapsed. Any other path is returned unchanged, so the function is a no-op off Windows.

Parameters:

path (pathlib.Path) – The path to normalize.

Returns:

pathlib.Path – The normalized path.

Return type:

Path

plain_path_text(path)[source]

Return path as text without any extended-length \\?\ prefix.

Unlike normalize_extended_path(), the prefix is dropped even when the plain form exceeds MAX_PATH: the result is the spelling a program that is not long-path aware (such as OpenFAST) would have to use, so its length can be checked against that program’s limit.

Parameters:

path (str or pathlib.Path) – The path to convert.

Returns:

str – The path text, with .. collapsed and the prefix removed when it had one; unchanged otherwise.

Return type:

str

resolve_path(path)[source]

Resolve path to an absolute path without a needless \\?\ prefix.

Parameters:

path (str or pathlib.Path) – The path to resolve.

Returns:

pathlib.Path – Path(path).resolve() passed through normalize_extended_path().

Return type:

Path

Bounded retry for transient Windows file-system errors.

On Windows, renaming, replacing, copying or deleting a file fails transiently with ERROR_ACCESS_DENIED (5), ERROR_SHARING_VIOLATION (32) or ERROR_LOCK_VIOLATION (33) while another process briefly holds a handle on the file or its directory: an antivirus or search-indexer scan of a freshly written file, or an OpenFAST process that has just exited and is still releasing its handles. Python raises these as PermissionError carrying the code in winerror. A sweep that writes hundreds of case files across many parallel workers meets them sooner or later, and retrying the operation a moment later succeeds.

retry_transient() runs an operation and retries it, with a short exponential backoff, only on such an error; any other exception – including a POSIX PermissionError, which never carries winerror and is a real permission problem – propagates at once, so on POSIX the helpers behave exactly like the operations they wrap. The last error is re-raised once the tries are exhausted.

TRANSIENT_WINERRORS: frozenset[int] = frozenset({5, 32, 33})

access denied, sharing violation and lock violation.

Type:

Windows error codes treated as transient

copy2_with_retry(source, destination)[source]

Copy a file with shutil.copy2(), with retry.

Parameters:
Returns:

str – The destination, as shutil.copy2() returns it (usable as a shutil.copytree() copy_function).

Return type:

str

is_transient_file_error(error)[source]

Return whether error is a transient Windows file-access error.

Parameters:

error (BaseException) – The exception raised by a file-system operation.

Returns:

bool – True for a PermissionError whose winerror is one of TRANSIENT_WINERRORS; False otherwise, and always on POSIX, where the attribute is absent.

Return type:

bool

Hard-link destination to source with os.link(), with retry.

Parameters:
Return type:

None

retry_transient(operation, *, description, tries=5, initial_delay=0.05)[source]

Run operation, retrying it on a transient Windows file-access error.

Parameters:
  • operation (Callable[[], T]) – The operation to run. It is called again from the start on each retry, so it must be safe to repeat after a failed try (e.g. it cleans up its own temporary file before raising).

  • description (str) – What the operation does, for the log message (e.g. "replace deck.fst").

  • tries (int, optional) – The maximum number of calls (default 5).

  • initial_delay (float, optional) – The pause before the first retry in seconds (default 0.05), doubled before each further retry – about 0.75 s in all with the defaults.

Returns:

T – The operation’s return value.

Raises:
  • ValueError – If tries is less than 1 or initial_delay is negative.

  • PermissionError – The last transient error, once tries calls have failed.

  • Exception – Any non-transient error raised by operation, at once.

Return type:

_T

rmtree_with_retry(path)[source]

Delete a directory tree with shutil.rmtree(), with retry.

A retry resumes the deletion: whatever the failed try already removed stays removed, and the tree’s root, which is removed last, still exists.

Parameters:

path (str or pathlib.Path) – The directory to delete.

Return type:

None

Delete a file with pathlib.Path.unlink(), with retry.

Parameters:
  • path (str or pathlib.Path) – The file to delete.

  • missing_ok (bool, optional) – Whether a missing file is ignored (default False).

Return type:

None