def getEphemerisDataLineForDatetime(dt):
    """Obtains the line of CSV text of planetary position data.

    Arguments:
    dt - datetime.datetime object with the timestamp seeked.  
    
    Returns:
    
    str in CSV format. Since there are a lot of fields, please See the
    section of code where we write the header info str for the format.
    """

    # Return value.
    rv = ""

    planetaryInfos = getPlanetaryInfosForDatetime(dt)

    log.debug("Just obtained planetaryInfos for timestamp: {}".\
              format(Ephemeris.datetimeToStr(dt)))
    
    # Planet geocentric longitude 15-degree axis points.
    for planetName in geocentricPlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                lon = pi.geocentric['tropical']['longitude']
                rv += "{:.3f},".format(lon % 15.0)
                    
    # Planet geocentric longitude.
    for planetName in geocentricPlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                lon = pi.geocentric['tropical']['longitude']
                rv += "{:.3f},".format(lon)
                    
    # Planet geocentric longitude in zodiac str format.
    for planetName in geocentricPlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                lon = pi.geocentric['tropical']['longitude']
                valueStr = \
                         AstrologyUtils.\
                         convertLongitudeToStrWithRasiAbbrev(lon)
                rv += valueStr + ","
                
    # Planet heliocentric longitude 15-degree axis points.
    for planetName in heliocentricPlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                lon = pi.heliocentric['tropical']['longitude']
                rv += "{:.3f},".format(lon % 15.0)
                    
    # Planet heliocentric longitude.
    for planetName in heliocentricPlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                lon = pi.heliocentric['tropical']['longitude']
                rv += "{:.3f},".format(lon)
                    
    # Planet heliocentric longitude in zodiac str format.
    for planetName in heliocentricPlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                lon = pi.heliocentric['tropical']['longitude']
                valueStr = \
                         AstrologyUtils.\
                         convertLongitudeToStrWithRasiAbbrev(lon)
                rv += valueStr + ","
                
    # Planet declination.
    for planetName in declinationPlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                declination = pi.geocentric['tropical']['declination']
                rv += "{:.3f},".format(declination)
    
    # Planet geocentric latitude.
    for planetName in geocentricLatitudePlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                latitude = pi.geocentric['tropical']['latitude']
                rv += "{:.3f},".format(latitude)
    
    # Planet heliocentric latitude.
    for planetName in heliocentricLatitudePlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                latitude = pi.heliocentric['tropical']['latitude']
                rv += "{:.3f},".format(latitude)
    
    
    # Remove trailing comma.
    rv = rv[:-1]

    return rv
headerLine += "CycleHit2P,CycleHit3P,"
headerLine += tacEphemerisHeaderFields + ","
headerLine += genericEphemerisHeaderFields + ","

# Remove the trailing comma.
headerLine = headerLine[:-1]

# Get the earliest date.
earliestDt = getEarliestTimestampFromData(priceBars,
                                          tacEphemerisData,
                                          cycleHitDates2PlanetSystem,
                                          cycleHitDates3PlanetSystem,
                                          tacEphemerisData)

log.debug("Earliest timestamp in all the files is: {}".\
          format(Ephemeris.datetimeToStr(earliestDt)))

latestDt = getLatestTimestampFromData(priceBars,
                                      tacEphemerisData,
                                      cycleHitDates2PlanetSystem,
                                      cycleHitDates3PlanetSystem,
                                      tacEphemerisData)

log.debug("Latest timestamp in all the files is: {}".\
          format(Ephemeris.datetimeToStr(latestDt)))


startDt = earliestDt
endDt = latestDt

# Initialize the currDt to the start date.  Manually set the hour and
def processPCDD(pcdd, tag):
    """Module for printing generic information about the PriceBars in a
    PriceChartDocumentData object.  The generic information that is
    printed includes:
 
     - Earliest pricebar timestamp as a datetime.datetime and a julian day.
     - Latest   pricebar timestamp as a datetime.datetime and a julian day.
     - highest  pricebar high price.
     - lowest   pricebar low price.

    Arguments:
    pcdd - PriceChartDocumentData object that will be modified.
    tag  - str containing the tag.  The value of this field
           may be "" if a tag is not specified by the user.
           This implementation doesn't use this field.
           
    Returns:
    0 if the changes are to be saved to file.
    1 if the changes are NOT to be saved to file.
    This implementation always returns 1.
    """

    # Return value.
    rv = 1

    # Get the number of PriceBars.
    numPriceBars = len(pcdd.priceBars)
    
    # Get the earliest and latest PriceBar.
    earliestPriceBar = None
    latestPriceBar = None
    lowestPrice = None
    highestPrice = None
    lowestClosePrice = None
    highestClosePrice = None

    for pb in pcdd.priceBars:
        if earliestPriceBar == None:
            earliestPriceBar = pb
        elif pb.timestamp < earliestPriceBar.timestamp:
            earliestPriceBar = pb

        if latestPriceBar == None:
            latestPriceBar = pb
        elif pb.timestamp > latestPriceBar.timestamp:
            latestPriceBar = pb

        if lowestPrice == None:
            lowestPrice = pb.low
        elif pb.low < lowestPrice:
            lowestPrice = pb.low

        if highestPrice == None:
            highestPrice = pb.high
        elif pb.high > highestPrice:
            highestPrice = pb.high
            
        if lowestClosePrice == None:
            lowestClosePrice = pb.close
        elif pb.close < lowestClosePrice:
            lowestClosePrice = pb.close

        if highestClosePrice == None:
            highestClosePrice = pb.close
        elif pb.close > highestClosePrice:
            highestClosePrice = pb.close
            
    log.info("")
    log.info("Number of PriceBars: {}".format(numPriceBars))

    if numPriceBars > 0:

        # Make sure we got values for everything.
        if earliestPriceBar == None or \
               latestPriceBar == None or \
               lowestPrice == None or \
               highestPrice == None or \
               lowestClosePrice == None or \
               highestClosePrice == None:
            
            log.error("PriceBars existed, but we are missing some set values.")
            rv = 1
            return rv
        
        # Convert the datetimes to julian day.
        earliestPriceBarJd = \
            Ephemeris.datetimeToJulianDay(earliestPriceBar.timestamp)
        latestPriceBarJd = \
            Ephemeris.datetimeToJulianDay(latestPriceBar.timestamp)

        # Print the information to log.
        log.info("EarliestPriceBar datetime   == {}".\
                 format(Ephemeris.datetimeToStr(earliestPriceBar.timestamp)))
        log.info("LatestPriceBar   datetime   == {}".\
                 format(Ephemeris.datetimeToStr(latestPriceBar.timestamp)))
        log.info("EarliestPriceBar julian day == {}".format(earliestPriceBarJd))
        log.info("LatestPriceBar   julian day == {}".format(latestPriceBarJd))
        log.info("Lowest  PriceBar LOW   price == {}".format(highestPrice))
        log.info("Highest PriceBar HIGH  price == {}".format(highestPrice))
        log.info("Lowest  PriceBar CLOSE price == {}".format(highestPrice))
        log.info("Highest PriceBar CLOSE price == {}".format(highestPrice))
        
    log.info("")

    rv = 1
    return rv
#if __name__ == "__main__":
    
# Initialize Ephemeris (required).
Ephemeris.initialize()

# Set the Location (required).
Ephemeris.setGeographicPosition(locationLongitude,
                                locationLatitude,
                                locationElevation)

# Log the parameters that are being used.
log.info("Location used is: {}  (lat={}, lon={})".\
         format(locationName, locationLatitude, locationLongitude))
log.info("Timezone used is: {}".format(timezone.zone))
log.info("Start timestamp:  {}".format(Ephemeris.datetimeToStr(startDt)))
log.info("End   timestamp:  {}".format(Ephemeris.datetimeToStr(endDt)))

# Compile the header line text.
headerLine = ""
headerLine += "Date" + ","
headerLine += "Day of week" + ","
headerLine += "Day count" + ","
headerLine += "Week count" + ","
headerLine += "Month count" + ","

# Planet geocentric longitude mod 15.
for planetName in geocentricPlanetNames:
    headerLine += "G." + planetName + "%15" + ","

# Planet geocentric longitude.
def processPCDD(pcdd, tag):
    """Module for drawing two veritcal lines (line segments) at 1/3
    and 2/3 of the way through the price chart.  The lines will have a
    tag str matching the given tag parameter.

    Arguments:
    pcdd - PriceChartDocumentData object that will be modified.
    tag  - str containing the tag.  

    Returns:
    0 if the changes are to be saved to file.
    1 if the changes are NOT to be saved to file.
    """

    # Return value.
    rv = 0

    # Check input.
    if tag == None or tag == "":
        log.error("Must specify a non-empty tag str value.")
        rv = 1
        return rv


    # Get the earliest and latest PriceBar.
    earliestPriceBar = None
    latestPriceBar = None
    lowestPrice = None
    highestPrice = None
    
    for pb in pcdd.priceBars:
        if earliestPriceBar == None:
            earliestPriceBar = pb
        elif pb.timestamp < earliestPriceBar.timestamp:
            earliestPriceBar = pb

        if latestPriceBar == None:
            latestPriceBar = pb
        elif pb.timestamp > latestPriceBar.timestamp:
            latestPriceBar = pb

        if lowestPrice == None:
            lowestPrice = pb.low
        elif pb.low < lowestPrice:
            lowestPrice = pb.low

        if highestPrice == None:
            highestPrice = pb.high
        elif pb.high > highestPrice:
            highestPrice = pb.high
            
    if earliestPriceBar == None or \
           latestPriceBar == None or \
           lowestPrice == None or \
           highestPrice == None:
        
        log.info("No pricebars were found in the document.  " + \
                 "Not doing anything.")
        rv = 1
        return rv

    # Convert the datetimes to julian day.
    earliestPriceBarJd = \
        Ephemeris.datetimeToJulianDay(earliestPriceBar.timestamp)
    latestPriceBarJd = \
        Ephemeris.datetimeToJulianDay(latestPriceBar.timestamp)

    log.debug("earliestPriceBar.timestamp == {}".\
              format(Ephemeris.datetimeToStr(earliestPriceBar.timestamp)))
    log.debug("latestPriceBar.timestamp == {}".\
              format(Ephemeris.datetimeToStr(latestPriceBar.timestamp)))

    log.debug("earliestPriceBarJd == {}".format(earliestPriceBarJd))
    log.debug("latestPriceBarJd == {}".format(latestPriceBarJd))

    diff = latestPriceBarJd - earliestPriceBarJd

    jdLine1 = earliestPriceBarJd + (diff / 3)
    jdLine2 = earliestPriceBarJd + ((diff / 3) * 2)

    dtLine1 = Ephemeris.julianDayToDatetime(jdLine1)
    dtLine2 = Ephemeris.julianDayToDatetime(jdLine2)

    log.debug("Creating scene...")

    # Scene for conversion functions.
    scene = PriceBarChartGraphicsScene()

    log.debug("Converting dt to x...")

    line1X = scene.datetimeToSceneXPos(dtLine1)
    line2X = scene.datetimeToSceneXPos(dtLine2)
    
    log.debug("Converting price to y...")

    lowY = scene.priceToSceneYPos(lowestPrice)
    highY = scene.priceToSceneYPos(highestPrice)
    
    log.debug("Creating line1 artifact ...")
    line1Artifact = PriceBarChartLineSegmentArtifact()
    line1Artifact.addTag(tag)
    line1Artifact.setTiltedTextFlag(False)
    line1Artifact.setAngleTextFlag(False)
    line1Artifact.setColor(QColor(Qt.red))
    line1Artifact.setStartPointF(QPointF(line1X, lowY))
    line1Artifact.setEndPointF(QPointF(line1X, highY))
    
    log.debug("Creating line2 artifact ...")
    line2Artifact = PriceBarChartLineSegmentArtifact()
    line2Artifact.addTag(tag)
    line2Artifact.setTiltedTextFlag(False)
    line2Artifact.setAngleTextFlag(False)
    line2Artifact.setColor(QColor(Qt.blue))
    line2Artifact.setStartPointF(QPointF(line2X, lowY))
    line2Artifact.setEndPointF(QPointF(line2X, highY))

    log.debug("Appending two lines...")

    pcdd.priceBarChartArtifacts.append(line1Artifact)
    pcdd.priceBarChartArtifacts.append(line2Artifact)

    log.debug("Done appending two lines.")

    rv = 0
    return rv
