""" HydrostaticCurves — curvas de formas hidrostáticas. Evalúa UprightHydrostatics en *n_points* calados equidistantes entre el 10 % del calado de diseño y el propio calado de diseño, produciendo las curvas clásicas usadas en arquitectura naval. Conforme con IACS Rec.34 §4 — verificación y §6 — trazabilidad. Autor: Álvaro Romero Módulo 2 — AR-ShipDesign """ from __future__ import annotations from dataclasses import dataclass, field from typing import Iterator import numpy as np from arshipdesign.hydrostatics.upright import UprightHydrostatics, compute_upright # Encabezados de exportación CSV (orden canónico) CSV_HEADERS: tuple[str, ...] = ( "T[m]", "V[m3]", "Delta[t]", "Awp[m2]", "LCB[m]", "LCF[m]", "KB[m]", "IT[m4]", "IL[m4]", "BMT[m]", "BML[m]", "KMT[m]", "KML[m]", "TPC[t/cm]", "MCT[tm/cm]", "Cb", "Cw", "Cm", "Cp", ) # Mapa campo → atributo de UprightHydrostatics (mismo orden que CSV_HEADERS) _FIELD_ATTRS: tuple[str, ...] = ( "draft", "volume", "displacement", "awp", "lcb", "lcf", "kb", "it", "il", "bmt", "bml", "kmt", "kml", "tpc", "mct", "cb", "cw", "cm", "cp", ) # --------------------------------------------------------------------------- # Dataclass principal # --------------------------------------------------------------------------- @dataclass class HydrostaticCurves: """Curvas de formas hidrostáticas de un casco naval. Se construye con el método de clase :meth:`compute`. Una vez creado, los puntos individuales se acceden con índice (``curves[i]``) o iteración, y los arrays vectorizados con propiedades nombradas (``curves.drafts``, ``curves.displacements``, etc.). Atributos --------- hull_name : str Nombre del casco. lpp : float Eslora entre perpendiculares [m]. beam : float Manga máxima [m]. design_draft : float Calado de diseño [m]. rho : float Densidad del agua usada [kg/m³]. points : list[UprightHydrostatics] Puntos de la curva, ordenados por calado creciente. """ hull_name: str lpp: float beam: float design_draft: float rho: float points: list[UprightHydrostatics] = field(default_factory=list) # ------------------------------------------------------------------ # Constructor principal # ------------------------------------------------------------------ @classmethod def compute( cls, hull, n_points: int = 20, rho: float = 1025.0, kg: float | None = None, t_min_fraction: float = 0.10, ) -> "HydrostaticCurves": """Calcula las curvas hidrostáticas completas para *hull*. Parameters ---------- hull : Hull Casco de referencia (``arshipdesign.core.hull.Hull``). n_points : int Número de calados a evaluar. Mínimo 5. rho : float Densidad del agua [kg/m³]. Default 1025 (agua salada). kg : float | None KG estimado [m]. Si None se usa ``hull.depth × 0.55``. t_min_fraction : float Fracción del calado de diseño para el calado mínimo de cálculo. Default 0.10 (10 %). Se aplica un piso de 1 mm. Returns ------- HydrostaticCurves """ n_points = max(5, int(n_points)) T_min = max(hull.draft * float(t_min_fraction), 1e-3) T_max = float(hull.draft) drafts = np.linspace(T_min, T_max, n_points) points = [ compute_upright(hull, float(T), rho=rho, kg=kg) for T in drafts ] return cls( hull_name = str(hull.name), lpp = float(hull.lpp), beam = float(hull.beam), design_draft = T_max, rho = float(rho), points = points, ) # ------------------------------------------------------------------ # Acceso individual y por iteración # ------------------------------------------------------------------ def __len__(self) -> int: return len(self.points) def __getitem__(self, idx: int) -> UprightHydrostatics: return self.points[idx] def __iter__(self) -> Iterator[UprightHydrostatics]: return iter(self.points) def __repr__(self) -> str: n = len(self.points) if n: T_range = f"{self.points[0].draft:.3f}–{self.points[-1].draft:.3f} m" else: T_range = "vacío" return f"HydrostaticCurves({self.hull_name!r}, n={n}, T={T_range})" # ------------------------------------------------------------------ # Arrays vectorizados (numpy) # ------------------------------------------------------------------ def _col(self, attr: str) -> np.ndarray: """Extrae un atributo de todos los puntos como array numpy.""" return np.array([getattr(p, attr) for p in self.points]) @property def drafts(self) -> np.ndarray: """Calados T [m].""" return self._col("draft") @property def volumes(self) -> np.ndarray: """Volúmenes de desplazamiento V [m³].""" return self._col("volume") @property def displacements(self) -> np.ndarray: """Desplazamientos Δ [t].""" return self._col("displacement") @property def awp_values(self) -> np.ndarray: """Áreas de flotación Awp [m²].""" return self._col("awp") @property def lcb_values(self) -> np.ndarray: """LCB desde AP [m].""" return self._col("lcb") @property def lcf_values(self) -> np.ndarray: """LCF desde AP [m].""" return self._col("lcf") @property def kb_values(self) -> np.ndarray: """KB sobre la quilla [m].""" return self._col("kb") @property def bmt_values(self) -> np.ndarray: """Radios metacéntricos transversales BM_T [m].""" return self._col("bmt") @property def bml_values(self) -> np.ndarray: """Radios metacéntricos longitudinales BM_L [m].""" return self._col("bml") @property def kmt_values(self) -> np.ndarray: """Alturas metacéntricas transversales KM_T [m].""" return self._col("kmt") @property def kml_values(self) -> np.ndarray: """Alturas metacéntricas longitudinales KM_L [m].""" return self._col("kml") @property def tpc_values(self) -> np.ndarray: """TPC [t/cm].""" return self._col("tpc") @property def mct_values(self) -> np.ndarray: """MCT [t·m/cm].""" return self._col("mct") @property def cb_values(self) -> np.ndarray: """Coeficientes de bloque Cb [-].""" return self._col("cb") @property def cw_values(self) -> np.ndarray: """Coeficientes de flotación Cw [-].""" return self._col("cw") @property def cm_values(self) -> np.ndarray: """Coeficientes de cuaderna maestra Cm [-].""" return self._col("cm") @property def cp_values(self) -> np.ndarray: """Coeficientes prismáticos Cp [-].""" return self._col("cp") # ------------------------------------------------------------------ # Interpolación a calado arbitrario # ------------------------------------------------------------------ def at_draft(self, T: float) -> UprightHydrostatics: """Interpola linealmente todos los hidrostáticos al calado *T*. Parameters ---------- T : float Calado de consulta [m]. Se clampea al rango [T_min, T_design]. Returns ------- UprightHydrostatics Valores interpolados en *T*. """ drafts_arr = self.drafts T_clamped = float(np.clip(T, drafts_arr[0], drafts_arr[-1])) kwargs: dict[str, float] = {"draft": T_clamped} for attr in _FIELD_ATTRS: if attr == "draft": continue col = self._col(attr) kwargs[attr] = float(np.interp(T_clamped, drafts_arr, col)) return UprightHydrostatics(**kwargs) # type: ignore[arg-type] # ------------------------------------------------------------------ # Exportación # ------------------------------------------------------------------ def to_csv_lines(self, sep: str = ",", decimal: str = ".") -> list[str]: """Genera las líneas CSV (encabezado + datos). No requiere pandas. Parameters ---------- sep : str Separador de campos. Default ``','``. decimal : str Separador decimal. Default ``'.'``. Returns ------- list[str] Lista de cadenas, cada una es una línea CSV sin ``\\n`` final. """ lines = [sep.join(CSV_HEADERS)] for p in self.points: row = [ f"{p.draft:.4f}", f"{p.volume:.4f}", f"{p.displacement:.4f}", f"{p.awp:.4f}", f"{p.lcb:.4f}", f"{p.lcf:.4f}", f"{p.kb:.4f}", f"{p.it:.4f}", f"{p.il:.4f}", f"{p.bmt:.4f}", f"{p.bml:.4f}", f"{p.kmt:.4f}", f"{p.kml:.4f}", f"{p.tpc:.4f}", f"{p.mct:.4f}", f"{p.cb:.4f}", f"{p.cw:.4f}", f"{p.cm:.4f}", f"{p.cp:.4f}", ] if decimal != ".": row = [v.replace(".", decimal) for v in row] lines.append(sep.join(row)) return lines def to_dict(self) -> dict: """Serializa a dict JSON-serializable. Útil para almacenar resultados en el archivo ``.arsd``. """ return { "hull_name": self.hull_name, "lpp": self.lpp, "beam": self.beam, "design_draft": self.design_draft, "rho": self.rho, "headers": list(CSV_HEADERS), "points": [ { "T": p.draft, "V": p.volume, "Delta": p.displacement, "Awp": p.awp, "LCB": p.lcb, "LCF": p.lcf, "KB": p.kb, "IT": p.it, "IL": p.il, "BMT": p.bmt, "BML": p.bml, "KMT": p.kmt, "KML": p.kml, "TPC": p.tpc, "MCT": p.mct, "Cb": p.cb, "Cw": p.cw, "Cm": p.cm, "Cp": p.cp, } for p in self.points ], }