Files
venus/dbus-anchor-alarm/catenary.py
2026-03-26 14:15:02 +00:00

158 lines
4.5 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.
"""
Catenary math for anchor rode calculations.
All functions are pure — no state, no D-Bus. They accept numeric inputs
and return plain values or lightweight dataclasses.
"""
import math
from dataclasses import dataclass
@dataclass(frozen=True)
class CatenaryResult:
total_distance_ft: float
horizontal_catenary_ft: float
chain_on_bottom_ft: float
cl_suspended_ft: float
all_chain_airborne: bool
pull_angle_deg: float
@dataclass(frozen=True)
class DynamicLoadResult:
total_force_lbs: float
wind_force_lbs: float
centripetal_force_lbs: float
snatch_factor: float
is_snatch: bool
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def wind_force_lbs(
wind_speed_kts: float,
windage_area_sqft: float,
drag_coefficient: float,
) -> float:
"""Steady-state aerodynamic force on the vessel (lbs)."""
return 0.0034 * wind_speed_kts ** 2 * windage_area_sqft * drag_coefficient
def catenary_distance(
chain_length_ft: float,
depth_ft: float,
freeboard_ft: float,
force_lbs: float,
chain_weight_lb_per_ft: float,
) -> CatenaryResult:
"""Horizontal distance from vessel bow-roller to anchor via catenary geometry."""
if chain_length_ft <= 0:
return CatenaryResult(0.0, 0.0, 0.0, 0.0, False, 0.0)
h = depth_ft + freeboard_ft
if h <= 0:
return CatenaryResult(chain_length_ft, 0.0, chain_length_ft, 0.0, False, 0.0)
if force_lbs <= 0 or chain_weight_lb_per_ft <= 0:
cl_suspended = h
chain_on_bottom = max(chain_length_ft - h, 0.0)
return CatenaryResult(
total_distance_ft=chain_on_bottom,
horizontal_catenary_ft=0.0,
chain_on_bottom_ft=chain_on_bottom,
cl_suspended_ft=min(cl_suspended, chain_length_ft),
all_chain_airborne=chain_length_ft <= h,
pull_angle_deg=90.0 if chain_length_ft <= h else 0.0,
)
cl_suspended = math.sqrt(
2.0 * h * force_lbs / chain_weight_lb_per_ft + h * h
)
all_chain_airborne = cl_suspended >= chain_length_ft
if all_chain_airborne:
cl_suspended = chain_length_ft
chain_on_bottom = max(chain_length_ft - cl_suspended, 0.0)
horizontal_catenary = force_lbs / chain_weight_lb_per_ft
total_distance = horizontal_catenary + chain_on_bottom
if all_chain_airborne:
pull_angle = math.degrees(math.atan2(h, horizontal_catenary))
else:
pull_angle = 0.0
return CatenaryResult(
total_distance_ft=total_distance,
horizontal_catenary_ft=horizontal_catenary,
chain_on_bottom_ft=chain_on_bottom,
cl_suspended_ft=cl_suspended,
all_chain_airborne=all_chain_airborne,
pull_angle_deg=pull_angle,
)
def scope_ratio(
chain_length_ft: float,
depth_ft: float,
freeboard_ft: float,
) -> float:
"""Simple scope ratio: chain length / total height."""
h = depth_ft + freeboard_ft
if h <= 0:
return 0.0
return chain_length_ft / h
def recommended_chain_length(
depth_ft: float,
freeboard_ft: float,
is_all_chain: bool = True,
) -> float:
"""NauticEd recommended rode length (feet).
All-chain: 50 ft + 2 × depth_ft
Chain + rope: 50 ft + 4 × depth_ft
*depth_ft* is water depth only (freeboard excluded from formula).
*freeboard_ft* accepted for API symmetry but unused.
"""
_ = freeboard_ft
multiplier = 2.0 if is_all_chain else 4.0
return 50.0 + multiplier * depth_ft
def dynamic_load(
wind_force_lbs: float,
vessel_weight_lbs: float,
lateral_speed_kts: float,
chain_length_ft: float,
is_swing_reversal: bool = False,
) -> DynamicLoadResult:
"""Total anchor load including swing / snatch dynamics."""
lateral_speed_ft_s = lateral_speed_kts * 1.687
vessel_mass_slugs = vessel_weight_lbs / 32.174
if chain_length_ft > 0:
f_centripetal = vessel_mass_slugs * lateral_speed_ft_s ** 2 / chain_length_ft
else:
f_centripetal = 0.0
f_total = wind_force_lbs + f_centripetal
snatch_factor = 1.0
if is_swing_reversal:
snatch_factor = 2.0 + 2.0 * min(lateral_speed_kts / 2.0, 1.0)
f_total *= snatch_factor
return DynamicLoadResult(
total_force_lbs=f_total,
wind_force_lbs=wind_force_lbs,
centripetal_force_lbs=f_centripetal,
snatch_factor=snatch_factor,
is_snatch=is_swing_reversal,
)