def getLongitudeAspectTimestamps(\
    startDt, endDt,
    planet1ParamsList,
    planet2ParamsList,
    degreeDifference,
    uniDirectionalAspectsFlag=False,
    maxErrorTd=datetime.timedelta(minutes=1)):
    """Obtains a list of datetime.datetime objects that contain
    the moments when the aspect specified is active.
        
    Warning on usage:
    When planet-longitude-averaging is utilized for the longitude
    of planet1 or planet2, the aspects returned by this function
    cannot be fully relied upon.
        
    This short-coming happens under these circumstances because it
    is possible that the longitude can abruptly 'jump' or hop a
    large distance when measurements are taken between timestamp
    steps.

    For example, this 'jumping' effect can occur if two planets A
    and B, are both around 355 degrees, and planet A crosses the 0
    degree mark.  Now the average goes from around 355 degrees
    (355 + 355 = 710 / 2 = 355), to about 180 degrees (355 + 0 =
    355 / 2 = about 180).

    While corrections for this can be made for the case of having
    only 2 planets involved, if more planets are involved then the
    adjustment required quickly becomes non-trivial.
        
    Arguments:
    startDt   - datetime.datetime object for the starting timestamp
                to do the calculations for artifacts.
    endDt     - datetime.datetime object for the ending timestamp
                to do the calculations for artifacts.

    planet1ParamsList - List of tuples that will be used as parameters
                  for planet1.  Each tuple contained in this list
                  represents parameters for each planet that will
                  get averaged to create what is known as planet1.

                  The contents of the tuple are:
                  (planetName, centricityType, longitudeType)

                  Where:
                  planetName - str holding the name of the second
                               planet to do the calculations for.
                  centricityType - str value holding either "geocentric",
                                   "topocentric", or "heliocentric".
                  longitudeType - str value holding either
                                  "tropical" or "sidereal".
                      
                  Example: So if someone wanted planet1 to be the
                  average location of of geocentric sidereal
                  Saturn and geocentric sidereal Uranus, the
                  'planet1ParamsList' parameter would be:

                  [("Saturn", "geocentric", "sidereal"),
                   ("Uranus", "geocentric", "sidereal")]

                  If the typical use-case is desired for the
                  longitude of just a single planet, pass a list
                  with only 1 tuple.  As an example, for Mercury
                  it would be:

                  [("Mercury", "heliocentric", "tropical")]
        
    planet2ParamsList - List of tuples that will be used as parameters
                  for planet2.  For additional details about the
                  format of this parameter field, please see the
                  description for parameter 'planet1ParamsList'
                      
    degreeDifference - float value for the number of degrees of
                       separation for this aspect.
                           
    uniDirectionalAspectsFlag - bool value for whether or not
                 uni-directional aspects are enabled or not.  By
                 default, aspects are bi-directional, so Saturn
                 square-aspect Jupiter would be the same as
                 Jupiter square-aspect Saturn.  If this flag is
                 set to True, then those two combinations would be
                 considered unique.  In the case where the flag is
                 set to True, for the aspect to be active,
                 planet2 would need to be 'degreeDifference'
                 degrees in front of planet1.
        
    maxErrorTd - datetime.timedelta object holding the maximum
                 time difference between the exact planetary
                 combination timestamp, and the one calculated.
                 This would define the accuracy of the
                 calculations.  
        
    Returns:
        
    List of datetime.datetime objects.  Each timestamp in the list
    is the moment where the aspect is active and satisfies the
    given parameters.  In the event of an error, the reference
    None is returned.

    """

    log.debug("Entered " + inspect.stack()[0][3] + "()")

    # List of timestamps of the aspects found.
    aspectTimestamps = []
        
    # Make sure the inputs are valid.
    if endDt < startDt:
        log.error("Invalid input: 'endDt' must be after 'startDt'")
        return None

    # Check to make sure planet lists were given.
    if len(planet1ParamsList) == 0:
        log.error("planet1ParamsList must contain at least 1 tuple.")
        return None
    if len(planet2ParamsList) == 0:
        log.error("planet2ParamsList must contain at least 1 tuple.")
        return None

    log.debug("planet1ParamsList passed in is: {}".\
              format(planet1ParamsList))
    log.debug("planet2ParamsList passed in is: {}".\
              format(planet2ParamsList))
        
    # Check for valid inputs in each of the planet parameter lists.
    for planetTuple in planet1ParamsList + planet2ParamsList:
        if len(planetTuple) != 3:
            log.error("Input error: " + \
                      "Not enough values given in planet tuple.")
            return None

        planetName = planetTuple[0]
        centricityType = planetTuple[1]
        longitudeType = planetTuple[2]
            
        loweredCentricityType = centricityType.lower()
        if loweredCentricityType != "geocentric" and \
            loweredCentricityType != "topocentric" and \
            loweredCentricityType != "heliocentric":

            log.error("Invalid input: Centricity type is invalid.  " + \
                  "Value given was: {}".format(centricityType))
            return None

        # Check inputs for longitude type.
        loweredLongitudeType = longitudeType.lower()
        if loweredLongitudeType != "tropical" and \
            loweredLongitudeType != "sidereal":

            log.error("Invalid input: Longitude type is invalid.  " + \
                  "Value given was: {}".format(longitudeType))
            return None
            
    # Field name we are getting.
    fieldName = "longitude"
        
    # Initialize the Ephemeris with the birth location.
    log.debug("Setting ephemeris location ...")
    Ephemeris.setGeographicPosition(locationLongitude,
                                    locationLatitude,
                                    locationElevation)

    # Set the step size.
    stepSizeTd = datetime.timedelta(days=1)
    for planetTuple in planet1ParamsList + planet2ParamsList:
        planetName = planetTuple[0]
            
        if Ephemeris.isHouseCuspPlanetName(planetName) or \
           Ephemeris.isAscmcPlanetName(planetName):
                
            # House cusps and ascmc planets need a smaller step size.
            stepSizeTd = datetime.timedelta(hours=1)
        elif planetName == "Moon":
            # Use a smaller step size for the moon so we can catch
            # smaller aspect sizes.
            stepSizeTd = datetime.timedelta(hours=3)
        
    log.debug("Step size is: {}".format(stepSizeTd))
        
    # Desired angles.  We need to check for planets at these angles.
    desiredAngleDegList = []

    desiredAngleDeg1 = Util.toNormalizedAngle(degreeDifference)
    desiredAngleDegList.append(desiredAngleDeg1)
    if Util.fuzzyIsEqual(desiredAngleDeg1, 0):
        desiredAngleDegList.append(360)
        
    if uniDirectionalAspectsFlag == False:
        desiredAngleDeg2 = \
            360 - Util.toNormalizedAngle(degreeDifference)
        if desiredAngleDeg2 not in desiredAngleDegList:
            desiredAngleDegList.append(desiredAngleDeg2)

    # Debug output.
    anglesStr = ""
    for angle in desiredAngleDegList:
        anglesStr += "{} ".format(angle)
    log.debug("Angles in desiredAngleDegList: " + anglesStr)

    # Iterate through, appending to aspectTimestamps list as we go.
    steps = []
    steps.append(copy.deepcopy(startDt))
    steps.append(copy.deepcopy(startDt))

    longitudesP1 = []
    longitudesP1.append(None)
    longitudesP1.append(None)
        
    longitudesP2 = []
    longitudesP2.append(None)
    longitudesP2.append(None)

    def getFieldValue(dt, planetParamsList, fieldName):
        """Creates the PlanetaryInfo object for the given
        planetParamsList and returns the value of the field
        desired.
        """
        
        log.debug("planetParamsList passed in is: {}".\
                  format(planetParamsList))
        
        unAveragedFieldValues = []
            
        for t in planetParamsList:
            planetName = t[0]
            centricityType = t[1]
            longitudeType = t[2]
                
            pi = Ephemeris.getPlanetaryInfo(planetName, dt)

            fieldValue = None
                
            if centricityType.lower() == "geocentric":
                fieldValue = pi.geocentric[longitudeType][fieldName]
            elif centricityType.lower() == "topocentric":
                fieldValue = pi.topocentric[longitudeType][fieldName]
            elif centricityType.lower() == "heliocentric":
                fieldValue = pi.heliocentric[longitudeType][fieldName]
            else:
                log.error("Unknown centricity type: {}".\
                          format(centricityType))
                fieldValue = None

            unAveragedFieldValues.append(fieldValue)

        log.debug("unAveragedFieldValues is: {}".\
                  format(unAveragedFieldValues))
        
        # Average the field values.
        total = 0.0
        for v in unAveragedFieldValues:
            total += v
        averagedFieldValue = total / len(unAveragedFieldValues)
        
        log.debug("averagedFieldValue is: {}".\
                  format(averagedFieldValue))
    
        return averagedFieldValue
            
    log.debug("Stepping through timestamps from {} to {} ...".\
              format(Ephemeris.datetimeToStr(startDt),
                     Ephemeris.datetimeToStr(endDt)))

    currDiff = None
    prevDiff = None
        

    while steps[-1] < endDt:
        currDt = steps[-1]
        prevDt = steps[-2]
            
        log.debug("Looking at currDt == {} ...".\
                  format(Ephemeris.datetimeToStr(currDt)))
            
        longitudesP1[-1] = \
            Util.toNormalizedAngle(\
            getFieldValue(currDt, planet1ParamsList, fieldName))
        longitudesP2[-1] = \
            Util.toNormalizedAngle(\
            getFieldValue(currDt, planet2ParamsList, fieldName))

        log.debug("{} {} is: {}".\
                  format(planet1ParamsList, fieldName,
                         longitudesP1[-1]))
        log.debug("{} {} is: {}".\
                  format(planet2ParamsList, fieldName,
                         longitudesP2[-1]))
        
        currDiff = Util.toNormalizedAngle(\
            longitudesP1[-1] - longitudesP2[-1])
        
        log.debug("prevDiff == {}".format(prevDiff))
        log.debug("currDiff == {}".format(currDiff))
        
        if prevDiff != None and \
               longitudesP1[-2] != None and \
               longitudesP2[-2] != None:
            
            if abs(prevDiff - currDiff) > 180:
                # Probably crossed over 0.  Adjust the prevDiff so
                # that the rest of the algorithm can continue to
                # work.
                if prevDiff > currDiff:
                    prevDiff -= 360
                else:
                    prevDiff += 360
                    
                log.debug("After adjustment: prevDiff == {}".\
                          format(prevDiff))
                log.debug("After adjustment: currDiff == {}".\
                          format(currDiff))

            for desiredAngleDeg in desiredAngleDegList:
                log.debug("Looking at desiredAngleDeg: {}".\
                          format(desiredAngleDeg))
                    
                desiredDegree = desiredAngleDeg
                    
                if prevDiff < desiredDegree and currDiff >= desiredDegree:
                    log.debug("Crossed over {} from below to above!".\
                              format(desiredDegree))

                    # This is the upper-bound of the error timedelta.
                    t1 = prevDt
                    t2 = currDt
                    currErrorTd = t2 - t1

                    # Refine the timestamp until it is less than
                    # the threshold.
                    while currErrorTd > maxErrorTd:
                        log.debug("Refining between {} and {}".\
                                  format(Ephemeris.datetimeToStr(t1),
                                         Ephemeris.datetimeToStr(t2)))

                        # Check the timestamp between.
                        timeWindowTd = t2 - t1
                        halfTimeWindowTd = \
                            datetime.\
                            timedelta(days=(timeWindowTd.days / 2.0),
                                seconds=(timeWindowTd.seconds / 2.0),
                                microseconds=\
                                      (timeWindowTd.microseconds / 2.0))
                        testDt = t1 + halfTimeWindowTd

                        testValueP1 = \
                            Util.toNormalizedAngle(getFieldValue(\
                            testDt, planet1ParamsList, fieldName))
                        testValueP2 = \
                            Util.toNormalizedAngle(getFieldValue(\
                            testDt, planet2ParamsList, fieldName))

                        log.debug("testValueP1 == {}".format(testValueP1))
                        log.debug("testValueP2 == {}".format(testValueP2))
                        
                        if longitudesP1[-2] > 240 and testValueP1 < 120:
                            # Planet 1 hopped over 0 degrees.
                            testValueP1 += 360
                        elif longitudesP1[-2] < 120 and testValueP1 > 240:
                            # Planet 1 hopped over 0 degrees.
                            testValueP1 -= 360
                            
                        if longitudesP2[-2] > 240 and testValueP2 < 120:
                            # Planet 2 hopped over 0 degrees.
                            testValueP2 += 360
                        elif longitudesP2[-2] < 120 and testValueP2 > 240:
                            # Planet 2 hopped over 0 degrees.
                            testValueP2 -= 360
                        
                        testDiff = Util.toNormalizedAngle(\
                            testValueP1 - testValueP2)

                        # Handle special cases of degrees 0 and 360.
                        # Here we adjust testDiff so that it is in the
                        # expected ranges.
                        if Util.fuzzyIsEqual(desiredDegree, 0):
                            if testDiff > 240:
                                testDiff -= 360
                        elif Util.fuzzyIsEqual(desiredDegree, 360):
                            if testDiff < 120:
                                testDiff += 360
                        
                        log.debug("testDiff == {}".format(testDiff))
                        
                        if testDiff < desiredDegree:
                            t1 = testDt
                        else:
                            t2 = testDt

                            # Update the curr values.
                            currDt = t2
                            currDiff = testDiff

                            longitudesP1[-1] = testValueP1
                            longitudesP2[-1] = testValueP2
            
                        currErrorTd = t2 - t1
                            
                    # Update our lists.
                    steps[-1] = currDt

                    # Store the aspect timestamp.
                    aspectTimestamps.append(currDt)
                 
                elif prevDiff > desiredDegree and currDiff <= desiredDegree:
                    log.debug("Crossed over {} from above to below!".\
                              format(desiredDegree))

                    # This is the upper-bound of the error timedelta.
                    t1 = prevDt
                    t2 = currDt
                    currErrorTd = t2 - t1

                    # Refine the timestamp until it is less than
                    # the threshold.
                    while currErrorTd > maxErrorTd:
                        log.debug("Refining between {} and {}".\
                                  format(Ephemeris.datetimeToStr(t1),
                                         Ephemeris.datetimeToStr(t2)))

                        # Check the timestamp between.
                        timeWindowTd = t2 - t1
                        halfTimeWindowTd = \
                            datetime.\
                            timedelta(days=(timeWindowTd.days / 2.0),
                                seconds=(timeWindowTd.seconds / 2.0),
                                microseconds=\
                                      (timeWindowTd.microseconds / 2.0))
                        testDt = t1 + halfTimeWindowTd

                        testValueP1 = \
                            Util.toNormalizedAngle(getFieldValue(\
                            testDt, planet1ParamsList, fieldName))
                        testValueP2 = \
                            Util.toNormalizedAngle(getFieldValue(\
                            testDt, planet2ParamsList, fieldName))

                        log.debug("testValueP1 == {}".format(testValueP1))
                        log.debug("testValueP2 == {}".format(testValueP2))
                        
                        if longitudesP1[-2] > 240 and testValueP1 < 120:
                            # Planet 1 hopped over 0 degrees.
                            testValueP1 += 360
                        elif longitudesP1[-2] < 120 and testValueP1 > 240:
                            # Planet 1 hopped over 0 degrees.
                            testValueP1 -= 360
                            
                        if longitudesP2[-2] > 240 and testValueP2 < 120:
                            # Planet 2 hopped over 0 degrees.
                            testValueP2 += 360
                        elif longitudesP2[-2] < 120 and testValueP2 > 240:
                            # Planet 2 hopped over 0 degrees.
                            testValueP2 -= 360

                        testDiff = Util.toNormalizedAngle(\
                            testValueP1 - testValueP2)

                        # Handle special cases of degrees 0 and 360.
                        # Here we adjust testDiff so that it is in the
                        # expected ranges.
                        if Util.fuzzyIsEqual(desiredDegree, 0):
                            if testDiff > 240:
                                testDiff -= 360
                        elif Util.fuzzyIsEqual(desiredDegree, 360):
                            if testDiff < 120:
                                testDiff += 360
                        
                        log.debug("testDiff == {}".format(testDiff))
                        
                        if testDiff > desiredDegree:
                            t1 = testDt
                        else:
                            t2 = testDt

                            # Update the curr values.
                            currDt = t2
                            currDiff = testDiff

                            longitudesP1[-1] = testValueP1
                            longitudesP2[-1] = testValueP2
                            
                        currErrorTd = t2 - t1

                    # Update our lists.
                    steps[-1] = currDt

                    # Store the aspect timestamp.
                    aspectTimestamps.append(currDt)
                 
        # Prepare for the next iteration.
        log.debug("steps[-1] is: {}".\
                  format(Ephemeris.datetimeToStr(steps[-1])))
        log.debug("stepSizeTd is: {}".format(stepSizeTd))
        
        steps.append(copy.deepcopy(steps[-1]) + stepSizeTd)
        del steps[0]
        longitudesP1.append(None)
        del longitudesP1[0]
        longitudesP2.append(None)
        del longitudesP2[0]
        
        # Update prevDiff as the currDiff.
        prevDiff = Util.toNormalizedAngle(currDiff)
        
    log.info("Number of timestamps obtained: {}".\
             format(len(aspectTimestamps)))
    
    log.debug("Exiting " + inspect.stack()[0][3] + "()")
    return aspectTimestamps
