# ISC License
#
# Copyright (c) 2020, 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:   Planet's Albedo
#   Author:        Demet Cilden-Guler
#   Creation Date: May 28, 2020
#
import os
import numpy as np
import pytest
from Basilisk import __path__
from Basilisk.architecture import messaging
from Basilisk.simulation import albedo
from Basilisk.utilities import SimulationBaseClass
from Basilisk.utilities import macros
from Basilisk.utilities import orbitalMotion as om
from Basilisk.utilities import simIncludeGravBody
from Basilisk.utilities import unitTestSupport
from Basilisk.architecture import bskLogging
bskPath = __path__[0]
path = os.path.dirname(os.path.abspath(__file__))
# uncomment this line if this test has an expected failure, adjust message as needed
# @pytest.mark.xfail(True)
[docs]@pytest.mark.parametrize("planetCase", ['earth', 'mars'])
@pytest.mark.parametrize("modelType", ['ALBEDO_AVG_IMPLICIT', 'ALBEDO_AVG_EXPLICIT', 'ALBEDO_DATA'])
@pytest.mark.parametrize("useEclipse", [True, False])
def test_unitAlbedo(show_plots, planetCase, modelType, useEclipse):
    """
    **Validation Test Description**
    This section describes the specific unit tests conducted on this module.
    The test contains 4 tests and is located at ``test_albedo.py``.
    The success criteria is to match the outputs with the generated truth.
    Args:
        planetCase (string): Defines which planet to use.  Options include "earth" and "mars".
        modelType (string):  Defines which albedo model to use. Options include "ALBEDO_AVG_EXPLICIT", "ALBEDO_AVG_IMPLICIT" and "ALBEDO_DATA".
        useEclipse (bool):  Defines if the eclipse is considered for this parameterized unit test.
    **Description of Variables Being Tested**
    In this file, we are checking the values of the variable:
    ``albedoAtInstrument``
    which are pulled from the log data to see if they match with the expected truth values.
    """
    # each test method requires a single assert method to be called
    [testResults, testMessage] = unitAlbedo(show_plots, planetCase, modelType, useEclipse)
    assert testResults < 1, testMessage 
