# 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.
#
# Thruster Unit Test
#
# Purpose:  Test the proper function of the Thruster Dynamics module.
#           This is done by comparing expected torques and forces to
#           what is simulated
# Author:   Thibaud Teil
# Creation Date:  Dec. 20, 2016
#
import inspect
import math
# @cond DOXYGEN_IGNORE
import os
import numpy as np
import pytest
filename = inspect.getframeinfo(inspect.currentframe()).filename
path = os.path.dirname(os.path.abspath(filename))
splitPath = path.split('simulation')
# @endcond
#Import all of the modules that we are going to call in this simulation
from Basilisk.utilities import unitTestSupport
import matplotlib.pyplot as plt
from Basilisk.utilities import SimulationBaseClass
from Basilisk.simulation import thrusterDynamicEffector
from Basilisk.simulation import stateArchitecture
from Basilisk.simulation import spacecraft
from Basilisk.utilities import macros
from Basilisk.architecture import messaging
class ResultsStore:
    def __init__(self):
        self.PassFail = []
    def texSnippet(self):
        for i in range(len(self.PassFail)):
            snippetName = 'Result' + str(i)
            if self.PassFail[i] == 'PASSED':
                textColor = 'ForestGreen'
            elif self.PassFail[i] == 'FAILED':
                textColor = 'Red'
            texSnippet =  r'\textcolor{' + textColor + '}{'+ self.PassFail[i] + '}'
            unitTestSupport.writeTeXSnippet(snippetName, texSnippet, path)
@pytest.fixture(scope="module")
def testFixture():
    listRes = ResultsStore()
    yield listRes
    listRes.texSnippet()
def thrusterEffectorAllTests(show_plots):
   [testResults, testMessage] = test_unitThrusters(show_plots)
# Create function to run the simulation who's results will be compared to expected values
def executeSimRun(simContainer, thrusterSet, simRate, totalTime):
    newStopTime = simContainer.TotalSim.CurrentNanos + totalTime
    while(simContainer.TotalSim.CurrentNanos < newStopTime):
        simContainer.ConfigureStopTime(simContainer.TotalSim.CurrentNanos + simRate)
        simContainer.ExecuteSimulation()
        timeStep = 1.0  # not explicity used in this test
        thrusterSet.computeForceTorque(simContainer.TotalSim.CurrentNanos*macros.NANO2SEC, timeStep)
        thrusterSet.computeForceTorque(simContainer.TotalSim.CurrentNanos*macros.NANO2SEC + simRate*macros.NANO2SEC/2.0, timeStep)
        thrusterSet.computeForceTorque(simContainer.TotalSim.CurrentNanos * macros.NANO2SEC + simRate * macros.NANO2SEC / 2.0, timeStep)
        thrusterSet.computeForceTorque(simContainer.TotalSim.CurrentNanos*macros.NANO2SEC + simRate*macros.NANO2SEC, timeStep)
        thrusterSet.computeStateContribution(simContainer.TotalSim.CurrentNanos * macros.NANO2SEC)
        thrusterSet.computeStateContribution(
            simContainer.TotalSim.CurrentNanos * macros.NANO2SEC + simRate * macros.NANO2SEC / 2.0)
        thrusterSet.computeStateContribution(
            simContainer.TotalSim.CurrentNanos * macros.NANO2SEC + simRate * macros.NANO2SEC / 2.0)
        thrusterSet.computeStateContribution(
            simContainer.TotalSim.CurrentNanos * macros.NANO2SEC + simRate * macros.NANO2SEC)
[docs]def fixMDotData(mDotData):
    """This test was written before a bug in variable logging was fixed.
    
    This bug made it so consecutive logged zeros would get removed, which
    is why we need to remove all zero rows at the beginning of mDotData
    but one.
    """
    firstNonZeroRow = np.nonzero(mDotData[:,1])[0][0]
    return np.row_stack([[0,0], mDotData[firstNonZeroRow:, :]]) 
# uncomment this line if this test has an expected failure, adjust message as needed
# @pytest.mark.xfail(True)
[docs]@pytest.mark.parametrize("ramp, thrustNumber , duration , long_angle, lat_angle, location, rate, cutoff, rampDown, swirlTorque", [
    ("OFF", 1, 5.0, 30., 15., [[1.125], [0.5], [2.0]], 1E8, "OFF", "OFF", 0.0),  #Test random thrust config
    ("OFF", 1, 0.1, 30.,  15., [[1.125], [0.5], [2.0]], 1E8, "OFF", "OFF", 0.0),  # Short fire test
    ("OFF", 1, 0.1, 30.,  15., [[1.125], [0.5], [2.0]], 1E6, "OFF", "OFF", 0.0),  # Short fire test with higher test rate
    ("OFF", 1, 5.0, 30.,  15., [[1.125], [0.5], [2.0]], 1E7, "OFF", "OFF", 0.0),  # rate test
    ("OFF", 1, 5.0, 10.,  35., [[1.125], [0.5], [2.0]], 1E8, "OFF", "OFF", 0.0),  # angle test
    ("OFF", 1, 5.0, 30.,  15., [[1.], [1.5], [0.0]], 1E8, "OFF", "OFF", 0.0),  # Position test
    ("OFF", 2, 5.0, 30.,  15., [[1.125], [0.5], [2.0]], 1E8, "OFF", "OFF", 0.0),  # Number of thrusters test
    ("ON", 1, 5.0, 30.,  15., [[1.125], [0.5], [2.0]], 1E8, "OFF", "OFF", 0.0),  # Basic ramp test
    ("ON", 1, 0.5, 30.,  15., [[1.125], [0.5], [2.0]], 1E8, "OFF", "OFF", 0.0),  # Short ramp test
    ("ON", 1, 5.0, 30.,  15., [[1.125], [0.5], [2.0]], 1E7, "OFF", "OFF", 0.0),  # rate ramp test
    ("ON", 1, 5.0, 30.,  15., [[1.125], [0.5], [2.0]], 1E8, "ON", "OFF", 0.0),  # Cuttoff test
    ("ON", 1, 5.0, 30.,  15., [[1.125], [0.5], [2.0]], 1E8, "ON", "ON", 0.0),  # Ramp down test
    ("OFF", 1, 5.0, 30.,  15., [[1.125], [0.5], [2.0]], 1E8, "OFF", "OFF", 2.0),  # Simple swirl torque test
    ("ON", 1, 5.0, 30., 15., [[1.125], [0.5], [2.0]], 1E8, "OFF", "OFF", 0.5),  # Basic ramp with swirl torque test
    ])
