176 lines
6.4 KiB
Python
176 lines
6.4 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
|
||
|
||
|
||
def _standard_sheer_z(
|
||
x_sta: np.ndarray, lpp: float, depth: float,
|
||
fwd_rise_frac: float = 0.03, aft_rise_frac: float = 0.015,
|
||
) -> np.ndarray:
|
||
"""Línea de cubierta parabólica: mínimo en cuaderna maestra, sube hacia proa/popa."""
|
||
xi = x_sta / lpp # 0=AP, 1=FP, 0.5=midship
|
||
return np.where(
|
||
xi >= 0.5,
|
||
depth * (1.0 + fwd_rise_frac * ((xi - 0.5) / 0.5) ** 2),
|
||
depth * (1.0 + aft_rise_frac * ((0.5 - xi) / 0.5) ** 2),
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 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)
|
||
sheer_z = _standard_sheer_z(x_sta, lpp, depth, fwd_rise_frac=0.03, aft_rise_frac=0.015)
|
||
z_wl = np.linspace(0.0, float(sheer_z.max()), 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, sheer_z=sheer_z,
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 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
|