def main():
    """
    """

    #global highPrice
    #global lowPrice

    
    # Return value.
    rv = 0


    # Check global variable inputs.
    if moddedHitValue >= modulusAmt:
        log.error("Global input variable 'moddedHitValue' " + \
                  "cannot be greater than the value for 'modulusAmt'.")
        # Don't save pccd.
        rv = 1
        return rv

    if startDt > endDt:
        log.error("Global input variable 'startDt' cannot be after 'endDt'.")
        # Don't save pccd.
        rv = 1
        return rv
    

    # Ephemeris earliest timestamp.  
    ephemerisEarliestTimestamp = None
    
    # Ephemeris latest timestamp.
    ephemerisLatestTimestamp = None
    
    # Cycle hit timestamps.
    cycleHitTimestamps = []
    
    # Previous and current values held across iterations of the lines
    # of the CSV file.
    prevDt = None
    currDt = None
    prevUnmoddedValue = None
    currUnmoddedValue = None
    
    # Counter for the current line number.
    lineNum = 0

    # Column name.  This is obtained from the first line in the CSV file.
    columnName = ""


    try:
        with open(inputCsvFilename) as f:

            for line in f:
                line = line.strip()
                #log.debug("Looking at line [{}]: {}".format(lineNum, line))
                
                # First line has the column headers.
                if lineNum == 0:
                    line = line.strip()
                    fieldValues = line.split(delimiter)
                                    
                    # Make sure we have enough columns of data.
                    if columnNumber >= len(fieldValues):
                        log.error("The input CSV file does not have enough " + \
                                  "columns.  Could not read column {}".\
                                  format(columnNumber))
                        # Don't save pcdd.
                        rv = 1
                        return rv
    
                    # Get the column name.
                    columnName = fieldValues[columnNumber]
                    
                if lineNum >= linesToSkip:
                    line = line.strip()
                    fieldValues = line.split(delimiter)
    
                    # Make sure we have enough columns of data.
                    if columnNumber >= len(fieldValues):
                        log.error("The input CSV file does not have enough " + \
                                  "columns.  Could not read column {}".\
                                  format(columnNumber))
                        # Don't save pcdd.
                        rv = 1
                        return rv
        
                    # Get the date from this line of text and convert
                    # to a datetime.
                    timestampStr = fieldValues[0]
                    currDt = convertTimestampStrToDatetime(timestampStr)
                    
                    # If conversion failed then don't save.
                    if currDt == None:
                        # Don't save pcdd.
                        rv = 1
                        return rv
    
                    # Store the earliest and latest timestamps of the
                    # Ephemeris from the CSV file, as read so far.
                    if ephemerisEarliestTimestamp == None:
                        ephemerisEarliestTimestamp = currDt
                    elif currDt < ephemerisEarliestTimestamp:
                        ephemerisEarliestTimestamp = currDt
    
                    if ephemerisLatestTimestamp == None:
                        ephemerisLatestTimestamp = currDt
                    elif currDt > ephemerisLatestTimestamp:
                        ephemerisLatestTimestamp = currDt
                    
                    # Continue only if the currDt is between the start and
                    # end timestamps.
                    if startDt < currDt < endDt:
                        
                        currUnmoddedValue = float(fieldValues[columnNumber])
                        currModdedValue = currUnmoddedValue % modulusAmt
                        
                        if prevDt != None and prevUnmoddedValue != None:
                            # Assuming heliocentric cycles, so values change
                            # in only one direction.
        
                            prevModdedValue = prevUnmoddedValue % modulusAmt
                            
                            if prevUnmoddedValue < currUnmoddedValue:
                                # Increasing values.
                                
                                if prevModdedValue < currModdedValue:
                                    # Curr value has not looped over the
                                    # modulus amount.  This is the normal case.
                                    
                                    if prevModdedValue < moddedHitValue and \
                                           moddedHitValue <= currModdedValue:
                                        # We crossed over the moddedHitValue.
                                        
                                        # Check to see whether the
                                        # previous or the current is
                                        # closer to the moddedHitValue.
    
                                        prevDiff = abs(moddedHitValue - \
                                                       prevModdedValue)
    
                                        currDiff = abs(currModdedValue - \
                                                       moddedHitValue)
    
                                        if prevDiff < currDiff:
                                            # Prev is closer.
                                            cycleHitTimestamps.append(prevDt)
                                        else:
                                            # Curr is closer.
                                            cycleHitTimestamps.append(currDt)
                                            
                                elif prevModdedValue > currModdedValue:
                                    # Curr value has looped over the modulus
                                    # amount.  We must make an adjustment when
                                    # doing calculations.
                                    
                                    if prevModdedValue < moddedHitValue:
                                        # We crossed over the moddedHitValue.
                                        
                                        # Check to see whether the
                                        # previous or the current is
                                        # closer to the moddedHitValue.
    
                                        prevDiff = abs(moddedHitValue - \
                                                       prevModdedValue)
    
                                        currDiff = abs(currModdedValue + \
                                                       modulusAmt - \
                                                       moddedHitValue)
                                        
                                        if prevDiff < currDiff:
                                            # Prev is closer.
                                            cycleHitTimestamps.append(prevDt)
                                        else:
                                            # Curr is closer.
                                            cycleHitTimestamps.append(currDt)
                                            
                                    elif moddedHitValue <= currModdedValue:
                                        # We crossed over the moddedHitValue.
                                        
                                        # Check to see whether the
                                        # previous or the current is
                                        # closer to the moddedHitValue.
    
                                        prevDiff = abs(moddedHitValue + \
                                                       modulusAmt - \
                                                       prevModdedValue)
    
                                        currDiff = abs(currModdedValue - \
                                                       moddedHitValue)
    
                                        if prevDiff < currDiff:
                                            # Prev is closer.
                                            cycleHitTimestamps.append(prevDt)
                                        else:
                                            # Curr is closer.
                                            cycleHitTimestamps.append(currDt)
                                            
                            elif prevUnmoddedValue > currUnmoddedValue:
                                # Decreasing values.
                                
                                if currModdedValue < prevModdedValue:
                                    # Curr value has not looped over 0.
                                    # This is the normal case.
                                    
                                    if currModdedValue <= moddedHitValue and \
                                           moddedHitValue < prevModdedValue:
                                        # We crossed over the moddedHitValue.
                                        
                                        # Check to see whether the
                                        # previous or the current is
                                        # closer to the moddedHitValue.
    
                                        currDiff = abs(moddedHitValue - \
                                                       currModdedValue)
    
                                        prevDiff = abs(prevModdedValue - \
                                                       moddedHitValue)
    
                                        if prevDiff < currDiff:
                                            # Prev is closer.
                                            cycleHitTimestamps.append(prevDt)
                                        else:
                                            # Curr is closer.
                                            cycleHitTimestamps.append(currDt)
                                            
                                elif currModdedValue > prevModdedValue:
                                    # Curr value has looped over 0.
                                    # We must make an adjustment when
                                    # doing calculations.
                                    
                                    if currModdedValue <= moddedHitValue:
                                        # We crossed over the moddedHitValue.
                                        
                                        # Check to see whether the
                                        # previous or the current is
                                        # closer to the moddedHitValue.
    
                                        currDiff = abs(moddedHitValue - \
                                                       currModdedValue)
    
                                        prevDiff = abs(prevModdedValue + \
                                                       modulusAmt - \
                                                       moddedHitValue)
                                        
                                        if prevDiff < currDiff:
                                            # Prev is closer.
                                            cycleHitTimestamps.append(prevDt)
                                        else:
                                            # Curr is closer.
                                            cycleHitTimestamps.append(currDt)
                                            
                                    elif moddedHitValue < prevModdedValue:
                                        # We crossed over the moddedHitValue.
                                        
                                        # Check to see whether the
                                        # previous or the current is
                                        # closer to the moddedHitValue.
    
                                        currDiff = abs(moddedHitValue + \
                                                       modulusAmt - \
                                                       currModdedValue)
    
                                        prevDiff = abs(prevModdedValue - \
                                                       moddedHitValue)
    
                                        if prevDiff < currDiff:
                                            # Prev is closer.
                                            cycleHitTimestamps.append(prevDt)
                                        else:
                                            # Curr is closer.
                                            cycleHitTimestamps.append(currDt)
                                            
                            else:
                                # Value is the same.  This is an error.
                                log.error("Found two values in column {} ".\
                                          format(columnNumber) + \
                                          "that are the same value.  " + \
                                          "See line {} in file '{}'".\
                                          format(lineNum, inputCsvFilename))
                
                # Update variables for reading the next line.
                prevDt = currDt
                currDt = None
                
                prevUnmoddedValue = currUnmoddedValue
                currUnmoddedValue = None

                lineNum += 1

    except IOError as e:
        log.error("Please check to make sure input CSV file '{}'".\
                  format(inputCsvFilename) + \
                  " is a file and exists.")
        
        # Don't save pcdd.
        rv = 1
        return rv

    
    
    # We now have all the cycle hit timestamps that are in the time
    # range desired.  All these timestamps should be in the list
    # 'cycleHitTimestamps'.
    
    
    # Replace spaces in the column name with underscores.
    columnName = columnName.replace(" ", "_")

    # Set the tag name that will be used for the vertical lines.
    tag = "{}_Mod_{}_HitTo_{}".\
          format(columnName, modulusAmt, moddedHitValue)

    # Newline.
    endl = "\n"
    
    # Write results to a CSV file.
    outputFileLines = []

    # Add column headers.
    outputFileLines.append("Cycle hit dates for: " + tag + endl)

    for dt in cycleHitTimestamps:
        dateStr = formatToDateStr(dt)
        outputFileLines.append(dateStr + endl)

    # Write to file.
    with open(outputCsvFilename, "w", encoding="utf-8") as f:
        for line in outputFileLines:
            f.write(line)
    log.info("Finished writing the cycle hit dates to CSV file.")
    
    # Add the vertical lines at these timestamps.
    #for dt in cycleHitTimestamps:
    #    PlanetaryCombinationsLibrary.\
    #        addVerticalLine(pcdd, dt,
    #                        highPrice, lowPrice, tag, color)

    # Calculate the minimum, maximum and average length of time
    # between the cycle hit timestamps.
    prevDt = None
    currDt = None
    
    minimumDiffTd = None
    maximumDiffTd = None
    averageDiffTd = None
    totalDiffTd = datetime.timedelta(0)
    
    for i in range(len(cycleHitTimestamps)):
        currDt = cycleHitTimestamps[i]

        # We skip calculating the timedelta for the first point
        # because we need two timestamps to calcate the timedelta.
        
        if i != 0 and prevDt != None:
            diffTd = currDt - prevDt

            # Update values if a new minimum or maximum is seen.
            
            if minimumDiffTd == None:
                minimumDiffTd = diffTd
            elif diffTd < minimumDiffTd:
                minimumDiffTd = diffTd
                
            if maximumDiffTd == None:
                maximumDiffTd = diffTd
            elif diffTd > maximumDiffTd:
                maximumDiffTd = diffTd

            # Add the diffTd to the total for the average calculation later.
            totalDiffTd += diffTd
            
        # Update for next iteration.
        prevDt = currDt
        currDt = None

    # Calculate the average.
    if len(cycleHitTimestamps) > 1:

        # Convert the timedelta to seconds.
        totalDiffSecs = \
            (totalDiffTd.microseconds + \
             (totalDiffTd.seconds + (totalDiffTd.days * 24 * 3600)) * 10**6) \
             / 10**6

        # Compute the average.
        averageDiffSec = totalDiffSecs / (len(cycleHitTimestamps) - 1)
        
        log.debug("totalDiffSecs == {}".format(totalDiffSecs))
        log.debug("averageDiffSec == {}".format(averageDiffSec))

        # Turn the average number of seconds to a timedelta.
        averageDiffTd = datetime.timedelta(seconds=averageDiffSec)
    
    # Print information about parameters and cycle hit timestamps that
    # were found.
    log.info("----------------------------------------------------")
    log.info("Ephemeris CSV filename: '{}'".format(inputCsvFilename))
    log.info("Ephemeris CSV data column number:  {}".format(columnNumber))
    log.info("Ephemeris earliest timestamp: {}".\
             format(ephemerisEarliestTimestamp))
    log.info("Ephemeris latest   timestamp: {}".\
             format(ephemerisLatestTimestamp))
    log.info("Modulus amount:    {}".format(modulusAmt))
    log.info("Modded hit value:  {}".format(moddedHitValue))
    log.info("startDt parameter: {}".format(startDt))
    log.info("endDt   parameter: {}".format(endDt))
    #log.info("modifyPcddFlag:    {}".format(modifyPcddFlag))
    #log.info("highPrice:        {}".format(highPrice))
    #log.info("lowPrice:         {}".format(lowPrice))
    log.info("Number of cycle hit points: {}".format(len(cycleHitTimestamps)))
    log.info("Smallest timedelta between cycle hit points: {}".\
             format(minimumDiffTd))
    log.info("Largest  timedelta between cycle hit points: {}".\
             format(maximumDiffTd))
    log.info("Average  timedelta between cycle hit points: {}".\
             format(averageDiffTd))
    
    # Print the cycle turn points to debug.
    if printCycleTurnPointsFlag == True:
        log.debug("----------------------------------------------------")
        log.debug("Cycle hit points:")
        log.debug("----------------------------------------------------")
        for dt in cycleHitTimestamps:
            log.debug("{}    {}".format(Ephemeris.datetimeToStr(dt), tag))
        log.debug("----------------------------------------------------")
    
    if modifyPcddFlag == True:
        # Save changes.
        rv = 0
    else:
        # Don't save changes.
        rv = 1

    return rv
