Skip to content

Constraints

Constraints define the criteria that must be satisfied for satellite access to ground locations. Brahe provides a comprehensive constraint system with built-in geometric constraints, logical composition operators, and support for custom user-defined constraints.

A constraint evaluates to true when access conditions are met and false otherwise. During access computation, the algorithm searches for continuous time periods where constraints remain true, identifying these as access windows.

Built-in Constraints

Elevation Constraint

The most common constraint - requires satellites to be above a minimum elevation angle. This accounts for terrain obstructions, atmospheric effects, and antenna pointing limits.

Basic elevation constraint:

1
2
3
4
5
6
7
import brahe as bh

# Require satellite to be at least 10 degrees above horizon
constraint = bh.ElevationConstraint(min_elevation_deg=10.0)

print(f"Created: {constraint}")
# Created:  ElevationConstraint(>= 10.00°)
use brahe as bh;

fn main() {
    // Require satellite to be at least 10 degrees above horizon
    let constraint = bh::ElevationConstraint::new(
        Some(10.0),  // min elevation (degrees)
        None  // max elevation (defaults to 90°)
    ).unwrap();

    println!("Created: {}", constraint);
    // Created: ElevationConstraint(>= 10.00°)
}

With maximum elevation:

1
2
3
4
5
6
7
import brahe as bh

# Side-looking sensor with elevation range
constraint = bh.ElevationConstraint(min_elevation_deg=10.0, max_elevation_deg=80.0)

print(f"Created: {constraint}")
# Created: ElevationConstraint(10.00° - 80.00°)
use brahe as bh;

fn main() {
    // Side-looking sensor with elevation range
    let constraint = bh::ElevationConstraint::new(
        Some(10.0),
        Some(80.0)
    ).unwrap();

    println!("Created: {}", constraint);
    // Created: ElevationConstraint(10.00° - 80.00°)
}

Elevation Mask Constraint

Models azimuth-dependent elevation masks for terrain profiles, mountains, or buildings blocking low-elevation views in specific directions.

import brahe as bh

# Define elevation mask: [(azimuth_deg, elevation_deg), ...]
# Azimuth clockwise from North (0-360)
mask_points = [
    (0.0, 5.0),  # North: 5° minimum
    (90.0, 15.0),  # East: 15° minimum (mountains)
    (180.0, 8.0),  # South: 8° minimum
    (270.0, 10.0),  # West: 10° minimum
    (360.0, 5.0),  # Back to North
]

constraint = bh.ElevationMaskConstraint(mask_points)

print(f"Created: {constraint}")
# Created: ElevationMaskConstraint(Min: 5.00° at 0.00°, Max: 15.00° at 90.00°)
use brahe as bh;

fn main() {
    // Define mask points in radians
    let mask_points = vec![
        (0.0, 5.0),
        (90.0, 15.0),
        (180.0, 8.0),
        (270.0, 10.0),
        (360.0, 5.0),
    ];

    let constraint = bh::ElevationMaskConstraint::new(mask_points);

    println!("Created: {}", constraint);
    // Created: ElevationMaskConstraint(Min: 5.00° at 0.00°, Max: 15.00° at 90.00°)
}

Off-Nadir Constraint

Limits the off-nadir viewing angle for imaging satellites. Off-nadir angle is measured from the satellite's nadir (straight down) to the target location.

Imaging payload:

1
2
3
4
5
6
7
import brahe as bh

# Imaging payload with 30° maximum off-nadir
constraint = bh.OffNadirConstraint(max_off_nadir_deg=30.0)

print(f"Created: {constraint}")
# Created: OffNadirConstraint(<= 30.0°)
use brahe as bh;

fn main() {
    // Imaging payload with 30° maximum off-nadir
    let constraint = bh::OffNadirConstraint::new(
        None,  // min off-nadir (defaults to 0°)
        Some(30.0)
    ).unwrap();

    println!("Created: {}", constraint);
    // Created: OffNadirConstraint(<= 30.0°)
}

Side-looking radar:

1
2
3
4
5
6
7
import brahe as bh

# Side-looking radar requiring specific geometry
constraint = bh.OffNadirConstraint(min_off_nadir_deg=20.0, max_off_nadir_deg=45.0)

print(f"Created: {constraint}")
# Created: OffNadirConstraint(20.0° - 45.0°)
use brahe as bh;

fn main() {
    // Side-looking radar
    let constraint = bh::OffNadirConstraint::new(
        Some(20.0),
        Some(45.0)
    ).unwrap();

    println!("Created: {}", constraint);
    // Created: OffNadirCOffNadirConstraint(20.0° - 45.0°)
}

