#
#  ISC License
#
#  Copyright (c) 2022, 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:        constrainedAttitudeManeuver
#   Author:             Riccardo Calaon
#   Creation Date:      April 16, 2022
#
import os
import numpy as np
import pytest
from Basilisk.architecture import BSpline
from Basilisk.architecture import messaging
from Basilisk.fswAlgorithms import constrainedAttitudeManeuver
from Basilisk.utilities import RigidBodyKinematics as rbk
# Import all of the modules that we are going to be called in this simulation
from Basilisk.utilities import SimulationBaseClass
from Basilisk.utilities import macros
from Basilisk.utilities import unitTestSupport
path = os.path.dirname(os.path.abspath(__file__))
dataFileName = None
def shadowSetMap(sigma, switch):
    sigma = np.array(sigma)
    if switch:
        s2 = (sigma[0]**2 + sigma[1]**2 + sigma[2]**2)
        if not s2 == 0:
            return -sigma / s2
        else:
            return sigma
    else:
        return sigma
class constraint:
    def __init__(self, axis, color):
        self.axis =  axis / np.linalg.norm(axis)
        self.color = color
        
class node:
    def __init__(self, sigma_BN, constraints, **kwargs):
        self.sigma_BN = np.array(sigma_BN)
        s = np.linalg.norm(self.sigma_BN)
        if s > 1:
            self.sigma_BN = shadowSetMap(sigma_BN, True)  # mapping to shadow set if |sigma| > 1
        if np.abs(s-1) < 1e-5:
            s = 1.
        self.isBoundary = False
        self.s = s
        if s == 1:
            self.isBoundary = True
        self.isFree = True
        self.color = ''
        self.neighbors = {}
        self.heuristic = 0
        self.priority = 0
        self.backpointer = self
        # check cosntraint compliance
        sigma_tilde = np.array([ [0,           -sigma_BN[2], sigma_BN[1]],
                                 [sigma_BN[2],      0,      -sigma_BN[0]],
                                 [-sigma_BN[1], sigma_BN[0],     0      ] ])
        BN = np.identity(3) + ( 8*np.matmul(sigma_tilde, sigma_tilde) -4*(1-s**2)*sigma_tilde ) / (1 + s**2)**2
        NB = BN.transpose()
        # checking for keepOut constraint violation
        if 'keepOut_b' in kwargs:
            for i in range(len(kwargs['keepOut_b'])):
                b_B = np.array(kwargs['keepOut_b'][i])
                b_N = np.matmul(NB, b_B)
                for c in constraints['keepOut']:
                    if np.dot(b_N, c.axis) >= np.cos(kwargs['keepOut_fov'][i]):
                        self.isFree = False
                        self.color = c.color
                        return
        # checking for keepIn constraint violation (at least one SS must see the Sun)
        if 'keepIn_b' in kwargs:
            b_N = []
            for i in range(len(kwargs['keepIn_b'])):
                b_B = np.array(kwargs['keepIn_b'][i])
                b_N.append(np.matmul(NB, b_B))
            isIn = False
            for c in constraints['keepIn']:
                for i in range(len(b_N)):
                    if np.dot(b_N[i], c.axis) >= np.cos(kwargs['keepIn_fov'][i]):
                        isIn = True
                        self.color = c.color
            if not isIn:
                self.isFree = False
def distanceCart(n1, n2):
    d1 = np.linalg.norm(n1.sigma_BN - n2.sigma_BN)
    sigma1norm2 = n1.sigma_BN[0]**2 + n1.sigma_BN[1]**2 + n1.sigma_BN[2]**2
    sigma2norm2 = n2.sigma_BN[0]**2 + n2.sigma_BN[1]**2 + n2.sigma_BN[2]**2
    if sigma2norm2 > 1e-8:
        sigma2_SS = shadowSetMap(n2.sigma_BN, True)
        d2 = np.linalg.norm(n1.sigma_BN - sigma2_SS)
    else:
        d2 = d1
    if sigma1norm2 > 1e-8:
        sigma1_SS = shadowSetMap(n1.sigma_BN, True)
        d3 = np.linalg.norm(sigma1_SS - n2.sigma_BN)
    else:
        d3 = d1
    if sigma1norm2 > 1e-8 and sigma2norm2 > 1e-8:
        d4 = np.linalg.norm(sigma1_SS - sigma2_SS)
    else:
        d4 = d1
    return min(d1, d2, d3, d4)