def getHeliocentricPlanetNodeInfo(planetName):
    """
    Returns a list of tuples, each tuple containing:
    (planetName,
    julianDay,
    datetime,
    "N" or "S",
    helioTropLongitudeOfNode,
    helioSidLongitudeOfNode)
    """

    # Return value.
    rv = []

    prevDt = None
    currDt = copy.deepcopy(startDt)

    prevLatitude = None
    currLatitude = None

    currTropLongitude = None
    currSidLongitude = None

    while currDt <= endDt:
        dt = currDt

        pi = Ephemeris.getPlanetaryInfo(planetName, dt)

        log.debug("Just obtained planetaryInfo for planet '{}', timestamp: {}".\
                  format(planetName, Ephemeris.datetimeToStr(dt)))

        # Get the heliocentric latitude and longitude.
        latitude = pi.heliocentric['tropical']['latitude']
        tropLongitude = pi.heliocentric['tropical']['longitude']
        sidLongitude = pi.heliocentric['sidereal']['longitude']

        # Store new current planet values.
        currLatitude = latitude
        currTropLongitude = tropLongitude
        currSidLongitude = sidLongitude

        log.debug("prevLatitude={}, currLatitude={}".\
                  format(prevLatitude, currLatitude))

        # We need two data points to proceed.
        if prevLatitude != None and currLatitude != None and prevDt != None:
            # Check to see if we passed over 0 degrees.

            if prevLatitude < 0.0 and currLatitude >= 0.0:
                # Crossed over from negative to positive!
                log.debug("Crossed over from negative to positive!")

                # This is the upper-bound of the error timedelta.
                t1 = prevDt
                t2 = currDt
                currErrorTd = t2 - t1

                # Refine the timestamp until it is less than the
                # desired threshold.
                while currErrorTd > maxErrorTd:
                    log.debug("Refining between {} and {}".\
                              format(Ephemeris.datetimeToStr(t1),
                                     Ephemeris.datetimeToStr(t2)))

                    # Check the timestamp between.
                    diffTd = t2 - t1
                    halfDiffTd = \
                        datetime.\
                        timedelta(days=(diffTd.days / 2.0),
                                  seconds=(diffTd.seconds / 2.0),
                                  microseconds=(diffTd.microseconds / 2.0))
                    testDt = t1 + halfDiffTd

                    pi = Ephemeris.getPlanetaryInfo(planetName, testDt)

                    testLatitude = pi.heliocentric['tropical']['latitude']
                    testTropLongitude = pi.heliocentric['tropical'][
                        'longitude']
                    testSidLongitude = pi.heliocentric['sidereal']['longitude']

                    if testLatitude >= 0.0:
                        t2 = testDt

                        # Update the curr values as the later boundary.
                        currDt = t2
                        currLatitude = testLatitude
                        currTropLongitude = testTropLongitude
                        currSidLongitude = testSidLongitude
                    else:
                        t1 = testDt

                    currErrorTd = t2 - t1

                # Broke out of while loop, meaning we have a timestamp
                # within our threshold.
                # Create a tuple to add to our list.
                tup = (planetName, Ephemeris.datetimeToJulianDay(currDt),
                       currDt, northStr, currTropLongitude, currSidLongitude)

                # Append to the list.
                rv.append(tup)

            elif prevLatitude > 0.0 and currLatitude <= 0.0:
                # Crossed over from positive to negative!
                log.debug("Crossed over from positive to negative!")

                # This is the upper-bound of the error timedelta.
                t1 = prevDt
                t2 = currDt
                currErrorTd = t2 - t1

                # Refine the timestamp until it is less than the
                # desired threshold.
                while currErrorTd > maxErrorTd:
                    log.debug("Refining between {} and {}".\
                              format(Ephemeris.datetimeToStr(t1),
                                     Ephemeris.datetimeToStr(t2)))

                    # Check the timestamp between.
                    diffTd = t2 - t1
                    halfDiffTd = \
                        datetime.\
                        timedelta(days=(diffTd.days / 2.0),
                                  seconds=(diffTd.seconds / 2.0),
                                  microseconds=(diffTd.microseconds / 2.0))
                    testDt = t1 + halfDiffTd

                    pi = Ephemeris.getPlanetaryInfo(planetName, testDt)

                    testLatitude = pi.heliocentric['tropical']['latitude']
                    testTropLongitude = pi.heliocentric['tropical'][
                        'longitude']
                    testSidLongitude = pi.heliocentric['sidereal']['longitude']

                    if testLatitude <= 0.0:
                        t2 = testDt

                        # Update the curr values as the later boundary.
                        currDt = t2
                        currLatitude = testLatitude
                        currTropLongitude = testTropLongitude
                        currSidLongitude = testSidLongitude
                    else:
                        t1 = testDt

                    currErrorTd = t2 - t1

                # Broke out of while loop, meaning we have a timestamp
                # within our threshold.
                # Create a tuple to add to our list.
                tup = (planetName, Ephemeris.datetimeToJulianDay(currDt),
                       currDt, southStr, currTropLongitude, currSidLongitude)

                # Append to the list.
                rv.append(tup)

        # Increment currDt timestamp.
        prevDt = currDt
        currDt = currDt + stepSizeTd

        # Move the previous currLatitude to prevLatitude.
        prevLatitude = currLatitude
        currLatitude = None
        currTropLongitude = None
        currSidLongitude = None

        log.debug("prevLatitude={}, currLatitude={}".\
                  format(prevLatitude, currLatitude))

    return rv
