Files
AR-Shipdesign/arshipdesign/hydrostatics/curves_of_form.py
T
alro65 98ff57ed08 Módulo 1 fixes + Módulo 2 motor hidrostático (Tasks 13–13b)
Fixes Module 1 UI:
- wizard_cruiser/sailing/planing: perfiles sin^n calibrados por Cm, V-bottom
  con ángulo de astilla, corrección zona sobre chine planeador
- viewer_3d: buffer hull pendiente para eliminar race condition 500ms
- viewer_lines: reescritura completa — waterlines visibles, control points
  interactivos (drag DelftShip-style), señal offsets_edited
- main_window: conecta offsets_edited → slot _on_offsets_edited_from_viewer
  que propaga cambios a todos los visores, editor, 3D y barra hidrostática

Módulo 2 — motor HydrostaticCurves (Task 13):
- integrator.py: integrate() (Simpson+trapz), waterplane_strips(), section_areas()
- upright.py: UprightHydrostatics (19 campos), compute_upright() single-pass
- curves_of_form.py: HydrostaticCurves.compute(), at_draft(), to_csv_lines(), to_dict()
- tests/test_module2_hydrostatics.py: 83 tests — Wigley V&V, monotonicidad,
  CSV export, IACS Rec.34 §4.3–4.5; todos los 224 tests pasan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 09:11:58 -04:00

343 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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
],
}