feat(upstream): merge a6f47abd engine, MCP, and orchestrator
Three-way-merge calculation modules and pl9-export into the product fork while keeping commercial API routes, Raman ayanamsa, and the consultation contract as a keypath superset. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+247
-32
@@ -17,6 +17,9 @@ from datetime import datetime
|
||||
import math
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
|
||||
'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
|
||||
@@ -671,7 +674,7 @@ def calc_tajika_yogas(planet_lons: Dict[str, float],
|
||||
results['yogas'].append(yoga)
|
||||
|
||||
# ── 2. Easarapha Yoga(分离瑜伽)──
|
||||
# 定义:两行星同星座,快行星已离开慢行星(separate),度数差<=3度
|
||||
# 定义:两行星同星座,且 shared motion contract 判定为 separating
|
||||
for i, p1 in enumerate(Planet_LIST):
|
||||
if p1 not in planet_lons:
|
||||
continue
|
||||
@@ -686,28 +689,23 @@ def calc_tajika_yogas(planet_lons: Dict[str, float],
|
||||
|
||||
fast = p1 if _is_faster(p1, p2) else p2
|
||||
slow = p2 if fast == p1 else p1
|
||||
|
||||
fast_lon = planet_lons[fast]
|
||||
slow_lon = planet_lons[slow]
|
||||
# 分离:快行星已过慢行星(在慢行星"后面")
|
||||
# 即慢行星度数 > 快行星度数(在同一圈内)
|
||||
slow_in_sign = slow_lon % 30
|
||||
fast_in_sign = fast_lon % 30
|
||||
if slow_in_sign > fast_in_sign:
|
||||
# 慢在前面,快在后面 → 分离状态
|
||||
diff = slow_in_sign - fast_in_sign
|
||||
if diff <= 3.0:
|
||||
yoga = {
|
||||
'type': 'Easarapha',
|
||||
'fast_planet': fast,
|
||||
'slow_planet': slow,
|
||||
'degree_diff': round(diff, 4),
|
||||
'sign': SIGNS[sign1],
|
||||
'strength': 'strong' if diff <= 1.0 else 'moderate',
|
||||
'interpretation': _easarapha_interp(fast, slow, diff),
|
||||
}
|
||||
results['easarapha'].append(yoga)
|
||||
results['yogas'].append(yoga)
|
||||
shared_pair = evaluate_tajika_pair(p1, p2, motion_payload)
|
||||
if shared_pair is None or shared_pair.get('motion') != 'separating':
|
||||
continue
|
||||
diff = abs(float(shared_pair.get('residual', 0.0)))
|
||||
yoga = {
|
||||
'type': 'Easarapha',
|
||||
'fast_planet': fast,
|
||||
'slow_planet': slow,
|
||||
'degree_diff': round(diff, 4),
|
||||
'sign': SIGNS[sign1],
|
||||
'strength': 'strong' if diff <= 1.0 else 'moderate',
|
||||
'interpretation': _easarapha_interp(fast, slow, diff),
|
||||
'shared_motion': shared_pair.get('motion'),
|
||||
'shared_average_deeptamsa': shared_pair.get('average_deeptamsa'),
|
||||
}
|
||||
results['easarapha'].append(yoga)
|
||||
results['yogas'].append(yoga)
|
||||
|
||||
# ── 3. Nakta Yoga(夜间瑜伽)──
|
||||
# 定义:太阳和月亮同星座(任何度数)
|
||||
@@ -909,6 +907,206 @@ def _is_daytime(birth_dt: datetime, sun_lon: float, asc_lon: float) -> bool:
|
||||
return sun_house in (1, 2, 3, 4, 5, 6)
|
||||
|
||||
|
||||
def _year_lord_is_daytime(
|
||||
*,
|
||||
annual_dt: Optional[datetime],
|
||||
lat: Optional[float],
|
||||
lon: Optional[float],
|
||||
tz: Optional[float],
|
||||
sun_lon: float,
|
||||
asc_lon: float,
|
||||
) -> bool:
|
||||
if annual_dt is not None and lat is not None and lon is not None and tz is not None:
|
||||
try:
|
||||
from saham_daynight import determine_daytime
|
||||
return bool(determine_daytime(annual_dt, lat=float(lat), lon=float(lon), tz=float(tz))['is_daytime'])
|
||||
except Exception:
|
||||
pass
|
||||
if annual_dt is None:
|
||||
annual_dt = datetime.utcnow()
|
||||
return _is_daytime(annual_dt, sun_lon, asc_lon)
|
||||
|
||||
|
||||
def _benefic_aspect_targets(source_sign_idx: int) -> List[int]:
|
||||
source_sign_idx = int(source_sign_idx) % 12
|
||||
return [
|
||||
(source_sign_idx + 4) % 12,
|
||||
(source_sign_idx + 8) % 12,
|
||||
(source_sign_idx + 2) % 12,
|
||||
(source_sign_idx + 10) % 12,
|
||||
]
|
||||
|
||||
|
||||
def _malefic_aspect_targets(source_sign_idx: int) -> List[int]:
|
||||
source_sign_idx = int(source_sign_idx) % 12
|
||||
return [
|
||||
source_sign_idx,
|
||||
(source_sign_idx + 3) % 12,
|
||||
(source_sign_idx + 9) % 12,
|
||||
(source_sign_idx + 6) % 12,
|
||||
]
|
||||
|
||||
|
||||
def _resolve_year_lord_panchavargiya_score(
|
||||
planet: str,
|
||||
lon_value: float,
|
||||
*,
|
||||
annual_jd: Optional[float],
|
||||
lat: Optional[float],
|
||||
lon: Optional[float],
|
||||
tz: Optional[float],
|
||||
) -> Dict:
|
||||
external = _external_pyjhora_panchavargiya_score(
|
||||
planet,
|
||||
annual_jd=annual_jd,
|
||||
lat=lat,
|
||||
lon=lon,
|
||||
tz=tz,
|
||||
)
|
||||
if external is not None:
|
||||
return external
|
||||
proxy = _calc_panchavargiya_bala_for_planet(planet, lon_value)
|
||||
return {
|
||||
'score': proxy['score'],
|
||||
'engine': 'native_proxy',
|
||||
'components': proxy['components'],
|
||||
}
|
||||
|
||||
|
||||
def _external_pyjhora_panchavargiya_score(
|
||||
planet: str,
|
||||
*,
|
||||
annual_jd: Optional[float],
|
||||
lat: Optional[float],
|
||||
lon: Optional[float],
|
||||
tz: Optional[float],
|
||||
) -> Optional[Dict]:
|
||||
if annual_jd is None or lat is None or lon is None or tz is None:
|
||||
return None
|
||||
try:
|
||||
from jhora.panchanga import drik as j_drik
|
||||
from jhora.horoscope.chart import strength as j_strength
|
||||
place = j_drik.Place('annual_year_lord_probe', float(lat), float(lon), float(tz))
|
||||
scores = j_strength.pancha_vargeeya_bala(float(annual_jd), place)
|
||||
planet_idx = PLANET_TO_INDEX.get(planet)
|
||||
if planet_idx is None:
|
||||
return None
|
||||
score = scores[planet_idx]
|
||||
return {
|
||||
'score': float(score),
|
||||
'engine': 'pyjhora',
|
||||
'components': {'pyjhora_pancha_vargeeya_bala': float(score)},
|
||||
}
|
||||
except Exception:
|
||||
return _external_pyjhora_panchavargiya_score_sidecar(planet, annual_jd, lat, lon, tz)
|
||||
|
||||
|
||||
def _external_pyjhora_panchavargiya_score_sidecar(
|
||||
planet: str,
|
||||
annual_jd: float,
|
||||
lat: float,
|
||||
lon: float,
|
||||
tz: float,
|
||||
) -> Optional[Dict]:
|
||||
"""Read optional PyJHora Panchavargiya Bala scores via an external Python.
|
||||
|
||||
PyJHora stays an optional black-box oracle. We do not vendor it, import it
|
||||
as a hard dependency, or fail native execution when it is absent.
|
||||
"""
|
||||
scores = _external_pyjhora_panchavargiya_scores_sidecar(annual_jd, lat, lon, tz)
|
||||
if not scores or planet not in scores:
|
||||
return None
|
||||
score = scores[planet]
|
||||
return {
|
||||
'score': score,
|
||||
'engine': 'pyjhora_sidecar',
|
||||
'components': {
|
||||
'pyjhora_pancha_vargeeya_bala_sidecar': score,
|
||||
'scores_by_planet': scores,
|
||||
'license_boundary': 'external_reference_only_do_not_vendor',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _external_pyjhora_panchavargiya_scores_sidecar(
|
||||
annual_jd: float,
|
||||
lat: float,
|
||||
lon: float,
|
||||
tz: float,
|
||||
) -> Optional[Dict[str, float]]:
|
||||
key = (round(float(annual_jd), 8), round(float(lat), 6), round(float(lon), 6), round(float(tz), 4))
|
||||
if key in _PYJHORA_PVB_SIDECAR_CACHE:
|
||||
return _PYJHORA_PVB_SIDECAR_CACHE[key]
|
||||
executable = _resolve_external_pyjhora_python()
|
||||
if not executable:
|
||||
_PYJHORA_PVB_SIDECAR_CACHE[key] = None
|
||||
return None
|
||||
code = """
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
from jhora.panchanga import drik as j_drik
|
||||
from jhora.horoscope.chart import strength as j_strength
|
||||
place = j_drik.Place('annual_year_lord_probe', float(sys.argv[2]), float(sys.argv[3]), float(sys.argv[4]))
|
||||
scores = j_strength.pancha_vargeeya_bala(float(sys.argv[1]), place)
|
||||
print(json.dumps({str(k): float(v) for k, v in dict(scores).items()}))
|
||||
"""
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[executable, '-c', code, str(float(annual_jd)), str(float(lat)), str(float(lon)), str(float(tz))],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
)
|
||||
parsed = json.loads(completed.stdout)
|
||||
scores = {
|
||||
INDEX_TO_PLANET[int(idx)]: float(score)
|
||||
for idx, score in parsed.items()
|
||||
if int(idx) in INDEX_TO_PLANET
|
||||
}
|
||||
_PYJHORA_PVB_SIDECAR_CACHE[key] = scores or None
|
||||
return _PYJHORA_PVB_SIDECAR_CACHE[key]
|
||||
except Exception:
|
||||
_PYJHORA_PVB_SIDECAR_CACHE[key] = None
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_external_pyjhora_python() -> Optional[str]:
|
||||
candidates = [
|
||||
os.environ.get('JYOTISH_PYJHORA_PYTHON'),
|
||||
shutil.which('python3.14'),
|
||||
shutil.which('python3'),
|
||||
]
|
||||
current = os.path.abspath(sys.executable)
|
||||
for candidate in candidates:
|
||||
if not candidate:
|
||||
continue
|
||||
candidate = os.path.abspath(candidate)
|
||||
if not os.path.exists(candidate):
|
||||
continue
|
||||
# Do not compare realpaths here: a venv executable can resolve to the
|
||||
# managed Python binary while still having a different site-packages
|
||||
# view. The direct managed interpreter may have optional PyJHora even
|
||||
# when the current venv does not.
|
||||
if candidate == current:
|
||||
continue
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[candidate, '-c', "import importlib.util; raise SystemExit(0 if importlib.util.find_spec('jhora') else 1)"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=8,
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
if completed.returncode == 0:
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def calc_all_sahams(planet_lons: Dict[str, float],
|
||||
asc_lon: float,
|
||||
birth_dt: datetime,
|
||||
@@ -1239,10 +1437,30 @@ def detect_tajika_yogas(varsha_planets: Dict, year_lord: str = None) -> List[Dic
|
||||
"""计算两星间的有效容许度(取较小者)"""
|
||||
return min(TAJIKA_ORBS.get(p1, 7), TAJIKA_ORBS.get(p2, 7))
|
||||
|
||||
def _build_shared_motion_payload():
|
||||
try:
|
||||
from scripts.tajika_kernel import evaluate_tajika_pair
|
||||
except ModuleNotFoundError: # pragma: no cover
|
||||
from tajika_kernel import evaluate_tajika_pair
|
||||
|
||||
speeds = {
|
||||
'Moon': 13.176, 'Mercury': 4.092, 'Venus': 1.602,
|
||||
'Sun': 0.986, 'Mars': 0.524, 'Jupiter': 0.083, 'Saturn': 0.034,
|
||||
}
|
||||
payload = {
|
||||
planet: {
|
||||
'longitude': _get_longitude(planet),
|
||||
'speed': speeds.get(planet, 0.0),
|
||||
}
|
||||
for planet in SEVEN
|
||||
}
|
||||
return evaluate_tajika_pair, payload
|
||||
|
||||
# ── 1. Ithasala Yoga(连接瑜伽)完整版 ──
|
||||
# 条件:快星追赶慢星(applying),orb ≤ 有效容许度
|
||||
ithasala_pairs = []
|
||||
checked = set()
|
||||
evaluate_tajika_pair, motion_payload = _build_shared_motion_payload()
|
||||
for p1 in SEVEN:
|
||||
for p2 in SEVEN:
|
||||
if p1 >= p2:
|
||||
@@ -1261,17 +1479,14 @@ def detect_tajika_yogas(varsha_planets: Dict, year_lord: str = None) -> List[Dic
|
||||
|
||||
fast = p1 if _is_faster(p1, p2) else p2
|
||||
slow = p2 if fast == p1 else p1
|
||||
fast_lon = _get_longitude(fast)
|
||||
slow_lon = _get_longitude(slow)
|
||||
|
||||
# 判断是否applying(快追慢)
|
||||
# 快星度数 < 慢星度数(同一方向)= applying
|
||||
applying = (fast_lon % 30) < (slow_lon % 30)
|
||||
|
||||
if applying or orb <= 3.0: # 3°内视为紧密连接
|
||||
shared_pair = evaluate_tajika_pair(p1, p2, motion_payload)
|
||||
if shared_pair is not None and shared_pair.get('motion') == 'applying':
|
||||
ithasala_pairs.append({
|
||||
'fast': fast, 'slow': slow, 'orb': orb,
|
||||
'fast_lon': fast_lon, 'slow_lon': slow_lon,
|
||||
'fast_lon': _get_longitude(fast), 'slow_lon': _get_longitude(slow),
|
||||
'shared_motion': shared_pair.get('motion'),
|
||||
'shared_average_deeptamsa': shared_pair.get('average_deeptamsa'),
|
||||
'shared_residual': shared_pair.get('residual'),
|
||||
})
|
||||
|
||||
for pair in ithasala_pairs:
|
||||
|
||||
Reference in New Issue
Block a user