# provide a unique test method name, starting with test_
def test_unitThrusters(testFixture, show_plots, ramp, thrustNumber , duration ,  long_angle, lat_angle,  location, rate, cutoff, rampDown, swirlTorque):
    """Module Unit Test"""
    # each test method requires a single assert method to be called
    [testResults, testMessage] = unitThrusters(testFixture, show_plots, ramp, thrustNumber , duration  ,  long_angle, lat_angle , location, rate, cutoff, rampDown, swirlTorque)
    assert testResults < 1, testMessage 
# Run the test
def unitThrusters(testFixture, show_plots, ramp, thrustNumber , duration  ,  long_angle, lat_angle, location, rate, cutoff, rampDown, swirlTorque):
    # The __tracebackhide__ setting influences pytest showing of tracebacks:
    # the mrp_steering_tracking() function will not be shown unless the
    # --fulltrace command line option is specified.
    __tracebackhide__ = True
    testFailCount = 0  # zero unit test result counter
    testMessages = []  # create empty list to store test log messages
    unitTaskName = "unitTask"               # arbitrary name (don't change)
    unitProcessName = "TestProcess"         # arbitrary name (don't change)
    # Constants
    g = 9.80665
    Isp = 226.7
    #  Create thrusters
    thrusterSet = thrusterDynamicEffector.ThrusterDynamicEffector()
    thrusterSet.ModelTag = "ACSThrusterDynamics"
    #  Create thruster characteristic parameters (position, angle thrust, ISP, time of thrust)
    angledeg_long = long_angle # Parametrized angle of thrust
    angledeg_lat = lat_angle
    anglerad_long = angledeg_long * math.pi/180.0
    anglerad_lat = angledeg_lat * math.pi / 180.0
    thruster1 = thrusterDynamicEffector.THRSimConfig()
    thruster1.thrLoc_B = location # Parametrized location for thruster
    thruster1.thrDir_B = [[math.cos(anglerad_long)*math.cos(anglerad_lat)], [math.sin(anglerad_long)*math.cos(anglerad_lat)], [math.sin(anglerad_lat)]]
    thruster1.MaxThrust = 1.0
    thruster1.steadyIsp = 226.7
    thruster1.MinOnTime = 0.006
    thruster1.MaxSwirlTorque = swirlTorque
    thrusterSet.addThruster(thruster1)
    loc1 = np.array([thruster1.thrLoc_B[0][0],thruster1.thrLoc_B[1][0],thruster1.thrLoc_B[2][0]])
    dir1 = np.array([thruster1.thrDir_B[0][0], thruster1.thrDir_B[1][0], thruster1.thrDir_B[2][0]])
    if thrustNumber==2:
        thruster2 = thrusterDynamicEffector.THRSimConfig()
        thruster2.thrLoc_B =np.array([[1.], [0.0], [0.0]]).reshape([3,1])
        thruster2.thrDir_B = np.array([[math.cos(anglerad_long+math.pi/4.)*math.cos(anglerad_lat-math.pi/4.)], [math.sin(anglerad_long+math.pi/4.)*math.cos(anglerad_lat-math.pi/4.)], [math.sin(anglerad_lat-math.pi/4.)]]).reshape([3,1])
        thruster2.MaxThrust = 1.0
        thruster2.steadyIsp = 226.7
        thruster2.MinOnTime = 0.006
        thrusterSet.addThruster(thruster2)
        loc2 = np.array([thruster2.thrLoc_B[0][0],thruster2.thrLoc_B[1][0],thruster2.thrLoc_B[2][0]])
        dir2 = np.array([thruster2.thrDir_B[0][0], thruster2.thrDir_B[1][0], thruster2.thrDir_B[2][0]])
    #  Create a Simulation
    testRate = int(rate) # Parametrized rate of test
    TotalSim = SimulationBaseClass.SimBaseClass()
    # Create a new process for the unit test task and add the module to tasking
    testProc = TotalSim.CreateNewProcess(unitProcessName)
    testProc.addTask(TotalSim.CreateNewTask(unitTaskName, testRate))
    TotalSim.AddModelToTask(unitTaskName, thrusterSet)
    TotalSim.scObject = spacecraft.Spacecraft()
    TotalSim.scObject.ModelTag = "spacecraftBody"
    #  Create a task manager
    TotalSim.newManager = stateArchitecture.DynParamManager()
    # TotalSim.AddModelToTask(unitTaskName, TotalSim.scObject)
    #  Define the start of the thrust and it's duration
    sparetime = 3.*1./macros.NANO2SEC
    thrStartTime=sparetime
    thrDurationTime=duration*1./macros.NANO2SEC # Parametrized thrust duration
    #Configure a single thruster firing, create a message for it
    thrusterSetLog = thrusterSet.logger(["forceExternal_B", "torqueExternalPntB_B", "mDotTotal"])
    TotalSim.AddModelToTask(unitTaskName, thrusterSetLog)
    ThrustMessage = messaging.THRArrayOnTimeCmdMsgPayload()
    if thrustNumber==1:
        ThrustMessage.OnTimeRequest = [0.]
    if thrustNumber==2:
        ThrustMessage.OnTimeRequest = [0., 0.]
    thrCmdMsg = messaging.THRArrayOnTimeCmdMsg().write(ThrustMessage)
    thrusterSet.cmdsInMsg.subscribeTo(thrCmdMsg)
    TotalSim.InitializeSimulation()
    #Configure the hub and link states
    TotalSim.newManager.createProperty("r_BN_N", [[0], [0], [0]])  # manually create the property
    TotalSim.scObject.hub.registerStates(TotalSim.newManager)
    thrusterSet.linkInStates(TotalSim.newManager)
    plt.close("all")
    if ramp == "OFF":
        # Run the simulation
        executeSimRun(TotalSim, thrusterSet, testRate, int(thrStartTime))
        if thrustNumber==1:
            ThrustMessage.OnTimeRequest = [thrDurationTime*macros.NANO2SEC]
        if thrustNumber==2:
            ThrustMessage.OnTimeRequest = [thrDurationTime * macros.NANO2SEC, thrDurationTime * macros.NANO2SEC]
        thrCmdMsg.write(ThrustMessage, TotalSim.TotalSim.CurrentNanos+testRate)
        executeSimRun(TotalSim, thrusterSet, testRate, int(thrDurationTime+sparetime))
        # Gather the Force and Torque results
        thrForce = unitTestSupport.addTimeColumn(thrusterSetLog.times(), thrusterSetLog.forceExternal_B)
        thrTorque = unitTestSupport.addTimeColumn(thrusterSetLog.times(), thrusterSetLog.torqueExternalPntB_B)
        mDotData = unitTestSupport.addTimeColumn(thrusterSetLog.times(), thrusterSetLog.mDotTotal)
        mDotData = fixMDotData(mDotData)
        # Auto Generate LaTex Figures
        format = r"width=0.8\textwidth"
        snippetName = "Snippet" + str(thrustNumber) + "Thrusters_" +  str(int(duration))+ "s_" +\
                      str(int(long_angle))+"deg_"+ "Loc"+ str(int(loc1[2])) + "_Rate"+str(int(1./(testRate*macros.NANO2SEC)))
        if thrustNumber==1:
            texSnippet = "The thruster is set at " +str(int(long_angle))+r"$^\circ$ off the x-axis " +str(int(lat_angle))+r"$^\circ$ off the z-axis, in the position $\bm r = \left("+\
                         str(loc1[0])+","+str(loc1[1])+"," +str(loc1[2])+ \
                         r"\right)$. The test is launched using " + str(thrustNumber) + " thruster, for " + \
                         str(duration)+ " seconds. The test rate is " + str(int(1./(testRate*macros.NANO2SEC))) + " steps per second"
        if thrustNumber==2:
            texSnippet = "The first thruster is set at " + str(int(long_angle)) + r"$^\circ$ off the x-axis " + str(
                int(lat_angle)) + r"$^\circ$ off the z-axis, in the position $\bm r = \left(" + \
                         str(loc1[0]) + "," + str(loc1[1]) + "," + str(loc1[2]) + \
                         r"\right)$. The second thruster is set at " + str(int(long_angle+45)) + r"$^\circ$ off the x-axis " + str(
                int(lat_angle+45)) + r"$^\circ$ off the z-axis, in the position $\bm r = \left(" + \
                         str(loc2[0]) + "," + str(loc2[1]) + "," + str(loc2[2]) + \
                         r"\right)$. The test uses these " + str(thrustNumber) + " thrusters for " + \
                         str(duration) + " seconds. The test rate is " + str(
                int(1. / (testRate * macros.NANO2SEC))) + " steps per second"
        unitTestSupport.writeTeXSnippet(snippetName, texSnippet, path)
        PlotName = "Force_" +  str(thrustNumber) + "Thrusters_" +  str(int(duration))+ "s_" +str(int(long_angle))+"deg_"+ "Loc"+str(int(location[2][0]))+ "_Rate"+str(int(1./(testRate*macros.NANO2SEC)))
        PlotTitle = "Force on Y with " + str(thrustNumber) + " thrusters, for "  +  str(int(duration))+ " sec at " +str(int(long_angle))+" deg "+ "Rate"+str(int(1./(testRate*macros.NANO2SEC)))
        plt.close("all")
        plt.figure(1)
        plt.clf()
        plt.plot(thrForce[:,0]*macros.NANO2SEC, thrForce[:,2])
        plt.xlabel('Time(s)')
        plt.ylabel('Thrust Factor (N)')
        plt.ylim(-0.2,1)
        unitTestSupport.writeFigureLaTeX(PlotName, PlotTitle, plt, format, path)
        if show_plots==True:
            plt.show()
            plt.close('all')
        PlotName = "Torque_" +  str(thrustNumber) + "Thrusters_" +  str(int(duration))+ "s_" + str(int(long_angle))+"deg_"+ "Loc"+str(int(location[2][0]))+ "_Rate"+str(int(1./(testRate*macros.NANO2SEC)))
        PlotTitle = "Torque on X with " + str(thrustNumber) + " thrusters, for "  +  str(int(duration))+ " sec at " + str(int(long_angle))+" deg " + "Rate"+str(int(1./(testRate*macros.NANO2SEC)))
        plt.figure(11)
        plt.clf()
        plt.plot(thrForce[:,0]*macros.NANO2SEC, thrTorque[:,1])
        plt.xlabel('Time(s)')
        plt.ylabel('Thrust Torque (Nm)')
        plt.ylim(-1.5, 2)
        unitTestSupport.writeFigureLaTeX(PlotName, PlotTitle, plt, format, path)
        if show_plots==True:
            plt.show()
            plt.close('all')
        PlotName =  str(thrustNumber) + "Thrusters_" +  str(int(duration))+ "s_" + str(int(long_angle))+"deg_"+ "Loc"+str(int(location[2][0]))+ "_Rate"+str(int(1./(testRate*macros.NANO2SEC)))
        PlotTitle = "All Forces and Torques " + str(thrustNumber) + " thrusters, for "  +  str(int(duration))+ " sec at " + str(int(long_angle))+" deg "+ "Rate"+str(int(1./(testRate*macros.NANO2SEC)))
        plt.figure(22)
        plt.clf()
        plt.plot(thrForce[:,0]*1.0E-9, thrForce[:,1], 'b', label='x Force')
        plt.plot(thrTorque[:,0]*1.0E-9, thrTorque[:,1], 'b--', label='x Torque')
        plt.plot(thrForce[:,0]*1.0E-9, thrForce[:,2], 'g', label='y Force')
        plt.plot(thrTorque[:,0]*1.0E-9, thrTorque[:,2], 'g--', label='y Torque')
        plt.plot(thrForce[:,0]*1.0E-9, thrForce[:,3], 'r', label = 'z Force')
        plt.plot(thrTorque[:,0]*1.0E-9, thrTorque[:,3], 'r--', label='z Torque')
        plt.legend(loc='upper right')
        plt.xlabel('Time(s)')
        plt.ylim(-1.5, 2)
        plt.legend(loc='upper right')
        unitTestSupport.writeFigureLaTeX(PlotName, PlotTitle, plt, format, path)
        if show_plots==True:
            plt.show()
            plt.close('all')
        # Create expected Force to test against thrForce
        expMDot = np.zeros([np.shape(np.array(mDotData))[0],1])
        mDotData = np.delete(mDotData, 0, axis=1)
        for i in range(np.shape(np.array(mDotData))[0]):
            if (i > 0 and i < int(round((thrDurationTime) / testRate)) + 1):
                expMDot[i, 0] = thrustNumber / (g * Isp)
        expectedpoints=np.zeros([3,np.shape(thrForce)[0]])
        for i in range(np.shape(thrForce)[0]):# Thrust fires 2 times steps after the pause of sim and restart
            if (i>int(round(thrStartTime/ testRate)) + 1 and i<int(round((thrStartTime+thrDurationTime)/ testRate)) + 2):
                if thrustNumber == 1:
                    expectedpoints[0:3,i] = dir1
                else:
                    expectedpoints[0:3, i] = dir1 + dir2
        # Modify expected values for comparison and define errorTolerance
        TruthForce = np.transpose(expectedpoints)
        ErrTolerance = 10E-9
        # Compare Force values
        thrForce = np.delete(thrForce, 0, axis=1)  # remove time column
        testFailCount, testMessages = unitTestSupport.compareArray(TruthForce, thrForce, ErrTolerance, "Force", testFailCount, testMessages)
        for i in range(0, len(np.array(mDotData))):
            if not unitTestSupport.isArrayEqual(np.array(mDotData)[i,:], expMDot[i,:], 1, ErrTolerance):
                testFailCount+=1
                testMessages.append('M dot failure')
        # Create expected Torque to test against thrTorque
        expectedpointstor = np.zeros([3, np.shape(thrTorque)[0]])
        for i in range(np.shape(thrForce)[0]): # Thrust fires 2 times steps after the pause of sim and restart
            if (i>int(round(thrStartTime/ testRate)) + 1 and i<int(round((thrStartTime+thrDurationTime)/ testRate)) + 2):
                if thrustNumber == 1:
                    expectedpointstor[0:3, i] = np.cross(loc1, dir1) + swirlTorque * dir1
                else:
                    expectedpointstor[0:3, i] = np.cross(loc1, dir1)  + swirlTorque * dir1 + np.cross(loc2, dir2)
        # Define errorTolerance
        TruthTorque = np.transpose(expectedpointstor)
        ErrTolerance = 10E-9
        # Compare Torque values
        # Compare Force values
        thrTorque = np.delete(thrTorque, 0, axis=1)  # remove time column
        testFailCount, testMessages = unitTestSupport.compareArray(TruthTorque, thrTorque, ErrTolerance, "Torque", testFailCount, testMessages)
    if ramp == "ON":
        format = r"width=0.8\textwidth"
        rampsteps = 10
        sparetime = 3.*1/macros.NANO2SEC
        thrStartTime = sparetime - 1.*1/macros.NANO2SEC
        # Setup thruster ramp on and ramp off configuration
        rampOnList = []
        rampOffList = []
        # Note that this ramp is totally linear and ramps up 30 ms using 30 steps
        for i in range(rampsteps):
            newElement = thrusterDynamicEffector.THRTimePair()
            newElement.TimeDelta = (i + 1.) * 0.1
            newElement.ThrustFactor = (i + 1.0) / 10.0
            newElement.IspFactor = (i + 1.0) / 10.0
            rampOnList.append(newElement)
            newElement = thrusterDynamicEffector.THRTimePair()
            newElement.TimeDelta = (i + 1) * 0.1
            newElement.ThrustFactor = 1.0 - (i + 1.0) / 10.0
            newElement.IspFactor = newElement.ThrustFactor
            rampOffList.append(newElement)
        # Set up the ramps
        thrusterSet.thrusterData[0].ThrusterOnRamp = \
            thrusterDynamicEffector.ThrusterTimeVector(rampOnList)
        thrusterSet.thrusterData[0].ThrusterOffRamp = \
            thrusterDynamicEffector.ThrusterTimeVector(rampOffList)
        if rampDown == "OFF":
            if cutoff == "OFF":
                # Execute a new firing that will use the thruster ramps
                executeSimRun(TotalSim, thrusterSet, testRate, int(thrStartTime))
                ThrustMessage.OnTimeRequest =  [thrDurationTime*macros.NANO2SEC]
                thrCmdMsg.write(ThrustMessage, TotalSim.TotalSim.CurrentNanos+testRate)
                executeSimRun(TotalSim, thrusterSet, testRate, int(thrDurationTime+sparetime))
                # Extract log variables and plot the results
                thrForce = unitTestSupport.addTimeColumn(thrusterSetLog.times(), thrusterSetLog.forceExternal_B)
                thrTorque = unitTestSupport.addTimeColumn(thrusterSetLog.times(), thrusterSetLog.torqueExternalPntB_B)
                mDotData = unitTestSupport.addTimeColumn(thrusterSetLog.times(), thrusterSetLog.mDotTotal)
                mDotData = fixMDotData(mDotData)
                snippetName = "Snippet" + "Ramp_" + str(rampsteps) +"steps_" + str(int(duration)) + "s"+  "_Cutoff" + cutoff + "_Rate" + str(
                    int(1. / (testRate * macros.NANO2SEC))) + "_Cutoff" + cutoff
                texSnippet = "We test the ramped thrust with " + str(rampsteps) + " incremental steps. The single thruster is set at the default " +str(int(long_angle))+r"$^\circ$ off the x-axis " +str(int(lat_angle))+r"$^\circ$ off the z-axis, at $\bm r = \left(" + \
                             str(loc1[0]) + "," + str(loc1[1]) + "," + str(loc1[2]) + \
                             r"\right)$. The thrust is set for " + \
                             str(duration) + " seconds with a test rate of " + str(
                    int(1. / (testRate * macros.NANO2SEC))) + " steps per second. The Cutoff test is " + cutoff
                unitTestSupport.writeTeXSnippet(snippetName, texSnippet, path)
                PlotName = "Ramp_" + str(rampsteps) + "steps_Cutoff" + cutoff +"_" + str(int(duration)) + "s"+"_testRate" + str(
                int(1. / (testRate * macros.NANO2SEC)))
                PlotTitle = "All Forces and Torques with " + str(rampsteps) + " step Ramp, thrust for " + str(int(duration)) + "s. Cutoff " + cutoff+", testRate" + str(
                int(1. / (testRate * macros.NANO2SEC)))
                plt.figure(22)
                plt.clf()
                plt.plot(thrForce[:, 0] * 1.0E-9, thrForce[:, 1], 'b', label='x Force')
                plt.plot(thrTorque[:, 0] * 1.0E-9, thrTorque[:, 1], 'b--', label='x Torque')
                plt.plot(thrForce[:, 0] * 1.0E-9, thrForce[:, 2], 'g', label='y Force')
                plt.plot(thrTorque[:, 0] * 1.0E-9, thrTorque[:, 2], 'g--', label='y Torque')
                plt.plot(thrForce[:, 0] * 1.0E-9, thrForce[:, 3], 'r', label='z Force')
                plt.plot(thrTorque[:, 0] * 1.0E-9, thrTorque[:, 3], 'r--', label='z Torque')
                plt.legend(loc='upper right')
                plt.xlabel('Time(s)')
                plt.ylim(-1.5, 2)
                plt.legend(loc='upper left')
                unitTestSupport.writeFigureLaTeX(PlotName, PlotTitle, plt, format, path)
                if show_plots == True:
                    plt.show()
                    plt.close('all')
                # Create expected Force to test against thrForce
                expectedpoints = np.zeros([3, np.shape(thrForce)[0]])
                RampFunction= np.zeros([np.shape(thrForce)[0]])
                ramplength = 1.
                if ramplength < thrDurationTime*macros.NANO2SEC:
                    for i in range(np.shape(thrForce)[0]):
                        if i<int(round(thrStartTime / testRate)) + 2:
                            RampFunction[i] = 0.0
                        if (i > int(round(thrStartTime / testRate)) + 1 and i < int(round((thrStartTime + ramplength*1.0/macros.NANO2SEC)/ testRate)) + 2) : #ramp up
                            RampFunction[i] = (i-int(round(thrStartTime / testRate)) - 2 + 1.0) * (macros.NANO2SEC*testRate)
                        if (i > int(round((thrStartTime + ramplength*1.0/macros.NANO2SEC)/ testRate)) + 1 and i < int(round((thrStartTime + thrDurationTime) / testRate)) + 2):
                            RampFunction[i]=1.0
                        if (i > int(round((thrStartTime + thrDurationTime) / testRate)) + 1 and i < int(round((thrStartTime + thrDurationTime+ ramplength*1.0/macros.NANO2SEC) / testRate)) + 2):
                            RampFunction[i] = 1.0 - (i - int(round((thrStartTime + thrDurationTime) / testRate))-2 + 1.0) *(macros.NANO2SEC*testRate)
                        if (i > int(round((thrStartTime + thrDurationTime+ ramplength*1.0/macros.NANO2SEC) / testRate)) + 1):
                            RampFunction[i] = 0.
                else:
                    for i in range(np.shape(thrForce)[0]):
                        if i<int(round(thrStartTime / testRate)) + 2:
                            RampFunction[i] = 0.0
                        if (i > int(round(thrStartTime / testRate)) + 1 and i < int(round((thrStartTime + thrDurationTime)/ testRate)) + 2) : #ramp up
                            RampFunction[i] = (i-int(round(thrStartTime / testRate)) - 2 + 1.0) * (macros.NANO2SEC*testRate)
                        if (i > int(round((thrStartTime + thrDurationTime) / testRate)) + 1 and i < int(round((thrStartTime + 2*thrDurationTime) / testRate)) + 2):
                            RampFunction[i] = RampFunction[int(round((thrStartTime + thrDurationTime) / testRate)) + 1] - (i - int(round((thrStartTime + thrDurationTime) / testRate))-2 + 1.0) *(macros.NANO2SEC*testRate)
                        if (i > int(round((thrStartTime + thrDurationTime+ ramplength*1.0/macros.NANO2SEC) / testRate)) + 1):
                            RampFunction[i] = 0.
                # Create expected Force to test against thrForce
                expMDot = np.zeros([np.shape(np.array(mDotData))[0], 1])
                for i in range(np.shape(RampFunction)[0]- (int(round(thrStartTime/testRate))+1)):
                    if (i>0 and RampFunction[i + int(round(thrStartTime/testRate))+1]!=0.):
                        expMDot[i, 0] = thrustNumber / (g * Isp)
                for i in range(np.shape(thrForce)[0]): # Thrust fires 2 times steps after the pause of sim and restart
                    if (i > int(round(thrStartTime / testRate)) + 1 and i < int(round((thrStartTime + thrDurationTime+ ramplength*1.0/macros.NANO2SEC) / testRate)) + 2):
                        expectedpoints[0:3, i] = dir1*RampFunction[i]
                # Modify expected values for comparison and define errorTolerance
                TruthForce = np.transpose(expectedpoints)
                ErrTolerance = 10E-9
                # Compare Force values and M-dot
                thrForce = np.delete(thrForce, 0, axis=1)  # remove time column
                testFailCount, testMessages = unitTestSupport.compareArray(TruthForce, thrForce, ErrTolerance, "Force",
                                                                           testFailCount, testMessages)
                mDotData = np.delete(mDotData, 0, axis=1)  # remove time column
                for i in range(0, len(np.array(mDotData))):
                    if not unitTestSupport.isArrayEqual(np.array(mDotData)[i, :], expMDot[i, :], 1, ErrTolerance):
                        testFailCount += 1
                        testMessages.append('M dot failure')
                # Create expected Torque to test against thrTorque
                expectedpointstor = np.zeros([3, np.shape(thrTorque)[0]])
                for i in range(np.shape(thrForce)[0]):  # Thrust fires 2 times steps after the pause of sim and restart
                    if (i > int(round(thrStartTime / testRate)) + 1 and i < int(round((thrStartTime + thrDurationTime+ ramplength*1.0/macros.NANO2SEC) / testRate)) + 2):
                            expectedpointstor[0:3, i] = (np.cross(loc1,dir1) + swirlTorque * dir1) * RampFunction[i]
                # Define errorTolerance
                TruthTorque = np.transpose(expectedpointstor)
                ErrTolerance = 10E-9
                # Compare Torque values
                # Compare Force values
                thrTorque = np.delete(thrTorque, 0, axis=1)  # remove time column
                testFailCount, testMessages = unitTestSupport.compareArray(TruthTorque, thrTorque, ErrTolerance,
                                                                           "Torque", testFailCount, testMessages)
            if cutoff == "ON":
                COtime = 0.2
                COrestart = 0.3
                executeSimRun(TotalSim, thrusterSet, testRate, int(thrStartTime))
                ThrustMessage.OnTimeRequest = [COtime * 10.]
                thrCmdMsg.write(ThrustMessage, TotalSim.TotalSim.CurrentNanos+testRate)
                executeSimRun(TotalSim, thrusterSet, testRate, int(COtime * 1.0 / macros.NANO2SEC))
                ThrustMessage.OnTimeRequest = [COrestart]
                thrCmdMsg.write(ThrustMessage, TotalSim.TotalSim.CurrentNanos+testRate)
                executeSimRun(TotalSim, thrusterSet, testRate, int(COrestart * 1.0 / macros.NANO2SEC + sparetime))
                # Extract log variables and plot the results
                thrForce = unitTestSupport.addTimeColumn(thrusterSetLog.times(), thrusterSetLog.forceExternal_B)
                thrTorque = unitTestSupport.addTimeColumn(thrusterSetLog.times(), thrusterSetLog.torqueExternalPntB_B)
                mDotData = unitTestSupport.addTimeColumn(thrusterSetLog.times(), thrusterSetLog.mDotTotal)
                mDotData = fixMDotData(mDotData)
                PlotName = "Ramp_" + str(rampsteps) + "steps_Cutoff" + cutoff +"_" + str(int(duration)) + "s"+"_testRate" + str(
                int(1. / (testRate * macros.NANO2SEC)))
                PlotTitle = "All Forces and Torques, with a "  + str(rampsteps) + " step Ramp, thrust for " + str(int(duration)) + "s. Cutoff " + cutoff+", testRate" + str(
                int(1. / (testRate * macros.NANO2SEC)))
                snippetName = "Snippet" + "Ramp_" + str(rampsteps) + "steps_Cutoff" + cutoff + "_Rate" + str(
                    int(1. / (testRate * macros.NANO2SEC)))  + "_Cutoff" + cutoff
                texSnippet = "We test the ramped thrust with " + str(rampsteps) + " incremental steps. The single thruster is set at the default " +str(int(long_angle))+r"$^\circ$ off the x-axis " +str(int(lat_angle))+r"$^\circ$ off the z-axis, at $\bm r = \left(" + \
                             str(loc1[0]) + "," + str(loc1[1]) + "," + str(loc1[2]) + \
                             r"\right)$. The thrust is set for " + \
                             str(duration) + " seconds with a test rate of " + str(
                    int(1. / (testRate * macros.NANO2SEC))) + " steps per second. The Cutoff test is " + cutoff
                unitTestSupport.writeTeXSnippet(snippetName, texSnippet, path)
                plt.figure(55)
                plt.clf()
                plt.plot(thrForce[:, 0] * 1.0E-9, thrForce[:, 1], 'b', label='x Force')
                plt.plot(thrTorque[:, 0] * 1.0E-9, thrTorque[:, 1], 'b--', label='x Torque')
                plt.plot(thrForce[:, 0] * 1.0E-9, thrForce[:, 2], 'g', label='y Force')
                plt.plot(thrTorque[:, 0] * 1.0E-9, thrTorque[:, 2], 'g--', label='y Torque')
                plt.plot(thrForce[:, 0] * 1.0E-9, thrForce[:, 3], 'r', label='z Force')
                plt.plot(thrTorque[:, 0] * 1.0E-9, thrTorque[:, 3], 'r--', label='z Torque')
                plt.legend(loc='upper right')
                plt.xlabel('Time(s)')
                plt.ylim(-1.5, 2)
                plt.legend(loc='upper left')
                unitTestSupport.writeFigureLaTeX(PlotName, PlotTitle, plt, format, path)
                if show_plots == True:
                    plt.show()
                    plt.close('all')
                # Create expected Force to test against thrForce
                expectedpoints = np.zeros([3, np.shape(thrForce)[0]])
                RampFunction= np.zeros([np.shape(thrForce)[0]])
                ramplength = 0.5
                for i in range(np.shape(thrForce)[0]):
                    if i<int(round(thrStartTime / testRate)) + 2:
                        RampFunction[i] = 0.0
                    if (i > int(round(thrStartTime / testRate)) + 1 and i < int(round((thrStartTime + ramplength*1.0/macros.NANO2SEC)/ testRate)) + 2) : #ramp up
                        RampFunction[i] = (i-int(round(thrStartTime / testRate)) - 2 + 1.0) * (macros.NANO2SEC*testRate)
                    if (i > int(round((thrStartTime + ramplength*1.0/macros.NANO2SEC) / testRate)) + 1 and i < int(round((thrStartTime + 2*ramplength*1.0/macros.NANO2SEC) / testRate)) + 2):
                        RampFunction[i] = RampFunction[int(round((thrStartTime + ramplength*1.0/macros.NANO2SEC) / testRate)) + 1] - (i - int(round((thrStartTime + ramplength*1.0/macros.NANO2SEC) / testRate))-2 + 1.0) *(macros.NANO2SEC*testRate)
                    if (i > int(round((thrStartTime + 2*ramplength*1.0/macros.NANO2SEC) / testRate)) + 1):
                        RampFunction[i] = 0.
                # Create expected Force to test against thrForce
                expMDot = np.zeros([np.shape(np.array(mDotData))[0], 1])
                for i in range(np.shape(RampFunction)[0]- (int(round(thrStartTime/testRate))+1)):
                    if (i>0 and RampFunction[i + int(round(thrStartTime/testRate))+1]!=0.):
                        expMDot[i, 0] = thrustNumber / (g * Isp)
                for i in range(np.shape(thrForce)[0]):# Thrust fires 2 times steps after the pause of sim and restart
                    if (i > int(round(thrStartTime / testRate)) + 1 and i < int(round((thrStartTime + thrDurationTime+ ramplength*1.0/macros.NANO2SEC) / testRate)) + 2):
                        expectedpoints[0:3, i] = dir1*RampFunction[i]
                # Modify expected values for comparison and define errorTolerance
                TruthForce = np.transpose(expectedpoints)
                ErrTolerance = 10E-9
                # Compare Force values
                thrForce = np.delete(thrForce, 0, axis=1)  # remove time column
                testFailCount, testMessages = unitTestSupport.compareArray(TruthForce, thrForce, ErrTolerance, "Force",
                                                                           testFailCount, testMessages)
                mDotData = np.delete(mDotData, 0, axis=1)  # remove time column
                for i in range(0, len(np.array(mDotData))):
                    if not unitTestSupport.isArrayEqual(np.array(mDotData)[i, :], expMDot[i, :], 1, ErrTolerance):
                        testFailCount += 1
                        testMessages.append('M dot failure')
                # Create expected Torque to test against thrTorque
                expectedpointstor = np.zeros([3, np.shape(thrTorque)[0]])
                for i in range(np.shape(thrForce)[0]): # Thrust fires 2 times steps after the pause of sim and restart
                    if (i > int(round(thrStartTime / testRate)) + 1 and i < int(round((thrStartTime + thrDurationTime+ ramplength*1.0/macros.NANO2SEC) / testRate)) + 2):
                            expectedpointstor[0:3, i] = (np.cross(loc1,dir1) + swirlTorque * dir1) * RampFunction[i]
                # Define errorTolerance
                TruthTorque = np.transpose(expectedpointstor)
                ErrTolerance = 10E-9
                # Compare Torque values
                # Compare Force values
                thrTorque = np.delete(thrTorque, 0, axis=1)  # remove time column
                testFailCount, testMessages = unitTestSupport.compareArray(TruthTorque, thrTorque, ErrTolerance,
                                                                           "Torque", testFailCount, testMessages)
        if rampDown == "ON":
            RDrestart = 0.2
            RDstart = 0.5
            RDlength = 1.5
            executeSimRun(TotalSim, thrusterSet, testRate, int(thrStartTime))
            ThrustMessage.OnTimeRequest = [RDstart]
            thrCmdMsg.write(ThrustMessage, TotalSim.TotalSim.CurrentNanos+testRate)
            executeSimRun(TotalSim, thrusterSet, testRate, int((RDstart+ RDrestart) * 1.0 / macros.NANO2SEC))
            ThrustMessage.OnTimeRequest = [RDlength]
            thrCmdMsg.write(ThrustMessage, TotalSim.TotalSim.CurrentNanos+testRate)
            executeSimRun(TotalSim, thrusterSet, testRate, int(RDlength * 1.0 / macros.NANO2SEC + sparetime))
            # Extract log variables and plot the results
            thrForce = unitTestSupport.addTimeColumn(thrusterSetLog.times(), thrusterSetLog.forceExternal_B)
            thrTorque = unitTestSupport.addTimeColumn(thrusterSetLog.times(), thrusterSetLog.torqueExternalPntB_B)
            mDotData = unitTestSupport.addTimeColumn(thrusterSetLog.times(), thrusterSetLog.mDotTotal)
            mDotData = fixMDotData(mDotData)
            PlotName = "Ramp_" + str(rampsteps) + "steps_Cutoff" + cutoff + "rampDown" + rampDown+"_testRate" + str(
                int(1. / (testRate * macros.NANO2SEC)))
            PlotTitle = "All Forces and Torques, with a " + str(rampsteps) + " step Ramp, Cutoff " + cutoff + ", RampDown" + rampDown +" testRate" + str(
                int(1. / (testRate * macros.NANO2SEC)))
            snippetName = "Snippet" + "Ramp_" + str(rampsteps) + "steps_Cutoff" + cutoff + "_Rate" + str(
                int(1. / (testRate * macros.NANO2SEC)))+ "rampDown" + rampDown
            texSnippet = "We test the ramped thrust with " + str(
                rampsteps) + " incremental steps. The single thruster is set at the default "+str(int(long_angle))+r"$^\circ$ off the x-axis " +str(int(lat_angle))+r"$^\circ$ off the z-axis, at $\bm r = \left(" + \
                         str(loc1[0]) + "," + str(loc1[1]) + "," + str(loc1[2]) + \
                         r"\right)$. The thrust is set for " + \
                         str(RDstart) + " seconds initially with a test rate of " + str(
                int(1. / (testRate * macros.NANO2SEC))) + " steps per second. The Cutoff test is " + cutoff + \
                        " the RampDown test is " + rampDown + "."
            unitTestSupport.writeTeXSnippet(snippetName, texSnippet, path)
            plt.figure(55)
            plt.clf()
            plt.plot(thrForce[:, 0] * 1.0E-9, thrForce[:, 1], 'b', label='x Force')
            plt.plot(thrTorque[:, 0] * 1.0E-9, thrTorque[:, 1], 'b--', label='x Torque')
            plt.plot(thrForce[:, 0] * 1.0E-9, thrForce[:, 2], 'g', label='y Force')
            plt.plot(thrTorque[:, 0] * 1.0E-9, thrTorque[:, 2], 'g--', label='y Torque')
            plt.plot(thrForce[:, 0] * 1.0E-9, thrForce[:, 3], 'r', label='z Force')
            plt.plot(thrTorque[:, 0] * 1.0E-9, thrTorque[:, 3], 'r--', label='z Torque')
            plt.legend(loc='upper right')
            plt.xlabel('Time(s)')
            plt.ylim(-1.5, 2)
            plt.legend(loc='upper left')
            unitTestSupport.writeFigureLaTeX(PlotName, PlotTitle, plt, format, path)
            if show_plots == True:
                plt.show()
                plt.close('all')
            # Create expected Force to test against thrForce
            expectedpoints = np.zeros([3, np.shape(thrForce)[0]])
            RampFunction = np.zeros([np.shape(thrForce)[0]])
            ramplength = 1.
            for i in range(np.shape(thrForce)[0]):
                if i < int(round(thrStartTime / testRate)) + 2:
                    RampFunction[i] = 0.0
                if (i > int(round(thrStartTime / testRate)) + 1 and i < int(round((thrStartTime + RDstart * 1.0 / macros.NANO2SEC) / testRate)) + 2):  # ramp up
                    RampFunction[i] = (i - int(round(thrStartTime / testRate)) - 2 + 1.0) * (macros.NANO2SEC * testRate)
                if (i > int(round((thrStartTime + RDstart * 1.0 / macros.NANO2SEC) / testRate)) + 1 and i < int(round((thrStartTime + (RDstart+RDrestart) * 1.0 / macros.NANO2SEC) / testRate)) + 2):
                    RampFunction[i] = RampFunction[int(round((thrStartTime + RDstart * 1.0 / macros.NANO2SEC) / testRate)) + 1] - (i - int(round((thrStartTime + (RDstart) * 1.0 / macros.NANO2SEC) / testRate)) - 2 + 1.0) * (macros.NANO2SEC * testRate)
                if (i > int(round((thrStartTime + (RDstart+RDrestart) * 1.0 / macros.NANO2SEC) / testRate)) + 1 and i < int(round((thrStartTime + (RDstart+RDrestart+(1. -RDstart+RDrestart)) * 1.0 / macros.NANO2SEC) / testRate)) + 2):  # ramp up
                    RampFunction[i] = RampFunction[int(round((thrStartTime + (RDstart+RDrestart) * 1.0 / macros.NANO2SEC) / testRate)) + 1]+ (i - int(round((thrStartTime+ (RDstart+RDrestart)* 1.0 / macros.NANO2SEC) / testRate)) - 2 + 1.0) * (macros.NANO2SEC * testRate)
                if (i > int(round((thrStartTime +  (RDstart+RDrestart+(1. -RDstart+RDrestart)) * 1.0 / macros.NANO2SEC) / testRate)) + 1 and i < int(round((thrStartTime + (RDstart+RDrestart + RDlength) * 1.0 / macros.NANO2SEC) / testRate)) + 2):
                    RampFunction[i] = 1.0
                if (i > int(round((thrStartTime + (RDstart+RDrestart + RDlength) * 1.0 / macros.NANO2SEC) / testRate)) + 1 and i < int(round((thrStartTime + (RDstart+RDrestart + RDlength+1) * 1.0 / macros.NANO2SEC) / testRate)) + 2):
                    RampFunction[i] = 1.0 - (i - int(round((thrStartTime + (RDstart+RDrestart + RDlength) * 1.0 / macros.NANO2SEC) / testRate)) - 2 + 1.0) * (macros.NANO2SEC * testRate)
                if (i > int(round((thrStartTime + (RDstart+RDrestart + RDlength+ ramplength) * 1.0 / macros.NANO2SEC) / testRate)) + 1):
                    RampFunction[i] = 0.
            # Create expected Force to test against thrForce
            expMDot = np.zeros([np.shape(np.array(mDotData))[0], 1])
            for i in range(1,np.shape(RampFunction)[0] - (int(round(thrStartTime / testRate))+1)):
                if (RampFunction[i + int(round(thrStartTime / testRate))+1] != 0.):
                    expMDot[i, 0] = thrustNumber / (g * Isp)
                    expMDot[i+1, 0] = thrustNumber / (g * Isp) # The way the last ramp is set up, we need another mdot value
            PlotName = "Ramp_function"
            PlotTitle = "Example of ramp function"
            plt.figure(11)
            plt.clf()
            plt.plot(thrForce[:, 0] * macros.NANO2SEC, RampFunction)
            plt.xlabel('Time(s)')
            plt.ylabel('Ramp(-)')
            plt.ylim(-1.5, 2)
            unitTestSupport.writeFigureLaTeX(PlotName, PlotTitle, plt, format, path)
            if show_plots == True:
                plt.show()
                plt.close('all')
            for i in range(np.shape(thrForce)[0]): # Thrust fires 2 times steps after the pause of sim and restart
                if (i > int(round(thrStartTime / testRate)) + 1 and i < int(
                        round((thrStartTime + thrDurationTime + ramplength * 1.0 / macros.NANO2SEC) / testRate)) + 2):
                    expectedpoints[0:3, i] = dir1*RampFunction[i]
            # Modify expected values for comparison and define errorTolerance
            TruthForce = np.transpose(expectedpoints)
            ErrTolerance = 10E-9
            # Compare Force values
            thrForce = np.delete(thrForce, 0, axis=1)  # remove time column
            testFailCount, testMessages = unitTestSupport.compareArray(TruthForce, thrForce, ErrTolerance, "Force",
                                                                       testFailCount, testMessages)
            mDotData = np.delete(mDotData, 0, axis=1)  # remove time column
            for i in range(0, len(np.array(mDotData))):
                if not unitTestSupport.isArrayEqual(np.array(mDotData)[i, :], expMDot[i, :], 1, ErrTolerance):
                    testFailCount += 1
                    testMessages.append('M dot failure')
            # Create expected Torque to test against thrTorque
            expectedpointstor = np.zeros([3, np.shape(thrTorque)[0]])
            for i in range(np.shape(thrForce)[0]): # Thrust fires 2 times steps after the pause of sim and restart
                if (i > int(round(thrStartTime / testRate)) + 1 and i < int(
                        round((thrStartTime + thrDurationTime + ramplength * 1.0 / macros.NANO2SEC) / testRate)) + 2):
                    expectedpointstor[0:3, i] = (np.cross(loc1, dir1) + swirlTorque * dir1) * RampFunction[i]
            # Define errorTolerance
            TruthTorque = np.transpose(expectedpointstor)
            ErrTolerance = 10E-9
            # Compare Torque values
            # Compare Force values
            thrTorque = np.delete(thrTorque, 0, axis=1)  # remove time column
            testFailCount, testMessages = unitTestSupport.compareArray(TruthTorque, thrTorque, ErrTolerance,
                                                                       "Torque", testFailCount, testMessages)
    if testFailCount == 0:
        print("PASSED")
        testFixture.PassFail.append("PASSED")
    else:
        testFixture.PassFail.append("FAILED")
        print(testMessages)
    # return fail count and join into a single string all messages in the list
    # testMessage
    return [testFailCount, ''.join(testMessages)]
if __name__ == "__main__":
    unitThrusters(ResultsStore(), True, "OFF", 1, 5.0, 30.,  15.,[[1.125], [0.5], [2.0]], 1E8, "OFF", "OFF", 0.0)