Proposer’s Cookbook: Planning for SALT

This page provides practical examples for using saltshaker to prepare and optimize observing strategies for the Southern African Large Telescope (SALT).

Important

Essential Usage Information: saltshaker is an independent pre-planning tool and is not an official SALT product. While it provides high-fidelity models, all final observing proposals must be validated and submitted using the official SALT Phase I Proposal Tool (PIPT).

Use these examples to screen targets, optimize your strategy, and generate preliminary plots for your Technical Justification, but always perform a final check in the PIPT before submission.

Preliminary Feasibility: Estimating Track Lengths

The most fundamental constraint at SALT is the tracker’s physical range. For any exposure, you should verify that the telescope can follow the target for the required duration.

The following plot can help you estimate the available “window of opportunity” for your intended exposures during the proposal phase.

import numpy as np
import matplotlib.pyplot as plt
from astropy.coordinates import SkyCoord
from astropy.time import Time
import astropy.units as u
from saltshaker import get_track_length

# Define your target and the night of interest
target = SkyCoord.from_name('Sirius')
obs_date = '2026-01-15'
start_time = Time(f"{obs_date} 12:00:00")

# Sample the tracking zone over 24 hours
times = start_time + np.linspace(0, 24, 1000) * u.hour
track_lengths = [get_track_length(target, t).to(u.second).value for t in times]

plt.figure(figsize=(10, 5))
plt.fill_between(times.plot_date, track_lengths, color='red', alpha=0.1)
plt.plot(times.plot_date, track_lengths, color='red', lw=2)

# Reference line for an intended 30-minute exposure
plt.axhline(1800, color='black', linestyle='--', label='Intended 30 min Exposure')

plt.title(f"Estimated Tracking Time for {target.name}")
plt.ylabel("Available Track Length (seconds)")
plt.xlabel("Time (UTC)")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

(Source code, png, pdf)

_images/examples-1.png

Nightly Planning: Visualizing Preliminary Tracks

Because SALT is a fixed-altitude telescope, targets often pass through the visibility zone twice (the East and West tracks), separated by a “Zenith Hole.”

This visualization helps you understand when your target is observable relative to astronomical twilight (-18°).

from saltshaker import get_visibility_windows, get_salt_observer
from astropy.coordinates import SkyCoord
from astropy.time import Time
import astropy.units as u
import matplotlib.pyplot as plt
from matplotlib.dates import DateFormatter

observer = get_salt_observer()
target = SkyCoord.from_name('Sirius')
date = '2026-01-15'

# 1. Calculate the estimated tracks and twilight
windows = get_visibility_windows(target, date)
start_time = Time(f"{date} 12:00:00")
eve_twi = observer.twilight_evening_astronomical(start_time, which='next')
morn_twi = observer.twilight_morning_astronomical(eve_twi, which='next')

# 2. Create the visualization
fig, ax = plt.subplots(figsize=(12, 4))

# Shade the dark time
ax.axvspan(eve_twi.plot_date, morn_twi.plot_date, color='black', alpha=0.15, label='Astronomical Dark')

# Shade the estimated visibility windows
for i, w in enumerate(windows):
    ax.axvspan(w.start_time.plot_date, w.end_time.plot_date, color='green', alpha=0.6,
               label='Est. SALT Visibility' if i==0 else "")
    # Label the tracks
    mid_time = w.start_time.plot_date + (w.end_time.plot_date - w.start_time.plot_date)/2
    ax.text(mid_time, 0.5, f"Track {i+1}", ha='center', va='center', fontweight='bold')

plt.title(f"Preliminary Nightly Windows: {target.name} on {date}")
ax.xaxis.set_major_formatter(DateFormatter('%H:%M'))
ax.set_yticks([])
plt.xlabel("Time (UTC)")
plt.legend(loc='upper right')
plt.grid(True, axis='x', alpha=0.3)
plt.show()

(Source code, png, pdf)

_images/examples-2.png

Preliminary Scheduling: Moon and Track Length

