Files

197 lines
7.2 KiB
Python
Raw Permalink 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.
"""
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
# Altura de la quilla por estación [m]. Permite quillas inclinadas
# (rise of keel / rocker) y quillas con perfil curvo sin alterar la
# cuadrícula de líneas de agua que permanece compartida.
# Default: cero en todas las estaciones (quilla plana sobre baseline).
keel_z: np.ndarray = field(default_factory=lambda: np.array([]))
# Desviación vertical per-nodo [m]. shape (n_sta, n_wl).
# La Z efectiva del nodo (i, j) = z_waterlines[j] + z_offsets[i, j].
# Default: ceros → todos los nodos en los planos horizontales de referencia.
z_offsets: np.ndarray = field(default_factory=lambda: np.zeros((0, 0)))
# Desviación longitudinal per-nodo [m]. shape (n_sta, n_wl).
# La X efectiva del nodo (i, j) = x_stations[i] + x_offsets[i, j].
# x_stations es la referencia paramétrica FIJA; nunca se modifica en drag.
x_offsets: np.ndarray = field(default_factory=lambda: np.zeros((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)
self.keel_z = np.asarray(self.keel_z, dtype=float)
self.z_offsets = np.asarray(self.z_offsets, dtype=float)
self.x_offsets = np.asarray(self.x_offsets, 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)]
# Inicializar keel_z si no se proporcionó o tiene dimensión incorrecta
if self.keel_z.shape != (n_sta,):
self.keel_z = np.zeros(n_sta)
# Inicializar z_offsets si no se proporcionó o tiene dimensiones incorrectas
if self.z_offsets.shape != (n_sta, n_wl):
self.z_offsets = np.zeros((n_sta, n_wl))
# Inicializar x_offsets si no se proporcionó o tiene dimensiones incorrectas
if self.x_offsets.shape != (n_sta, n_wl):
self.x_offsets = np.zeros((n_sta, n_wl))
# ------------------------------------------------------------------
# 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 + self.z_offsets[i, :]).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]."""
# Para cada estación: interpola la semi-manga a la altura z usando las
# z efectivas per-nodo (z_waterlines[j] + z_offsets[i, j]).
col_y = np.array([
float(np.interp(z, self.z_waterlines + self.z_offsets[i, :], self.data[i, :]))
for i in range(len(self.x_stations))
])
# Interpolar el resultado en x
return float(np.interp(x, self.x_stations, 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})"
)