a4f8620a71
## 新增模块 (16个) ### P0 精度修复 - ashtakavarga: calc_prastara_av() + calc_sodhita_av() - kakshya.py: Kakshya评分系统 (8区间×3.75°) - shadbala.py: Sputa Drishti + Yuddha Bala ### P1 核心升级 - bhava_bala.py: 宫位三元力量 (jyotishganit MIT) - pancha_mahapurusha.py: PMC完整检测含4层失效条件 - sade_sati.py: Sade Sati+Kantaka Shani - sudarshana_chakra.py: 三参考点盘+收敛分析 - tajika.py: Sahams 7→36 + Tajika Yogas 10种 - birth_time_rectifier.py: 生时矫正 ### P2 覆盖扩展 - kp_system.py: KP Sublord+ABCD Significator (diliprk/VedicAstro MIT) - synastry.py: 16因子合盘36分制 (dashaflow MIT) - muhurtha_election.py: 6活动选举 (dashaflow MIT) - career_analysis.py: 结构化事业引擎 - relationship_analysis.py: 结构化感情引擎 - conditional_dashas.py: Dwisaptati+Shattrimsa+Dwadashottari - divisional_charts_extended: D81/D108/D144 - remedies.py: 5类补救系统 ## 修改文件 jaimini/dasha_calculator/shadbala/SKILL.md/COVERAGE_AUDIT等12个 ## 开源复用: 4个MIT项目
615 lines
32 KiB
Python
615 lines
32 KiB
Python
from flatlib import const, aspects
|
|
from flatlib.chart import Chart
|
|
from flatlib.geopos import GeoPos
|
|
from flatlib.datetime import Datetime, Date
|
|
from flatlib.object import GenericObject
|
|
|
|
import collections
|
|
import polars as pl
|
|
from .utils import *
|
|
|
|
|
|
## GLOBAL VARS
|
|
RASHIS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra',
|
|
'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
|
|
|
|
ROMAN_HOUSE_NUMBERS = {'House1': 'I', 'House2': 'II', 'House3': 'III', 'House4': 'IV', 'House5': 'V', 'House6': 'VI',
|
|
'House7': 'VII', 'House8': 'VIII', 'House9': 'IX', 'House10': 'X', 'House11': 'XI', 'House12': 'XII'
|
|
}
|
|
|
|
## Lords of the 12 Zodiac Signs
|
|
SIGN_LORDS = ["Mars", "Venus", "Mercury", "Moon", "Sun", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Saturn", "Jupiter"]
|
|
|
|
NAKSHATRAS = ['Ashwini','Bharani','Krittika','Rohini','Mrigashīrsha', 'Ardra', 'Punarvasu', 'Pushya', 'Āshleshā',
|
|
'Maghā', 'PūrvaPhalgunī', 'UttaraPhalgunī', 'Hasta', 'Chitra', 'Svati', 'Vishakha', 'Anuradha', 'Jyeshtha', 'Mula',
|
|
'PurvaAshadha','UttaraAshadha', 'Shravana', 'Dhanishta','Shatabhisha', 'PurvaBhādrapadā', 'UttaraBhādrapadā', 'Revati']
|
|
|
|
AYANAMSA_MAPPING = { "Lahiri": const.AY_LAHIRI, "Lahiri_1940" : const.AY_LAHIRI_1940,
|
|
"Lahiri_VP285": const.AY_LAHIRI_VP285, "Lahiri_ICRC" : const.AY_LAHIRI_ICRC, "Raman": const.AY_RAMAN,
|
|
"Krishnamurti": const.AY_KRISHNAMURTI, "Krishnamurti_Senthilathiban": const.AY_KRISHNAMURTI_SENTHILATHIBAN,
|
|
}
|
|
|
|
|
|
HOUSE_SYSTEM_MAPPING = { "Placidus": const.HOUSES_PLACIDUS, "Equal": const.HOUSES_EQUAL,
|
|
"Equal 2": const.HOUSES_EQUAL_2, "Whole Sign": const.HOUSES_WHOLE_SIGN,
|
|
}
|
|
|
|
ASPECT_MAPPING = { const.NO_ASPECT: "No Aspect", const.CONJUNCTION: "Conjunction", const.SEXTILE: "Sextile",
|
|
const.SQUARE: "Square", const.TRINE: "Trine", const.OPPOSITION: "Opposition",
|
|
const.SEMISEXTILE: "Semi Sextile", const.SEMIQUINTILE: "Semi Quintile",
|
|
const.SEMISQUARE: "Semi Square", const.QUINTILE: "Quintile",
|
|
const.SESQUIQUINTILE: "Sesqui Quintile", const.SESQUISQUARE: "Sesqui Square",
|
|
const.BIQUINTILE: "Bi Quintile", const.QUINCUNX: "Quincunx",
|
|
}
|
|
|
|
# Columns names for NamedTuple Collections / Final Output DataFrames
|
|
HOUSES_TABLE_COLS = ["Object", "HouseNr","Rasi", "LonDecDeg", "SignLonDMS", "SignLonDecDeg", "DegSize",
|
|
"Nakshatra", "RasiLord", "NakshatraLord", "SubLord", "SubSubLord"]
|
|
|
|
PLANETS_TABLE_COLS = ["Object", "Rasi", "isRetroGrade", "LonDecDeg", "SignLonDMS", "SignLonDecDeg", "LatDMS",
|
|
"Nakshatra", "RasiLord", "NakshatraLord", "SubLord", "SubSubLord" ,"HouseNr"]
|
|
|
|
|
|
class VedicHoroscopeData:
|
|
def __init__(self, year:int, month:int, day:int, hour:int, minute:int, second : int,
|
|
latitude:float, longitude:float, tz : str = None, ayanamsa: str = "Krishnamurti", house_system : str = "Placidus"):
|
|
"""
|
|
Generates Planetary and House Positions Data for a time and place input.
|
|
|
|
Parameters
|
|
==========
|
|
year: Year input to generate chart, int (Eg: 2024)
|
|
month: Month input to generate chart, int (1 - 12)
|
|
day: Day of the month input to generate chart, int (Eg: 1 - 30,31)
|
|
hour: Hour input to generate chart, int (Eg: 0 - 23)
|
|
minute: Minute input to generate chart, int (Eg: 0 - 59)
|
|
second: Second input to generate chart, int (Eg: 0 - 59)
|
|
latitude: latitude, float
|
|
longitude: longitude, float
|
|
time_zone: timezone input to generate chart, str (Eg: America/New_York)
|
|
ayanamsa: ayanamsa input to generate chart, str
|
|
house: House System to generate chart,
|
|
"""
|
|
self.year = year
|
|
self.month = month
|
|
self.day = day
|
|
self.hour = hour
|
|
self.minute = minute
|
|
self.second = second
|
|
self.latitude = latitude
|
|
self.longitude = longitude
|
|
self.ayanamsa = ayanamsa
|
|
self.house_system = house_system
|
|
self.time_zone = tz if tz else TimezoneFinder().timezone_at(lat=self.latitude, lng=self.longitude)
|
|
self.chart_time = datetime(self.year, self.month, self.day, self.hour, self.minute)
|
|
self.utc,_ = get_utc_offset(self.time_zone, self.chart_time)
|
|
|
|
def get_ayanamsa(self):
|
|
"""Returns an Ayanamsa System from flatlib.sidereal library, based on user input"""
|
|
return AYANAMSA_MAPPING.get(self.ayanamsa, None)
|
|
|
|
def get_house_system(self):
|
|
"""Returns an House System from flatlib.sidereal library, based on user input"""
|
|
return HOUSE_SYSTEM_MAPPING.get(self.house_system, None)
|
|
|
|
def generate_chart(self):
|
|
"""Generates a `flatlib.Chart` object for the given time and location data"""
|
|
date = Datetime([self.year, self.month, self.day], ["+",self.hour, self.minute, self.second], self.utc)
|
|
geopos = GeoPos(self.latitude, self.longitude)
|
|
chart = Chart(date, geopos, IDs=const.LIST_OBJECTS, hsys=self.get_house_system(), mode = self.get_ayanamsa())
|
|
return chart
|
|
|
|
def get_planetary_aspects(self, chart: Chart):
|
|
"""Computes planetary aspects using flatlib modules getAspect"""
|
|
planets = [const.SUN, const.MOON, const.MARS, const.MERCURY, const.JUPITER, const.VENUS, const.SATURN,
|
|
const.URANUS, const.NEPTUNE, const.PLUTO, const.NORTH_NODE, const.SOUTH_NODE]
|
|
# aspects_output = []
|
|
aspects_dict = []
|
|
|
|
for p1 in planets:
|
|
for p2 in planets:
|
|
if p1 != p2:
|
|
obj1 = chart.get(p1)
|
|
obj2 = chart.get(p2)
|
|
aspect = aspects.getAspect(obj1, obj2, const.ALL_ASPECTS)
|
|
## Replace North and South nodes with conventional names
|
|
p1_new = p1.replace("North Node", "Rahu").replace("South Node", "Ketu")
|
|
p2_new = p2.replace("North Node", "Rahu").replace("South Node", "Ketu")
|
|
if aspect.exists():
|
|
aspect_type = ASPECT_MAPPING[int(aspect.type)] # Use global variable here
|
|
aspect_orb = round(aspect.orb, 3) # get the orb value
|
|
# Calculate longitude difference
|
|
p1_lon = round(obj1.lon, 3)
|
|
p2_lon = round(obj2.lon, 3)
|
|
lon_diff = round(abs(p1_lon - p2_lon), 3)
|
|
if lon_diff > 180:
|
|
lon_diff = 360 - lon_diff
|
|
|
|
aspects_dict.append({"P1":p1_new, "P2": p2_new, "AspectType" : aspect_type,
|
|
"AspectDeg" : aspect.type, "AspectOrb" : aspect_orb,
|
|
"P1_Lon": p1_lon,"P2_Lon": p2_lon,"LonDiff": lon_diff})
|
|
|
|
|
|
return aspects_dict
|
|
|
|
def get_planetary_aspects_15(self, chart: Chart):
|
|
"""
|
|
Computes exact planetary aspects based on multiples of 15 degrees without using flatlib's aspect functions.
|
|
"""
|
|
planets = [const.SUN, const.MOON, const.MARS, const.MERCURY, const.JUPITER, const.VENUS, const.SATURN,
|
|
const.URANUS, const.NEPTUNE, const.PLUTO, const.NORTH_NODE, const.SOUTH_NODE]
|
|
aspects_dict = []
|
|
|
|
for p1 in planets:
|
|
for p2 in planets:
|
|
if p1 != p2:
|
|
# Skip Rahu-Ketu pair as they're always 180 degrees apart
|
|
if {p1, p2} == {const.NORTH_NODE, const.SOUTH_NODE}:
|
|
continue
|
|
|
|
obj1 = chart.get(p1)
|
|
obj2 = chart.get(p2)
|
|
|
|
# Replace North and South nodes with conventional names
|
|
p1_name = p1.replace("North Node", "Rahu").replace("South Node", "Ketu")
|
|
p2_name = p2.replace("North Node", "Rahu").replace("South Node", "Ketu")
|
|
|
|
p1_lon = round(obj1.lon, 3)
|
|
p2_lon = round(obj2.lon, 3)
|
|
lon_diff = abs(p1_lon - p2_lon)
|
|
if lon_diff > 180:
|
|
lon_diff = 360 - lon_diff
|
|
lon_diff = round(lon_diff, 3)
|
|
|
|
# Check if lon_diff is a multiple of 15 degrees with a small tolerance
|
|
if abs(lon_diff % 15) == 0.0 :
|
|
|
|
aspects_dict.append({
|
|
"P1": p1_name,
|
|
"P2": p2_name,
|
|
"P1_Lon": p1_lon,
|
|
"P2_Lon": p2_lon,
|
|
"AspectType": f"{int(lon_diff)}° Aspect",
|
|
"AspectDeg": lon_diff
|
|
})
|
|
|
|
# Remove duplicate entries using tuple sort
|
|
unique_aspects = {}
|
|
for aspect in aspects_dict:
|
|
planet_pair = tuple(sorted([aspect['P1'], aspect['P2']]))
|
|
if planet_pair not in unique_aspects:
|
|
unique_aspects[planet_pair] = aspect
|
|
|
|
return list(unique_aspects.values())
|
|
|
|
def get_planetary_aspects_vedic(self, planets_data: collections.namedtuple):
|
|
"""
|
|
Computes the major planetary aspects according to Vedic astrology, focusing on the positions of planets in houses and signs.
|
|
"""
|
|
|
|
# Get the planets data and Filter planets_data to remove objects like "Asc", "Chiron", "Syzygy", "Fortuna"
|
|
planets_data = [planet for planet in planets_data if planet.Object not in ["Asc", "Chiron", "Syzygy", "Fortuna"]]
|
|
|
|
# Define Vedic aspect rules based on sign and house positions
|
|
vedic_aspects_rules = {
|
|
'Conjunction': lambda p1, p2: p1.Rasi == p2.Rasi or p1.HouseNr == p2.HouseNr,
|
|
'Opposition': lambda p1, p2: ((RASHIS.index(p1.Rasi) - RASHIS.index(p2.Rasi)) % 12 == 6 or abs(p1.HouseNr - p2.HouseNr) == 6),
|
|
'Trine': lambda p1, p2: ((RASHIS.index(p1.Rasi) - RASHIS.index(p2.Rasi)) % 12 in [4, 8] or abs(p1.HouseNr - p2.HouseNr) in [4, 8]),
|
|
'Square': lambda p1, p2: ((RASHIS.index(p1.Rasi) - RASHIS.index(p2.Rasi)) % 12 in [3, 9] or abs(p1.HouseNr - p2.HouseNr) in [3, 9]),
|
|
'Sextile': lambda p1, p2: ((RASHIS.index(p1.Rasi) - RASHIS.index(p2.Rasi)) % 12 in [2, 10] or abs(p1.HouseNr - p2.HouseNr) in [2, 10])
|
|
}
|
|
|
|
aspects_vedic_output = []
|
|
vedic_aspects_dict = []
|
|
|
|
# Check each pair of planets for aspects
|
|
for i in range(len(planets_data)):
|
|
for j in range(i + 1, len(planets_data)):
|
|
for aspect_name, check_func in vedic_aspects_rules.items():
|
|
if check_func(planets_data[i], planets_data[j]):
|
|
aspects_vedic_output.append(f"{planets_data[i].Object} and {planets_data[j].Object} are in {aspect_name}")
|
|
vedic_aspects_dict.append({"P1":planets_data[i].Object, "P2": planets_data[j].Object, "Aspect" : aspect_name,
|
|
"P1_HouseNr": planets_data[i].HouseNr, "P2_HouseNr": planets_data[j].HouseNr,
|
|
"P1_Rasi": planets_data[i].Rasi, "P2_Rasi": planets_data[j].Rasi}
|
|
)
|
|
return vedic_aspects_dict, aspects_vedic_output
|
|
|
|
def get_ascendant_data(self, asc_data: GenericObject, PlanetsDataCollection : collections.namedtuple):
|
|
"""Generates Ascendant Data and returns the data in the format of the PlanetsDataCollection Named Tuple"""
|
|
asc_chart_data = clean_select_objects_split_str(str(asc_data))
|
|
asc_rl_nl_sl_data = self.get_rl_nl_sl_data(deg = asc_data.lon)
|
|
# Create a dictionary with None values for all fields
|
|
data_dict = {field: None for field in PlanetsDataCollection._fields}
|
|
# Update the specific fields with the ascendant data
|
|
data_dict["Object"] = asc_chart_data[0]
|
|
data_dict["Rasi"] = asc_chart_data[1]
|
|
data_dict["SignLonDMS"] = asc_chart_data[2]
|
|
data_dict["Nakshatra"] = asc_rl_nl_sl_data.get("Nakshatra", None)
|
|
data_dict["RasiLord"] = asc_rl_nl_sl_data.get("RasiLord", None)
|
|
data_dict["SubLord"] = asc_rl_nl_sl_data.get("SubLord", None)
|
|
data_dict["SubSubLord"] = asc_rl_nl_sl_data.get("SubSubLord", None)
|
|
data_dict["NakshatraLord"] = asc_rl_nl_sl_data.get("NakshatraLord", None)
|
|
data_dict["isRetroGrade"] = None
|
|
data_dict["LonDecDeg"] = round(asc_data.lon, 3)
|
|
data_dict["SignLonDecDeg"] = dms_to_decdeg(asc_chart_data[2])
|
|
data_dict["LatDMS"] = None
|
|
data_dict["HouseNr"] = 1
|
|
|
|
# Return a new PlanetsDataCollection instance with the data
|
|
return PlanetsDataCollection(**data_dict)
|
|
|
|
def get_rl_nl_sl_data(self, deg : float):
|
|
"""
|
|
Returns the Rashi (Sign) Lord, Nakshatra, Nakshatra Pada, Nakshatra Lord, Sub Lord and Sub Sub Lord
|
|
corresponding to the given degree.
|
|
"""
|
|
duration = [7, 20, 6, 10, 7, 18, 16, 19, 17]
|
|
|
|
lords = ["Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury"]
|
|
|
|
star_lords = lords * 3 ## lords for the 27 Nakshatras
|
|
|
|
## Compute Sign lords
|
|
sign_deg = deg % 360 # Normalize degree to [0, 360)
|
|
sign_index = int(sign_deg // 30) # Each zodiac sign is 30 degrees
|
|
|
|
# Compute Nakshatra details
|
|
nakshatra_deg = sign_deg % 13.332 # Each nakshatra is 13.332 degrees
|
|
nakshatra_index = int(sign_deg // 13.332) # Find the nakshatra index
|
|
pada = int((nakshatra_deg % 13.332) // 3.325) + 1 # Each pada is 3.325 degrees
|
|
|
|
# Ensure nakshatra_index is within bounds
|
|
nakshatra_index = nakshatra_index % len(NAKSHATRAS)
|
|
|
|
# Compute SubLords
|
|
deg = deg - 120 * int(deg / 120)
|
|
degcum = 0
|
|
i = 0
|
|
|
|
while i < 9:
|
|
deg_nl = 360 / 27
|
|
j = i
|
|
while True:
|
|
deg_sl = deg_nl * duration[j] / 120
|
|
k = j
|
|
while True:
|
|
deg_ss = deg_sl * duration[k] / 120
|
|
degcum += deg_ss
|
|
if degcum >= deg:
|
|
return {"Nakshatra": NAKSHATRAS[nakshatra_index], "Pada": pada,
|
|
"NakshatraLord": star_lords[nakshatra_index], "RasiLord": SIGN_LORDS[sign_index],
|
|
"SubLord": lords[j], "SubSubLord": lords[k] }
|
|
k = (k + 1) % 9
|
|
if k == j:
|
|
break
|
|
j = (j + 1) % 9
|
|
if j == i:
|
|
break
|
|
i += 1
|
|
|
|
|
|
def get_transit_details(self):
|
|
"""
|
|
Captures the rl_nl_sl transit data for all planets at the current chart time.
|
|
|
|
Returns
|
|
=======
|
|
A named tuple collection containing the transit details for all planets.
|
|
"""
|
|
# Define the named tuple for transit details
|
|
TransitDetails = collections.namedtuple('TransitDetails', [
|
|
'timestamp', 'PlanetName', 'PlanetLon', 'PlanetSign', 'Nakshatra',
|
|
'NakshatraLord', 'SubLord', 'SubLordSign', 'isRetrograde'
|
|
])
|
|
chart = self.generate_chart()
|
|
transit_data = []
|
|
timestamp = f"{self.year}-{self.month:02d}-{self.day:02d} {self.hour:02d}:{self.minute:02d}:00"
|
|
for planet in chart.objects:
|
|
if planet.id not in ["Chiron", "Syzygy", "Pars Fortuna"]:
|
|
planet_name = clean_select_objects_split_str(str(planet))[0]
|
|
## Get additional details like Nakshatra, RL, NL, SL details
|
|
rl_nl_sl_data = self.get_rl_nl_sl_data(deg = planet.lon)
|
|
planet_star = rl_nl_sl_data.get("Nakshatra", None)
|
|
planet_star_lord = rl_nl_sl_data.get("NakshatraLord", None)
|
|
planet_sub_lord = rl_nl_sl_data.get("SubLord", None)
|
|
sub_lord_sign = chart.get(planet_sub_lord.replace("Rahu","North Node").replace("Ketu", "South Node")).sign
|
|
|
|
## Append data to NamedTuple Collection
|
|
transit_data.append(TransitDetails(timestamp, planet_name, round(planet.lon,3), planet.sign, planet_star,
|
|
planet_star_lord, planet_sub_lord, sub_lord_sign, planet.isRetrograde()))
|
|
return transit_data
|
|
|
|
def get_planets_data_from_chart(self, chart: Chart, new_houses_chart: Chart = None):
|
|
"""
|
|
Generate the planets data table given a `flatlib.Chart` object.
|
|
Parameters
|
|
==========
|
|
chart: flatlib Chart object using which planetary positions have to be generated
|
|
new_houses_chart: flatlib Chart Object using which new house numbers have to be
|
|
computed, typically used along with KP Horary Method
|
|
"""
|
|
PlanetsData = collections.namedtuple("PlanetsData",PLANETS_TABLE_COLS)
|
|
|
|
# Get the house each planet is in
|
|
planet_in_house = self.get_planet_in_house(planets_chart = chart, houses_chart = new_houses_chart) if new_houses_chart \
|
|
else self.get_planet_in_house(planets_chart = chart, houses_chart = chart)
|
|
|
|
### Get Ascendant Data
|
|
ascendant_data = self.get_ascendant_data(asc_data = chart.get(const.ASC), PlanetsDataCollection = PlanetsData)
|
|
|
|
planets_data = []
|
|
planets_data.append(ascendant_data)
|
|
for planet in chart.objects:
|
|
planet_obj = clean_select_objects_split_str(str(planet))
|
|
planet_name, planet_lon_deg, planet_lat_deg = planet_obj[0], planet_obj[2], planet_obj[3]
|
|
|
|
## Get additional details like Nakshatra, RL, NL, SL details
|
|
rl_nl_sl_data = self.get_rl_nl_sl_data(deg = planet.lon)
|
|
planet_star = rl_nl_sl_data.get("Nakshatra", None)
|
|
planet_rasi_lord = rl_nl_sl_data.get("RasiLord", None)
|
|
planet_star_lord = rl_nl_sl_data.get("NakshatraLord", None)
|
|
planet_sub_lord = rl_nl_sl_data.get("SubLord", None)
|
|
planet_ss_lord = rl_nl_sl_data.get("SubSubLord", None)
|
|
|
|
# Get the house the planet is in
|
|
planet_house = planet_in_house.get(planet_name, None)
|
|
|
|
## Append data to NamedTuple Collection
|
|
planets_data.append(PlanetsData(planet_name, planet.sign, planet.isRetrograde(), round(planet.lon,3),
|
|
planet_lon_deg, round(planet.signlon, 3), planet_lat_deg, planet_star,
|
|
planet_rasi_lord, planet_star_lord, planet_sub_lord, planet_ss_lord, planet_house))
|
|
return planets_data
|
|
|
|
def get_houses_data_from_chart(self, chart: Chart):
|
|
"""Generate the houses data table given a `flatlib.Chart` object"""
|
|
HousesData = collections.namedtuple("HousesData", HOUSES_TABLE_COLS) # Create NamedTuple Collection to store data
|
|
houses_data = []
|
|
for house in chart.houses:
|
|
house_obj = str(house).strip('<').strip('>').split()
|
|
house_name, house_lon_deg, house_size = house_obj[0], house_obj[2], round(float(house_obj[3]), 3)
|
|
house_nr = int(house_name.strip("House"))
|
|
house_roman_nr = ROMAN_HOUSE_NUMBERS.get(house_name)
|
|
|
|
## Get additional details like Nakshatra, RL, NL, SL details
|
|
rl_nl_sl_data = self.get_rl_nl_sl_data(deg = house.lon)
|
|
house_star = rl_nl_sl_data.get("Nakshatra", None)
|
|
house_star_lord = rl_nl_sl_data.get("NakshatraLord", None)
|
|
house_rasi_lord = rl_nl_sl_data.get("RasiLord", None)
|
|
house_sub_lord = rl_nl_sl_data.get("SubLord", None)
|
|
house_ss_lord = rl_nl_sl_data.get("SubSubLord", None)
|
|
|
|
|
|
## Append data to NamedTuple Collection
|
|
houses_data.append(HousesData(house_roman_nr, house_nr, house.sign, round(house.lon,3), house_lon_deg, round(house.signlon, 3),
|
|
house_size, house_star, house_rasi_lord, house_star_lord, house_sub_lord, house_ss_lord))
|
|
return houses_data
|
|
|
|
def get_consolidated_chart_data(self, planets_data: collections.namedtuple, houses_data: collections.namedtuple,
|
|
return_style : str = None):
|
|
"""
|
|
Create consolidated dict data where all objects (both planets and houses) are listed by rasi (sign).
|
|
If `return_style == "dataframe_records"`, returns the consolidated data in the form of a list of dictionaries
|
|
If `return_style == None`, returns the consolidated data in the form of a dictionary grouped by rasi
|
|
"""
|
|
# Construct polars DataFrame of planets and houses data from the flatlib_sidereal Chart object
|
|
req_cols = ["Rasi","Object","isRetroGrade", "LonDecDeg" ,"SignLonDMS", "SignLonDecDeg"]
|
|
planets_df = pl.DataFrame(planets_data).select(req_cols)
|
|
houses_df = pl.DataFrame(houses_data).with_columns(pl.lit(False).alias("isRetroGrade")).select(req_cols)
|
|
|
|
## Create joined dataframe of planets and houses data
|
|
df_concat = pl.concat([houses_df, planets_df])
|
|
|
|
# Group by 'Rasi' and aggregate all columns into a list or first non-null value
|
|
result_df = df_concat.group_by('Rasi').agg([
|
|
pl.col('Object').alias('Object'),
|
|
pl.col('isRetroGrade').alias('isRetroGrade'),
|
|
pl.col('LonDecDeg').alias('LonDecDeg'),
|
|
pl.col('SignLonDMS').alias('SignLonDMS'),
|
|
pl.col('SignLonDecDeg').alias('SignLonDecDeg'),
|
|
# pl.col('LatDMS').alias('LatDMS')
|
|
])
|
|
|
|
## Sort by Rashis Order from `Aries` to `Pisces` in Clockwise Order
|
|
result_df = result_df.with_columns(pl.col('Rasi').map_elements(lambda rasi: RASHIS.index(rasi), return_dtype=pl.Int32).alias('RashiOrder'))
|
|
result_df = result_df.sort('RashiOrder').drop('RashiOrder') ## Sort by RashiOrder and Drop the column
|
|
|
|
|
|
if return_style == "dataframe_records":
|
|
return result_df.to_dicts()
|
|
else:
|
|
return self.get_consolidated_chart_data_rasi_wise(df = result_df)
|
|
|
|
def get_consolidated_chart_data_rasi_wise(self, df: pl.DataFrame):
|
|
"""Returns in dict format, the consolidated chart data stored in a polars DataFrame grouped by Rasi"""
|
|
final_dict = {}
|
|
columns = df.columns
|
|
|
|
for row in df.iter_rows():
|
|
rasi = row[columns.index('Rasi')]
|
|
final_dict[rasi] = {}
|
|
for obj, is_retrograde, lon_dd, lon_dms, sign_lon_dd in zip(row[columns.index('Object')],
|
|
row[columns.index('isRetroGrade')] ,
|
|
row[columns.index('LonDecDeg')] ,
|
|
row[columns.index('SignLonDMS')] ,
|
|
row[columns.index('SignLonDecDeg')] ):
|
|
|
|
final_dict[rasi][obj] = {"is_Retrograde": is_retrograde, "LonDecDeg": lon_dd,
|
|
"SignLonDMS" : lon_dms, "SignLonDecDeg": sign_lon_dd}
|
|
return final_dict
|
|
|
|
|
|
def get_planet_in_house(self, houses_chart: Chart, planets_chart: Chart):
|
|
"""Determine which house each planet is in given a `flatlib.Chart` object"""
|
|
planet_in_house = {}
|
|
|
|
# Get the list of cusps (the boundary between two houses) along with their house numbers
|
|
cusps = sorted([(house.lon, int(house.id.replace('House', ''))) for house in houses_chart.houses])
|
|
# Add the first cusp (plus 360 degrees) as the end of the last house
|
|
cusps.append((cusps[0][0] + 360, cusps[0][1]))
|
|
# print("Cusps ADJ:",cusps)
|
|
|
|
for planet in planets_chart.objects:
|
|
planet_name = clean_select_objects_split_str(str(planet))[0]
|
|
planet_lon = planet.lon
|
|
for i in range(12):
|
|
if cusps[i][0] <= planet_lon < cusps[i+1][0]:
|
|
planet_in_house[planet_name] = cusps[i][1]
|
|
break
|
|
## Check if planet is overlapping into the last cusp
|
|
elif cusps[i][0] <= planet_lon + 360 < cusps[i+1][0]:
|
|
planet_in_house[planet_name] = cusps[i][1]
|
|
break
|
|
|
|
return planet_in_house
|
|
|
|
def get_unique_house_nrs_for_rasi_lord(self, planets_df : pl.DataFrame, planet_name: str):
|
|
"""Returns the unique set of house numbers where the given planet is the rasi lord"""
|
|
# Group by RasiLord and aggregate HouseNr into a list
|
|
grouped_df = planets_df.group_by("RasiLord").agg([pl.col('HouseNr')\
|
|
.map_elements(list, return_dtype=pl.Object).alias('HouseNr')])
|
|
|
|
# Filter the DataFrame for the given planet
|
|
filtered_df = grouped_df.filter(pl.col('RasiLord') == planet_name)
|
|
|
|
# If there are no matching rows, return an empty list
|
|
if filtered_df.shape[0] == 0:
|
|
return []
|
|
|
|
# Get the list of house numbers for the given planet
|
|
house_nrs = filtered_df['HouseNr'].to_list()[0]
|
|
|
|
# Remove duplicates by converting the list to a set and then back to a list
|
|
unique_house_nrs = list(set(house_nrs))
|
|
|
|
return unique_house_nrs
|
|
|
|
def get_planet_wise_significators(self, planets_data: collections.namedtuple, houses_data: collections.namedtuple):
|
|
"""Generate the ABCD significators table for each planet"""
|
|
significators_table_cols = ["Planet", "A", "B", "C", "D"]
|
|
SignificatorsData = collections.namedtuple("PlanetSignificators", significators_table_cols)
|
|
|
|
# Get the planets data and Filter planets_data to remove objects like "Asc", "Chiron", "Syzygy", "Fortuna"
|
|
planets_data = [planet for planet in planets_data if planet.Object not in ["Asc", "Chiron", "Syzygy", "Fortuna"]]
|
|
|
|
# Get the house each planet is in
|
|
planets_house_deposition = {data.Object: data.HouseNr for data in planets_data}
|
|
|
|
|
|
significators_data = []
|
|
for planet in planets_data:
|
|
# A. House occupied by the star lord (Nakshatra Lord) of the planet
|
|
A = planets_house_deposition.get(planet.NakshatraLord, None)
|
|
|
|
# B. House occupied by the planet itself
|
|
B = planet.HouseNr
|
|
|
|
# C. House nrs where the star lord planet is also the rashi lord
|
|
C = [data.HouseNr for data in houses_data if data.RasiLord == planet.NakshatraLord]
|
|
|
|
# D. House nrs where the planet itself is also the rashi lord
|
|
D = [data.HouseNr for data in houses_data if data.RasiLord == planet.Object]
|
|
|
|
# Append data to NamedTuple Collection
|
|
significators_data.append(SignificatorsData(planet.Object, A, B, C, D))
|
|
|
|
return significators_data
|
|
|
|
def get_house_wise_significators(self, planets_data : collections.namedtuple, houses_data: collections.namedtuple):
|
|
"""Generate the ABCD significators table for each house"""
|
|
significators_table_cols = ["House", "A", "B", "C", "D"]
|
|
SignificatorsData = collections.namedtuple("HouseSignificators", significators_table_cols)
|
|
|
|
# Get the planets data and Filter planets_data to remove objects like "Asc", "Chiron", "Syzygy", "Fortuna"
|
|
planets_data = [planet for planet in planets_data if planet.Object not in ["Asc", "Chiron", "Syzygy", "Fortuna"]]
|
|
|
|
# Create a mapping of planets to their star lords (Nakshatra Lords)
|
|
planet_to_star_lord = {data.Object: data.NakshatraLord for data in planets_data}
|
|
|
|
significators_data = []
|
|
for house in houses_data:
|
|
# A. Planets in the star of occupants of that house
|
|
# A1.1) Get all the rows which match with the house.HouseNr
|
|
occupant_house_planets = [planet.Object for planet in planets_data if planet.HouseNr == house.HouseNr]
|
|
# A1.2.) For each planet in occupant_house_planets find out if that planet is the Nakshatra Lord for any row in the planets_data
|
|
A = [planet.Object for planet in planets_data if planet.NakshatraLord in occupant_house_planets]
|
|
|
|
# B. Planets in that house
|
|
B = [planet.Object for planet in planets_data if planet.HouseNr == house.HouseNr]
|
|
|
|
# C. Planets in the star of owners of that house
|
|
C = [planet for planet, star_lord in planet_to_star_lord.items() if star_lord == house.RasiLord]
|
|
|
|
# D. Owner of that house
|
|
D = house.RasiLord
|
|
|
|
# Append data to NamedTuple Collection
|
|
significators_data.append(SignificatorsData(house.Object, A, B, C, D))
|
|
|
|
return significators_data
|
|
|
|
|
|
def compute_vimshottari_dasa(self, chart: Chart):
|
|
"""Computes the Vimshottari Dasa for the chart"""
|
|
# Get the moon object from the chart
|
|
moon = chart.get(const.MOON)
|
|
moon_details = clean_select_objects_split_str(str(moon))
|
|
|
|
# Moon's Details
|
|
moon_rl_nl_sl = self.get_rl_nl_sl_data(deg = moon.lon)
|
|
moon_nakshatra = moon_rl_nl_sl["Nakshatra"]
|
|
moon_nakshatra_lord = moon_rl_nl_sl["NakshatraLord"]
|
|
moon_sign_lord = moon_rl_nl_sl["RasiLord"]
|
|
# moon_sub_lord = moon_rl_nl_sl["SubLord"]
|
|
|
|
# Helper function to format datetime objects to string
|
|
dt_tuple_str = lambda start_date: start_date.strftime("%d-%m-%Y")
|
|
|
|
# Define the sequence of the Dasa periods and their lengths
|
|
dasa_sequence = ['Ketu', 'Venus', 'Sun', 'Moon', 'Mars', 'Rahu', 'Jupiter', 'Saturn', 'Mercury']
|
|
dasa_lengths = [7, 20, 6, 10, 7, 18, 16, 19, 17]
|
|
|
|
# Find the starting point of the Dasa sequence
|
|
start_index = dasa_sequence.index(moon_nakshatra_lord)
|
|
|
|
# Reorder the Dasa sequence to start with the moon's Nakshatra Lord
|
|
dasa_sequence = dasa_sequence[start_index:] + dasa_sequence[:start_index]
|
|
dasa_lengths = dasa_lengths[start_index:] + dasa_lengths[:start_index]
|
|
dasa_order = dict(zip(dasa_sequence, dasa_lengths))
|
|
|
|
# Compute the remaining portion of the starting Dasa
|
|
typical_nakshatra_arc = 800 #degree - mins
|
|
nakshatra_start = NAKSHATRAS.index(moon_nakshatra) * typical_nakshatra_arc
|
|
moon_lon_mins = round(moon.lon * 60, 2) # dms_to_mins(moon_lon_dms)
|
|
elapsed_moon_mins = moon_lon_mins - nakshatra_start
|
|
remaining_arc_mins = typical_nakshatra_arc - elapsed_moon_mins
|
|
starting_dasa_duration = dasa_order[moon_nakshatra_lord]
|
|
start_dasa_remaining_duration = (starting_dasa_duration/typical_nakshatra_arc) * remaining_arc_mins
|
|
start_dasa_elapsed_duration = starting_dasa_duration - start_dasa_remaining_duration
|
|
|
|
# Compute the start and end times of each Maha Dasa and Bhukti
|
|
vimshottari_dasa = {} # collections.OrderedDict()
|
|
chart_date = (self.year, self.month, self.day, self.hour, self.minute)
|
|
dasa_start_date = compute_new_date(start_date = chart_date, diff_value = start_dasa_elapsed_duration, direction = "backward")
|
|
for i in range(len(dasa_sequence)):
|
|
dasa = dasa_sequence[i]
|
|
dasa_length = dasa_lengths[i]
|
|
dasa_end_date = compute_new_date(start_date = tuple(dasa_start_date.timetuple())[:5], diff_value = dasa_length, direction = "forward")
|
|
vimshottari_dasa[dasa] = {'start': dt_tuple_str(dasa_start_date), 'end': dt_tuple_str(dasa_end_date), 'bhuktis': {} } #collections.OrderedDict()
|
|
bhukti_start_date = dasa_start_date
|
|
|
|
# Find the starting point of the Dasa sequence
|
|
start_index = dasa_sequence.index(dasa)
|
|
# Reorder the Bhukti sequence to start with the current main dasa
|
|
bhukti_sequence = dasa_sequence[start_index:] + dasa_sequence[:start_index]
|
|
bhukti_lengths = dasa_lengths[start_index:] + dasa_lengths[:start_index]
|
|
for j in range(len(bhukti_sequence)):
|
|
bhukti = bhukti_sequence[j]
|
|
bhukti_length = dasa_length * bhukti_lengths[j] / 120 # The total length of all Bhuktis in a Maha Dasa is 120 years
|
|
bhukti_end_date = compute_new_date(start_date = tuple(bhukti_start_date.timetuple())[:5], diff_value = bhukti_length, direction="forward")
|
|
vimshottari_dasa[dasa]['bhuktis'][bhukti] = {'start': dt_tuple_str(bhukti_start_date), 'end': dt_tuple_str(bhukti_end_date)}
|
|
# vimshottari_dasa[dasa]['bhuktis'][bhukti] = collections.OrderedDict([('start', dt_tuple_str(bhukti_start_date)), ('end', dt_tuple_str(bhukti_end_date))])
|
|
bhukti_start_date = bhukti_end_date
|
|
dasa_start_date = dasa_end_date
|
|
|
|
return vimshottari_dasa
|