""" 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))