def distanceMRP(n1, n2):    
    s1 = n1.sigma_BN
    s2 = n2.sigma_BN
    sigma1norm2 = n1.sigma_BN[0]**2 + n1.sigma_BN[1]**2 + n1.sigma_BN[2]**2
    sigma2norm2 = n2.sigma_BN[0]**2 + n2.sigma_BN[1]**2 + n2.sigma_BN[2]**2
    D = 1 + (sigma1norm2*sigma2norm2)**2 + 2*np.dot(s1, s2)
    if abs(D) < 1e-5:
        s2 = shadowSetMap(s2, True)
        sigma2norm2 = 1 / sigma2norm2
        D = 1 + (sigma1norm2*sigma2norm2)**2 + 2*np.dot(s1, s2)
    s12 = ( (1-sigma2norm2)*s1 - (1-sigma1norm2)*s2 + 2*np.cross(s1, s2) ) / D
    sigma12norm2 = np.linalg.norm(s12)**2
    if sigma12norm2 > 1:
        s12 = shadowSetMap(s12, True)
    return 4*np.arctan(np.linalg.norm(s12))
def mirrorFunction(i, j, k):
    return [ [i, j, k], [-i, j, k], [i, -j, k], [i, j, -k], [-i, -j, k], [-i, j, -k], [i, -j, -k], [-i, -j, -k] ]
def neighboringNodes(i, j, k):
    return [ [i-1,  j,  k], [i+1,  j,  k], [i,  j-1,  k], [i,  j+1,  k], [i,  j,  k-1], [i,  j,  k+1],
             [i-1, j-1, k], [i+1, j-1, k], [i-1, j+1, k], [i+1, j+1, k], [i-1, j, k-1], [i+1, j, k-1],
             [i-1, j, k+1], [i+1, j, k+1], [i, j-1, k-1], [i, j+1, k-1], [i, j-1, k+1], [i, j+1, k+1],
             [i-1,j-1,k-1], [i+1,j-1,k-1], [i-1,j+1,k-1], [i-1,j-1,k+1], [i+1,j+1,k-1], [i+1,j-1,k+1], [i-1,j+1,k+1], [i+1,j+1,k+1] ]
