feat(sprint2): wizard 'Nuevo Proyecto' + 5 generadores paramétricos de casco

Generadores paramétricos (arshipdesign/parametric/):
  - wizard_planing.py    → V-fondo, chine dura, deadrise variable AP→FP
  - wizard_cruiser.py    → Carena redonda, plan form Lackenby, Cm ajustable
  - wizard_workboat.py   → Sección cajón, pantoque duro, fondo plano
  - wizard_sailing_mono.py → Velero fin keel, sección fina, LCB a popa
  - series60.py          → Serie 60 / mercante full, Cm ~ 0.96
  - __init__.py          → API unificada generate_hull(family, lpp, beam, draft)
                           + HullFamily enum con labels, rangos Cb, descripciones

Wizard UI (arshipdesign/ui/dialogs/wizards.py):
  - NewShipWizard: QDialog 4 pasos con barra de progreso animada
  - _StepDimensions:  nombre, Lpp, B, puntal, calado + ratios L/B y B/T en vivo
  - _StepFamily:      6 FamilyCard con HullThumbnail QPainter (sección maestra)
  - _StepRefine:      sliders Cb y LCB, spinboxes discretización
  - _StepPreview:     tabla hidrostáticos completa (V, D, Cb, LCB, KB, Awp...)
  - Al aceptar → Hull cargado en visor 3D del viewport Perspectiva

