""" Generador paramétrico — Workboat / Supply / Remolcador (sección cajón). Genera cascos de trabajo con: - Sección transversal rectangular-redondeada - Fondo casi plano (quilla de barra) - Radio de pantoque duro - Cuadernas muy llenas (Cm 0.92–0.97) - Proa de bulbo cilíndrico o proa plana Parámetros típicos: Cb: 0.60 – 0.75 Velocidad/Froude: Fn 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.05, aft_rise_frac: float = 0.025, ) -> np.ndarray: """Línea de cubierta parabólica: mínimo en cuaderna maestra, sube hacia proa/popa. El puntal de trazado (depth) es el valor en cuaderna maestra. """ 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_workboat_hull( name: str = "Workboat / Supply", lpp: float = 15.0, beam: float = 5.0, draft: float = 1.80, depth: float = 2.60, cb: float = 0.67, lcb_frac: float = 0.50, bilge_radius_frac: float = 0.12, # radio de pantoque / calado flat_bottom_frac: float = 0.85, # ancho fondo plano / manga n_stations: int = 21, n_waterlines: int = 11, ) -> Hull: """Genera un casco tipo workboat con sección cajón. Parámetros ---------- bilge_radius_frac : float Radio del pantoque como fracción del calado (0.08–0.18). flat_bottom_frac : float Anchura del fondo plano como fracción de la manga (0.80–0.92). """ x_sta = np.linspace(0.0, lpp, n_stations) sheer_z = _standard_sheer_z(x_sta, lpp, depth, fwd_rise_frac=0.035, 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) # Plan form: muy llena, extremos redondeados pero no agudos f_plan = _workboat_plan_form(xi, cb, lcb_shift) # Geometría del pantoque r_bilge = draft * bilge_radius_frac # radio en metros y_flat = (beam / 2.0) * flat_bottom_frac # semi-manga del fondo plano data = np.zeros((n_stations, n_waterlines)) for i in range(n_stations): y_wl = (beam / 2.0) * f_plan[i] # Escalar fondo plano y radio de pantoque con el ancho de la estación scale = f_plan[i] y_flat_i = y_flat * scale r_bilge_i = r_bilge * max(scale, 0.3) for j, z in enumerate(z_wl): if z <= r_bilge_i: # ── Zona de pantoque redondeado ────────────────────── # Arco de círculo: y² + (z - r)² = r² → y = sqrt(r² - (r-z)²) 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: # ── Zona de costado (cuasi-vertical) ───────────────── # Ligero flare hacia afuera flare = 0.04 y = y_flat_i + (y_wl - y_flat_i) * (z - r_bilge_i) / (draft - r_bilge_i + 1e-9) 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 _workboat_plan_form( xi: np.ndarray, cb: float, lcb_shift: float ) -> np.ndarray: """Plan form para workboat: extremos redondeados pero llenos.""" n = _workboat_plan_exponent(cb) xi_s = np.clip(xi - lcb_shift, -1.0, 1.0) f = np.maximum(0.0, 1.0 - np.abs(xi_s) ** n) return f def _workboat_plan_exponent(cb: float) -> float: """Cb → exponente plan form (workboat más llena que displacement).""" cb_vals = [0.55, 0.62, 0.68, 0.73, 0.78] n_vals = [2.5, 3.0, 3.8, 4.5, 5.5] return float(np.interp(cb, cb_vals, n_vals))