Santa Monica College

Inspiring the
Next Generation
of Engineers

Welcome to the AIAA SMC Student Initiative — where students unite to learn, build, and innovate in aviation and space exploration.

Scroll

About Us

The AIAA SMC Student Initiative is a student-led organization passionate about all things aerospace. We collaborate on hands-on projects, connect with industry professionals, and inspire the next generation of engineers and explorers.

Aerospace SMC Event
Club Activity
Aerospace SMC Activity
Club Activity

Light Curve Analysis

Light Curve Analysis Graph
Astrophysics · TESS Data

Stellar Flare Detection & Variable Stars

Our members are working with real TESS satellite data to detect and characterize stellar flares and study brightness variations of celestial objects over time. The pipeline builds aperture-masked light curves, computes flux-weighted centroids, and measures centroid shifts to distinguish on-target flares from nearby contaminants.

An upcoming large-scale extension will survey hundreds of stars in the NGC 2516 open cluster using Gaia DR3 membership data and automated anomaly detection.

View the Code

AIAA Space Team Design

We are competing in the AIAA Undergraduate Team Space Design Competition — a national challenge where student teams develop a full mission concept and proposal. Read the brief below, and track our upcoming deliverables.

Official Competition Brief

2025–2026 Undergraduate Team Space Design

Read the full competition requirements, challenge statement, and submission guidelines from AIAA.

View PDF

Upcoming Deadlines

Quad Chart
Due Mon, March 16
11:59 PM PT
Submitted
Org Chart
Due Sun, March 22
11:59 PM PT
Due Soon
Shell Doc
Due Sun, March 22
11:59 PM PT
Due Soon
First Draft of Proposal
Due Sun, April 19
11:59 PM PT
Upcoming
Final Proposal
Due Fri, May 15
4:59 PM PT
Upcoming

Code & Pipelines

Explore the actual Python code powering our astrophysics research — from single-star flare detection to large-scale open cluster surveys.

Stellar Flare Detection — pipeline.py

Detects and characterizes stellar flares from TESS Target Pixel File (TPF) data. Builds aperture-masked light curves, computes flux-weighted centroids at every cadence, and measures centroid shifts between the pre-flare baseline and the flare peak — revealing whether the emission is on the target star or a nearby contaminant.

lightkurve astropy numpy TESS TPF WCS
pipeline.py
"""
flare_ch.py
-----------
Detects and characterizes stellar flares from TESS Target Pixel File (TPF) data.

Pipeline overview:
  1. Load a TESS TPF cutout and generate an aperture-masked light curve.
  2. Identify the brightest cadence (strongest flare candidate).
  3. Compute flux-weighted centroids over time using only aperture pixels.
  4. Measure the centroid shift between the pre-flare baseline and the flare peak.
  5. Project the target star's sky coordinates onto the pixel grid via WCS.

A significant centroid shift during the flare suggests the emission originates
from a nearby contaminating source rather than the target star itself.
"""

from lightkurve import TessTargetPixelFile
import matplotlib.pyplot as plt
import numpy as np
from astropy.coordinates import SkyCoord
import astropy.units as u

# ---------------------------------------------------------------------------
# 1. Load data
# ---------------------------------------------------------------------------

tpf_path = r"/Users/noel/Downloads/AIAA/Star Project/tess-s0080-2-2_270.420554_28.932725_30x30_astrocut.fits"
tpf = TessTargetPixelFile(tpf_path)

# ---------------------------------------------------------------------------
# 2. Build aperture mask and light curve
#
# The same mask is reused for the light curve, flare detection, and centroid
# computation to ensure all three are internally consistent.
# Pixels with flux > 3x the background median are included.
# ---------------------------------------------------------------------------

aperture_mask = tpf.create_threshold_mask(threshold=3)
lc = tpf.to_lightcurve(aperture_mask=aperture_mask)

plt.figure(figsize=(10, 4))
lc.plot()
plt.show()

