feat(rectification): pass ayanamsa per request

This commit is contained in:
Jesse_Chen
2026-09-04 00:25:16 +08:00
parent 3b09bbbee0
commit 44d3d3931b
15 changed files with 135 additions and 16 deletions
+7 -5
View File
@@ -289,7 +289,9 @@ def _controlled_transit_rules(
transit_chart = domain_calculation_service.compute_chart({
"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": AYANAMSA, "node_mode": NODE_MODE,
"tz": request["tz"],
"ayanamsa": request.get("ayanamsa", AYANAMSA),
"node_mode": request.get("node_mode", NODE_MODE),
})
rules: list[str] = []
for planet in ("Jupiter", "Saturn"):
@@ -425,8 +427,8 @@ def build_candidate_static_context(
"lat": request["lat"],
"lon": request["lon"],
"tz": request["tz"],
"ayanamsa": AYANAMSA,
"node_mode": NODE_MODE,
"ayanamsa": request.get("ayanamsa", AYANAMSA),
"node_mode": request.get("node_mode", NODE_MODE),
})
planet_longitudes = {
name: float(data["lon"])
@@ -642,8 +644,8 @@ def _canonical_input_contract(request: RectificationEventRequest) -> tuple[dict,
"summary": event.get("summary", ""),
} for event in request["events"]],
"calculation": {
"ayanamsa": AYANAMSA,
"node_mode": NODE_MODE,
"ayanamsa": request.get("ayanamsa", AYANAMSA),
"node_mode": request.get("node_mode", NODE_MODE),
"ephemeris_source": "swisseph_calc_ut",
},
}
+2
View File
@@ -73,6 +73,8 @@ class RectificationEventRequest(TypedDict):
lat: float
lon: float
tz: float
ayanamsa: NotRequired[str]
node_mode: NotRequired[str]
events: list[LifeEvent]
+41 -4
View File
@@ -16,6 +16,9 @@ if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR))
from active_rectification_selector import select_next_questions
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME
DEFAULT_NODE_MODE = "mean"
OPTIONS = [
{"key": "A", "label": "明确有,且时间大致吻合", "score": 2},
@@ -60,7 +63,8 @@ def _candidate_scan(
lat: float | None = None,
lon: float | None = None,
tz: float | None = None,
ayanamsa: str = "raman",
ayanamsa: str = DEFAULT_AYANAMSA_NAME,
node_mode: str = DEFAULT_NODE_MODE,
) -> dict[str, Any]:
start = center - timedelta(minutes=uncertainty_minutes)
end = center + timedelta(minutes=uncertainty_minutes)
@@ -84,7 +88,14 @@ def _candidate_scan(
"cluster": cluster,
"sensitivity_flags": _sensitivity_flags(abs(offset)),
}
recast = _candidate_recast(candidate, lat=lat, lon=lon, tz=tz, ayanamsa=ayanamsa)
recast = _candidate_recast(
candidate,
lat=lat,
lon=lon,
tz=tz,
ayanamsa=ayanamsa,
node_mode=node_mode,
)
if recast:
sample.update(recast)
samples.append(sample)
@@ -106,6 +117,8 @@ def _candidate_scan(
"lat": lat,
"lon": lon,
"tz": tz,
"ayanamsa": ayanamsa,
"node_mode": node_mode,
},
uncertainty_minutes=uncertainty_minutes,
step_minutes=step_minutes,
@@ -126,6 +139,10 @@ def _candidate_scan(
"cluster_labels": ["early_candidate_cluster", "middle_candidate_cluster", "late_candidate_cluster"],
"samples": samples,
"minute_scan": minute_scan,
"calculation": {
"ayanamsa": ayanamsa,
"node_mode": node_mode,
},
"sensitivity_summary": {
"method": "minute_feature_scan_v2",
"high_value_layers": ["D2", "D4", "D9", "D10", "D24", "D30", "D60", "UL", "A7", "A10", "KP_cusp"],
@@ -152,6 +169,7 @@ def _candidate_recast(
lon: float | None,
tz: float | None,
ayanamsa: str,
node_mode: str = DEFAULT_NODE_MODE,
) -> dict[str, Any] | None:
if lat is None or lon is None or tz is None:
return None
@@ -171,6 +189,7 @@ def _candidate_recast(
"lon": lon,
"tz": tz,
"ayanamsa": ayanamsa,
"node_mode": node_mode,
})
planet_lons = {
name: data["lon"]
@@ -231,7 +250,8 @@ def build_questionnaire(
lat: float | None = None,
lon: float | None = None,
tz: float | None = None,
ayanamsa: str = "raman",
ayanamsa: str = DEFAULT_AYANAMSA_NAME,
node_mode: str = DEFAULT_NODE_MODE,
) -> dict[str, Any]:
questions = []
for qid, round_id, domain, sensitivity, window, prompt, yes_bias, no_bias in QUESTION_TEMPLATES:
@@ -270,6 +290,7 @@ def build_questionnaire(
lon=lon,
tz=tz,
ayanamsa=ayanamsa,
node_mode=node_mode,
)
selection = select_next_questions(
{"questions": questions, "candidate_scan": candidate_scan}, {}, limit=1
@@ -279,6 +300,10 @@ def build_questionnaire(
"schema_version": 1,
"candidate_scan": candidate_scan,
"selection": selection,
"calculation": {
"ayanamsa": ayanamsa,
"node_mode": node_mode,
},
"workflow": [
"candidate_time_scan",
"varga_arudha_kp_sensitivity_diff",
@@ -433,6 +458,10 @@ def score_answers(
"truth_status": "not_birth_time_truth",
"formula_unit_parity_status": "partial",
"timing_claim_status": "exploratory_unvalidated",
"calculation": questionnaire.get("calculation", {
"ayanamsa": DEFAULT_AYANAMSA_NAME,
"node_mode": DEFAULT_NODE_MODE,
}),
"answered_count": len(applied),
"candidate_cluster_rankings": rankings,
"technique_audit_table": [
@@ -494,10 +523,18 @@ def main() -> int:
parser.add_argument("--birth-time", required=True, help="Approximate local birth time, YYYY-MM-DD HH:MM")
parser.add_argument("--uncertainty-minutes", type=int, default=30)
parser.add_argument("--step-minutes", type=int, default=1)
parser.add_argument("--ayanamsa", default=DEFAULT_AYANAMSA_NAME)
parser.add_argument("--node-mode", choices=("mean", "true"), default=DEFAULT_NODE_MODE)
parser.add_argument("--answers-json", default="", help="Optional JSON object mapping question id to A/B/C/D")
parser.add_argument("--pretty", action="store_true")
args = parser.parse_args()
questionnaire = build_questionnaire(args.birth_time, args.uncertainty_minutes, args.step_minutes)
questionnaire = build_questionnaire(
args.birth_time,
args.uncertainty_minutes,
args.step_minutes,
ayanamsa=args.ayanamsa,
node_mode=args.node_mode,
)
report = score_answers(questionnaire, json.loads(args.answers_json)) if args.answers_json else questionnaire
print(json.dumps(report, ensure_ascii=False, indent=2 if args.pretty else None))
return 0
@@ -34,6 +34,12 @@ def _engine_json(command: str, payload: dict[str, Any], *, timeout: int = 20) ->
args = [sys.executable, str(ENGINE), command]
for key in ("year", "month", "day", "hour", "minute", "lat", "lon", "tz"):
args.extend([f"--{key}", str(payload[key])])
args.extend([
"--ayanamsa",
str(payload.get("ayanamsa") or DEFAULT_AYANAMSA_NAME).strip().lower(),
"--node-mode",
str(payload.get("node_mode", payload.get("nodeMode", "mean"))).strip().lower(),
])
if command == "varga-full":
args.extend(["--divisions", ",".join(_VARGAS)])
completed = subprocess.run(args, cwd=ROOT, capture_output=True, text=True, timeout=timeout, check=True)
@@ -148,6 +154,8 @@ def main() -> int:
parser.add_argument(f"--{field}", required=True, type=cast)
parser.add_argument("--uncertainty-minutes", type=int, default=30)
parser.add_argument("--step-minutes", type=int, default=1)
parser.add_argument("--ayanamsa", default=DEFAULT_AYANAMSA_NAME)
parser.add_argument("--node-mode", choices=("mean", "true"), default="mean")
args = parser.parse_args()
print(json.dumps(scan_candidate_times(vars(args), uncertainty_minutes=args.uncertainty_minutes, step_minutes=args.step_minutes), ensure_ascii=False, indent=2))
return 0
+22 -2
View File
@@ -818,6 +818,19 @@ def _request_ayanamsa(*sources) -> str:
return DEFAULT_AYANAMSA_NAME
def _request_node_mode(*sources) -> str:
for source in sources:
if isinstance(source, dict):
raw = source.get('node_mode', source.get('nodeMode'))
else:
raw = source
if raw not in (None, ''):
if not isinstance(raw, str) or raw.strip().lower() not in {'mean', 'true'}:
raise BadRequest('node_mode must be mean or true')
return raw.strip().lower()
return 'mean'
def _consultation_declared_routes(body: dict) -> set[str]:
values: list[str] = []
for key in ('strict_workflow_route', 'theme'):
@@ -8792,8 +8805,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
lon = self._get_float(body, 'lon', 0, -180, 180)
tz = self._get_float(body, 'tz', 0, -14, 14)
ayanamsa = _request_ayanamsa(body)
if not isinstance(ayanamsa, str):
raise BadRequest('ayanamsa must be a string')
node_mode = _request_node_mode(body)
try:
module = _load_local_module('active_rectification_questions')
result = module.build_questionnaire(
@@ -8804,6 +8816,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
lon=lon,
tz=tz,
ayanamsa=ayanamsa,
node_mode=node_mode,
)
except ValueError as e:
raise BadRequest('birth_time must be YYYY-MM-DD HH:MM') from e
@@ -8839,6 +8852,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
def _compute_active_rectification_events(self, body):
allowed_fields = {
'birth_date', 'start_time', 'end_time', 'lat', 'lon', 'tz', 'events', 'high_rigor',
'ayanamsa', 'node_mode',
}
unsupported_fields = sorted(set(body) - allowed_fields)
if unsupported_fields:
@@ -8864,6 +8878,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
lat = self._get_float(body, 'lat', 0, -90, 90)
lon = self._get_float(body, 'lon', 0, -180, 180)
tz = self._get_float(body, 'tz', 0, -14, 14)
ayanamsa = _request_ayanamsa(body)
node_mode = _request_node_mode(body)
events = body.get('events')
if not isinstance(events, list) or len(events) < MIN_SCORING_EVENTS:
raise BadRequest(f'events must contain at least {MIN_SCORING_EVENTS} item')
@@ -8911,6 +8927,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
'lat': lat,
'lon': lon,
'tz': tz,
'ayanamsa': ayanamsa,
'node_mode': node_mode,
'events': normalized_events,
})
external_validation = {
@@ -8935,6 +8953,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
'lat': lat,
'lon': lon,
'tz': tz,
'ayanamsa': ayanamsa,
'node_mode': node_mode,
}
three_engine_packet = build_packet(engine_case)
vedastro_events = [{
+18 -1
View File
@@ -6,6 +6,8 @@ import uuid
from datetime import date
from typing import Any, Literal, NotRequired, TypedDict, cast
from scripts.ayanamsa_utils import UnsupportedAyanamsaError, normalize_ayanamsa_name
DatePrecision = Literal["day", "month", "quarter", "year", "range"]
EVENT_CONTRACT_VERSION = "rectification-event-contract-v2"
@@ -48,7 +50,10 @@ _EVENT_PROVENANCE_FIELDS = frozenset({
"date_source", "date_reliability", "date_corroboration", "date_conflict_status",
"source_turn_id", "subject",
})
_REQUEST_FIELDS = frozenset({"birth_date", "start_time", "end_time", "lat", "lon", "tz", "events"}) | _REQUEST_PROVENANCE_FIELDS
_REQUEST_FIELDS = frozenset({
"birth_date", "start_time", "end_time", "lat", "lon", "tz", "events",
"ayanamsa", "node_mode",
}) | _REQUEST_PROVENANCE_FIELDS
_EVENT_FIELDS = frozenset({"id", "domain", "event_kind", "date_start", "date_end", "precision", "summary"}) | _EVENT_PROVENANCE_FIELDS
_CLOCK = re.compile(r"(?:[01]\d|2[0-3]):[0-5]\d\Z")
@@ -77,6 +82,8 @@ class RectificationRequest(TypedDict):
lon: float
tz: float
events: list[LifeEvent]
ayanamsa: NotRequired[str]
node_mode: NotRequired[str]
birth_time_source: NotRequired[str | None]
timezone_id: NotRequired[str | None]
timezone_source: NotRequired[str | None]
@@ -227,6 +234,16 @@ def normalize_rectification_request(body: Any, *, today: date | None = None) ->
"tz": _bounded_number(body, "tz", -14, 14),
"events": cleaned_events,
}
if "ayanamsa" in body:
try:
cleaned_request["ayanamsa"] = normalize_ayanamsa_name(body.get("ayanamsa"))
except UnsupportedAyanamsaError as exc:
raise ValueError(str(exc)) from exc
if "node_mode" in body:
node_mode = body.get("node_mode")
if not isinstance(node_mode, str) or node_mode.strip().lower() not in {"mean", "true"}:
raise ValueError("node_mode must be mean or true")
cleaned_request["node_mode"] = node_mode.strip().lower()
_copy_nullable_text(body, cleaned_request, "birth_time_source", "birth_time_source", 120, _BIRTH_TIME_SOURCES)
_copy_nullable_text(body, cleaned_request, "timezone_id", "timezone_id", 120)
_copy_nullable_text(body, cleaned_request, "timezone_source", "timezone_source", 80)
+8 -2
View File
@@ -103,7 +103,7 @@ def sample_event_dates(event: LifeEvent) -> list[str]:
def _legacy_request(request: RectificationRequest, event: LifeEvent, sampled_date: str) -> dict[str, Any]:
engine_domain, _ = _ENGINE_KIND_BY_NATIVE_KIND[event["event_kind"]]
return {
legacy_request = {
"birth_date": request["birth_date"],
"start_time": request["start_time"],
"end_time": request["end_time"],
@@ -116,6 +116,10 @@ def _legacy_request(request: RectificationRequest, event: LifeEvent, sampled_dat
"date": sampled_date, "precision": "day", "summary": event.get("summary", ""),
}],
}
for key in ("ayanamsa", "node_mode"):
if key in request:
legacy_request[key] = request[key]
return legacy_request
def _canonical(value: Any) -> str:
@@ -349,7 +353,9 @@ def calculation_spec(request: RectificationRequest) -> dict[str, Any]:
"latitude": json_number(request["lat"]),
"longitude": json_number(request["lon"]),
"timezoneOffsetHours": json_number(request["tz"]),
"ayanamsa": "raman", "nodeMode": "mean", "minuteStep": 1,
"ayanamsa": request.get("ayanamsa", "raman"),
"nodeMode": request.get("node_mode", "mean"),
"minuteStep": 1,
}
for source, target in (
("birth_time_source", "birthTimeSource"),