diff --git a/src/vane/io/openfast_compat.py b/src/vane/io/openfast_compat.py index ccaf420..3b9b55e 100644 --- a/src/vane/io/openfast_compat.py +++ b/src/vane/io/openfast_compat.py @@ -502,7 +502,7 @@ def _unique_index(names: list[str], requested: str) -> int: return matches[0] -def _require_zero(values: np.ndarray, atol: float, label: str) -> None: +def _require_zero(values: npt.NDArray[np.float64], atol: float, label: str) -> None: if not np.all(np.abs(values) <= atol): maximum = float(np.max(np.abs(values))) msg = f"{label} is not zero within atol={atol:g} (max={maximum:g})" diff --git a/src/vane/modal/averaging.py b/src/vane/modal/averaging.py index e36e695..032a5dd 100644 --- a/src/vane/modal/averaging.py +++ b/src/vane/modal/averaging.py @@ -213,7 +213,7 @@ def interval_weights(azimuths: npt.ArrayLike) -> npt.NDArray[np.float64]: ) raise ValueError(msg) sorted_weights = 0.5 * (np.roll(gaps, 1) + gaps) / _TWO_PI - weights = np.empty_like(sorted_weights) + weights: npt.NDArray[np.float64] = np.empty_like(sorted_weights) weights[order] = sorted_weights return weights @@ -957,7 +957,10 @@ def azimuth_tangent( stacklevel=2, ) vector = basis.right[:, index] / basis.right[position, index] - return np.asarray(vector.real, dtype=np.float64) + tangent = np.array(vector.real, dtype=np.float64) + # z / z need not round to exactly one on every platform. + tangent[position] = 1.0 + return tangent def pin_azimuth( diff --git a/src/vane/modal/averaging_error_estimate.py b/src/vane/modal/averaging_error_estimate.py index 5f0f1a3..8524b54 100644 --- a/src/vane/modal/averaging_error_estimate.py +++ b/src/vane/modal/averaging_error_estimate.py @@ -1571,7 +1571,7 @@ def azimuth_count_table( basis = eigen_basis(model.mean) labels = _resolve_branches(basis, branches) tails: dict[str, AliasTail] = {} - scale = max(float(np.linalg.norm(model.mean)), np.finfo(np.float64).tiny) + scale = max(float(np.linalg.norm(model.mean)), float(np.finfo(np.float64).tiny)) if extrapolate: for label, j in labels.items(): orders, sensitivity = _sensitivities(model, basis, j, b) diff --git a/src/vane/modal/floquet.py b/src/vane/modal/floquet.py index 622b3e7..bb2887c 100644 --- a/src/vane/modal/floquet.py +++ b/src/vane/modal/floquet.py @@ -151,7 +151,10 @@ def _fourier_coefficients( coeffs: dict[int, npt.NDArray[np.complex128]] = {} for n in range(-n_max, n_max + 1): phase = np.exp(-1j * n * angles) - coeffs[n] = np.tensordot(phase, matrices, axes=(0, 0)) / n_azimuth + coeffs[n] = np.asarray( + np.tensordot(phase, matrices, axes=(0, 0)) / n_azimuth, + dtype=np.complex128, + ) return coeffs diff --git a/src/vane/modal/identifier.py b/src/vane/modal/identifier.py index d20ce43..fbfb37d 100644 --- a/src/vane/modal/identifier.py +++ b/src/vane/modal/identifier.py @@ -951,7 +951,9 @@ def _best_path( parent: list[npt.NDArray[np.int_]] = [] for op in range(n_op): start = np.where(used[op], _NEG_INF, 0.0) - link_parent = np.full(used[op].shape[0], -1, dtype=np.int_) + link_parent: npt.NDArray[np.int_] = np.full( + used[op].shape[0], -1, dtype=np.int_ + ) if op > 0 and best[op - 1].size and start.size: candidate = best[op - 1][:, np.newaxis] + affinities[op - 1] from_prev = candidate.max(axis=0) if candidate.size else start diff --git a/src/vane/modal/labeler.py b/src/vane/modal/labeler.py index 98e4685..fa18dc9 100644 --- a/src/vane/modal/labeler.py +++ b/src/vane/modal/labeler.py @@ -716,7 +716,7 @@ def _number_tower_modes( """ frequencies = np.asarray(solution.natural_frequencies_hz, dtype=np.float64) eigenvalues = np.asarray(solution.eigenvalues, dtype=np.complex128) - flags = np.zeros(len(labels), dtype=np.bool_) + flags: npt.NDArray[np.bool_] = np.zeros(len(labels), dtype=np.bool_) if np.shape(solution.is_degenerate) == flags.shape: flags = np.asarray(solution.is_degenerate, dtype=np.bool_) if frequencies.shape != flags.shape or eigenvalues.shape != flags.shape: @@ -1201,7 +1201,9 @@ def label_solution( mbc_coordinates = solution.dof_mbc_coordinates # A degenerate mode's eigenvector is an arbitrary rotation within its subspace, # so its multi-blade (whirl) type is not defined; it gets no suffix. - degenerate = np.zeros(solution.mode_shapes.shape[1], dtype=np.bool_) + degenerate: npt.NDArray[np.bool_] = np.zeros( + solution.mode_shapes.shape[1], dtype=np.bool_ + ) if np.shape(solution.is_degenerate) == degenerate.shape: degenerate = np.asarray(solution.is_degenerate, dtype=np.bool_) columns = [ diff --git a/src/vane/run/acceptance.py b/src/vane/run/acceptance.py index d756142..ee78a17 100644 --- a/src/vane/run/acceptance.py +++ b/src/vane/run/acceptance.py @@ -611,7 +611,9 @@ def _component_indices( return tuple(indices) -def _values(files: Sequence[LinFile], indices: tuple[int, ...]) -> np.ndarray: +def _values( + files: Sequence[LinFile], indices: tuple[int, ...] +) -> npt.NDArray[np.float64]: """Stack the selected state operating-point values by azimuth.""" if not indices: return np.empty((len(files), 0), dtype=np.float64) @@ -622,7 +624,7 @@ def _values(files: Sequence[LinFile], indices: tuple[int, ...]) -> np.ndarray: def _vector_metrics( - values: np.ndarray, + values: npt.NDArray[np.float64], weights: npt.NDArray[np.float64], ) -> tuple[tuple[float, ...] | None, float | None, float | None]: """Return component mean, norm of that mean, and maximum instantaneous norm.""" diff --git a/src/vane/sysid/kalman_filter.py b/src/vane/sysid/kalman_filter.py index c917d9f..bbb0d39 100644 --- a/src/vane/sysid/kalman_filter.py +++ b/src/vane/sysid/kalman_filter.py @@ -96,7 +96,7 @@ class FilterResult: def _as_available( available: npt.NDArray[np.bool_] | None, n_outputs: int -) -> np.ndarray: +) -> npt.NDArray[np.bool_]: """Return a boolean output mask of length ``n_outputs`` (all-True if ``None``).""" if available is None: return np.ones(n_outputs, dtype=bool) @@ -242,7 +242,7 @@ def __init__( self.state_names = list(model.state_names) n = self.a.shape[0] if initial_state is None: - self.x = np.zeros(n, dtype=np.float64) + self.x: npt.NDArray[np.float64] = np.zeros(n, dtype=np.float64) else: self.x = np.asarray(initial_state, dtype=np.float64).ravel() if self.x.shape != (n,): @@ -648,7 +648,7 @@ def __init__( ) raise ValueError(msg) if initial_state is None: - self.x = np.zeros(n, dtype=np.float64) + self.x: npt.NDArray[np.float64] = np.zeros(n, dtype=np.float64) else: self.x = np.asarray(initial_state, dtype=np.float64).ravel() if self.x.shape != (n,): diff --git a/src/vane/sysid/observability.py b/src/vane/sysid/observability.py index 3fec1e0..c04a80d 100644 --- a/src/vane/sysid/observability.py +++ b/src/vane/sysid/observability.py @@ -647,7 +647,7 @@ def observability_scores( np.trace(_gramian_from_information(a, info_i, system.dt, horizon)) ) - per_mode = np.empty(0, dtype=np.float64) + per_mode: npt.NDArray[np.float64] = np.empty(0, dtype=np.float64) if mode_states is not None: information = _output_information(c, measurement_noise) gramian = _gramian_from_information(a, information, system.dt, horizon) diff --git a/src/vane/sysid/periodic.py b/src/vane/sysid/periodic.py index ae2a5f4..79274d5 100644 --- a/src/vane/sysid/periodic.py +++ b/src/vane/sysid/periodic.py @@ -12,7 +12,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import numpy as np @@ -131,7 +131,7 @@ def discretized(self, dt: float) -> PeriodicStateSpace: def _shape(matrix: npt.NDArray[np.float64] | None) -> tuple[int, int] | None: - return None if matrix is None else matrix.shape + return None if matrix is None else cast("tuple[int, int]", matrix.shape) def _blend( diff --git a/src/vane/sysid/physical_lpv.py b/src/vane/sysid/physical_lpv.py index 7e87870..0c82f66 100644 --- a/src/vane/sysid/physical_lpv.py +++ b/src/vane/sysid/physical_lpv.py @@ -375,7 +375,8 @@ def _dropped_coupling( chosen = set(x_idx) dropped = [j for j in range(a.shape[0]) if j not in chosen] a_ratio, a_cols = _relative_block_norms(a[list(x_idx), :], dropped) - c_ratio, c_cols = 0.0, np.zeros(len(dropped), dtype=np.float64) + c_ratio = 0.0 + c_cols: npt.NDArray[np.float64] = np.zeros(len(dropped), dtype=np.float64) if c is not None and y_idx: c_ratio, c_cols = _relative_block_norms(c[list(y_idx), :], dropped) shares = np.maximum(a_cols, c_cols) diff --git a/src/vane/sysid/rom_family.py b/src/vane/sysid/rom_family.py index 343f44c..048a33c 100644 --- a/src/vane/sysid/rom_family.py +++ b/src/vane/sysid/rom_family.py @@ -341,7 +341,7 @@ def build_rom_family( if prev_residues is None: # Reference: track the top-n_modes (modal_reduction ranks by residue). order = list(range(n_modes)) - mac_row = np.ones(n_modes, dtype=np.float64) + mac_row: npt.NDArray[np.float64] = np.ones(n_modes, dtype=np.float64) else: # Match ALL previously tracked modes jointly against the FULL candidate # set by output-residue MAC with the frequency tie-breaker, so a tracked @@ -710,7 +710,7 @@ def _transfer_eligible( n_modes = system.n_states // 2 c_modal = c[:, 0::2] - 1j * c[:, 1::2] # (n_outputs, n_modes) b_modal = b[0::2] + 1j * b[1::2] # (n_modes, n_inputs) - scores = np.zeros(n_modes, dtype=np.float64) + scores: npt.NDArray[np.float64] = np.zeros(n_modes, dtype=np.float64) for output, input_channel in pairs: row = _resolve_output(system, output) column = _resolve_input(system, input_channel) diff --git a/src/vane/sysid/serialize.py b/src/vane/sysid/serialize.py index 9fdb2f0..0c5cc38 100644 --- a/src/vane/sysid/serialize.py +++ b/src/vane/sysid/serialize.py @@ -21,7 +21,7 @@ import json import os from dataclasses import fields, is_dataclass -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np @@ -32,6 +32,9 @@ from vane.sysid.rom_family import RomFamily from vane.sysid.state_space import StateSpace +if TYPE_CHECKING: + import numpy.typing as npt + __all__ = [ "LPV_FAMILY_SCHEMA_VERSION", "load_lpv_family", @@ -170,7 +173,7 @@ def save_result(result: _Result, path: str | os.PathLike[str]) -> None: if type(result) not in (FilterResult, ScheduledFilterResult): msg = f"save_result does not handle {name}" raise TypeError(msg) - arrays: dict[str, np.ndarray] = {} + arrays: dict[str, npt.NDArray[Any]] = {} meta_fields: dict[str, Any] = {} for field in fields(result): value = getattr(result, field.name) @@ -186,7 +189,7 @@ def save_result(result: _Result, path: str | os.PathLike[str]) -> None: } # Store the metadata as a 0-d string array so the archive is a single, pickle-free # npz (the arrays share the same file under their field names). - payload: dict[str, np.ndarray] = {"__meta__": np.asarray(json.dumps(meta))} + payload: dict[str, npt.NDArray[Any]] = {"__meta__": np.asarray(json.dumps(meta))} payload.update(arrays) # mypy reads a splatted dict as a possible ``allow_pickle=`` (bool) keyword; none of # our keys is that reserved name, so the arrays are all stored as named arrays. The @@ -381,7 +384,7 @@ def save_rom_family(family: RomFamily, path: str | os.PathLike[str]) -> None: """ _validate_rom_family(family) systems_meta: list[dict[str, Any]] = [] - payload: dict[str, np.ndarray] = { + payload: dict[str, npt.NDArray[Any]] = { "operating_points": family.operating_points, "frequencies_hz": family.frequencies_hz, "damping_ratios": family.damping_ratios, @@ -459,7 +462,7 @@ def load_rom_family(path: str | os.PathLike[str]) -> RomFamily: raise ValueError(msg) prefix = f"system_{index:04d}_" required.add(prefix + "a") - matrices: dict[str, np.ndarray | None] = {} + matrices: dict[str, npt.NDArray[Any] | None] = {} for name in ("b", "c", "d"): present = presence[name] if not isinstance(present, bool): @@ -635,7 +638,7 @@ def save_lpv_family(family: LPVFamily, path: str | os.PathLike[str]) -> None: layout (matrix shapes, names and sample time). """ _validate_lpv_family(family) - payload: dict[str, np.ndarray] = { + payload: dict[str, npt.NDArray[Any]] = { "scheduling_points": family.scheduling_points, "dropped_coupling_a": family.dropped_coupling_a, "dropped_coupling_c": family.dropped_coupling_c, @@ -719,7 +722,7 @@ def _load_lpv_system( msg = f"LPV-family system {index} has invalid matrix metadata" raise ValueError(msg) prefix = f"system_{index:04d}_" - matrices: dict[str, np.ndarray | None] = {} + matrices: dict[str, npt.NDArray[Any] | None] = {} for name in ("a", "b", "c", "d"): present = True if name == "a" else presence[name] if not isinstance(present, bool): diff --git a/src/vane/sysid/spectral_transfer.py b/src/vane/sysid/spectral_transfer.py index da85fc5..a23f9ea 100644 --- a/src/vane/sysid/spectral_transfer.py +++ b/src/vane/sysid/spectral_transfer.py @@ -248,7 +248,9 @@ def frequency_response( b = system.b[:, inputs] c = system.c[outputs] if system.d is None: - d = np.zeros((len(outputs), len(inputs)), dtype=np.float64) + d: npt.NDArray[np.float64] = np.zeros( + (len(outputs), len(inputs)), dtype=np.float64 + ) else: d = system.d[np.ix_(outputs, inputs)] identity = np.eye(system.n_states) diff --git a/src/vane/viz/_style.py b/src/vane/viz/_style.py index 40fd271..5b4a797 100644 --- a/src/vane/viz/_style.py +++ b/src/vane/viz/_style.py @@ -270,7 +270,7 @@ def plot_style() -> Iterator[None]: ... fig, ax = plt.subplots() ... lines = ax.plot([0.0, 1.0], [0.0, 1.0]) """ - with mpl.rc_context(rc=style_rc()): + with mpl.rc_context(rc=cast("dict[Any, Any]", style_rc())): yield @@ -316,7 +316,7 @@ def apply_style(target: Figure | Axes | go.Figure | None = None) -> None: If ``target`` is none of the supported types. """ if target is None: - mpl.rcParams.update(style_rc()) + mpl.rcParams.update(cast("dict[Any, Any]", style_rc())) elif isinstance(target, go.Figure): target.update_layout(template=resolve_template(None)) elif isinstance(target, Figure): @@ -352,7 +352,7 @@ def static_style( else: stack.enter_context(matplotlib.style.context(style)) if size is not None: - stack.enter_context(mpl.rc_context(rc=size.rc())) + stack.enter_context(mpl.rc_context(rc=cast("dict[Any, Any]", size.rc()))) yield @@ -521,7 +521,7 @@ def operating_range_label(parameter_name: str) -> str: def rc_points(key: str) -> float: """Return a font-size ``rcParams`` entry in points (``"medium"`` is resolved).""" - return float(FontProperties(size=mpl.rcParams[key]).get_size()) + return float(FontProperties(size=mpl.rcParams[cast("Any", key)]).get_size()) def text_color(color: str, shade: float = 0.8) -> str: diff --git a/src/vane/viz/qoi_plot.py b/src/vane/viz/qoi_plot.py index c831a99..bca400e 100644 --- a/src/vane/viz/qoi_plot.py +++ b/src/vane/viz/qoi_plot.py @@ -62,7 +62,7 @@ def _heatmap_data( if min_normalized <= 0.0: # The documented escape hatch: keep every mode, including any that are # completely dormant (zero contribution to every load). - keep = np.arange(matrix.shape[1]) + keep: npt.NDArray[np.intp] = np.arange(matrix.shape[1]) else: keep = np.flatnonzero(matrix.max(axis=0) > min_normalized) if keep.size == 0: diff --git a/tests/test_run/test_auto_sweep.py b/tests/test_run/test_auto_sweep.py index b673c5e..0bab3b5 100644 --- a/tests/test_run/test_auto_sweep.py +++ b/tests/test_run/test_auto_sweep.py @@ -1103,7 +1103,10 @@ def test_shared_servodyn_allowed_without_override( """Without an override ServoDyn is only read, so it may stay shared.""" from vane.run import auto_sweep as auto_module from vane.run import driver as driver_module + from vane.run import floating_auto as floating_module + # The deck names a ``.dll``; make it a host library on every test platform. + monkeypatch.setattr(floating_module, "_host_library_suffix", lambda: ".dll") shared = tmp_path / "shared" shared.mkdir() servo = _SERVO_DLL.replace(