File size: 9,618 Bytes
e7652e0
 
 
0843376
e7652e0
 
4121b93
 
 
b6bb994
 
37a4c67
 
e7652e0
 
d507774
37a4c67
 
 
f08be6b
 
e7652e0
d507774
e7652e0
 
f08be6b
 
d507774
 
 
 
 
de98c8f
e67f755
37a4c67
d507774
 
 
99faed2
37a4c67
 
 
 
 
e67f755
 
d507774
 
e67f755
 
37a4c67
d507774
99faed2
d507774
e67f755
d507774
 
e67f755
d507774
e67f755
d507774
 
99faed2
e67f755
d507774
e67f755
99faed2
37a4c67
99faed2
 
37a4c67
d507774
 
 
 
 
0386735
d507774
 
 
 
 
 
 
 
 
 
 
d2eb13d
6ac4d9e
d507774
 
 
37a4c67
d507774
 
 
 
 
0386735
d507774
37a4c67
 
 
 
d507774
 
 
 
37a4c67
6ac4d9e
99faed2
37a4c67
7f6411d
d507774
7f6411d
d507774
 
b6bb994
3ffaa24
 
b6bb994
e67f755
5a2f1ac
37a4c67
7f6411d
 
 
 
b6bb994
d507774
e67f755
d507774
 
7f6411d
 
d507774
7f6411d
d507774
 
e67f755
d507774
7f6411d
d507774
 
 
 
7f6411d
d507774
e67f755
 
d507774
 
 
 
 
 
 
7f6411d
d507774
 
 
 
 
 
 
 
37a4c67
7f6411d
 
d507774
 
 
7f6411d
 
 
 
 
 
d507774
7f6411d
 
 
 
 
 
 
 
 
d507774
7f6411d
 
 
 
37a4c67
7f6411d
f08be6b
 
37a4c67
 
d507774
37a4c67
d507774
 
37a4c67
 
 
d507774
37a4c67
d507774
37a4c67
d507774
 
 
 
 
 
 
37a4c67
d507774
37a4c67
f08be6b
 
 
 
d507774
f08be6b
 
4121b93
37a4c67
d507774
 
e67f755
 
d507774
 
e67f755
99faed2
 
d507774
 
99faed2
e7652e0
 
d507774
 
e7652e0
 
 
d507774
37a4c67
d507774
 
 
 
 
 
 
 
 
 
 
 
99faed2
d507774
37a4c67
e7652e0
 
d507774
e7652e0
 
 
d507774
 
 
 
 
 
 
 
 
 
e7652e0
d507774
 
 
e7652e0
 
d507774
e7652e0
d507774
 
 
 
3ffaa24
d507774
 
99faed2
d507774
 
 
 
 
 
 
f08be6b
d507774
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
import gradio as gr
import matplotlib.pyplot as plt
from datetime import datetime
from dateutil import parser
from io import BytesIO
from PIL import Image
from geopy.geocoders import Nominatim
from timezonefinder import TimezoneFinder
import pytz
import swisseph as swe

# Initialize Swiss Ephemeris
swe.set_ephe_path(None)

# Russian translations for planets
PLANET_RU = {
    'Sun': 'Солнце', 'Moon': 'Луна', 'Mercury': 'Меркурий',
    'Venus': 'Венера', 'Mars': 'Марс', 
    'Jupiter': 'Юпитер', 'Saturn': 'Сатурн'
}

# Planet symbols for plotting
PLANET_SYMBOLS = {
    'Sun': '☉', 'Moon': '☾', 'Mercury': '☿', 'Venus': '♀',
    'Mars': '♂', 'Jupiter': '♃', 'Saturn': '♄'
}

# Zodiac signs in Russian
ZODIAC_SIGNS = [
    "Овен", "Телец", "Близнецы", "Рак", "Лев", "Дева",
    "Весы", "Скорпион", "Стрелец", "Козерог", "Водолей", "Рыбы"
]

def parse_query(query):
    """Parse the query into date, time, and location."""
    if not query.startswith("PLadder "):
        return None, None, "Query must start with 'PLadder'"
    
    try:
        parts = query.split(maxsplit=3)
        if len(parts) < 4:
            return None, None, "Incomplete query (need date, time, and location)"
        
        _, date_str, time_str, location = parts
        dt = parser.parse(f"{date_str} {time_str}")
        return dt, location, None
    except ValueError as e:
        return None, None, f"Invalid format: {str(e)}"

