3. MICROLENSING PLANET DETECTION
What It Is
When a foreground star with planets passes in front of a background star, the light magnifies. Planets create additional spikes lasting hours. Without continuous coverage, you miss them.
Why It's Irreplaceable
- 24-hour coverage is mandatory: Planetary signals last 1-24 hours
- Single site has 50% duty cycle (daylight)
- Bulge is only visible 4-6 hours/night from any longitude
- 3+ longitudes = near-continuous coverage
Technical Requirements
| Parameter |
Minimum |
Ideal |
Why |
| Photometric precision |
2% |
0.5% |
Low-amplitude anomalies |
| Cadence |
15 min |
5 min |
Catch short anomalies |
| Longitude spread |
120° |
180° |
24-hour coverage |
| Limiting magnitude |
I~18 |
I~20 |
Bulge fields are crowded |
| FOV |
10 arcmin |
20+ arcmin |
Source field crowding |
Equipment That Works
| Tier |
Setup |
Cost |
Capability |
| Minimum |
14" + CCD + I filter |
$10,000 |
I~16 in bulge |
| Good |
20" + back-illuminated CCD |
$30,000 |
I~18 in bulge |
| Optimal |
0.5m+ + research CCD |
$100,000+ |
I~20 in bulge |
What Goes Wrong
| Failure Mode |
Cause |
Mitigation |
| Missing anomaly |
Gap in coverage |
More sites, more longitudes |
| Crowded field confusion |
PSF overlap |
PSF photometry, difference imaging |
| Alert delay |
Slow processing |
Real-time pipeline |
| Wrong source |
Blend with neighbor |
High-resolution verification |
Why Professionals Can't Replace This
- KMTNet: Only 3 sites, significant gaps remain
- OGLE: Single site (Chile)
- The community explicitly calls for more longitude coverage
4. GAMMA-RAY BURST AFTERGLOW LIGHT CURVES
What It Is
GRB optical afterglows fade from naked-eye brightness to undetectable in hours to days. Early light curves constrain jet physics, progenitor models.
Why It's Irreplaceable
- Speed + geographic coverage = early detection
- Afterglow can fade 5 magnitudes in first hour
- Single site may not be in darkness when GRB triggers
- First 15 minutes are most scientifically valuable
Technical Requirements
| Parameter |
Minimum |
Ideal |
Why |
| Response time |
< 5 min |
< 1 min |
Catch early emission |
| Automation |
Full robotic |
Full robotic |
No human delay |
| Limiting magnitude |
V~16 |
V~18 |
Catch fading sources |
| Cadence (early) |
30 sec |
10 sec |
Rapid fading phase |
| Alert reception |
GCN socket |
GCN socket |
Real-time triggers |
Equipment That Works
| Tier |
Setup |
Cost |
Capability |
| Minimum |
8" robotic + CMOS |
$8,000 |
V~14, 2 min response |
| Good |
14" robotic + CCD + filters |
$20,000 |
V~17, 1 min response |
| Optimal |
0.5m fully robotic |
$100,000+ |
V~19, <30 sec response |
What Goes Wrong
| Failure Mode |
Cause |
Mitigation |
| Too slow |
Mount slew time |
Pre-point to GCN region |
| Wrong field |
Large GCN error box |
Multi-site tiling |
| Mistaken transient |
Asteroid, variable |
Cross-reference catalogs |
| Clouds |
Single site clouded |
Network redundancy |
5. GRAVITATIONAL WAVE KILONOVA FOLLOW-UP
What It Is
Neutron star mergers produce gravitational waves AND optical/IR counterparts (kilonovae) that fade in days. Finding them in 100+ sq deg error boxes requires coordinated search.
Why It's Irreplaceable
- Error boxes are HUGE: 10-1000 sq deg
- Need to tile rapidly, simultaneously from multiple sites
- Single large telescope can only cover fraction of error box
- Wide-field + coordination beats aperture
Technical Requirements
| Parameter |
Minimum |
Ideal |
Why |
| FOV |
0.5° |
2°+ |
Cover large error boxes |
| Limiting magnitude |
r~19 |
r~21 |
Kilonovae are faint |
| Cadence |
30 min |
15 min |
Confirm candidates |
| Coordination |
Central scheduler |
Real-time allocation |
No duplicate coverage |
Equipment That Works
| Tier |
Setup |
Cost |
Capability |
| Minimum |
200mm lens + CMOS |
$5,000 |
r~15, 5° FOV |
| Good |
8" Rowe-Ackermann + QHY600 |
$15,000 |
r~18, 2° FOV |
| Optimal |
0.5m + large-format CCD |
$150,000+ |
r~21, 1° FOV |
TIER 2: HIGH-VALUE SCIENCE (Distributed Has Major Advantages)
6. VARIABLE STAR MONITORING (Long-Period)
The Problem Professionals Have
- TESS: 27-day sectors, misses long-period variables
- ZTF/LSST: 3-day cadence, aliases periods
- Need YEARS of continuous monitoring
Requirements
| Parameter |
Minimum |
Notes |
| Photometric precision |
2% |
Standard Johnson/Cousins |
| Cadence |
Daily |
For periods >10 days |
| Filters |
V minimum |
BVRI for color evolution |
| Duration |
1+ years |
Period coverage |
7. COMET ACTIVITY MONITORING
Why Distributed Wins
- Outbursts are unpredictable, last hours to days
- Coverage across longitudes catches transient activity
- Professionals don't have time for continuous comet monitoring
Requirements
| Parameter |
Minimum |
Notes |
| Photometric precision |
10% |
Coma photometry is fuzzy |
| FOV |
30+ arcmin |
Tail structure |
| Cadence |
4-8 hours |
Catch outbursts |
8. NEO ASTROMETRY AND PHOTOMETRY
Why Distributed Wins
- Newly discovered NEOs need immediate follow-up
- Parallax from multiple sites improves orbits
- Color photometry constrains composition
Requirements
| Parameter |
Minimum |
Notes |
| Astrometric precision |
0.5" |
Sub-arcsec for orbit improvement |
| Limiting magnitude |
V~18 |
Faint NEOs |
| Response time |
< 2 hours |
Objects can be lost |
9. ECLIPSING BINARY TIMING
Why Distributed Wins
- Eclipse timing over decades reveals mass transfer, period changes
- AAVSO does this but more coverage = better precision
- Some systems need longitude coverage for complete eclipses
Requirements
| Parameter |
Minimum |
Notes |
| Timing precision |
30 sec |
For slow period changes |
| Photometric precision |
1% |
Eclipse shape |
| Baseline |
Years |
Long-term trends |
10. BLAZAR MONITORING
Why Distributed Wins
- Blazars vary on all timescales: minutes to years
- Correlates with gamma-ray monitoring
- 24-hour coverage catches intraday variability
Requirements
| Parameter |
Minimum |
Notes |
| Photometric precision |
3% |
Blazars are variable |
| Cadence |
1-4 hours |
Intraday variability |
| Longitude coverage |
120°+ |
Catch rapid flares |
TIER 3: NICHE SCIENCE (Unique Opportunities)
11. Lunar Impact Flash Detection
- Multiple simultaneous observers confirm real impacts
- Triangulation gives impact location
- Requires high-speed video of dark lunar limb
12. Satellite Photometry (Space Situational Awareness)
- Multi-angle photometry constrains tumble rates
- Commercial and military interest
- Potential funding source
13. Interstellar Object Characterization
- When next 'Oumuamua found, coverage is time-critical
- Rotation period, color, variability before it fades
- Days to weeks of opportunity
14. Flare Star Monitoring
- M-dwarf superflares are stochastic
- Continuous monitoring catches rare events
- Relevant to exoplanet habitability
15. Exocomet Transits
- Some stars show transient dimming from dusty comets
- Needs extended monitoring of candidate stars
- Serendipitous discovery potential
SMART SCHEDULER LOGIC
Core Concept
The scheduler assigns targets to sites based on:
1. Site capability (aperture, FOV, pixel scale, limiting mag)
2. Site conditions (sky quality, altitude, current weather)
3. Target requirements (timing precision, photometric depth, cadence)
4. Target observability (altitude, moon distance, time constraints)
5. Network coordination (avoid duplication, maximize coverage)
Capability Scoring Model
1. Calculate Site Capability Score
def calculate_site_capability(site, target):
"""
Score how well a site can observe a target (0-100)
"""
score = 100 # Start with perfect score, subtract for limitations
# === APERTURE CHECK ===
# Estimate limiting magnitude:
# Rule of thumb: limiting mag ≈ 2 + 5*log10(aperture_mm)
limiting_mag = 2 + 5 * math.log10(site.aperture_mm)
# Adjust for sky quality (Bortle scale)
# Bortle 1-3: no penalty
# Bortle 4-5: -0.5 mag
# Bortle 6-7: -1.0 mag
# Bortle 8-9: -2.0 mag
if site.bortle >= 8:
limiting_mag -= 2.0
elif site.bortle >= 6:
limiting_mag -= 1.0
elif site.bortle >= 4:
limiting_mag -= 0.5
# Adjust for exposure time (assumes 60s; longer = deeper)
# Every 4x exposure time = +1 mag (sqrt law)
exposure_factor = math.log(site.typical_exposure / 60) / math.log(4)
limiting_mag += exposure_factor
# Check if site can reach target magnitude
if target.magnitude > limiting_mag:
return 0 # Can't observe this target
# Penalty for being close to limit (noisy)
mag_margin = limiting_mag - target.magnitude
if mag_margin < 1:
score -= 30 # Marginal detection
elif mag_margin < 2:
score -= 15 # Okay but not ideal
# === PIXEL SCALE CHECK ===
# Calculate plate scale: 206265 * pixel_size_um / focal_length_mm arcsec/pixel
plate_scale = 206.265 * site.pixel_size_um / site.focal_length_mm
# For stellar photometry, want 2-4 pixels across FWHM
# Typical seeing is 2-4 arcsec, so want 1-2 arcsec/pixel
if plate_scale < 0.5:
score -= 20 # Oversampled, wastes pixels
elif plate_scale > 3.0:
score -= 30 # Undersampled, poor PSF
elif plate_scale > 2.0:
score -= 10 # Slightly undersampled
# === FOV CHECK ===
# Calculate FOV in arcmin
fov_arcmin = (site.sensor_width_mm / site.focal_length_mm) * 3438 # degrees to arcmin
if target.required_fov and fov_arcmin < target.required_fov:
score -= 40 # Can't fit required field
# === TIMING CAPABILITY ===
if target.requires_gps_timing and not site.has_gps:
score -= 50 # Critical for occultations
if target.requires_high_cadence:
if site.min_exposure > target.max_exposure:
score -= 40 # Can't achieve required cadence
# === FILTER CHECK ===
if target.required_filter and target.required_filter not in site.available_filters:
score -= 30 # Wrong filter, but unfiltered might work
return max(0, score)
2. Calculate Observability Score
def calculate_observability(site, target, obs_time):
"""
Score observability from this site at this time (0-100)
"""
from astropy.coordinates import SkyCoord, EarthLocation, AltAz, get_sun, get_body
from astropy.time import Time
import astropy.units as u
location = EarthLocation(
lat=site.latitude * u.deg,
lon=site.longitude * u.deg,
height=site.altitude * u.m
)
time = Time(obs_time)
# Target position
coord = SkyCoord(ra=target.ra * u.deg, dec=target.dec * u.deg)
altaz = coord.transform_to(AltAz(obstime=time, location=location))
altitude = altaz.alt.deg
# === ALTITUDE CHECK ===
if altitude < 15:
return 0 # Below horizon or too low
score = 100
# Optimal altitude: 45-75 degrees
if altitude < 30:
score -= 30 # High airmass, poor seeing
elif altitude < 45:
score -= 15
elif altitude > 80:
score -= 10 # Near zenith, some mount issues
# === AIRMASS CALCULATION ===
# Airmass ≈ sec(zenith_angle) = 1/cos(90-altitude)
airmass = 1 / math.cos(math.radians(90 - altitude))
# Penalize high airmass
if airmass > 2.0:
score -= 20
elif airmass > 1.5:
score -= 10
# === SUN CHECK ===
sun = get_sun(time)
sun_altaz = sun.transform_to(AltAz(obstime=time, location=location))
sun_alt = sun_altaz.alt.deg
if sun_alt > -6:
return 0 # Civil twilight, too bright
elif sun_alt > -12:
score -= 40 # Nautical twilight
elif sun_alt > -18:
score -= 20 # Astronomical twilight
# === MOON CHECK ===
moon = get_body('moon', time)
moon_altaz = moon.transform_to(AltAz(obstime=time, location=location))
moon_alt = moon_altaz.alt.deg
# Calculate moon-target separation
moon_sep = coord.separation(moon).deg
# Moon illumination (approximate)
# Full moon = problem, new moon = no problem
# For simplicity, use a lookup or calculate from phase
if moon_alt > 0 and moon_sep < 30:
score -= 30 # Moon nearby and up
elif moon_alt > 0 and moon_sep < 60:
score -= 15
return max(0, score)
3. Target Priority Scoring
def calculate_priority(target, network_state):
"""
Calculate dynamic priority for a target (0-100)
"""
score = target.base_priority # Set by science case
# === TIME-CRITICAL EVENTS ===
if target.target_type == 'occultation':
time_to_event = (target.event_time - datetime.now(timezone.utc)).total_seconds()
if time_to_event < 3600: # Within 1 hour
score += 50 # CRITICAL
elif time_to_event < 86400: # Within 1 day
score += 30
if target.target_type == 'transit':
time_to_ingress = (target.ingress_time - datetime.now(timezone.utc)).total_seconds()
if 0 < time_to_ingress < 7200: # Within 2 hours
score += 40
if target.target_type == 'grb_afterglow':
age = (datetime.now(timezone.utc) - target.trigger_time).total_seconds()
if age < 600: # Within 10 minutes
score += 60 # VERY CRITICAL
elif age < 3600: # Within 1 hour
score += 40
elif age < 86400:
score += 20
# === COVERAGE DEFICIT ===
# Check how many observations we have vs need
obs_count = get_observation_count(target.id)
if obs_count < target.min_observations:
deficit = target.min_observations - obs_count
score += min(deficit * 5, 25) # Up to +25 for under-observed
# === RECENCY ===
# Penalize if recently observed (avoid redundant observations)
last_obs = get_last_observation_time(target.id)
if last_obs:
hours_since = (datetime.now(timezone.utc) - last_obs).total_seconds() / 3600
if hours_since < target.min_cadence_hours:
score -= 20 # Recently observed, lower priority
# === NETWORK COORDINATION ===
# Check if other sites are already observing this target
active_observers = get_active_observers(target.id)
if active_observers > 0:
if target.requires_simultaneous:
score += 20 # WANT multiple simultaneous (occultation)
else:
score -= 15 # Avoid duplication
return max(0, min(100, score))
4. Final Target Assignment
def get_targets_for_site(site):
"""
Return ranked list of targets for a specific site to observe
"""
now = datetime.now(timezone.utc)
# Get all active targets
targets = db.query(Target).filter(
Target.active == True,
Target.expires_at > now
).all()
scored_targets = []
for target in targets:
# Calculate component scores
capability = calculate_site_capability(site, target)
if capability == 0:
continue # Site can't observe this target
observability = calculate_observability(site, target, now)
if observability == 0:
continue # Not observable right now
priority = calculate_priority(target, get_network_state())
# Combined score with weights
# Priority is most important, then observability, then capability
combined_score = (
priority * 0.50 +
observability * 0.30 +
capability * 0.20
)
scored_targets.append({
'target_id': target.id,
'name': target.name,
'ra': target.ra,
'dec': target.dec,
'type': target.target_type,
'score': round(combined_score, 1),
'breakdown': {
'priority': priority,
'observability': observability,
'capability': capability
},
'exposure_recommendation': calculate_exposure(site, target),
'filter_recommendation': recommend_filter(site, target)
})
# Sort by combined score
scored_targets.sort(key=lambda x: x['score'], reverse=True)
return scored_targets[:20] # Return top 20
5. Exposure Time Calculator
def calculate_exposure(site, target):
"""
Recommend optimal exposure time for this target at this site
"""
# Estimate SNR for 60s exposure
# SNR ∝ sqrt(t) for sky-limited
limiting_mag = estimate_limiting_mag(site, 60) # For 60s exposure
mag_diff = target.magnitude - limiting_mag
# We want SNR of at least 50 for 1% photometry
# SNR scales as 10^(0.4 * mag_diff) for same exposure
if mag_diff > 0:
# Target is fainter than 60s limit
# Need to expose longer
snr_ratio = 10 ** (0.4 * mag_diff)
exposure = 60 * (snr_ratio ** 2) # SNR scales as sqrt(t)
else:
# Target is brighter than limit
# Can expose less
exposure = 60 / (10 ** (0.4 * abs(mag_diff)))
# Apply constraints
exposure = max(exposure, site.min_exposure)
exposure = min(exposure, 300) # Cap at 5 minutes for tracking
# For high-cadence targets, cap exposure
if target.requires_high_cadence and target.max_exposure:
exposure = min(exposure, target.max_exposure)
return round(exposure, 1)
DATABASE SCHEMA FOR SMART SCHEDULING
from sqlalchemy import Column, Integer, Float, String, Boolean, DateTime, Enum
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Site(Base):
__tablename__ = 'sites'
id = Column(Integer, primary_key=True)
name = Column(String(100))
# Location
latitude = Column(Float) # degrees
longitude = Column(Float) # degrees
altitude = Column(Float) # meters
timezone = Column(String(50))
# Equipment
aperture_mm = Column(Integer) # Primary aperture in mm
focal_length_mm = Column(Integer) # Focal length in mm
pixel_size_um = Column(Float) # Pixel size in microns
sensor_width_mm = Column(Float) # Sensor width in mm
sensor_height_mm = Column(Float) # Sensor height in mm
# Capabilities
has_gps = Column(Boolean, default=False)
available_filters = Column(String(100)) # Comma-separated: "V,R,I,Clear"
min_exposure = Column(Float, default=0.1) # seconds
max_exposure = Column(Float, default=300) # seconds
is_robotic = Column(Boolean, default=False)
# Conditions
bortle_class = Column(Integer) # 1-9
typical_seeing = Column(Float) # arcsec
clear_nights_per_year = Column(Integer)
# Status
is_active = Column(Boolean, default=True)
last_seen = Column(DateTime)
api_key = Column(String(64))
class Target(Base):
__tablename__ = 'targets'
id = Column(Integer, primary_key=True)
name = Column(String(100))
# Coordinates
ra = Column(Float) # degrees, J2000
dec = Column(Float) # degrees, J2000
# Classification
target_type = Column(String(50)) # occultation, transit, grb, variable, etc.
# Magnitude
magnitude = Column(Float) # Expected V magnitude
# Requirements
base_priority = Column(Integer, default=50) # 1-100
min_observations = Column(Integer, default=1)
min_cadence_hours = Column(Float, nullable=True) # Minimum time between obs
required_filter = Column(String(10), nullable=True)
required_fov = Column(Float, nullable=True) # arcmin
requires_gps_timing = Column(Boolean, default=False)
requires_high_cadence = Column(Boolean, default=False)
max_exposure = Column(Float, nullable=True) # For high-cadence targets
requires_simultaneous = Column(Boolean, default=False) # Multiple sites same time
# Time constraints
event_time = Column(DateTime, nullable=True) # For occultations
ingress_time = Column(DateTime, nullable=True) # For transits
egress_time = Column(DateTime, nullable=True)
trigger_time = Column(DateTime, nullable=True) # For GRBs
expires_at = Column(DateTime, nullable=True)
# Status
active = Column(Boolean, default=True)
created_at = Column(DateTime)
notes = Column(String(500))
class Observation(Base):
__tablename__ = 'observations'
id = Column(Integer, primary_key=True)
site_id = Column(Integer, ForeignKey('sites.id'), index=True)
target_id = Column(Integer, ForeignKey('targets.id'), index=True)
# Timing
start_time = Column(DateTime, index=True)
end_time = Column(DateTime)
mid_time = Column(DateTime)
# Results
filter_used = Column(String(10))
exposure_time = Column(Float) # seconds
num_frames = Column(Integer)
magnitude = Column(Float, nullable=True)
magnitude_error = Column(Float, nullable=True)
# Quality
airmass = Column(Float)
seeing = Column(Float, nullable=True) # arcsec
sky_background = Column(Float, nullable=True)
quality_flag = Column(Integer, default=0) # 0=good, 1=warning, 2=bad
# Metadata
submitted_at = Column(DateTime)
notes = Column(String(500))
API ENDPOINTS FOR SMART SCHEDULING
from fastapi import FastAPI, Depends, HTTPException
from datetime import datetime, timezone
app = FastAPI()
@app.get("/api/v1/schedule")
async def get_schedule(site: Site = Depends(verify_site)):
"""
Get prioritized target list for this site.
Called by client every 60 seconds.
"""
targets = get_targets_for_site(site)
return {
"timestamp": datetime.now(timezone.utc).isoformat(),
"site_id": site.id,
"targets": targets,
"next_poll_seconds": 60
}
@app.get("/api/v1/target/{target_id}/observation_plan")
async def get_observation_plan(target_id: int, site: Site = Depends(verify_site)):
"""
Get detailed observation plan for a specific target.
Includes exposure settings, timing, comparison stars.
"""
target = db.query(Target).get(target_id)
if not target:
raise HTTPException(404, "Target not found")
plan = {
"target": {
"id": target.id,
"name": target.name,
"ra": target.ra,
"dec": target.dec
},
"observation_settings": {
"recommended_exposure": calculate_exposure(site, target),
"recommended_filter": recommend_filter(site, target),
"cadence_seconds": target.max_exposure or 60,
"total_duration_minutes": estimate_duration(target)
},
"timing": {
"optimal_start": calculate_optimal_start(site, target),
"optimal_end": calculate_optimal_end(site, target),
"event_time": target.event_time.isoformat() if target.event_time else None
},
"comparison_stars": get_comparison_stars(target),
"finder_chart_url": generate_finder_chart_url(target)
}
return plan
@app.post("/api/v1/observations")
async def submit_observation(obs: ObservationSubmit, site: Site = Depends(verify_site)):
"""
Submit completed observation data.
"""
# Validate
if obs.start_time > obs.end_time:
raise HTTPException(400, "Invalid time range")
# Create observation record
new_obs = Observation(
site_id=site.id,
target_id=obs.target_id,
start_time=obs.start_time,
end_time=obs.end_time,
mid_time=obs.mid_time,
filter_used=obs.filter,
exposure_time=obs.exposure_time,
num_frames=obs.num_frames,
magnitude=obs.magnitude,
magnitude_error=obs.mag_error,
airmass=obs.airmass,
seeing=obs.seeing,
quality_flag=validate_quality(obs),
submitted_at=datetime.now(timezone.utc)
)
db.add(new_obs)
db.commit()
# Trigger any necessary actions (alerts, etc.)
check_observation_triggers(new_obs)
return {"status": "accepted", "observation_id": new_obs.id}
@app.get("/api/v1/network/status")
async def network_status():
"""
Public endpoint showing current network state.
"""
return {
"active_sites": get_active_site_count(),
"sites_currently_observing": get_observing_sites(),
"observations_last_24h": get_observation_count_24h(),
"active_targets": get_active_target_count(),
"priority_targets": get_top_priority_targets(5)
}
FAILURE MODES BY SCIENCE CASE
| Science Case |
Primary Failure Mode |
Detection |
Mitigation |
| Occultation |
Timing drift |
Post-hoc comparison |
Require GPS |
| Occultation |
Wrong star |
Plate solve fails |
Automated verification |
| Transit |
Ephemeris error |
Transit not detected |
Use updated ephemerides |
| Transit |
Systematic noise |
Scatter > 1% |
Multiple comparison stars |
| Microlensing |
Coverage gap |
Missing anomaly |
More longitudes |
| GRB |
Slow response |
Missed early emission |
Full automation |
| Kilonova |
Duplicate coverage |
Wasted time |
Central coordination |
| Variable |
Period aliasing |
Wrong period |
Cadence optimization |
This scheduler prioritizes science return while accounting for real-world equipment and site limitations. Start simple, add complexity as needed.