1
import math
from typing import List, Tuple, Any

# ---------- parsing helpers ----------
def _to_float(x: Any) -> float:
    try:
        return float(x)
    except Exception:
        return None

def parse_ra(ra: Any) -> float:
    """
    Right ascension -> radians.
    Accepts:
      - decimal degrees (e.g., 217.427)   -> degrees
      - 'hh mm ss.s'  or 'hh:mm:ss.s'     -> hours
      - '14h 29m 43.0s'                    -> hours
    """
    f = _to_float(ra)
    if f is not None:
        deg = f
    else:
        s = str(ra).strip().lower().replace("h", " ").replace("m", " ").replace("s", " ")
        s = s.replace(":", " ")
        hh, mm, ss = [float(t) for t in s.split()]
        hours = hh + mm/60 + ss/3600
        deg = hours * 15.0
    return math.radians(deg)

def parse_dec(dec: Any) -> float:
    """
    Declination -> radians.
    Accepts:
      - decimal degrees (e.g., -62.6794)
      - '±dd mm ss' or '±dd:mm:ss'
      - '−62° 40′ 46″' (unicode symbols OK)
    """
    f = _to_float(dec)
    if f is not None:
        deg = f
    else:
        s = str(dec).strip().lower()
        # normalize unicode/degree markers to spaces
        for ch in ["°", "º", "′", "’", "'", "″", '"']:
            s = s.replace(ch, " ")
        s = s.replace(":", " ")
        sign = -1.0 if s.lstrip().startswith(("-", "−")) else 1.0
        s = s.replace("−", "").replace("-", "")  # strip minus for split
        parts = [p for p in s.split() if p]
        dd, mm, ss = [float(t) for t in parts[:3]]
        deg = sign * (abs(dd) + mm/60 + ss/3600)
    return math.radians(deg)

# ---------- coordinate conversion ----------
def radec_to_cartesian_ly(dist_ly: float, ra_rad: float, dec_rad: float) -> Tuple[float, float, float]:
    # standard astronomical convention
    x = dist_ly * math.cos(dec_rad) * math.cos(ra_rad)
    y = dist_ly * math.cos(dec_rad) * math.sin(ra_rad)
    z = dist_ly * math.sin(dec_rad)
    return (x, y, z)

def euclid(a: Tuple[float, float, float], b: Tuple[float, float, float]) -> float:
    return math.dist(a, b)

# ---------- main: nearest neighbors ----------
def nearest_neighbors(
    stars: List[Tuple[str, float, Any, Any]],
    top_k_examples: int = 10
):
    """
    stars: list of [name, distance_ly, RA, Dec]
           RA/Dec can be decimal deg or strings as shown above.
    """
    names, xyz = [], []
    for name, d_ly, ra, dec in stars:
        ra_r = parse_ra(ra)
        dec_r = parse_dec(dec)
        names.append(name)
        xyz.append(radec_to_cartesian_ly(d_ly, ra_r, dec_r))

    n = len(names)
    nn = []  # (i, j, dist)
    for i in range(n):
        best_j, best_d = None, float("inf")
        for j in range(n):
            if i == j: continue
            d = euclid(xyz[i], xyz[j])
            if d < best_d:
                best_d, best_j = d, j
        nn.append((i, best_j, best_d))

    avg = sum(d for _, _, d in nn) / n if n else float("nan")
    print(f"Average nearest-neighbor distance: {avg:.3f} ly")

    # show a few examples
    for i, (ii, jj, d) in enumerate(sorted(nn, key=lambda t: t[2])[:top_k_examples]):
        print(f"{i+1:>2}. {names[ii]}{names[jj]} : {d:.3f} ly")

    return nn, avg

# ---------- example usage ----------
stars = [
    ["Sun", 0, "0", "0"],
    ["Proxima Centauri", 4.2465, "14h 29m 43.0s", "−62° 40′ 46″"],
    ["Alpha Centauri A", 4.367,  "14h 39m 36.5s", "−60° 50′ 02″"],
    ["Alpha Centauri B", 4.367,  "14h 39m 35.1s", "−60° 50′ 14″"],
    ["Barnard's Star",   5.9629, "17h 57m 48.5s", "+04° 41′ 36″"],
    # add as many as you like…
]

if __name__ == "__main__":
    nearest_neighbors(stars)

For immediate assistance, please email our customer support: [email protected]

Download RAW File