# ---------------------------------------------------------------------------
# 3. Extract raw flux and time arrays
# ---------------------------------------------------------------------------

flux = tpf.flux.value   # shape: (n_cadences, ny, nx)
time = tpf.time.value   # shape: (n_cadences,) — TBJD

print("Flux shape:", flux.shape)
print("Time len:  ", len(time))

# ---------------------------------------------------------------------------
# 4. Apply aperture mask and detect the strongest flare
#
# Non-aperture pixels are set to NaN so they contribute nothing to sums.
# The cadence with the highest total in-aperture flux is taken as the flare peak.
# ---------------------------------------------------------------------------

masked_flux = flux.copy()
masked_flux[:, ~aperture_mask] = np.nan                        # blank out background pixels

aperture_flux = np.nansum(masked_flux, axis=(1, 2))            # total in-aperture flux per cadence
flare_idx     = np.nanargmax(aperture_flux)                    # index of peak brightness

print("Index of strongest flare:", flare_idx)
print("Time of strongest flare: ", time[flare_idx])

# ---------------------------------------------------------------------------
# 5. Compute flux-weighted centroids at every cadence
#
# Centroid formula:  x_c = sum(x * F) / sum(F)
# Computed only over aperture pixels; NaNs are excluded by nansum.
# ---------------------------------------------------------------------------

ny, nx = flux.shape[1], flux.shape[2]
y, x   = np.mgrid[0:ny, 0:nx]           # pixel coordinate grids

num_x = np.nansum(x * masked_flux, axis=(1, 2))   # sum(x * F)
num_y = np.nansum(y * masked_flux, axis=(1, 2))   # sum(y * F)
den   = np.nansum(masked_flux,     axis=(1, 2))   # sum(F)

centroid_x = num_x / den   # flux-weighted x centroid at each cadence
centroid_y = num_y / den   # flux-weighted y centroid at each cadence

# ---------------------------------------------------------------------------
# 6. Select pre-flare baseline index
#
# Prefer 10 cadences before the peak. If the flare occurs within the first
# 10 cadences, fall back to 10 cadences after the peak.
# NaN frames are skipped in either direction.
# ---------------------------------------------------------------------------

if flare_idx >= 10:
    pre_idx = flare_idx - 10
    while pre_idx > 0 and np.all(np.isnan(masked_flux[pre_idx])):
        pre_idx -= 1
else:
    pre_idx = flare_idx + 10
    while pre_idx < len(time) - 1 and np.all(np.isnan(masked_flux[pre_idx])):
        pre_idx += 1
    print("Warning: flare near start of data; using post-flare frame as baseline")

peak_idx = flare_idx
while peak_idx > 0 and np.all(np.isnan(masked_flux[peak_idx])):
    peak_idx -= 1

print("Using pre-flare index:", pre_idx,  "time:", time[pre_idx])
print("Using peak index:     ", peak_idx, "time:", time[peak_idx])

# ---------------------------------------------------------------------------
# 7. Measure centroid shift between baseline and flare peak
# ---------------------------------------------------------------------------

x_pre  = centroid_x[pre_idx]
y_pre  = centroid_y[pre_idx]
x_peak = centroid_x[peak_idx]
y_peak = centroid_y[peak_idx]

print("Centroid BEFORE flare:  x =", x_pre,  " y =", y_pre)
print("Centroid DURING flare:  x =", x_peak, " y =", y_peak)

dx = x_peak - x_pre
dy = y_peak - y_pre
print("Centroid shift (dx, dy) =", dx, dy)

# ---------------------------------------------------------------------------
# 8. Project target star coordinates onto the pixel grid via WCS
# ---------------------------------------------------------------------------

wcs           = tpf.wcs
coord         = SkyCoord(ra=270.420554*u.deg, dec=28.932725*u.deg)   # TIC 1603615314
x_tic, y_tic  = wcs.world_to_pixel(coord)

