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:
@@ -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)
|
||||
|
||||
|
||||
@@ -89,9 +89,11 @@ def make_planing_hull(
|
||||
tan_dr = max(np.tan(dr), 0.01)
|
||||
y_chine_dr = z_c / tan_dr
|
||||
y_chine = min(y_chine_dr, y_max)
|
||||
# Ligero ensanchamiento por encima del chine
|
||||
y = y_chine + (y_max - y_chine) * flare * (z - z_c) / (draft - z_c + 1e-9)
|
||||
y = min(y, y_max)
|
||||
# Costado recto (hard-chine) desde y_chine hasta y_max en cubierta.
|
||||
# El parámetro flare añade ensanche adicional sobre y_max.
|
||||
t_side = (z - z_c) / (draft - z_c + 1e-9)
|
||||
y = y_chine + (y_max * (1.0 + flare) - y_chine) * t_side
|
||||
y = min(y, y_max * (1.0 + flare))
|
||||
|
||||
data[i, j] = max(0.0, y)
|
||||
|
||||
|
||||
@@ -16,12 +16,59 @@ 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 — velero (V-fondo + cuerpo redondeado)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _sailing_section(
|
||||
z: float, T: float, y_wl: float, Cm: float, deadrise_deg: float
|
||||
) -> float:
|
||||
"""
|
||||
Sección de velero con V-fondo y cuerpo redondeado.
|
||||
|
||||
Dos zonas:
|
||||
- [0, z_b]: Fondo en V recto definido por el ángulo de astilla muerta.
|
||||
- [z_b, T]: Cuerpo redondeado (sin^n) desde el pantoque hasta la flotación.
|
||||
|
||||
Propiedades:
|
||||
y(z=0) = 0 (quilla)
|
||||
y(z=T) = y_wl (manga en flotación)
|
||||
Continuidad C⁰ en z=z_b
|
||||
"""
|
||||
if y_wl < 1e-9 or T < 1e-9:
|
||||
return 0.0
|
||||
t_full = min(1.0, max(0.0, z / T))
|
||||
if t_full < 1e-12:
|
||||
return 0.0
|
||||
|
||||
# Altura del pantoque (bilge): 40 % del calado
|
||||
z_b = 0.40 * T
|
||||
# Semi-manga en el pantoque desde el ángulo de astilla
|
||||
# y_b_v = z_b / tan(deadrise) → capped al 65% de y_wl
|
||||
dr_rad = math.radians(max(5.0, min(deadrise_deg, 80.0)))
|
||||
y_b_v = z_b / math.tan(dr_rad)
|
||||
y_b = min(y_b_v, 0.65 * y_wl)
|
||||
|
||||
if z <= z_b:
|
||||
# Fondo en V: lineal desde (0,0) hasta (z_b, y_b)
|
||||
return y_b * z / z_b if z_b > 1e-9 else 0.0
|
||||
else:
|
||||
# Cuerpo redondeado desde (z_b, y_b) hasta (T, y_wl)
|
||||
t = (z - z_b) / (T - z_b)
|
||||
_CM = [0.42, 0.55, 0.65, 0.75, 0.83]
|
||||
_N = [2.50, 1.70, 1.20, 0.82, 0.55]
|
||||
n = float(np.interp(Cm, _CM, _N))
|
||||
return float(y_b + (y_wl - y_b) * math.sin(math.pi / 2.0 * t ** n))
|
||||
|
||||
|
||||
def make_sailing_hull(
|
||||
name: str = "Velero Monocasco",
|
||||
lpp: float = 10.0,
|
||||
@@ -59,24 +106,22 @@ def make_sailing_hull(
|
||||
# Plan form: fina en proa, moderadamente llena a popa
|
||||
f_plan = _sailing_plan_form(xi, cb, lcb_shift)
|
||||
|
||||
# Exponente de sección
|
||||
alpha_mid = max(0.30, 2.0 * (1.0 - cm)) # ≈ 0.50 para Cm=0.75
|
||||
|
||||
data = np.zeros((n_stations, n_waterlines))
|
||||
dr_mid_rad = np.radians(deadrise_mid)
|
||||
|
||||
for i in range(n_stations):
|
||||
y_wl = (beam / 2.0) * f_plan[i]
|
||||
|
||||
# Interpolar entre sección en V (extremos) y sección redonda (midship)
|
||||
local_f = f_plan[i]
|
||||
alpha = alpha_mid + (0.75 - alpha_mid) * (1.0 - local_f ** 0.7)
|
||||
alpha = np.clip(alpha, alpha_mid, 0.80)
|
||||
# Cm y deadrise varían a lo largo de la eslora.
|
||||
# Midship: Cm=cm, deadrise=deadrise_mid
|
||||
# Extremos: más en V (menor Cm, mayor deadrise)
|
||||
local_cm = float(np.clip(
|
||||
cm * (0.38 + 0.62 * f_plan[i] ** 0.45),
|
||||
0.42, cm
|
||||
))
|
||||
local_dr = deadrise_mid + (60.0 - deadrise_mid) * max(0.0, 1.0 - f_plan[i] ** 0.5)
|
||||
|
||||
for j, z in enumerate(z_wl):
|
||||
v = z / draft
|
||||
y = y_wl * (v ** alpha)
|
||||
data[i, j] = max(0.0, y)
|
||||
data[i, j] = max(0.0, _sailing_section(z, draft, y_wl, local_cm, local_dr))
|
||||
|
||||
offsets = OffsetsTable(
|
||||
x_stations=x_sta,
|
||||
|
||||
Reference in New Issue
Block a user