def generateGrid(n_start, n_goal, N, constraints, data):
    
    u = np.linspace(0, 1, N, endpoint = True)
    nodes = {}
    # add internal nodes (|sigma| <= 1)
    for i in range(N):
        for j in range(N):
            for k in range(N):
                if (u[i]**2+u[j]**2+u[k]**2) <= 1:
                    for m in mirrorFunction(i, j, k):
                        if (m[0], m[1], m[2]) not in nodes:
                            nodes[(m[0], m[1], m[2])] = node([np.sign(m[0])*u[i], np.sign(m[1])*u[j], np.sign(m[2])*u[k]], constraints, **data)
    # add missing boundary nodes (|sigma| = 1)
    for i in range(N-1):
        for j in range(N-1):
            for k in range(N-1):
                if (i, j, k) in nodes:
                    if not nodes[(i, j, k)].isBoundary:
                        if (i+1, j, k) not in nodes:
                            for m in mirrorFunction(i+1, j, k):
                                if (m[0], m[1], m[2]) not in nodes:
                                    nodes[(m[0], m[1], m[2])] = node([np.sign(m[0])*(1-u[j]**2-u[k]**2)**0.5, np.sign(m[1])*u[j], np.sign(m[2])*u[k]], constraints, **data)
                        if (i, j+1, k) not in nodes:
                            for m in mirrorFunction(i, j+1, k):
                                if (m[0], m[1], m[2]) not in nodes:
                                    nodes[(m[0], m[1], m[2])] = node([np.sign(m[0])*u[i], np.sign(m[1])*(1-u[i]**2-u[k]**2)**0.5, np.sign(m[2])*u[k]], constraints, **data)
                        if (i, j, k+1) not in nodes:
                            for m in mirrorFunction(i, j, k+1):
                                if (m[0], m[1], m[2]) not in nodes:
                                    nodes[(m[0], m[1], m[2])] = node([np.sign(m[0])*u[i], np.sign(m[1])*u[j], np.sign(m[2])*(1-u[i]**2-u[j]**2)**0.5], constraints, **data)
    
    # link nodes
    for key1 in nodes:
        i = key1[0]
        j = key1[1]
        k = key1[2]
        # link nodes to immediate neighbors
        for n in neighboringNodes(i, j, k):
            key2 = (n[0], n[1], n[2])
            if key2 in nodes:
                if nodes[key1].isFree and nodes[key2].isFree:
                    nodes[key1].neighbors[key2] = nodes[key2]
        # link boundary nodes to neighbors of respective shadow sets
        if nodes[key1].isBoundary:
            if (-i, -j, -k) in nodes:
                for key2 in nodes[(-i, -j, -k)].neighbors:
                    if nodes[key1].isFree and nodes[key2].isFree and key2 not in nodes[key1].neighbors:
                        nodes[key1].neighbors[key2] = nodes[key2]
    # add start and goal nodes
    # looking for closest nodes to start and goal
    ds = 10
    dg = 10
    for key in nodes:
        if nodes[key].isFree:
            d1 = distanceCart(nodes[key], n_start)
            if abs(d1-ds) < 1e-6:
                if np.linalg.norm(nodes[key].sigma_BN) < np.linalg.norm(nodes[key_s].sigma_BN):
                    ds = d1
                    n_s = nodes[key]
                    key_s = key
            else:
                if d1 < ds:
                    ds = d1
                    n_s = nodes[key]
                    key_s = key
            d2 = distanceCart(nodes[key], n_goal)
            if abs(d2-dg) < 1e-6:
                if np.linalg.norm(nodes[key].sigma_BN) < np.linalg.norm(nodes[key_g].sigma_BN):
                    dg = d2
                    n_g = nodes[key]
                    key_g = key
            else:
                if d2 < dg:
                    dg = d2
                    n_g = nodes[key]
                    key_g = key
    for key in n_s.neighbors:
        n_start.neighbors[key] = n_s.neighbors[key]
    for key in n_g.neighbors:
        nodes[key].neighbors[key_g] = n_goal
    nodes[key_s] = n_start 
    nodes[key_g] = n_goal
    return nodes
def backtrack(n, n_start):
    if n == n_start:
        path = [n]
        return path
    else:
        path = backtrack(n.backpointer, n_start)
        path.append(n)
        return path
def pathHandle(path, avgOmega):
    T = [0]
    S = 0
    for n in range(len(path)-1):
        T.append(T[n] + distanceCart(path[n], path[n+1]))
        S += T[n+1] - T[n]
    X1 = []
    X2 = []
    X3 = []
    shadowSet = False
    for n in range(len(path)-1):
        if not shadowSet:
            sigma = path[n].sigma_BN
        else:
            if unitTestSupport.isVectorEqual(path[n].sigma_BN, [0,0,0], 1e-6):
                for m in range(0,n):
                    s2 = (X1[m]**2 + X2[m]**2 + X3[m]**2)**0.5
                    X1[m] = -X1[m] / s2
                    X2[m] = -X2[m] / s2
                    X3[m] = -X3[m] / s2
                shadowSet = not shadowSet
            sigma = rbk.MRPswitch(path[n].sigma_BN, 0)
        delSigma = path[n+1].sigma_BN - path[n].sigma_BN
        if (np.linalg.norm(delSigma) > 1):
            shadowSet = not shadowSet
        X1.append(sigma[0])
        X2.append(sigma[1])
        X3.append(sigma[2])
    if shadowSet:
        sigma = rbk.MRPswitch(path[-1].sigma_BN, 0)
    else:
        sigma = path[-1].sigma_BN
    X1.append(sigma[0])
    X2.append(sigma[1])
    X3.append(sigma[2])
    Input = BSpline.InputDataSet(X1, X2, X3)
    Input.setT( np.array(T) * 4 * S / (T[-1] * avgOmega) )
    return Input
