Source code for test_unitSpice

''' '''
'''
 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.

'''
import pytest
import os, inspect

#
# Spice Unit Test
#
# Purpose:  Test the proper function of the Spice Ephemeris module.
#           Proper function is tested by comparing Spice Ephemeris to
#           JPL Horizons Database for different planets and times of year
# Author:   Thibaud Teil
# Creation Date:  Dec. 20, 2016
#

filename = inspect.getframeinfo(inspect.currentframe()).filename
path = os.path.dirname(os.path.abspath(filename))
from Basilisk import __path__
bskPath = __path__[0]


import datetime
from Basilisk.utilities import unitTestSupport
from Basilisk.utilities import SimulationBaseClass
import numpy
from Basilisk.simulation import spice_interface
from Basilisk.utilities import macros
import matplotlib.pyplot as plt


# Class in order to plot using data accross the different paramatrized scenarios
class DataStore:
    def __init__(self):
        self.Date = [] # replace these with appropriate containers for the data to be stored for plotting
        self.MarsPosErr = []
        self.EarthPosErr = []
        self.SunPosErr = []


    def plotData(self):
        fig1 = plt.figure(1)
        rect =fig1.patch
        rect.set_facecolor('white')

        plt.xticks(numpy.arange(len(self.Date)), self.Date)
        plt.plot(self.MarsPosErr, 'r', label='Mars')
        plt.plot(self.EarthPosErr, 'bs', label='Earth')
        plt.plot(self.SunPosErr, 'yo', label='Sun')
#        plt.ylim(0,0.000005)
#        plt.ylim(0,0.02)

        plt.rc('font', size=50)
        plt.legend(loc='upper left')
        fig1.autofmt_xdate()
        plt.xlabel('Date of test')
        plt.ylabel('Position Error [m]')
        plt.show()

    def giveData(self):
        plt.figure(1)
        plt.close(1)
        fig1 = plt.figure(1,figsize=(7,5), dpi=80, facecolor='w', edgecolor='k')

        plt.xticks(numpy.arange(len(self.Date)), self.Date)
        plt.plot(self.MarsPosErr, 'r', label='Mars')
        plt.plot(self.EarthPosErr, 'b', label='Earth')
        plt.plot(self.SunPosErr, 'y', label='Sun')

        plt.legend(loc='upper left')
        fig1.autofmt_xdate()
        plt.xlabel('Date of test')
        plt.ylabel('Position Error [m]')

        return plt

# Py.test fixture in order to plot
@pytest.fixture(scope="module")
def testPlottingFixture(show_plots):
    dataStore = DataStore()
    yield dataStore

    plt = dataStore.giveData()
    unitTestSupport.writeFigureLaTeX('EphemMars', 'Ephemeris Error on Mars', plt, 'height=0.7\\textwidth, keepaspectratio', path)
    plt.ylim(0, 2E-2)
    unitTestSupport.writeFigureLaTeX('EphemEarth', 'Ephemeris Error on Earth', plt, 'height=0.7\\textwidth, keepaspectratio', path)
    plt.ylim(0, 5E-6)
    unitTestSupport.writeFigureLaTeX('EphemSun', 'Ephemeris Error on Sun', plt, 'height=0.7\\textwidth, keepaspectratio', path)
    if show_plots:
        dataStore.plotData()


# 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(True)

