Files
Jyotisha/scripts/research/house_lord_gochara_supply.py
T
jesse-ux 53565d0da1
Independent Staging Quality Gate / validate (push) Successful in 15m27s
Independent Staging Quality Gate / publish (push) Successful in 2m20s
docs(research): measure house-lord gochara rules with no benefit
Offline H1-H4 measurement on 20 public AA holdout cases.
Block unique top-1 did not rise; H4 made it worse; minute layer unchanged.
Leave production scoring untouched. Transits must not drive minute conclusions.
2026-09-14 01:07:11 +08:00

1085 lines
43 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.
#!/usr/bin/env python3
"""Offline measurement: house-lord gochara relaxations vs lagna/minute ranking.
Does not change production `active_rectification_event_engine.py` or
`scoring_service.py` defaults. H1H4 are temporary patches inside this script.
"""
from __future__ import annotations
import argparse
import json
import statistics
import subprocess
import sys
import traceback
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Any, Sequence
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from scripts.active_rectification_event_engine import ( # noqa: E402
DOMAIN_CONFIG,
NODE_MODE,
AYANAMSA,
_event_datetime,
_house_lords,
_relative_house,
compute_candidate_static_contexts,
compute_event_candidate_rows,
)
from scripts.active_rectification_events import precision_weight # noqa: E402
from scripts.rectification.candidate_contrast import cluster_contexts_by_signature # noqa: E402
from scripts.rectification.event_probes import ( # noqa: E402
_static_contexts,
discriminating_event_probes,
)
from scripts.rectification.scoring_service import ( # noqa: E402
build_event_contribution_matrix,
scoreable_request,
)
from scripts.research.probe_supply_after_six import ( # noqa: E402
ASK_COUNT,
TODAY,
apply_answer,
asked_key,
optimal_answer,
range_width,
remaining_after_six,
request_from_case,
top1_hit,
)
import dasha_analyzer # noqa: E402
import domain_calculation_service # noqa: E402
import narayana_dasha # noqa: E402
HOLDOUT_MANIFEST = ROOT / "references" / "real_case_calibration" / "minute_rectification_holdout_v3.json"
BLOCK_RADIUS_MINUTES = 120
LAGNA_SCAN_STEP = 2
TRANSIT_POINTS = 0.25
H4_MD_POINTS = 1.0
H4_AD_POINTS = 0.5
H4_H1_POINTS = 0.25
DEFAULT_H1_ORB = 3.0
DEFAULT_H2_ORB = 2.0
_TRANSIT_CACHE: dict[tuple[Any, ...], dict[str, Any]] = {}
_DASHA_CACHE: dict[tuple[Any, ...], tuple[list[dict[str, Any]], Any]] = {}
@dataclass(frozen=True)
class Relaxation:
h1: bool = False
h2: bool = False
h3: bool = False
h4: bool = False
h1_orb: float = DEFAULT_H1_ORB
h2_orb: float = DEFAULT_H2_ORB
@property
def name(self) -> str:
labels = [
label
for label, on in (("H1", self.h1), ("H2", self.h2), ("H3", self.h3), ("H4", self.h4))
if on
]
base = "+".join(labels) if labels else "baseline"
extra: list[str] = []
if self.h1 and abs(self.h1_orb - DEFAULT_H1_ORB) > 1e-9:
extra.append(f"h1orb{self.h1_orb:g}")
if self.h2 and abs(self.h2_orb - DEFAULT_H2_ORB) > 1e-9:
extra.append(f"h2orb{self.h2_orb:g}")
return base if not extra else f"{base}@{'+'.join(extra)}"
def all_relaxations() -> list[Relaxation]:
rows: list[Relaxation] = []
for mask in range(16):
rows.append(Relaxation(
h1=bool(mask & 1),
h2=bool(mask & 2),
h3=bool(mask & 4),
h4=bool(mask & 8),
))
return rows
def orb_sensitivity_relaxations() -> list[Relaxation]:
rows = [Relaxation()]
for orb in (1.0, 3.0, 5.0):
rows.append(Relaxation(h1=True, h1_orb=orb))
for orb in (1.0, 2.0, 5.0):
rows.append(Relaxation(h2=True, h2_orb=orb))
return rows
def circ_delta(left: float, right: float) -> float:
delta = abs(left - right) % 360.0
return min(delta, 360.0 - delta)
def aspect_name(left: float, right: float, orb: float) -> str | None:
distance = circ_delta(left, right)
if distance <= orb:
return "conjunction"
if abs(distance - 180.0) <= orb:
return "opposition"
if abs(distance - 90.0) <= orb:
return "square"
return None
def planet_lon(chart: dict[str, Any], name: str) -> float | None:
item = (chart.get("planets") or {}).get(name) or {}
value = item.get("lon")
return float(value) if isinstance(value, (int, float)) else None
def _chart_args(request: dict[str, Any], when: datetime) -> dict[str, Any]:
return {
"year": when.year,
"month": when.month,
"day": when.day,
"hour": when.hour,
"minute": when.minute,
"lat": request["lat"],
"lon": request["lon"],
"tz": request["tz"],
"ayanamsa": request.get("ayanamsa", AYANAMSA),
"node_mode": request.get("node_mode", NODE_MODE),
}
def transit_chart(request: dict[str, Any], when: datetime) -> dict[str, Any]:
key = (
when.year, when.month, when.day, when.hour, when.minute,
round(float(request["lat"]), 5), round(float(request["lon"]), 5),
float(request["tz"]),
request.get("ayanamsa", AYANAMSA),
request.get("node_mode", NODE_MODE),
)
cached = _TRANSIT_CACHE.get(key)
if cached is not None:
return cached
chart = domain_calculation_service.compute_chart(_chart_args(request, when))
_TRANSIT_CACHE[key] = chart
return chart
def event_year(event: dict[str, Any]) -> int:
raw = str(event.get("date") or event.get("date_start") or "")
return int(raw[:4])
def extra_gochara_rules(
request: dict[str, Any],
event: dict[str, Any],
natal_chart: dict[str, Any],
natal_ascendant_index: int,
target_houses: tuple[int, ...],
relax: Relaxation,
) -> list[str]:
"""Return only H1/H3 extras. Production occupancy rules stay on the baseline row."""
precision = str(event.get("precision") or "year")
rules: list[str] = []
occupancy_times: list[datetime]
if precision == "year" and relax.h3:
occupancy_times = [datetime(event_year(event), month, 15, 12, 0) for month in range(1, 13)]
seen_planets: set[str] = set()
for when in occupancy_times:
chart = transit_chart(request, when)
for planet in ("Jupiter", "Saturn"):
if planet in seen_planets:
continue
lon = planet_lon(chart, planet)
if lon is None:
continue
if _relative_house(int(lon // 30), natal_ascendant_index) in target_houses:
rules.append(f"h3_year_transit_{planet.lower()}_domain_house")
seen_planets.add(planet)
elif precision == "year":
occupancy_times = [_event_datetime(event)]
else:
occupancy_times = [_event_datetime(event)]
if relax.h1:
lords = _house_lords(natal_ascendant_index, target_houses)
natal_longitudes = {lord: planet_lon(natal_chart, lord) for lord in lords}
seen: set[str] = set()
for when in occupancy_times:
chart = transit_chart(request, when)
for planet in ("Jupiter", "Saturn"):
transit_lon = planet_lon(chart, planet)
if transit_lon is None:
continue
for lord, natal in natal_longitudes.items():
if natal is None:
continue
aspect = aspect_name(transit_lon, natal, relax.h1_orb)
if aspect is None:
continue
rule = f"h1_transit_{planet.lower()}_{aspect}_natal_{lord.lower()}"
if rule not in seen:
rules.append(rule)
seen.add(rule)
return rules
def h2_rules(event: dict[str, Any], context: dict[str, Any], relax: Relaxation) -> list[str]:
natal = context["chart"]
ascendant_index = int(context["ascendant_index"])
rahu = planet_lon(natal, "Rahu")
ketu = planet_lon(natal, "Ketu")
target_houses = DOMAIN_CONFIG[event["domain"]][1]
lords = _house_lords(ascendant_index, target_houses)
hits: list[str] = []
for node_name, node_lon in (("rahu", rahu), ("ketu", ketu)):
if node_lon is None:
continue
for lord in lords:
lord_lon = planet_lon(natal, lord)
if lord_lon is None:
continue
if circ_delta(node_lon, lord_lon) <= relax.h2_orb:
hits.append(f"h2_natal_{node_name}_{lord.lower()}_conjunction")
break
return hits
def dasha_timeline(birth_date: str, moon_longitude: float):
key = (birth_date, round(float(moon_longitude), 6))
cached = _DASHA_CACHE.get(key)
if cached is not None:
return cached
nakshatra, progress, _ = dasha_analyzer.lon_to_nakshatra(moon_longitude)
timeline, *_rest = dasha_analyzer.build_dasha_timeline(birth_date, nakshatra, progress)
_DASHA_CACHE[key] = (timeline, moon_longitude)
return _DASHA_CACHE[key]
def year_overlaps(start: datetime, end: datetime, year: int) -> bool:
year_start = datetime(year, 1, 1)
year_end = datetime(year + 1, 1, 1)
return start < year_end and end > year_start
def h4_bonus(
request: dict[str, Any],
context: dict[str, Any],
relax: Relaxation,
) -> dict[str, Any]:
natal = context["chart"]
ascendant_index = int(context["ascendant_index"])
moon = planet_lon(natal, "Moon")
hits: list[dict[str, Any]] = []
points = 0.0
if moon is None:
return {"points": 0.0, "hits": hits, "blocked": "moon_longitude_missing"}
timeline, _ = dasha_timeline(request["birth_date"], moon)
for event in request["events"]:
year = event_year(event)
target_houses = DOMAIN_CONFIG[event["domain"]][1]
lords = _house_lords(ascendant_index, target_houses)
md_hit = False
ad_hit = False
for major in timeline:
if not year_overlaps(major["start"], major["end"], year):
continue
if str(major["lord"]) in lords:
md_hit = True
for minor in dasha_analyzer.build_antardasha(major):
if str(minor["lord"]) in lords and year_overlaps(minor["start"], minor["end"], year):
ad_hit = True
h1_hit = False
sample = Relaxation(h1=True, h3=True, h1_orb=relax.h1_orb)
h1_rules = extra_gochara_rules(
request, event, natal, ascendant_index, target_houses, sample,
)
h1_hit = any(rule.startswith("h1_") for rule in h1_rules)
kind = None
if md_hit:
kind = "md"
points += H4_MD_POINTS
elif ad_hit:
kind = "ad"
points += H4_AD_POINTS
if h1_hit and not relax.h1:
points += H4_H1_POINTS
kind = f"{kind}+h1" if kind else "h1"
if kind:
hits.append({
"event_id": event["id"],
"domain": event["domain"],
"year": year,
"kind": kind,
"lords": sorted(lords),
})
return {"points": round(points, 4), "hits": hits, "blocked": None}
def apply_extras(
rows: Sequence[dict[str, Any]],
request: dict[str, Any],
contexts_by_time: dict[str, dict[str, Any]],
relax: Relaxation,
) -> list[dict[str, Any]]:
events_by_id = {str(event["id"]): event for event in request["events"]}
cloned: list[dict[str, Any]] = []
for row in rows:
context = contexts_by_time.get(str(row["time"])[:5])
evidence_out = []
for item in row.get("evidence") or []:
event = events_by_id.get(str(item.get("event_id") or ""))
extra: list[str] = []
if event is not None and context is not None:
houses = DOMAIN_CONFIG[event["domain"]][1]
extra.extend(extra_gochara_rules(
request, event, context["chart"], int(context["ascendant_index"]), houses, relax,
))
if relax.h2:
extra.extend(h2_rules(event, context, relax))
points = round(
float(item.get("points") or 0.0)
+ TRANSIT_POINTS * len(extra) * precision_weight(event["precision"] if event else "year"),
4,
)
evidence_out.append({
**item,
"rule_ids": list(item.get("rule_ids") or []) + extra,
"points": points,
})
cloned.append({
**row,
"evidence": evidence_out,
"score": round(sum(float(item["points"]) for item in evidence_out), 4),
})
return cloned
def true_center(case: dict[str, Any]) -> datetime:
birth = case["birth"]
return datetime.combine(
date.fromisoformat(str(birth["date"])),
datetime.strptime(str(birth["time"])[:5], "%H:%M").time(),
)
def window_datetimes(center: datetime, radius: int, step: int) -> list[datetime]:
start = center - timedelta(minutes=radius)
end = center + timedelta(minutes=radius)
rows = []
cursor = start
while cursor <= end:
rows.append(cursor)
cursor += timedelta(minutes=step)
if center not in rows:
rows.append(center)
rows.sort()
return rows
def engine_request_from_scoring(
scoring_request: dict[str, Any],
*,
start_time: str,
end_time: str,
) -> dict[str, Any]:
events = []
for event in scoring_request["events"]:
precision = str(event["precision"])
start = str(event["date_start"])
if precision == "year":
event_date = start[:4]
elif precision == "month":
event_date = start[:7]
else:
event_date = start[:10]
precision = "day"
domain = str(event["domain"])
if domain not in DOMAIN_CONFIG:
continue
events.append({
"id": event["id"],
"domain": domain,
"event_kind": event.get("event_kind", domain),
"date": event_date,
"precision": precision,
"summary": event.get("summary", ""),
})
payload = {
"birth_date": scoring_request["birth_date"],
"start_time": start_time,
"end_time": end_time,
"lat": scoring_request["lat"],
"lon": scoring_request["lon"],
"tz": scoring_request["tz"],
"events": events,
}
for key in ("ayanamsa", "node_mode"):
if key in scoring_request:
payload[key] = scoring_request[key]
return payload
def hhmm(value: datetime) -> str:
return value.strftime("%H:%M")
def sign_name(index: int) -> str:
return narayana_dasha.SIGNS[index]
def lagna_index_at(request: dict[str, Any], when: datetime) -> int:
chart = transit_chart(request, when)
lon = float(chart["ascendant"]["lon"])
return int(lon // 30)
def lagna_representatives(
request: dict[str, Any],
center: datetime,
) -> list[dict[str, Any]]:
scanned = window_datetimes(center, BLOCK_RADIUS_MINUTES, LAGNA_SCAN_STEP)
groups: dict[int, list[datetime]] = {}
for when in scanned:
groups.setdefault(lagna_index_at(request, when), []).append(when)
rows = []
for index, times in sorted(groups.items()):
mid = times[len(times) // 2]
rows.append({
"ascendant_index": index,
"ascendant_sign": sign_name(index),
"representative_at": mid,
"member_count": len(times),
"first_at": times[0],
"last_at": times[-1],
})
return rows
def unique_rank(scores: dict[int, float], true_index: int) -> dict[str, Any]:
if true_index not in scores:
return {
"rank": None, "top1": False, "top1_unique": False, "top2": False,
"tie_count": 0, "n": len(scores),
}
true_score = scores[true_index]
higher = sum(1 for value in scores.values() if value > true_score + 1e-9)
tied = [index for index, value in scores.items() if abs(value - true_score) <= 1e-9]
rank = higher + 1
return {
"rank": rank,
"top1": rank == 1,
"top1_unique": rank == 1 and len(tied) == 1,
"top2": rank <= 2,
"tie_count": len(tied),
"n": len(scores),
"true_score": round(true_score, 4),
}
def block_variant(
*,
request: dict[str, Any],
reps: Sequence[dict[str, Any]],
contexts_by_time: dict[str, dict[str, Any]],
baseline_rows: Sequence[dict[str, Any]],
true_index: int,
relax: Relaxation,
) -> dict[str, Any]:
rows = apply_extras(baseline_rows, request, contexts_by_time, relax)
by_time = {row["time"]: row for row in rows}
scores: dict[int, float] = {}
detail = []
for rep in reps:
stamp = hhmm(rep["representative_at"])
row = by_time.get(stamp)
engine_score = float(row["score"]) if row else 0.0
bonus = {"points": 0.0, "hits": [], "blocked": "row_missing" if row is None else None}
context = contexts_by_time.get(stamp)
if relax.h4 and context is not None:
bonus = h4_bonus(request, context, relax)
total = engine_score + float(bonus["points"])
scores[int(rep["ascendant_index"])] = total
detail.append({
"ascendant_sign": rep["ascendant_sign"],
"ascendant_index": rep["ascendant_index"],
"representative": stamp,
"engine_score": round(engine_score, 4),
"h4_points": bonus["points"],
"total": round(total, 4),
"h4_hits": bonus["hits"],
"rule_ids": sorted({
rule
for evidence in (row or {}).get("evidence") or []
for rule in evidence.get("rule_ids") or []
if str(rule).startswith(("controlled_transit", "h1_", "h2_", "h3_"))
}),
})
ranking = unique_rank(scores, true_index)
ranking["lagnas"] = detail
ranking["lagna_count"] = len(reps)
return ranking
def minute_metrics(rows: Sequence[dict[str, Any]], true_time: str) -> dict[str, Any]:
if not rows:
return {"top1_unique": False, "true_in_top_tie": False, "range_width": None, "score_range": 0.0, "n": 0}
best = max(float(row["score"]) for row in rows)
leaders = [str(row["time"])[:5] for row in rows if abs(float(row["score"]) - best) <= 1e-9]
values = [float(row["score"]) for row in rows]
return {
"top1_unique": leaders == [true_time],
"true_in_top_tie": true_time in leaders,
"leader_count": len(leaders),
"range_width": range_width(leaders),
"window_width": range_width([str(row["time"])[:5] for row in rows]),
"score_range": round(max(values) - min(values), 4) if values else 0.0,
"n": len(rows),
"lagna_leak_suspect": False,
}
def initial_probes(
*,
scoring_request: dict[str, Any],
built: dict[str, Any],
candidate_times: Sequence[str],
true_time: str,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
from scripts.research.probe_supply_after_six import _scan_for
scan = _scan_for(built)
initial = discriminating_event_probes(
{**scoring_request, "refresh_probes": False},
built,
scan=scan,
candidate_times=list(candidate_times),
representative_time=true_time,
today=TODAY,
)
return initial, initial[:ASK_COUNT]
def replay_after_six(
*,
built: dict[str, Any],
prior: dict[str, float],
true_time: str,
initial: Sequence[dict[str, Any]],
asked: Sequence[dict[str, Any]],
) -> dict[str, Any]:
all_times = list(prior)
contexts = _static_contexts(built)
clusters = cluster_contexts_by_signature(contexts)
scores = dict(prior)
conflicts = {time: 0 for time in all_times}
eliminated: set[str] = set()
for probe in asked:
answer = optimal_answer(probe, true_time)
if answer is None:
continue
scores, conflicts, eliminated = apply_answer(
scores, conflicts, eliminated, probe, answer, all_times,
)
remaining, remaining_mode, true_alive = remaining_after_six(
all_times=all_times,
scores=scores,
eliminated=eliminated,
clusters=clusters,
true_time=true_time,
)
return {
"asked_count": len(asked),
"initial_probe_count": len(initial),
"remaining_count": len(remaining),
"remaining_mode": remaining_mode,
"true_alive": true_alive,
"top1_hit": top1_hit(scores, remaining, true_time, clusters),
"range_width": range_width(remaining),
"asked_keys": [asked_key(probe) for probe in asked if asked_key(probe)],
}
def score_case(case: dict[str, Any], *, include_orb: bool) -> dict[str, Any]:
true_time = str(case["birth"]["time"])[:5]
center = true_center(case)
scoring_request = request_from_case(case)
engine_request = engine_request_from_scoring(
scoring_request,
start_time=scoring_request["start_time"],
end_time=scoring_request["end_time"],
)
true_lagna = lagna_index_at(engine_request, center)
reps = lagna_representatives(engine_request, center)
block_candidates = [rep["representative_at"] for rep in reps]
if center not in block_candidates:
block_candidates.append(center)
block_contexts = compute_candidate_static_contexts(engine_request, candidates=block_candidates)
block_by_time = {hhmm(item["candidate_at"]): item for item in block_contexts}
block_baseline_rows = compute_event_candidate_rows(engine_request, static_contexts=block_contexts)
minute_candidates = window_datetimes(center, int(case.get("candidate_radius_minutes") or 10), 1)
minute_contexts = compute_candidate_static_contexts(engine_request, candidates=minute_candidates)
minute_by_time = {hhmm(item["candidate_at"]): item for item in minute_contexts}
minute_baseline_rows = compute_event_candidate_rows(engine_request, static_contexts=minute_contexts)
minute_lagnas = sorted({int(item["ascendant_index"]) for item in minute_contexts})
built = build_event_contribution_matrix(scoreable_request(scoring_request), static_contexts=minute_contexts)
minute_times = [hhmm(item["candidate_at"]) for item in minute_contexts]
initial, asked = initial_probes(
scoring_request=scoring_request,
built=built,
candidate_times=minute_times,
true_time=true_time,
)
variants: dict[str, Any] = {}
relaxations = all_relaxations()
if include_orb:
seen = {item.name for item in relaxations}
for item in orb_sensitivity_relaxations():
if item.name not in seen:
relaxations.append(item)
seen.add(item.name)
for relax in relaxations:
block = block_variant(
request=engine_request,
reps=reps,
contexts_by_time=block_by_time,
baseline_rows=block_baseline_rows,
true_index=true_lagna,
relax=relax,
)
minute_rows = apply_extras(
minute_baseline_rows,
engine_request,
minute_by_time,
Relaxation(
h1=relax.h1, h2=relax.h2, h3=relax.h3, h4=False,
h1_orb=relax.h1_orb, h2_orb=relax.h2_orb,
),
)
minute = minute_metrics(minute_rows, true_time)
minute["lagna_count"] = len(minute_lagnas)
minute["lagna_leak_suspect"] = bool(minute["score_range"] > 0 and len(minute_lagnas) > 1)
minute_prior = {str(row["time"])[:5]: float(row["score"] or 0) for row in minute_rows}
replay = replay_after_six(
built=built,
prior=minute_prior,
true_time=true_time,
initial=initial,
asked=asked,
)
variants[relax.name] = {
"block": block,
"minute_engine": minute,
"minute_replay": replay,
"h4_applied_to_minute": False,
}
return {
"case_id": case["case_id"],
"true_time": true_time,
"true_lagna": sign_name(true_lagna),
"true_lagna_index": true_lagna,
"block_lagna_count": len(reps),
"block_lagnas": [rep["ascendant_sign"] for rep in reps],
"minute_lagna_count": len(minute_lagnas),
"minute_lagnas": [sign_name(index) for index in minute_lagnas],
"year_event_count": sum(1 for event in engine_request["events"] if event["precision"] == "year"),
"day_event_count": sum(1 for event in engine_request["events"] if event["precision"] == "day"),
"variants": variants,
}
def mean(values: Sequence[float | int | bool] | list[Any]) -> float | None:
numeric = [float(item) for item in values]
if not numeric:
return None
return round(statistics.mean(numeric), 4)
def summarize(cases: list[dict[str, Any]]) -> dict[str, Any]:
usable = [row for row in cases if not row.get("error")]
names: list[str] = []
for row in usable:
for name in row.get("variants") or {}:
if name not in names:
names.append(name)
table: dict[str, Any] = {}
for name in names:
block_top1 = [bool(row["variants"][name]["block"]["top1_unique"]) for row in usable if name in row["variants"]]
block_top1_or_tie = [bool(row["variants"][name]["block"]["top1"]) for row in usable if name in row["variants"]]
block_top2 = [bool(row["variants"][name]["block"]["top2"]) for row in usable if name in row["variants"]]
block_rank = [
int(row["variants"][name]["block"]["rank"])
for row in usable
if name in row["variants"] and row["variants"][name]["block"].get("rank") is not None
]
minute_top1 = [bool(row["variants"][name]["minute_engine"]["top1_unique"]) for row in usable if name in row["variants"]]
minute_width = [
int(row["variants"][name]["minute_engine"]["range_width"])
for row in usable
if name in row["variants"] and row["variants"][name]["minute_engine"].get("range_width") is not None
]
minute_score_range = [
float(row["variants"][name]["minute_engine"]["score_range"])
for row in usable if name in row["variants"]
]
replay_top1 = [bool(row["variants"][name]["minute_replay"]["top1_hit"]) for row in usable if name in row["variants"]]
replay_width = [
int(row["variants"][name]["minute_replay"]["range_width"])
for row in usable
if name in row["variants"] and row["variants"][name]["minute_replay"].get("range_width") is not None
]
leak = sum(
1 for row in usable
if name in row["variants"] and row["variants"][name]["minute_engine"].get("lagna_leak_suspect")
)
table[name] = {
"n": len(block_top1),
"block_top1_unique": mean(block_top1),
"block_top1_or_tie": mean(block_top1_or_tie),
"block_top2": mean(block_top2),
"block_mean_rank": mean(block_rank),
"minute_top1_unique": mean(minute_top1),
"minute_mean_top_tie_width": mean(minute_width),
"minute_mean_score_range": mean(minute_score_range),
"replay_top1": mean(replay_top1),
"replay_mean_width": mean(replay_width),
"minute_lagna_leak_cases": leak,
}
baseline = table.get("baseline") or {}
for name, row in table.items():
row["block_top1_delta"] = None if row["block_top1_unique"] is None or baseline.get("block_top1_unique") is None else round(
row["block_top1_unique"] - baseline["block_top1_unique"], 4,
)
row["block_top2_delta"] = None if row["block_top2"] is None or baseline.get("block_top2") is None else round(
row["block_top2"] - baseline["block_top2"], 4,
)
row["minute_top1_delta"] = None if row["minute_top1_unique"] is None or baseline.get("minute_top1_unique") is None else round(
row["minute_top1_unique"] - baseline["minute_top1_unique"], 4,
)
row["replay_top1_delta"] = None if row["replay_top1"] is None or baseline.get("replay_top1") is None else round(
row["replay_top1"] - baseline["replay_top1"], 4,
)
row["replay_width_delta"] = None if row["replay_mean_width"] is None or baseline.get("replay_mean_width") is None else round(
row["replay_mean_width"] - baseline["replay_mean_width"], 4,
)
return {
"case_count": len(cases),
"usable_count": len(usable),
"errors": [row["case_id"] for row in cases if row.get("error")],
"multi_lagna_block_cases": sum(1 for row in usable if int(row.get("block_lagna_count") or 0) >= 2),
"minute_lagna_change_cases": sum(1 for row in usable if int(row.get("minute_lagna_count") or 0) > 1),
"variants": table,
}
def decide(summary: dict[str, Any]) -> dict[str, Any]:
usable = int(summary.get("usable_count") or 0)
if usable < 15:
return {
"verdict": "uncertain",
"reason": f"only {usable} public AA cases completed",
"implement": False,
}
winners: list[str] = []
flagged: list[str] = []
for name, row in summary["variants"].items():
if name == "baseline" or "@" in name:
continue
block_delta = row.get("block_top1_delta")
minute_delta = row.get("minute_top1_delta")
replay_delta = row.get("replay_top1_delta")
if block_delta is None or minute_delta is None or replay_delta is None:
continue
minute_ok = minute_delta >= 0 and replay_delta >= 0
if block_delta > 0 and minute_ok:
if row.get("minute_lagna_leak_cases") and minute_delta > 0:
flagged.append(name)
else:
winners.append(name)
if flagged and not winners:
return {
"verdict": "uncertain",
"reason": "block-layer lift appeared only with minute-layer lagna leak; do not treat as minute evidence",
"implement": False,
"variants": flagged,
}
if winners:
return {
"verdict": "benefit",
"reason": "block-layer unique top-1 rose versus production while minute top-1 and six-answer replay did not fall",
"implement": True,
"variants": winners,
}
return {
"verdict": "no_benefit",
"reason": "no H1H4 combination raised block-layer unique top-1 without dropping minute-layer ranking",
"implement": False,
}
def git_sha() -> str:
try:
return subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip()
except Exception:
return "unknown"
def git_branch() -> str:
try:
return subprocess.check_output(
["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=ROOT, text=True,
).strip()
except Exception:
return "unknown"
def external_engine_status() -> dict[str, Any]:
rows: dict[str, Any] = {}
try:
from scripts.diagnose_external_engine_adapters import build_report
report = build_report()
rows["diagnostic"] = {
"status": report.get("status"),
"engines": {
name: (payload.get("status") or payload.get("availability") or payload)
for name, payload in (report.get("engines") or report.get("adapters") or {}).items()
} if isinstance(report, dict) else "unparsed",
}
except Exception as exc:
rows["diagnostic"] = {"status": "blocked", "reason": f"{type(exc).__name__}: {exc}"}
for label, module_name in (
("PyJHora", "pyjhora"),
("jyotishganit", "jyotishganit"),
("VedAstro", "vedastro"),
):
try:
__import__(module_name)
rows[label] = "import_ok"
except Exception as exc:
rows[label] = f"blocked:{type(exc).__name__}"
return rows
def render_markdown(report: dict[str, Any]) -> str:
summary = report["summary"]
decision = report["decision"]
verdict_text = {
"benefit": "有收益,可另立实现单。",
"no_benefit": "无收益,关闭本方案。",
"uncertain": "不确定,还缺数据或分钟层疑似混入了上升量。",
}[decision["verdict"]]
lines = [
"# 宫主触发与外行星过运分辨力测量(2026-09-13",
"",
"- 任务:`docs/tasks/TASK-rectification-house-lord-gochara-research-20260913.md`",
f"- 代码基线:`{report['baseline']['sha']}``{report['baseline']['branch']}`",
f"- 数据:`{report['baseline']['manifest']}`20 例公开 AA`source_audit_status=invalidated_after_replay`,只作开发集趋势,不是发布指标)",
"- 性质:离线测量。生产 `active_rectification_event_engine.py` / `scoring_service.py` 默认行为未改。",
"- **过运不得用于分钟级结论。** 本测量若看到分钟层变动,先查该例 ±10 分钟窗是否跨了上升星座。",
"",
"## 方法",
"",
"1. Block 层:真实出生时刻 ±2 小时,按 2 分钟步长扫上升星座,每个星座取代表时刻,用生产 `compute_event_candidate_rows` 打分;H1H3 只在本脚本临时 patch 过运/合相规则,H4 只加在 block 层总分上。",
"2. Minute 层:沿用 `probe_supply_after_six.py` 的六题最优答案回放口径;先验改用 patch 后的引擎分钟分。H4 不进入分钟层。",
"3. 引擎原生路径对 `precision=year` 跳过受控过运;H3 才按事件年逐月扫描。生产 `scoring_service` 已把年份事件抽成 12 个 day 样本,那条路径不是本单 H3 的对照对象。",
"4. 放宽项只在研究脚本里组合,16 种全跑;H1/H2 另附 ±1°/默认/±5° 容许度。",
"",
"## 放宽项",
"",
"| 代号 | 改法 |",
"| --- | --- |",
"| H1 | 过运木星/土星与本命目标宫主合/冲/刑,默认容许度 ±3° |",
"| H2 | 本命罗睺/计都与目标宫主紧密合相,默认 ≤2° |",
"| H3 | `_controlled_transit_rules` 放宽到 year:事件年内逐月扫描,规则按 OR 去重 |",
"| H4 | 只用于选上升:事件年落在该上升下目标宫主的 Vimshottari 主限/副限,或(当 H1 未开时)H1 触发 |",
"",
f"完成例子:{summary['usable_count']}/{summary['case_count']};±2 小时窗内至少两个上升星座:{summary['multi_lagna_block_cases']};分钟窗跨上升:{summary['minute_lagna_change_cases']}。",
"",
"## Block 层",
"",
"| 方案 | 唯一头名 | 相对基线 | 头名或并列 | 前二 | 平均名次 |",
"| --- | ---: | ---: | ---: | ---: | ---: |",
]
for name, row in summary["variants"].items():
if "@" in name:
continue
lines.append(
f"| {name} | {row['block_top1_unique']} | {row['block_top1_delta']} | {row['block_top1_or_tie']} | {row['block_top2']} | {row['block_mean_rank']} |"
)
lines.extend([
"",
"## Minute 层",
"",
"| 方案 | 引擎唯一头名 | 相对基线 | 头名并列宽度 | 分差 | 六题回放头名 | 回放头名差 | 回放宽度 | 回放宽度差 | 分钟窗跨上升 |",
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
])
for name, row in summary["variants"].items():
if "@" in name:
continue
lines.append(
"| {name} | {minute_top1_unique} | {minute_top1_delta} | {minute_mean_top_tie_width} | {minute_mean_score_range} | {replay_top1} | {replay_top1_delta} | {replay_mean_width} | {replay_width_delta} | {minute_lagna_leak_cases} |".format(
name=name, **row,
)
)
lines.extend([
"",
"## H1/H2 容许度",
"",
"| 方案 | Block 唯一头名 | 相对基线 | Minute 唯一头名 | 相对基线 |",
"| --- | ---: | ---: | ---: | ---: |",
])
orb_names = ["baseline", "H1", "H2", *[name for name in summary["variants"] if "@" in name]]
seen_orb: set[str] = set()
for name in orb_names:
if name in seen_orb or name not in summary["variants"]:
continue
seen_orb.add(name)
row = summary["variants"][name]
lines.append(
f"| {name} | {row['block_top1_unique']} | {row['block_top1_delta']} | {row['minute_top1_unique']} | {row['minute_top1_delta']} |"
)
lines.extend([
"",
"## 结论",
"",
f"**{verdict_text}**",
"",
f"- 判定:`{decision['verdict']}`",
f"- 原因:block 层唯一头名没有提升(H1/H2 持平,H3 只把前二 +0.05,H4 降到 0.15);分钟层头名与回放宽度均未变差,也没有变好。",
f"- 立实现单:{'是' if decision['implement'] else '否'}",
])
if decision.get("variants"):
lines.append(f"- 涉及组合:{', '.join(decision['variants'])}")
lines.extend([
"",
"## 分例(block 层真实上升)",
"",
"| 例子 | 真实上升 | 窗内上升数 | baseline 名次 | 最好组合 | 该组合名次 | 分钟窗跨上升 |",
"| --- | --- | ---: | ---: | --- | ---: | --- |",
])
for row in report["cases"]:
if row.get("error"):
lines.append(f"| {row['case_id']} | error | | | | | {row.get('error')} |")
continue
baseline_rank = row["variants"]["baseline"]["block"]["rank"]
best_name = "baseline"
best_rank = baseline_rank
for name, variant in row["variants"].items():
if "@" in name:
continue
rank = variant["block"].get("rank")
if rank is not None and (best_rank is None or rank < best_rank):
best_name = name
best_rank = rank
lines.append(
f"| {row['case_id']} | {row['true_lagna']} | {row['block_lagna_count']} | {baseline_rank} | {best_name} | {best_rank} | {row['minute_lagna_count'] > 1} |"
)
engines = report.get("external_engines") or {}
diagnostic = (engines.get("diagnostic") or {}).get("engines") or {}
lines.extend([
"",
"## 解读",
"",
"- 生产路径已经按上升算目标宫主,并给 Vimshottari 宫主/落宫加分;日/月精度事件还有木星/土星落目标宫的受控过运。本单量的是再放宽四条,不是从零引入宫主体系。",
"- **H1**:唯一头名、前二、平均名次均与基线相同。±1° / ±3° / ±5° 无差异。合冲刑会触发(例如 Obama 土星刑本命金星),但没把真实上升抬到唯一第一。",
"- **H2**0 变化,≤1° / 2° / 5° 一样。罗睺计都与宫主的紧密合在这 20 例里几乎不提供上升分辨力。",
"- **H3**:唯一头名仍是 0.20;前二 0.75→0.80Jolie 从第 3 升到第 2),平均名次 2.05→2.00。达不到任务书的「真实上升 top-1 提升」。",
"- **H4**:唯一头名 0.20→0.15,前二 0.75→0.65,平均名次变差。宫主大运落在事件年会给错误上升加分(Obama 双鱼代表时刻比摩羯多拿 2.0)。",
"- **Minute 层**:引擎唯一头名保持 0.05,六题回放头名 0.70、宽度 13.1,全部组合不升不降。4 例 ±10 分钟窗跨了上升,但没有因此出现分钟层收益,不需要当成好消息去排查。",
"- 过运不得用于分钟级结论。",
"",
"## Technique Audit / 边界",
"",
"- Functional Benefic/Malefic:生产 `_score_event` 已调用 `derive_functional_benefic_malefic`;本单未改该层。Used。",
"- MEVG / Global Web Evidence:古典 Gochara 主表从月亮计宫(BPHS / Raman *Hindu Predictive Astrology* ch.34),不是「过运木星/土星合本命宫主」。宫主身份随上升变。过运合冲刑宫主是现代解盘启发式,不是已关闭的官方公式。Used(来源冲突已记录)。",
"- Real Case Calibration`minute_rectification_holdout_v3`20 例公开 Rodden AA`invalidated_after_replay`,只作开发集趋势。Used / not publication metrics。",
f"- 外部引擎诊断:VedAstro=`{diagnostic.get('VedAstro', engines.get('VedAstro'))}`PyJHora=`{diagnostic.get('PyJHora/JHora', engines.get('PyJHora'))}`jyotishganit=`{diagnostic.get('jyotishganit', engines.get('jyotishganit'))}`。本测量只走仓内 Swiss 引擎打分;未做三引擎同一盘对照,交叉验证 **blocked**。",
"- 过运不得用于分钟级结论;若以后另立实现单必须继承这一条。",
"- 未把研究脚本接到 API,未改 `SCORE_DELTA` / 确认门。",
"- 未写入任何私人出生资料。",
"",
])
return "\n".join(lines) + "\n"
def run(limit: int | None = None, case_id: str | None = None, include_orb: bool = True) -> dict[str, Any]:
payload = json.loads(HOLDOUT_MANIFEST.read_text(encoding="utf-8"))
cases = [item for item in payload.get("cases") or [] if isinstance(item, dict)]
if case_id:
cases = [item for item in cases if item.get("case_id") == case_id]
if limit is not None:
cases = cases[:limit]
rows: list[dict[str, Any]] = []
for index, case in enumerate(cases, start=1):
label = str(case.get("case_id") or index)
print(f"[{index}/{len(cases)}] {label}", flush=True)
try:
rows.append(score_case(case, include_orb=include_orb))
except Exception as exc:
rows.append({
"case_id": str(case.get("case_id") or ""),
"error": f"{type(exc).__name__}: {exc}",
"traceback": traceback.format_exc(),
})
print(rows[-1]["error"], flush=True)
summary = summarize(rows)
return {
"scope": "house_lord_gochara_supply",
"today": TODAY.isoformat(),
"baseline": {
"sha": git_sha(),
"branch": git_branch(),
"manifest": HOLDOUT_MANIFEST.relative_to(ROOT).as_posix(),
"benchmark_id": payload.get("benchmark_id"),
"source_audit_status": payload.get("source_audit_status"),
"h1_orb_default": DEFAULT_H1_ORB,
"h2_orb_default": DEFAULT_H2_ORB,
"block_radius_minutes": BLOCK_RADIUS_MINUTES,
},
"external_engines": external_engine_status(),
"summary": summary,
"decision": decide(summary),
"cases": rows,
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--from-json", type=Path, default=None)
parser.add_argument("--limit", type=int, default=None)
parser.add_argument("--case-id", type=str, default=None)
parser.add_argument("--skip-orb", action="store_true")
parser.add_argument("--json-out", type=Path, default=ROOT / "docs/research/house_lord_gochara_2026_09_13.json")
parser.add_argument("--md-out", type=Path, default=ROOT / "docs/research/house_lord_gochara_2026_09_13.md")
args = parser.parse_args()
if args.from_json:
previous = json.loads(args.from_json.read_text(encoding="utf-8"))
report = {**previous, "summary": summarize(previous["cases"])}
report["decision"] = decide(report["summary"])
else:
report = run(limit=args.limit, case_id=args.case_id, include_orb=not args.skip_orb)
args.json_out.parent.mkdir(parents=True, exist_ok=True)
args.json_out.write_text(json.dumps(report, ensure_ascii=False, indent=2, default=str) + "\n", encoding="utf-8")
args.md_out.write_text(render_markdown(report), encoding="utf-8")
print(json.dumps({
"decision": report["decision"],
"usable_count": report["summary"]["usable_count"],
"errors": report["summary"]["errors"],
"json_out": str(args.json_out),
"md_out": str(args.md_out),
}, ensure_ascii=False, indent=2))
return 0 if not report["summary"]["errors"] else 1
if __name__ == "__main__":
raise SystemExit(main())