Visibility Calculation
The across.tools.visibility module provides classes and functions for computing
target visibility windows from astronomical observatories. It determines when celestial
targets are observable by applying various observational constraints such as Sun angle,
Moon angle, Earth limb avoidance, and more.
Overview
The visibility module computes time windows during which a celestial target can be observed from a given observatory, accounting for:
Angular constraints: Sun, Moon, and Earth limb avoidance
Geographic constraints: South Atlantic Anomaly (SAA) avoidance
Local constraints: Altitude and azimuth limits for ground observatories
Multi-observatory coordination: Joint visibility across multiple instruments
The module provides two main visibility calculators:
EphemerisVisibility: Computes visibility for a single observatory using ephemeris data
JointVisibility: Computes combined visibility across multiple observatories
Visibility Constraints
Constraints define conditions that must be met for a target to be observable. When a
constraint is violated (returns True), the target is considered not visible.
Sun Angle Constraint
The SunAngleConstraint restricts observations based on the angular separation
between the target and the Sun. This is essential for protecting instruments from
direct sunlight and reducing scattered light.
from across.tools.visibility.constraints import SunAngleConstraint
# Target must be at least 45° from the Sun
sun_constraint = SunAngleConstraint(min_angle=45)
# Target must be within 135° of the Sun (anti-Sun constraint)
anti_sun = SunAngleConstraint(max_angle=135)
# Combined: target between 45° and 135° from the Sun
combined = SunAngleConstraint(min_angle=45, max_angle=135)
Parameter |
Type |
Description |
|---|---|---|
|
float |
Minimum angle from the Sun in degrees (0-180) |
|
float |
Maximum angle from the Sun in degrees (0-180) |
Moon Angle Constraint
The MoonAngleConstraint restricts observations based on the angular separation
between the target and the Moon. This helps avoid scattered moonlight contamination.
from across.tools.visibility.constraints import MoonAngleConstraint
# Target must be at least 15° from the Moon
moon_constraint = MoonAngleConstraint(min_angle=15)
# Target must be within 90° of the Moon
near_moon = MoonAngleConstraint(max_angle=90)
Parameter |
Type |
Description |
|---|---|---|
|
float |
Minimum angle from the Moon in degrees (0-180) |
|
float |
Maximum angle from the Moon in degrees (0-180) |
Earth Limb Constraint
The EarthLimbConstraint restricts observations for space-based observatories based
on the angular distance from Earth’s limb. This prevents Earth occultation and reduces
atmospheric airglow contamination.
from across.tools.visibility.constraints import EarthLimbConstraint
# Target must be at least 20° above Earth's limb
earth_constraint = EarthLimbConstraint(min_angle=20)
Parameter |
Type |
Description |
|---|---|---|
|
float |
Minimum angle above Earth’s limb in degrees (0-180) |
|
float |
Maximum angle above Earth’s limb in degrees (0-180) |
Note
The Earth limb constraint accounts for the varying angular size of Earth as seen from different orbital altitudes. The angular radius is computed dynamically from the ephemeris data.
South Atlantic Anomaly (SAA) Constraint
The SAAPolygonConstraint restricts observations when the spacecraft is within
the South Atlantic Anomaly region, where increased radiation can affect instruments.
from shapely import Polygon
from across.tools.visibility.constraints import SAAPolygonConstraint
# Define SAA polygon (longitude, latitude coordinates)
saa_polygon = Polygon([
(-90, -30), (-40, -30), (-30, 0), (-40, 5),
(-90, 5), (-90, -30)
])
saa_constraint = SAAPolygonConstraint(polygon=saa_polygon)
Parameter |
Type |
Description |
|---|---|---|
|
Polygon |
Shapely Polygon defining the SAA boundary in (longitude, latitude) |
Altitude/Azimuth Constraint
The AltAzConstraint restricts observations for ground-based observatories based
on altitude and azimuth limits. This accounts for horizon obstructions, zenith blind
spots, and other local constraints.
from across.tools.visibility.constraints import AltAzConstraint
# Target must be above 30° altitude
alt_constraint = AltAzConstraint(altitude_min=30)
# Target must be between 30° and 85° altitude
alt_range = AltAzConstraint(altitude_min=30, altitude_max=85)
# Azimuth restriction (e.g., building obstruction)
az_constraint = AltAzConstraint(azimuth_min=45, azimuth_max=135)
You can also define complex exclusion regions using a Shapely polygon:
from shapely import Polygon
# Define an exclusion region in (altitude, azimuth) space
exclusion = Polygon([(30, 0), (30, 45), (60, 45), (60, 0)])
complex_constraint = AltAzConstraint(polygon=exclusion)
Parameter |
Type |
Description |
|---|---|---|
|
float |
Minimum altitude in degrees (0-90) |
|
float |
Maximum altitude in degrees (0-90) |
|
float |
Minimum azimuth in degrees (0-360) |
|
float |
Maximum azimuth in degrees (0-360) |
|
Polygon |
Shapely Polygon defining exclusion region in (alt, az) space |
Visibility Constraints with Logical Operators
Visibility constraints represent when a target is NOT visible (blocked). Combine multiple constraints with logical operators to create complex observation criteria:
from across.tools.visibility import compute_ephemeris_visibility
from across.tools.visibility.constraints import (
SunAngleConstraint,
MoonAngleConstraint,
EarthLimbConstraint,
)
# Create individual constraints (each defines when target is NOT visible)
sun = SunAngleConstraint(min_angle=45) # Not visible if <45° from Sun
moon = MoonAngleConstraint(min_angle=10) # Not visible if <10° from Moon
earth = EarthLimbConstraint(min_angle=33) # Not visible if <33° above limb
# Typical usage: OR logic (if ANY constraint blocks, target not visible)
constraints = sun | moon | earth
# Rarely used: AND logic (if ALL constraints block together)
both_required = sun & moon & earth
# Rarely used: NOT logic (inverts constraint)
not_earth_blocked = ~earth
# Use in visibility calculation
visibility = compute_ephemeris_visibility(
begin=begin,
end=end,
ephemeris=ephemeris,
constraints=constraints, # Works with composite constraints too
ra=83.6333,
dec=22.0145,
)
Ephemeris Visibility
The EphemerisVisibility class computes visibility windows for a single observatory
using pre-computed ephemeris data and a set of constraints.
Basic Usage
from datetime import datetime
from across.tools.ephemeris import compute_tle_ephemeris
from across.tools.tle import get_tle
from across.tools.visibility import compute_ephemeris_visibility
from across.tools.visibility.constraints import (
SunAngleConstraint,
MoonAngleConstraint,
EarthLimbConstraint,
)
# First, compute ephemeris for the spacecraft
tle = get_tle(norad_id=28485, epoch=datetime(2024, 1, 1))
ephem = compute_tle_ephemeris(
begin=datetime(2024, 1, 1),
end=datetime(2024, 1, 8),
step_size=60,
tle=tle
)
# Define constraints
constraints = [
SunAngleConstraint(min_angle=45),
MoonAngleConstraint(min_angle=15),
EarthLimbConstraint(min_angle=20),
]
# Compute visibility for a target
visibility = compute_ephemeris_visibility(
ra=180.0, # Right ascension in degrees
dec=45.0, # Declination in degrees
begin=datetime(2024, 1, 1),
end=datetime(2024, 1, 8),
ephemeris=ephem,
constraints=constraints,
observatory_name="Swift",
)
# Access visibility windows
print(f"Found {len(visibility.visibility_windows)} visibility windows")
for window in visibility.visibility_windows:
print(f" {window.window.begin.datetime} to {window.window.end.datetime}")
print(f" Duration: {window.max_visibility_duration} seconds")
print(f" Start reason: {window.constraint_reason.start_reason}")
print(f" End reason: {window.constraint_reason.end_reason}")
Using SkyCoord Instead of RA/Dec
You can also specify the target using an astropy SkyCoord object:
from astropy.coordinates import SkyCoord
import astropy.units as u
target = SkyCoord(ra=180*u.deg, dec=45*u.deg)
visibility = compute_ephemeris_visibility(
coordinate=target,
begin=datetime(2024, 1, 1),
end=datetime(2024, 1, 8),
ephemeris=ephem,
constraints=constraints,
observatory_name="Swift",
)
Parameters
Parameter |
Type |
Description |
|---|---|---|
|
float |
Right ascension in degrees (0-360) |
|
float |
Declination in degrees (-90 to 90) |
|
SkyCoord |
Alternative to ra/dec specification |
|
datetime/Time |
Start time for visibility calculation |
|
datetime/Time |
End time for visibility calculation |
|
Ephemeris |
Pre-computed ephemeris for the observatory |
|
list[Constraint] |
List of constraint objects to apply |
|
Quantity |
Time step size (default: 60 seconds) |
|
str |
Name of the observatory |
|
UUID |
Unique identifier for the observatory (optional) |
|
int |
Minimum visibility duration in seconds (default: 0) |
Checking Visibility at Specific Times
After computing visibility, you can check if the target is visible at specific times:
from astropy.time import Time
# Check single time
t = Time("2024-01-01T12:00:00")
is_visible = visibility.visible(t)
print(f"Visible at {t}: {is_visible}")
# Check multiple times
times = Time(["2024-01-01T12:00:00", "2024-01-01T18:00:00", "2024-01-02T00:00:00"])
visible_array = visibility.visible(times)
print(f"Visibility: {visible_array}")
Computed Values
The computed_values attribute provides access to geometric quantities calculated
during visibility analysis. After running a visibility calculation, you can inspect
these values for further analysis or visualization.
Overview
During visibility calculations, constraints compute various geometric quantities such as
angular separations and positional coordinates. These values are stored in the
computed_values attribute (a VisibilityComputedValues object) so you can access
them after the calculation completes.
Available Computed Values
Field |
Type |
Description |
|---|---|---|
|
Quantity (array) |
Angular separation between target and Sun at each time step (degrees) |
|
Quantity (array) |
Angular separation between target and Moon at each time step (degrees) |
|
Quantity (array) |
Angular separation between target and Earth limb at each time step (degrees) |
|
SkyCoord |
Altitude-azimuth coordinates of target from observatory |
Note
Not all fields are populated for every visibility calculation. Values are only computed and stored by constraints that need them.
Accessing Computed Values
After running a visibility calculation, you can access the computed values for analysis:
# Compute visibility with Sun and Moon constraints
visibility = compute_ephemeris_visibility(
ra=180.0, dec=45.0,
begin=begin, end=end,
ephemeris=ephem,
constraints=[
SunAngleConstraint(min_angle=45),
MoonAngleConstraint(min_angle=15),
],
observatory_name="Swift",
)
# Access computed angular separations
if visibility.computed_values.sun_angle is not None:
print(f"Min Sun angle: {visibility.computed_values.sun_angle.min():.2f}")
print(f"Max Sun angle: {visibility.computed_values.sun_angle.max():.2f}")
print(f"Mean Sun angle: {visibility.computed_values.sun_angle.mean():.2f}")
if visibility.computed_values.moon_angle is not None:
print(f"Min Moon angle: {visibility.computed_values.moon_angle.min():.2f}")
print(f"Max Moon angle: {visibility.computed_values.moon_angle.max():.2f}")
# Use for plotting or further analysis
import matplotlib.pyplot as plt
if visibility.computed_values.sun_angle is not None:
plt.plot(visibility.timestamp.datetime, visibility.computed_values.sun_angle)
plt.xlabel('Time')
plt.ylabel('Sun Angle (degrees)')
plt.title('Target - Sun Angular Separation Over Time')
plt.show()
Which Constraints Populate Which Values
Different constraints compute and store different values:
SunAngleConstraint→ storessun_angleMoonAngleConstraint→ storesmoon_angleEarthLimbConstraint→ storesearth_angleAltAzConstraint→ storesalt_az
Only the constraints you actually use will populate their corresponding fields in
computed_values.
Joint Visibility
The JointVisibility class computes combined visibility windows across multiple
observatories or instruments. This is useful for coordinated multi-observatory campaigns.
Basic Usage
from uuid import uuid4
from across.tools.visibility import compute_joint_visibility
# Compute visibility for multiple observatories
vis1 = compute_ephemeris_visibility(
ra=180.0, dec=45.0,
begin=begin, end=end,
ephemeris=ephem1,
constraints=constraints1,
observatory_name="Observatory1",
)
vis2 = compute_ephemeris_visibility(
ra=180.0, dec=45.0,
begin=begin, end=end,
ephemeris=ephem2,
constraints=constraints2,
observatory_name="Observatory2",
)
# Compute joint visibility (intersection of all visibility windows)
joint = compute_joint_visibility(
visibilities=[vis1, vis2],
instrument_ids=[uuid4(), uuid4()],
)
print(f"Joint visibility windows: {len(joint.visibility_windows)}")
for window in joint.visibility_windows:
print(f" {window.window.begin.datetime} to {window.window.end.datetime}")
Requirements for Joint Visibility
All input visibility objects must have:
The same target coordinates (within ~15 arcsec tolerance)
The same begin and end times
The same step size
Parameters
Parameter |
Type |
Description |
|---|---|---|
|
list[Visibility] |
List of Visibility objects to combine |
|
list[UUID] |
Unique identifiers for each instrument/observatory |
Visibility Windows
Visibility calculations return VisibilityWindow objects containing detailed
information about each visibility period.
VisibilityWindow Attributes
Attribute |
Description |
|---|---|
|
Start of visibility window (ConstrainedDate with datetime and constraint info) |
|
End of visibility window (ConstrainedDate with datetime and constraint info) |
|
Duration of window in seconds |
|
Description of why visibility started (which constraint was satisfied) |
|
Description of why visibility ended (which constraint was violated) |
Example: Accessing Window Details
for window in visibility.visibility_windows:
# Window timing
start = window.window.begin.datetime
end = window.window.end.datetime
duration = window.max_visibility_duration
# Constraint information
start_constraint = window.window.begin.constraint
end_constraint = window.window.end.constraint
# Human-readable reasons
start_reason = window.constraint_reason.start_reason
end_reason = window.constraint_reason.end_reason
print(f"Window: {start} to {end}")
print(f" Duration: {duration} seconds")
print(f" Started because: {start_reason}")
print(f" Ended because: {end_reason}")
Plotting Visibility
Both EphemerisVisibility and JointVisibility objects can be visualized with
the plot method. The returned object is a Plotly Figure, so it can be shown
directly in notebooks, or further customized before display.
Single-observatory plot
fig = visibility.plot(
offset=0,
width=700,
height=1000,
)
fig.show()
Joint visibility plot
joint = compute_joint_visibility(
visibilities=[vis_swift, vis_lsst],
instrument_ids=[obs_id_swift, obs_id_lsst],
)
fig = joint.plot(
offset=0,
width=700,
height=1000,
)
fig.show()
In joint plots, individual instrument windows are shown in separate columns and joint windows are highlighted across all instrument columns.
Example JointVisibility.plot output.
The plot method accepts the following keyword arguments:
fig: Existing Plotly figure to add traces to.offset: Horizontal placement of window columns on the x-axis.width: Figure width in pixels.height: Figure height in pixels.
Constraint Serialization
Constraints can be serialized to/from JSON for storage or transmission:
from across.tools.visibility import constraints_to_json, constraints_from_json
from across.tools.visibility.constraints import SunAngleConstraint, MoonAngleConstraint
# Define constraints
constraints = [
SunAngleConstraint(min_angle=45, max_angle=180),
MoonAngleConstraint(min_angle=15),
]
# Serialize to JSON
json_str = constraints_to_json(constraints)
print(json_str)
# Output: [{"name":"Sun Angle","short_name":"Sun","min_angle":45.0,"max_angle":180.0},...]
# Deserialize from JSON
loaded_constraints = constraints_from_json(json_str)
Complete Example
Here’s a complete example showing a typical visibility calculation workflow:
from datetime import datetime
from across.tools.ephemeris import compute_tle_ephemeris
from across.tools.tle import get_tle
from across.tools.visibility import compute_ephemeris_visibility
from across.tools.visibility.constraints import (
SunAngleConstraint,
MoonAngleConstraint,
EarthLimbConstraint,
SAAPolygonConstraint,
)
from shapely import Polygon
# Step 1: Get TLE data for the spacecraft
tle = get_tle(
norad_id=28485, # Swift
epoch=datetime(2024, 1, 1),
spacetrack_user="your_username",
spacetrack_pwd="your_password"
)
# Step 2: Compute ephemeris
ephem = compute_tle_ephemeris(
begin=datetime(2024, 1, 1),
end=datetime(2024, 1, 8),
step_size=60,
tle=tle
)
# Step 3: Define SAA polygon
saa = Polygon([
(-90, -30), (-40, -30), (-30, 0), (-40, 5),
(-90, 5), (-90, -30)
])
# Step 4: Define constraints
constraints = [
SunAngleConstraint(min_angle=45),
MoonAngleConstraint(min_angle=15),
EarthLimbConstraint(min_angle=20),
SAAPolygonConstraint(polygon=saa),
]
# Step 5: Compute visibility
visibility = compute_ephemeris_visibility(
ra=266.417, # Galactic Center
dec=-29.008,
begin=datetime(2024, 1, 1),
end=datetime(2024, 1, 8),
ephemeris=ephem,
constraints=constraints,
observatory_name="Swift",
min_vis=300, # Minimum 5 minute windows
)
# Step 6: Analyze results
total_visibility = sum(w.max_visibility_duration for w in visibility.visibility_windows)
print(f"Total visibility: {total_visibility / 3600:.1f} hours over 7 days")
print(f"Number of windows: {len(visibility.visibility_windows)}")
for i, window in enumerate(visibility.visibility_windows[:5], 1):
print(f"\nWindow {i}:")
print(f" Start: {window.window.begin.datetime}")
print(f" End: {window.window.end.datetime}")
print(f" Duration: {window.max_visibility_duration / 60:.1f} minutes")
API Reference
See the API Reference for complete class and function documentation.