Skip to content

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.