MainWindow:
  - _on_new_project() abre NewShipWizard (antes creaba proyecto vacío)
  - Tras accept(): carga hull en Viewer3DWidget si disponible

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 07:52:46 -04:00
parent 503e00bfc9
commit 002c00aff3
8 changed files with 1685 additions and 15 deletions
+169 -1
View File
@@ -1 +1,169 @@
# arshipdesign/parametric
"""
arshipdesign.parametric — Generadores paramétricos de cascos.
Exporta la función unificada ``generate_hull`` y los constantes
de familia usados por el wizard de nuevo proyecto.
Uso rápido
----------
>>> from arshipdesign.parametric import generate_hull, HullFamily
>>> hull = generate_hull(
... family=HullFamily.DISPLACEMENT,
... lpp=15.0, beam=4.0, draft=1.60, depth=2.30,
... cb=0.55,
... )
"""
from __future__ import annotations
from enum import Enum, auto
from typing import Any
from arshipdesign.core.hull import Hull
class HullFamily(str, Enum):
"""Familia de carena disponible en el wizard."""
PLANING = "planing" # Planeador — V-fondo, chine dura
DISPLACEMENT = "displacement" # Desplazamiento — carena redonda
SEMI_DISP = "semi_disp" # Semi-desplazamiento
WORKBOAT = "workboat" # Workboat / Supply / Remolcador
SAILING = "sailing" # Velero monocasco fin keel
MERCHANT = "merchant" # Buque mercante / Serie 60
@property
def label_es(self) -> str:
return {
"planing": "Planeo",
"displacement":"Desplazamiento",
"semi_disp": "Semi-desplazamiento",
"workboat": "Workboat / Supply",
"sailing": "Velero",
"merchant": "Mercante / Supply full",
}[self.value]
@property
def cb_default(self) -> float:
return {
"planing": 0.43,
"displacement":0.55,
"semi_disp": 0.50,
"workboat": 0.67,
"sailing": 0.40,
"merchant": 0.70,
}[self.value]
@property
def cb_range(self) -> tuple[float, float]:
return {
"planing": (0.38, 0.48),
"displacement":(0.45, 0.65),
"semi_disp": (0.46, 0.58),
"workboat": (0.60, 0.75),
"sailing": (0.35, 0.46),
"merchant": (0.60, 0.82),
}[self.value]
@property
def description_es(self) -> str:
return {
"planing":
"Embarcación rápida con fondo en V y chine dura.\n"
"Fn > 0.50 — lanchas, patrulleras, RIBs.",
"displacement":
"Carena redondeada de velocidad moderada.\n"
"Fn 0.200.35 — cruceros, pesqueros, ferrys.",
"semi_disp":
"Compromiso entre planeo y desplazamiento.\n"
"Fn 0.350.55 — yates de motor, patrulleras.",
"workboat":
"Sección cajón con pantoque duro.\n"
"Fn < 0.22 — remolcadores, supply, barcazas.",
"sailing":
"Cuerpo fino con quilla de aleta.\n"
"Veleros de recreo y regata.",
"merchant":
"Formas llenas tipo Serie 60.\n"
"Fn < 0.20 — carga, RORO, buques de trabajo.",
}[self.value]
# ---------------------------------------------------------------------------
# Función unificada
# ---------------------------------------------------------------------------
def generate_hull(
family: HullFamily | str,
lpp: float,
beam: float,
draft: float,
depth: float | None = None,
name: str = "",
n_stations: int = 21,
n_waterlines: int = 11,
**kwargs: Any,
) -> Hull:
"""Genera un Hull paramétrico de la familia indicada.
Parámetros
----------
family : HullFamily
Tipo de carena (ver HullFamily).
lpp : float
Eslora entre perpendiculares [m].
beam : float
Manga máxima [m].
draft : float
Calado de diseño [m].
depth : float, optional
Puntal de trazado [m]. Si es None usa draft * 1.45.
name : str
Nombre del proyecto/casco.
n_stations : int
Número de estaciones transversales (≥ 7, default 21).
n_waterlines : int
Número de líneas de agua (≥ 5, default 11).
**kwargs
Parámetros adicionales específicos de cada familia
(p.ej. deadrise_mid para planing, cb para displacement, etc.)
Retorna
-------
Hull
"""
fam = HullFamily(family) if isinstance(family, str) else family
if depth is None:
depth = draft * 1.45
if not name:
name = f"{fam.label_es} {lpp:.0f}m"
common = dict(
name=name, lpp=lpp, beam=beam, draft=draft, depth=depth,
n_stations=n_stations, n_waterlines=n_waterlines,
)
common.update(kwargs)
if fam == HullFamily.PLANING:
from arshipdesign.parametric.wizard_planing import make_planing_hull
return make_planing_hull(**common)
elif fam in (HullFamily.DISPLACEMENT, HullFamily.SEMI_DISP):
from arshipdesign.parametric.wizard_cruiser import make_displacement_hull
if fam == HullFamily.SEMI_DISP and "cb" not in kwargs:
common.setdefault("cb", 0.50)
return make_displacement_hull(**common)
elif fam == HullFamily.WORKBOAT:
from arshipdesign.parametric.wizard_workboat import make_workboat_hull
return make_workboat_hull(**common)
elif fam == HullFamily.SAILING:
from arshipdesign.parametric.wizard_sailing_mono import make_sailing_hull
return make_sailing_hull(**common)
elif fam == HullFamily.MERCHANT:
from arshipdesign.parametric.series60 import make_merchant_hull
return make_merchant_hull(**common)
else:
raise ValueError(f"Familia de carena desconocida: {fam!r}")
+110 -2
View File
@@ -1,2 +1,110 @@
"""Serie 60. Stub — Sprint 11."""
raise NotImplementedError("series60 — Sprint 11")
"""
Generador paramétrico — Buque mercante / Supply full (inspirado en Serie 60).
Genera cascos para buques de carga, supply vessels y embarcaciones de trabajo
con coeficientes de bloque altos:
- Fondo casi plano en la zona central
- Cuadernas rectangulares con pantoque redondeado
- Proa de bulbo (simplificada) o proa recta
- Popa transom o popa de crucero
Parámetros típicos:
Cb: 0.60 0.80
Froude: 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 make_merchant_hull(
name: str = "Buque Mercante / Supply",
lpp: float = 20.0,
beam: float = 6.0,
draft: float = 2.40,
depth: float = 3.20,
cb: float = 0.70,
lcb_frac: float = 0.50,
cm: float = 0.96, # sección maestra casi rectangular
bilge_radius_frac: float = 0.08,
flat_bottom_frac: float = 0.90,
n_stations: int = 21,
n_waterlines: int = 11,
) -> Hull:
"""Genera un casco tipo Serie 60 / buque mercante.
Parámetros
----------
cb : float
Coeficiente de bloque objetivo (0.600.80).
cm : float
Coeficiente de cuaderna maestra (0.930.98).
bilge_radius_frac : float
Radio del pantoque / calado (0.060.14).
flat_bottom_frac : float
Ancho del fondo plano / manga (0.850.94).
"""
x_sta = np.linspace(0.0, lpp, n_stations)
z_wl = np.linspace(0.0, draft, n_waterlines)
xi = (x_sta / lpp - 0.5) * 2.0
lcb_shift = 2.0 * (lcb_frac - 0.5)
f_plan = _merchant_plan_form(xi, cb, lcb_shift)
r_bilge = draft * bilge_radius_frac
y_flat_b = (beam / 2.0) * flat_bottom_frac
data = np.zeros((n_stations, n_waterlines))
for i in range(n_stations):
y_wl = (beam / 2.0) * f_plan[i]
scale = f_plan[i]
y_flat_i = y_flat_b * scale
r_bilge_i = r_bilge * max(scale, 0.25)
for j, z in enumerate(z_wl):
if z <= r_bilge_i:
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:
# Costados casi verticales (Cm alto)
t = (z - r_bilge_i) / max(draft - r_bilge_i, 1e-6)
# Cm ajusta plenitud de la zona de costados
flare_side = (1.0 - cm) * 0.15
y = y_flat_i + (y_wl - y_flat_i) * (t + flare_side * t * (1 - t))
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
)
def _merchant_plan_form(
xi: np.ndarray, cb: float, lcb_shift: float
) -> np.ndarray:
"""Plan form mercante: central muy llena, extremos redondeados."""
n = _merchant_plan_exponent(cb)
xi_s = np.clip(xi - lcb_shift, -1.0, 1.0)
return np.maximum(0.0, 1.0 - np.abs(xi_s) ** n)
def _merchant_plan_exponent(cb: float) -> float:
cb_vals = [0.58, 0.65, 0.70, 0.75, 0.82]
n_vals = [3.0, 4.0, 5.0, 6.0, 8.0]
return float(np.interp(cb, cb_vals, n_vals))
+130 -2
View File
@@ -1,2 +1,130 @@
"""Wizard crucero. Stub — Sprint 11."""
raise NotImplementedError("wizard_cruiser — Sprint 11")
"""
Generador paramétrico — Casco de Desplazamiento / Crucero (carena redonda).
Genera una tabla de offsets para embarcaciones de desplazamiento:
- Carena redondeada (round bilge)
- Sección maestra llena (Cm 0.820.90)
- Proa fina, popa de espejo o crucero
- LCB ajustable
Parámetros típicos:
Cb: 0.45 0.65
Velocidad/Froude: Fn 0.20 0.35
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 make_displacement_hull(
name: str = "Crucero de Desplazamiento",
lpp: float = 12.0,
beam: float = 3.80,
draft: float = 1.40,
depth: float = 2.20,
cb: float = 0.55,
lcb_frac: float = 0.52, # fracción de Lpp desde AP
cm: float = 0.86, # coeficiente de cuaderna maestra
n_stations: int = 21,
n_waterlines: int = 11,
) -> Hull:
"""Genera un casco de desplazamiento de carena redonda.
Parámetros
----------
cb : float
Coeficiente de bloque objetivo (0.450.65).
lcb_frac : float
Posición del LCB como fracción de Lpp desde AP (0.500.55).
cm : float
Coeficiente de cuaderna maestra (0.820.92).
"""
x_sta = np.linspace(0.0, lpp, n_stations)
z_wl = np.linspace(0.0, draft, n_waterlines)
xi = (x_sta / lpp - 0.5) * 2.0 # ∈ [1, 1], 0=midship
# ── Plan form (semi-manga en flotación) ────────────────────────────
# LCB desplazado del midship
lcb_shift = 2.0 * (lcb_frac - 0.5) # ∈ [1, 1]
f_plan = _displacement_plan_form(xi, cb, lcb_shift)
# ── Exponente de forma de sección ──────────────────────────────────
# alpha controla la plenitud de la sección transversal
# alpha pequeño → sección llena (Cm alto)
# alpha = 2*(1 - Cm) según aproximación de Munro-Smith
alpha_mid = max(0.25, 2.0 * (1.0 - cm)) # ≈ 0.28 para Cm=0.86
data = np.zeros((n_stations, n_waterlines))
for i in range(n_stations):
y_wl = (beam / 2.0) * f_plan[i]
# El exponente de sección varía: más fino en proa/popa
local_fullness = f_plan[i]
# En extremos (local_fullness→0) el exponente sube → sección más en V
alpha = alpha_mid + (1.0 - alpha_mid) * (1.0 - local_fullness ** 0.5)
alpha = np.clip(alpha, alpha_mid, 0.80)
for j, z in enumerate(z_wl):
v = z / draft # ∈ [0, 1]
data[i, j] = y_wl * (v ** alpha)
data = np.clip(data, 0.0, None)
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
)
def _displacement_plan_form(
xi: np.ndarray, cb: float, lcb_shift: float
) -> np.ndarray:
"""
Plan form normalizada para carena de desplazamiento.
Usa una distribución tipo seno modificado con el apex desplazado
según la posición del LCB.
xi ∈ [1, 1], 1=AP, +1=FP.
Retorna f ∈ [0, 1].
"""
# Exponent de la plan form: más alto = más llena = Cb mayor
n = _plan_exponent(cb)
xi_shifted = xi - lcb_shift
xi_shifted = np.clip(xi_shifted, -1.0, 1.0)
# Plan form base: bell curve asimétrica
# Parte de entrada (FP): más suave
# Parte de salida (AP): ligeramente más llena
f = np.zeros_like(xi)
for k, x in enumerate(xi_shifted):
if x <= 0:
# Run (desde midship hacia AP): ligeramente más llena
t = -x
f[k] = max(0.0, 1.0 - (t ** (n * 0.90)))
else:
# Entry (desde midship hacia FP): estándar
t = x
f[k] = max(0.0, 1.0 - (t ** n))
return f
def _plan_exponent(cb: float) -> float:
"""Mapa Cb → exponente de plan form."""
cb_vals = [0.35, 0.45, 0.55, 0.65, 0.75]
n_vals = [1.2, 1.6, 2.0, 2.7, 3.5]
return float(np.interp(cb, cb_vals, n_vals))
+158 -2
View File
@@ -1,2 +1,158 @@
"""Wizard planeador. Stub — Sprint 11."""
raise NotImplementedError("wizard_planing — Sprint 11")
"""
Generador paramétrico — Casco Planeador (V-fondo, chine dura).
Genera una tabla de offsets realista para embarcaciones de planeo:
- Fondo en V con ángulo de astilla muerta variable
- Chine dura recorriendo toda la eslora
- Popa de espejo plana
- Proa aguda con alto ángulo de astilla muerta
- Sección transversal: dos regiones (bajo/sobre chine)
Parámetros típicos:
Cb: 0.40 0.48
Deadrise midship: 12° 22°
Deadrise bow: 35° 55°
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
# ---------------------------------------------------------------------------
# API pública
# ---------------------------------------------------------------------------
def make_planing_hull(
name: str = "Planeador",
lpp: float = 10.0,
beam: float = 3.0,
draft: float = 0.70,
depth: float = 1.20,
deadrise_mid: float = 18.0, # grados en cuaderna maestra
deadrise_bow: float = 45.0, # grados en proa
deadrise_stern: float = 8.0, # grados en popa
chine_frac: float = 0.55, # z_chine / draft
flare: float = 0.10, # ensanchamiento por encima del chine [fracción]
n_stations: int = 21,
n_waterlines: int = 11,
) -> Hull:
"""Genera un casco planeador con forma V y chine dura.
Parámetros
----------
deadrise_mid : float
Ángulo de astilla muerta [°] en la cuaderna maestra (midship).
Valores típicos: 12°–22°.
deadrise_bow : float
Ángulo de astilla muerta [°] en proa.
Valores típicos: 35°–55°.
chine_frac : float
Altura del chine como fracción del calado (01).
flare : float
Fracción de ensanchamiento por encima del chine (0 = sin ensanche).
"""
x_sta = np.linspace(0.0, lpp, n_stations)
z_wl = np.linspace(0.0, draft, n_waterlines)
xi = (x_sta / lpp - 0.5) * 2.0 # normalizado ∈ [1, 1], 0=midship
# ── Plan form: ancho en línea de agua por estación ─────────────────
# El planeador tiene popa muy ancha y proa más estrecha
# f_stern > f_bow para que la popa sea la sección más llena
f_plan = _planing_plan_form(xi) # ∈ [0, 1]
# ── Ángulo de astilla muerta varía a lo largo de la eslora ─────────
dr_arr = _deadrise_distribution(xi, deadrise_mid, deadrise_bow, deadrise_stern)
data = np.zeros((n_stations, n_waterlines))
for i in range(n_stations):
y_max = (beam / 2.0) * f_plan[i]
z_c = draft * chine_frac # altura del chine
dr = np.radians(dr_arr[i]) # ángulo de astilla en esta estación
for j, z in enumerate(z_wl):
if z <= z_c:
# ── Zona V-fondo (bajo el chine) ──────────────────────
# y_chine se calcula desde el ángulo de astilla muerta
# tan(dr) = z_c / y_chine → y_chine = z_c / tan(dr)
tan_dr = max(np.tan(dr), 0.01)
y_chine_dr = z_c / tan_dr
y_chine = min(y_chine_dr, y_max)
y = y_chine * (z / z_c)
else:
# ── Zona sobre el chine (costado) ─────────────────────
tan_dr = max(np.tan(dr), 0.01)
y_chine_dr = z_c / tan_dr
y_chine = min(y_chine_dr, y_max)
# Ligero ensanchamiento por encima del chine
y = y_chine + (y_max - y_chine) * flare * (z - z_c) / (draft - z_c + 1e-9)
y = min(y, y_max)
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
)
# ---------------------------------------------------------------------------
# Helpers internos
# ---------------------------------------------------------------------------
def _planing_plan_form(xi: np.ndarray) -> np.ndarray:
"""
Plan form del planeador: sección de popa llena, proa estrecha.
xi ∈ [1, 1], xi=1 AP (popa), xi=+1 FP (proa).
El planeador tiene la sección maestra desplazada hacia popa (~35% Lpp desde FP).
"""
# Apex en xi ≈ 0.3 (30% desde popa) → máximo ancho allí
apex = -0.30
# Forma asimétrica: run suave hacia popa, proa afilada
f = np.zeros_like(xi)
for k, x in enumerate(xi):
if x >= apex:
# Desde apex hacia proa: caída rápida (proa aguda)
t = (x - apex) / (1.0 - apex)
f[k] = (1.0 - t ** 1.6)
else:
# Desde apex hacia popa: espejo plano, caída suave
t = (apex - x) / (1.0 + apex)
f[k] = (1.0 - t ** 2.8)
return np.maximum(f, 0.0)
def _deadrise_distribution(
xi: np.ndarray,
dr_mid: float,
dr_bow: float,
dr_stern: float,
) -> np.ndarray:
"""Distribución del ángulo de astilla muerta a lo largo de la eslora.
Varía suavemente de dr_stern (popa) → dr_mid (midship) → dr_bow (proa).
xi ∈ [1, 1], 1=AP, +1=FP.
"""
dr = np.zeros_like(xi)
for k, x in enumerate(xi):
if x >= 0.0:
# midship → proa
t = x
dr[k] = dr_mid + (dr_bow - dr_mid) * t ** 1.5
else:
# midship → popa
t = -x
dr[k] = dr_mid + (dr_stern - dr_mid) * t ** 2.0
return dr
+116 -2
View File
@@ -1,2 +1,116 @@
"""Wizard velero mono. Stub — Sprint 11."""
raise NotImplementedError("wizard_sailing_mono — Sprint 11")
"""
Generador paramétrico — Velero Monocasco (fin keel).
Genera cascos para veleros de quilla de aleta:
- Cuerpo del casco: fino, secciones en V
- Quilla de aleta: apéndice separado con perfil NACA simplificado
- Proa fina (ángulo de entrada 10°–16°)
- Popa de espejo amplia
Parámetros típicos:
Cb (cuerpo): 0.35 0.44
L/B: 3.0 3.8
Froude de diseño: 0.30 0.45
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 make_sailing_hull(
name: str = "Velero Monocasco",
lpp: float = 10.0,
beam: float = 3.20,
draft: float = 0.55, # calado del cuerpo (sin quilla)
depth: float = 1.80,
draft_with_keel: float = 1.80, # calado total con quilla
cb: float = 0.40,
lcb_frac: float = 0.53, # ligeramente a popa del midship
cm: float = 0.75,
deadrise_mid: float = 18.0,
n_stations: int = 21,
n_waterlines: int = 9,
) -> Hull:
"""Genera un casco de velero de quilla de aleta.
El Hull resultante representa el cuerpo del casco sin la quilla.
La quilla de aleta se añade como apéndice en el módulo de geometría.
Parámetros
----------
draft : float
Calado del cuerpo del casco (sin quilla) [m].
draft_with_keel : float
Calado total incluyendo la quilla [m].
deadrise_mid : float
Ángulo de astilla muerta en cuaderna maestra [°].
"""
x_sta = np.linspace(0.0, lpp, n_stations)
z_wl = np.linspace(0.0, draft, n_waterlines)
xi = (x_sta / lpp - 0.5) * 2.0
lcb_shift = 2.0 * (lcb_frac - 0.5)
# Plan form: fina en proa, moderadamente llena a popa
f_plan = _sailing_plan_form(xi, cb, lcb_shift)
# Exponente de sección
alpha_mid = max(0.30, 2.0 * (1.0 - cm)) # ≈ 0.50 para Cm=0.75
data = np.zeros((n_stations, n_waterlines))
dr_mid_rad = np.radians(deadrise_mid)
for i in range(n_stations):
y_wl = (beam / 2.0) * f_plan[i]
# Interpolar entre sección en V (extremos) y sección redonda (midship)
local_f = f_plan[i]
alpha = alpha_mid + (0.75 - alpha_mid) * (1.0 - local_f ** 0.7)
alpha = np.clip(alpha, alpha_mid, 0.80)
for j, z in enumerate(z_wl):
v = z / draft
y = y_wl * (v ** alpha)
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
)
def _sailing_plan_form(
xi: np.ndarray, cb: float, lcb_shift: float
) -> np.ndarray:
"""Plan form para velero: fina en proa, moderada en popa."""
n = _sailing_plan_exponent(cb)
xi_s = np.clip(xi - lcb_shift, -1.0, 1.0)
f = np.zeros_like(xi)
for k, x in enumerate(xi_s):
if x <= 0:
# Run (AP): más llena que la entry
t = -x
f[k] = max(0.0, 1.0 - t ** (n * 0.85))
else:
# Entry (FP): fina, típico de velero
t = x
f[k] = max(0.0, 1.0 - t ** n)
return f
def _sailing_plan_exponent(cb: float) -> float:
cb_vals = [0.30, 0.36, 0.42, 0.48, 0.54]
n_vals = [1.0, 1.3, 1.6, 2.0, 2.4]
return float(np.interp(cb, cb_vals, n_vals))
+112 -2
View File
@@ -1,2 +1,112 @@
"""Wizard workboat. Stub — Sprint 11."""
raise NotImplementedError("wizard_workboat Sprint 11")
"""
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.920.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 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.080.18).
flat_bottom_frac : float
Anchura del fondo plano como fracción de la manga (0.800.92).
"""
x_sta = np.linspace(0.0, lpp, n_stations)
z_wl = np.linspace(0.0, draft, 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
)
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))