Files
Jyotisha/scripts/cmd_muhurta.py
T
ClaudeandClaude Opus 5.5 afac4ea92f
Independent Staging Quality Gate / validate-frontend (push) Successful in 6m51s
Independent Staging Quality Gate / validate-python (push) Successful in 8m26s
Independent Staging Quality Gate / publish (push) Successful in 3m1s
fix(accept): #179 module moves into scripts/ondemand_calc (scripts-root budget)
test_python_hygiene_budget forbids new root-level scripts/*.py; the brief named
scripts/muhurta_search_api.py. Same code at scripts/ondemand_calc/muhurta_search.py;
route, CLI import and tests follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
2026-11-09 10:07:39 +08:00

309 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
cmd_muhurta.py v6.0.21 — muhurta 子命令实现
用法:
python jyotish_engine.py muhurta --date 2026-06-15 --activity marriage
python jyotish_engine.py muhurta --date 2026-06-15 (全部活动)
python jyotish_engine.py muhurta --scan-days 7 --activity business (扫描7天)
对于完整解盘(full-reading),也会附带当日 Muhurta 基本信息。
"""
from __future__ import annotations
from typing import Dict, Optional, List
from datetime import datetime, timedelta
import sys
import os
import logging
logger = logging.getLogger(__name__)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME, sidereal_flags
def _weekday_to_vara(py_weekday: int) -> int:
"""Python weekday (0=Mon) → Vara index (0=Sun)"""
return (py_weekday + 1) % 7
def _get_sun_moon_lons(year: int, month: int, day: int,
hour: int = 12, ayanamsa_name: str = DEFAULT_AYANAMSA_NAME) -> tuple:
"""获取太阳/月亮恒星黄经。优先 swisseph,退化为近似算法。"""
def _primary_longitude(value):
if isinstance(value, (list, tuple)):
return float(value[0])
return float(value)
try:
import swisseph as swe
jd_ut = swe.julday(year, month, day, hour)
flags = sidereal_flags(swe, ayanamsa_name)
sun_res = swe.calc_ut(jd_ut, swe.SUN, flags)
moon_res = swe.calc_ut(jd_ut, swe.MOON, flags)
return _primary_longitude(sun_res[0]), _primary_longitude(moon_res[0]), True
except Exception as exc:
logger.warning("cmd_muhurta._get_sun_moon_lons degraded: %s", type(exc).__name__)
logger.debug("cmd_muhurta._get_sun_moon_lons degraded", exc_info=True)
# 近似算法
from muhurta import _approx_sun_moon_lon
sun_lon, moon_lon = _approx_sun_moon_lon(year, month, day)
return sun_lon, moon_lon, False
def _format_panchanga_summary(panchanga: Dict) -> List[str]:
lines = []
p = panchanga
lines.append(f" Tithi: {p['tithi']['full_name']} ({p['tithi']['quality']})")
lines.append(f" Nakshatra: {p['nakshatra']['nakshatra']} [{p['nakshatra']['type']}] ({p['nakshatra']['quality']})")
lines.append(f" Yoga: {p['yoga']['yoga']} ({p['yoga']['quality']})")
lines.append(f" Karana: {p['karana']['karana']} ({p['karana']['quality']})")
lines.append(f" Vara: {p['vara']['vara']} / {p['vara']['vara_lord']} ({p['vara']['quality']})")
lines.append(f" Hora: {p['hora']['hora_lord']} ({p['hora']['quality']})")
lines.append(f" 综合评分: {p['overall_quality']} ({p['overall_score']:.0%},吉 {p['auspicious_count']}/{p['total_elements']})")
if p['warnings']:
for w in p['warnings']:
lines.append(f" {w}")
return lines
def _format_activity_checks(activity_checks: Dict, target_activity: Optional[str] = None) -> List[str]:
lines = []
ACTIVITY_NAMES_ZH = {
'marriage': '婚礼/伴侣(Vivaha)',
'business': '开业/签约(Vyapar)',
'travel': '出行(Yatra)',
'medical': '手术/医疗(Chikitsa)',
'education': '学习/入学(Vidyarambha)',
}
for act, chk in activity_checks.items():
if target_activity and act != target_activity:
continue
zh_name = ACTIVITY_NAMES_ZH.get(act, act)
verdict = chk.get('verdict', '未知')
icon = '✅' if '大吉' in verdict or ('吉' in verdict and '不宜' not in verdict) else \
'⚠️' if '一般' in verdict or '中' in verdict else '❌'
lines.append(f" {icon} {zh_name}: {verdict}")
if chk.get('notes'):
for note in chk['notes'][:3]:
lines.append(f" → {note}")
return lines
def _print_location_exact(
date_str: str,
activity: Optional[str],
lat: float,
lon: float,
tz: Optional[float],
timezone_id: Optional[str],
ayanamsa: Optional[str],
node_mode: Optional[str],
time_window: Optional[Dict],
limit: int,
) -> int:
"""给了地点:走 muhurta_search_api,按实际日出与逐时段五要素输出。"""
from ondemand_calc.muhurta_search import muhurta_search_api, MuhurtaSearchInputError
if not ayanamsa or not node_mode:
print("[ERROR] 给了地点就必须显式给 --ayanamsa 与 --node-mode(不要让默认值替用户选择)")
return 1
body = {
"activity": activity or "business",
"start_date": date_str,
"end_date": date_str,
"lat": lat,
"lon": lon,
"ayanamsa": ayanamsa,
"node_mode": node_mode,
"limit": limit,
}
if tz is not None:
body["tz"] = tz
if timezone_id:
body["timezone_id"] = timezone_id
if time_window:
body["time_window"] = time_window
try:
result = muhurta_search_api(body)
except MuhurtaSearchInputError as exc:
print(f"[ERROR] {exc}")
return 1
print("=" * 64)
print("🕐 Muhurta 择时分析(地点精确,实际日出与日落)")
print("=" * 64)
profile = result.get("profile") or {}
offset_text = profile.get('first_day_offset')
policy = profile.get('tz_policy') or 'fixed'
print(f"地点 {lat}, {lon}|时区策略 {policy}(首日偏移 {offset_text})"
f"|岁差 {profile.get('ayanamsa')}|交点 {profile.get('node_mode')}")
print(f"状态 {result.get('calculation_status')}|指纹 {result.get('input_fingerprint', '')[:16]}")
for day in (result.get("raw") or {}).get("days") or []:
print()
if day.get("calculation_status") != "executed":
print(f"📅 {day.get('date')}:阻塞({day.get('reason')}),没有可用日出,不出结论")
continue
print(f"📅 {day.get('date')} 日出 {day.get('sunrise_local')} 日落 {day.get('sunset_local')}")
windows = day.get("windows") or []
if not windows:
print(" 这一天没有候选时段")
for window in windows:
if window.get("calculation_status") != "executed":
print(f" · {window.get('name')} {window.get('start_local')}–{window.get('end_local')}:阻塞")
continue
start = window.get("elements_at_start") or {}
end = window.get("elements_at_end") or {}
print(f" · {window.get('name')} {window.get('start_local')}–{window.get('end_local')}"
f"(UTC {window.get('start_utc')}–{window.get('end_utc')})")
print(f" 起:Tithi {start.get('tithi_full_name')}|Nakshatra {start.get('nakshatra')}"
f" pada {start.get('nakshatra_pada')}|Yoga {start.get('yoga')}|Karana {start.get('karana')}"
f"|Hora {start.get('hora')}")
print(f" 止:Tithi {end.get('tithi_full_name')}|Nakshatra {end.get('nakshatra')}"
f" pada {end.get('nakshatra_pada')}|Yoga {end.get('yoga')}|Karana {end.get('karana')}"
f"|Hora {end.get('hora')}")
for field, change in (window.get("element_changes_within_window") or {}).items():
print(f" ⚠️ 时段内 {field} 变化:{change.get('from')} → {change.get('to')}(约 {change.get('at_local')})")
print(f" 判定:起 {window.get('verdict_at_start')}|止 {window.get('verdict_at_end')}"
f"|本时段 {window.get('window_verdict')}")
legacy = day.get("legacy_reference") or {}
if legacy:
print(f" 旧单点参考(不作最终判定):{legacy.get('quality')} {legacy.get('score')}")
gaps = result.get("gaps") or []
if gaps:
print()
print("缺口:")
for gap in gaps:
print(f" · {gap.get('field')}:{gap.get('reason')}")
print()
print("━" * 64)
print("口径:地点、时区、岁差、交点都由调用方显式给出;五要素在时段起止两端各取一次。")
print("━" * 64)
return 0
def cmd_muhurta(args, chart_data: Optional[Dict] = None) -> int:
"""
Muhurta 子命令主入口。
args 属性:
args.date: '2026-06-15'(查询日期,默认今天)
args.activity: 活动类型(可选,默认全部)
args.scan_days: 扫描天数(可选,>1 时扫描多天)
args.hour_from_sunrise: 从日出起算的小时(只用于无地点的 legacy 概览)
args.lat / args.lon / args.tz / args.timezone_id: 地点与时区(给了就做地点精确择时)
args.ayanamsa / args.node_mode: 口径(给了地点时必填)
"""
from muhurta import muhurta_full_report, ACTIVITY_RULES
if hasattr(args, 'date') and args.date:
try:
query_dt = datetime.strptime(args.date, '%Y-%m-%d')
except ValueError:
print(f"[ERROR] 日期格式错误: {args.date},请用 YYYY-MM-DD")
return 1
else:
query_dt = datetime.now()
hour_from_sunrise = getattr(args, 'hour_from_sunrise', 6.0)
target_activity = getattr(args, 'activity', None)
scan_days = getattr(args, 'scan_days', 1)
lat = getattr(args, 'lat', None)
lon = getattr(args, 'lon', None)
tz = getattr(args, 'tz', None)
timezone_id = getattr(args, 'timezone_id', None)
ayanamsa = getattr(args, 'ayanamsa', None)
node_mode = getattr(args, 'node_mode', None)
ayanamsa_name = ayanamsa or DEFAULT_AYANAMSA_NAME
activities = list(ACTIVITY_RULES.keys())
if target_activity and target_activity not in activities:
print(f"[ERROR] 未知活动: {target_activity}。支持: {', '.join(activities)}")
return 1
# 占星师 Q9 / 第五轮 9:有地点才做地点精确择时;没地点只给近似概览,不给择时结论。
if lat is not None and lon is not None and (tz is not None or timezone_id):
return _print_location_exact(
query_dt.strftime('%Y-%m-%d'),
target_activity,
float(lat),
float(lon),
None if tz is None else float(tz),
timezone_id,
ayanamsa,
node_mode,
None,
5,
)
print("⚠️ 未提供地点:结果为近似值,不能作为择时结论")
print("=" * 64)
print("🕐 Muhurta 五要素概览(无地点,近似)")
print("=" * 64)
dates_to_check = [query_dt + timedelta(days=i) for i in range(max(1, int(scan_days)))]
has_swe = True
for dt in dates_to_check:
# 原值:swe.julday(year, month, day, int(hour_from_sunrise + 6)),被当成 UT,等于 UT 12:00
# 新值:显式按 UT 12:00 取概览并写明是 UT,不再假装它是当地时间
# 原因:占星师 Q9 / 第五轮 9:择时须用地点、实际日出、时区
sun_lon, moon_lon, has_swe = _get_sun_moon_lons(
dt.year, dt.month, dt.day, 12.0,
ayanamsa_name=ayanamsa_name,
)
vara_idx = _weekday_to_vara(dt.weekday())
date_str = dt.strftime('%Y-%m-%d')
report = muhurta_full_report(
sun_lon=sun_lon,
moon_lon=moon_lon,
weekday=vara_idx,
hour_from_sunrise=hour_from_sunrise,
query_date_str=date_str,
activities=activities,
)
precision_note = '' if has_swe else '(近似值,±2°精度)'
print(f"\n📅 {date_str} UT 12:00 概览 {precision_note}")
print(f" Sun: {sun_lon:.1f}° Moon: {moon_lon:.1f}°")
print()
print("── Panchanga 五要素 ──")
for line in _format_panchanga_summary(report['panchanga']):
print(line)
print()
print("── Abhijit Muhurta ──")
abh = report['abhijit_muhurta']
print(f" {abh['description']}")
print(f" 持续: {abh['duration_minutes']} 分钟 | {abh['warning']}")
print()
print("── 活动适宜性 ──")
for line in _format_activity_checks(report['activity_checks'], target_activity):
print(line)
print()
summary = report['summary']
best = summary.get('best_activities', [])
avoid = summary.get('avoid_activities', [])
if best:
print(f" ✨ 适宜: {', '.join(best)}")
if avoid:
print(f" 🚫 不宜: {', '.join(avoid)}")
if len(dates_to_check) > 1:
print("-" * 40)
print()
print("━" * 64)
print("注意事项:")
print(" 1. 本概览没有地点,日出日落与五要素都不是当地实际值,不能作为择时结论")
print(" 2. 需要地点精确择时,请加 --lat --lon --tz(或 --timezone-id)--ayanamsa --node-mode")
print(" 3. 精确 Muhurta 还需考虑出生盘 Lagna 与目标时间的相位关系")
if not has_swe:
print(" 4. ⚠️ 当前使用近似算法(swisseph 未安装),精度约 ±2°")
print("━" * 64)
return 0