Source code for test_cssWlsEstUnitTest

''' '''
'''
 ISC License

 Copyright (c) 2016, Autonomous Vehicle Systems Lab, University of Colorado at Boulder

 Permission to use, copy, modify, and/or distribute this software for any
 purpose with or without fee is hereby granted, provided that the above
 copyright notice and this permission notice appear in all copies.

 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

'''
#
#   Unit Test Script
#   Module Name:        cssWlsEst()
#   Author:             Hanspeter Schaub
#   Creation Date:      April 29, 2018
#

import pytest
import sys, os, inspect
# import packages as needed e.g. 'numpy', 'ctypes, 'math' etc.
import numpy
import math
import logging

# Import all of the modules that we are going to be called in this simulation
from Basilisk.utilities import SimulationBaseClass
from Basilisk.utilities import unitTestSupport                  # general support file with common unit test functions
import matplotlib.pyplot as plt
from Basilisk.utilities import macros
from Basilisk.fswAlgorithms.cssWlsEst import cssWlsEst
from Basilisk.fswAlgorithms.fswMessages import fswMessages
from Basilisk.simulation.simFswInterfaceMessages import simFswInterfaceMessages

filename = inspect.getframeinfo(inspect.currentframe()).filename
path = os.path.dirname(os.path.abspath(filename))





# Function that takes a sun pointing vector and array of CSS normal vectors and
# returns the measurements associated with those normal vectors.
def createCosList(sunPointVec, sensorPointList):
    outList = []
    for sensorPoint in sensorPointList:
        outList.append(numpy.dot(sunPointVec, sensorPoint))
        if(outList[-1] < 0.0):
            outList[-1] = 0.0
    return outList


# Method that checks that all of the numActive outputs from the data array
# that are greater than threshold thresh are consistent with the values in
# measVec
def checkNumActiveAccuracy(measVec, numActiveUse, numActiveFailCriteria, thresh):
    numActivePred = 0
    testFailCount = 0
    # Iterate through measVec and find all valid signals
    for i in range(0, 32):
        obsVal = measVec.CosValue[i]
        if (obsVal > thresh):
            numActivePred += 1

    # Iterate through the numActive array and sum up all numActive estimates
    numActiveTotal = numpy.array([0.])
    j = 0
    while j < numActiveUse.shape[0]:
        numActiveTotal += numActiveUse[j, 1:]
        j += 1
    numActiveTotal /= j  # Mean number of numActive
    # If we violate the test criteria, increment the failure count and alert user
    if (abs(numActiveTotal[0] - numActivePred) > numActiveFailCriteria):
        testFailCount += 1
        errorString = "Active number failure for count of: "
        errorString += str(numActivePred)
        logging.error(errorString)
    return testFailCount


# This method takes the sHat estimate output by the estimator and compares that
# against the actual sun vector passed in as an argument.  If it doesn't match
# to the specified tolerance, increment failure counter and alert the user
def checksHatAccuracy(testVec, sHatEstUse, angleFailCriteria, TotalSim):
    j = 0
    testFailCount = 0
    sHatTotal = numpy.array([0.0, 0.0, 0.0])
    # Sum up all of the sHat estimates from the execution
    while j < sHatEstUse.shape[0]:
        sHatTotal += sHatEstUse[j, 1:]
        j += 1
    sHatTotal /= j  # mean sHat estimate
    # This logic is to protect cases where the dot product numerically breaks acos
    dot_value = numpy.dot(sHatTotal, testVec)
    if (abs(dot_value > 1.0)):
        dot_value -= 2.0 * (dot_value - math.copysign(1.0, dot_value))

    # If we violate the failure criteria, increment failure count and alert user
    if (abs(math.acos(dot_value)) > angleFailCriteria):
        testFailCount += 1
        errorString = "Angle fail criteria violated for test vector:"
        errorString += str(testVec).strip('[]') + "\n"
        errorString += "Criteria violation of: "
        errorString += str(abs(math.acos(numpy.dot(sHatTotal, testVec))))
        logging.error(errorString)
    return testFailCount