Local Time Constraint

Filters access windows by local solar time at the ground location. Useful for daylight-only imaging or night-time astronomy observations.

Single time window:

1
2
3
4
5
6
7
8
import brahe as bh

# Daylight imaging: 8:00 AM to 6:00 PM local solar time
# Times in military format: HHMM
constraint = bh.LocalTimeConstraint(time_windows=[(800, 1800)])

print(f"Created: {constraint}")
# Created: LocalTimeConstraint(08:00-18:00)
use brahe as bh;

fn main() {
    // Daylight imaging: 8:00 AM to 6:00 PM
    let constraint = bh::LocalTimeConstraint::new(
        vec![(800, 1800)]
    ).unwrap();

    println!("Created: {}", constraint);
    // Created: LocalTimeConstraint(08:00-18:00)
}

Multiple time windows:

1
2
3
4
5
6
7
import brahe as bh

# Multiple windows: dawn and dusk passes
constraint = bh.LocalTimeConstraint(time_windows=[(600, 800), (1800, 2000)])

print(f"Created: {constraint}")
# Created: LocalTimeConstraint(06:00-08:00, 18:00-20:00)
use brahe as bh;

fn main() {
    // Multiple windows
    let constraint = bh::LocalTimeConstraint::new(
        vec![(600, 800), (1800, 2000)]
    ).unwrap();

    println!("Created: {}", constraint);
    // Created: LocalTimeConstraint(06:00-08:00, 18:00-20:00)
}

Using decimal hours:

1
2
3
4
5
6
7
import brahe as bh

# Alternative: specify in decimal hours
constraint = bh.LocalTimeConstraint.from_hours(time_windows=[(8.0, 18.0)])

print(f"Created: {constraint}")
# Created: LocalTimeConstraint(08:00-18:00)
use brahe as bh;

fn main() {
    // From decimal hours
    let constraint = bh::LocalTimeConstraint::from_hours(
        vec![(8.0, 18.0)]
    ).unwrap();

    println!("Created: {}", constraint);
    // Created: LocalTimeConstraint(08:00-18:00)
}

Local Solar Time

Local solar time is based on the Sun's position relative to the location, not clock time zones. Noon (1200) is when the Sun is highest in the sky.

Look Direction Constraint

Requires the satellite to look in a specific direction relative to its velocity vector - left, right, or either side.

Left-looking:

1
2
3
4
5
6
7
8
import brahe as bh
from brahe import LookDirection

# Require left-looking geometry
constraint = bh.LookDirectionConstraint(allowed=LookDirection.LEFT)

print(f"Created: {constraint}")
# Created: LookDirectionConstraint(Left)
use brahe as bh;

fn main() {
    // Require left-looking geometry
    let constraint = bh::LookDirectionConstraint::new(
        bh::LookDirection::Left
    );

    println!("Created: {}", constraint);
    // Created: LookDirectionConstraint(Left)
}

Ascending-Descending Constraint

Filters passes by whether the satellite is ascending (moving south-to-north) or descending (north-to-south) over the location.

Ascending passes:

1
2
3
4
5
6
7
8
import brahe as bh
from brahe import AscDsc

# Only ascending passes
constraint = bh.AscDscConstraint(allowed=AscDsc.ASCENDING)

print(f"Created: {constraint}")
# Created: AscDscConstraint(Ascending)
use brahe as bh;

fn main() {
    // Only ascending passes
    let constraint = bh::AscDscConstraint::new(
        bh::AscDsc::Ascending
    );

    println!("Created: {}", constraint);
    // Created: AscDscConstraint(Ascending)
}

Constraint Composition

Combine constraints using Boolean logic to express complex requirements.

ConstraintAll (AND Logic)

All child constraints must be satisfied simultaneously:

import brahe as bh

# Elevation > 10° AND daylight hours
elev = bh.ElevationConstraint(min_elevation_deg=10.0)
daytime = bh.LocalTimeConstraint(time_windows=[(800, 1800)])

constraint = bh.ConstraintAll(constraints=[elev, daytime])

print(f"Created: {constraint}")
# Created: ElevationConstraint(>= 60.00°) || LookDirectionConstraint(Right)
fn main() {
    // Elevation > 10° AND daylight hours
    let elev = Box::new(bh::ElevationConstraint::new(Some(10.0), None).unwrap());
    let daytime = Box::new(bh::LocalTimeConstraint::new(vec![(800, 1800)]).unwrap());

    let constraint = bh::ConstraintComposite::All(vec![elev, daytime]);

    println!("Created: {}", constraint);
    // Created: ElevationConstraint(>= 60.00°) || LookDirectionConstraint(Right)
}