Using saltshaker with astroplan allows you to estimate when your target meets both SALT’s tracking requirements and your project’s preliminary lunar constraints. This visualization includes environmental context (night/day) to help you understand the specific observing windows.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import astropy.units as u
from astropy.coordinates import SkyCoord
from astropy.time import Time
from astroplan import FixedTarget, is_event_observable
from saltshaker import (
    get_salt_observer,
    SaltTrackLengthConstraint,
    SaltMoonConstraint
)

# 1. Setup the Observer and Target
observer = get_salt_observer()
target = FixedTarget(coord=SkyCoord.from_name('Sirius'), name='Sirius')

# 2. Define Constraints (e.g., Gray moon and minimum 20 min track)
constraints = [
    SaltTrackLengthConstraint(min_track_length=20 * u.minute),
    SaltMoonConstraint(max_illumination=0.5)
]

# 3. Evaluate over a 48-hour period
start_time = Time('2026-01-15 12:00:00')
times = start_time + np.linspace(0, 48, 300) * u.hour

# 4. Calculate observability and night/day states
observable = is_event_observable(constraints, observer, target, times=times)[0]
is_night = observer.is_night(times)

# 5. Visualize the Timeline
fig, ax = plt.subplots(figsize=(10, 2.5))

# Shade the Night Time (in light grey)
ax.fill_between(times.plot_date, 0, 1, where=is_night,
                color='lightgrey', alpha=0.5, label='Night Time')

# Shade the Observability Window (in blue)
ax.fill_between(times.plot_date, 0, 1, where=observable,
                color='blue', alpha=0.7, label='Meets Constraints')

# Formatting the plot
ax.set_yticks([])
ax.set_ylim(0, 1)
ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M\n%b %d'))

plt.title(f"Estimated Observability: {target.name}", fontsize=12, fontweight='bold')
plt.xlabel("Time (UTC)")
plt.legend(loc='upper right', bbox_to_anchor=(1, 1.3), ncol=2, frameon=False)
plt.tight_layout()
plt.show()

(Source code, png, pdf)

_images/examples-3.png

Long-term Planning: Annual Visibility Cycles

The “Annual Plot” provides a rough guide for when your target is best placed during the semester. This visualization uses a daily sampling rate to create a smooth “carpet” effect, showing how visibility windows shift across the night as the Earth orbits the Sun.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.lines import Line2D
from astropy.coordinates import SkyCoord
from astropy.time import Time
import astropy.units as u
from saltshaker import get_salt_observer, get_visibility_windows

# --- Configuration & Setup ---
target_name = 'Sirius'
year = 2026

observer = get_salt_observer()
target = SkyCoord.from_name(target_name)

# Sample every 1 day for a smooth, continuous "carpet" effect
dates = Time(f"{year}-01-01") + np.arange(0, 365, 1) * u.day

# --- Color Palette (Clean Documentation) ---
BG_COLOR = '#FFFFFF'        # Pure white background
NIGHT_COLOR = '#E5E7EB'     # Soft light-grey for dark time
TRACK_COLOR = '#1D4ED8'     # Bold, high-contrast blue for visibility tracks
TEXT_COLOR = '#111827'      # Near-black for crisp, legible text
GRID_COLOR = '#D1D5DB'      # Subtle grey for gridlines

# --- Plotting ---
plt.figure(figsize=(10, 6))

for date in dates:
    windows = get_visibility_windows(target, date)
    try:
        # Calculate twilight limits
        eve = observer.twilight_evening_astronomical(date, which='next')
        morn = observer.twilight_morning_astronomical(eve, which='next')

        # Base time: 10:00 UTC (12:00 SAST) to keep the night continuous
        base = Time(f"{date.iso.split()[0]} 10:00:00")
        to_h = lambda t: (t - base).to(u.hour).value

        # Plot Dark Time as vertical slices
        plt.plot([date.datetime, date.datetime], [to_h(eve), to_h(morn)],
                 color=NIGHT_COLOR, alpha=1.0, lw=2)

        # Plot SALT Visibility Tracks
        for w in windows:
            plt.plot([date.datetime, date.datetime], [to_h(w.start_time), to_h(w.end_time)],
                     color=TRACK_COLOR, lw=2, alpha=0.9)
    except Exception:
        continue

# --- Formatting & Styling ---
ax = plt.gca()
ax.set_facecolor(BG_COLOR)
plt.gcf().patch.set_facecolor(BG_COLOR)