# The following 'parametrize' function decorator provides the parameters and expected results for each
#   of the multiple test runs for this test.
[docs]@pytest.mark.parametrize("DateSpice, DatePlot , MarsTruthPos , EarthTruthPos, , SunTruthPos", [ ("2015 February 10, 00:00:00.0 TDB", "02/10/15" ,[2.049283795042291E+08, 4.654550957513031E+07, 1.580778617009296E+07] ,[-1.137790671899544E+08, 8.569008401822130E+07, 3.712507705247846E+07],[4.480338216752146E+05, -7.947764237588293E+04, -5.745748832696378E+04]), ("2015 February 20, 00:00:00.0 TDB", "02/20/15" ,[ 1.997780190793433E+08, 6.636509140613769E+07, 2.503734390917690E+07], [-1.286636391244711E+08, 6.611156946652703E+07, 2.863736192793162E+07],[4.535258225415821E+05, -7.180260790174162E+04, -5.428151563919459E+04]), ("2015 April 10, 00:00:00.0 TDB", "04/10/15" ,[1.468463828343225E+08, 1.502909016357645E+08, 6.495986693265321E+07],[-1.406808744055921E+08, -4.614996219219251E+07, -2.003047222725225E+07],[4.786772771370058E+05, -3.283487082146838E+04, -3.809690999538498E+04]), ("2015 April 20, 00:00:00.0 TDB", "04/20/15" ,[1.313859463489376E+08, 1.637422864185919E+08, 7.154683454681394E+07], [-1.304372112275012E+08, -6.769916175223185E+07, -2.937179538983412E+07],[4.834188130324496E+05, -2.461799304268214E+04, -3.467083541217462E+04]), ("2015 June 10, 00:00:00.0 TDB", "06/10/15" , [3.777078276008123E+07, 2.080046702252371E+08, 9.437503998071109E+07],[-2.946784585692780E+07, -1.365779215199542E+08, -5.923299652516938E+07],[5.052070933145228E+05, 1.837038638578682E+04, -1.665575509449521E+04]), ("2015 June 20, 00:00:00.0 TDB", "06/20/15" , [1.778745850655047E+07, 2.116712756672253E+08, 9.659608128589308E+07], [-4.338053580692466E+06, -1.393743714818930E+08, -6.044500073550620E+07],[5.090137386469169E+05, 2.694272566092012E+04, -1.304862690440150E+04]), ("2015 August 10, 00:00:00.0 TDB", "08/10/15" , [-8.302866300591002E+07, 2.053384243312354E+08, 9.641192179982041E+07],[1.111973130587749E+08, -9.507060674145325E+07, -4.123957889640842E+07],[5.263951240980130E+05, 7.113788303899391E+04, 5.601415938789949E+03]), ("2015 August 20, 00:00:00.0 TDB", "08/20/15" , [-1.015602120938274E+08, 1.994877113707506E+08, 9.422840996376510E+07], [1.267319535735967E+08, -7.664279872369213E+07, -3.325170492059625E+07],[5.294368386862668E+05, 7.989635630604146E+04, 9.307380417148834E+03]), ("2015 October 10, 00:00:00.0 TDB", "10/10/15" , [-1.828944464171771E+08, 1.498117608528323E+08, 7.363786404040858E+07],[1.440636517249971E+08, 3.827074461651713E+07, 1.656440704503509E+07],[5.433268959250135E+05, 1.251717566539142E+05, 2.849999378507032E+04]), ("2015 October 20, 00:00:00.0 TDB", "10/20/15" , [-1.955685835661002E+08, 1.367530082668284E+08, 6.799006120628108E+07],[1.343849818314204E+08, 6.019977403127116E+07, 2.607024615553362E+07],[5.457339294611338E+05, 1.342148834831663E+05, 3.234185290692726E+04]), ("2015 December 10, 00:00:00.0 TDB", "12/10/15" , [-2.392022492203017E+08, 5.847873056287902E+07, 3.326431285934674E+07],[3.277145427626925E+07, 1.320956053465003E+08, 5.723804900679157E+07],[5.559607335969181E+05, 1.813263443761486E+05, 5.242991145066972E+04]), ("2015 December 20, 00:00:00.0 TDB", "12/20/15" , [-2.432532635321551E+08, 4.156075241419497E+07, 2.561357000250468E+07], [6.866134677340530E+06, 1.351080593806486E+08, 5.854460725992832E+07],[5.575179227151767E+05, 1.907337298309725E+05, 5.645553733620559E+04]), ("2016 February 10, 00:00:00.0 TDB", "02/10/16" , [-2.385499316808322E+08, -4.818711325555268E+07, -1.567983931044450E+07],[-1.132504603146183E+08, 8.647183658089657E+07, 3.746046979137553E+07],[5.630027736619673E+05, 2.402590276126245E+05, 7.772804647512530E+04]), ("2016 February 20, 00:00:00.0 TDB", "02/20/16" , [-2.326189626865872E+08, -6.493600278878165E+07,-2.352250994262638E+07], [-1.282197228963156E+08, 6.695033443521750E+07, 2.899822684259482E+07],[5.635403728358555E+05, 2.498445036007692E+05, 8.185973180152237E+04]), ("2016 April 10, 00:00:00.0 TDB", "04/10/16" , [-1.795928090776869E+08, -1.395102817786797E+08, -5.916067235603344E+07],[-1.399773606371217E+08, -4.749126225182984E+07, -2.061331784067504E+07],[5.639699901756521E+05, 2.977755140828988E+05, 1.025609223621660E+05]), ("2016 April 20, 00:00:00.0 TDB", "04/20/16" , [-1.646488222290798E+08, -1.517361128528306E+08, -6.517204439842616E+07], [-1.294310199316289E+08, -6.890085351639988E+07, -2.989515576444890E+07],[5.636348418836893E+05, 3.073681895822543E+05, 1.067117713233756E+05]), ("2016 June 10, 00:00:00.0 TDB", "06/10/16" , [-7.085675304355654E+07, -1.933788289169757E+08, -8.680583098365544E+07],[-2.756178030784301E+07, -1.365892814324490E+08, -5.923888961370525E+07],[5.597493293268962E+05, 3.563312003229167E+05, 1.279448896916322E+05]), ("2016 June 20, 00:00:00.0 TDB", "06/20/16" , [-4.994076874358515E+07, -1.967336617729592E+08, -8.890950206156498E+07], [-2.413995783152328E+06, -1.390828611356292E+08, -6.032062925759617E+07],[5.585605124166313E+05, 3.659402953599973E+05, 1.321213212542782E+05]), ("2016 August 10, 00:00:00.0 TDB", "08/10/16" , [5.965895856067932E+07, -1.851251207911916E+08, -8.654489416392270E+07],[1.124873641861596E+08, -9.344410235679612E+07, -4.053621796412993E+07],[5.501477436262188E+05, 4.149525682073312E+05, 1.534983234437988E+05]), ("2016 August 20, 00:00:00.0 TDB", "08/20/16" , [8.021899957229958E+07, -1.770523551156136E+08, -8.339737954004113E+07] , [1.277516713236801E+08, -7.484361731649198E+07, -3.247232896320245E+07],[5.480148786547601E+05, 4.245199573757614E+05, 1.576874582857746E+05]), ("2016 October 10, 00:00:00.0 TDB", "10/10/16" , [1.674991232418756E+08, -1.090427183510760E+08, -5.456038490399034E+07],[ 1.434719792031415E+08, 4.029387436854774E+07, 1.744057129058395E+07],[5.348794087050703E+05, 4.727986075510736E+05, 1.788947974297997E+05]), ("2016 October 20, 00:00:00.0 TDB", "10/20/16" , [1.797014954219412E+08, -9.133803506487390E+07, -4.676932170648168E+07] , [ 1.334883617893556E+08, 6.209917895944476E+07, 2.689423661839254E+07],[5.318931290166953E+05, 4.821605725626923E+05, 1.830201949404063E+05]), ("2016 December 10, 00:00:00.0 TDB", "12/10/16" , [2.087370835407280E+08, 1.035903131428964E+07, -9.084001492689754E+05] ,[3.082308903715757E+07, 1.328026978502711E+08, 5.754559376449955E+07],[5.147792796720199E+05, 5.293951386498961E+05, 2.038816823549219E+05]), ("2016 December 20, 00:00:00.0 TDB", "12/20/16" , [2.075411186038260E+08, 3.092173198610598E+07, 8.555251204561792E+06], [4.885421269793877E+06, 1.355125217382336E+08, 5.872015978003684E+07],[5.110736843893399E+05, 5.385860060393942E+05, 2.079481168492821E+05]) ]) @pytest.mark.parametrize("useMsg", [False, True]) # provide a unique test method name, starting with test_ def test_unitSpice(testPlottingFixture, show_plots, DateSpice, DatePlot , MarsTruthPos , EarthTruthPos, SunTruthPos, useMsg): """Module Unit Test""" # each test method requires a single assert method to be called [testResults, testMessage] = unitSpice(testPlottingFixture, show_plots, DateSpice, DatePlot , MarsTruthPos , EarthTruthPos, SunTruthPos, useMsg) assert testResults < 1, testMessage
# Run unit test def unitSpice(testPlottingFixture, show_plots, DateSpice, DatePlot , MarsTruthPos , EarthTruthPos, SunTruthPos, useMsg): testFailCount = 0 # zero unit test result counter testMessages = [] # create empty array to store test log messages # Create a sim module as an empty container unitTaskName = "unitTask" # arbitrary name (don't change) unitProcessName = "TestProcess" # arbitrary name (don't change) # Create a sim module as an empty container TotalSim = SimulationBaseClass.SimBaseClass() DynUnitTestProc = TotalSim.CreateNewProcess(unitProcessName) # create the dynamics task and specify the integration update time DynUnitTestProc.addTask(TotalSim.CreateNewTask(unitTaskName, macros.sec2nano(0.1))) # Initialize the spice modules that we are using. SpiceObject = spice_interface.SpiceInterface() SpiceObject.ModelTag = "SpiceInterfaceData" SpiceObject.SPICEDataPath = bskPath + '/supportData/EphemerisData/' SpiceObject.outputBufferCount = 10000 SpiceObject.planetNames = spice_interface.StringVector(["earth", "mars barycenter", "sun"]) SpiceObject.UTCCalInit = DateSpice TotalSim.AddModelToTask(unitTaskName, SpiceObject) if useMsg: SpiceObject.epochInMsgName = "simEpoch" epochMsg = unitTestSupport.timeStringToGregorianUTCMsg(DateSpice) unitTestSupport.setMessage(TotalSim.TotalSim, unitProcessName, SpiceObject.epochInMsgName, epochMsg) # The following value is set, but should not be used by the module. This checks that the above # epoch message over-rules any info set in this variable. SpiceObject.UTCCalInit = "1990 February 10, 00:00:00.0 TDB" # Configure simulation TotalSim.ConfigureStopTime(int(60.0 * 1E9)) TotalSim.AddVariableForLogging('SpiceInterfaceData.GPSSeconds') TotalSim.AddVariableForLogging('SpiceInterfaceData.J2000Current') TotalSim.AddVariableForLogging('SpiceInterfaceData.julianDateCurrent') TotalSim.AddVariableForLogging('SpiceInterfaceData.GPSWeek') # Execute simulation TotalSim.InitializeSimulation() TotalSim.ExecuteSimulation() # Get the logged variables (GPS seconds, Julian Date) DataGPSSec = TotalSim.GetLogVariableData('SpiceInterfaceData.GPSSeconds') DataJD = TotalSim.GetLogVariableData('SpiceInterfaceData.julianDateCurrent') #Get parametrized date from DatePlot year = "".join(("20",DatePlot[6:8])) date = datetime.datetime( int(year) , int(DatePlot[0:2]) , int(DatePlot[3:5])) testPlottingFixture.Date.append(DatePlot[0:8]) #Get the GPS time date2 = datetime.datetime(1980, 0o1, 6) #Start of GPS time timeDiff = date-date2 #Time in days since GPS time GPSTimeSeconds = timeDiff.days*86400 #Get the Julian day JulianStartDate = date.toordinal()+1721424.5 # # Begin testing module results to truth values # # Truth values # For Time delta check GPSRow = DataGPSSec[0, :] InitDiff = GPSRow[1] - GPSRow[0] * 1.0E-9 i = 1 # Compare the GPS seconds values AllowTolerance = 1E-6 while (i < DataGPSSec.shape[0]): if date.isoweekday() == 7: # Skip test on Sundays, because it's the end of a GPS week (seconds go to zero) i += 1 else: GPSRow = DataGPSSec[i, :] CurrDiff = GPSRow[1] - GPSRow[0] * 1.0E-9 if (abs(CurrDiff - InitDiff) > AllowTolerance): testFailCount += 1 testMessages.append("FAILED: Time delta check failed with difference of: %(DiffVal)f \n"% \ {"DiffVal": CurrDiff - InitDiff}) i += 1 # Truth values # For absolute GPS time check if (date>datetime.datetime(2015,0o6,30)): # Taking into account the extra leap second added on 6/30/2015 GPSEndTime = GPSTimeSeconds + 17 + 60.0 - 68.184 # 17 GPS skip seconds passed that date else: GPSEndTime = GPSTimeSeconds + 16 + 60.0 - 67.184 # 16 GPS skip seconds before GPSWeek = int(GPSEndTime / (86400 * 7)) GPSSecondAssumed = GPSEndTime - GPSWeek * 86400 * 7 GPSSecDiff = abs(GPSRow[1] - GPSSecondAssumed) # TestResults['GPSAbsTimeCheck'] = True AllowTolerance = 1E-4 if useMsg: AllowTolerance = 2E-2 if (date.isoweekday() != 7 and GPSSecDiff > AllowTolerance): #Skip test days that are Sunday because of the end of a GPS week testFailCount += 1 testMessages.append("FAILED: Absolute GPS time check failed with difference of: %(DiffVal)f \n" % \ {"DiffVal": GPSSecDiff}) # Truth values # For absolute Julian date time check if (date>datetime.datetime(2015,0o6,30)): # Taking into account the extra leap second added on 6/30/2015 JDEndTime = JulianStartDate + 0.0006944440 - 68.184 / (86400) else: JDEndTime = JulianStartDate + 0.0006944440 - 67.184 / (86400) #Simulated values JDEndSim = DataJD[i - 1, 1] JDTimeErrorAllow = 0.1 / (24.0 * 3600.0) if (abs(JDEndSim - JDEndTime) > JDTimeErrorAllow): testFailCount += 1 testMessages.append("FAILED: Absolute Julian Date time check failed with difference of: %(DiffVal)f \n" % \ {"DiffVal": abs(JDEndSim - JDEndTime)}) # Truth values # For Mars position check MarsPosEnd = numpy.array(MarsTruthPos) MarsPosEnd = MarsPosEnd * 1000.0 # Get Simulated values FinalMarsMessage = spice_interface.SpicePlanetStateSimMsg() TotalSim.TotalSim.GetWriteData("mars barycenter_planet_data", 120, FinalMarsMessage, 0) MarsPosVec = FinalMarsMessage.PositionVector # Compare Mars position values MarsPosArray = numpy.array([MarsPosVec[0], MarsPosVec[1], MarsPosVec[2]]) MarsPosDiff = MarsPosArray - MarsPosEnd PosDiffNorm = numpy.linalg.norm(MarsPosDiff) # Plot Mars postion values testPlottingFixture.MarsPosErr.append(PosDiffNorm) # Test Mars position values PosErrTolerance = 250 if useMsg: PosErrTolerance = 1000 if (PosDiffNorm > PosErrTolerance): testFailCount += 1 testMessages.append("FAILED: Mars position check failed with difference of: %(DiffVal)f \n" % \ {"DiffVal": PosDiffNorm}) # Truth values # For Earth position check EarthPosEnd = numpy.array(EarthTruthPos) EarthPosEnd = EarthPosEnd * 1000.0 #Simulated Earth position values FinalEarthMessage = spice_interface.SpicePlanetStateSimMsg() TotalSim.TotalSim.GetWriteData("earth_planet_data", 120, FinalEarthMessage, 0) EarthPosVec = FinalEarthMessage.PositionVector # Compare Earth position values EarthPosArray = numpy.array([EarthPosVec[0], EarthPosVec[1], EarthPosVec[2]]) EarthPosDiff = EarthPosArray - EarthPosEnd PosDiffNorm = numpy.linalg.norm(EarthPosDiff) # Plot Earth position values testPlottingFixture.EarthPosErr.append(PosDiffNorm) # TestResults['EarthPosCheck'] = True if (PosDiffNorm > PosErrTolerance): testFailCount += 1 testMessages.append("FAILED: Earth position check failed with difference of: %(DiffVal)f \n" % \ {"DiffVal": PosDiffNorm}) # Truth Sun position values SunPosEnd = numpy.array(SunTruthPos) SunPosEnd = SunPosEnd * 1000.0 #Simulated Sun position values FinalSunMessage = spice_interface.SpicePlanetStateSimMsg() TotalSim.TotalSim.GetWriteData("sun_planet_data", 120, FinalSunMessage, 0) SunPosVec =FinalSunMessage.PositionVector # Compare Sun position values SunPosArray = numpy.array([SunPosVec[0], SunPosVec[1], SunPosVec[2]]) SunPosDiff = SunPosArray - SunPosEnd PosDiffNorm = numpy.linalg.norm(SunPosDiff) # plot Sun position values testPlottingFixture.SunPosErr.append(PosDiffNorm) # Test Sun position values if (PosDiffNorm > PosErrTolerance): testFailCount += 1 testMessages.append("FAILED: Sun position check failed with difference of: %(DiffVal)f \n" % \ {"DiffVal": PosDiffNorm}) if date==datetime.datetime(2016,12,20): # Only test the false files on the last test # Test non existing directory SpiceObject.SPICEDataPath = "ADirectoryThatDoesntreallyexist" SpiceObject.SPICELoaded = False #TotalSim.ConfigureStopTime(int(1E9)) #Uncomment these 3 lines to test false directory #TotalSim.InitializeSimulation() #TotalSim.ExecuteSimulation() # Test overly long planet name SpiceObject.SPICEDataPath = "" SpiceObject.SPICELoaded = False SpiceObject.planetNames = spice_interface.StringVector(["earth", "mars", "sun", "thisisaplanetthatisntreallyanythingbutIneedthenametobesolongthatIhitaninvalidconditioninmycode"]) #TotalSim.ConfigureStopTime(int(1E9)) #Uncomment these 3 lines to test false planet names #TotalSim.InitializeSimulation() #TotalSim.ExecuteSimulation() # print out success message if no error were found if testFailCount == 0: print(" \n PASSED ") # 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 unit test scrip can be run as a # stand-along python script # if __name__ == "__main__": test_unitSpice( testPlottingFixture, False, # show_plots "2015 February 10, 00:00:00.00 TDB", "02/10/15", [2.049283795042291E+08, 4.654550957513031E+07, 1.580778617009296E+07], [-1.137790671899544E+08, 8.569008401822130E+07, 3.712507705247846E+07], [4.480338216752146E+05, -7.947764237588293E+04, -5.745748832696378E+04], True # useMsg )