标准大运表以前只印主运。已有出处的子运写进主包副本并出表,Kalachakra 子运按简化模型挂在现有主运上。
Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Grok (agent loop)
Cursor
parent
1c60926a13
commit
6b906f85c4
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
Reference in New Issue
Block a user