|
54 | 54 | ``√(I_yaw / m)`` — a larger value is almost always a coordinate- |
55 | 55 | origin offset leaking into a field that means "CM offset from the |
56 | 56 | tower axis", which mislabels the rigid-body modes (issue #95). |
57 | | -8. The requested ``n_modes`` does not exceed the model's DOF count. |
58 | | -9. The polynomial-fit design matrix on the mesh stations is not |
59 | | - ill-conditioned (cond > 1e4 ⇒ WARN, > 1e6 ⇒ ERROR). |
| 57 | +8. Floating platform inertia is physical: positive ``mass_pform`` and |
| 58 | + strictly-positive ``i_matrix`` diagonal (ERROR otherwise). |
| 59 | +9. Floating model carries hydrodynamic added mass (``hydro_M`` not all |
| 60 | + zero) — omitting it biases every rigid-body frequency high (WARN). |
| 61 | +10. Floating model has *some* restoring (``hydro_K`` or ``mooring_K`` |
| 62 | + non-zero); with neither, the rigid-body modes collapse to ~0 Hz |
| 63 | + (WARN). |
| 64 | +11. The requested ``n_modes`` does not exceed the model's DOF count. |
| 65 | +12. The polynomial-fit design matrix on the mesh stations is not |
| 66 | + ill-conditioned (cond > 1e4 ⇒ WARN, > 1e6 ⇒ ERROR). |
| 67 | +
|
| 68 | +Checks 7–10 are the "floating-model readiness" gates (``hub_conn = 2`` |
| 69 | +only): they catch the seakeeping omissions a non-specialist makes when |
| 70 | +a WindIO ``.yaml`` (geometry + material only) is treated as sufficient |
| 71 | +for a floating system — it is not, the way it is for a land tower |
| 72 | +(issue #95). |
60 | 73 | """ |
61 | 74 |
|
62 | 75 | from __future__ import annotations |
@@ -152,6 +165,9 @@ def check_model( |
152 | 165 | _check_rna_vs_tower_mass(bmi, sp, out) |
153 | 166 | _check_support_conditioning(bmi, out) |
154 | 167 | _check_platform_cm_offset(bmi, out) |
| 168 | + _check_platform_inertia_physical(bmi, out) |
| 169 | + _check_added_mass_present(bmi, out) |
| 170 | + _check_restoring_present(bmi, out) |
155 | 171 | if n_modes is not None: |
156 | 172 | _check_n_modes_vs_dof(bmi, n_modes, out) |
157 | 173 | _check_polyfit_conditioning(bmi, out) |
@@ -447,6 +463,123 @@ def _check_platform_cm_offset(bmi, out: list[ModelWarning]) -> None: |
447 | 463 | )) |
448 | 464 |
|
449 | 465 |
|
| 466 | +_FLOATING_FIX_HINT = ( |
| 467 | + "A floating model needs more than the WindIO geometry + material: the " |
| 468 | + "rigid-body behaviour is set by hydrodynamics (added mass + hydrostatic " |
| 469 | + "restoring) and mooring, none of which live in the .yaml. Supply them via " |
| 470 | + "the companion decks — Tower.from_windio_floating(yaml, hydrodyn_dat=…, " |
| 471 | + "moordyn_dat=…, elastodyn_dat=…) — or an explicit, correctly-assembled " |
| 472 | + "PlatformSupport. The deck path reads the WAMIT A_inf / C_hst and the " |
| 473 | + "MoorDyn system in their correct reference frames and is the validated " |
| 474 | + "(BModes-JJ ≈ 0.0003 %) route." |
| 475 | +) |
| 476 | + |
| 477 | + |
| 478 | +def _check_platform_inertia_physical(bmi, out: list[ModelWarning]) -> None: |
| 479 | + """Flag a non-physical platform inertia (zero / negative mass or |
| 480 | + diagonal moment of inertia). |
| 481 | +
|
| 482 | + A real floating body has strictly positive mass and rotational |
| 483 | + inertia about every axis; a zero or negative diagonal in the 6×6 |
| 484 | + ``i_matrix`` (or a non-positive ``mass_pform``) is a transcription |
| 485 | + error that produces meaningless rigid-body modes. ERROR severity. |
| 486 | + """ |
| 487 | + from pybmodes.io.bmi import PlatformSupport |
| 488 | + |
| 489 | + if not isinstance(bmi.support, PlatformSupport): |
| 490 | + return |
| 491 | + sup = bmi.support |
| 492 | + i_mat = np.asarray(sup.i_matrix, dtype=float) |
| 493 | + mass = float(getattr(sup, "mass_pform", 0.0) or 0.0) |
| 494 | + if mass <= 0.0 and i_mat.size and i_mat.ndim == 2 \ |
| 495 | + and np.isfinite(i_mat[0, 0]): |
| 496 | + mass = float(i_mat[0, 0]) # i_matrix[0,0] is the platform mass |
| 497 | + if mass <= 0.0: |
| 498 | + out.append(ModelWarning( |
| 499 | + "ERROR", |
| 500 | + f"Platform mass is not positive (mass_pform = {mass:.3g} kg). A " |
| 501 | + f"floating body must have a strictly positive mass; verify the " |
| 502 | + f"PlatformSupport / ElastoDyn PtfmMass.", |
| 503 | + "bmi.support.mass_pform", |
| 504 | + )) |
| 505 | + if i_mat.ndim == 2 and i_mat.shape[0] == i_mat.shape[1] and i_mat.size: |
| 506 | + diag = np.diag(i_mat) |
| 507 | + bad = [int(k) for k in range(diag.size) |
| 508 | + if np.isfinite(diag[k]) and diag[k] <= 0.0] |
| 509 | + if bad: |
| 510 | + out.append(ModelWarning( |
| 511 | + "ERROR", |
| 512 | + f"Platform inertia matrix has non-positive diagonal entries at " |
| 513 | + f"DOF index {bad} (value(s) {[float(diag[k]) for k in bad]}). " |
| 514 | + f"Every translational mass and rotational moment of inertia on " |
| 515 | + f"the i_matrix diagonal must be > 0 for a physical body.", |
| 516 | + "bmi.support.i_matrix", |
| 517 | + )) |
| 518 | + |
| 519 | + |
| 520 | +def _check_added_mass_present(bmi, out: list[ModelWarning]) -> None: |
| 521 | + """Warn when a floating model carries no hydrodynamic added mass. |
| 522 | +
|
| 523 | + ``hydro_M`` (the infinite-frequency added-mass matrix ``A_inf``) is |
| 524 | + typically large for a floating platform — often comparable to the |
| 525 | + structural mass in surge / sway / heave — so omitting it biases |
| 526 | + *every* rigid-body frequency high. An all-zero ``hydro_M`` is the |
| 527 | + single most common seakeeping omission for a non-specialist hand- |
| 528 | + assembling a PlatformSupport (issue #95). WARN severity (a zero |
| 529 | + added mass is occasionally a deliberate screening simplification). |
| 530 | + """ |
| 531 | + from pybmodes.io.bmi import PlatformSupport |
| 532 | + |
| 533 | + if not isinstance(bmi.support, PlatformSupport): |
| 534 | + return |
| 535 | + h_m = np.asarray(bmi.support.hydro_M, dtype=float) |
| 536 | + has_added_mass = bool(h_m.size and np.any(np.isfinite(h_m) & (h_m != 0.0))) |
| 537 | + if not has_added_mass: |
| 538 | + out.append(ModelWarning( |
| 539 | + "WARN", |
| 540 | + "Floating model has no hydrodynamic added mass (hydro_M / A_inf is " |
| 541 | + "zero). Added mass is typically large for a floating platform " |
| 542 | + "(often comparable to the structural mass in surge / sway / heave); " |
| 543 | + "omitting it biases all rigid-body frequencies high (commonly by " |
| 544 | + "10–30 %). " + _FLOATING_FIX_HINT, |
| 545 | + "bmi.support.hydro_M", |
| 546 | + )) |
| 547 | + |
| 548 | + |
| 549 | +def _check_restoring_present(bmi, out: list[ModelWarning]) -> None: |
| 550 | + """Warn when a floating model has no restoring at all. |
| 551 | +
|
| 552 | + A floating platform's rigid-body modes are set by hydrostatic |
| 553 | + restoring (``hydro_K`` / ``C_hst``, the waterplane + buoyancy) plus |
| 554 | + mooring stiffness (``mooring_K``). With neither, surge / sway / |
| 555 | + heave / roll / pitch / yaw have no restoring and collapse to ~0 Hz — |
| 556 | + a non-physical "free body in vacuum" rather than a station-kept |
| 557 | + floater. WARN severity (the solve still completes, just meaningless). |
| 558 | + """ |
| 559 | + from pybmodes.io.bmi import PlatformSupport |
| 560 | + |
| 561 | + if not isinstance(bmi.support, PlatformSupport): |
| 562 | + return |
| 563 | + sup = bmi.support |
| 564 | + any_restoring = False |
| 565 | + for mat in (sup.hydro_K, sup.mooring_K): |
| 566 | + arr = np.asarray(mat, dtype=float) |
| 567 | + if arr.size and np.any(np.isfinite(arr) & (arr != 0.0)): |
| 568 | + any_restoring = True |
| 569 | + break |
| 570 | + if not any_restoring: |
| 571 | + out.append(ModelWarning( |
| 572 | + "WARN", |
| 573 | + "Floating model has no restoring: both the hydrostatic restoring " |
| 574 | + "(hydro_K / C_hst) and the mooring stiffness (mooring_K) are zero. " |
| 575 | + "The platform's rigid-body modes are entirely set by this " |
| 576 | + "restoring; with none, surge / sway / heave / roll / pitch / yaw " |
| 577 | + "collapse to ~0 Hz (a free body, not a station-kept floater). " |
| 578 | + + _FLOATING_FIX_HINT, |
| 579 | + "bmi.support.mooring_K", |
| 580 | + )) |
| 581 | + |
| 582 | + |
450 | 583 | def _check_n_modes_vs_dof( |
451 | 584 | bmi, n_modes: int, out: list[ModelWarning] |
452 | 585 | ) -> None: |
|
0 commit comments