# This method takes the sHat estimate output by the estimator and compares that
# against the actual sun vector passed in as an argument.  If it doesn't match
# to the specified tolerance, increment failure counter and alert the user
def checkResidAccuracy(testVec, sResids, sThresh, TotalSim):
    j = 0
    testFailCount = 0
    # Sum up all of the sHat estimates from the execution
    while j < sResids.shape[0]:
        sNormObs = numpy.linalg.norm(sResids[j,1:])
        if(sNormObs > sThresh):
            testFailCount += 1
            errorString = "Residual error computation failure:"
            errorString += str(testVec).strip('[]') + "\n"
            errorString += "Criteria violation of: "
            errorString += str(sNormObs)
            logging.error(errorString)
        j += 1
    return testFailCount


# uncomment this line is this test is to be skipped in the global unit test run, adjust message as needed
# @pytest.mark.skipif(conditionstring)
# uncomment this line if this test has an expected failure, adjust message as needed
# @pytest.mark.xfail(conditionstring)

[docs]@pytest.mark.parametrize("testSunHeading, testRate", [ ("True", "False") ,("False", "True") ]) # provide a unique test method name, starting with test_ def test_module(show_plots, testSunHeading, testRate): # update "module" in this function name to reflect the module name """Module Unit Test""" # each test method requires a single assert method to be called # pass on the testPlotFixture so that the main test function may set the DataStore attributes if testSunHeading: [testResults, testMessage] = cssWlsEstTestFunction(show_plots) assert testResults < 1, testMessage if testRate: [testResults, testMessage] = cssRateTestFunction(show_plots) assert testResults < 1, testMessage
def cssWlsEstTestFunction(show_plots): testFailCount = 0 # zero unit test result counter testMessages = [] # create empty array to store test log messages unitTaskName = "unitTask" # arbitrary name (don't change) unitProcessName = "TestProcess" # arbitrary name (don't change) # Create a sim module as an empty container unitTestSim = SimulationBaseClass.SimBaseClass() # Create test thread testProc = unitTestSim.CreateNewProcess(unitProcessName) testProc.addTask(unitTestSim.CreateNewTask(unitTaskName, int(1E8))) # Construct algorithm and associated C++ container CSSWlsEstFSWConfig = cssWlsEst.CSSWLSConfig() CSSWlsWrap = unitTestSim.setModelDataWrap(CSSWlsEstFSWConfig) CSSWlsWrap.ModelTag = "CSSWlsEst" # Add module to runtime call list unitTestSim.AddModelToTask(unitTaskName, CSSWlsWrap, CSSWlsEstFSWConfig) # Initialize the WLS estimator configuration data CSSWlsEstFSWConfig.cssDataInMsgName = "css_data_aggregate" CSSWlsEstFSWConfig.cssConfigInMsgName = "css_config_data" CSSWlsEstFSWConfig.navStateOutMsgName = "css_nav_sunHeading" CSSWlsEstFSWConfig.cssWLSFiltResOutMsgName = "css_est_pfr" CSSWlsEstFSWConfig.useWeights = False CSSWlsEstFSWConfig.sensorUseThresh = 0.15 CSSOrientationList = [ [0.70710678118654746, -0.5, 0.5], [0.70710678118654746, -0.5, -0.5], [0.70710678118654746, 0.5, -0.5], [0.70710678118654746, 0.5, 0.5], [-0.70710678118654746, 0, 0.70710678118654757], [-0.70710678118654746, 0.70710678118654757, 0.0], [-0.70710678118654746, 0, -0.70710678118654757], [-0.70710678118654746, -0.70710678118654757, 0.0], ] numCSS = len(CSSOrientationList) # set the CSS unit vectors cssConfigData = fswMessages.CSSConfigFswMsg() totalCSSList = [] for CSSHat in CSSOrientationList: CSSConfigElement = fswMessages.CSSUnitConfigFswMsg() CSSConfigElement.CBias = 1.0 CSSConfigElement.nHat_B = CSSHat totalCSSList.append(CSSConfigElement) cssConfigData.nCSS = numCSS cssConfigData.cssVals = totalCSSList unitTestSupport.setMessage(unitTestSim.TotalSim, unitProcessName, CSSWlsEstFSWConfig.cssConfigInMsgName, cssConfigData) # Initialize input message cssDataMsg = simFswInterfaceMessages.CSSArraySensorIntMsg() unitTestSupport.setMessage(unitTestSim.TotalSim, unitProcessName, CSSWlsEstFSWConfig.cssDataInMsgName, cssDataMsg) angleFailCriteria = 17.5 * math.pi / 180.0 # Get 95% effective charging in this case numActiveFailCriteria = 0.000001 # basically zero residFailCriteria = 1.0E-12 #Essentially numerically "small" # Log the output message as well as the internal numACtiveCss variables unitTestSim.TotalSim.logThisMessage(CSSWlsEstFSWConfig.navStateOutMsgName, int(1E8)) unitTestSim.TotalSim.logThisMessage(CSSWlsEstFSWConfig.cssWLSFiltResOutMsgName, int(1E8)) unitTestSim.AddVariableForLogging("CSSWlsEst.numActiveCss", int(1E8)) # Initial test is all of the principal body axes TestVectors = [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [0.0, 0.0, 1.0]] # Initialize test and then step through all of the test vectors in a loop unitTestSim.InitializeSimulation() CSSWlsWrap.Reset(0) # this module reset function needs a time input (in NanoSeconds) stepCount = 0 logLengthPrev = 0 truthData = [] for testVec in TestVectors: if (stepCount > 1): # Doing this to test permutations and get code coverage CSSWlsEstFSWConfig.useWeights = True # Get observation data based on sun pointing and CSS orientation data cssDataMsg.CosValue = createCosList(testVec, CSSOrientationList) # Write in the observation data to the input message unitTestSim.TotalSim.WriteMessageData(CSSWlsEstFSWConfig.cssDataInMsgName, cssDataMsg.getStructSize(), 0, cssDataMsg) # Increment the stop time to new termination value unitTestSim.ConfigureStopTime(int((stepCount + 1) * 1E9)) # Execute simulation to current stop time unitTestSim.ExecuteSimulation() stepCount += 1 # Pull logged data out into workspace for analysis sHatEst = unitTestSim.pullMessageLogData(CSSWlsEstFSWConfig.navStateOutMsgName + '.vehSunPntBdy', list(range(3))) numActive = unitTestSim.GetLogVariableData("CSSWlsEst.numActiveCss") sHatEstUse = sHatEst[logLengthPrev:, :] # Only data for this subtest numActiveUse = numActive[logLengthPrev + 1:, :] # Only data for this subtest # Check failure criteria and add test failures testFailCount += checksHatAccuracy(testVec, sHatEstUse, angleFailCriteria, unitTestSim) testFailCount += checkNumActiveAccuracy(cssDataMsg, numActiveUse, numActiveFailCriteria, CSSWlsEstFSWConfig.sensorUseThresh) filtRes = unitTestSim.pullMessageLogData(CSSWlsEstFSWConfig.cssWLSFiltResOutMsgName + '.postFitRes', list(range(8))) testFailCount += checkResidAccuracy(testVec, filtRes, residFailCriteria, unitTestSim) # Pop truth state onto end of array for plotting purposes currentRow = [sHatEstUse[0, 0]] currentRow.extend(testVec) truthData.append(currentRow) currentRow = [sHatEstUse[-1, 0]] currentRow.extend(testVec) truthData.append(currentRow) logLengthPrev = sHatEst.shape[0] # Hand construct case where we get low coverage (2 valid sensors) LonVal = 0.0 LatVal = 40.68 * math.pi / 180.0 doubleTestVec = [math.sin(LatVal), math.cos(LatVal) * math.sin(LonVal), math.cos(LatVal) * math.cos(LonVal)] cssDataMsg.CosValue = createCosList(doubleTestVec, CSSOrientationList) # Write in double coverage conditions and ensure that we get correct outputs unitTestSim.TotalSim.WriteMessageData(CSSWlsEstFSWConfig.cssDataInMsgName, cssDataMsg.getStructSize(), 0, cssDataMsg) unitTestSim.ConfigureStopTime(int((stepCount + 1) * 1E9)) unitTestSim.ExecuteSimulation() stepCount += 1 sHatEst = unitTestSim.pullMessageLogData(CSSWlsEstFSWConfig.navStateOutMsgName + '.vehSunPntBdy', list(range(3))) numActive = unitTestSim.GetLogVariableData("CSSWlsEst.numActiveCss") sHatEstUse = sHatEst[logLengthPrev:, :] numActiveUse = numActive[logLengthPrev + 1:, :] logLengthPrev = sHatEst.shape[0] currentRow = [sHatEstUse[0, 0]] currentRow.extend(doubleTestVec) truthData.append(currentRow) currentRow = [sHatEstUse[-1, 0]] currentRow.extend(doubleTestVec) truthData.append(currentRow) # Check test criteria again testFailCount += checksHatAccuracy(doubleTestVec, sHatEstUse, angleFailCriteria, unitTestSim) testFailCount += checkNumActiveAccuracy(cssDataMsg, numActiveUse, numActiveFailCriteria, CSSWlsEstFSWConfig.sensorUseThresh) # Same test as above, but zero first element to get to a single coverage case cssDataMsg.CosValue[0] = 0.0 unitTestSim.TotalSim.WriteMessageData(CSSWlsEstFSWConfig.cssDataInMsgName, cssDataMsg.getStructSize(), 0, cssDataMsg) unitTestSim.ConfigureStopTime(int((stepCount + 1) * 1E9)) unitTestSim.ExecuteSimulation() stepCount += 1 numActive = unitTestSim.GetLogVariableData("CSSWlsEst.numActiveCss") numActiveUse = numActive[logLengthPrev + 1:, :] sHatEst = unitTestSim.pullMessageLogData(CSSWlsEstFSWConfig.navStateOutMsgName + '.vehSunPntBdy', list(range(3))) sHatEstUse = sHatEst[logLengthPrev + 1:, :] logLengthPrev = sHatEst.shape[0] testFailCount += checkNumActiveAccuracy(cssDataMsg, numActiveUse, numActiveFailCriteria, CSSWlsEstFSWConfig.sensorUseThresh) currentRow = [sHatEstUse[0, 0]] currentRow.extend(doubleTestVec) truthData.append(currentRow) currentRow = [sHatEstUse[-1, 0]] currentRow.extend(doubleTestVec) truthData.append(currentRow) # Same test as above, but zero first and fourth elements to get to zero coverage cssDataMsg.CosValue[0] = 0.0 cssDataMsg.CosValue[3] = 0.0 unitTestSim.TotalSim.WriteMessageData(CSSWlsEstFSWConfig.cssDataInMsgName, cssDataMsg.getStructSize(), 0, cssDataMsg) unitTestSim.ConfigureStopTime(int((stepCount + 1) * 1E9)) unitTestSim.ExecuteSimulation() numActive = unitTestSim.GetLogVariableData("CSSWlsEst.numActiveCss") numActiveUse = numActive[logLengthPrev:, :] logLengthPrev = numActive.shape[0] testFailCount += checkNumActiveAccuracy(cssDataMsg, numActiveUse, numActiveFailCriteria, CSSWlsEstFSWConfig.sensorUseThresh) # Format data for plotting truthData = numpy.array(truthData) sHatEst = unitTestSim.pullMessageLogData(CSSWlsEstFSWConfig.navStateOutMsgName + '.vehSunPntBdy', list(range(3))) numActive = unitTestSim.GetLogVariableData("CSSWlsEst.numActiveCss") # # test the case where all CSS signals are zero # cssDataMsg.CosValue = numpy.zeros(len(CSSOrientationList)) unitTestSim.TotalSim.WriteMessageData(CSSWlsEstFSWConfig.cssDataInMsgName, cssDataMsg.getStructSize(), 0, cssDataMsg) unitTestSim.ConfigureStopTime(int((stepCount + 2) * 1E9)) unitTestSim.ExecuteSimulation() sHatEstZero = unitTestSim.pullMessageLogData(CSSWlsEstFSWConfig.navStateOutMsgName + '.vehSunPntBdy', list(range(3))) sHatEstZeroUse = sHatEstZero[logLengthPrev + 1:, :] trueVector = [[0.0, 0.0, 0.0]]*len(sHatEstZeroUse) for i in range(0,len(trueVector)): # check a vector values if not unitTestSupport.isArrayEqual(sHatEstZeroUse[i],trueVector[i],3,1e-12): testFailCount += 1 testMessages.append("FAILED: " + CSSWlsWrap.ModelTag + " Module failed " + CSSWlsEstFSWConfig.navStateOutMsgName + " unit test at t=" + str(sHatEstZeroUse[i,0] * macros.NANO2SEC) + "sec\n") if show_plots: plt.figure(1) plt.plot(sHatEst[:, 0] * 1.0E-9, sHatEst[:, 1], label='x-Sun') plt.plot(sHatEst[:, 0] * 1.0E-9, sHatEst[:, 2], label='y-Sun') plt.plot(sHatEst[:, 0] * 1.0E-9, sHatEst[:, 3], label='z-Sun') plt.legend(loc='upper left') plt.xlabel('Time (s)') plt.ylabel('Unit Component (--)') plt.figure(2) plt.plot(numActive[:, 0] * 1.0E-9, numActive[:, 1]) plt.xlabel('Time (s)') plt.ylabel('Number Active CSS (--)') plt.figure(3) plt.subplot(3, 1, 1) plt.plot(sHatEst[:, 0] * 1.0E-9, sHatEst[:, 1], label='Est') plt.plot(truthData[:, 0] * 1.0E-9, truthData[:, 1], 'r--', label='Truth') plt.xlabel('Time (s)') plt.ylabel('X Component (--)') plt.legend(loc='lower right') plt.subplot(3, 1, 2) plt.plot(sHatEst[:, 0] * 1.0E-9, sHatEst[:, 2], label='Est') plt.plot(truthData[:, 0] * 1.0E-9, truthData[:, 2], 'r--', label='Truth') plt.xlabel('Time (s)') plt.ylabel('Y Component (--)') plt.subplot(3, 1, 3) plt.plot(sHatEst[:, 0] * 1.0E-9, sHatEst[:, 3], label='Est') plt.plot(truthData[:, 0] * 1.0E-9, truthData[:, 3], 'r--', label='Truth') plt.xlabel('Time (s)') plt.ylabel('Z Component (--)') plt.show() plt.close('all') # print out success message if no error were found if testFailCount == 0: print("PASSED: " + CSSWlsWrap.ModelTag) # each test method requires a single assert method to be called # this check below just makes sure no sub-test failures were found return [testFailCount, ''.join(testMessages)] def cssRateTestFunction(show_plots): testFailCount = 0 # zero unit test result counter testMessages = [] # create empty array to store test log messages unitTaskName = "unitTask" # arbitrary name (don't change) unitProcessName = "TestProcess" # arbitrary name (don't change) # Create a sim module as an empty container unitTestSim = SimulationBaseClass.SimBaseClass() # Create test thread testProc = unitTestSim.CreateNewProcess(unitProcessName) testProcessRate = macros.sec2nano(0.5) # update process rate update time testProc.addTask(unitTestSim.CreateNewTask(unitTaskName, testProcessRate)) # Construct algorithm and associated C++ container moduleConfig = cssWlsEst.CSSWLSConfig() moduleWrap = unitTestSim.setModelDataWrap(moduleConfig) moduleWrap.ModelTag = "CSSWlsEst" # Add module to runtime call list unitTestSim.AddModelToTask(unitTaskName, moduleWrap, moduleConfig) # Initialize the WLS estimator configuration data moduleConfig.cssDataInMsgName = "css_data_aggregate" moduleConfig.cssConfigInMsgName = "css_config_data" moduleConfig.navStateOutMsgName = "css_nav_sunHeading" moduleConfig.useWeights = False moduleConfig.sensorUseThresh = 0.15 CSSOrientationList = [ [0.70710678118654746, -0.5, 0.5], [0.70710678118654746, -0.5, -0.5], [0.70710678118654746, 0.5, -0.5], [0.70710678118654746, 0.5, 0.5], [-0.70710678118654746, 0, 0.70710678118654757], [-0.70710678118654746, 0.70710678118654757, 0.0], [-0.70710678118654746, 0, -0.70710678118654757], [-0.70710678118654746, -0.70710678118654757, 0.0], ] numCSS = len(CSSOrientationList) # set the CSS unit vectors cssConfigData = fswMessages.CSSConfigFswMsg() totalCSSList = [] for CSSHat in CSSOrientationList: CSSConfigElement = fswMessages.CSSUnitConfigFswMsg() CSSConfigElement.CBias = 1.0 CSSConfigElement.nHat_B = CSSHat totalCSSList.append(CSSConfigElement) cssConfigData.nCSS = numCSS cssConfigData.cssVals = totalCSSList unitTestSupport.setMessage(unitTestSim.TotalSim, unitProcessName, moduleConfig.cssConfigInMsgName, cssConfigData) # Initialize input message cssDataMsg = simFswInterfaceMessages.CSSArraySensorIntMsg() unitTestSupport.setMessage(unitTestSim.TotalSim, unitProcessName, moduleConfig.cssDataInMsgName, cssDataMsg) # Log the output message as well as the internal numACtiveCss variables unitTestSim.TotalSim.logThisMessage(moduleConfig.navStateOutMsgName, testProcessRate) # Get observation data based on sun pointing and CSS orientation data cssDataMsg.CosValue = createCosList([1.0, 0.0, 0.0], CSSOrientationList) # Write in the observation data to the input message unitTestSim.TotalSim.WriteMessageData(moduleConfig.cssDataInMsgName, cssDataMsg.getStructSize(), 0, cssDataMsg) # Initialize test and then step through all of the test vectors in a loop unitTestSim.InitializeSimulation() # Increment the stop time to new termination value unitTestSim.ConfigureStopTime(macros.sec2nano(1.0)) # Execute simulation to current stop time unitTestSim.ExecuteSimulation() # rotate sun heading by 90 degrees cssDataMsg.CosValue = createCosList([0.0, 1.0, 0.0], CSSOrientationList) # Write in the observation data to the input message unitTestSim.TotalSim.WriteMessageData(moduleConfig.cssDataInMsgName, cssDataMsg.getStructSize(), 0, cssDataMsg) unitTestSim.ConfigureStopTime(macros.sec2nano(2.0)) unitTestSim.ExecuteSimulation() # test the module reset function moduleWrap.Reset(1) # this module reset function needs a time input (in NanoSeconds) unitTestSim.ConfigureStopTime(macros.sec2nano(2.5)) unitTestSim.ExecuteSimulation() cssDataMsg.CosValue = createCosList([1.0, 0.0, 0.0], CSSOrientationList) unitTestSim.TotalSim.WriteMessageData(moduleConfig.cssDataInMsgName, cssDataMsg.getStructSize(), 0, cssDataMsg) unitTestSim.ConfigureStopTime(macros.sec2nano(3.0)) unitTestSim.ExecuteSimulation() # Pull logged data out into workspace for analysis omegaEst = unitTestSim.pullMessageLogData(moduleConfig.navStateOutMsgName + '.omega_BN_B', list(range(3))) accuracy = 1e-6 trueVector = [ [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, -3.14159265], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, +3.14159265] ] testFailCount, testMessages = unitTestSupport.compareArray(trueVector, omegaEst, accuracy, "CSS Rate Vector", testFailCount, testMessages) # print out success message if no error were found snippentName = "passFailRate" if testFailCount == 0: colorText = 'ForestGreen' print("PASSED: " + moduleWrap.ModelTag) passedText = r'\textcolor{' + colorText + '}{' + "PASSED" + '}' else: colorText = 'Red' print("Failed: " + moduleWrap.ModelTag) passedText = r'\textcolor{' + colorText + '}{' + "Failed" + '}' unitTestSupport.writeTeXSnippet(snippentName, passedText, path) # each test method requires a single assert method to be called # this check below just makes sure no sub-test failures were found return [testFailCount, ''.join(testMessages)] # # This statement below ensures that the unitTestScript can be run as a # stand-along python script # if __name__ == "__main__": test_module( False, # show_plots False, # testSunHeading Flag True # testRate Flag )