<jupyter_start><jupyter_text>`earthorbit` library tutorial: cubesat simulation walkthroughtThis Jupyter notebook will guide you throught the entire simulation of a cubesat. You will learn how to:- Create the informations of your simulation (groundstations, regions of interest)- Create the informations of your satellite (commands to be executed, hardware destination for these commands)- Gather the orbital informations for your satellite and append the orbit into the script- Make a simulation of this orbit during a period of time, will generate events related to sky events, groundstation events, ROI events- Append commands to be send to satellite, and target attitudes suring simulation- Generate VTS (CNES software) files for this simulation in order to visualise and make verifications- Generate a final `.json` file containing all the commands to be executed by satellite in order to do everything you asked. File ready to be converted into binary file.  Before starting- The library uses `arrow` for time manipulation, `pyquaternion` for quaternion manipulation, and `numpy` of course.- All units for physical values are specified *everywhere.* If it is not the case, consider using SI units.- GCRS means Geocentric Celestial Reference System: Frame where origin is mass center of Earth, `x` along vernal point, `z` along north pole- ITRS MEANS International Terrestial Reference System: Frame where origin is mass center of Earth `x` is along `(0°, 0°)` GPS location, `z` along north pole<jupyter_code>import sys
import json
import os

from earthorbit.attitude import Attitude
from earthorbit.simulation import Simulation
from earthorbit.orbit import Orbit
from earthorbit.macro import Macro
from earthorbit.maths import Maths
from earthorbit.timeconversion import TimeConversion
from earthorbit.vts import Vts

import numpy as np
import arrow
import matplotlib.image as mpimg
import pyquaternion as pq<jupyter_output><empty_output><jupyter_text>Gather information for simulation Groundstations (GS) informationsGroundstations informations are stored in a .json file inside the directory. `.json` file is a list containing objets. Objets has to have this syntax```json{        "name": "your_gs_name_here",         "lon": 0,        "lat": 0,        "distance": 123456,        "semi_angle_visibility": 3.141592653589793,        "color": "3030a0"}```- `lon` `lat` elements are longitude and latitude for GS. value is float, unit degree \[°\]- `distance` element is altitude for GS. value is float, unit meter \[m\]- `semi_angle_visibility` element is the maximum angle (radial vector, vector from GS to satellite) where satellite is visible by GS \[rad\]- `color` element is a hexadecimal color stored in a string. chose whatever your want. this can be used for UINow load the file and store it inside a variable<jupyter_code>gs_file = open("groundstations.json", "r")
gs = json.loads(gs_file.read())<jupyter_output><empty_output><jupyter_text>Region Of Interest (ROI) informations for simulationROI are areas that can be detected when flightover by satellite.Each ROI is stored inside `.bmp` (bitmap) file, size `360x180,` where each pixel covers an area of `1°x1°` of the equirectangular projection of the Earth.If a pixel is red in this file, the area corresponding will be asigned to this ROI. The top-left pixel `(0,0)` corresponds to coordinates `(lon=-180°, lat=90°).`The bottom-left pixel `(0, 180)` corresponds to coordinates `(lon=-180°, lat=-90°)`The middle pixel `(180, 90)` corresponds to coordinates `(lon=0°, lat=0°)`It is recommended, in order to manually draw your ROI, to use a software --such as **MS Paint** on Windows-- that can edit and save files to `.bmp` format.A useful template for creating a ROI this way can be found in the directory 'files'FYI: make sure to save the file with "Save as..." in order not to overwrite the template and make it unnusable again<jupyter_code>roi = [] # list that will contain the filenames for ROIs

for filename in os.listdir(): # loop for all the files in directory, to gather the file names of your images
    if filename.endswith(".bmp"): # we assume if filename ends with '.bmp' it is a file used for ROI
        roi.append(filename)<jupyter_output><empty_output><jupyter_text>Commands list for satelliteCommands used by satellite are stored in `.json` file.`.json` file is an object where each key is the ID of the command, and its value is an object with two elements:- `name` element is a string, containing the name of the command- `args` element is a list, where its elements are string. These corresponds to the TYPE of the argument that has to be passed to the satellite for command execution. Careful, position of the strings therefore matter! Example:```json{    "0": {        "name": "BOOT",        "args": [            "uint8",            "float"        ]    },    "1234": {        "name": "HELLO_WORLD",        "args": []    },    "1": {        "name": "TAKE_PIC",        "args": [            "uint8"        ]    },    "2": {        "name": "SEND_PIC",        "args": [            "bool"        ]    }}```As before, we load the file and parse it into a Python dictionary<jupyter_code>commands_file = open("commands.json", "r")