# Titles and Labels
plt.title(f"Preliminary Annual Visibility Cycle: {target_name} (SALT)",
          color=TEXT_COLOR, fontsize=13, pad=12, fontweight='bold')
plt.xlabel("Date", color=TEXT_COLOR, fontsize=11, fontweight='500')
plt.ylabel("Hours from Noon SAST", color=TEXT_COLOR, fontsize=11, fontweight='500')

# Axis Ticks and Spines styling
ax.xaxis.set_major_locator(mdates.MonthLocator())
ax.xaxis.set_major_formatter(mdates.DateFormatter('%b'))
ax.tick_params(colors=TEXT_COLOR, which='both', labelsize=10)

for spine in ax.spines.values():
    spine.set_color(GRID_COLOR)
    spine.set_linewidth(1)

# Grid and limits
plt.grid(True, axis='y', alpha=0.6, color=GRID_COLOR, linestyle=':')
ax.set_ylim(4, 20)
ax.invert_yaxis()

# Custom Legend
custom_lines = [
    Line2D([0], [0], color=NIGHT_COLOR, lw=4),
    Line2D([0], [0], color=TRACK_COLOR, lw=4)
]
legend = plt.legend(custom_lines, ['Astronomical Dark Time', 'Est. SALT Visibility'],
                    loc='upper right', framealpha=1.0,
                    facecolor=BG_COLOR, edgecolor=GRID_COLOR, fontsize=10)
for text in legend.get_texts():
    text.set_color(TEXT_COLOR)

plt.tight_layout()
plt.show()

(Source code, png, pdf)

_images/examples-4.png

Semester Statistics: Guiding Time Requests

Calculating preliminary statistics on observable hours helps you determine if your project is realistic for a given semester.

import astropy.units as u
from astropy.coordinates import SkyCoord
from saltshaker import get_visibility_windows, get_semester_nights

target = SkyCoord.from_name('NGC 300')
year, semester = 2026, 1
nights = get_semester_nights(year, semester)

total_sec = 0
observable_nights = 0

for eve, morn in nights:
    windows = get_visibility_windows(target, eve)
    night_sec = 0
    for w in windows:
        # Estimate overlap of the SALT track with dark time
        start = max(w.start_time, eve)
        end = min(w.end_time, morn)
        if start < end:
            night_sec += (end - start).to(u.second).value

    if night_sec > 0:
        total_sec += night_sec
        observable_nights += 1

print(f"Preliminary Statistics for {target.name} (Semester {year}-{semester}):")
print(f"  - Estimated Total Observable Hours: {total_sec / 3600:.1f} hours")
print(f"  - Number of Observable Nights: {observable_nights}")
print(f"  - Estimated Average Track per Night: {(total_sec/observable_nights)/60:.1f} minutes")

Example Output:

Preliminary Statistics for NGC 300 (Semester 2026-1):
  - Estimated Total Observable Hours: 161.3 hours
  - Number of Observable Nights: 132
  - Estimated Average Track per Night: 73.3 minutes

Catalog Screening: Preliminary Catalog Feasibility

If your project involves a large catalog of targets, you can use these functions to quickly screen for objects that fall within SALT’sreachable range.

import pandas as pd
from astropy.coordinates import SkyCoord
from saltshaker import is_target_observable

# Preliminary target catalog
catalog = [
    ('M31', '00h42m44s', '+41d16m09s'),
    ('M42', '05h35m17s', '-05d23m28s'),
    ('Omega Cen', '13h26m47s', '-47d28m46s'),
    ('Centaurus A', '13h25m27s', '-43d01m08s'),
]

results = []
for name, ra, dec in catalog:
    coord = SkyCoord(ra, dec, frame='icrs')
    # is_target_observable provides a quick preliminary check
    observable = is_target_observable(coord)
    results.append({'Target': name, 'Dec': dec, 'Est. SALT Observable': observable})

df = pd.DataFrame(results)
print(df.to_string(index=False))

Example Output:

     Target         Dec  Est. SALT Observable
        M31  +41d16m09s                 False
        M42  -05d23m28s                  True
  Omega Cen  -47d28m46s                  True
Centaurus A  -43d01m08s                  True