标准大运表以前只印主运。已有出处的子运写进主包副本并出表,Kalachakra 子运按简化模型挂在现有主运上。

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jesse-ux
2026-10-22 23:01:40 +08:00
co-authored by Grok (agent loop) Cursor
parent 1c60926a13
commit 6b906f85c4
4 changed files with 393 additions and 2 deletions
+209
View File
@@ -0,0 +1,209 @@
"""Attach sourced dasha children onto a master-pack copy.
The natal modules keep their own rows. This module never imports PyJHora.
Kalachakra children are the upstream proportional model, labeled simplified,
and they hang on the majors this repository already calculated.
"""
from __future__ import annotations
from copy import deepcopy
from datetime import datetime, timedelta
from typing import Any
def _rotate_cycle_rows(cycle_rows: list[dict], start_rashi: str | None) -> list[dict]:
if not cycle_rows:
return []
if not start_rashi:
return list(cycle_rows)
start_index = next(
(index for index, row in enumerate(cycle_rows) if row.get("rashi") == start_rashi),
0,
)
return list(cycle_rows[start_index:]) + list(cycle_rows[:start_index])
def _build_subperiods(
parent_period: dict,
cycle_rows: list[dict],
total_cycle_years: float,
level_key: str,
year_days: float = 365.25,
) -> list[dict]:
"""Proportional Kalachakra children. Last child ends on the parent end."""
if not cycle_rows:
return []
rotated_rows = _rotate_cycle_rows(cycle_rows, parent_period.get("rashi"))
cycle_basis_total = sum(float(row.get("years") or 0.0) for row in rotated_rows)
denominator = cycle_basis_total if cycle_basis_total > 0 else float(total_cycle_years or 0.0)
if denominator <= 0:
return []
start_dt = datetime.fromisoformat(str(parent_period["start_date"]))
current_dt = start_dt
child_periods = []
for row in rotated_rows:
child_years = float(parent_period["years"]) * float(row["years"]) / denominator
end_dt = current_dt + timedelta(days=child_years * year_days)
child_periods.append({
"level": level_key,
"lord": row.get("lord"),
"rashi": row.get("rashi"),
"sign": row.get("rashi"),
"rashi_index": row.get("rashi_index"),
"years": child_years,
"start_date": current_dt.isoformat(),
"end_date": end_dt.isoformat(),
"status": "parameter_sensitive",
"derivation": "native_kalachakra_recursive_proportional_variant",
"model": "simplified",
})
current_dt = end_dt
if child_periods:
child_periods[-1]["end_date"] = parent_period["end_date"]
return child_periods
def attach_kalachakra_simplified_children(
major_periods: list[dict],
total_cycle_years: float | None = None,
year_days: float = 365.25,
) -> tuple[list[dict], list[dict], list[dict]]:
"""Return a copy of the existing majors with simplified AD and PD rows.
The caller's major rows are not modified. Replacing those majors is out of
scope: if a row has no start_date, this raises instead of inventing one.
"""
majors = deepcopy(major_periods)
cycle_rows = [
{
"lord": row.get("lord"),
"rashi": row.get("rashi"),
"rashi_index": row.get("rashi_index"),
"years": row.get("years"),
}
for row in majors
if isinstance(row, dict)
]
if total_cycle_years is None:
total_cycle_years = sum(float(row.get("years") or 0.0) for row in cycle_rows)
all_antardashas: list[dict] = []
all_pratyantardashas: list[dict] = []
for period in majors:
if not isinstance(period, dict):
continue
antardasha = _build_subperiods(period, cycle_rows, float(total_cycle_years), "antardasha", year_days)
for child in antardasha:
child["parent_lord"] = period.get("lord")
child["parent_rashi"] = period.get("rashi")
child["parent_sign"] = period.get("rashi")
period["antardasha"] = antardasha
all_antardashas.extend(antardasha)
for child in antardasha:
pratyantardasha = _build_subperiods(
child, cycle_rows, float(total_cycle_years), "pratyantardasha", year_days,
)
for grand in pratyantardasha:
grand["parent_lord"] = child.get("lord")
grand["parent_rashi"] = child.get("rashi")
grand["parent_sign"] = child.get("rashi")
grand["main"] = period.get("rashi") or period.get("lord")
grand["parent_antardasha"] = child.get("rashi") or child.get("lord")
child["pratyantardasha"] = pratyantardasha
all_pratyantardashas.extend(pratyantardasha)
return majors, all_antardashas, all_pratyantardashas
def _copy_rows(rows: list[dict]) -> list[dict]:
return deepcopy([row for row in rows if isinstance(row, dict)])
def _narayana_periods(module: dict) -> list[dict]:
sequence = module.get("mahadasha_sequence")
if not isinstance(sequence, list):
return []
children_by_parent: dict[str, list[dict]] = {}
boundaries = module.get("dense_boundaries")
if isinstance(boundaries, list):
for row in boundaries:
if not isinstance(row, dict) or row.get("level") != "AD" or not row.get("sign"):
continue
parent = str(row.get("parent_period_key") or row.get("parent_md") or "")
children_by_parent.setdefault(parent, []).append({
"level": "antardasha",
"sign": row.get("sign"),
"lord": row.get("lord"),
"years": row.get("duration_years"),
"start": row.get("start_age"),
"end": row.get("end_age"),
"parent_sign": row.get("parent_md"),
"parent_lord": row.get("parent_md"),
"source": "narayana_dense_boundaries",
})
periods = []
for row in sequence:
if not isinstance(row, dict):
continue
sign = row.get("sign")
parent_key = str(row.get("period_key") or sign or "")
periods.append({
"sign": sign,
"lord": row.get("lord"),
"years": row.get("years"),
"start": row.get("start_age"),
"end": row.get("end_age"),
"antardasha": children_by_parent.get(parent_key, []),
})
return periods
def _yogini_periods(module: dict, rows: list[dict]) -> list[dict]:
if any(isinstance(row.get("antardasha"), list) and row.get("antardasha") for row in rows):
return rows
try:
from yogini_dasha import yogini_antardashas
except ImportError:
from scripts.yogini_dasha import yogini_antardashas
year_days = module.get("dasha_year_days") or 365.25
birth = None
if rows:
birth = rows[0].get("start_date")
if not birth:
return rows
attached = []
for row in rows:
copied = dict(row)
try:
copied["antardasha"] = yogini_antardashas(row, birth_dt=birth, year_days=float(year_days))
except (KeyError, TypeError, ValueError) as exc:
raise ValueError(f"yogini antardasha failed for {row.get('yogini')}: {exc}") from exc
for child in copied["antardasha"]:
child["parent_lord"] = row.get("planet") or row.get("lord")
child["main"] = row.get("yogini")
attached.append(copied)
return attached
def enrich_master_family(key: str, module: dict | None, periods: list | None) -> list[dict]:
"""Copy family rows and attach children that already have a sourced rule.
Vimshottari is left untouched: its three layers are rendered from the
dasha timeline, not from this pack.
"""
module = module if isinstance(module, dict) else {}
rows = _copy_rows(periods or [])
if key == "vimshottari":
return rows
if key == "narayana":
return _narayana_periods(module)
if key == "yogini":
return _yogini_periods(module, rows)
if key == "kala_chakra":
if not rows:
return rows
enriched, _ad, _pd = attach_kalachakra_simplified_children(
rows,
module.get("total_cycle"),
)
return enriched
return rows
+9 -1
View File
@@ -2977,10 +2977,18 @@ def _native_dasha_master_families(modules: dict) -> dict:
'chaturshitisama': modules.get('chaturshitisama_dasha'),
'shastihayani': modules.get('shastihayani_dasha'),
}
try:
from annual_native.dasha_children import enrich_master_family
except ImportError:
from scripts.annual_native.dasha_children import enrich_master_family
families = {}
for key, module in mapping.items():
family = _native_dasha_family_status(module)
periods = _normalize_dasha_family_periods(module)
periods = enrich_master_family(
key,
module if isinstance(module, dict) else {},
_normalize_dasha_family_periods(module),
)
if periods:
family['periods'] = periods
families[key] = family
+31 -1
View File
@@ -1955,7 +1955,17 @@ def render_pl9_parity_markdown(packet: dict) -> str:
periods = _list(family.get('periods'))
if not periods:
continue
lines.extend([f'### p{page} {title_text}', ''])
heading = f'### p{page} {title_text}'
if 'Kala Chakra' in title_text:
heading += ' (Simplified model)' if _lang == 'en' else '(简化模型)'
lines.extend([heading, ''])
if 'Kala Chakra' in title_text:
lines.extend([
'- Sub-periods use a simplified model. The major periods are unchanged.'
if _lang == 'en'
else '- 子运和次小运按简化模型,挂在现有主运上。主运本身没有改。',
'',
])
if 'Narayana' in title_text:
# BUG-1215: the product Narayana is the legacy algorithm.
if _lang == 'en':
@@ -2032,6 +2042,26 @@ def render_pl9_parity_markdown(packet: dict) -> str:
f"{_period_start(child) or _cell(child_raw.get('start_date') or child_raw.get('start'))} | "
f"{_period_end(child) or _cell(child_raw.get('end_date') or child_raw.get('end'))} |"
)
pd_rows = []
for child in child_rows:
for grand in _list(child.get('pratyantardasha')):
if isinstance(grand, dict):
pd_rows.append((child, grand))
if pd_rows:
lines.extend([
'',
'| PD | 主层 | 子层 | 次小运 | 起始 | 结束 |' if _lang != 'en' else '| PD | Maha | Antar | Pratyantar | Start | End |',
'|----|------|------|--------|------|------|',
])
for pd_index, (child, grand) in enumerate(pd_rows, start=1):
grand_lord = grand.get('lord') or grand.get('planet') or grand.get('rashi') or grand.get('sign')
lines.append(
f"| {pd_index} | {_cell(_token(child.get('parent_sign') or child.get('parent_lord') or child.get('main')))} | "
f"{_cell(_token(child.get('lord') or child.get('rashi') or child.get('sign')))} | "
f"{_cell(_token(grand_lord))} | {_period_start(grand)} | {_period_end(grand)} |"
)
else:
lines.append('This system does not provide sub-periods.' if _lang == 'en' else '本体系未提供子运')
lines.append('')
# BUG-1215 / BUG-1218: the full-data edition keeps the legacy label and the
# Rath table already printed by the reference renderer. Display only.
+144
View File
@@ -0,0 +1,144 @@
"""T5: sourced dasha children are copied into the master pack and rendered."""
from __future__ import annotations
from datetime import datetime
from scripts.annual_native.dasha_children import (
attach_kalachakra_simplified_children,
enrich_master_family,
)
from scripts.ashtottari_dasha import calculate_ashtottari_dasha
from scripts.jyotish_engine import _native_dasha_master_families
from scripts.kalachakra_dasha import calculate_kalachakra_dasha
from scripts.pl9_reader_export import render_pl9_parity_markdown
from scripts.yogini_dasha import calculate_yogini_dasha
def _major_view(row: dict) -> tuple:
return (
row.get("lord") or row.get("planet") or row.get("rashi") or row.get("sign"),
row.get("rashi") or row.get("sign"),
row.get("years"),
row.get("start_date") or row.get("start"),
row.get("end_date") or row.get("end"),
)
def _applicable_ashtottari() -> dict:
for index in range(27):
result = calculate_ashtottari_dasha({
"moon_nakshatra_index": index,
"is_shukla_paksha": True,
"moon_longitude": index * (360.0 / 27.0) + 1.0,
"birth_datetime": datetime(1990, 1, 1, 12, 0),
})
if result.get("major"):
return result
raise AssertionError("no applicable Ashtottari fixture")
def test_kalachakra_children_hang_on_existing_majors() -> None:
source = calculate_kalachakra_dasha({
"moon_nakshatra_index": 0,
"moon_pada": 1,
"birth_datetime": datetime(1990, 6, 15, 10, 30),
})
before = [_major_view(row) for row in source["major"]]
enriched, antardasha, pratyantar = attach_kalachakra_simplified_children(
source["major"], source["total_cycle"],
)
assert [_major_view(row) for row in source["major"]] == before
assert "antardasha" not in source["major"][0]
assert [_major_view(row) for row in enriched] == before
assert antardasha and pratyantar
assert enriched[0]["antardasha"][-1]["end_date"] == enriched[0]["end_date"]
assert enriched[0]["antardasha"][0]["model"] == "simplified"
assert enriched[0]["antardasha"][0]["pratyantardasha"]
def test_master_pack_keeps_sourced_children_and_leaves_modules_alone() -> None:
ashtottari = _applicable_ashtottari()
yogini = calculate_yogini_dasha({
"birth_datetime": datetime(1990, 6, 15, 10, 30),
"moon_longitude": 10.0,
"as_of": datetime(2020, 1, 1),
})
kalachakra = calculate_kalachakra_dasha({
"moon_nakshatra_index": 0,
"moon_pada": 1,
"birth_datetime": datetime(1990, 6, 15, 10, 30),
})
narayana = {
"mahadasha_sequence": [{
"sign": "Aries", "lord": "Mars", "years": 7,
"start_age": 0, "end_age": 7, "period_key": "md-0",
}],
"dense_boundaries": [{
"level": "AD", "sign": "Taurus", "lord": "Venus",
"duration_years": 1, "start_age": 0, "end_age": 1,
"parent_md": "Aries", "parent_period_key": "md-0",
}],
}
tribhagi = {"major": [{
"lord": "Sun", "years": 1,
"start_date": "2000-01-01T00:00:00", "end_date": "2001-01-01T00:00:00",
}]}
families = _native_dasha_master_families({
"ashtottari_dasha": ashtottari,
"yogini_dasha": yogini,
"kalachakra_dasha": kalachakra,
"narayana_dasha": narayana,
"tribhagi_dasha": tribhagi,
"dasha": {"timeline": [{"lord": "Sun", "start": "2000-01-01", "end": "2006-01-01"}]},
})
assert families["ashtottari"]["periods"][0]["antardasha"]
assert families["yogini"]["periods"][0]["antardasha"]
assert families["kala_chakra"]["periods"][0]["antardasha"][0]["pratyantardasha"]
assert families["narayana"]["periods"][0]["antardasha"][0]["sign"] == "Taurus"
assert "antardasha" not in tribhagi["major"][0]
assert "antardasha" not in yogini["major"][0]
assert "antardasha" not in kalachakra["major"][0]
assert "antardasha" not in narayana["mahadasha_sequence"][0]
assert [_major_view(row) for row in families["kala_chakra"]["periods"]] == [
_major_view(row) for row in kalachakra["major"]
]
assert families["vimshottari"]["periods"][0]["lord"] == "Sun"
assert "antardasha" not in families["vimshottari"]["periods"][0]
def test_missing_child_rule_and_kalachakra_model_are_labeled() -> None:
packet = {
"report_language": "zh",
"worksheets": {"timing_and_predictive_systems": {"dasha_master_pack": {"families": {
"tribhagi": {"periods": [{
"lord": "Sun", "years": 1,
"start_date": "2000-01-01T00:00:00", "end_date": "2001-01-01T00:00:00",
}]},
"kala_chakra": {"periods": [{
"lord": "Sun", "rashi": "Aries", "years": 7,
"start_date": "2000-01-01T00:00:00", "end_date": "2007-01-01T00:00:00",
"antardasha": [{
"lord": "Moon", "rashi": "Cancer", "parent_rashi": "Aries",
"start_date": "2000-01-01T00:00:00", "end_date": "2001-01-01T00:00:00",
"pratyantardasha": [{
"lord": "Mars", "rashi": "Aries",
"start_date": "2000-01-01T00:00:00", "end_date": "2000-02-01T00:00:00",
}],
}],
}]},
}}}},
}
chinese = render_pl9_parity_markdown(packet)
assert "本体系未提供子运" in chinese
assert "简化模型" in chinese
assert "次小运" in chinese
english = render_pl9_parity_markdown({**packet, "report_language": "en"})
assert "This system does not provide sub-periods." in english
assert "Simplified model" in english
def test_enrich_does_not_invent_children_for_vimshottari() -> None:
rows = enrich_master_family("vimshottari", {"timeline": []}, [{"lord": "Sun", "years": 6}])
assert rows == [{"lord": "Sun", "years": 6}]
assert "antardasha" not in rows[0]