base_agent/uas/route/routes/water_route.py

399 lines
16 KiB
Python
Raw 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.

# -*- 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,
)