use crate::coordinates::position_ecef_to_geodetic;
use crate::time::{Epoch, TimeSystem};
use crate::utils::BraheError;
use nalgebra::{Vector3, Vector6};
use super::geometry::{
compute_asc_dsc, compute_azimuth, compute_elevation, compute_look_direction, compute_off_nadir,
};
pub trait AccessConstraint: Send + Sync + std::any::Any {
fn evaluate(
&self,
epoch: &Epoch,
sat_state_ecef: &Vector6<f64>,
location_ecef: &Vector3<f64>,
) -> bool;
fn name(&self) -> &str;
fn format_string(&self) -> String {
self.name().to_string()
}
}
pub trait AccessConstraintComputer: Send + Sync {
fn evaluate(
&self,
epoch: &Epoch,
sat_state_ecef: &Vector6<f64>,
location_ecef: &Vector3<f64>,
) -> bool;
fn name(&self) -> &str;
}
pub struct AccessConstraintComputerWrapper<T: AccessConstraintComputer> {
computer: T,
}
impl<T: AccessConstraintComputer> AccessConstraintComputerWrapper<T> {
pub fn new(computer: T) -> Self {
Self { computer }
}
}
impl<T: AccessConstraintComputer + 'static> AccessConstraint
for AccessConstraintComputerWrapper<T>
{
fn evaluate(
&self,
epoch: &Epoch,
sat_state_ecef: &Vector6<f64>,
location_ecef: &Vector3<f64>,
) -> bool {
self.computer.evaluate(epoch, sat_state_ecef, location_ecef)
}
fn name(&self) -> &str {
self.computer.name()
}
}
#[derive(Debug, Clone)]
pub struct ElevationConstraint {
pub min_elevation_deg: Option<f64>,
pub max_elevation_deg: Option<f64>,
name: String,
}
#[derive(Debug, Clone)]
pub struct ElevationMaskConstraint {
pub mask: Vec<(f64, f64)>,
name: String,
}
impl ElevationConstraint {
pub fn new(
min_elevation_deg: Option<f64>,
max_elevation_deg: Option<f64>,
) -> Result<Self, BraheError> {
if min_elevation_deg.is_none() && max_elevation_deg.is_none() {
return Err(BraheError::Error(
"At least one bound (min or max) must be specified for ElevationConstraint"
.to_string(),
));
}
let name = match (min_elevation_deg, max_elevation_deg) {
(Some(min), Some(max)) => format!("ElevationConstraint({:.2}° - {:.2}°)", min, max),
(Some(min), None) => format!("ElevationConstraint(>= {:.2}°)", min),
(None, Some(max)) => format!("ElevationConstraint(<= {:.2}°)", max),
(None, None) => unreachable!(),
};
Ok(Self {
min_elevation_deg,
max_elevation_deg,
name,
})
}
}
impl AccessConstraint for ElevationConstraint {
fn evaluate(
&self,
_epoch: &Epoch,
sat_state_ecef: &Vector6<f64>,
location_ecef: &Vector3<f64>,
) -> bool {
let sat_pos = sat_state_ecef.fixed_rows::<3>(0).into_owned();
let elevation = compute_elevation(&sat_pos, location_ecef);
let min_satisfied = self.min_elevation_deg.is_none_or(|min| elevation >= min);
let max_satisfied = self.max_elevation_deg.is_none_or(|max| elevation <= max);
min_satisfied && max_satisfied
}
fn name(&self) -> &str {
&self.name
}
}
impl std::fmt::Display for ElevationConstraint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
impl ElevationMaskConstraint {
pub fn new(mask: Vec<(f64, f64)>) -> Self {
let mut mask = mask;
mask.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let (min_az, min_el) = mask
.iter()
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.cloned()
.unwrap_or((0.0, 0.0));
let (max_az, max_el) = mask
.iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.cloned()
.unwrap_or((0.0, 0.0));
Self {
mask,
name: format!(
"ElevationMaskConstraint(Min: {:.2}° at {:.2}°, Max: {:.2}° at {:.2}°)",
min_el, min_az, max_el, max_az
),
}
}
fn interpolate_min_elevation(&self, azimuth_deg: f64) -> f64 {
if self.mask.is_empty() {
return 0.0;
}
if self.mask.len() == 1 {
return self.mask[0].1;
}
let az = ((azimuth_deg % 360.0) + 360.0) % 360.0;
let mut lower_idx = 0;
let mut upper_idx = 0;
for (i, &(mask_az, _)) in self.mask.iter().enumerate() {
if mask_az <= az {
lower_idx = i;
}
if mask_az >= az && upper_idx == 0 {
upper_idx = i;
break;
}
}
let az1 = self.mask[lower_idx].0;
let el1 = self.mask[lower_idx].1;
let az2: f64;
let el2: f64;
if upper_idx == 0 {
az2 = self.mask[upper_idx].0 + 360.0; el2 = self.mask[upper_idx].1;
} else {
az2 = self.mask[upper_idx].0;
el2 = self.mask[upper_idx].1;
}
if lower_idx == upper_idx {
return self.mask[lower_idx].1;
}
let t = (az - az1) / (az2 - az1);
el1 + t * (el2 - el1)
}
}
impl AccessConstraint for ElevationMaskConstraint {
fn evaluate(
&self,
_epoch: &Epoch,
sat_state_ecef: &Vector6<f64>,
location_ecef: &Vector3<f64>,
) -> bool {
let sat_pos = sat_state_ecef.fixed_rows::<3>(0).into_owned();
let azimuth = compute_azimuth(&sat_pos, location_ecef);
let elevation = compute_elevation(&sat_pos, location_ecef);
let min_elevation = self.interpolate_min_elevation(azimuth);
elevation >= min_elevation
}
fn name(&self) -> &str {
&self.name
}
}
impl std::fmt::Display for ElevationMaskConstraint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug, Clone)]
pub struct OffNadirConstraint {
pub min_off_nadir_deg: Option<f64>,
pub max_off_nadir_deg: Option<f64>,
name: String,
}
impl OffNadirConstraint {
pub fn new(
min_off_nadir_deg: Option<f64>,
max_off_nadir_deg: Option<f64>,
) -> Result<Self, BraheError> {
if min_off_nadir_deg.is_none() && max_off_nadir_deg.is_none() {
return Err(BraheError::Error(
"At least one bound (min or max) must be specified for OffNadirConstraint"
.to_string(),
));
}
if let Some(min) = min_off_nadir_deg.filter(|&m| m < 0.0) {
return Err(BraheError::Error(format!(
"Minimum off-nadir angle must be non-negative, got: {}",
min
)));
}
if let Some(max) = max_off_nadir_deg.filter(|&m| m < 0.0) {
return Err(BraheError::Error(format!(
"Maximum off-nadir angle must be non-negative, got: {}",
max
)));
}
let name = match (min_off_nadir_deg, max_off_nadir_deg) {
(Some(min), Some(max)) => format!("OffNadirConstraint({:.1}° - {:.1}°)", min, max),
(Some(min), None) => format!("OffNadirConstraint(>= {:.1}°)", min),
(None, Some(max)) => format!("OffNadirConstraint(<= {:.1}°)", max),
(None, None) => unreachable!(),
};
Ok(Self {
min_off_nadir_deg,
max_off_nadir_deg,
name,
})
}
}
impl AccessConstraint for OffNadirConstraint {
fn evaluate(
&self,
_epoch: &Epoch,
sat_state_ecef: &Vector6<f64>,
location_ecef: &Vector3<f64>,
) -> bool {
let sat_pos = sat_state_ecef.fixed_rows::<3>(0).into_owned();
let off_nadir = compute_off_nadir(&sat_pos, location_ecef);
let min_satisfied = self.min_off_nadir_deg.is_none_or(|min| off_nadir >= min);
let max_satisfied = self.max_off_nadir_deg.is_none_or(|max| off_nadir <= max);
min_satisfied && max_satisfied
}
fn name(&self) -> &str {
&self.name
}
}
impl std::fmt::Display for OffNadirConstraint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug, Clone)]
pub struct LocalTimeConstraint {
pub time_windows: Vec<(f64, f64)>,
name: String,
}
impl LocalTimeConstraint {
pub fn new(time_windows_military: Vec<(u16, u16)>) -> Result<Self, BraheError> {
let mut time_windows: Vec<(f64, f64)> = Vec::new();
for (start_mil, end_mil) in &time_windows_military {
if *start_mil > 2400 {
return Err(BraheError::Error(format!(
"Start military time must be in [0, 2400], got: {}",
start_mil
)));
}
if *end_mil > 2400 {
return Err(BraheError::Error(format!(
"End military time must be in [0, 2400], got: {}",
end_mil
)));
}
let start_h = start_mil / 100;
let start_m = start_mil % 100;
let end_h = end_mil / 100;
let end_m = end_mil % 100;
if start_m >= 60 {
return Err(BraheError::Error(format!(
"Start minutes must be < 60, got: {} (from military time {})",
start_m, start_mil
)));
}
if end_m >= 60 {
return Err(BraheError::Error(format!(
"End minutes must be < 60, got: {} (from military time {})",
end_m, end_mil
)));
}
let start_hour = f64::from(start_h) + f64::from(start_m) / 60.0;
let end_hour = f64::from(end_h) + f64::from(end_m) / 60.0;
time_windows.push((start_hour, end_hour));
}
let mut window_parts = Vec::new();
for (start_hour, end_hour) in &time_windows {
if start_hour <= end_hour {
let s_h = start_hour.floor() as u32;
let s_m = ((start_hour - start_hour.floor()) * 60.0).round() as u32;
let e_h = end_hour.floor() as u32;
let e_m = ((end_hour - end_hour.floor()) * 60.0).round() as u32;
window_parts.push(format!("{:02}:{:02}-{:02}:{:02}", s_h, s_m, e_h, e_m));
} else {
let s_h = start_hour.floor() as u32;
let s_m = ((start_hour - start_hour.floor()) * 60.0).round() as u32;
let e_h = end_hour.floor() as u32;
let e_m = ((end_hour - end_hour.floor()) * 60.0).round() as u32;
window_parts.push(format!("{:02}:{:02}-24:00", s_h, s_m));
window_parts.push(format!("00:00-{:02}:{:02}", e_h, e_m));
}
}
let window_str = window_parts.join(", ");
Ok(Self {
time_windows,
name: format!("LocalTimeConstraint({})", window_str),
})
}
pub fn from_hours(time_windows: Vec<(f64, f64)>) -> Result<Self, BraheError> {
for (start, end) in &time_windows {
if *start < 0.0 || *start >= 24.0 {
return Err(BraheError::Error(format!(
"Start hour must be in [0, 24), got: {}",
start
)));
}
if *end < 0.0 || *end >= 24.0 {
return Err(BraheError::Error(format!(
"End hour must be in [0, 24), got: {}",
end
)));
}
}
let mut window_parts = Vec::new();
for (start_hour, end_hour) in &time_windows {
if start_hour <= end_hour {
let s_h = start_hour.floor() as u32;
let s_m = ((start_hour - start_hour.floor()) * 60.0).round() as u32;
let e_h = end_hour.floor() as u32;
let e_m = ((end_hour - end_hour.floor()) * 60.0).round() as u32;
window_parts.push(format!("{:02}:{:02}-{:02}:{:02}", s_h, s_m, e_h, e_m));
} else {
let s_h = start_hour.floor() as u32;
let s_m = ((start_hour - start_hour.floor()) * 60.0).round() as u32;
let e_h = end_hour.floor() as u32;
let e_m = ((end_hour - end_hour.floor()) * 60.0).round() as u32;
window_parts.push(format!("{:02}:{:02}-24:00", s_h, s_m));
window_parts.push(format!("00:00-{:02}:{:02}", e_h, e_m));
}
}
let window_str = window_parts.join(", ");
Ok(Self {
time_windows,
name: format!("LocalTimeConstraint({})", window_str),
})
}
pub fn from_seconds(time_windows_seconds: Vec<(f64, f64)>) -> Result<Self, BraheError> {
let mut time_windows: Vec<(f64, f64)> = Vec::new();
for (start_sec, end_sec) in &time_windows_seconds {
if *start_sec < 0.0 || *start_sec >= 86400.0 {
return Err(BraheError::Error(format!(
"Start seconds must be in [0, 86400), got: {}",
start_sec
)));
}
if *end_sec < 0.0 || *end_sec >= 86400.0 {
return Err(BraheError::Error(format!(
"End seconds must be in [0, 86400), got: {}",
end_sec
)));
}
let start_hour = start_sec / 3600.0;
let end_hour = end_sec / 3600.0;
time_windows.push((start_hour, end_hour));
}
let mut window_parts = Vec::new();
for (start_hour, end_hour) in &time_windows {
if start_hour <= end_hour {
let s_h = start_hour.floor() as u32;
let s_m = ((start_hour - start_hour.floor()) * 60.0).round() as u32;
let e_h = end_hour.floor() as u32;
let e_m = ((end_hour - end_hour.floor()) * 60.0).round() as u32;
window_parts.push(format!("{:02}:{:02}-{:02}:{:02}", s_h, s_m, e_h, e_m));
} else {
let s_h = start_hour.floor() as u32;
let s_m = ((start_hour - start_hour.floor()) * 60.0).round() as u32;
let e_h = end_hour.floor() as u32;
let e_m = ((end_hour - end_hour.floor()) * 60.0).round() as u32;
window_parts.push(format!("{:02}:{:02}-24:00", s_h, s_m));
window_parts.push(format!("00:00-{:02}:{:02}", e_h, e_m));
}
}
let window_str = window_parts.join(", ");
Ok(Self {
time_windows,
name: format!("LocalTimeConstraint({})", window_str),
})
}
fn compute_local_time(&self, epoch: &Epoch, location_ecef: &Vector3<f64>) -> f64 {
use crate::constants::AngleFormat;
let geodetic = position_ecef_to_geodetic(*location_ecef, AngleFormat::Radians);
let longitude_rad = geodetic.x;
let (_year, _month, _day, hour, minute, second, nanosecond) =
epoch.to_datetime_as_time_system(TimeSystem::UTC);
let utc_hour =
f64::from(hour) + f64::from(minute) / 60.0 + second / 3600.0 + nanosecond / 3.6e12;
let local_time = utc_hour + longitude_rad.to_degrees() / 15.0;
(local_time % 24.0 + 24.0) % 24.0
}
}
impl AccessConstraint for LocalTimeConstraint {
fn evaluate(
&self,
epoch: &Epoch,
_sat_state_ecef: &Vector6<f64>,
location_ecef: &Vector3<f64>,
) -> bool {
let local_time = self.compute_local_time(epoch, location_ecef);
self.time_windows.iter().any(|(start, end)| {
if start <= end {
local_time >= *start && local_time <= *end
} else {
local_time >= *start || local_time <= *end
}
})
}
fn name(&self) -> &str {
&self.name
}
}
impl std::fmt::Display for LocalTimeConstraint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum LookDirection {
Left,
Right,
Either,
}
impl std::fmt::Display for LookDirection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LookDirection::Left => write!(f, "Left"),
LookDirection::Right => write!(f, "Right"),
LookDirection::Either => write!(f, "Either"),
}
}
}
#[derive(Debug, Clone)]
pub struct LookDirectionConstraint {
pub allowed: LookDirection,
name: String,
}
impl LookDirectionConstraint {
pub fn new(allowed: LookDirection) -> Self {
let direction_str = match allowed {
LookDirection::Left => "Left",
LookDirection::Right => "Right",
LookDirection::Either => "Either",
};
Self {
allowed,
name: format!("LookDirectionConstraint({})", direction_str),
}
}
}
impl AccessConstraint for LookDirectionConstraint {
fn evaluate(
&self,
_epoch: &Epoch,
sat_state_ecef: &Vector6<f64>,
location_ecef: &Vector3<f64>,
) -> bool {
let look_dir = compute_look_direction(sat_state_ecef, location_ecef);
match self.allowed {
LookDirection::Either => true,
allowed => look_dir == allowed,
}
}
fn name(&self) -> &str {
&self.name
}
}
impl std::fmt::Display for LookDirectionConstraint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum AscDsc {
Ascending,
Descending,
Either,
}
impl std::fmt::Display for AscDsc {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AscDsc::Ascending => write!(f, "Ascending"),
AscDsc::Descending => write!(f, "Descending"),
AscDsc::Either => write!(f, "Either"),
}
}
}
#[derive(Debug, Clone)]
pub struct AscDscConstraint {
pub allowed: AscDsc,
name: String,
}
impl AscDscConstraint {
pub fn new(allowed: AscDsc) -> Self {
let type_str = match allowed {
AscDsc::Ascending => "Ascending",
AscDsc::Descending => "Descending",
AscDsc::Either => "Either",
};
Self {
allowed,
name: format!("AscDscConstraint({})", type_str),
}
}
}
impl AccessConstraint for AscDscConstraint {
fn evaluate(
&self,
_epoch: &Epoch,
sat_state_ecef: &Vector6<f64>,
_location_ecef: &Vector3<f64>,
) -> bool {
let pass_type = compute_asc_dsc(sat_state_ecef);
match self.allowed {
AscDsc::Either => true,
allowed => pass_type == allowed,
}
}
fn name(&self) -> &str {
&self.name
}
}
impl std::fmt::Display for AscDscConstraint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
pub enum ConstraintComposite {
All(Vec<Box<dyn AccessConstraint>>),
Any(Vec<Box<dyn AccessConstraint>>),
Not(Box<dyn AccessConstraint>),
}
impl AccessConstraint for ConstraintComposite {
fn evaluate(
&self,
epoch: &Epoch,
sat_state_ecef: &Vector6<f64>,
location_ecef: &Vector3<f64>,
) -> bool {
match self {
ConstraintComposite::All(constraints) => constraints
.iter()
.all(|c| c.evaluate(epoch, sat_state_ecef, location_ecef)),
ConstraintComposite::Any(constraints) => constraints
.iter()
.any(|c| c.evaluate(epoch, sat_state_ecef, location_ecef)),
ConstraintComposite::Not(constraint) => {
!constraint.evaluate(epoch, sat_state_ecef, location_ecef)
}
}
}
fn name(&self) -> &str {
match self {
ConstraintComposite::All(_) => "All",
ConstraintComposite::Any(_) => "Any",
ConstraintComposite::Not(_) => "Not",
}
}
fn format_string(&self) -> String {
format!("{}", self)
}
}
impl std::fmt::Display for ConstraintComposite {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConstraintComposite::All(constraints) => {
if constraints.len() == 1 {
write!(f, "{}", format_constraint(&*constraints[0]))
} else {
for (i, constraint) in constraints.iter().enumerate() {
if i > 0 {
write!(f, " && ")?;
}
write_constraint_with_precedence(f, constraint.as_ref(), Precedence::And)?;
}
Ok(())
}
}
ConstraintComposite::Any(constraints) => {
if constraints.len() == 1 {
write!(f, "{}", format_constraint(&*constraints[0]))
} else {
for (i, constraint) in constraints.iter().enumerate() {
if i > 0 {
write!(f, " || ")?;
}
write_constraint_with_precedence(f, constraint.as_ref(), Precedence::Or)?;
}
Ok(())
}
}
ConstraintComposite::Not(constraint) => {
write!(f, "!")?;
write_constraint_with_precedence(f, constraint.as_ref(), Precedence::Not)
}
}
}
}
impl std::fmt::Debug for ConstraintComposite {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConstraintComposite::All(constraints) => f
.debug_tuple("All")
.field(&constraints.iter().map(|c| c.name()).collect::<Vec<_>>())
.finish(),
ConstraintComposite::Any(constraints) => f
.debug_tuple("Any")
.field(&constraints.iter().map(|c| c.name()).collect::<Vec<_>>())
.finish(),
ConstraintComposite::Not(constraint) => {
f.debug_tuple("Not").field(&constraint.name()).finish()
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Precedence {
Or = 1, And = 2, Not = 3, }
fn write_constraint_with_precedence(
f: &mut std::fmt::Formatter<'_>,
constraint: &dyn AccessConstraint,
parent_precedence: Precedence,
) -> std::fmt::Result {
let is_composite =
constraint.name() == "All" || constraint.name() == "Any" || constraint.name() == "Not";
if !is_composite {
write!(f, "{}", constraint.name())
} else {
let constraint_precedence = match constraint.name() {
"All" => Precedence::And,
"Any" => Precedence::Or,
"Not" => Precedence::Not,
_ => Precedence::Not,
};
let needs_parens = constraint_precedence < parent_precedence;
if needs_parens {
write!(f, "(")?;
}
write!(f, "{}", constraint.format_string())?;
if needs_parens {
write!(f, ")")?;
}
Ok(())
}
}
fn format_constraint(constraint: &dyn AccessConstraint) -> String {
constraint.format_string()
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use crate::constants::{AngleFormat, R_EARTH};
use crate::coordinates::coordinate_types::EllipsoidalConversionType;
use crate::coordinates::{position_geodetic_to_ecef, state_koe_to_eci};
use crate::frames::state_eci_to_ecef;
use crate::time::{Epoch, TimeSystem};
use crate::utils::testing::setup_global_test_eop;
fn test_epoch() -> Epoch {
Epoch::from_datetime(2025, 10, 16, 0, 0, 0.0, 0.0, TimeSystem::UTC)
}
fn test_sat_ecef_from_oe(oe: Vector6<f64>) -> Vector6<f64> {
let state_eci = state_koe_to_eci(oe, AngleFormat::Degrees);
state_eci_to_ecef(test_epoch(), state_eci)
}
fn test_location_from_oe(oe: Vector6<f64>) -> Vector3<f64> {
let loc_ecef_state = test_sat_ecef_from_oe(oe);
let loc_geod = position_ecef_to_geodetic(
loc_ecef_state.fixed_rows::<3>(0).into_owned(),
AngleFormat::Radians,
);
let sub_loc_geod = Vector3::new(loc_geod.x, loc_geod.y, 0.0); position_geodetic_to_ecef(sub_loc_geod, AngleFormat::Radians).unwrap()
}
fn test_geometry_west_asc() -> (Epoch, Vector6<f64>, Vector3<f64>) {
setup_global_test_eop();
let sat_oe = Vector6::new(
R_EARTH + 500e3, 0.0, 90.0, 0.0, 0.0, 0.0, );
let sat_ecef = test_sat_ecef_from_oe(sat_oe);
let loc_oe = Vector6::new(
R_EARTH + 500e3, 0.0, 90.0, 358.0, 0.0, 0.0, );
let loc_ecef = test_location_from_oe(loc_oe);
(test_epoch(), sat_ecef, loc_ecef)
}
fn test_geometry_west_dsc() -> (Epoch, Vector6<f64>, Vector3<f64>) {
setup_global_test_eop();
let sat_oe = Vector6::new(
R_EARTH + 500e3, 0.0, 90.0, 180.0, 0.0, 180.0, );
let sat_ecef = test_sat_ecef_from_oe(sat_oe);
let loc_oe = Vector6::new(
R_EARTH + 500e3, 0.0, 90.0, 358.0, 0.0, 0.0, );
let loc_ecef = test_location_from_oe(loc_oe);
(test_epoch(), sat_ecef, loc_ecef)
}
fn compute_sat_position_from_azel(
lat_deg: f64,
lon_deg: f64,
alt_m: f64,
az_deg: f64,
el_deg: f64,
range_m: f64,
) -> Vector3<f64> {
use crate::coordinates::relative_position_enz_to_ecef;
use std::f64::consts::PI;
let location_geod = Vector3::new(lat_deg, lon_deg, alt_m);
let location_ecef = position_geodetic_to_ecef(location_geod, AngleFormat::Degrees).unwrap();
let az_rad = az_deg * PI / 180.0;
let el_rad = el_deg * PI / 180.0;
let e = range_m * el_rad.cos() * az_rad.sin();
let n = range_m * el_rad.cos() * az_rad.cos();
let z = range_m * el_rad.sin();
let relative_enz = Vector3::new(e, n, z);
relative_position_enz_to_ecef(
location_ecef,
relative_enz,
EllipsoidalConversionType::Geodetic,
)
}
#[test]
fn test_constraint_name() {
let elev = ElevationConstraint::new(Some(5.0), Some(90.0)).unwrap();
assert_eq!(elev.name(), "ElevationConstraint(5.00° - 90.00°)");
let elev_min_only = ElevationConstraint::new(Some(5.0), None).unwrap();
assert_eq!(elev_min_only.name(), "ElevationConstraint(>= 5.00°)");
let elev_max_only = ElevationConstraint::new(None, Some(90.0)).unwrap();
assert_eq!(elev_max_only.name(), "ElevationConstraint(<= 90.00°)");
let off_nadir = OffNadirConstraint::new(Some(10.0), Some(45.0)).unwrap();
assert_eq!(off_nadir.name(), "OffNadirConstraint(10.0° - 45.0°)");
let off_nadir_max_only = OffNadirConstraint::new(None, Some(45.0)).unwrap();
assert_eq!(off_nadir_max_only.name(), "OffNadirConstraint(<= 45.0°)");
let off_nadir_min_only = OffNadirConstraint::new(Some(10.0), None).unwrap();
assert_eq!(off_nadir_min_only.name(), "OffNadirConstraint(>= 10.0°)");
let local_time = LocalTimeConstraint::new(vec![(600, 1800)]).unwrap();
assert_eq!(local_time.name(), "LocalTimeConstraint(06:00-18:00)");
let local_time_multi = LocalTimeConstraint::new(vec![(200, 600), (1800, 2200)]).unwrap();
assert_eq!(
local_time_multi.name(),
"LocalTimeConstraint(02:00-06:00, 18:00-22:00)"
);
let local_time_wrap = LocalTimeConstraint::new(vec![(2200, 200)]).unwrap();
assert_eq!(
local_time_wrap.name(),
"LocalTimeConstraint(22:00-24:00, 00:00-02:00)"
);
let local_time_sec = LocalTimeConstraint::from_seconds(vec![(39600.0, 46800.0)]).unwrap();
assert_eq!(local_time_sec.name(), "LocalTimeConstraint(11:00-13:00)");
let local_time_hour = LocalTimeConstraint::from_hours(vec![(11.0, 13.0)]).unwrap();
assert_eq!(local_time_hour.name(), "LocalTimeConstraint(11:00-13:00)");
let look_dir = LookDirectionConstraint::new(LookDirection::Right);
assert_eq!(look_dir.name(), "LookDirectionConstraint(Right)");
let look_dir = LookDirectionConstraint::new(LookDirection::Left);
assert_eq!(look_dir.name(), "LookDirectionConstraint(Left)");
let look_dir = LookDirectionConstraint::new(LookDirection::Either);
assert_eq!(look_dir.name(), "LookDirectionConstraint(Either)");
let asc_dsc = AscDscConstraint::new(AscDsc::Ascending);
assert_eq!(asc_dsc.name(), "AscDscConstraint(Ascending)");
let asc_dsc = AscDscConstraint::new(AscDsc::Either);
assert_eq!(asc_dsc.name(), "AscDscConstraint(Either)");
let asc_dsc = AscDscConstraint::new(AscDsc::Descending);
assert_eq!(asc_dsc.name(), "AscDscConstraint(Descending)");
let elev_mask = ElevationMaskConstraint::new(vec![(0.0, 10.0), (180.0, 5.0)]);
assert_eq!(
elev_mask.name(),
"ElevationMaskConstraint(Min: 5.00° at 180.00°, Max: 10.00° at 0.00°)"
);
}
#[test]
fn test_elevation_constraint_satisfied() {
setup_global_test_eop();
let constraint = ElevationConstraint::new(Some(10.0), None).unwrap();
let location = Vector3::new(0.0, 0.0, 0.0);
let location_ecef = position_geodetic_to_ecef(location, AngleFormat::Degrees).unwrap();
let sat_pos_ecef = compute_sat_position_from_azel(
0.0, 0.0, 0.0, 90.0, 45.0, 1000e3, );
let sat_state = Vector6::new(
sat_pos_ecef.x,
sat_pos_ecef.y,
sat_pos_ecef.z,
0.0, 0.0,
0.0,
);
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
assert!(constraint.evaluate(&epoch, &sat_state, &location_ecef));
}
#[test]
fn test_elevation_constraint_at_limit() {
setup_global_test_eop();
let constraint = ElevationConstraint::new(Some(10.0), None).unwrap();
let location = Vector3::new(0.0, 0.0, 0.0);
let location_ecef = position_geodetic_to_ecef(location, AngleFormat::Degrees).unwrap();
let sat_pos_ecef = compute_sat_position_from_azel(
0.0, 0.0, 0.0, 90.0, 10.0, 1000e3, );
let sat_state = Vector6::new(
sat_pos_ecef.x,
sat_pos_ecef.y,
sat_pos_ecef.z,
0.0, 0.0,
0.0,
);
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
assert!(constraint.evaluate(&epoch, &sat_state, &location_ecef));
}
#[test]
fn test_elevation_constraint_violated() {
let constraint = ElevationConstraint::new(Some(70.0), Some(90.0)).unwrap();
let location = Vector3::new(0.0, 0.0, 0.0);
let location_ecef = position_geodetic_to_ecef(location, AngleFormat::Degrees).unwrap();
let sat_pos_ecef = compute_sat_position_from_azel(
0.0, 0.0, 0.0, 90.0, 45.0, 1000e3, );
let sat_state = Vector6::new(
sat_pos_ecef.x,
sat_pos_ecef.y,
sat_pos_ecef.z,
0.0, 0.0,
0.0,
);
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
assert!(!constraint.evaluate(&epoch, &sat_state, &location_ecef));
}
#[test]
fn test_elevation_constraint_both_none_error() {
let result = ElevationConstraint::new(None, None);
assert!(result.is_err());
}
#[test]
fn test_elevation_mask_interpolation() {
let mask = vec![(0.0, 10.0), (90.0, 10.0), (180.0, 20.0), (270.0, 20.0)];
let constraint = ElevationMaskConstraint::new(mask);
let min_el = constraint.interpolate_min_elevation(90.0);
assert_eq!(min_el, 10.0);
let min_el = constraint.interpolate_min_elevation(135.0);
assert_eq!(min_el, 15.0);
let min_el = constraint.interpolate_min_elevation(225.0);
assert_eq!(min_el, 20.0);
let min_el = constraint.interpolate_min_elevation(315.0);
assert_eq!(min_el, 15.0);
}
#[test]
fn test_elevation_mask_constraint() {
setup_global_test_eop();
let epoch: Epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let mask = vec![
(0.0, 10.0), (90.0, 10.0), (180.0, 20.0), (270.0, 20.0), ];
let constraint = ElevationMaskConstraint::new(mask);
let location = Vector3::new(0.0, 0.0, 0.0);
let location_ecef = position_geodetic_to_ecef(location, AngleFormat::Degrees).unwrap();
let sat_pos_ecef = compute_sat_position_from_azel(
0.0, 0.0, 0.0, 45.0, 30.0, 1000e3, );
let sat_state_north = Vector6::new(
sat_pos_ecef.x,
sat_pos_ecef.y,
sat_pos_ecef.z,
0.0, 0.0,
0.0,
);
assert!(constraint.evaluate(&epoch, &sat_state_north, &location_ecef));
let sat_pos_ecef = compute_sat_position_from_azel(
0.0, 0.0, 0.0, 135.0, 15.0, 1000e3, );
let sat_state_southeast = Vector6::new(
sat_pos_ecef.x,
sat_pos_ecef.y,
sat_pos_ecef.z,
0.0, 0.0,
0.0,
);
assert!(constraint.evaluate(&epoch, &sat_state_southeast, &location_ecef));
let sat_pos_ecef = compute_sat_position_from_azel(
0.0, 0.0, 0.0, 225.0, 15.0, 1000e3, );
let sat_state_southwest = Vector6::new(
sat_pos_ecef.x,
sat_pos_ecef.y,
sat_pos_ecef.z,
0.0, 0.0,
0.0,
);
assert!(!constraint.evaluate(&epoch, &sat_state_southwest, &location_ecef));
}
#[test]
fn test_off_nadir_constraint() {
setup_global_test_eop();
let (epoch, sat_state, location) = test_geometry_west_asc();
let constraint = OffNadirConstraint::new(None, Some(60.0)).unwrap();
assert!(constraint.evaluate(&epoch, &sat_state, &location));
let constraint = OffNadirConstraint::new(None, Some(5.0)).unwrap();
assert!(!constraint.evaluate(&epoch, &sat_state, &location));
}
#[test]
fn test_off_nadir_constraint_both_none_error() {
let result = OffNadirConstraint::new(None, None);
assert!(result.is_err());
}
#[test]
fn test_off_nadir_constraint_negative_error() {
let result = OffNadirConstraint::new(Some(-5.0), Some(45.0));
assert!(result.is_err());
let result = OffNadirConstraint::new(Some(10.0), Some(-5.0));
assert!(result.is_err());
}
#[test]
fn test_local_time_constraint() {
setup_global_test_eop();
let constraint = LocalTimeConstraint::new(vec![(1100, 1300)]).unwrap();
let location = Vector3::new(0.0, 0.0, 0.0);
let location_ecef = position_geodetic_to_ecef(location, AngleFormat::Degrees).unwrap();
let sat_state = Vector6::zeros();
let epoch_noon = Epoch::from_datetime(2024, 1, 1, 12, 0, 0.0, 0.0, TimeSystem::UTC);
assert!(constraint.evaluate(&epoch_noon, &sat_state, &location_ecef));
let epoch_midnight = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
assert!(!constraint.evaluate(&epoch_midnight, &sat_state, &location_ecef));
}
#[test]
fn test_local_time_hour_validation() {
let result = LocalTimeConstraint::from_hours(vec![(25.0, 26.0)]);
assert!(result.is_err());
}
#[test]
fn test_local_time_military_validation() {
let result = LocalTimeConstraint::new(vec![(2500, 2600)]);
assert!(result.is_err());
let result = LocalTimeConstraint::new(vec![(1065, 1200)]);
assert!(result.is_err());
}
#[test]
fn test_local_time_seconds_validation() {
let result = LocalTimeConstraint::from_seconds(vec![(90000.0, 95000.0)]);
assert!(result.is_err());
}
#[test]
fn test_look_direction_constraint_asc() {
setup_global_test_eop();
let (epoch, sat_state, location) = test_geometry_west_asc();
let constraint = LookDirectionConstraint::new(LookDirection::Left);
assert!(constraint.evaluate(&epoch, &sat_state, &location));
let constraint = LookDirectionConstraint::new(LookDirection::Right);
assert!(!constraint.evaluate(&epoch, &sat_state, &location));
let constraint = LookDirectionConstraint::new(LookDirection::Either);
assert!(constraint.evaluate(&epoch, &sat_state, &location));
}
#[test]
fn test_look_direction_constraint_dsc() {
setup_global_test_eop();
let (epoch, sat_state, location) = test_geometry_west_dsc();
let constraint = LookDirectionConstraint::new(LookDirection::Right);
assert!(constraint.evaluate(&epoch, &sat_state, &location));
let constraint = LookDirectionConstraint::new(LookDirection::Left);
assert!(!constraint.evaluate(&epoch, &sat_state, &location));
let constraint = LookDirectionConstraint::new(LookDirection::Either);
assert!(constraint.evaluate(&epoch, &sat_state, &location));
}
#[test]
fn test_asc_dsc_constraint_ascending() {
let (epoch, sat_state, location) = test_geometry_west_asc();
let constraint = AscDscConstraint::new(AscDsc::Ascending);
assert!(constraint.evaluate(&epoch, &sat_state, &location));
}
#[test]
fn test_asc_dsc_constraint_descending() {
let (epoch, sat_state, location) = test_geometry_west_dsc();
let constraint = AscDscConstraint::new(AscDsc::Descending);
assert!(constraint.evaluate(&epoch, &sat_state, &location));
}
#[test]
fn test_asc_dsc_constraint_either() {
let constraint = AscDscConstraint::new(AscDsc::Either);
let (epoch_asc, sat_state_asc, location_asc) = test_geometry_west_asc();
assert!(constraint.evaluate(&epoch_asc, &sat_state_asc, &location_asc));
let (epoch_dsc, sat_state_dsc, location_dsc) = test_geometry_west_dsc();
assert!(constraint.evaluate(&epoch_dsc, &sat_state_dsc, &location_dsc));
}
#[test]
fn test_constraint_composite_chaining() {
let elev = Box::new(ElevationConstraint::new(Some(5.0), None).unwrap());
let look = Box::new(LookDirectionConstraint::new(LookDirection::Right));
let off_nadir = Box::new(OffNadirConstraint::new(None, Some(30.0)).unwrap());
let not_right = Box::new(ConstraintComposite::Not(look));
let inner_or = Box::new(ConstraintComposite::Any(vec![not_right, off_nadir]));
let outer_and = ConstraintComposite::All(vec![elev, inner_or]);
let epoch = Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
let sat_state = Vector6::new(7000000.0, 0.0, 0.0, 0.0, 7500.0, 5000.0);
let location = Vector3::new(6378137.0, 0.0, 0.0);
let _ = outer_and.evaluate(&epoch, &sat_state, &location);
}
#[test]
fn test_constraint_composite_display() {
let c1 = Box::new(ElevationConstraint::new(Some(5.0), None).unwrap());
let c2 = Box::new(OffNadirConstraint::new(None, Some(45.0)).unwrap());
let and_composite = ConstraintComposite::All(vec![c1, c2]);
let display = format!("{}", and_composite);
assert!(display.contains("&&"));
assert!(display.contains("ElevationConstraint"));
assert!(display.contains("OffNadirConstraint"));
assert!(!display.contains("["));
let c3 = Box::new(LookDirectionConstraint::new(LookDirection::Right));
let not_composite = ConstraintComposite::Not(c3);
let display_not = format!("{}", not_composite);
assert!(display_not.contains("!"));
assert!(display_not.contains("LookDirectionConstraint"));
assert!(!display_not.contains("["));
let c4 = Box::new(AscDscConstraint::new(AscDsc::Ascending));
let c5 = Box::new(AscDscConstraint::new(AscDsc::Descending));
let or_composite = ConstraintComposite::Any(vec![c4, c5]);
let display_or = format!("{}", or_composite);
assert!(display_or.contains("||"));
assert!(!display_or.contains("["));
let c6 = Box::new(ElevationConstraint::new(Some(10.0), None).unwrap());
let single_all = ConstraintComposite::All(vec![c6]);
let display_single = format!("{}", single_all);
eprintln!("Single constraint display: '{}'", display_single);
assert!(display_single.contains("ElevationConstraint"));
}
#[test]
fn test_constraint_composite_nested_display() {
let c1 = Box::new(ElevationConstraint::new(Some(5.0), None).unwrap());
let c2 = Box::new(OffNadirConstraint::new(None, Some(30.0)).unwrap());
let c3 = Box::new(LookDirectionConstraint::new(LookDirection::Left));
let inner_or = Box::new(ConstraintComposite::Any(vec![c2, c3]));
let outer_and = ConstraintComposite::All(vec![c1, inner_or]);
let display = format!("{}", outer_and);
assert!(display.contains("&&"));
assert!(display.contains("||")); assert!(display.contains("(")); assert!(display.contains(")"));
assert!(!display.contains("["));
let c4 = Box::new(LookDirectionConstraint::new(LookDirection::Right));
let c5 = Box::new(OffNadirConstraint::new(None, Some(45.0)).unwrap());
let not_c4 = Box::new(ConstraintComposite::Not(c4));
let and_with_not = ConstraintComposite::All(vec![not_c4, c5]);
let display_not = format!("{}", and_with_not);
assert!(display_not.contains("!"));
assert!(display_not.contains("&&"));
let c6 = Box::new(ElevationConstraint::new(Some(10.0), None).unwrap());
let c7 = Box::new(OffNadirConstraint::new(None, Some(20.0)).unwrap());
let c8 = Box::new(AscDscConstraint::new(AscDsc::Ascending));
let inner_and = Box::new(ConstraintComposite::All(vec![c6, c7]));
let outer_or = ConstraintComposite::Any(vec![inner_and, c8]);
let display_or = format!("{}", outer_or);
assert!(display_or.contains("||"));
assert!(display_or.contains("&&"));
assert!(display_or.contains("(")); }
#[test]
fn test_elevation_constraint_display() {
let constraint_min = ElevationConstraint::new(Some(10.0), None).unwrap();
let display_min = format!("{}", constraint_min);
assert_eq!(display_min, "ElevationConstraint(>= 10.00°)");
let constraint_max = ElevationConstraint::new(None, Some(80.0)).unwrap();
let display_max = format!("{}", constraint_max);
assert_eq!(display_max, "ElevationConstraint(<= 80.00°)");
let constraint_both = ElevationConstraint::new(Some(5.0), Some(75.5)).unwrap();
let display_both = format!("{}", constraint_both);
assert_eq!(display_both, "ElevationConstraint(5.00° - 75.50°)");
}
#[test]
fn test_elevation_mask_constraint_display() {
let mask = vec![(0.0, 10.0), (90.0, 5.0), (180.0, 15.0), (270.0, 8.0)];
let constraint = ElevationMaskConstraint::new(mask);
let display = format!("{}", constraint);
assert!(display.starts_with("ElevationMaskConstraint"));
assert!(display.contains("5.00°")); assert!(display.contains("15.00°")); }
#[test]
fn test_off_nadir_constraint_display() {
let constraint_min = OffNadirConstraint::new(Some(10.0), None).unwrap();
let display_min = format!("{}", constraint_min);
assert_eq!(display_min, "OffNadirConstraint(>= 10.0°)");
let constraint_max = OffNadirConstraint::new(None, Some(30.0)).unwrap();
let display_max = format!("{}", constraint_max);
assert_eq!(display_max, "OffNadirConstraint(<= 30.0°)");
let constraint_both = OffNadirConstraint::new(Some(5.0), Some(45.0)).unwrap();
let display_both = format!("{}", constraint_both);
assert_eq!(display_both, "OffNadirConstraint(5.0° - 45.0°)");
}
#[test]
fn test_local_time_constraint_display() {
let constraint_single = LocalTimeConstraint::new(vec![(800, 1800)]).unwrap();
let display_single = format!("{}", constraint_single);
assert_eq!(display_single, "LocalTimeConstraint(08:00-18:00)");
let constraint_multi = LocalTimeConstraint::new(vec![(600, 900), (1700, 2000)]).unwrap();
let display_multi = format!("{}", constraint_multi);
assert_eq!(
display_multi,
"LocalTimeConstraint(06:00-09:00, 17:00-20:00)"
);
let constraint_wrap = LocalTimeConstraint::new(vec![(2200, 200)]).unwrap();
let display_wrap = format!("{}", constraint_wrap);
assert_eq!(
display_wrap,
"LocalTimeConstraint(22:00-24:00, 00:00-02:00)"
);
}
#[test]
fn test_look_direction_constraint_display() {
let constraint_left = LookDirectionConstraint::new(LookDirection::Left);
assert_eq!(
format!("{}", constraint_left),
"LookDirectionConstraint(Left)"
);
let constraint_right = LookDirectionConstraint::new(LookDirection::Right);
assert_eq!(
format!("{}", constraint_right),
"LookDirectionConstraint(Right)"
);
let constraint_either = LookDirectionConstraint::new(LookDirection::Either);
assert_eq!(
format!("{}", constraint_either),
"LookDirectionConstraint(Either)"
);
}
#[test]
fn test_asc_dsc_constraint_display() {
let constraint_asc = AscDscConstraint::new(AscDsc::Ascending);
assert_eq!(format!("{}", constraint_asc), "AscDscConstraint(Ascending)");
let constraint_dsc = AscDscConstraint::new(AscDsc::Descending);
assert_eq!(
format!("{}", constraint_dsc),
"AscDscConstraint(Descending)"
);
let constraint_either = AscDscConstraint::new(AscDsc::Either);
assert_eq!(format!("{}", constraint_either), "AscDscConstraint(Either)");
}
#[test]
fn test_look_direction_enum_display() {
assert_eq!(format!("{}", LookDirection::Left), "Left");
assert_eq!(format!("{}", LookDirection::Right), "Right");
assert_eq!(format!("{}", LookDirection::Either), "Either");
}
#[test]
fn test_asc_dsc_enum_display() {
assert_eq!(format!("{}", AscDsc::Ascending), "Ascending");
assert_eq!(format!("{}", AscDsc::Descending), "Descending");
assert_eq!(format!("{}", AscDsc::Either), "Either");
}
#[test]
fn test_local_time_constraint_wrap_around_evaluation() {
setup_global_test_eop();
let constraint = LocalTimeConstraint::new(vec![(2200, 200)]).unwrap();
let location = Vector3::new(0.0, 0.0, 0.0); let location_ecef = position_geodetic_to_ecef(location, AngleFormat::Degrees).unwrap();
let sat_state = Vector6::zeros();
let epoch_2300 = Epoch::from_datetime(2024, 1, 1, 23, 0, 0.0, 0.0, TimeSystem::UTC);
assert!(
constraint.evaluate(&epoch_2300, &sat_state, &location_ecef),
"23:00 should be inside 22:00-02:00 window"
);
let epoch_0100 = Epoch::from_datetime(2024, 1, 1, 1, 0, 0.0, 0.0, TimeSystem::UTC);
assert!(
constraint.evaluate(&epoch_0100, &sat_state, &location_ecef),
"01:00 should be inside 22:00-02:00 window"
);
let epoch_1200 = Epoch::from_datetime(2024, 1, 1, 12, 0, 0.0, 0.0, TimeSystem::UTC);
assert!(
!constraint.evaluate(&epoch_1200, &sat_state, &location_ecef),
"12:00 should be outside 22:00-02:00 window"
);
let epoch_0300 = Epoch::from_datetime(2024, 1, 1, 3, 0, 0.0, 0.0, TimeSystem::UTC);
assert!(
!constraint.evaluate(&epoch_0300, &sat_state, &location_ecef),
"03:00 should be outside 22:00-02:00 window"
);
}
#[test]
fn test_local_time_constraint_multiple_windows_evaluation() {
setup_global_test_eop();
let constraint = LocalTimeConstraint::new(vec![(600, 900), (1700, 2000)]).unwrap();
let location = Vector3::new(0.0, 0.0, 0.0);
let location_ecef = position_geodetic_to_ecef(location, AngleFormat::Degrees).unwrap();
let sat_state = Vector6::zeros();
let epoch_0700 = Epoch::from_datetime(2024, 1, 1, 7, 0, 0.0, 0.0, TimeSystem::UTC);
assert!(constraint.evaluate(&epoch_0700, &sat_state, &location_ecef));
let epoch_1800 = Epoch::from_datetime(2024, 1, 1, 18, 0, 0.0, 0.0, TimeSystem::UTC);
assert!(constraint.evaluate(&epoch_1800, &sat_state, &location_ecef));
let epoch_1200 = Epoch::from_datetime(2024, 1, 1, 12, 0, 0.0, 0.0, TimeSystem::UTC);
assert!(!constraint.evaluate(&epoch_1200, &sat_state, &location_ecef));
let epoch_2100 = Epoch::from_datetime(2024, 1, 1, 21, 0, 0.0, 0.0, TimeSystem::UTC);
assert!(!constraint.evaluate(&epoch_2100, &sat_state, &location_ecef));
}
#[test]
fn test_local_time_constraint_boundary_cases() {
setup_global_test_eop();
let constraint = LocalTimeConstraint::new(vec![(600, 1800)]).unwrap();
let location = Vector3::new(0.0, 0.0, 0.0);
let location_ecef = position_geodetic_to_ecef(location, AngleFormat::Degrees).unwrap();
let sat_state = Vector6::zeros();
let epoch_0600 = Epoch::from_datetime(2024, 1, 1, 6, 0, 0.0, 0.0, TimeSystem::UTC);
assert!(constraint.evaluate(&epoch_0600, &sat_state, &location_ecef));
let epoch_1800 = Epoch::from_datetime(2024, 1, 1, 18, 0, 0.0, 0.0, TimeSystem::UTC);
assert!(constraint.evaluate(&epoch_1800, &sat_state, &location_ecef));
let epoch_0559 = Epoch::from_datetime(2024, 1, 1, 5, 59, 0.0, 0.0, TimeSystem::UTC);
assert!(!constraint.evaluate(&epoch_0559, &sat_state, &location_ecef));
}
#[test]
fn test_off_nadir_constraint_evaluate_min_only() {
setup_global_test_eop();
let constraint = OffNadirConstraint::new(Some(20.0), None).unwrap();
let location = Vector3::new(0.0, 0.0, 0.0); let location_ecef = position_geodetic_to_ecef(location, AngleFormat::Degrees).unwrap();
let sat_oe = Vector6::new(
R_EARTH + 500e3, 0.0, 0.0, 0.0, 0.0, 0.0, );
let sat_state = test_sat_ecef_from_oe(sat_oe);
let _result = constraint.evaluate(&test_epoch(), &sat_state, &location_ecef);
let sat_oe_angled = Vector6::new(
R_EARTH + 500e3, 0.0, 45.0, 45.0, 0.0, 0.0, );
let sat_state_angled = test_sat_ecef_from_oe(sat_oe_angled);
let _result_angled = constraint.evaluate(&test_epoch(), &sat_state_angled, &location_ecef);
}
#[test]
fn test_off_nadir_constraint_evaluate_both_bounds() {
setup_global_test_eop();
let constraint = OffNadirConstraint::new(Some(10.0), Some(30.0)).unwrap();
let location = Vector3::new(0.0, 0.0, 0.0);
let location_ecef = position_geodetic_to_ecef(location, AngleFormat::Degrees).unwrap();
let sat_oe = Vector6::new(R_EARTH + 500e3, 0.0, 0.0, 0.0, 0.0, 0.0);
let sat_state = test_sat_ecef_from_oe(sat_oe);
let _result = constraint.evaluate(&test_epoch(), &sat_state, &location_ecef);
let sat_oe2 = Vector6::new(R_EARTH + 500e3, 0.0, 45.0, 90.0, 0.0, 45.0);
let sat_state2 = test_sat_ecef_from_oe(sat_oe2);
let _result2 = constraint.evaluate(&test_epoch(), &sat_state2, &location_ecef);
}
#[test]
fn test_elevation_mask_constraint_evaluate_interpolation() {
setup_global_test_eop();
let mask = vec![(0.0, 15.0), (90.0, 5.0), (180.0, 10.0), (270.0, 5.0)];
let constraint = ElevationMaskConstraint::new(mask);
let location = Vector3::new(0.0, 45.0, 0.0); let location_ecef = position_geodetic_to_ecef(location, AngleFormat::Degrees).unwrap();
let sat_oe = Vector6::new(R_EARTH + 500e3, 0.0, 45.0, 0.0, 0.0, 0.0);
let sat_state = test_sat_ecef_from_oe(sat_oe);
let _result = constraint.evaluate(&test_epoch(), &sat_state, &location_ecef);
}
#[test]
fn test_elevation_mask_constraint_evaluate_azimuth_wrap() {
setup_global_test_eop();
let mask = vec![
(0.0, 10.0),
(90.0, 5.0),
(180.0, 5.0),
(270.0, 5.0),
(350.0, 12.0), ];
let constraint = ElevationMaskConstraint::new(mask);
let location = Vector3::new(0.0, 0.0, 0.0);
let location_ecef = position_geodetic_to_ecef(location, AngleFormat::Degrees).unwrap();
let sat_oe = Vector6::new(R_EARTH + 500e3, 0.0, 45.0, 0.0, 0.0, 0.0);
let sat_state = test_sat_ecef_from_oe(sat_oe);
let _result = constraint.evaluate(&test_epoch(), &sat_state, &location_ecef);
}
#[test]
fn test_access_constraint_computer_wrapper_new_and_evaluate() {
setup_global_test_eop();
struct TestConstraintComputer {
value: f64,
}
impl AccessConstraintComputer for TestConstraintComputer {
fn evaluate(
&self,
_epoch: &Epoch,
sat_state_ecef: &Vector6<f64>,
_location_ecef: &Vector3<f64>,
) -> bool {
let sat_pos = sat_state_ecef.fixed_rows::<3>(0).into_owned();
let altitude = sat_pos.norm() - R_EARTH;
altitude > self.value
}
fn name(&self) -> &str {
"TestConstraintComputer"
}
}
let computer = TestConstraintComputer { value: 400e3 }; let wrapper = AccessConstraintComputerWrapper::new(computer);
let location = Vector3::new(0.0, 0.0, 0.0);
let location_ecef = position_geodetic_to_ecef(location, AngleFormat::Degrees).unwrap();
let sat_oe_high = Vector6::new(R_EARTH + 500e3, 0.0, 45.0, 0.0, 0.0, 0.0);
let sat_state_high = test_sat_ecef_from_oe(sat_oe_high);
assert!(
wrapper.evaluate(&test_epoch(), &sat_state_high, &location_ecef),
"Satellite at 500 km should satisfy altitude > 400 km constraint"
);
let sat_oe_low = Vector6::new(R_EARTH + 300e3, 0.0, 45.0, 0.0, 0.0, 0.0);
let sat_state_low = test_sat_ecef_from_oe(sat_oe_low);
assert!(
!wrapper.evaluate(&test_epoch(), &sat_state_low, &location_ecef),
"Satellite at 300 km should not satisfy altitude > 400 km constraint"
);
assert_eq!(wrapper.name(), "TestConstraintComputer");
}
}