// user-01 · Pole stars over time // Generated by scripts/user_01_pole_stars_over_time.py on 2026-03-25 08:11:35Z // Source: Vizier I/239/hip_main fetched once and cached locally at /Users/sunder/projects/cahc/cahc-utils/presentations/2026-03-23-iks-astro-talk/data/hip_main_vizier.tsv // Method: // 1. Fetch and normalize the Hipparcos main catalog. // 2. Prefilter stars by V magnitude <= 4.50 and ecliptic-latitude band // within ±12.0° of the north precession circle (~66.56°). // 3. Scan epochs -4000 to 14000 in 10-year steps with Astropy. // 4. Build two views: // - STAR_BEST: one best-fit row per HIP at its minimum separation from the north celestial pole. // - EPOCH_BEST: one winning star per epoch, compressed into chronological eras. // Data rows use: // STAR_BEST: [year, ra_j2000_deg, dec_j2000_deg, hip, name, min_sep_deg, magnitude] // EPOCH_BEST: [start_year, end_year, best_year, hip, name, min_sep_deg, magnitude] // Summary: // catalog rows cached: 117955 // stars scanned after prefilter: 66 // STAR_BEST rows with min_sep_deg <= 7.00: 30 // EPOCH_BEST compressed eras: 20 var STAR_BEST = [ [ -4000, 231.232392, 58.966068, 75458, "Edasich (ι Dra)", 6.26, 3.29 ], [ -4000, 216.299151, 51.850745, 70497, "θ Boo", 6.283, 4.04 ], [ -2800, 211.097284, 64.375851, 68756, "Thuban (α Dra)", 0.102, 3.67 ], [ -1300, 188.370592, 69.788236, 61281, "κ Dra", 4.671, 3.85 ], [ -1060, 222.676346, 74.155507, 72607, "Kochab (β UMi)", 6.528, 2.07 ], [ -720, 216.881413, 75.695994, 70692, "5 UMi", 4.931, 4.25 ], [ 960, 144.272016, 81.326377, 47193, "", 5.863, 4.28 ], [ 1920, 263.054122, 86.586466, 85822, "Yildun (δ UMi)", 3.386, 4.35 ], [ 2100, 37.954982, 89.264108, 11767, "Polaris (α UMi)", 0.461, 1.97 ], [ 2610, 17.187087, 86.257091, 5372, "2 UMi", 1.364, 4.24 ], [ 4140, 354.836901, 77.632279, 116727, "Errai (γ Cep)", 1.85, 3.21 ], [ 4620, 346.974406, 75.3875, 114222, "π Cep", 1.038, 4.41 ], [ 5930, 322.165002, 70.560721, 106032, "Alfirk (β Cep)", 4.481, 3.23 ], [ 6100, 342.420086, 66.200412, 112724, "ι Cep", 4.078, 3.5 ], [ 6690, 330.947738, 64.627976, 108917, "Kurhah (ξ Cep)", 1.159, 4.26 ], [ 7450, 326.362202, 61.120811, 107418, "ν Cep", 1.347, 4.25 ], [ 7450, 332.713665, 58.201266, 109492, "ζ Cep", 5.692, 3.39 ], [ 7540, 319.644893, 62.585578, 105199, "Alderamin (α Cep)", 1.92, 2.45 ], [ 7770, 325.876932, 58.780051, 107259, "μ Cep", 2.691, 4.23 ], [ 7880, 311.322406, 61.838788, 102422, "η Cep", 5.456, 3.41 ], [ 8190, 307.395364, 62.994111, 101093, "θ Cep", 6.916, 4.21 ], [ 9670, 303.349448, 56.567728, 99655, "33 Cyg", 4.368, 4.28 ], [ 10620, 303.868014, 47.714214, 99848, "ο2 Cyg", 3.21, 3.96 ], [ 10740, 303.40795, 46.741335, 99675, "ο1 Cyg", 3.926, 3.8 ], [ 11400, 294.110569, 50.221109, 96441, "θ Cyg", 2.525, 4.49 ], [ 11490, 292.426502, 51.729785, 95853, "ι Cyg", 3.987, 3.76 ], [ 11560, 296.243664, 45.130816, 97165, "Fawaris (δ Cyg)", 3.28, 2.86 ], [ 11720, 289.275709, 53.368465, 94779, "κ Cyg", 6.22, 3.8 ], [ 13050, 283.833761, 43.946094, 92862, "13 Lyr", 1.844, 4.08 ], [ 13620, 279.23474, 38.783698, 91262, "Vega (α Lyr)", 5.894, 0.03 ] ]; var EPOCH_BEST = [ [ -4000, -3940, -4000, 75458, "Edasich (ι Dra)", 6.26, 3.29 ], [ -3930, -1810, -2800, 68756, "Thuban (α Dra)", 0.102, 3.67 ], [ -1800, -980, -1300, 61281, "κ Dra", 4.671, 3.85 ], [ -970, 300, -720, 70692, "5 UMi", 4.931, 4.25 ], [ 310, 980, 960, 47193, "", 5.863, 4.28 ], [ 990, 1240, 1240, 85822, "Yildun (δ UMi)", 4.866, 4.35 ], [ 1250, 2400, 2100, 11767, "Polaris (α UMi)", 0.461, 1.97 ], [ 2410, 3390, 2610, 5372, "2 UMi", 1.364, 4.24 ], [ 3400, 4300, 4140, 116727, "Errai (γ Cep)", 1.85, 3.21 ], [ 4310, 5490, 4620, 114222, "π Cep", 1.038, 4.41 ], [ 5500, 5750, 5750, 106032, "Alfirk (β Cep)", 4.569, 3.23 ], [ 5760, 5980, 5980, 112724, "ι Cep", 4.14, 3.5 ], [ 5990, 7070, 6690, 108917, "Kurhah (ξ Cep)", 1.159, 4.26 ], [ 7080, 7770, 7450, 107418, "ν Cep", 1.347, 4.25 ], [ 7780, 7940, 7780, 105199, "Alderamin (α Cep)", 2.295, 2.45 ], [ 7950, 8760, 7950, 107259, "μ Cep", 2.874, 4.23 ], [ 8770, 10020, 9670, 99655, "33 Cyg", 4.368, 4.28 ], [ 10030, 10900, 10620, 99848, "ο2 Cyg", 3.21, 3.96 ], [ 10910, 12230, 11400, 96441, "θ Cyg", 2.525, 4.49 ], [ 12240, 14000, 13050, 92862, "13 Lyr", 1.844, 4.08 ] ]; var $JD_0 = 1721057.284468; function BCE(y) { return $JD_0 - (y - 1) * 365.25; } function CE(y) { return $JD_0 + y * 365.25; } function W(x) { if (!x) x = 0.1; core.wait(x); } function toJd(year) { return year < 0 ? BCE(-year) : CE(year); } function epochStr(year) { return year < 0 ? (-year) + " BCE" : year + " CE"; } function paceFactor(index, total) { if (total < 8) { return 1.0; } var start = Math.floor(total * 0.125); var end = Math.ceil(total * 0.875) - 1; if (index >= start && index <= end) { return 0.34; } return 1.0; } function resetLabels() { LabelMgr.deleteAllLabels(); MarkerMgr.deleteAllMarkers(); CustomObjectMgr.removeCustomObjects(); } function label(text, x, y, size, color) { var id = LabelMgr.labelScreen(text, x, y, false, size, color); LabelMgr.setLabelShow(id, true); return id; } function displayName(row) { var name = row[4]; if (name && name.length > 0) { return name; } return "HIP " + row[3]; } function displayEpochWinnerName(row) { var name = row[4]; if (name && name.length > 0) { return name; } return "HIP " + row[3]; } function epochWinnerLabelText(row) { return " " + displayEpochWinnerName(row) + " (" + epochStr(row[2]) + ")"; } function starBestLabelText(row) { return " " + epochStr(row[0]) + " (" + displayName(row) + ")"; } function eraStr(startYear, endYear) { if (startYear === endYear) { return epochStr(startYear); } return epochStr(startYear) + " → " + epochStr(endYear); } function isSpecialName(name) { return name.indexOf("Polaris") >= 0 || name.indexOf("Thuban") >= 0 || name.indexOf("Vega") >= 0 || name.indexOf("Errai") >= 0 || name.indexOf("Kochab") >= 0; } function drawLogEntry(text, x, y, size, color, isSpecial) { var adjustedSize = isSpecial ? size + 1 : size; label(text, x, y, adjustedSize, color); if (isSpecial) { label(text, x + 1, y, adjustedSize, color); } } function safeMarkerObject(objName, markerType, color, size) { try { return MarkerMgr.markerObject(objName, true, markerType, color, size, false, 0); } catch (err) { try { return MarkerMgr.markerObject(objName, true, "diamond", color, size, false, 0); } catch (err2) { return null; } } } function safeLabelObject(text, objName, size, color) { try { var id = LabelMgr.labelObject(text, objName, true, size, color, "E"); LabelMgr.setLabelShow(id, true); return id; } catch (err) { return null; } } var _titleLabelId = null; var _epochLabelId = null; var _sampleLabelId = null; var _detailLine1Id = null; var _detailLine2Id = null; var _objectLabelId = null; var _leftLogX = 0; var _leftLogY = 0; var _rightLogX = 0; var _rightLogY = 0; var _leftLogLineH = 0; var _rightLogLineH = 0; var _persistentObjectLabels = {}; function hideLiveLabels() { if (_epochLabelId !== null) { try { LabelMgr.setLabelShow(_epochLabelId, false); LabelMgr.deleteLabel(_epochLabelId); } catch (err) {} _epochLabelId = null; } if (_sampleLabelId !== null) { try { LabelMgr.setLabelShow(_sampleLabelId, false); LabelMgr.deleteLabel(_sampleLabelId); } catch (err) {} _sampleLabelId = null; } if (_detailLine1Id !== null) { try { LabelMgr.setLabelShow(_detailLine1Id, false); LabelMgr.deleteLabel(_detailLine1Id); } catch (err) {} _detailLine1Id = null; } if (_detailLine2Id !== null) { try { LabelMgr.setLabelShow(_detailLine2Id, false); LabelMgr.deleteLabel(_detailLine2Id); } catch (err) {} _detailLine2Id = null; } if (_objectLabelId !== null) { try { LabelMgr.setLabelShow(_objectLabelId, false); LabelMgr.deleteLabel(_objectLabelId); } catch (err) {} _objectLabelId = null; } } function settleCurrentObjectLabel(objName, text, size, color) { if (_objectLabelId !== null) { try { LabelMgr.setLabelShow(_objectLabelId, false); LabelMgr.deleteLabel(_objectLabelId); } catch (err) {} _objectLabelId = null; } if (_persistentObjectLabels[objName] !== undefined && _persistentObjectLabels[objName] !== null) { try { LabelMgr.setLabelShow(_persistentObjectLabels[objName], false); LabelMgr.deleteLabel(_persistentObjectLabels[objName]); } catch (err2) {} } _persistentObjectLabels[objName] = safeLabelObject(text, objName, size, color); } function initTourPhase() { var maxRows = STAR_BEST.length > EPOCH_BEST.length ? STAR_BEST.length : EPOCH_BEST.length; var usableHeight = Math.max(300, core.getScreenHeight() - 360); var lineH = Math.max(9, Math.min(13, Math.floor(usableHeight / Math.max(1, maxRows)))); _leftLogLineH = lineH; _rightLogLineH = lineH; _leftLogX = 36; _leftLogY = 246; _rightLogX = Math.max(840, core.getScreenWidth() - 390); _rightLogY = 246; _titleLabelId = label("Pole Stars Over Time", 80, 42, 28, "#22FFFF"); label("+", _leftLogX, 208, 22, "#FFD700"); label("Best star for epoch", _leftLogX + 20, 206, 20, "#FFD166"); label("o", _rightLogX, 208, 22, "#66CCFF"); label("Best epoch for star", _rightLogX + 20, 206, 20, "#93C5FD"); } function renderEdgeLogs() { var y = _leftLogY; for (var i = 0; i < EPOCH_BEST.length; i++) { var row = EPOCH_BEST[i]; var rank = (i + 1); var rankText = rank < 10 ? "0" + rank : "" + rank; var display = displayEpochWinnerName(row); var entry = rankText + ". " + display + " (" + eraStr(row[0], row[1]) + ") " + row[5].toFixed(2) + "°"; drawLogEntry(entry, _leftLogX, y, _leftLogLineH, isSpecialName(display) ? "#A16207" : "#6B7280", isSpecialName(display)); y += _leftLogLineH + 2; } y = _rightLogY; for (var j = 0; j < STAR_BEST.length; j++) { var srow = STAR_BEST[j]; var srank = (j + 1); var srankText = srank < 10 ? "0" + srank : "" + srank; var sdisplay = displayName(srow); var sentry = srankText + ". " + epochStr(srow[0]) + " (" + sdisplay + ") " + srow[5].toFixed(2) + "°"; drawLogEntry(sentry, _rightLogX, y, _rightLogLineH, isSpecialName(sdisplay) ? "#0EA5E9" : "#64748B", isSpecialName(sdisplay)); y += _rightLogLineH + 2; } } function highlightEpochLog(index) { var row = EPOCH_BEST[index]; var rank = (index + 1); var rankText = rank < 10 ? "0" + rank : "" + rank; var display = displayEpochWinnerName(row); var entry = rankText + ". " + display + " (" + eraStr(row[0], row[1]) + ") " + row[5].toFixed(2) + "°"; var y = _leftLogY + index * (_leftLogLineH + 2); drawLogEntry(entry, _leftLogX, y, _leftLogLineH, isSpecialName(display) ? "#FFF2B2" : "#F8FAFC", isSpecialName(display)); } function highlightStarLog(index) { var row = STAR_BEST[index]; var rank = (index + 1); var rankText = rank < 10 ? "0" + rank : "" + rank; var display = displayName(row); var entry = rankText + ". " + epochStr(row[0]) + " (" + display + ") " + row[5].toFixed(2) + "°"; var y = _rightLogY + index * (_rightLogLineH + 2); drawLogEntry(entry, _rightLogX, y, _rightLogLineH, isSpecialName(display) ? "#BAE6FD" : "#E0F2FE", isSpecialName(display)); } resetLabels(); core.setGuiVisible(false); core.setObserverLocation("Kurukshetra, India", "Earth"); LandscapeMgr.setFlagLandscape(false); LandscapeMgr.setFlagAtmosphere(false); LandscapeMgr.setFlagFog(false); ConstellationMgr.setFlagLines(true); ConstellationMgr.setFlagLabels(false); ConstellationMgr.setFlagArt(false); GridLinesMgr.setFlagEquatorGrid(true); GridLinesMgr.setFlagAzimuthalGrid(false); GridLinesMgr.setFlagPrecessionCircles(true); GridLinesMgr.setFlagEclipticLine(false); GridLinesMgr.setFlagEquatorLine(false); StelMovementMgr.setEquatorialMount(true); StelMovementMgr.setFlagTracking(false); core.setJDay(toJd(-4000)); core.moveToRaDec(0, 89.6, 0); StelMovementMgr.zoomTo(62, 1.5); W(1.5); label("Pole Stars Over Time", 80, 120, 36, "#FFDD88"); label("Precession moves the north celestial pole across the sky.", 80, 176, 20, "#F8FAFC"); label("Astropy scanned the catalog; Stellarium now shows the result.", 80, 208, 20, "#A7F3D0"); W(6); resetLabels(); initTourPhase(); renderEdgeLogs(); for (var i = 0; i < EPOCH_BEST.length; i++) { var pace = paceFactor(i, EPOCH_BEST.length); var erow = EPOCH_BEST[i]; var eyear = erow[2]; var ehip = erow[3]; var esep = erow[5]; var emag = erow[6]; var eobjName = "HIP " + ehip; hideLiveLabels(); core.setJDay(toJd(eyear)); W(0.2 * pace); core.moveToRaDec(0, 89.6, 0.4); W(0.2 * pace); try { core.selectObjectByName(eobjName, true); W(0.2 * pace); safeMarkerObject(eobjName, "cross", "#FFD700", isSpecialName(displayEpochWinnerName(erow)) ? 18 : 16); _objectLabelId = safeLabelObject(epochWinnerLabelText(erow), eobjName, isSpecialName(displayEpochWinnerName(erow)) ? 19 : 18, "#FFE680"); } catch (err) { } StelMovementMgr.zoomTo(54, 1.0); W(0.4 * pace); StelMovementMgr.zoomTo(46, 1.0); _epochLabelId = label("Best star for epoch: " + eraStr(erow[0], erow[1]), 80, 88, 22, "#FFD166"); _sampleLabelId = label("Epoch winner " + (i + 1) + " / " + EPOCH_BEST.length, Math.max(900, _rightLogX - 120), 48, 16, "#CBD5E1"); _detailLine1Id = label("Star: " + displayEpochWinnerName(erow), 80, 120, 18, "#FFF2B2"); _detailLine2Id = label("Closest pole distance in era: " + esep.toFixed(2) + "° | Magnitude: " + emag.toFixed(2), 80, 150, 18, "#F8FAFC"); highlightEpochLog(i); W(1.0 * pace); settleCurrentObjectLabel(eobjName, epochWinnerLabelText(erow), isSpecialName(displayEpochWinnerName(erow)) ? 13 : 12, "#D4AF37"); W(3.8 * pace); } for (var j = 0; j < STAR_BEST.length; j++) { var sp = paceFactor(j, STAR_BEST.length); var row = STAR_BEST[j]; var year = row[0]; var hip = row[3]; var sep = row[5]; var mag = row[6]; var objName = "HIP " + hip; hideLiveLabels(); core.setJDay(toJd(year)); W(0.2 * sp); core.moveToRaDec(0, 89.6, 0.4); W(0.2 * sp); try { core.selectObjectByName(objName, true); W(0.2 * sp); safeMarkerObject(objName, "circle", "#66CCFF", isSpecialName(displayName(row)) ? 15 : 13); _objectLabelId = safeLabelObject(starBestLabelText(row), objName, isSpecialName(displayName(row)) ? 18 : 17, "#BAE6FD"); } catch (err) { } StelMovementMgr.zoomTo(56, 1.0); W(0.4 * sp); StelMovementMgr.zoomTo(48, 1.0); _epochLabelId = label("Best epoch for star: " + epochStr(year), 80, 88, 22, "#93C5FD"); _sampleLabelId = label("Star candidate " + (j + 1) + " / " + STAR_BEST.length, Math.max(900, _rightLogX - 120), 48, 16, "#CBD5E1"); _detailLine1Id = label("Star: " + displayName(row), 80, 120, 18, "#AAFFAA"); _detailLine2Id = label("Closest pole distance: " + sep.toFixed(2) + "° | Magnitude: " + mag.toFixed(2), 80, 150, 18, "#F8FAFC"); highlightStarLog(j); W(1.0 * sp); settleCurrentObjectLabel(objName, starBestLabelText(row), isSpecialName(displayName(row)) ? 12 : 11, "#7DD3FC"); W(3.8 * sp); } hideLiveLabels(); core.moveToRaDec(0, 89.6, 0.8); StelMovementMgr.zoomTo(62, 2.0); label("Precession is the main idea.", 80, Math.max(_leftLogY, _rightLogY) + Math.max(EPOCH_BEST.length * (_leftLogLineH + 2), STAR_BEST.length * (_rightLogLineH + 2)) + 28, 28, "#FFD166"); label("Left: best star for epoch. Right: best epoch for star.", 80, Math.max(_leftLogY, _rightLogY) + Math.max(EPOCH_BEST.length * (_leftLogLineH + 2), STAR_BEST.length * (_rightLogLineH + 2)) + 66, 18, "#F8FAFC"); label("Astropy is efficient for the scan. Stellarium is clear for the visual story.", 80, Math.max(_leftLogY, _rightLogY) + Math.max(EPOCH_BEST.length * (_leftLogLineH + 2), STAR_BEST.length * (_rightLogLineH + 2)) + 96, 18, "#A7F3D0"); W(8);