Brahe provides three fixed-epoch star catalogs - FK5, Hipparcos, and Tycho-2 - for reference-frame realization and star-based attitude determination. Unlike other datasets, star catalogs are static: once published they are not expected to change, so cached copies never go stale by default.
The Fifth Fundamental Catalogue (FK5) is a fixed catalog of 1,535 bright fundamental stars at epoch J2000.0. It is the classical realization of the mean equator/equinox reference system that preceded ICRS-based catalogs.
The Hipparcos Catalogue is a fixed astrometric catalog of ~118,000 stars derived from the ESA Hipparcos satellite mission, referred to the ICRS at epoch J1991.25. The hip_main source file does not carry a radial velocity column, so StarRecord::radial_velocity always returns None/null for Hipparcos records.
The Tycho-2 Catalogue is a fixed astrometric catalog of ~2.54 million stars derived from the Hipparcos satellite's star mapper data, referred to the ICRS. Tycho-2 does not carry a parallax or radial velocity column.
Tycho-2 download size
The Tycho-2 source file is large (~526 MB, ~2.54 million records), so the first call to get_tycho2() may take some time. Subsequent calls use the cached copy.
A small fraction of Tycho-2 entries (pflag == "X") have no mean astrometric solution: their ra/dec/pm_ra/pm_dec/epoch_ra/epoch_dec fields are all missing. For these records, the catalog's derived quantities (id, name, unit_vector, radec_at_epoch) fall back to the always-present observed position (ra_observed/dec_observed, epoch ~1991.5) instead.
Star catalog data is downloaded from https://www.simplespacedata.org/star_catalog/cds with file-based caching:
Cache location: ~/.cache/brahe/star_catalogs/ (or $BRAHE_CACHE/star_catalogs/ if set). Setting BRAHE_NETWORK_MODE=offline serves cached data without any request; see Environment Variables.
Default TTL: none - the cached copy never goes stale, since published star catalogs are not expected to change
Force refresh: Pass cache_max_age=0 (Python) / Some(0.0) (Rust) to bypass the cache and download fresh data
This differs from GCAT's 24-hour TTL: FK5, Hipparcos, and Tycho-2 are fixed, one-time publications, so there is no staleness to guard against by default.
importbraheasbh# Download the Hipparcos catalog (cached permanently after the first download)hipparcos=bh.datasets.star_catalogs.get_hipparcos()print(f"Loaded {len(hipparcos)} Hipparcos records")# Filter to naked-eye-bright stars (Vmag < 5.2)bright=hipparcos.filter_by_magnitude(5.2)print(f"Stars brighter than Vmag 5.2: {len(bright)}")# Sort by magnitude and print the 5 brightest namesrecords=sorted(bright.records(),key=lambdar:r.vmag)print("\n5 brightest stars:")forrinrecords[:5]:print(f" {r.name()orr.id()}: Vmag={r.vmag:.2f}")df=bright.to_dataframe()print("\nDataFrame head:")print(df.head())
#[allow(unused_imports)]usebraheasbh;usebh::datasets::star_catalogs;usebh::datasets::star_catalogs::StarRecord;fnmain(){// Download the Hipparcos catalog (cached permanently after the first download)lethipparcos=star_catalogs::get_hipparcos_catalog(None).unwrap();println!("Loaded {} Hipparcos records",hipparcos.len());// Filter to naked-eye-bright stars (Vmag < 5.2)letbright=hipparcos.filter_by_magnitude(5.2);println!("Stars brighter than Vmag 5.2: {}",bright.len());// Sort by magnitude and print the 5 brightest namesletmutrecords=bright.records().to_vec();records.sort_by(|a,b|a.vmag.partial_cmp(&b.vmag).unwrap());println!("\n5 brightest stars:");forrinrecords.iter().take(5){println!(" {}: Vmag={:.2}",r.name().unwrap_or_else(||r.id()),r.vmag.unwrap());}letdf=bright.to_dataframe().unwrap();lethead=df.head(Some(5));println!("\nDataFrame head shape: {:?}",head.shape());println!("Columns: {:?}",head.get_column_names());}
Every catalog supports lookup by identifier, magnitude filtering, and cone-search filtering. Filter methods return a new catalog instance (immutable pattern), so the original catalog is never modified and filters can be chained. The example below downloads FK5, looks up a star by its running number, then filters by magnitude and cone search, chaining both:
importbraheasbh# Download the FK5 catalog (cached permanently after the first download)fk5=bh.datasets.star_catalogs.get_fk5()print(f"Loaded {len(fk5)} FK5 records")# Look up a specific star by its FK5 running numberrec=fk5.get_by_id(699)vmag_str=f"{rec.vmag:.2f}"ifrec.vmagisnotNoneelse"N/A"print(f"FK5 699: {rec.name()orrec.id()}, ra={rec.ra:.4f} deg, dec={rec.dec:.4f} deg, vmag={vmag_str}")# Magnitude filter: keeps vmag <= max_mag (smaller/more negative is brighter)bright=fk5.filter_by_magnitude(3.0)print(f"Stars brighter than Vmag 3.0: {len(bright)}")# Cone search around a right ascension/declination, in degreesnearby=fk5.filter_by_cone(101.28,-16.72,5.0,bh.AngleFormat.DEGREES)print(f"Stars within 5 deg of (101.28, -16.72): {len(nearby)}")# Chained: bright stars within 5 degrees of a target (filter methods return a# new catalog instance, so the original catalog is never modified)bright_nearby=fk5.filter_by_magnitude(3.0).filter_by_cone(101.28,-16.72,5.0,bh.AngleFormat.DEGREES)print(f"Bright stars within 5 deg of target: {len(bright_nearby)}")forrinbright_nearby.records():print(f" {r.name()orr.id()}: Vmag={r.vmag:.2f}")
#[allow(unused_imports)]usebraheasbh;usebh::datasets::star_catalogs;usebh::datasets::star_catalogs::StarRecord;fnmain(){// Download the FK5 catalog (cached permanently after the first download)letfk5=star_catalogs::get_fk5_catalog(None).unwrap();println!("Loaded {} FK5 records",fk5.len());// Look up a specific star by its FK5 running numberletrec=fk5.get_by_id(699).unwrap();letvmag_str=rec.vmag.map(|v|format!("{:.2}",v)).unwrap_or_else(||"N/A".to_string());println!("FK5 699: {}, ra={:.4} deg, dec={:.4} deg, vmag={}",rec.name().unwrap_or_else(||rec.id()),rec.ra,rec.dec,vmag_str);// Magnitude filter: keeps vmag <= max_mag (smaller/more negative is brighter)letbright=fk5.filter_by_magnitude(3.0);println!("Stars brighter than Vmag 3.0: {}",bright.len());// Cone search around a right ascension/declination, in degreesletnearby=fk5.filter_by_cone(101.28,-16.72,5.0,bh::AngleFormat::Degrees);println!("Stars within 5 deg of (101.28, -16.72): {}",nearby.len());// Chained: bright stars within 5 degrees of a target (filter methods// return a new catalog instance, so the original catalog is never// modified)letbright_nearby=fk5.filter_by_magnitude(3.0).filter_by_cone(101.28,-16.72,5.0,bh::AngleFormat::Degrees);println!("Bright stars within 5 deg of target: {}",bright_nearby.len());forrinbright_nearby.records(){println!(" {}: Vmag={:.2}",r.name().unwrap_or_else(||r.id()),r.vmag.unwrap());}}
Loaded 1535 FK5 records
FK5 699: HD 172167, ra=279.2347 deg, dec=38.7837 deg, vmag=0.03
Stars brighter than Vmag 3.0: 157
Stars within 5 deg of (101.28, -16.72): 2
Bright stars within 5 deg of target: 1
HD 48915: Vmag=-1.46
Loaded 1535 FK5 records
FK5 699: HD 172167, ra=279.2347 deg, dec=38.7837 deg, vmag=0.03
Stars brighter than Vmag 3.0: 157
Stars within 5 deg of (101.28, -16.72): 2
Bright stars within 5 deg of target: 1
HD 48915: Vmag=-1.46
All three catalogs support conversion to Polars DataFrames for analysis. In Python, to_dataframe() returns a polars.DataFrame; in Rust, it returns a Result<polars::DataFrame, BraheError>:
#[allow(unused_imports)]usebraheasbh;usebh::datasets::star_catalogs;fnmain(){lethipparcos=star_catalogs::get_hipparcos_catalog(None).unwrap();letdf=hipparcos.to_dataframe().unwrap();println!("DataFrame shape: {:?}",df.shape());println!("Columns: {:?}",df.get_column_names());// The `polars` dependency here is built with only the `lazy` feature and// isn't itself a nameable crate for this example, so filter with the// catalog's own `filter_by_magnitude` before exporting, rather than the// Python example's Polars-level `.str.contains()` expression.letbright=hipparcos.filter_by_magnitude(4.0);letbright_df=bright.to_dataframe().unwrap();println!("Stars brighter than Vmag 4.0: {}",bright_df.height());}
Catalog positions are only valid at the catalog's reference epoch (J2000.0 for FK5, J1991.25 for Hipparcos, J2000.0 for Tycho-2). Every record exposes radec_at_epoch to propagate its position to a different epoch using proper motion (and parallax/radial velocity, when known). The example below filters Hipparcos by magnitude and cone search to locate Sirius, then propagates its catalog position forward to J2030.0:
#[allow(unused_imports)]usebraheasbh;usebh::datasets::star_catalogs;usebh::datasets::star_catalogs::StarRecord;fnmain(){lethipparcos=star_catalogs::get_hipparcos_catalog(None).unwrap();// Magnitude filter: keeps vmag <= max_mag (smaller/more negative is brighter)letbright=hipparcos.filter_by_magnitude(2.0);println!("Stars brighter than Vmag 2.0: {}",bright.len());// Cone search around Sirius' catalog position, in degreesletnearby=bright.filter_by_cone(101.28,-16.72,5.0,bh::AngleFormat::Degrees);println!("Bright stars within 5 deg of Sirius: {}",nearby.len());forrinnearby.records(){println!(" {}: Vmag={:.2}",r.name().unwrap_or_else(||r.id()),r.vmag.unwrap());}// Propagate Sirius (HIP 32349) to a future epoch using radec_at_epoch// (same proper-motion transformation as apply_proper_motion, applied// directly to a catalog record)letsirius=hipparcos.get_by_id(32349).unwrap();letepc=bh::Epoch::from_datetime(2030,1,1,0,0,0.0,0.0,bh::TimeSystem::UTC);let(ra,dec)=sirius.radec_at_epoch(epc,bh::AngleFormat::Degrees);println!("\nSirius at J2030.0: RA={:.6} deg, Dec={:.6} deg",ra,dec);println!("Sirius at J1991.25: RA={:.6} deg, Dec={:.6} deg",sirius.ra,sirius.dec);}
Catalog positions are returned in the catalog's native reference frame, exactly as published. Brahe applies no frame correction: ra/dec, unit_vector, and radec_at_epoch all return raw catalog-frame values. Which frame that is differs by catalog, and this matters when catalog directions are combined with a GCRF orbit state or fed into the IAU 2006/2000A (CIO-based) frame transformations.
Catalog
Native frame
Relation to Brahe's GCRF/ECI
Hipparcos
ICRS
Same axes (aligned to ~10 µas); no rotation needed
Tycho-2
ICRS
Same axes; no rotation needed
FK5
Mean equator/equinox of J2000.0 (EME2000)
Offset by the ~23 mas frame bias; rotate EME2000 → GCRF
ICRS catalogs are GCRF-ready; FK5 is not
The "epoch" in a catalog's description refers to two different things depending on the catalog. For Hipparcos, ICRS at epoch J1991.25 means the ICRS axes (which are fixed and epoch-independent, tied to extragalactic radio sources) with each star's position measured at J1991.25. Because Brahe's GCRF is aligned to the ICRS by construction, Hipparcos and Tycho-2 directions are already GCRF directions - the only step needed to use them at another date is proper-motion propagation via radec_at_epoch.
For FK5 at epoch J2000.0, J2000.0 pins both the position epoch and the axes: FK5 realizes the dynamical mean equator and equinox of J2000.0, which is the same frame Brahe exposes as EME2000, not the ICRS/GCRF axes. FK5 directions therefore differ from GCRF by the ~23 mas frame bias and must be rotated with position_eme2000_to_gcrf (or rotation_eme2000_to_gcrf) before they are mixed with a GCRF state or passed to the GCRF → ITRF transform. (To FK5's own systematic accuracy this identification is exact; it does not carry FK5's residual zonal/equinox errors.)
The example below propagates an FK5 star to an observation epoch, rotates the resulting direction from EME2000 into GCRF, and then into the Earth-fixed ITRF frame with the IAU 2006/2000A transform - the full catalog → GCRF → ITRF chain:
importnumpyasnpimportbraheasbhbh.initialize_eop()# Download FK5 and look up a star by its running numberfk5=bh.datasets.star_catalogs.get_fk5()rec=fk5.get_by_id(699)print(f"FK5 699: {rec.name()orrec.id()}")print(f" Catalog (EME2000) position, J2000.0: RA={rec.ra:.6f} deg, Dec={rec.dec:.6f} deg")# Propagate the catalog position to the observation epoch with proper motion.# The FK5 axes are fixed and do not depend on the position epoch, so the# propagated (ra, dec) is still expressed on the EME2000 axes.epc=bh.Epoch.from_datetime(2024,3,20,12,0,0.0,0.0,bh.UTC)ra,dec=rec.radec_at_epoch(epc,bh.AngleFormat.DEGREES)print(f" Propagated to epoch: RA={ra:.6f} deg, Dec={dec:.6f} deg")# Direction as a unit vector on the EME2000 axes ...u_eme2000=bh.position_radec_to_inertial(np.array([ra,dec,1.0]),bh.AngleFormat.DEGREES)# ... rotated onto the GCRF/ICRS axes the IAU 2006/2000A transforms expect.u_gcrf=bh.position_eme2000_to_gcrf(u_eme2000)# The frame bias is a small (~23 mas) but non-zero rotation.sep_rad=np.arctan2(np.linalg.norm(np.cross(u_eme2000,u_gcrf)),np.dot(u_eme2000,u_gcrf))print(f"\nEME2000 -> GCRF frame-bias shift: {np.degrees(sep_rad)*3.6e6:.2f} mas")# The GCRF direction can now be rotated into the Earth-fixed (ITRF) frame with# the IAU 2006/2000A CIO-based transform (e.g. for a topocentric pointing).u_itrf=bh.rotation_eci_to_ecef(epc)@u_gcrfprint(f"GCRF unit vector: [{u_gcrf[0]:.6f}, {u_gcrf[1]:.6f}, {u_gcrf[2]:.6f}]")print(f"ITRF unit vector: [{u_itrf[0]:.6f}, {u_itrf[1]:.6f}, {u_itrf[2]:.6f}]")
#[allow(unused_imports)]usebraheasbh;usebh::datasets::star_catalogs;usebh::datasets::star_catalogs::StarRecord;usenalgebraasna;fnmain(){bh::initialize_eop().unwrap();// Download FK5 and look up a star by its running numberletfk5=star_catalogs::get_fk5_catalog(None).unwrap();letrec=fk5.get_by_id(699).unwrap();println!("FK5 699: {}",rec.name().unwrap_or_else(||rec.id()));println!(" Catalog (EME2000) position, J2000.0: RA={:.6} deg, Dec={:.6} deg",rec.ra,rec.dec);// Propagate the catalog position to the observation epoch with proper// motion. The FK5 axes are fixed and do not depend on the position epoch,// so the propagated (ra, dec) is still expressed on the EME2000 axes.letepc=bh::Epoch::from_datetime(2024,3,20,12,0,0.0,0.0,bh::TimeSystem::UTC);let(ra,dec)=rec.radec_at_epoch(epc,bh::AngleFormat::Degrees);println!(" Propagated to epoch: RA={ra:.6} deg, Dec={dec:.6} deg");// Direction as a unit vector on the EME2000 axes ...letu_eme2000=bh::position_radec_to_inertial(na::Vector3::new(ra,dec,1.0),bh::AngleFormat::Degrees);// ... rotated onto the GCRF/ICRS axes the IAU 2006/2000A transforms expect.letu_gcrf=bh::position_eme2000_to_gcrf(u_eme2000);// The frame bias is a small (~23 mas) but non-zero rotation.letsep_rad=u_eme2000.cross(&u_gcrf).norm().atan2(u_eme2000.dot(&u_gcrf));println!("\nEME2000 -> GCRF frame-bias shift: {:.2} mas",sep_rad.to_degrees()*3.6e6);// The GCRF direction can now be rotated into the Earth-fixed (ITRF) frame// with the IAU 2006/2000A CIO-based transform (e.g. for a topocentric// pointing).letu_itrf=bh::rotation_eci_to_ecef(epc)*u_gcrf;println!("GCRF unit vector: [{:.6}, {:.6}, {:.6}]",u_gcrf[0],u_gcrf[1],u_gcrf[2]);println!("ITRF unit vector: [{:.6}, {:.6}, {:.6}]",u_itrf[0],u_itrf[1],u_itrf[2]);}
Johnson V-band approximation (optional; see below)
tycho1_flag
-
Set ("T") if this entry also has a Tycho-1 record (optional)
hip_id
-
Hipparcos catalog identifier, if this star is also in Hipparcos (optional)
ra_observed, dec_observed
deg
Observed right ascension/declination, epoch ~1991.5; always present, even when ra/dec are missing
Tycho-2 has no parallax or radial_velocity column: both always return None/null via StarRecord. The vmag field is computed from the catalog's BT/VT photometry as \(V_T - 0.090\,(B_T - V_T)\) when both are present, falling back to vt_mag alone, per the Tycho-2 catalog documentation.