""" 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 numpy as np from arshipdesign.core.hull import Hull from arshipdesign.core.offsets import OffsetsTable 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) z_wl = np.linspace(0.0, draft, 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) # 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) for j, z in enumerate(z_wl): v = z / draft y = y_wl * (v ** alpha) 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 ) 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))