Files

179 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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))