commands = json.loads(commands_file.read())<jupyter_output><empty_output><jupyter_text>Destination list for satelliteDestinations available where to send the commands.`.json` file is an object where each key is the ID of the destination, and its value is a string corresponding to the display name of the destination Example:```json{    "0": "SOMEWHERE",    "1": "ANOTHER_DESTINATION"}```<jupyter_code>destinations_file = open("destinations.json", "r")
destinations = json.loads(destinations_file.read())<jupyter_output><empty_output><jupyter_text>Get initial orientation of satelliteIn order to make correct computations, you have to know how your satellite is oriented into space.For this you have to compute by yourself the orientation quaternion of the satellite for the beggining of simulation.*NB: the satellite's camera is located along the `x` axis*<jupyter_code>start_attitude_quat = pq.Quaternion(w=0, x=1, y=0, z=0) # quaternion rotation in GCRS. for this example the rotation quaternion is null, which means the x axis of satellite will be along x axis of GCRS coordinates<jupyter_output><empty_output><jupyter_text>Creation of the orbitNow is time to create the orbit of your satellite. You can do so by many ways, and we are going to give you an example for each way.- CATNR (catalog number for Celestrak.com)- classical orbit elements (semi major axis, eccentricity, raan, arg of periaster, inclination, mean motion, mean anomaly, and epoch)- state vectors at given epoch (position in  \[m\], velocity in \[m/s\])- TLE string<jupyter_code>epoch = arrow.utcnow() # firstly we gather the current time<jupyter_output><empty_output><jupyter_text>From classical elementsangles should be in \[rad\], mean motion should be in \[rad/s\], and epoch should be an `arrow` Python object<jupyter_code>e = 0.0000452
i = np.deg2rad(51.6462) # conversion [°] -> [rad]
raan = np.deg2rad(33.4496) # conversion [°] -> [rad]
argp = np.deg2rad(211.9192) # conversion [°] -> [rad]
M = np.deg2rad(275.9732) # conversion [°] -> [rad]
n = TimeConversion.revperday2radpersec(15.49281578) # conversion [rev/day] -> [rad/s]

orbit = Orbit(epoch, e, i, raan, argp, n, M, "ISS_test_1!")<jupyter_output><empty_output><jupyter_text>From state vectorsposition in \[m\], velocity in \[m/s\]<jupyter_code>pos = np.array([-3819725.8559629 , -4986069.94117591, -2596794.58492448])
vel = np.array([5403.09180268, -1402.73349405, -5242.91201507])

orbit = Orbit.from_state_vectors(pos, vel, epoch, name="ISS_test_2!")<jupyter_output><empty_output><jupyter_text>From TLE<jupyter_code>tle = """ISS (ZARYA)
1 25544U 98067A   21012.36590653  .00001229  00000+0  30152-4 0  9991
2 25544  51.6462  33.4496 0000452 211.9192 275.9732 15.49281578264446"""

orbit = Orbit.from_tle(tle)<jupyter_output><empty_output><jupyter_text>From Celestrak .jsonYou can also make your own request to Celestrak and copy/paste the `.json` result as a string<jupyter_code>str_json = '[{"OBJECT_NAME":"ISS (ZARYA)","OBJECT_ID":"1998-067A","EPOCH":"2021-01-12T08:46:54.324192","MEAN_MOTION":15.49281578,"ECCENTRICITY":4.52e-5,"INCLINATION":51.6462,"RA_OF_ASC_NODE":33.4496,"ARG_OF_PERICENTER":211.9192,"MEAN_ANOMALY":275.9732,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":25544,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":26444,"BSTAR":3.0152e-5,"MEAN_MOTION_DOT":1.229e-5,"MEAN_MOTION_DDOT":0}]'

