@@ -8,6 +8,56 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
88
99## [ Unreleased]
1010
11+ ## [ 1.10.1] — 2026-05-23
12+
13+ A labelling-only bug-fix release: the floating-platform rigid-body mode
14+ classifier (` ModalResult.mode_labels ` ) mislabelled asymmetric platforms.
15+ Frequencies and mode shapes are unchanged on every deck.
16+
17+ ### Fixed
18+
19+ - ** Asymmetric-platform rigid-body mode labels (` ModalResult.mode_labels ` ).**
20+ The floating-platform rigid-mode classifier
21+ (` pybmodes.fem.platform_modes.classify_platform_modes ` ) named each of the
22+ six lowest modes by a * greedy* per-mode argmax over mass-weighted
23+ base-node modal kinetic energy, dropping any DOF name already taken by an
24+ earlier mode. On an asymmetric platform (horizontal CM offset or
25+ off-diagonal mooring/hydro coupling) a genuine surge mode carrying a small
26+ parasitic high-inertia rotation reads, mass-weighted, as yaw-dominated —
27+ so it stole ` yaw ` , the true yaw mode then hit the "already used" branch
28+ and was left ` None ` , and the mislabel cascaded onto the neighbouring modes
29+ (reported as "surge classified as yaw, sway as surge, third mode
30+ unclassified"). The labels are now assigned by a ** global Hungarian
31+ optimal matching** (` scipy.optimize.linear_sum_assignment ` , already used
32+ in ` pybmodes.mac ` ) over the 6 rigid candidates × 6 platform DOFs, so the
33+ mode that best expresses each DOF wins that label and no DOF is ever named
34+ twice. The conservative dominance threshold (0.6) still gates each
35+ assignment, so a genuinely coupled / rotated pair whose energy splits
36+ across DOFs stays ` None ` rather than mislabelled. (#93 )
37+
38+ - ** Deterministic rigid-body labels for symmetric platforms (degenerate
39+ pairs).** On a (bi)symmetric platform surge≈sway and roll≈pitch share an
40+ eigenvalue, so the eigensolver may return any rotation of that 2-D
41+ eigenspace — a basis that depends on BLAS thread ordering (the same
42+ non-determinism hazard fixed for the FA/SS * tower* pair in 1.10.0). A
43+ 45°-mixed degenerate pair reads 50 / 50 across two DOFs and would fall
44+ below the dominance threshold, so the modes could be silently left
45+ ` None ` . ` classify_platform_modes ` now takes the modal ` frequencies ` and
46+ rotates each frequency-degenerate rigid pair back onto its platform axes
47+ before labelling (the rigid-body analog of ` _rotate_degenerate_pairs ` in
48+ ` pybmodes.elastodyn.params ` ), accepting the rotation only when it cleanly
49+ separates the pair into two distinct dominant DOFs. The result no longer
50+ depends on the arbitrary eigensolver basis. Asymmetric platforms break the
51+ degeneracy, so this step is a no-op there.
52+
53+ Both changes are confined to mode ** labelling** ; frequencies and mode
54+ shapes are byte-for-byte unchanged, and symmetric decks (OC3 Hywind, IEA-15
55+ UMaineSemi, …) — whose rigid modes are ~ 98 % single-DOF — keep their
56+ existing labels. The new ` frequencies ` argument on
57+ ` classify_platform_modes ` is optional and keyword-only-friendly (defaults to
58+ ` None ` , which skips the degeneracy resolution), so the change is
59+ backward-compatible.
60+
1161## [ 1.10.0] — 2026-05-22
1262
1363Static-review follow-up on top of 1.9.0: a deterministic fix for the
0 commit comments