print("TIC 1603615314 pixel position:  x =", x_tic, " y =", y_tic)

Variable Star Anomaly Detection — scanning.py

An upcoming large-scale pipeline targeting the NGC 2516 open cluster (~400–500 members). It queries Gaia DR3 for cluster membership, cross-matches to the TESS Input Catalog, then bulk-downloads and stitches TESS light curves for every star — building a dataset for automated variability and anomaly detection across hundreds of objects.

astroquery lightkurve Gaia DR3 TESS TIC pandas NGC 2516
scanning.py
"""
Variable Star Anomaly Detection Pipeline
Target: NGC 2516 open cluster (~400-500 members)
Phase 1: Gaia membership query + TESS light curve download
"""

import pandas as pd
from pathlib import Path
from astroquery.gaia import Gaia
from astroquery.mast import Catalogs
import lightkurve as lk
import warnings

warnings.filterwarnings("ignore")

# -- Output directory ----------------------------------------------------------
DATA_DIR = Path("data/ngc2516")
DATA_DIR.mkdir(parents=True, exist_ok=True)

# -- 1. Query Gaia DR3 membership for NGC 2516 ---------------------------------
# Center: RA=119.52, Dec=-60.75 | Distance ~408 pc -> parallax ~2.45 mas
# Membership filter: parallax + proper motion box from Cantat-Gaudin 2018

GAIA_QUERY = """
SELECT
    source_id,
    ra,
    dec,
    parallax,
    parallax_error,
    pmra,
    pmdec,
    phot_g_mean_mag,
    phot_bp_mean_mag,
    phot_rp_mean_mag,
    bp_rp,
    radial_velocity
FROM gaiadr3.gaia_source
WHERE CONTAINS(
    POINT(ra, dec),
    CIRCLE(119.52, -60.75, 0.75)
) = 1
AND parallax BETWEEN 2.1 AND 2.9
AND pmra    BETWEEN -5.5 AND -3.5
AND pmdec   BETWEEN 11.0 AND 13.0
AND phot_g_mean_mag < 14.0
AND parallax_error < 0.2
"""

print("Querying Gaia DR3 for NGC 2516 members...")
job = Gaia.launch_job(GAIA_QUERY)
gaia_members = job.get_results().to_pandas()
print(f"  Found {len(gaia_members)} candidate members\n")

gaia_members.to_csv(DATA_DIR / "gaia_members.csv", index=False)


# -- 2. Cross-match Gaia sources to TESS Input Catalog (TIC) ------------------

print("Cross-matching to TESS Input Catalog...")

tic_ids  = []
tic_tmag = []

for _, row in gaia_members.iterrows():
    result = Catalogs.query_region(
        f"{row['ra']} {row['dec']}",
        radius="5s",           # 5 arcsec search radius
        catalog="TIC"
    )
    if len(result) > 0:
        result.sort("Tmag")    # take brightest match within radius
        tic_ids.append(int(result["ID"][0]))
        tic_tmag.append(float(result["Tmag"][0]))
    else:
        tic_ids.append(None)
        tic_tmag.append(None)

gaia_members["tic_id"] = tic_ids
gaia_members["Tmag"]   = tic_tmag

matched = gaia_members.dropna(subset=["tic_id"])
matched = matched[matched["Tmag"] < 13.5].reset_index(drop=True)
matched["tic_id"] = matched["tic_id"].astype(int)

print(f"  {len(matched)} stars matched to TIC with Tmag < 13.5\n")
matched.to_csv(DATA_DIR / "tic_matched.csv", index=False)


# -- 3. Download TESS light curves --------------------------------------------
# NGC 2516 covered in Sectors 1, 27, 28 (Southern CVZ-adjacent)
# Priority: 2-min cadence SPOC PDCSAP; fallback to QLP (FFI-based)

TARGET_SECTORS     = [100]
PREFERRED_AUTHORS  = ["SPOC", "QLP"]

