Solar Calculator
```
"""
Sunrise and Sunset Calculator using NAOO

This script calculates the sunrise and sunset times for a given location (latitude and longitude) and date.
The calculation accounts for the date's day of the year and timezone adjustments.
It uses astronomical calculations derived from the NOAA solar calculator and,
relies on custom implementations of trigonometric functions (sin, cos, tan, asin, acos, atan) based on Taylor series expansions.
Most importantly, it uses no external libraries, making it a self-contained solution.

Resources:
- Trigonometric function implementations based on Taylor series:
  - https://en.wikipedia.org/wiki/Taylor_series#Trigonometric_functions
- Sunrise and sunset calculation algorithm:
  - https://en.wikipedia.org/wiki/Sunrise_equation
  - https://www.aa.quae.nl/en/reken/zonpositie.html
  - https://gml.noaa.gov/grad/solcalc/solareqns.PDF
"""

# Define constants
PI = 3.14159265358979323846264338327950288419716939937510  # Pi to high precision
EPSILON = 1e-15  # Threshold for term convergence in series expansions
MAX_TERMS = 10

def radians(degrees):
    """
    Convert degrees to radians.

    Parameters:
        degrees (float): Angle in degrees.

    Returns:
        float: Angle in radians.
    """
    return degrees * PI / 180

def degrees(radians):
    """
    Convert radians to degrees.

    Parameters:
        radians (float): Angle in radians.

    Returns:
        float: Angle in degrees.
    """
    return radians * 180 / PI

def sin(x):
    """
    Calculate the sine of x (in degrees) using Taylor series expansion.

    Parameters:
        x (float): The angle in degrees.

    Returns:
        float: The sine of the angle.
    """
    # Convert degrees to radians
    x = radians(x)
    
    # Range reduction to improve convergence: bring x within (-π, π]
    x = x % (2 * PI)
    if x > PI:
        x -= (2 * PI)
    
    # Initialize variables for Taylor series
    term = x  # First term of the series (x^1 / 1!)
    sin_x = term
    n = 1  # Term counter
    
    while n < MAX_TERMS:
        # Compute the next term using the recurrence relation
        term *= -x * x / ((2 * n) * (2 * n + 1))
        sin_x += term
        
        # Check if the current term is small enough to stop
        if abs(term) < EPSILON:
            break
        n += 1
    
    return sin_x

def cos(x):
    """
    Calculate the cosine of x (in degrees) using Taylor series expansion.

    Parameters:
        x (float): The angle in degrees.

    Returns:
        float: The cosine of the angle.
    """
    # Convert degrees to radians
    x = radians(x)
    
    # Range reduction to improve convergence: bring x within (-π, π]
    x = x % (2 * PI)
    if x > PI:
        x -= (2 * PI)
    
    # Initialize variables for Taylor series
    term = 1.0  # First term of the series (x^0 / 0!)
    cos_x = term
    n = 1  # Term counter
    
    while n < MAX_TERMS:
        # Compute the next term using the recurrence relation
        term *= -x * x / ((2 * n - 1) * (2 * n))
        cos_x += term
        
        # Check if the current term is small enough to stop
        if abs(term) < EPSILON:
            break
        n += 1
    
    return cos_x

def tan(x):
    """
    Calculate the tangent of x (in degrees) using sine and cosine functions.

    Parameters:
        x (float): The angle in degrees.

    Returns:
        float: The tangent of the angle.

    Raises:
        ValueError: If cosine of x is zero (tan is undefined).
    """
    cosine = cos(x)
    if abs(cosine) < EPSILON:
        raise ValueError("Tangent undefined for angles where cosine is zero.")
    return sin(x) / cosine

def asin(x):
    """
    Calculate the arcsine of x using the standard Taylor series expansion.

    Parameters:
        x (float): The value for which to compute arcsin. Must be in [-1, 1].

    Returns:
        float: The arcsine of x in degrees.

    Raises:
        ValueError: If x is outside the domain [-1, 1].
    """
    if x < -1.0 or x > 1.0:
        raise ValueError("asin(x) is undefined for |x| > 1.")

    # Use the standard Taylor series expansion for asin(x)
    term = x
    result = term
    n = 1

    while n < MAX_TERMS:
        term *= x * x * (2 * n - 1) / (2 * n)
        current_term = term / (2 * n + 1)
        result += current_term

        if abs(current_term) < EPSILON:
            break
        n += 1

    return degrees(result)

def acos(x):
    """
    Calculate the arccosine of x using the identity acos(x) = π/2 - asin(x).

    Parameters:
        x (float): The value for which to compute arccos. Must be in [-1, 1].

    Returns:
        float: The arccosine of x in degrees.

    Raises:
        ValueError: If x is outside the domain [-1, 1].
    """
    return 90 - asin(x)

def atan(x):
    """
    Calculate the arctangent of x using Taylor series expansion.

    Parameters:
        x (float): The value for which to compute arctan.

    Returns:
        float: The arctangent of x in degrees.
    """
    # Handle special case where x is 0
    if x == 0.0:
        return 0.0

    # Determine the sign of x
    sign = 1
    if x < 0:
        sign = -1
        x = -x  # Work with positive x for simplicity

    if x <= 1.0:
        # Use Taylor series expansion for |x| <= 1
        term = x  # First term of the series
        result = term
        n = 1  # Term counter

        while n < MAX_TERMS:
            term *= -x * x
            current_term = term / (2 * n + 1)
            result += current_term

            # Check if the current term is small enough to stop
            if abs(current_term) < EPSILON:
                break
            n += 1
    else:
        # Use identity arctan(x) = π/2 - arctan(1/x) for x > 1
        inverse_x = 1.0 / x
        term = inverse_x  # First term of the series
        atan_inverse = term
        n = 1  # Term counter

        while n < MAX_TERMS:
            term *= -inverse_x * inverse_x
            current_term = term / (2 * n + 1)
            atan_inverse += current_term

            # Check if the current term is small enough to stop
            if abs(current_term) < EPSILON:
                break
            n += 1

        result = (PI / 2) - atan_inverse

    # Convert the result from radians to degrees and apply the original sign
    return degrees(sign * result)

def mod(a, b):
    """
    Compute the modulus of a and b.

    Parameters:
        a (float): Dividend.
        b (float): Divisor.

    Returns:
        float: The modulus of a by b.
    """
    return a % b

def is_leap_year(year):
    """
    Determine if a given year is a leap year.

    Parameters:
        year (int): The year to check.

    Returns:
        int: 1 if leap year, 0 otherwise.
    """
    return int(year % 4 == 0 and (year % 100 != 0 or year % 400 == 0))

def day_of_year(year, month, day):
    """
    Calculate the day of the year for a given date.

    Parameters:
        year (int): The year of the date.
        month (int): The month of the date.
        day (int): The day of the date.

    Returns:
        int: The day of the year (1-366).
    """
    # Days in each month, accounting for leap years in February
    month_days = [31, 28 + is_leap_year(year), 31, 30, 31, 30,
                  31, 31, 30, 31, 30, 31]
    
    if day > month_days[month - 1]:
        raise ValueError("Invalid day for the given month.")
    
    return sum(month_days[:month - 1]) + day

def format_time(time_in_hours, seconds=True):
    """
    Format time from decimal hours to HH:MM:SS AM/PM format.
    """
    hour = int(time_in_hours)
    remainder = time_in_hours - hour
    minute = int(remainder * 60)
    remainder = remainder * 60 - minute
    second = int(round(remainder * 60))

    if second >= 60:
        second -= 60
        minute += 1
    if minute >= 60:
        minute -= 60
        hour += 1
    hour %= 24

    # Determine AM or PM
    meridiem = "AM" if hour < 12 else "PM"
    # Convert to 12-hour format
    hour_display = hour % 12
    if hour_display == 0:
        hour_display = 12

    if seconds:
        return f"{hour_display}:{minute:02d}:{second:02d} {meridiem}"
    else:
        if second >= 30:
            minute += 1
            if minute >= 60:
                minute -= 60
                hour += 1
                hour %= 24
                meridiem = "AM" if hour < 12 else "PM"
                hour_display = hour % 12
                if hour_display == 0:
                    hour_display = 12
        return f"{hour_display}:{minute:02d} {meridiem}"

def calculate_sunrise_sunset(latitude, longitude, year, month, day, timezone_offset=0):
    """
    Calculate sunrise and sunset times using NOAA's method for a given location and date.

    Parameters:
        latitude (float): Latitude in decimal degrees (positive for north).
        longitude (float): Longitude in decimal degrees (positive for east).
        year (int): Year of the date.
        month (int): Month of the date.
        day (int): Day of the date.
        timezone_offset (float): Offset from UTC in hours.

    Returns:
        tuple: Sunrise and sunset times in decimal hours (0-24), adjusted for timezone.
               Returns (None, None) if the sun does not rise or set on the given date/location.
    """
    # Step 1: Calculate Julian Day
    if month <= 2:
        year -= 1
        month += 12
    A = int(year / 100)
    B = 2 - A + int(A / 4)
    JD = int(365.25 * (year + 4716)) + int(30.6001 * (month + 1)) + day + B - 1524.5

    # Step 2: Calculate Julian Century
    JC = (JD - 2451545.0) / 36525.0

    # Step 3: Calculate Geometric Mean Longitude of the Sun
    L0 = mod(280.46646 + JC * (36000.76983 + JC * 0.0003032), 360)

    # Step 4: Calculate Geometric Mean Anomaly of the Sun
    M = 357.52911 + JC * (35999.05029 - 0.0001537 * JC)

    # Step 5: Calculate Eccentricity of Earth's Orbit
    e = 0.016708634 - JC * (0.000042037 + 0.0000001267 * JC)

    # Step 6: Calculate Sun's Equation of the Center
    C = sin(M) * (1.914602 - JC * (0.004817 + 0.000014 * JC)) + sin(2 * M) * (0.019993 - 0.000101 * JC) + sin(3 * M) * 0.000289

    # Step 7: Calculate Sun's True Longitude
    true_long = L0 + C

    # Step 8: Calculate Sun's Apparent Longitude
    omega = 125.04 - 1934.136 * JC
    lambda_sun = true_long - 0.00569 - 0.00478 * sin(omega)

    # Step 9: Calculate Mean Obliquity of the Ecliptic
    U = JC / 100
    epsilon0 = 23 + (26 + ((21.448 - U * (46.815 + U * (0.00059 - U * 0.001813)))) / 60) / 60

    # Step 10: Calculate Corrected Obliquity
    epsilon = epsilon0 + 0.00256 * cos(omega)

    # Step 11: Calculate Sun's Declination
    sin_decl = sin(epsilon) * sin(lambda_sun)
    cos_decl = cos(asin(sin_decl))

    # Step 12: Calculate Equation of Time
    y = tan(epsilon / 2) ** 2
    EoT = 4 * degrees(y * sin(2 * L0) - 2 * e * sin(M) + 4 * e * y * sin(M) * cos(2 * L0) - 0.5 * y ** 2 * sin(4 * L0) - 1.25 * e ** 2 * sin(2 * M))

    # Step 13: Calculate Hour Angle
    cos_ha = (cos(90.8333) / (cos(latitude) * cos_decl)) - (tan(latitude) * tan(asin(sin_decl)))
    if abs(cos_ha) > 1:
        return None, None  # Sun never rises or sets
    ha = acos(cos_ha)

    # Step 14: Calculate Solar Noon
    solar_noon = (720 - 4 * longitude - EoT + timezone_offset * 60) / 1440

    # Step 15: Calculate Sunrise and Sunset Times
    sunrise_time = solar_noon - ha * 4 / 1440
    sunset_time = solar_noon + ha * 4 / 1440
    
    # Convert times to decimal hours
    sunrise = mod(sunrise_time * 24, 24)
    sunset = mod(sunset_time * 24, 24)
    
    return sunrise, sunset

if __name__ == "__main__":
    # Location coordinates
    latitude  = 9.9500167   # Latitude in degrees (positive for north)
    longitude = 76.283925  # Longitude in degrees (positive for east)
    
    # Date and timezone information
    day, month, year = 14, 10, 2024
    timezone_offset = 5.5  # UTC offset in hours (e.g., IST is UTC+5.5)
    
    # Calculate sunrise and sunset times
    sunrise, sunset = calculate_sunrise_sunset(latitude, longitude, year, month, day, timezone_offset)
    
    if sunrise is not None and sunset is not None:
        # Format and display the results
        print(f"Sunrise: {format_time(sunrise, seconds=True)}")
        print(f"Sunset: {format_time(sunset, seconds=True)}")
    else:
        # Handle cases where sunrise or sunset does not occur
        print("Sunrise and sunset times not available for this location on the given date.")
```

