""" OffsetsTable — tabla de offsets naval clásica. Estructura: filas = estaciones (x), columnas = líneas de agua (z). Valores: semi-manga y(x, z) en metros. Autor: Álvaro Romero Sprint 1 — AR-ShipDesign """ from __future__ import annotations from dataclasses import dataclass, field from typing import Optional import numpy as np from arshipdesign.core.section import Section @dataclass class OffsetsTable: """Tabla de offsets del casco. Atributos --------- x_stations : np.ndarray, shape (n_sta,) Posiciones longitudinales [m] de cada estación, creciente. z_waterlines : np.ndarray, shape (n_wl,) Alturas de líneas de agua [m] desde la quilla, creciente. data : np.ndarray, shape (n_sta, n_wl) Semi-mangas y[i, j] en metros. y[i, j] = semi-manga en la estación x_stations[i], línea z_waterlines[j]. station_labels : list[str] Etiquetas opcionales para las estaciones. lpp : float Eslora entre perpendiculares [m]. beam : float Manga máxima [m]. draft : float Calado de diseño [m]. """ x_stations: np.ndarray z_waterlines: np.ndarray data: np.ndarray station_labels: list[str] = field(default_factory=list) lpp: float = 0.0 beam: float = 0.0 draft: float = 0.0 def __post_init__(self) -> None: self.x_stations = np.asarray(self.x_stations, dtype=float) self.z_waterlines = np.asarray(self.z_waterlines, dtype=float) self.data = np.asarray(self.data, dtype=float) n_sta = len(self.x_stations) n_wl = len(self.z_waterlines) if self.data.shape != (n_sta, n_wl): raise ValueError( f"data.shape {self.data.shape} ≠ ({n_sta}, {n_wl})" ) if not self.station_labels: self.station_labels = [str(i) for i in range(n_sta)] # ------------------------------------------------------------------ # Fábrica: casco Wigley analítico # ------------------------------------------------------------------ @classmethod def from_wigley( cls, lpp: float = 10.0, beam: float = 1.5, draft: float = 0.75, n_stations: int = 21, n_waterlines: int = 11, ) -> "OffsetsTable": """Genera tabla de offsets para el casco Wigley matemático. Fórmula analítica: y(x, z) = (B/2) · [1 − (2ξ/L)²] · [1 − (ζ/T)²] donde ξ ∈ [−L/2, L/2] y ζ ∈ [−T, 0]. El AP corresponde a x=0, el FP a x=Lpp. La quilla está a z=0, la flotación de diseño a z=draft. """ # Posiciones longitudinales: 0 (AP) → Lpp (FP) x_sta = np.linspace(0.0, lpp, n_stations) # Líneas de agua: 0 (quilla) → draft (calado diseño) z_wl = np.linspace(0.0, draft, n_waterlines) # Convertir a coordenadas centradas en el midship xi = x_sta - lpp / 2.0 # ξ ∈ [-Lpp/2, Lpp/2] zeta = z_wl - draft # ζ ∈ [-T, 0] # Factores de forma f_xi = 1.0 - (2.0 * xi / lpp) ** 2 # (n_sta,) f_zeta = 1.0 - (zeta / draft) ** 2 # (n_wl,) # y[i, j] = B/2 · f_xi[i] · f_zeta[j] data = (beam / 2.0) * np.outer(f_xi, f_zeta) data = np.clip(data, 0.0, None) # evitar negativos por redondeo labels = [f"S{i}" for i in range(n_stations)] return cls( x_stations=x_sta, z_waterlines=z_wl, data=data, station_labels=labels, lpp=lpp, beam=beam, draft=draft, ) # ------------------------------------------------------------------ # Conversión a secciones # ------------------------------------------------------------------ def to_sections(self) -> list[Section]: """Convierte la tabla en una lista de objetos Section.""" sections = [] for i, x in enumerate(self.x_stations): sta = self.station_labels[i] if i < len(self.station_labels) else str(i) sec = Section( station=sta, x=float(x), half_breadths=self.data[i, :].copy(), z_positions=self.z_waterlines.copy(), label=f"x={x:.3f} m", ) sections.append(sec) return sections # ------------------------------------------------------------------ # Acceso # ------------------------------------------------------------------ def half_breadth(self, x: float, z: float) -> float: """Interpola la semi-manga en cualquier (x, z) [m].""" # Interpolar en x col_y = np.array([ float(np.interp(x, self.x_stations, self.data[:, j])) for j in range(len(self.z_waterlines)) ]) # Interpolar en z return float(np.interp(z, self.z_waterlines, col_y)) @property def n_stations(self) -> int: return len(self.x_stations) @property def n_waterlines(self) -> int: return len(self.z_waterlines) @property def max_half_breadth(self) -> float: return float(self.data.max()) # ------------------------------------------------------------------ # Dunder # ------------------------------------------------------------------ def __repr__(self) -> str: return ( f"OffsetsTable(Lpp={self.lpp} m, B={self.beam} m, T={self.draft} m, " f"stations={self.n_stations}, waterlines={self.n_waterlines})" )