""" 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 # --------------------------------------------------------------------------- # 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) z_wl = np.linspace(0.0, draft, 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) # Ligero ensanchamiento por encima del chine y = y_chine + (y_max - y_chine) * flare * (z - z_c) / (draft - z_c + 1e-9) y = min(y, y_max) 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 ) # --------------------------------------------------------------------------- # 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