feat(stability): Módulo 3 — Curva GZ + criterios IMO IS Code 2008

- gz_integrator.py: GZCurve, GZPoint, compute_gz_wall_sided (fórmula
  pared lateral), compute_gz_direct (integración Sutherland-Hodgman)
- imo_is2008.py: IMOCriterion, IMOResult, check_imo_is2008 —
  6 criterios A.2.1.1–A.2.1.6 del IS Code 2008 Cap.2
- gz_curve_widget.py: GZCurveWidget QPainter — curva cian, áreas
  sombreadas, líneas IMO, marcador AVS, tabla PASS/FAIL integrada
- main_window.py: GZCurveWidget en MOD_STABILITY, _compute_and_show_gz,
  _on_show_stability conectado al ribbon
- dark.qss: estilos GZCurveWidget
- test_module3_stability.py: 33 tests S-01..S-28 (315 total, todos pasan)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 13:59:32 -04:00
parent 62de89d63c
commit 0f85935fc8
7 changed files with 1871 additions and 3 deletions
+152
View File
@@ -0,0 +1,152 @@
"""
imo_is2008.py — IMO IS Code 2008, Capítulo 2 — criterios de estabilidad intacta.
Criterios A.2.1 para buques de carga general y pequeñas embarcaciones:
2.1.1 Área 030° ≥ 0.055 m·rad
2.1.2 Área 040° ≥ 0.090 m·rad
2.1.3 Área 3040° ≥ 0.030 m·rad
2.1.4 GZ a 30° ≥ 0.200 m
2.1.5 Ángulo de GZ máximo ≥ 25°
2.1.6 GM₀ ≥ 0.150 m
Referencia: IMO MSC.267(85) — IS Code 2008, Parte A, Cap. 2.
"""
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class IMOCriterion:
"""Un criterio individual del IS Code 2008."""
code: str # e.g. "A.2.1.1"
description: str # Descripción corta
required: float # Valor mínimo requerido
achieved: float # Valor obtenido de la curva GZ
unit: str # Unidades (m·rad, m, °)
passed: bool # True si achieved >= required
@dataclass
class IMOResult:
"""Resultado completo de la verificación IMO IS Code 2008."""
criteria: list[IMOCriterion]
overall_passed: bool
def table_rows(self) -> list[tuple[str, str, str, str, bool]]:
"""Devuelve filas para la tabla: (code, description, required_str, achieved_str, passed).
El formato de los strings varía según las unidades:
- m·rad: 4 decimales
- m: 3 decimales
- °: 1 decimal
"""
rows = []
for c in self.criteria:
if c.unit == "m·rad":
req_str = f"{c.required:.4f} {c.unit}"
ach_str = f"{c.achieved:.4f} {c.unit}"
elif c.unit == "m":
req_str = f"{c.required:.3f} {c.unit}"
ach_str = f"{c.achieved:.3f} {c.unit}"
elif c.unit == "°":
req_str = f"{c.required:.1f}{c.unit}"
ach_str = f"{c.achieved:.1f}{c.unit}"
else:
req_str = f"{c.required} {c.unit}"
ach_str = f"{c.achieved} {c.unit}"
rows.append((c.code, c.description, req_str, ach_str, c.passed))
return rows
def check_imo_is2008(gz) -> IMOResult:
"""Verifica todos los criterios IS Code 2008 Cap.2 para la curva GZ dada.
Parameters
----------
gz : GZCurve
Curva de estabilidad calculada.
Returns
-------
IMOResult
Contiene la lista de criterios individuales y el resultado global.
"""
import numpy as np
from arshipdesign.stability.gz_integrator import GZCurve
def _criterion(
code: str,
desc: str,
req: float,
ach: float,
unit: str,
) -> IMOCriterion:
return IMOCriterion(
code=code,
description=desc,
required=req,
achieved=ach,
unit=unit,
passed=(ach >= req),
)
criteria: list[IMOCriterion] = []
# A.2.1.1 — Área bajo la curva GZ entre 0° y 30°
criteria.append(_criterion(
"A.2.1.1",
"Área 030°",
0.055,
float(gz.area_0_30),
"m·rad",
))
# A.2.1.2 — Área bajo la curva GZ entre 0° y 40°
criteria.append(_criterion(
"A.2.1.2",
"Área 040°",
0.090,
float(gz.area_0_40),
"m·rad",
))
# A.2.1.3 — Área bajo la curva GZ entre 30° y 40°
criteria.append(_criterion(
"A.2.1.3",
"Área 3040°",
0.030,
float(gz.area_30_40),
"m·rad",
))
# A.2.1.4 — GZ a 30° de escora
gz30 = float(np.interp(30.0, gz.angles_deg, gz.gz_values))
criteria.append(_criterion(
"A.2.1.4",
"GZ a 30°",
0.200,
gz30,
"m",
))
# A.2.1.5 — Ángulo en que se produce el GZ máximo
criteria.append(_criterion(
"A.2.1.5",
"Ángulo GZ máx",
25.0,
float(gz.phi_gz_max),
"°",
))
# A.2.1.6 — Altura metacéntrica inicial GM₀
criteria.append(_criterion(
"A.2.1.6",
"GM₀",
0.150,
float(gz.gm),
"m",
))
overall = all(c.passed for c in criteria)
return IMOResult(criteria=criteria, overall_passed=overall)