orbit = Orbit.from_celestrak_json(str_json)<jupyter_output><empty_output><jupyter_text>From CATNRThe easiest way. If you know your satellite catalog number (example: 25544 for ISS)IMPORTANT: This method makes a request to Celestrak.com. Therefore you have to be connected to the internet for this way to work!<jupyter_code>catnr = 25544 # catalog number for ISS
orbit = Orbit.from_celestrak_norad_cat_id(catnr) # give the CATNR in the argument. and voilà!
# You can print the orbit informations to make sure it is ok
print("Orbital elements of satellite:")
print(orbit.orbital_elements)<jupyter_output><empty_output><jupyter_text>Orbit simulationNow that the orbit is created, it is time to to make a simulation for a given amount of time.For the purpose of this example, we are going to make a simulation from now, for a duration of 7 daysWe are going to use the informations about groundstations, ROI, commands, etc. We are going to use the `Simulation` class of our library. Here is the list of the parameters- the actual orbit- the epoch corresponding to beggining of simulation- the duration of simulation \[s\]- the name for the simulation (string)- groundstation informations we gathered above- roi images we gathered above- `print_progress` optional boolean parameter to prompt into console the progress of simulation (recommended because simulation can take up to few minutes!)- `step_time` optional int parameter >= 1; the delta time of simulation<jupyter_code>simu = Simulation(orbit, orbit.epoch, 60*60*24, "ISS", gs, roi, print_progress=True)<jupyter_output><empty_output><jupyter_text>Now the simulation is completed! For each time step we have a timestamp containing all of the infos for the satellite(aka position/velocity in GCRS, ITRS, GPS coordinates, Sun/Moon/GS visible, above what ROI, etc.)Here are a few examples of what you can do with the simulation.<jupyter_code>simu.timestamps # is a list, each element is a dict containing all of the info at an epoch (usually every second)

# If you want to know these informations for a given time (let's say 1h after the beggining of simulation) you can retrieve a timestamp like this
epoch_onehour_shifted = orbit.epoch.shift(minutes=1) # see 'Arrow' library in Python to know some features for epoch manipulation
ts_onehour_shifted = simu.get_closest_timestamp(epoch_onehour_shifted)
print(ts_onehour_shifted) # here is printed the dict for a certain epoch, you can see what is in there

# You can see the events when each GS is visible by satellite, and when satellite is above each ROI
simu.gs_events # events for GS
simu.roi_events # events for ROI<jupyter_output><empty_output><jupyter_text>The longer is the simulation, the more it is going to weight in the memory (because every info for every second).This means it is recommended to have enough RAM on your computer (4GB or more recommended).You can see how much memory is used like this:<jupyter_code>print("The simulation is taking {}MB of memory.".format(simu.size_simu_MB()))<jupyter_output><empty_output><jupyter_text>Commands and attitude handlingFirst, you have to know what is the command needed for (QSW) rotation, and the destination.And store the ID of these (corresponding int key of dict) in a tuple: (command id, destination id)<jupyter_code>qsw_rot_id = (111, 0) # in this example, the corresponding command rotation is located at the key '111'<jupyter_output><empty_output><jupyter_text>Now we are going to use `Attitude` class of our library to handle commands and orientations. The parameters for instantiation are:- the actual simulation- the list of commands gathered above- the list of destinations gathered above- the orientation attitude you computed above- `av_rot_vel` optional float parameter is the average rotation speed for satellite, given in \[°/s\]<jupyter_code>att = Attitude(simu, commands, destinations, start_attitude_quat, qsw_rot_id, av_rot_vel=0.33)<jupyter_output><empty_output><jupyter_text>Push commandsWe are now going to see how to push commands. Example: you want to execute the command 'HELLO_WORLD' to destination 'SOMEWHERE' at precise epochYou find the corresponding ID of command and destination by printing the list with this command:<jupyter_code>att.print_list_commands()
id_cmd = 1234
id_dest = 0
args = [] # this command takes no arguments, so empty list
epoch_to_exe = orbit.epoch.shift(minutes=3) # 3 minutes after beggining of simulation, why not

pushed_cmd_0 = att.push_command(epoch_to_exe, id_cmd, id_dest, args) # we push the command like this, and the function retunrs the ID assosiated with the push (you have to store it if you want to delete it later)<jupyter_output><empty_output><jupyter_text>That's it! Another example with 'TEST_FX' to 'SOMEWHERE' which takes for argument an integer 'uint8', a float, and a boolean.<jupyter_code>id_cmd = 10101
id_dest = 0
args = [32, 12.0, True] # int, float, and boolean
epoch_to_exe = epoch_to_exe.shift(minutes=10) # why not again

