fix(rectification): restore classic eight methods and observe KP Placidus cusps
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -44,6 +44,7 @@ import saham_daynight # noqa: E402
|
||||
import special_lagnas # noqa: E402
|
||||
import varga # noqa: E402
|
||||
from scripts.rectification.refinement_packet import NAKSHATRA_SPAN # noqa: E402
|
||||
from scripts.rectification.kp_cusp_observation import observe_kp_cusps # noqa: E402
|
||||
|
||||
AYANAMSA: Final = "lahiri"
|
||||
NODE_MODE: Final = "mean"
|
||||
@@ -61,9 +62,12 @@ DOMAIN_CONFIG: Final[dict[EventDomain, DomainConfig]] = {
|
||||
"family": (("D12", "D7", "D3"), (3, 4, 5, 9)),
|
||||
# Dated appearance/marks: D1 lagna / 1st house only. Not a primary formula.
|
||||
"appearance": ((), (1,)),
|
||||
# Occupation notes: D10 + D1 10th house, auxiliary, no type labels.
|
||||
"occupation": (("D10",), (10,)),
|
||||
}
|
||||
AUXILIARY_DOMAINS: Final[frozenset[str]] = frozenset({"appearance"})
|
||||
AUXILIARY_DOMAINS: Final[frozenset[str]] = frozenset({"appearance", "occupation"})
|
||||
AUXILIARY_SCORE_FACTOR: Final = 0.4
|
||||
OBSERVATION_ONLY_LAYERS: Final[frozenset[str]] = frozenset({"KP_cusps"})
|
||||
|
||||
|
||||
class RectificationEventCalculationError(RuntimeError):
|
||||
@@ -234,7 +238,11 @@ def _score_event(
|
||||
if sign_index is not None and _relative_house(sign_index, ascendant_index) in target_houses:
|
||||
rules.append(f"{label}_domain_house")
|
||||
points += weight
|
||||
arudha_keys = ("A7", "UL") if event["domain"] == "relationship" else ("A10",) if event["domain"] == "career" else ()
|
||||
arudha_keys = (
|
||||
("A7", "UL") if event["domain"] == "relationship"
|
||||
else ("A10",) if event["domain"] in {"career", "occupation"}
|
||||
else ()
|
||||
)
|
||||
arudha_signs = {
|
||||
value.get("sign_idx") for key in arudha_keys
|
||||
if isinstance((value := arudha_padas.get(key)), dict) and isinstance(value.get("sign_idx"), int)
|
||||
@@ -249,7 +257,11 @@ def _score_event(
|
||||
event_kind = event.get("event_kind", event["domain"])
|
||||
if event["domain"] in AUXILIARY_DOMAINS:
|
||||
points *= AUXILIARY_SCORE_FACTOR
|
||||
rules.append("appearance_auxiliary_not_primary")
|
||||
rules.append(
|
||||
"occupation_auxiliary_not_primary"
|
||||
if event["domain"] == "occupation"
|
||||
else "appearance_auxiliary_not_primary"
|
||||
)
|
||||
if not rules:
|
||||
rules.append("no_domain_activation")
|
||||
rules.append(f"event_kind:{event_kind}")
|
||||
@@ -435,7 +447,7 @@ def build_candidate_static_context(
|
||||
for prefix in ("D2", "D3", "D4", "D5", "D7", "D9", "D10", "D11", "D12", "D24", "D30")
|
||||
}
|
||||
available_layers = ["D1"]
|
||||
blocked_layers = ["KP_cusps"]
|
||||
blocked_layers: list[str] = []
|
||||
varga_ascendants: dict[str, int] = {}
|
||||
for prefix, value in varga_charts.items():
|
||||
ascendant = (value or {}).get("Ascendant") or {}
|
||||
@@ -471,6 +483,17 @@ def build_candidate_static_context(
|
||||
except (KeyError, TypeError, ValueError):
|
||||
blocked_layers.append("Shadbala")
|
||||
|
||||
birth_info = chart.get("birth_info") if isinstance(chart.get("birth_info"), dict) else {}
|
||||
kp_snapshot = observe_kp_cusps(
|
||||
birth_info.get("julian_day"),
|
||||
float(request["lat"]),
|
||||
float(request["lon"]),
|
||||
)
|
||||
if kp_snapshot.get("status") == "executed":
|
||||
available_layers.append("KP_cusps")
|
||||
else:
|
||||
blocked_layers.append("KP_cusps")
|
||||
|
||||
feature_payload = {
|
||||
"time": candidate_at.strftime("%H:%M"),
|
||||
"ascendant_degree": ascendant_longitude,
|
||||
@@ -484,6 +507,8 @@ def build_candidate_static_context(
|
||||
sun_degree=planet_longitudes.get("Sun") if isinstance(planet_longitudes.get("Sun"), (int, float)) else None,
|
||||
moon_degree=planet_longitudes.get("Moon") if isinstance(planet_longitudes.get("Moon"), (int, float)) else None,
|
||||
),
|
||||
**(kp_snapshot.get("indices") or {}),
|
||||
"kp_cusps": kp_snapshot,
|
||||
"varga_ascendants": varga_ascendants,
|
||||
"arudha_signs": arudha_signs,
|
||||
"available_layers": sorted(set(available_layers)),
|
||||
@@ -585,7 +610,10 @@ def _candidate_row(
|
||||
"time": candidate_at.strftime("%H:%M"),
|
||||
"score": round(sum(item["points"] for item in evidence), 4),
|
||||
"evidence": evidence,
|
||||
"missing_layers": sorted(set(missing_layers + context["feature"]["blocked_layers"])),
|
||||
"missing_layers": sorted(set(
|
||||
missing_layers
|
||||
+ [layer for layer in context["feature"]["blocked_layers"] if layer not in OBSERVATION_ONLY_LAYERS]
|
||||
)),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ EventDomain = Literal[
|
||||
"health_pressure",
|
||||
"family",
|
||||
"appearance",
|
||||
"occupation",
|
||||
]
|
||||
Confidence = Literal["low", "medium", "high"]
|
||||
|
||||
|
||||
@@ -19,6 +19,13 @@ from sade_sati import calc_sade_sati_complete
|
||||
|
||||
CONTRACT_VERSION = "1.0.0"
|
||||
_SWISSEPH_LOCK = threading.RLock()
|
||||
|
||||
|
||||
def swiss_ephemeris_lock() -> threading.RLock:
|
||||
"""Shared Swiss Ephemeris lock. Sidereal mode is process-global."""
|
||||
return _SWISSEPH_LOCK
|
||||
|
||||
|
||||
_PLANET_IDS = {
|
||||
"Jupiter": swe.JUPITER,
|
||||
"Saturn": swe.SATURN,
|
||||
|
||||
@@ -29,10 +29,12 @@ EVENT_KINDS: dict[str, frozenset[str]] = {
|
||||
"family": frozenset({"family_event"}),
|
||||
"appearance": frozenset({"appearance_note"}),
|
||||
"marks": frozenset({"birthmark_or_scar"}),
|
||||
"occupation": frozenset({"occupation_note"}),
|
||||
"horary": frozenset({"horary_query"}),
|
||||
"other": frozenset({"other"}),
|
||||
}
|
||||
BACKGROUND_EVENT_KINDS = frozenset({"other"})
|
||||
AUXILIARY_EVENT_KINDS = frozenset({"appearance_note", "birthmark_or_scar"})
|
||||
BACKGROUND_EVENT_KINDS = frozenset({"other", "horary_query"})
|
||||
AUXILIARY_EVENT_KINDS = frozenset({"appearance_note", "birthmark_or_scar", "occupation_note"})
|
||||
SCOREABLE_EVENT_KINDS: dict[str, frozenset[str]] = {
|
||||
domain: frozenset(kind for kind in kinds if kind not in BACKGROUND_EVENT_KINDS)
|
||||
for domain, kinds in EVENT_KINDS.items()
|
||||
|
||||
@@ -13,6 +13,7 @@ from scripts.rectification.contracts import (
|
||||
is_scoreable_event,
|
||||
)
|
||||
from scripts.rectification.house_table import compact_house_table_from_contexts
|
||||
from scripts.rectification.horary_observation import build_horary_observation
|
||||
from scripts.rectification.refinement_packet import build_refinement_packet
|
||||
from scripts.rectification.scoring_service import precision_weight
|
||||
from scripts.rectification.sealed_holdout import holdout_passed, load_sealed_minute_holdout
|
||||
@@ -269,11 +270,7 @@ def build_technique_audit(
|
||||
continue
|
||||
label, note = _AUDIT_LABELS[method]
|
||||
rows.append({"technique": label, "status": "executed", "note": note})
|
||||
rows.append({
|
||||
"technique": "KP 宫头",
|
||||
"status": "blocked",
|
||||
"note": "KP 宫头政策跳过,不参与提出门或确认门。",
|
||||
})
|
||||
rows.append(_kp_audit_row(built))
|
||||
rows.extend((
|
||||
vedastro_audit_row(vedastro_status),
|
||||
unique_minute_audit_row(confirmation_allowed),
|
||||
@@ -281,6 +278,29 @@ def build_technique_audit(
|
||||
return rows
|
||||
|
||||
|
||||
def _kp_audit_row(built: dict[str, Any]) -> dict[str, str]:
|
||||
executed = False
|
||||
for context in built.get("static_contexts") or []:
|
||||
if not isinstance(context, dict):
|
||||
continue
|
||||
feature = context.get("feature")
|
||||
snapshot = feature.get("kp_cusps") if isinstance(feature, dict) else None
|
||||
if isinstance(snapshot, dict) and snapshot.get("status") == "executed":
|
||||
executed = True
|
||||
break
|
||||
if executed:
|
||||
return {
|
||||
"technique": "KP 宫头",
|
||||
"status": "executed",
|
||||
"note": "已按 Swiss Ephemeris Placidus + Krishnamurti 观察 12 宫头;不计分,不参与提出门或确认门。",
|
||||
}
|
||||
return {
|
||||
"technique": "KP 宫头",
|
||||
"status": "blocked",
|
||||
"note": "Swiss Ephemeris Placidus 宫头无法计算或尚未执行。KP 观察不计分,不挡提出门。",
|
||||
}
|
||||
|
||||
|
||||
def _quantized_score(row: CandidateScoreRow) -> Decimal:
|
||||
return _decimal(row.get("score")).quantize(SCORE_QUANTUM, rounding=ROUND_HALF_UP)
|
||||
|
||||
@@ -581,6 +601,7 @@ def build_decision_receipt(
|
||||
"nakshatra_boundary": packet["nakshatra_boundary"],
|
||||
"precision_stage": packet["precision_stage"],
|
||||
"oos_blind_prompts": packet["oos_blind_prompts"],
|
||||
"horary_observation": build_horary_observation(request),
|
||||
"unique_minute_claim": False,
|
||||
})
|
||||
return apply_confirmation_decision(receipt)
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Display-only Horary recast for birth-time rectification.
|
||||
|
||||
If the user gives the time they first asked the question, recast a natal-like
|
||||
chart at that local datetime. Never score it. Never block propose or confirm.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from scripts.rectification.house_table import SIGNS_CN
|
||||
from scripts.rectification.kp_cusp_observation import observe_kp_cusps
|
||||
|
||||
_CLOCK = re.compile(r"(?:[01]?\d|2[0-3]):[0-5]\d")
|
||||
|
||||
|
||||
def _question_clock(event: dict[str, Any]) -> str:
|
||||
summary = str(event.get("summary") or "")
|
||||
match = _CLOCK.search(summary)
|
||||
if match:
|
||||
hour, minute = match.group(0).split(":")
|
||||
return f"{int(hour):02d}:{int(minute):02d}"
|
||||
return "12:00"
|
||||
|
||||
|
||||
def build_horary_observation(request: dict[str, Any]) -> dict[str, Any]:
|
||||
events = [
|
||||
event for event in (request.get("events") or [])
|
||||
if isinstance(event, dict) and event.get("event_kind") == "horary_query"
|
||||
]
|
||||
if not events:
|
||||
return {
|
||||
"status": "skipped",
|
||||
"user_meaning": "没有第一次问起这件事的时间,占问观察未执行。",
|
||||
"unique_minute_claim": False,
|
||||
}
|
||||
try:
|
||||
lat = float(request["lat"])
|
||||
lon = float(request["lon"])
|
||||
tz = float(request["tz"])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
return {
|
||||
"status": "blocked",
|
||||
"user_meaning": "占问时间已记录,但观察盘未能重算。不计分,不挡提出门。",
|
||||
"unique_minute_claim": False,
|
||||
}
|
||||
event = events[0]
|
||||
date_text = str(event.get("date_start") or "")[:10]
|
||||
clock = _question_clock(event)
|
||||
try:
|
||||
year, month, day = (int(part) for part in date_text.split("-"))
|
||||
hour, minute = (int(part) for part in clock.split(":"))
|
||||
import domain_calculation_service
|
||||
chart = domain_calculation_service.compute_chart({
|
||||
"year": year,
|
||||
"month": month,
|
||||
"day": day,
|
||||
"hour": hour,
|
||||
"minute": minute,
|
||||
"lat": float(request["lat"]),
|
||||
"lon": float(request["lon"]),
|
||||
"tz": float(request["tz"]),
|
||||
"ayanamsa": "lahiri",
|
||||
"node_mode": "mean",
|
||||
})
|
||||
asc = chart.get("ascendant") or {}
|
||||
sign = str(asc.get("sign") or "")
|
||||
lagna = SIGNS_CN.get(sign, sign)
|
||||
birth_info = chart.get("birth_info") if isinstance(chart.get("birth_info"), dict) else {}
|
||||
kp = observe_kp_cusps(birth_info.get("julian_day"), float(request["lat"]), float(request["lon"]))
|
||||
angles = {}
|
||||
if kp.get("status") == "executed":
|
||||
for house in ("1", "4", "7", "10"):
|
||||
row = (kp.get("houses") or {}).get(house) or {}
|
||||
angles[house] = {
|
||||
"sub_lord": row.get("sub_lord"),
|
||||
"nakshatra_lord": row.get("nakshatra_lord"),
|
||||
}
|
||||
return {
|
||||
"status": "executed",
|
||||
"question_date": date_text,
|
||||
"question_time": clock,
|
||||
"lagna": lagna if lagna else None,
|
||||
"kp_angles": angles,
|
||||
"user_meaning": (
|
||||
f"已按问起时间 {date_text} {clock} 重算占问盘作观察,不计分,不确认唯一分钟。"
|
||||
),
|
||||
"unique_minute_claim": False,
|
||||
}
|
||||
except (TypeError, ValueError, KeyError, OverflowError, OSError):
|
||||
return {
|
||||
"status": "blocked",
|
||||
"question_date": date_text,
|
||||
"question_time": clock,
|
||||
"user_meaning": "占问时间已记录,但观察盘未能重算。不计分,不挡提出门。",
|
||||
"unique_minute_claim": False,
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Display-only KP Placidus cusp observation for birth-time rectification.
|
||||
|
||||
This lane uses Swiss Ephemeris Placidus house cusps and Krishnamurti ayanamsa.
|
||||
It must never mix into ranking, propose, or unique-minute confirmation.
|
||||
Whole-sign midpoints are not a substitute.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ayanamsa_utils import apply_ayanamsa, current_ayanamsa_name
|
||||
from kp_system import KP_LORDS, get_kp_lords
|
||||
from scripts.domain_calculation_service import swiss_ephemeris_lock
|
||||
|
||||
SCAN_HOUSES = (1, 4, 7, 10)
|
||||
_BLOCKED = {
|
||||
"status": "blocked",
|
||||
"house_system": "placidus",
|
||||
"ayanamsa": "krishnamurti",
|
||||
"houses": {},
|
||||
"indices": {},
|
||||
"note": "Swiss Ephemeris Placidus 宫头无法计算,KP 观察未执行。不计分,不挡提出门。",
|
||||
}
|
||||
|
||||
|
||||
def _cusp_row(degree: float) -> dict[str, Any]:
|
||||
lords = get_kp_lords(degree)
|
||||
sub_lord = str(lords.get("sub_lord") or "")
|
||||
try:
|
||||
sub_index = KP_LORDS.index(sub_lord)
|
||||
except ValueError:
|
||||
sub_index = None
|
||||
return {
|
||||
"cusp_degree": round(float(degree) % 360.0, 4),
|
||||
"sign": lords.get("sign"),
|
||||
"rasi_lord": lords.get("rasi_lord"),
|
||||
"nakshatra": lords.get("nakshatra"),
|
||||
"nakshatra_lord": lords.get("nakshatra_lord"),
|
||||
"pada": lords.get("pada"),
|
||||
"sub_lord": sub_lord,
|
||||
"sub_sub_lord": lords.get("sub_sub_lord"),
|
||||
"sub_index": sub_index,
|
||||
}
|
||||
|
||||
|
||||
def observe_kp_cusps(jd: float | None, lat: float | None, lon: float | None) -> dict[str, Any]:
|
||||
"""Return Placidus + Krishnamurti 12-cusp snapshot, or a blocked observation."""
|
||||
if not isinstance(jd, (int, float)) or not isinstance(lat, (int, float)) or not isinstance(lon, (int, float)):
|
||||
return dict(_BLOCKED)
|
||||
if abs(float(lat)) >= 66.0:
|
||||
return dict(_BLOCKED)
|
||||
previous = current_ayanamsa_name()
|
||||
try:
|
||||
import swisseph as swe
|
||||
except ImportError:
|
||||
return dict(_BLOCKED)
|
||||
with swiss_ephemeris_lock():
|
||||
try:
|
||||
apply_ayanamsa("krishnamurti", swe)
|
||||
cusps_raw, _ascmc = swe.houses(float(jd), float(lat), float(lon), b"P")
|
||||
ayanamsa = float(swe.get_ayanamsa(float(jd)))
|
||||
houses: dict[str, dict[str, Any]] = {}
|
||||
indices: dict[str, int] = {}
|
||||
for house in range(1, 13):
|
||||
tropical = float(cusps_raw[house - 1])
|
||||
if tropical != tropical or abs(tropical) > 720: # NaN / absurd
|
||||
return dict(_BLOCKED)
|
||||
sidereal = (tropical - ayanamsa) % 360.0
|
||||
row = _cusp_row(sidereal)
|
||||
houses[str(house)] = row
|
||||
if house in SCAN_HOUSES and isinstance(row["sub_index"], int):
|
||||
indices[f"kp{house}_sub_index"] = row["sub_index"]
|
||||
if len(houses) != 12 or len(indices) != 4:
|
||||
return dict(_BLOCKED)
|
||||
return {
|
||||
"status": "executed",
|
||||
"house_system": "placidus",
|
||||
"ayanamsa": "krishnamurti",
|
||||
"houses": houses,
|
||||
"indices": indices,
|
||||
"note": "已按 Swiss Ephemeris Placidus + Krishnamurti 观察 12 宫头;不计分,不参与提出门或确认门。",
|
||||
}
|
||||
except (TypeError, ValueError, OverflowError, OSError, RuntimeError, ArithmeticError):
|
||||
return dict(_BLOCKED)
|
||||
finally:
|
||||
apply_ayanamsa(previous or "lahiri", swe)
|
||||
@@ -130,6 +130,10 @@ _LAYER_LABEL = {
|
||||
"ghati": "Ghati Lagna",
|
||||
"bhava": "Bhava Lagna",
|
||||
"pranapada": "Pranapada Lagna",
|
||||
"kp1": "KP 1宫子主",
|
||||
"kp4": "KP 4宫子主",
|
||||
"kp7": "KP 7宫子主",
|
||||
"kp10": "KP 10宫子主",
|
||||
}
|
||||
|
||||
|
||||
@@ -152,12 +156,16 @@ def _scan_layer_value(feature: dict[str, Any], layer: str) -> int | None:
|
||||
"ghati": feature.get("ghati_sign_index"),
|
||||
"bhava": feature.get("bhava_sign_index"),
|
||||
"pranapada": feature.get("pranapada_sign_index"),
|
||||
"kp1": feature.get("kp1_sub_index"),
|
||||
"kp4": feature.get("kp4_sub_index"),
|
||||
"kp7": feature.get("kp7_sub_index"),
|
||||
"kp10": feature.get("kp10_sub_index"),
|
||||
}.get(layer)
|
||||
return raw if isinstance(raw, int) else None
|
||||
|
||||
|
||||
def window_scan(built: dict[str, Any]) -> dict[str, Any]:
|
||||
"""D1/D9/D10/D4/D5/D7/D12/D24/D2/D11/D30 plus display-only pada/Hora/Ghati/Bhava/Pranapada."""
|
||||
"""D1/D9/D10/D4/D5/D7/D12/D24/D2/D11/D30 plus display-only pada/Hora/Ghati/Bhava/Pranapada/KP."""
|
||||
counts: dict[str, set[int]] = {layer: set() for layer in _LAYER_LABEL}
|
||||
transitions: list[dict[str, Any]] = []
|
||||
previous: dict[str, int | None] | None = None
|
||||
|
||||
@@ -12,7 +12,7 @@ from scripts.active_rectification_event_engine import compute_candidate_static_c
|
||||
from scripts.active_rectification_events import CandidateScoreRow
|
||||
from scripts.rectification.contracts import LifeEvent, RectificationRequest, is_scoreable_event
|
||||
|
||||
ALGORITHM_VERSION = "rectification-v5-matrix-scoring-5"
|
||||
ALGORITHM_VERSION = "rectification-v5-matrix-scoring-6"
|
||||
INPUT_CONTRACT_VERSION = "rectification-calculation-spec-v4"
|
||||
PRECISION_WEIGHTS = {
|
||||
"day": 1.0,
|
||||
@@ -53,6 +53,7 @@ _ENGINE_KIND_BY_NATIVE_KIND: dict[str, tuple[str, str]] = {
|
||||
"family_event": ("family", "family_event"),
|
||||
"appearance_note": ("appearance", "appearance_note"),
|
||||
"birthmark_or_scar": ("appearance", "birthmark_or_scar"),
|
||||
"occupation_note": ("occupation", "occupation_note"),
|
||||
}
|
||||
|
||||
|
||||
@@ -167,6 +168,7 @@ _KIND_SEMANTICS: dict[str, tuple[int, float]] = {
|
||||
"family_event": (0, 1.0),
|
||||
"appearance_note": (0, 0.8),
|
||||
"birthmark_or_scar": (-1, 0.8),
|
||||
"occupation_note": (0, 0.8),
|
||||
}
|
||||
|
||||
|
||||
@@ -195,6 +197,8 @@ def public_technique_layers(domain: str, rule_ids: Sequence[str]) -> list[str]:
|
||||
layers.update({"d1-rashi", "d30-trimshamsha"})
|
||||
elif domain in {"appearance", "marks"}:
|
||||
layers.add("d1-rashi")
|
||||
elif domain == "occupation":
|
||||
layers.update({"d1-rashi", "d10-dashamsa"})
|
||||
return sorted(layers)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user