def spline(Input, omegaS, omegaG):
    sigmaS = [Input.X1[0][0],  Input.X2[0][0],  Input.X3[0][0]]
    sigmaG = [Input.X1[-1][0], Input.X2[-1][0], Input.X3[-1][0]]
    sigmaDotS = rbk.dMRP(sigmaS, omegaS)
    sigmaDotG = rbk.dMRP(sigmaG, omegaG)
    Input.setXDot_0(sigmaDotS)
    Input.setXDot_N(sigmaDotG)
    Output = BSpline.OutputDataSet()
    BSpline.interpolate(Input, 100, 4, Output)
    return Output
def computeTorque(sigma, sigmaDot, sigmaDDot, I):
    omega = rbk.dMRP2Omega(sigma, sigmaDot)
    omegaDot = rbk.ddMRP2dOmega(sigma, sigmaDot, sigmaDDot)
    return np.matmul(I, omegaDot) + np.cross(omega, np.matmul(I, omega))
def effortEvaluation(Output, I):
    effort = 0
    sigma     = [Output.X1[0][0], Output.X2[0][0], Output.X3[0][0]]
    sigmaDot  = [Output.XD1[0][0], Output.XD2[0][0], Output.XD3[0][0]]
    sigmaDDot = [Output.XDD1[0][0], Output.XDD2[0][0], Output.XDD3[0][0]]
    L_a = computeTorque(sigma, sigmaDot, sigmaDDot, I)
    for n in range(len(Output.T)-1):
        sigma     = [Output.X1[n+1][0], Output.X2[n+1][0], Output.X3[n+1][0]]
        sigmaDot  = [Output.XD1[n+1][0], Output.XD2[n+1][0], Output.XD3[n+1][0]]
        sigmaDDot = [Output.XDD1[n+1][0], Output.XDD2[n+1][0], Output.XDD3[n+1][0]]
        L_b = computeTorque(sigma, sigmaDot, sigmaDDot, I)
        effort += (np.linalg.norm(L_a) + np.linalg.norm(L_b)) * (Output.T[n+1][0] - Output.T[n][0]) / 2
        L_a = L_b
    return effort
def AStar(nodes, n_start, n_goal):
    for key in nodes:
        nodes[key].heuristic = distanceCart(nodes[key], n_goal)
    O = [n_start]
    C = []
    n = 0
    while O[0] != n_goal and n < 10000:
        n += 1
        C.append(O[0])
        for key in O[0].neighbors:
            if nodes[key] not in C:
                p = nodes[key].heuristic + distanceCart(O[0], nodes[key]) + O[0].priority - O[0].heuristic
                if nodes[key] in O:
                    if p < nodes[key].priority:
                        nodes[key].priority = p
                        nodes[key].backpointer = O[0]
                else:
                    nodes[key].priority = p
                    nodes[key].backpointer = O[0]
                    O.append(nodes[key])
        O.pop(0)
        if not O:
            print("Dead end")
        else:
            O.sort(key = lambda x: x.priority)
    path = backtrack(O[0], n_start)
    return path