pushed_cmd_1 = att.push_command(epoch_to_exe, id_cmd, id_dest, args)<jupyter_output><empty_output><jupyter_text>You can always see your pushed commands by accessing the list:<jupyter_code>print(att.cmd_request)<jupyter_output><empty_output><jupyter_text>Delete pushed commandsIf you want to delete a pushed command you previously pushed, just use the delete command with the ID returned when command pushing<jupyter_code>att.del_command(pushed_cmd_1) # this deletes the second pushed command we entered before<jupyter_output><empty_output><jupyter_text>Now you can use all the power of Python to push your commands however you want :)  Push attitudeYou could rotate your satellite by using the `push_command` function, but a lot of complex calculation is needed for orientation...Hopefully there are special functions for "rotation related" commands. Example: you want your satellite to be oriented along `z` axis in GCRS coordinates (direction from Earth center to north pole), at given epoch.<jupyter_code>north_dir = np.array([0, 0, 1])
north_quat = Maths.get_orientation_quat(north_dir) # function to get the corresponding orientation quaternion from 3D vector direction
random_epoch = orbit.epoch.shift(minutes=12) # at this exact epoch, the satellite will be oriented correctly

pushed_att_0 = att.push_target(random_epoch, north_quat) # same as command, the ID for the pushed attitude is returned by the function<jupyter_output><empty_output><jupyter_text>But you will probably not use this exact function, because most of the time the desired orientation is not a specific direction, but a direction to something, like the ground (NADIR), Moon, follow a GS, opposite of Sun, etc.Thankfully the library supports this, and this pretty simple.<jupyter_code>random_gs_name = gs[0]["name"] # gather the name of the first GS inside list. for later purposes

pa1 = att.push_target_antisun(random_epoch) # The satellite will be oriented in the opposite direction of Sun (for avoiding BBQ) at the epoch you give in argument 
pa2 = att.push_target_moon(random_epoch.shift(minutes=2)) # The satellite will be oriented in the direction of Moon (for calibration) at the epoch you give in argument 
pa3 = att.push_target_nadir(random_epoch.shift(minutes=4)) # The satellite will be oriented NADIR (camera pointing to ground) at the epoch you give in argument 
pa4 = att.push_target_limbs_ortho(random_epoch.shift(minutes=6)) # The satellite will be oriented LIMBS (camera pointing perpendicular to ground, and also velocity) at the epoch you give in argument 
pa5 = att.push_target_limbs_vel(random_epoch.shift(minutes=8)) # The satellite will be oriented NADIR (camera pointing perpendicular to ground, along velocity) at the epoch you give in argument 
pa6 = att.push_target_gs(random_epoch.shift(minutes=10), random_gs_name) # The satellite will be pointing to the given groundstation (name given in argument), at the epoch you give in argument 

att.gcrs_target_request # You can always see your pushed targets by accessing the list:<jupyter_output><empty_output><jupyter_text>Delete attitudeSame as command deletion.<jupyter_code>att.del_target(pa3) # ...
att.del_target(pa5) # ...<jupyter_output><empty_output><jupyter_text>Using macros for multiple tasksNow let's say you have a sequence of multiple commands/targets to make in a specific order. And let's say you will have to apply this sequence multiple times in the simulation duration. Example: a routine for taking pics: you have to rotate the satellite properly, take a few pictures, and then send the pictures...You can make this task very easy to do by using `Macro.`Firstly you create a list of dictionaries, each dictionary contain the action to perform, the RELATIVE time of execution (aka number of seconds after the application of macro), and the arguments for each action. Ponctual macro application<jupyter_code>macro_pics_list = [
    {
        "fx": Attitude.push_command,
        "exe_time": 0,
        "extra_args": [0, 0, [42, 42.0]] # "BOOT"
    },
    {
        "fx": Attitude.push_target_limbs_ortho, # rotate to limbs for pictures
        "exe_time": 10,
        "extra_args": []
    },
    {
        "fx": Attitude.push_command,
        "exe_time": 45,
        "extra_args": [1, 0, [69]] # "TAKE_PIC" few times in a row
    },
    {
        "fx": Attitude.push_command,
        "exe_time": 50,
        "extra_args": [1, 0, [69]] # ...
    },
    {
        "fx": Attitude.push_command,
        "exe_time": 55,
        "extra_args": [1, 0, [69]] # ...
    },
    {
        "fx": Attitude.push_command,
        "exe_time": 60,
        "extra_args": [2, 1, [True]] # "SEND_PIC" 
    }
]

