Módulo 1: visores 2D del plano de líneas + hidrostáticos en vivo
- viewer_lines.py: BodyPlanViewer, ProfileViewer, PlanViewer (QPainter, zoom/paneo, tema dark navy); conectados a los tres viewports 2D del layout 4-viewport (bodyplan / profile / plan). - hull.py: añadidos waterplane_coefficient (Cw), it_waterplane (IT), il_waterplane (IL), bm_transverse (BMT), bm_longitudinal (BML), km_transverse (KMT), tpc, mct1cm — todos verificados analíticamente contra el casco Wigley (IACS Rec.34 §4.3). - main_window.py: _load_hull_viewers() conecta los 4 visores y el panel hidrostáticos al crear un nuevo proyecto; _update_hydrostatics() puebla los 11 campos de la barra inferior en vivo. - test_module1_hydrostatics.py: 35 tests nuevos (IT analítico exacto, consistencia BMT=IT/V, KMT=KB+BMT, TPC=Awp·ρ/1e5, visores headless). Suite total: 86 tests — 86 passed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -237,6 +237,102 @@ class Hull:
|
||||
V = self.volume_of_displacement(draft)
|
||||
return V * rho / 1000.0
|
||||
|
||||
def waterplane_coefficient(self, draft: Optional[float] = None) -> float:
|
||||
"""Coeficiente de plano de flotación Cw = Awp / (Lpp · B).
|
||||
|
||||
IACS Rec.34 §3.3 — parámetro adimensional de la forma del plano de flotación.
|
||||
"""
|
||||
T = draft if draft is not None else self.draft
|
||||
awp = self.waterplane_area(T)
|
||||
return awp / (self.lpp * self.beam)
|
||||
|
||||
def it_waterplane(self, draft: Optional[float] = None) -> float:
|
||||
"""Segundo momento de área del plano de flotación sobre el eje de crujía IT [m⁴].
|
||||
|
||||
IT = (2/3) · ∫₀^L y(x,T)³ dx
|
||||
|
||||
Rawson & Tupper, "Basic Ship Theory" 5ª ed., Cap. 3.
|
||||
"""
|
||||
T = draft if draft is not None else self.draft
|
||||
x = self.offsets.x_stations
|
||||
y_wl = np.array([self.offsets.half_breadth(xi, T) for xi in x])
|
||||
integrand = (2.0 / 3.0) * y_wl ** 3
|
||||
if len(x) >= 3:
|
||||
return abs(float(simpson(integrand, x=x)))
|
||||
return abs(float(np.trapz(integrand, x)))
|
||||
|
||||
def il_waterplane(self, draft: Optional[float] = None) -> float:
|
||||
"""Segundo momento de área del plano de flotación sobre el centro de flotación IL [m⁴].
|
||||
|
||||
IL = ∫₀^L (x − LCF)² · 2y(x,T) dx
|
||||
|
||||
Rawson & Tupper, "Basic Ship Theory" 5ª ed., Cap. 3.
|
||||
"""
|
||||
T = draft if draft is not None else self.draft
|
||||
x = self.offsets.x_stations
|
||||
y_wl = np.array([self.offsets.half_breadth(xi, T) for xi in x])
|
||||
strip = 2.0 * y_wl
|
||||
if len(x) >= 3:
|
||||
awp = float(simpson(strip, x=x))
|
||||
if awp > 1e-12:
|
||||
lcf = float(simpson(strip * x, x=x)) / awp
|
||||
else:
|
||||
lcf = self.lpp / 2.0
|
||||
return abs(float(simpson(strip * (x - lcf) ** 2, x=x)))
|
||||
awp = float(np.trapz(strip, x))
|
||||
lcf = float(np.trapz(strip * x, x)) / awp if awp > 1e-12 else self.lpp / 2.0
|
||||
return abs(float(np.trapz(strip * (x - lcf) ** 2, x)))
|
||||
|
||||
def bm_transverse(self, draft: Optional[float] = None) -> float:
|
||||
"""Radio metacéntrico transversal BM_T = IT / V [m]."""
|
||||
T = draft if draft is not None else self.draft
|
||||
vol = self.volume_of_displacement(T)
|
||||
return self.it_waterplane(T) / vol if vol > 1e-12 else 0.0
|
||||
|
||||
def bm_longitudinal(self, draft: Optional[float] = None) -> float:
|
||||
"""Radio metacéntrico longitudinal BM_L = IL / V [m]."""
|
||||
T = draft if draft is not None else self.draft
|
||||
vol = self.volume_of_displacement(T)
|
||||
return self.il_waterplane(T) / vol if vol > 1e-12 else 0.0
|
||||
|
||||
def km_transverse(self, draft: Optional[float] = None) -> float:
|
||||
"""Altura del metacentro transversal KM_T = KB + BM_T [m].
|
||||
|
||||
Rawson & Tupper, "Basic Ship Theory" 5ª ed., §3.2.
|
||||
"""
|
||||
T = draft if draft is not None else self.draft
|
||||
return self.vcb(T) + self.bm_transverse(T)
|
||||
|
||||
def tpc(self, draft: Optional[float] = None, rho: float = 1025.0) -> float:
|
||||
"""Toneladas por centímetro de inmersión TPC [t/cm].
|
||||
|
||||
TPC = Awp · ρ / 100 000
|
||||
Equivale a la masa añadida necesaria para aumentar el calado 1 cm.
|
||||
"""
|
||||
T = draft if draft is not None else self.draft
|
||||
return self.waterplane_area(T) * rho / 100_000.0
|
||||
|
||||
def mct1cm(
|
||||
self,
|
||||
draft: Optional[float] = None,
|
||||
rho: float = 1025.0,
|
||||
kg: Optional[float] = None,
|
||||
) -> float:
|
||||
"""Momento para cambiar asiento 1 cm MCT [t·m/cm].
|
||||
|
||||
MCT = Δ · GM_L / (100 · Lpp)
|
||||
GM_L = KB + BM_L − KG
|
||||
|
||||
Si *kg* es None se usa la estimación KG ≈ depth × 0.55
|
||||
(válida para embarcaciones con DWT vacío sin peso de carga).
|
||||
"""
|
||||
T = draft if draft is not None else self.draft
|
||||
if kg is None:
|
||||
kg = self.depth * 0.55
|
||||
gml = max(self.vcb(T) + self.bm_longitudinal(T) - kg, 0.0)
|
||||
delta = self.displacement_tonnes(T, rho)
|
||||
return delta * gml / (100.0 * self.lpp)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Malla PyVista para visualización 3D
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user