Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6922e2f96c | ||
|
|
11fee0a3e9 | ||
|
|
ad89e410e5 |
+4
-2
@@ -17569,11 +17569,13 @@
|
||||
|
||||
## BUG-1308 | 父母静态是非题(D12)对出生时间分界两侧有没有区分力未经评测
|
||||
|
||||
- 状态:open(研究单 `docs/tasks/TASK-rectification-d12-parent-probes-research-20261012.md` 待领取)
|
||||
- 状态:closed_by_design(研究欠力,方向暂停。2026-10-12 研究跑完:计入案例低于预登记的 60,不是不上线的终局,也不是有功效的「无效」。不能据此改题库或计分。编码对不上原句的问题记在研究报告,不改已有编码。方向暂停,不再跑同一份维基。没有交付修复,不是 resolved)
|
||||
- 首次发现 / 最近更新:2026-10-12 / 2026-10-12
|
||||
- 来源:同 BUG-1307 的上游样例:上游用五道父母是非题区分 D12 换段两侧。同一输入上游两次结论相反,下结论的那次靠单条父母事实的事后解读。
|
||||
- 现象:我方无静态父母题;上游 10-02 的 D12 绑定(`bbb16115` 等)针对带日期的父母事件,静态题两边都没有评测。
|
||||
- 决策:先研究,公开案例、预登记、乱序与正对照,过门才立实现单;过门前不进题库、不计分。
|
||||
- 决策(2026-10-12,F4):方向暂停。不立实现单,不再用同一份维基重跑。被否决的替代是换传记或访谈、两名独立编码员、另开一轮预登记。状态是 closed_by_design,不是 resolved。暂停理由沿用上面的本轮数字,不另算。
|
||||
- 本轮结果:77 个公开案例都编码了。至少 4 道是/否的只有 6 例。结果 `decision=control_underpowered`。四个格子的正对照 `pipeline_exact` 都为真,但两侧不同的案例最少 5,低于预登记的 20;父宫 9 的对照精确 p 是 0.110000,没有四个格子都低于 0.01。真实一侧选中:父 9 为 4/6(p 0.309000),父 10 为 1/6(低于乱序均值)。岁差改变比例 0.361111 与 0.416667,高于 0.2。数字见 `docs/research/rectification_d12_parent_probes_2026_10_12.md` 与同目录结果 JSON。预登记提交在评测之前,评测后未改判据。
|
||||
- 相关记录:BUG-1306、BUG-1307、BUG-1141 / BUG-1240(同类研究均未过门)。
|
||||
- 复发自:无。
|
||||
- 修复版本:不适用(研究)。
|
||||
- 修复版本:不适用
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"id": "d12_parent_probes_preregistration_2026_10_12",
|
||||
"bug": "BUG-1308",
|
||||
"task": "docs/tasks/TASK-rectification-d12-parent-probes-research-20261012.md",
|
||||
"locked_before_scoring": true,
|
||||
"baseline_commit": "e6baff95",
|
||||
"does_not_overturn": [
|
||||
"docs/research/rectification_angle_timing_2026_10_01.md 与 docs/research/rectification_nadi_seconds_2026_10_05.md 里不带年份的题、以及分钟级时间技法信号弱的结论。本登记只检验静态父母是非题对 D12 上升换段两侧有没有区分力。",
|
||||
"上游 bbb16115 / fe56dd84 绑的是带日期的父母事件。本登记不搬那些代码,只借用「父亲主宫默认 9、北印备选 10」这个口径。"
|
||||
],
|
||||
"birth_records": {
|
||||
"file": "references/real_case_calibration/minute_rectification_holdout_v5.json",
|
||||
"expected_case_count": 77,
|
||||
"case_id_sha256": "e3acb0c9c8005c0c1f88ce7f37b5e274d3fcf5346b7c9f0f441089aa06687ee5",
|
||||
"case_id_sha256_note": "sha256 of the sorted case_id values joined by newline, UTF-8, no trailing newline after the last id.",
|
||||
"roster_file_on_this_commit": "references/real_case_calibration/holdout_v5_roster.json lists 57 of these 77. The other 20 are the inherited v4 cases stored in the holdout file. Both are the tracked public Rodden-AA set. No birth field is added from outside these files.",
|
||||
"clock": "Recorded minute, second fixed at 0. Timezone is the file's timezone_offset, held fixed across the window. Do not re-infer an IANA zone.",
|
||||
"privacy": "Public figures only. No real-user birth data and no private sample."
|
||||
},
|
||||
"facts": {
|
||||
"file": "references/real_case_calibration/parent_facts_v1.json",
|
||||
"answers": ["yes", "no", "unclear"],
|
||||
"inclusion": "A case is scored only if all six probes are present and at least four answers are yes or no. Others are listed as excluded and are not imputed.",
|
||||
"source_grades": ["biography", "interview", "encyclopedia"],
|
||||
"source_rule": "Every scored fact needs a URL opened for this study and a verbatim excerpt. Silent sources stay unclear. Conflicting opened sources stay unclear. Do not code a fact from an unopened page.",
|
||||
"coding": {
|
||||
"mother_more_daily_care": {
|
||||
"yes": "The source says the mother raised the child, was the main day-to-day caregiver, or the child lived with the mother and not the father during childhood.",
|
||||
"no": "The source says the father raised the child, or the child lived with the father and not the mother.",
|
||||
"unclear": "Otherwise. Naming both parents, or saying they lived together, is not a comparison and stays unclear. A parent's death is not enough unless the source says who then did the daily care."
|
||||
},
|
||||
"father_financial_lead": {
|
||||
"yes": "The source says the father supported the family, was the breadwinner, or the household lived on his income or business.",
|
||||
"no": "The source says the mother or another named person was the breadwinner, or the father did not support the family.",
|
||||
"unclear": "Otherwise. A job title alone is not financial leadership."
|
||||
},
|
||||
"father_guided_study": {
|
||||
"yes": "The source says the father taught the child, chose or insisted on the schooling, or supervised the studies.",
|
||||
"no": "The source says the father opposed the schooling, refused to support it, or was uninvolved while another named person directed it.",
|
||||
"unclear": "Otherwise. The father being educated, or a teacher of other people, is not enough. 'Parents sent the child to school' stays unclear."
|
||||
},
|
||||
"father_work_institutional": {
|
||||
"yes": "The principal occupation is military, civil service, elected or appointed government office, teacher, professor, academic, physician, surgeon, lawyer, judge, clergy of an organized religion, police, engineer, scientist, or a salaried post in a state, church, school, hospital, or firm where the source does not call him the owner.",
|
||||
"no": "A principal occupation is stated and it is trade, a self-owned business, farming the family's land, or a skill practiced on his own account, and the yes list is not the principal job.",
|
||||
"unclear": "Occupation unstated, or several jobs with no principal one."
|
||||
},
|
||||
"father_work_commerce": {
|
||||
"yes": "The principal occupation is merchant, trader, shopkeeper, owner-banker, industrialist, factory owner, company founder, or another self-owned business.",
|
||||
"no": "A principal occupation is stated and it is not commerce.",
|
||||
"unclear": "Occupation unstated. Both this probe and father_work_institutional may be yes when the source fits both clauses. Do not force one label."
|
||||
},
|
||||
"parents_long_apart": {
|
||||
"yes": "Before the subject was 18, the source says the parents lived in different places for a prolonged period, separated, divorced, or one parent was away from the family home for years.",
|
||||
"no": "The source says the parents lived together through childhood, or explicitly did not separate.",
|
||||
"unclear": "Otherwise. Death of a parent is not geographic separation by itself."
|
||||
}
|
||||
}
|
||||
},
|
||||
"engine": {
|
||||
"chart": "domain_calculation_service.compute_chart",
|
||||
"varga": "varga.calc_varga(longitude, 12) in classical_local mode, field sign_idx",
|
||||
"longitude_field": "lon",
|
||||
"d1_sign": "int(longitude // 30) % 12 for the ascendant; planet sign_idx from compute_chart for D1 bodies",
|
||||
"house_system": "whole sign",
|
||||
"house_of_sign": "(body_sign_idx - lagna_sign_idx) % 12 + 1",
|
||||
"lord_of_house": "SIGN_LORDS of SIGNS[(lagna_sign_idx + house - 1) % 12], from scripts/varga.py",
|
||||
"do_not_use": [
|
||||
"the D1 planet house field",
|
||||
"bhava cusps",
|
||||
"a newly written ephemeris",
|
||||
"vedastro varga mode"
|
||||
],
|
||||
"settings": {
|
||||
"baseline_ayanamsa": "raman",
|
||||
"baseline_node_mode": "mean",
|
||||
"ayanamsa_alt": "lahiri",
|
||||
"node_alt": "true"
|
||||
},
|
||||
"ephemeris_version": "Record swisseph version in the results. Do not choose a version in order to pass."
|
||||
},
|
||||
"grid": {
|
||||
"radii_minutes": [10, 30],
|
||||
"step_minutes": 1,
|
||||
"offsets": "Every integer from -radius through +radius, inclusive. Local clock plus the offset, date may roll.",
|
||||
"boundary": "A cut sits between offset o and o+1 when both are on the grid and their D12 lagna sign indexes differ. The representative minutes are o and o+1.",
|
||||
"nearest_cut": "Minimum of min(|o|, |o+1|); ties break by smaller |o|; then by smaller o.",
|
||||
"true_side": "The representative minute whose D12 lagna sign index equals the sign index at offset 0. The other representative is the other side. If that assignment is not exactly one minute, abort the case as a runner error. Do not guess.",
|
||||
"no_cut": "If the window has no cut, the case is no_boundary and is outside the selection denominator.",
|
||||
"segment": "A maximal run of offsets with one D12 lagna sign. Its representative is the offset in the run with smallest absolute value, ties taking the smaller offset."
|
||||
},
|
||||
"profiles": {
|
||||
"father_9": {
|
||||
"chart": "D12",
|
||||
"father_house": 9,
|
||||
"mother_house": 4,
|
||||
"note": "Parashari default, the upstream fe56dd84 main profile. Father indicators are the Sun and the lord of house 9. Mother indicators are the Moon and the lord of house 4."
|
||||
},
|
||||
"father_10": {
|
||||
"chart": "D12",
|
||||
"father_house": 10,
|
||||
"mother_house": 4,
|
||||
"note": "North-Indian alternate stored by fe56dd84. Father indicators are the Sun and the lord of house 10. Mother side stays house 4."
|
||||
},
|
||||
"d1_father_9": {
|
||||
"chart": "D1",
|
||||
"father_house": 9,
|
||||
"mother_house": 4,
|
||||
"note": "Control only. Same six formulas, read on the rasi chart. Not part of the gate. There is no D1 10th-father profile."
|
||||
}
|
||||
},
|
||||
"probes": [
|
||||
"mother_more_daily_care",
|
||||
"father_financial_lead",
|
||||
"father_guided_study",
|
||||
"father_work_institutional",
|
||||
"father_work_commerce",
|
||||
"parents_long_apart"
|
||||
],
|
||||
"stems_zh": {
|
||||
"mother_more_daily_care": "母亲是否比父亲更多负责日常照料",
|
||||
"father_financial_lead": "父亲是否是家庭财务的主要来源",
|
||||
"father_guided_study": "父亲是否指导过本人的学业",
|
||||
"father_work_institutional": "父亲的主要职业是否属于体制内或受雇专业",
|
||||
"father_work_commerce": "父亲的主要职业是否是自己经商",
|
||||
"parents_long_apart": "父母在童年是否长期分隔两地"
|
||||
},
|
||||
"reading_rules": {
|
||||
"shared": {
|
||||
"unique_indicators": "If the karaka and the house lord are the same planet, the indicator set contains that planet once.",
|
||||
"kendra": [1, 4, 7, 10],
|
||||
"conjunct": "Same sign index in the chart being read.",
|
||||
"career_house": "C = ((father_house - 1 + 9) % 12) + 1. This is the 10th from the father house. father_house 9 gives 6. father_house 10 gives 7.",
|
||||
"separated_signs": "For sign indexes a and b, d = (b - a) % 12. The pair is separated when d is 1, 5, 7, or 11. The same sign is not separated. The same planet is not separated from itself.",
|
||||
"labels": ["yes", "no", "abstain"]
|
||||
},
|
||||
"mother_more_daily_care": "Let Mk be how many distinct mother indicators sit in a kendra, and Fk the same count for father indicators. yes when Mk > Fk. no when Fk > Mk. If the counts are equal: yes when the Moon is in a kendra and the Sun is not; no when the Sun is in a kendra and the Moon is not; otherwise abstain. This is the only probe that may abstain.",
|
||||
"father_financial_lead": "yes when any father indicator is in house 2 or house 11, or the lord of house 2 is in the father house, or the lord of house 2 is conjunct any father indicator. no otherwise.",
|
||||
"father_guided_study": "yes when any father indicator is in house 5, or the lord of house 5 is in the father house, or Jupiter is in the father house, or Jupiter is conjunct any father indicator. no otherwise.",
|
||||
"father_work_institutional": "yes when Saturn or the Sun occupies C, or the lord of C is Saturn or the Sun. no otherwise.",
|
||||
"father_work_commerce": "yes when Mercury or Venus occupies C, or the lord of C is Mercury or Venus. no otherwise.",
|
||||
"parents_long_apart": "yes when the Sun and Moon are a separated pair, or the lord of house 4 and the father-house lord are a separated pair, or Rahu occupies house 4 or the father house. no otherwise. Rahu is the planet named Rahu in compute_chart. If that key is missing, the Rahu clause is false. Do not substitute another body."
|
||||
},
|
||||
"scoring": {
|
||||
"counted_probe": "The biographical answer is yes or no, and this side's prediction is yes or no. unclear and abstain are not counted.",
|
||||
"side_score": "matches minus mismatches among counted probes. A side with no counted probe scores 0.",
|
||||
"true_side_selected": "The true side's score is strictly greater than the other side's score.",
|
||||
"tie": "Equal scores. A tie is not a selection.",
|
||||
"selection_rate": "true_side_selected count divided by the number of included cases that have a cut in that window. The denominator does not drop ties.",
|
||||
"head_segment_hit": "Among segments in the same window, the true segment's score is strictly greater than every other segment's score. Not a gate.",
|
||||
"true_segment_retained": "The true segment's score is greater than or equal to every other segment's score. Not a gate.",
|
||||
"segment_denominator": "Included cases that have a cut. A one-segment window is no_boundary and is outside both segment rates."
|
||||
},
|
||||
"permutation": {
|
||||
"n": 999,
|
||||
"seed": 20261012,
|
||||
"algorithm": "Fisher-Yates driven by random.Random(seed).randrange. Do not call Random.shuffle. One stream builds 999 permutations of the included case_id list sorted alphabetically. The same 999 assignments are reused for every profile and both radii.",
|
||||
"unit": "The whole six-answer vector moves with the case. Probes are not shuffled separately.",
|
||||
"p": "(count of permutation rates greater than or equal to the observed rate, plus 1) divided by 1000. Compare this exact ratio. Do not compare a rounded display.",
|
||||
"null_mean": "Arithmetic mean of the 999 permutation rates.",
|
||||
"percentile": "Count of permutation rates strictly below the observed rate, divided by 999. Report it. The gate does not use the percentile."
|
||||
},
|
||||
"positive_control": {
|
||||
"seed": 20261013,
|
||||
"n": 999,
|
||||
"construction": "Ignore the biographical answers. For each probe, if the true side predicts yes or no, plant that label; if it abstains, plant unclear. Then use the same selection statistic and the same Fisher-Yates procedure with the control seed.",
|
||||
"pipeline_exact": "Among included cases that have a cut and whose two sides differ on at least one probe the true side does not abstain, every case must select the true side.",
|
||||
"minimum_differing_cases": 20,
|
||||
"permutation_p_max": 0.01,
|
||||
"failure": "If pipeline_exact fails, or differing cases are under 20, or the planted rate's exact p is not below 0.01, the control failed. Do not call the study a pass, and do not call the astrology result 不上线. Write 欠力 for an under-powered control and 结论无效 for a pipeline that cannot see a planted signal."
|
||||
},
|
||||
"family": {
|
||||
"cells": [
|
||||
["father_9", 10],
|
||||
["father_9", 30],
|
||||
["father_10", 10],
|
||||
["father_10", 30]
|
||||
],
|
||||
"bonferroni_alpha": 0.0125,
|
||||
"cell_significant": "Exact permutation p is strictly below 0.0125, and the observed selection rate is strictly greater than that cell's null mean.",
|
||||
"d1_not_in_family": true
|
||||
},
|
||||
"robustness": {
|
||||
"measured_on": "The baseline cell's two representative minutes. Recompute that profile's six labels at those same clock offsets under lahiri/mean and under raman/true.",
|
||||
"change": "A label in {yes, no, abstain} differs.",
|
||||
"denominator": "Boundary cases times 2 sides times 6 probes.",
|
||||
"gate_max_change_rate": 0.2,
|
||||
"node_clause": "Parents_long_apart is the only probe that reads Rahu. Still count all six probes in the denominator.",
|
||||
"concession": "If included cases are under 60 and true-node charts cannot be computed, report the blockage and apply the 0.20 line to the ayanamsa switch only. Do not waive the ayanamsa line. Do not waive either line when included cases are at least 60."
|
||||
},
|
||||
"gates": {
|
||||
"sample": "Included cases at least 60. Below that the status is 欠力 even if every numeric line is met. 欠力 is not 不上线 and is not 过门.",
|
||||
"discrimination": "At least one family cell is cell_significant.",
|
||||
"control": "The positive control meets pipeline_exact, the differing-case minimum, and permutation_p_max.",
|
||||
"robustness": "Every cell_significant cell has both change rates strictly below 0.20, subject to the small-sample node concession.",
|
||||
"pass": "sample AND discrimination AND control AND robustness. Only a pass may recommend a question, a score, or a tie-break attached to the nearest-boundary opening.",
|
||||
"fail_closed": "Included cases at least 60, the control passed, and either no cell is significant or a significant cell is not robust. Write 不上线. Do not recommend an implementation."
|
||||
},
|
||||
"not_a_gate": [
|
||||
"d1_father_9",
|
||||
"head_segment_hit",
|
||||
"true_segment_retained",
|
||||
"per-probe accuracy",
|
||||
"the permutation percentile",
|
||||
"any rule, threshold, or seed written down after the results were seen"
|
||||
],
|
||||
"after_the_fact": "A change to a reading rule, the coding manual, a seed, the family, or the alpha after results exist is a new round. Keep the old results. The new round cannot inherit this round's gate."
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,112 @@
|
||||
# D12 父母静态是非题:分界两侧有没有区分力(2026-10-12)
|
||||
|
||||
结论:**欠力**。预登记要求至少 60 个可计分案例。本轮打开了 77 个公开页,按锁定的编码规则只有 **6** 例至少有 4 道题答成「是」或「否」。结果文件的 `decision` 是 `control_underpowered`,`decision_note` 是「欠力」。
|
||||
|
||||
这不是「不上线」的终局,也不是「这套题无效」。样本不够,不能据此改题库、计分或平局裁决。先前「不带年份的题信号弱」「分钟级时间技法信号弱」的结论也不被本轮推翻或改写。
|
||||
|
||||
给产品的一句口径:这轮公开传记里,能严格答成是非、并且够四题的父母静态题只有 6 例,不到事先定的 60 例,所以还不能决定这种题上不上校正。下一步是暂停这条方向,不是再跑同一份维基。
|
||||
|
||||
## 这轮做了什么
|
||||
|
||||
- 预登记 `docs/research/d12_parent_probes_preregistration_2026_10_12.json` 单独提交在评测之前(`ad89e410`)。评测之后没有改判据、种子、题干或编码手册。
|
||||
- 出生记录只用 `minute_rectification_holdout_v5.json` 里已有的 77 个公开 Rodden AA 案例。没有从外面补出生时间。
|
||||
- 父母事实在 `references/real_case_calibration/parent_facts_v1.json`。每题都有打开过的 https 页面、等级 `encyclopedia`(英文维基百科)和原句。来源表是该文件的 `source_table`。拿不到原句的题记「不清楚」,不编造。
|
||||
- 盘用 `domain_calculation_service.compute_chart`,D12 用 `varga.calc_varga` 的 `classical_local`。没有新写星历。基线是 Raman / mean node。对照是 Lahiri / mean,以及 Raman / true node。星历版本写在结果里:`2.10.03`。
|
||||
- 结果:`docs/research/d12_parent_probes_results_2026_10_12.json`。`PYTHONHASHSEED=0` 下连跑两次,文件 SHA256 都是 `cbf324567ed8ddf14ac4769c4f8fb90f015aeab6711b2022a81b473f81748fe8`。
|
||||
|
||||
## 不清楚率
|
||||
|
||||
分母是结果文件里的 77 条已编码案例(`unclear_rate_coded`)。六位小数用本轮同一个 `display_ratio`,由文件里的计数打出,不是另算的一套率。
|
||||
|
||||
| 题 | 是 | 否 | 不清楚 | 不清楚率 |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| 母亲更多日常照料 | 9 | 1 | 67 | 67/77 = 0.870130 |
|
||||
| 父亲经济主导 | 3 | 2 | 72 | 72/77 = 0.935065 |
|
||||
| 父亲指导学业 | 11 | 1 | 65 | 65/77 = 0.844156 |
|
||||
| 父亲体制内职业 | 24 | 12 | 41 | 41/77 = 0.532468 |
|
||||
| 父亲自己经商 | 14 | 38 | 25 | 25/77 = 0.324675 |
|
||||
| 父母长期分开 | 23 | 1 | 53 | 53/77 = 0.688312 |
|
||||
|
||||
职业两题相对能答。照料、谁养家、谁管学业,多数页面没有对上预登记的句子,所以保持不清楚。不满 4 道是/否的 71 例记在 `excluded_few_decisive`,不插值。`not_coded` 是空的:77 例都进了事实文件。
|
||||
|
||||
计入的 6 例(`included_case_ids`):`barack_obama_1961_aa_v4_holdout`、`donald_trump_1946_aa_v5_holdout`、`imelda_marcos_1929_aa_v5_holdout`、`kamala_harris_1964_aa_v5_holdout`、`rafael_nadal_1986_aa_v5_holdout`、`sophia_loren_1934_aa_v5_holdout`。
|
||||
|
||||
## 真实一侧选中率
|
||||
|
||||
6 例在 ±10 和 ±30 分钟里都有 D12 上升换段(`no_boundary` 都是 0)。最近分界的两侧分钟在两个窗口里是同一对,所以选中率相同。平局不算选中。
|
||||
|
||||
| 格子 | 选中 | 显示 | 乱序均值 | 高于均值 | 精确 p | 低于真实值的乱序比例 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 父 9,±10 | 4/6 | 0.666667 | 0.490157 | 是 | 309/1000 = 0.309000 | 691/999 = 0.691692 |
|
||||
| 父 9,±30 | 4/6 | 0.666667 | 0.490157 | 是 | 309/1000 = 0.309000 | 691/999 = 0.691692 |
|
||||
| 父 10,±10 | 1/6 | 0.166667 | 0.338672 | 否 | 946/1000 = 0.946000 | 54/999 = 0.054054 |
|
||||
| 父 10,±30 | 1/6 | 0.166667 | 0.338672 | 否 | 946/1000 = 0.946000 | 54/999 = 0.054054 |
|
||||
|
||||
四个格子的 `significant` 都是 false。Bonferroni 线是 p < 0.0125 且高于乱序均值。父 9 的 p 是 0.309000,远在线的另一侧。父 10 的真实选中率低于乱序均值。
|
||||
|
||||
同窗全部 D12 段(不是过门项):
|
||||
|
||||
| 格子 | 头段命中 | 真值段保留 |
|
||||
| --- | --- | --- |
|
||||
| 父 9,±10 | 4/6 = 0.666667 | 4/6 = 0.666667 |
|
||||
| 父 9,±30 | 2/6 = 0.333333 | 4/6 = 0.666667 |
|
||||
| 父 10,±10 | 0/6 = 0.000000 | 2/6 = 0.333333 |
|
||||
| 父 10,±30 | 0/6 = 0.000000 | 1/6 = 0.166667 |
|
||||
|
||||
D1 只作对照,不是门。D1 父 9 在 ±10 和 ±30 的真实一侧选中都是 0/6(显示 0.000000)。记录分钟两侧的 D1 读法没有把真实一侧选出来。
|
||||
|
||||
## 乱序和正对照
|
||||
|
||||
乱序:999 次,种子 20261012,整组答案在计入案例之间对调。上表的 p 和分位就是这 999 次。
|
||||
|
||||
正对照:把真实一侧的是/否埋进答案(弃权改成不清楚),种子 20261013。四个格子的 `pipeline_exact` 都是 true,`missed` 都是空的。两侧读法不同的案例,流程都选中了真实一侧。
|
||||
|
||||
| 格子 | 两侧不同的案例 | 埋入后选中 | p | p < 0.01 |
|
||||
| --- | ---: | --- | --- | --- |
|
||||
| 父 9,±10 与 ±30 | 5 | 5/6 = 0.833333 | 110/1000 = 0.110000 | 否 |
|
||||
| 父 10,±10 与 ±30 | 6 | 6/6 = 1.000000 | 5/1000 = 0.005000 | 是 |
|
||||
|
||||
预登记还要求:不同案例至少 20,并且四个格子的对照 p 都低于 0.01。这里最少只有 5,父 9 的对照 p 是 0.110000。所以对照是欠力,不是「流程测不出」。测不出才会记成结论无效。
|
||||
|
||||
## 岁差和交点
|
||||
|
||||
分母是 6 个有分界的案例 × 两侧分钟 × 6 题 = 72。门是改变比例严格小于 0.2。
|
||||
|
||||
| 格子 | Lahiri 相对 Raman | 真交点相对平均交点 | 两样都低于 0.2 |
|
||||
| --- | --- | --- | --- |
|
||||
| 父 9 | 26/72 = 0.361111 | 1/72 = 0.013889 | 否 |
|
||||
| 父 10 | 30/72 = 0.416667 | 0/72 = 0.000000 | 否 |
|
||||
|
||||
交点几乎不改判读。岁差改判读的比例高于 20% 线。四个格子的 `robust` 都是 false。即便选中率以后在更大样本里显著,这一条岁差结果也不能单独当成过门。
|
||||
|
||||
## 判定
|
||||
|
||||
过门需要:计入至少 60,正对照达到预登记的力度,并且至少一个家族格子既显著又稳健。本轮三条都没有。
|
||||
|
||||
不写实现建议。题干、计分、平局裁决都不接。这是欠力,不是做完后的否定结论。方向见下一节,上面的数字不改。
|
||||
|
||||
编码按预登记从紧:只写职业头衔不算养家,也不算父亲亲自管学业;父母一起送学不算;死亡本身不算两地分开;页面没写出主业就保持不清楚。名誉领事这类短任期、页面没有写成主业的,没有记成体制内。这些取舍都在看分数之前定死,看完分数后没有为了凑 60 例而放宽。下一节核对之后,短任期这一条并没有在每例上用同一把尺子。
|
||||
|
||||
## 下一步:方向暂停
|
||||
|
||||
结论:**方向暂停**。上一节的判定保持不变:欠力,77 个公开案例计入 6,不是有功效的「无效」,也不是终局「不上线」。不改题库,不计分,不改预登记,也不改 `parent_facts_v1.json` 里已经写下的编码。
|
||||
|
||||
### 编码问题清单
|
||||
|
||||
下面每条都只对照事实文件里已经存着的原句。原句对不上修复单里的说法时,写文件实际写了什么,不另补传记。
|
||||
|
||||
1. 奥巴马(`barack_obama_1961_aa_v4_holdout`)`mother_more_daily_care` 记成 yes。这条的原句是:「Francis of Assisi Catholic Elementary School) for two years and Sekolah Dasar Negeri Menteng 01 (State Elementary School Menteng 01) for one and a half years, supplemented by English-language Calvert School homeschooling by his mother.」修复单说这句只谈上了哪所小学。文件不是这样:后半句还写了母亲用 Calvert School 做英语家庭补习。预登记的「是」要写成母亲抚养、母亲是主要日常照料者,或童年跟母亲住、不跟父亲住。这句两头都没有。记成 yes 仍然对不上预登记。他在计入的 6 例里,这里只记编码问题,不改 6 这个数。
|
||||
|
||||
2. 伊梅尔达·马科斯(`imelda_marcos_1929_aa_v5_holdout`)`father_work_institutional` 记成 yes。这条引文是:「Her parents were Vicente Orestes Romuáldez, a lawyer, and his second wife, María Remedios Trinidad who was born and raised in Baliwag, Bulacan.」它只说父亲是 a lawyer,没有说他自己开业。同一条案例里,财务题另有一句:「In November 1938, Imelda's father gave up living in Manila due to his declining fortunes in his law practice and returned to Tacloban, where he could support his family with a simpler lifestyle.」这句才出现 his law practice。预登记的「是」把 lawyer 放进体制内或受雇专业;「否」又把自营手艺算出去,条件是 yes 名单并不是主业。律师这个词在「是」的名单里,law practice 又像自己执业。两套说法在这一例上对不齐。不能把「他自己开业」写成体制内那条引文的原文。她在计入的 6 例里。编码不改,6 也不改。
|
||||
|
||||
3. 索菲亚·罗兰(`sophia_loren_1934_aa_v5_holdout`)`father_work_institutional` 记成 yes。原句是:「Her mother was a piano teacher and aspiring actress, and her father a failed engineer who worked temporarily for the national railway Ferrovie dello Stato Italiane.」这句写的是 failed engineer,以及在国营铁路 Ferrovie dello Stato Italiane 的临时工作(worked temporarily)。预登记没有「短任期不算主业」这句。它写的是 principal occupation(主要职业);不清楚是职业没写,或几份工作里没有一份主业。本报告上一节把「名誉领事这类短任期、页面没有写成主业」说成不记体制内。奥黛丽·赫本(`audrey_hepburn_1929_aa_v5_holdout`)的 `father_work_institutional` 记成 unclear,原句是:「He was an Honorary British Consul in Semarang, Dutch East Indies, from 1923 to 1924.」一年的名誉领事记成不清楚,罗兰这篇写明 temporarily 的国营铁路却记成 yes。两例不是同一把尺子。赫本不在计入的 6 例里。罗兰在。两处编码都不改。
|
||||
|
||||
4. 李小龙(`bruce_lee_1940_aa_v5_holdout`)`father_guided_study` 记成 yes。原句是:「Lee's first introduction to martial arts was through his father, from whom he learned the fundamentals of Wu-style tai chi.」这是父亲教吴氏太极的基本功,不是预登记写的教功课、选定或坚持学业、督导学习。他不在计入的 6 例里:六题里只有指导学业为 yes、自己经商为 no,其余不清楚,不满 4 道是/否。这里只把他当作已经排除的编码例子,不是计分命中,也不进 4/6 或 1/6。
|
||||
|
||||
### 不再跑同一份维基
|
||||
|
||||
被否决的另一条路:换新来源(传记或访谈),两名编码员各自编码,再开一轮新的预登记。本轮不走这条。
|
||||
|
||||
决定是方向暂停。维基这一轮 77 例只产出 6 例可计分,大约 8%(6/77)。按这个比例凑到预登记的 60,要大约 770 个同样能打开的 AA 案例,也就是 700 这个量级。这份 77 例的数据给不出。岁差改判读是 26/72 与 30/72,显示 0.361111 与 0.416667,大约 36% 和 42%,高于严格小于 0.2 的稳健性门。同一套设计把样本做大,仍不太可能过门。
|
||||
|
||||
BUG-1308 记为 closed_by_design(研究欠力,方向暂停)。没有交付修复,不是 resolved。修复版本仍是不适用。
|
||||
@@ -0,0 +1,57 @@
|
||||
# 进度:D12 父母静态题研究(BUG-1308)
|
||||
|
||||
结论:**欠力**。77 个公开案例都写了事实,计入 6,低于预登记的 60。`decision=control_underpowered`。不写不上线,不写实现建议。F4 把方向定为暂停,BUG-1308 改为 closed_by_design,不是 resolved。未推送。
|
||||
|
||||
## 范围
|
||||
|
||||
- 工作树:`codex/rectification-d12-parent-probes-research-20261012`,基线 `e6baff95`,无 upstream。
|
||||
- 只新增研究脚本、定向测试、研究文档、父母事实,以及本进度、BUG-1308 和 `docs/tasks/README.md` 的这一行。
|
||||
- 未改生产代码、打分、题库、`decision_policy`、算法身份、`.gitea`。未使用 stash。
|
||||
|
||||
## 预检
|
||||
|
||||
开工跑过 `scripts/pre_work_check.py`。该次因超时退出码 1(片段扫描与聚焦测试的时限),远端与适配器在同一次输出里没有失败。本进度不把预检写成通过。盘计算入口是现成的 `compute_chart` + `calc_varga`,没有新写星历,也没有改 `BLOCKED.md`。
|
||||
|
||||
## 预登记和数据
|
||||
|
||||
- 预登记单独提交:`ad89e410`(`research(rectification): preregister D12 parent probes (BUG-1308)`)。文件 SHA256 在结果里是 `dde1c584c58014f3e1a9ba30b6ee5c728746a3688dbb3556d6ec1a12e9bfac3e`。
|
||||
- 评测之后没有改判据。
|
||||
- 事实文件 SHA256:`1746b70102a025c412089e28ae7d250164587d02cd9d2cb0e86ee037bddca131`。
|
||||
- 来源都是本轮打开的英文维基百科,等级 encyclopedia。不清楚率见研究报告的表,计数来自结果 JSON 的 `unclear_rate_coded`。
|
||||
|
||||
## 评测
|
||||
|
||||
- Python:`C:\Users\74082\anaconda3\python.exe`(3.11.7)。仓库无 `.venv`。`PYTHONHASHSEED=0`。
|
||||
- 星历:Swiss Ephemeris `2.10.03`。
|
||||
- 结果两次运行 SHA256 相同:`cbf324567ed8ddf14ac4769c4f8fb90f015aeab6711b2022a81b473f81748fe8`。
|
||||
- 真实一侧:父 9 为 4/6(p 0.309000),父 10 为 1/6(p 0.946000,低于乱序均值)。±10 与 ±30 的最近分界是同一对分钟。
|
||||
- 正对照:四格 `pipeline_exact=true`。父 9 两侧不同 5 例,p 0.110000;父 10 两侧不同 6 例,p 0.005000。未达到「至少 20 且四格 p<0.01」。
|
||||
- 稳健性:岁差改变 26/72 与 30/72;交点改变 1/72 与 0/72。岁差不低于 0.2,`robust=false`。
|
||||
|
||||
## 测试
|
||||
|
||||
- `tests/test_d12_parent_probes.py`:15 项通过,退出码 0。
|
||||
- `tests/test_repo_privacy_markers.py`:退出码 0(在暂存之后跑,能扫到本轮新文件)。
|
||||
- `scripts/run_quality_gate.py --profile quick`:Python 段通过,其中 pytest 一步 1310.3 秒打勾。随后 `npm test` 退出码 1,因为这个工作树没有 `frontend/node_modules/tsx`。没有把依赖装进仓库。门禁停在这一步,lint 和 build 没有往下跑。这是环境缺口,不是本轮研究脚本的失败。
|
||||
|
||||
## 未做
|
||||
|
||||
- 不推送,不升 `main`,不改 workflow。
|
||||
- 不放宽编码去凑 60 例。评分当时写下的下一轮条件是:必须新的预登记,并保留本轮结果。F4 不采用这条,见下。
|
||||
|
||||
## F4 编码复核与方向暂停
|
||||
|
||||
结论:**方向暂停**。判定仍是欠力,计入 6/77,`decision=control_underpowered`。不是有功效的「无效」,也不是终局「不上线」。本分支没有 `TASK-rectification-1012-batch-fix-20261012.md`,按本轮交代写,不从别处摘任务书。
|
||||
|
||||
- 只改研究报告、本进度、BUG-1308、状态板这一行。未改生产代码、打分、题库、预登记 JSON、结果 JSON、`parent_facts_v1.json`。
|
||||
- 编码只对照事实文件里已经存着的原句。对不上的地方写文件实际写了什么。没有改编码,也没有新补传记。
|
||||
- 奥巴马照料题记成 yes 的原句是小学名称,句末还有母亲的 Calvert 家庭补习。不能写成「这句只谈小学」。预登记的「是」仍对不上:没有写成主要日常照料,也没有写成跟母亲住、不跟父亲住。他在计入的 6 例里,6 不改。
|
||||
- 马科斯体制内题记成 yes 的原句只写 a lawyer,没有写他自己开业。同一案例的财务题另有 his law practice。预登记的「是」含 lawyer,「否」含自营手艺。尺度不合。她在计入的 6 例里,编码不改。
|
||||
- 罗兰体制内题记成 yes 的原句是 failed engineer,以及国营铁路的临时工作。预登记没有「短任期不算主业」这句,写的是主要职业。赫本父亲 1923 到 1924 年的名誉领事记成 unclear,而且不在计入的 6 例里。两例不是同一把尺子。两处编码都不改。
|
||||
- 李小龙指导学业记成 yes 的原句是父亲教吴氏太极的基本功,不是学业。他不在计入的 6 例里:六题里是/否只有两道。只作已经排除的编码例子,不是计分命中,也不进 4/6 或 1/6。
|
||||
- 被否决的替代:换传记或访谈做新来源,两名编码员各自编码,再开一轮新的预登记。不采用。
|
||||
- 维基产出 6/77,大约 8%。按这个比例凑到预登记的 60,大约要 770 个 AA 案例,量级是 700。这份 77 例给不出。岁差改判读 26/72 与 30/72,显示 0.361111 与 0.416667,大约 36% 和 42%,高于严格小于 0.2 的门。同一套设计把样本做大,仍不太可能过门。
|
||||
- BUG-1308 改为 closed_by_design(研究欠力,方向暂停)。修复版本仍是不适用。不是 resolved。状态板只改 BUG-1308 这一行,没有引进别的看板。
|
||||
- 未改 `CHANGELOG.md`。仓库里没有 2026-10-12 的研究条目可追加一句。暂停的研究不是用户可见的技能变化。
|
||||
- 定向测试没有快照研究报告正文,所以没有改断言,也没有原值 / 新值 / 原因三栏。
|
||||
- `C:\Users\74082\anaconda3\python.exe -m pytest tests/test_repo_privacy_markers.py tests/test_d12_parent_probes.py -q --tb=line`:收集 78 项(隐私标记 63,D12 15),进度全是通过标记,到 100%,退出码 0。没有失败,没有跳过。
|
||||
@@ -126,7 +126,7 @@
|
||||
| `TASK-rectification-session-composer-guard-20260917.md` | `PROGRESS-rectification-session-composer-guard-20260917.md` | 校正会话激活但校正面未开(刷新揭幕超时 / 打开失败不重试 / 删除与 popstate 回退落到列表第一条)时露出可用的普通输入框,问题发到 `/api/consult` 回 404「咨询会话不存在」;`send()` 无会话类型守卫;服务端无独立错误码。关联 BUG-505。BUG 段 924 起 | 已验收 | `6d81062b`(Claude 验收:T1–T5 全通过;409 `session_not_consultation` 在扣点前、不扣点;回退路径多修了归档一条;page.tsx 1830→1830。残留 P3:列表只剩校正会话时 `activeSession` 为空,输入框可用但发送静默无效 → BUG-935 在 fix 单);真机三条欠 |
|
||||
| `TASK-rectification-explicit-entry-20261012.md` | — | **明说要校正就直接进入(BUG-1306)**:模型判断意图(禁正则),服务端判定后直接开案并绑定来源对话;首轮排盘合同对此轮豁免;拒绝必须一句话说明(推翻 BUG-1248 静默两条);普通对话不得假装校正、申报时段不写子运起止。**先做** | 待领取 | — |
|
||||
| `TASK-rectification-nearest-boundary-opening-20261012.md` | — | **最近分界开场(BUG-1307)**:开场说记录时间离最近分盘换段点多近、两侧哪些层不同,近分界时首轮只问该分界;不改打分 / 闸门 / 4 件 3 域,77 例回放不降;改 page.tsx / 校正开场组件的部分须在 BUG-1306 合入后 | 待领取 | — |
|
||||
| `TASK-rectification-d12-parent-probes-research-20261012.md` | — | **父母静态是非题(D12)研究(BUG-1308)**:公开案例 ≥60、预登记、乱序与正对照,过门才立实现单;纯研究可并行 | 待领取 | — |
|
||||
| `TASK-rectification-d12-parent-probes-research-20261012.md` | `PROGRESS-rectification-d12-parent-probes-research-20261012.md` | **父母静态是非题(D12)研究(BUG-1308)**:公开案例 ≥60、预登记、乱序与正对照,过门才立实现单;纯研究可并行 | 方向暂停,BUG-1308 closed_by_design | `docs/research/rectification_d12_parent_probes_2026_10_12.md` |
|
||||
|
||||
### 聊天主链路与首页
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,595 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Pre-registered D12 parent-probe scoring. Research only.
|
||||
|
||||
The reading rules live in
|
||||
``docs/research/d12_parent_probes_preregistration_2026_10_12.json``.
|
||||
This module does not compute an ephemeris. Callers pass longitudes from
|
||||
``domain_calculation_service.compute_chart`` and signs from ``varga.calc_varga``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from random import Random
|
||||
from typing import Any
|
||||
|
||||
_SCRIPTS = str(Path(__file__).resolve().parents[1])
|
||||
if _SCRIPTS not in sys.path:
|
||||
sys.path.insert(0, _SCRIPTS)
|
||||
|
||||
import varga
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
PREREG_PATH = ROOT / "docs" / "research" / "d12_parent_probes_preregistration_2026_10_12.json"
|
||||
HOLDOUT_PATH = ROOT / "references" / "real_case_calibration" / "minute_rectification_holdout_v5.json"
|
||||
FACTS_PATH = ROOT / "references" / "real_case_calibration" / "parent_facts_v1.json"
|
||||
RESULTS_PATH = ROOT / "docs" / "research" / "d12_parent_probes_results_2026_10_12.json"
|
||||
|
||||
PROBES: tuple[str, ...] = (
|
||||
"mother_more_daily_care",
|
||||
"father_financial_lead",
|
||||
"father_guided_study",
|
||||
"father_work_institutional",
|
||||
"father_work_commerce",
|
||||
"parents_long_apart",
|
||||
)
|
||||
KENDRA = frozenset({1, 4, 7, 10})
|
||||
SEPARATED_DELTAS = frozenset({1, 5, 7, 11})
|
||||
ANSWERS = frozenset({"yes", "no", "unclear"})
|
||||
PREDICTIONS = frozenset({"yes", "no", "abstain"})
|
||||
GRADES = frozenset({"biography", "interview", "encyclopedia"})
|
||||
ALPHA = Decimal("0.0125")
|
||||
ROBUST_MAX = Decimal("0.2")
|
||||
CONTROL_P_MAX = Decimal("0.01")
|
||||
MIN_DECISIVE = 4
|
||||
MIN_INCLUDED = 60
|
||||
MIN_DIFFERING = 20
|
||||
PERMUTATIONS = 999
|
||||
PERM_SEED = 20261012
|
||||
CONTROL_SEED = 20261013
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Placement:
|
||||
d12_lagna: int
|
||||
d1_lagna: int
|
||||
d12_signs: Mapping[str, int]
|
||||
d1_signs: Mapping[str, int]
|
||||
|
||||
|
||||
def canonical_bytes(payload: Any) -> bytes:
|
||||
return (
|
||||
json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=2) + "\n"
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def case_id_digest(case_ids: Sequence[str]) -> str:
|
||||
text = "\n".join(sorted(case_ids))
|
||||
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def load_prereg(path: Path = PREREG_PATH) -> dict[str, Any]:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def assert_prereg_locked(prereg: Mapping[str, Any]) -> None:
|
||||
if prereg.get("id") != "d12_parent_probes_preregistration_2026_10_12":
|
||||
raise SystemExit("preregistration id mismatch")
|
||||
if list(prereg.get("probes") or []) != list(PROBES):
|
||||
raise SystemExit("preregistration probe list mismatch")
|
||||
if int(prereg["permutation"]["n"]) != PERMUTATIONS:
|
||||
raise SystemExit("preregistration permutation count mismatch")
|
||||
if int(prereg["permutation"]["seed"]) != PERM_SEED:
|
||||
raise SystemExit("preregistration permutation seed mismatch")
|
||||
if int(prereg["positive_control"]["seed"]) != CONTROL_SEED:
|
||||
raise SystemExit("preregistration control seed mismatch")
|
||||
if int(prereg["positive_control"]["minimum_differing_cases"]) != MIN_DIFFERING:
|
||||
raise SystemExit("preregistration control minimum mismatch")
|
||||
if int(prereg["birth_records"]["expected_case_count"]) != 77:
|
||||
raise SystemExit("preregistration case count mismatch")
|
||||
if "at least 60" not in str(prereg["gates"]["sample"]):
|
||||
raise SystemExit("preregistration sample gate mismatch")
|
||||
if format(prereg["family"]["bonferroni_alpha"], ".4f") != "0.0125":
|
||||
raise SystemExit("preregistration alpha mismatch")
|
||||
if format(prereg["robustness"]["gate_max_change_rate"], ".1f") != "0.2":
|
||||
raise SystemExit("preregistration robustness line mismatch")
|
||||
if format(prereg["positive_control"]["permutation_p_max"], ".2f") != "0.01":
|
||||
raise SystemExit("preregistration control p mismatch")
|
||||
|
||||
|
||||
def house_of(body_sign: int, lagna: int) -> int:
|
||||
return (int(body_sign) - int(lagna)) % 12 + 1
|
||||
|
||||
|
||||
def lord_of(lagna: int, house: int) -> str:
|
||||
sign_index = (int(lagna) + int(house) - 1) % 12
|
||||
return varga.SIGN_LORDS[varga.SIGNS[sign_index]]
|
||||
|
||||
|
||||
def career_house(father_house: int) -> int:
|
||||
return ((int(father_house) - 1 + 9) % 12) + 1
|
||||
|
||||
|
||||
def separated(sign_a: int, sign_b: int) -> bool:
|
||||
return (int(sign_b) - int(sign_a)) % 12 in SEPARATED_DELTAS
|
||||
|
||||
|
||||
def _unique(planets: Sequence[str]) -> tuple[str, ...]:
|
||||
return tuple(dict.fromkeys(planets))
|
||||
|
||||
|
||||
def _in_kendra(signs: Mapping[str, int], lagna: int, planet: str) -> bool:
|
||||
return house_of(signs[planet], lagna) in KENDRA
|
||||
|
||||
|
||||
def read_probes(
|
||||
signs: Mapping[str, int],
|
||||
lagna: int,
|
||||
father_house: int,
|
||||
) -> dict[str, str]:
|
||||
"""Return yes / no / abstain for the six pre-registered probes."""
|
||||
mother = _unique(("Moon", lord_of(lagna, 4)))
|
||||
father = _unique(("Sun", lord_of(lagna, father_house)))
|
||||
mother_kendra = sum(_in_kendra(signs, lagna, planet) for planet in mother)
|
||||
father_kendra = sum(_in_kendra(signs, lagna, planet) for planet in father)
|
||||
if mother_kendra > father_kendra:
|
||||
care = "yes"
|
||||
elif father_kendra > mother_kendra:
|
||||
care = "no"
|
||||
elif _in_kendra(signs, lagna, "Moon") and not _in_kendra(signs, lagna, "Sun"):
|
||||
care = "yes"
|
||||
elif _in_kendra(signs, lagna, "Sun") and not _in_kendra(signs, lagna, "Moon"):
|
||||
care = "no"
|
||||
else:
|
||||
care = "abstain"
|
||||
|
||||
lord2 = lord_of(lagna, 2)
|
||||
financial = (
|
||||
any(house_of(signs[planet], lagna) in {2, 11} for planet in father)
|
||||
or house_of(signs[lord2], lagna) == father_house
|
||||
or any(signs[lord2] == signs[planet] for planet in father)
|
||||
)
|
||||
lord5 = lord_of(lagna, 5)
|
||||
study = (
|
||||
any(house_of(signs[planet], lagna) == 5 for planet in father)
|
||||
or house_of(signs[lord5], lagna) == father_house
|
||||
or house_of(signs["Jupiter"], lagna) == father_house
|
||||
or any(signs["Jupiter"] == signs[planet] for planet in father)
|
||||
)
|
||||
career = career_house(father_house)
|
||||
career_lord = lord_of(lagna, career)
|
||||
institutional = (
|
||||
house_of(signs["Saturn"], lagna) == career
|
||||
or house_of(signs["Sun"], lagna) == career
|
||||
or career_lord in {"Saturn", "Sun"}
|
||||
)
|
||||
commerce = (
|
||||
house_of(signs["Mercury"], lagna) == career
|
||||
or house_of(signs["Venus"], lagna) == career
|
||||
or career_lord in {"Mercury", "Venus"}
|
||||
)
|
||||
lord4 = lord_of(lagna, 4)
|
||||
father_lord = lord_of(lagna, father_house)
|
||||
lords_apart = lord4 != father_lord and separated(signs[lord4], signs[father_lord])
|
||||
rahu_house = house_of(signs["Rahu"], lagna) if "Rahu" in signs else None
|
||||
apart = (
|
||||
separated(signs["Sun"], signs["Moon"])
|
||||
or lords_apart
|
||||
or rahu_house in {4, father_house}
|
||||
)
|
||||
return {
|
||||
"mother_more_daily_care": care,
|
||||
"father_financial_lead": "yes" if financial else "no",
|
||||
"father_guided_study": "yes" if study else "no",
|
||||
"father_work_institutional": "yes" if institutional else "no",
|
||||
"father_work_commerce": "yes" if commerce else "no",
|
||||
"parents_long_apart": "yes" if apart else "no",
|
||||
}
|
||||
|
||||
|
||||
def placement_from_longitudes(
|
||||
ascendant_longitude: float,
|
||||
planet_longitudes: Mapping[str, float],
|
||||
planet_d1_signs: Mapping[str, int],
|
||||
) -> Placement:
|
||||
d12_signs = {
|
||||
name: int(varga.calc_varga(float(longitude), 12)["sign_idx"])
|
||||
for name, longitude in planet_longitudes.items()
|
||||
}
|
||||
d1_signs = {name: int(sign) for name, sign in planet_d1_signs.items()}
|
||||
ascendant = float(ascendant_longitude)
|
||||
return Placement(
|
||||
d12_lagna=int(varga.calc_varga(ascendant, 12)["sign_idx"]),
|
||||
d1_lagna=int(ascendant // 30) % 12,
|
||||
d12_signs=d12_signs,
|
||||
d1_signs=d1_signs,
|
||||
)
|
||||
|
||||
|
||||
def predict(placement: Placement, chart: str, father_house: int) -> dict[str, str]:
|
||||
if chart == "D12":
|
||||
return read_probes(placement.d12_signs, placement.d12_lagna, father_house)
|
||||
if chart == "D1":
|
||||
return read_probes(placement.d1_signs, placement.d1_lagna, father_house)
|
||||
raise ValueError(f"unknown chart {chart}")
|
||||
|
||||
|
||||
def cuts(sign_by_offset: Mapping[int, int]) -> list[tuple[int, int]]:
|
||||
offsets = sorted(int(item) for item in sign_by_offset)
|
||||
found: list[tuple[int, int]] = []
|
||||
for left, right in zip(offsets, offsets[1:]):
|
||||
if right == left + 1 and sign_by_offset[left] != sign_by_offset[right]:
|
||||
found.append((left, right))
|
||||
return found
|
||||
|
||||
|
||||
def nearest_cut(pairs: Sequence[tuple[int, int]]) -> tuple[int, int] | None:
|
||||
if not pairs:
|
||||
return None
|
||||
return min(pairs, key=lambda pair: (min(abs(pair[0]), abs(pair[1])), abs(pair[0]), pair[0]))
|
||||
|
||||
|
||||
def true_side(
|
||||
sign_by_offset: Mapping[int, int],
|
||||
pair: tuple[int, int],
|
||||
) -> tuple[int, int]:
|
||||
truth = sign_by_offset[0]
|
||||
left, right = pair
|
||||
left_true = sign_by_offset[left] == truth
|
||||
right_true = sign_by_offset[right] == truth
|
||||
if left_true == right_true:
|
||||
raise RuntimeError("boundary does not separate the recorded sign")
|
||||
if left_true:
|
||||
return left, right
|
||||
return right, left
|
||||
|
||||
|
||||
def segments(sign_by_offset: Mapping[int, int]) -> list[list[int]]:
|
||||
offsets = sorted(int(item) for item in sign_by_offset)
|
||||
if not offsets:
|
||||
return []
|
||||
runs: list[list[int]] = [[offsets[0]]]
|
||||
for offset in offsets[1:]:
|
||||
if offset == runs[-1][-1] + 1 and sign_by_offset[offset] == sign_by_offset[runs[-1][-1]]:
|
||||
runs[-1].append(offset)
|
||||
else:
|
||||
runs.append([offset])
|
||||
return runs
|
||||
|
||||
|
||||
def segment_representative(run: Sequence[int]) -> int:
|
||||
return min(run, key=lambda offset: (abs(offset), offset))
|
||||
|
||||
|
||||
def side_score(prediction: Mapping[str, str], answers: Mapping[str, str]) -> int:
|
||||
score = 0
|
||||
for probe in PROBES:
|
||||
answer = answers[probe]
|
||||
label = prediction[probe]
|
||||
if answer not in {"yes", "no"} or label not in {"yes", "no"}:
|
||||
continue
|
||||
score += 1 if label == answer else -1
|
||||
return score
|
||||
|
||||
|
||||
def fisher_yates(items: Sequence[str], rng: Random) -> list[str]:
|
||||
out = list(items)
|
||||
for index in range(len(out) - 1, 0, -1):
|
||||
swap = rng.randrange(index + 1)
|
||||
out[index], out[swap] = out[swap], out[index]
|
||||
return out
|
||||
|
||||
|
||||
def permutation_maps(case_ids: Sequence[str], seed: int, count: int = PERMUTATIONS) -> list[list[str]]:
|
||||
rng = Random(seed)
|
||||
ordered = sorted(case_ids)
|
||||
return [fisher_yates(ordered, rng) for _ in range(count)]
|
||||
|
||||
|
||||
def assign_answers(
|
||||
destinations: Sequence[str],
|
||||
sources: Sequence[str],
|
||||
answers: Mapping[str, Mapping[str, str]],
|
||||
) -> dict[str, Mapping[str, str]]:
|
||||
if len(destinations) != len(sources):
|
||||
raise RuntimeError("permutation length mismatch")
|
||||
return {dest: answers[source] for dest, source in zip(destinations, sources)}
|
||||
|
||||
|
||||
def selection_count(
|
||||
case_ids: Sequence[str],
|
||||
answers: Mapping[str, Mapping[str, str]],
|
||||
true_predictions: Mapping[str, Mapping[str, str]],
|
||||
other_predictions: Mapping[str, Mapping[str, str]],
|
||||
) -> int:
|
||||
selected = 0
|
||||
for case_id in case_ids:
|
||||
true_score = side_score(true_predictions[case_id], answers[case_id])
|
||||
other_score = side_score(other_predictions[case_id], answers[case_id])
|
||||
if true_score > other_score:
|
||||
selected += 1
|
||||
return selected
|
||||
|
||||
|
||||
def ratio_below(numer: int, denom: int, threshold: Decimal) -> bool:
|
||||
if denom <= 0:
|
||||
return False
|
||||
return Decimal(numer) / Decimal(denom) < threshold
|
||||
|
||||
|
||||
def display_ratio(numer: int, denom: int) -> str:
|
||||
if denom <= 0:
|
||||
return "0.000000"
|
||||
return format(numer / denom, ".6f")
|
||||
|
||||
|
||||
def permutation_summary(observed: int, denom: int, perm_counts: Sequence[int]) -> dict[str, Any]:
|
||||
extreme = sum(1 for count in perm_counts if count >= observed)
|
||||
below = sum(1 for count in perm_counts if count < observed)
|
||||
null_sum = sum(perm_counts)
|
||||
n_perm = len(perm_counts)
|
||||
greater = observed * n_perm > null_sum if denom > 0 else False
|
||||
return {
|
||||
"observed_numer": observed,
|
||||
"observed_denom": denom,
|
||||
"observed_display": display_ratio(observed, denom),
|
||||
"extreme": extreme,
|
||||
"p_numer": extreme + 1,
|
||||
"p_denom": n_perm + 1,
|
||||
"p_display": display_ratio(extreme + 1, n_perm + 1),
|
||||
"null_sum": null_sum,
|
||||
"null_mean_display": display_ratio(null_sum, n_perm * denom) if denom else "0.000000",
|
||||
"above_null_mean": greater,
|
||||
"percentile_numer": below,
|
||||
"percentile_denom": n_perm,
|
||||
"percentile_display": display_ratio(below, n_perm),
|
||||
}
|
||||
|
||||
|
||||
def cell_is_significant(summary: Mapping[str, Any]) -> bool:
|
||||
return bool(summary["above_null_mean"]) and ratio_below(
|
||||
int(summary["p_numer"]),
|
||||
int(summary["p_denom"]),
|
||||
ALPHA,
|
||||
)
|
||||
|
||||
|
||||
def vectors_differ(
|
||||
true_prediction: Mapping[str, str],
|
||||
other_prediction: Mapping[str, str],
|
||||
) -> bool:
|
||||
return any(
|
||||
true_prediction[probe] in {"yes", "no"} and true_prediction[probe] != other_prediction[probe]
|
||||
for probe in PROBES
|
||||
)
|
||||
|
||||
|
||||
def plant_answers(true_prediction: Mapping[str, str]) -> dict[str, str]:
|
||||
planted: dict[str, str] = {}
|
||||
for probe in PROBES:
|
||||
label = true_prediction[probe]
|
||||
planted[probe] = label if label in {"yes", "no"} else "unclear"
|
||||
return planted
|
||||
|
||||
|
||||
def change_count(
|
||||
baseline: Mapping[str, Mapping[int, Mapping[str, str]]],
|
||||
alternate: Mapping[str, Mapping[int, Mapping[str, str]]],
|
||||
sides: Mapping[str, tuple[int, int]],
|
||||
) -> int:
|
||||
changes = 0
|
||||
for case_id, pair in sides.items():
|
||||
for offset in pair:
|
||||
for probe in PROBES:
|
||||
if baseline[case_id][offset][probe] != alternate[case_id][offset][probe]:
|
||||
changes += 1
|
||||
return changes
|
||||
|
||||
|
||||
def decisive_count(answers: Mapping[str, str]) -> int:
|
||||
return sum(answer in {"yes", "no"} for answer in answers.values())
|
||||
|
||||
|
||||
def validate_fact_case(case: Mapping[str, Any]) -> dict[str, str]:
|
||||
probes = case.get("probes")
|
||||
if not isinstance(probes, dict) or set(probes) != set(PROBES):
|
||||
raise SystemExit(f"fact probes incomplete for {case.get('case_id')}")
|
||||
answers: dict[str, str] = {}
|
||||
for probe in PROBES:
|
||||
row = probes[probe]
|
||||
if not isinstance(row, dict):
|
||||
raise SystemExit(f"fact row missing for {case.get('case_id')} {probe}")
|
||||
answer = str(row.get("answer") or "")
|
||||
grade = str(row.get("source_grade") or "")
|
||||
url = str(row.get("url") or "")
|
||||
excerpt = str(row.get("excerpt") or "").strip()
|
||||
if answer not in ANSWERS or grade not in GRADES or not url.startswith("https://") or not excerpt:
|
||||
raise SystemExit(f"fact source incomplete for {case.get('case_id')} {probe}")
|
||||
answers[probe] = answer
|
||||
return answers
|
||||
|
||||
|
||||
def answer_tally(cases: Sequence[Mapping[str, str]]) -> dict[str, dict[str, int]]:
|
||||
tally = {probe: {"yes": 0, "no": 0, "unclear": 0} for probe in PROBES}
|
||||
for answers in cases:
|
||||
for probe in PROBES:
|
||||
tally[probe][answers[probe]] += 1
|
||||
return tally
|
||||
|
||||
|
||||
def window_offsets(radius: int) -> list[int]:
|
||||
return list(range(-int(radius), int(radius) + 1))
|
||||
|
||||
|
||||
def _slice_map(values: Mapping[int, Any], offsets: Sequence[int]) -> dict[int, Any]:
|
||||
return {int(offset): values[int(offset)] for offset in offsets}
|
||||
|
||||
|
||||
def case_window(
|
||||
sign_by_offset: Mapping[int, int],
|
||||
prediction_by_offset: Mapping[int, Mapping[str, str]],
|
||||
answers: Mapping[str, str],
|
||||
radius: int,
|
||||
) -> dict[str, Any]:
|
||||
signs = _slice_map(sign_by_offset, window_offsets(radius))
|
||||
pair = nearest_cut(cuts(signs))
|
||||
if pair is None:
|
||||
return {
|
||||
"boundary": False,
|
||||
"cut": None,
|
||||
"true_offset": None,
|
||||
"other_offset": None,
|
||||
"true_score": None,
|
||||
"other_score": None,
|
||||
"selected": False,
|
||||
"true_predictions": None,
|
||||
"other_predictions": None,
|
||||
"segments": 1 if signs else 0,
|
||||
"head_hit": None,
|
||||
"retained": None,
|
||||
}
|
||||
true_offset, other_offset = true_side(signs, pair)
|
||||
true_prediction = dict(prediction_by_offset[true_offset])
|
||||
other_prediction = dict(prediction_by_offset[other_offset])
|
||||
true_score = side_score(true_prediction, answers)
|
||||
other_score = side_score(other_prediction, answers)
|
||||
runs = segments(signs)
|
||||
scored: list[tuple[list[int], int]] = []
|
||||
for run in runs:
|
||||
representative = segment_representative(run)
|
||||
scored.append((run, side_score(prediction_by_offset[representative], answers)))
|
||||
true_segment = next(score for run, score in scored if 0 in run)
|
||||
other_scores = [score for run, score in scored if 0 not in run]
|
||||
return {
|
||||
"boundary": True,
|
||||
"cut": [pair[0], pair[1]],
|
||||
"true_offset": true_offset,
|
||||
"other_offset": other_offset,
|
||||
"true_score": true_score,
|
||||
"other_score": other_score,
|
||||
"selected": true_score > other_score,
|
||||
"true_predictions": true_prediction,
|
||||
"other_predictions": other_prediction,
|
||||
"segments": len(runs),
|
||||
"head_hit": true_segment > max(other_scores),
|
||||
"retained": true_segment >= max(other_scores),
|
||||
"differ": vectors_differ(true_prediction, other_prediction),
|
||||
}
|
||||
|
||||
|
||||
def evaluate_cell(
|
||||
case_ids: Sequence[str],
|
||||
signs: Mapping[str, Mapping[int, int]],
|
||||
predictions: Mapping[str, Mapping[int, Mapping[str, str]]],
|
||||
answers: Mapping[str, Mapping[str, str]],
|
||||
radius: int,
|
||||
perm_maps: Sequence[Sequence[str]],
|
||||
) -> dict[str, Any]:
|
||||
ordered = sorted(case_ids)
|
||||
rows = {
|
||||
case_id: case_window(signs[case_id], predictions[case_id], answers[case_id], radius)
|
||||
for case_id in ordered
|
||||
}
|
||||
boundary_ids = [case_id for case_id in ordered if rows[case_id]["boundary"]]
|
||||
true_predictions = {case_id: rows[case_id]["true_predictions"] for case_id in boundary_ids}
|
||||
other_predictions = {case_id: rows[case_id]["other_predictions"] for case_id in boundary_ids}
|
||||
observed = selection_count(boundary_ids, answers, true_predictions, other_predictions)
|
||||
perm_counts = []
|
||||
for sources in perm_maps:
|
||||
assigned = assign_answers(ordered, sources, answers)
|
||||
perm_counts.append(
|
||||
selection_count(boundary_ids, assigned, true_predictions, other_predictions)
|
||||
)
|
||||
summary = permutation_summary(observed, len(boundary_ids), perm_counts)
|
||||
head_hits = sum(1 for case_id in boundary_ids if rows[case_id]["head_hit"])
|
||||
retained = sum(1 for case_id in boundary_ids if rows[case_id]["retained"])
|
||||
return {
|
||||
"summary": summary,
|
||||
"significant": cell_is_significant(summary),
|
||||
"head_hit_numer": head_hits,
|
||||
"head_hit_denom": len(boundary_ids),
|
||||
"head_hit_display": display_ratio(head_hits, len(boundary_ids)),
|
||||
"retained_numer": retained,
|
||||
"retained_denom": len(boundary_ids),
|
||||
"retained_display": display_ratio(retained, len(boundary_ids)),
|
||||
"no_boundary": len(ordered) - len(boundary_ids),
|
||||
"cases": rows,
|
||||
"not_a_gate_segment_rates": True,
|
||||
}
|
||||
|
||||
|
||||
def positive_control_cell(
|
||||
case_ids: Sequence[str],
|
||||
signs: Mapping[str, Mapping[int, int]],
|
||||
predictions: Mapping[str, Mapping[int, Mapping[str, str]]],
|
||||
radius: int,
|
||||
) -> dict[str, Any]:
|
||||
"""Plant the true side's own labels. Permute only cases that have a cut.
|
||||
|
||||
Those are the cases with a planted vector. The stream starts at the
|
||||
pre-registered control seed for every cell.
|
||||
"""
|
||||
blank = {probe: "unclear" for probe in PROBES}
|
||||
boundary_ids: list[str] = []
|
||||
true_predictions: dict[str, Mapping[str, str]] = {}
|
||||
other_predictions: dict[str, Mapping[str, str]] = {}
|
||||
planted: dict[str, dict[str, str]] = {}
|
||||
missed: list[str] = []
|
||||
differing = 0
|
||||
for case_id in sorted(case_ids):
|
||||
row = case_window(signs[case_id], predictions[case_id], blank, radius)
|
||||
if not row["boundary"]:
|
||||
continue
|
||||
boundary_ids.append(case_id)
|
||||
true_predictions[case_id] = row["true_predictions"]
|
||||
other_predictions[case_id] = row["other_predictions"]
|
||||
planted[case_id] = plant_answers(row["true_predictions"])
|
||||
if row["differ"]:
|
||||
differing += 1
|
||||
checked = case_window(signs[case_id], predictions[case_id], planted[case_id], radius)
|
||||
if not checked["selected"]:
|
||||
missed.append(case_id)
|
||||
maps = permutation_maps(boundary_ids, CONTROL_SEED, PERMUTATIONS)
|
||||
observed = selection_count(boundary_ids, planted, true_predictions, other_predictions)
|
||||
perm_counts = [
|
||||
selection_count(
|
||||
boundary_ids,
|
||||
assign_answers(boundary_ids, sources, planted),
|
||||
true_predictions,
|
||||
other_predictions,
|
||||
)
|
||||
for sources in maps
|
||||
]
|
||||
summary = permutation_summary(observed, len(boundary_ids), perm_counts)
|
||||
return {
|
||||
"summary": summary,
|
||||
"differing": differing,
|
||||
"pipeline_exact": not missed,
|
||||
"missed": missed,
|
||||
"p_below": ratio_below(int(summary["p_numer"]), int(summary["p_denom"]), CONTROL_P_MAX),
|
||||
}
|
||||
|
||||
|
||||
def decide_status(
|
||||
*,
|
||||
included: int,
|
||||
control_exact: bool,
|
||||
differing: int,
|
||||
control_p_below: bool,
|
||||
any_robust_significant: bool,
|
||||
) -> str:
|
||||
if not control_exact:
|
||||
return "control_invalid"
|
||||
if differing < MIN_DIFFERING or not control_p_below:
|
||||
return "control_underpowered"
|
||||
if included < MIN_INCLUDED:
|
||||
return "underpowered_sample"
|
||||
if any_robust_significant:
|
||||
return "pass"
|
||||
return "fail_closed"
|
||||
@@ -0,0 +1,338 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Score the pre-registered D12 parent probes. Research only.
|
||||
|
||||
Charts come from domain_calculation_service.compute_chart. D12 signs come
|
||||
from varga.calc_varga. The script writes one JSON file and no timestamps.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_SCRIPTS = str(Path(__file__).resolve().parents[1])
|
||||
if _SCRIPTS not in sys.path:
|
||||
sys.path.insert(0, _SCRIPTS)
|
||||
|
||||
import domain_calculation_service
|
||||
import swisseph as swe
|
||||
|
||||
from scripts.research import d12_parent_probes_lib as lib
|
||||
|
||||
PROFILES = {
|
||||
"father_9": ("D12", 9),
|
||||
"father_10": ("D12", 10),
|
||||
"d1_father_9": ("D1", 9),
|
||||
}
|
||||
FAMILY = (("father_9", 10), ("father_9", 30), ("father_10", 10), ("father_10", 30))
|
||||
SETTINGS = {
|
||||
"baseline": ("raman", "mean"),
|
||||
"lahiri_mean": ("lahiri", "mean"),
|
||||
"raman_true": ("raman", "true"),
|
||||
}
|
||||
OFFSETS = list(range(-30, 31))
|
||||
NEEDED = ("Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Rahu")
|
||||
|
||||
|
||||
def file_sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def birth_moment(birth: dict[str, Any]) -> datetime:
|
||||
clock = str(birth["time"])[:5]
|
||||
return datetime.strptime(f"{birth['date']} {clock}", "%Y-%m-%d %H:%M")
|
||||
|
||||
|
||||
def placement_at(moment: datetime, birth: dict[str, Any], ayanamsa: str, node_mode: str) -> lib.Placement:
|
||||
chart = domain_calculation_service.compute_chart(
|
||||
{
|
||||
"year": moment.year,
|
||||
"month": moment.month,
|
||||
"day": moment.day,
|
||||
"hour": moment.hour,
|
||||
"minute": moment.minute,
|
||||
"second": 0,
|
||||
"lat": float(birth["latitude"]),
|
||||
"lon": float(birth["longitude"]),
|
||||
"tz": float(birth["timezone_offset"]),
|
||||
"ayanamsa": ayanamsa,
|
||||
"node_mode": node_mode,
|
||||
}
|
||||
)
|
||||
planets = chart["planets"]
|
||||
longitudes: dict[str, float] = {}
|
||||
d1_signs: dict[str, int] = {}
|
||||
for name in NEEDED:
|
||||
row = planets.get(name)
|
||||
if not isinstance(row, dict) or "lon" not in row:
|
||||
raise RuntimeError(f"chart is missing {name}")
|
||||
longitudes[name] = float(row["lon"])
|
||||
if "sign_idx" in row:
|
||||
d1_signs[name] = int(row["sign_idx"])
|
||||
else:
|
||||
d1_signs[name] = int(float(row["lon"]) // 30) % 12
|
||||
ascendant = chart.get("ascendant") or {}
|
||||
if "lon" not in ascendant:
|
||||
raise RuntimeError("chart is missing the ascendant longitude")
|
||||
return lib.placement_from_longitudes(float(ascendant["lon"]), longitudes, d1_signs)
|
||||
|
||||
|
||||
def sky_grid(cases: list[dict[str, Any]], ayanamsa: str, node_mode: str) -> dict[str, dict[int, lib.Placement]]:
|
||||
grid: dict[str, dict[int, lib.Placement]] = {}
|
||||
for index, case in enumerate(cases, start=1):
|
||||
moment = birth_moment(case["birth"])
|
||||
rows: dict[int, lib.Placement] = {}
|
||||
for offset in OFFSETS:
|
||||
rows[offset] = placement_at(moment + timedelta(minutes=offset), case["birth"], ayanamsa, node_mode)
|
||||
grid[str(case["case_id"])] = rows
|
||||
print(f"{ayanamsa}/{node_mode} {index}/{len(cases)}", file=sys.stderr)
|
||||
return grid
|
||||
|
||||
|
||||
def signs_of(grid: dict[str, dict[int, lib.Placement]]) -> dict[str, dict[int, int]]:
|
||||
return {
|
||||
case_id: {offset: placement.d12_lagna for offset, placement in rows.items()}
|
||||
for case_id, rows in grid.items()
|
||||
}
|
||||
|
||||
|
||||
def predictions_of(
|
||||
grid: dict[str, dict[int, lib.Placement]],
|
||||
chart: str,
|
||||
father_house: int,
|
||||
) -> dict[str, dict[int, dict[str, str]]]:
|
||||
return {
|
||||
case_id: {
|
||||
offset: lib.predict(placement, chart, father_house)
|
||||
for offset, placement in rows.items()
|
||||
}
|
||||
for case_id, rows in grid.items()
|
||||
}
|
||||
|
||||
|
||||
def load_births(prereg: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
payload = json_load(lib.HOLDOUT_PATH)
|
||||
cases = list(payload["cases"])
|
||||
digest = lib.case_id_digest([str(case["case_id"]) for case in cases])
|
||||
if digest != prereg["birth_records"]["case_id_sha256"]:
|
||||
raise SystemExit("birth case-id digest does not match the preregistration")
|
||||
if len(cases) != 77:
|
||||
raise SystemExit("birth file does not contain 77 cases")
|
||||
return cases
|
||||
|
||||
|
||||
def json_load(path: Path) -> dict[str, Any]:
|
||||
import json
|
||||
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def load_answers(birth_ids: set[str]) -> tuple[dict[str, dict[str, str]], list[dict[str, Any]], list[str]]:
|
||||
if not lib.FACTS_PATH.is_file():
|
||||
raise SystemExit(f"missing facts: {lib.FACTS_PATH}")
|
||||
payload = json_load(lib.FACTS_PATH)
|
||||
coded: dict[str, dict[str, str]] = {}
|
||||
excluded: list[dict[str, Any]] = []
|
||||
for case in payload.get("cases") or []:
|
||||
case_id = str(case.get("case_id") or "")
|
||||
if case_id not in birth_ids:
|
||||
raise SystemExit(f"fact case is not in the frozen birth list: {case_id}")
|
||||
answers = lib.validate_fact_case(case)
|
||||
decisive = lib.decisive_count(answers)
|
||||
if decisive >= lib.MIN_DECISIVE:
|
||||
coded[case_id] = answers
|
||||
else:
|
||||
excluded.append({"case_id": case_id, "decisive": decisive})
|
||||
not_coded = sorted(birth_ids - set(coded) - {row["case_id"] for row in excluded})
|
||||
return coded, excluded, not_coded
|
||||
|
||||
|
||||
def robustness_block(
|
||||
rows: dict[str, dict[str, Any]],
|
||||
baseline: dict[str, dict[int, dict[str, str]]],
|
||||
alternate: dict[str, dict[int, dict[str, str]]],
|
||||
) -> dict[str, Any]:
|
||||
sides = {
|
||||
case_id: (int(row["true_offset"]), int(row["other_offset"]))
|
||||
for case_id, row in rows.items()
|
||||
if row["boundary"]
|
||||
}
|
||||
changes = lib.change_count(baseline, alternate, sides)
|
||||
denom = len(sides) * 2 * len(lib.PROBES)
|
||||
return {
|
||||
"changes": changes,
|
||||
"denom": denom,
|
||||
"display": lib.display_ratio(changes, denom),
|
||||
"below_line": lib.ratio_below(changes, denom, lib.ROBUST_MAX),
|
||||
}
|
||||
|
||||
|
||||
def public_cases(rows: dict[str, dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
published = []
|
||||
for case_id in sorted(rows):
|
||||
row = dict(rows[case_id])
|
||||
row["case_id"] = case_id
|
||||
row.pop("differ", None)
|
||||
published.append(row)
|
||||
return published
|
||||
|
||||
|
||||
def ephemeris_version() -> str:
|
||||
version = getattr(swe, "version", None)
|
||||
return str(version) if version else "unknown"
|
||||
|
||||
|
||||
def run() -> dict[str, Any]:
|
||||
prereg = lib.load_prereg()
|
||||
lib.assert_prereg_locked(prereg)
|
||||
births = load_births(prereg)
|
||||
birth_ids = {str(case["case_id"]) for case in births}
|
||||
answers, excluded, not_coded = load_answers(birth_ids)
|
||||
included_ids = sorted(answers)
|
||||
included_births = [case for case in births if str(case["case_id"]) in answers]
|
||||
included_births.sort(key=lambda case: str(case["case_id"]))
|
||||
skies = {
|
||||
name: sky_grid(included_births, ayanamsa, node_mode)
|
||||
for name, (ayanamsa, node_mode) in SETTINGS.items()
|
||||
}
|
||||
sign_grid = signs_of(skies["baseline"])
|
||||
predicted = {
|
||||
profile: predictions_of(skies["baseline"], chart, father_house)
|
||||
for profile, (chart, father_house) in PROFILES.items()
|
||||
}
|
||||
alternate = {
|
||||
setting: {
|
||||
profile: predictions_of(skies[setting], "D12", father_house)
|
||||
for profile, father_house in (("father_9", 9), ("father_10", 10))
|
||||
}
|
||||
for setting in ("lahiri_mean", "raman_true")
|
||||
}
|
||||
maps = lib.permutation_maps(included_ids, lib.PERM_SEED, lib.PERMUTATIONS)
|
||||
cells: dict[str, Any] = {}
|
||||
controls: dict[str, Any] = {}
|
||||
any_robust_significant = False
|
||||
for profile, radius in FAMILY:
|
||||
key = f"{profile}_r{radius}"
|
||||
evaluated = lib.evaluate_cell(
|
||||
included_ids,
|
||||
sign_grid,
|
||||
predicted[profile],
|
||||
answers,
|
||||
radius,
|
||||
maps,
|
||||
)
|
||||
ayanamsa_shift = robustness_block(
|
||||
evaluated["cases"],
|
||||
predicted[profile],
|
||||
alternate["lahiri_mean"][profile],
|
||||
)
|
||||
node_shift = robustness_block(
|
||||
evaluated["cases"],
|
||||
predicted[profile],
|
||||
alternate["raman_true"][profile],
|
||||
)
|
||||
robust = bool(ayanamsa_shift["below_line"] and node_shift["below_line"])
|
||||
if evaluated["significant"] and robust:
|
||||
any_robust_significant = True
|
||||
control = lib.positive_control_cell(included_ids, sign_grid, predicted[profile], radius)
|
||||
cells[key] = {
|
||||
"profile": profile,
|
||||
"radius_minutes": radius,
|
||||
"summary": evaluated["summary"],
|
||||
"significant": evaluated["significant"],
|
||||
"head_hit_numer": evaluated["head_hit_numer"],
|
||||
"head_hit_denom": evaluated["head_hit_denom"],
|
||||
"head_hit_display": evaluated["head_hit_display"],
|
||||
"retained_numer": evaluated["retained_numer"],
|
||||
"retained_denom": evaluated["retained_denom"],
|
||||
"retained_display": evaluated["retained_display"],
|
||||
"no_boundary": evaluated["no_boundary"],
|
||||
"robustness_ayanamsa": ayanamsa_shift,
|
||||
"robustness_node": node_shift,
|
||||
"robust": robust,
|
||||
"not_a_gate_segment_rates": True,
|
||||
"cases": public_cases(evaluated["cases"]),
|
||||
}
|
||||
controls[key] = {
|
||||
"summary": control["summary"],
|
||||
"differing": control["differing"],
|
||||
"pipeline_exact": control["pipeline_exact"],
|
||||
"missed": control["missed"],
|
||||
"p_below_0_01": control["p_below"],
|
||||
}
|
||||
d1_cells = {}
|
||||
for radius in (10, 30):
|
||||
evaluated = lib.evaluate_cell(
|
||||
included_ids,
|
||||
sign_grid,
|
||||
predicted["d1_father_9"],
|
||||
answers,
|
||||
radius,
|
||||
maps,
|
||||
)
|
||||
d1_cells[f"d1_father_9_r{radius}"] = {
|
||||
"summary": evaluated["summary"],
|
||||
"significant_if_it_were_in_the_family": evaluated["significant"],
|
||||
"head_hit_display": evaluated["head_hit_display"],
|
||||
"retained_display": evaluated["retained_display"],
|
||||
"no_boundary": evaluated["no_boundary"],
|
||||
"not_a_gate": True,
|
||||
}
|
||||
control_exact = all(row["pipeline_exact"] for row in controls.values())
|
||||
min_differing = min((row["differing"] for row in controls.values()), default=0)
|
||||
control_p_below = all(row["p_below_0_01"] for row in controls.values())
|
||||
status = lib.decide_status(
|
||||
included=len(included_ids),
|
||||
control_exact=control_exact,
|
||||
differing=min_differing,
|
||||
control_p_below=control_p_below,
|
||||
any_robust_significant=any_robust_significant,
|
||||
)
|
||||
coded_rows = []
|
||||
facts_payload = json_load(lib.FACTS_PATH)
|
||||
for case in facts_payload.get("cases") or []:
|
||||
coded_rows.append(lib.validate_fact_case(case))
|
||||
return {
|
||||
"id": "d12_parent_probes_results_2026_10_12",
|
||||
"preregistration": "docs/research/d12_parent_probes_preregistration_2026_10_12.json",
|
||||
"preregistration_sha256": file_sha256(lib.PREREG_PATH),
|
||||
"facts_sha256": file_sha256(lib.FACTS_PATH),
|
||||
"ephemeris": ephemeris_version(),
|
||||
"python": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
||||
"settings": {
|
||||
"baseline": ["raman", "mean"],
|
||||
"ayanamsa_alt": ["lahiri", "mean"],
|
||||
"node_alt": ["raman", "true"],
|
||||
},
|
||||
"coded_cases": len(coded_rows),
|
||||
"included_cases": len(included_ids),
|
||||
"included_case_ids": included_ids,
|
||||
"excluded_few_decisive": excluded,
|
||||
"not_coded": not_coded,
|
||||
"unclear_rate_coded": lib.answer_tally(coded_rows),
|
||||
"unclear_rate_included": lib.answer_tally([answers[case_id] for case_id in included_ids]),
|
||||
"cells": cells,
|
||||
"positive_control": controls,
|
||||
"d1_control": d1_cells,
|
||||
"decision": status,
|
||||
"decision_note": {
|
||||
"pass": "过门",
|
||||
"fail_closed": "不上线",
|
||||
"underpowered_sample": "欠力",
|
||||
"control_underpowered": "欠力",
|
||||
"control_invalid": "结论无效",
|
||||
}[status],
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
payload = run()
|
||||
lib.RESULTS_PATH.write_bytes(lib.canonical_bytes(payload))
|
||||
print(f"wrote {lib.RESULTS_PATH} decision={payload['decision']}", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,254 @@
|
||||
"""D12 parent-probe research helpers. Public charts only; no production edits."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.research import d12_parent_probes_lib as lib
|
||||
|
||||
|
||||
BODIES = ("Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Rahu", "Ketu")
|
||||
|
||||
|
||||
def signs(**updates: int) -> dict[str, int]:
|
||||
base = {name: 0 for name in BODIES}
|
||||
base.update(updates)
|
||||
return base
|
||||
|
||||
|
||||
def test_preregistration_locks_the_gate_numbers() -> None:
|
||||
prereg = lib.load_prereg()
|
||||
lib.assert_prereg_locked(prereg)
|
||||
births = lib.json.loads(lib.HOLDOUT_PATH.read_text(encoding="utf-8"))["cases"]
|
||||
digest = lib.case_id_digest([case["case_id"] for case in births])
|
||||
assert digest == prereg["birth_records"]["case_id_sha256"]
|
||||
assert len(births) == 77
|
||||
|
||||
|
||||
def test_house_lord_career_and_separation_follow_the_preregistration() -> None:
|
||||
assert lib.house_of(0, 0) == 1
|
||||
assert lib.house_of(8, 0) == 9
|
||||
assert lib.lord_of(0, 9) == "Jupiter"
|
||||
assert lib.lord_of(0, 4) == "Moon"
|
||||
assert lib.career_house(9) == 6
|
||||
assert lib.career_house(10) == 7
|
||||
assert lib.separated(0, 5)
|
||||
assert lib.separated(0, 1)
|
||||
assert not lib.separated(0, 0)
|
||||
assert not lib.separated(0, 2)
|
||||
|
||||
|
||||
def test_care_abstains_only_on_a_kendra_tie_and_financial_uses_house_2() -> None:
|
||||
abstain = lib.read_probes(signs(**{name: 1 for name in BODIES}), 0, 9)
|
||||
assert abstain["mother_more_daily_care"] == "abstain"
|
||||
assert abstain["father_financial_lead"] == "yes"
|
||||
study_no = lib.read_probes(
|
||||
signs(Sun=0, Moon=1, Jupiter=3, Mercury=4, Venus=4, Mars=4, Saturn=2, Rahu=4),
|
||||
1,
|
||||
9,
|
||||
)
|
||||
assert study_no["father_guided_study"] == "no"
|
||||
institutional = lib.read_probes(signs(Saturn=5), 0, 9)
|
||||
assert institutional["father_work_institutional"] == "yes"
|
||||
|
||||
|
||||
def test_missing_rahu_does_not_invent_a_node() -> None:
|
||||
bare = {name: 0 for name in BODIES if name != "Rahu"}
|
||||
labels = lib.read_probes(bare, 0, 9)
|
||||
assert labels["parents_long_apart"] in {"yes", "no"}
|
||||
|
||||
|
||||
def test_nearest_cut_tie_break_and_true_side() -> None:
|
||||
signs_at = {-2: 1, -1: 1, 0: 1, 1: 2, 2: 2}
|
||||
assert lib.nearest_cut(lib.cuts(signs_at)) == (0, 1)
|
||||
tied = {-3: 0, -2: 1, -1: 1, 0: 1, 1: 1, 2: 2, 3: 2}
|
||||
assert lib.nearest_cut(lib.cuts(tied)) == (1, 2)
|
||||
mirror = {-3: 0, -2: 0, -1: 1, 0: 1, 1: 1, 2: 1, 3: 2}
|
||||
assert lib.nearest_cut(lib.cuts(mirror)) == (-2, -1)
|
||||
both = {-1: 0, 0: 1, 1: 2}
|
||||
assert lib.nearest_cut(lib.cuts(both)) == (0, 1)
|
||||
assert lib.true_side(signs_at, (0, 1)) == (0, 1)
|
||||
with pytest.raises(RuntimeError):
|
||||
lib.true_side({0: 1, 1: 1}, (0, 1))
|
||||
|
||||
|
||||
def test_segment_representative_prefers_the_recorded_minute() -> None:
|
||||
assert lib.segment_representative([-2, -1, 0, 1]) == 0
|
||||
assert lib.segment_representative([-4, -3]) == -3
|
||||
assert lib.segment_representative([2, 3, 4]) == 2
|
||||
|
||||
|
||||
def test_tie_is_not_a_selection_and_unclear_is_not_counted() -> None:
|
||||
answers = {probe: "yes" for probe in lib.PROBES}
|
||||
same = {probe: "yes" for probe in lib.PROBES}
|
||||
assert lib.side_score(same, answers) == 6
|
||||
mixed = dict(same)
|
||||
mixed["parents_long_apart"] = "abstain"
|
||||
unclear = dict(answers)
|
||||
unclear["father_guided_study"] = "unclear"
|
||||
assert lib.side_score(mixed, unclear) == 4
|
||||
assert lib.selection_count(
|
||||
["a"],
|
||||
{"a": answers},
|
||||
{"a": same},
|
||||
{"a": same},
|
||||
) == 0
|
||||
|
||||
|
||||
def test_fisher_yates_is_repeatable_and_shuffles_whole_vectors() -> None:
|
||||
first = lib.permutation_maps(["b", "a", "c"], seed=20261012, count=3)
|
||||
second = lib.permutation_maps(["b", "a", "c"], seed=20261012, count=3)
|
||||
assert first == second
|
||||
assert all(sorted(row) == ["a", "b", "c"] for row in first)
|
||||
assigned = lib.assign_answers(
|
||||
["a", "b", "c"],
|
||||
["c", "a", "b"],
|
||||
{"a": {"k": "yes"}, "b": {"k": "no"}, "c": {"k": "unclear"}},
|
||||
)
|
||||
assert assigned["a"]["k"] == "unclear"
|
||||
assert assigned["b"]["k"] == "yes"
|
||||
|
||||
|
||||
def test_window_selects_the_matching_side_and_the_head_segment() -> None:
|
||||
offsets = [-1, 0, 1]
|
||||
sign_at = {offset: 0 if offset < 1 else 1 for offset in offsets}
|
||||
true_labels = {probe: "yes" for probe in lib.PROBES}
|
||||
other_labels = {probe: "no" for probe in lib.PROBES}
|
||||
predictions = {-1: true_labels, 0: true_labels, 1: other_labels}
|
||||
answers = {probe: "yes" for probe in lib.PROBES}
|
||||
row = lib.case_window(sign_at, predictions, answers, 1)
|
||||
assert row["boundary"] is True
|
||||
assert row["true_offset"] == 0
|
||||
assert row["other_offset"] == 1
|
||||
assert row["selected"] is True
|
||||
assert row["head_hit"] is True
|
||||
assert row["retained"] is True
|
||||
planted = lib.plant_answers(true_labels)
|
||||
assert planted["mother_more_daily_care"] == "yes"
|
||||
assert lib.plant_answers({"mother_more_daily_care": "abstain", **{probe: "no" for probe in lib.PROBES if probe != "mother_more_daily_care"}})[
|
||||
"mother_more_daily_care"
|
||||
] == "unclear"
|
||||
|
||||
|
||||
def test_permutation_summary_uses_the_exact_ratio() -> None:
|
||||
summary = lib.permutation_summary(5, 10, [0, 1, 5, 6])
|
||||
assert summary["extreme"] == 2
|
||||
assert summary["p_numer"] == 3
|
||||
assert summary["p_denom"] == 5
|
||||
assert summary["above_null_mean"] is True
|
||||
assert summary["percentile_numer"] == 2
|
||||
assert lib.ratio_below(12, 1000, lib.ALPHA) is True
|
||||
assert lib.ratio_below(13, 1000, lib.ALPHA) is False
|
||||
assert lib.ratio_below(199, 1000, lib.ROBUST_MAX) is True
|
||||
assert lib.ratio_below(200, 1000, lib.ROBUST_MAX) is False
|
||||
assert lib.cell_is_significant(
|
||||
{"above_null_mean": True, "p_numer": 12, "p_denom": 1000}
|
||||
) is True
|
||||
assert lib.cell_is_significant(
|
||||
{"above_null_mean": True, "p_numer": 13, "p_denom": 1000}
|
||||
) is False
|
||||
|
||||
|
||||
def test_positive_control_detects_a_planted_difference() -> None:
|
||||
sign_at = {offset: 0 if offset <= 0 else 1 for offset in range(-1, 2)}
|
||||
yes_labels = {probe: "yes" for probe in lib.PROBES}
|
||||
no_labels = {probe: "no" for probe in lib.PROBES}
|
||||
case_ids = [f"c{index:02d}" for index in range(20)]
|
||||
signs = {case_id: sign_at for case_id in case_ids}
|
||||
predicted = {}
|
||||
for index, case_id in enumerate(case_ids):
|
||||
if index % 2 == 0:
|
||||
predicted[case_id] = {-1: yes_labels, 0: yes_labels, 1: no_labels}
|
||||
else:
|
||||
predicted[case_id] = {-1: no_labels, 0: no_labels, 1: yes_labels}
|
||||
control = lib.positive_control_cell(case_ids, signs, predicted, 1)
|
||||
assert control["pipeline_exact"] is True
|
||||
assert control["differing"] == 20
|
||||
assert control["summary"]["observed_numer"] == 20
|
||||
assert control["p_below"] is True
|
||||
|
||||
|
||||
def test_decision_status_does_not_pass_a_small_or_blind_sample() -> None:
|
||||
assert lib.decide_status(
|
||||
included=77,
|
||||
control_exact=True,
|
||||
differing=30,
|
||||
control_p_below=True,
|
||||
any_robust_significant=True,
|
||||
) == "pass"
|
||||
assert lib.decide_status(
|
||||
included=59,
|
||||
control_exact=True,
|
||||
differing=30,
|
||||
control_p_below=True,
|
||||
any_robust_significant=True,
|
||||
) == "underpowered_sample"
|
||||
assert lib.decide_status(
|
||||
included=77,
|
||||
control_exact=True,
|
||||
differing=30,
|
||||
control_p_below=True,
|
||||
any_robust_significant=False,
|
||||
) == "fail_closed"
|
||||
assert lib.decide_status(
|
||||
included=77,
|
||||
control_exact=True,
|
||||
differing=19,
|
||||
control_p_below=True,
|
||||
any_robust_significant=True,
|
||||
) == "control_underpowered"
|
||||
assert lib.decide_status(
|
||||
included=77,
|
||||
control_exact=False,
|
||||
differing=30,
|
||||
control_p_below=True,
|
||||
any_robust_significant=True,
|
||||
) == "control_invalid"
|
||||
|
||||
|
||||
def test_fact_row_requires_an_opened_https_source() -> None:
|
||||
row = {
|
||||
"answer": "unclear",
|
||||
"source_grade": "encyclopedia",
|
||||
"url": "https://en.wikipedia.org/wiki/Example",
|
||||
"excerpt": "The page names the parents and does not say who did the daily care.",
|
||||
}
|
||||
case = {"case_id": "example", "probes": {probe: dict(row) for probe in lib.PROBES}}
|
||||
answers = lib.validate_fact_case(case)
|
||||
assert lib.decisive_count(answers) == 0
|
||||
broken = {"case_id": "example", "probes": {probe: dict(row) for probe in lib.PROBES}}
|
||||
broken["probes"]["parents_long_apart"]["url"] = "http://insecure.example"
|
||||
with pytest.raises(SystemExit):
|
||||
lib.validate_fact_case(broken)
|
||||
|
||||
|
||||
def test_change_count_and_canonical_bytes_are_stable() -> None:
|
||||
labels = {probe: "yes" for probe in lib.PROBES}
|
||||
flipped = dict(labels)
|
||||
flipped["parents_long_apart"] = "no"
|
||||
baseline = {"a": {0: labels, 1: labels}}
|
||||
alternate = {"a": {0: labels, 1: flipped}}
|
||||
assert lib.change_count(baseline, alternate, {"a": (0, 1)}) == 1
|
||||
payload = {"b": 1, "a": {"d": 2, "c": 3}}
|
||||
assert lib.canonical_bytes(payload) == lib.canonical_bytes(payload)
|
||||
assert lib.canonical_bytes(payload).endswith(b"\n")
|
||||
assert Decimal("0.0125") == lib.ALPHA
|
||||
|
||||
|
||||
def test_production_chart_feeds_d12_without_a_second_ephemeris() -> None:
|
||||
from scripts.research.d12_parent_probes_run import placement_at
|
||||
|
||||
birth = {
|
||||
"date": "2000-01-01",
|
||||
"time": "12:00",
|
||||
"latitude": 0.0,
|
||||
"longitude": 0.0,
|
||||
"timezone_offset": 0.0,
|
||||
}
|
||||
placement = placement_at(datetime(2000, 1, 1, 12, 0), birth, "raman", "mean")
|
||||
assert 0 <= placement.d12_lagna <= 11
|
||||
assert set(lib.predict(placement, "D12", 9)) == set(lib.PROBES)
|
||||
assert set(lib.predict(placement, "D1", 9)) == set(lib.PROBES)
|
||||
Reference in New Issue
Block a user