add secondary progression and solar arc evidence

This commit is contained in:
732642856
2026-07-15 11:48:13 +08:00
parent ce2bc79e6e
commit 451cb45e31
6 changed files with 124 additions and 8 deletions
@@ -32,7 +32,9 @@ To add only requested native time evidence, pass:
{
"western_timing": {
"transit_date": "2026-07-09",
"solar_return_year": 2026
"solar_return_year": 2026,
"secondary_progression_date": "2026-07-09",
"solar_arc_date": "2026-07-09"
}
}
```
@@ -40,8 +42,11 @@ To add only requested native time evidence, pass:
`transit_date` produces a local-date transit-to-natal major-aspect snapshot.
`solar_return_year` locates the exact tropical solar return and calculates its
return chart at the supplied birthplace/location. Both are calculation data,
not event verdicts. Secondary progressions and solar arcs remain unavailable
until separately implemented and tested.
not event verdicts. `secondary_progression_date` uses one ephemeris day per
tropical year for progressed planets. `solar_arc_date` applies the true
secondary-progressed-Sun arc to natal planets/ASC/MC. Both remain `partial`:
progressed angles, converses, parans, midpoints, duration, and interpretation
are not asserted.
## Accepted Input
+4 -2
View File
@@ -202,14 +202,16 @@ def _western_evidence_packet_from_body(
**birth,
transit_date=timing_request.get('transit_date'),
solar_return_year=timing_request.get('solar_return_year'),
secondary_progression_date=timing_request.get('secondary_progression_date'),
solar_arc_date=timing_request.get('solar_arc_date'),
)
if timing:
packet['timing_techniques'] = timing
packet['sections']['timing_techniques'] = {'status': 'used', 'source_path': 'western.native_timing'}
packet['missing_sections'] = [item for item in packet['missing_sections'] if item != 'timing_techniques']
packet['boundary'] = (
'Native calculations include only requested transit snapshots and/or exact solar-return charts; '
'they do not infer duration, outcomes, progressions, solar arcs, returns beyond solar, or interpretation.'
'Native calculations include only explicitly requested transit, solar-return, secondary-progression, '
'and solar-arc layers; they do not infer duration, outcomes, or interpretation.'
)
return packet
except Exception as exc: # pragma: no cover - defensive boundary
+1 -1
View File
@@ -48,7 +48,7 @@ Do not add private birth data, API keys, or desktop oracle screenshots to this p
如果没有 VedAstro official_raw_response,请标记 official_blocked 或 local_fallback。
如果我提供西方占星导出,请作为 western_oracle_payload 进入统一主链,不要把单边西占信号说成双系统互证。
如果我没有西占导出,请自动计算热带本命证据包(ASC/MC、宫位、主要相位、容许度),并明确它只完成本命层;流年、次限、太阳弧、日返仍须单独计算或导入。
如需西占时间技术,请传 western_timing`{"transit_date":"YYYY-MM-DD","solar_return_year":YYYY}`;当前支持指定日 transit精确太阳回归,不把未计算的次限/太阳弧标成已用。
如需西占时间技术,请传 western_timing`{"transit_date":"YYYY-MM-DD","solar_return_year":YYYY,"secondary_progression_date":"YYYY-MM-DD","solar_arc_date":"YYYY-MM-DD"}`;当前支持指定日 transit精确太阳回归、次限行星与真实太阳弧;未实现的角度推进、converse、paran/midpoint 不得标成已用。
## Highest Quality Mode
+82
View File
@@ -113,10 +113,86 @@ def calculate_solar_return(*, target_year: int, **birth: Any) -> dict[str, Any]:
}
def _birth_jd(**birth: Any) -> float:
zone, _ = _birth_zone(birth["timezone"])
local = datetime(
int(birth["year"]), int(birth["month"]), int(birth["day"]),
int(birth["hour"]), int(birth["minute"]), int(birth.get("second", 0)), tzinfo=zone,
)
utc = local.astimezone(ZoneInfo("UTC"))
return swe.julday(utc.year, utc.month, utc.day, utc.hour + utc.minute / 60 + utc.second / 3600)
def _progressed_planets(progressed_jd: float) -> dict[str, dict[str, Any]]:
flags = swe.FLG_SWIEPH | swe.FLG_SPEED
planets: dict[str, dict[str, Any]] = {}
for name, planet_id in _PLANETS.items():
values, _ = swe.calc_ut(progressed_jd, planet_id, flags)
planets[name] = _point(values[0], speed=values[3])
return planets
def calculate_secondary_progressions(*, target_date: str, **birth: Any) -> dict[str, Any]:
"""Calculate progressed planets using one ephemeris day per tropical year."""
natal_chart = build_tropical_natal_chart(**birth)
target_jd, local = _target_jd(target_date, birth["timezone"])
birth_jd = _birth_jd(**birth)
elapsed_years = (target_jd - birth_jd) / 365.242189
progressed_jd = birth_jd + elapsed_years
planets = _progressed_planets(progressed_jd)
natal_points = {
**natal_chart["natal"]["planets"],
"ascendant": natal_chart["natal"]["angles"]["ascendant"],
"mc": natal_chart["natal"]["angles"]["mc"],
}
return {
"technique": "secondary_progressions",
"status": "partial",
"method": "one_ephemeris_day_per_tropical_year",
"target_date": target_date,
"target_local_time": local.isoformat(),
"elapsed_tropical_years": round(elapsed_years, 8),
"progressed_julian_day_ut": round(progressed_jd, 8),
"natal_sun_longitude": natal_chart["natal"]["planets"]["sun"]["longitude"],
"progressed_planets": planets,
"aspects": _cross_aspects(planets, natal_points),
"boundary": "Progressed planets only. Progressed angles, lunar phases, stations, duration, and interpretation remain separate audited layers.",
}
def calculate_solar_arc_directions(*, target_date: str, **birth: Any) -> dict[str, Any]:
"""Direct natal points by the true arc of the secondary progressed Sun."""
natal_chart = build_tropical_natal_chart(**birth)
progressions = calculate_secondary_progressions(target_date=target_date, **birth)
natal_sun = natal_chart["natal"]["planets"]["sun"]["longitude"]
progressed_sun = progressions["progressed_planets"]["sun"]["longitude"]
arc = _longitude(progressed_sun - natal_sun)
natal_points = {
**natal_chart["natal"]["planets"],
"ascendant": natal_chart["natal"]["angles"]["ascendant"],
"mc": natal_chart["natal"]["angles"]["mc"],
}
directed = {name: _point(point["longitude"] + arc) for name, point in natal_points.items()}
return {
"technique": "solar_arc_directions",
"status": "partial",
"method": "secondary_progressed_sun_arc",
"target_date": target_date,
"natal_sun_longitude": natal_sun,
"progressed_sun_longitude": progressed_sun,
"solar_arc_degrees": round(arc, 8),
"directed_points": directed,
"aspects": _cross_aspects(directed, natal_points),
"boundary": "True secondary-progressed-Sun arc applied to natal planets/ASC/MC. Directional converse, latitude, parans, midpoint, duration, and event interpretation are not inferred.",
}
def build_timing_techniques(
*,
transit_date: str | None = None,
solar_return_year: int | None = None,
secondary_progression_date: str | None = None,
solar_arc_date: str | None = None,
**birth: Any,
) -> dict[str, Any]:
"""Materialize only the requested, independently auditable timing layers."""
@@ -125,4 +201,10 @@ def build_timing_techniques(
techniques["transits"] = calculate_transit_to_natal(target_date=transit_date, **birth)
if solar_return_year is not None:
techniques["solar_return"] = calculate_solar_return(target_year=int(solar_return_year), **birth)
if secondary_progression_date:
techniques["secondary_progressions"] = calculate_secondary_progressions(
target_date=secondary_progression_date, **birth
)
if solar_arc_date:
techniques["solar_arc_directions"] = calculate_solar_arc_directions(target_date=solar_arc_date, **birth)
return techniques
+9 -2
View File
@@ -80,7 +80,12 @@ def test_workflow_adds_only_explicit_western_timing_layers() -> None:
packet = _western_evidence_packet_from_body(
{
"entry_mode": "direct_chart",
"western_timing": {"transit_date": "2026-07-09", "solar_return_year": 2026},
"western_timing": {
"transit_date": "2026-07-09",
"solar_return_year": 2026,
"secondary_progression_date": "2026-07-09",
"solar_arc_date": "2026-07-09",
},
},
{"primary_theme": "career"},
birth_payload={
@@ -89,7 +94,9 @@ def test_workflow_adds_only_explicit_western_timing_layers() -> None:
},
)
assert set(packet["timing_techniques"]) == {"transits", "solar_return"}
assert set(packet["timing_techniques"]) == {
"transits", "solar_return", "secondary_progressions", "solar_arc_directions",
}
assert packet["sections"]["timing_techniques"]["status"] == "used"
+20
View File
@@ -4,6 +4,8 @@ from __future__ import annotations
from scripts.western_timing_engine import (
build_timing_techniques,
calculate_secondary_progressions,
calculate_solar_arc_directions,
calculate_solar_return,
calculate_transit_to_natal,
)
@@ -37,3 +39,21 @@ def test_timing_builder_only_contains_requested_techniques() -> None:
timing = build_timing_techniques(**_BIRTH, transit_date="2026-07-09", solar_return_year=2026)
assert set(timing) == {"transits", "solar_return"}
def test_secondary_progressions_use_declared_day_for_year_contract() -> None:
progressions = calculate_secondary_progressions(**_BIRTH, target_date="2026-07-09")
assert progressions["technique"] == "secondary_progressions"
assert progressions["method"] == "one_ephemeris_day_per_tropical_year"
assert progressions["progressed_planets"]["sun"]["longitude"] != progressions["natal_sun_longitude"]
assert progressions["aspects"]
def test_solar_arc_uses_secondary_progressed_sun_arc() -> None:
directions = calculate_solar_arc_directions(**_BIRTH, target_date="2026-07-09")
assert directions["technique"] == "solar_arc_directions"
assert directions["method"] == "secondary_progressed_sun_arc"
assert 0 < directions["solar_arc_degrees"] < 40
assert directions["directed_points"]["sun"]["longitude"] != directions["natal_sun_longitude"]