S1005403_RisCC/temp.py

197 lines
9.0 KiB
Python

# target_simulator/core/models.py
"""
Defines the core data models for the application, including 3D dynamic Targets
with programmable trajectories and Scenarios.
"""
from __future__ import annotations
import math
from enum import Enum
from dataclasses import dataclass, field
from typing import Dict, List, Optional
# --- Constants ---
MIN_TARGET_ID = 0
MAX_TARGET_ID = 15
KNOTS_TO_FPS = 1.68781
FPS_TO_KNOTS = 1 / KNOTS_TO_FPS
NM_TO_FT = 6076.12
class ManeuverType(Enum):
FLY_TO_POINT = "Fly to Point"
FLY_FOR_DURATION = "Fly for Duration"
@dataclass
class Waypoint:
"""Represents a segment of a target's trajectory, defining a maneuver."""
maneuver_type: ManeuverType = ManeuverType.FLY_FOR_DURATION
# Parameters used by FLY_FOR_DURATION and as final state for FLY_TO_POINT
target_velocity_fps: Optional[float] = None
target_heading_deg: Optional[float] = None
# Parameters used by both
duration_s: Optional[float] = 10.0
target_altitude_ft: Optional[float] = 10000.0
# Parameters used by FLY_TO_POINT
target_range_nm: Optional[float] = None
target_azimuth_deg: Optional[float] = None
# Internal state for interpolation, set when waypoint becomes active
_start_velocity_fps: float = field(init=False, repr=False, default=0.0)
_start_heading_deg: float = field(init=False, repr=False, default=0.0)
_start_altitude_ft: float = field(init=False, repr=False, default=0.0)
_calculated_duration_s: Optional[float] = field(init=False, default=None, repr=False)
def to_dict(self) -> Dict:
data = {"maneuver_type": self.maneuver_type.value}
# Use dataclasses.fields to iterate reliably
for f in field(self):
if not f.name.startswith('_') and f.name != "maneuver_type":
val = getattr(self, f.name)
if val is not None:
data[f.name] = val
return data
@dataclass
class Target:
"""Represents a dynamic 3D target with a programmable trajectory."""
target_id: int
trajectory: List[Waypoint] = field(default_factory=list)
active: bool = True
traceable: bool = True
# --- Current simulation state (dynamic) ---
current_range_nm: float = field(init=False, default=0.0)
current_azimuth_deg: float = field(init=False, default=0.0)
current_altitude_ft: float = field(init=False, default=0.0)
current_velocity_fps: float = field(init=False, default=0.0)
current_heading_deg: float = field(init=False, default=0.0)
current_pitch_deg: float = field(init=False, default=0.0)
_pos_x_ft: float = field(init=False, repr=False, default=0.0)
_pos_y_ft: float = field(init=False, repr=False, default=0.0)
_pos_z_ft: float = field(init=False, repr=False, default=0.0)
_sim_time_s: float = field(init=False, default=0.0)
_current_waypoint_index: int = field(init=False, default=-1)
_time_in_waypoint_s: float = field(init=False, default=0.0)
def __post_init__(self):
if not (MIN_TARGET_ID <= self.target_id <= MAX_TARGET_ID):
raise ValueError(f"Target ID must be between {MIN_TARGET_ID} and {MAX_TARGET_ID}.")
self.reset_simulation()
def reset_simulation(self):
"""Resets the target to its initial state defined by the first waypoint."""
self._sim_time_s = 0.0
self._current_waypoint_index = -1
self._time_in_waypoint_s = 0.0
if not self.trajectory or self.trajectory[0].maneuver_type != ManeuverType.FLY_TO_POINT:
self._pos_x_ft = self._pos_y_ft = self._pos_z_ft = 0.0
self.current_velocity_fps = 0.0; self.current_heading_deg = 0.0; self.current_pitch_deg = 0.0
else:
first_wp = self.trajectory[0]
self.current_range_nm = first_wp.target_range_nm or 0.0
self.current_azimuth_deg = first_wp.target_azimuth_deg or 0.0
self.current_altitude_ft = first_wp.target_altitude_ft or 0.0
self.current_velocity_fps = first_wp.target_velocity_fps or 0.0
self.current_heading_deg = first_wp.target_heading_deg or 0.0
self.current_pitch_deg = 0.0 # Pitch is calculated, not set directly at start
range_ft = self.current_range_nm * NM_TO_FT
az_rad = math.radians(self.current_azimuth_deg)
self._pos_x_ft = range_ft * math.sin(az_rad)
self._pos_y_ft = range_ft * math.cos(az_rad)
self._pos_z_ft = self.current_altitude_ft
self._activate_next_waypoint()
self._update_current_polar_coords()
def _activate_next_waypoint(self):
"""Pre-calculates and activates the next waypoint in the trajectory."""
self._current_waypoint_index += 1
self._time_in_waypoint_s = 0.0
if self._current_waypoint_index >= len(self.trajectory):
self._current_waypoint_index = -1; return
wp = self.trajectory[self._current_waypoint_index]
wp._start_velocity_fps = self.current_velocity_fps
wp._start_heading_deg = self.current_heading_deg
wp._start_altitude_ft = self.current_altitude_ft
if wp.maneuver_type == ManeuverType.FLY_TO_POINT:
target_range_ft = (wp.target_range_nm or 0.0) * NM_TO_FT
target_az_rad = math.radians(wp.target_azimuth_deg or 0.0)
target_x = target_range_ft * math.sin(target_az_rad)
target_y = target_range_ft * math.cos(target_az_rad)
target_z = wp.target_altitude_ft or self._pos_z_ft
dx, dy, dz = target_x - self._pos_x_ft, target_y - self._pos_y_ft, target_z - self._pos_z_ft
dist_3d = math.sqrt(dx**2 + dy**2 + dz**2)
dist_2d = math.sqrt(dx**2 + dy**2)
# These become the constant kinematics for this segment
self.current_heading_deg = math.degrees(math.atan2(dx, dy)) % 360
self.current_pitch_deg = math.degrees(math.atan2(dz, dist_2d)) if dist_2d > 1 else 90.0 if dz > 0 else -90.0
if wp.duration_s and wp.duration_s > 0:
self.current_velocity_fps = dist_3d / wp.duration_s
wp._calculated_duration_s = wp.duration_s
else: # Invalid waypoint, make it static
self.current_velocity_fps = 0
wp._calculated_duration_s = 999999
def update_state(self, delta_time_s: float):
if not self.active or self._current_waypoint_index == -1: return
self._sim_time_s += delta_time_s
self._time_in_waypoint_s += delta_time_s
wp = self.trajectory[self._current_waypoint_index]
duration = wp._calculated_duration_s if wp._calculated_duration_s is not None else wp.duration_s or 0.0
if self._time_in_waypoint_s >= duration:
# Finalize state to match waypoint target
if wp.maneuver_type == ManeuverType.FLY_FOR_DURATION:
self.current_velocity_fps = wp.target_velocity_fps or self.current_velocity_fps
self.current_heading_deg = wp.target_heading_deg or self.current_heading_deg
self.current_altitude_ft = wp.target_altitude_ft or self.current_altitude_ft
self._activate_next_waypoint()
if self._current_waypoint_index == -1: return
wp = self.trajectory[self._current_waypoint_index] # Get new active waypoint
duration = wp._calculated_duration_s if wp._calculated_duration_s is not None else wp.duration_s or 0.0
progress = self._time_in_waypoint_s / duration if duration > 0 else 1.0
if wp.maneuver_type == ManeuverType.FLY_FOR_DURATION:
# Interpolate kinematics during the maneuver
self.current_velocity_fps = wp._start_velocity_fps + ((wp.target_velocity_fps or wp._start_velocity_fps) - wp._start_velocity_fps) * progress
self.current_altitude_ft = wp._start_altitude_ft + ((wp.target_altitude_ft or wp._start_altitude_ft) - wp._start_altitude_ft) * progress
delta_heading = ((wp.target_heading_deg or wp._start_heading_deg) - wp._start_heading_deg + 180) % 360 - 180
self.current_heading_deg = (wp._start_heading_deg + delta_heading * progress) % 360
# Update position using current 3D vector
heading_rad = math.radians(self.current_heading_deg)
pitch_rad = math.radians(self.current_pitch_deg)
dist_moved = self.current_velocity_fps * delta_time_s
dist_2d = dist_moved * math.cos(pitch_rad)
self._pos_x_ft += dist_2d * math.sin(heading_rad)
self._pos_y_ft += dist_2d * math.cos(heading_rad)
self._pos_z_ft += dist_moved * math.sin(pitch_rad)
self._update_current_polar_coords()
def _update_current_polar_coords(self):
"""Recalculates current polar coordinates from Cartesian ones."""
self.current_range_nm = math.sqrt(self._pos_x_ft**2 + self._pos_y_ft**2) / NM_TO_FT
self.current_azimuth_deg = math.degrees(math.atan2(self._pos_x_ft, self._pos_y_ft))
self.current_altitude_ft = self._pos_z_ft
# ... il resto dei metodi come to_dict e la classe Scenario rimangono uguali a prima ...