def getOnePlanetLongitudeAspectTimestamps(\
    startDt, endDt,
    planet1Params,
    fixedDegree,
    degreeDifference,
    uniDirectionalAspectsFlag=False,
    maxErrorTd=datetime.timedelta(hours=1)):
    """Obtains a list of datetime.datetime objects that contain
    the moments when the aspect specified is active.
    The aspect is measured by formula:
       (planet longitude) - (fixed longitude degree)
    
    Arguments:
    startDt   - datetime.datetime object for the starting timestamp
                to do the calculations for artifacts.
    endDt     - datetime.datetime object for the ending timestamp
                to do the calculations for artifacts.
    highPrice - float value for the high price to end the vertical line.
    lowPrice  - float value for the low price to end the vertical line.
    
    planet1Params - Tuple containing:
                  (planetName, centricityType, longitudeType)

                  Where:
                  planetName - str holding the name of the second
                               planet to do the calculations for.
                  centricityType - str value holding either "geocentric",
                                   "topocentric", or "heliocentric".
                  longitudeType - str value holding either
                                  "tropical" or "sidereal".
                  
    fixedDegree - float holding the fixed degree in the zodiac circle.
                  
    degreeDifference - float value for the number of degrees of
                       separation for this aspect.
                       
    uniDirectionalAspectsFlag - bool value for whether or not
                 uni-directional aspects are enabled or not.  By
                 default, aspects are bi-directional, so Saturn
                 square-aspect Jupiter would be the same as
                 Jupiter square-aspect Saturn.  If this flag is
                 set to True, then those two combinations would be
                 considered unique.  In the case where the flag is
                 set to True, for the aspect to be active,
                 planet2 would need to be 'degreeDifference'
                 degrees in front of planet1.
        
    maxErrorTd - datetime.timedelta object holding the maximum
                 time difference between the exact planetary
                 combination timestamp, and the one calculated.
                 This would define the accuracy of the
                 calculations.  
    
    Returns:
    
    List of datetime.datetime objects.  Each timestamp in the list
    is the moment where the aspect is active and satisfies the
    given parameters.  In the event of an error, the reference
    None is returned.
    """

    log.debug("Entered " + inspect.stack()[0][3] + "()")

    # List of timestamps of the aspects found.
    aspectTimestamps = []
        
    # Make sure the inputs are valid.
    if endDt < startDt:
        log.error("Invalid input: 'endDt' must be after 'startDt'")
        return None

    # Check to make sure planet params were given.
    if len(planet1Params) != 3:
        log.error("planet1Params must be a tuple with 3 elements.")
        return None
    if not isinstance(fixedDegree, (int, float, complex)):
        log.error("fixedDegree must be a number.")
        return None

    # Normalize the fixed degree.
    fixedDegree = Util.toNormalizedAngle(fixedDegree)
        
    log.debug("planet1Params passed in is: {}".\
              format(planet1Params))
    log.debug("fixedDegree passed in is: {}".\
              format(fixedDegree))

    # Check inputs of planet parameters.
    planetName = planet1Params[0]
    centricityType = planet1Params[1]
    longitudeType = planet1Params[2]

    # Check inputs for centricity type.
    loweredCentricityType = centricityType.lower()
    if loweredCentricityType != "geocentric" and \
        loweredCentricityType != "topocentric" and \
        loweredCentricityType != "heliocentric":

        log.error("Invalid input: Centricity type is invalid.  " + \
                  "Value given was: {}".format(centricityType))
        return None
        
    # Check inputs for longitude type.
    loweredLongitudeType = longitudeType.lower()
    if loweredLongitudeType != "tropical" and \
        loweredLongitudeType != "sidereal":

        log.error("Invalid input: Longitude type is invalid.  " + \
                  "Value given was: {}".format(longitudeType))
        return None

    # Field name we are getting.
    fieldName = "longitude"
        
    # Initialize the Ephemeris with the birth location.
    log.debug("Setting ephemeris location ...")
    Ephemeris.setGeographicPosition(locationLongitude,
                                    locationLatitude,
                                    locationElevation)

    # Set the step size.
    stepSizeTd = datetime.timedelta(days=1)

    planetName = planet1Params[0]
        
    if Ephemeris.isHouseCuspPlanetName(planetName) or \
        Ephemeris.isAscmcPlanetName(planetName):
                
        # House cusps and ascmc planets need a smaller step size.
        stepSizeTd = datetime.timedelta(hours=1)
    elif planetName == "Moon":
        # Use a smaller step size for the moon so we can catch
        # smaller aspect sizes.
        stepSizeTd = datetime.timedelta(hours=3)
        
    log.debug("Step size is: {}".format(stepSizeTd))
        
    # Desired angles.  We need to check for planets at these angles.
    desiredAngleDegList = []
        
    desiredAngleDeg1 = Util.toNormalizedAngle(degreeDifference)
    desiredAngleDegList.append(desiredAngleDeg1)
    if Util.fuzzyIsEqual(desiredAngleDeg1, 0):
        desiredAngleDegList.append(360)
        
    if uniDirectionalAspectsFlag == False:
        desiredAngleDeg2 = \
            360 - Util.toNormalizedAngle(degreeDifference)
        if desiredAngleDeg2 not in desiredAngleDegList:
            desiredAngleDegList.append(desiredAngleDeg2)
        
    # Debug output.
    anglesStr = ""
    for angle in desiredAngleDegList:
        anglesStr += "{} ".format(angle)
    log.debug("Angles in desiredAngleDegList: " + anglesStr)

    # Iterate through, appending to aspectTimestamps list as we go.
    steps = []
    steps.append(copy.deepcopy(startDt))
    steps.append(copy.deepcopy(startDt))

    longitudesP1 = []
    longitudesP1.append(None)
    longitudesP1.append(None)
        
    longitudesP2 = []
    longitudesP2.append(None)
    longitudesP2.append(None)
        
    def getFieldValue(dt, planetParams, fieldName):
        """Creates the PlanetaryInfo object for the given
        planetParamsList and returns the value of the field
        desired.
        """
        
        log.debug("planetParams passed in is: {}".\
                  format(planetParams))
        t = planetParams
        
        planetName = t[0]
        centricityType = t[1]
        longitudeType = t[2]
                
        pi = Ephemeris.getPlanetaryInfo(planetName, dt)

        log.debug("Planet {} has geo sid longitude: {}".\
                  format(planetName,
                         pi.geocentric["sidereal"]["longitude"]))
            
        fieldValue = None
                
        if centricityType.lower() == "geocentric":
            fieldValue = pi.geocentric[longitudeType][fieldName]
        elif centricityType.lower() == "topocentric":
            fieldValue = pi.topocentric[longitudeType][fieldName]
        elif centricityType.lower() == "heliocentric":
            fieldValue = pi.heliocentric[longitudeType][fieldName]
        else:
            log.error("Unknown centricity type: {}".\
                      format(centricityType))
            fieldValue = None

        return fieldValue
            
    log.debug("Stepping through timestamps from {} to {} ...".\
              format(Ephemeris.datetimeToStr(startDt),
                     Ephemeris.datetimeToStr(endDt)))

    currDiff = None
    prevDiff = None
        

    while steps[-1] < endDt:
        currDt = steps[-1]
        prevDt = steps[-2]
            
        log.debug("Looking at currDt == {} ...".\
                  format(Ephemeris.datetimeToStr(currDt)))
            
        longitudesP1[-1] = \
            Util.toNormalizedAngle(\
            getFieldValue(currDt, planet1Params, fieldName))
            
        longitudesP2[-1] = fixedDegree

        log.debug("{} {} is: {}".\
                  format(planet1Params, fieldName,
                         longitudesP1[-1]))
        log.debug("fixedDegree is: {}".format(longitudesP2[-1]))
            
        currDiff = Util.toNormalizedAngle(\
            longitudesP1[-1] - longitudesP2[-1])
            
        log.debug("prevDiff == {}".format(prevDiff))
        log.debug("currDiff == {}".format(currDiff))
            
        if prevDiff != None and \
               longitudesP1[-2] != None and \
               longitudesP2[-2] != None:
                
            if abs(prevDiff - currDiff) > 180:
                # Probably crossed over 0.  Adjust the prevDiff so
                # that the rest of the algorithm can continue to
                # work.
                if prevDiff > currDiff:
                    prevDiff -= 360
                else:
                    prevDiff += 360
                        
                log.debug("After adjustment: prevDiff == {}".\
                          format(prevDiff))
                log.debug("After adjustment: currDiff == {}".\
                          format(currDiff))

            for desiredAngleDeg in desiredAngleDegList:
                log.debug("Looking at desiredAngleDeg: {}".\
                          format(desiredAngleDeg))
                    
                desiredDegree = desiredAngleDeg
                    
                if prevDiff < desiredDegree and currDiff >= desiredDegree:
                    log.debug("Crossed over {} from below to above!".\
                              format(desiredDegree))
    
                    # This is the upper-bound of the error timedelta.
                    t1 = prevDt
                    t2 = currDt
                    currErrorTd = t2 - t1
    
                    # Refine the timestamp until it is less than
                    # the threshold.
                    while currErrorTd > maxErrorTd:
                        log.debug("Refining between {} and {}".\
                                  format(Ephemeris.datetimeToStr(t1),
                                         Ephemeris.datetimeToStr(t2)))
    
                        # Check the timestamp between.
                        timeWindowTd = t2 - t1
                        halfTimeWindowTd = \
                            datetime.\
                            timedelta(days=(timeWindowTd.days / 2.0),
                                seconds=(timeWindowTd.seconds / 2.0),
                                microseconds=\
                                      (timeWindowTd.microseconds / 2.0))
                        testDt = t1 + halfTimeWindowTd

                        testValueP1 = \
                            Util.toNormalizedAngle(getFieldValue(\
                            testDt, planet1Params, fieldName))
                        testValueP2 = \
                            Util.toNormalizedAngle(fixedDegree)
    
                        log.debug("testValueP1 == {}".format(testValueP1))
                        log.debug("testValueP2 == {}".format(testValueP2))
                            
                        if longitudesP1[-2] > 240 and testValueP1 < 120:
                            # Planet 1 hopped over 0 degrees.
                            testValueP1 += 360
                        elif longitudesP1[-2] < 120 and testValueP1 > 240:
                            # Planet 1 hopped over 0 degrees.
                            testValueP1 -= 360
                                
                        if longitudesP2[-2] > 240 and testValueP2 < 120:
                            # Planet 2 hopped over 0 degrees.
                            testValueP2 += 360
                        elif longitudesP2[-2] < 120 and testValueP2 > 240:
                            # Planet 2 hopped over 0 degrees.
                            testValueP2 -= 360
                            
                        testDiff = Util.toNormalizedAngle(\
                            testValueP1 - testValueP2)
    
                        # Handle special cases of degrees 0 and 360.
                        # Here we adjust testDiff so that it is in the
                        # expected ranges.
                        if Util.fuzzyIsEqual(desiredDegree, 0):
                            if testDiff > 240:
                                testDiff -= 360
                        elif Util.fuzzyIsEqual(desiredDegree, 360):
                            if testDiff < 120:
                                testDiff += 360
                            
                        log.debug("testDiff == {}".format(testDiff))
                            
                        if testDiff < desiredDegree:
                            t1 = testDt
                        else:
                            t2 = testDt
    
                            # Update the curr values.
                            currDt = t2
                            currDiff = testDiff
    
                            longitudesP1[-1] = testValueP1
                            longitudesP2[-1] = testValueP2
                
                        currErrorTd = t2 - t1
                                
                    # Update our lists.
                    steps[-1] = currDt
    
                    # Store the aspect timestamp.
                    aspectTimestamps.append(currDt)
                     
                elif prevDiff > desiredDegree and currDiff <= desiredDegree:
                    log.debug("Crossed over {} from above to below!".\
                              format(desiredDegree))

                    # This is the upper-bound of the error timedelta.
                    t1 = prevDt
                    t2 = currDt
                    currErrorTd = t2 - t1
    
                    # Refine the timestamp until it is less than
                    # the threshold.
                    while currErrorTd > maxErrorTd:
                        log.debug("Refining between {} and {}".\
                                  format(Ephemeris.datetimeToStr(t1),
                                         Ephemeris.datetimeToStr(t2)))
    
                        # Check the timestamp between.
                        timeWindowTd = t2 - t1
                        halfTimeWindowTd = \
                            datetime.\
                            timedelta(days=(timeWindowTd.days / 2.0),
                                seconds=(timeWindowTd.seconds / 2.0),
                                microseconds=\
                                      (timeWindowTd.microseconds / 2.0))
                        testDt = t1 + halfTimeWindowTd

                        testValueP1 = \
                            Util.toNormalizedAngle(getFieldValue(\
                            testDt, planet1ParamsList, fieldName))
                        testValueP2 = \
                            Util.toNormalizedAngle(fixedDegree)
    
                        log.debug("testValueP1 == {}".format(testValueP1))
                        log.debug("testValueP2 == {}".format(testValueP2))
                            
                        if longitudesP1[-2] > 240 and testValueP1 < 120:
                            # Planet 1 hopped over 0 degrees.
                            testValueP1 += 360
                        elif longitudesP1[-2] < 120 and testValueP1 > 240:
                            # Planet 1 hopped over 0 degrees.
                            testValueP1 -= 360
                                
                        if longitudesP2[-2] > 240 and testValueP2 < 120:
                            # Planet 2 hopped over 0 degrees.
                            testValueP2 += 360
                        elif longitudesP2[-2] < 120 and testValueP2 > 240:
                            # Planet 2 hopped over 0 degrees.
                            testValueP2 -= 360
    
                        testDiff = Util.toNormalizedAngle(\
                            testValueP1 - testValueP2)
    
                        # Handle special cases of degrees 0 and 360.
                        # Here we adjust testDiff so that it is in the
                        # expected ranges.
                        if Util.fuzzyIsEqual(desiredDegree, 0):
                            if testDiff > 240:
                                testDiff -= 360
                        elif Util.fuzzyIsEqual(desiredDegree, 360):
                            if testDiff < 120:
                                testDiff += 360
                            
                        log.debug("testDiff == {}".format(testDiff))
                            
                        if testDiff > desiredDegree:
                            t1 = testDt
                        else:
                            t2 = testDt
    
                            # Update the curr values.
                            currDt = t2
                            currDiff = testDiff
    
                            longitudesP1[-1] = testValueP1
                            longitudesP2[-1] = testValueP2
                                
                        currErrorTd = t2 - t1
    
                    # Update our lists.
                    steps[-1] = currDt
    
                    # Store the aspect timestamp.
                    aspectTimestamps.append(currDt)
                     
        # Prepare for the next iteration.
        log.debug("steps[-1] is: {}".\
                  format(Ephemeris.datetimeToStr(steps[-1])))
        log.debug("stepSizeTd is: {}".format(stepSizeTd))
            
        steps.append(copy.deepcopy(steps[-1]) + stepSizeTd)
        del steps[0]
        longitudesP1.append(None)
        del longitudesP1[0]
        longitudesP2.append(None)
        del longitudesP2[0]
            
        # Update prevDiff as the currDiff.
        prevDiff = Util.toNormalizedAngle(currDiff)
            
    log.debug("Number of timestamps obtained: {}".\
             format(len(aspectTimestamps)))
        
    log.debug("Exiting " + inspect.stack()[0][3] + "()")
    return aspectTimestamps
Ejemplo n.º 10
0
def processPCDD(pcdd, tag):
    """
    Module for printing information about the BirthInfo in a
    PriceChartDocumentData object.  BirthInfo printed includes:
 
     - Birth location name.
     - Birth country name.
     - Birth location coordinates.
     - Birth timestamp as a UTC datetime.datetime
     - Birth timestamp as a julian day.

    Arguments:
    pcdd - PriceChartDocumentData object that will be modified.
    tag  - str containing the tag.  The value of this field
           may be "" if a tag is not specified by the user.
           This implementation doesn't use this field.
           
    Returns:
    0 if the changes are to be saved to file.
    1 if the changes are NOT to be saved to file.
    This implementation always returns 1.
    """

    # Return value.
    rv = 1

    birthInfo = pcdd.birthInfo

    # Convert longitude from a float value to degrees,
    # minutes, seconds and East/West polarity.
    (lonDegrees, lonMinutes, lonSeconds, lonPolarity) = \
        GeoInfo.longitudeToDegMinSec(birthInfo.longitudeDegrees)
    
    # Convert latitude from a float value to degrees, minutes,
    # seconds and North/South polarity.
    (latDegrees, latMinutes, latSeconds, latPolarity) = \
        GeoInfo.latitudeToDegMinSec(birthInfo.latitudeDegrees)
    
    log.info("")
    log.info("Birth location name: {}".format(birthInfo.locationName))
    log.info("Birth location country: {}".format(birthInfo.countryName))
    log.info("Birth location longitude: {} {} {}' {} ({})".\
             format(lonDegrees,
                    lonPolarity,
                    lonMinutes,
                    lonSeconds,
                    birthInfo.longitudeDegrees))
    log.info("Birth location latitude:  {} {} {}' {} ({})".\
             format(latDegrees,
                    latPolarity,
                    latMinutes,
                    latSeconds,
                    birthInfo.latitudeDegrees))

    birthLocalizedDatetime = birthInfo.getBirthLocalizedDatetime()
    birthUtcDatetime = birthInfo.getBirthUtcDatetime()
    birthJd = Ephemeris.datetimeToJulianDay(birthUtcDatetime)
    log.info("Birth timestamp (localized):  {}".\
             format(Ephemeris.datetimeToStr(birthLocalizedDatetime)))
    log.info("Birth timestamp (UTC):        {}".\
             format(Ephemeris.datetimeToStr(birthUtcDatetime)))
    log.info("Birth timestamp (julian day): {}".\
             format(birthJd))
    log.info("")

    rv = 1
    return rv
