98ff57ed08
Fixes Module 1 UI: - wizard_cruiser/sailing/planing: perfiles sin^n calibrados por Cm, V-bottom con ángulo de astilla, corrección zona sobre chine planeador - viewer_3d: buffer hull pendiente para eliminar race condition 500ms - viewer_lines: reescritura completa — waterlines visibles, control points interactivos (drag DelftShip-style), señal offsets_edited - main_window: conecta offsets_edited → slot _on_offsets_edited_from_viewer que propaga cambios a todos los visores, editor, 3D y barra hidrostática Módulo 2 — motor HydrostaticCurves (Task 13): - integrator.py: integrate() (Simpson+trapz), waterplane_strips(), section_areas() - upright.py: UprightHydrostatics (19 campos), compute_upright() single-pass - curves_of_form.py: HydrostaticCurves.compute(), at_draft(), to_csv_lines(), to_dict() - tests/test_module2_hydrostatics.py: 83 tests — Wigley V&V, monotonicidad, CSV export, IACS Rec.34 §4.3–4.5; todos los 224 tests pasan Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
535 lines
22 KiB
Python
535 lines
22 KiB
Python
"""
|
||
Tests Módulo 2 — Motor de Curvas Hidrostáticas.
|
||
|
||
Verifica:
|
||
- UprightHydrostatics a calado único vs solución analítica Wigley
|
||
- HydrostaticCurves: barrido de calados, monotonicidad, coeficientes
|
||
- Exportación CSV y dict
|
||
- Interpolación at_draft()
|
||
- IACS Rec.34 V024–V036: verificación y trazabilidad
|
||
|
||
Autor: Álvaro Romero | Módulo 2 — AR-ShipDesign
|
||
IACS Rec.34 par.4.3, 4.4, 4.5 — verificación analítica, convergencia y simetría.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import math
|
||
|
||
import numpy as np
|
||
import pytest
|
||
|
||
from arshipdesign.core.hull import Hull
|
||
from arshipdesign.hydrostatics import (
|
||
HydrostaticCurves,
|
||
UprightHydrostatics,
|
||
compute_upright,
|
||
CSV_HEADERS,
|
||
)
|
||
from arshipdesign.parametric import generate_hull, HullFamily
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Fixtures
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@pytest.fixture(scope="module")
|
||
def wigley_hull() -> Hull:
|
||
return Hull.from_wigley(lpp=15.0, beam=4.0, draft=1.60,
|
||
n_stations=41, n_waterlines=21)
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def wigley_upright(wigley_hull: Hull) -> UprightHydrostatics:
|
||
return compute_upright(wigley_hull, draft=1.60)
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def wigley_curves(wigley_hull: Hull) -> HydrostaticCurves:
|
||
return HydrostaticCurves.compute(wigley_hull, n_points=20)
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def displacement_hull() -> Hull:
|
||
return generate_hull(HullFamily.DISPLACEMENT, lpp=20.0, beam=6.0,
|
||
draft=2.40, depth=3.20, cb=0.55)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 1. UprightHydrostatics — calado único (Wigley analítico)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestUprightHydrostaticsWigley:
|
||
"""IACS Rec.34 §4.3 — verificación analítica."""
|
||
|
||
# Constantes analíticas para Wigley Lpp=15, B=4, T=1.60
|
||
L, B, T = 15.0, 4.0, 1.60
|
||
|
||
@property
|
||
def V_ana(self): return (4.0/9.0) * self.L * self.B * self.T
|
||
@property
|
||
def Cb_ana(self): return 4.0/9.0
|
||
@property
|
||
def Awp_ana(self): return (2.0/3.0) * self.L * self.B
|
||
@property
|
||
def Cw_ana(self): return 2.0/3.0
|
||
@property
|
||
def KB_ana(self): return 5.0 * self.T / 8.0
|
||
@property
|
||
def IT_ana(self):
|
||
# IT = (2/3) * (B/2)^3 * (L/2) * ∫₋₁¹(1-u²)³du = 32/35
|
||
return (2.0/3.0) * (self.B/2)**3 * (self.L/2) * (32.0/35.0)
|
||
@property
|
||
def Cm_ana(self): return 2.0/3.0
|
||
|
||
def test_volume_analytic(self, wigley_upright):
|
||
assert abs(wigley_upright.volume - self.V_ana) / self.V_ana < 1e-6
|
||
|
||
def test_displacement_from_volume(self, wigley_upright):
|
||
rho = 1025.0
|
||
expected = wigley_upright.volume * rho / 1000.0
|
||
assert abs(wigley_upright.displacement - expected) < 1e-9
|
||
|
||
def test_awp_analytic(self, wigley_upright):
|
||
assert abs(wigley_upright.awp - self.Awp_ana) / self.Awp_ana < 1e-6
|
||
|
||
def test_cb_analytic(self, wigley_upright):
|
||
assert abs(wigley_upright.cb - self.Cb_ana) < 1e-5
|
||
|
||
def test_cw_analytic(self, wigley_upright):
|
||
assert abs(wigley_upright.cw - self.Cw_ana) < 1e-5
|
||
|
||
def test_kb_analytic(self, wigley_upright):
|
||
"""IACS Rec.34 §4.3 — KB = 5T/8 para el casco Wigley."""
|
||
assert abs(wigley_upright.kb - self.KB_ana) / self.KB_ana < 1e-5
|
||
|
||
def test_it_analytic(self, wigley_upright):
|
||
"""IT = (2/3)(B/2)³(L/2)(32/35) para el casco Wigley."""
|
||
assert abs(wigley_upright.it - self.IT_ana) / self.IT_ana < 0.001 # < 0.1%
|
||
|
||
def test_bmt_equals_it_over_v(self, wigley_upright):
|
||
expected = wigley_upright.it / wigley_upright.volume
|
||
assert abs(wigley_upright.bmt - expected) < 1e-9
|
||
|
||
def test_kmt_equals_kb_plus_bmt(self, wigley_upright):
|
||
assert abs(wigley_upright.kmt - (wigley_upright.kb + wigley_upright.bmt)) < 1e-9
|
||
|
||
def test_bml_equals_il_over_v(self, wigley_upright):
|
||
expected = wigley_upright.il / wigley_upright.volume
|
||
assert abs(wigley_upright.bml - expected) < 1e-9
|
||
|
||
def test_kml_equals_kb_plus_bml(self, wigley_upright):
|
||
assert abs(wigley_upright.kml - (wigley_upright.kb + wigley_upright.bml)) < 1e-9
|
||
|
||
def test_lcb_symmetry(self, wigley_upright, wigley_hull):
|
||
"""IACS Rec.34 §4.5 — LCB = Lpp/2 para cascos simétricos."""
|
||
assert abs(wigley_upright.lcb - wigley_hull.lpp / 2.0) < 1e-4
|
||
|
||
def test_lcf_symmetry(self, wigley_upright, wigley_hull):
|
||
"""IACS Rec.34 §4.5 — LCF = Lpp/2 para cascos simétricos."""
|
||
assert abs(wigley_upright.lcf - wigley_hull.lpp / 2.0) < 1e-4
|
||
|
||
def test_cm_analytic(self, wigley_upright):
|
||
assert abs(wigley_upright.cm - self.Cm_ana) < 1e-4
|
||
|
||
def test_cp_from_cb_and_cm(self, wigley_upright):
|
||
"""Cp = Cb / Cm — relación de identidad fundamental."""
|
||
expected = wigley_upright.cb / wigley_upright.cm
|
||
assert abs(wigley_upright.cp - expected) < 1e-4
|
||
|
||
def test_tpc_positive_and_consistent(self, wigley_upright):
|
||
rho = 1025.0
|
||
expected = wigley_upright.awp * rho / 100_000.0
|
||
assert wigley_upright.tpc > 0.0
|
||
assert abs(wigley_upright.tpc - expected) < 1e-9
|
||
|
||
def test_mct_positive(self, wigley_upright):
|
||
assert wigley_upright.mct >= 0.0
|
||
|
||
def test_zero_draft_returns_zeros(self, wigley_hull):
|
||
uh = compute_upright(wigley_hull, draft=0.0)
|
||
assert uh.volume == 0.0
|
||
assert uh.displacement == 0.0
|
||
assert uh.awp == 0.0
|
||
|
||
def test_cb_bounded(self, wigley_upright):
|
||
assert 0.0 < wigley_upright.cb <= 1.0
|
||
|
||
def test_cw_bounded(self, wigley_upright):
|
||
assert 0.0 < wigley_upright.cw <= 1.0
|
||
|
||
def test_cm_bounded(self, wigley_upright):
|
||
assert 0.0 < wigley_upright.cm <= 1.0
|
||
|
||
def test_cp_bounded(self, wigley_upright):
|
||
assert 0.0 < wigley_upright.cp <= 1.0
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 2. compute_upright vs métodos de Hull
|
||
# (IACS Rec.34 §4.3 — consistencia entre métodos)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestUprightVsHullMethods:
|
||
"""Los resultados de compute_upright deben coincidir con los métodos del Hull."""
|
||
|
||
def test_volume_matches_hull(self, wigley_hull):
|
||
uh = compute_upright(wigley_hull, draft=1.60)
|
||
assert abs(uh.volume - wigley_hull.volume_of_displacement()) < 1e-9
|
||
|
||
def test_displacement_matches_hull(self, wigley_hull):
|
||
uh = compute_upright(wigley_hull, draft=1.60)
|
||
assert abs(uh.displacement - wigley_hull.displacement_tonnes()) < 1e-9
|
||
|
||
def test_awp_matches_hull(self, wigley_hull):
|
||
uh = compute_upright(wigley_hull, draft=1.60)
|
||
assert abs(uh.awp - wigley_hull.waterplane_area()) < 1e-9
|
||
|
||
def test_kb_matches_hull(self, wigley_hull):
|
||
uh = compute_upright(wigley_hull, draft=1.60)
|
||
assert abs(uh.kb - wigley_hull.vcb()) < 1e-9
|
||
|
||
def test_kmt_matches_hull(self, wigley_hull):
|
||
uh = compute_upright(wigley_hull, draft=1.60)
|
||
assert abs(uh.kmt - wigley_hull.km_transverse()) < 1e-9
|
||
|
||
def test_tpc_matches_hull(self, wigley_hull):
|
||
uh = compute_upright(wigley_hull, draft=1.60)
|
||
assert abs(uh.tpc - wigley_hull.tpc()) < 1e-9
|
||
|
||
def test_cb_matches_hull(self, wigley_hull):
|
||
uh = compute_upright(wigley_hull, draft=1.60)
|
||
assert abs(uh.cb - wigley_hull.block_coefficient()) < 1e-9
|
||
|
||
def test_cw_matches_hull(self, wigley_hull):
|
||
uh = compute_upright(wigley_hull, draft=1.60)
|
||
assert abs(uh.cw - wigley_hull.waterplane_coefficient()) < 1e-9
|
||
|
||
def test_partial_draft_volume(self, wigley_hull):
|
||
"""compute_upright a T parcial debe coincidir con Hull a ese T."""
|
||
T_partial = 0.80
|
||
uh = compute_upright(wigley_hull, draft=T_partial)
|
||
hull_v = wigley_hull.volume_of_displacement(T_partial)
|
||
assert abs(uh.volume - hull_v) < 1e-9
|
||
|
||
def test_rho_scaling(self, wigley_hull):
|
||
"""Desplazamiento debe escalar linealmente con rho."""
|
||
uh_salt = compute_upright(wigley_hull, draft=1.60, rho=1025.0)
|
||
uh_fresh = compute_upright(wigley_hull, draft=1.60, rho=1000.0)
|
||
ratio = uh_salt.displacement / uh_fresh.displacement
|
||
assert abs(ratio - 1025.0 / 1000.0) < 1e-9
|
||
|
||
def test_tpc_scales_with_rho(self, wigley_hull):
|
||
uh_salt = compute_upright(wigley_hull, draft=1.60, rho=1025.0)
|
||
uh_fresh = compute_upright(wigley_hull, draft=1.60, rho=1000.0)
|
||
ratio = uh_salt.tpc / uh_fresh.tpc
|
||
assert abs(ratio - 1025.0 / 1000.0) < 1e-9
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 3. HydrostaticCurves — barrido de calados
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestHydrostaticCurves:
|
||
def test_default_n_points(self, wigley_hull):
|
||
c = HydrostaticCurves.compute(wigley_hull)
|
||
assert len(c) == 20
|
||
|
||
def test_custom_n_points(self, wigley_hull):
|
||
c = HydrostaticCurves.compute(wigley_hull, n_points=10)
|
||
assert len(c) == 10
|
||
|
||
def test_min_n_points_enforced(self, wigley_hull):
|
||
"""n_points < 5 se eleva a 5."""
|
||
c = HydrostaticCurves.compute(wigley_hull, n_points=2)
|
||
assert len(c) == 5
|
||
|
||
def test_design_draft_is_last_point(self, wigley_hull, wigley_curves):
|
||
assert abs(wigley_curves.points[-1].draft - wigley_hull.draft) < 1e-12
|
||
|
||
def test_design_draft_matches_compute_upright(self, wigley_hull, wigley_curves):
|
||
"""El último punto debe coincidir con compute_upright al calado de diseño."""
|
||
uh = compute_upright(wigley_hull, wigley_hull.draft)
|
||
last = wigley_curves.points[-1]
|
||
assert abs(last.volume - uh.volume) < 1e-9
|
||
assert abs(last.awp - uh.awp) < 1e-9
|
||
assert abs(last.kmt - uh.kmt) < 1e-9
|
||
|
||
def test_hull_name_preserved(self, wigley_hull, wigley_curves):
|
||
assert wigley_curves.hull_name == wigley_hull.name
|
||
|
||
def test_lpp_beam_preserved(self, wigley_hull, wigley_curves):
|
||
assert wigley_curves.lpp == wigley_hull.lpp
|
||
assert wigley_curves.beam == wigley_hull.beam
|
||
|
||
def test_getitem(self, wigley_curves):
|
||
p = wigley_curves[0]
|
||
assert isinstance(p, UprightHydrostatics)
|
||
|
||
def test_iter(self, wigley_curves):
|
||
pts = list(wigley_curves)
|
||
assert len(pts) == len(wigley_curves)
|
||
|
||
def test_repr_contains_hull_name(self, wigley_curves):
|
||
assert "Wigley" in repr(wigley_curves)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 4. Monotonicidad de las curvas
|
||
# (IACS Rec.34 §4.4 — verificación de tendencias)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestCurvesMonotonicity:
|
||
"""V024–V028: las curvas hidrostáticas deben ser monótonamente crecientes."""
|
||
|
||
def test_v024_displacement_monotone(self, wigley_curves):
|
||
"""V024 — Δ(T) es monótonamente creciente."""
|
||
d = np.diff(wigley_curves.displacements)
|
||
assert np.all(d > 0), f"Δ no es monótono: diff mín = {d.min():.6f}"
|
||
|
||
def test_v025_volume_monotone(self, wigley_curves):
|
||
"""V025 — V(T) es monótonamente creciente."""
|
||
d = np.diff(wigley_curves.volumes)
|
||
assert np.all(d > 0), f"V no es monótono: diff mín = {d.min():.6f}"
|
||
|
||
def test_v026_kb_monotone(self, wigley_curves):
|
||
"""V026 — KB(T) es monótonamente creciente."""
|
||
d = np.diff(wigley_curves.kb_values)
|
||
assert np.all(d > 0), f"KB no es monótono: diff mín = {d.min():.6f}"
|
||
|
||
def test_v027_awp_monotone(self, wigley_curves):
|
||
"""V027 — Awp(T) es monótonamente creciente (para el casco Wigley)."""
|
||
d = np.diff(wigley_curves.awp_values)
|
||
assert np.all(d > 0), f"Awp no es monótono: diff mín = {d.min():.6f}"
|
||
|
||
def test_v028_tpc_monotone(self, wigley_curves):
|
||
"""V028 — TPC(T) es monótonamente creciente."""
|
||
d = np.diff(wigley_curves.tpc_values)
|
||
assert np.all(d > 0), f"TPC no es monótono: diff mín = {d.min():.6f}"
|
||
|
||
def test_cb_within_bounds_all_drafts(self, wigley_curves):
|
||
"""Cb ∈ (0, 1) para todos los calados."""
|
||
cb = wigley_curves.cb_values
|
||
assert np.all(cb > 0)
|
||
assert np.all(cb <= 1.0)
|
||
|
||
def test_cw_within_bounds_all_drafts(self, wigley_curves):
|
||
"""Cw ∈ (0, 1] para todos los calados."""
|
||
cw = wigley_curves.cw_values
|
||
assert np.all(cw > 0)
|
||
assert np.all(cw <= 1.0)
|
||
|
||
def test_all_families_monotone_displacement(self):
|
||
"""V029 — Δ monótono para las 5 familias paramétricas."""
|
||
for family in HullFamily:
|
||
hull = generate_hull(family, lpp=12.0, beam=3.5,
|
||
draft=1.20, depth=2.00)
|
||
c = HydrostaticCurves.compute(hull, n_points=10)
|
||
d = np.diff(c.displacements)
|
||
assert np.all(d > 0), \
|
||
f"{family.value}: Δ no es monótono (diff mín={d.min():.6f})"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 5. Interpolación at_draft()
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestAtDraft:
|
||
def test_at_design_draft_matches_last_point(self, wigley_hull, wigley_curves):
|
||
T_d = wigley_hull.draft
|
||
interp = wigley_curves.at_draft(T_d)
|
||
last = wigley_curves.points[-1]
|
||
assert abs(interp.volume - last.volume) < 1e-9
|
||
assert abs(interp.kmt - last.kmt) < 1e-9
|
||
|
||
def test_at_min_draft_matches_first_point(self, wigley_curves):
|
||
T_min = wigley_curves.points[0].draft
|
||
interp = wigley_curves.at_draft(T_min)
|
||
first = wigley_curves.points[0]
|
||
assert abs(interp.volume - first.volume) < 1e-9
|
||
|
||
def test_clamp_below_min(self, wigley_curves):
|
||
T_min = wigley_curves.points[0].draft
|
||
interp = wigley_curves.at_draft(-1.0)
|
||
first = wigley_curves.points[0]
|
||
assert abs(interp.volume - first.volume) < 1e-9
|
||
|
||
def test_clamp_above_max(self, wigley_curves):
|
||
T_max = wigley_curves.points[-1].draft
|
||
interp = wigley_curves.at_draft(T_max + 5.0)
|
||
last = wigley_curves.points[-1]
|
||
assert abs(interp.volume - last.volume) < 1e-9
|
||
|
||
def test_mid_draft_between_bounds(self, wigley_curves):
|
||
"""Valor interpolado intermedio debe estar entre los extremos."""
|
||
T_mid = (wigley_curves.drafts[0] + wigley_curves.drafts[-1]) / 2.0
|
||
interp = wigley_curves.at_draft(T_mid)
|
||
assert wigley_curves.points[0].volume < interp.volume < wigley_curves.points[-1].volume
|
||
|
||
def test_returns_upright_hydrostatics_instance(self, wigley_curves):
|
||
assert isinstance(wigley_curves.at_draft(1.0), UprightHydrostatics)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 6. Vectorized array properties
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestArrayProperties:
|
||
def test_drafts_length(self, wigley_curves):
|
||
assert len(wigley_curves.drafts) == len(wigley_curves)
|
||
|
||
def test_volumes_length(self, wigley_curves):
|
||
assert len(wigley_curves.volumes) == len(wigley_curves)
|
||
|
||
def test_displacements_positive(self, wigley_curves):
|
||
assert np.all(wigley_curves.displacements > 0)
|
||
|
||
def test_kb_positive(self, wigley_curves):
|
||
assert np.all(wigley_curves.kb_values > 0)
|
||
|
||
def test_kmt_positive(self, wigley_curves):
|
||
assert np.all(wigley_curves.kmt_values > 0)
|
||
|
||
def test_tpc_positive(self, wigley_curves):
|
||
assert np.all(wigley_curves.tpc_values > 0)
|
||
|
||
def test_mct_non_negative(self, wigley_curves):
|
||
assert np.all(wigley_curves.mct_values >= 0)
|
||
|
||
def test_all_array_attrs_same_length(self, wigley_curves):
|
||
n = len(wigley_curves)
|
||
for attr in ("drafts", "volumes", "displacements", "awp_values",
|
||
"lcb_values", "lcf_values", "kb_values",
|
||
"bmt_values", "bml_values", "kmt_values", "kml_values",
|
||
"tpc_values", "mct_values",
|
||
"cb_values", "cw_values", "cm_values", "cp_values"):
|
||
arr = getattr(wigley_curves, attr)
|
||
assert len(arr) == n, f"{attr} tiene longitud {len(arr)} ≠ {n}"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 7. Exportación CSV y dict
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestExport:
|
||
def test_csv_header_count(self, wigley_curves):
|
||
lines = wigley_curves.to_csv_lines()
|
||
assert len(lines) == len(wigley_curves) + 1 # header + datos
|
||
|
||
def test_csv_header_matches_constant(self, wigley_curves):
|
||
lines = wigley_curves.to_csv_lines()
|
||
assert lines[0] == ",".join(CSV_HEADERS)
|
||
|
||
def test_csv_row_column_count(self, wigley_curves):
|
||
lines = wigley_curves.to_csv_lines()
|
||
n_cols = len(CSV_HEADERS)
|
||
for row in lines[1:]:
|
||
assert len(row.split(",")) == n_cols
|
||
|
||
def test_csv_first_value_is_draft(self, wigley_curves):
|
||
lines = wigley_curves.to_csv_lines()
|
||
first_draft = float(lines[1].split(",")[0])
|
||
assert abs(first_draft - wigley_curves.points[0].draft) < 1e-3
|
||
|
||
def test_csv_semicolon_separator(self, wigley_curves):
|
||
lines = wigley_curves.to_csv_lines(sep=";")
|
||
assert ";" in lines[0]
|
||
assert "," not in lines[0]
|
||
|
||
def test_csv_decimal_comma(self, wigley_curves):
|
||
lines = wigley_curves.to_csv_lines(sep=";", decimal=",")
|
||
# Los números deben usar coma decimal
|
||
row_parts = lines[1].split(";")
|
||
assert "," in row_parts[0] # e.g. "0,1600"
|
||
|
||
def test_to_dict_has_required_keys(self, wigley_curves):
|
||
d = wigley_curves.to_dict()
|
||
for key in ("hull_name", "lpp", "beam", "design_draft", "rho",
|
||
"headers", "points"):
|
||
assert key in d, f"Falta clave '{key}' en to_dict()"
|
||
|
||
def test_to_dict_json_serializable(self, wigley_curves):
|
||
d = wigley_curves.to_dict()
|
||
txt = json.dumps(d)
|
||
assert len(txt) > 100
|
||
|
||
def test_to_dict_points_count(self, wigley_curves):
|
||
d = wigley_curves.to_dict()
|
||
assert len(d["points"]) == len(wigley_curves)
|
||
|
||
def test_to_dict_first_point_keys(self, wigley_curves):
|
||
d = wigley_curves.to_dict()
|
||
pt = d["points"][0]
|
||
for key in ("T", "V", "Delta", "Awp", "LCB", "LCF", "KB",
|
||
"IT", "IL", "BMT", "BML", "KMT", "KML",
|
||
"TPC", "MCT", "Cb", "Cw", "Cm", "Cp"):
|
||
assert key in pt, f"Falta clave '{key}' en points[0]"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 8. IACS Rec.34 — verificaciones V030–V036
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestIACSVerification:
|
||
"""Verificaciones adicionales según IACS Rec.34 §4."""
|
||
|
||
def test_v030_wigley_cb_accuracy_at_design_draft(self, wigley_curves):
|
||
"""V030 — Cb en calado de diseño ≈ 4/9 (error < 0.1%)."""
|
||
cb_last = wigley_curves.points[-1].cb
|
||
assert abs(cb_last - 4.0/9.0) < 0.001, \
|
||
f"Cb={cb_last:.6f} ≠ 4/9={4/9:.6f}"
|
||
|
||
def test_v031_wigley_cw_at_design_draft(self, wigley_curves):
|
||
"""V031 — Cw en calado de diseño ≈ 2/3 (error < 0.1%)."""
|
||
cw_last = wigley_curves.points[-1].cw
|
||
assert abs(cw_last - 2.0/3.0) < 0.001, \
|
||
f"Cw={cw_last:.6f} ≠ 2/3={2/3:.6f}"
|
||
|
||
def test_v032_wigley_symmetry_all_drafts(self, wigley_hull, wigley_curves):
|
||
"""V032 — LCB = LCF = Lpp/2 en todos los calados (simetría)."""
|
||
L_mid = wigley_hull.lpp / 2.0
|
||
for p in wigley_curves:
|
||
assert abs(p.lcb - L_mid) < 1e-3, \
|
||
f"T={p.draft:.3f}: LCB={p.lcb:.4f} ≠ {L_mid}"
|
||
assert abs(p.lcf - L_mid) < 1e-3, \
|
||
f"T={p.draft:.3f}: LCF={p.lcf:.4f} ≠ {L_mid}"
|
||
|
||
def test_v033_kmt_greater_than_kb(self, wigley_curves):
|
||
"""V033 — KMT > KB en todos los calados (BMT > 0)."""
|
||
for p in wigley_curves:
|
||
assert p.kmt > p.kb, \
|
||
f"T={p.draft:.3f}: KMT={p.kmt:.4f} ≤ KB={p.kb:.4f}"
|
||
|
||
def test_v034_kml_greater_than_kmt(self, wigley_curves):
|
||
"""V034 — KML > KMT en todos los calados (BML >> BMT)."""
|
||
for p in wigley_curves:
|
||
assert p.kml > p.kmt, \
|
||
f"T={p.draft:.3f}: KML={p.kml:.4f} ≤ KMT={p.kmt:.4f}"
|
||
|
||
def test_v035_cp_identity(self, wigley_curves):
|
||
"""V035 — Cp = Cb / Cm (identidad de coeficientes)."""
|
||
for p in wigley_curves:
|
||
if p.cm > 0.01:
|
||
expected = p.cb / p.cm
|
||
assert abs(p.cp - expected) < 1e-4, \
|
||
f"T={p.draft:.3f}: Cp={p.cp:.4f} ≠ Cb/Cm={expected:.4f}"
|
||
|
||
def test_v036_displacement_hull_monotone(self, displacement_hull):
|
||
"""V036 — desplazamiento monótono para casco de desplazamiento parametrico."""
|
||
c = HydrostaticCurves.compute(displacement_hull, n_points=15)
|
||
d = np.diff(c.displacements)
|
||
assert np.all(d > 0), \
|
||
f"DISPLACEMENT: Δ no monótono (diff mín={d.min():.6f})"
|
||
|
||
def test_v037_mesh_convergence_volume(self, wigley_hull):
|
||
"""V037 — IACS §4.4: convergencia de malla. V con 41 sta ≈ V con 81 sta."""
|
||
hull_fine = Hull.from_wigley(
|
||
lpp=15.0, beam=4.0, draft=1.60,
|
||
n_stations=81, n_waterlines=41
|
||
)
|
||
uh_coarse = compute_upright(wigley_hull, 1.60)
|
||
uh_fine = compute_upright(hull_fine, 1.60)
|
||
# Convergencia < 0.1%
|
||
diff_pct = abs(uh_coarse.volume - uh_fine.volume) / uh_fine.volume * 100
|
||
assert diff_pct < 0.1, f"Convergencia de malla: error V = {diff_pct:.4f}%"
|