feat(rectification): drive questions from candidate contrast
This commit is contained in:
@@ -11,7 +11,7 @@ DatePrecision = Literal["day", "month", "quarter", "year", "range"]
|
||||
SCOREABLE_EVENT_KINDS: dict[str, frozenset[str]] = {
|
||||
"education": frozenset({"education_milestone"}),
|
||||
"relocation": frozenset({"relocation"}),
|
||||
"relationship": frozenset({"relationship_start", "relationship_end", "relationship_change"}),
|
||||
"relationship": frozenset({"relationship_start", "relationship_change"}),
|
||||
"career": frozenset({"career_change"}),
|
||||
"finance": frozenset({"finance_change"}),
|
||||
"health_pressure": frozenset({"self_health_event"}),
|
||||
|
||||
@@ -40,6 +40,48 @@ def _subtract(rows: Sequence[CandidateScoreRow], removed_ids: set[str]) -> list[
|
||||
return [{**row, "score": round(row["score"] - sum(item["points"] for item in row["evidence"] if item["event_id"] in removed_ids), 4)} for row in rows]
|
||||
|
||||
|
||||
def _candidate_feature_contrast(built: dict[str, Any], primary_time: str, secondary_time: str) -> list[str]:
|
||||
features = {
|
||||
value["time"]: value
|
||||
for context in built.get("static_contexts") or []
|
||||
if isinstance((value := context.get("feature")), dict) and isinstance(value.get("time"), str)
|
||||
}
|
||||
primary = features.get(primary_time)
|
||||
secondary = features.get(secondary_time)
|
||||
if not primary or not secondary:
|
||||
return []
|
||||
layers = []
|
||||
for section in ("varga_ascendants", "arudha_signs"):
|
||||
primary_values = primary.get(section) or {}
|
||||
secondary_values = secondary.get(section) or {}
|
||||
layers.extend(
|
||||
key for key in set(primary_values) | set(secondary_values)
|
||||
if primary_values.get(key) != secondary_values.get(key)
|
||||
)
|
||||
fingerprints = (("ashtakavarga", "Ashtakavarga"), ("shadbala", "Shadbala"))
|
||||
primary_fingerprints = primary.get("fingerprints") or {}
|
||||
secondary_fingerprints = secondary.get("fingerprints") or {}
|
||||
layers.extend(
|
||||
layer for key, layer in fingerprints
|
||||
if primary_fingerprints.get(key) != secondary_fingerprints.get(key)
|
||||
)
|
||||
return sorted(set(layers))[:8]
|
||||
|
||||
|
||||
def _candidate_contrast(built: dict[str, Any], primary_time: str, secondary_time: str) -> tuple[list[str], list[str]]:
|
||||
event_deltas: list[tuple[float, str]] = []
|
||||
for event_id, candidates in built["matrix"].items():
|
||||
primary = candidates.get(primary_time)
|
||||
secondary = candidates.get(secondary_time)
|
||||
if not primary or not secondary:
|
||||
continue
|
||||
delta = abs(float(primary["points"]) - float(secondary["points"]))
|
||||
if delta > 1e-9:
|
||||
event_deltas.append((delta, event_id))
|
||||
events = [event_id for _, event_id in sorted(event_deltas, key=lambda item: (-item[0], item[1]))]
|
||||
return _candidate_feature_contrast(built, primary_time, secondary_time), events
|
||||
|
||||
|
||||
def run_diagnostics(request: RectificationRequest, rows: list[CandidateScoreRow], built: dict[str, Any]) -> dict[str, Any]:
|
||||
primary = set(_primary_cluster(rows))
|
||||
event_runs = []
|
||||
@@ -74,11 +116,12 @@ def run_diagnostics(request: RectificationRequest, rows: list[CandidateScoreRow]
|
||||
clusters = [_primary_cluster(rows)]
|
||||
candidate_splits = []
|
||||
if secondary and clusters[0]:
|
||||
contrast_layers, contrast_event_ids = _candidate_contrast(built, top[0]["time"], secondary["time"])
|
||||
candidate_splits.append({
|
||||
"left_cluster": {"start": clusters[0][0], "end": clusters[0][-1]},
|
||||
"right_cluster": {"start": secondary["time"], "end": secondary["time"]},
|
||||
"technique_layers": [name for name, _ in sorted(layers.items(), key=lambda item: item[1], reverse=True)[:8]],
|
||||
"event_ids": [item["event_id"] for item in secondary["evidence"] if item["points"] != 0],
|
||||
"technique_layers": contrast_layers,
|
||||
"event_ids": contrast_event_ids,
|
||||
})
|
||||
return {
|
||||
"primary_cluster_retention_rate": 1.0 if primary else 0.0,
|
||||
|
||||
@@ -11,7 +11,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
|
||||
|
||||
ALGORITHM_VERSION = "rectification-v5-matrix-scoring-1"
|
||||
ALGORITHM_VERSION = "rectification-v5-matrix-scoring-2"
|
||||
INPUT_CONTRACT_VERSION = "rectification-calculation-spec-v4"
|
||||
|
||||
|
||||
@@ -82,6 +82,42 @@ def _cached_rows(serialized: str) -> tuple[CandidateScoreRow, ...]:
|
||||
return tuple(compute_event_candidate_rows(json.loads(serialized)))
|
||||
|
||||
|
||||
_RELATIONSHIP_SUPPORT_RULES = (
|
||||
"functional_benefic_auxiliary",
|
||||
"arudha_auxiliary",
|
||||
"ashtakavarga_target_house_support_auxiliary",
|
||||
"shadbala_sthana_drik_naisargika_support_auxiliary",
|
||||
"controlled_transit_jupiter_domain_house",
|
||||
)
|
||||
_RELATIONSHIP_CHANGE_RULES = (
|
||||
"functional_malefic_auxiliary",
|
||||
"ashtakavarga_target_house_pressure_auxiliary",
|
||||
"shadbala_sthana_drik_naisargika_pressure_auxiliary",
|
||||
"controlled_transit_saturn_domain_house",
|
||||
)
|
||||
|
||||
|
||||
def _relationship_kind_factor(event_kind: str, rule_ids: Sequence[str]) -> float:
|
||||
if event_kind not in {"relationship_start", "relationship_change"}:
|
||||
return 1.0
|
||||
support = sum(any(rule.endswith(marker) for marker in _RELATIONSHIP_SUPPORT_RULES) for rule in rule_ids)
|
||||
change = sum(any(rule.endswith(marker) for marker in _RELATIONSHIP_CHANGE_RULES) for rule in rule_ids)
|
||||
direction = support - change if event_kind == "relationship_start" else change - support
|
||||
return max(0.8, min(1.2, 1 + 0.08 * direction))
|
||||
|
||||
|
||||
def _kind_adjusted_evidence(event: LifeEvent, evidence: dict[str, Any]) -> dict[str, Any]:
|
||||
if event["domain"] != "relationship":
|
||||
return evidence
|
||||
event_kind = event["event_kind"]
|
||||
rules = list(evidence["rule_ids"])
|
||||
return {
|
||||
**evidence,
|
||||
"rule_ids": [*rules, f"event_kind_profile:{event_kind}"],
|
||||
"points": round(float(evidence["points"]) * _relationship_kind_factor(event_kind, rules), 4),
|
||||
}
|
||||
|
||||
|
||||
def build_event_contribution_matrix(
|
||||
request: RectificationRequest,
|
||||
row_provider: Callable[[dict[str, Any]], Sequence[CandidateScoreRow]] | None = None,
|
||||
@@ -94,7 +130,14 @@ def build_event_contribution_matrix(
|
||||
candidate_grid: list[str] | None = None
|
||||
for event in request["events"]:
|
||||
samples = sample_event_dates(event)
|
||||
sample_rows = [list(provider(_legacy_request(request, event, sampled))) for sampled in samples]
|
||||
sample_rows = []
|
||||
for sampled in samples:
|
||||
rows = list(provider(_legacy_request(request, event, sampled)))
|
||||
sample_rows.append([
|
||||
{**row, "score": adjusted["points"], "evidence": [adjusted]}
|
||||
for row in rows
|
||||
for adjusted in [_kind_adjusted_evidence(event, row["evidence"][0])]
|
||||
])
|
||||
grids = [[row["time"] for row in rows] for rows in sample_rows]
|
||||
if any(grid != grids[0] for grid in grids[1:]) or (candidate_grid is not None and grids[0] != candidate_grid):
|
||||
raise ValueError("candidate_grid_mismatch")
|
||||
@@ -109,7 +152,12 @@ def build_event_contribution_matrix(
|
||||
matrix[event["id"]][candidate_time] = {
|
||||
"points": round(sum(points) / len(points), 4),
|
||||
"rule_ids": sorted({rule for item in evidences for rule in item["rule_ids"]}),
|
||||
"technique_layers": sorted({rule.split(":", 1)[0] for item in evidences for rule in item["rule_ids"]}),
|
||||
"technique_layers": sorted({
|
||||
rule.split(":", 1)[0]
|
||||
for item in evidences
|
||||
for rule in item["rule_ids"]
|
||||
if not rule.startswith(("event_kind:", "event_kind_profile:"))
|
||||
}),
|
||||
}
|
||||
winner = max(set(winners), key=winners.count)
|
||||
mean = sum(matrix[event["id"]][time]["points"] for time in candidate_grid) / len(candidate_grid)
|
||||
|
||||
Reference in New Issue
Block a user