fix(engine): dispositor cycles are not collapsed to one planet; dasa convergence drops uncalibrated probabilities (upstream e9beae6b)

final_dispositor_from_chain and its regression test are upstream
yinduzhanxing e9beae6b; probability is None with probability_basis. The
Narayana convergence additions of that commit are out of scope.
TASK-upstream-sync4-20261002 T7.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
Jesse_Chen
2026-10-02 13:56:31 +08:00
co-authored by Claude Opus 5.5
parent 08376cf0a8
commit bc9b9f89bb
2 changed files with 66 additions and 21 deletions
+28 -21
View File
@@ -472,6 +472,29 @@ def calc_inter_chart_linkage(planet_name, d1_data, d9_data, d10_data, d12_data=N
return result
def final_dispositor_from_chain(chain):
"""Return a terminal only for a self-loop; retain multi-body cycles.
Ported from upstream yinduzhanxing e9beae6b (TASK-upstream-sync4-20261002 T7).
"""
path = []
for row in chain:
planet, target = row.get('planet'), row.get('dispositor')
if not planet or not target or (path and path[-1] != planet):
return None
if not path:
path.append(planet)
if target == planet:
return planet
if target in path:
cycle = path[path.index(target):]
rotations = [cycle[i:] + cycle[:i] for i in range(len(cycle))]
cycle = min(rotations)
return 'Cycle: ' + ' -> '.join(cycle + cycle[:1])
path.append(target)
return None # incomplete or depth-limited chain is not a terminal
def calc_all_dispositor_chains(planets_data):
"""为所有行星计算定位星链"""
result = {}
@@ -15023,21 +15046,20 @@ def _calc_dasa_convergence(dasha_result, chara_dasha_result, yogini_result, plan
sc = info['system_count']
if sc >= 4:
info['convergence_level'] = 'L5'
info['probability'] = '85-92%'
info['interpretation'] = '四个及以上推运系统同时激活,顶级收敛信号'
elif sc >= 3:
info['convergence_level'] = 'L4'
info['probability'] = '75-85%'
info['interpretation'] = '三系统同时激活,极强信号'
elif sc >= 2:
info['convergence_level'] = 'L3'
info['probability'] = '50-65%'
info['interpretation'] = '双系统激活,强信号'
else:
info['convergence_level'] = 'L1'
info['probability'] = '+15-20%'
info['interpretation'] = '单系统激活,需Transit确认'
info['probability'] = None
info['probability_basis'] = 'No empirical probability calibration; level counts activating systems only.'
# 收敛窗口(最高优先级的领域)
top_domains = sorted(domain_activations.items(), key=lambda x: x[1]['system_count'], reverse=True)[:5]
@@ -15725,23 +15747,8 @@ def cmd_full_reading(args):
report['modules']['inter_chart_linkage'] = ic_result
# 补充:每个行星的"最终定位星"(Dispositor Chain 的最后一个非循环元素)
final_dispositors = {}
for pname, chain in dc_result.items():
if chain:
# 找到最后一个不重复的元素
seen = set()
last_valid = None
for item in chain:
if item['dispositor'] in seen:
break
seen.add(item['dispositor'])
last_valid = item
if last_valid:
final_dispositors[pname] = last_valid['dispositor']
else:
final_dispositors[pname] = chain[-1]['dispositor'] if chain else None
else:
final_dispositors[pname] = None
# A multi-planet cycle has no unique terminal dispositor.
final_dispositors = {p: final_dispositor_from_chain(c) for p, c in dc_result.items()}
report['modules']['final_dispositors'] = final_dispositors
except Exception as e:
+38
View File
@@ -0,0 +1,38 @@
"""Do not collapse a directed dispositor cycle into an arbitrary terminal."""
import ast
from pathlib import Path
def resolver():
# Isolate the pure function without importing the large CLI and adapters.
source = Path(__file__).parents[1] / 'scripts/jyotish_engine.py'
tree = ast.parse(source.read_text())
node = next(n for n in tree.body if isinstance(n, ast.FunctionDef)
and n.name == 'final_dispositor_from_chain')
namespace = {}
exec(compile(ast.Module(body=[node], type_ignores=[]), str(source), 'exec'), namespace)
return namespace[node.name]
def chain(*names):
return [{'planet':a,'dispositor':b} for a,b in zip(names,names[1:])]
def test_cycle_rotation_and_leading_planet():
f = resolver()
expected = 'Cycle: Jupiter -> Mercury -> Jupiter'
assert f(chain('Mercury','Jupiter','Mercury')) == expected
assert f(chain('Jupiter','Mercury','Jupiter')) == expected
assert f(chain('Venus','Jupiter','Mercury','Jupiter')) == expected
def test_terminal_and_incomplete_are_distinct():
f = resolver()
assert f(chain('Sun','Mars','Saturn','Saturn')) == 'Saturn'
assert f(chain('Sun','Mars')) is None
assert f([]) is None
assert f(chain('Sun','Mars') + chain('Venus','Venus')) is None
def test_three_body_cycle_preserves_direction():
assert resolver()(chain('Sun','Mars','Moon','Sun')) == 'Cycle: Mars -> Moon -> Sun -> Mars'