def processSwingFileData(swingFileData):
    """Processes the given SwingFileData.
    
    What this means in detail is it will scan through the PriceBars in
    the given SwingFileData, looking for relative highs and lows
    within our parameters for a window size, and if the PriceBar is
    determined to be a high or low, then the PriceBar will be tagged
    appropriately.  When this function is complete, the SwingFileData
    will only contain PriceBars that are tagged and deemed to be a
    high or low.  This function will also set the field
    'swingFileDescription' within the given SwingFileData object with
    information about the parameters used to filter out the PriceBar
    highs and lows.
    """

    # TODO: Add a flag and the code needed to implement filtering out highs and lows if they are not sharp enough.  (i.e. Filter out roving turns).
    
    # Parameters used to process the swing file:
    scanVars = []

    # Dictionaries below have the following keys-value fields:
    #
    # "tag": Text string for the tag.  The character 'H' starting the
    #     string indicates a high is being seeked.  The character 'L'
    #     starting the string indicates a low is being seeked.
    #
    # "priceRangeRequired": Percentage required for the range of
    #     PriceBar prices in the scanning window to have in order for
    #     the PriceBar to be able to be tagged as a high or low.
    #     These are percentages with values in the range [0, 1].
    # 
    # "param": The parameter value for 'X' in the following
    #     explanation of how highs and lows are determined.
    # 
    #     A PriceBar is considered a high if the following are true:
    #
    #     1) It has X number of PriceBars with lower or equal prices to it,
    #        coming before the timestamp of the PriceBar.
    #     2) It has X number of PriceBars with lower or equal prices to it.
    #        coming after the timestamp of the PriceBar.
    #  
    #     In case of a tie between PriceBars that are next to each other,
    #     the later bar wins.
    #
    #     A PriceBar is considered a low if the following are true:
    #
    #     1) It has X number of PriceBars with higher or equal prices to it,
    #        coming before the timestamp of the PriceBar.
    #     2) It has X number of PriceBars with higher or equal prices to it.
    #        coming after the timestamp of the PriceBar.
    #  
    #     In case of a tie between PriceBars that are next to each other,
    #     the later bar wins.
    #
    # 'window': List of PriceBars, used as the window for our
    #     scanning.  The PriceBar being evaluated would be the one in
    #     the middle of this list.  The size of the list would thus be
    #     determined with the formula: (2 * param) + 1.
    #
    
    #scanVars.append(\
    #    { "tag" : "H",
    #      "priceRangeRequired" : 0.05,
    #      "param" : 5,
    #      "window" : [] })
    scanVars.append(\
        { "tag" : "HH",
          "priceRangeRequired" : 0.10,
          "param" : 20,
          "window" : [] })
    scanVars.append(\
        { "tag" : "HHH",
          "priceRangeRequired" : 0.20,
          "param" : 30,
          "window" : [] })
    scanVars.append(\
        { "tag" : "HHHH",
          "priceRangeRequired" : 0.25,
          "param" : 80,
          "window" : [] })
    scanVars.append(\
        { "tag" : "HHHHH",
          "priceRangeRequired" : 0.30,
          "param" : 160,
          "window" : [] })

    #scanVars.append(\
    #    { "tag" : "L",
    #      "priceRangeRequired" : 0.05,
    #      "param" : 5,
    #      "window" : [] })
    scanVars.append(\
        { "tag" : "LL",
          "priceRangeRequired" : 0.10,
          "param" : 20,
          "window" : [] })
    scanVars.append(\
        { "tag" : "LLL",
          "priceRangeRequired" : 0.20,
          "param" : 30,
          "window" : [] })
    scanVars.append(\
        { "tag" : "LLLL",
          "priceRangeRequired" : 0.25,
          "param" : 80,
          "window" : [] })
    scanVars.append(\
        { "tag" : "LLLLL",
          "priceRangeRequired" : 0.30,
          "param" : 160,
          "window" : [] })

    scanVarsStr = "["
    for i in range(len(scanVars)):
        scanVar = scanVars[i]
        scanVarsStr += "{}".format(scanVar)
        if i != len(scanVars) - 1:
            scanVarsStr += ","
    scanVarsStr += "]"
    
    # Get the timestamp of the earliest PriceBar.
    earliestPriceBarTimestamp = None
    latestPriceBarTimestamp = None
    if len(swingFileData.priceBars) > 0:
        earliestPriceBarTimestamp = swingFileData.priceBars[0].timestamp
        latestPriceBarTimestamp = swingFileData.priceBars[-1].timestamp
    
    # Before we start doing any tagging ourselves, count how many
    # PriceBars are already tagged.
    count = 0
    originalPriceBarsLen = len(swingFileData.priceBars)
    for pb in swingFileData.priceBars:
        if len(pb.tags) > 0:
            count += 1
        pb.clearTags()
    if count > 0:
        log.warn("Before scanning, tagging, and extracting, " + \
                 "{} out of {} PriceBars already have tags.".\
                 format(count, originalPriceBarsLen))

    log.info("Scanning for highs and lows among {} PriceBars ...".\
             format(originalPriceBarsLen))

    # Start scanning.
    for pb in swingFileData.priceBars:
        log.debug("=============================================")
        log.debug("Looking at PriceBar with timestamp: {}".\
                  format(Ephemeris.datetimeToStr(pb.timestamp)))
        
        for scanVar in scanVars:
            log.debug("---------------------------------------------")
            log.debug("Currently looking at tag: {}".format(scanVar['tag']))
            
            # Calculate the required window size for this scanVar.
            requiredWindowSize = (scanVar['param'] * 2) + 1
            log.debug("requiredWindowSize == {}".format(requiredWindowSize))

            # Alias for the window.
            window = scanVar['window']
            log.debug("Before appending PriceBar, len(window) == {}".\
                      format(len(window)))

            # Index into the window, pointing to the PriceBar
            # currently being inspected for a high or low.
            currIndex = scanVar['param']
            log.debug("currIndex == {}".format(currIndex))

            # Append the PriceBar.
            window.append(pb)
            
            # If the size of the window is now larger than the
            # required size, then shrink it appropriately.
            while len(window) > requiredWindowSize:
                # Drop the PriceBar at the beginning of the window.
                window = window[1:]
            
            # If the size of the window is not large enough, don't do
            # any checks for a high or low yet.
            if len(window) < requiredWindowSize:
                continue
            
            log.debug("For scanVar['tag'] == '{}', len(window) == {}".\
                      format(scanVar['tag'], len(window)))
                      
            # First make sure the price range of these PriceBars is
            # sufficient for this scanVar.
            highPrice = None
            lowPrice = None
            for i in range(len(window)):
                if highPrice == None:
                    highPrice = window[i].high
                if lowPrice == None:
                    lowPrice = window[i].low

                if highPrice <= window[i].high:
                    highPrice = window[i].high
                if lowPrice >= window[i].low:
                    lowPrice = window[i].low
            priceRange = highPrice - lowPrice
            percentage = priceRange / highPrice
            
            if percentage < scanVar['priceRangeRequired']:
                log.debug("Price range not wide enough to warrant " + \
                          "investigating this PriceBar.  " + \
                          "tag=\'{}\', percentage={}, percentageRequired={}".\
                          format(scanVar['tag'],
                                 percentage,
                                 scanVar['priceRangeRequired']))
                
                # Go on to the next scanVar.
                continue
            else:
                log.debug("Price range is wide enough.")
                #log.debug("Price range is wide enough.  " + \
                #          "tag=\'{}\', percentage={}, percentageRequired={}".\
                #          format(scanVar['tag'],
                #                 percentage,
                #                 scanVar['priceRangeRequired']))
                pass

            if scanVar['tag'].startswith("L"):
                # We are looking for a low.
                currIndexIsTheLowest = True
                for i in range(len(window)):
                    log.debug("i == {}, currIndex == {}".format(i, currIndex))
                    if i != currIndex:
                        log.debug("window[i].low         == {}".\
                                  format(window[i].low))
                        log.debug("window[currIndex].low == {}".\
                                  format(window[currIndex].low))
                        if window[i].low < window[currIndex].low:
                            currIndexIsTheLowest = False
                            log.debug("currIndexIsTheLowest == False")
                            break
                if currIndexIsTheLowest == True:
                    log.debug("Checking the next index after currIndex.")
                    # Check the PriceBar after currIndex, to see if it
                    # has a low equal to this one.  If it does, then
                    # currIndex is not a low for our definition.
                    if currIndex + 1 < len(window):
                        log.debug("window[{}].low == {}".\
                                  format(currIndex, window[currIndex].low))
                        log.debug("window[{}].low == {}".\
                                  format(currIndex+1, window[currIndex+1].low))
                        if window[currIndex+1].low <= window[currIndex].low:
                            currIndexIsTheLowest = False
                            log.debug("currIndexIsTheLowest == False")

                # If the PriceBar is still the lowest, then tag it.
                if currIndexIsTheLowest == True:
                    log.debug("Adding tag '{}' to PriceBar with timestamp: {}".\
                              format(scanVar['tag'],
                                     Ephemeris.datetimeToStr(\
                                         window[currIndex].timestamp)))
                    window[currIndex].addTag(scanVar['tag'])
                    log.debug("PriceBar is now: {}".\
                              format(window[currIndex].toString()))
                
            elif scanVar['tag'].startswith("H"):
                # We are looking for a high.
                currIndexIsTheHighest = True
                for i in range(len(window)):
                    log.debug("i == {}, currIndex == {}".format(i, currIndex))
                    if i != currIndex:
                        log.debug("window[i].high         == {}".\
                                  format(window[i].high))
                        log.debug("window[currIndex].high == {}".\
                                  format(window[currIndex].high))
                        if window[i].high > window[currIndex].high:
                            currIndexIsTheHighest = False
                            log.debug("currIndexIsTheHighest == False")
                            break
                if currIndexIsTheHighest == True:
                    log.debug("Checking the next index after currIndex.")
                    # Check the PriceBar after currIndex, to see if it
                    # has a high equal to this one.  If it does, then
                    # currIndex is not a high for our definition.
                    if currIndex + 1 < len(window):
                        log.debug("window[{}].high == {}".\
                                  format(currIndex, window[currIndex].high))
                        log.debug("window[{}].high == {}".\
                                  format(currIndex+1, window[currIndex+1].high))
                        if window[currIndex+1].high >= window[currIndex].high:
                            currIndexIsTheHighest = False
                            log.debug("currIndexIsTheHighest == False")

                # If the PriceBar is still the highest, then tag it.
                if currIndexIsTheHighest == True:
                    log.debug("Adding tag '{}' to PriceBar with timestamp: {}".\
                              format(scanVar['tag'],
                                     Ephemeris.datetimeToStr(\
                                         window[currIndex].timestamp)))
                    window[currIndex].addTag(scanVar['tag'])
                    log.debug("PriceBar is now: {}".\
                              format(window[currIndex].toString()))
                
            else:
                log.warn("Warning, this tag name is not supported.")

    log.debug("Done scanning.")
    
    # Grab all the PriceBars with tags.
    taggedPriceBars = []
    for pb in swingFileData.priceBars:
        if len(pb.tags) > 0:
            debugStr = "PriceBar at {} has the following tags: ".\
                       format(Ephemeris.datetimeToStr(pb.timestamp))
            debugStr += "["
            for i in range(len(pb.tags)):
                debugStr += pb.tags[i]
                if i != len(pb.tags) - 1:
                    debugStr += ","
            debugStr += "]"
            log.debug(debugStr)
            
            taggedPriceBars.append(pb)

    log.info("Scanning for highs and lows complete.")
    log.info("Out of {} PriceBars, {} were extracted and tagged.".\
             format(originalPriceBarsLen, len(taggedPriceBars)))
    
    # Replace swingFileData.priceBars with the list with only tagged PriceBars.
    swingFileData.priceBars = taggedPriceBars

    # Set the string holding the parameters that were used to scan and
    # filter for highs and lows.
    swingFileDescriptionText = \
        "Swing file creation timestamp: {}.  ".\
        format(datetime.datetime.now(tz=pytz.utc)) + \
        "The swings included in this file were extracted from " + \
        "{} PriceBars, starting from {} to {}.  ".\
        format(originalPriceBarsLen,
               Ephemeris.datetimeToStr(earliestPriceBarTimestamp),
               Ephemeris.datetimeToStr(latestPriceBarTimestamp)) + \
        "Total number of PriceBars tagged as highs and lows " + \
        "in this swing file is: {}.  ".\
        format(len(swingFileData.priceBars)) + \
        "The parameters used in scanning and tagging were: " + \
        scanVarsStr

    swingFileData.swingFileDescription = swingFileDescriptionText

    log.debug("swingFileData.swingFileDescription == {}".\
              format(swingFileData.swingFileDescription))
              
    log.debug("Done processing swing file data.")
