179 lines
5.7 KiB
Python
179 lines
5.7 KiB
Python
"""
|
||
Generador paramétrico — Velero Monocasco (fin keel).
|
||
|
||
Genera cascos para veleros de quilla de aleta:
|
||
- Cuerpo del casco: fino, secciones en V
|
||
- Quilla de aleta: apéndice separado con perfil NACA simplificado
|
||
- Proa fina (ángulo de entrada 10°–16°)
|
||
- Popa de espejo amplia
|
||
|
||
Parámetros típicos:
|
||
Cb (cuerpo): 0.35 – 0.44
|
||
L/B: 3.0 – 3.8
|
||
Froude de diseño: 0.30 – 0.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
|
||
|
||
|
||
def _standard_sheer_z(
|
||
x_sta: np.ndarray, lpp: float, depth: float,
|
||
fwd_rise_frac: float = 0.08, aft_rise_frac: float = 0.04,
|
||
) -> 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),
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 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
|
||
if z >= T:
|
||
return y_wl # plumb topside above design waterline
|
||
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,
|
||
beam: float = 3.20,
|
||
draft: float = 0.55, # calado del cuerpo (sin quilla)
|
||
depth: float = 1.80,
|
||
draft_with_keel: float = 1.80, # calado total con quilla
|
||
cb: float = 0.40,
|
||
lcb_frac: float = 0.53, # ligeramente a popa del midship
|
||
cm: float = 0.75,
|
||
deadrise_mid: float = 18.0,
|
||
n_stations: int = 21,
|
||
n_waterlines: int = 9,
|
||
) -> Hull:
|
||
"""Genera un casco de velero de quilla de aleta.
|
||
|
||
El Hull resultante representa el cuerpo del casco sin la quilla.
|
||
La quilla de aleta se añade como apéndice en el módulo de geometría.
|
||
|
||
Parámetros
|
||
----------
|
||
draft : float
|
||
Calado del cuerpo del casco (sin quilla) [m].
|
||
draft_with_keel : float
|
||
Calado total incluyendo la quilla [m].
|
||
deadrise_mid : float
|
||
Ángulo de astilla muerta en cuaderna maestra [°].
|
||
"""
|
||
x_sta = np.linspace(0.0, lpp, n_stations)
|
||
sheer_z = _standard_sheer_z(x_sta, lpp, depth, fwd_rise_frac=0.08, aft_rise_frac=0.04)
|
||
z_wl = np.linspace(0.0, float(sheer_z.max()), n_waterlines)
|
||
xi = (x_sta / lpp - 0.5) * 2.0
|
||
|
||
lcb_shift = 2.0 * (lcb_frac - 0.5)
|
||
|
||
# Plan form: fina en proa, moderadamente llena a popa
|
||
f_plan = _sailing_plan_form(xi, cb, lcb_shift)
|
||
|
||
data = np.zeros((n_stations, n_waterlines))
|
||
|
||
for i in range(n_stations):
|
||
y_wl = (beam / 2.0) * f_plan[i]
|
||
|
||
# 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):
|
||
data[i, j] = max(0.0, _sailing_section(z, draft, y_wl, local_cm, local_dr))
|
||
|
||
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,
|
||
)
|
||
|
||
|
||
def _sailing_plan_form(
|
||
xi: np.ndarray, cb: float, lcb_shift: float
|
||
) -> np.ndarray:
|
||
"""Plan form para velero: fina en proa, moderada en popa."""
|
||
n = _sailing_plan_exponent(cb)
|
||
xi_s = np.clip(xi - lcb_shift, -1.0, 1.0)
|
||
|
||
f = np.zeros_like(xi)
|
||
for k, x in enumerate(xi_s):
|
||
if x <= 0:
|
||
# Run (AP): más llena que la entry
|
||
t = -x
|
||
f[k] = max(0.0, 1.0 - t ** (n * 0.85))
|
||
else:
|
||
# Entry (FP): fina, típico de velero
|
||
t = x
|
||
f[k] = max(0.0, 1.0 - t ** n)
|
||
return f
|
||
|
||
|
||
def _sailing_plan_exponent(cb: float) -> float:
|
||
cb_vals = [0.30, 0.36, 0.42, 0.48, 0.54]
|
||
n_vals = [1.0, 1.3, 1.6, 2.0, 2.4]
|
||
return float(np.interp(cb, cb_vals, n_vals))
|