fix(rectification): 候选分钟不变量记忆化(BUG-721)
Shadbala / Ashtakavarga / Dasha 时间轴进 static context;过境盘按事件日期缓存 chart;探针网格相同时只算一次。打分与决策回执与基线 golden 逐字相同(剔除计时字段)。
This commit is contained in:
@@ -107,13 +107,15 @@ def _active_vimshottari(
|
||||
birth_date: str,
|
||||
moon_longitude: float,
|
||||
event_at: datetime,
|
||||
timeline: list[dict[str, Any]] | None = None,
|
||||
) -> tuple[str, str, str]:
|
||||
nakshatra, progress, _ = dasha_analyzer.lon_to_nakshatra(moon_longitude)
|
||||
timeline, _, _, _ = dasha_analyzer.build_dasha_timeline(
|
||||
birth_date,
|
||||
nakshatra,
|
||||
progress,
|
||||
)
|
||||
if timeline is None:
|
||||
nakshatra, progress, _ = dasha_analyzer.lon_to_nakshatra(moon_longitude)
|
||||
timeline, _, _, _ = dasha_analyzer.build_dasha_timeline(
|
||||
birth_date,
|
||||
nakshatra,
|
||||
progress,
|
||||
)
|
||||
_, major = dasha_analyzer.find_current(timeline, event_at)
|
||||
minor = dasha_analyzer.find_current_sub(
|
||||
dasha_analyzer.build_antardasha(major),
|
||||
@@ -131,11 +133,13 @@ def _active_narayana(
|
||||
planet_longitudes: dict[str, float],
|
||||
birth_at: datetime,
|
||||
event_at: datetime,
|
||||
periods: list[dict[str, Any]] | None = None,
|
||||
) -> tuple[int | None, int | None]:
|
||||
periods = narayana_dasha.calc_narayana_mahadasha(
|
||||
ascendant_index,
|
||||
planet_longitudes,
|
||||
)
|
||||
if periods is None:
|
||||
periods = narayana_dasha.calc_narayana_mahadasha(
|
||||
ascendant_index,
|
||||
planet_longitudes,
|
||||
)
|
||||
age = max((event_at - birth_at).total_seconds() / (365.2425 * 86_400), 0.0)
|
||||
active = narayana_dasha.get_current_narayana_dasha(periods, age)
|
||||
major = active.get("md") or {}
|
||||
@@ -280,23 +284,45 @@ def _score_event(
|
||||
}
|
||||
|
||||
|
||||
def _transit_chart_cache_key(
|
||||
request: RectificationEventRequest,
|
||||
event_at: datetime,
|
||||
) -> tuple[Any, ...]:
|
||||
return (
|
||||
event_at.date().isoformat(),
|
||||
float(request["lat"]),
|
||||
float(request["lon"]),
|
||||
float(request["tz"]),
|
||||
request.get("ayanamsa", AYANAMSA),
|
||||
request.get("node_mode", NODE_MODE),
|
||||
)
|
||||
|
||||
|
||||
def _controlled_transit_rules(
|
||||
request: RectificationEventRequest,
|
||||
event: LifeEvent,
|
||||
natal_ascendant_index: int,
|
||||
target_houses: tuple[int, ...],
|
||||
transit_chart_cache: dict[tuple[Any, ...], dict[str, Any]] | None = None,
|
||||
) -> list[str]:
|
||||
"""Use only Jupiter/Saturn and only day/month dated events as a weak check."""
|
||||
if event["precision"] == "year":
|
||||
return []
|
||||
event_at = _event_datetime(event)
|
||||
transit_chart = domain_calculation_service.compute_chart({
|
||||
payload = {
|
||||
"year": event_at.year, "month": event_at.month, "day": event_at.day,
|
||||
"hour": 12, "minute": 0, "lat": request["lat"], "lon": request["lon"],
|
||||
"tz": request["tz"],
|
||||
"ayanamsa": request.get("ayanamsa", AYANAMSA),
|
||||
"node_mode": request.get("node_mode", NODE_MODE),
|
||||
})
|
||||
}
|
||||
cache_key = _transit_chart_cache_key(request, event_at)
|
||||
if transit_chart_cache is not None and cache_key in transit_chart_cache:
|
||||
transit_chart = transit_chart_cache[cache_key]
|
||||
else:
|
||||
transit_chart = domain_calculation_service.compute_chart(payload)
|
||||
if transit_chart_cache is not None:
|
||||
transit_chart_cache[cache_key] = transit_chart
|
||||
rules: list[str] = []
|
||||
for planet in ("Jupiter", "Saturn"):
|
||||
item = transit_chart.get("planets", {}).get(planet) or {}
|
||||
@@ -305,9 +331,16 @@ def _controlled_transit_rules(
|
||||
return rules
|
||||
|
||||
|
||||
def _ashtakavarga_auxiliary(natal_chart: dict, ascendant_index: int, target_houses: tuple[int, ...]) -> tuple[list[str], float]:
|
||||
def _ashtakavarga_auxiliary(
|
||||
natal_chart: dict,
|
||||
ascendant_index: int,
|
||||
target_houses: tuple[int, ...],
|
||||
ashtakavarga_result: dict[str, Any] | None = None,
|
||||
) -> tuple[list[str], float]:
|
||||
"""Return a bounded SAV consistency adjustment, never a standalone trigger."""
|
||||
result = ashtakavarga.calc_ashtakavarga(natal_chart.get("planets", {}), ascendant_index)
|
||||
result = ashtakavarga_result
|
||||
if result is None:
|
||||
result = ashtakavarga.calc_ashtakavarga(natal_chart.get("planets", {}), ascendant_index)
|
||||
if not result.get("all_bav_valid") or not (result.get("sav") or {}).get("valid"):
|
||||
return [], 0.0
|
||||
house_scores = result.get("house_scores_full") or {}
|
||||
@@ -323,17 +356,24 @@ def _ashtakavarga_auxiliary(natal_chart: dict, ascendant_index: int, target_hous
|
||||
return [], 0.0
|
||||
|
||||
|
||||
def _shadbala_verified_components_auxiliary(natal_chart: dict, birth_hour: float, dasha_lords: tuple[str, str, str]) -> tuple[list[str], float]:
|
||||
def _shadbala_verified_components_auxiliary(
|
||||
natal_chart: dict,
|
||||
birth_hour: float,
|
||||
dasha_lords: tuple[str, str, str],
|
||||
shadbala_result: dict[str, Any] | None = None,
|
||||
) -> tuple[list[str], float]:
|
||||
"""Use only Sthana/Drik/Naisargika, whose oracle comparison is already matched."""
|
||||
planets = natal_chart.get("planets", {})
|
||||
sun = planets.get("Sun") or {}
|
||||
moon = planets.get("Moon") or {}
|
||||
if not isinstance(sun.get("lon"), (int, float)) or not isinstance(moon.get("lon"), (int, float)):
|
||||
return [], 0.0
|
||||
result = shadbala.calc_shadbala(
|
||||
planets, str(natal_chart["ascendant"].get("sign") or "Aries"), birth_hour,
|
||||
float(sun["lon"]), float(moon["lon"]),
|
||||
)
|
||||
result = shadbala_result
|
||||
if result is None:
|
||||
result = shadbala.calc_shadbala(
|
||||
planets, str(natal_chart["ascendant"].get("sign") or "Aries"), birth_hour,
|
||||
float(sun["lon"]), float(moon["lon"]),
|
||||
)
|
||||
values = {
|
||||
planet: float((row.get("sthana_bala") or {}).get("total", 0)) + float(row.get("drik_bala", 0)) + float(row.get("naisargika_bala", 0))
|
||||
for planet, row in (result.get("planets") or {}).items()
|
||||
@@ -490,6 +530,26 @@ def build_candidate_static_context(
|
||||
except (KeyError, TypeError, ValueError):
|
||||
blocked_layers.append("Shadbala")
|
||||
|
||||
vimshottari_timeline = None
|
||||
try:
|
||||
nakshatra, progress, _ = dasha_analyzer.lon_to_nakshatra(planet_longitudes["Moon"])
|
||||
vimshottari_timeline, _, _, _ = dasha_analyzer.build_dasha_timeline(
|
||||
candidate_at.date().isoformat(),
|
||||
nakshatra,
|
||||
progress,
|
||||
)
|
||||
except (KeyError, TypeError, ValueError):
|
||||
vimshottari_timeline = None
|
||||
|
||||
narayana_periods = None
|
||||
try:
|
||||
narayana_periods = narayana_dasha.calc_narayana_mahadasha(
|
||||
ascendant_index,
|
||||
planet_longitudes,
|
||||
)
|
||||
except (KeyError, TypeError, ValueError):
|
||||
narayana_periods = None
|
||||
|
||||
birth_info = chart.get("birth_info") if isinstance(chart.get("birth_info"), dict) else {}
|
||||
kp_snapshot = observe_kp_cusps(
|
||||
birth_info.get("julian_day"),
|
||||
@@ -538,6 +598,10 @@ def build_candidate_static_context(
|
||||
"arudha_padas": arudha_padas,
|
||||
"varga_charts": varga_charts,
|
||||
"feature": feature_payload,
|
||||
"ashtakavarga_result": ashtakavarga_result,
|
||||
"shadbala_result": shadbala_result,
|
||||
"vimshottari_timeline": vimshottari_timeline,
|
||||
"narayana_periods": narayana_periods,
|
||||
}
|
||||
|
||||
|
||||
@@ -572,7 +636,12 @@ def _candidate_row(
|
||||
missing_layers.extend(prefixes)
|
||||
continue
|
||||
try:
|
||||
vimshottari = _active_vimshottari(candidate_at.date().isoformat(), moon_longitude, event_at)
|
||||
vimshottari = _active_vimshottari(
|
||||
candidate_at.date().isoformat(),
|
||||
moon_longitude,
|
||||
event_at,
|
||||
context.get("vimshottari_timeline"),
|
||||
)
|
||||
except (KeyError, TypeError, ValueError):
|
||||
missing_layers.append("Vimshottari_MD_AD_PD")
|
||||
continue
|
||||
@@ -582,6 +651,7 @@ def _candidate_row(
|
||||
planet_longitudes,
|
||||
candidate_at,
|
||||
event_at,
|
||||
context.get("narayana_periods"),
|
||||
)
|
||||
except (KeyError, TypeError, ValueError):
|
||||
missing_layers.append("Narayana_MD_AD")
|
||||
@@ -598,16 +668,30 @@ def _candidate_row(
|
||||
narayana=narayana,
|
||||
arudha_padas=arudha_padas,
|
||||
))
|
||||
transit_rules = _controlled_transit_rules(request, event, ascendant_index, DOMAIN_CONFIG[event["domain"]][1])
|
||||
transit_rules = _controlled_transit_rules(
|
||||
request,
|
||||
event,
|
||||
ascendant_index,
|
||||
DOMAIN_CONFIG[event["domain"]][1],
|
||||
context.get("_transit_chart_cache"),
|
||||
)
|
||||
if transit_rules:
|
||||
evidence[-1]["rule_ids"].extend(transit_rules)
|
||||
evidence[-1]["points"] = round(evidence[-1]["points"] + 0.25 * len(transit_rules) * precision_weight(event["precision"]), 4)
|
||||
av_rules, av_points = _ashtakavarga_auxiliary(chart, ascendant_index, DOMAIN_CONFIG[event["domain"]][1])
|
||||
av_rules, av_points = _ashtakavarga_auxiliary(
|
||||
chart,
|
||||
ascendant_index,
|
||||
DOMAIN_CONFIG[event["domain"]][1],
|
||||
context.get("ashtakavarga_result"),
|
||||
)
|
||||
if av_rules:
|
||||
evidence[-1]["rule_ids"].extend(av_rules)
|
||||
evidence[-1]["points"] = round(evidence[-1]["points"] + av_points * precision_weight(event["precision"]), 4)
|
||||
shadbala_rules, shadbala_points = _shadbala_verified_components_auxiliary(
|
||||
chart, candidate_at.hour + candidate_at.minute / 60, vimshottari,
|
||||
chart,
|
||||
candidate_at.hour + candidate_at.minute / 60,
|
||||
vimshottari,
|
||||
context.get("shadbala_result"),
|
||||
)
|
||||
if shadbala_rules:
|
||||
evidence[-1]["rule_ids"].extend(shadbala_rules)
|
||||
@@ -719,4 +803,8 @@ def compute_event_candidate_rows(
|
||||
) -> list[CandidateScoreRow]:
|
||||
"""Return every computed minute row while reusing one static chart scan per candidate."""
|
||||
contexts = list(static_contexts) if static_contexts is not None else compute_candidate_static_contexts(request, candidates=candidates)
|
||||
return [_candidate_row(request, context) for context in contexts]
|
||||
transit_chart_cache: dict[tuple[Any, ...], dict[str, Any]] = {}
|
||||
return [
|
||||
_candidate_row(request, {**context, "_transit_chart_cache": transit_chart_cache})
|
||||
for context in contexts
|
||||
]
|
||||
|
||||
@@ -697,7 +697,11 @@ def build_refinement_packet(
|
||||
"unique_minute_claim": False,
|
||||
"confirmation_allowed": False,
|
||||
}
|
||||
from scripts.rectification.candidate_contrast import PROBE_PHASE_HOLDOUT_VALIDATION, event_year
|
||||
from scripts.rectification.candidate_contrast import (
|
||||
PROBE_PHASE_HOLDOUT_VALIDATION,
|
||||
event_year,
|
||||
opportunity_from_probe,
|
||||
)
|
||||
from scripts.rectification.case_holdout import reserved_holdout_events
|
||||
from scripts.rectification.event_probes import (
|
||||
candidate_contrast_opportunities,
|
||||
@@ -729,13 +733,16 @@ def build_refinement_packet(
|
||||
dropped = list(bundle["dropped"])
|
||||
clarification = event_clarification_probes(request)
|
||||
collection = evidence_collection_probes(request)
|
||||
opportunities = candidate_contrast_opportunities(
|
||||
request,
|
||||
built,
|
||||
scan=scan,
|
||||
candidate_times=grid_times,
|
||||
representative_time=representative_time,
|
||||
)
|
||||
if probe_times == grid_times:
|
||||
opportunities = [opportunity_from_probe(probe) for probe in probes]
|
||||
else:
|
||||
opportunities = candidate_contrast_opportunities(
|
||||
request,
|
||||
built,
|
||||
scan=scan,
|
||||
candidate_times=grid_times,
|
||||
representative_time=representative_time,
|
||||
)
|
||||
reserved = reserved_holdout_events(request.get("events") or [])
|
||||
holdout = [
|
||||
{
|
||||
|
||||
@@ -5,7 +5,6 @@ import json
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Sequence
|
||||
from datetime import date, timedelta
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
from scripts.active_rectification_event_engine import compute_candidate_static_contexts, compute_event_candidate_rows
|
||||
@@ -126,11 +125,6 @@ def _canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
@lru_cache(maxsize=4096)
|
||||
def _cached_rows(serialized: str) -> tuple[CandidateScoreRow, ...]:
|
||||
return tuple(compute_event_candidate_rows(json.loads(serialized)))
|
||||
|
||||
|
||||
_SUPPORT_RULES = (
|
||||
"functional_benefic_auxiliary",
|
||||
"arudha_auxiliary",
|
||||
|
||||
Reference in New Issue
Block a user