test_step1_ground_truth.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. """
  2. Step 1: Ground truth — compute houses/angles locally using swisseph directly.
  3. No server, no container, no network. Pure Python + Swiss Ephemeris.
  4. This gives us the reference values to compare all servers against.
  5. """
  6. from __future__ import annotations
  7. import sys
  8. import os
  9. # Add ephemeris-mcp to path so we can import swisseph from its venv
  10. sys.path.insert(0, os.path.expanduser("~/.openclaw/workspace/ephemeris-mcp/src"))
  11. import swisseph as swe
  12. # ── Reference birth data ──────────────────────────────────────────────
  13. PEOPLE = {
  14. "trump": {
  15. "label": "Donald Trump",
  16. "datetime_utc": "1946-06-14T14:54:00", # 10:54 AM EDT = 14:54 UTC
  17. "lat": 40.6413,
  18. "lon": -73.7781,
  19. },
  20. "einstein": {
  21. "label": "Albert Einstein",
  22. "datetime_utc": "1879-03-14T10:37:00", # 11:30 AM LMT (UTC+0:53) = 10:37 UTC
  23. "lat": 48.39841,
  24. "lon": 9.99155,
  25. },
  26. "chaka": {
  27. "label": "Chaka Khan",
  28. "datetime_utc": "1953-03-24T03:05:00", # 9:05 PM CST (UTC-6) = 03:05 UTC next day
  29. "lat": 41.87811,
  30. "lon": -87.62980,
  31. },
  32. }
  33. TOLERANCE = 0.01 # degrees — very tight
  34. def _parse_utc_dt(dt_str):
  35. from datetime import datetime
  36. return datetime.fromisoformat(dt_str)
  37. def _compute_houses(dt_utc, lat, lon, house_system=b"P"):
  38. jd = swe.julday(dt_utc.year, dt_utc.month, dt_utc.day,
  39. dt_utc.hour + dt_utc.minute / 60 + dt_utc.second / 3600)
  40. cusps, ascmc = swe.houses(jd, lat, lon, house_system)
  41. return {
  42. "jd": jd,
  43. "cusps": list(cusps),
  44. "asc": ascmc[0],
  45. "mc": ascmc[1],
  46. "dsc": ascmc[0] + 180.0 if ascmc[0] < 180.0 else ascmc[0] - 180.0,
  47. "ic": ascmc[1] + 180.0 if ascmc[1] < 180.0 else ascmc[1] - 180.0,
  48. }
  49. def _fmt(lon):
  50. signs = ["Ari", "Tau", "Gem", "Can", "Leo", "Vir",
  51. "Lib", "Sco", "Sag", "Cap", "Aqu", "Pis"]
  52. idx = int(lon // 30) % 12
  53. deg = lon % 30.0
  54. return f"{signs[idx]} {deg:05.2f}° (abs {lon:.6f}°)"
  55. def main():
  56. print("=" * 70)
  57. print("STEP 1: Ground truth — swisseph direct computation")
  58. print("=" * 70)
  59. results = {}
  60. for key, p in PEOPLE.items():
  61. dt = _parse_utc_dt(p["datetime_utc"])
  62. h = _compute_houses(dt, p["lat"], p["lon"])
  63. results[key] = h
  64. print(f"\n{p['label']}:")
  65. print(f" UTC: {p['datetime_utc']} lat={p['lat']} lon={p['lon']}")
  66. print(f" JD: {h['jd']:.6f}")
  67. print(f" ASC: {_fmt(h['asc'])}")
  68. print(f" MC: {_fmt(h['mc'])}")
  69. print(f" DSC: {_fmt(h['dsc'])}")
  70. print(f" IC: {_fmt(h['ic'])}")
  71. print(f" Cusps: ", end="")
  72. cusp_strs = [f"H{i+1}={_fmt(c)}" for i, c in enumerate(h["cusps"])]
  73. print(" | ".join(cusp_strs))
  74. # Print machine-readable summary for easy comparison
  75. print("\n" + "=" * 70)
  76. print("MACHINE-READABLE REFERENCE VALUES:")
  77. print("=" * 70)
  78. for key, p in PEOPLE.items():
  79. h = results[key]
  80. print(f"{key}: ASC={h['asc']:.6f} MC={h['mc']:.6f} "
  81. f"DSC={h['dsc']:.6f} IC={h['ic']:.6f} "
  82. f"JD={h['jd']:.6f}")
  83. return results
  84. if __name__ == "__main__":
  85. main()