def get_utc_time(dt, location):
    """Convert local time to UTC using location's time zone."""
    geolocator = Nominatim(user_agent="pladder_app")
    try:
        loc = geolocator.geocode(location, timeout=10)
        if not loc:
            return None, None, None, "Location not found"
        
        lat, lon = loc.latitude, loc.longitude
        tz_str = TimezoneFinder().timezone_at(lng=lon, lat=lat)
        if not tz_str:
            return None, None, None, "Time zone not found"
        
        tz = pytz.timezone(tz_str)
        local_dt = tz.localize(dt)
        utc_dt = local_dt.astimezone(pytz.UTC)
        return utc_dt, lat, lon, None
    except Exception as e:
        return None, None, None, f"Error: {str(e)}"

def format_coords(lat, lon):
    """Format coordinates as degrees, minutes, seconds."""
    def dms(value, pos_dir, neg_dir):
        direction = pos_dir if value >= 0 else neg_dir
        abs_value = abs(value)
        degrees = int(abs_value)
        minutes = int((abs_value - degrees) * 60)
        seconds = int(round(((abs_value - degrees) * 60 - minutes) * 60))
        
        # Handle rounding overflow
        if seconds >= 60:
            seconds -= 60
            minutes += 1
        if minutes >= 60:
            minutes -= 60
            degrees += 1
            
        return f"{degrees}°{minutes:02}'{seconds:02}\" {direction}"
    
    return f"{dms(lat, 'N', 'S')}, {dms(lon, 'E', 'W')}"

