base_agent/uas/route/routes/water_route.py

399 lines
16 KiB
Python
Raw Normal View History

2026-06-18 03:28:14 +00:00
# -*- coding: utf-8 -*-
"""
水中航线WaterRoute 完整版
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
from typing import List, Optional
from PyQt5.QtCore import QObject
from uas.route.base_params import WaterParams
from uas.route.base_route import BaseRoute
from uas.route.base_segment import BaseSegment, BaseTransition, SegmentKind
from uas.route.base_waypoint import EquipmentDomain
from uas.utils.geo_utils import GeoUtils
# ─────────────────────────────────────────────────────────────────
# 水中直线段
# ─────────────────────────────────────────────────────────────────
@dataclass
class WaterSegment(BaseSegment):
"""水中直线航行段(水面/水下通用)"""
domain: EquipmentDomain = field(default=EquipmentDomain.WATER, repr=False)
kind: SegmentKind = field(default=SegmentKind.STRAIGHT, repr=False)
depth_start: float = 0.0
depth_end: float = 0.0
is_subsurface: bool = False
def query(self, t: float) -> WaterParams:
return _water_seg_interpolate(self, t)
def interpolate(self, ratio: float) -> WaterParams:
t = self.t_start + max(0.0, min(1.0, ratio)) * self.duration
return _water_seg_interpolate(self, t)
def validate(self) -> list:
errors = []
if self.distance < 0:
errors.append("距离不能为负")
if self.duration < 0:
errors.append("时长不能为负")
if self.depth_start < 0 or self.depth_end < 0:
errors.append("深度不能为负")
return errors
@property
def depth_change(self) -> float:
return self.depth_end - self.depth_start
@property
def is_diving(self) -> bool:
return self.depth_change > 1.0
@property
def is_surfacing(self) -> bool:
return self.depth_change < -1.0
def _water_seg_interpolate(seg: WaterSegment, t: float) -> WaterParams:
"""水中直线段内匀加速插值"""
dt = t - seg.t_start
ratio = dt / seg.duration if seg.duration > 1e-6 else 1.0
ratio = max(0.0, min(1.0, ratio))
dist = seg.v_start * dt + 0.5 * seg.acceleration * dt * dt
dist = max(0.0, min(dist, seg.distance))
new_lat, new_lon = GeoUtils.offset_position(seg.start_lat, seg.start_lon, seg.bearing, dist)
# new_lat, new_lon = _offset_position(
# seg.start_lat, seg.start_lon, seg.bearing, dist
# )
speed = max(seg.v_start + seg.acceleration * dt, 0.0)
alt = seg.start_alt + (seg.end_alt - seg.start_alt) * ratio
depth = seg.depth_start + (seg.depth_end - seg.depth_start) * ratio
return WaterParams(
timestamp = t,
latitude = new_lat,
longitude = new_lon,
altitude = alt,
heading = seg.bearing,
speed = speed,
acceleration = seg.acceleration,
depth = depth,
is_subsurface = seg.is_subsurface,
domain = EquipmentDomain.WATER,
)
# ─────────────────────────────────────────────────────────────────
# 水中回转机动段
# ─────────────────────────────────────────────────────────────────
@dataclass
class WaterTurnSegment(BaseTransition):
"""水中回转机动段(舰艇大半径圆弧转向)"""
turn_radius: float = 500.0
depth: float = 0.0
domain: EquipmentDomain = field(default=EquipmentDomain.WATER, repr=False)
kind: SegmentKind = field(default=SegmentKind.MANEUVER, repr=False)
_center_lat: float = field(default=0.0, init=False, repr=False)
_center_lon: float = field(default=0.0, init=False, repr=False)
_exit_lat: float = field(default=0.0, init=False, repr=False)
_exit_lon: float = field(default=0.0, init=False, repr=False)
def __post_init__(self):
self._compute_geometry()
def _compute_geometry(self):
angle = self.turn_angle
r = self.turn_radius
if self.turn_direction >= 0:
center_bearing = (self.entry_heading + 90) % 360
else:
center_bearing = (self.entry_heading - 90) % 360
self._center_lat, self._center_lon = GeoUtils.offset_position(
self.entry_lat, self.entry_lon, center_bearing, r
)
exit_from_center = (center_bearing + 180 + angle) % 360
self._exit_lat, self._exit_lon = GeoUtils.offset_position(
self._center_lat, self._center_lon,
(exit_from_center + 180) % 360, r,
)
arc_len = abs(math.radians(angle)) * r
self.duration = arc_len / self.speed if self.speed > 1e-6 else 0.0
@property
def exit_lat(self) -> float:
return self._exit_lat
@property
def exit_lon(self) -> float:
return self._exit_lon
def query(self, t: float) -> WaterParams:
dt = t - self.t_start
ratio = dt / self.duration if self.duration > 1e-6 else 1.0
ratio = max(0.0, min(1.0, ratio))
swept_angle = self.turn_angle * ratio
if self.turn_direction >= 0:
center_bearing = (self.entry_heading + 90) % 360
else:
center_bearing = (self.entry_heading - 90) % 360
entry_from_center = (center_bearing + 180) % 360
current_from_center = (entry_from_center + swept_angle) % 360
cur_lat, cur_lon = GeoUtils.offset_position(
self._center_lat, self._center_lon,
current_from_center, self.turn_radius,
)
cur_heading = (self.entry_heading + swept_angle) % 360
turn_rate = (self.turn_angle / self.duration
if self.duration > 1e-6 else 0.0)
rudder_angle = min(35.0, abs(turn_rate) * 2) * self.turn_direction
return WaterParams(
timestamp = t,
latitude = cur_lat,
longitude = cur_lon,
altitude = self.altitude,
heading = cur_heading,
speed = self.speed,
depth = self.depth,
rudder_angle = rudder_angle,
domain = EquipmentDomain.WATER,
)
def validate(self) -> list:
errors = []
if self.turn_radius <= 0:
errors.append(f"回转半径必须为正: {self.turn_radius}")
if self.speed <= 0:
errors.append(f"速度必须为正: {self.speed}")
return errors
# ─────────────────────────────────────────────────────────────────
# 水中航线
# ─────────────────────────────────────────────────────────────────
class WaterRoute(BaseRoute):
"""水中装备航线(水面舰艇 / 潜艇 / UUV"""
def __init__(
self,
start_time: float = 0.0,
default_turn_radius: float = 500.0,
min_turn_radius: float = 100.0,
is_subsurface: bool = False,
parent: QObject = None,
):
super().__init__(start_time=start_time, parent=parent)
self.default_turn_radius = default_turn_radius
self.min_turn_radius = min_turn_radius
self.is_subsurface = is_subsurface
@property
def domain(self) -> EquipmentDomain:
return EquipmentDomain.WATER
def _rebuild(self) -> None:
self._segments.clear()
self._transitions.clear()
n = len(self._waypoints)
if n < 2:
self.total_duration = 0.0
return
bearings: List[float] = [
# _bearing(
# self._waypoints[i].latitude, self._waypoints[i].longitude,
# self._waypoints[i+1].latitude, self._waypoints[i+1].longitude,
# )
GeoUtils.bearing( self._waypoints[i].latitude, self._waypoints[i].longitude,
self._waypoints[i+1].latitude, self._waypoints[i+1].longitude,)
for i in range(n - 1)
]
t_cursor = self.start_time
prev_exit_lat = self._waypoints[0].latitude
prev_exit_lon = self._waypoints[0].longitude
for i in range(n - 1):
wp_a = self._waypoints[i]
wp_b = self._waypoints[i + 1]
brg_in = bearings[i]
brg_out = bearings[i + 1] if (i + 1) < len(bearings) else brg_in
r = max(
getattr(wp_b, "turn_radius", 0.0) or self.default_turn_radius,
self.min_turn_radius,
)
turn_angle = GeoUtils.heading_diff(brg_in, brg_out)
need_turn = i < n - 2 and abs(turn_angle) > 0.5
if need_turn:
tip_dist_m = r * abs(math.tan(math.radians(turn_angle / 2)))
tip_lat, tip_lon = GeoUtils.offset_position(
wp_b.latitude, wp_b.longitude,
(brg_in + 180) % 360, tip_dist_m,
)
seg_total = GeoUtils.haversine( prev_exit_lat, prev_exit_lon,
wp_b.latitude, wp_b.longitude,)
# seg_total = _haversine(
# prev_exit_lat, prev_exit_lon,
# wp_b.latitude, wp_b.longitude,
# )
tip_check = GeoUtils.haversine(
wp_b.latitude, wp_b.longitude, tip_lat, tip_lon,
)
# tip_check = _haversine(
# wp_b.latitude, wp_b.longitude, tip_lat, tip_lon,
# )
if tip_check > seg_total * 0.95:
need_turn = False
tip_lat, tip_lon = wp_b.latitude, wp_b.longitude
else:
tip_lat, tip_lon = wp_b.latitude, wp_b.longitude
seg_dist = GeoUtils.haversine(
prev_exit_lat, prev_exit_lon, tip_lat, tip_lon
)
# seg_dist = _haversine(
# prev_exit_lat, prev_exit_lon, tip_lat, tip_lon
# )
v0 = wp_a.speed
v1 = wp_b.speed
v_avg = (v0 + v1) / 2.0 or 1.0
duration = seg_dist / v_avg if v_avg > 1e-6 else 0.0
acc = (v1 - v0) / duration if duration > 1e-6 else 0.0
depth_a = getattr(wp_a, "depth", 0.0)
depth_b = getattr(wp_b, "depth", 0.0)
seg = self._make_segment(
wp_from = wp_a, wp_to = wp_b,
start_lat = prev_exit_lat,
start_lon = prev_exit_lon,
start_alt = wp_a.altitude,
end_lat = tip_lat,
end_lon = tip_lon,
end_alt = wp_b.altitude,
bearing = brg_in,
t_start = t_cursor,
v_start = v0, v_end = v1,
acc = acc, dist = seg_dist,
duration = duration,
depth_start = depth_a, depth_end = depth_b,
)
self._segments.append(seg)
t_cursor += duration
if need_turn:
trans = self._make_transition(
entry_lat = tip_lat, entry_lon = tip_lon,
entry_alt = wp_b.altitude,
entry_heading = brg_in, exit_heading = brg_out,
speed = wp_b.speed, altitude = wp_b.altitude,
t_start = t_cursor, turn_radius = r,
depth = depth_b,
)
self._transitions.append(trans)
if trans is not None:
t_cursor += trans.duration
prev_exit_lat = trans.exit_lat
prev_exit_lon = trans.exit_lon
else:
prev_exit_lat = tip_lat
prev_exit_lon = tip_lon
else:
self._transitions.append(None)
prev_exit_lat = tip_lat
prev_exit_lon = tip_lon
self._update_total_duration()
def _make_segment(
self,
wp_from, wp_to,
start_lat, start_lon, start_alt,
end_lat, end_lon, end_alt,
bearing, t_start,
v_start=0.0, v_end=0.0, acc=0.0,
dist=0.0, duration=0.0,
depth_start=0.0, depth_end=0.0,
**kwargs,
) -> WaterSegment:
return WaterSegment(
wp_from = wp_from, wp_to = wp_to,
start_lat = start_lat, start_lon = start_lon,
start_alt = start_alt, end_lat = end_lat,
end_lon = end_lon, end_alt = end_alt,
distance = dist, bearing = bearing,
t_start = t_start, t_end = t_start + duration,
duration = duration, v_start = v_start,
v_end = v_end, acceleration = acc,
depth_start = depth_start, depth_end = depth_end,
is_subsurface = self.is_subsurface,
)
def _make_transition(
self,
entry_lat, entry_lon, entry_alt,
entry_heading, exit_heading,
speed, altitude, t_start,
turn_radius=500.0, depth=0.0,
**kwargs,
) -> Optional[WaterTurnSegment]:
return WaterTurnSegment(
entry_lat = entry_lat, entry_lon = entry_lon,
entry_alt = entry_alt, entry_heading = entry_heading,
exit_heading = exit_heading, speed = speed,
altitude = altitude, t_start = t_start,
turn_radius = turn_radius, depth = depth,
)
def _interpolate_segment(self, seg: BaseSegment, t: float) -> WaterParams:
return _water_seg_interpolate(seg, t) # type: ignore[arg-type]
def _state_at_start(self) -> WaterParams:
seg = self._segments[0]
return WaterParams(
timestamp = self.t_start,
latitude = seg.start_lat,
longitude = seg.start_lon,
altitude = seg.start_alt,
heading = seg.bearing,
speed = seg.v_start,
depth = seg.depth_start, # type: ignore[attr-defined]
is_subsurface = self.is_subsurface,
domain = EquipmentDomain.WATER,
)
def _state_at_end(self) -> WaterParams:
last_trans = self._transitions[-1] if self._transitions else None
if last_trans is not None:
s = last_trans.query(last_trans.t_end)
s.timestamp = self.t_end
return s
last_seg = self._segments[-1]
return WaterParams(
timestamp = self.t_end,
latitude = last_seg.end_lat,
longitude = last_seg.end_lon,
altitude = last_seg.end_alt,
heading = last_seg.bearing,
speed = last_seg.v_end,
depth = last_seg.depth_end, # type: ignore[attr-defined]
is_subsurface = self.is_subsurface,
domain = EquipmentDomain.WATER,
)