def effortBasedAStar(nodes, n_start, n_goal, omegaS, omegaG, avgOmega, I):
    O = [n_start]
    C = []
    n = 0
    while O[0] != n_goal and n < 10000:
        n += 1
        print(n)
        C.append(O[0])
        for key in O[0].neighbors:
            if nodes[key] not in C:
                path = backtrack(O[0], n_start)
                path.append(nodes[key])
                if nodes[key] != n_goal:
                    path.append(n_goal)
                Input = pathHandle(path, avgOmega)
                Output = spline(Input, omegaS, omegaG)
                p = effortEvaluation(Output, I)
                if nodes[key] in O:
                    if p < nodes[key].priority:
                        nodes[key].priority = p
                        nodes[key].backpointer = O[0]
                else:
                    nodes[key].priority = p
                    nodes[key].backpointer = O[0]
                    O.append(nodes[key])
        O.pop(0)
        if not O:
            print("Dead end")
        else:
            O.sort(key = lambda x: x.priority)
    path = backtrack(O[0], n_start)
    return path
# 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("N", [5,6])
@pytest.mark.parametrize("keepOutFov", [20])
@pytest.mark.parametrize("keepInFov", [70]) 
@pytest.mark.parametrize("costFcnType", [0,1])
@pytest.mark.parametrize("accuracy", [1e-12])
def test_constrainedAttitudeManeuver(show_plots, N, keepOutFov, keepInFov, costFcnType, accuracy):
    r"""
    **Validation Test Description**
    This unit test script tests the correctness of the path computed by the ConstrainedAttitudeManeuver module.
    The module is tested against Python scripts that mirror the same functions contained in the module. Tests are run for
    different grid coarseness levels, different keep-out fields of view, and one keep-in field of view.
    **Test Parameters**
    Args:
        N (int) : grid coarseness;
        keepOutFov (float) : Field of View (in radiants) of the keep-out boresight;
        keepInFov (float) : Field of View (in radiants) of the keep-in boresight;
        costFcnType (int) : 0 for the minimum MRP cartesian distance graph search, 1 for the effort-based graph search.
        accuracy (float): absolute accuracy value used in the validation tests
    **Description of Variables Being Tested**
    The tests to show the correctness of the module are the following:
    
    - First of all, an equivalent grid is built in python. The first test consists in comparing the nodes generated
      in Python versus the nodes generated in C++. The check consists in verifying whether the same key indices
      :math:`(i,j,k)` generate the same node coordinates :math:`\sigma_{BN}`. Secondly, it is checked whether 
      the same node is constraint-compliant or -incompliant both in Python and in C++.
    - After running the graph-search algorithm, a check is conduced to ensure the equivalence of the computed paths.
      Note that this unit test does not run the effort-based version of A*, due to the slow nature of the Python 
      implementation. If the user wishes, it is possible to uncomment line 429 to also test the effort-based
      graph-search algorithm.
    - The interpolated trajectory obtained in Python is checked versus the interpolated trajectory
      obtained in C++. The Python code uses the BSK-native :ref:`BSpline` library, which has its own unit test.
    - Lastly, a check is run on the norm of the required control torque for each time step of the
      interpolated trajectory.  The correctness of this check should imply the correctness of the
      functions used in the effort-based graph-search algorithm as well.
    """
    # each test method requires a single assert method to be called
    [testResults, testMessage] = CAMTestFunction(N, keepOutFov, keepInFov, costFcnType, accuracy)
    assert testResults < 1, testMessage 
