Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Astronomical data processing with Astropy, FITS files, and sky surveys
.claude/skills/brycewang-stanford-astrophysics-data-guide/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 24% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 147% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 88% | 0% |
| case-08 | ✗→✓ | ▲ Improved | -2% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 197% | 0% |
A skill for processing and analyzing astronomical data using standard astrophysics tools. Covers FITS file handling, coordinate transformations, photometric analysis, spectral analysis, catalog cross-matching, and accessing major sky survey archives.
FITS (Flexible Image Transport System) is the standard data format in astronomy:
pythonfrom astropy.io import fits import numpy as np def inspect_fits(filepath: str) -> dict: """ Inspect the structure of a FITS file. Returns information about each HDU (Header/Data Unit). """ with fits.open(filepath) as hdul: info = [] for i, hdu in enumerate(hdul): entry = { "index": i, "name": hdu.name, "type": type(hdu).__name__, } if hdu.data is not None: entry["shape"] = hdu.data.shape entry["dtype"] = str(hdu.data.dtype) if hasattr(hdu, "columns") and hdu.columns is not None: entry["columns"] = [c.name for c in hdu.columns] info.append(entry) return {"filename": filepath, "n_hdus": len(hdul), "hdus": info} def read_fits_image(filepath: str, hdu_index: int = 0) -> tuple: """Read a FITS image and its WCS (World Coordinate System).""" from astropy.wcs import WCS with fits.open(filepath) as hdul: data = hdul[hdu_index].data header = hdul[hdu_index].header wcs = WCS(header) return data, wcs, header
pythonfrom astropy.table import Table def read_fits_catalog(filepath: str, hdu: int = 1) -> Table: """Read a FITS binary table extension as an Astropy Table.""" catalog = Table.read(filepath, hdu=hdu) print(f"Catalog: {len(catalog)} objects, {len(catalog.columns)} columns") print(f"Columns: {catalog.colnames}") return catalog
pythonfrom astropy.coordinates import SkyCoord, EarthLocation, AltAz from astropy.time import Time import astropy.units as u def coordinate_transforms(ra_deg: float, dec_deg: float) -> dict: """ Transform between astronomical coordinate systems. ra_deg, dec_deg: right ascension and declination in degrees (ICRS/J2000) """ coord = SkyCoord(ra=ra_deg * u.degree, dec=dec_deg * u.degree, frame="icrs") return { "icrs": { "ra": coord.ra.to_string(unit=u.hourangle, precision=2), "dec": coord.dec.to_string(unit=u.degree, precision=2), }, "galactic": { "l": round(coord.galactic.l.degree, 4), "b": round(coord.galactic.b.degree, 4), }, "ecliptic": { "lon": round(coord.geocentricmeanecliptic.lon.degree, 4), "lat": round(coord.geocentricmeanecliptic.lat.degree, 4), }, } def compute_altaz(ra_deg: float, dec_deg: float, obs_time: str, location: tuple) -> dict: """ Compute altitude and azimuth for a target from a given location and time. location: (latitude_deg, longitude_deg, elevation_m) """ target = SkyCoord(ra=ra_deg * u.degree, dec=dec_deg * u.degree) time = Time(obs_time) loc = EarthLocation( lat=location[0] * u.degree, lon=location[1] * u.degree, height=location[2] * u.m, ) altaz_frame = AltAz(obstime=time, location=loc) altaz = target.transform_to(altaz_frame) return { "altitude_deg": round(altaz.alt.degree, 2), "azimuth_deg": round(altaz.az.degree, 2), "airmass": round(altaz.secz.value, 3) if altaz.alt.degree > 0 else None, "is_observable": altaz.alt.degree > 10, }
pythonfrom photutils.aperture import CircularAperture, CircularAnnulus from photutils.aperture import aperture_photometry def perform_aperture_photometry(image: np.ndarray, positions: list[tuple], aperture_radius: float = 5.0, annulus_inner: float = 10.0, annulus_outer: float = 15.0) -> list[dict]: """ Perform aperture photometry with local background subtraction. image: 2D numpy array (flux/counts) positions: list of (x, y) pixel coordinates of sources """ apertures = CircularAperture(positions, r=aperture_radius) annuli = CircularAnnulus(positions, r_in=annulus_inner, r_out=annulus_outer) # Measure flux in aperture and annulus phot_table = aperture_photometry(image, [apertures, annuli]) results = [] for row in phot_table: # Background per pixel from annulus annulus_area = np.pi * (annulus_outer**2 - annulus_inner**2) bkg_per_pixel = row["aperture_sum_1"] / annulus_area # Background-subtracted flux aperture_area = np.pi * aperture_radius**2 net_flux = row["aperture_sum_0"] - bkg_per_pixel * aperture_area # Instrumental magnitude if net_flux > 0: inst_mag = -2.5 * np.log10(net_flux) else: inst_mag = float("nan") results.append({ "x": float(row["xcenter"]), "y": float(row["ycenter"]), "raw_flux": float(row["aperture_sum_0"]), "net_flux": round(float(net_flux), 2), "bkg_per_pixel": round(float(bkg_per_pixel), 2), "inst_mag": round(inst_mag, 4), }) return results
pythonfrom photutils.detection import DAOStarFinder from astropy.stats import sigma_clipped_stats def detect_sources(image: np.ndarray, fwhm: float = 3.0, threshold_sigma: float = 5.0) -> Table: """ Detect point sources in an astronomical image using DAOFind algorithm. """ mean, median, std = sigma_clipped_stats(image, sigma=3.0) daofind = DAOStarFinder(fwhm=fwhm, threshold=threshold_sigma * std) sources = daofind(image - median) if sources is not None: sources.sort("flux", reverse=True) print(f"Detected {len(sources)} sources") return sources
pythonfrom specutils import Spectrum1D, SpectralRegion from specutils.analysis import line_flux, equivalent_width, centroid import astropy.units as u def analyze_spectrum(wavelength: np.ndarray, flux: np.ndarray, line_center: float, line_width: float = 10.0) -> dict: """ Analyze an emission or absorption line in a 1D spectrum. wavelength: array in Angstroms flux: array in erg/s/cm2/Angstrom line_center: expected line center in Angstroms line_width: width of spectral region to analyze """ spectrum = Spectrum1D( spectral_axis=wavelength * u.Angstrom, flux=flux * u.Unit("erg / (s cm2 Angstrom)"), ) region = SpectralRegion( (line_center - line_width) * u.Angstrom, (line_center + line_width) * u.Angstrom, ) measured_flux = line_flux(spectrum, regions=region) ew = equivalent_width(spectrum, regions=region) center = centroid(spectrum, region) # Redshift from line center offset rest_wavelength = line_center # assumed rest frame z = (center.value - rest_wavelength) / rest_wavelength return { "line_flux": f"{measured_flux:.4e}", "equivalent_width": f"{ew:.2f}", "measured_center_A": round(center.value, 2), "redshift": round(z, 6), "velocity_km_s": round(z * 299792.458, 1), }
pythonfrom astroquery.vizier import Vizier from astroquery.simbad import Simbad from astroquery.sdss import SDSS def query_simbad(object_name: str) -> dict: """Query SIMBAD for basic object information.""" result = Simbad.query_object(object_name) if result is None: return {"found": False} return { "found": True, "name": object_name, "ra": str(result["RA"][0]), "dec": str(result["DEC"][0]), "object_type": str(result["OTYPE"][0]), } def cone_search_vizier(ra_deg: float, dec_deg: float, radius_arcmin: float = 1.0, catalog: str = "II/246") -> Table: """ Cone search in a VizieR catalog. Default catalog II/246 = 2MASS Point Source Catalog. """ coord = SkyCoord(ra=ra_deg * u.degree, dec=dec_deg * u.degree) result = Vizier.query_region( coord, radius=radius_arcmin * u.arcmin, catalog=catalog ) return result[0] if result else None
| Survey | Band | Coverage | Resolution | Key Science | |--------|------|----------|-----------|-------------| | SDSS | ugriz | 14,555 sq deg | 1.3" | Galaxy evolution, QSOs | | 2MASS | JHK | All-sky | 2" | Stellar populations, MW structure | | WISE | 3.4-22 um | All-sky | 6-12" | Brown dwarfs, AGN, dusty galaxies | | Gaia DR3 | G, BP, RP | All-sky | 0.1 mas | Astrometry, stellar parameters | | DESI | Spectroscopic | 14,000 sq deg | Fiber | Dark energy, BAO | | JWST | 0.6-28 um | Pointed | 0.03-0.1" | Early universe, exoplanets |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 34,295 | 20,633 | -40% | 1 | 1 | 0% | 5,894 | 7,311 | +24% | 0 | 0 | — |
case-02 | fail→fail | 18,827 | 17,807 | -5% | 1 | 1 | 0% | 3,877 | 6,281 | +62% | 0 | 0 | — |
case-03 | fail→fail | 21,219 | 13,051 | -38% | 1 | 1 | 0% | 4,181 | 5,697 | +36% | 0 | 0 | — |
case-04 | fail→fail | 15,501 | 9,311 | -40% | 1 | 1 | 0% | 2,913 | 4,780 | +64% | 0 | 0 | — |
case-05 | pass→pass | 12,103 | 10,907 | -10% | 1 | 1 | 0% | 2,338 | 4,846 | +107% | 0 | 0 | — |
case-06 | fail→pass | 8,296 | 4,635 | -44% | 1 | 1 | 0% | 1,516 | 3,750 | +147% | 0 | 0 | — |
case-07 | fail→pass | 12,488 | 7,805 | -38% | 1 | 1 | 0% | 2,353 | 4,417 | +88% | 0 | 0 | — |
case-08 | fail→pass | 32,762 | 14,700 | -55% | 1 | 1 | 0% | 5,926 | 5,782 | -2% | 0 | 0 | — |
case-09 | fail→pass | 7,818 | 6,947 | -11% | 1 | 1 | 0% | 1,415 | 4,197 | +197% | 0 | 0 | — |
case-10 | pass→pass | 26,309 | 15,763 | -40% | 1 | 1 | 0% | 3,305 | 5,324 | +61% | 0 | 0 | — |
case-11 | pass→pass | 10,059 | 10,482 | +4% | 1 | 1 | 0% | 1,808 | 4,704 | +160% | 0 | 0 | — |
case-12 | fail→fail | 14,616 | 14,552 | -0% | 1 | 1 | 0% | 2,563 | 5,498 | +115% | 0 | 0 | — |
case-13 | pass→pass | 7,759 | 3,939 | -49% | 1 | 1 | 0% | 1,345 | 3,630 | +170% | 0 | 0 | — |
case-14 | fail→pass | 5,638 | 6,635 | +18% | 1 | 1 | 0% | 1,006 | 3,929 | +291% | 0 | 0 | — |
case-15 | fail→fail | 8,915 | 12,308 | +38% | 1 | 1 | 0% | 1,827 | 5,142 | +181% | 0 | 0 | — |
case-16 | pass→pass | 14,282 | 11,187 | -22% | 1 | 1 | 0% | 2,623 | 5,221 | +99% | 0 | 0 | — |
case-17 | pass→pass | 13,034 | 7,887 | -39% | 1 | 1 | 0% | 2,011 | 4,253 | +111% | 0 | 0 | — |
case-18 | fail→pass | 15,975 | 12,421 | -22% | 1 | 1 | 0% | 2,440 | 5,025 | +106% | 0 | 0 | — |
case-19 | pass→pass | 20,580 | 23,728 | +15% | 1 | 1 | 0% | 2,977 | 6,668 | +124% | 0 | 0 | — |
case-20 | fail→fail | 23,550 | 19,246 | -18% | 1 | 1 | 0% | 3,972 | 6,709 | +69% | 0 | 0 | — |
case-21 | fail→fail | 25,480 | 23,362 | -8% | 1 | 1 | 0% | 4,051 | 6,712 | +66% | 0 | 0 | — |
case-22 | fail→fail | 10,224 | 9,038 | -12% | 1 | 1 | 0% | 1,857 | 4,564 | +146% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +32 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.