UNPKG

astrology-insights

Version:

Comprehensive Vedic astrology engine for Node.js — Panchang, birth charts (Kundli), Vimshottari Dasha, divisional charts, dosha analysis, and planetary remedies. Swiss Ephemeris precision, validated against Drik Panchang.

739 lines (628 loc) 31.7 kB
import * as swisseph from 'swisseph'; import { Position, Location } from '../types/panchang'; import { normalizeAngle } from '../utils/index'; import { PlanetaryPosition, Planetary } from './planetary'; import * as path from 'path'; export interface AyanamsaInfo { name: string; id: number; degree: number; description: string; } export interface CelestialPosition extends Position { distance: number; longitudeSpeed: number; latitudeSpeed: number; distanceSpeed: number; } export class Ephemeris { private readonly planet_map: { [key: string]: number } = { 'Sun': 0, // SE_SUN 'Moon': 1, // SE_MOON 'Mercury': 2, // SE_MERCURY 'Venus': 3, // SE_VENUS 'Mars': 4, // SE_MARS 'Jupiter': 5, // SE_JUPITER 'Saturn': 6, // SE_SATURN 'Uranus': 7, // SE_URANUS 'Neptune': 8, // SE_NEPTUNE 'Pluto': 9, // SE_PLUTO 'Rahu': 11, // SE_MEAN_NODE (North Node) 'Ketu': -1 // Special handling for South Node (180° from Rahu) }; private ephemeris_path: string = ''; constructor(ephemeris_path?: string) { // Use the local ephe directory that contains Swiss Ephemeris files if (!ephemeris_path) { // Try multiple possible paths for the ephemeris files const possible_paths = [ path.join(__dirname, '../../ephe'), // Local ephe directory path.join(__dirname, '../node_modules/swisseph/ephe'), // node_modules path.join(process.cwd(), 'ephe'), // Current working directory path.join(process.cwd(), 'node_modules/swisseph/ephe') // CWD node_modules ]; // Use the first existing path this.ephemeris_path = possible_paths.find(p => { try { const fs = require('fs'); return fs.existsSync(p); } catch { return false; } }) || possible_paths[0]; } else { this.ephemeris_path = ephemeris_path; } this.initialize_swiss_eph(); } private initialize_swiss_eph(): void { try { swisseph.swe_set_ephe_path(this.ephemeris_path); } catch (error) { console.warn('Could not set ephemeris path, using default built-in panchang'); } } calculatePosition(date: Date, body: string): Position { const jd = this.date_to_julian(date); const planet_id = this.get_planet_id(body); try { let result: any; if (body === 'Ketu') { // Ketu is 180° opposite to Rahu result = swisseph.swe_calc_ut(jd, swisseph.SE_MEAN_NODE, swisseph.SEFLG_SWIEPH); if (result && 'longitude' in result) { const ketuLongitude = normalizeAngle(result.longitude + 180); return { longitude: ketuLongitude, latitude: -result.latitude // Opposite latitude }; } } else { result = swisseph.swe_calc_ut(jd, planet_id, swisseph.SEFLG_SWIEPH); } if (result && 'longitude' in result) { return { longitude: normalizeAngle(result.longitude), latitude: result.latitude }; } // Fallback if Swiss Ephemeris fails return this.get_fallback_position(body, date); } catch (error) { console.warn(`Swiss Ephemeris calculation failed for ${body}, using fallback`); return this.get_fallback_position(body, date); } } calculateSiderealPosition(date: Date, body: string, ayanamsa?: number): Position { const current_ayanamsa = ayanamsa || this.calculate_lahiri_ayanamsa(date); const tropicalPosition = this.calculatePosition(date, body); return { longitude: normalizeAngle(tropicalPosition.longitude - current_ayanamsa), latitude: tropicalPosition.latitude }; } calculate_lahiri_ayanamsa(date: Date): number { try { const jd = this.date_to_julian(date); // Set Lahiri ayanamsa (SE_SIDM_LAHIRI = 1) swisseph.swe_set_sid_mode(swisseph.SE_SIDM_LAHIRI, jd, 0); const ayanamsa = swisseph.swe_get_ayanamsa_ut(jd); return ayanamsa || this.get_fallback_lahiri_ayanamsa(date); } catch (error) { return this.get_fallback_lahiri_ayanamsa(date); } } private get_fallback_lahiri_ayanamsa(date: Date): number { // Simple Lahiri ayanamsa fallback const year = date.getFullYear() + (date.getMonth() + 1) / 12 + date.getDate() / 365.25; const t = (year - 1900.0) / 100.0; return 22.46000 + 1.3915817 * t - 0.0130125 * t * t; } /** * Get all available ayanamsa systems with their degrees for a given date * @param date Date for ayanamsa calculation * @returns Array of ayanamsa information including name, ID, degree, and description */ getAyanamsa(date: Date): AyanamsaInfo[] { const jd = this.date_to_julian(date); // Swiss Ephemeris Ayanamsa Systems (SE_SIDM constants) const ayanamsa_systems = [ { id: 0, name: 'Fagan/Bradley', description: 'Fagan/Bradley (Western Sidereal)' }, { id: 1, name: 'Lahiri', description: 'Lahiri (Chitrapaksha) - Official Indian Government' }, { id: 2, name: 'De Luce', description: 'De Luce ayanamsa' }, { id: 3, name: 'Raman', description: 'B.V. Raman ayanamsa' }, { id: 4, name: 'Ushashashi', description: 'Ushashashi ayanamsa' }, { id: 5, name: 'Krishnamurti', description: 'Krishnamurti ayanamsa (KP System)' }, { id: 6, name: 'Djwhal Khul', description: 'Djwhal Khul ayanamsa' }, { id: 7, name: 'Yukteshwar', description: 'Sri Yukteshwar ayanamsa' }, { id: 8, name: 'J.N. Bhasin', description: 'J.N. Bhasin ayanamsa' }, { id: 9, name: 'Babylonian (Kugler 1)', description: 'Babylonian ayanamsa (Kugler 1)' }, { id: 10, name: 'Babylonian (Kugler 2)', description: 'Babylonian ayanamsa (Kugler 2)' }, { id: 11, name: 'Babylonian (Kugler 3)', description: 'Babylonian ayanamsa (Kugler 3)' }, { id: 12, name: 'Babylonian (Huber)', description: 'Babylonian ayanamsa (Huber)' }, { id: 13, name: 'Eta Piscium', description: 'Eta Piscium ayanamsa' }, { id: 14, name: 'Aldebaran 15 Tau', description: 'Aldebaran at 15° Taurus' }, { id: 15, name: 'Hipparchos', description: 'Hipparchos ayanamsa' }, { id: 16, name: 'Sassanian', description: 'Sassanian ayanamsa' }, { id: 17, name: 'Galact. Center (Brand)', description: 'Galactic Center ayanamsa (Brand)' }, { id: 18, name: 'J2000', description: 'J2000.0 reference frame' }, { id: 19, name: 'J1900', description: 'J1900.0 reference frame' }, { id: 20, name: 'B1950', description: 'B1950.0 reference frame' }, { id: 21, name: 'Suryasiddhanta', description: 'Suryasiddhanta ayanamsa' }, { id: 22, name: 'Suryasiddhanta (mean Sun)', description: 'Suryasiddhanta (mean Sun)' }, { id: 23, name: 'Aryabhata', description: 'Aryabhata ayanamsa' }, { id: 24, name: 'Aryabhata 522', description: 'Aryabhata 522 CE ayanamsa' }, { id: 25, name: 'Babylonian (Britton)', description: 'Babylonian ayanamsa (Britton)' }, { id: 26, name: 'True Chitra', description: 'True Chitra ayanamsa' }, { id: 27, name: 'True Revati', description: 'True Revati ayanamsa' }, { id: 28, name: 'True Pushya', description: 'True Pushya ayanamsa' }, { id: 29, name: 'Galactic (Gil Brand)', description: 'Galactic Center (Gil Brand)' }, { id: 30, name: 'Galactic Equator (IAU1958)', description: 'Galactic Equator (IAU1958)' }, { id: 31, name: 'Galactic Equator', description: 'Galactic Equator' }, { id: 32, name: 'Galactic Equator (mid-Mula)', description: 'Galactic Equator at mid-Mula' }, { id: 33, name: 'Skydram (Mardyks)', description: 'Skydram ayanamsa (Mardyks)' }, { id: 34, name: 'True Mula', description: 'True Mula ayanamsa' }, { id: 35, name: 'Dhruva Galactic Center', description: 'Dhruva Galactic Center ayanamsa' }, { id: 36, name: 'Aryabhata Mean Sun', description: 'Aryabhata Mean Sun ayanamsa' }, { id: 37, name: 'Lahiri VP285', description: 'Lahiri VP285 ayanamsa' }, { id: 38, name: 'Krishnamurti VP291', description: 'Krishnamurti VP291 ayanamsa' }, { id: 39, name: 'Lahiri ICRC', description: 'Lahiri ICRC ayanamsa' } ]; const results: AyanamsaInfo[] = []; ayanamsa_systems.forEach(system => { try { // Set the ayanamsa mode swisseph.swe_set_sid_mode(system.id, jd, 0); // Get ayanamsa value for the given date const ayanamsaValue = swisseph.swe_get_ayanamsa_ut(jd); results.push({ name: system.name, id: system.id, degree: ayanamsaValue || this.get_fallback_ayanamsa(system.id, date), description: system.description }); } catch (error) { results.push({ name: system.name, id: system.id, degree: this.get_fallback_ayanamsa(system.id, date), description: system.description }); } }); // Sort by degree value for easier comparison results.sort((a, b) => a.degree - b.degree); return results; } /** * Get a specific ayanamsa value by name or ID * @param date Date for calculation * @param ayanamsaId Ayanamsa ID or name * @returns Ayanamsa information */ getSpecificAyanamsa(date: Date, ayanamsa_id: number | string): AyanamsaInfo | null { const allAyanamsas = this.getAyanamsa(date); if (typeof ayanamsa_id === 'number') { return allAyanamsas.find(a => a.id === ayanamsa_id) || null; } else { const exact_match = allAyanamsas.find(a => a.name.toLowerCase() === (ayanamsa_id as string).toLowerCase() ); if (exact_match) { return exact_match; } return allAyanamsas.find(a => a.name.toLowerCase().includes((ayanamsa_id as string).toLowerCase()) ) || null; } } private get_fallback_ayanamsa(systemId: number, date: Date): number { const year = date.getFullYear(); const t = (year - 1900) / 100; // Approximate calculations for different ayanamsa systems switch (systemId) { case 0: // Fagan/Bradley return 24.740 + 1.39 * t - 0.01 * t * t; case 1: // Lahiri return 22.460 + 1.39 * t - 0.01 * t * t; case 3: // Raman return 21.580 + 1.39 * t - 0.01 * t * t; case 5: // Krishnamurti return 23.230 + 1.39 * t - 0.01 * t * t; case 7: // Yukteshwar return 22.460 + 1.39 * t - 0.01 * t * t; default: // Default to Lahiri approximation return 22.460 + 1.39 * t - 0.01 * t * t; } } calculateSunrise(date: Date, location: Location): Date | null { try { // Improved sunrise calculation using NOAA Solar Calculator algorithm const year = date.getUTCFullYear(); const month = date.getUTCMonth() + 1; const day = date.getUTCDate(); // Calculate Julian day number const a = Math.floor((14 - month) / 12); const y = year - a; const m = month + 12 * a - 3; const jd = day + Math.floor((153 * m + 2) / 5) + 365 * y + Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400) - 32045; // Calculate day of year const dayOfYear = jd - Math.floor((14 - 1) / 12) * 365 - Math.floor(y / 4) + Math.floor(y / 100) - Math.floor(y / 400) + Math.floor((153 * (1 + 12 * Math.floor((14 - 1) / 12) - 3) + 2) / 5) + 1 - 32045; // More accurate solar calculations const P = Math.asin(0.39795 * Math.cos(0.98563 * (dayOfYear - 173) * Math.PI / 180)); const argumentum = Math.tan(location.latitude * Math.PI / 180) * Math.tan(P); if (Math.abs(argumentum) > 1) { return null; // Polar day or night } const hourAngle = Math.acos(-argumentum) * 180 / Math.PI; const sunrise = 12 - hourAngle / 15 - location.longitude / 15; // Adjust for UTC let sunriseUTC = sunrise; if (sunriseUTC < 0) sunriseUTC += 24; if (sunriseUTC >= 24) sunriseUTC -= 24; const sunriseHours = Math.floor(sunriseUTC); const sunriseMinutes = Math.floor((sunriseUTC - sunriseHours) * 60); const sunriseSeconds = Math.floor(((sunriseUTC - sunriseHours) * 60 - sunriseMinutes) * 60); return new Date(Date.UTC(year, month - 1, day, sunriseHours, sunriseMinutes, sunriseSeconds)); } catch (error) { console.warn('Sunrise calculation failed:', error); // Fallback calculation return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 6, 0, 0, 0); } } private calculate_sun_altitude(sunLon: number, sunLat: number, location: Location, jd: number): number { // Convert ecliptic coordinates to equatorial const obliquity = 23.43929111; // Mean obliquity of ecliptic for J2000 const sunLonRad = sunLon * Math.PI / 180; const sunLatRad = sunLat * Math.PI / 180; const oblRad = obliquity * Math.PI / 180; // Calculate right ascension and declination const ra = Math.atan2( Math.sin(sunLonRad) * Math.cos(oblRad) - Math.tan(sunLatRad) * Math.sin(oblRad), Math.cos(sunLonRad) ); const dec = Math.asin( Math.sin(sunLatRad) * Math.cos(oblRad) + Math.cos(sunLatRad) * Math.sin(oblRad) * Math.sin(sunLonRad) ); // Calculate Greenwich Mean Sidereal Time const t = (jd - 2451545.0) / 36525; const gmst0 = 100.46061837 + 36000.770053608 * t + 0.000387933 * t * t - t * t * t / 38710000; const gmst = gmst0 + 15.04106864 * ((jd - Math.floor(jd)) * 24); const lst = (gmst + location.longitude + 360) % 360; // Calculate hour angle const ha = (lst - ra * 180 / Math.PI) * Math.PI / 180; // Calculate altitude const latRad = location.latitude * Math.PI / 180; const altitude = Math.asin( Math.sin(latRad) * Math.sin(dec) + Math.cos(latRad) * Math.cos(dec) * Math.cos(ha) ); return altitude * 180 / Math.PI; } calculateMoonrise(date: Date, location: Location): Date | null { try { const jd = this.date_to_julian(date); for (let hour = 0; hour < 48; hour += 0.1) { // Check 48 hours for moonrise const testJd = jd - 0.5 + hour / 24; const moonPos = swisseph.swe_calc_ut(testJd, swisseph.SE_MOON, swisseph.SEFLG_SWIEPH); if (moonPos && 'longitude' in moonPos && moonPos.longitude !== undefined) { const altitude = this.calculate_moon_altitude(moonPos.longitude, moonPos.latitude, location, testJd); if (altitude > -0.8333 && hour > 3) { return this.julian_to_date(testJd); } } } return null; } catch (error) { console.warn('Swiss Ephemeris moonrise calculation failed:', error); return null; } } calculateMoonset(date: Date, location: Location): Date | null { try { const jd = this.date_to_julian(date); for (let hour = 0; hour < 48; hour += 0.1) { // Check 48 hours for moonset const testJd = jd - 0.5 + hour / 24; const moonPos = swisseph.swe_calc_ut(testJd, swisseph.SE_MOON, swisseph.SEFLG_SWIEPH); if (moonPos && 'longitude' in moonPos && moonPos.longitude !== undefined) { const altitude = this.calculate_moon_altitude(moonPos.longitude, moonPos.latitude, location, testJd); if (altitude < -0.8333 && hour > 3) { return this.julian_to_date(testJd); } } } return null; } catch (error) { console.warn('Swiss Ephemeris moonset calculation failed:', error); return null; } } private calculate_moon_altitude(moonLon: number, moonLat: number, location: Location, jd: number): number { // This is a simplified calculation and can be improved with more precise models const obliquity = 23.43929111; const moonLonRad = moonLon * Math.PI / 180; const moonLatRad = moonLat * Math.PI / 180; const oblRad = obliquity * Math.PI / 180; const ra = Math.atan2( Math.sin(moonLonRad) * Math.cos(oblRad) - Math.tan(moonLatRad) * Math.sin(oblRad), Math.cos(moonLonRad) ); const dec = Math.asin( Math.sin(moonLatRad) * Math.cos(oblRad) + Math.cos(moonLatRad) * Math.sin(oblRad) * Math.sin(moonLonRad) ); const t = (jd - 2451545.0) / 36525; const gmst0 = 100.46061837 + 36000.770053608 * t + 0.000387933 * t * t - t * t * t / 38710000; const gmst = gmst0 + 15.04106864 * ((jd - Math.floor(jd)) * 24); const lst = (gmst + location.longitude + 360) % 360; const ha = (lst - ra * 180 / Math.PI) * Math.PI / 180; const latRad = location.latitude * Math.PI / 180; const altitude = Math.asin( Math.sin(latRad) * Math.sin(dec) + Math.cos(latRad) * Math.cos(dec) * Math.cos(ha) ); return altitude * 180 / Math.PI; } calculateSunset(date: Date, location: Location): Date | null { try { // Improved sunset calculation using NOAA Solar Calculator algorithm const year = date.getUTCFullYear(); const month = date.getUTCMonth() + 1; const day = date.getUTCDate(); // Calculate Julian day number const a = Math.floor((14 - month) / 12); const y = year - a; const m = month + 12 * a - 3; const jd = day + Math.floor((153 * m + 2) / 5) + 365 * y + Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400) - 32045; // Calculate day of year const dayOfYear = jd - Math.floor((14 - 1) / 12) * 365 - Math.floor(y / 4) + Math.floor(y / 100) - Math.floor(y / 400) + Math.floor((153 * (1 + 12 * Math.floor((14 - 1) / 12) - 3) + 2) / 5) + 1 - 32045; // More accurate solar calculations const P = Math.asin(0.39795 * Math.cos(0.98563 * (dayOfYear - 173) * Math.PI / 180)); const argumentum = Math.tan(location.latitude * Math.PI / 180) * Math.tan(P); if (Math.abs(argumentum) > 1) { return null; // Polar day or night } const hourAngle = Math.acos(-argumentum) * 180 / Math.PI; const sunset = 12 + hourAngle / 15 - location.longitude / 15; // Adjust for UTC let sunsetUTC = sunset; if (sunsetUTC < 0) sunsetUTC += 24; if (sunsetUTC >= 24) sunsetUTC -= 24; const sunsetHours = Math.floor(sunsetUTC); const sunsetMinutes = Math.floor((sunsetUTC - sunsetHours) * 60); const sunsetSeconds = Math.floor(((sunsetUTC - sunsetHours) * 60 - sunsetMinutes) * 60); return new Date(Date.UTC(year, month - 1, day, sunsetHours, sunsetMinutes, sunsetSeconds)); } catch (error) { console.warn('Sunset calculation failed:', error); // Fallback calculation return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 18, 0, 0, 0); } } calculateNakshatra(longitude: number): { nakshatra: number; pada: number; name: string } { const nakshatraNames = [ 'Ashwini', 'Bharani', 'Krittika', 'Rohini', 'Mrigashira', 'Ardra', 'Punarvasu', 'Pushya', 'Ashlesha', 'Magha', 'Purva Phalguni', 'Uttara Phalguni', 'Hasta', 'Chitra', 'Swati', 'Vishakha', 'Anuradha', 'Jyeshtha', 'Mula', 'Purva Ashadha', 'Uttara Ashadha', 'Shravana', 'Dhanishta', 'Shatabhisha', 'Purva Bhadrapada', 'Uttara Bhadrapada', 'Revati' ]; const oneNakshatra = 360 / 27; // 13°20' const onePada = oneNakshatra / 4; // 3°20' const normalizedLon = normalizeAngle(longitude); const nakshatraNum = Math.floor(normalizedLon / oneNakshatra) + 1; const remainder = normalizedLon % oneNakshatra; const padaNum = Math.floor(remainder / onePada) + 1; return { nakshatra: nakshatraNum, pada: padaNum, name: nakshatraNames[nakshatraNum - 1] || 'Unknown' }; } private date_to_julian(date: Date): number { // CRITICAL: Use UTC components to ensure consistent Julian Day calculation // This preserves the exact moment represented by the Date object let year = date.getUTCFullYear(); let month = date.getUTCMonth() + 1; const day = date.getUTCDate(); const hour = date.getUTCHours() + date.getUTCMinutes() / 60 + date.getUTCSeconds() / 3600 + date.getUTCMilliseconds() / 3600000; try { // Use Swiss Ephemeris for accurate Julian Day calculation return swisseph.swe_julday(year, month, day, hour, swisseph.SE_GREG_CAL); } catch (error) { // High-precision fallback Julian Day calculation // Algorithm from Meeus "Astronomical Algorithms" let a: number, b: number; if (month <= 2) { year = year - 1; month = month + 12; } a = Math.floor(year / 100); b = 2 - a + Math.floor(a / 4); const jd = Math.floor(365.25 * (year + 4716)) + Math.floor(30.6001 * (month + 1)) + day + hour/24 + b - 1524.5; return jd; } } private julian_to_date(jd: number): Date { try { const result = swisseph.swe_revjul(jd, swisseph.SE_GREG_CAL); return new Date(result.year, result.month - 1, result.day, Math.floor(result.hour), Math.floor((result.hour % 1) * 60)); } catch (error) { // Fallback conversion return new Date((jd - 2440587.5) * 86400000); } } private get_planet_id(body: string): number { return this.planet_map[body] !== undefined ? this.planet_map[body] : 0; } private get_fallback_position(body: string, date: Date): Position { // Simple fallback using basic orbital elements // Use proper UTC epoch calculation const epoch = new Date(Date.UTC(2000, 0, 1, 12, 0, 0, 0)); // J2000.0 epoch const daysSinceEpoch = (date.getTime() - epoch.getTime()) / 86400000; const positions: { [key: string]: { lon: number; motion: number } } = { 'Sun': { lon: 280.460, motion: 0.985647 }, 'Moon': { lon: 218.316, motion: 13.176396 }, 'Mercury': { lon: 252.251, motion: 4.092317 }, 'Venus': { lon: 181.980, motion: 1.602136 }, 'Mars': { lon: 355.433, motion: 0.524071 }, 'Jupiter': { lon: 34.351, motion: 0.083056 }, 'Saturn': { lon: 50.078, motion: 0.033371 } }; const body_data = positions[body] || positions['Sun']; const longitude = normalizeAngle(body_data.lon + body_data.motion * daysSinceEpoch); return { longitude, latitude: 0 }; } getCurrentPlanets(date: Date = new Date(), ayanamsaId: number = 1): PlanetaryPosition[] { const planetary = new Planetary(); const planets = ['Sun', 'Moon', 'Mercury', 'Venus', 'Mars', 'Jupiter', 'Saturn']; const positions: PlanetaryPosition[] = []; // Get ayanamsa value for the date const ayanamsaInfo = this.getSpecificAyanamsa(date, ayanamsaId); const ayanamsa = ayanamsaInfo ? ayanamsaInfo.degree : 24.0; // Default to approximate Lahiri for (const planet of planets) { try { const position = this.calculatePosition(date, planet); // Convert to sidereal longitude by subtracting ayanamsa const siderealLongitude = normalizeAngle(position.longitude - ayanamsa); // Calculate Rashi and Nakshatra const rashi = planetary.calculateRashi(siderealLongitude); const nakshatra = planetary.calculateNakshatra(siderealLongitude); positions.push({ planet: planet, longitude: siderealLongitude, latitude: position.latitude, rashi: rashi, nakshatra: nakshatra }); } catch (error) { console.warn(`Could not calculate position for ${planet}:`, error); // Add with fallback panchang const fallbackPos = this.get_fallback_position(planet, date); const siderealLongitude = normalizeAngle(fallbackPos.longitude - ayanamsa); positions.push({ planet: planet, longitude: siderealLongitude, latitude: fallbackPos.latitude, rashi: planetary.calculateRashi(siderealLongitude), nakshatra: planetary.calculateNakshatra(siderealLongitude) }); } } return positions; } /** * Calculate house cusps and ascendant using Swiss Ephemeris swe_houses. * @param date UTC Date for calculation * @param latitude Geographic latitude * @param longitude Geographic longitude * @param houseSystem House system type ('whole_sign' | 'equal' | 'placidus') * @returns Object with ascendant, mc, and cusps[1..12] */ calculateHouseCusps( date: Date, latitude: number, longitude: number, houseSystem: string, ): { ascendant: number; mc: number; cusps: number[] } { const jd = this.date_to_julian(date); // Map house system to Swiss Ephemeris code const systemCodes: Record<string, string> = { whole_sign: 'W', equal: 'E', placidus: 'P', }; const hsys = systemCodes[houseSystem] || 'W'; try { const result = (swisseph as any).swe_houses(jd, latitude, longitude, hsys); if (result && result.house) { // result.house is array of 12 cusp longitudes (0-indexed) // ascendant and mc may be in ascmc array or as direct properties const cusps: number[] = [0]; // index 0 is unused for (let i = 0; i < 12; i++) { cusps.push(result.house[i]); } let ascendant: number; let mc: number; if (result.ascmc) { ascendant = result.ascmc[0]; mc = result.ascmc[1]; } else { ascendant = result.ascendant || cusps[1]; mc = result.mc || cusps[10] || 0; } return { ascendant, mc, cusps }; } throw new Error('swe_houses returned invalid result'); } catch (error) { // Fallback: use Sun position as rough ascendant proxy console.warn('swe_houses failed, using fallback:', error); const sunPos = this.calculatePosition(date, 'Sun'); const asc = normalizeAngle(sunPos.longitude); const cusps: number[] = [0]; for (let i = 0; i < 12; i++) { cusps.push(normalizeAngle(asc + i * 30)); } return { ascendant: asc, mc: normalizeAngle(asc + 270), cusps }; } } /** * Calculate position WITH speed (degrees/day) for a celestial body. * Uses SEFLG_SPEED flag. * @param date Date for calculation * @param body Celestial body name * @returns Object with longitude, latitude, and speed */ calculatePositionWithSpeed(date: Date, body: string): { longitude: number; latitude: number; speed: number; } { const jd = this.date_to_julian(date); const planet_id = this.get_planet_id(body); try { let result: any; if (body === 'Ketu') { result = swisseph.swe_calc_ut( jd, swisseph.SE_MEAN_NODE, swisseph.SEFLG_SWIEPH | swisseph.SEFLG_SPEED, ); if (result && 'longitude' in result) { return { longitude: normalizeAngle(result.longitude + 180), latitude: -result.latitude, speed: -(result.longitudeSpeed || 0), }; } } else { result = swisseph.swe_calc_ut( jd, planet_id, swisseph.SEFLG_SWIEPH | swisseph.SEFLG_SPEED, ); } if (result && 'longitude' in result) { return { longitude: normalizeAngle(result.longitude), latitude: result.latitude, speed: result.longitudeSpeed || 0, }; } throw new Error('swe_calc_ut returned invalid result'); } catch (error) { // Return fallback with zero speed const pos = this.get_fallback_position(body, date); return { longitude: pos.longitude, latitude: pos.latitude, speed: 0 }; } } cleanup(): void { try { swisseph.swe_close(); } catch (error) { console.warn('Error closing Swiss Ephemeris:', error); } } }