158 lines
4.5 KiB
Python
158 lines
4.5 KiB
Python
"""
|
||
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,
|
||
)
|