rip.py
```
import datetime
import pytz
from math import cos, radians, sin, degrees, acos, tan, asin, fabs, sqrt

SUN_APPARENT_RADIUS = 32.0 / (60.0 * 2.0)
  
def geom_mean_anomaly_sun(juliancentury: float) -> float:
    """Calculate the geometric mean anomaly of the sun"""
    return 357.52911 + juliancentury * (35999.05029 - 0.0001537 * juliancentury)
  
def sun_eq_of_center(juliancentury: float) -> float:
    """Calculate the equation of the center of the sun"""
    m = geom_mean_anomaly_sun(juliancentury)

    mrad = radians(m)
    sinm = sin(mrad)
    sin2m = sin(mrad + mrad)
    sin3m = sin(mrad + mrad + mrad)

    c = (
        sinm * (1.914602 - juliancentury * (0.004817 + 0.000014 * juliancentury))
        + sin2m * (0.019993 - 0.000101 * juliancentury)
        + sin3m * 0.000289
    )

    return c

def geom_mean_long_sun(juliancentury: float) -> float:
    """Calculate the geometric mean longitude of the sun"""
    l0 = 280.46646 + juliancentury * (36000.76983 + 0.0003032 * juliancentury)
    return l0 % 360.0

def sun_true_long(juliancentury: float) -> float:
    """Calculate the sun's true longitude"""
    l0 = geom_mean_long_sun(juliancentury)
    c = sun_eq_of_center(juliancentury)

    return l0 + c

def mean_obliquity_of_ecliptic(juliancentury: float) -> float:
    seconds = 21.448 - juliancentury * (
        46.815 + juliancentury * (0.00059 - juliancentury * (0.001813))
    )
    return 23.0 + (26.0 + (seconds / 60.0)) / 60.0

def obliquity_correction(juliancentury: float) -> float:
    e0 = mean_obliquity_of_ecliptic(juliancentury)

    omega = 125.04 - 1934.136 * juliancentury
    return e0 + 0.00256 * cos(radians(omega))

def sun_apparent_long(juliancentury: float) -> float:
    true_long = sun_true_long(juliancentury)

    omega = 125.04 - 1934.136 * juliancentury
    return true_long - 0.00569 - 0.00478 * sin(radians(omega))

def sun_declination(juliancentury: float) -> float:
    """Calculate the sun's declination"""
    e = obliquity_correction(juliancentury)
    lambd = sun_apparent_long(juliancentury)

    sint = sin(radians(e)) * sin(radians(lambd))
    return degrees(asin(sint))
  
def julianday(
    at, calendar = 1
) -> float:
    """Calculate the Julian Day (number) for the specified date/time

    julian day numbers for dates are calculated for the start of the day
    """

    def _time_to_seconds(t: datetime.time) -> int:
        return int(t.hour * 3600 + t.minute * 60 + t.second)

    year = at.year
    month = at.month
    day = at.day
    day_fraction = 0
    if isinstance(at, datetime.datetime):
        t = _time_to_seconds(at.time())
        day_fraction = t / (24 * 60 * 60)
    else:
        day_fraction = 0

    if month <= 2:
        year -= 1
        month += 12

    a = int(year / 100)
    if calendar == 1:
        b = 2 - a + int(a / 4)
    else:
        b = 0
    jd = (
        int(365.25 * (year + 4716))
        + int(30.6001 * (month + 1))
        + day
        + day_fraction
        + b
        - 1524.5
    )

    return jd


def julianday_to_juliancentury(julianday: float) -> float:
    """Convert a Julian Day number to a Julian Century"""
    return (julianday - 2451545.0) / 36525.0


def juliancentury_to_julianday(juliancentury: float) -> float:
    """Convert a Julian Century number to a Julian Day"""
    return (juliancentury * 36525.0) + 2451545.0


def julianday_2000(at) -> float:
    """Calculate the numer of Julian Days since Jan 1.5, 2000"""
    return julianday(at) - 2451545.0

def now(tz = None) -> datetime.datetime:
    """Returns the current time in the specified time zone"""
    now_utc = datetime.datetime.now(datetime.timezone.utc)
    if tz is None:
        return now_utc

    return now_utc.astimezone(tz)

def refraction_at_zenith(zenith: float) -> float:
    """Calculate the degrees of refraction of the sun due to the sun's elevation."""

    elevation = 90 - zenith
    if elevation >= 85.0:
        return 0

    refraction_correction = 0.0
    te = tan(radians(elevation))
    if elevation > 5.0:
        refraction_correction = (
            58.1 / te - 0.07 / (te * te * te) + 0.000086 / (te * te * te * te * te)
        )
    elif elevation > -0.575:
        step1 = -12.79 + elevation * 0.711
        step2 = 103.4 + elevation * step1
        step3 = -518.2 + elevation * step2
        refraction_correction = 1735.0 + elevation * step3
    else:
        refraction_correction = -20.774 / te

    refraction_correction = refraction_correction / 3600.0

    return refraction_correction

def zenith_and_azimuth(
    observer,
    dateandtime,
    with_refraction: bool = True,
):
    if observer.latitude > 89.8:
        latitude = 89.8
    elif observer.latitude < -89.8:
        latitude = -89.8
    else:
        latitude = observer.latitude

    longitude = observer.longitude

    if dateandtime.tzinfo is None:
        zone = 0.0
        utc_datetime = dateandtime
    else:
        zone = -dateandtime.utcoffset().total_seconds() / 3600.0  # type: ignore
        utc_datetime = dateandtime.astimezone(datetime.timezone.utc)

    jd = julianday(utc_datetime)
    t = julianday_to_juliancentury(jd)
    declination = sun_declination(t)
    eqtime = eq_of_time(t)

    # 360deg * 4 == 1440 minutes, 60*24 = 1440 minutes == 1 rotation
    solarTimeFix = eqtime + (4.0 * longitude) + (60 * zone)
    trueSolarTime = (
        dateandtime.hour * 60.0
        + dateandtime.minute
        + dateandtime.second / 60.0
        + solarTimeFix
    )
    #    in minutes as a float, fractional part is seconds

    while trueSolarTime > 1440:
        trueSolarTime = trueSolarTime - 1440

    hourangle = trueSolarTime / 4.0 - 180.0
    #    Thanks to <PRESIDIO_ANONYMIZED_PERSON> for the next line:
    if hourangle < -180:
        hourangle = hourangle + 360.0

    ch = cos(radians(hourangle))
    # sh = sin(radians(hourangle))
    cl = cos(radians(latitude))
    sl = sin(radians(latitude))
    sd = sin(radians(declination))
    cd = cos(radians(declination))

    csz = cl * cd * ch + sl * sd

    if csz > 1.0:
        csz = 1.0
    elif csz < -1.0:
        csz = -1.0

    zenith = degrees(acos(csz))

    azDenom = cl * sin(radians(zenith))

    if abs(azDenom) > 0.001:
        azRad = ((sl * cos(radians(zenith))) - sd) / azDenom

        if abs(azRad) > 1.0:
            if azRad < 0:
                azRad = -1.0
            else:
                azRad = 1.0

        azimuth = 180.0 - degrees(acos(azRad))

        if hourangle > 0.0:
            azimuth = -azimuth
    else:
        if latitude > 0.0:
            azimuth = 180.0
        else:
            azimuth = 0.0

    if azimuth < 0.0:
        azimuth = azimuth + 360.0

    if with_refraction:
        zenith -= refraction_at_zenith(zenith)
        # elevation = 90 - zenith

    return zenith, azimuth

def zenith(
    observer,
    dateandtime,
    with_refraction: bool = True,
) -> float:
    """Calculate the zenith angle of the sun.

    Args:
        observer:    Observer to calculate the solar zenith for
        dateandtime: The date and time for which to calculate the angle.
                     If `dateandtime` is None or is a naive Python datetime
                     then it is assumed to be in the UTC timezone.
        with_refraction: If True adjust zenith to take refraction into account

    Returns:
        The zenith angle in degrees.
    """

    if dateandtime is None:
        dateandtime = now(datetime.timezone.utc)

    return zenith_and_azimuth(observer, dateandtime, with_refraction)[0]

def eccentric_location_earth_orbit(juliancentury: float) -> float:
    """Calculate the eccentricity of Earth's orbit"""
    return 0.016708634 - juliancentury * (0.000042037 + 0.0000001267 * juliancentury)

def var_y(juliancentury: float) -> float:
    epsilon = obliquity_correction(juliancentury)
    y = tan(radians(epsilon) / 2.0)
    return y * y

def eq_of_time(juliancentury: float):
    l0 = geom_mean_long_sun(juliancentury)
    e = eccentric_location_earth_orbit(juliancentury)
    m = geom_mean_anomaly_sun(juliancentury)

    y = var_y(juliancentury)

    sin2l0 = sin(2.0 * radians(l0))
    sinm = sin(radians(m))
    cos2l0 = cos(2.0 * radians(l0))
    sin4l0 = sin(4.0 * radians(l0))
    sin2m = sin(2.0 * radians(m))

    Etime = (
        y * sin2l0
        - 2.0 * e * sinm
        + 4.0 * e * y * sinm * cos2l0
        - 0.5 * y * y * sin4l0
        - 1.25 * e * e * sin2m
    )

    return degrees(Etime) * 4.0

def adjust_to_horizon(elevation: float) -> float:
    """Calculate the extra degrees of depression that you can see round the earth
    due to the increase in elevation.

    Args:
        elevation: Elevation above the earth in metres

    Returns:
        A number of degrees to add to adjust for the elevation of the observer
    """

    if elevation <= 0:
        return 0

    r = 6356900  # radius of the earth
    a1 = r
    h1 = r + elevation
    theta1 = acos(a1 / h1)
    return degrees(theta1)

def adjust_to_obscuring_feature(elevation) -> float:
    """Calculate the number of degrees to adjust for an obscuring feature"""
    if elevation[0] == 0.0:
        return 0.0

    sign = -1 if elevation[0] < 0.0 else 1
    return sign * degrees(
        acos(fabs(elevation[0]) / sqrt(pow(elevation[0], 2) + pow(elevation[1], 2)))
    )

def hour_angle(
    latitude: float, declination: float, zenith: float, direction
) -> float:
    """Calculate the hour angle of the sun

    See https://en.wikipedia.org/wiki/Hour_angle#Solar_hour_angle

    Args:
        latitude: The latitude of the obersver
        declination: The declination of the sun
        zenith: The zenith angle of the sun
        direction: The direction of traversal of the sun

    Raises:
        ValueError
    """

    latitude_rad = radians(latitude)
    declination_rad = radians(declination)
    zenith_rad = radians(zenith)

    h = (cos(zenith_rad) - sin(latitude_rad) * sin(declination_rad)) / (
        cos(latitude_rad) * cos(declination_rad)
    )

    hour_angle = acos(h)
    if direction == -1:
        hour_angle = -hour_angle
    return hour_angle

# region Backend
def minutes_to_timedelta(minutes: float) -> datetime.timedelta:
    """Convert a floating point number of minutes to a
    :class:`~datetime.timedelta`
    """
    d = int(minutes / 1440)
    minutes = minutes - (d * 1440)
    minutes = minutes * 60
    s = int(minutes)
    sfrac = minutes - s
    us = int(sfrac * 1_000_000)

    return datetime.timedelta(days=d, seconds=s, microseconds=us)

def time_of_transit(
    observer,
    date,
    zenith,
    direction,
    with_refraction = True,
) -> datetime.datetime:
    """Calculate the time in the UTC timezone when the sun transits the
    specificed zenith

    Args:
        observer: An observer viewing the sun at a specific, latitude, longitude
            and elevation
        date: The date to calculate for
        zenith: The zenith angle for which to calculate the transit time
        direction: The direction that the sun is traversing

    Raises:
        ValueError if the zenith is not transitted by the sun

    Returns:
        the time when the sun transits the specificed zenith
    """
    if observer.latitude > 89.8:
        latitude = 89.8
    elif observer.latitude < -89.8:
        latitude = -89.8
    else:
        latitude = observer.latitude

    adjustment_for_elevation = 0.0
    if isinstance(observer.elevation, float) and observer.elevation > 0.0:
        adjustment_for_elevation = adjust_to_horizon(observer.elevation)
    elif isinstance(observer.elevation, tuple):
        adjustment_for_elevation = adjust_to_obscuring_feature(observer.elevation)

    if with_refraction:
        adjustment_for_refraction = refraction_at_zenith(
            zenith + adjustment_for_elevation
        )
    else:
        adjustment_for_refraction = 0.0

    jd = julianday(date)
    adjustment = 0.0
    timeUTC = 0.0

    for _ in range(2):
        jc = julianday_to_juliancentury(jd + adjustment)
        declination = sun_declination(jc)

        hourangle = hour_angle(
            latitude,
            declination,
            zenith + adjustment_for_elevation + adjustment_for_refraction,
            direction,
        )

        delta = -observer.longitude - degrees(hourangle)

        eqtime = eq_of_time(jc)
        offset = delta * 4.0 - eqtime

        if offset < -720.0:
            offset += 1440

        timeUTC = 720.0 + offset
        adjustment = timeUTC / 1440.0

    td = minutes_to_timedelta(timeUTC)
    dt = datetime.datetime(date.year, date.month, date.day) + td
    dt = dt.replace(tzinfo=datetime.timezone.utc)  # pylint: disable=E1120
    return dt

def noon(
    observer,
    date,
    tzinfo
) -> datetime.datetime:
    """Calculate solar noon time when the sun is at its highest point.

    Args:
        observer: An observer viewing the sun at a specific, latitude, longitude
                  and elevation
        date:     Date to calculate for. Default is today for the specified tzinfo.
        tzinfo:   Timezone to return times in. Default is UTC.

    Returns:
        Date and time at which noon occurs.
    """
    if isinstance(tzinfo, str):
        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore

    if date is None:
        date = today(tzinfo)  # type: ignore

    jc = julianday_to_juliancentury(julianday(date))
    eqtime = eq_of_time(jc)
    timeUTC = (720.0 - (4 * observer.longitude) - eqtime) / 60.0

    hour = int(timeUTC)
    minute = int((timeUTC - hour) * 60)
    second = int((((timeUTC - hour) * 60) - minute) * 60)

    if second > 59:
        second -= 60
        minute += 1
    elif second < 0:
        second += 60
        minute -= 1

    if minute > 59:
        minute -= 60
        hour += 1
    elif minute < 0:
        minute += 60
        hour -= 1

    if hour > 23:
        hour -= 24
        date += datetime.timedelta(days=1)
    elif hour < 0:
        hour += 24
        date -= datetime.timedelta(days=1)

    noon = datetime.datetime(
        date.year,
        date.month,
        date.day,
        hour,
        minute,
        second,
        tzinfo=datetime.timezone.utc,
    )
    return noon.astimezone(tzinfo)  # type: ignore pylint: disable=E1120

def calc_sunrise(
    observer, date, tzinfo
) -> datetime.datetime:
    """Calculate sunrise time.

    Args:
        observer: Observer to calculate sunrise for
        date:     Date to calculate for. Default is today's date in the
                  timezone `tzinfo`.
        tzinfo:   Timezone to return times in. Default is UTC.

    Returns:
        Date and time at which sunrise occurs.

    Raises:
        ValueError: if the sun does not reach the horizon on the specified date
    """
    if isinstance(tzinfo, str):
        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore

    if date is None:
        date = today(tzinfo)  # type: ignore
    elif isinstance(date, datetime.datetime):
        tzinfo = date.tzinfo or tzinfo
        date = date.date()

    try:
        tot = time_of_transit(
            observer,
            date,
            90.0 + SUN_APPARENT_RADIUS,
            1,
        ).astimezone(
            tzinfo  # type: ignore
        )

        tot_date = tot.date()
        if tot_date != date:
            if tot_date < date:
                delta = datetime.timedelta(days=1)
            else:
                delta = datetime.timedelta(days=-1)
            new_date = date + delta

            tot = time_of_transit(
                observer,
                new_date,
                90.0 + SUN_APPARENT_RADIUS,
                1,
            ).astimezone(
                tzinfo  # type: ignore
            )
            tot_date = tot.date()
            if tot_date != date:
                raise ValueError("Unable to find a sunrise time on the date specified")
        return tot
    except ValueError as exc:
        if exc.args[0] == "math domain error":
            z = zenith(observer, noon(observer, date))
            if z > 90.0:
                msg = "Sun is always below the horizon on this day, at this location."
            else:
                msg = "Sun is always above the horizon on this day, at this location."
            raise ValueError(msg) from exc
        else:
            raise


def calc_sunset(
    observer, date, tzinfo
) -> datetime.datetime:
    """Calculate sunset time.

    Args:
        observer: Observer to calculate sunset for
        date:     Date to calculate for. Default is today's date in the
                  timezone `tzinfo`.
        tzinfo:   Timezone to return times in. Default is UTC.

    Returns:
        Date and time at which sunset occurs.

    Raises:
        ValueError: if the sun does not reach the horizon
    """

    if isinstance(tzinfo, str):
        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore

    if date is None:
        date = today(tzinfo)  # type: ignore
    elif isinstance(date, datetime.datetime):
        tzinfo = date.tzinfo or tzinfo
        date = date.date()

    try:
        tot = time_of_transit(
            observer,
            date,
            90.0 + SUN_APPARENT_RADIUS,
            -1,
        ).astimezone(
            tzinfo  # type: ignore
        )

        tot_date = tot.date()
        if tot_date != date:
            if tot_date < date:
                delta = datetime.timedelta(days=1)
            else:
                delta = datetime.timedelta(days=-1)
            new_date = date + delta

            tot = time_of_transit(
                observer,
                new_date,
                90.0 + SUN_APPARENT_RADIUS,
                -1,
            ).astimezone(
                tzinfo  # type: ignore
            )
            tot_date = tot.date()
            if tot_date != date:
                raise ValueError("Unable to find a sunset time on the date specified")
        return tot
    except ValueError as exc:
        if exc.args[0] == "math domain error":
            z = zenith(observer, noon(observer, date))
            if z > 90.0:
                msg = "Sun is always below the horizon on this day, at this location."
            else:
                msg = "Sun is always above the horizon on this day, at this location."
            raise ValueError(msg) from exc
        else:
            raise
        
def calculate_sunrise_sunset_astral(observer, timezone, date=None):
    if date is None:
        date = datetime.now()
    timezone = pytz.timezone(timezone)
    
    sunrise_str = calc_sunrise(observer, date, timezone)
    sunset_str = calc_sunset(observer, date, timezone)

    return sunrise_str, sunset_str

if __name__ == "__main__":
    # Location coordinates
    class Observer:
        latitude = 9.9500167
        longitude = 76.283925
        elevation = 0.0

    # Date and timezone information
    day, month, year = 14, 10, 2024
    
    date = datetime.datetime(year, month, day)
    timezone = 'Asia/Kolkata'
    sunrise, sunset = calculate_sunrise_sunset_astral(Observer, timezone, date)
    print(f"Sunrise: {sunrise}")
    print(f"Sunset: {sunset}")
    # sunrise_str = sunrise.strftime("%dth %B %Y %I:%M %p")
    # sunset_str = sunset.strftime("%dth %B %Y %I:%M %p")
    sunrise_str = sunrise.strftime("%dth %B %Y %I:%M:%S %p")
    sunset_str = sunset.strftime("%dth %B %Y %I:%M:%S %p")
    print(f"Sunrise: {sunrise_str}")
    print(f"Sunset: {sunset_str}")
```

Notice How clean and detailed the solar calculator file is? Make sure rip.py file is also that way, with proper docstrings, proper namings and explanation, etc... Clean the file and make it clean. Thanks! Return full rip.py cleaned please. Thanks!