├── .gitignore ├── LICENSE ├── README.md ├── _auxiliary ├── __init__.py ├── asteroid_aux.py └── func.py ├── _databases └── _comets │ └── mpc_comets.db ├── _kernels ├── lsk │ └── naif0012.tls ├── pck │ ├── gm_de431.tpc │ └── pck00010.tpc └── spk │ ├── 67P_CHURY_GERAS_2004_2016.BSP │ ├── C_G_1000012_2012_2017.bsp │ ├── codes_300ast_20100725.cmt │ ├── codes_300ast_20100725.tf │ ├── de432s.bsp │ └── solo_ANC_soc-orbit_20200210-20301120_L015_V1_00024_V01.bsp ├── part1 ├── SpaceSciencePython_part1.ipynb └── SpaceSciencePython_part1.py ├── part10 ├── SpaceSciencePython_part10.ipynb ├── SpaceSciencePython_part10.py └── kernel_meta.txt ├── part11 ├── SpaceSciencePython_part11.ipynb └── SpaceSciencePython_part11.py ├── part12 ├── SpaceSciencePython_part12.ipynb └── SpaceSciencePython_part12.py ├── part13 ├── SpaceSciencePython_part13.ipynb ├── SpaceSciencePython_part13.py └── kernel_meta.txt ├── part14 ├── SpaceSciencePython_part14.ipynb ├── SpaceSciencePython_part14.py └── kernel_meta.txt ├── part15 ├── SpaceSciencePython_part15.ipynb ├── SpaceSciencePython_part15.py └── kernel_meta.txt ├── part16 ├── SpaceSciencePython_part16.ipynb └── SpaceSciencePython_part16.py ├── part17 ├── 2020_JX1_data.csv ├── SpaceSciencePython_part17.ipynb ├── SpaceSciencePython_part17.py └── kernel_meta.txt ├── part18 ├── SpaceSciencePython_part18.py └── Untitled.ipynb ├── part19 ├── SpaceSciencePython_part19.ipynb └── SpaceSciencePython_part19.py ├── part2 ├── SSB_WRT_SUN.png ├── SpaceSciencePython_part2.ipynb ├── SpaceSciencePython_part2.py └── kernel_meta.txt ├── part20 ├── SpaceSciencePython_part20.ipynb ├── SpaceSciencePython_part20.py └── kernel_meta.txt ├── part23 └── mylib │ ├── __init__.py │ ├── general │ ├── __init__.py │ └── vec.py │ ├── pytest.ini │ └── tests │ ├── __init__.py │ └── test_general_vec.py ├── part3 ├── PLANETS_SUN_SSB_PHASE_ANGLE.png ├── SSB2SUN_DISTANCE.png ├── SpaceSciencePython_part3.ipynb ├── SpaceSciencePython_part3.py └── kernel_meta.txt ├── part4 ├── SpaceSciencePython_part4.ipynb ├── SpaceSciencePython_part4.py ├── VENUS_SUN_MOON.png └── kernel_meta.txt ├── part5 ├── SpaceSciencePython_part5.ipynb ├── SpaceSciencePython_part5.py ├── eclipj2000_sky_map.png ├── empty_aitoff.png ├── j2000_sky_map.png └── kernel_meta.txt ├── part6 ├── SpaceSciencePython_part6.ipynb ├── SpaceSciencePython_part6.py └── kernel_meta.txt ├── part7 ├── SpaceSciencePython_part7.ipynb ├── SpaceSciencePython_part7.py └── kernel_meta.txt ├── part8 ├── SpaceSciencePython_part8.ipynb ├── SpaceSciencePython_part8.py ├── comets_kde_incl_.png └── comets_scatter_plot_Q_i.png └── part9 ├── SpaceSciencePython_part9.ipynb ├── SpaceSciencePython_part9.py ├── comets_kde_tisserand_jup.png └── kernel_meta.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .ipynb_checkpoints 3 | */.ipynb_checkpoints/* 4 | 5 | # IPython 6 | profile_default/ 7 | ipython_config.py 8 | _kernels/_misc/codes_300ast_20100725.tf 9 | _kernels/spk/codes_300ast_20100725.bsp 10 | *.pyc 11 | part7/raw_data/cometels.json.gz 12 | *.gif 13 | *.png 14 | *.OBJ 15 | part16/magnitude_lookup_table.xlsx 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ThomasAlbin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Space Science with Python 2 | This repository is part of a "Space Science with Python" tutorial series. The corresponding articles that explain each part are available on Medium (https://medium.com/@thomas.albin). If you do not have a Medium account, feel free to use the Medium Friends Links listed below 3 | 4 | Each part is stored in a sub-directory. Some folders have a generic purpose and will be used in several tutorials. Please comment on my Medium articles or write me via Reddit (https://www.reddit.com/user/MrAstroThomas) if you have any ideas (topics or improvement), if you want to provide some feedback or if you need additional information. 5 | 6 | For all Twitter users who want to see recent updates in their feed: I tweet all Medium Friends Links as well as other topics I am interested in (Space Science, Astronomy, Machine Learning, Data Science): https://twitter.com/MrAstroThomas 7 | 8 | A summary Medium article with short descriptions of each article can be read here: https://medium.com/@thomas.albin/space-science-with-python-a-data-science-tutorial-series-57ad95660056?source=friends_link&sk=6917c1130f6c210a6990816575024538 9 | 10 | # 0. Space Science with Python — An Introduction 11 | https://medium.com/@thomas.albin/space-science-with-python-an-introduction-2de33e26c7b2?source=friends_link&sk=8f1cb55f833595bf9317acba095abd17 12 | 13 | # 1. Space Science with Python — Setup and first steps 14 | https://medium.com/@thomas.albin/space-science-with-python-setup-and-first-steps-1-8551334118f6?source=friends_link&sk=dd1c9a350ad3f618921dc07cbef81e70 15 | 16 | # 2. Space Science with Python — A look at Kepler’s first law 17 | https://medium.com/@thomas.albin/space-science-with-python-2-a-look-at-keplers-first-law-84caa6c75a35?source=friends_link&sk=8982f1b665d206b2ce8d5cbc13e5a4ea 18 | 19 | # 3. Space Science with Python — The Solar System centre 20 | https://medium.com/@thomas.albin/space-science-with-python-the-solar-system-centre-6b8ad8d7ea96?source=friends_link&sk=1157e3e480e1b162d8726d9f5b296fc1 21 | 22 | # 4. Space Science with Python — The dance of Venus 23 | https://medium.com/@thomas.albin/space-science-with-python-the-dance-of-venus-926905875afb?source=friends_link&sk=97115ae96452f4366a1ed6352deec4ec 24 | 25 | # 5. Space Science with Python — Space maps 26 | https://medium.com/@thomas.albin/space-science-with-python-space-maps-747c7d1eaf7f?source=friends_link&sk=5418db70a1e0f3c12fcf91d93b513257 27 | 28 | # 6. Space Science with Python — Around the Sun 29 | https://medium.com/@thomas.albin/space-science-with-python-quite-around-the-sun-6faa206a1210?source=friends_link&sk=e94c82a7b7de43612e0a9ee02b9ac834 30 | 31 | # 7. Space Science with Python: Comets — Visitors from afar 32 | https://medium.com/@thomas.albin/comets-visitors-from-afar-4d432cf0f3b?source=friends_link&sk=15313bd24e81936f6139bd1d81fff46d 33 | 34 | # 8. Space Science with Python — The Origin of Comets 35 | https://medium.com/@thomas.albin/space-science-with-python-the-origin-of-comets-3b2aa57470e7?source=friends_link&sk=e8384a1eac7666bb6bf3280914c4f1ef 36 | 37 | # 9. Space Science with Python — A Rendezvous with Jupiter 38 | https://medium.com/@thomas.albin/space-science-with-python-a-rendezvous-with-jupiter-55713e4ce340?source=friends_link&sk=c18510e728bad5c55661efc79fbd9076 39 | 40 | # 10. Space Science with Python — Supplements for Papers 41 | https://medium.com/@thomas.albin/space-science-with-python-supplements-for-papers-4876ec46b418?source=friends_link&sk=93002e113b1431397d3af131fb918bd6 42 | 43 | # 11. Space Science with Python — Did we observe everything? 44 | https://medium.com/@thomas.albin/space-science-with-python-did-we-observe-everything-617a8221e750?source=friends_link&sk=c422567d9ffce98d62f2b8a97a4f0dcc 45 | 46 | # 12. Space Science with Python — A comet in 3 D 47 | https://medium.com/@thomas.albin/space-science-with-python-a-comet-in-3-d-3774b1d71d9b?source=friends_link&sk=c0c6c3214d7a4447122b8566c45b2fa1 48 | 49 | # 13. Space Science with Python — Turbulent times of a comet 50 | https://medium.com/@thomas.albin/space-science-with-python-turbulent-times-of-a-comet-7fecedd78169?source=friends_link&sk=899cd7a241865f9adcec7cf15c1b5497 51 | 52 | # 14. Space Science with Python — An Invisible Visitor 53 | https://medium.com/@thomas.albin/space-science-with-python-an-invisible-visitor-2c8d759509cd?source=friends_link&sk=10dbf929f933a9bcd3471030baa49e4a 54 | 55 | # 15. Space Science with Python - The Solar Orbiter and comet ATLAS 56 | https://medium.com/@thomas.albin/space-science-with-python-the-solar-orbiter-and-comet-atlas-8150d66f79aa?sk=6ca909c57bf42b221de677666954c204 57 | 58 | # 16. Space Science with Python - Bright Dots in the Dark Sky 59 | https://medium.com/@thomas.albin/space-science-with-python-bright-dots-in-the-dark-sky-73909507a0ca?source=friends_link&sk=805a7ef8c8a06d362d6a2f28a6f82c2e 60 | 61 | # 17. Space Science with Python — Uncertain Movements of an Asteroid 62 | https://medium.com/@thomas.albin/space-science-with-python-uncertain-movements-of-an-asteroid-f651b94f7008?sk=21ee3e0d9c8e673be0e99e6103553558 63 | 64 | # 18. Space Science with Python — Density Estimators in the Sky 65 | https://medium.com/@thomas.albin/space-science-with-python-density-estimators-in-the-sky-87fbcfb089a6?source=friends_link&sk=ded09a125fb0344452ed5256ae350391 66 | 67 | # 19. Space Science with Python - A very bright Opposition 68 | https://medium.com/@thomas.albin/space-science-with-python-a-very-bright-opposition-62e248abfe62?source=friends_link&sk=20df82d085fbaae0f9ef47cb30cf7674 69 | 70 | # 20. Space Science with Python - Ceres in the Sky 71 | https://medium.com/@thomas.albin/space-science-with-python-ceres-in-the-sky-fec20fee3f9d?source=friends_link&sk=eda324f6026c621920eb94e91b37c26c 72 | 73 | # 21. Space Science with Python - Asteroid Project (Part 1) 74 | https://towardsdatascience.com/space-science-with-python-asteroid-project-part-1-4fa8809f8bde?source=friends_link&sk=032715a7707c89518c76b8b56c14f2a9 75 | 76 | # 22. Asteroid Project (Part 2) — Test Driven Development 77 | https://towardsdatascience.com/asteroid-project-part-2-test-driven-development-ed7af6c1820e?source=friends_link&sk=29e6524214e1b581364f8ebff2337956 78 | 79 | # 23. Space Science with Python — Asteroid Project (Part 3) 80 | https://towardsdatascience.com/space-science-with-python-asteroid-project-part-3-d7dc0941a717?source=friends_link&sk=8561e2be024b751caba9fea18805ddad 81 | 82 | # 24. Space Science with Python - ASteroid Project (Part 4) 83 | Please check also the resulting Pyton library SolarY on my GitHub profile that is still being developed. 84 | 85 | https://towardsdatascience.com/space-science-with-python-asteroid-project-part-4-ea1361540033?sk=110e8fbe45e5fb913951fcd0ae324733 86 | -------------------------------------------------------------------------------- /_auxiliary/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAlbin/SpaceScienceTutorial/9ca5c1340480a29a112dec91e075b7ee2eff38ed/_auxiliary/__init__.py -------------------------------------------------------------------------------- /_auxiliary/asteroid_aux.py: -------------------------------------------------------------------------------- 1 | # Import modules 2 | import numpy as np 3 | 4 | def phi_func(index, phase_angle): 5 | """ 6 | Phase function that is needed for the reduced magnitude. The function has 7 | two versions, depending on the index ('1' or '2'). 8 | 9 | Parameters 10 | ---------- 11 | index : str 12 | Phase function index / version. '1' or '2'. 13 | phase_angle : float 14 | Phase angle of the asteroid in radians. 15 | 16 | Returns 17 | ------- 18 | phi : float 19 | Phase function result. 20 | 21 | """ 22 | 23 | # Dictionary that contains the A and B constants, depending on the index / 24 | # version 25 | a_factor = {'1': 3.33, \ 26 | '2': 1.87} 27 | 28 | b_factor = {'1': 0.63, \ 29 | '2': 1.22} 30 | 31 | # Phase function 32 | phi = np.exp(-1.0 * a_factor[index] \ 33 | *+ ((np.tan(0.5 * phase_angle) ** b_factor[index]))) 34 | 35 | # Return the phase function result 36 | return phi 37 | 38 | def red_mag(abs_mag, phase_angle, slope_g): 39 | """ 40 | Reduced magnitude of an asteroid, depending on the absolute magnitude, 41 | phase angle and slope parameter (G) 42 | 43 | Parameters 44 | ---------- 45 | abs_mag : float 46 | Absolute magnitude. 47 | phase_angle : float 48 | Phase angle in radians. 49 | slope_g : float 50 | Slope parameter (G), between 0 and 1. 51 | 52 | Returns 53 | ------- 54 | r_mag : float 55 | Reduced magnitude. 56 | 57 | """ 58 | 59 | # Computation of the reduced magnitude 60 | r_mag = abs_mag - 2.5 * np.log10((1.0 - slope_g) \ 61 | * phi_func(index='1', \ 62 | phase_angle=phase_angle) \ 63 | + slope_g \ 64 | * phi_func(index='2', \ 65 | phase_angle=phase_angle)) 66 | 67 | # Return the reduced magnitude 68 | return r_mag 69 | 70 | def app_mag(abs_mag, phase_angle, slope_g, d_ast_sun, d_ast_earth): 71 | """ 72 | Apparent / Visual magnitude of an asteroid (not considering atmospheric 73 | attenuation), depending on the absolute magnitude, phase angle, the slope 74 | parameter (G) as well as the distance between the asteroid and Earth, 75 | respectively the Sun 76 | 77 | Parameters 78 | ---------- 79 | abs_mag : float 80 | Absolute magnitude. 81 | phase_angle : float 82 | Phase angle in radians. 83 | slope_g : float 84 | Slope parameter (G). 85 | d_ast_sun : float 86 | Distance between the asteroid and the Sun in AU. 87 | d_ast_earth : float 88 | Distance between the asteroid and the Earth in AU. 89 | 90 | Returns 91 | ------- 92 | mag : float 93 | Apparent / visual magnitude. 94 | 95 | """ 96 | 97 | # Compute the apparent / visual magnitude 98 | mag = red_mag(abs_mag, phase_angle, slope_g) \ 99 | + 5.0 * np.log10(d_ast_sun * d_ast_earth) 100 | 101 | # Return the apparent magnitude 102 | return mag 103 | -------------------------------------------------------------------------------- /_auxiliary/func.py: -------------------------------------------------------------------------------- 1 | # Import the modules 2 | import datetime 3 | import pathlib 4 | import urllib.request 5 | import os 6 | 7 | 8 | import numpy as np 9 | import spiceypy 10 | 11 | #%% 12 | 13 | # We define a function that is useful for downloading files. Some files, like 14 | # SPICE kernel files, are large and cannot be uploaded on the GitHub 15 | # repository. Thus, this helper function shall support you for the future 16 | # file and kernel management (if needed). 17 | def download_file(dl_path, dl_url): 18 | """ 19 | download_file(DL_PATH, DL_URL) 20 | 21 | This helper function supports one to download files from the Internet and 22 | stores them in a local directory. 23 | 24 | Parameters 25 | ---------- 26 | DL_PATH : str 27 | Download path on the local machine, relative to this function. 28 | DL_URL : str 29 | Download url of the requested file. 30 | """ 31 | 32 | # Obtain the file name from the url string. The url is split at 33 | # the "/", thus the very last entry of the resulting list is the file's 34 | # name 35 | file_name = dl_url.split('/')[-1] 36 | 37 | # Create necessary sub-directories in the DL_PATH direction (if not 38 | # existing) 39 | pathlib.Path(dl_path).mkdir(parents=True, exist_ok=True) 40 | 41 | # If the file is not present in the download directory -> download it 42 | if not os.path.isfile(dl_path + file_name): 43 | 44 | # Download the file with the urllib package 45 | urllib.request.urlretrieve(dl_url, dl_path + file_name) 46 | 47 | #%% 48 | 49 | # We define a function to add a new column in an already existing database 50 | # table. This code snippet may be helpful in the future 51 | def add_col2tab(con_db, cur_db, tab_name, col_name, col_type): 52 | """ 53 | This function adds a new column to an already existing SQLite table. 54 | Setting a new or editing an existing key (primary or foreign) is not 55 | possible. 56 | 57 | Parameters 58 | ---------- 59 | con_db : sqlite3.Connection 60 | Connection object to the SQLite database. 61 | cur_db : sqlite3.Cursor 62 | Connection corresponding cursor. 63 | tab_name : str 64 | Table name. 65 | col_name : str 66 | New column name that shall be added. 67 | col_type : str 68 | New column name corresponding SQLite column type. 69 | 70 | Returns 71 | ------- 72 | None. 73 | 74 | """ 75 | 76 | # Iterate through all existing column names of the database table using 77 | # the PRAGMA table_info command 78 | for row in cur_db.execute(f'PRAGMA table_info({tab_name})'): 79 | 80 | # If the column exists: exit the function 81 | if row[1] == col_name: 82 | break 83 | 84 | # If the column is not existing yet, add the new column 85 | else: 86 | cur_db.execute(f'ALTER TABLE {tab_name} ' \ 87 | f'ADD COLUMN {col_name} {col_type}') 88 | con_db.commit() -------------------------------------------------------------------------------- /_databases/_comets/mpc_comets.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAlbin/SpaceScienceTutorial/9ca5c1340480a29a112dec91e075b7ee2eff38ed/_databases/_comets/mpc_comets.db -------------------------------------------------------------------------------- /_kernels/lsk/naif0012.tls: -------------------------------------------------------------------------------- 1 | KPL/LSK 2 | 3 | 4 | LEAPSECONDS KERNEL FILE 5 | =========================================================================== 6 | 7 | Modifications: 8 | -------------- 9 | 10 | 2016, Jul. 14 NJB Modified file to account for the leapsecond that 11 | will occur on December 31, 2016. 12 | 13 | 2015, Jan. 5 NJB Modified file to account for the leapsecond that 14 | will occur on June 30, 2015. 15 | 16 | 2012, Jan. 5 NJB Modified file to account for the leapsecond that 17 | will occur on June 30, 2012. 18 | 19 | 2008, Jul. 7 NJB Modified file to account for the leapsecond that 20 | will occur on December 31, 2008. 21 | 22 | 2005, Aug. 3 NJB Modified file to account for the leapsecond that 23 | will occur on December 31, 2005. 24 | 25 | 1998, Jul 17 WLT Modified file to account for the leapsecond that 26 | will occur on December 31, 1998. 27 | 28 | 1997, Feb 22 WLT Modified file to account for the leapsecond that 29 | will occur on June 30, 1997. 30 | 31 | 1995, Dec 14 KSZ Corrected date of last leapsecond from 1-1-95 32 | to 1-1-96. 33 | 34 | 1995, Oct 25 WLT Modified file to account for the leapsecond that 35 | will occur on Dec 31, 1995. 36 | 37 | 1994, Jun 16 WLT Modified file to account for the leapsecond on 38 | June 30, 1994. 39 | 40 | 1993, Feb. 22 CHA Modified file to account for the leapsecond on 41 | June 30, 1993. 42 | 43 | 1992, Mar. 6 HAN Modified file to account for the leapsecond on 44 | June 30, 1992. 45 | 46 | 1990, Oct. 8 HAN Modified file to account for the leapsecond on 47 | Dec. 31, 1990. 48 | 49 | 50 | Explanation: 51 | ------------ 52 | 53 | The contents of this file are used by the routine DELTET to compute the 54 | time difference 55 | 56 | [1] DELTA_ET = ET - UTC 57 | 58 | the increment to be applied to UTC to give ET. 59 | 60 | The difference between UTC and TAI, 61 | 62 | [2] DELTA_AT = TAI - UTC 63 | 64 | is always an integral number of seconds. The value of DELTA_AT was 10 65 | seconds in January 1972, and increases by one each time a leap second 66 | is declared. Combining [1] and [2] gives 67 | 68 | [3] DELTA_ET = ET - (TAI - DELTA_AT) 69 | 70 | = (ET - TAI) + DELTA_AT 71 | 72 | The difference (ET - TAI) is periodic, and is given by 73 | 74 | [4] ET - TAI = DELTA_T_A + K sin E 75 | 76 | where DELTA_T_A and K are constant, and E is the eccentric anomaly of the 77 | heliocentric orbit of the Earth-Moon barycenter. Equation [4], which ignores 78 | small-period fluctuations, is accurate to about 0.000030 seconds. 79 | 80 | The eccentric anomaly E is given by 81 | 82 | [5] E = M + EB sin M 83 | 84 | where M is the mean anomaly, which in turn is given by 85 | 86 | [6] M = M + M t 87 | 0 1 88 | 89 | where t is the number of ephemeris seconds past J2000. 90 | 91 | Thus, in order to compute DELTA_ET, the following items are necessary. 92 | 93 | DELTA_TA 94 | K 95 | EB 96 | M0 97 | M1 98 | DELTA_AT after each leap second. 99 | 100 | The numbers, and the formulation, are taken from the following sources. 101 | 102 | 1) Moyer, T.D., Transformation from Proper Time on Earth to 103 | Coordinate Time in Solar System Barycentric Space-Time Frame 104 | of Reference, Parts 1 and 2, Celestial Mechanics 23 (1981), 105 | 33-56 and 57-68. 106 | 107 | 2) Moyer, T.D., Effects of Conversion to the J2000 Astronomical 108 | Reference System on Algorithms for Computing Time Differences 109 | and Clock Rates, JPL IOM 314.5--942, 1 October 1985. 110 | 111 | The variable names used above are consistent with those used in the 112 | Astronomical Almanac. 113 | 114 | \begindata 115 | 116 | DELTET/DELTA_T_A = 32.184 117 | DELTET/K = 1.657D-3 118 | DELTET/EB = 1.671D-2 119 | DELTET/M = ( 6.239996D0 1.99096871D-7 ) 120 | 121 | DELTET/DELTA_AT = ( 10, @1972-JAN-1 122 | 11, @1972-JUL-1 123 | 12, @1973-JAN-1 124 | 13, @1974-JAN-1 125 | 14, @1975-JAN-1 126 | 15, @1976-JAN-1 127 | 16, @1977-JAN-1 128 | 17, @1978-JAN-1 129 | 18, @1979-JAN-1 130 | 19, @1980-JAN-1 131 | 20, @1981-JUL-1 132 | 21, @1982-JUL-1 133 | 22, @1983-JUL-1 134 | 23, @1985-JUL-1 135 | 24, @1988-JAN-1 136 | 25, @1990-JAN-1 137 | 26, @1991-JAN-1 138 | 27, @1992-JUL-1 139 | 28, @1993-JUL-1 140 | 29, @1994-JUL-1 141 | 30, @1996-JAN-1 142 | 31, @1997-JUL-1 143 | 32, @1999-JAN-1 144 | 33, @2006-JAN-1 145 | 34, @2009-JAN-1 146 | 35, @2012-JUL-1 147 | 36, @2015-JUL-1 148 | 37, @2017-JAN-1 ) 149 | 150 | \begintext 151 | 152 | 153 | -------------------------------------------------------------------------------- /_kernels/pck/gm_de431.tpc: -------------------------------------------------------------------------------- 1 | KPL/PCK 2 | 3 | Assign mass parameters to planets & satellites. 4 | 5 | Parameter "BODY000_GMLIST" contains list of included objects in 6 | ASCENDING ID code order. 7 | 8 | Source: DE-431 "ASTRO-VALUES", Folkner [1-10,199,299,301,399] 9 | Jacobson satellite file release forms [non-Lunar satellites, planets] 10 | SB431-BIG16 small-body integration perturber file 11 | 12 | UNITS: km^3/s^2 13 | 14 | Modification history: 15 | 16 | DATE Who Change 17 | ----------- --- ------------------------------------------------------- 18 | 2000-Nov-28 JDG Version 1.0 19 | 2002-Oct-17 JDG C-P-V values current 20 | 2003-Feb-26 JDG Pluto/Charon values consistent w/Jacobson PLU006 21 | 2003-Mar-13 JDG Update all satellite/planet GMs 22 | 2005-Mar-02 JDG Update Saturnians 601-609,699 to SAT192 values 23 | 2005-Mar-07 JDG Update Pluto system (901,999) to PLU009 values 24 | 2005-Mar-18 JDG Update 610-611, add 615-617 (SAT196) 25 | 2006-Apr-28 JDG Update 601-609, 699 (SAT242) and 401-402, 499 (MAR063) 26 | 2006-Sep-28 JDG Update 601-609, 699 (SAT252) 27 | 2008-Aug-11 JDG Revert to DE405 for Pluto system GM (9) 28 | 2008-Sep-05 JDG Over-ride DE405 "4" with 499+401+402. 29 | 2008-Sep-25 JDG Update 4,499,401,402 for MAR080. 30 | 2013-Jul-23 JDG Version 2.0 31 | Updated to DE431 values (from DE405) 32 | 401-402,499 : MAR097 33 | 501-505,599 : JUP230 34 | 601-609,699 : SAT359 35 | 610-611,615-617: SAT357 36 | 701-705,799 : URA083 37 | 801,899 : NEP081 38 | 901-904,999 : PLU042 (902-904 newly added) 39 | 2000001-2000004: BIG16 (smb perturber file value) 40 | 2000006-2000007: BIG16 41 | 2000010 : BIG16 42 | 2000015-2000016: BIG16 43 | 2000029 : BIG16 44 | 2000052 : BIG16 45 | 2000065 : BIG16 46 | 2000087-2000088: BIG16 47 | 2000433 : Yeomans et al. (2000) Science v.289,pp.2085-2088 48 | 2000511 : BIG16 49 | 2000704 : BIG16 50 | 2013-Dec-30 JDG Updated for SAT360, PLU043 51 | 2014-Jan-08 JDG Updated for URA111/112. 52 | 53 | Key: 54 | JDG= Jon.D.Giorgini@jpl.nasa.gov 55 | 56 | \begindata 57 | 58 | BODY000_GMLIST = ( 1 2 3 4 5 6 7 8 9 10 59 | 199 299 60 | 301 399 61 | 401 402 499 62 | 501 502 503 504 505 599 63 | 601 602 603 604 605 606 607 608 609 610 611 699 64 | 701 702 703 704 705 799 65 | 801 899 66 | 901 902 903 904 999 67 | 2000001 2000002 2000003 2000004 2000006 2000007 2000010 68 | 2000015 2000016 2000029 2000052 2000065 2000087 2000088 69 | 2000433 2000511 2000704 ) 70 | 71 | BODY1_GM = ( 2.2031780000000021E+04 ) 72 | BODY2_GM = ( 3.2485859200000006E+05 ) 73 | BODY3_GM = ( 4.0350323550225981E+05 ) 74 | BODY4_GM = ( 4.2828375214000022E+04 ) 75 | BODY5_GM = ( 1.2671276480000021E+08 ) 76 | BODY6_GM = ( 3.7940585200000003E+07 ) 77 | BODY7_GM = ( 5.7945486000000080E+06 ) 78 | BODY8_GM = ( 6.8365271005800236E+06 ) 79 | BODY9_GM = ( 9.7700000000000068E+02 ) 80 | BODY10_GM = ( 1.3271244004193938E+11 ) 81 | 82 | BODY199_GM = ( 2.2031780000000021E+04 ) 83 | BODY299_GM = ( 3.2485859200000006E+05 ) 84 | BODY399_GM = ( 3.9860043543609598E+05 ) 85 | BODY499_GM = ( 4.282837362069909E+04 ) 86 | BODY599_GM = ( 1.266865349218008E+08 ) 87 | BODY699_GM = ( 3.793120749865224E+07 ) 88 | BODY799_GM = ( 5.793951322279009E+06 ) 89 | BODY899_GM = ( 6.835099502439672E+06 ) 90 | BODY999_GM = ( 8.696138177608748E+02 ) 91 | 92 | BODY301_GM = ( 4.9028000661637961E+03 ) 93 | 94 | BODY401_GM = ( 7.087546066894452E-04 ) 95 | BODY402_GM = ( 9.615569648120313E-05 ) 96 | 97 | BODY501_GM = ( 5.959916033410404E+03 ) 98 | BODY502_GM = ( 3.202738774922892E+03 ) 99 | BODY503_GM = ( 9.887834453334144E+03 ) 100 | BODY504_GM = ( 7.179289361397270E+03 ) 101 | BODY505_GM = ( 1.378480571202615E-01 ) 102 | 103 | BODY601_GM = ( 2.503522884661795E+00 ) 104 | BODY602_GM = ( 7.211292085479989E+00 ) 105 | BODY603_GM = ( 4.121117207701302E+01 ) 106 | BODY604_GM = ( 7.311635322923193E+01 ) 107 | BODY605_GM = ( 1.539422045545342E+02 ) 108 | BODY606_GM = ( 8.978138845307376E+03 ) 109 | BODY607_GM = ( 3.718791714191668E-01 ) 110 | BODY608_GM = ( 1.205134781724041E+02 ) 111 | BODY609_GM = ( 5.531110414633374E-01 ) 112 | BODY610_GM = ( 1.266231296945636E-01 ) 113 | BODY611_GM = ( 3.513977490568457E-02 ) 114 | BODY615_GM = ( 3.759718886965353E-04 ) 115 | BODY616_GM = ( 1.066368426666134E-02 ) 116 | BODY617_GM = ( 9.103768311054300E-03 ) 117 | 118 | BODY701_GM = ( 8.346344431770477E+01 ) 119 | BODY702_GM = ( 8.509338094489388E+01 ) 120 | BODY703_GM = ( 2.269437003741248E+02 ) 121 | BODY704_GM = ( 2.053234302535623E+02 ) 122 | BODY705_GM = ( 4.319516899232100E+00 ) 123 | 124 | BODY801_GM = ( 1.427598140725034E+03 ) 125 | 126 | BODY901_GM = ( 1.058799888601881E+02 ) 127 | BODY902_GM = ( 3.048175648169760E-03 ) 128 | BODY903_GM = ( 3.211039206155255E-03 ) 129 | BODY904_GM = ( 1.110040850536676E-03 ) 130 | 131 | BODY2000001_GM = ( 6.3130000000000003E+01 ) 132 | BODY2000002_GM = ( 1.3730000000000000E+01 ) 133 | BODY2000003_GM = ( 1.8200000000000001E+00 ) 134 | BODY2000004_GM = ( 1.7289999999999999E+01 ) 135 | BODY2000006_GM = ( 9.3000000000000005E-01 ) 136 | BODY2000007_GM = ( 8.5999999999999999E-01 ) 137 | BODY2000010_GM = ( 5.7800000000000002E+00 ) 138 | BODY2000015_GM = ( 2.1000000000000001E+00 ) 139 | BODY2000016_GM = ( 1.8100000000000001E+00 ) 140 | BODY2000029_GM = ( 8.5999999999999999E-01 ) 141 | BODY2000052_GM = ( 1.5900000000000001E+00 ) 142 | BODY2000065_GM = ( 9.1000000000000003E-01 ) 143 | BODY2000087_GM = ( 9.8999999999999999E-01 ) 144 | BODY2000088_GM = ( 1.0200000000000000E+00 ) 145 | BODY2000433_GM = ( 4.463E-4 ) 146 | BODY2000511_GM = ( 2.2599999999999998E+00 ) 147 | BODY2000704_GM = ( 2.1899999999999999E+00 ) 148 | 149 | \begintext 150 | 151 | -------------------------------------------------------------------------------- /_kernels/spk/67P_CHURY_GERAS_2004_2016.BSP: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAlbin/SpaceScienceTutorial/9ca5c1340480a29a112dec91e075b7ee2eff38ed/_kernels/spk/67P_CHURY_GERAS_2004_2016.BSP -------------------------------------------------------------------------------- /_kernels/spk/C_G_1000012_2012_2017.bsp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAlbin/SpaceScienceTutorial/9ca5c1340480a29a112dec91e075b7ee2eff38ed/_kernels/spk/C_G_1000012_2012_2017.bsp -------------------------------------------------------------------------------- /_kernels/spk/de432s.bsp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAlbin/SpaceScienceTutorial/9ca5c1340480a29a112dec91e075b7ee2eff38ed/_kernels/spk/de432s.bsp -------------------------------------------------------------------------------- /_kernels/spk/solo_ANC_soc-orbit_20200210-20301120_L015_V1_00024_V01.bsp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAlbin/SpaceScienceTutorial/9ca5c1340480a29a112dec91e075b7ee2eff38ed/_kernels/spk/solo_ANC_soc-orbit_20200210-20301120_L015_V1_00024_V01.bsp -------------------------------------------------------------------------------- /part1/SpaceSciencePython_part1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | #%% 5 | 6 | # Import the SPICE module 7 | import spiceypy 8 | 9 | #%% 10 | 11 | # We want to determine the position of our home planet with respect to the 12 | # Sun. 13 | # The datetime shall be set as "today" (midnight). SPICE requires the 14 | # Ephemeris Time (ET); thus, we need to convert a UTC datetime string to ET. 15 | 16 | import datetime 17 | 18 | # get today's date 19 | DATE_TODAY = datetime.datetime.today() 20 | 21 | # convert the datetime to a string, replacing the time with midnight 22 | DATE_TODAY = DATE_TODAY.strftime('%Y-%m-%dT00:00:00') 23 | 24 | # convert the utc midnight string to the corresponding ET 25 | ET_TODAY_MIDNIGHT = spiceypy.utc2et(DATE_TODAY) 26 | 27 | #%% 28 | 29 | # oh... an error occurred. The error tells us that a so called "kernel" is 30 | # missing. These kernels store all information that are required for time 31 | # conversion, pointing, position determination etc. For this tutorial the Git 32 | # repository contains already the necessary kernel. We need to load it first 33 | spiceypy.furnsh('../_kernels/lsk/naif0012.tls') 34 | 35 | #%% 36 | 37 | # Let's re-try our first time conversion command 38 | ET_TODAY_MIDNIGHT = spiceypy.utc2et(DATE_TODAY) 39 | 40 | #%% 41 | 42 | # It works! How does the value look like? 43 | print(ET_TODAY_MIDNIGHT) 44 | 45 | #%% 46 | 47 | # Can we compute now the position and velocity (so called state) of the Earth 48 | # with respect to the Sun? We use the following function to determine the 49 | # state vector and the so called light time (travel time of the light between 50 | # the Sun and our home planet). Positions are always given in km, velocities 51 | # in km/s and times in seconds 52 | 53 | # targ : Object that shall be pointed at 54 | # et : The ET of the computation 55 | # ref : The reference frame. Here, it is ECLIPJ2000 (so Medium article) 56 | # obs : The observer respectively the center of our state vector computation 57 | EARTH_STATE_WRT_SUN, EARTH_SUN_LT = spiceypy.spkgeo(targ=399, \ 58 | et=ET_TODAY_MIDNIGHT, \ 59 | ref='ECLIPJ2000', \ 60 | obs=10) 61 | 62 | #%% 63 | 64 | # An error occured. Again a kernel error. Well, we need to load a so called 65 | # spk to load positional information: 66 | spiceypy.furnsh('../_kernels/spk/de432s.bsp') 67 | 68 | #%% 69 | 70 | # Let's re-try the computation again 71 | EARTH_STATE_WRT_SUN, EARTH_SUN_LT = spiceypy.spkgeo(targ=399, \ 72 | et=ET_TODAY_MIDNIGHT, \ 73 | ref='ECLIPJ2000', obs=10) 74 | 75 | #%% 76 | 77 | # The state vector is 6 dimensional: x,y,z in km and the corresponding 78 | # velocities in km/s 79 | print('State vector of the Earth w.r.t. the Sun for "today" (midnight):\n', \ 80 | EARTH_STATE_WRT_SUN) 81 | 82 | #%% 83 | 84 | # The (Euclidean) distance should be around 1 AU. Why "around"? Well the Earth 85 | # revolves the Sun in a slightly non-perfect circle (elliptic orbit). First, 86 | # we compute the distance in km. 87 | import math 88 | EARTH_SUN_DISTANCE = math.sqrt(EARTH_STATE_WRT_SUN[0]**2.0 \ 89 | + EARTH_STATE_WRT_SUN[1]**2.0 \ 90 | + EARTH_STATE_WRT_SUN[2]**2.0) 91 | 92 | #%% 93 | 94 | # Convert the distance in astronomical units (1 AU) 95 | # Instead of searching for the "most recent" value, we use the default value 96 | # in SPICE. This way, we can easily compare our results with the results of 97 | # others. 98 | EARTH_SUN_DISTANCE_AU = spiceypy.convrt(EARTH_SUN_DISTANCE, 'km', 'AU') 99 | 100 | # Cool, it works! 101 | print('Current distance between the Earth and the Sun in AU:', \ 102 | EARTH_SUN_DISTANCE_AU) 103 | 104 | 105 | # # Orbital speed of the Earth 106 | # For this, we need the equation to determine the orbital speed. We assume 107 | # that the Sun's mass is greater than the mass of the Earth and we assume 108 | # that our planet is moving on an almost circular orbit. The orbit velocity 109 | # $v_{\text{orb}}$ can be approximated with, where $G$ is the gravitational 110 | # constant, $M$ is the mass of the Sun and $r$ is the distance between the 111 | # Earth and the Sun: 112 | # \begin{align} 113 | # v_{\text{orb}}\approx\sqrt{\frac{GM}{r}} 114 | # \end{align} 115 | 116 | #%% 117 | 118 | # First, we compute the actual orbital speed of the Earth around the Sun 119 | EARTH_ORB_SPEED_WRT_SUN = math.sqrt(EARTH_STATE_WRT_SUN[3]**2.0 \ 120 | + EARTH_STATE_WRT_SUN[4]**2.0 \ 121 | + EARTH_STATE_WRT_SUN[5]**2.0) 122 | 123 | # It's around 30 km/s 124 | print('Current orbital speed of the Earth around the Sun in km/s:', \ 125 | EARTH_ORB_SPEED_WRT_SUN) 126 | 127 | #%% 128 | 129 | # Now let's compute the theoretical expectation. First, we load a pck file 130 | # that contain miscellanoeus information, like the G*M values for different 131 | # objects 132 | 133 | # First, load the kernel 134 | spiceypy.furnsh('../_kernels/pck/gm_de431.tpc') 135 | _, GM_SUN = spiceypy.bodvcd(bodyid=10, item='GM', maxn=1) 136 | 137 | # Now compute the orbital speed 138 | V_ORB_FUNC = lambda gm, r: math.sqrt(gm/r) 139 | EARTH_ORB_SPEED_WRT_SUN_THEORY = V_ORB_FUNC(GM_SUN[0], EARTH_SUN_DISTANCE) 140 | 141 | # Print the result 142 | print('Theoretical orbital speed of the Earth around the Sun in km/s:', \ 143 | EARTH_ORB_SPEED_WRT_SUN_THEORY) 144 | 145 | -------------------------------------------------------------------------------- /part10/SpaceSciencePython_part10.py: -------------------------------------------------------------------------------- 1 | # Import the standard modules 2 | import pathlib 3 | 4 | # Import spiceypy 5 | import spiceypy 6 | 7 | # Load necessary SPICE kernels 8 | spiceypy.furnsh('kernel_meta.txt') 9 | 10 | # Import the installed modules 11 | import numpy as np 12 | 13 | # Import matplotlib for plotting 14 | from matplotlib import pyplot as plt 15 | 16 | #%% 17 | 18 | # Create sample Ephemeris Time (ET) 19 | SAMPLE_ET = spiceypy.utc2et('2000-001T00:00:00') 20 | 21 | # Compute the state vector of Jupiter's barycentre at the defined ET as seen 22 | # from the Sun 23 | STATE_VEC_JUPITER, _ = spiceypy.spkgeo(targ=5, \ 24 | et=SAMPLE_ET, \ 25 | ref='ECLIPJ2000', \ 26 | obs=10) 27 | 28 | # Get the G*M value of the Sun 29 | _, GM_SUN = spiceypy.bodvcd(bodyid=10, item='GM', maxn=1) 30 | GM_SUN = GM_SUN[0] 31 | 32 | # Compute the orbital elements of Jupiter ... 33 | ORB_ELEM_JUPITER = spiceypy.oscltx(STATE_VEC_JUPITER, SAMPLE_ET, GM_SUN) 34 | 35 | # ... extract the semi-major axis and convert it from km to AU 36 | A_JUPITER_KM = ORB_ELEM_JUPITER[-2] 37 | A_JUPITER_AU = spiceypy.convrt(A_JUPITER_KM, 'km', 'AU') 38 | 39 | #%% 40 | 41 | # Set the inclination range (however, only from 0 to 90 degrees) and convert 42 | # the degrees values to radians 43 | INCL_RANGE_DEG = np.linspace(0, 90, 100) 44 | INCL_RANGE_RAD = np.radians(INCL_RANGE_DEG) 45 | 46 | # Set an array for the eccentricity 47 | E_RANGE = np.linspace(0, 1, 100) 48 | 49 | # Tisserand parameter w.r.t. to Jupiter as a lambda function (a is the 50 | # semi-major axis, i is the inclination and e is the eccentricty of the 51 | # object) 52 | tisserand_jup = lambda a, i, e: (A_JUPITER_AU / a) \ 53 | + 2 * np.cos(i) \ 54 | * np.sqrt((a / A_JUPITER_AU) * (1 - (e**2.0))) 55 | 56 | # Create a mesh grid for the 2 D colour contour plot 57 | E_MESH, INCL_RAD_MESH = np.meshgrid(E_RANGE, INCL_RANGE_RAD) 58 | 59 | #%% 60 | 61 | # Import the matplotlib colormap 62 | from matplotlib import cm 63 | 64 | # Let's set a dark background 65 | plt.style.use('dark_background') 66 | 67 | # Set a default font size for better readability 68 | plt.rcParams.update({'font.size': 14}) 69 | 70 | # Set an array for the semi-major axis (for each semi-major axis an individual 71 | # contour plot will be created). Let's set a step-size of 0.1 AU 72 | DELTA_A = 0.1 73 | A_ARRAY = np.arange(1.0, 8.0 + DELTA_A, DELTA_A) 74 | 75 | #%% 76 | 77 | # Create a temporary folder to store all figures 78 | pathlib.Path('temp/').mkdir(parents=True, exist_ok=True) 79 | 80 | # Import tqdm for a proper progress visualisation 81 | from tqdm import tqdm 82 | 83 | # Set a figure size 84 | fig, _ = plt.subplots(figsize=(12, 8)) 85 | 86 | # Iterate through the semi-major axis array, create and save the resulting 87 | # contour plots 88 | for a_for in tqdm(range(len(A_ARRAY))): 89 | 90 | # Clear the plot for each re-draw 91 | plt.clf() 92 | 93 | # Set the x and y axis limits (eccentricity and inclination) 94 | plt.xlim(0, 1) 95 | plt.ylim(0, 90) 96 | 97 | # Compute the Tisserand parameter 98 | tiss_jup_mesh = tisserand_jup(A_ARRAY[a_for], INCL_RAD_MESH, E_MESH) 99 | 100 | # Display relevant information 101 | plt.title(f'Semi-major axis in AU: {round(A_ARRAY[a_for], 2)}') 102 | plt.xlabel('Eccentricity') 103 | plt.ylabel('Inclination in degrees') 104 | 105 | # Create a contour plot 106 | contr_plt = plt.contourf(E_MESH, np.degrees(INCL_RAD_MESH), \ 107 | tiss_jup_mesh, vmin=2, vmax=3, cmap=cm.CMRmap, \ 108 | extend='both', levels=np.linspace(2, 3, 11)) 109 | 110 | # Add a colorbar that is also used in the following 111 | colbar = fig.colorbar(contr_plt) 112 | colbar.ax.set_ylabel('Tisserand Parameter') 113 | 114 | # Save the figure. Note: 100 DPI have been chosen, since the GIF that 115 | # shall be created in a moment cannot exceed 25 MB for the Medium article 116 | plt.savefig('temp/'+str(a_for+1).zfill(3)+'.png', dpi=100) 117 | 118 | #%% 119 | 120 | # In this part all figures are merged into one animation (GIF) 121 | 122 | # Import glob to get the paths to all figures in the temporary folder 123 | import glob 124 | FILE_NAMES = glob.glob('temp/*.png') 125 | 126 | # Our animation should be though-out. Let's create a GIF that starts with 127 | # a = 1, goes up to the last image ... waits for short period of time and then 128 | # reverses back to a = 1. The result: A nice repeating "back and forth" 129 | # animation without any image "glitches" or "jumps" 130 | 131 | # Create a list that goes from a = 1 to a = 8 AU 132 | file_name_list = sorted(FILE_NAMES) 133 | 134 | # Extend the list with 25 images of the last entry (a = 8 AU) 135 | file_name_list.extend([file_name_list[-1]] * round(25)) 136 | 137 | # "Go back in time" by extending the array with a reversed copy (a = 8 AU to 138 | # a = 1 AU) 139 | file_name_list.extend(sorted(FILE_NAMES)[::-1]) 140 | 141 | # Add again the initial frame (a = 1 AU) for a second break 142 | file_name_list.extend([file_name_list[-1]] * round(25)) 143 | 144 | #%% 145 | 146 | # Import imageio. This library supports us to create an animation 147 | import imageio 148 | 149 | # Set an empty list that will contain all images 150 | tisserand_images = [] 151 | 152 | # Iterate through the list of image paths, read the image with the imageio 153 | # library and append the result to the list 154 | for figure_f in file_name_list: 155 | tisserand_images.append(imageio.imread(figure_f)) 156 | 157 | # Save the list of images as a GIF. The duration of a single image is given 158 | # in seconds 159 | imageio.mimsave('tisserand_animated_vis.gif', tisserand_images, duration=0.04) 160 | -------------------------------------------------------------------------------- /part10/kernel_meta.txt: -------------------------------------------------------------------------------- 1 | \begintext 2 | 3 | This meta file contains the relative paths to all needed SPICE kernels. 4 | For each tutorial we will set up an individual kernel_meta.txt. A common 5 | meta file could be easily stored in the main folder (next to the _kernel 6 | directory), however it would be over-loaded at some point. 7 | 8 | \begindata 9 | 10 | KERNELS_TO_LOAD = ( 11 | '../_kernels/pck/gm_de431.tpc', 12 | '../_kernels/spk/de432s.bsp', 13 | '../_kernels/lsk/naif0012.tls', 14 | ) 15 | -------------------------------------------------------------------------------- /part11/SpaceSciencePython_part11.py: -------------------------------------------------------------------------------- 1 | # Import the standard modules 2 | import sqlite3 3 | 4 | # Import installed modules 5 | from matplotlib import pyplot as plt 6 | import pandas as pd 7 | import numpy as np 8 | 9 | #%% 10 | 11 | # Connect to the comet database. This database has been created in tutorial 12 | # part 7, however, due to its small size the database is uploaded on GitHub 13 | con = sqlite3.connect('../_databases/_comets/mpc_comets.db') 14 | 15 | # Create a pandas dataframe that contains the perihelion and the absolute 16 | # magnitude 17 | COMETS_DF = pd.read_sql('SELECT PERIHELION_AU, ABSOLUTE_MAGNITUDE' \ 18 | ' FROM comets_main WHERE ECCENTRICITY < 1', \ 19 | con) 20 | 21 | #%% 22 | 23 | # Print some descriptive statistics 24 | print('Descriptive Statistics of the Absolute Magnitude of Comets') 25 | print(f'{COMETS_DF["ABSOLUTE_MAGNITUDE"].describe()}') 26 | 27 | #%% 28 | 29 | # Define a histogram bins array 30 | BINS_RANGE = np.arange(-2, 22 + 2, 2) 31 | 32 | # Let's set a dark background 33 | plt.style.use('dark_background') 34 | 35 | # Set a default font size for better readability 36 | plt.rcParams.update({'font.size': 14}) 37 | 38 | # Create a figure and axis 39 | fig, ax = plt.subplots(figsize=(12, 8)) 40 | 41 | # Plot a histogram of the absolute magnitude distribution 42 | ax.hist(COMETS_DF['ABSOLUTE_MAGNITUDE'], bins=BINS_RANGE, color='tab:orange', \ 43 | alpha=0.7) 44 | 45 | # Set labels for the x and y axes 46 | ax.set_xlabel('Absolute Magnitude') 47 | ax.set_ylabel('Number of Comets') 48 | 49 | # Set a grid 50 | ax.grid(axis='both', linestyle='dashed', alpha=0.2) 51 | 52 | # Save the figure 53 | plt.savefig('comets_abs_mag_hist.png', dpi=300) 54 | 55 | #%% 56 | 57 | # The histogram provides a simple overview of the distribution of the 58 | # Absolute Magnitudes ... what does it imply? Are there really, only a few 59 | # smaller comets in the Solar System? 60 | 61 | # Let's create a cumulative histogram as a scatter plot for a better 62 | # visibility of possible trends 63 | 64 | # Compute a cumulative distribution of the absolute magnitude 65 | ABS_MAG_HIST, BINS_EDGE = np.histogram(COMETS_DF['ABSOLUTE_MAGNITUDE'], \ 66 | bins=BINS_RANGE) 67 | CUMUL_HIST = np.cumsum(ABS_MAG_HIST) 68 | 69 | # Create a figure and axis 70 | fig, ax = plt.subplots(figsize=(12, 8)) 71 | 72 | # Create a scatter plot of the cumulative distribution. Consider, to shift the 73 | # bin array by half of the bins' width 74 | ax.scatter(BINS_EDGE[:-1]+1, CUMUL_HIST, color='tab:orange', alpha=0.7, \ 75 | marker='o') 76 | 77 | # Set labels for the x and y axes 78 | ax.set_xlabel('Absolute Magnitude') 79 | ax.set_ylabel('Cumulative Number of Comets') 80 | 81 | # Set a grid 82 | ax.grid(axis='both', linestyle='dashed', alpha=0.2) 83 | 84 | # Save the figure 85 | plt.savefig('comets_abs_mag_cumul_hist.png', dpi=300) 86 | 87 | #%% 88 | 89 | # The plot ... does not help a lot ... what about a logarithmic scale? 90 | 91 | # Create a figure and axis 92 | fig, ax = plt.subplots(figsize=(12, 8)) 93 | 94 | # Create a scatter plot of the cumulative distribution. 95 | ax.scatter(BINS_EDGE[:-1]+1, CUMUL_HIST, color='tab:orange', alpha=0.7, \ 96 | marker='o') 97 | 98 | # Set labels for the x and y axes 99 | ax.set_xlabel('Absolute Magnitude') 100 | ax.set_ylabel('Cumulative Number of Comets') 101 | 102 | # Set a grid 103 | ax.grid(axis='both', linestyle='dashed', alpha=0.2) 104 | 105 | # Set a logarithmic y axis 106 | ax.set_yscale('log') 107 | 108 | plt.savefig('comets_abs_mag_log10cumul_hist.png', dpi=300) 109 | 110 | #%% 111 | 112 | # The logarithmic plots appears to be promising. Let's assume that we know all 113 | # larger comets; we use the first 5 data points to create a linear regression 114 | # model in semi-log space 115 | 116 | # Create two arrays that contain the abs. mag. for the fitting and plotting 117 | # routine 118 | ABS_MAG_FIT = BINS_EDGE[:5]+1 119 | ABS_MAG_PLOT = BINS_EDGE[:-1]+1 120 | 121 | # Get the first 5 cumulative results 122 | CUMUL_FIT = CUMUL_HIST[:5] 123 | 124 | # Import the linear model from scikit-learn 125 | from sklearn import linear_model 126 | reg = linear_model.LinearRegression() 127 | 128 | # Fit the linear regression model with the data 129 | reg.fit(ABS_MAG_FIT.reshape(-1, 1), np.log10(CUMUL_FIT)) 130 | 131 | # Compute a linear plot for the entire abs. mag. range 132 | CUMULATIVE_ABS_MAG_PRED = reg.predict(ABS_MAG_PLOT.reshape(-1, 1)) 133 | 134 | #%% 135 | 136 | # Create a figure and axis 137 | fig, ax = plt.subplots(figsize=(12, 8)) 138 | 139 | # Plot the used data points as white dots and ... 140 | ax.scatter(ABS_MAG_FIT, CUMUL_FIT, color='white', alpha=0.7, marker='o', \ 141 | s=100) 142 | 143 | # ... plot also the complete data set 144 | ax.scatter(BINS_EDGE[:-1]+1, CUMUL_HIST, color='tab:orange', alpha=0.7, \ 145 | marker='o') 146 | 147 | # Plot the linear regression. 148 | ax.plot(ABS_MAG_PLOT, 10**CUMULATIVE_ABS_MAG_PRED, 'w--', alpha=0.7) 149 | 150 | # Set labels for the x and y axes as well as a grid 151 | ax.set_xlabel('Absolute Magnitude') 152 | ax.set_ylabel('Cumulative Number of Comets') 153 | ax.grid(axis='both', linestyle='dashed', alpha=0.2) 154 | 155 | # Set a log y scale 156 | ax.set_yscale('log') 157 | 158 | # Save the figure 159 | plt.savefig('comets_abs_mag_log10cumul_hist_linreg.png', dpi=300) 160 | 161 | #%% 162 | 163 | # Let's see wether we find a dependency between the perihelion and the abs. 164 | # mag. 165 | fig, ax = plt.subplots(figsize=(12, 8)) 166 | 167 | # To visualise the relation between abs. mag. and size better, we scale the 168 | # scatter plot dot size w.r.t. to the abs. mag. 169 | # A large abs. mag. corresponds to a small size. First, subtract the values 170 | # by the maximum and subtract 1 (otherwise the largest value will become 0) 171 | comet_size_plot = abs(COMETS_DF['ABSOLUTE_MAGNITUDE'] \ 172 | - COMETS_DF['ABSOLUTE_MAGNITUDE'].max() - 1) 173 | 174 | # Second and third, normalise the results and scale them by a factor of 100 175 | comet_size_plot /= max(comet_size_plot) 176 | comet_size_plot *= 100 177 | 178 | # Create a scatter plot of the perihelion vs. the abs. mag. with the marker 179 | # sizing 180 | ax.scatter(COMETS_DF['PERIHELION_AU'], COMETS_DF['ABSOLUTE_MAGNITUDE'], \ 181 | color='white', s=comet_size_plot, alpha=0.3) 182 | 183 | # Invert the y axis to create a graph that is similar to the logic of the 184 | # Malmquist Bias shown in the article 185 | ax.invert_yaxis() 186 | 187 | # Set labels and a grid 188 | ax.set_xlabel('Perihelion in AU') 189 | ax.set_ylabel('Absolute Magnitude') 190 | ax.grid(axis='both', linestyle='dashed', alpha=0.2) 191 | 192 | # Save the figure 193 | plt.savefig('comets_abs_mag_vs_perih.png', dpi=300) 194 | -------------------------------------------------------------------------------- /part12/SpaceSciencePython_part12.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# Import installed modules\n", 10 | "import pandas as pd\n", 11 | "import numpy as np\n", 12 | "import imageio\n", 13 | "from tqdm import tqdm\n", 14 | "\n", 15 | "# Import the Python script func from the auxiliary folder\n", 16 | "import sys\n", 17 | "sys.path.insert(1, '../_auxiliary')\n", 18 | "import func" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 2, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "# Set a local download path and the URL to the 67P shape model data set\n", 28 | "DL_PATH = 'data/'\n", 29 | "DL_URL = 'https://repos.cosmos.esa.int/socci/projects/SPICE_KERNELS/repos/rosetta/raw/kernels/' \\\n", 30 | " + 'dsk/ROS_CG_M001_OSPCLPS_N_V1.OBJ'\n", 31 | "\n", 32 | "# Download the shape model, create (if needed) the download path and store the data set\n", 33 | "func.download_file(DL_PATH, DL_URL)" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": 3, 39 | "metadata": {}, 40 | "outputs": [ 41 | { 42 | "name": "stdout", 43 | "output_type": "stream", 44 | "text": [ 45 | "Some statistics and parameters of the shape model\n", 46 | "Rows and columns of the data set: (1791747, 4)\n", 47 | "Number of vertices: 597251\n", 48 | "Number of faces: 1194496\n", 49 | "\n", 50 | "\n" 51 | ] 52 | } 53 | ], 54 | "source": [ 55 | "# Load the shape model. The first column lists whether the row is a vertex or face. The second,\n", 56 | "# third and fourth column list the coordiantes (vertex) and vertex indices (faces)\n", 57 | "COMET_67P_SHAPE_OBJ = pd.read_csv('data/ROS_CG_M001_OSPCLPS_N_V1.OBJ', delim_whitespace=True, \\\n", 58 | " names=['TYPE', 'X1', 'X2', 'X3'])\n", 59 | "\n", 60 | "# Print some shape model information\n", 61 | "print('Some statistics and parameters of the shape model')\n", 62 | "print(f'Rows and columns of the data set: {COMET_67P_SHAPE_OBJ.shape}')\n", 63 | "print(f'Number of vertices: {COMET_67P_SHAPE_OBJ.loc[COMET_67P_SHAPE_OBJ[\"TYPE\"]==\"v\"].shape[0]}')\n", 64 | "print(f'Number of faces: {COMET_67P_SHAPE_OBJ.loc[COMET_67P_SHAPE_OBJ[\"TYPE\"]==\"f\"].shape[0]}')\n", 65 | "print('\\n')" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": 4, 71 | "metadata": {}, 72 | "outputs": [ 73 | { 74 | "name": "stdout", 75 | "output_type": "stream", 76 | "text": [ 77 | "Vertices (Sample)\n", 78 | " TYPE X1 X2 X3\n", 79 | "0 v 0.570832 -1.000444 0.532569\n", 80 | "1 v 0.564360 -1.000224 0.525360\n", 81 | "2 v 0.557853 -0.997863 0.520762\n", 82 | "3 v 0.553592 -0.998414 0.512192\n", 83 | "4 v 0.550212 -0.992514 0.507304\n", 84 | "Faces (Sample)\n", 85 | " TYPE X1 X2 X3\n", 86 | "597251 f 474.0 522.0 305.0\n", 87 | "597252 f 474.0 719.0 522.0\n", 88 | "597253 f 1961.0 2160.0 1651.0\n", 89 | "597254 f 1961.0 2159.0 2160.0\n", 90 | "597255 f 5513.0 5668.0 5285.0\n", 91 | "\n", 92 | "\n" 93 | ] 94 | } 95 | ], 96 | "source": [ 97 | "# Print some examplarily extractions from the vertex and face sub set\n", 98 | "print('Vertices (Sample)')\n", 99 | "print(f'{COMET_67P_SHAPE_OBJ.loc[COMET_67P_SHAPE_OBJ[\"TYPE\"]==\"v\"].head()}')\n", 100 | "print('Faces (Sample)')\n", 101 | "print(f'{COMET_67P_SHAPE_OBJ.loc[COMET_67P_SHAPE_OBJ[\"TYPE\"]==\"f\"].head()}')\n", 102 | "print('\\n')" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": 5, 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [ 111 | "# Assign the VERTICES and faces\n", 112 | "VERTICES = COMET_67P_SHAPE_OBJ.loc[COMET_67P_SHAPE_OBJ['TYPE'] == 'v'][['X1', 'X2', 'X3']].values \\\n", 113 | " .tolist()\n", 114 | "faces = COMET_67P_SHAPE_OBJ.loc[COMET_67P_SHAPE_OBJ['TYPE'] == 'f'][['X1', 'X2', 'X3']].values" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 6, 120 | "metadata": {}, 121 | "outputs": [ 122 | { 123 | "name": "stdout", 124 | "output_type": "stream", 125 | "text": [ 126 | "Minimum vertex index in faces: 1.0\n", 127 | "Maximum vertex index in faces: 597251.0\n", 128 | "\n", 129 | "\n" 130 | ] 131 | } 132 | ], 133 | "source": [ 134 | "# Print the minimum and maximum vertex indices in the face sub set\n", 135 | "print(f'Minimum vertex index in faces: {np.min(faces)}')\n", 136 | "print(f'Maximum vertex index in faces: {np.max(faces)}')\n", 137 | "print('\\n')" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": 7, 143 | "metadata": {}, 144 | "outputs": [], 145 | "source": [ 146 | "# The index in the faces sub set starts at 1. For Python, it needs to start at 0.\n", 147 | "faces = faces - 1\n", 148 | "\n", 149 | "# Convert the indices to integer\n", 150 | "faces = faces.astype(int)\n", 151 | "\n", 152 | "# Convert the numpy array to a Python list\n", 153 | "faces = faces.tolist()" 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": 8, 159 | "metadata": {}, 160 | "outputs": [], 161 | "source": [ 162 | "# Now we need to define a main window class that is needed to set a window size / resolution.\n", 163 | "# Based on the QT4 example:\n", 164 | "# https://github.com/almarklein/visvis/blob/master/examples/embeddingInQt4.py\n", 165 | "from PyQt5.QtWidgets import QWidget, QHBoxLayout\n", 166 | "\n", 167 | "# Import visvis\n", 168 | "import visvis as vv\n", 169 | "\n", 170 | "# Define the class\n", 171 | "class MainWindow(QWidget):\n", 172 | " def __init__(self, *args):\n", 173 | " QWidget.__init__(self, *args)\n", 174 | " self.fig = vv.backends.backend_pyqt5.Figure(self)\n", 175 | " self.sizer = QHBoxLayout(self)\n", 176 | " self.sizer.addWidget(self.fig._widget)\n", 177 | " self.setLayout(self.sizer)\n", 178 | " self.setWindowTitle('Rosetta')\n", 179 | " self.show()" 180 | ] 181 | }, 182 | { 183 | "cell_type": "code", 184 | "execution_count": 9, 185 | "metadata": {}, 186 | "outputs": [], 187 | "source": [ 188 | "# Create visvis application\n", 189 | "app = vv.use()\n", 190 | "app.Create()\n", 191 | "\n", 192 | "# Create main window frame and set a resolution.\n", 193 | "main_w = MainWindow()\n", 194 | "main_w.resize(1200, 800)\n", 195 | "\n", 196 | "# Create the 3 D shape model as a mesh. verticesPerFace equals 3 since triangles define the\n", 197 | "# mesh's surface in this case\n", 198 | "vv.mesh(vertices=VERTICES, faces=faces, verticesPerFace=3)\n", 199 | "\n", 200 | "# Get axes objects\n", 201 | "axes = vv.gca()\n", 202 | "\n", 203 | "# Set a black background\n", 204 | "axes.bgcolor = 'black'\n", 205 | "\n", 206 | "# Deactivate the grid and make the x, y, z axes invisible\n", 207 | "axes.axis.showGrid = False\n", 208 | "axes.axis.visible = False\n", 209 | "\n", 210 | "# Set some camera settings\n", 211 | "# Please note: if you want to \"fly\" arond the comet with w, a, s, d (translation) and i, j, k, l\n", 212 | "# (tilt) replace '3d' with 'fly'\n", 213 | "axes.camera = '3d'\n", 214 | "\n", 215 | "# Field of view in degrees\n", 216 | "axes.camera.fov = 60\n", 217 | "\n", 218 | "# Set default azmiuth and elevation angle in degrees\n", 219 | "axes.camera.azimuth = 120\n", 220 | "axes.camera.elevation = 25\n", 221 | "\n", 222 | "# ... and run the application!\n", 223 | "app.Run()" 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": 10, 229 | "metadata": {}, 230 | "outputs": [ 231 | { 232 | "name": "stderr", 233 | "output_type": "stream", 234 | "text": [ 235 | "100%|██████████| 300/300 [00:10<00:00, 27.90it/s]\n" 236 | ] 237 | } 238 | ], 239 | "source": [ 240 | "# Now let's create an animation\n", 241 | "\n", 242 | "# Create visvis application\n", 243 | "app = vv.use()\n", 244 | "app.Create()\n", 245 | "\n", 246 | "# Create main window frame and set a resolution.\n", 247 | "main_w = MainWindow()\n", 248 | "main_w.resize(500, 400)\n", 249 | "\n", 250 | "# Create the 3 D shape model as a mesh. verticesPerFace equals 3 since triangles define the\n", 251 | "# mesh's surface in this case\n", 252 | "shape_obj = vv.mesh(vertices=VERTICES, faces=faces, verticesPerFace=3)\n", 253 | "shape_obj.specular = 0.0\n", 254 | "shape_obj.diffuse = 0.9\n", 255 | "\n", 256 | "# Get figure\n", 257 | "figure = vv.gcf()\n", 258 | "\n", 259 | "# Get axes objects and set figure parameters\n", 260 | "axes = vv.gca()\n", 261 | "axes.bgcolor = (0, 0, 0)\n", 262 | "axes.axis.showGrid = False\n", 263 | "axes.axis.visible = False\n", 264 | "\n", 265 | "# Set camera settings\n", 266 | "#\n", 267 | "axes.camera = '3d'\n", 268 | "axes.camera.fov = 60\n", 269 | "axes.camera.zoom = 0.1\n", 270 | "\n", 271 | "# Turn off the main light\n", 272 | "axes.light0.Off()\n", 273 | "\n", 274 | "# Create a fixed light source\n", 275 | "light_obj = axes.lights[1]\n", 276 | "light_obj.On()\n", 277 | "light_obj.position = (5.0, 5.0, 5.0, 0.0)\n", 278 | "\n", 279 | "# Empty array that contains all images of the comet's rotation\n", 280 | "comet_images = []\n", 281 | "\n", 282 | "# Rotate camera in 300 steps in azimuth\n", 283 | "for azm_angle in tqdm(range(300)):\n", 284 | "\n", 285 | " # Change azimuth angle of the camera\n", 286 | " axes.camera.azimuth = 360 * float(azm_angle) / 300\n", 287 | "\n", 288 | " # Draw the axes and figure\n", 289 | " axes.Draw()\n", 290 | " figure.DrawNow()\n", 291 | "\n", 292 | " # Get the current image\n", 293 | " temp_image = vv.getframe(vv.gca())\n", 294 | "\n", 295 | " # Apped the current image in 8 bit integer\n", 296 | " comet_images.append((temp_image*255).astype(np.uint8))\n", 297 | "\n", 298 | "# Save the images as an animated GIF\n", 299 | "imageio.mimsave('Comet67P.gif', comet_images, duration=0.04)" 300 | ] 301 | } 302 | ], 303 | "metadata": { 304 | "kernelspec": { 305 | "display_name": "Python 3", 306 | "language": "python", 307 | "name": "python3" 308 | }, 309 | "language_info": { 310 | "codemirror_mode": { 311 | "name": "ipython", 312 | "version": 3 313 | }, 314 | "file_extension": ".py", 315 | "mimetype": "text/x-python", 316 | "name": "python", 317 | "nbconvert_exporter": "python", 318 | "pygments_lexer": "ipython3", 319 | "version": "3.8.2" 320 | } 321 | }, 322 | "nbformat": 4, 323 | "nbformat_minor": 4 324 | } 325 | -------------------------------------------------------------------------------- /part12/SpaceSciencePython_part12.py: -------------------------------------------------------------------------------- 1 | # Import installed modules 2 | import pandas as pd 3 | import numpy as np 4 | import imageio 5 | from tqdm import tqdm 6 | 7 | # Import the Python script func from the auxiliary folder 8 | import sys 9 | sys.path.insert(1, '../_auxiliary') 10 | import func 11 | 12 | #%% 13 | 14 | # Set a local download path and the URL to the 67P shape model data set 15 | DL_PATH = 'data/' 16 | DL_URL = 'https://repos.cosmos.esa.int/socci/projects/SPICE_KERNELS/repos/rosetta/raw/kernels/' \ 17 | + 'dsk/ROS_CG_M001_OSPCLPS_N_V1.OBJ' 18 | 19 | # Download the shape model, create (if needed) the download path and store the data set 20 | func.download_file(DL_PATH, DL_URL) 21 | 22 | #%% 23 | 24 | # Load the shape model. The first column lists whether the row is a vertex or face. The second, 25 | # third and fourth column list the coordiantes (vertex) and vertex indices (faces) 26 | COMET_67P_SHAPE_OBJ = pd.read_csv('data/ROS_CG_M001_OSPCLPS_N_V1.OBJ', delim_whitespace=True, \ 27 | names=['TYPE', 'X1', 'X2', 'X3']) 28 | 29 | # Print some shape model information 30 | print('Some statistics and parameters of the shape model') 31 | print(f'Rows and columns of the data set: {COMET_67P_SHAPE_OBJ.shape}') 32 | print(f'Number of vertices: {COMET_67P_SHAPE_OBJ.loc[COMET_67P_SHAPE_OBJ["TYPE"]=="v"].shape[0]}') 33 | print(f'Number of faces: {COMET_67P_SHAPE_OBJ.loc[COMET_67P_SHAPE_OBJ["TYPE"]=="f"].shape[0]}') 34 | print('\n') 35 | 36 | #%% 37 | 38 | # Print some examplarily extractions from the vertex and face sub set 39 | print('Vertices (Sample)') 40 | print(f'{COMET_67P_SHAPE_OBJ.loc[COMET_67P_SHAPE_OBJ["TYPE"]=="v"].head()}') 41 | print('Faces (Sample)') 42 | print(f'{COMET_67P_SHAPE_OBJ.loc[COMET_67P_SHAPE_OBJ["TYPE"]=="f"].head()}') 43 | print('\n') 44 | 45 | #%% 46 | 47 | # Assign the vertices and faces 48 | VERTICES = COMET_67P_SHAPE_OBJ.loc[COMET_67P_SHAPE_OBJ['TYPE'] == 'v'][['X1', 'X2', 'X3']].values \ 49 | .tolist() 50 | faces = COMET_67P_SHAPE_OBJ.loc[COMET_67P_SHAPE_OBJ['TYPE'] == 'f'][['X1', 'X2', 'X3']].values 51 | 52 | #%% 53 | 54 | # Print the minimum and maximum vertex indices in the face sub set 55 | print(f'Minimum vertex index in faces: {np.min(faces)}') 56 | print(f'Maximum vertex index in faces: {np.max(faces)}') 57 | print('\n') 58 | 59 | #%% 60 | 61 | # The index in the faces sub set starts at 1. For Python, it needs to start at 0. 62 | faces = faces - 1 63 | 64 | # Convert the indices to integer 65 | faces = faces.astype(int) 66 | 67 | # Convert the numpy array to a Python list 68 | faces = faces.tolist() 69 | 70 | #%% 71 | 72 | # Now we need to define a main window class that is needed to set a window size / resolution. 73 | # Based on the QT4 example: 74 | # https://github.com/almarklein/visvis/blob/master/examples/embeddingInQt4.py 75 | from PyQt5.QtWidgets import QWidget, QHBoxLayout 76 | 77 | # Import visvis 78 | import visvis as vv 79 | 80 | # Define the class 81 | class MainWindow(QWidget): 82 | def __init__(self, *args): 83 | QWidget.__init__(self, *args) 84 | self.fig = vv.backends.backend_pyqt5.Figure(self) 85 | self.sizer = QHBoxLayout(self) 86 | self.sizer.addWidget(self.fig._widget) 87 | self.setLayout(self.sizer) 88 | self.setWindowTitle('Rosetta') 89 | self.show() 90 | 91 | #%% 92 | 93 | # Create visvis application 94 | app = vv.use() 95 | app.Create() 96 | 97 | # Create main window frame and set a resolution. 98 | main_w = MainWindow() 99 | main_w.resize(1200, 800) 100 | 101 | # Create the 3 D shape model as a mesh. verticesPerFace equals 3 since triangles define the 102 | # mesh's surface in this case 103 | vv.mesh(vertices=VERTICES, faces=faces, verticesPerFace=3) 104 | 105 | # Get axes objects 106 | axes = vv.gca() 107 | 108 | # Set a black background 109 | axes.bgcolor = 'black' 110 | 111 | # Deactivate the grid and make the x, y, z axes invisible 112 | axes.axis.showGrid = False 113 | axes.axis.visible = False 114 | 115 | # Set some camera settings 116 | # Please note: if you want to "fly" arond the comet with w, a, s, d (translation) and i, j, k, l 117 | # (tilt) replace '3d' with 'fly' 118 | axes.camera = '3d' 119 | 120 | # Field of view in degrees 121 | axes.camera.fov = 60 122 | 123 | # Set default azmiuth and elevation angle in degrees 124 | axes.camera.azimuth = 120 125 | axes.camera.elevation = 25 126 | 127 | # ... and run the application! 128 | app.Run() 129 | 130 | #%% 131 | 132 | # Now let's create an animation 133 | 134 | # Create visvis application 135 | app = vv.use() 136 | app.Create() 137 | 138 | # Create main window frame and set a resolution. 139 | main_w = MainWindow() 140 | main_w.resize(700, 320) 141 | 142 | # Create the 3 D shape model as a mesh. verticesPerFace equals 3 since triangles define the 143 | # mesh's surface in this case 144 | shape_obj = vv.mesh(vertices=VERTICES, faces=faces, verticesPerFace=3) 145 | shape_obj.specular = 0.0 146 | shape_obj.diffuse = 0.9 147 | 148 | # Get figure 149 | figure = vv.gcf() 150 | 151 | # Get axes objects and set figure parameters 152 | axes = vv.gca() 153 | axes.bgcolor = (0, 0, 0) 154 | axes.axis.showGrid = False 155 | axes.axis.visible = False 156 | 157 | # Set camera settings 158 | # 159 | axes.camera = '3d' 160 | axes.camera.fov = 60 161 | axes.camera.zoom = 0.1 162 | 163 | # Turn off the main light 164 | axes.light0.Off() 165 | 166 | # Create a fixed light source 167 | light_obj = axes.lights[1] 168 | light_obj.On() 169 | light_obj.position = (5.0, 5.0, 5.0, 0.0) 170 | 171 | # Empty array that contains all images of the comet's rotation 172 | comet_images = [] 173 | 174 | # Rotate camera in 300 steps in azimuth 175 | for azm_angle in tqdm(range(300)): 176 | 177 | # Change azimuth angle of the camera 178 | axes.camera.azimuth = 360 * float(azm_angle) / 300 179 | 180 | # Draw the axes and figure 181 | axes.Draw() 182 | figure.DrawNow() 183 | 184 | # Get the current image 185 | temp_image = vv.getframe(vv.gca()) 186 | 187 | # Apped the current image in 8 bit integer 188 | comet_images.append((temp_image*255).astype(np.uint8)) 189 | 190 | # Save the images as an animated GIF 191 | imageio.mimsave('Comet67P.gif', comet_images, duration=0.04) 192 | -------------------------------------------------------------------------------- /part13/SpaceSciencePython_part13.py: -------------------------------------------------------------------------------- 1 | # Import the modules 2 | import sqlite3 3 | 4 | import spiceypy 5 | import numpy as np 6 | import pandas as pd 7 | from matplotlib import pyplot as plt 8 | 9 | #%% 10 | 11 | # Establish a connection to the comet database 12 | CON = sqlite3.connect('../_databases/_comets/mpc_comets.db') 13 | 14 | # Extract information about the comet 67P 15 | COMET_67P_FROM_DB = pd.read_sql('SELECT NAME, PERIHELION_AU, ' \ 16 | 'SEMI_MAJOR_AXIS_AU, ' \ 17 | 'APHELION_AU, ECCENTRICITY, ' \ 18 | 'ARG_OF_PERIH_DEG, INCLINATION_DEG ' \ 19 | 'FROM comets_main WHERE NAME LIKE "67P%"', CON) 20 | 21 | #%% 22 | 23 | # Print the orbital elements of 67P 24 | print(f'{COMET_67P_FROM_DB.iloc[0]}') 25 | 26 | #%% 27 | 28 | # Load SPICE kernels (meta file) 29 | spiceypy.furnsh('kernel_meta.txt') 30 | 31 | # Get the G*M value for the Sun 32 | _, GM_SUN_PRE = spiceypy.bodvcd(bodyid=10, item='GM', maxn=1) 33 | GM_SUN = GM_SUN_PRE[0] 34 | 35 | #%% 36 | 37 | # Set an initial and end time 38 | INI_DATETIME = pd.Timestamp('2004-01-01') 39 | END_DATETIME = pd.Timestamp('2014-01-01') 40 | 41 | # Create a numpy array with 1000 timesteps between the initial and end time 42 | datetime_range = np.linspace(INI_DATETIME.value, END_DATETIME.value, 1000) 43 | 44 | # Convert the numpy arraay to a pandas date-time object 45 | datetime_range = pd.to_datetime(datetime_range) 46 | 47 | #%% 48 | 49 | # Set an initial dataframe for the 67P computations 50 | comet_67p_df = pd.DataFrame([]) 51 | 52 | # Set the UTC date-times 53 | comet_67p_df.loc[:, 'UTC'] = datetime_range 54 | 55 | # Convert the UTC date-time strings to ET 56 | comet_67p_df.loc[:, 'ET'] = comet_67p_df['UTC'].apply(lambda x: \ 57 | spiceypy.utc2et(x.strftime('%Y-%m-%dT%H:%M:%S'))) 58 | 59 | # Compute the ET corresponding state vectors 60 | comet_67p_df.loc[:, 'STATE_VEC'] = \ 61 | comet_67p_df['ET'].apply(lambda x: spiceypy.spkgeo(targ=1000012, \ 62 | et=x, \ 63 | ref='ECLIPJ2000', \ 64 | obs=10)[0]) 65 | 66 | # Compute the state vectors corresponding orbital elements 67 | comet_67p_df.loc[:, 'STATE_VEC_ORB_ELEM'] = \ 68 | comet_67p_df.apply(lambda x: spiceypy.oscltx(state=x['STATE_VEC'], \ 69 | et=x['ET'], \ 70 | mu=GM_SUN), \ 71 | axis=1) 72 | 73 | #%% 74 | 75 | # Assign miscellaneous orbital elements as individual columns 76 | # Set the perihelion. Convert km to AU 77 | comet_67p_df.loc[:, 'PERIHELION_AU'] = \ 78 | comet_67p_df['STATE_VEC_ORB_ELEM'].apply(lambda x: \ 79 | spiceypy.convrt(x[0], \ 80 | inunit='km', \ 81 | outunit='AU')) 82 | 83 | # Set the eccentricity 84 | comet_67p_df.loc[:, 'ECCENTRICITY'] = \ 85 | comet_67p_df['STATE_VEC_ORB_ELEM'].apply(lambda x: x[1]) 86 | 87 | # Set the inclination in degrees 88 | comet_67p_df.loc[:, 'INCLINATION_DEG'] = \ 89 | comet_67p_df['STATE_VEC_ORB_ELEM'].apply(lambda x: np.degrees(x[2])) 90 | 91 | # Set the longitude of ascending node in degrees 92 | comet_67p_df.loc[:, 'LONG_OF_ASC_NODE_DEG'] = \ 93 | comet_67p_df['STATE_VEC_ORB_ELEM'].apply(lambda x: np.degrees(x[3])) 94 | 95 | # Set the argument of perihelion in degrees 96 | comet_67p_df.loc[:, 'ARG_OF_PERIH_DEG'] = \ 97 | comet_67p_df['STATE_VEC_ORB_ELEM'].apply(lambda x: np.degrees(x[4])) 98 | 99 | # Set the semi-major axis in AU 100 | comet_67p_df.loc[:, 'SEMI_MAJOR_AXIS_AU'] = \ 101 | comet_67p_df['STATE_VEC_ORB_ELEM'].apply(lambda x: \ 102 | spiceypy.convrt(x[-2], \ 103 | inunit='km', \ 104 | outunit='AU')) 105 | 106 | # Compute the aphelion, based on the semi-major axis and eccentricity 107 | comet_67p_df.loc[:, 'APHELION_AU'] = \ 108 | comet_67p_df.apply(lambda x: x['SEMI_MAJOR_AXIS_AU'] \ 109 | * (1.0 + x['ECCENTRICITY']), \ 110 | axis=1) 111 | 112 | #%% 113 | 114 | # Let's plot the perihelion, eccentricity and argument of perihelion 115 | 116 | # Let's set a dark background 117 | plt.style.use('dark_background') 118 | 119 | # Set a default font size for better readability 120 | plt.rcParams.update({'font.size': 14}) 121 | 122 | # We plot the data dynamically in a for loop. col_name represents the column 123 | # name for both dataframes; ylabel_name is used to change the y label. 124 | for col_name, ylabel_name in zip(['PERIHELION_AU', \ 125 | 'ECCENTRICITY', \ 126 | 'ARG_OF_PERIH_DEG'], \ 127 | ['Perihelion in AU', \ 128 | 'Eccentricity', \ 129 | 'Arg. of. peri. in degrees']): 130 | 131 | # Set a figure with a certain figure size 132 | fig, ax = plt.subplots(figsize=(12, 8)) 133 | 134 | # Line plot of the parameter vs. the UTC date-time from the SPICE data 135 | ax.plot(comet_67p_df['UTC'], \ 136 | comet_67p_df[col_name], \ 137 | color='tab:orange', alpha=0.7, label='SPICE Kernel') 138 | 139 | # As a guideline, plot the parameter data from the MPC data set as a 140 | # horizontal line 141 | ax.hlines(y=COMET_67P_FROM_DB[col_name], \ 142 | xmin=INI_DATETIME, \ 143 | xmax=END_DATETIME, \ 144 | color='tab:orange', linestyles='dashed', label='MPC Data') 145 | 146 | 147 | # Set a grid for better readability 148 | ax.grid(axis='both', linestyle='dashed', alpha=0.2) 149 | 150 | # Set labels for the x and y axis 151 | ax.set_xlabel('Time in UTC') 152 | ax.set_ylabel(ylabel_name) 153 | 154 | # Now we set a legend. However, the marker opacity in the legend has the 155 | # same value as in the plot ... 156 | leg = ax.legend(fancybox=True, loc='upper right', framealpha=1) 157 | 158 | # ... thus, we set the markers' opacity to 1 with this small code 159 | for lh in leg.legendHandles: 160 | lh.set_alpha(1) 161 | 162 | # Save the plot in high quality 163 | plt.savefig(f'67P_{col_name}.png', dpi=300) 164 | 165 | #%% 166 | 167 | # Assignments: 168 | 169 | # 1. Does the Tisserand Parameter w.r.t. Jupiter change over time? 170 | # 2. Visualise the distance between Jupiter and 67P between the initial and 171 | # end time. Use spiceypy.spkgps for this purpose and think about the 172 | # targ and obs parameters. Convert the results in AU. 173 | -------------------------------------------------------------------------------- /part13/kernel_meta.txt: -------------------------------------------------------------------------------- 1 | \begintext 2 | 3 | This meta file contains the relative paths to all needed SPICE kernels. 4 | For each tutorial we will set up an individual kernel_meta.txt. A common 5 | meta file could be easily stored in the main folder (next to the _kernel 6 | directory), however it would be over-loaded at some point. 7 | 8 | \begindata 9 | 10 | KERNELS_TO_LOAD = ( 11 | '../_kernels/spk/de432s.bsp', 12 | '../_kernels/spk/67P_CHURY_GERAS_2004_2016.BSP', 13 | '../_kernels/lsk/naif0012.tls', 14 | '../_kernels/pck/gm_de431.tpc' 15 | ) 16 | -------------------------------------------------------------------------------- /part14/SpaceSciencePython_part14.py: -------------------------------------------------------------------------------- 1 | # Import standard modules 2 | import datetime 3 | 4 | # Import installed modules 5 | import spiceypy 6 | import numpy as np 7 | 8 | # Load the SPICE kernel meta file 9 | spiceypy.furnsh('kernel_meta.txt') 10 | 11 | #%% 12 | 13 | # Get the G*M value of the Sun 14 | _, GM_SUN_PRE = spiceypy.bodvcd(bodyid=10, item='GM', maxn=1) 15 | GM_SUN = GM_SUN_PRE[0] 16 | 17 | # Set the G*M value of Jupiter 18 | _, GM_JUPITER_PRE = spiceypy.bodvcd(bodyid=5, item='GM', maxn=1) 19 | GM_JUPITER = GM_JUPITER_PRE[0] 20 | 21 | #%% 22 | 23 | # Set a sample Ephemeris Time to compute a sample Jupiter state vector and the 24 | # corresponding orbital elements 25 | sample_et = spiceypy.utc2et('2000-001T12:00:00') 26 | 27 | # Compute the state vector of Jupiter as seen from the Sun in ECLIPJ2000 28 | JUPITER_STATE, _ = spiceypy.spkgeo(targ=5, \ 29 | et=sample_et, \ 30 | ref='ECLIPJ2000', \ 31 | obs=10) 32 | 33 | # Determine the corresponding orbital elements of Jupiter 34 | JUPITER_ORB_ELEM = spiceypy.oscltx(state=JUPITER_STATE, \ 35 | et=sample_et, \ 36 | mu=GM_SUN) 37 | 38 | # Extract the semi-major axis of Jupiter ... 39 | JUPITER_A = JUPITER_ORB_ELEM[-2] 40 | 41 | # ... and print the results in AU 42 | print('Semi-major axis of Jupiter in AU: ' \ 43 | f'{spiceypy.convrt(JUPITER_A, inunit="km", outunit="AU")}') 44 | print('\n') 45 | 46 | #%% 47 | 48 | # Compute the SOI radius of Jupiter 49 | SOI_JUPITER_R = JUPITER_A * (GM_JUPITER/GM_SUN) ** (2.0/5.0) * 1 50 | 51 | # Print the SOI's radius 52 | print('SOI of Jupiter in AU: ' \ 53 | f'{spiceypy.convrt(SOI_JUPITER_R, inunit="km", outunit="AU")}') 54 | print('\n') 55 | 56 | #%% 57 | 58 | # Compute the state vector and the corresponding orbital elements of 67P as 59 | # seen from the Sun 60 | 61 | # Create an ET (2004 day 1 is the minimum date-time in the corresponding SPICE 62 | # spk kernel) 63 | sample_et = spiceypy.utc2et('2004-001T00:00:00') 64 | 65 | # Compute the state vector of 67P ... 66 | COMET_67P_STATE, _ = spiceypy.spkgeo(targ=1000012, \ 67 | et=sample_et, \ 68 | ref='ECLIPJ2000', \ 69 | obs=10) 70 | 71 | # ... and the corresponding orbital elements 72 | COMET_67P_ORB_ELEM = spiceypy.oscelt(state=COMET_67P_STATE, \ 73 | et=sample_et, \ 74 | mu=GM_SUN) 75 | 76 | #%% 77 | 78 | # Now we want to determine when 67P enters the SOI of Jupiter. As a starting 79 | # date we set the 1st January 2017 and compute everything back in time 80 | datetime_stamp = datetime.datetime(year=2017, month=1, day=1) 81 | 82 | # Our computation will be performed within a while condition (to check whether 83 | # 67P entered the SOI or not); thus we need to set an initial value for the 84 | # while condition. Here: a very large distance between 67P and Jupiter 85 | comet_jup_dist = 10.0**10 86 | 87 | # While condition: Compute the following coding part as long as 67P did not 88 | # enter Jupiter's SOI 89 | while comet_jup_dist > SOI_JUPITER_R: 90 | 91 | # Add one hour to the date-time stamp and convert it ot ET 92 | datetime_stamp = datetime_stamp + datetime.timedelta(hours=1) 93 | et_stamp = spiceypy.datetime2et(datetime_stamp) 94 | 95 | # Compute the state vector of 67P based on the initial orbital elements 96 | # (Sun-centric in ECLIPJ2000) 97 | COMET_67P_STATE_ORB = spiceypy.conics(COMET_67P_ORB_ELEM, et_stamp) 98 | 99 | # Compute Jupiter's state vector in as seen from the Sun 100 | JUPITER_STATE, _ = spiceypy.spkgeo(targ=5, \ 101 | et=et_stamp, \ 102 | ref='ECLIPJ2000', \ 103 | obs=10) 104 | 105 | # Compute the distance between Jupiter and 67P 106 | comet_jup_dist = spiceypy.vnorm(JUPITER_STATE[:3]-COMET_67P_STATE_ORB[:3]) 107 | 108 | #%% 109 | 110 | # If the while condition is not fulfilled, 67P crosses Jupiter's SOI! Let's 111 | # take a look when this happened and also let's verify the distance to 112 | # Jupiter: 113 | print(f'67P entering Jupiter\'s SOI: {datetime_stamp.strftime("%Y-%m-%d")}') 114 | print('67P distance to Jupiter at SOI crossing in AU: ' \ 115 | f'{spiceypy.convrt(comet_jup_dist, inunit="km", outunit="AU")}') 116 | 117 | #%% 118 | 119 | # Transform the state vector of 67P from a Sun-centric system to a Jupiter- 120 | # centric system ... 121 | COMET_67P_STATE_JUP_CNTR = COMET_67P_STATE_ORB - JUPITER_STATE 122 | 123 | # ... and compute the corresponding orbital elements. This time, we need the 124 | # G*M value of Jupiter! 125 | COMET_67P_ORB_ELEM_JUP_CNTR = spiceypy.oscelt(state=COMET_67P_STATE_JUP_CNTR, \ 126 | et=et_stamp, \ 127 | mu=GM_JUPITER) 128 | 129 | #%% 130 | 131 | # Let's take a look at the perijove. This will tell us at what distance 132 | # 67P will have it's closes encounter with Jupiter 133 | print('Closest distance between 67P and Jupiter in km: ' \ 134 | f'{COMET_67P_ORB_ELEM_JUP_CNTR[0]}') 135 | 136 | print('Closest distance between 67P and Jupiter in SOI radius percentage: ' \ 137 | f'{round(COMET_67P_ORB_ELEM_JUP_CNTR[0] / SOI_JUPITER_R, 2) * 100}') 138 | 139 | # Not surprisingly, 67P is not bound to Jupiter. The eccentricity in this 140 | # Jupiter-centric computation is larger than 1: 141 | print('67P\'s eccentricity in a Jupiter-centric system: ' \ 142 | f'{COMET_67P_ORB_ELEM_JUP_CNTR[1]}') 143 | print('\n') 144 | 145 | #%% 146 | 147 | # In an additional while condition we compute the trajectory of 67P within 148 | # Jupiter's SOI until it reaches, again, the SOI border 149 | while comet_jup_dist <= SOI_JUPITER_R: 150 | 151 | # Add one hour to the ET from the last while condition and convert it to 152 | # ET 153 | datetime_stamp = datetime_stamp + datetime.timedelta(hours=1) 154 | et_stamp = spiceypy.datetime2et(datetime_stamp) 155 | 156 | # Compute an ET corresponding Jupiter-centric state vector of 67P 157 | comet_67p_state_orb_jup_cntr = \ 158 | spiceypy.conics(COMET_67P_ORB_ELEM_JUP_CNTR, et_stamp) 159 | 160 | # Since we compute everything in a Jupiter-centric system, the norm of the 161 | # state vector is also the distance to Jupiter 162 | comet_jup_dist = spiceypy.vnorm(comet_67p_state_orb_jup_cntr[:3]) 163 | 164 | #%% 165 | 166 | # When did 67P leave the SOI? 167 | print(f'67P leaving Jupiter\'s SOI: {datetime_stamp.strftime("%Y-%m-%d")}') 168 | 169 | #%% 170 | 171 | # Now we need to re-transform the Jupiter centric state vector back to a Sun- 172 | # centric one. First, compute the state vector of Jupiter as seen form the Sun 173 | # at the time when 67P leaves the SOI of Jupiter: 174 | JUPITER_STATE, _ = spiceypy.spkgeo(targ=5, et=et_stamp, ref='ECLIPJ2000', \ 175 | obs=10) 176 | 177 | # A simple vector addition leads to a Sun-centric 67P state vector 178 | COMET_67P_STATE_ORB_AFTER = comet_67p_state_orb_jup_cntr + JUPITER_STATE 179 | 180 | #%% 181 | 182 | # And now we can compute the state vector after the close encounter with 183 | # Jupiter: 184 | COMET_67P_ORB_ELEM_AFTER = spiceypy.oscelt(state=COMET_67P_STATE_ORB_AFTER, \ 185 | et=et_stamp, mu=GM_SUN) 186 | 187 | #%% 188 | 189 | # Finally, let's plot the differences between the "old" and "new" orbital 190 | # elements 191 | print('Perihelion in AU '\ 192 | 'before: ' \ 193 | f'{round(spiceypy.convrt(COMET_67P_ORB_ELEM[0], "km", "AU"), 2)}, ' \ 194 | 'after: ' \ 195 | f'{round(spiceypy.convrt(COMET_67P_ORB_ELEM_AFTER[0], "km", "AU"), 2)}') 196 | 197 | print('Eccentricity '\ 198 | 'before: ' \ 199 | f'{round(COMET_67P_ORB_ELEM[1], 4)}, ' \ 200 | 'after: ' \ 201 | f'{round(COMET_67P_ORB_ELEM_AFTER[1], 4)}') 202 | 203 | print('Inclination in degrees '\ 204 | 'before: ' \ 205 | f'{round(np.degrees(COMET_67P_ORB_ELEM[2]), 2)}, ' \ 206 | 'after: ' \ 207 | f'{round(np.degrees(COMET_67P_ORB_ELEM_AFTER[2]), 2)}') 208 | 209 | print('Longitude of ascending node in degrees '\ 210 | 'before: ' \ 211 | f'{round(np.degrees(COMET_67P_ORB_ELEM[3]), 2)}, ' \ 212 | 'after: ' \ 213 | f'{round(np.degrees(COMET_67P_ORB_ELEM_AFTER[3]), 2)}') 214 | 215 | print('Argument of perihelion in degrees '\ 216 | 'before: ' \ 217 | f'{round(np.degrees(COMET_67P_ORB_ELEM[4]), 2)}, ' \ 218 | 'after: ' \ 219 | f'{round(np.degrees(COMET_67P_ORB_ELEM_AFTER[4]), 2)}') 220 | -------------------------------------------------------------------------------- /part14/kernel_meta.txt: -------------------------------------------------------------------------------- 1 | \begintext 2 | 3 | This meta file contains the relative paths to all needed SPICE kernels. 4 | For each tutorial we will set up an individual kernel_meta.txt. A common 5 | meta file could be easily stored in the main folder (next to the _kernel 6 | directory), however it would be over-loaded at some point. 7 | 8 | \begindata 9 | 10 | KERNELS_TO_LOAD = ( 11 | '../_kernels/spk/de432s.bsp', 12 | '../_kernels/spk/67P_CHURY_GERAS_2004_2016.BSP', 13 | '../_kernels/lsk/naif0012.tls', 14 | '../_kernels/pck/gm_de431.tpc' 15 | ) 16 | -------------------------------------------------------------------------------- /part15/SpaceSciencePython_part15.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# Import standard modules\n", 10 | "import sqlite3\n", 11 | "from datetime import datetime, timedelta\n", 12 | "\n", 13 | "# Import installed modules\n", 14 | "import spiceypy\n", 15 | "import numpy as np\n", 16 | "import pandas as pd" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 2, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "# Load the SPICE kernel meta file\n", 26 | "spiceypy.furnsh('kernel_meta.txt')" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 3, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "# Get the G*M value of the Sun\n", 36 | "_, GM_SUN_PRE = spiceypy.bodvcd(bodyid=10, item='GM', maxn=1)\n", 37 | "GM_SUN = GM_SUN_PRE[0]" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 4, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "# Connect to the comet database\n", 47 | "CON = sqlite3.connect('../_databases/_comets/mpc_comets.db')\n", 48 | "\n", 49 | "# Extract orbit data of the comet C/2019 Y4 (ATLAS)\n", 50 | "ATLAS_ORB_EL = pd.read_sql('SELECT NAME, PERIHELION_AU, ' \\\n", 51 | " 'ECCENTRICITY, INCLINATION_DEG, ' \\\n", 52 | " 'LONG_OF_ASC_NODE_DEG, ARG_OF_PERIH_DEG, ' \\\n", 53 | " 'MEAN_ANOMALY_DEG, EPOCH_ET ' \\\n", 54 | " 'FROM comets_main ' \\\n", 55 | " 'WHERE NAME=\"C/2019 Y4 (ATLAS)\"', CON)\n", 56 | "\n", 57 | "# Convert the perihelion, that is given in AU, to km\n", 58 | "ATLAS_ORB_EL.loc[:, 'PERIHELION_KM'] = \\\n", 59 | " ATLAS_ORB_EL['PERIHELION_AU'].apply(lambda x: \\\n", 60 | " spiceypy.convrt(x, inunit='AU', \\\n", 61 | " outunit='km'))\n", 62 | "\n", 63 | "# Convert all angular parameters to radians, since the entries in the database\n", 64 | "# are stored in degrees. The for-loop iterates through all column names that\n", 65 | "# contain the word \"DEG\"\n", 66 | "for angle_col_name in [col for col in ATLAS_ORB_EL.columns if 'DEG' in col]:\n", 67 | " ATLAS_ORB_EL.loc[:, angle_col_name.replace('DEG', 'RAD')] = \\\n", 68 | " np.radians(ATLAS_ORB_EL[angle_col_name])\n", 69 | "\n", 70 | "# Add the G*M value of the Sun\n", 71 | "ATLAS_ORB_EL.loc[:, 'SUN_GM'] = GM_SUN" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 5, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "# Extract all orbital elements / information in a SPICE compatible order (see\n", 81 | "# function conics)\n", 82 | "ATLAS_SPICE_ORB_EL = ATLAS_ORB_EL[['PERIHELION_KM', 'ECCENTRICITY', \\\n", 83 | " 'INCLINATION_RAD', 'LONG_OF_ASC_NODE_RAD', \\\n", 84 | " 'ARG_OF_PERIH_RAD', 'MEAN_ANOMALY_DEG', \\\n", 85 | " 'EPOCH_ET', 'SUN_GM']].iloc[0].values" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 6, 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "# Set an initial time and end time for the computation procedure\n", 95 | "INI_DATETIME = datetime(year=2020, month=5, day=20)\n", 96 | "END_DATETIME = datetime(year=2020, month=6, day=10)\n", 97 | "\n", 98 | "# Create an array that covers the initial and end time in 1 hour steps\n", 99 | "TIME_ARRAY = np.arange(INI_DATETIME, END_DATETIME, \\\n", 100 | " timedelta(hours=1)).astype(datetime)" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": 7, 106 | "metadata": {}, 107 | "outputs": [], 108 | "source": [ 109 | "# Set an empty array that will store the distances between the Sun\n", 110 | "# and ATLAS\n", 111 | "atlas_vecs = []\n", 112 | "\n", 113 | "# Set an empty array that will store the distances between the Sun\n", 114 | "# and the Solar Orbiter\n", 115 | "solar_orb_vecs = []\n", 116 | "\n", 117 | "# Iterate through the time array (comet ATLAS)\n", 118 | "for atlas_time_step in TIME_ARRAY:\n", 119 | "\n", 120 | " # Compute the ET\n", 121 | " atlas_et = spiceypy.datetime2et(atlas_time_step)\n", 122 | "\n", 123 | " # Compute the ET corresponding state vector of the comet ATLAS\n", 124 | " atlas_state_vec = spiceypy.conics(ATLAS_SPICE_ORB_EL, atlas_et)\n", 125 | "\n", 126 | " # Store the position vector\n", 127 | " atlas_vecs.append(atlas_state_vec[:3])\n", 128 | "\n", 129 | "# Iterate through the time array (Solar Orbiter)\n", 130 | "for so_time_step in TIME_ARRAY:\n", 131 | "\n", 132 | " # Compute the ET\n", 133 | " so_et = spiceypy.datetime2et(so_time_step)\n", 134 | "\n", 135 | " # Compute the state vector of the Solar Orbiter (NAIF ID: -144)\n", 136 | " solar_orb_state_vec, _ = spiceypy.spkgeo(targ=-144, et=so_et, \\\n", 137 | " ref='ECLIPJ2000', obs=10)\n", 138 | "\n", 139 | " # Store the position vector\n", 140 | " solar_orb_vecs.append(solar_orb_state_vec[:3])\n", 141 | "\n", 142 | "# Convert the lists that contain the vectors to numpy lists\n", 143 | "atlas_vecs = np.array(atlas_vecs)\n", 144 | "solar_orb_vecs = np.array(solar_orb_vecs)" 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": 8, 150 | "metadata": {}, 151 | "outputs": [ 152 | { 153 | "name": "stdout", 154 | "output_type": "stream", 155 | "text": [ 156 | "Minimum distance ATLAS - Sun in AU: 0.25281227838079623\n", 157 | "Minimum distance Solar Orbiter - Sun in AU: 0.5208229568917663\n", 158 | "\n", 159 | "\n" 160 | ] 161 | } 162 | ], 163 | "source": [ 164 | "# Minimum distance ATLAS - Sun\n", 165 | "MIN_DIST_ATLAS_SUN = np.min(np.linalg.norm(atlas_vecs, axis=1))\n", 166 | "print('Minimum distance ATLAS - Sun in AU: ' \\\n", 167 | " f'{spiceypy.convrt(MIN_DIST_ATLAS_SUN, \"km\", \"AU\")}')\n", 168 | "\n", 169 | "# Minimum distance Solar Orbiter - Sun\n", 170 | "MIN_DIST_SOLAR_ORB_SUN = np.min(np.linalg.norm(solar_orb_vecs, axis=1))\n", 171 | "print('Minimum distance Solar Orbiter - Sun in AU: ' \\\n", 172 | " f'{spiceypy.convrt(MIN_DIST_SOLAR_ORB_SUN, \"km\", \"AU\")}')\n", 173 | "print('\\n')" 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": 9, 179 | "metadata": {}, 180 | "outputs": [ 181 | { 182 | "name": "stdout", 183 | "output_type": "stream", 184 | "text": [ 185 | "Minimum distance between ATLAS and Solar Orbiter in km: 40330530.0\n", 186 | "\n", 187 | "\n" 188 | ] 189 | } 190 | ], 191 | "source": [ 192 | "# What is the closest approach between both trajectories?\n", 193 | "# Compute a matrix that contains all possible distances, using the scipy\n", 194 | "# function cdist\n", 195 | "import scipy.spatial\n", 196 | "MIN_DIST_MATRIX = scipy.spatial.distance.cdist(atlas_vecs, solar_orb_vecs)\n", 197 | "\n", 198 | "# Print the minimum distance\n", 199 | "print('Minimum distance between ATLAS and Solar Orbiter in km: ' \\\n", 200 | " f'{np.min(np.round(MIN_DIST_MATRIX))}')\n", 201 | "print('\\n')" 202 | ] 203 | }, 204 | { 205 | "cell_type": "code", 206 | "execution_count": 10, 207 | "metadata": {}, 208 | "outputs": [ 209 | { 210 | "name": "stdout", 211 | "output_type": "stream", 212 | "text": [ 213 | "ATLAS Index of close approach: 292\n", 214 | "Solar Orbiter Index of close approach: 503\n", 215 | "\n", 216 | "\n" 217 | ] 218 | } 219 | ], 220 | "source": [ 221 | "# The timing needs to be correct too! The comet produces ions and creates\n", 222 | "# its tail within the spacecraft's trajectory. Thus, the comet needs to pass\n", 223 | "# by the minimum distance first\n", 224 | "\n", 225 | "# Determine the distance matrix indices of the closest approach\n", 226 | "indices_min = np.where(MIN_DIST_MATRIX == np.min(MIN_DIST_MATRIX))\n", 227 | "indices_min = [k.item() for k in indices_min]\n", 228 | "\n", 229 | "# Let's print the indices for\n", 230 | "# ATLAS\n", 231 | "print(f'ATLAS Index of close approach: {indices_min[0]}')\n", 232 | "\n", 233 | "# Solar Orbiter\n", 234 | "print(f'Solar Orbiter Index of close approach: {indices_min[1]}')\n", 235 | "print('\\n')" 236 | ] 237 | }, 238 | { 239 | "cell_type": "code", 240 | "execution_count": 11, 241 | "metadata": {}, 242 | "outputs": [ 243 | { 244 | "name": "stdout", 245 | "output_type": "stream", 246 | "text": [ 247 | "ATLAS closest approach date-time: 2020-06-01 04:00:00\n", 248 | "Solar Orbiter closest approach date-time: 2020-06-09 23:00:00\n", 249 | "\n", 250 | "\n" 251 | ] 252 | } 253 | ], 254 | "source": [ 255 | "# Corresponding times (only a few days apart. Thus, an ion tail could be\n", 256 | "# detectable)\n", 257 | "print(f'ATLAS closest approach date-time: {TIME_ARRAY[indices_min[0]]}')\n", 258 | "print('Solar Orbiter closest approach date-time: ' \\\n", 259 | " f'{TIME_ARRAY[indices_min[1]]}')\n", 260 | "print('\\n')" 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": 12, 266 | "metadata": {}, 267 | "outputs": [ 268 | { 269 | "name": "stdout", 270 | "output_type": "stream", 271 | "text": [ 272 | "Minimum angular distance between a possible ion tail and the Solar Orbiter's trajectory in degrees: 7.74\n" 273 | ] 274 | } 275 | ], 276 | "source": [ 277 | "# ... but is the ion tail \"aiming\" towards the trajectory of the spacecraft?\n", 278 | "# (at least within a few degrees?)\n", 279 | "# Compute the angular distance between the trajectories' closest approach\n", 280 | "\n", 281 | "# Set the closest approach vectors, based on the obtained indices for ATLAS and\n", 282 | "# the Solar Orbiter, respectively\n", 283 | "VEC_ATLAS_AP = atlas_vecs[indices_min[0]]\n", 284 | "VEC_SOLAR_ORB_AP = solar_orb_vecs[indices_min[1]]\n", 285 | "\n", 286 | "# Determine the norm of both closest approach vectors\n", 287 | "ATLAS_NORM_AP = spiceypy.vnorm(VEC_ATLAS_AP)\n", 288 | "SOLORB_NORM_AP = spiceypy.vnorm(VEC_SOLAR_ORB_AP)\n", 289 | "\n", 290 | "# Compute the dot product\n", 291 | "DOT_PRODUCT_AP = np.dot(VEC_ATLAS_AP, VEC_SOLAR_ORB_AP)\n", 292 | "\n", 293 | "# Compute the angle\n", 294 | "ANGULAR_DIST_AP = np.degrees(np.arccos((DOT_PRODUCT_AP) \\\n", 295 | " / (ATLAS_NORM_AP * SOLORB_NORM_AP)))\n", 296 | "\n", 297 | "# Print the angular distance between ATLAS' ion tail direction and the position\n", 298 | "# vector of the spacecraft at the closest approach\n", 299 | "print('Minimum angular distance between a possible ion tail and the ' \\\n", 300 | " 'Solar Orbiter\\'s trajectory in degrees: ' \\\n", 301 | " f'{np.round(ANGULAR_DIST_AP, 2)}')" 302 | ] 303 | } 304 | ], 305 | "metadata": { 306 | "kernelspec": { 307 | "display_name": "Python 3", 308 | "language": "python", 309 | "name": "python3" 310 | }, 311 | "language_info": { 312 | "codemirror_mode": { 313 | "name": "ipython", 314 | "version": 3 315 | }, 316 | "file_extension": ".py", 317 | "mimetype": "text/x-python", 318 | "name": "python", 319 | "nbconvert_exporter": "python", 320 | "pygments_lexer": "ipython3", 321 | "version": "3.8.2" 322 | } 323 | }, 324 | "nbformat": 4, 325 | "nbformat_minor": 4 326 | } 327 | -------------------------------------------------------------------------------- /part15/SpaceSciencePython_part15.py: -------------------------------------------------------------------------------- 1 | # Import standard modules 2 | import sqlite3 3 | from datetime import datetime, timedelta 4 | 5 | # Import installed modules 6 | import spiceypy 7 | import numpy as np 8 | import pandas as pd 9 | 10 | #%% 11 | 12 | # Load the SPICE kernel meta file 13 | spiceypy.furnsh('kernel_meta.txt') 14 | 15 | #%% 16 | 17 | # Get the G*M value of the Sun 18 | _, GM_SUN_PRE = spiceypy.bodvcd(bodyid=10, item='GM', maxn=1) 19 | GM_SUN = GM_SUN_PRE[0] 20 | 21 | #%% 22 | 23 | # Connect to the comet database 24 | CON = sqlite3.connect('../_databases/_comets/mpc_comets.db') 25 | 26 | # Extract orbit data of the comet C/2019 Y4 (ATLAS) 27 | ATLAS_ORB_EL = pd.read_sql('SELECT NAME, PERIHELION_AU, ' \ 28 | 'ECCENTRICITY, INCLINATION_DEG, ' \ 29 | 'LONG_OF_ASC_NODE_DEG, ARG_OF_PERIH_DEG, ' \ 30 | 'MEAN_ANOMALY_DEG, EPOCH_ET ' \ 31 | 'FROM comets_main ' \ 32 | 'WHERE NAME="C/2019 Y4 (ATLAS)"', CON) 33 | 34 | # Convert the perihelion, that is given in AU, to km 35 | ATLAS_ORB_EL.loc[:, 'PERIHELION_KM'] = \ 36 | ATLAS_ORB_EL['PERIHELION_AU'].apply(lambda x: \ 37 | spiceypy.convrt(x, inunit='AU', \ 38 | outunit='km')) 39 | 40 | # Convert all angular parameters to radians, since the entries in the database 41 | # are stored in degrees. The for-loop iterates through all column names that 42 | # contain the word "DEG" 43 | for angle_col_name in [col for col in ATLAS_ORB_EL.columns if 'DEG' in col]: 44 | ATLAS_ORB_EL.loc[:, angle_col_name.replace('DEG', 'RAD')] = \ 45 | np.radians(ATLAS_ORB_EL[angle_col_name]) 46 | 47 | # Add the G*M value of the Sun 48 | ATLAS_ORB_EL.loc[:, 'SUN_GM'] = GM_SUN 49 | 50 | #%% 51 | 52 | # Extract all orbital elements / information in a SPICE compatible order (see 53 | # function conics) 54 | ATLAS_SPICE_ORB_EL = ATLAS_ORB_EL[['PERIHELION_KM', 'ECCENTRICITY', \ 55 | 'INCLINATION_RAD', 'LONG_OF_ASC_NODE_RAD', \ 56 | 'ARG_OF_PERIH_RAD', 'MEAN_ANOMALY_DEG', \ 57 | 'EPOCH_ET', 'SUN_GM']].iloc[0].values 58 | 59 | #%% 60 | 61 | # Set an initial time and end time for the computation procedure 62 | INI_DATETIME = datetime(year=2020, month=5, day=20) 63 | END_DATETIME = datetime(year=2020, month=6, day=10) 64 | 65 | # Create an array that covers the initial and end time in 1 hour steps 66 | TIME_ARRAY = np.arange(INI_DATETIME, END_DATETIME, \ 67 | timedelta(hours=1)).astype(datetime) 68 | 69 | #%% 70 | 71 | # Set an empty array that will store the distances between the Sun 72 | # and ATLAS 73 | atlas_vecs = [] 74 | 75 | # Set an empty array that will store the distances between the Sun 76 | # and the Solar Orbiter 77 | solar_orb_vecs = [] 78 | 79 | # Iterate through the time array (comet ATLAS) 80 | for atlas_time_step in TIME_ARRAY: 81 | 82 | # Compute the ET 83 | atlas_et = spiceypy.datetime2et(atlas_time_step) 84 | 85 | # Compute the ET corresponding state vector of the comet ATLAS 86 | atlas_state_vec = spiceypy.conics(ATLAS_SPICE_ORB_EL, atlas_et) 87 | 88 | # Store the position vector 89 | atlas_vecs.append(atlas_state_vec[:3]) 90 | 91 | # Iterate through the time array (Solar Orbiter) 92 | for so_time_step in TIME_ARRAY: 93 | 94 | # Compute the ET 95 | so_et = spiceypy.datetime2et(so_time_step) 96 | 97 | # Compute the state vector of the Solar Orbiter (NAIF ID: -144) 98 | solar_orb_state_vec, _ = spiceypy.spkgeo(targ=-144, et=so_et, \ 99 | ref='ECLIPJ2000', obs=10) 100 | 101 | # Store the position vector 102 | solar_orb_vecs.append(solar_orb_state_vec[:3]) 103 | 104 | # Convert the lists that contain the vectors to numpy lists 105 | atlas_vecs = np.array(atlas_vecs) 106 | solar_orb_vecs = np.array(solar_orb_vecs) 107 | 108 | #%% 109 | 110 | # Minimum distance ATLAS - Sun 111 | MIN_DIST_ATLAS_SUN = np.min(np.linalg.norm(atlas_vecs, axis=1)) 112 | print('Minimum distance ATLAS - Sun in AU: ' \ 113 | f'{spiceypy.convrt(MIN_DIST_ATLAS_SUN, "km", "AU")}') 114 | 115 | # Minimum distance Solar Orbiter - Sun 116 | MIN_DIST_SOLAR_ORB_SUN = np.min(np.linalg.norm(solar_orb_vecs, axis=1)) 117 | print('Minimum distance Solar Orbiter - Sun in AU: ' \ 118 | f'{spiceypy.convrt(MIN_DIST_SOLAR_ORB_SUN, "km", "AU")}') 119 | print('\n') 120 | 121 | #%% 122 | 123 | # What is the closest approach between both trajectories? 124 | # Compute a matrix that contains all possible distances, using the scipy 125 | # function cdist 126 | import scipy.spatial 127 | MIN_DIST_MATRIX = scipy.spatial.distance.cdist(atlas_vecs, solar_orb_vecs) 128 | 129 | # Print the minimum distance 130 | print('Minimum distance between ATLAS and Solar Orbiter in km: ' \ 131 | f'{np.min(np.round(MIN_DIST_MATRIX))}') 132 | print('\n') 133 | 134 | #%% 135 | 136 | # The timing needs to be correct too! The comet produces ions and creates 137 | # its tail within the spacecraft's trajectory. Thus, the comet needs to pass 138 | # by the minimum distance first 139 | 140 | # Determine the distance matrix indices of the closest approach 141 | indices_min = np.where(MIN_DIST_MATRIX == np.min(MIN_DIST_MATRIX)) 142 | indices_min = [k.item() for k in indices_min] 143 | 144 | # Let's print the indices for 145 | # ATLAS 146 | print(f'Atlas Index of close approach: {indices_min[0]}') 147 | 148 | # Solar Orbiter 149 | print(f'Solar Orbiter Index of close approach: {indices_min[1]}') 150 | print('\n') 151 | 152 | #%% 153 | 154 | # Corresponding times (only a few days apart. Thus, an ion tail could be 155 | # detectable) 156 | print(f'ATLAS closest approach date-time: {TIME_ARRAY[indices_min[0]]}') 157 | print('Solar Orbiter closest approach date-time: ' \ 158 | f'{TIME_ARRAY[indices_min[1]]}') 159 | print('\n') 160 | 161 | #%% 162 | 163 | # ... but is the ion tail "aiming" towards the trajectory of the spacecraft? 164 | # (at least within a few degrees?) 165 | # Compute the angular distance between the trajectories' closest approach 166 | 167 | # Set the closest approach vectors, based on the obtained indices for ATLAS and 168 | # the Solar Orbiter, respectively 169 | VEC_ATLAS_AP = atlas_vecs[indices_min[0]] 170 | VEC_SOLAR_ORB_AP = solar_orb_vecs[indices_min[1]] 171 | 172 | # Determine the norm of both closest approach vectors 173 | ATLAS_NORM_AP = spiceypy.vnorm(VEC_ATLAS_AP) 174 | SOLORB_NORM_AP = spiceypy.vnorm(VEC_SOLAR_ORB_AP) 175 | 176 | # Compute the dot product 177 | DOT_PRODUCT_AP = np.dot(VEC_ATLAS_AP, VEC_SOLAR_ORB_AP) 178 | 179 | # Compute the angle 180 | ANGULAR_DIST_AP = np.degrees(np.arccos((DOT_PRODUCT_AP) \ 181 | / (ATLAS_NORM_AP * SOLORB_NORM_AP))) 182 | 183 | # Print the angular distance between ATLAS' ion tail direction and the position 184 | # vector of the spacecraft at the closest approach 185 | print('Minimum angular distance between a possible ion tail and the ' \ 186 | 'Solar Orbiter\'s trajectory in degrees: ' \ 187 | f'{np.round(ANGULAR_DIST_AP, 2)}') 188 | -------------------------------------------------------------------------------- /part15/kernel_meta.txt: -------------------------------------------------------------------------------- 1 | \begintext 2 | 3 | This meta file contains the relative paths to all needed SPICE kernels. 4 | For each tutorial we will set up an individual kernel_meta.txt. A common 5 | meta file could be easily stored in the main folder (next to the _kernel 6 | directory), however it would be over-loaded at some point. 7 | 8 | \begindata 9 | 10 | KERNELS_TO_LOAD = ( 11 | '../_kernels/spk/de432s.bsp', 12 | '../_kernels/spk/solo_ANC_soc-orbit_20200210-20301120_L015_V1_00024_V01.bsp', 13 | '../_kernels/lsk/naif0012.tls', 14 | '../_kernels/pck/gm_de431.tpc' 15 | ) 16 | -------------------------------------------------------------------------------- /part16/SpaceSciencePython_part16.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# Import modules\n", 10 | "import pandas as pd\n", 11 | "import numpy as np\n", 12 | "\n", 13 | "# Create an empty dataframe lookup table\n", 14 | "mag_lookup_df = pd.DataFrame([])" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 2, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "# Function that converts magnitude to irradiance in W/m^2\n", 24 | "def mag2irr(mag, use_attn=False):\n", 25 | " \"\"\"\n", 26 | " This function converts the apparent magnitude to the corresponding\n", 27 | " irradiance value given in [W/m^2].\n", 28 | "\n", 29 | " Parameters\n", 30 | " ----------\n", 31 | " mag : float\n", 32 | " The astronomical magnitude.\n", 33 | " use_attn : bool, optional\n", 34 | " Boolean value. If True, a constant factor will be applied that\n", 35 | " represents a simple atmospheric attenuation. The default is False.\n", 36 | "\n", 37 | " Returns\n", 38 | " -------\n", 39 | " irr : float\n", 40 | " The resulting irradiance given in [W/m^2].\n", 41 | " \"\"\"\n", 42 | "\n", 43 | " # If the user wants the atmospheric attenuation, a constant value of 0.4\n", 44 | " # is set ...\n", 45 | " if use_attn:\n", 46 | " attn = 0.4\n", 47 | "\n", 48 | " # ... otherwise a value of 0 is set\n", 49 | " else:\n", 50 | " attn = 0.0\n", 51 | "\n", 52 | " # Compute the irradiance\n", 53 | " irr = 10.0 ** (0.4 * (-mag - 19.0 + attn))\n", 54 | "\n", 55 | " # Return the irradiance\n", 56 | " return irr" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": 3, 62 | "metadata": {}, 63 | "outputs": [], 64 | "source": [ 65 | "# Function that converts the irradiance to power given in W\n", 66 | "def irr2pwr(irr, area):\n", 67 | " \"\"\"\n", 68 | " This function converts the irradiance given in [W/m^2] to the power [W]\n", 69 | " for a user defined area given in [m^2]\n", 70 | "\n", 71 | " Parameters\n", 72 | " ----------\n", 73 | " irr : float\n", 74 | " The irradiance given in [W/m^2].\n", 75 | " area : float\n", 76 | " The area given in [m^2].\n", 77 | "\n", 78 | " Returns\n", 79 | " -------\n", 80 | " pwr : float\n", 81 | " The resulting power given in [W].\n", 82 | " \"\"\"\n", 83 | " pwr = irr * area\n", 84 | "\n", 85 | " return pwr" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 4, 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "# Function that converts the power given in [W] to energy given in [J]\n", 95 | "def pwr2enr(pwr, time):\n", 96 | " \"\"\"\n", 97 | " This function converts the power given in [W] to the corresponding energy\n", 98 | " [J], depending on the input time given in [s]\n", 99 | "\n", 100 | " Parameters\n", 101 | " ----------\n", 102 | " pwr : float\n", 103 | " The power given in [W].\n", 104 | " time : float\n", 105 | " The time given in [s].\n", 106 | "\n", 107 | " Returns\n", 108 | " -------\n", 109 | " enr : float\n", 110 | " The energy given in [J].\n", 111 | " \"\"\"\n", 112 | "\n", 113 | " # Compute the energy\n", 114 | " enr = pwr * time\n", 115 | "\n", 116 | " # Return the energy\n", 117 | " return enr" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 5, 123 | "metadata": {}, 124 | "outputs": [ 125 | { 126 | "name": "stdout", 127 | "output_type": "stream", 128 | "text": [ 129 | " magnitude irradiance [W/m^2] irradiance attn [W/m^2]\n", 130 | "0 -2.0 1.584893e-07 2.290868e-07\n", 131 | "1 -1.0 6.309573e-08 9.120108e-08\n", 132 | "2 0.0 2.511886e-08 3.630781e-08\n", 133 | "3 1.0 1.000000e-08 1.445440e-08\n", 134 | "4 2.0 3.981072e-09 5.754399e-09\n", 135 | "5 3.0 1.584893e-09 2.290868e-09\n", 136 | "6 4.0 6.309573e-10 9.120108e-10\n", 137 | "7 5.0 2.511886e-10 3.630781e-10\n", 138 | "8 6.0 1.000000e-10 1.445440e-10\n" 139 | ] 140 | } 141 | ], 142 | "source": [ 143 | "# Set a magnitude range\n", 144 | "mag_range = np.arange(-2.0, 7.0, 1.0)\n", 145 | "\n", 146 | "# Fill the lookup table with the magnitudes\n", 147 | "mag_lookup_df.loc[:, 'magnitude'] = mag_range\n", 148 | "\n", 149 | "# Convert the magnitudes to irradiance\n", 150 | "mag_lookup_df.loc[:, 'irradiance [W/m^2]'] = \\\n", 151 | " mag_lookup_df['magnitude'].apply(lambda x: mag2irr(x))\n", 152 | "\n", 153 | "# Convert the magnitudes to irradiance considering the atmospheric attenuation\n", 154 | "mag_lookup_df.loc[:, 'irradiance attn [W/m^2]'] = \\\n", 155 | " mag_lookup_df['magnitude'].apply(lambda x: mag2irr(x, use_attn=True))\n", 156 | "\n", 157 | "# Print the resulting lookup table\n", 158 | "print(mag_lookup_df)" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": 6, 164 | "metadata": {}, 165 | "outputs": [], 166 | "source": [ 167 | "# Save the lookup table as an excel sheet\n", 168 | "mag_lookup_df.to_excel('magnitude_lookup_table.xlsx')" 169 | ] 170 | } 171 | ], 172 | "metadata": { 173 | "kernelspec": { 174 | "display_name": "Python 3", 175 | "language": "python", 176 | "name": "python3" 177 | }, 178 | "language_info": { 179 | "codemirror_mode": { 180 | "name": "ipython", 181 | "version": 3 182 | }, 183 | "file_extension": ".py", 184 | "mimetype": "text/x-python", 185 | "name": "python", 186 | "nbconvert_exporter": "python", 187 | "pygments_lexer": "ipython3", 188 | "version": "3.8.2" 189 | } 190 | }, 191 | "nbformat": 4, 192 | "nbformat_minor": 4 193 | } 194 | -------------------------------------------------------------------------------- /part16/SpaceSciencePython_part16.py: -------------------------------------------------------------------------------- 1 | # Import modules 2 | import pandas as pd 3 | import numpy as np 4 | 5 | # Create an empty dataframe lookup table 6 | mag_lookup_df = pd.DataFrame([]) 7 | 8 | #%% 9 | 10 | # Function that converts magnitude to irradiance in W/m^2 11 | def mag2irr(mag, use_attn=False): 12 | """ 13 | This function converts the apparent magnitude to the corresponding 14 | irradiance value given in [W/m^2]. 15 | 16 | Parameters 17 | ---------- 18 | mag : float 19 | The astronomical magnitude. 20 | use_attn : bool, optional 21 | Boolean value. If True, a constant factor will be applied that 22 | represents a simple atmospheric attenuation. The default is False. 23 | 24 | Returns 25 | ------- 26 | irr : float 27 | The resulting irradiance given in [W/m^2]. 28 | """ 29 | 30 | # If the user wants the atmospheric attenuation, a constant value of 0.4 31 | # is set ... 32 | if use_attn: 33 | attn = 0.4 34 | 35 | # ... otherwise a value of 0 is set 36 | else: 37 | attn = 0.0 38 | 39 | # Compute the irradiance 40 | irr = 10.0 ** (0.4 * (-mag - 19.0 + attn)) 41 | 42 | # Return the irradiance 43 | return irr 44 | 45 | #%% 46 | 47 | # Function that converts the irradiance to power given in W 48 | def irr2pwr(irr, area): 49 | """ 50 | This function converts the irradiance given in [W/m^2] to the power [W] 51 | for a user defined area given in [m^2] 52 | 53 | Parameters 54 | ---------- 55 | irr : float 56 | The irradiance given in [W/m^2]. 57 | area : float 58 | The area given in [m^2]. 59 | 60 | Returns 61 | ------- 62 | pwr : float 63 | The resulting power given in [W]. 64 | """ 65 | pwr = irr * area 66 | 67 | return pwr 68 | 69 | #%% 70 | 71 | # Function that converts the power given in [W] to energy given in [J] 72 | def pwr2enr(pwr, time): 73 | """ 74 | This function converts the power given in [W] to the corresponding energy 75 | [J], depending on the input time given in [s] 76 | 77 | Parameters 78 | ---------- 79 | pwr : float 80 | The power given in [W]. 81 | time : float 82 | The time given in [s]. 83 | 84 | Returns 85 | ------- 86 | enr : float 87 | The energy given in [J]. 88 | """ 89 | 90 | # Compute the energy 91 | enr = pwr * time 92 | 93 | # Return the energy 94 | return enr 95 | 96 | #%% 97 | 98 | # Set a magnitude range 99 | mag_range = np.arange(-2.0, 7.0, 1.0) 100 | 101 | # Fill the lookup table with the magnitudes 102 | mag_lookup_df.loc[:, 'magnitude'] = mag_range 103 | 104 | # Convert the magnitudes to irradiance 105 | mag_lookup_df.loc[:, 'irradiance [W/m^2]'] = \ 106 | mag_lookup_df['magnitude'].apply(lambda x: mag2irr(x)) 107 | 108 | # Convert the magnitudes to irradiance considering the atmospheric attenuation 109 | mag_lookup_df.loc[:, 'irradiance attn [W/m^2]'] = \ 110 | mag_lookup_df['magnitude'].apply(lambda x: mag2irr(x, use_attn=True)) 111 | 112 | # Print the resulting lookup table 113 | print(mag_lookup_df) 114 | 115 | #%% 116 | 117 | # Save the lookup table as an excel sheet 118 | mag_lookup_df.to_excel('magnitude_lookup_table.xlsx') 119 | -------------------------------------------------------------------------------- /part17/SpaceSciencePython_part17.py: -------------------------------------------------------------------------------- 1 | # Import modules 2 | import pandas as pd 3 | import numpy as np 4 | import spiceypy 5 | 6 | # Import the SPICE meta kernel file 7 | spiceypy.furnsh('kernel_meta.txt') 8 | 9 | # Get the G*M value of the Sun 10 | _, GM_SUN_PRE = spiceypy.bodvcd(bodyid=10, item='GM', maxn=1) 11 | GM_SUN = GM_SUN_PRE[0] 12 | 13 | #%% 14 | 15 | # Get the orbital elements (mean values) of the asteroid 2020 JX1 from: 16 | # https://ssd.jpl.nasa.gov/sbdb.cgi?sstr=2020%20JX1 17 | # ;old=0;orb=0;cov=1;log=0;cad=0#elem 18 | 19 | # This time, we simply set constants. The later re-sampled values are stored 20 | # in a pandas dataframe 21 | AST_2020JX1_Q_AU = 1.006032017749843 # AU 22 | AST_2020JX1_E = 0.2934873932898034 23 | AST_2020JX1_I_DEG = 3.54821709679635 # degrees 24 | AST_2020JX1_LNODE_DEG = 274.5788314475524 # degrees 25 | AST_2020JX1_ARGP_DEG = 12.81238701655224 # degrees 26 | AST_2020JX1_M0_DEG = 0.0 # degrees (and also radians, since its 0 ...) 27 | 28 | # Orbital elements corresponding Epoch given in Julian Date (JD) 29 | AST_2020JX1_EPOCH_JD = 2459038.680298393594 30 | 31 | #%% 32 | 33 | # The covariance matrix on the JPL site assumes the same dimensions as the mean 34 | # values (AU, degrees, etc.) 35 | # Further, the order is (column: left to right, rows: top to bottom) 36 | # eccentricity 37 | # perihelion 38 | # epoch 39 | # longitude of ascending node 40 | # argument of periapsis 41 | # inclination 42 | AST_2020JX1_COV_MAT = \ 43 | np.array([[3.5E-9, -1.1E-10, -8.8E-9, -5.6E-9, 8.8E-9, 3.3E-8], \ 44 | [-1.1E-10, 3.7E-12, 2.8E-10, 1.8E-10, -2.8E-10, -1.0E-9], \ 45 | [-8.8E-9, 2.8E-10, 2.5E-8, 1.5E-8, -2.0E-8, -8.1E-8], \ 46 | [-5.6E-9, 1.8E-10, 1.5E-8, 1.0E-8, -1.4E-8, -5.2E-8], \ 47 | [8.8E-9, -2.8E-10, -2.0E-8, -1.4E-8, 2.3E-8, 8.1E-8], \ 48 | [3.3E-8, -1.0E-9, -8.1E-8, -5.2E-8, 8.1E-8, 3.0E-7]]) 49 | 50 | #%% 51 | 52 | # Now, we re-sample possible solutions, based on the mean values and the 53 | # covariance matrix. Re-sample size: 1000 54 | AST_2020_JX1_SAMPLE = \ 55 | np.random.multivariate_normal(mean=[AST_2020JX1_E, \ 56 | AST_2020JX1_Q_AU, \ 57 | AST_2020JX1_EPOCH_JD, \ 58 | AST_2020JX1_LNODE_DEG, \ 59 | AST_2020JX1_ARGP_DEG, \ 60 | AST_2020JX1_I_DEG], \ 61 | cov=AST_2020JX1_COV_MAT, size=1000) 62 | 63 | #%% 64 | 65 | # Store all re-samples now in a pandas dataframe. Consider the order of the 66 | # orbital elements as defined by the covariance matrix 67 | ast_2020_jx1_df = pd.DataFrame([]) 68 | 69 | # Eccentricity 70 | ast_2020_jx1_df.loc[:, 'ECC'] = AST_2020_JX1_SAMPLE[:, 0] 71 | 72 | # Perihelion (convert AU to km) 73 | ast_2020_jx1_df.loc[:, 'PERIH_KM'] = \ 74 | spiceypy.convrt(AST_2020_JX1_SAMPLE[:, 1], 'AU', 'km') 75 | 76 | # Epoch in JD 77 | ast_2020_jx1_df.loc[:, 'EPOCH_JD'] = AST_2020_JX1_SAMPLE[:, 2] 78 | 79 | # Convert Epoch given in JD to Ephemeris Time 80 | ast_2020_jx1_df.loc[:, 'EPOCH_ET'] = \ 81 | ast_2020_jx1_df['EPOCH_JD'].apply(lambda x: \ 82 | spiceypy.utc2et(str(x) + ' JD')) 83 | 84 | # Longitude of ascending node, argument of periapsis and the inclination 85 | # (in radians) 86 | ast_2020_jx1_df.loc[:, 'LNODE_RAD'] = np.radians(AST_2020_JX1_SAMPLE[:, 3]) 87 | ast_2020_jx1_df.loc[:, 'ARGP_RAD'] = np.radians(AST_2020_JX1_SAMPLE[:, 4]) 88 | ast_2020_jx1_df.loc[:, 'I_RAD'] = np.radians(AST_2020_JX1_SAMPLE[:, 5]) 89 | 90 | # Finally, set the G*M value of the Sun 91 | ast_2020_jx1_df.loc[:, 'GM_SUN'] = GM_SUN 92 | 93 | #%% 94 | 95 | # Import matplotlib 96 | from matplotlib import pyplot as plt 97 | 98 | # Let's set a dark background 99 | plt.style.use('dark_background') 100 | 101 | # Set a default font size for better readability 102 | plt.rcParams.update({'font.size': 14}) 103 | 104 | # Create a figure and axis 105 | fig, ax = plt.subplots(figsize=(12, 8)) 106 | 107 | # Plot the perihelion values vs. the eccentricity values in a scatter plot. 108 | # Shift it by the mean values and finally scale it for nicer x and y ticks 109 | ax.scatter((AST_2020_JX1_SAMPLE[:, 0] - AST_2020JX1_E) / (10.0**(-4)), \ 110 | (AST_2020_JX1_SAMPLE[:, 1] - AST_2020JX1_Q_AU) / (10.0**(-6)), \ 111 | color='tab:orange', alpha=0.3, marker='o') 112 | 113 | # Add a grid for better readability 114 | ax.grid(axis='both', linestyle='dashed', alpha=0.2) 115 | 116 | # Set an x and y label 117 | ax.set_xlabel('Eccentricity - Mean Eccentricity $(10^{-4})$') 118 | ax.set_ylabel('Perihelion - Mean Perihelion in AU $(10^{-6})$') 119 | 120 | # Save the figure 121 | plt.savefig('asteroid_resample_e_q.png', dpi=300) 122 | 123 | #%% 124 | 125 | # Set the date-time of the Asteroid Day (yeah) 126 | SAMPLE_ET = spiceypy.utc2et('2020-06-30T00:00:00') 127 | 128 | # Set the Asteroid Day ET and mean anomaly (in radians) 129 | ast_2020_jx1_df.loc[:, 'COMP_ET'] = SAMPLE_ET 130 | ast_2020_jx1_df.loc[:, 'M0_RAD'] = np.radians(AST_2020JX1_M0_DEG) 131 | 132 | # Compute the state vectors for all re-sampled orbital elements 133 | ast_2020_jx1_df.loc[:, 'STATE_VEC'] = \ 134 | ast_2020_jx1_df.apply(lambda x: \ 135 | spiceypy.conics(elts=x[['PERIH_KM', \ 136 | 'ECC', \ 137 | 'I_RAD', \ 138 | 'LNODE_RAD', \ 139 | 'ARGP_RAD', \ 140 | 'M0_RAD', \ 141 | 'EPOCH_ET', \ 142 | 'GM_SUN']].values, \ 143 | et=x['COMP_ET']), axis=1) 144 | 145 | # And extract the positional information from the state vector 146 | ast_2020_jx1_df.loc[:, 'POS_VEC_KM'] = \ 147 | ast_2020_jx1_df['STATE_VEC'].apply(lambda x: x[:3]) 148 | 149 | #%% 150 | 151 | # Let's compute the largest deviation / distance of the re-sampled position 152 | # vector by using the scipy function cdist 153 | from scipy.spatial.distance import cdist 154 | 155 | DIST_MAT = cdist(np.array(list(ast_2020_jx1_df['POS_VEC_KM'].values)), \ 156 | np.array(list(ast_2020_jx1_df['POS_VEC_KM'].values))) 157 | 158 | # Print the maximum distance of all position solution space / domain 159 | print('Maximum distance of the predicted positions of 2020 JX1 in km: ' \ 160 | f'{np.max(DIST_MAT)}') 161 | print('\n') 162 | 163 | #%% 164 | 165 | # We want to compute the ecliptic coordinates of the asteroid as seen from 166 | # Earth ... or let's say: the solution space 167 | # First, we need to compute the position vector of our home planet ... 168 | EARTH_POSITION_KM, _ = spiceypy.spkgps(targ=399, et=SAMPLE_ET, \ 169 | ref='ECLIPJ2000', obs=10) 170 | 171 | #%% 172 | 173 | # ... to determine the asteroid's position as seen from Earth 174 | ast_2020_jx1_df.loc[:, 'POS_VEC_WRT_EARTH_KM'] = \ 175 | ast_2020_jx1_df['POS_VEC_KM'].apply(lambda x: x - EARTH_POSITION_KM) 176 | 177 | #%% 178 | 179 | # Now let's compute the ecliptic longitude and latitude values 180 | ast_2020_jx1_df.loc[:, 'ECLIP_LONG_DEG'] = \ 181 | ast_2020_jx1_df['POS_VEC_WRT_EARTH_KM'] \ 182 | .apply(lambda x: np.degrees(spiceypy.recrad(x)[1])) 183 | ast_2020_jx1_df.loc[:, 'ECLIP_LAT_DEG'] = \ 184 | ast_2020_jx1_df['POS_VEC_WRT_EARTH_KM'] \ 185 | .apply(lambda x: np.degrees(spiceypy.recrad(x)[2])) 186 | 187 | # Print the statistics of the results 188 | print('Statistics for the Ecliptic Longitude: \n' \ 189 | f'{ast_2020_jx1_df["ECLIP_LONG_DEG"].describe()}') 190 | print('\n') 191 | print('Statistics for the Ecliptic Latitude: \n' \ 192 | f'{ast_2020_jx1_df["ECLIP_LAT_DEG"].describe()}') 193 | 194 | #%% 195 | 196 | # Assignment: 197 | # 1. Visualise the long, lat coordinates in a sky map 198 | # (see: https://towardsdatascience.com/ 199 | # space-science-with-python-space-maps-747c7d1eaf7f) 200 | # 2. Compute the probability density function in spherical coordinates 201 | # (and plot it in the sky map. Hint: Use the Haversine metric!) 202 | 203 | #%% 204 | 205 | # Store the data in a csv file 206 | ast_2020_jx1_df.to_csv('2020_JX1_data.csv', sep=';') -------------------------------------------------------------------------------- /part17/kernel_meta.txt: -------------------------------------------------------------------------------- 1 | \begintext 2 | 3 | This meta file contains the relative paths to all needed SPICE kernels. 4 | For each tutorial we will set up an individual kernel_meta.txt. A common 5 | meta file could be easily stored in the main folder (next to the _kernel 6 | directory), however it would be over-loaded at some point. 7 | 8 | \begindata 9 | 10 | KERNELS_TO_LOAD = ( 11 | '../_kernels/spk/de432s.bsp', 12 | '../_kernels/lsk/naif0012.tls', 13 | '../_kernels/pck/gm_de431.tpc' 14 | ) 15 | -------------------------------------------------------------------------------- /part18/SpaceSciencePython_part18.py: -------------------------------------------------------------------------------- 1 | # Import modules 2 | import numpy as np 3 | import pandas as pd 4 | from tqdm import tqdm 5 | from matplotlib import pyplot as plt 6 | 7 | # Open the data from the last tutorial 8 | ast_2020_jx1_df = pd.read_csv('../part17/2020_JX1_data.csv', sep=';') 9 | 10 | #%% 11 | 12 | # Convert to radians 13 | ast_2020_jx1_df.loc[:, 'ECLIP_LONG_RAD'] = \ 14 | np.radians(ast_2020_jx1_df['ECLIP_LONG_DEG']) 15 | 16 | ast_2020_jx1_df.loc[:, 'ECLIP_LAT_RAD'] = \ 17 | np.radians(ast_2020_jx1_df['ECLIP_LAT_DEG']) 18 | 19 | #%% 20 | 21 | # The KDE algorithm in scikit learn does not allow one to set different 22 | # bandwidths for different dimensions / axes. Using Scott's rule of thumb we 23 | # could simply apply one bandwidth for all dimensions, right? 24 | # 25 | # To check this, let's determine the standard deviation of the longitude and 26 | # latitude values, respectively. 27 | print('Standard deviation of the longitude in radians: ' 28 | f'{np.std(ast_2020_jx1_df["ECLIP_LONG_RAD"])}') 29 | print('Standard deviation of the latitude in radians: ' 30 | f'{np.std(ast_2020_jx1_df["ECLIP_LAT_RAD"])}') 31 | print('\n') 32 | 33 | #%% 34 | 35 | # We need a multivariate alternative to scikit learn... 36 | # 37 | # https://www.statsmodels.org/stable/generated/ 38 | # statsmodels.nonparametric.kernel_density.KDEMultivariate.html 39 | import statsmodels.nonparametric.kernel_density as statmKDE 40 | 41 | # Get the longitude and latitude values of the asteroid 42 | AST_LONG_LAT = ast_2020_jx1_df[['ECLIP_LONG_RAD', 'ECLIP_LAT_RAD']].values 43 | 44 | # Compute now the 2D multivariate KDE 45 | DENS_MODEL = statmKDE.KDEMultivariate(data=AST_LONG_LAT, \ 46 | var_type='cc', \ 47 | bw='normal_reference') 48 | 49 | #%% 50 | 51 | # Let's print the bandwidth results 52 | print(f'Bandwidth longitude in radians (normal ref.): {DENS_MODEL.bw[0]}') 53 | print(f'Bandwidth latitude in radians (normal ref.): {DENS_MODEL.bw[1]}') 54 | print('\n') 55 | 56 | #%% 57 | 58 | # Do the results from other bw-determining methods differ? 59 | DENS_MODEL_TEMP = statmKDE.KDEMultivariate(data=AST_LONG_LAT, \ 60 | var_type='cc', \ 61 | bw='cv_ml') 62 | 63 | print(f'Bandwidth longitude in radians (cv_ml): {DENS_MODEL_TEMP.bw[0]}') 64 | print(f'Bandwidth latitude in radians (cv_ml): {DENS_MODEL_TEMP.bw[1]}') 65 | print('\n') 66 | 67 | #%% 68 | 69 | # To obtain a probability density function (pdf) that is based on the model, 70 | # we need to compute the pdf in a for-loop in 'latitude-slices'. The resulting 71 | # 'pdf per latitude results' are stored in the following placeholder list 72 | pdf_final = [] 73 | 74 | # Iterate through the latitude, from the minimum to maximum latitude values 75 | # that are present in the data 76 | for _lat in tqdm(np.linspace(ast_2020_jx1_df.ECLIP_LAT_RAD.min(), \ 77 | ast_2020_jx1_df.ECLIP_LAT_RAD.max(), \ 78 | 100)): 79 | 80 | # Compute a temporary array that contains 1000 longitude values from the 81 | # long. min. to the long. max. value and a fixed latitude value from the 82 | # for-loop 83 | long_lat_array = np.linspace((ast_2020_jx1_df.ECLIP_LONG_RAD.min(), \ 84 | _lat), \ 85 | (ast_2020_jx1_df.ECLIP_LONG_RAD.max(), \ 86 | _lat), \ 87 | 100) 88 | 89 | # Compute the corresponding pdf ... 90 | pdf = DENS_MODEL.pdf(long_lat_array) 91 | 92 | # ... and store it in the final list 93 | pdf_final.append(pdf) 94 | 95 | # Convert the final list to a numpy array 96 | pdf_final = np.array(pdf_final) 97 | 98 | # Invert the result 99 | pdf_final = pdf_final[::-1] 100 | 101 | #%% 102 | 103 | # Use a dark background 104 | plt.style.use('dark_background') 105 | 106 | # Set a figure 107 | plt.figure(figsize=(12, 8)) 108 | 109 | # Plot the possible coordinates of the asteroid as a scatter plot 110 | plt.scatter(x=ast_2020_jx1_df['ECLIP_LONG_DEG'], \ 111 | y=ast_2020_jx1_df['ECLIP_LAT_DEG'], \ 112 | c='white', alpha=0.3, s=10, marker='o') 113 | 114 | # Create an overlay of the pdf. Use the extent argument to properly set the 115 | # x and y axes 116 | plt.imshow(pdf_final, \ 117 | extent=[ast_2020_jx1_df.ECLIP_LONG_DEG.min(), \ 118 | ast_2020_jx1_df.ECLIP_LONG_DEG.max(), \ 119 | ast_2020_jx1_df.ECLIP_LAT_DEG.min(), \ 120 | ast_2020_jx1_df.ECLIP_LAT_DEG.max()], \ 121 | cmap='copper') 122 | 123 | # Set a grid 124 | plt.grid(True, linestyle='dashed', alpha=0.5) 125 | 126 | # Get the axes 127 | ax = plt.gca() 128 | ax.ticklabel_format(useOffset=False, style='plain') 129 | 130 | # Set long. / lat. labels 131 | plt.xlabel('Long. in deg') 132 | plt.ylabel('Lat. in deg') 133 | 134 | # Save the figure 135 | plt.savefig('2020_jx1_eclip_coords.png', dpi=300) 136 | -------------------------------------------------------------------------------- /part19/SpaceSciencePython_part19.py: -------------------------------------------------------------------------------- 1 | # Import modules 2 | import numpy as np 3 | from matplotlib import pyplot as plt 4 | 5 | #%% 6 | 7 | def phi_func(index, phase_angle): 8 | """ 9 | Phase function that is needed for the reduced magnitude. The function has 10 | two versions, depending on the index ('1' or '2'). 11 | 12 | Parameters 13 | ---------- 14 | index : str 15 | Phase function index / version. '1' or '2'. 16 | phase_angle : float 17 | Phase angle of the asteroid in radians. 18 | 19 | Returns 20 | ------- 21 | phi : float 22 | Phase function result. 23 | 24 | """ 25 | 26 | # Dictionary that contains the A and B constants, depending on the index / 27 | # version 28 | a_factor = {'1': 3.33, \ 29 | '2': 1.87} 30 | 31 | b_factor = {'1': 0.63, \ 32 | '2': 1.22} 33 | 34 | # Phase function 35 | phi = np.exp(-1.0 * a_factor[index] \ 36 | *+ ((np.tan(0.5 * phase_angle) ** b_factor[index]))) 37 | 38 | # Return the phase function result 39 | return phi 40 | 41 | #%% 42 | 43 | def red_mag(abs_mag, phase_angle, slope_g): 44 | """ 45 | Reduced magnitude of an asteroid, depending on the absolute magnitude, 46 | phase angle and slope parameter (G) 47 | 48 | Parameters 49 | ---------- 50 | abs_mag : float 51 | Absolute magnitude. 52 | phase_angle : float 53 | Phase angle in radians. 54 | slope_g : float 55 | Slope parameter (G), between 0 and 1. 56 | 57 | Returns 58 | ------- 59 | r_mag : float 60 | Reduced magnitude. 61 | 62 | """ 63 | 64 | # Computation of the reduced magnitude 65 | r_mag = abs_mag - 2.5 * np.log10((1.0 - slope_g) \ 66 | * phi_func(index='1', \ 67 | phase_angle=phase_angle) \ 68 | + slope_g \ 69 | * phi_func(index='2', \ 70 | phase_angle=phase_angle)) 71 | 72 | # Return the reduced magnitude 73 | return r_mag 74 | 75 | #%% 76 | 77 | def app_mag(abs_mag, phase_angle, slope_g, d_ast_sun, d_ast_earth): 78 | """ 79 | Apparent / Visual magnitude of an asteroid (not considering atmospheric 80 | attenuation), depending on the absolute magnitude, phase angle, the slope 81 | parameter (G) as well as the distance between the asteroid and Earth, 82 | respectively the Sun 83 | 84 | Parameters 85 | ---------- 86 | abs_mag : float 87 | Absolute magnitude. 88 | phase_angle : float 89 | Phase angle in radians. 90 | slope_g : float 91 | Slope parameter (G). 92 | d_ast_sun : float 93 | Distance between the asteroid and the Sun in AU. 94 | d_ast_earth : float 95 | Distance between the asteroid and the Earth in AU. 96 | 97 | Returns 98 | ------- 99 | mag : float 100 | Apparent / visual magnitude. 101 | 102 | """ 103 | 104 | # Compute the apparent / visual magnitude 105 | mag = red_mag(abs_mag, phase_angle, slope_g) \ 106 | + 5.0 * np.log10(d_ast_sun * d_ast_earth) 107 | 108 | # Return the apparent magnitude 109 | return mag 110 | 111 | #%% 112 | 113 | # Execute a main part of the script 114 | if __name__ == '__main__': 115 | 116 | # Let's set some values that correspond approximately with the values of 117 | # the dwarf planet Ceres: https://ssd.jpl.nasa.gov/sbdb.cgi?sstr=Ceres 118 | # Set hard-coded distances between the Earth-Sun and Asteroid-Sun in AU 119 | D_EARTH_SUN = 1.0 120 | D_AST_SUN = 3.0 121 | 122 | # Set hard-coded values for the absolute magnitude and slope parameter G 123 | ABS_MAG_SAMPLE = 3.4 124 | SLOPE_G_SAMPLE = 0.12 125 | 126 | # Set an array with the phase angles between 0 and 45 degrees (convert to 127 | # radians), as seen from the Sun (between Earth and Asteroid) 128 | ANG_EARTH_AST_ORG_SUN = np.radians(np.linspace(0, 45, 1000)) 129 | 130 | # Compute the distance between the Earth and the Asteroid depending on the 131 | # phase angle 132 | D_AST_EARTH = np.sqrt(D_EARTH_SUN**2.0 + D_AST_SUN**2.0 \ 133 | - 2.0 * D_EARTH_SUN * D_AST_SUN \ 134 | * np.cos(ANG_EARTH_AST_ORG_SUN)) 135 | 136 | # Compute the corresponding phase angle as seen from the asteroid 137 | # (between the Sun and Earth) 138 | PHASE_ANG_AST = np.arcsin((D_EARTH_SUN * np.sin(ANG_EARTH_AST_ORG_SUN)) 139 | / (D_AST_EARTH)) 140 | 141 | # Compute now the apparent / visual magnitude of the body 142 | APP_MAG_SAMPLE = app_mag(abs_mag=ABS_MAG_SAMPLE, \ 143 | phase_angle=PHASE_ANG_AST, \ 144 | slope_g=SLOPE_G_SAMPLE, \ 145 | d_ast_sun=D_AST_SUN, \ 146 | d_ast_earth=D_AST_EARTH) 147 | 148 | # Let's plot the results: 149 | # Use a dark background 150 | plt.style.use('dark_background') 151 | 152 | # Set a figure 153 | plt.figure(figsize=(12, 8)) 154 | 155 | # To visualise possible effects a little bit better, let's plot the 156 | # magnitude vs. the phase angle and mirror the phase angle results. 157 | # The resulting "negative phase angles" are mathematically not correct! 158 | plt.plot(np.concatenate((-1.0*np.degrees(PHASE_ANG_AST[::-1]), \ 159 | np.degrees(PHASE_ANG_AST))), \ 160 | np.concatenate((APP_MAG_SAMPLE[::-1], APP_MAG_SAMPLE)), \ 161 | color='tab:orange') 162 | 163 | # Set a grid 164 | plt.grid(True, linestyle='dashed', alpha=0.5) 165 | 166 | # Get the axes 167 | ax = plt.gca() 168 | ax.ticklabel_format(useOffset=False, style='plain') 169 | 170 | # Set the labels for the x and y axes 171 | plt.xlabel('Phase angle in degrees') 172 | plt.ylabel('Apparent magnitude') 173 | 174 | # Save the figure 175 | plt.savefig('app_mag_asteroid.png', dpi=300) 176 | -------------------------------------------------------------------------------- /part2/SSB_WRT_SUN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAlbin/SpaceScienceTutorial/9ca5c1340480a29a112dec91e075b7ee2eff38ed/part2/SSB_WRT_SUN.png -------------------------------------------------------------------------------- /part2/SpaceSciencePython_part2.py: -------------------------------------------------------------------------------- 1 | # Import modules 2 | import datetime 3 | import spiceypy 4 | import numpy as np 5 | 6 | # Load the SPICE kernels via a meta file 7 | spiceypy.furnsh('kernel_meta.txt') 8 | 9 | # We want to compute the Solar System barycentre (SSB) w.r.t to the centre of 10 | # the Sun for a certain time interval. 11 | # First, we set an initial time in UTC. 12 | INIT_TIME_UTC = datetime.datetime(year=2000, month=1, day=1, \ 13 | hour=0, minute=0, second=0) 14 | 15 | # Add a number of days; you can play around with the datetime variables; but 16 | # leave it as it is for the first try, since other computations and comments 17 | # are based on this value. 18 | DELTA_DAYS = 10000 19 | END_TIME_UTC = INIT_TIME_UTC + datetime.timedelta(days=DELTA_DAYS) 20 | 21 | # Convert the datetime objects now to strings 22 | INIT_TIME_UTC_STR = INIT_TIME_UTC.strftime('%Y-%m-%dT%H:%M:%S') 23 | END_TIME_UTC_STR = END_TIME_UTC.strftime('%Y-%m-%dT%H:%M:%S') 24 | 25 | # Print the starting and end times 26 | print('Init time in UTC: %s' % INIT_TIME_UTC_STR) 27 | print('End time in UTC: %s\n' % END_TIME_UTC_STR) 28 | 29 | # Convert to Ephemeris Time (ET) using the SPICE function utc2et. 30 | INIT_TIME_ET = spiceypy.utc2et(INIT_TIME_UTC_STR) 31 | END_TIME_ET = spiceypy.utc2et(END_TIME_UTC_STR) 32 | 33 | #%% 34 | 35 | # A day has 86400 seconds (24 hours * 60 minutes * 60 seconds) 36 | # In our example, we set a time period of 10000 days. Thus, we expect the 37 | # difference between INIT_TIME_ET and END_TIME_ET to be 10000*86400 seconds 38 | # Let's have a look at the delta. 39 | print('Covered time interval in seconds: %s\n' % (END_TIME_ET - INIT_TIME_ET)) 40 | # We see that 5.0012845 seconds are added in this time period (leap-seconds) 41 | 42 | # Create a numpy array that covers a time interval in delta = 1 day step 43 | TIME_INTERVAL_ET = np.linspace(INIT_TIME_ET, END_TIME_ET, DELTA_DAYS) 44 | 45 | #%% 46 | 47 | # Now we compute the position of the Solar System's barycentre w.r.t. our Sun: 48 | # First we set an empty list that stores later all x, y, z components for each 49 | # time step 50 | SSB_WRT_SUN_POSITION = [] 51 | 52 | # Each time step is used in this for-loop to compute the position of the SSB 53 | # w.r.t. the Sun. We use the function spkgps. 54 | for TIME_INTERVAL_ET_f in TIME_INTERVAL_ET: 55 | _position, _ = spiceypy.spkgps(targ=0, et=TIME_INTERVAL_ET_f, \ 56 | ref='ECLIPJ2000', obs=10) 57 | 58 | # Append the result to the final list 59 | SSB_WRT_SUN_POSITION.append(_position) 60 | 61 | # Convert the list to a numpy array 62 | SSB_WRT_SUN_POSITION = np.array(SSB_WRT_SUN_POSITION) 63 | 64 | #%% 65 | 66 | # Let's have a look at the first entry and ... 67 | print('Position (components) of the Solar System Barycentre w.r.t the\n' \ 68 | 'centre of the Sun (at initial time): \n' \ 69 | 'X = %s km\n' \ 70 | 'Y = %s km\n' \ 71 | 'Z = %s km\n' % tuple(np.round(SSB_WRT_SUN_POSITION[0]))) 72 | 73 | # ... let's determine and print the corresponding distance using the numpy 74 | # function linalg.norm() 75 | print('Distance between the Solar System Barycentre w.r.t the\n' \ 76 | 'centre of the Sun (at initial time): \n' \ 77 | 'd = %s km\n' % round(np.linalg.norm(SSB_WRT_SUN_POSITION[0]))) 78 | 79 | #%% 80 | 81 | # We want to visualise the results, to a get feeling of the movement. Is the 82 | # movement somehow interesting and / or significant? 83 | 84 | # Using km are not intuitive. AU would scale it to severly. Since we compute 85 | # the SSB w.r.t the Sun; and since we expect it to be close to the Sun, we 86 | # scale the x, y, z component w.r.t the radius of the Sun. We extract the 87 | # Sun radii (x, y, z components of the Sun ellipsoid) and use the x component 88 | _, RADII_SUN = spiceypy.bodvcd(bodyid=10, item='RADII', maxn=3) 89 | 90 | RADIUS_SUN = RADII_SUN[0] 91 | 92 | # Scale the position values using the Sun's radius 93 | SSB_WRT_SUN_POSITION_SCALED = SSB_WRT_SUN_POSITION / RADIUS_SUN 94 | 95 | #%% 96 | 97 | # We plot now the trajectory of the SSB w.r.t the Sun using matplotlib 98 | from matplotlib import pyplot as plt 99 | 100 | # We only plot the x, y components (view on the ecliptic plane) 101 | SSB_WRT_SUN_POSITION_SCALED_XY = SSB_WRT_SUN_POSITION_SCALED[:, 0:2] 102 | 103 | # Set a dark background... since... space is dark 104 | plt.style.use('dark_background') 105 | 106 | # Create a figure and ax 107 | FIG, AX = plt.subplots(figsize=(12, 8)) 108 | 109 | # Create a yellow circle that represents the Sun, add it to the ax 110 | SUN_CIRC = plt.Circle((0.0, 0.0), 1.0, color='yellow', alpha=0.8) 111 | AX.add_artist(SUN_CIRC) 112 | 113 | # Plot the SSB movement 114 | AX.plot(SSB_WRT_SUN_POSITION_SCALED_XY[:, 0], \ 115 | SSB_WRT_SUN_POSITION_SCALED_XY[:, 1], \ 116 | ls='solid', color='royalblue') 117 | 118 | # Set some parameters for the plot, set an equal ratio, set a grid, and set 119 | # the x and y limits 120 | AX.set_aspect('equal') 121 | AX.grid(True, linestyle='dashed', alpha=0.5) 122 | AX.set_xlim(-2, 2) 123 | AX.set_ylim(-2, 2) 124 | 125 | # Some labelling 126 | AX.set_xlabel('X in Sun-Radius') 127 | AX.set_ylabel('Y in Sun-Radius') 128 | 129 | # Saving the figure in high quality 130 | plt.savefig('SSB_WRT_SUN.png', dpi=300) 131 | 132 | #%% 133 | 134 | # How many days is the SSB outside the Sun? First, we compute the euclidean 135 | # distance between the SSB and Sun. 136 | SSB_WRT_SUN_DISTANCE_SCALED = np.linalg.norm(SSB_WRT_SUN_POSITION_SCALED, \ 137 | axis=1) 138 | 139 | print('Computation time: %s days\n' % DELTA_DAYS) 140 | 141 | # Compute number of days outside the Sun 142 | SSB_OUTSIDE_SUN_DELTA_DAYS = len(np.where(SSB_WRT_SUN_DISTANCE_SCALED > 1)[0]) 143 | 144 | print('Fraction of time where the SSB\n' \ 145 | 'was outside the Sun: %s %%' % (100 * SSB_OUTSIDE_SUN_DELTA_DAYS \ 146 | / DELTA_DAYS)) 147 | -------------------------------------------------------------------------------- /part2/kernel_meta.txt: -------------------------------------------------------------------------------- 1 | \begintext 2 | 3 | This meta file contains the relative paths to all needed SPICE kernels. 4 | For each tutorial we will set up an individual kernel_meta.txt. A common 5 | meta file could be easily stored in the main folder (next to the _kernel 6 | directory), however it would be over-loaded at some point. 7 | 8 | \begindata 9 | 10 | KERNELS_TO_LOAD = ( 11 | '../_kernels/spk/de432s.bsp', 12 | '../_kernels/lsk/naif0012.tls', 13 | '../_kernels/pck/pck00010.tpc' 14 | ) 15 | -------------------------------------------------------------------------------- /part20/SpaceSciencePython_part20.py: -------------------------------------------------------------------------------- 1 | # Import installed modules 2 | import spiceypy 3 | import numpy as np 4 | import pandas as pd 5 | from matplotlib import pyplot as plt 6 | 7 | # Load the SPICE kernel meta file 8 | spiceypy.furnsh('kernel_meta.txt') 9 | 10 | # Import our auxiliary sub-module that contains the apparent magnitude 11 | # equation from last time 12 | import sys 13 | sys.path.insert(1, '../_auxiliary') 14 | import asteroid_aux 15 | 16 | #%% 17 | 18 | # Set the Ceres NAIF ID 19 | CERES_ID = 2000001 20 | 21 | # Get the absolute magnitude and slope parameter (G) from NASA's small body 22 | # database 23 | # https://ssd.jpl.nasa.gov/sbdb.cgi#top 24 | CERES_ABS_MAG = 3.4 25 | CERES_SLOPE_G = 0.12 26 | 27 | #%% 28 | 29 | # Set a pandas dataframe. This dataframe will contain all computed parameters 30 | ceres_df = pd.DataFrame([]) 31 | 32 | # Set a date-time range for the entire year in 1 Week steps 33 | DATETIME_RANGE = pd.date_range(start='2020-01-01T00:00:00', \ 34 | end='2020-12-31T00:00:00', \ 35 | freq='1W') 36 | 37 | # Append the UTC date-times to the Ceres' dataframe 38 | ceres_df.loc[:, 'UTC_TIME'] = DATETIME_RANGE 39 | 40 | # Add another column, where the date-time is converted to YEAR-DAYOFYEAR 41 | ceres_df.loc[:, 'UTC_PARSED'] = DATETIME_RANGE.strftime('%Y-%j') 42 | 43 | # Convert the date-time to Ephemeris Time 44 | ceres_df.loc[:, 'ET_TIME'] = ceres_df['UTC_TIME'] \ 45 | .apply(lambda x: spiceypy.utc2et(str(x))) 46 | 47 | #%% 48 | 49 | # Compute the distance between Ceres and the Sun and convert the resulting 50 | # distance value given in km to AU 51 | ceres_df.loc[:, 'DIST_SUN_AU'] = \ 52 | ceres_df['ET_TIME'].apply(lambda x: \ 53 | spiceypy.convrt( \ 54 | spiceypy.vnorm( \ 55 | spiceypy.spkgps(targ=CERES_ID, \ 56 | et=x, \ 57 | ref='ECLIPJ2000', \ 58 | obs=10)[0]), \ 59 | 'km', 'AU')) 60 | 61 | # # Compute the distance between Ceres and the Earth and convert the resulting 62 | # distance value given in km to AU 63 | ceres_df.loc[:, 'DIST_EARTH_AU'] = \ 64 | ceres_df['ET_TIME'].apply(lambda x: \ 65 | spiceypy.convrt( \ 66 | spiceypy.vnorm( \ 67 | spiceypy.spkgps(targ=CERES_ID, \ 68 | et=x, \ 69 | ref='ECLIPJ2000', \ 70 | obs=399)[0]), \ 71 | 'km', 'AU')) 72 | 73 | #%% 74 | 75 | # Compute the phase angle between the Earth and the Sun as seen from Ceres 76 | ceres_df.loc[:, 'PHASE_ANGLE_EARTH2SUN_RAD'] = \ 77 | ceres_df['ET_TIME'].apply(lambda x: spiceypy.phaseq(et=x, \ 78 | target=str(CERES_ID), \ 79 | illmn='10', \ 80 | obsrvr='399', \ 81 | abcorr='NONE')) 82 | 83 | # Convert the phase angle results to degrees (for plotting) 84 | ceres_df.loc[:, 'PHASE_ANGLE_EARTH2SUN_DEG'] = \ 85 | np.degrees(ceres_df['PHASE_ANGLE_EARTH2SUN_RAD']) 86 | 87 | #%% 88 | 89 | # Compute the apparent magnitude of Ceres 90 | ceres_df.loc[:, 'APP_MAG'] = \ 91 | ceres_df.apply(lambda x: \ 92 | asteroid_aux.app_mag(abs_mag=CERES_ABS_MAG, \ 93 | phase_angle=x['PHASE_ANGLE_EARTH2SUN_RAD'], \ 94 | slope_g=CERES_SLOPE_G, \ 95 | d_ast_sun=x['DIST_SUN_AU'], \ 96 | d_ast_earth=x['DIST_EARTH_AU']), \ 97 | axis=1) 98 | 99 | #%% 100 | 101 | # Determine the ecliptic coordinates of Ceres in longitude and ... 102 | ceres_df.loc[:, 'ECLIP_LONG_RAD'] = \ 103 | ceres_df['ET_TIME'].apply(lambda x: \ 104 | spiceypy.recrad(spiceypy.spkgps(targ=CERES_ID, \ 105 | et=x, \ 106 | ref='ECLIPJ2000', \ 107 | obs=399)[0])[1]) 108 | 109 | # ... latitude 110 | ceres_df.loc[:, 'ECLIP_LAT_RAD'] = \ 111 | ceres_df['ET_TIME'].apply(lambda x: \ 112 | spiceypy.recrad(spiceypy.spkgps(targ=CERES_ID, \ 113 | et=x, \ 114 | ref='ECLIPJ2000', \ 115 | obs=399)[0])[2]) 116 | 117 | # Convert the resulting values from radians to degrees 118 | ceres_df.loc[:, 'ECLIP_LONG_DEG'] = \ 119 | np.degrees(ceres_df['ECLIP_LONG_RAD']) 120 | 121 | ceres_df.loc[:, 'ECLIP_LAT_DEG'] = \ 122 | np.degrees(ceres_df['ECLIP_LAT_RAD']) 123 | 124 | #%% 125 | 126 | # What do we want to achieve? Well a sky plot with the path of Ceres would be 127 | # nice. Scaling and colouring the individual positions based on the apparent 128 | # magnitude would guide the eye. Remember: a smaller magnitude corresponds to a 129 | # brighter object. Scaling a bright object smaller would be kind of 130 | # contra-intuitive. 131 | 132 | # Let's scale the scatter dot with the logic: smaller magnitude -> larger dot 133 | # size 134 | from sklearn import preprocessing 135 | 136 | # Set a pre-scaled array, where the apparent magnitude is scaled w.r.t. the 137 | # minimum value (maximum brightness) 138 | PRE_SCALED = np.array(np.min(ceres_df['APP_MAG']) \ 139 | / ceres_df['APP_MAG'].values).reshape(-1, 1) 140 | 141 | # Set an scikit-learn scaler ... 142 | scaler = preprocessing.MinMaxScaler() 143 | 144 | # .. and fit it based on the pre-scaled data 145 | scaler.fit(PRE_SCALED) 146 | 147 | # Transform now the pre-scaled data from 0 to 1 ... 148 | marker_pre_scale = scaler.transform(PRE_SCALED) 149 | 150 | # ... and multiply it by 50. The resulting array is used to set the marker 151 | # size for the plot 152 | marker_scale = marker_pre_scale * 50 153 | 154 | # Append the marker size array to the Ceres dataframe 155 | ceres_df.loc[:, 'PLOT_MARKER_SIZE'] = marker_scale 156 | 157 | #%% 158 | 159 | # Use a dark background 160 | plt.style.use('dark_background') 161 | 162 | # Set a figure 163 | plt.figure(figsize=(12, 8)) 164 | 165 | # Set a colormap for the scatter plot 166 | cm = plt.cm.get_cmap('viridis_r') 167 | 168 | # Plot the coordinates of Ceres as a scatter plot. Set a colour and marker 169 | # size accordingly to the apparent magnitude 170 | plt.scatter(x=ceres_df['ECLIP_LONG_DEG'], \ 171 | y=ceres_df['ECLIP_LAT_DEG'], \ 172 | c=ceres_df['APP_MAG'], \ 173 | alpha=1, \ 174 | s=ceres_df['PLOT_MARKER_SIZE'], \ 175 | marker='o', \ 176 | cmap=cm) 177 | 178 | # Add a white dashed path line, to improve the readability of the plot 179 | plt.plot(ceres_df['ECLIP_LONG_DEG'], \ 180 | ceres_df['ECLIP_LAT_DEG'], \ 181 | marker=None, \ 182 | linestyle='dashed', \ 183 | color='white', \ 184 | alpha=0.3, \ 185 | lw=1) 186 | 187 | # To get a better feeling how Ceres moves along the sky during the year, we 188 | # add some date-time text fields along the path 189 | for date_str, ceres_x, ceres_y in ceres_df[['UTC_PARSED', \ 190 | 'ECLIP_LONG_DEG', \ 191 | 'ECLIP_LAT_DEG']].values[2::10]: 192 | 193 | # Use matplotlib's annotate functionality to add date-time stamps along 194 | # the sky trajectory 195 | plt.annotate(date_str, 196 | (ceres_x, ceres_y), 197 | textcoords="offset points", 198 | xytext=(12, 2), 199 | ha='left', 200 | color='white', \ 201 | alpha=0.7, \ 202 | fontsize=8) 203 | 204 | # Set a grid 205 | plt.grid(True, linestyle='dashed', alpha=0.5) 206 | 207 | # Get the axes 208 | ax = plt.gca() 209 | ax.ticklabel_format(useOffset=False, style='plain') 210 | 211 | # Add a colorbar 212 | cbar = plt.colorbar() 213 | cbar.ax.invert_yaxis() 214 | cbar.set_label('Apparent Magnitude') 215 | 216 | # Set long. / lat. labels 217 | plt.xlabel('Eclip. Long. in deg') 218 | plt.ylabel('Eclip. Lat. in deg') 219 | 220 | # Set an x-lim range 221 | plt.xlim(np.min(ceres_df['ECLIP_LONG_DEG'])*0.98, \ 222 | np.max(ceres_df['ECLIP_LONG_DEG'])*1.02) 223 | 224 | # Set a title 225 | plt.title('Movement of Ceres in the Sky') 226 | 227 | # Save the figure 228 | plt.savefig('ceres_sky_map_movement.png', dpi=300) 229 | -------------------------------------------------------------------------------- /part20/kernel_meta.txt: -------------------------------------------------------------------------------- 1 | \begintext 2 | 3 | This meta file contains the relative paths to all needed SPICE kernels. 4 | For each tutorial we will set up an individual kernel_meta.txt. A common 5 | meta file could be easily stored in the main folder (next to the _kernel 6 | directory), however it would be over-loaded at some point. 7 | 8 | \begindata 9 | 10 | KERNELS_TO_LOAD = ( 11 | '../_kernels/spk/de432s.bsp', 12 | '../_kernels/lsk/naif0012.tls', 13 | '../_kernels/pck/gm_de431.tpc', 14 | '../_kernels/spk/codes_300ast_20100725.bsp' 15 | '../_kernels/spk/codes_300ast_20100725.tf' 16 | '../_kernels/spk/codes_300ast_20100725.cmt' 17 | ) 18 | -------------------------------------------------------------------------------- /part23/mylib/__init__.py: -------------------------------------------------------------------------------- 1 | from . import tests 2 | from . import general 3 | -------------------------------------------------------------------------------- /part23/mylib/general/__init__.py: -------------------------------------------------------------------------------- 1 | from . import vec 2 | -------------------------------------------------------------------------------- /part23/mylib/general/vec.py: -------------------------------------------------------------------------------- 1 | """ 2 | general/vec.py 3 | 4 | This Python file contains miscellaneous, generic functions for vector 5 | computations like the computation of a norm, the dot product or the enclosed 6 | angle between 2 vectors. 7 | """ 8 | 9 | # Import standard libraries 10 | import math 11 | 12 | def vec_norm(vector, norm='p2'): 13 | """ 14 | Compute the norm of an n-dimensional vector. 15 | 16 | Parameters 17 | ---------- 18 | vector : list 19 | Single n-dimensional vector as a Python list. 20 | norm : str, optional 21 | Requested norm type. The default is 'p2'. 22 | - pX: P Norm. X represents any number larger than 0. E.g., p2 is the 23 | Euclidean Norm 24 | 25 | Returns 26 | ------- 27 | norm_res : float 28 | The resulting norm. 29 | 30 | """ 31 | 32 | # if-elif statement for the requested norm 33 | if 'p' in norm: 34 | 35 | # The second entry of the input string is the p norm value (only valid 36 | # up to 9) 37 | p_value = float(norm[1]) 38 | 39 | # Compute the norm 40 | norm_res = math.sqrt(sum(abs(elem)**p_value for elem in vector)) 41 | 42 | return norm_res 43 | 44 | def vec_dotprod(vector1, vector2): 45 | """ 46 | Dot product of 2 vectors 47 | 48 | Parameters 49 | ---------- 50 | vector1 : list 51 | Input vector 1. 52 | vector2 : list 53 | Input vector 2. 54 | 55 | Returns 56 | ------- 57 | dotp_res : float 58 | Resulting dot product. 59 | 60 | """ 61 | dotp_res = sum(v1_i * v2_i for v1_i, v2_i in zip(vector1, vector2)) 62 | 63 | return dotp_res 64 | 65 | def vec_angle(vector1, vector2): 66 | 67 | angle_rad = math.acos(vec_dotprod(vector1, vector2) \ 68 | / (vec_norm(vector1) * vec_norm(vector2))) 69 | 70 | return angle_rad -------------------------------------------------------------------------------- /part23/mylib/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | vec_norm : general.vec.vec_norm 4 | vec_dotprod : general.vec.vec_dotprod 5 | vec_angle :general.vec.vec_angle 6 | -------------------------------------------------------------------------------- /part23/mylib/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from . import test_general_vec 2 | -------------------------------------------------------------------------------- /part23/mylib/tests/test_general_vec.py: -------------------------------------------------------------------------------- 1 | import math 2 | import mylib 3 | import pytest 4 | 5 | @pytest.mark.vec_norm 6 | def test_vec_norm(): 7 | 8 | vec_norm_res1 = mylib.general.vec.vec_norm(vector=[1.0, 0.0]) 9 | assert vec_norm_res1 == 1.0 10 | 11 | vec_norm_res2 = mylib.general.vec.vec_norm(vector=[1.0, 1.0]) 12 | assert vec_norm_res2 == math.sqrt(2) 13 | 14 | vec_norm_res3 = mylib.general.vec.vec_norm(vector=[5.0, 4.0]) 15 | assert vec_norm_res3 == math.sqrt(41) 16 | 17 | vec_norm_res4 = mylib.general.vec.vec_norm(vector=[1.0, 1.0, 1.0]) 18 | assert vec_norm_res4 == math.sqrt(3) 19 | 20 | vec_norm_res5 = mylib.general.vec.vec_norm(vector=[2.0, 4.0, -5.0, 6.0], \ 21 | norm='p2') 22 | assert vec_norm_res5 == 9.0 23 | 24 | @pytest.mark.vec_dotprod 25 | def test_vec_dotprod(): 26 | 27 | dot_res1 = mylib.general.vec.vec_dotprod(vector1=[1.0, 2.0, 3.0], \ 28 | vector2=[-2.0, 5.0, 8.0]) 29 | assert dot_res1 == 32.0 30 | 31 | dot_res2 = mylib.general.vec.vec_dotprod(vector1=[-10.0, 20.0], \ 32 | vector2=[-1.0, 0.0]) 33 | assert dot_res2 == 10.0 34 | 35 | dot_res3 = mylib.general.vec.vec_dotprod(vector1=[23.0, 10.0], \ 36 | vector2=[2.0, 0.01]) 37 | assert dot_res3 == 46.1 38 | 39 | @pytest.mark.vec_angle 40 | def test_vec_angle(): 41 | 42 | angle_res1 = mylib.general.vec.vec_angle(vector1=[1.0, 0.0], \ 43 | vector2=[0.0, 1.0]) 44 | assert angle_res1 == math.pi / 2.0 -------------------------------------------------------------------------------- /part3/PLANETS_SUN_SSB_PHASE_ANGLE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAlbin/SpaceScienceTutorial/9ca5c1340480a29a112dec91e075b7ee2eff38ed/part3/PLANETS_SUN_SSB_PHASE_ANGLE.png -------------------------------------------------------------------------------- /part3/SSB2SUN_DISTANCE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAlbin/SpaceScienceTutorial/9ca5c1340480a29a112dec91e075b7ee2eff38ed/part3/SSB2SUN_DISTANCE.png -------------------------------------------------------------------------------- /part3/SpaceSciencePython_part3.py: -------------------------------------------------------------------------------- 1 | # Import modules 2 | import datetime 3 | import spiceypy 4 | import numpy as np 5 | import pandas as pd 6 | 7 | # Load the SPICE kernels via a meta file 8 | spiceypy.furnsh('kernel_meta.txt') 9 | 10 | # We want to compute miscellaneous positions w.r.t. the centre of 11 | # the Sun for a certain time interval. 12 | # First, we set an initial time in UTC. 13 | INIT_TIME_UTC = datetime.datetime(year=2000, month=1, day=1, \ 14 | hour=0, minute=0, second=0) 15 | 16 | # Add a number of days; you can play around with the datetime variables; but 17 | # leave it as it is for the first try, since other computations and comments 18 | # are based on this value. 19 | DELTA_DAYS = 10000 20 | END_TIME_UTC = INIT_TIME_UTC + datetime.timedelta(days=DELTA_DAYS) 21 | 22 | # Convert the datetime objects now to strings 23 | INIT_TIME_UTC_STR = INIT_TIME_UTC.strftime('%Y-%m-%dT%H:%M:%S') 24 | END_TIME_UTC_STR = END_TIME_UTC.strftime('%Y-%m-%dT%H:%M:%S') 25 | 26 | # Print the starting and end times 27 | print('Init time in UTC: %s' % INIT_TIME_UTC_STR) 28 | print('End time in UTC: %s\n' % END_TIME_UTC_STR) 29 | 30 | # Convert to Ephemeris Time (ET) using the SPICE function utc2et 31 | INIT_TIME_ET = spiceypy.utc2et(INIT_TIME_UTC_STR) 32 | END_TIME_ET = spiceypy.utc2et(END_TIME_UTC_STR) 33 | 34 | # Create a numpy array that covers a time interval in delta = 1 day step 35 | TIME_INTERVAL_ET = np.linspace(INIT_TIME_ET, END_TIME_ET, DELTA_DAYS) 36 | 37 | #%% 38 | 39 | # Using km is not intuitive. AU would scale it too severely. Since we compute 40 | # the Solar System Barycentre (SSB) w.r.t. the Sun; and since we expect it to 41 | # be close to the Sun, we scale the x, y, z component w.r.t the radius of the 42 | # Sun. We extract the Sun radii (x, y, z components of the Sun ellipsoid) and 43 | # use the x component 44 | _, RADII_SUN = spiceypy.bodvcd(bodyid=10, item='RADII', maxn=3) 45 | 46 | RADIUS_SUN = RADII_SUN[0] 47 | 48 | 49 | #%% 50 | 51 | # All our computed parameters, positions etc. shall be stored in a pandas 52 | # dataframe. First, we create an empty one 53 | SOLAR_SYSTEM_DF = pd.DataFrame() 54 | 55 | # Set the column ET that stores all ETs 56 | SOLAR_SYSTEM_DF.loc[:, 'ET'] = TIME_INTERVAL_ET 57 | 58 | # The column UTC transforms all ETs back to a UTC format. The function 59 | # spicepy.et2datetime is NOT an official part of SPICE (there you can find 60 | # et2utc). 61 | # However this function returns immediately a datetime object 62 | SOLAR_SYSTEM_DF.loc[:, 'UTC'] = \ 63 | SOLAR_SYSTEM_DF['ET'].apply(lambda x: spiceypy.et2datetime(et=x).date()) 64 | 65 | # Here, the position of the SSB, as seen from the Sun, is computed. Since 66 | # spicepy.spkgps returns the position and the corresponding light time, 67 | # we add the index [0] to obtain only the position array 68 | SOLAR_SYSTEM_DF.loc[:, 'POS_SSB_WRT_SUN'] = \ 69 | SOLAR_SYSTEM_DF['ET'].apply(lambda x: spiceypy.spkgps(targ=0, \ 70 | et=x, \ 71 | ref='ECLIPJ2000', \ 72 | obs=10)[0]) 73 | 74 | # Now the SSB position vector is scaled with the Sun's radius 75 | SOLAR_SYSTEM_DF.loc[:, 'POS_SSB_WRT_SUN_SCALED'] = \ 76 | SOLAR_SYSTEM_DF['POS_SSB_WRT_SUN'].apply(lambda x: x / RADIUS_SUN) 77 | 78 | # Finally the distance between the Sun and the SSB is computed. The length 79 | # (norm) of the vector needs to be determined with the SPICE function vnorm(). 80 | # numpy provides an identical function in: numpy.linalg.norm() 81 | SOLAR_SYSTEM_DF.loc[:, 'SSB_WRT_SUN_SCALED_DIST'] = \ 82 | SOLAR_SYSTEM_DF['POS_SSB_WRT_SUN_SCALED'].apply(lambda x: \ 83 | spiceypy.vnorm(x)) 84 | 85 | #%% 86 | 87 | # Import the matplotlib library 88 | from matplotlib import pyplot as plt 89 | 90 | # Set a figure 91 | FIG, AX = plt.subplots(figsize=(12, 8)) 92 | 93 | # Plot the distance between the Sun and the SSB 94 | AX.plot(SOLAR_SYSTEM_DF['UTC'], SOLAR_SYSTEM_DF['SSB_WRT_SUN_SCALED_DIST'], \ 95 | color='tab:blue') 96 | 97 | # Set a label for the x and y axis and color the y ticks accordingly 98 | AX.set_xlabel('Date in UTC') 99 | AX.set_ylabel('SSB Dist. in Sun Radii', color='tab:blue') 100 | AX.tick_params(axis='y', labelcolor='tab:blue') 101 | 102 | # Set limits for the x and y axis 103 | AX.set_xlim(min(SOLAR_SYSTEM_DF['UTC']), max(SOLAR_SYSTEM_DF['UTC'])) 104 | AX.set_ylim(0, 2) 105 | 106 | # Set a grid 107 | AX.grid(axis='x', linestyle='dashed', alpha=0.5) 108 | 109 | # Saving the figure in high quality 110 | plt.savefig('SSB2SUN_DISTANCE.png', dpi=300) 111 | 112 | #%% 113 | 114 | # Additionally, we want to compute the position vector of all outer gas 115 | # giants. We define a dictionary with a planet's barycentre abbreviation and 116 | # corresponding NAIF ID code 117 | NAIF_ID_DICT = {'JUP': 5, \ 118 | 'SAT': 6, \ 119 | 'URA': 7, \ 120 | 'NEP': 8} 121 | 122 | # Iterate through the dictionary and compute the position vector for each 123 | # planet as seen from the Sun. Further, compute the phase angle between the 124 | # SSB and the planet as seen from the Sun 125 | for planets_name_key in NAIF_ID_DICT: 126 | 127 | # Define the pandas dataframe column for each planet (position and phase 128 | # angle). Each '%s' substring is replaced with the planets name as 129 | # indicated after the "%" 130 | planet_pos_col = 'POS_%s_WRT_SUN' % planets_name_key 131 | planet_angle_col = 'PHASE_ANGLE_SUN_%s2SSB' % planets_name_key 132 | 133 | # Get the corresponding NAIF ID of the planet's barycentre 134 | planet_id = NAIF_ID_DICT[planets_name_key] 135 | 136 | # Compute the planet's position as seen from the Sun. 137 | SOLAR_SYSTEM_DF.loc[:, planet_pos_col] = \ 138 | SOLAR_SYSTEM_DF['ET'].apply(lambda x: \ 139 | spiceypy.spkgps(targ=planet_id, \ 140 | et=x, \ 141 | ref='ECLIPJ2000', \ 142 | obs=10)[0]) 143 | 144 | # Compute the phase angle between the SSB and the planet as seen from the 145 | # Sun. Since we apply a lambda function on all columns we need to set 146 | # axis=1, otherwise we get an error! 147 | SOLAR_SYSTEM_DF.loc[:, planet_angle_col] = \ 148 | SOLAR_SYSTEM_DF.apply(lambda x: \ 149 | np.degrees(spiceypy.vsep(x[planet_pos_col], \ 150 | x['POS_SSB_WRT_SUN'])),\ 151 | axis=1) 152 | 153 | #%% 154 | 155 | # Let's verify the function vsep and compute the phase angle between the SSB 156 | # and Jupiter as seen from the Sun (we use the very first array entries). 157 | # Define a lambda function the computes the angle between two vectors 158 | COMP_ANGLE = lambda vec1, vec2: np.arccos(np.dot(vec1, vec2) \ 159 | / (np.linalg.norm(vec1) \ 160 | * np.linalg.norm(vec2))) 161 | 162 | print('Phase angle between the SSB and Jupiter as seen from the Sun (first ' \ 163 | 'array entry, lambda function): %s' % \ 164 | np.degrees(COMP_ANGLE(SOLAR_SYSTEM_DF['POS_SSB_WRT_SUN'].iloc[0], \ 165 | SOLAR_SYSTEM_DF['POS_JUP_WRT_SUN'].iloc[0]))) 166 | 167 | 168 | print('Phase angle between the SSB and Jupiter as seen from the Sun (first ' \ 169 | 'array entry, SPICE vsep function): %s' % \ 170 | np.degrees(spiceypy.vsep(SOLAR_SYSTEM_DF['POS_SSB_WRT_SUN'].iloc[0], \ 171 | SOLAR_SYSTEM_DF['POS_JUP_WRT_SUN'].iloc[0]))) 172 | 173 | #%% 174 | 175 | # Create a 4 axes plot where all 4 plots are vertically aligned and share the 176 | # x axis (date in UTC) 177 | FIG, (AX1, AX2, AX3, AX4) = plt.subplots(4, 1, sharex=True, figsize=(8, 20)) 178 | 179 | # We iterate through the planets (from Jupiter to Neptune) and plot the 180 | # phase angle between the planet and the SSB, as seen from the Sun, in each 181 | # axis individually 182 | for ax_f, planet_abr, planet_name in zip([AX1, AX2, AX3, AX4], \ 183 | ['JUP', 'SAT', 'URA', 'NEP'], \ 184 | ['Jupiter', 'Saturn', 'Uranus', \ 185 | 'Neptune']): 186 | 187 | # First, we set the planet's name as the sub plot title (instead of 188 | # setting a legend) 189 | ax_f.set_title(planet_name, color='tab:orange') 190 | 191 | # The distance between the SSB and the Sun is plotted. 192 | ax_f.plot(SOLAR_SYSTEM_DF['UTC'], \ 193 | SOLAR_SYSTEM_DF['SSB_WRT_SUN_SCALED_DIST'], \ 194 | color='tab:blue') 195 | 196 | # A y label is set and the color of labels and ticks are adjusted for 197 | # better visibility 198 | ax_f.set_ylabel('SSB Dist. in Sun Radii', color='tab:blue') 199 | ax_f.tick_params(axis='y', labelcolor='tab:blue') 200 | 201 | # Set x (based on the min and max date) and y limits (the SSB has varying 202 | # distances between 0 and 2 Sun Radii) 203 | ax_f.set_xlim(min(SOLAR_SYSTEM_DF['UTC']), max(SOLAR_SYSTEM_DF['UTC'])) 204 | ax_f.set_ylim(0, 2) 205 | 206 | # We add now the phase angle values and copy the x axis for this purpose 207 | ax_f_add = ax_f.twinx() 208 | 209 | # Plot the phase angle between the SSB and planet as seen from the Sun 210 | ax_f_add.plot(SOLAR_SYSTEM_DF['UTC'], \ 211 | SOLAR_SYSTEM_DF['PHASE_ANGLE_SUN_%s2SSB' % planet_abr], \ 212 | color='tab:orange', \ 213 | linestyle='-') 214 | 215 | # Set the y label's name and color accordingly 216 | ax_f_add.set_ylabel('Planet ph. ang. in deg', color='tab:orange') 217 | ax_f_add.tick_params(axis='y', labelcolor='tab:orange') 218 | 219 | # Invert the y axis and set the limits. We invert the axis so that a 220 | # possible anti-correlation (large phase angle corresponds to a smaller 221 | # distance between the Sun's centre and the SSB) becomes more obvious 222 | ax_f_add.invert_yaxis() 223 | ax_f_add.set_ylim(180, 0) 224 | 225 | # Set a grid (only date) 226 | ax_f.grid(axis='x', linestyle='dashed', alpha=0.5) 227 | 228 | 229 | # Finally we set the x label ... 230 | AX4.set_xlabel('Date in UTC') 231 | 232 | # ... tight the figures a bit ... 233 | FIG.tight_layout() 234 | 235 | # ... reduce the distance between the axes ... 236 | plt.subplots_adjust(hspace=0.2) 237 | 238 | # ... and save the figure in high quality 239 | plt.savefig('PLANETS_SUN_SSB_PHASE_ANGLE.png', dpi=300) 240 | -------------------------------------------------------------------------------- /part3/kernel_meta.txt: -------------------------------------------------------------------------------- 1 | \begintext 2 | 3 | This meta file contains the relative paths to all needed SPICE kernels. 4 | For each tutorial we will set up an individual kernel_meta.txt. A common 5 | meta file could be easily stored in the main folder (next to the _kernel 6 | directory), however it would be over-loaded at some point. 7 | 8 | \begindata 9 | 10 | KERNELS_TO_LOAD = ( 11 | '../_kernels/spk/de432s.bsp', 12 | '../_kernels/lsk/naif0012.tls', 13 | '../_kernels/pck/pck00010.tpc' 14 | ) 15 | -------------------------------------------------------------------------------- /part4/SpaceSciencePython_part4.py: -------------------------------------------------------------------------------- 1 | # Import modules 2 | import datetime 3 | import spiceypy 4 | import numpy as np 5 | import pandas as pd 6 | 7 | # Load the SPICE kernels via a meta file 8 | spiceypy.furnsh('kernel_meta.txt') 9 | 10 | # Create an initial and ending time date-time object that is converted to a 11 | # string 12 | INIT_TIME_UTC_STR = datetime.datetime(year=2020, month=1, day=1) \ 13 | .strftime('%Y-%m-%dT%H:%M:%S') 14 | END_TIME_UTC_STR = datetime.datetime(year=2020, month=6, day=1) \ 15 | .strftime('%Y-%m-%dT%H:%M:%S') 16 | 17 | # Convert to Ephemeris Time (ET) using the SPICE function utc2et 18 | INIT_TIME_ET = spiceypy.utc2et(INIT_TIME_UTC_STR) 19 | END_TIME_ET = spiceypy.utc2et(END_TIME_UTC_STR) 20 | 21 | # Set the number of seconds per hours. This value is used to compute the phase 22 | # angles in 1 hour steps (the ET is given in seconds) 23 | DELTA_HOUR_IN_SECONDS = 3600.0 24 | TIME_INTERVAL_ET = np.arange(INIT_TIME_ET, END_TIME_ET, DELTA_HOUR_IN_SECONDS) 25 | 26 | #%% 27 | 28 | # All our computed parameters, positions etc. shall be stored in a pandas 29 | # dataframe. First, we create an empty one 30 | INNER_SOLSYS_DF = pd.DataFrame() 31 | 32 | # Set the column ET that stores all ETs 33 | INNER_SOLSYS_DF.loc[:, 'ET'] = TIME_INTERVAL_ET 34 | 35 | # The column UTC transforms all ETs back to a UTC format. The function 36 | # spicepy.et2datetime is NOT an official part of SPICE (there you can find 37 | # et2utc). 38 | # However this function returns immediately a date-time object 39 | INNER_SOLSYS_DF.loc[:, 'UTC'] = \ 40 | INNER_SOLSYS_DF['ET'].apply(lambda x: spiceypy.et2datetime(et=x)) 41 | 42 | # Compute now the phase angle between Venus and Sun as seen from Earth 43 | # 44 | # For this computation we need the SPICE function phaseq. et is the ET. Based 45 | # on SPICE's logic the target is the Earth (399) and the illumination source 46 | # (illmn) is the Sun (10), the observer (obsrvr) is Venus with the ID 299. 47 | # We apply a correction that considers the movement of the planets and the 48 | # light time (LT+S). 49 | INNER_SOLSYS_DF.loc[:, 'EARTH_VEN2SUN_ANGLE'] = \ 50 | INNER_SOLSYS_DF['ET'].apply(lambda x: \ 51 | np.degrees(spiceypy.phaseq(et=x, \ 52 | target='399', \ 53 | illmn='10', \ 54 | obsrvr='299', \ 55 | abcorr='LT+S'))) 56 | 57 | #%% 58 | 59 | # Compute the angle between the Moon and the Sun. We apply the same function 60 | # (phaseq). The Moon NAIF ID is 301 61 | INNER_SOLSYS_DF.loc[:, 'EARTH_MOON2SUN_ANGLE'] = \ 62 | INNER_SOLSYS_DF['ET'].apply(lambda x: \ 63 | np.degrees(spiceypy.phaseq(et=x, \ 64 | target='399', \ 65 | illmn='10', \ 66 | obsrvr='301', \ 67 | abcorr='LT+S'))) 68 | 69 | #%% 70 | 71 | # Compute finally the phase angle between the Moon and Venus 72 | INNER_SOLSYS_DF.loc[:, 'EARTH_MOON2VEN_ANGLE'] = \ 73 | INNER_SOLSYS_DF['ET'].apply(lambda x: \ 74 | np.degrees(spiceypy.phaseq(et=x, \ 75 | target='399', \ 76 | illmn='299', \ 77 | obsrvr='301', \ 78 | abcorr='LT+S'))) 79 | 80 | #%% 81 | 82 | # Are photos of both objects "photogenic"? Let's apply a pandas filtering 83 | # with some artificially set angular distances and create a binary tag for 84 | # photogenic (1) and non-photogenic (0) constellations 85 | # 86 | # Angular distance Venus - Sun: > 30 degrees 87 | # Angular distance Moon - Sun: > 30 degrees 88 | # Angular distance Moon - Venus: < 10 degrees 89 | INNER_SOLSYS_DF.loc[:, 'PHOTOGENIC'] = \ 90 | INNER_SOLSYS_DF.apply(lambda x: 1 if (x['EARTH_VEN2SUN_ANGLE'] > 30.0) \ 91 | & (x['EARTH_MOON2SUN_ANGLE'] > 30.0) \ 92 | & (x['EARTH_MOON2VEN_ANGLE'] < 10.0) \ 93 | else 0, axis=1) 94 | 95 | #%% 96 | 97 | # Print the temporal results (number of computed hours, and number of 98 | # "photogenic" hours) 99 | print('Number of hours computed: %s (around %s days)' \ 100 | % (len(INNER_SOLSYS_DF), round(len(INNER_SOLSYS_DF) / 24))) 101 | 102 | print('Number of photogenic hours: %s (around %s days)' \ 103 | % (len(INNER_SOLSYS_DF.loc[INNER_SOLSYS_DF['PHOTOGENIC'] == 1]), \ 104 | round(len(INNER_SOLSYS_DF.loc[INNER_SOLSYS_DF['PHOTOGENIC'] == 1]) \ 105 | / 24))) 106 | 107 | #%% 108 | 109 | # Import the matplotlib library 110 | from matplotlib import pyplot as plt 111 | import matplotlib.dates as matpl_dates 112 | 113 | # Set a figure 114 | FIG, AX = plt.subplots(figsize=(12, 8)) 115 | 116 | # Plot the miscellaneous phase angles; apply different colors for the curves 117 | # and set a legend label 118 | AX.plot(INNER_SOLSYS_DF['UTC'], INNER_SOLSYS_DF['EARTH_VEN2SUN_ANGLE'], \ 119 | color='tab:orange', label='Venus - Sun') 120 | 121 | AX.plot(INNER_SOLSYS_DF['UTC'], INNER_SOLSYS_DF['EARTH_MOON2SUN_ANGLE'], \ 122 | color='tab:gray', label='Moon - Sun') 123 | 124 | AX.plot(INNER_SOLSYS_DF['UTC'], INNER_SOLSYS_DF['EARTH_MOON2VEN_ANGLE'], \ 125 | color='black', label='Moon - Venus') 126 | 127 | # Set a label for the x and y axis accordingly 128 | AX.set_xlabel('Date in UTC') 129 | AX.set_ylabel('Angle in degrees') 130 | 131 | # Set limits for the x and y axis 132 | AX.set_xlim(min(INNER_SOLSYS_DF['UTC']), max(INNER_SOLSYS_DF['UTC'])) 133 | 134 | # Set a grid 135 | AX.grid(axis='x', linestyle='dashed', alpha=0.5) 136 | 137 | # Set a month and day locator for the plot 138 | AX.xaxis.set_major_locator(matpl_dates.MonthLocator()) 139 | AX.xaxis.set_minor_locator(matpl_dates.DayLocator()) 140 | 141 | # Set a format for the date-time (Year + Month name) 142 | AX.xaxis.set_major_formatter(matpl_dates.DateFormatter('%Y-%b')) 143 | 144 | # Iterate through the "photogenic" results and draw vertical lines where the 145 | # "photogenic" conditions apply 146 | for photogenic_utc in INNER_SOLSYS_DF.loc[INNER_SOLSYS_DF['PHOTOGENIC'] == 1]['UTC']: 147 | AX.axvline(photogenic_utc, color='tab:blue', alpha=0.2) 148 | 149 | # Create the legend in the top right corner of the plot 150 | AX.legend(fancybox=True, loc='upper right', framealpha=1) 151 | 152 | # Rotate the date-times 153 | plt.xticks(rotation=45) 154 | 155 | # Saving the figure in high quality 156 | plt.savefig('VENUS_SUN_MOON.png', dpi=300) 157 | -------------------------------------------------------------------------------- /part4/VENUS_SUN_MOON.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAlbin/SpaceScienceTutorial/9ca5c1340480a29a112dec91e075b7ee2eff38ed/part4/VENUS_SUN_MOON.png -------------------------------------------------------------------------------- /part4/kernel_meta.txt: -------------------------------------------------------------------------------- 1 | \begintext 2 | 3 | This meta file contains the relative paths to all needed SPICE kernels. 4 | For each tutorial we will set up an individual kernel_meta.txt. A common 5 | meta file could be easily stored in the main folder (next to the _kernel 6 | directory), however it would be over-loaded at some point. 7 | 8 | \begindata 9 | 10 | KERNELS_TO_LOAD = ( 11 | '../_kernels/spk/de432s.bsp', 12 | '../_kernels/lsk/naif0012.tls', 13 | ) 14 | -------------------------------------------------------------------------------- /part5/SpaceSciencePython_part5.py: -------------------------------------------------------------------------------- 1 | # Import modules 2 | import datetime 3 | import spiceypy 4 | import numpy as np 5 | import pandas as pd 6 | from matplotlib import pyplot as plt 7 | 8 | # Load the SPICE kernels via a meta file 9 | spiceypy.furnsh('kernel_meta.txt') 10 | 11 | # Create an initial date-time object that is converted to a string 12 | DATETIME_UTC = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S') 13 | 14 | # Convert to Ephemeris Time (ET) using the SPICE function utc2et 15 | DATETIME_ET = spiceypy.utc2et(DATETIME_UTC) 16 | 17 | #%% 18 | 19 | # We want to compute the coordinates for different Solar System bodies as seen 20 | # from our planet. First, a pandas dataframe is set that is used to append the 21 | # computed data 22 | solsys_df = pd.DataFrame() 23 | 24 | # Add the ET and the corresponding UTC date-time string 25 | solsys_df.loc[:, 'ET'] = [DATETIME_ET] 26 | solsys_df.loc[:, 'UTC'] = [DATETIME_UTC] 27 | 28 | # Set a dictionary that lists some body names and the corresponding NAIF ID 29 | # code. Mars has the ID 499, however the loaded kernels do not contain the 30 | # positional information. We use the Mars barycentre instead 31 | SOLSYS_DICT = {'SUN': 10, 'VENUS': 299, 'MOON': 301, 'MARS': 4} 32 | 33 | #%% 34 | 35 | # Iterate through the dictionary and compute miscellaneous positional 36 | # parameters 37 | for body_name in SOLSYS_DICT: 38 | 39 | # First, compute the directional vector Earth - body in ECLIPJ2000. Use 40 | # LT+S light time correction. spkezp returns the directional vector and 41 | # light time. Apply [0] to get only the vector 42 | solsys_df.loc[:, f'dir_{body_name}_wrt_earth_ecl'] = solsys_df['ET'] \ 43 | .apply(lambda x: spiceypy.spkezp(targ=SOLSYS_DICT[body_name], \ 44 | et=x, \ 45 | ref='ECLIPJ2000', \ 46 | abcorr='LT+S', \ 47 | obs=399)[0]) 48 | 49 | # Compute the longitude and latitude of the body in radians in ECLIPJ2000 50 | # using the function recrad. recrad returns the distance, longitude and 51 | # latitude value; thus, apply [1] and [2] to get the longitude and 52 | # latitude, respectively 53 | solsys_df.loc[:, f'{body_name}_long_rad_ecl'] = \ 54 | solsys_df[f'dir_{body_name}_wrt_earth_ecl'] \ 55 | .apply(lambda x: spiceypy.recrad(x)[1]) 56 | 57 | solsys_df.loc[:, f'{body_name}_lat_rad_ecl'] = \ 58 | solsys_df[f'dir_{body_name}_wrt_earth_ecl'] \ 59 | .apply(lambda x: spiceypy.recrad(x)[2]) 60 | 61 | #%% 62 | 63 | # Create an empty matplotlib example plot to show how matplotlib displays 64 | # projected data 65 | 66 | # Use a dark background 67 | plt.style.use('dark_background') 68 | 69 | # Set a figure 70 | plt.figure(figsize=(12, 8)) 71 | 72 | # Apply the aitoff projection and activate the grid 73 | plt.subplot(projection="aitoff") 74 | plt.grid(True) 75 | 76 | # Set long. / lat. labels 77 | plt.xlabel('Long. in deg') 78 | plt.ylabel('Lat. in deg') 79 | 80 | # Save the figure 81 | plt.savefig('empty_aitoff.png', dpi=300) 82 | 83 | #%% 84 | 85 | # Before we plot the data, we need to convert the longitude data into a 86 | # matplotlib compatible format. We computed longitude values between 0 and 87 | # 2*pi (360 degrees). matplotlib expects values between -pi and +pi. Further, 88 | # sky maps count from 0 degrees longitude to the left. Thus we need also to 89 | # invert the longitude values 90 | for body_name in SOLSYS_DICT: 91 | 92 | solsys_df.loc[:, f'{body_name}_long_rad4plot_ecl'] = \ 93 | solsys_df[f'{body_name}_long_rad_ecl'] \ 94 | .apply(lambda x: -1*((x % np.pi) - np.pi) if x > np.pi \ 95 | else -1*x) 96 | 97 | #%% 98 | 99 | # Create now a sky map of the results 100 | 101 | # Set a dark background (the night sky is ... dark) 102 | plt.style.use('dark_background') 103 | 104 | # Create a figure and then apply the aitoff projection 105 | plt.figure(figsize=(12, 8)) 106 | plt.subplot(projection="aitoff") 107 | 108 | # Set the UTC time string as a title 109 | plt.title(f'{DATETIME_UTC} UTC', fontsize=10) 110 | 111 | # Each body shall have an individual color; set a list with some colors 112 | BODY_COLOR_ARRAY = ['y', 'tab:orange', 'tab:gray', 'tab:red'] 113 | 114 | # Iterate through the pandas dataframe. And plot each celestial body 115 | for body_name, body_color in zip(SOLSYS_DICT, BODY_COLOR_ARRAY): 116 | 117 | # Plot the longitude and latitude data. Apply the color, and other 118 | # formatting parameters 119 | plt.plot(solsys_df[f'{body_name}_long_rad4plot_ecl'], \ 120 | solsys_df[f'{body_name}_lat_rad_ecl'], \ 121 | color=body_color, marker='o', linestyle='None', markersize=12, \ 122 | label=body_name.capitalize()) 123 | 124 | # Replace the standard x ticks (longitude) with the ecliptic coordinates 125 | plt.xticks(ticks=np.radians([-150, -120, -90, -60, -30, 0, \ 126 | 30, 60, 90, 120, 150]), 127 | labels=['150°', '120°', '90°', '60°', '30°', '0°', \ 128 | '330°', '300°', '270°', '240°', '210°']) 129 | 130 | # Set the axes labels 131 | plt.xlabel('Eclip. long. in deg') 132 | plt.ylabel('Eclip. lat. in deg') 133 | 134 | # Create a legend and grid 135 | plt.legend() 136 | plt.grid(True) 137 | 138 | # Save the figure 139 | plt.savefig('eclipj2000_sky_map.png', dpi=300) 140 | 141 | #%% 142 | 143 | # Now we want the coordinates in equatorial J2000. For this purpose we 144 | # iterate through all celestial bodies 145 | for body_name in SOLSYS_DICT: 146 | 147 | # First, compute the directional vector of the body as seen from Earth in 148 | # J2000 149 | solsys_df.loc[:, f'dir_{body_name}_wrt_earth_equ'] = solsys_df['ET'] \ 150 | .apply(lambda x: spiceypy.spkezp(targ=SOLSYS_DICT[body_name], \ 151 | et=x, \ 152 | ref='J2000', \ 153 | abcorr='LT+S', \ 154 | obs=399)[0]) 155 | 156 | # Compute the longitude and latitude values in equatorial J2000 157 | # coordinates 158 | solsys_df.loc[:, f'{body_name}_long_rad_equ'] = \ 159 | solsys_df[f'dir_{body_name}_wrt_earth_equ'] \ 160 | .apply(lambda x: spiceypy.recrad(x)[1]) 161 | solsys_df.loc[:, f'{body_name}_lat_rad_equ'] = \ 162 | solsys_df[f'dir_{body_name}_wrt_earth_equ'] \ 163 | .apply(lambda x: spiceypy.recrad(x)[2]) 164 | 165 | # Apply the same logic as shown before to compute the longitudes for the 166 | # matplotlib figure 167 | solsys_df.loc[:, f'{body_name}_long_rad4plot_equ'] = \ 168 | solsys_df[f'{body_name}_long_rad_equ'] \ 169 | .apply(lambda x: -1*((x % np.pi) - np.pi) if x > np.pi \ 170 | else -1*x) 171 | 172 | #%% 173 | 174 | # Before we plot the data, let's add the Ecliptic plane for the visualisation. 175 | # In ECLIPJ2000 the Ecliptic plane is the equator line (see corresponding 176 | # figure. The latitude is 0 degrees. 177 | 178 | # First, we create a separate dataframe for the ecliptic plane 179 | eclip_plane_df = pd.DataFrame() 180 | 181 | # Add the ecliptic longitude and latitude values for the plane. Note: here, 182 | # we need to use pi/2 (90 degrees) as the latitude, since we will apply a 183 | # SPICE function that expects spherical coordinates 184 | eclip_plane_df.loc[:, 'ECLIPJ2000_long_rad'] = np.linspace(0, 2*np.pi, 100) 185 | eclip_plane_df.loc[:, 'ECLIPJ2000_lat_rad'] = np.pi/2.0 186 | 187 | # Compute the directional vectors of the ecliptic plane for the different 188 | # longitude values (the latitude is constant). Apply the SPICE function sphrec 189 | # to transform the spherical coordinates to vectors. r=1 is the distance, 190 | # here in our case: normalised distance 191 | eclip_plane_df.loc[:, 'ECLIPJ2000_direction'] = \ 192 | eclip_plane_df\ 193 | .apply(lambda x: spiceypy.sphrec(r=1, \ 194 | colat=x['ECLIPJ2000_lat_rad'], \ 195 | lon=x['ECLIPJ2000_long_rad']), \ 196 | axis=1) 197 | 198 | #%% 199 | 200 | # Compute a transformation matrix between ECLIPJ2000 and J2000 for a fixed 201 | # date-time. Since both coordinate system are inertial (not changing in time) 202 | # the resulting matrix is the same for different ETs 203 | ECL2EQU_MAT = spiceypy.pxform(fromstr='ECLIPJ2000', \ 204 | tostr='J2000', \ 205 | et=DATETIME_ET) 206 | 207 | # Compute the direction vectors of the Ecliptic plane in J2000 using the 208 | # transformation matrix 209 | eclip_plane_df.loc[:, 'j2000_direction'] = \ 210 | eclip_plane_df['ECLIPJ2000_direction'].apply(lambda x: ECL2EQU_MAT.dot(x)) 211 | 212 | # Compute now the longitude (and matplotlib compatible version) and the 213 | # latitude values using the SPICE function recrad 214 | eclip_plane_df.loc[:, 'j2000_long_rad'] = \ 215 | eclip_plane_df['j2000_direction'].apply(lambda x: spiceypy.recrad(x)[1]) 216 | 217 | eclip_plane_df.loc[:, 'j2000_long_rad4plot'] = \ 218 | eclip_plane_df['j2000_long_rad'] \ 219 | .apply(lambda x: -1*((x % np.pi) - np.pi) if x > np.pi \ 220 | else -1*x) 221 | 222 | eclip_plane_df.loc[:, 'j2000_lat_rad'] = \ 223 | eclip_plane_df['j2000_direction'].apply(lambda x: spiceypy.recrad(x)[2]) 224 | 225 | #%% 226 | 227 | # We plot now the data in equatorial J2000. Again with a dark background and 228 | # the same properties as before 229 | plt.style.use('dark_background') 230 | plt.figure(figsize=(12, 8)) 231 | plt.subplot(projection="aitoff") 232 | plt.title(f'{DATETIME_UTC} UTC', fontsize=10) 233 | 234 | # Iterate through the celestial bodies and plot them 235 | for body_name, body_color in zip(SOLSYS_DICT, BODY_COLOR_ARRAY): 236 | 237 | plt.plot(solsys_df[f'{body_name}_long_rad4plot_equ'], \ 238 | solsys_df[f'{body_name}_lat_rad_equ'], \ 239 | color=body_color, marker='o', linestyle='None', markersize=12, \ 240 | label=body_name.capitalize()) 241 | 242 | # Plot the Ecliptic plane as a blue dotted line 243 | plt.plot(eclip_plane_df['j2000_long_rad4plot'], \ 244 | eclip_plane_df['j2000_lat_rad'], color='tab:blue', linestyle='None', \ 245 | marker='o', markersize=2) 246 | 247 | # Convert the longitude values finally in right ascension hours 248 | plt.xticks(ticks=np.radians([-150, -120, -90, -60, -30, 0, \ 249 | 30, 60, 90, 120, 150]), 250 | labels=['10h', '8h', '6h', '4h', '2h', '0h', \ 251 | '22h', '20h', '18h', '16h', '14h']) 252 | 253 | # Plot the labels 254 | plt.xlabel('Right ascension in hours') 255 | plt.ylabel('Declination in deg.') 256 | 257 | # Create a legend and grid 258 | plt.legend() 259 | plt.grid(True) 260 | 261 | # Save the figure 262 | plt.savefig('j2000_sky_map.png', dpi=300) 263 | -------------------------------------------------------------------------------- /part5/eclipj2000_sky_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAlbin/SpaceScienceTutorial/9ca5c1340480a29a112dec91e075b7ee2eff38ed/part5/eclipj2000_sky_map.png -------------------------------------------------------------------------------- /part5/empty_aitoff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAlbin/SpaceScienceTutorial/9ca5c1340480a29a112dec91e075b7ee2eff38ed/part5/empty_aitoff.png -------------------------------------------------------------------------------- /part5/j2000_sky_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAlbin/SpaceScienceTutorial/9ca5c1340480a29a112dec91e075b7ee2eff38ed/part5/j2000_sky_map.png -------------------------------------------------------------------------------- /part5/kernel_meta.txt: -------------------------------------------------------------------------------- 1 | \begintext 2 | 3 | This meta file contains the relative paths to all needed SPICE kernels. 4 | For each tutorial we will set up an individual kernel_meta.txt. A common 5 | meta file could be easily stored in the main folder (next to the _kernel 6 | directory), however it would be over-loaded at some point. 7 | 8 | \begindata 9 | 10 | KERNELS_TO_LOAD = ( 11 | '../_kernels/spk/de432s.bsp', 12 | '../_kernels/lsk/naif0012.tls', 13 | ) 14 | -------------------------------------------------------------------------------- /part6/SpaceSciencePython_part6.py: -------------------------------------------------------------------------------- 1 | # Import the modules 2 | import datetime 3 | import pathlib 4 | import urllib.request 5 | import os 6 | 7 | import numpy as np 8 | import spiceypy 9 | 10 | #%% 11 | 12 | # We define a function that is useful for downloading SPICE 13 | # kernel files. Some kernels are large and cannot be uploaded on the GitHub 14 | # repository. Thus, this helper function shall support you for future kernel 15 | # management (if needed). 16 | def download_kernel(dl_path, dl_url): 17 | """ 18 | download_kernel(DL_PATH, DL_URL) 19 | 20 | This helper function supports one to download kernel files from the NASA 21 | NAIF repository and stores them in the _kernel directory. 22 | 23 | Parameters 24 | ---------- 25 | DL_PATH : str 26 | Download path on the local machine, relative to this function. 27 | DL_URL : str 28 | Download url of the requested kernel file. 29 | """ 30 | 31 | # Obtain the kernel file name from the url string. The url is split at 32 | # the "/", thus the very last entry of the resulting list is the file's 33 | # name 34 | file_name = dl_url.split('/')[-1] 35 | 36 | # Create necessary sub-directories in the DL_PATH direction (if not 37 | # existing) 38 | pathlib.Path(dl_path).mkdir(exist_ok=True) 39 | 40 | # If the file is not present in the download directory -> download it 41 | if not os.path.isfile(dl_path + file_name): 42 | 43 | # Download the file with the urllib package 44 | urllib.request.urlretrieve(dl_url, dl_path + file_name) 45 | 46 | #%% 47 | 48 | # Download the asteroids spk kernel file. First, set a download path, then 49 | # the url and call the download function 50 | PATH = '../_kernels/spk/' 51 | URL = 'https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/asteroids/' \ 52 | + 'codes_300ast_20100725.bsp' 53 | 54 | download_kernel(PATH, URL) 55 | 56 | # Download an auxiliary file from the repository that contains the NAIF ID 57 | # codes and a reference frame kernel that is needed. Since we have a mixture 58 | # of different kernel types we store the data in a sub-directory called _misc 59 | PATH = '../_kernels/_misc/' 60 | URL = 'https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/asteroids/' \ 61 | + 'codes_300ast_20100725.tf' 62 | 63 | download_kernel(PATH, URL) 64 | 65 | #%% 66 | 67 | # Load the SPICE kernels via a meta file 68 | spiceypy.furnsh('kernel_meta.txt') 69 | 70 | # Create an initial date-time object that is converted to a string 71 | DATETIME_UTC = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S') 72 | 73 | # Convert to Ephemeris Time (ET) using the SPICE function utc2et 74 | DATETIME_ET = spiceypy.utc2et(DATETIME_UTC) 75 | 76 | #%% 77 | 78 | # ECLIPJ2000_DE405 and ECLIPJ2000 appear to be similar?! A transformation 79 | # matrix between both coordinate systems (for state vectors) should be 80 | # consequently the identity matrix 81 | MAT = spiceypy.sxform(instring='ECLIPJ2000_DE405', \ 82 | tostring='ECLIPJ2000', \ 83 | et=DATETIME_ET) 84 | 85 | # Let's print the transformation matrix row-wise (spoiler alert: it is the 86 | # identity matrix) 87 | print('Transformation matrix between ECLIPJ2000_DE405 and ECLIPJ2000') 88 | for mat_row in MAT: 89 | print(f'{np.round(mat_row, 2)}') 90 | print('\n') 91 | 92 | #%% 93 | 94 | # Compute the state vector of Ceres in ECLIPJ2000 as seen from the Sun 95 | CERES_STATE_VECTOR, _ = spiceypy.spkgeo(targ=2000001, \ 96 | et=DATETIME_ET, \ 97 | ref='ECLIPJ2000', 98 | obs=10) 99 | 100 | #%% 101 | 102 | # Get the G*M value for the Sun 103 | _, GM_SUN_PRE = spiceypy.bodvcd(bodyid=10, item='GM', maxn=1) 104 | 105 | GM_SUN = GM_SUN_PRE[0] 106 | 107 | #%% 108 | 109 | # Compute the orbital elements of Ceres using the computed state vector 110 | CERES_ORBITAL_ELEMENTS = spiceypy.oscltx(state=CERES_STATE_VECTOR, \ 111 | et=DATETIME_ET, \ 112 | mu=GM_SUN) 113 | 114 | # Set and convert the semi-major axis and perihelion from km to AU 115 | CERES_SEMI_MAJOR_AU = spiceypy.convrt(CERES_ORBITAL_ELEMENTS[9], \ 116 | inunit='km', outunit='AU') 117 | CERES_PERIHELION_AU = spiceypy.convrt(CERES_ORBITAL_ELEMENTS[0], \ 118 | inunit='km', outunit='AU') 119 | 120 | # Set the eccentricity 121 | CERES_ECC = CERES_ORBITAL_ELEMENTS[1] 122 | 123 | # Set and convert miscellaneous angular values from radians to degrees: 124 | # inc: Inclination 125 | # lnode: Longitude of ascending node 126 | # argp: Argument of perihelion 127 | CERES_INC_DEG = np.degrees(CERES_ORBITAL_ELEMENTS[2]) 128 | CERES_LNODE_DEG = np.degrees(CERES_ORBITAL_ELEMENTS[3]) 129 | CERES_ARGP_DEG = np.degrees(CERES_ORBITAL_ELEMENTS[4]) 130 | 131 | # Set the orbit period. Convert from seconds to years 132 | CERES_ORB_TIME_YEARS = CERES_ORBITAL_ELEMENTS[10] / (86400.0 * 365.0) 133 | 134 | #%% 135 | 136 | # Compare the results with the data from the Minor Planet Center 137 | # https://www.minorplanetcenter.net/dwarf_planets 138 | 139 | # Print the results next to the MPC results 140 | print('Ceres\' Orbital Elements') 141 | print(f'Semi-major axis in AU: {round(CERES_SEMI_MAJOR_AU, 2)} (MPC: 2.77)') 142 | print(f'Perihelion in AU: {round(CERES_PERIHELION_AU, 2)} (MPC: 2.56)') 143 | 144 | print(f'Eccentricity: {round(CERES_ECC, 2)} (MPC: 0.08)') 145 | 146 | print(f'Inclination in degrees: {round(CERES_INC_DEG, 1)} (MPC: 10.6)') 147 | print(f'Long. of. asc. node in degrees: {round(CERES_LNODE_DEG, 1)} ' \ 148 | '(MPC: 80.3)') 149 | print(f'Argument of perih. in degrees: {round(CERES_ARGP_DEG, 1)} ' \ 150 | '(MPC: 73.6)') 151 | 152 | print(f'Orbit period in years: {round(CERES_ORB_TIME_YEARS, 2)} ' \ 153 | '(MPC: 4.61)') 154 | print('\n') 155 | 156 | #%% 157 | 158 | # Convert the orbital elements back to the state vector 159 | CERES_STATE_RE = spiceypy.conics([CERES_ORBITAL_ELEMENTS[0], \ 160 | CERES_ORBITAL_ELEMENTS[1], \ 161 | CERES_ORBITAL_ELEMENTS[2], \ 162 | CERES_ORBITAL_ELEMENTS[3], \ 163 | CERES_ORBITAL_ELEMENTS[4], \ 164 | CERES_ORBITAL_ELEMENTS[5], \ 165 | CERES_ORBITAL_ELEMENTS[6], \ 166 | GM_SUN], DATETIME_ET) 167 | 168 | print('State vector of Ceres from the kernel:\n' \ 169 | f'{CERES_STATE_VECTOR}') 170 | print('State vector of Ceres based on the determined orbital elements:\n' \ 171 | f'{CERES_STATE_RE}') 172 | print('\n') 173 | 174 | #%% 175 | 176 | # On spaceweather.com we can see that an asteroid has a close Earth fly-by: 177 | # 136795(1997BQ) on 2020-May-21 at a distance of 16.1 Lunar Distance 178 | # 179 | # Will the encounter alter the orbit of the asteroid? Let's have a first look 180 | # on the so-called sphere of influence (SOI) of our planet. 181 | # A simple model assumes that the SOI is a sphere. The semi major axis is set 182 | # to 1 AU: 183 | 184 | # 1 AU in km 185 | ONE_AU = spiceypy.convrt(x=1, inunit='AU', outunit='km') 186 | 187 | # Set the G*M parameter of our planet 188 | _, GM_EARTH_PRE = spiceypy.bodvcd(bodyid=399, item='GM', maxn=1) 189 | GM_EARTH = GM_EARTH_PRE[0] 190 | 191 | # Compute the SOI radius of the Earth 192 | SOI_EARTH_R = ONE_AU * (GM_EARTH/GM_SUN) ** (2.0/5.0) 193 | 194 | # Set one Lunar Distance (LD) in km (value from spaceweather.com) 195 | ONE_LD = 384401.0 196 | 197 | print(f'SOI of the Earth in LD: {SOI_EARTH_R/ONE_LD}') 198 | print('\n') 199 | 200 | #%% 201 | 202 | # Now we can compute the current position of the object. We obtain the orbit 203 | # elements data from https://ssd.jpl.nasa.gov/sbdb.cgi?sstr=136795 204 | 205 | # Before we compute a state vector of the asteroid and the current distance 206 | # to our home planet we need to define a function to round the data. A common 207 | # convention for scientific work is to round the data to two significant 208 | # digits. We create a lambda function that rounds the values based on the 209 | # provided measurement error 210 | round_sig = lambda value, err: np.round(value, \ 211 | -1*(int(np.floor(np.log10(err))))+1) 212 | 213 | #%% 214 | 215 | # Set now the perihelion in km 216 | NEO_1997BQ_PERIHELION_KM = spiceypy.convrt(round_sig(0.9109776989775201, \ 217 | 9.5537e-08), \ 218 | inunit='AU', outunit='km') 219 | 220 | # Set the eccentricity 221 | NEO_1997BQ_ECC = round_sig(0.4786097161397527, 5.364e-08) 222 | 223 | # Set the inclination, longitude of ascending node and argument of periapsis 224 | # in radians 225 | NEO_1997BQ_INC_RAD = np.radians(round_sig(10.99171566990081, 7.6286e-06)) 226 | NEO_1997BQ_LNODE_RAD = np.radians(round_sig(50.19104637224941, 3.6206e-05)) 227 | NEO_1997BQ_ARGP_RAD = np.radians(round_sig(147.4553849006326, 3.6033e-05)) 228 | 229 | # Set the mean anomaly and corresponding epoch in Julian Date (JD) 230 | NEO_1997BQ_M0_AT_T0_RAD = np.radians(round_sig(17.87249899172771, 1.0297e-05)) 231 | NEO_1997BQ_T0 = spiceypy.utc2et('2459000.5 JD') 232 | 233 | #%% 234 | 235 | # Set the orbital elements array 236 | NEO_1997BQ_ORBITAL_ELEMENTS = [NEO_1997BQ_PERIHELION_KM, \ 237 | NEO_1997BQ_ECC, \ 238 | NEO_1997BQ_INC_RAD, \ 239 | NEO_1997BQ_LNODE_RAD, \ 240 | NEO_1997BQ_ARGP_RAD, \ 241 | NEO_1997BQ_M0_AT_T0_RAD, \ 242 | NEO_1997BQ_T0, \ 243 | GM_SUN] 244 | 245 | # Compute the state vector 246 | NEO_1997BQ_STATE_VECTOR = spiceypy.conics(NEO_1997BQ_ORBITAL_ELEMENTS, DATETIME_ET) 247 | 248 | print(f'Current state vector of 1997BQ in km and km/s ({DATETIME_UTC})):\n' \ 249 | f'{NEO_1997BQ_STATE_VECTOR}') 250 | print('\n') 251 | 252 | #%% 253 | 254 | # Now compute the state vector of the Earth: 255 | EARTH_STATE_VECTOR, _ = spiceypy.spkgeo(targ=399, \ 256 | et=DATETIME_ET, \ 257 | ref='ECLIPJ2000', 258 | obs=10) 259 | 260 | # Compute the current distance of the Earth and the asteroids in LD 261 | EARTH_1997BQ_DIST_KM = spiceypy.vnorm(EARTH_STATE_VECTOR[:3] \ 262 | - NEO_1997BQ_STATE_VECTOR[:3]) 263 | print(f'Current distance between the Earth and 1997BQ ({DATETIME_UTC}):\n' \ 264 | f'{EARTH_1997BQ_DIST_KM / ONE_LD} LDe') 265 | -------------------------------------------------------------------------------- /part6/kernel_meta.txt: -------------------------------------------------------------------------------- 1 | \begintext 2 | 3 | This meta file contains the relative paths to all needed SPICE kernels. 4 | For each tutorial we will set up an individual kernel_meta.txt. A common 5 | meta file could be easily stored in the main folder (next to the _kernel 6 | directory), however it would be over-loaded at some point. 7 | 8 | \begindata 9 | 10 | KERNELS_TO_LOAD = ( 11 | '../_kernels/spk/de432s.bsp', 12 | '../_kernels/spk/codes_300ast_20100725.bsp', 13 | '../_kernels/_misc/codes_300ast_20100725.tf', 14 | '../_kernels/lsk/naif0012.tls', 15 | '../_kernels/pck/gm_de431.tpc' 16 | ) 17 | -------------------------------------------------------------------------------- /part7/SpaceSciencePython_part7.py: -------------------------------------------------------------------------------- 1 | # Import the standard modules 2 | import datetime 3 | import pathlib 4 | import sqlite3 5 | 6 | # Import installed modules 7 | import pandas as pd 8 | import numpy as np 9 | import spiceypy 10 | 11 | # Import the Python script func from the auxiliary folder 12 | import sys 13 | sys.path.insert(1, '../_auxiliary') 14 | import func 15 | 16 | #%% 17 | 18 | # Set a local download path and the URL to the comet data from the Minor 19 | # Planet Center 20 | DL_PATH = 'raw_data/' 21 | DL_URL = 'https://www.minorplanetcenter.net/Extended_Files/cometels.json.gz' 22 | 23 | # Download the comet data and store them in the directory 24 | func.download_file(DL_PATH, DL_URL) 25 | 26 | #%% 27 | 28 | # Load the SPICE kernel meta file 29 | spiceypy.furnsh('kernel_meta.txt') 30 | 31 | #%% 32 | 33 | # Read the g-zipped json file with pandas read_json. The function allows one 34 | # to read compressed data 35 | c_df = pd.read_json('raw_data/cometels.json.gz', compression='gzip') 36 | 37 | #%% 38 | 39 | # First we parse the date and time information. The dataset contains two 40 | # time related information: the date-time of the last perihelion passage and 41 | # another variable called Epoch. However, "epoch" is not related to the mean 42 | # anomaly related epoch and represents other time information in this case. 43 | # 44 | # For our "actual" Epoch case we need to create a UTC time string based on the 45 | # date and time of the last perihelion passage (the time corresponds to a mean 46 | # anomaly of 0 degrees). The Day is given in DAY.FRACTION_OF_DAY. We extract 47 | # only the day 48 | c_df.loc[:, 'EPOCH_UTC_DATE'] = \ 49 | c_df.apply(lambda x: str(x['Year_of_perihelion']) + '-' \ 50 | + str(x['Month_of_perihelion']) + '-' \ 51 | + str(x['Day_of_perihelion']).split('.')[0], \ 52 | axis=1) 53 | 54 | # Now we need to parse the .FRACTION_OF_DAY given between (0.0, 1.0). First, 55 | # create a place-holder date 56 | PRE_TIME = datetime.datetime(year=2000, month=1, day=1) 57 | 58 | # Use the pre_time date-time object and add the days and fraction of days with 59 | # the timedelta function from the datetime library. Extract only the time 60 | # substring ... 61 | c_df.loc[:, 'EPOCH_UTC_TIME'] = \ 62 | c_df['Day_of_perihelion'] \ 63 | .apply(lambda x: (PRE_TIME + datetime.timedelta(days=x)).\ 64 | strftime('%H:%M:%S')) 65 | 66 | # ... and based with the date, create now the UTC date-time 67 | c_df.loc[:, 'EPOCH_UTC'] = c_df.apply(lambda x: x['EPOCH_UTC_DATE'] \ 68 | + 'T' \ 69 | + x['EPOCH_UTC_TIME'],\ 70 | axis=1) 71 | 72 | # Convert the UTC datetime to ET 73 | c_df.loc[:, 'EPOCH_ET'] = c_df['EPOCH_UTC'].apply(lambda x: spiceypy.utc2et(x)) 74 | 75 | #%% 76 | 77 | # Let's compute a state vector of the comet Hale-Bopp as an example 78 | 79 | # Extract the G*M value of the Sun and assign it to a constant 80 | _, GM_SUN_PRE = spiceypy.bodvcd(bodyid=10, item='GM', maxn=1) 81 | GM_SUN = GM_SUN_PRE[0] 82 | 83 | # Get the Hale-Bopp data 84 | HALE_BOPP_DF = c_df.loc[c_df['Designation_and_name'].str.contains('Hale-Bopp')] 85 | 86 | # Set an array with orbital elements in a required format for the conics 87 | # function. Note: the mean anomaly is 0 degrees and will be set as a default 88 | # value in the SQLite database 89 | HALE_BOPP_ORB_ELEM = [spiceypy.convrt(HALE_BOPP_DF['Perihelion_dist'] \ 90 | .iloc[0], 'AU', 'km'), \ 91 | HALE_BOPP_DF['e'].iloc[0], \ 92 | np.radians(HALE_BOPP_DF['i'].iloc[0]), \ 93 | np.radians(HALE_BOPP_DF['Node'].iloc[0]), \ 94 | np.radians(HALE_BOPP_DF['Peri'].iloc[0]), \ 95 | 0.0, \ 96 | HALE_BOPP_DF['EPOCH_ET'].iloc[0], \ 97 | GM_SUN] 98 | 99 | #%% 100 | 101 | # Compute the state vector for midnight 2020-05-10 102 | HALE_BOPP_ST_VEC = spiceypy.conics(HALE_BOPP_ORB_ELEM, \ 103 | spiceypy.utc2et('2020-05-10')) 104 | 105 | # Compare with results from https://ssd.jpl.nasa.gov/horizons.cgi 106 | print('Comparison of the computed state \n' \ 107 | 'vector with the NASA HORIZONS results') 108 | print('==========================================') 109 | print(f'X in km (Comp): {HALE_BOPP_ST_VEC[0]:e}') 110 | print('X in km (NASA): 5.348377806424425E+08') 111 | print('==========================================') 112 | print(f'Y in km (Comp): {HALE_BOPP_ST_VEC[1]:e}') 113 | print('Y in km (NASA): -2.702225247057124E+09') 114 | print('==========================================') 115 | print(f'Z in km (Comp): {HALE_BOPP_ST_VEC[2]:e}') 116 | print('Z in km (NASA): -5.904425343521862E+09') 117 | print('==========================================') 118 | print(f'VX in km/s (Comp): {HALE_BOPP_ST_VEC[3]:e}') 119 | print('VX in km/s (NASA): 6.857065492623227E-01') 120 | print('==========================================') 121 | print(f'VY in km/s (Comp): {HALE_BOPP_ST_VEC[4]:e}') 122 | print('VY in km/s (NASA): -3.265390887669909E+00') 123 | print('==========================================') 124 | print(f'VZ in km/s (Comp): {HALE_BOPP_ST_VEC[5]:e}') 125 | print('VZ in km/s (NASA): -3.265390887669909E+00') 126 | 127 | #%% 128 | 129 | # Compute the semi-major axis for closed orbits ... 130 | c_df.loc[:, 'SEMI_MAJOR_AXIS_AU'] = \ 131 | c_df.apply(lambda x: x['Perihelion_dist'] / (1.0 - x['e']) if x['e'] < 1 \ 132 | else np.nan, \ 133 | axis=1) 134 | 135 | # ... as well as the APHELION (if applicable) 136 | c_df.loc[:, 'APHELION_AU'] = \ 137 | c_df.apply(lambda x: (1.0 + x['e']) * x['SEMI_MAJOR_AXIS_AU'] \ 138 | if x['e'] < 1 else np.nan, \ 139 | axis=1) 140 | 141 | #%% 142 | 143 | # Create a sub-directory in the main directory of this repository, where a 144 | # comet database shall be stored 145 | pathlib.Path('../_databases/_comets/').mkdir(parents=True, exist_ok=True) 146 | 147 | # Create / Connect to a comet database and set the cursor 148 | con = sqlite3.connect('../_databases/_comets/mpc_comets.db') 149 | cur = con.cursor() 150 | 151 | # Create (if not existing) a comets' main table, where miscellaneous 152 | # parameters are stored 153 | cur.execute('CREATE TABLE IF NOT EXISTS ' \ 154 | 'comets_main(NAME TEXT PRIMARY KEY, ' \ 155 | 'ORBIT_TYPE TEXT, ' \ 156 | 'PERIHELION_AU REAL, ' \ 157 | 'SEMI_MAJOR_AXIS_AU REAL, ' \ 158 | 'APHELION_AU REAL, ' \ 159 | 'ECCENTRICITY, ' \ 160 | 'INCLINATION_DEG REAL, ' \ 161 | 'ARG_OF_PERIH_DEG REAL, ' \ 162 | 'LONG_OF_ASC_NODE_DEG REAL, ' \ 163 | 'MEAN_ANOMALY_DEG REAL DEFAULT 0.0, ' \ 164 | 'EPOCH_UTC TEXT, ' \ 165 | 'EPOCH_ET REAL, ' \ 166 | 'ABSOLUTE_MAGNITUDE REAL, ' \ 167 | 'SLOPE_PARAMETER REAL' 168 | ')') 169 | 170 | #%% 171 | 172 | # Insert the data 173 | cur.executemany('INSERT OR REPLACE INTO ' \ 174 | 'comets_main(NAME, ' \ 175 | 'ORBIT_TYPE, ' \ 176 | 'PERIHELION_AU, ' \ 177 | 'SEMI_MAJOR_AXIS_AU, ' \ 178 | 'APHELION_AU, ' \ 179 | 'ECCENTRICITY, ' \ 180 | 'INCLINATION_DEG, ' \ 181 | 'ARG_OF_PERIH_DEG, ' \ 182 | 'LONG_OF_ASC_NODE_DEG, ' \ 183 | 'EPOCH_UTC, ' \ 184 | 'EPOCH_ET, ' \ 185 | 'ABSOLUTE_MAGNITUDE, ' \ 186 | 'SLOPE_PARAMETER' 187 | ') ' \ 188 | 'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', \ 189 | c_df[['Designation_and_name', \ 190 | 'Orbit_type', \ 191 | 'Perihelion_dist', \ 192 | 'SEMI_MAJOR_AXIS_AU', \ 193 | 'APHELION_AU', \ 194 | 'e', \ 195 | 'i', \ 196 | 'Peri', \ 197 | 'Node', \ 198 | 'EPOCH_UTC', \ 199 | 'EPOCH_ET', \ 200 | 'H', \ 201 | 'G']].values) 202 | 203 | # Commit 204 | con.commit() 205 | 206 | # Close the database. The database shall be the fundament for the next 207 | # tutorial sessions 208 | con.close() 209 | -------------------------------------------------------------------------------- /part7/kernel_meta.txt: -------------------------------------------------------------------------------- 1 | \begintext 2 | 3 | This meta file contains the relative paths to all needed SPICE kernels. 4 | For each tutorial we will set up an individual kernel_meta.txt. A common 5 | meta file could be easily stored in the main folder (next to the _kernel 6 | directory), however it would be over-loaded at some point. 7 | 8 | \begindata 9 | 10 | KERNELS_TO_LOAD = ( 11 | '../_kernels/pck/gm_de431.tpc', 12 | '../_kernels/lsk/naif0012.tls', 13 | ) 14 | -------------------------------------------------------------------------------- /part8/SpaceSciencePython_part8.py: -------------------------------------------------------------------------------- 1 | # Import the standard modules 2 | import sqlite3 3 | 4 | # Import the installed modules 5 | import pandas as pd 6 | import numpy as np 7 | 8 | # Import matplotlib for plotting 9 | from matplotlib import pyplot as plt 10 | 11 | #%% 12 | 13 | # Connect to the comet database. This database has been created in tutorial 14 | # part 7, however, due to its small size the database is uploaded on GitHub 15 | CON = sqlite3.connect('../_databases/_comets/mpc_comets.db') 16 | 17 | # Create a pandas dataframe that contains the aphelion and inclination data 18 | # for P type ... 19 | P_TYPE_DF = pd.read_sql('SELECT APHELION_AU, INCLINATION_DEG ' \ 20 | 'FROM comets_main WHERE ORBIT_TYPE="P"', CON) 21 | 22 | # ... and C type comets. For this type: include also the eccentricity 23 | C_TYPE_DF = pd.read_sql('SELECT APHELION_AU, INCLINATION_DEG, ECCENTRICITY ' \ 24 | 'FROM comets_main WHERE ORBIT_TYPE="C"', CON) 25 | 26 | #%% 27 | 28 | # Print some descriptive statistics of the P type comets 29 | print('Descriptive statistics of P comets') 30 | print(f'{P_TYPE_DF.describe()}') 31 | print('\n') 32 | 33 | # Print some descriptive statistics of the C type comets (differentiate 34 | # between bound (e<1) and un-bound (e>=1) comets) 35 | print('Descriptive statistics of C comets with an eccentricity < 1') 36 | print(f'{C_TYPE_DF.loc[C_TYPE_DF["ECCENTRICITY"]<1].describe()}') 37 | print('\n') 38 | 39 | print('Descriptive statistics of C comets with an eccentricity >= 1') 40 | print(f'{C_TYPE_DF.loc[C_TYPE_DF["ECCENTRICITY"]>=1].describe()}') 41 | print('\n') 42 | 43 | #%% 44 | 45 | # We plot the Inclination data vs. the aphelion data to determine differences 46 | # between P and C comets 47 | 48 | # Let's set a dark background 49 | plt.style.use('dark_background') 50 | 51 | # Set a default font size for better readability 52 | plt.rcParams.update({'font.size': 14}) 53 | 54 | # Set a figure with a certain figure size 55 | fig, ax = plt.subplots(figsize=(12, 8)) 56 | 57 | # Scatter plot of the P type comet inclination vs. the aphelion 58 | ax.scatter(P_TYPE_DF['APHELION_AU'], \ 59 | P_TYPE_DF['INCLINATION_DEG'], \ 60 | marker='.', color='tab:orange', alpha=0.1, label='P Type') 61 | 62 | # Scatter plot of the C type comet inclination vs. the aphelion (consider 63 | # only the bound orbits!) 64 | ax.scatter(C_TYPE_DF[C_TYPE_DF['ECCENTRICITY'] < 1]['APHELION_AU'], \ 65 | C_TYPE_DF[C_TYPE_DF['ECCENTRICITY'] < 1]['INCLINATION_DEG'], \ 66 | marker='^', color='tab:blue', alpha=0.5, label='C Type') 67 | 68 | # The aphelion data vary between a few AU and hundreds of AU. We convert the 69 | # x scale to a log10 scale 70 | ax.set_xscale('log') 71 | 72 | # Set a limit for the inclination; between 0 and 180 degrees 73 | ax.set_ylim(0, 180) 74 | 75 | # Set a grid for better readability 76 | ax.grid(axis='both', linestyle='dashed', alpha=0.2) 77 | 78 | # Set a title, and labels for the x and y axis 79 | ax.set_title('Comets with an eccentricity e<1') 80 | ax.set_xlabel('Aphelion in AU') 81 | ax.set_ylabel('Inclination in degrees') 82 | 83 | # Now we set a legend. However, the marker opacity in the legend has the 84 | # same value as in the plot. A value of 0.1 would be difficult to see ... 85 | leg = ax.legend(fancybox=True, loc='upper right', framealpha=1) 86 | 87 | # ... thus, we set the markers' opacity to 1 with this small code 88 | for lh in leg.legendHandles: 89 | lh.set_alpha(1) 90 | 91 | # Save the plot in high quality 92 | plt.savefig('comets_scatter_plot_Q_i.png', dpi=300) 93 | 94 | #%% 95 | 96 | # It appears that the aphelion and inclination values of the C comets are more 97 | # dispersed than the P comet values. P comets are more "concentrated" in the 98 | # inner part of the Solar System. Since we will have a closer look on P comets 99 | # another time, let's have a look at the inclination distribution of the 100 | # comets. 101 | # 102 | # "From which direction do they come from" (w.r.t. ECLIPJ2000)? 103 | 104 | # We analyse the complete inclination definition range. So let's set an array 105 | # that covers 0 to 180 degrees 106 | INCL_RANGE = np.linspace(0, 180, 1000) 107 | 108 | # Two plots will be created. First: a histogram. A rule-of-thumb is defined in 109 | # this lambda function: the floor value of the square-root of the total number 110 | # of observations is used 111 | nr_of_bins = lambda data_array: int(np.floor(np.sqrt(len(data_array)))) 112 | 113 | #%% 114 | 115 | # Second: To derive a continuous distribution a Kernel-Density Estimator (KDE) 116 | # is used. We apply the standard settings. Import the scipy module first 117 | from scipy import stats 118 | 119 | # Kernel and distribution computation for the P type comets 120 | P_TYPE_INC_KERNEL = stats.gaussian_kde(P_TYPE_DF['INCLINATION_DEG']) 121 | P_TYPE_INC_DISTR = P_TYPE_INC_KERNEL(INCL_RANGE) 122 | 123 | # Kernel and distribution computation for the C type comets 124 | C_TYPE_INC_KERNEL = stats.gaussian_kde(C_TYPE_DF['INCLINATION_DEG']) 125 | C_TYPE_INC_DISTR = C_TYPE_INC_KERNEL(INCL_RANGE) 126 | 127 | #%% 128 | 129 | # Create a figure and axis 130 | fig, ax = plt.subplots(figsize=(12, 8)) 131 | 132 | # Histogram of the P and C type comets' inclination. 133 | ax.hist(P_TYPE_DF['INCLINATION_DEG'], \ 134 | bins=nr_of_bins(P_TYPE_DF['INCLINATION_DEG']), \ 135 | density=True, color='tab:orange', alpha=0.5, label='P Type') 136 | 137 | ax.hist(C_TYPE_DF['INCLINATION_DEG'], \ 138 | bins=nr_of_bins(C_TYPE_DF['INCLINATION_DEG']), \ 139 | density=True, color='tab:blue', alpha=0.5, label='C Type') 140 | 141 | # Plot the KDE of the P type comets 142 | ax.plot(INCL_RANGE, P_TYPE_INC_DISTR, color='tab:orange', alpha=1, linestyle='solid') 143 | 144 | # Plot the KDE of the C type comets 145 | ax.plot(INCL_RANGE, C_TYPE_INC_DISTR, color='tab:blue', alpha=1, linestyle='solid') 146 | 147 | # Set an x axis limits (inclination range) 148 | ax.set_xlim(0, 180) 149 | 150 | # Add a grid for better readability 151 | ax.grid(axis='both', linestyle='dashed', alpha=0.2) 152 | 153 | # Set an x and y label 154 | ax.set_xlabel('Inclination in degrees') 155 | ax.set_ylabel('Normalised Distribution') 156 | 157 | # Again: We re-define the opacity (alpha value) of the markers / lines in the 158 | # legend for better visibility 159 | leg = ax.legend(fancybox=True, loc='upper right', framealpha=1) 160 | for lh in leg.legendHandles: 161 | lh.set_alpha(1) 162 | 163 | # Save the figure 164 | plt.savefig('comets_kde_incl_.png', dpi=300) 165 | -------------------------------------------------------------------------------- /part8/comets_kde_incl_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAlbin/SpaceScienceTutorial/9ca5c1340480a29a112dec91e075b7ee2eff38ed/part8/comets_kde_incl_.png -------------------------------------------------------------------------------- /part8/comets_scatter_plot_Q_i.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAlbin/SpaceScienceTutorial/9ca5c1340480a29a112dec91e075b7ee2eff38ed/part8/comets_scatter_plot_Q_i.png -------------------------------------------------------------------------------- /part9/SpaceSciencePython_part9.py: -------------------------------------------------------------------------------- 1 | # Import the standard modules 2 | import sqlite3 3 | import spiceypy 4 | 5 | # Import the installed modules 6 | import pandas as pd 7 | import numpy as np 8 | 9 | # Import matplotlib for plotting 10 | from matplotlib import pyplot as plt 11 | 12 | # Import scipy for the Kernel Density Estimator functionality 13 | from scipy import stats 14 | 15 | #%% 16 | 17 | # Connect to the comet database. This database has been created in tutorial 18 | # part 7, however, due to its small size the database is uploaded on GitHub 19 | con = sqlite3.connect('../_databases/_comets/mpc_comets.db') 20 | 21 | # Set a cursor 22 | cur = con.cursor() 23 | 24 | # Create a pandas dataframe that contains the name of the comet (needed later), 25 | # the semi-major axis, inclination and eccentricity 26 | # for P type ... 27 | P_TYPE_DF = pd.read_sql('SELECT NAME, SEMI_MAJOR_AXIS_AU, INCLINATION_DEG, ' \ 28 | 'ECCENTRICITY FROM comets_main WHERE ORBIT_TYPE="P"', \ 29 | con) 30 | 31 | # ... and C type comets. For this type: set the eccentricity smaller 1 (bound 32 | # orbits) 33 | C_TYPE_DF = pd.read_sql('SELECT NAME, SEMI_MAJOR_AXIS_AU, INCLINATION_DEG, ' \ 34 | 'ECCENTRICITY FROM comets_main WHERE ORBIT_TYPE="C" ' \ 35 | 'AND ECCENTRICITY<1', con) 36 | 37 | #%% 38 | 39 | # The Tisserand parameter will help us to distinguish between Jupiter Family 40 | # Comets (JFCs) and Non-JFCss more easily. For this parameter (next block) we 41 | # need the semi-major axis of Jupiter 42 | 43 | # Import a kernel meta file 44 | spiceypy.furnsh('kernel_meta.txt') 45 | 46 | # Set any Ephemeris time (ET) 47 | SAMPLE_ET = spiceypy.utc2et('2000-001T00:00:00') 48 | 49 | # Compute the state vector of Jupiter in ECLIPJ2000 (Jupiter (599) is not 50 | # available in the kernel, we use the barycentre (5)) 51 | STATE_VEC_JUPITER, _ = spiceypy.spkgeo(targ=5, \ 52 | et=SAMPLE_ET, \ 53 | ref='ECLIPJ2000', \ 54 | obs=10) 55 | 56 | # Get the G*M value of the Sun 57 | _, GM_SUN_PRE = spiceypy.bodvcd(bodyid=10, item='GM', maxn=1) 58 | GM_SUN = GM_SUN_PRE[0] 59 | 60 | # Compute the orbital elements of Jupiter 61 | ORB_ELEM_JUPITER = spiceypy.oscltx(STATE_VEC_JUPITER, SAMPLE_ET, GM_SUN) 62 | 63 | # Get the semi-major axis value 64 | A_JUPITER_KM = ORB_ELEM_JUPITER[-2] 65 | 66 | # Convert the value from km to AU 67 | A_JUPITER_AU = spiceypy.convrt(A_JUPITER_KM, 'km', 'AU') 68 | 69 | #%% 70 | 71 | # Define a lambda function for the Tisserand parameter, a, i and e are the 72 | # input parameters semi-major axis, inclination and eccentricity, respectively 73 | tisr_jup = lambda a, i, e: (A_JUPITER_AU / a) + 2 * np.cos(i) \ 74 | * np.sqrt((a / A_JUPITER_AU) * (1 - (e**2.0))) 75 | 76 | # Create a new dataframe columns that contains the Tisserand parameter 77 | P_TYPE_DF.loc[:, 'TISSERAND_JUP'] = \ 78 | P_TYPE_DF.apply(lambda x: (tisr_jup(a=x['SEMI_MAJOR_AXIS_AU'], \ 79 | i=np.radians(x['INCLINATION_DEG']), \ 80 | e=x['ECCENTRICITY'])), axis=1) 81 | 82 | C_TYPE_DF.loc[:, 'TISSERAND_JUP'] = \ 83 | C_TYPE_DF.apply(lambda x: (tisr_jup(a=x['SEMI_MAJOR_AXIS_AU'], \ 84 | i=np.radians(x['INCLINATION_DEG']), \ 85 | e=x['ECCENTRICITY'])), axis=1) 86 | 87 | #%% 88 | 89 | # Print some descriptive statistics of the P type comets 90 | print('Descriptive statistics of the Tisserand parameter of P type comets') 91 | print(f'{P_TYPE_DF["TISSERAND_JUP"].describe()}') 92 | print('\n') 93 | 94 | # Compute the percentage of Jupiter-Family Comets (JFCs) based on P types 95 | PERC_P_TYPE_JFCS = len(P_TYPE_DF.loc[(P_TYPE_DF["TISSERAND_JUP"] > 2) \ 96 | & (P_TYPE_DF["TISSERAND_JUP"] < 3)]) \ 97 | / len(P_TYPE_DF.index) * 100 98 | PERC_P_TYPE_JFCS = round(PERC_P_TYPE_JFCS, 0) 99 | 100 | # Print how many P comets have a Tisserand parameter between 2 and 3: 101 | print('Percentage of P type comets with a Tisserand parameter between ' \ 102 | f'2 and 3: {PERC_P_TYPE_JFCS}%') 103 | print('\n') 104 | 105 | # Print some descriptive statistics of the C type comets 106 | print('Descriptive statistics of the Tisserand parameter of C type comets') 107 | print(f'{C_TYPE_DF["TISSERAND_JUP"].describe()}') 108 | print('\n') 109 | 110 | #%% 111 | 112 | # We define a function to add a new column in an already existing database 113 | # table. This code snippet may be helpful in the future 114 | def add_col2tab(con_db, cur_db, tab_name, col_name, col_type): 115 | """ 116 | This function adds a new column to an already existing SQLite table. 117 | Setting a new or editing an existing key (primary or foreign) is not 118 | possible. 119 | 120 | Parameters 121 | ---------- 122 | con_db : sqlite3.Connection 123 | Connection object to the SQLite database. 124 | cur_db : sqlite3.Cursor 125 | Connection corresponding cursor. 126 | tab_name : str 127 | Table name. 128 | col_name : str 129 | New column name that shall be added. 130 | col_type : str 131 | New column name corresponding SQLite column type. 132 | 133 | Returns 134 | ------- 135 | None. 136 | 137 | """ 138 | 139 | # Iterate through all existing column names of the database table using 140 | # the PRAGMA table_info command 141 | for row in cur_db.execute(f'PRAGMA table_info({tab_name})'): 142 | 143 | # If the column exists: exit the function 144 | if row[1] == col_name: 145 | break 146 | 147 | # If the column is not existing yet, add the new column 148 | else: 149 | cur_db.execute(f'ALTER TABLE {tab_name} ' \ 150 | f'ADD COLUMN {col_name} {col_type}') 151 | con_db.commit() 152 | 153 | # Add a new column in the comets_main table for the Tisserand parameters 154 | add_col2tab(con_db=con, \ 155 | cur_db=cur, \ 156 | tab_name='comets_main', \ 157 | col_name='TISSERAND_JUP', \ 158 | col_type='REAL') 159 | 160 | #%% 161 | 162 | # Add the Tisserand parameter results to the database 163 | cur.executemany('UPDATE comets_main SET TISSERAND_JUP=? WHERE NAME=?', \ 164 | P_TYPE_DF[['TISSERAND_JUP', 'NAME']].values) 165 | con.commit() 166 | 167 | cur.executemany('UPDATE comets_main SET TISSERAND_JUP=? WHERE NAME=?', \ 168 | C_TYPE_DF[['TISSERAND_JUP', 'NAME']].values) 169 | con.commit() 170 | 171 | #%% 172 | 173 | # Compute the KDE distribution for the Tisserand values, ranging from -1 to 174 | # 5 175 | TISSERAND_RANGE = np.linspace(0, 5, 1000) 176 | 177 | # Kernel and distribution computation for the P type comets 178 | P_TYPE_TISR_KERNEL = stats.gaussian_kde(P_TYPE_DF['TISSERAND_JUP']) 179 | P_TYPE_TISR_DISTR = P_TYPE_TISR_KERNEL(TISSERAND_RANGE) 180 | 181 | # Kernel and distribution computation for the C type comets 182 | C_TYPE_TISR_KERNEL = stats.gaussian_kde(C_TYPE_DF['TISSERAND_JUP']) 183 | C_TYPE_TISR_DISTR = C_TYPE_TISR_KERNEL(TISSERAND_RANGE) 184 | 185 | #%% 186 | 187 | # Square-root choice for the histograms number of bins 188 | nr_of_bins = lambda data_array: int(np.floor(np.sqrt(len(data_array)))) 189 | 190 | # Let's set a dark background 191 | plt.style.use('dark_background') 192 | 193 | # Set a default font size for better readability 194 | plt.rcParams.update({'font.size': 14}) 195 | 196 | # Create a figure and axis 197 | fig, ax = plt.subplots(figsize=(12, 8)) 198 | 199 | # Histogram of the P and C type comets' Tisserand parameter. 200 | ax.hist(P_TYPE_DF['TISSERAND_JUP'], \ 201 | bins=nr_of_bins(P_TYPE_DF['TISSERAND_JUP']), \ 202 | density=True, color='tab:orange', alpha=0.5, label='P Type') 203 | 204 | ax.hist(C_TYPE_DF['TISSERAND_JUP'], \ 205 | bins=nr_of_bins(C_TYPE_DF['TISSERAND_JUP']), \ 206 | density=True, color='tab:blue', alpha=0.5, label='C Type') 207 | 208 | # Plot the KDE of the P type comets 209 | ax.plot(TISSERAND_RANGE, P_TYPE_TISR_DISTR, color='tab:orange', alpha=1, linestyle='solid') 210 | 211 | # Plot the KDE of the C type comets 212 | ax.plot(TISSERAND_RANGE, C_TYPE_TISR_DISTR, color='tab:blue', alpha=1, linestyle='solid') 213 | 214 | # Set an x axis limits 215 | ax.set_xlim(0, 5) 216 | 217 | # Add a grid for better readability 218 | ax.grid(axis='both', linestyle='dashed', alpha=0.2) 219 | 220 | # Set an x and y label 221 | ax.set_xlabel('Tisserand Parameter w.r.t. Jupiter') 222 | ax.set_ylabel('Normalised Distribution') 223 | 224 | # Re-define the opacity (alpha value) of the markers / lines in the 225 | # legend for better visibility 226 | leg = ax.legend(fancybox=True, loc='upper right', framealpha=1) 227 | for lh in leg.legendHandles: 228 | lh.set_alpha(1) 229 | 230 | # Save the figure 231 | plt.savefig('comets_kde_tisserand_jup.png', dpi=300) 232 | -------------------------------------------------------------------------------- /part9/comets_kde_tisserand_jup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAlbin/SpaceScienceTutorial/9ca5c1340480a29a112dec91e075b7ee2eff38ed/part9/comets_kde_tisserand_jup.png -------------------------------------------------------------------------------- /part9/kernel_meta.txt: -------------------------------------------------------------------------------- 1 | \begintext 2 | 3 | This meta file contains the relative paths to all needed SPICE kernels. 4 | For each tutorial we will set up an individual kernel_meta.txt. A common 5 | meta file could be easily stored in the main folder (next to the _kernel 6 | directory), however it would be over-loaded at some point. 7 | 8 | \begindata 9 | 10 | KERNELS_TO_LOAD = ( 11 | '../_kernels/pck/gm_de431.tpc', 12 | '../_kernels/spk/de432s.bsp', 13 | '../_kernels/lsk/naif0012.tls', 14 | ) 15 | --------------------------------------------------------------------------------