def CAMTestFunction(N, keepOutFov, keepInFov, costFcnType, accuracy):
    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)
    Inertia = [0.02 / 3,  0.,         0.,
               0.,        0.1256 / 3, 0.,
               0.,        0.,         0.1256 / 3]
    InertiaTensor = unitTestSupport.np2EigenMatrix3d(Inertia)
    PlanetInertialPosition = np.array([10, 0, 0])
    SCInertialPosition = np.array([1, 0, 0])
    SCInitialAttitude = np.array([0, 0, -0.5])
    SCTargetAttitude = np.array([0, 0.5, 0])
    SCInitialAngRate = np.array([0, 0, 0])
    SCTargetAngRate = np.array([0, 0, 0])
    SCAvgAngRate = 0.03
    keepOutBoresight_B = [[1, 0, 0]]
    keepInBoresight_B = [[0, 1, 0], [0, 0, 1]]
    # convert Fov angles to radiants
    keepOutFov = keepOutFov * macros.D2R
    keepInFov = keepInFov * macros.D2R
    constraints = {'keepOut' : [], 'keepIn' : []}
    constraints['keepOut'].append( constraint(PlanetInertialPosition-SCInertialPosition, 'r') )
    constraints['keepIn'].append( constraint(PlanetInertialPosition-SCInertialPosition, 'g') )
    data =  {'keepOut_b' : keepOutBoresight_B, 'keepOut_fov' : [keepOutFov], 
             'keepIn_b' : keepInBoresight_B, 'keepIn_fov' : [keepInFov, keepInFov]}
    n_start = node(SCInitialAttitude, constraints, **data)
    n_goal  = node(SCTargetAttitude, constraints, **data)
    nodes = generateGrid(n_start, n_goal, N, constraints, data)
    if costFcnType == 0:
        path = AStar(nodes, n_start, n_goal)
    else:
        path = effortBasedAStar(nodes, n_start, n_goal, SCInitialAngRate, SCTargetAngRate, SCAvgAngRate, InertiaTensor)
    Input = pathHandle(path, SCAvgAngRate)
    Output = spline(Input, SCInitialAngRate, SCTargetAngRate)
    pathCost = effortEvaluation(Output, InertiaTensor)
    # Create a sim module as an empty container
    unitTestSim = SimulationBaseClass.SimBaseClass()
    # Create test thread
    simulationTime = macros.min2nano(2)
    testProcessRate = macros.sec2nano(0.5)     # update process rate update time
    testProc = unitTestSim.CreateNewProcess(unitProcessName)
    testProc.addTask(unitTestSim.CreateNewTask(unitTaskName, testProcessRate))
    
    testModule = constrainedAttitudeManeuver.ConstrainedAttitudeManeuver(N)
    testModule.sigma_BN_goal = SCTargetAttitude
    testModule.omega_BN_B_goal = SCTargetAngRate
    testModule.avgOmega = SCAvgAngRate
    testModule.BSplineType = 0
    testModule.costFcnType = costFcnType
    testModule.appendKeepOutDirection(keepOutBoresight_B[0], keepOutFov)
    testModule.appendKeepInDirection(keepInBoresight_B[0], keepInFov)
    testModule.appendKeepInDirection(keepInBoresight_B[1], keepInFov)
    testModule.ModelTag = "testModule"
    unitTestSim.AddModelToTask(unitTaskName, testModule)
	
    # connect messages
    SCStatesMsgData = messaging.SCStatesMsgPayload()
    SCStatesMsgData.r_BN_N = SCInertialPosition
    SCStatesMsgData.sigma_BN = SCInitialAttitude
    SCStatesMsgData.omega_BN_B = SCInitialAngRate
    SCStatesMsg = messaging.SCStatesMsg().write(SCStatesMsgData)
    VehicleConfigMsgData = messaging.VehicleConfigMsgPayload()
    VehicleConfigMsgData.ISCPntB_B = Inertia
    VehicleConfigMsg = messaging.VehicleConfigMsg().write(VehicleConfigMsgData)
    PlanetStateMsgData = messaging.SpicePlanetStateMsgPayload()
    PlanetStateMsgData.PositionVector = PlanetInertialPosition
    PlanetStateMsg = messaging.SpicePlanetStateMsg().write(PlanetStateMsgData)
    testModule.scStateInMsg.subscribeTo(SCStatesMsg)
    testModule.vehicleConfigInMsg.subscribeTo(VehicleConfigMsg)
    testModule.keepOutCelBodyInMsg.subscribeTo(PlanetStateMsg)
    testModule.keepInCelBodyInMsg.subscribeTo(PlanetStateMsg)
    numDataPoints = 200
    samplingTime = unitTestSupport.samplingTime(simulationTime, testProcessRate, numDataPoints)
    CAMLog = testModule.attRefOutMsg.recorder(samplingTime)
    unitTestSim.AddModelToTask(unitTaskName, CAMLog)
    CAMLogC = testModule.attRefOutMsgC.recorder(samplingTime)
    unitTestSim.AddModelToTask(unitTaskName, CAMLogC)
    # Need to call the self-init and cross-init methods
    unitTestSim.InitializeSimulation()
    # Set the simulation time.
    # NOTE: the total simulation time may be longer than this value. The
    # simulation is stopped at the next logging event on or after the
    # simulation end time.
    unitTestSim.ConfigureStopTime(simulationTime)        # seconds to stop simulation
    # Begin the simulation time run set above
    unitTestSim.ExecuteSimulation()
    timeData = CAMLog.times() * macros.NANO2SEC
    print(timeData)
    # print(len(CAMLog.sigma_RN))
    # check correctness of grid points:
    for i in range(-N,N+1):
        for j in range(-N,N+1):
            for k in range(-N,N+1):
                if (i, j, k) in nodes:
                    sigma_BN = nodes[(i, j, k)].sigma_BN
                    sigma_BN_BSK = []
                    for p in range(3):
                        sigma_BN_BSK.append( testModule.returnNodeCoord([i, j, k], p) )
                    if not unitTestSupport.isVectorEqual(sigma_BN, sigma_BN_BSK, accuracy):
                        testFailCount += 1
                        testMessages.append("FAILED: " + testModule.ModelTag + " Error in the coordinates of node ({},{},{}) \n".format(i, j, k))
                    if not nodes[(i, j, k)].isFree == testModule.returnNodeState([i, j, k]):
                        testFailCount += 1
                        testMessages.append("FAILED: " + testModule.ModelTag + " Error in the state of node ({},{},{}) \n".format(i, j, k))
    # check that the same path is produced
    for p in range(len(path)):
        sigma_BN = path[p].sigma_BN
        sigma_BN_BSK = []
        for j in range(3):
            sigma_BN_BSK.append(testModule.returnPathCoord(p,j))
        if not unitTestSupport.isVectorEqual(sigma_BN, sigma_BN_BSK, accuracy):
            testFailCount += 1
            testMessages.append("FAILED: " + testModule.ModelTag + " Error in waypoint number {} in path \n".format(p))
    # check interpolated path compliance
    for n in range(len(Output.T)):
        T_BSK = testModule.Output.T[n][0]
        sigma_BSK = np.array([testModule.Output.X1[n][0], testModule.Output.X2[n][0], testModule.Output.X3[n][0]])
        sigmaDot_BSK = np.array([testModule.Output.XD1[n][0], testModule.Output.XD2[n][0], testModule.Output.XD3[n][0]])
        sigmaDDot_BSK = np.array([testModule.Output.XDD1[n][0], testModule.Output.XDD2[n][0], testModule.Output.XDD3[n][0]])
        T         = Output.T[n][0]
        sigma     = np.array([Output.X1[n][0], Output.X2[n][0], Output.X3[n][0]])
        sigmaDot  = np.array([Output.XD1[n][0], Output.XD2[n][0], Output.XD3[n][0]])
        sigmaDDot = np.array([Output.XDD1[n][0], Output.XDD2[n][0], Output.XDD3[n][0]])
        if not unitTestSupport.isDoubleEqual(T, T_BSK, accuracy):
            testFailCount += 1
            testMessages.append("FAILED: " + testModule.ModelTag + " Error in time at index #{} in trajectory \n".format(n))
        if not unitTestSupport.isVectorEqual(sigma, sigma_BSK, accuracy):
            testFailCount += 1
            testMessages.append("FAILED: " + testModule.ModelTag + " Error in sigma at index #{} in trajectory \n".format(n))
        if not unitTestSupport.isVectorEqual(sigmaDot, sigmaDot_BSK, accuracy):
            testFailCount += 1
            testMessages.append("FAILED: " + testModule.ModelTag + " Error in sigmaDot at index #{} in trajectory \n".format(n))
        if not unitTestSupport.isVectorEqual(sigmaDDot, sigmaDDot_BSK, accuracy):
            testFailCount += 1
            testMessages.append("FAILED: " + testModule.ModelTag + " Error in sigmaDDot at index #{} in trajectory \n".format(n))
    # check same path cost for every spline point
    for n in range(len(Output.T)):
        c1 = testModule.computeTorqueNorm(n, Inertia)
        sigma     = [Output.X1[n][0], Output.X2[n][0], Output.X3[n][0]]
        sigmaDot  = [Output.XD1[n][0], Output.XD2[n][0], Output.XD3[n][0]]
        sigmaDDot = [Output.XDD1[n][0], Output.XDD2[n][0], Output.XDD3[n][0]]
        c2 = np.linalg.norm(computeTorque(sigma, sigmaDot, sigmaDDot, InertiaTensor))
        if not unitTestSupport.isDoubleEqual(c1, c2, accuracy):
            testFailCount += 1
            testMessages.append("FAILED: " + testModule.ModelTag + " Error torque norm in point {} in trajectory \n".format(n))
    if not unitTestSupport.isDoubleEqual(pathCost, testModule.pathCost, accuracy):
        testFailCount += 1
        testMessages.append("FAILED: " + testModule.ModelTag + " Error in path cost \n")
    # check correctness of output message
    for n in range(len(timeData)):
        t = timeData[n]
        sigma_RN     = [Output.getStates(t,0,0), Output.getStates(t,0,1), Output.getStates(t,0,2)]
        sigmaDot_RN  = [Output.getStates(t,1,0), Output.getStates(t,1,1), Output.getStates(t,1,2)]
        sigmaDDot_RN = [Output.getStates(t,2,0), Output.getStates(t,2,1), Output.getStates(t,2,2)]
        RN = rbk.MRP2C(sigma_RN)
        omega_RN_R = rbk.dMRP2Omega(sigma_RN, sigmaDot_RN)
        omega_RN_N = np.matmul(omega_RN_R, RN)
        omegaDot_RN_R = rbk.ddMRP2dOmega(sigma_RN, sigmaDot_RN, sigmaDDot_RN)
        omegaDot_RN_N = np.matmul(omegaDot_RN_R, RN)
        if not unitTestSupport.isVectorEqual(sigma_RN, CAMLog.sigma_RN[n], accuracy):
            testFailCount += 1
            testMessages.append("FAILED: " + testModule.ModelTag + " Error in attitude reference message at t = {} sec \n".format(t))
        if not unitTestSupport.isVectorEqual(omega_RN_N, CAMLog.omega_RN_N[n], accuracy):
            testFailCount += 1
            testMessages.append("FAILED: " + testModule.ModelTag + " Error in attitude reference message at t = {} sec \n".format(t))
        if not unitTestSupport.isVectorEqual(omegaDot_RN_N, CAMLog.domega_RN_N[n], accuracy):
            testFailCount += 1
            testMessages.append("FAILED: " + testModule.ModelTag + " Error in attitude reference message at t = {} sec \n".format(t))
        if not unitTestSupport.isVectorEqual(sigma_RN, CAMLogC.sigma_RN[n], accuracy):
            testFailCount += 1
            testMessages.append("FAILED: " + testModule.ModelTag + " Error in C attitude reference message at t = {} sec \n".format(t))
        if not unitTestSupport.isVectorEqual(omega_RN_N, CAMLogC.omega_RN_N[n], accuracy):
            testFailCount += 1
            testMessages.append("FAILED: " + testModule.ModelTag + " Error in C attitude reference message at t = {} sec \n".format(t))
        if not unitTestSupport.isVectorEqual(omegaDot_RN_N, CAMLogC.domega_RN_N[n], accuracy):
            testFailCount += 1
            testMessages.append("FAILED: " + testModule.ModelTag + " Error in C attitude reference message at t = {} sec \n".format(t)) 
    return [testFailCount, ''.join(testMessages)]
	   
#
# This statement below ensures that the unitTestScript can be run as a
# stand-along python script
#
if __name__ == "__main__":
    CAMTestFunction(
        5,      # grid coarsness N
        20,      # keepOutFov
        70,      # keepInFov
        1,       # costFcnType
        1e-12    # accuracy
        )