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)
SunAngleConstraint Parameters

Parameter

Type

Description

min_angle

float

Minimum angle from the Sun in degrees (0-180)

max_angle

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)
MoonAngleConstraint Parameters

Parameter

Type

Description

min_angle

float

Minimum angle from the Moon in degrees (0-180)

max_angle

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)
EarthLimbConstraint Parameters

Parameter

Type

Description

min_angle

float

Minimum angle above Earth’s limb in degrees (0-180)

max_angle

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)
SAAPolygonConstraint Parameters

Parameter

Type

Description

polygon

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)
AltAzConstraint Parameters

Parameter

Type

Description

altitude_min

float

Minimum altitude in degrees (0-90)

altitude_max

float

Maximum altitude in degrees (0-90)

azimuth_min

float

Minimum azimuth in degrees (0-360)

azimuth_max

float

Maximum azimuth in degrees (0-360)

polygon

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

compute_ephemeris_visibility Parameters

Parameter

Type

Description

ra

float

Right ascension in degrees (0-360)

dec

float

Declination in degrees (-90 to 90)

coordinate

SkyCoord

Alternative to ra/dec specification

begin

datetime/Time

Start time for visibility calculation

end

datetime/Time

End time for visibility calculation

ephemeris

Ephemeris

Pre-computed ephemeris for the observatory

constraints

list[Constraint]

List of constraint objects to apply

step_size

Quantity

Time step size (default: 60 seconds)

observatory_name

str

Name of the observatory

observatory_id

UUID

Unique identifier for the observatory (optional)

min_vis

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

Computed Values Fields

Field

Type

Description

sun_angle

Quantity (array)

Angular separation between target and Sun at each time step (degrees)

moon_angle

Quantity (array)

Angular separation between target and Moon at each time step (degrees)

earth_angle

Quantity (array)

Angular separation between target and Earth limb at each time step (degrees)

alt_az

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:

  1. SunAngleConstraint → stores sun_angle

  2. MoonAngleConstraint → stores moon_angle

  3. EarthLimbConstraint → stores earth_angle

  4. AltAzConstraint → stores alt_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

compute_joint_visibility Parameters

Parameter

Type

Description

visibilities

list[Visibility]

List of Visibility objects to combine

instrument_ids

list[UUID]

Unique identifiers for each instrument/observatory

Visibility Windows

Visibility calculations return VisibilityWindow objects containing detailed information about each visibility period.

VisibilityWindow Attributes

VisibilityWindow Attributes

Attribute

Description

window.begin

Start of visibility window (ConstrainedDate with datetime and constraint info)

window.end

End of visibility window (ConstrainedDate with datetime and constraint info)

max_visibility_duration

Duration of window in seconds

constraint_reason.start_reason

Description of why visibility started (which constraint was satisfied)

constraint_reason.end_reason

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 joint visibility plot with two observatories and highlighted overlap windows.

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.