def lon_to_sign(lon_deg):
    """
    Convert ecliptic longitude to zodiac sign with position.
    Now includes seconds in the output.
    """
    sign_idx = int(lon_deg // 30)
    degrees_in_sign = lon_deg % 30
    degrees = int(degrees_in_sign)
    minutes = int((degrees_in_sign - degrees) * 60)
    seconds = int(round(((degrees_in_sign - degrees) * 60 - minutes) * 60))
    
    # Handle rounding overflow
    if seconds >= 60:
        seconds -= 60
        minutes += 1
    if minutes >= 60:
        minutes -= 60
        degrees += 1
    
    return f"{ZODIAC_SIGNS[sign_idx]} {degrees}°{minutes:02}'{seconds:02}\""

def PLadder_ZSizes(utc_dt, lat, lon):
    """Calculate Planetary Ladder and Zone Sizes using Swiss Ephemeris."""
    if not -13000 <= utc_dt.year <= 17000:
        return {"error": "Date out of supported range (-13,000–17,000 CE)"}
    
    # Planet mapping with Swiss Ephemeris constants
    PLANET_OBJECTS = {
        'Sun': swe.SUN, 'Moon': swe.MOON, 'Mercury': swe.MERCURY,
        'Venus': swe
.VENUS, 'Mars': swe.MARS, 
        'Jupiter': swe.JUPITER, 'Saturn': swe.SATURN
    }
    
    # Calculate Julian Day
    jd_utc = swe.julday(
        utc_dt.year, utc_dt.month, utc_dt.day,
        utc_dt.hour + utc_dt.minute/60 + utc_dt.second/3600
    )
    
    # Calculate planetary positions
    longitudes = {}
    for planet, planet_id in PLANET_OBJECTS.items():
        flags = swe.FLG_SWIEPH | swe.FLG_SPEED
        xx, _ = swe.calc_ut(jd_utc, planet_id, flags)
        lon = xx[0] % 360  # Normalize to 0-360°
        longitudes[planet] = lon
    
    # Sort planets by longitude
    sorted_planets = sorted(longitudes.items(), key=lambda x: x[1])
    PLadder = [p for p, _ in sorted_planets]
    sorted_lons = [lon for _, lon in sorted_planets]
    
    # Calculate zone sizes
    zone_sizes = [sorted_lons[0]]  # First zone
    zone_sizes.extend(sorted_lons[i+1] - sorted_lons[i] for i in range(6))  # Middle zones
    zone_sizes.append(360 - sorted_lons[-1])  # Last zone
    
    # Classify zone sizes
    ZSizes = []
    for i, size in enumerate(zone_sizes):
        # Get bordering planets
        if i == 0:
            bord = [PLadder[0]]
        elif i == len(zone_sizes)-1:
            bord = [PLadder[-1]]
        else:
            bord = [PLadder[i-1], PLadder[i]]
        
        # Determine zone classification
        if any(p in ['Sun', 'Moon'] for p in bord):
            X = 7
        elif any(p in ['Mercury', 'Venus', 'Mars'] for p in bord):
            X = 6
        else:
            X = 5
            
        # Format size with seconds
        d = int(size)
        m = int((size - d) * 60)
        s = int(round(((size - d) * 60 - m) * 60))
        
        # Handle rounding overflow
        if s >= 60:
            s -= 60
            m += 1
        if m >= 60:
            m -= 60
            d += 1
            
        classification = (
            'Swallowed' if size <= 1 else
            'Tiny' if size <= X else
            'Small' if size <= 40 else
            'Ideal' if 50 <= size <= 52 else
            'Normal' if size < 60 else
            'Big'
        )
        
        ZSizes.append((f"{d}°{m:02}'{s:02}\"", classification))
    
    return {
        'PLadder': PLadder,
        'ZSizes': ZSizes,
        'longitudes': longitudes  # Raw degrees for calculations
    }

def plot_pladder(PLadder):
    """Generate the original version of the planetary ladder visualization."""
    fig, ax = plt.subplots(figsize=(6, 6))
    
    # Draw the main triangle
    ax.plot([0, 1.5, 3, 0], [0, 3, 0, 0], 'k-', linewidth=2)
    
    # Draw horizontal divisions (original style)
    ax.plot([0.5, 2.5], [1, 1], 'k--')
    ax.plot([1, 2], [2, 2], 'k--')
    
    # Original planet symbol positions
    symbol_positions = [
        (-0.2, 0.2), (0.3, 1.2), (0.8, 2.2),
        (1.5, 3.2), (2.2, 2.2), (2.7, 1.2), (3.2, 0.2)
    ]
    
    # Add planet symbols
    for (x, y), planet in zip(symbol_positions, PLadder):
        ax.text(x, y, PLANET_SYMBOLS[planet], 
               ha='center', va='center', 
               fontsize=24)
    
    # Configure plot appearance (original style)
    ax.set_xlim(-0.5, 3.5)
    ax.set_ylim(-0.5, 3.5)
    ax.set_aspect('equal')
    ax.axis('off')
    
    return fig

def chat_interface(query):
    """Process the user query and return text and plot."""
    # Parse input
    dt, location, error = parse_query(query)
    if error:
        return error, None
    
    # Get UTC time and coordinates
    utc_dt, lat, lon, error = get_utc_time(dt, location)
    if error:
        return error, None
    
    # Calculate planetary positions
    result = PLadder_ZSizes(utc_dt, lat, lon)
    if "error" in result:
        return result["error"], None
    
    # Format output
    PLadder = result["PLadder"]
    ZSizes = result["ZSizes"]
    longitudes = result["longitudes"]
    
    # Generate planet list text with full DMS
    planet_list = "\n".join(
        f"{PLANET_RU[p]}: {lon_to_sign(longitudes[p])}"
        for p in PLadder
    )
    
    # Generate zone sizes text
    zones_text = "\n".join(
        f"Zone {i+1}: {size} ({cls})"
        for i, (size, cls) in enumerate(ZSizes)
    )
    
    # Generate coordinates text
    coords_text = format_coords(lat, lon)
    
    # Create visualization (original style)
    fig = plot_pladder(PLadder)
    buf = BytesIO()
    fig.savefig(buf, format='png', dpi=120, bbox_inches='tight')
    buf.seek(0)
    img = Image.open(buf)
    plt.close(fig)
    
    # Compose final output
    output_text = (
        f"Planetary Ladder:\n{planet_list}\n\n"
        f"Zone Sizes:\n{zones_text}\n\n"
        f"Coordinates: {coords_text}\n"
        f"Calculation Time: {utc_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}"
    )
    
    return output_text, img

# Gradio Interface
with gr.Blocks(title="Planetary Ladder Calculator") as interface:
    gr.Markdown("## Planetary Ladder Calculator")
    with gr.Row():
        with gr.Column(scale=2):
            output_text = gr.Textbox(label="Astrological Data", lines=12)
        with gr.Column(scale=1):
            output_image = gr.Image(label="Visualization")
    with gr.Row():
        query_text = gr.Textbox(
            label="Input Query",
            placeholder="Example for Elon Mask: PLadder 28-06-1971 12:03:00 Pretoria",
            max_lines=1
        )
    with gr.Row():
        submit_button = gr.Button("Calculate", variant="primary")
    
    submit_button.click(
        fn=chat_interface,
        inputs=query_text,
        outputs=[output_text, output_image]
    )

if __name__ == "__main__":
    interface.launch()