""" Generador paramétrico — Buque mercante / Supply full (inspirado en Serie 60). Genera cascos para buques de carga, supply vessels y embarcaciones de trabajo con coeficientes de bloque altos: - Fondo casi plano en la zona central - Cuadernas rectangulares con pantoque redondeado - Proa de bulbo (simplificada) o proa recta - Popa transom o popa de crucero Parámetros típicos: Cb: 0.60 – 0.80 Froude: 0.12 – 0.22 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 _standard_sheer_z( x_sta: np.ndarray, lpp: float, depth: float, fwd_rise_frac: float = 0.04, aft_rise_frac: float = 0.020, ) -> 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), ) def make_merchant_hull( name: str = "Buque Mercante / Supply", lpp: float = 20.0, beam: float = 6.0, draft: float = 2.40, depth: float = 3.20, cb: float = 0.70, lcb_frac: float = 0.50, cm: float = 0.96, # sección maestra casi rectangular bilge_radius_frac: float = 0.08, flat_bottom_frac: float = 0.90, n_stations: int = 21, n_waterlines: int = 11, ) -> Hull: """Genera un casco tipo Serie 60 / buque mercante. Parámetros ---------- cb : float Coeficiente de bloque objetivo (0.60–0.80). cm : float Coeficiente de cuaderna maestra (0.93–0.98). bilge_radius_frac : float Radio del pantoque / calado (0.06–0.14). flat_bottom_frac : float Ancho del fondo plano / manga (0.85–0.94). """ x_sta = np.linspace(0.0, lpp, n_stations) sheer_z = _standard_sheer_z(x_sta, lpp, depth, fwd_rise_frac=0.04, aft_rise_frac=0.020) 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) f_plan = _merchant_plan_form(xi, cb, lcb_shift) r_bilge = draft * bilge_radius_frac y_flat_b = (beam / 2.0) * flat_bottom_frac data = np.zeros((n_stations, n_waterlines)) for i in range(n_stations): y_wl = (beam / 2.0) * f_plan[i] scale = f_plan[i] y_flat_i = y_flat_b * scale r_bilge_i = r_bilge * max(scale, 0.25) for j, z in enumerate(z_wl): if z <= r_bilge_i: dz = r_bilge_i - z inner = max(0.0, r_bilge_i**2 - dz**2) y = y_flat_i - r_bilge_i + np.sqrt(inner) else: # Costados casi verticales (Cm alto) t = (z - r_bilge_i) / max(draft - r_bilge_i, 1e-6) # Cm ajusta plenitud de la zona de costados flare_side = (1.0 - cm) * 0.15 y = y_flat_i + (y_wl - y_flat_i) * (t + flare_side * t * (1 - t)) y = min(y, y_wl) 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, sheer_z=sheer_z, ) def _merchant_plan_form( xi: np.ndarray, cb: float, lcb_shift: float ) -> np.ndarray: """Plan form mercante: central muy llena, extremos redondeados.""" n = _merchant_plan_exponent(cb) xi_s = np.clip(xi - lcb_shift, -1.0, 1.0) return np.maximum(0.0, 1.0 - np.abs(xi_s) ** n) def _merchant_plan_exponent(cb: float) -> float: cb_vals = [0.58, 0.65, 0.70, 0.75, 0.82] n_vals = [3.0, 4.0, 5.0, 6.0, 8.0] return float(np.interp(cb, cb_vals, n_vals))