Ejemplo n.º 12
0
def getEphemerisDataLineForDatetime(dt):
    """Obtains the line of CSV text of planetary position data.

    Arguments:
    dt - datetime.datetime object with the timestamp seeked.  
    
    Returns:
    
    str in CSV format. Since there are a lot of fields, please See the
    section of code where we write the header info str for the format.
    """

    # Return value.
    rv = ""

    planetaryInfos = getPlanetaryInfosForDatetime(dt)

    log.debug("Just obtained planetaryInfos for timestamp: {}".\
              format(Ephemeris.datetimeToStr(dt)))
    
    # Planet geocentric longitude 15-degree axis points.
    for planetName in geocentricPlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                lon = pi.geocentric['tropical']['longitude']
                rv += "{:.3f},".format(lon % 15.0)
                    
    # Planet geocentric longitude.
    for planetName in geocentricPlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                lon = pi.geocentric['tropical']['longitude']
                rv += "{:.3f},".format(lon)
                    
    # Planet geocentric longitude in zodiac str format.
    for planetName in geocentricPlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                lon = pi.geocentric['tropical']['longitude']
                valueStr = \
                         AstrologyUtils.\
                         convertLongitudeToStrWithRasiAbbrev(lon)
                rv += valueStr + ","
                
    # Planet heliocentric longitude 15-degree axis points.
    for planetName in heliocentricPlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                lon = pi.heliocentric['tropical']['longitude']
                rv += "{:.3f},".format(lon % 15.0)
                    
    # Planet heliocentric longitude.
    for planetName in heliocentricPlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                lon = pi.heliocentric['tropical']['longitude']
                rv += "{:.3f},".format(lon)
                    
    # Planet heliocentric longitude in zodiac str format.
    for planetName in heliocentricPlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                lon = pi.heliocentric['tropical']['longitude']
                valueStr = \
                         AstrologyUtils.\
                         convertLongitudeToStrWithRasiAbbrev(lon)
                rv += valueStr + ","
                
    # Planet declination.
    for planetName in declinationPlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                declination = pi.geocentric['tropical']['declination']
                rv += "{:.3f},".format(declination)
    
    # Planet geocentric latitude.
    for planetName in geocentricLatitudePlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                latitude = pi.geocentric['tropical']['latitude']
                rv += "{:.3f},".format(latitude)
    
    # Planet heliocentric latitude.
    for planetName in heliocentricLatitudePlanetNames:
        for pi in planetaryInfos:
            if pi.name == planetName:
                latitude = pi.heliocentric['tropical']['latitude']
                rv += "{:.3f},".format(latitude)
    
    
    # Remove trailing comma.
    rv = rv[:-1]

    return rv
Ejemplo n.º 13
0
#if __name__ == "__main__":
    
# Initialize Ephemeris (required).
Ephemeris.initialize()

# Set the Location (required).
Ephemeris.setGeographicPosition(locationLongitude,
                                locationLatitude,
                                locationElevation)

# Log the parameters that are being used.
log.info("Location used is: {}  (lat={}, lon={})".\
         format(locationName, locationLatitude, locationLongitude))
log.info("Timezone used is: {}".format(timezone.zone))
log.info("Start timestamp:  {}".format(Ephemeris.datetimeToStr(startDt)))
log.info("End   timestamp:  {}".format(Ephemeris.datetimeToStr(endDt)))

# Compile the header line text.
headerLine = ""
headerLine += "Date" + ","
headerLine += "Day of week" + ","
headerLine += "Day count" + ","
headerLine += "Week count" + ","
headerLine += "Month count" + ","

# Planet geocentric longitude mod 15.
for planetName in geocentricPlanetNames:
    headerLine += "G." + planetName + "%15" + ","

# Planet geocentric longitude.
 
 for planetName in geocentricPlanetNames:
     listOfTuplesForPlanet = results[planetName]
     for tup in listOfTuplesForPlanet:
         planetName = tup[0]
         jd = tup[1]
         dt = tup[2]
         retroOrDirect = tup[3]
         geoTropLongitudeOfPlanet = tup[4]
         geoSidLongitudeOfPlanet = tup[5]
         
         # Assemble the line that will go into the CSV file.
         line = ""
         line += "{}".format(planetName) + ","
         line += "{}".format(jd) + ","
         line += "{}".format(Ephemeris.datetimeToStr(dt)) + ","
         line += "{}".format(retroOrDirect) + ","
         line += "{}".format(geoTropLongitudeOfPlanet) + ","
         line += "{}".format(geoSidLongitudeOfPlanet) + ","
         
         # Remove last trailing comma.
         line = line[:-1]
 
         # Append to the output lines.
         outputLines.append(line)
 
 
 # Write outputLines to output file.
 with open(outputFilename, "w", encoding="utf-8") as f:
     log.info("Writing to output file '{}' ...".format(outputFilename))
 
def getLongitudeDiffBetweenDatetimes(planetName,
                                     centricityType,
                                     dt1,
                                     loc1Tuple,
                                     dt2,
                                     loc2Tuple):

    startTimestamp = dt1
    endTimestamp = dt2

    loc1Name = loc1Tuple[0]
    loc1Longitude = loc1Tuple[1]
    loc1Latitude = loc1Tuple[2]
    loc1Elevation = loc1Tuple[3]

    loc2Name = loc2Tuple[0]
    loc2Longitude = loc2Tuple[1]
    loc2Latitude = loc2Tuple[2]
    loc2Elevation = loc2Tuple[3]


    # maxErrorTd - datetime.timedelta object holding the maximum
    #              time difference between the exact planetary
    #              timestamp for the phenomena, and the one
    #              calculated.  This would define the accuracy of
    #              the calculations.
    #
    maxErrorTd = datetime.timedelta(seconds=4)

    # Size of a circle, in degrees.
    #
    # Here we define our own value instead of using the value in
    # AstrologyUtils.degreesInCircle because it is possible we may
    # want to test different sizes of a 'circle'.
    circleSizeInDegrees = 360.0
        
    # All references to longitude_speed need to
    # be from tropical zodiac measurements!  If I use
    # sidereal zodiac measurements for getting the
    # longitude_speed, then the measurements from the
    # Swiss Ephemeris do not yield the correct values.
    # I use the following variable in these locations.
    zodiacTypeForLongitudeSpeed = "tropical"

    tropicalZodiacFlag = True

    # Text to set in the text item.
    text = ""

    # Total number of degrees elapsed.
    totalDegrees = 0
    
    Ephemeris.setGeographicPosition(loc1Longitude,
                                    loc1Latitude,
                                    loc1Elevation)
    
    # List of PlanetaryInfo objects for this particular
    # planet, sorted by timestamp.
    planetData = []
                
    # Step size to use in populating the data list with
    # PlanetaryInfos.
    #
    # The step size should cause the planet to move less
    # than 120 degrees in all cases, and idealy much less
    # than this, that way we can easily narrow down when
    # the planet passes the 0 degree or 360 degree
    # threshold, and also so it is easier to narrow down
    # when retrograde periods happen.  If the step size is
    # too large, it is possible that we would miss a whole
    # time window of retrograde movement, so discretion
    # has to be used in determining what to use for this value.
    #
    # Here we will set it to 1 day for the default case,
    # but if the planet name is a house cusp then shrink
    # the step size so we will get the correct resolution.
    # Also, if the planet name is an outer planet with a
    # large period, we can increase the step size slightly
    # to improve performance.
    stepSizeTd = datetime.timedelta(days=1)

    
    if Ephemeris.isHouseCuspPlanetName(planetName) or \
           Ephemeris.isAscmcPlanetName(planetName):
                    
        stepSizeTd = datetime.timedelta(hours=1)
                    
    elif planetName == "Jupiter" or \
         planetName == "Saturn" or \
         planetName == "Neptune" or \
         planetName == "Uranus" or \
         planetName == "Pluto":
                    
        stepSizeTd = datetime.timedelta(days=5)
                
        log.debug("Stepping through from {} to {} ...".\
                  format(Ephemeris.datetimeToStr(startTimestamp),
                         Ephemeris.datetimeToStr(endTimestamp)))
                
    # Current datetime as we step through all the
    # timestamps between the start and end timestamp.
    currDt = copy.deepcopy(startTimestamp)
                
    # Step through the timestamps, calculating the planet positions.
    while currDt < endTimestamp:
        p = Ephemeris.getPlanetaryInfo(planetName, currDt)
        planetData.append(p)
                    
        # Increment step size.
        currDt += stepSizeTd
                    
    # We must also append the planet calculation for the end timestamp.
    Ephemeris.setGeographicPosition(loc2Longitude,
                                    loc2Latitude,
                                    loc2Elevation)
    p = Ephemeris.getPlanetaryInfo(planetName, endTimestamp)
    planetData.append(p)
                
    # Geocentric measurement.
    if centricityType == "geocentric":
                    
        # Get the PlanetaryInfos for the timestamps of the
        # planet at the moment right after the
        # longitude_speed polarity changes.
        additionalPlanetaryInfos = []
                    
        prevLongitudeSpeed = None
                    
        for i in range(len(planetData)):
            currLongitudeSpeed = \
                planetData[i].geocentric[zodiacTypeForLongitudeSpeed]['longitude_speed']
                        
            if prevLongitudeSpeed != None and \
               ((prevLongitudeSpeed < 0 and currLongitudeSpeed >= 0) or \
               (prevLongitudeSpeed >= 0 and currLongitudeSpeed < 0)):
                            
                # Polarity changed.
                # Try to narrow down the exact moment in
                # time when this occured.
                t1 = planetData[i-1].dt
                t2 = planetData[i].dt
                currErrorTd = t2 - t1
                            
                while currErrorTd > maxErrorTd:
                    if log.isEnabledFor(logging.DEBUG) == True:
                        log.debug("Refining between {} and {}".\
                                       format(Ephemeris.datetimeToStr(t1),
                                              Ephemeris.datetimeToStr(t2)))
                                
                    # Check the timestamp between.
                    diffTd = t2 - t1
                    halfDiffTd = \
                        datetime.\
                        timedelta(days=(diffTd.days / 2.0),
                                  seconds=(diffTd.seconds / 2.0),
                                  microseconds=(diffTd.\
                                                microseconds / 2.0))
                    testDt = t1 + halfDiffTd

                    p = Ephemeris.getPlanetaryInfo(planetName, testDt)
                    testLongitudeSpeed = \
                        p.geocentric[zodiacTypeForLongitudeSpeed]['longitude_speed']

                    if ((prevLongitudeSpeed < 0 and \
                         testLongitudeSpeed >= 0) or \
                        (prevLongitudeSpeed >= 0 and \
                         testLongitudeSpeed < 0)):

                        # Polarity change at the test timestamp.
                        t2 = testDt

                    else:
                        # No polarity change yet.
                        t1 = testDt

                    # Update the currErrorTd.
                    currErrorTd = t2 - t1
                            
                log.debug("Broke out of loop to find " + \
                               "velocity polarity change.  " + \
                               "currErrorTd is: {}, ".\
                               format(currErrorTd))
                                           
                # Timestamp at t2 is now within the amount
                # of the time error threshold ('maxErrorTd')
                # following the polarity change.
                # Append this value to the list.
                p = Ephemeris.getPlanetaryInfo(planetName, t2)
                additionalPlanetaryInfos.append(p)

                t1pi = planetData[i-1]
                t2pi = Ephemeris.getPlanetaryInfo(planetName, t2)
                            
                if log.isEnabledFor(logging.DEBUG) == True:
                    log.debug("t1 == {}, ".\
                               format(Ephemeris.datetimeToStr(t1pi.dt)) + \
                               "longitude(tropical) == {}, ".\
                               format(t1pi.geocentric['tropical']['longitude']) + \
                               "longitude(sidereal) == {}, ".\
                               format(t1pi.geocentric['sidereal']['longitude']) + \
                               "longitude_speed == {}, ".\
                               format(t1pi.geocentric[zodiacTypeForLongitudeSpeed]['longitude_speed']))
                                
                    log.debug("t2 == {}, ".\
                               format(Ephemeris.datetimeToStr(t2pi.dt)) + \
                               "longitude(tropical) == {}, ".\
                               format(t2pi.geocentric['tropical']['longitude']) + \
                               "longitude(sidereal) == {}, ".\
                               format(t2pi.geocentric['sidereal']['longitude']) + \
                               "longitude_speed == {}, ".\
                               format(t2pi.geocentric[zodiacTypeForLongitudeSpeed]['longitude_speed']))
                            
                # There is no need to update
                # currLongitudeSpeed here, because the
                # longitude_speed for 'p' should be the
                # same polarity.
                            
            # Update prevLongitudeSpeed.
            prevLongitudeSpeed = currLongitudeSpeed
                        
        # Sort all the extra PlanetaryInfo objects by timestamp.
        additionalPlanetaryInfos = \
            sorted(additionalPlanetaryInfos, key=lambda c: c.dt)
                    
        # Insert PlanetaryInfos from
        # 'additionalPlanetaryInfos' into 'planetData' at
        # the timestamp-ordered location.
        currLoc = 0
        for i in range(len(additionalPlanetaryInfos)):
            pi = additionalPlanetaryInfos[i]

            insertedFlag = False
                        
            while currLoc < len(planetData):
                if pi.dt < planetData[currLoc].dt:
                    planetData.insert(currLoc, pi)
                    insertedFlag = True
                    currLoc += 1
                    break
                else:
                    currLoc += 1
                        
            if insertedFlag == False:
                # PlanetaryInfo 'pi' has a timestamp that
                # is later than the last PlanetaryInfo in
                # 'planetData', so just append it.
                planetData.append(pi)

                # Increment currLoc so that the rest of
                # the PlanetaryInfos in
                # 'additionalPlanetaryInfos' can be
                # appended without doing anymore timestamp tests.
                currLoc += 1

        # Do summations to determine the measurements.
        showGeocentricRetroAsNegativeTextFlag = True
        if showGeocentricRetroAsNegativeTextFlag == True:
            if tropicalZodiacFlag == True:
                totalDegrees = 0
                zodiacType = "tropical"
                            
                for i in range(len(planetData)):
                    if i != 0:
                        prevPi = planetData[i-1]
                        currPi = planetData[i]

                        if prevPi.geocentric[zodiacTypeForLongitudeSpeed]['longitude_speed'] >= 0:
                            # Direct motion.
                            # Elapsed amount for this segment should be positive.
                                        
                            # Find the amount of longitude elasped.
                            longitudeElapsed = \
                                currPi.geocentric[zodiacType]['longitude'] - \
                                prevPi.geocentric[zodiacType]['longitude']
                                        
                            # See if there was a crossing of the
                            # 0 degree point or the 360 degree point.
                            # If so, make the necessary adjustments
                            # so that the longitude elapsed is
                            # correct.
                            longitudeElapsed = \
                                Util.toNormalizedAngle(longitudeElapsed)

                            totalDegrees += longitudeElapsed
                        else:
                            # Retrograde motion.
                            # Elapsed amount for this segment should be negative.
                            
                            # Find the amount of longitude elasped.
                            longitudeElapsed = \
                                currPi.geocentric[zodiacType]['longitude'] - \
                                prevPi.geocentric[zodiacType]['longitude']

                            # See if there was a crossing of the
                            # 0 degree point or the 360 degree point.
                            # If so, make the necessary adjustments
                            # so that the longitude elapsed is
                            # correct.
                            if longitudeElapsed > 0:
                                longitudeElapsed -= 360

                            totalDegrees += longitudeElapsed
                                    
                # Line of text.  We append measurements to
                # this line of text depending on what
                # measurements are enabled.
                line = "G T {} moves ".format(planetName)

                numCircles = totalDegrees / circleSizeInDegrees
                numBiblicalCircles = \
                    totalDegrees / AstrologyUtils.degreesInBiblicalCircle
                            
                line += "{:.2f} deg ".format(totalDegrees)

                # Append last part of the line.
                line += "(r as -)"
                            
                text += line + os.linesep
                            
    if centricityType == "heliocentric":

        if tropicalZodiacFlag == True:
            totalDegrees = 0
            zodiacType = "tropical"
                        
            for i in range(len(planetData)):
                if i != 0:
                    prevPi = planetData[i-1]
                    currPi = planetData[i]
                                
                    if prevPi.heliocentric[zodiacTypeForLongitudeSpeed]['longitude_speed'] >= 0:
                        # Direct motion.
                        # Elapsed amount for this segment should be positive.
                                    
                        # Find the amount of longitude elasped.
                        longitudeElapsed = \
                            currPi.heliocentric[zodiacType]['longitude'] - \
                            prevPi.heliocentric[zodiacType]['longitude']
                                    
                        # See if there was a crossing of the
                        # 0 degree point or the 360 degree point.
                        # If so, make the necessary adjustments
                        # so that the longitude elapsed is
                        # correct.
                        longitudeElapsed = \
                            Util.toNormalizedAngle(longitudeElapsed)
                                    
                        totalDegrees += longitudeElapsed
                    else:
                        # Retrograde motion.
                        # Elapsed amount for this segment should be negative.
                                    
                        # Find the amount of longitude elasped.
                        longitudeElapsed = \
                            currPi.heliocentric[zodiacType]['longitude'] - \
                            prevPi.heliocentric[zodiacType]['longitude']
                                    
                        # See if there was a crossing of the
                        # 0 degree point or the 360 degree point.
                        # If so, make the necessary adjustments
                        # so that the longitude elapsed is
                        # correct.
                        if longitudeElapsed > 0:
                            longitudeElapsed -= 360
                                        
                        totalDegrees += longitudeElapsed
                                    
            # Line of text.  We append measurements to
            # this line of text depending on what
            # measurements are enabled.
            line = "H T {} moves ".format(planetName)
                            
            numCircles = totalDegrees / circleSizeInDegrees
            numBiblicalCircles = \
                totalDegrees / AstrologyUtils.degreesInBiblicalCircle
            
            line += "{:.2f} deg ".format(totalDegrees)

            text += line + os.linesep
                        
    text = text.rstrip()
    
    return totalDegrees