print("Downloading TESS light curves...")
light_curves = {}
download_log = []

for _, row in matched.iterrows():
    tic_id = row["tic_id"]
    target = f"TIC {tic_id}"

    try:
        search = lk.search_lightcurve(
            target,
            mission="TESS",
            sector=TARGET_SECTORS,
            author=PREFERRED_AUTHORS,
            exptime="short"        # prefer 2-min cadence
        )

        # Fallback: if no 2-min, accept 10-min (QLP FFI)
        if len(search) == 0:
            search = lk.search_lightcurve(
                target, mission="TESS",
                sector=TARGET_SECTORS, author="QLP"
            )

        if len(search) == 0:
            download_log.append({"tic_id": tic_id, "status": "no_data"})
            continue

        lc_collection = search.download_all(
            flux_column="pdcsap_flux",
            quality_bitmask="hardest"
        )

        if lc_collection is None or len(lc_collection) == 0:
            download_log.append({"tic_id": tic_id, "status": "download_failed"})
            continue

        # Stitch sectors: normalize each to unit median before combining
        lc_stitched = lc_collection.stitch(corrector_func=lambda lc: lc.normalize())

        lc_clean = (
            lc_stitched
            .remove_nans()
            .remove_outliers(sigma=5.0)
        )

        if len(lc_clean) < 200:
            download_log.append({"tic_id": tic_id, "status": "too_short",
                                  "n_points": len(lc_clean)})
            continue

        light_curves[tic_id] = lc_clean
        download_log.append({
            "tic_id":   tic_id,
            "status":   "ok",
            "n_points": len(lc_clean),
            "sectors":  list(lc_collection.sector) if hasattr(lc_collection, "sector")
                        else TARGET_SECTORS,
            "Tmag":     row["Tmag"],
            "bp_rp":    row["bp_rp"]
        })

    except Exception as e:
        download_log.append({"tic_id": tic_id, "status": f"error: {str(e)}"})
        continue

# -- 4. Summary ----------------------------------------------------------------

log_df = pd.DataFrame(download_log)
log_df.to_csv(DATA_DIR / "download_log.csv", index=False)

ok = log_df[log_df["status"] == "ok"]
print(f"Download complete:")
print(f"  Successfully downloaded: {len(ok)} light curves")
print(f"  No data available:       {len(log_df[log_df['status'] == 'no_data'])}")
print(f"  Too short / failed:      {len(log_df) - len(ok) - len(log_df[log_df['status'] == 'no_data'])}")
print(f"\nLight curves ready for feature extraction: {len(light_curves)}")
print(f"Data saved to: {DATA_DIR.resolve()}")

Meeting Schedule

Weekly sessions focused on the AIAA Space Design Competition — from onboarding through final proposal submission. All meetings held online.

Session 1 Mar 1, 2026 · 9:30 PM AIAA Onboarding + Introduction Done
Session 2 Mar 8, 2026 · 9:30 PM Quad Charts + Proposal Details Done
Session 3 Mar 16, 2026 · 9:30 PM Org Chart + Shell Doc Done
Session 4 Mar 22, 2026 · 9:30 PM Review and Brainstorm with Team Next Up
Session 5 Mar 29, 2026 · 9:30 PM LaTeX Code Workshop Upcoming
Session 6 Apr 5, 2026 · 9:30 PM Proposal Writing 1 Upcoming
Session 7 Apr 12, 2026 · 9:30 PM Proposal Writing 2 Upcoming
Session 8 Apr 19, 2026 · 9:30 PM Proposal Writing 3 Upcoming
Session 9 TBA Proposal Writing 4 TBA
Session 10 TBA Review / Closeout TBA

Connect With Us

Stay in touch, get updates, and join the conversation online.

Get In Touch

Have a question or want to get involved? We'd love to hear from you. Reach out anytime — we welcome new members and collaborators at all levels.

aiaa.smc.student.branch@gmail.com