# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import Set, Sequence, Tuple
import numpy as np
from h5py import Group
from hermespy.core import Direction, HDFSerializable, Serializable
from ..channel import ChannelSampleHook, LinkState
from ..consistent import ConsistentGenerator, ConsistentRealization, ConsistentUniform
from .radar import RadarChannelBase, RadarTargetPath, RadarChannelRealization, RadarChannelSample
__author__ = "Andre Noll Barreto"
__copyright__ = "Copyright 2024, Barkhausen Institut gGmbH"
__credits__ = ["Andre Noll Barreto", "Jan Adler"]
__license__ = "AGPLv3"
__version__ = "1.4.0"
__maintainer__ = "Jan Adler"
__email__ = "jan.adler@barkhauseninstitut.org"
__status__ = "Prototype"
[docs]
class SingleTargetRadarChannelRealization(RadarChannelRealization):
"""Realization of a single target radar channel.
Generated by the :meth:`realize<SingleTargetRadarChannel.realize>` method of :class:`SingleTargetRadarChannel`.
"""
def __init__(
self,
consistent_realization: ConsistentRealization,
target_range_variable: ConsistentUniform,
target_azimuth_variable: ConsistentUniform,
target_zenith_variable: ConsistentUniform,
target_velocity_variable: ConsistentUniform,
target_phase_variable: ConsistentUniform,
target_range: float | Tuple[float, float] | None,
target_azimuth: float | Tuple[float, float],
target_zenith: float | Tuple[float, float],
target_cross_section: float,
target_velocity: float | np.ndarray | Tuple[float, float],
attenuate: bool,
sample_hooks: Set[ChannelSampleHook[RadarChannelSample]],
gain: float,
) -> None:
"""
Args:
gain (float):
Linear power gain factor a signal experiences when being propagated over this realization.
"""
# Initialize the base class
RadarChannelRealization.__init__(self, sample_hooks, gain)
# Initialize class attributes
self.__consistent_realization = consistent_realization
self.__target_range_variable = target_range_variable
self.__target_azimuth_variable = target_azimuth_variable
self.__target_zenith_variable = target_zenith_variable
self.__target_velocity_variable = target_velocity_variable
self.__target_phase_variable = target_phase_variable
self.__target_range = target_range
self.__target_azimuth = target_azimuth
self.__target_zenith = target_zenith
self.__target_cross_section = target_cross_section
self.__target_velocity = target_velocity
self.__attenuate = attenuate
def _generate_paths(self, state: LinkState) -> Sequence[RadarTargetPath]:
# If the targe rante is None, then the target is not considered
if self.__target_range is None:
return []
consistent_sample = self.__consistent_realization.sample(
state.transmitter.position, state.receiver.position
)
# Generate the targe's absolute range from the receiver
if isinstance(self.__target_range, (tuple, list)):
target_range = float(
self.__target_range[0]
+ (self.__target_range[1] - self.__target_range[0])
* self.__target_range_variable.sample(consistent_sample)
)
else:
target_range = self.__target_range
# Generate the target's azimuth angle of arrival
if isinstance(self.__target_azimuth, (tuple, list)):
target_azimuth = float(
self.__target_azimuth[0]
+ (self.__target_azimuth[1] - self.__target_azimuth[0])
* self.__target_azimuth_variable.sample(consistent_sample)
)
else:
target_azimuth = self.__target_azimuth
# Generate the target's zenith angle of arrival
if isinstance(self.__target_zenith, (tuple, list)):
target_zenith = float(
self.__target_zenith[0]
+ (self.__target_zenith[1] - self.__target_zenith[0])
* self.__target_zenith_variable.sample(consistent_sample)
)
else:
target_zenith = self.__target_zenith
# Generate the target's direction
unit_direction = Direction.From_Spherical(target_azimuth, target_zenith).view(np.ndarray)
# Generate the target's velocity
if isinstance(self.__target_velocity, (tuple, list)):
absolute_target_velocity = self.__target_velocity[0] + (
self.__target_velocity[1] - self.__target_velocity[0]
) * self.__target_velocity_variable.sample(consistent_sample)
target_velocity = unit_direction * absolute_target_velocity
elif isinstance(self.__target_velocity, np.ndarray): # pragma: no cover
target_velocity = self.__target_velocity
else:
target_velocity = unit_direction * self.__target_velocity
target_path = RadarTargetPath(
unit_direction * target_range,
target_velocity,
self.__target_cross_section,
float(2 * np.pi * self.__target_phase_variable.sample(consistent_sample)),
self.__attenuate,
False,
)
return [target_path]
[docs]
def to_HDF(self, group: Group) -> None:
self.__consistent_realization.to_HDF(
HDFSerializable._create_group(group, "consistent_realization")
)
if self.__target_range is not None:
HDFSerializable._range_to_HDF(group, "target_range", self.__target_range)
HDFSerializable._range_to_HDF(group, "target_azimuth", self.__target_azimuth)
HDFSerializable._range_to_HDF(group, "target_zenith", self.__target_zenith)
group.attrs["target_cross_section"] = self.__target_cross_section
if isinstance(self.__target_velocity, np.ndarray): # pragma: no cover
HDFSerializable._write_dataset(group, "target_velocity", self.__target_velocity)
else:
HDFSerializable._range_to_HDF(group, "target_velocity", self.__target_velocity)
group.attrs["attenuate"] = self.__attenuate
group.attrs["gain"] = self.gain
[docs]
@staticmethod
def From_HDF(
group: Group,
target_range_variable: ConsistentUniform,
target_azimuth_variable: ConsistentUniform,
target_zenith_variable: ConsistentUniform,
target_velocity_variable: ConsistentUniform,
target_phase_variable: ConsistentUniform,
sample_hooks: Set[ChannelSampleHook[RadarChannelSample]],
) -> SingleTargetRadarChannelRealization:
target_velocity: np.ndarray | Tuple[float, float] | float
if "target_velocity" in group: # pragma: no cover
target_velocity = np.asarray(group["target_velocity"], dtype=np.float64)
else:
target_velocity = HDFSerializable._range_from_HDF(group, "target_velocity")
target_range = None
if "target_range" in group: # pragma: no cover
target_range = HDFSerializable._range_from_HDF(group, "target_range")
return SingleTargetRadarChannelRealization(
ConsistentRealization.from_HDF(group["consistent_realization"]),
target_range_variable,
target_azimuth_variable,
target_zenith_variable,
target_velocity_variable,
target_phase_variable,
target_range,
HDFSerializable._range_from_HDF(group, "target_azimuth"),
HDFSerializable._range_from_HDF(group, "target_zenith"),
group.attrs["target_cross_section"],
target_velocity,
group.attrs["attenuate"],
sample_hooks,
group.attrs["gain"],
)
[docs]
class SingleTargetRadarChannel(RadarChannelBase[SingleTargetRadarChannelRealization], Serializable):
"""Model of a radar channel featuring a single reflecting target."""
yaml_tag = "RadarChannel"
__target_range: float | Tuple[float, float]
__radar_cross_section: float
__target_azimuth: float | Tuple[float, float]
__target_zenith: float | Tuple[float, float]
__target_exists: bool
__target_velocity: float | Tuple[float, float] | np.ndarray
def __init__(
self,
target_range: float | Tuple[float, float],
radar_cross_section: float,
target_azimuth: float | Tuple[float, float] = 0.0,
target_zenith: float | Tuple[float, float] = 0.0,
target_exists: bool = True,
velocity: float | Tuple[float, float] | np.ndarray = 0,
attenuate: bool = True,
decorrelation_distance: float = float("inf"),
**kwargs,
) -> None:
"""
Args:
target_range (float | Tuple[float, float]):
Absolute distance of target and radar sensor in meters.
Either a specific distance or a range of minimal and maximal target distance.
radar_cross_section (float):
Radar cross section (RCS) of the assumed single-point reflector in m**2
target_azimuth (float | Tuple[float, float]), optional):
Target location azimuth angle in radians, considering spherical coordinates.
Zero by default.
target_zenith (float | Tuple[float, float]), optional):
Target location zenith angle in radians, considering spherical coordinates.
Zero by default.
target_exists (bool, optional):
True if a target exists, False if there is only noise/clutter (default 0 True)
velocity (float | Tuple[float, float] | np.ndarray , optional):
Velocity as a 3D vector (or as a float), in m/s (default = 0)
attenuate (bool, optional):
If True, then signal will be attenuated depending on the range, RCS and losses.
If False, then received power is equal to transmit power.
Raises:
ValueError:
If radar_cross_section < 0.
If carrier_frequency <= 0.
If more than one antenna is considered.
"""
# Initialize base class
RadarChannelBase.__init__(self, attenuate=attenuate, **kwargs)
# Initialize class properties
self.__consistent_generator = ConsistentGenerator(self)
self.__target_range_variable = self.__consistent_generator.uniform()
self.__target_azimuth_variable = self.__consistent_generator.uniform()
self.__target_zenith_variable = self.__consistent_generator.uniform()
self.__target_velocity_variable = self.__consistent_generator.uniform()
self.__target_phase_variable = self.__consistent_generator.uniform()
self.target_range = target_range
self.radar_cross_section = radar_cross_section
self.target_azimuth = target_azimuth
self.target_zenith = target_zenith
self.target_exists = target_exists
self.target_velocity = velocity
self.decorrelation_distance = decorrelation_distance
@property
def target_range(self) -> float | Tuple[float, float]:
"""Absolute distance of target and radar sensor.
Returns: Target range in meters.
Raises:
ValueError: If the range is smaller than zero.
"""
return self.__target_range
@target_range.setter
def target_range(self, value: float | Tuple[float, float]) -> None:
if isinstance(value, (float, int)):
if value < 0.0:
raise ValueError("Target range must be greater or equal to zero")
elif isinstance(value, (tuple, list)):
if len(value) != 2:
raise ValueError("Target range span must be a tuple of two")
if value[1] < value[0]:
raise ValueError("Target range span second value must be greater than first value")
if value[0] < 0.0:
raise ValueError("Target range span minimum must be greater or equal to zero")
else:
raise ValueError("Unknown targer range format")
self.__target_range = value
@property
def target_velocity(self) -> float | Tuple[float, float] | np.ndarray:
"""Perceived target velocity.
Returns: Velocity in m/s.
"""
return self.__target_velocity
@target_velocity.setter
def target_velocity(self, value: float | Tuple[float, float] | np.ndarray) -> None:
if isinstance(value, (tuple, list)):
if len(value) != 2:
raise ValueError("Target velocity span must be a tuple of two")
if value[1] < value[0]:
raise ValueError(
"Target velocity span second value must be greater than first value"
)
self.__target_velocity = value
@property
def radar_cross_section(self) -> float:
"""Access configured radar cross section.
Returns:
float: radar cross section [m**2]
"""
return self.__radar_cross_section
@radar_cross_section.setter
def radar_cross_section(self, value: float) -> None:
"""Modify the configured number of the radar cross section
Args:
value (float): The new RCS.
Raises:
ValueError: If `value` is less than zero.
"""
if value < 0:
raise ValueError("Radar cross section be greater than or equal to zero")
self.__radar_cross_section = value
@property
def target_azimuth(self) -> float | Tuple[float, float]:
"""Target position azimuth in spherical coordiantes.
Returns:
Azimuth angle in radians.
"""
return self.__target_azimuth
@target_azimuth.setter
def target_azimuth(self, value: float | Tuple[float, float]) -> None:
if isinstance(value, (tuple, list)):
if len(value) != 2:
raise ValueError("Target azimuth span must be a tuple of two")
if value[1] < value[0]:
raise ValueError(
"Target azimuth span second value must be greater than first value"
)
self.__target_azimuth = value
@property
def target_zenith(self) -> float | Tuple[float, float]:
"""Target position zenith in spherical coordiantes.
Returns:
Zenith angle in radians.
"""
return self.__target_zenith
@target_zenith.setter
def target_zenith(self, value: float | Tuple[float, float]) -> None:
if isinstance(value, (tuple, list)):
if len(value) != 2:
raise ValueError("Target zenith span must be a tuple of two")
if value[1] < value[0]:
raise ValueError("Target zenith span second value must be greater than first value")
self.__target_zenith = value
@property
def target_exists(self) -> bool:
"""Does an illuminated target exist?"""
return self.__target_exists
@target_exists.setter
def target_exists(self, value: bool) -> None:
self.__target_exists = value
@property
def decorrelation_distance(self) -> float:
"""Decorrelation distance of the channel.
Raises:
ValueError: If the decorrelation distance is smaller than zero.
"""
return self.__decorrelation_distance
@decorrelation_distance.setter
def decorrelation_distance(self, value: float) -> None:
if value < 0.0:
raise ValueError("Decorrelation distance must be greater or equal to zero")
self.__decorrelation_distance = value
[docs]
def _realize(self) -> SingleTargetRadarChannelRealization:
return SingleTargetRadarChannelRealization(
self.__consistent_generator.realize(self.decorrelation_distance),
self.__target_range_variable,
self.__target_azimuth_variable,
self.__target_zenith_variable,
self.__target_velocity_variable,
self.__target_phase_variable,
self.target_range if self.target_exists else None,
self.target_azimuth,
self.target_zenith,
self.radar_cross_section,
self.target_velocity,
self.attenuate,
self.sample_hooks,
self.gain,
)
[docs]
def recall_realization(self, group: Group) -> SingleTargetRadarChannelRealization:
return SingleTargetRadarChannelRealization.From_HDF(
group,
self.__target_range_variable,
self.__target_azimuth_variable,
self.__target_zenith_variable,
self.__target_velocity_variable,
self.__target_phase_variable,
self.sample_hooks,
)