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:
@@ -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.20–0.35 — cruceros, pesqueros, ferrys.",
|
||||
"semi_disp":
|
||||
"Compromiso entre planeo y desplazamiento.\n"
|
||||
"Fn 0.35–0.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}")
|
||||
|
||||
@@ -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.60–0.80).
|
||||
cm : float
|
||||
Coeficiente de cuaderna maestra (0.93–0.98).
|
||||
bilge_radius_frac : float
|
||||
Radio del pantoque / calado (0.06–0.14).
|
||||
flat_bottom_frac : float
|
||||
Ancho del fondo plano / manga (0.85–0.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))
|
||||
|
||||
@@ -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.82–0.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.45–0.65).
|
||||
lcb_frac : float
|
||||
Posición del LCB como fracción de Lpp desde AP (0.50–0.55).
|
||||
cm : float
|
||||
Coeficiente de cuaderna maestra (0.82–0.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))
|
||||
|
||||
@@ -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 (0–1).
|
||||
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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.92–0.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.08–0.18).
|
||||
flat_bottom_frac : float
|
||||
Anchura del fondo plano como fracción de la manga (0.80–0.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))
|
||||
|
||||
Reference in New Issue
Block a user