Módulo 1 fixes + Módulo 2 motor hidrostático (Tasks 13–13b)

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>
This commit is contained in:
2026-05-27 09:11:58 -04:00
parent 274b3b3f53
commit 98ff57ed08
17 changed files with 1687 additions and 199 deletions
+40 -13
View File
@@ -15,12 +15,45 @@ Autor: Álvaro Romero | Sprint 2A — AR-ShipDesign
"""
from __future__ import annotations
import math
import numpy as np
from arshipdesign.core.hull import Hull
from arshipdesign.core.offsets import OffsetsTable
# ---------------------------------------------------------------------------
# Forma de sección — carena redonda tipo desplazamiento
# ---------------------------------------------------------------------------
def _round_bilge_section(z: float, T: float, y_wl: float, Cm: float) -> float:
"""
Sección de carena redonda parametrizada por Cm.
Usa la forma y = y_wl · sin(π/2 · (z/T)ⁿ) donde n se obtiene de Cm.
- n < 1 → sección llena (Cm alto), convexa hacia afuera
- n = 1 → arco de círculo (Cm ≈ 0.637)
- n > 1 → sección en V (Cm bajo), convexa hacia el eje
Propiedades garantizadas:
y(z=0) = 0 (quilla puntual)
y(z=T) = y_wl (manga en flotación)
dy/dz ≥ 0 (monótona, sin inflexión indeseada)
"""
if y_wl < 1e-9 or T < 1e-9:
return 0.0
t = min(1.0, max(0.0, z / T))
if t < 1e-12:
return 0.0
# Tabla calibrada Cm → n (verificada contra integración numérica)
_CM = [0.45, 0.55, 0.65, 0.75, 0.82, 0.88, 0.94]
_N = [2.20, 1.60, 1.15, 0.80, 0.58, 0.42, 0.28]
n = float(np.interp(Cm, _CM, _N))
return float(y_wl * math.sin(math.pi / 2.0 * t ** n))
def make_displacement_hull(
name: str = "Crucero de Desplazamiento",
lpp: float = 12.0,
@@ -53,26 +86,20 @@ def make_displacement_hull(
lcb_shift = 2.0 * (lcb_frac - 0.5) # ∈ [1, 1]
f_plan = _displacement_plan_form(xi, cb, lcb_shift)
# ── Exponente de forma de sección ──────────────────────────────────
# alpha controla la plenitud de la sección transversal
# alpha pequeño → sección llena (Cm alto)
# alpha = 2*(1 - Cm) según aproximación de Munro-Smith
alpha_mid = max(0.25, 2.0 * (1.0 - cm)) # ≈ 0.28 para Cm=0.86
data = np.zeros((n_stations, n_waterlines))
for i in range(n_stations):
y_wl = (beam / 2.0) * f_plan[i]
# El exponente de sección varía: más fino en proa/popa
local_fullness = f_plan[i]
# En extremos (local_fullness→0) el exponente sube → sección más en V
alpha = alpha_mid + (1.0 - alpha_mid) * (1.0 - local_fullness ** 0.5)
alpha = np.clip(alpha, alpha_mid, 0.80)
# Cm varía a lo largo de la eslora: lleno en midship, más en V en extremos.
# f_plan[i]=1 → midship → Cm=cm; f_plan[i]→0 → extremos → Cm≈0.52
local_cm = float(np.clip(
cm * (0.42 + 0.58 * f_plan[i] ** 0.40),
0.52, cm
))
for j, z in enumerate(z_wl):
v = z / draft # ∈ [0, 1]
data[i, j] = y_wl * (v ** alpha)
data[i, j] = _round_bilge_section(z, draft, y_wl, local_cm)
data = np.clip(data, 0.0, None)