def unitAlbedo(show_plots, planetCase, modelType, useEclipse):
    __tracebackhide__ = True
    testFailCount = 0
    testMessages = []
    testTaskName = "unitTestTask"
    testProcessName = "unitTestProcess"
    testTaskRate = macros.sec2nano(1.0)
    # Create a simulation container
    unitTestSim = SimulationBaseClass.SimBaseClass()
    testProc = unitTestSim.CreateNewProcess(testProcessName)
    testProc.addTask(unitTestSim.CreateNewTask(testTaskName, testTaskRate))
    # create planet input message
    planetInMsg = messaging.SpicePlanetStateMsg()
    # Albedo A1
    albModule = albedo.Albedo()
    albModule.ModelTag = "Albedo_0"
    if modelType == 'ALBEDO_DATA':
        dataPath = os.path.abspath(bskPath + "/supportData/AlbedoData/")
        if planetCase == 'earth':
            fileName = "Earth_ALB_2018_CERES_All_10x10.csv"
        else:
            fileName = "Mars_ALB_TES_10x10.csv"
        albModule.addPlanetandAlbedoDataModel(planetInMsg, dataPath, fileName)
    else:
        ALB_avg = 0.5
        numLat = 200
        numLon = 400
        if modelType == 'ALBEDO_AVG_EXPLICIT':
            albModule.addPlanetandAlbedoAverageModel(planetInMsg, ALB_avg, numLat, numLon)
        else:
            albModule.addPlanetandAlbedoAverageModel(planetInMsg)
    if useEclipse:
        albModule.eclipseCase = True
    # Create dummy sun message
    sunPositionMsg = messaging.SpicePlanetStateMsgPayload()
    # Create dummy planet message
    planetPositionMsg = messaging.SpicePlanetStateMsgPayload()
    planetPositionMsg.PositionVector = [0., 0., 0.]
    gravFactory = simIncludeGravBody.gravBodyFactory()
    if planetCase == 'earth':
        planet = gravFactory.createEarth()
        sunPositionMsg.PositionVector = [-om.AU * 1000., 0.0, 0.0]
    elif planetCase == 'mars':
        planet = gravFactory.createMars()
        sunPositionMsg.PositionVector = [-1.5 * om.AU * 1000., 0.0, 0.0]
    planetPositionMsg.PlanetName = planetCase
    planetPositionMsg.J20002Pfix = np.identity(3)
    req = planet.radEquator
    sunMessage = "sun_message"
    # Create dummy spacecraft message
    scStateMsg = messaging.SCStatesMsgPayload()
    rSC = req + 6000 * 1000  # meters
    alpha = 71. * macros.D2R
    scStateMsg.r_BN_N = np.dot(rSC, [np.cos(alpha), np.sin(alpha), 0.0])
    scStateMsg.sigma_BN = [0., 0., 0.]
    # Albedo instrument configuration
    config1 = albedo.instConfig_t()
    config1.fov = 80. * macros.D2R
    config1.nHat_B = np.array([-np.cos(alpha), -np.sin(alpha), 0.0])
    config1.r_IB_B = np.array([0., 0., 0.])
    albModule.addInstrumentConfig(config1)
    sunInMsg = messaging.SpicePlanetStateMsg().write(sunPositionMsg)
    albModule.sunPositionInMsg.subscribeTo(sunInMsg)
    planetInMsg.write(planetPositionMsg)
    scInMsg = messaging.SCStatesMsg().write(scStateMsg)
    albModule.spacecraftStateInMsg.subscribeTo(scInMsg)
    unitTestSim.AddModelToTask(testTaskName, albModule)
    # setup logging
    dataLog = albModule.albOutMsgs[0].recorder()
    unitTestSim.AddModelToTask(testTaskName, dataLog)
    # Initialize and run simulation one step at a time
    unitTestSim.InitializeSimulation()
    # Execute the simulation for one time step
    unitTestSim.TotalSim.SingleStepProcesses()
    # This pulls the actual data log from the simulation run.
    dataAlb0 = dataLog.albedoAtInstrument
    errTol = 1E-12
    if planetCase == 'earth':
        if modelType == 'ALBEDO_DATA':
            if useEclipse:
                truthAlb = 0.0022055492477917
            else:
                truthAlb = 0.0022055492477917
        else:
            if modelType == 'ALBEDO_AVG_EXPLICIT':
                if useEclipse:
                    truthAlb = 0.0041742091531996
                else:
                    truthAlb = 0.004174209177079
            else:
                if useEclipse:
                    truthAlb = 0.002421222716229847
                else:
                    truthAlb = 0.002421222716229847
    else:
        if modelType == 'ALBEDO_DATA':
            if useEclipse:
                truthAlb = 0.0014001432717662
            else:
                truthAlb = 0.0014001432717662
        else:
            if modelType == 'ALBEDO_AVG_EXPLICIT':
                if useEclipse:
                    truthAlb = 0.0035681407388827
                else:
                    truthAlb = 0.0035681407390035
            else:
                if useEclipse:
                    truthAlb = 0.0011418311186365906
                else:
                    truthAlb = 0.0011418311186365906
    if not unitTestSupport.isDoubleEqual(dataAlb0[0], truthAlb, errTol):
        testFailCount += 1
    #   print out success or failure message
    if testFailCount == 0:
            print("PASSED: " + albModule.ModelTag)
    else:
            print("Failed: " + albModule.ModelTag)
    print("This test uses a relative accuracy value of " + str(errTol * 100) + " percent")
    return [testFailCount, ''.join(testMessages)]
[docs]def test_albedo_invalid_file(tmp_path):
    """Verify that Albedo model returns gracefully when file cannot be loaded.
    
    Regression test for BSK-428 where model would segfault when invalid file
    was specified.
    .. note:: The model is not in a usable state if this initialization fails.
        Ideally an exception would be thrown, but the SWIG infrastructure doesn't
        appear to be setup to handle C++ exceptions, so we settle for printing a
        message and not segfaulting.
    """
    albModule = albedo.Albedo()
    # silence expected error message
    albModule.bskLogger.setLogLevel(bskLogging.BSK_SILENT)
    gravFactory = simIncludeGravBody.gravBodyFactory()
    gravFactory.createEarth()
    planetPositionMsg = messaging.SpicePlanetStateMsgPayload()
    planetPositionMsg.PlanetName = "earth"
    planetInMsg = messaging.SpicePlanetStateMsg().write(planetPositionMsg)
    sunPositionMsg = messaging.SpicePlanetStateMsgPayload()
    sunInMsg = messaging.SpicePlanetStateMsg().write(sunPositionMsg)
    albModule.sunPositionInMsg.subscribeTo(sunInMsg)
    scStateMsg = messaging.SCStatesMsgPayload()
    scInMsg = messaging.SCStatesMsg().write(scStateMsg)
    albModule.spacecraftStateInMsg.subscribeTo(scInMsg)
    albModule.addPlanetandAlbedoDataModel(planetInMsg, str(tmp_path), "does_not_exit.file")
    
    # this call would previously segfault
    albModule.Reset(0)
    # the fact that we got here without segfaulting means the test
    # passed
    assert True 
if __name__ == "__main__":
    # unitAlbedo(False, 'earth', 'ALBEDO_AVG_EXPLICIT', True)
    unitAlbedo(False, 'mars', 'ALBEDO_AVG_IMPLICIT', False)