Access properties are geometric and temporal measurements computed for each access window. Brahe automatically calculates core properties during access searches, and provides both built-in and custom property computers for mission-specific analysis.
Core properties are attributes of the AccessWindow object returned by access computations and can be accessed directly like window.window_open or window.elevation_max.
Below are examples of accessing core properties in Python and Rust.
Property computers allow users to extend the access computation system to define and compute custom properties for each access window beyond the core set. These computations are performed after access windows are identified and refined.
Python users can implement property computers by subclassing AccessPropertyComputer, while in Rust you implement the AccessPropertyComputer trait. These traits require the implementation of the sampling_config and compute methods. sampling_config defines how satellite states are sampled during the access window, and compute performs the actual property calculation using those sampled states.
Brahe defines a few built-in property computers for common use cases, and users can create custom property computers for application-specific needs.
Property computers use SamplingConfig to determine when satellite states are sampled within the access window. That is, what epoch, state pairs are provided to the computer for its calculations.
You can choose from several sampling modes:
relative_points([0.0, 0.5, 1.0]) - Samples at specified fractions of the window duration with 0.0 being the start and 1.0 being the end
fixed_count(n) - Samples a fixed number of evenly spaced points within the window
fixed_interval(interval, offset) - Samples at regular time intervals (defined by seconds between samples) throughout the window with an optional offset
midpoint - Samples only at the midpoint of the window
This allows you to compute time-series data at specific intervals or points.
//! ```#[allow(unused_imports)]usebraheasbh;usebh::access::{DopplerComputer,SamplingConfig,PropertyValue};usebh::utils::Identifiable;fnmain()->Result<(),Box<dynstd::error::Error>>{bh::initialize_eop()?;// S-band downlink only (8.4 GHz)let_doppler=DopplerComputer::new(None,// No uplinkSome(8.4e9),// Downlink frequencySamplingConfig::FixedInterval{interval:0.1,offset:0.0}// 0.1 seconds);println!("Downlink only: uplink=None, downlink=8.4e9 Hz");// Downlink only: uplink=None, downlink=8.4e9 Hz// Both uplink (2.0 GHz) and downlink (8.4 GHz)letdoppler=DopplerComputer::new(Some(2.0e9),// Uplink frequencySome(8.4e9),// Downlink frequencySamplingConfig::FixedCount(100));println!("Both frequencies: uplink=2.0e9 Hz, downlink=8.4e9 Hz");// Both frequencies: uplink=2.0e9 Hz, downlink=8.4e9 Hz// ISS orbitlettle_line1="1 25544U 98067A 25306.42331346 .00010070 00000-0 18610-3 0 9999";lettle_line2="2 25544 51.6344 342.0717 0004969 8.9436 351.1640 15.49700017536601";letpropagator=bh::SGPPropagator::from_tle(tle_line1,tle_line2,60.0)?.with_name("ISS");letepoch_start=propagator.epoch;letepoch_end=epoch_start+24.0*3600.0;// Ground station (lon, lat, alt)letlocation=bh::PointLocation::new(-74.0060,40.7128,0.0);// Compute accesses with Dopplerletconstraint=bh::ElevationConstraint::new(Some(10.0),None)?;letwindows=bh::location_accesses(&location,&propagator,epoch_start,epoch_end,&constraint,Some(&[&doppler]),// Property computersNone,// Use default configNone,// No time tolerance)?;// Access computed propertiesletwindow=&windows[0];letdoppler_data=window.properties.additional.get("doppler_downlink").unwrap();// Extract values from TimeSeriesletvalues=matchdoppler_data{PropertyValue::TimeSeries{values,..}=>values,_=>panic!("Expected TimeSeries"),};letmin_val=values.iter().fold(f64::INFINITY,|a:f64,&b|a.min(b));letmax_val=values.iter().fold(f64::NEG_INFINITY,|a:f64,&b|a.max(b));println!("\nFirst pass downlink Doppler shift range: {:.1} to {:.1} Hz",min_val,max_val);// First pass Doppler shift range: -189220.9 to 189239.8 HzOk(())}
Doppler Physics:
Uplink: \(\Delta f = f_0\frac{v_{los}}{c - v_{los}}\) - Ground station pre-compensates transmit frequency
Downlink: \(\Delta f = -f_0\frac{v_{los}}{c}\) - Ground station adjusts receive frequency
Where \(v_{los}\) is the velocity of the object along the line of sight from the observer. With \(v_{los} < 0\) when approaching and \(v_{los} > 0\) when receding.
importbraheasbhbh.initialize_eop()# Compute range at 50 evenly-spaced pointsrange_comp=bh.RangeComputer(sampling_config=bh.SamplingConfig.fixed_count(50))print(f"Range computer: {range_comp}")# Range computer: RangeComputer(sampling=FixedCount(50))# Create a simple scenario to demonstrate usage# ISS orbittle_line1="1 25544U 98067A 25306.42331346 .00010070 00000-0 18610-3 0 9999"tle_line2="2 25544 51.6344 342.0717 0004969 8.9436 351.1640 15.49700017536601"propagator=bh.SGPPropagator.from_tle(tle_line1,tle_line2,60.0).with_name("ISS")epoch_start=propagator.epochepoch_end=epoch_start+24*3600.0# 24 hours# Ground stationlocation=bh.PointLocation(-74.0060,40.7128,0.0)# Compute accesses with rangeconstraint=bh.ElevationConstraint(min_elevation_deg=10.0)windows=bh.location_accesses(location,propagator,epoch_start,epoch_end,constraint,property_computers=[range_comp],)# Access computed propertieswindow=windows[0]range_data=window.properties.additional["range"]distances_m=range_data["values"]# metersdistances_km=[d/1000.0fordindistances_m]print(f"\nRange varies from {min(distances_km):.1f} to {max(distances_km):.1f} km")# Range varies from 658.9 to 1501.0 km
//! ```#[allow(unused_imports)]usebraheasbh;usebh::access::{RangeComputer,SamplingConfig,PropertyValue};usebh::utils::Identifiable;fnmain()->Result<(),Box<dynstd::error::Error>>{bh::initialize_eop()?;// Compute range at 50 evenly-spaced pointsletrange_comp=RangeComputer::new(SamplingConfig::FixedCount(50));println!("Range computer: sampling=FixedCount(50)");// Range computer: sampling=FixedCount(50)// ISS orbitlettle_line1="1 25544U 98067A 25306.42331346 .00010070 00000-0 18610-3 0 9999";lettle_line2="2 25544 51.6344 342.0717 0004969 8.9436 351.1640 15.49700017536601";letpropagator=bh::SGPPropagator::from_tle(tle_line1,tle_line2,60.0)?.with_name("ISS");letepoch_start=propagator.epoch;letepoch_end=epoch_start+24.0*3600.0;// Ground stationletlocation=bh::PointLocation::new(-74.0060,40.7128,0.0);// Compute accesses with rangeletconstraint=bh::ElevationConstraint::new(Some(10.0),None)?;letwindows=bh::location_accesses(&location,&propagator,epoch_start,epoch_end,&constraint,Some(&[&range_comp]),// Property computersNone,// Use default configNone,// No time tolerance)?;// Access computed propertiesletwindow=&windows[0];letrange_data=window.properties.additional.get("range").unwrap();// Extract values from TimeSeriesletdistances_m=matchrange_data{PropertyValue::TimeSeries{values,..}=>values,_=>panic!("Expected TimeSeries"),};// Convert to kmletmin_km=distances_m.iter().fold(f64::INFINITY,|a:f64,&b|a.min(b))/1000.0;letmax_km=distances_m.iter().fold(f64::NEG_INFINITY,|a:f64,&b|a.max(b))/1000.0;println!("\nRange varies from {:.1} to {:.1} km",min_km,max_km);// Range varies from 658.9 to 1501.0 kmOk(())}
Computes line-of-sight velocity (range rate) with the convention that positive values indicate increasing range (satellite receding) and negative values indicate decreasing range (satellite approaching):
You can also create your own property computer to compute application-specific properties values. The system will pre-sample the satellite state at the specified times defined by your SamplingConfig, so you don't need to manually propagate the trajectory.
This section provides examples of custom property computers in both Python and Rust.
importbraheasbhimportnumpyasnpbh.initialize_eop()classMaxSpeedComputer(bh.AccessPropertyComputer):"""Computes maximum ground speed during access."""defsampling_config(self):# Sample every 0.5 secondsreturnbh.SamplingConfig.fixed_interval(0.5,0.0)defcompute(self,window,sample_times,sample_states_ecef,location_ecef,location_geodetic):# Extract velocities from statesvelocities=sample_states_ecef[:,3:6]speeds=np.linalg.norm(velocities,axis=1)max_speed=np.max(speeds)# Single value -> returns as scalarreturn{"max_ground_speed":max_speed,# Will be stored as Scalar}defproperty_names(self):return["max_ground_speed"]# ISS orbittle_line1="1 25544U 98067A 25306.42331346 .00010070 00000-0 18610-3 0 9999"tle_line2="2 25544 51.6344 342.0717 0004969 8.9436 351.1640 15.49700017536601"propagator=bh.SGPPropagator.from_tle(tle_line1,tle_line2,60.0).with_name("ISS")epoch_start=propagator.epochepoch_end=epoch_start+24*3600.0# 24 hours# Ground stationlocation=bh.PointLocation(-74.0060,40.7128,0.0)# Compute with custom propertymax_speed=MaxSpeedComputer()constraint=bh.ElevationConstraint(min_elevation_deg=10.0)windows=bh.location_accesses(location,propagator,epoch_start,epoch_end,constraint,property_computers=[max_speed],)forwindowinwindows:speed=window.properties.additional["max_ground_speed"]print(f"Max speed: {speed:.1f} m/s")# Output example:# Max speed: 7360.1 m/s# Max speed: 7365.5 m/s# Max speed: 7361.2 m/s# Max speed: 7357.5 m/s# Max speed: 7357.8 m/s# Max speed: 7360.0 m/s
To implement a custom property computer in Rust, create a struct that implements the AccessPropertyComputer trait by defining the sampling_config and compute methods.