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>
161 lines
5.8 KiB
Python
161 lines
5.8 KiB
Python
"""
|
||
Generador paramétrico — Casco Planeador (V-fondo, chine dura).
|
||
|
||
Genera una tabla de offsets realista para embarcaciones de planeo:
|
||
- Fondo en V con ángulo de astilla muerta variable
|
||
- Chine dura recorriendo toda la eslora
|
||
- Popa de espejo plana
|
||
- Proa aguda con alto ángulo de astilla muerta
|
||
- Sección transversal: dos regiones (bajo/sobre chine)
|
||
|
||
Parámetros típicos:
|
||
Cb: 0.40 – 0.48
|
||
Deadrise midship: 12° – 22°
|
||
Deadrise bow: 35° – 55°
|
||
|
||
Autor: Álvaro Romero | Sprint 2A — AR-ShipDesign
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import numpy as np
|
||
|
||
from arshipdesign.core.hull import Hull
|
||
from arshipdesign.core.offsets import OffsetsTable
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# API pública
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def make_planing_hull(
|
||
name: str = "Planeador",
|
||
lpp: float = 10.0,
|
||
beam: float = 3.0,
|
||
draft: float = 0.70,
|
||
depth: float = 1.20,
|
||
deadrise_mid: float = 18.0, # grados en cuaderna maestra
|
||
deadrise_bow: float = 45.0, # grados en proa
|
||
deadrise_stern: float = 8.0, # grados en popa
|
||
chine_frac: float = 0.55, # z_chine / draft
|
||
flare: float = 0.10, # ensanchamiento por encima del chine [fracción]
|
||
n_stations: int = 21,
|
||
n_waterlines: int = 11,
|
||
) -> Hull:
|
||
"""Genera un casco planeador con forma V y chine dura.
|
||
|
||
Parámetros
|
||
----------
|
||
deadrise_mid : float
|
||
Ángulo de astilla muerta [°] en la cuaderna maestra (midship).
|
||
Valores típicos: 12°–22°.
|
||
deadrise_bow : float
|
||
Ángulo de astilla muerta [°] en proa.
|
||
Valores típicos: 35°–55°.
|
||
chine_frac : float
|
||
Altura del chine como fracción del calado (0–1).
|
||
flare : float
|
||
Fracción de ensanchamiento por encima del chine (0 = sin ensanche).
|
||
"""
|
||
x_sta = np.linspace(0.0, lpp, n_stations)
|
||
z_wl = np.linspace(0.0, draft, n_waterlines)
|
||
xi = (x_sta / lpp - 0.5) * 2.0 # normalizado ∈ [−1, 1], 0=midship
|
||
|
||
# ── Plan form: ancho en línea de agua por estación ─────────────────
|
||
# El planeador tiene popa muy ancha y proa más estrecha
|
||
# f_stern > f_bow para que la popa sea la sección más llena
|
||
f_plan = _planing_plan_form(xi) # ∈ [0, 1]
|
||
|
||
# ── Ángulo de astilla muerta varía a lo largo de la eslora ─────────
|
||
dr_arr = _deadrise_distribution(xi, deadrise_mid, deadrise_bow, deadrise_stern)
|
||
|
||
data = np.zeros((n_stations, n_waterlines))
|
||
|
||
for i in range(n_stations):
|
||
y_max = (beam / 2.0) * f_plan[i]
|
||
z_c = draft * chine_frac # altura del chine
|
||
dr = np.radians(dr_arr[i]) # ángulo de astilla en esta estación
|
||
|
||
for j, z in enumerate(z_wl):
|
||
if z <= z_c:
|
||
# ── Zona V-fondo (bajo el chine) ──────────────────────
|
||
# y_chine se calcula desde el ángulo de astilla muerta
|
||
# tan(dr) = z_c / y_chine → y_chine = z_c / tan(dr)
|
||
tan_dr = max(np.tan(dr), 0.01)
|
||
y_chine_dr = z_c / tan_dr
|
||
y_chine = min(y_chine_dr, y_max)
|
||
y = y_chine * (z / z_c)
|
||
else:
|
||
# ── Zona sobre el chine (costado) ─────────────────────
|
||
tan_dr = max(np.tan(dr), 0.01)
|
||
y_chine_dr = z_c / tan_dr
|
||
y_chine = min(y_chine_dr, 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)
|
||
|
||
offsets = OffsetsTable(
|
||
x_stations=x_sta,
|
||
z_waterlines=z_wl,
|
||
data=data,
|
||
station_labels=[f"S{i}" for i in range(n_stations)],
|
||
lpp=lpp, beam=beam, draft=draft,
|
||
)
|
||
return Hull(
|
||
name=name, lpp=lpp, beam=beam, depth=depth, draft=draft, offsets=offsets
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Helpers internos
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def _planing_plan_form(xi: np.ndarray) -> np.ndarray:
|
||
"""
|
||
Plan form del planeador: sección de popa llena, proa estrecha.
|
||
xi ∈ [−1, 1], xi=−1 AP (popa), xi=+1 FP (proa).
|
||
|
||
El planeador tiene la sección maestra desplazada hacia popa (~35% Lpp desde FP).
|
||
"""
|
||
# Apex en xi ≈ −0.3 (30% desde popa) → máximo ancho allí
|
||
apex = -0.30
|
||
# Forma asimétrica: run suave hacia popa, proa afilada
|
||
f = np.zeros_like(xi)
|
||
for k, x in enumerate(xi):
|
||
if x >= apex:
|
||
# Desde apex hacia proa: caída rápida (proa aguda)
|
||
t = (x - apex) / (1.0 - apex)
|
||
f[k] = (1.0 - t ** 1.6)
|
||
else:
|
||
# Desde apex hacia popa: espejo plano, caída suave
|
||
t = (apex - x) / (1.0 + apex)
|
||
f[k] = (1.0 - t ** 2.8)
|
||
return np.maximum(f, 0.0)
|
||
|
||
|
||
def _deadrise_distribution(
|
||
xi: np.ndarray,
|
||
dr_mid: float,
|
||
dr_bow: float,
|
||
dr_stern: float,
|
||
) -> np.ndarray:
|
||
"""Distribución del ángulo de astilla muerta a lo largo de la eslora.
|
||
|
||
Varía suavemente de dr_stern (popa) → dr_mid (midship) → dr_bow (proa).
|
||
xi ∈ [−1, 1], −1=AP, +1=FP.
|
||
"""
|
||
dr = np.zeros_like(xi)
|
||
for k, x in enumerate(xi):
|
||
if x >= 0.0:
|
||
# midship → proa
|
||
t = x
|
||
dr[k] = dr_mid + (dr_bow - dr_mid) * t ** 1.5
|
||
else:
|
||
# midship → popa
|
||
t = -x
|
||
dr[k] = dr_mid + (dr_stern - dr_mid) * t ** 2.0
|
||
return dr
|