macro_pics = Macro(macro_pics_list) # Now we create the macro using the list we created
epoch_for_pics = orbit.epoch.shift(minutes=3) # Once again, we give an epoch for when we want to apply the macro
macro_pics.apply_macro(att, epoch_for_pics) # Applying, ONCE, the macro to our attitudes. All actions given will be requested at given epoch + execution time given for each action.
macro_pics.apply_macro(att, epoch_for_pics.shift(minutes=2)) # .. and once again 12 minutes later
macro_pics.apply_macro(att, epoch_for_pics.shift(minutes=4)) # ..<jupyter_output><empty_output><jupyter_text>Periodic macro applicationYou can make the above tasks even more automatic with periodic macros.We are going to take another example: let's say you want to point NADIR for a period of time<jupyter_code>macro_follow_center_list = [
    {
        "fx": Attitude.push_target_nadir,
        "exe_time": 0,
        "extra_args": []
    }
]

epoch_beg = orbit.epoch.shift(minutes=30)
epoch_end = epoch_beg.shift(minutes=120)
macro_follow_center = Macro(macro_follow_center_list) # Now we create the macro using the list we created
macro_follow_center.apply_macro_periodic(att, epoch_beg, epoch_end, 60) # The macro will be applied from the given beg epoch to end epoch, every 60 seconds.<jupyter_output><empty_output><jupyter_text>Ponctual macro application during an eventIf you need to execute a list of commands once during an event. Example: Protecting satellite's camera by orientating satellite at Sun's opposite everytime the Sun is visible. And pointing NADIR when Sun not visible anymore.<jupyter_code>macro_protectsun_list = [
    {
        "fx": Attitude.push_target_antisun,
        "exe_time": 0,
        "extra_args": []
    }
]
macro_protectsun = Macro(macro_protectsun_list)

for ev in simu.sun_events: # looping for every events when sun is visible
    macro_protectsun.apply_macro_event(att, ev, relative_exe_time=0.0) # will be executing the macro after a time = 0% of the duration of the event (therefore at the very beggining)
    macro_follow_center.apply_macro_event(att, ev, relative_exe_time=1.0) # will be executing the macro after a time = 100% of the duration of the event (therefore at the very end of the event)<jupyter_output><empty_output><jupyter_text>Periodic macro application during an eventIf you need to execute a list of commands periodically during an event. Example: Following a specific groundstation each time the satellite sees it<jupyter_code>gs_random_name = simu.gs[0]["name"] # we take the name of the first groundstation in the list, could be anything
macro_follow_gs_list = [{
        "fx": Attitude.push_target_gs,
        "exe_time": 0,
        "extra_args": [gs_random_name]
    }]
macro_follow_gs = Macro(macro_follow_gs_list)

for ev in simu.gs_events[gs_random_name]: # looping for every events for a specific groundstation ('gs_random_name' that we definied above)
    macro_follow_gs.apply_macro_periodic_event(att, 1, ev) # every second<jupyter_output><empty_output><jupyter_text>Generating the command listFinally, it is time to generate our command list, that will be ready to be translated to binary and send to satellite!When you are done pushing all your targets/commands, you generate your commands list like this:<jupyter_code>alerts = att.generate_list_commands()
att.generated_commands # The list of generated commands can be found here<jupyter_output><empty_output><jupyter_text>The function returns `True` if command generated correctly, or returns a list of warnings if something dangerous as been encoutered during generation (BBQ detected, for instance)<jupyter_code>if alerts:
    print("list of commands generated successfully!")
    print(att.generated_commands)
else:
    print("list of commands generated but some warnings prompted:")
    print(alerts)<jupyter_output><empty_output><jupyter_text>And now you just have to save the list of commands as a `.json` file like this:<jupyter_code>f = open("generated_commands.json", "w")
str_generated_commands = json.dumps(att.generated_commands) # we stringify the list of commands into .json
f.write(str_generated_commands) # and write the string into this file
f.close()<jupyter_output><empty_output><jupyter_text>Files for VTS visualisationAnd you can finally generate files for VTS (position, orientation, and events for executed commands) like this:<jupyter_code>f = open("vts_pos.txt", "w")
f.write(Vts.generate_position(simu))
f.close()
f = open("vts_quat.txt", "w")
f.write(Vts.generate_orientation(att))
f.close()
f = open("vts_commands_event.txt", "w")
f.write(Vts.generate_commands(att))<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08483414399018448, 'fraction_numerical': 0.023706235484860436, 'mean_word_length': 5.079381992541289, 'pattern_counts': {'":': 64, '<': 129, '<?xml version=': 0, '>': 135, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '32', 'dir': 'jupyter-structured-clean-dedup', 'id': '610218', 'license': 'MIT', 'n_tokens_mistral': 7220, 'n_tokens_neox': 6696, 'n_words': 2668, 'path': 'examples/tutorial.ipynb', 'repo_name': 'leogarithm/earthorbit', 'size': '22821'}