def getGeocentricPlanetDirectRetrogradeInfo(planetName):
    """
    Returns a list of tuples, each tuple containing:
    (planetName,
    julianDay,
    datetime,
    "R" or "D",
    geoTropLongitudeOfPlanet,
    geoSidLongitudeOfPlanet)
    """

    # Return value.
    rv = []

    prevDt = None
    currDt = copy.deepcopy(startDt)

    prevTropLongitudeSpeed = None
    currTropLongitudeSpeed = None

    currTropLongitude = None
    currSidLongitude = None
    
    while currDt <= endDt:
        dt = currDt

        pi = Ephemeris.getPlanetaryInfo(planetName, dt)
        
        log.debug("Just obtained planetaryInfo for planet '{}', timestamp: {}".\
                  format(planetName, Ephemeris.datetimeToStr(dt)))

        # Get the geocentric longitude and geocentric longitude speed.
        tropLongitudeSpeed = pi.geocentric['tropical']['longitude_speed']
        tropLongitude = pi.geocentric['tropical']['longitude']
        sidLongitude = pi.geocentric['sidereal']['longitude']

        # Store new current planet values.
        currTropLongitudeSpeed = tropLongitudeSpeed
        currTropLongitude = tropLongitude
        currSidLongitude = sidLongitude

        log.debug("prevTropLongitudeSpeed={}, currTropLongitudeSpeed={}".\
                  format(prevTropLongitudeSpeed, currTropLongitudeSpeed))
        
        # We need two data points to proceed.
        if prevTropLongitudeSpeed != None and \
               currTropLongitudeSpeed != None and \
               prevDt != None:
            
            # Check to see if we passed over 0 degrees.
            
            if prevTropLongitudeSpeed < 0.0 and currTropLongitudeSpeed >= 0.0:
                # Crossed over from negative to positive!
                log.debug("Crossed over from negative to positive!")

                # This is the upper-bound of the error timedelta.
                t1 = prevDt
                t2 = currDt
                currErrorTd = t2 - t1
                
                # Refine the timestamp until it is less than the
                # desired threshold.
                while currErrorTd > maxErrorTd:
                    log.debug("Refining between {} and {}".\
                              format(Ephemeris.datetimeToStr(t1),
                                     Ephemeris.datetimeToStr(t2)))
                    
                    # Check the timestamp between.
                    diffTd = t2 - t1
                    halfDiffTd = \
                        datetime.\
                        timedelta(days=(diffTd.days / 2.0),
                                  seconds=(diffTd.seconds / 2.0),
                                  microseconds=(diffTd.microseconds / 2.0))
                    testDt = t1 + halfDiffTd
                    
                    pi = Ephemeris.getPlanetaryInfo(planetName, testDt)
                    
                    testTropLongitudeSpeed = \
                        pi.geocentric['tropical']['longitude_speed']
                    testTropLongitude = pi.geocentric['tropical']['longitude']
                    testSidLongitude = pi.geocentric['sidereal']['longitude']

                    if testTropLongitudeSpeed >= 0.0:
                        t2 = testDt
                        
                        # Update the curr values as the later boundary.
                        currDt = t2
                        currTropLongitudeSpeed = testTropLongitudeSpeed
                        currTropLongitude = testTropLongitude
                        currSidLongitude = testSidLongitude
                    else:
                        t1 = testDt

                    currErrorTd = t2 - t1
                        
                # Broke out of while loop, meaning we have a timestamp
                # within our threshold.
                # Create a tuple to add to our list.
                tup = (planetName,
                       Ephemeris.datetimeToJulianDay(currDt),
                       currDt,
                       directStr,
                       currTropLongitude,
                       currSidLongitude)

                # Append to the list.
                rv.append(tup)
                
            elif prevTropLongitudeSpeed > 0.0 and currTropLongitudeSpeed <= 0.0:
                # Crossed over from positive to negative!
                log.debug("Crossed over from positive to negative!")
                
                # This is the upper-bound of the error timedelta.
                t1 = prevDt
                t2 = currDt
                currErrorTd = t2 - t1
                
                # Refine the timestamp until it is less than the
                # desired threshold.
                while currErrorTd > maxErrorTd:
                    log.debug("Refining between {} and {}".\
                              format(Ephemeris.datetimeToStr(t1),
                                     Ephemeris.datetimeToStr(t2)))
                    
                    # Check the timestamp between.
                    diffTd = t2 - t1
                    halfDiffTd = \
                        datetime.\
                        timedelta(days=(diffTd.days / 2.0),
                                  seconds=(diffTd.seconds / 2.0),
                                  microseconds=(diffTd.microseconds / 2.0))
                    testDt = t1 + halfDiffTd
                    
                    pi = Ephemeris.getPlanetaryInfo(planetName, testDt)
                    
                    testTropLongitudeSpeed = \
                        pi.geocentric['tropical']['longitude_speed']
                    testTropLongitude = pi.geocentric['tropical']['longitude']
                    testSidLongitude = pi.geocentric['sidereal']['longitude']

                    if testTropLongitudeSpeed <= 0.0:
                        t2 = testDt
                        
                        # Update the curr values as the later boundary.
                        currDt = t2
                        currTropLongitudeSpeed = testTropLongitudeSpeed
                        currTropLongitude = testTropLongitude
                        currSidLongitude = testSidLongitude
                    else:
                        t1 = testDt

                    currErrorTd = t2 - t1
                    
                # Broke out of while loop, meaning we have a timestamp
                # within our threshold.
                # Create a tuple to add to our list.
                tup = (planetName,
                       Ephemeris.datetimeToJulianDay(currDt),
                       currDt,
                       retrogradeStr,
                       currTropLongitude,
                       currSidLongitude)

                # Append to the list.
                rv.append(tup)
                
            
        # Increment currDt timestamp.
        prevDt = currDt
        currDt = currDt + stepSizeTd
        
        # Move the previous currTropLongitudeSpeed to prevTropLongitudeSpeed.
        prevTropLongitudeSpeed = currTropLongitudeSpeed
        currTropLongitudeSpeed = None
        currTropLongitude = None
        currSidLongitude = None

        log.debug("prevTropLongitudeSpeed={}, currTropLongitudeSpeed={}".\
                  format(prevTropLongitudeSpeed, currTropLongitudeSpeed))
        
    return rv
            # node of this planet.
            helioSidLongitudeOfNorthNode = None
            if node == northStr:
                helioSidLongitudeOfNorthNode = helioSidLongitudeOfNode
            elif node == southStr:
                helioSidLongitudeOfNorthNode = \
                    Util.toNormalizedAngle(helioSidLongitudeOfNode + 180)
            else:
                log.error("Unknown node type.")
                shutdown(1)

            # Assemble the line that will go into the CSV file.
            line = ""
            line += "{}".format(planetName) + ","
            line += "{}".format(jd) + ","
            line += "{}".format(Ephemeris.datetimeToStr(dt)) + ","
            line += "{}".format(node) + ","
            line += "{}".format(helioTropLongitudeOfNode) + ","
            line += "{}".format(helioSidLongitudeOfNode) + ","
            line += "{}".format(helioTropLongitudeOfNorthNode) + ","
            line += "{}".format(helioSidLongitudeOfNorthNode) + ","

            # Remove last trailing comma.
            line = line[:-1]

            # Append to the output lines.
            outputLines.append(line)

    # Write outputLines to output file.
    with open(outputFilename, "w", encoding="utf-8") as f:
        log.info("Writing to output file '{}' ...".format(outputFilename))