ConstraintAny (OR Logic)

At least one child constraint must be satisfied:

import brahe as bh

# High elevation OR right-looking geometry
high_elev = bh.ElevationConstraint(min_elevation_deg=60.0)
right_look = bh.LookDirectionConstraint(allowed=bh.LookDirection.RIGHT)

constraint = bh.ConstraintAny(constraints=[high_elev, right_look])

print(f"Created: {constraint}")
# Created: ConstraintAny(constraints: [ElevationConstraint(...), LookDirectionConstraint(...)])
fn main() {
    // High elevation OR right-looking geometry
    let high_elev = Box::new(bh::ElevationConstraint::new(Some(60.0), None).unwrap());
    let right_look = Box::new(bh::LookDirectionConstraint::new(bh::LookDirection::Right));

    let constraint = bh::ConstraintComposite::Any(vec![high_elev, right_look]);

    println!("Created: {}", constraint);
    // Created: ElevationConstraint(>= 60.00°) || LookDirectionConstraint(Right)
}

ConstraintNot (Negation)

Inverts a constraint - access occurs when the child constraint is NOT satisfied:

1
2
3
4
5
6
7
8
import brahe as bh

# Avoid daylight (e.g., for night-time astronomy)
daytime = bh.LocalTimeConstraint(time_windows=[(600, 2000)])
night_only = bh.ConstraintNot(constraint=daytime)

print(f"Created: {night_only}")
# Created: !LocalTimeConstraint(06:00-20:00)
1
2
3
4
5
6
7
8
fn main() {
    // Avoid daylight (e.g., for night-time astronomy)
    let daytime = Box::new(bh::LocalTimeConstraint::new(vec![(600, 2000)]).unwrap());
    let night_only = bh::ConstraintComposite::Not(daytime);

    println!("Created: {}", night_only);
    // Created: !LocalTimeConstraint(06:00-20:00)
}

Complex Composition

Build complex logic by combining multiple constraints:

import brahe as bh

# Complex constraint: (High elevation AND daylight)
# Note: Python API currently supports single-level composition
# For nested constraints, use Rust API with Box<dyn AccessConstraint>

# High elevation AND daylight
high_elev = bh.ElevationConstraint(min_elevation_deg=60.0)
daytime = bh.LocalTimeConstraint(time_windows=[(800, 1800)])
look_right = bh.LookDirectionConstraint(allowed=bh.LookDirection.RIGHT)

# Combine multiple constraints with AND
constraint = bh.ConstraintAll(constraints=[high_elev, daytime, look_right])

print(f"Created: {constraint}")
# Created: ElevationConstraint(>= 60.00°) && LocalTimeConstraint(08:00-18:00) && LookDirectionConstraint(Right)
fn main() {
    // Complex constraint: High elevation AND daylight AND right-looking
    let high_elev = Box::new(bh::ElevationConstraint::new(Some(60.0), None).unwrap());
    let daytime = Box::new(bh::LocalTimeConstraint::new(vec![(800, 1800)]).unwrap());
    let look_right = Box::new(bh::LookDirectionConstraint::new(bh::LookDirection::Right));

    // Combine multiple constraints with AND
    let constraint = bh::ConstraintComposite::All(vec![high_elev, daytime, look_right]);

    println!("Created: {}", constraint);
    // Created: ElevationConstraint(>= 60.00°) && LocalTimeConstraint(08:00-18:00) && LookDirectionConstraint(Right)
}

Custom Constraints (Python)

Python users can create fully custom constraints by implementing the AccessConstraintComputer interface:

import brahe as bh
import numpy as np


class MaxRangeConstraint(bh.AccessConstraintComputer):
    """Limit access to satellites within a maximum range."""

    def __init__(self):
        self.max_range_m = 2000.0 * 1000.0  # 2000 km in meters

    def evaluate(self, epoch, satellite_state_ecef, location_ecef):
        """Return True when constraint is satisfied"""
        # Compute range vector from location to satellite
        range_vec = satellite_state_ecef[:3] - location_ecef
        range_m = np.linalg.norm(range_vec)

        return range_m <= self.max_range_m

    def name(self):
        return f"MaxRange({self.max_range_m / 1000:.0f}km)"


# Use custom constraint
constraint = MaxRangeConstraint()

print(f"Created: {constraint.name()}")
# Created: MaxRange(2000km)

Custom Constraints in Rust

Rust users implement the AccessConstraint trait directly. This provides maximum performance but requires recompiling the library.


See Also