├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE.txt ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── config_generators ├── image_satellite │ ├── create_config.py │ ├── create_precompute.py │ ├── gs.txt │ └── imaging.tle └── iot │ ├── create_iot_config.py │ ├── create_random_iot.py │ ├── gs.txt │ ├── iot.txt │ └── sat.txt ├── configs ├── README.md ├── config.json ├── examples │ ├── config_1000iot.json │ └── config_imagesat.json └── testconfigs │ ├── config_testfovhelperlarge.json │ ├── config_testgenerator.json │ ├── config_testimaginglink.json │ ├── config_testisl.json │ ├── config_testlora.json │ ├── config_testloralink.json │ ├── config_testmaclayer.json │ ├── config_testmodelfovtime.json │ ├── config_testmodelhelperfov.json │ ├── config_testorchestrator.json │ └── config_testpower.json ├── dependencies └── de440s.bsp ├── environment.yml ├── examples ├── analytics_samples │ ├── analyze_datalayer.py │ ├── analyze_loraradios.py │ └── analyze_power.py ├── imagesatellite.py └── iotnetwork.py ├── figs ├── Class_diagram.pdf └── simulator_architecture.svg ├── main.py ├── pipelines.yml ├── requirements.txt └── src ├── __init__.py ├── analytics ├── README.md ├── smas │ ├── isma.py │ ├── smadatagenerator.py │ ├── smadatastore.py │ ├── smagenericradio.py │ ├── smaloraradiodevicerx.py │ ├── smaloraradiodevicetx.py │ ├── smapowerbasic.py │ └── smatimebasedfov.py └── summarizers │ ├── isummarizers.py │ ├── summarizerdatalayer.py │ ├── summarizerloraradiodevice.py │ ├── summarizermultiplepower.py │ └── summarizerpower.py ├── global_schedulers ├── hungarianscheduler.py └── iglobalscheduler.py ├── models ├── README.md ├── imodel.py ├── models_data │ ├── modeldatagenerator.py │ ├── modeldatarelay.py │ └── modeldatastore.py ├── models_fov │ ├── modelfovtimebased.py │ └── modelhelperfov.py ├── models_imaging │ └── modelimaginglogicbased.py ├── models_mac │ ├── modelmacgateway.py │ ├── modelmacgs.py │ ├── modelmaciot.py │ └── modelmacttnc.py ├── models_orbital │ ├── modelfixedorbit.py │ ├── modelorbit.py │ └── modelorbitonefullupdate.py ├── models_power │ └── modelpower.py ├── models_radio │ ├── modelaggregatorradio.py │ ├── modeldownlinkradio.py │ ├── modelgenericradio.py │ ├── modelimagingradio.py │ ├── modelisl.py │ └── modelloraradio.py ├── models_scheduling │ ├── modelcompute.py │ └── modeledgecompute.py ├── models_tumbling │ └── modeladacs.py └── network │ ├── address.py │ ├── channel.py │ ├── data │ ├── genericdata.py │ ├── image.py │ └── sensorappdata.py │ ├── frame.py │ ├── imaging │ ├── imagingchannel.py │ ├── imaginglink.py │ └── imagingradiodevice.py │ ├── isl │ ├── islchannel.py │ ├── isllink.py │ └── islradiodevice.py │ ├── link.py │ ├── lora │ ├── lorachannel.py │ ├── loraframe.py │ ├── loralink.py │ └── loraradiodevice.py │ ├── macdata │ ├── genericmac.py │ ├── macack.py │ ├── macbeacon.py │ ├── macbulkack.py │ ├── maccontrol.py │ └── macdata.py │ └── radiodevice.py ├── nodes ├── README.md ├── gsbasic.py ├── inode.py ├── iotbasic.py ├── itopology.py ├── satellitebasic.py └── topology.py ├── sim ├── README.md ├── imanager.py ├── loggerinits.py ├── managerparallel.py ├── modelinits.py ├── nodeinits.py ├── orchestrator.py └── simulator.py ├── simlogging ├── ilogger.py ├── loggercmd.py ├── loggerfile.py └── loggerfilechunkwise.py ├── test ├── README.md ├── __init__.py ├── test_datageneration.py ├── test_imaginglink.py ├── test_imagingradiomodel.py ├── test_isl.py ├── test_location.py ├── test_loggerfile.py ├── test_loggerfilechunkwise.py ├── test_lora.py ├── test_loralink.py ├── test_maclayer.py ├── test_modelhelpfov.py ├── test_orchestrator.py ├── test_power.py ├── test_satellitebasic.py ├── test_sma.py └── test_timefov.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | test-output.xml 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/#use-with-ide 111 | .pdm.toml 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | # PyCharm 157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 159 | # and can be added to the global gitignore or merged into this file. For a more nuclear 160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 161 | #.idea/ 162 | 163 | # Ignore visual studio code files 164 | .vscode/* 165 | !.vscode/settings.json 166 | !.vscode/tasks.json 167 | !.vscode/launch.json 168 | !.vscode/extensions.json 169 | !.vscode/*.code-snippets 170 | 171 | # Local History for Visual Studio Code 172 | .history/ 173 | 174 | # Built Visual Studio Code Extensions 175 | *.vsix -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to 4 | agree to a Contributor License Agreement (CLA) declaring that you have the right to, 5 | and actually do, grant us the rights to use your contribution. For details, visit 6 | https://cla.microsoft.com. 7 | 8 | When you submit a pull request, a CLA-bot will automatically determine whether you need 9 | to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repositories using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 13 | or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 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 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | NOTICES AND INFORMATION 2 | Do Not Translate or Localize 3 | 4 | This software incorporates material from third parties. 5 | Microsoft makes certain open source code available at https://3rdpartysource.microsoft.com, 6 | or you may send a check or money order for US $5.00, including the product name, 7 | the open source component name, platform, and version number, to: 8 | 9 | Source Code Compliance Team 10 | Microsoft Corporation 11 | One Microsoft Way 12 | Redmond, WA 98052 13 | USA 14 | 15 | Notwithstanding any other terms, you may reverse engineer this software to the extent 16 | required to debug changes to any libraries licensed under the GNU Lesser General Public License. 17 | 18 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | 2 | # Support 3 | 4 | ## How to file issues and get help 5 | 6 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 7 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 8 | feature request as a new Issue. 9 | 10 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 11 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 12 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 13 | 14 | ## Microsoft Support Policy 15 | 16 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 17 | -------------------------------------------------------------------------------- /config_generators/image_satellite/create_precompute.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a quick script to generate the configuration file for the IoT application 3 | Usage: python3 create_config.py tle_file gs_file start_time end_time delta output_file 4 | I assume a 3LE file. 5 | I assume a GS file with lat, long. 6 | I assume start_time and end_time are YYYY-MM-DD HH:MM:SS 7 | """ 8 | 9 | #The tles can be found at: The tles can be found from: 10 | # https://celestrak.org/NORAD/elements/gp.php?GROUP=planet&FORMAT=tle 11 | #The current gs locations are approximated from: 12 | # Kiruthika Devaraj, Ryan Kingsbury, Matt Ligon, Joseph Breu, Vivek 13 | # Vittaldev, Bryan Klofas, Patrick Yeon, and Kyle Colton. Dove High 14 | # Speed Downlink System. In Small Satellite Conference, 2017. 15 | #and also: 16 | # Transmitting, Fast and Slow: Scheduling Satellite Traffic through Space and Time 17 | # Bill Tao, Maleeha Masood, Indranil Gupta, Deepak Vasisht 18 | # MobiCom 2023 19 | 20 | #The power numbers are all converted numbers from: 21 | # Orbital Edge Computing: Nanosatellite Constellations as a New Class of Computing System 22 | # Bradley Denby and Brandon Lucia 23 | # ASPLOS 2020 24 | 25 | #The radio numbers are all from: 26 | # Kiruthika Devaraj, Matt Ligon, Eric Blossom, Joseph Breu, Bryan Klofas, 27 | # Kyle Colton, and Ryan Kingsbury. Planet High Speed Radio: 28 | # Crossing Gbps from a 3U Cubesat. In Small Satellite Conference, 2019. 29 | 30 | 31 | import os 32 | import sys 33 | 34 | def get_satellite_string(node_id, tle_line_1, tle_line_2): 35 | string = """ 36 | { 37 | "type": "SAT", 38 | "iname": "SatelliteBasic", 39 | "nodeid": %d, 40 | "loglevel": "info", 41 | "tle_1": "%s", 42 | "tle_2": "%s", 43 | "additionalargs": "", 44 | "models":[ 45 | { 46 | "iname": "ModelOrbit" 47 | }, 48 | { 49 | "iname": "ModelFovTimeBased", 50 | "min_elevation": 5 51 | }, 52 | { 53 | "iname": "ModelImagingRadio", 54 | "self_ctrl": true, 55 | "radio_physetup":{ 56 | "_frequency": 8.09e9, 57 | "_bandwidth": 96e6, 58 | "_tx_power": 8.2, 59 | "_tx_antenna_gain": 15, 60 | "_tx_line_loss": 2, 61 | "_rx_antenna_gain": 49, 62 | "_rx_line_loss": 1.8, 63 | "_gain_to_temperature": 29, 64 | "_symbol_rate": 76.8e6, 65 | "_num_channels": 6 66 | } 67 | } 68 | ] 69 | }""" % (node_id, tle_line_1, tle_line_2) 70 | 71 | return string 72 | 73 | def get_groundstation_string(node_id, gs_lat, gs_lon): 74 | string = """ 75 | { 76 | "type": "GS", 77 | "iname": "GSBasic", 78 | "nodeid": %d, 79 | "loglevel": "info", 80 | "latitude": %f, 81 | "longitude": %f, 82 | "elevation": 0.0, 83 | "additionalargs": "", 84 | "models":[ 85 | { 86 | "iname": "ModelFovTimeBased", 87 | "min_elevation": 5 88 | }, 89 | { 90 | "iname": "ModelImagingRadio", 91 | "self_ctrl": false, 92 | "radio_physetup":{ 93 | "_frequency": 8.09e9, 94 | "_bandwidth": 96e6, 95 | "_tx_power": 8.2, 96 | "_tx_antenna_gain": 15, 97 | "_tx_line_loss": 2, 98 | "_rx_antenna_gain": 49, 99 | "_rx_line_loss": 1.8, 100 | "_gain_to_temperature": 29 101 | } 102 | } 103 | ] 104 | }""" % (node_id, gs_lat, gs_lon) 105 | return string 106 | 107 | 108 | if __name__ == "__main__": 109 | ##Usage: python3 create_iot_config.py tle_file gs_file start_time end_time delta output_file 110 | tle_file = sys.argv[1] 111 | gs_file = sys.argv[2] 112 | start_time = sys.argv[3] 113 | end_time = sys.argv[4] 114 | delta = sys.argv[5] 115 | 116 | output_file = open(sys.argv[6], "w+") 117 | 118 | base_str = """ 119 | { 120 | "topologies": 121 | [ 122 | { 123 | "name": "ImagingSatConstellation", 124 | "id": 0, 125 | "nodes": 126 | [ 127 | """ 128 | output_file.write(base_str) 129 | 130 | #add tle nodes 131 | node_id = 0 132 | 133 | with open(tle_file, "r") as f: 134 | lines = f.readlines() 135 | for i in range(0, len(lines), 3): 136 | line = lines[i:i+3] 137 | node_id += 1 138 | tle_line_1 = line[1][:-1] 139 | tle_line_2 = line[2][:-1] #Ignore the newlines 140 | output_file.write(get_satellite_string(node_id, tle_line_1, tle_line_2)) 141 | output_file.write(",\n") 142 | node_id += 1 143 | 144 | #add groundstations 145 | 146 | with open(gs_file, "r") as f: 147 | for line in f: 148 | gs_lat = float(line.split(",")[0]) 149 | gs_lon = float(line.split(",")[1]) 150 | output_file.write(get_groundstation_string(node_id, gs_lat, gs_lon)) 151 | output_file.write(",\n") 152 | 153 | node_id += 1 154 | 155 | #remove last comma 156 | output_file.seek(output_file.tell() - 2, os.SEEK_SET) 157 | 158 | 159 | #add end of file 160 | end_str = """ 161 | ] 162 | } 163 | ], 164 | "simtime": 165 | { 166 | "starttime": "%s", 167 | "endtime": "%s", 168 | "delta": %s 169 | }, 170 | "simlogsetup": 171 | { 172 | "loghandler": "LoggerFileChunkwise", 173 | "logfolder": "imagingLogs", 174 | "logchunksize": 1000000 175 | } 176 | } 177 | """ % (start_time, end_time, delta) 178 | 179 | output_file.write(end_str) 180 | output_file.close() -------------------------------------------------------------------------------- /config_generators/image_satellite/gs.txt: -------------------------------------------------------------------------------- 1 | 50.3,-4.9 2 | 50.0,5.6 3 | 65.1,-22.1 4 | -26.0,152.6 5 | -46.3,168.0 6 | 62,-150.5 7 | 19.6,-155.8 8 | 48.1,-121.3 9 | 32.4,-107.6 10 | 40.7,-83.0 11 | 47.6,-97.7 12 | 47.5,-95.6 -------------------------------------------------------------------------------- /config_generators/iot/create_random_iot.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | ''' 5 | #Usage: python create_random_iot.py numIot output_file 6 | import random 7 | import csv 8 | import sys 9 | def generate_random_lat_lon(num_points): 10 | lat_lon_values = [] 11 | for _ in range(num_points): 12 | # Generate random latitude between -90 and 90 degrees 13 | latitude = random.uniform(-90, 90) 14 | 15 | # Generate random longitude between -180 and 180 degrees 16 | longitude = random.uniform(-180, 180) 17 | 18 | # Add the value 10220 to each pair 19 | lat_lon_values.append((latitude, longitude, 10, 220)) 20 | return lat_lon_values 21 | # Number of random lat-long values to generate 22 | num_points = int(sys.argv[1]) 23 | 24 | generated_values = generate_random_lat_lon(num_points) 25 | 26 | f = open(sys.argv[2], 'w') 27 | with f: 28 | writer = csv.writer(f) 29 | for row in generated_values: 30 | writer.writerow(row) -------------------------------------------------------------------------------- /config_generators/iot/gs.txt: -------------------------------------------------------------------------------- 1 | 70.2125, -148.4081 2 | 40.479889, -111.822889 3 | 34.6485, -83.547333 4 | -14.3172, -170.7644 5 | 17.9774, -67.1365 6 | 65.117516, -147.432431 7 | 37.401, -122.057222 8 | 41.385472, -74.527194 9 | 48.147, -119.696 10 | 13.415617, 144.687867 11 | 20.900528, -156.504611 12 | -77.8500, -166.6667 13 | 38.12083, -25.57935 -------------------------------------------------------------------------------- /configs/README.md: -------------------------------------------------------------------------------- 1 | # Configure 2 | The configuration for the simulation setup is supplied in JSON format. Several sample config files can be found in the [configs](/configs/) folder. In this section, we will explain the process of creating and customizing a config file. 3 | 4 | ## Simulation time ("simtime") 5 | As stated in our primary [README](/README.md), this simulator operates on a discrete-time basis. Hence, while setting up the simulation, it is necessary to specify the start time, end time, and epoch interval. These details can be entered within the `"simtime"` JSON object. For proper formatting, please use the `"yyyy-mm-dd hh:mm:ss"` format for both the `"starttime"` and `"endtime"`. The `"delta"` field represents the epoch interval, measured in seconds. 6 | 7 | ## Logging setup 8 | The `"simlogsetup"` object contains the logging setup for the simulator. Within this setup, we offer various log handlers, including command prompt-based, file-based, and others (find all the log handlers [here](/src/simlogging/)). You can select the desired log handler by setting the `"loghandler"` value to the class name of the corresponding log handler. Some log handlers may require additional configuration arguments from the user, such as the path to the log dumping directory. To provide these arguments, include them in the `"simlogsetup"` section. To understand the required arguments for a specific log handler, please refer to the corresponding log handler implementation module. 9 | 10 | ## Topologies (`"topologies"`) 11 | To include one or more topologies in your simulation setup, use the `"topologies"` array. Each topology should have a distinct name and unique ID. Within each topology, you can define an array of nodes. For additional information about working with topologies, please refer to [this](/src/nodes/README.md) resource. 12 | 13 | ### Nodes (`"nodes"`) 14 | In the simulator, a node represents a physical entity, such as a satellite, ground station, user terminal, IoT device, and more. Node configurations are presented as an array inside `"topologies"`. 15 | 16 | Each node object consists of four mandatory properties: 17 | 1. Unique ID of the node, `"nodeid"`. 18 | 2. Type of node (`"type"`), denoted by abbreviations like SAT for satellite and GS for ground station. This aids in understanding generated logs for each node. 19 | 3. Logging level (`"loglevel"`), which supports different types of log messages, including "error", "warn", "debug", "info", "logic", and "all". You can set any of these values for `"loglevel"`. 20 | 4. Implementation name (`"iname"`) that corresponds to the desired node class. Multiple [implementations of node classes](/src/nodes/) are available. Simply use the name of your desired node class as the value of `"iname"`. 21 | 22 | Note that the chosen node class may require additional configuration properties, which can be provided in the same JSON object. To learn about the required config properties, please refer to the corresponding node implementation module or [here](/src/nodes/README.md). 23 | 24 | As previously mentioned, a node may encompass one or multiple models. The configuration of models for a node is provided within the corresponding node object as an array. 25 | 26 | ### Models (`"models"`) 27 | Each JSON object for model configuration must include the `"iname"` property, which determines the desired implementation of the model class that the user wants to use in the node. Multiple [implementations of model classes](/src/models/) are at your disposal. 28 | 29 | Additionally, certain model classes may necessitate additional configuration inputs, which we include as properties in the same JSON object. To learn about the specific configuration inputs required for a particular model class, please refer to the corresponding model implementation module or [here](/src/models/README.md). 30 | 31 | A skeleton config file looks as following. 32 | 33 | ```JSON 34 | { 35 | "topologies": 36 | [ 37 | // here goes the list of topology config objects 38 | { 39 | // config for the topology: 1 40 | 41 | "nodes": 42 | [ 43 | // here goes the list of config objects for nodes in topology: 1 44 | { 45 | // config for the node: 1 46 | 47 | "models": 48 | [ 49 | // here goes the list of config objects for models in node: 1 50 | { 51 | // config for model 1 52 | }, 53 | { 54 | // config for model 2 55 | } 56 | ] 57 | }, 58 | { 59 | // config for the node:2 60 | } 61 | ] 62 | 63 | }, 64 | { 65 | // config for the topology: 2 66 | } 67 | ] 68 | } 69 | ``` 70 | 71 | # Config file generator 72 | Creating a config file for a topology that includes hundreds of nodes can be a cumbersome task. To simplify this process, we offer config file generator scripts [here](/config_generators/). Utilizing these scripts, you can easily generate a config file for any desired number of nodes. 73 | 74 | To use the scripts, you only need to provide two files: 75 | 1. A file containing the TLEs (Two-Line Elements) of the satellites. 76 | 2. A file containing the locations of ground stations or IoT devices (if applicable). 77 | 78 | In the script, you are required to provide the standard config for a satellite node, ground station, and IoT device (if present). The script will then generate the config for all your satellites, ground stations, and IoT devices, following the provided standard config. 79 | -------------------------------------------------------------------------------- /configs/testconfigs/config_testgenerator.json: -------------------------------------------------------------------------------- 1 | { 2 | "topologies": 3 | [ 4 | { 5 | "name": "Constln1", 6 | "id": 0, 7 | "nodes": 8 | [ 9 | { 10 | "type": "IoT", 11 | "iname": "IoTBasic", 12 | "nodeid": 1, 13 | "loglevel": "info", 14 | "additionalargs": "", 15 | "latitude": 0.1, 16 | "longitude": 0.9, 17 | "elevation": 0.0, 18 | "models": [ 19 | { 20 | "iname": "ModelDataGenerator", 21 | "data_poisson_lambda": 5, 22 | "data_size": 2048, 23 | "queue_size": 3000 24 | } 25 | ] 26 | 27 | } 28 | ] 29 | } 30 | ], 31 | "simtime": 32 | { 33 | "starttime": "2022-11-14 12:00:00", 34 | "endtime": "2022-11-14 12:01:00", 35 | "delta": 1 36 | }, 37 | "simlogsetup": 38 | { 39 | "loghandler": "LoggerCmd", 40 | "logfolder": "" 41 | } 42 | } -------------------------------------------------------------------------------- /configs/testconfigs/config_testimaginglink.json: -------------------------------------------------------------------------------- 1 | { 2 | "topologies": 3 | [ 4 | { 5 | "name": "ImagingSatConstellation", 6 | "id": 0, 7 | "nodes": 8 | [ 9 | { 10 | "type": "SAT", 11 | "iname": "SatelliteBasic", 12 | "nodeid": 0, 13 | "loglevel": "info", 14 | "tle_1": "1 52750U 22057U 23097.30444441 .00093670 00000+0 27574-2 0 9991", 15 | "tle_2": "2 52750 97.5350 216.3378 0010863 230.3166 129.7119 15.34639252 48067", 16 | "starttime": "2023-04-07 17:29:00", 17 | "endtime": "2023-04-07 17:40:00", 18 | "additionalargs": "", 19 | "models":[ 20 | { 21 | "iname": "ModelFixedOrbit", 22 | "lat": 0.0, 23 | "lon": -0.0, 24 | "alt": 1407e3, 25 | "sunlit": true 26 | }, 27 | { 28 | "iname": "ModelHelperFoV", 29 | "min_elevation": 0 30 | }, 31 | { 32 | "iname": "ModelImagingRadio", 33 | "self_ctrl": false, 34 | "radio_physetup":{ 35 | "_frequency": 8.09e9, 36 | "_symbol_rate": 76.8e6, 37 | "_bandwidth": 96e6, 38 | "_tx_power": -4.8, 39 | "_tx_antenna_gain": 15, 40 | "_tx_line_loss": 2, 41 | "_rx_antenna_gain": 49, 42 | "_rx_line_loss": 1.8, 43 | "_gain_to_temperature": 29, 44 | "_num_channels": 6, 45 | "_atmosphere_loss": 1.5 46 | } 47 | } 48 | ] 49 | 50 | }, 51 | { 52 | "type": "SAT", 53 | "iname": "SatelliteBasic", 54 | "nodeid": 1, 55 | "loglevel": "info", 56 | "tle_1": "1 52750U 22057U 23097.30444441 .00093670 00000+0 27574-2 0 9991", 57 | "tle_2": "2 52750 97.5350 216.3378 0010863 230.3166 129.7119 15.34639252 48067", 58 | "starttime": "2023-04-07 17:29:00", 59 | "endtime": "2023-04-07 17:40:00", 60 | "additionalargs": "", 61 | "models":[ 62 | { 63 | "iname": "ModelFixedOrbit", 64 | "lat": 0.0, 65 | "lon": -0.0, 66 | "alt": 1408e3, 67 | "sunlit": true 68 | }, 69 | { 70 | "iname": "ModelHelperFoV", 71 | "min_elevation": 0 72 | }, 73 | { 74 | "iname": "ModelImagingRadio", 75 | "self_ctrl": false, 76 | "radio_physetup":{ 77 | "_frequency": 8.09e9, 78 | "_symbol_rate": 76.8e6, 79 | "_bandwidth": 96e6, 80 | "_tx_power": -4.8, 81 | "_tx_antenna_gain": 15, 82 | "_tx_line_loss": 2, 83 | "_rx_antenna_gain": 49, 84 | "_rx_line_loss": 1.8, 85 | "_gain_to_temperature": 29, 86 | "_num_channels": 6, 87 | "_atmosphere_loss": 1.5 88 | } 89 | } 90 | ] 91 | 92 | }, 93 | { 94 | 95 | "type": "GS", 96 | "iname": "GSBasic", 97 | "nodeid": 2, 98 | "loglevel": "info", 99 | "latitude": 0.0, 100 | "longitude": -0.0, 101 | "elevation": 0.0, 102 | "additionalargs": "", 103 | "models":[ 104 | { 105 | "iname": "ModelHelperFoV", 106 | "min_elevation": 0 107 | }, 108 | { 109 | "iname": "ModelImagingRadio", 110 | "self_ctrl": false, 111 | "radio_physetup":{ 112 | "_frequency": 8.09e9, 113 | "_bandwidth": 96e6, 114 | "_symbol_rate": 76.8e6, 115 | "_tx_power": -4.8, 116 | "_tx_antenna_gain": 15, 117 | "_tx_line_loss": 2, 118 | "_rx_antenna_gain": 49, 119 | "_rx_line_loss": 1.8, 120 | "_gain_to_temperature": 29, 121 | "_num_channels": 6 122 | } 123 | }, 124 | { 125 | "iname": "ModelDataStore", 126 | "queue_size": 1 127 | } 128 | ] 129 | } 130 | ] 131 | } 132 | ], 133 | "simtime": 134 | { 135 | "starttime": "2023-04-07 18:29:00", 136 | "endtime": "2023-04-07 18:40:00", 137 | "delta": 1 138 | }, 139 | "simlogsetup": 140 | { 141 | "loghandler": "LoggerCmd" 142 | } 143 | } -------------------------------------------------------------------------------- /configs/testconfigs/config_testisl.json: -------------------------------------------------------------------------------- 1 | { 2 | "topologies": 3 | [ 4 | { 5 | "name": "ImagingSatConstellation", 6 | "id": 0, 7 | "nodes": 8 | [ 9 | { 10 | "type": "SAT", 11 | "iname": "SatelliteBasic", 12 | "nodeid": 1, 13 | "loglevel": "info", 14 | "tle_1": "1 52750U 22057U 23097.30444441 .00093670 00000+0 27574-2 0 9991", 15 | "tle_2": "2 52750 97.5350 216.3378 0010863 230.3166 129.7119 15.34639252 48067", 16 | "additionalargs": "", 17 | "models":[ 18 | { 19 | "iname": "ModelOrbit" 20 | }, 21 | { 22 | "iname": "ModelISL", 23 | "connected_nodeIDs": [2,3], 24 | "radio_physetup": { 25 | "datarate": 1e6, 26 | "MTU": 1500, 27 | "BER": 1e-6, 28 | "_bits_allowed": 2 29 | } 30 | } 31 | ] 32 | }, 33 | { 34 | "type": "SAT", 35 | "iname": "SatelliteBasic", 36 | "nodeid": 2, 37 | "loglevel": "info", 38 | "tle_1": "1 52750U 22057U 23097.30444441 .00093670 00000+0 27574-2 0 9991", 39 | "tle_2": "2 52750 97.5350 216.3378 0010863 230.3166 129.7119 15.34639252 48067", 40 | "additionalargs": "", 41 | "models":[ 42 | { 43 | "iname": "ModelOrbit" 44 | }, 45 | { 46 | "iname": "ModelISL", 47 | "connected_nodeIDs": [1,3], 48 | "radio_physetup": { 49 | "datarate": 1e6, 50 | "MTU": 1500, 51 | "BER": 1e-6, 52 | "_bits_allowed": 2 53 | } 54 | } 55 | ] 56 | }, 57 | { 58 | "type": "SAT", 59 | "iname": "SatelliteBasic", 60 | "nodeid": 3, 61 | "loglevel": "info", 62 | "tle_1": "1 52750U 22057U 23097.30444441 .00093670 00000+0 27574-2 0 9991", 63 | "tle_2": "2 52750 97.5350 216.3378 0010863 230.3166 129.7119 15.34639252 48067", 64 | "additionalargs": "", 65 | "models":[ 66 | { 67 | "iname": "ModelOrbit" 68 | }, 69 | { 70 | "iname": "ModelISL", 71 | "connected_nodeIDs": [1,2], 72 | "radio_physetup": { 73 | "datarate": 1e6, 74 | "MTU": 1500, 75 | "BER": 1e-6, 76 | "_bits_allowed": 2 77 | } 78 | } 79 | ] 80 | } 81 | ] 82 | } 83 | ], 84 | "simtime": 85 | { 86 | "starttime": "2023-04-07 18:30:00", 87 | "endtime": "2023-04-07 18:40:00", 88 | "delta": 60 89 | }, 90 | "simlogsetup": 91 | { 92 | "loghandler": "LoggerCmd", 93 | "logfolder": "" 94 | } 95 | } -------------------------------------------------------------------------------- /configs/testconfigs/config_testlora.json: -------------------------------------------------------------------------------- 1 | { 2 | "topologies": 3 | [ 4 | { 5 | "name": "Constln1", 6 | "id": 0, 7 | "nodes": 8 | [ 9 | { 10 | "type": "SAT", 11 | "iname": "SatelliteBasic", 12 | "nodeid": 1, 13 | "loglevel": "info", 14 | "tle_1": "1 50985U 22002B 22290.71715197 .00032099 00000+0 13424-2 0 9994", 15 | "tle_2": "2 50985 97.4784 357.5505 0011839 353.6613 6.4472 15.23462773 42039", 16 | "additionalargs": "", 17 | "models":[ 18 | { 19 | "iname": "ModelOrbitOneFullUpdate" 20 | }, 21 | { 22 | "iname": "ModelHelperFoV", 23 | "min_elevation": 0 24 | }, 25 | { 26 | "iname": "ModelLoraRadio", 27 | "self_ctrl": true, 28 | "radio_physetup":{ 29 | "_frequency": 401.7e6, 30 | "_bandwidth": 125e3, 31 | "_sf": 11, 32 | "_coding_rate": 5, 33 | "_preamble": 8, 34 | "_tx_power": 22, 35 | "_tx_antenna_gain": 0, 36 | "_tx_line_loss": 1, 37 | "_rx_antenna_gain": 0, 38 | "_rx_line_loss": 0.1, 39 | "_gain_to_temperature": -30.1, 40 | "_snr_offset": -20, 41 | "_bits_allowed": 2 42 | } 43 | } 44 | ] 45 | 46 | }, 47 | { 48 | 49 | "type": "GS", 50 | "iname": "GSBasic", 51 | "nodeid": 2, 52 | "loglevel": "info", 53 | "latitude": 82.1, 54 | "longitude": 81.8, 55 | "elevation": 0.0, 56 | "additionalargs": "", 57 | "models":[ 58 | { 59 | "iname": "ModelHelperFoV", 60 | "min_elevation": 0 61 | }, 62 | { 63 | "iname": "ModelLoraRadio", 64 | "self_ctrl": true, 65 | "radio_physetup":{ 66 | "_frequency": 401.7e6, 67 | "_bandwidth": 125e3, 68 | "_sf": 11, 69 | "_coding_rate": 5, 70 | "_preamble": 8, 71 | "_tx_antenna_gain": 12, 72 | "_tx_power": 22, 73 | "_tx_line_loss": 1, 74 | "_rx_antenna_gain": 12, 75 | "_rx_line_loss": 1, 76 | "_gain_to_temperature": -15.2, 77 | "_snr_offset": -20, 78 | "_bits_allowed": 2 79 | } 80 | } 81 | 82 | ] 83 | }, 84 | { 85 | 86 | "type": "GS", 87 | "iname": "GSBasic", 88 | "nodeid": 3, 89 | "loglevel": "info", 90 | "latitude": 83.1, 91 | "longitude": 81.8, 92 | "elevation": 0.0, 93 | "additionalargs": "", 94 | "models":[ 95 | { 96 | "iname": "ModelHelperFoV", 97 | "min_elevation": 0 98 | }, 99 | { 100 | "iname": "ModelLoraRadio", 101 | "self_ctrl": true, 102 | "radio_physetup":{ 103 | "_frequency": 401.7e6, 104 | "_bandwidth": 125e3, 105 | "_sf": 11, 106 | "_coding_rate": 5, 107 | "_preamble": 8, 108 | "_tx_antenna_gain": 12, 109 | "_tx_power": 22, 110 | "_tx_line_loss": 1, 111 | "_rx_antenna_gain": 12, 112 | "_rx_line_loss": 1, 113 | "_gain_to_temperature": -15.2, 114 | "_snr_offset": -20, 115 | "_bits_allowed": 2 116 | } 117 | } 118 | ] 119 | } 120 | ] 121 | } 122 | ], 123 | "simtime": 124 | { 125 | "starttime": "2022-11-14 12:00:00", 126 | "endtime": "2022-11-14 12:03:00", 127 | "delta": 1 128 | }, 129 | "simlogsetup": 130 | { 131 | "loghandler": "LoggerCmd", 132 | "logfolder": "" 133 | } 134 | } -------------------------------------------------------------------------------- /configs/testconfigs/config_testloralink.json: -------------------------------------------------------------------------------- 1 | { 2 | "topologies": 3 | [ 4 | { 5 | "name": "LORACONSTELLATION", 6 | "id": 0, 7 | "nodes": 8 | [ 9 | { 10 | "type": "SAT", 11 | "iname": "SatelliteBasic", 12 | "nodeid": 1, 13 | "loglevel": "info", 14 | "tle_1": "1 52750U 22057U 23097.30444441 .00093670 00000+0 27574-2 0 9991", 15 | "tle_2": "2 52750 97.5350 216.3378 0010863 230.3166 129.7119 15.34639252 48067", 16 | "additionalargs": "", 17 | "models":[ 18 | { 19 | "iname": "ModelFixedOrbit", 20 | "lat": 0.0, 21 | "lon": -0.0, 22 | "alt": 637e3, 23 | "sunlit": true 24 | }, 25 | { 26 | "iname": "ModelHelperFoV", 27 | "min_elevation": 0 28 | }, 29 | { 30 | "iname": "ModelLoraRadio", 31 | "self_ctrl": false, 32 | "radio_physetup":{ 33 | "_frequency": 0.138e9, 34 | "_bandwidth": 30e3, 35 | "_sf": 11, 36 | "_coding_rate": 5, 37 | "_preamble": 8, 38 | "_tx_power": 1.76, 39 | "_tx_antenna_gain": 2.18, 40 | "_tx_line_loss": 1, 41 | "_rx_antenna_gain": -2.18, 42 | "_rx_line_loss": 1, 43 | "_gain_to_temperature": -30.1, 44 | "_bits_allowed": 2 45 | } 46 | } 47 | ] 48 | 49 | }, 50 | { 51 | 52 | "type": "GS", 53 | "iname": "GSBasic", 54 | "nodeid": 2, 55 | "loglevel": "info", 56 | "latitude": 0.0, 57 | "longitude": -0.0, 58 | "elevation": 0.0, 59 | "additionalargs": "", 60 | "models":[ 61 | { 62 | "iname": "ModelHelperFoV", 63 | "min_elevation": 0 64 | }, 65 | { 66 | "iname": "ModelLoraRadio", 67 | "self_ctrl": false, 68 | "radio_physetup":{ 69 | "_frequency": 0.149e9, 70 | "_bandwidth": 30e3, 71 | "_sf": 11, 72 | "_coding_rate": 5, 73 | "_preamble": 8, 74 | "_tx_power": 1.76, 75 | "_tx_antenna_gain": 2.84, 76 | "_tx_line_loss": 1, 77 | "_rx_antenna_gain": -3.49, 78 | "_rx_line_loss": 1, 79 | "_gain_to_temperature": -30.1, 80 | "_bits_allowed": 2 81 | } 82 | }, 83 | { 84 | "iname": "ModelDataStore", 85 | "queue_size": 1 86 | } 87 | ] 88 | } 89 | ] 90 | } 91 | ], 92 | "simtime": 93 | { 94 | "starttime": "2023-04-07 18:29:00", 95 | "endtime": "2023-04-07 18:40:00", 96 | "delta": 1 97 | }, 98 | "simlogsetup": 99 | { 100 | "loghandler": "LoggerCmd" 101 | } 102 | } -------------------------------------------------------------------------------- /configs/testconfigs/config_testorchestrator.json: -------------------------------------------------------------------------------- 1 | { 2 | "topologies": 3 | [ 4 | { 5 | "name": "Constln1", 6 | "id": 0, 7 | "nodes": 8 | [ 9 | { 10 | "type": "SAT", 11 | "iname": "SatelliteBasic", 12 | "nodeid": 1, 13 | "loglevel": "all", 14 | "tle_1": "1 50985U 22002B 22290.71715197 .00032099 00000+0 13424-2 0 9994", 15 | "tle_2": "2 50985 97.4784 357.5505 0011839 353.6613 6.4472 15.23462773 42039", 16 | "additionalargs": "", 17 | "models":[ 18 | { 19 | "iname": "ModelOrbit" 20 | }, 21 | { 22 | "iname": "ModelHelperFoV", 23 | "min_elevation": 10 24 | } 25 | ] 26 | 27 | }, 28 | { 29 | 30 | "type": "SAT", 31 | "iname": "SatelliteBasic", 32 | "nodeid": 2, 33 | "loglevel": "all", 34 | "tle_1": "1 52750U 22057U 22307.12792818 .00032886 00000+0 16217-2 0 9992", 35 | "tle_2": "2 52750 97.5321 60.4918 0010549 43.5060 316.7004 15.17803350 24430", 36 | "additionalargs": "", 37 | "models":[ 38 | { 39 | "iname": "ModelOrbit" 40 | 41 | }, 42 | { 43 | "iname": "ModelHelperFoV", 44 | "min_elevation": 10 45 | } 46 | ] 47 | }, 48 | { 49 | 50 | "type": "GS", 51 | "iname": "GSBasic", 52 | "nodeid": 3, 53 | "loglevel": "all", 54 | "latitude": 49.3, 55 | "longitude": -122.2, 56 | "elevation": 0.0, 57 | "additionalargs": "", 58 | "models":[ 59 | { 60 | "iname": "ModelHelperFoV", 61 | "min_elevation": 10 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | ], 68 | "simtime": 69 | { 70 | "starttime": "2022-11-14 12:00:00", 71 | "endtime": "2022-11-14 12:10:00", 72 | "delta": 5.0 73 | }, 74 | "simlogsetup": 75 | { 76 | "loghandler": "LoggerCmd", 77 | "logfolder": "" 78 | } 79 | } -------------------------------------------------------------------------------- /configs/testconfigs/config_testpower.json: -------------------------------------------------------------------------------- 1 | { 2 | "topologies": 3 | [ 4 | { 5 | "name": "Constln1", 6 | "id": 0, 7 | "nodes": 8 | [ 9 | { 10 | "type": "SAT", 11 | "iname": "SatelliteBasic", 12 | "nodeid": 1, 13 | "loglevel": "all", 14 | "tle_1": "1 50985U 22002B 22290.71715197 .00032099 00000+0 13424-2 0 9994", 15 | "tle_2": "2 50985 97.4784 357.5505 0011839 353.6613 6.4472 15.23462773 42039", 16 | "additionalargs": "", 17 | "models":[ 18 | { 19 | "iname": "ModelOrbit" 20 | }, 21 | { 22 | "iname": "ModelPower", 23 | "power_consumption": { 24 | "TXRADIO": 0.532, 25 | "HEATER": 0.532, 26 | "RXRADIO": 0.133, 27 | "CONCENTRATOR": 0.266, 28 | "GPS": 0.190 29 | }, 30 | "power_configurations": { 31 | "MAX_CAPACITY": 25308, 32 | "MIN_CAPACITY": 15185, 33 | "INITIAL_CAPACITY": 25308 34 | }, 35 | "power_generations":{ 36 | "SOLAR": 1.666667 37 | }, 38 | "always_on": ["GPS", "CONCENTRATOR", "RXRADIO", "HEATER"], 39 | "efficiency": 0.85, 40 | "delta": 5 41 | } 42 | 43 | ] 44 | } 45 | ] 46 | } 47 | ], 48 | "simtime": 49 | { 50 | "starttime": "2022-11-14 12:00:00", 51 | "endtime": "2022-11-14 14:00:00", 52 | "delta": 5.0 53 | }, 54 | "simlogsetup": 55 | { 56 | "loghandler": "LoggerCmd", 57 | "logfolder": "" 58 | } 59 | } -------------------------------------------------------------------------------- /dependencies/de440s.bsp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/CosmicBeats-Simulator/dc3fff419f454bd66975fade54dbd7985d336181/dependencies/de440s.bsp -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | channels: 2 | - conda-forge 3 | - defaults 4 | dependencies: 5 | - _libgcc_mutex=0.1=main 6 | - _openmp_mutex=5.1=1_gnu 7 | - blas=1.0=mkl 8 | - bottleneck=1.3.4=py38hce1f21e_0 9 | - brotli=1.0.9=he6710b0_2 10 | - ca-certificates=2022.6.15=ha878542_0 11 | - certifi=2022.6.15=py38h578d9bd_0 12 | - cycler=0.11.0=pyhd3eb1b0_0 13 | - dbus=1.13.18=hb2f20db_0 14 | - expat=2.4.4=h295c915_0 15 | - fontconfig=2.13.1=h6c09931_0 16 | - fonttools=4.25.0=pyhd3eb1b0_0 17 | - freetype=2.11.0=h70c0345_0 18 | - giflib=5.2.1=h7b6447c_0 19 | - glib=2.69.1=h4ff587b_1 20 | - gst-plugins-base=1.14.0=h8213a91_2 21 | - gstreamer=1.14.0=h28cd5cc_2 22 | - icu=58.2=he6710b0_3 23 | - intel-openmp=2021.4.0=h06a4308_3561 24 | - jpeg=9e=h7f8727e_0 25 | - jplephem=2.17=pyhf3f802f_0 26 | - kiwisolver=1.4.2=py38h295c915_0 27 | - lcms2=2.12=h3be6417_0 28 | - ld_impl_linux-64=2.38=h1181459_1 29 | - libffi=3.3=he6710b0_2 30 | - libgcc-ng=11.2.0=h1234567_1 31 | - libgomp=11.2.0=h1234567_1 32 | - libpng=1.6.37=hbc83047_0 33 | - libstdcxx-ng=11.2.0=h1234567_1 34 | - libtiff=4.2.0=h2818925_1 35 | - libuuid=1.0.3=h7f8727e_2 36 | - libwebp=1.2.2=h55f646e_0 37 | - libwebp-base=1.2.2=h7f8727e_0 38 | - libxcb=1.15=h7f8727e_0 39 | - libxml2=2.9.14=h74e7548_0 40 | - lz4-c=1.9.3=h295c915_1 41 | - mkl=2021.4.0=h06a4308_640 42 | - mkl-service=2.4.0=py38h7f8727e_0 43 | - mkl_fft=1.3.1=py38hd3c417c_0 44 | - mkl_random=1.2.2=py38h51133e4_0 45 | - munkres=1.1.4=py_0 46 | - ncurses=6.3=h7f8727e_2 47 | - numexpr=2.8.1=py38h807cd23_2 48 | - numpy=1.22.3=py38he7a7128_0 49 | - numpy-base=1.22.3=py38hf524024_0 50 | - openssl=1.1.1l=h7f98852_0 51 | - packaging=21.3=pyhd3eb1b0_0 52 | - pandas=1.4.2=py38h295c915_0 53 | - pcre=8.45=h295c915_0 54 | - pillow=9.0.1=py38h22f2fdc_0 55 | - pip=21.2.4=py38h06a4308_0 56 | - pyerfa=2.0.0=py38h27cfd23_0 57 | - pyparsing=3.0.4=pyhd3eb1b0_0 58 | - pyqt=5.9.2=py38h05f1152_4 59 | - python=3.8.10=h12debd9_8 60 | - python-dateutil=2.8.2=pyhd3eb1b0_0 61 | - python_abi=3.8=2_cp38 62 | - pytz=2022.1=py38h06a4308_0 63 | - pyyaml=6.0=py38h7f8727e_1 64 | - qt=5.9.7=h5867ecd_1 65 | - readline=8.1.2=h7f8727e_1 66 | - setuptools=61.2.0=py38h06a4308_0 67 | - sgp4=2.21=py38h514daf8_0 68 | - sip=4.19.13=py38h295c915_0 69 | - six=1.16.0=pyhd3eb1b0_1 70 | - sqlite=3.38.5=hc218d9a_0 71 | - tk=8.6.12=h1ccaba5_0 72 | - tornado=6.1=py38h27cfd23_0 73 | - wheel=0.37.1=pyhd3eb1b0_0 74 | - xz=5.2.5=h7f8727e_1 75 | - yaml=0.2.5=h7b6447c_0 76 | - zlib=1.2.12=h7f8727e_2 77 | - zstd=1.5.2=ha4553b6_0 78 | - pip: 79 | - astropy==5.1 80 | - basemap==1.3.3 81 | - basemap-data==1.3.2 82 | - bidict==0.22.0 83 | - itur==0.3.4 84 | - matplotlib==3.5.2 85 | - pyastronomy==0.17.1 86 | - pyproj==3.3.1 87 | - pyshp==2.1.3 88 | - quantities==0.13.0 89 | - scipy==1.8.1 90 | - skyfield==1.42 91 | -------------------------------------------------------------------------------- /examples/analytics_samples/analyze_datalayer.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | Created by: Om Chabra 5 | Created on: 11 Jul 2023 6 | @desc 7 | This finds the end-to-end delay of data moving throughout the system. 8 | ''' 9 | import sys 10 | import os 11 | 12 | #Let's add the path to the src folder so that we can import the modules 13 | sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) 14 | 15 | from src.analytics.smas.smadatagenerator import init_SMADataGenerator 16 | from src.analytics.smas.smadatastore import init_SMADataStore 17 | from src.analytics.summarizers.summarizerdatalayer import init_SummarizerDataLayer 18 | 19 | if __name__ == '__main__': 20 | _directoryOfLogs = sys.argv[1] 21 | 22 | #Let's get all the log files which are satellite logs 23 | _files = os.listdir(_directoryOfLogs) 24 | _iotFiles = [i for i in _files if i.split('_')[3] == 'IoT'] 25 | _gsFiles = [i for i in _files if i.split('_')[3] == 'GS'] 26 | _satFiles = [i for i in _files if i.split('_')[3] == 'SAT'] 27 | 28 | #Now, let's setup the SMAs 29 | _iotSMAs = [] 30 | for _iotFile in _iotFiles: 31 | _iotSMAs.append(init_SMADataGenerator(modelLogPath=os.path.join(_directoryOfLogs, _iotFile))) 32 | 33 | _gsSMAs = [] 34 | for _gsFile in _gsFiles: 35 | _gsSMAs.append(init_SMADataStore(modelLogPath=os.path.join(_directoryOfLogs, _gsFile))) 36 | 37 | _satSMAs = [] 38 | for _satFile in _satFiles: 39 | _satSMAs.append(init_SMADataStore(modelLogPath=os.path.join(_directoryOfLogs, _satFile))) 40 | 41 | #Now, let's run the SMAs. 42 | print("Running IoT SMAs") 43 | for _sma in _iotSMAs: 44 | _sma.Execute() 45 | print("Running GS SMAs") 46 | for _sma in _gsSMAs: 47 | _sma.Execute() 48 | print("Running SAT SMAs") 49 | for _sma in _satSMAs: 50 | _sma.Execute() 51 | 52 | _sumarizer = init_SummarizerDataLayer(_gsDataStoreSMAs = _gsSMAs, _generatorSMAs = _iotSMAs, _satelliteDataStoreSMAs=_satSMAs) 53 | print("Running Summarizer") 54 | _sumarizer.Execute() 55 | print("Results: ", _sumarizer.get_Results()) -------------------------------------------------------------------------------- /examples/analytics_samples/analyze_loraradios.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | Created by: Om Chabra 5 | Created on: 11 Jul 2023 6 | @desc 7 | This finds out how many collisions among other things that are happening in the satelite's radio 8 | ''' 9 | import sys 10 | import os 11 | 12 | #Let's add the path to the src folder so that we can import the modules 13 | sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) 14 | 15 | from src.analytics.smas.smaloraradiodevicerx import init_SMALoraRadioDeviceRx 16 | from src.analytics.smas.smaloraradiodevicetx import init_SMALoraRadioDeviceTx 17 | from src.analytics.summarizers.summarizerloraradiodevice import init_SummarizerLoraRadioDevice 18 | 19 | if __name__ == '__main__': 20 | _directoryOfLogs = sys.argv[1] 21 | 22 | #Let's get all the log files which are satellite logs 23 | _files = os.listdir(_directoryOfLogs) 24 | _satFiles = [i for i in _files if i.split('_')[3] == 'SAT'] 25 | 26 | #Now, let's setup the SMAs 27 | _txSmas = [] 28 | _rxSMAs = [] 29 | for _satFile in _satFiles: 30 | _fullPath = os.path.join(_directoryOfLogs, _satFile) 31 | _txSmas.append(init_SMALoraRadioDeviceTx(modelLogPath=_fullPath)) 32 | _rxSMAs.append(init_SMALoraRadioDeviceRx(modelLogPath=_fullPath)) 33 | 34 | #Now, let's run the SMAs. 35 | for _sma in _txSmas: 36 | _sma.Execute() 37 | for _sma in _rxSMAs: 38 | _sma.Execute() 39 | 40 | #Now, let's setup the summarizers 41 | _satSummarizers = [] 42 | for _sat in range(len(_satFiles)): 43 | _satSummarizers.append(init_SummarizerLoraRadioDevice(_txSMA = _txSmas[_sat], _rxSMA = _rxSMAs[_sat])) 44 | 45 | for _summarizer in _satSummarizers: 46 | _summarizer.Execute() 47 | 48 | _res = [] 49 | for _summarizer in _satSummarizers: 50 | _res.append(_summarizer.get_Results()) 51 | print("One sample satellite: ", _satFiles[0], " has output: ", _res[0]) 52 | 53 | print("\nTotal collisions across all satellites: ", sum([i['numFramesCollided'] for i in _res])) 54 | 55 | -------------------------------------------------------------------------------- /examples/analytics_samples/analyze_power.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 11 Jul 2023 7 | @desc 8 | This finds the overall power consumption of the system. 9 | ''' 10 | import sys 11 | import os 12 | 13 | #Let's add the path to the src folder so that we can import the modules 14 | sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) 15 | 16 | from src.analytics.smas.smapowerbasic import init_SMAPowerBasic 17 | from src.analytics.summarizers.summarizerpower import init_SummarizerPower 18 | from src.analytics.summarizers.summarizermultiplepower import init_SummarizerMultiplePower 19 | 20 | if __name__ == '__main__': 21 | _directoryOfLogs = sys.argv[1] 22 | 23 | #Let's get all the log files which are satellite logs 24 | _files = os.listdir(_directoryOfLogs) 25 | _satFiles = [i for i in _files if i.split('_')[3] == 'SAT'] 26 | 27 | #Now, let's setup the SMAs 28 | _satSMAs = [] 29 | for _satFile in _satFiles: 30 | _fullPath = os.path.join(_directoryOfLogs, _satFile) 31 | _satSMAs.append(init_SMAPowerBasic(modelLogPath=_fullPath)) 32 | 33 | #Now, let's run the SMAs. 34 | for _sma in _satSMAs: 35 | _sma.Execute() 36 | 37 | #Now, let's setup the summarizers 38 | _satSummarizers = [] 39 | for _sma in _satSMAs: 40 | _satSummarizers.append(init_SummarizerPower(_powerModelSMA = _sma)) 41 | _satSummarizers[-1].Execute() 42 | 43 | #Now, let's print the results of one of the summarizers so we can see what the data looks like 44 | print("One Sample Satellite", _satFiles[0], " Has output: ",_satSummarizers[0].get_Results()) 45 | 46 | #Now let's run the overall summarizer 47 | _overallSummarizer = init_SummarizerMultiplePower(_powerSummarizers = _satSummarizers) 48 | _overallSummarizer.Execute() 49 | print("\nOverall Results Across Satellites: ", _overallSummarizer.get_Results()) 50 | 51 | 52 | -------------------------------------------------------------------------------- /examples/imagesatellite.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | ''' 5 | 6 | import sys 7 | import os 8 | import time 9 | import threading 10 | import random 11 | 12 | #Let's add the path to the src folder so that we can import the modules 13 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 14 | 15 | from src.sim.simulator import Simulator 16 | 17 | 18 | if __name__ == "__main__": 19 | random.seed(0) 20 | _filepath = '' 21 | 22 | #look for the config file path in the command line arguments 23 | 24 | if(len(sys.argv) > 1): 25 | _filepath = sys.argv[1] 26 | else: 27 | _filepath = "configs/examples/config_imagesat.json" 28 | 29 | _sim = Simulator(_filepath) 30 | 31 | _startTime = time.perf_counter() 32 | 33 | #run execute method in a separate thread 34 | _thread_sim = threading.Thread(target=_sim.execute) 35 | 36 | # Let's compute all the FOVs before starting the simulation. This will make the simulation faster. 37 | # WARNING: Remove this part if you are getting error on a Windows machine. It may slow down the simulation. 38 | print("[Simulator Info] Computing FOVs...") 39 | _ret = _sim.call_RuntimeAPIs("compute_FOVs") 40 | print("[Simulator Info] FOVs computed.") 41 | 42 | # Now, let's start the simulation 43 | _thread_sim.start() 44 | _thread_sim.join() 45 | 46 | _endTime = time.perf_counter() 47 | 48 | print(f"[Simulator Info] Time required to run the simulation: {_endTime-_startTime} seconds.") -------------------------------------------------------------------------------- /examples/iotnetwork.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | ''' 5 | import sys 6 | import os 7 | import time 8 | import threading 9 | import random 10 | 11 | #Let's add the path to the src folder so that we can import the modules 12 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 13 | 14 | from src.sim.simulator import Simulator 15 | 16 | 17 | if __name__ == "__main__": 18 | random.seed(0) 19 | _filepath = '' 20 | 21 | #look for the config file path in the command line arguments 22 | 23 | if(len(sys.argv) > 1): 24 | _filepath = sys.argv[1] 25 | else: 26 | _filepath = "configs/examples/config_1000iot.json" 27 | 28 | _sim = Simulator(_filepath) 29 | 30 | _startTime = time.perf_counter() 31 | 32 | #run execute method in a separate thread 33 | _thread_sim = threading.Thread(target=_sim.execute) 34 | 35 | # Let's compute all the FOVs before starting the simulation. This will make the simulation faster. 36 | # WARNING: Remove this part if you are getting error on a Windows machine. It may slow down the simulation. 37 | print("[Simulator Info] Computing FOVs...") 38 | _ret = _sim.call_RuntimeAPIs("compute_FOVs") 39 | print("[Simulator Info] FOVs computed.") 40 | 41 | # Now, let's start the simulation 42 | _thread_sim.start() 43 | _thread_sim.join() 44 | 45 | _endTime = time.perf_counter() 46 | 47 | print(f"[Simulator Info] Time required to run the simulation: {_endTime-_startTime} seconds.") -------------------------------------------------------------------------------- /figs/Class_diagram.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/CosmicBeats-Simulator/dc3fff419f454bd66975fade54dbd7985d336181/figs/Class_diagram.pdf -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | ''' 5 | from src.sim.simulator import Simulator 6 | import sys 7 | import time 8 | import random 9 | 10 | if __name__ == "__main__": 11 | random.seed(0) 12 | _filepath = '' 13 | 14 | #look for the config file path in the command line arguments 15 | 16 | if(len(sys.argv) > 1): 17 | _filepath = sys.argv[1] 18 | else: 19 | _filepath = "configs/config.json" 20 | 21 | _sim = Simulator(_filepath) 22 | 23 | _startTime = time.perf_counter() 24 | 25 | # Now, let's start the simulation 26 | 27 | _sim.execute() 28 | 29 | _endTime = time.perf_counter() 30 | 31 | print(f"[Simulator Info] Time required to run the simulation: {_endTime-_startTime} seconds.") 32 | 33 | -------------------------------------------------------------------------------- /pipelines.yml: -------------------------------------------------------------------------------- 1 | # https://aka.ms/yaml 2 | 3 | trigger: 4 | - main 5 | 6 | strategy: 7 | matrix: 8 | ubuntumachine: 9 | python.version: '3.11' 10 | vmImage: ubuntu-latest 11 | windowsmachine: 12 | python.version: '3.11' 13 | vmImage: windows-latest 14 | 15 | pool: 16 | vmImage: $(vmImage) 17 | 18 | 19 | steps: 20 | - task: ComponentGovernanceComponentDetection@0 21 | inputs: 22 | scanType: 'Register' 23 | verbosity: 'Verbose' 24 | alertWarningLevel: 'High' 25 | - task: CodeQL3000Init@0 26 | - task: UsePythonVersion@0 27 | inputs: 28 | versionSpec: '$(python.version)' 29 | displayName: 'Use Python $(python.version)' 30 | 31 | - script: | 32 | python -m pip install --upgrade pip 33 | pip install -r requirements.txt 34 | displayName: 'Install dependencies' 35 | 36 | - script: | 37 | pip install pytest pytest-azurepipelines 38 | displayName: 'Install pytest azure pipelines' 39 | 40 | - script: | 41 | pytest -Wignore src/test/ 42 | displayName: 'Test all components' 43 | 44 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | astropy==5.3.1 2 | dask==2023.7.1 3 | geopy==2.3.0 4 | gradio==3.39.0 5 | numpy==1.25.1 6 | pandas==2.0.3 7 | plotly==5.15.0 8 | skyfield==1.46 9 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/CosmicBeats-Simulator/dc3fff419f454bd66975fade54dbd7985d336181/src/__init__.py -------------------------------------------------------------------------------- /src/analytics/smas/isma.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 11 Jul 2023 7 | @desc 8 | This implements the single model analyzer (SMA) interface. 9 | ''' 10 | 11 | from abc import ABC, abstractmethod 12 | from pandas import DataFrame 13 | 14 | class ISMA(ABC): 15 | ''' 16 | This serves as an interface implementation for the Single Model Analyzer (SMA). 17 | Each SMA implementation should inherit from this interface. 18 | The SMA is responsible for analyzing the outcomes of a single model. 19 | It accepts the logs of a single model from a node or the output of an existing SMA implementation as input. 20 | It then analyzes the logs, generates the results, and presents them in a table format. 21 | ''' 22 | 23 | @property 24 | @abstractmethod 25 | def iName(self) -> str: 26 | """ 27 | @type 28 | str 29 | @desc 30 | A string representing the name of the SMA class. For example, smapower 31 | Note that the name should exactly match to your class name. 32 | """ 33 | pass 34 | 35 | @property 36 | @abstractmethod 37 | def supportedModelNames(self) -> 'list[str]': 38 | ''' 39 | @type 40 | String 41 | @desc 42 | supportedModelNames gives the list of name of the models, the log of which this SMA can process. 43 | ''' 44 | pass 45 | 46 | @property 47 | @abstractmethod 48 | def supportedSMANames(self) -> 'list[str]': 49 | ''' 50 | @type 51 | String 52 | @desc 53 | supportedSMANames gives the list of name of the SMAs, the output of which this SMA can process. 54 | ''' 55 | pass 56 | 57 | 58 | @abstractmethod 59 | def call_APIs( 60 | self, 61 | _apiName: str, 62 | **_kwargs): 63 | ''' 64 | This method acts as an API interface of the SMA. 65 | An API offered by the SMA can be invoked through this method. 66 | @param[in] _apiName 67 | Name of the API. Each SMA should have a list of the API names. 68 | @param[in] _kwargs 69 | Keyworded arguments that are passed to the corresponding API handler 70 | @return 71 | The API return 72 | ''' 73 | pass 74 | 75 | @abstractmethod 76 | def Execute(self): 77 | """ 78 | This method executes the tasks that needed to be performed by the SMA. 79 | """ 80 | pass 81 | 82 | @abstractmethod 83 | def get_Results(self) -> DataFrame: 84 | ''' 85 | @desc 86 | This method returns the results of the SMA in the form of a DataFrame table once it is executed. 87 | @return 88 | A DataFrame table containing the results of the SMA. 89 | ''' 90 | pass -------------------------------------------------------------------------------- /src/analytics/smas/smadatagenerator.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 14 Jul 2023 7 | @desc 8 | This module analyzes the logs produced by the modelDataGenerator and produces a table in the specified format: 9 | 1. timestamp 10 | 2. action 11 | 3. id 12 | 4. queueSize 13 | 5. sourceNodeID 14 | The expected log message from the modelDataGenerator must precisely match the following format: 15 | [Action] dataID: [id]. queueSize: [size] 16 | 17 | The "Action" field currently supports values "Generated" or "Dropped," but it can accommodate any other value as well. 18 | ''' 19 | 20 | from src.analytics.smas.isma import ISMA 21 | import dask.dataframe as dd 22 | import dask 23 | from pandas import DataFrame 24 | from dask import delayed 25 | 26 | class SMADataGenerator(ISMA): 27 | ''' 28 | @desc 29 | This class analyzes the logs produced by the modelDataGenerator and produces a table in the specified format: 30 | timestamp 31 | action 32 | id 33 | queueSize 34 | sourceNodeID 35 | The expected log message from the modelDataGenerator must precisely match the following format: 36 | [Action] dataID: [id]. queueSize: [size] 37 | ''' 38 | __supportedSMANames = [] # No dependency on any other SMA 39 | __supportedModelNames = ['ModelDataGenerator'] # Dependency on the model 40 | @property 41 | def iName(self) -> str: 42 | """ 43 | @type 44 | str 45 | @desc 46 | A string representing the name of the SMA class. For example, smapower 47 | Note that the name should exactly match to your class name. 48 | """ 49 | return self.__class__.__name__ 50 | 51 | @property 52 | def supportedModelNames(self) -> 'list[str]': 53 | ''' 54 | @type 55 | String 56 | @desc 57 | supportedModelNames gives the list of name of the models, the log of which this SMA can process. 58 | ''' 59 | return self.__supportedModelNames 60 | @property 61 | def supportedSMANames(self) -> 'list[str]': 62 | ''' 63 | @type 64 | String 65 | @desc 66 | supportedSMANames gives the list of name of the SMAs, the output of which this SMA can process. 67 | ''' 68 | return self.__supportedSMANames 69 | 70 | def call_APIs( 71 | self, 72 | _apiName: str, 73 | **_kwargs): 74 | ''' 75 | This method acts as an API interface of the SMA. 76 | An API offered by the SMA can be invoked through this method. 77 | @param[in] _apiName 78 | Name of the API. Each SMA should have a list of the API names. 79 | @param[in] _kwargs 80 | Keyworded arguments that are passed to the corresponding API handler 81 | @return 82 | The API return 83 | ''' 84 | pass 85 | 86 | def Execute(self): 87 | ''' 88 | This method executes the tasks that needed to be performed by the SMA. 89 | ''' 90 | 91 | #let's read the whole log file. Let's use dask because this log file might be huge 92 | _logData = dd.read_csv(self.__logFile, quotechar='"', delimiter=',', skipinitialspace=True) 93 | 94 | #we should have the following columns: logLevel, timestamp, modelName, message 95 | #We only need the ones where modelName matches our dependencyModelName 96 | _modelLogData = _logData[_logData['modelName'] == "ModelDataGenerator"] 97 | 98 | #We are only interested in the following string: 99 | #[Action] dataID: [id]. queueSize: [size] 100 | #This string should be the only one this model creates 101 | 102 | #Now, let's use regular expressions to extract the information from the log messages. Each one of the following is a dask series 103 | _times = dd.to_datetime(_modelLogData['timestamp']) 104 | _actions = _modelLogData['message'].str.extract(r'(\b\w+) ', expand=False) 105 | _ids = _modelLogData['message'].str.extract(r'dataID: (\d+)', expand=False) 106 | _queueSize = _modelLogData['message'].str.extract(r'queueSize: (\d+)', expand=False) 107 | 108 | #let's create the results table. We do it this way because we want to keep things in parallel as much as possible 109 | _columns = ['timestamp', 'action', 'id', 'queueSize'] 110 | _seriesList = [_times, _actions, _ids, _queueSize] 111 | _dfList = [s.to_frame(name=label) for s, label in zip(_seriesList, _columns)] 112 | 113 | _results = dd.concat(_dfList, axis=1, ignore_unknown_divisions=True) 114 | _results = _results.reset_index(drop=True) 115 | 116 | #Let's also add in a column for the nodeID 117 | _results['sourceNodeID'] = self.__nodeID 118 | 119 | #Let's now make everything to the right types 120 | _dataTypes = { 121 | 'timestamp': 'datetime64[ns]', 122 | 'action': 'str', 123 | 'id': 'int64', 124 | 'queueSize': 'int64', 125 | 'sourceNodeID': 'int64' 126 | } 127 | _results = _results.astype(_dataTypes) 128 | 129 | self.__results = _results 130 | 131 | def get_Results(self) -> DataFrame: 132 | ''' 133 | @desc 134 | This method returns the results of the SMA in the form of a DataFrame table once it is executed. 135 | @return 136 | A DataFrame table containing the results of the SMA. 137 | ''' 138 | #make the dask dataframe into a pandas dataframe 139 | result = dask.compute(self.__results)[0] 140 | return result 141 | 142 | def __init__(self, 143 | _modelLogPath: str): 144 | ''' 145 | @desc 146 | Constructor 147 | @param[in] _modelLogPath 148 | Path to the log file of the model 149 | ''' 150 | self.__logFile = _modelLogPath 151 | self.__nodeID = self.__logFile.split('/')[-1].split('_')[-1].split('.')[0] #get the nodeID from the log file name 152 | self.__results = None 153 | 154 | def init_SMADataGenerator(**_kwargs) -> ISMA: 155 | ''' 156 | @desc 157 | Initializes the SMADataGenerator class 158 | @param[in] _kwargs 159 | Keyworded arguments that are passed to the constructor of the SMADataGenerator class. 160 | It should have the following (key, value) pairs: 161 | @key modelLogPath 162 | Path to the log file of the model 163 | @return 164 | An instance of the SMAPowerBasic class 165 | ''' 166 | #check if the keyworded arguments has the key 'modelLogPath' 167 | if 'modelLogPath' not in _kwargs: 168 | raise Exception('[Simulator Exception] The keyworded argument modelLogPath is missing') 169 | 170 | #create an instance of the SMADataGenerator class 171 | _sma = SMADataGenerator(_kwargs['modelLogPath']) 172 | return _sma 173 | -------------------------------------------------------------------------------- /src/analytics/smas/smatimebasedfov.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 11 Jul 2023 7 | @desc 8 | This module serves as the implementation of the Single Model Analyzer (SMA) for the Field of View (FOV) model (src file: modelfovtimebased.py). 9 | 10 | The SMA generates a dataframe with the following columns: 11 | 1. nodeID 12 | 2. otherNodeID 13 | 3. nodeType 14 | 4. startTimes 15 | 5. endTimes 16 | 17 | The SMA specifically focuses on the following string: 18 | "Pass. nodeID: (int). nodeType: (int). startTimeUnix: (float). endTimeUnix: (float)" 19 | ''' 20 | 21 | from src.analytics.smas.isma import ISMA 22 | from pandas import DataFrame 23 | import pandas as pd 24 | import dask.dataframe as dd 25 | 26 | class SMAFovTimeBased(ISMA): 27 | ''' 28 | This class implements the SMA for the power model. It takes the time based logs of the power model and generates basic power related insights. 29 | For example, how much power was generated, how much power was consumed by which component, etc. 30 | ''' 31 | __supportedSMANames = [] # No dependency on any other SMA 32 | __supportedModelNames = ['ModelFovTimeBased'] # Dependency on the power model 33 | 34 | @property 35 | def iName(self) -> str: 36 | """ 37 | @type 38 | str 39 | @desc 40 | A string representing the name of the SMA class. For example, smapower 41 | Note that the name should exactly match to your class name. 42 | """ 43 | return self.__class__.__name__ 44 | 45 | 46 | @property 47 | def supportedModelNames(self) -> 'list[str]': 48 | ''' 49 | @type 50 | String 51 | @desc 52 | supportedModelNames gives the list of name of the models, the log of which this SMA can process. 53 | ''' 54 | return self.__supportedModelNames 55 | 56 | @property 57 | def supportedSMANames(self) -> 'list[str]': 58 | ''' 59 | @type 60 | String 61 | @desc 62 | supportedSMANames gives the list of name of the SMAs, the output of which this SMA can process. 63 | ''' 64 | return self.__supportedSMANames 65 | 66 | def call_APIs( 67 | self, 68 | _apiName: str, 69 | **_kwargs): 70 | ''' 71 | This method acts as an API interface of the SMA. 72 | An API offered by the SMA can be invoked through this method. 73 | @param[in] _apiName 74 | Name of the API. Each SMA should have a list of the API names. 75 | @param[in] _kwargs 76 | Keyworded arguments that are passed to the corresponding API handler 77 | @return 78 | The API return 79 | ''' 80 | pass 81 | 82 | def Execute(self): 83 | """ 84 | This method executes the tasks that needed to be performed by the SMA. 85 | """ 86 | #let's read the whole log file. Let's use dask because this log file might be huge 87 | _logData = dd.read_csv(self.__logFile, quotechar='"', delimiter=',', skipinitialspace=True) 88 | _modelInfo = _logData[_logData['modelName'] == "ModelFovTimeBased"] 89 | 90 | #We are only interested in the following string: 91 | #Pass. nodeID: (int). nodeType: (int). startTimeUnix: (float). endTimeUnix: (float) 92 | #Let's extract all the information in the following format: 93 | 94 | _regex = r'Pass\. nodeID: (?P\d+)\. nodeType: (?P\d+)\. startTimeUnix: (?P[\d.]+)\. endTimeUnix: (?P[\d.]+)' 95 | 96 | #Let's create a new dataframe with the extracted information 97 | _extracted = _modelInfo['message'].str.extractall(_regex) 98 | 99 | #Let's hope that the extracted dataframe fits into memory 100 | _df = _extracted.compute() 101 | 102 | #Before we convert to lists, let's make sure that the columns are in the right data type 103 | _dtypes = {'otherNodeID': int, 'nodeType': int, 'startTimeUnix': float, 'endTimeUnix': float} 104 | _df = _df.astype(_dtypes) 105 | 106 | #Let's now aggregate the extracted dataframe to combine the start and end times if the otherNodeID is the same 107 | _df = _df.groupby('otherNodeID').agg(list).reset_index() #This will make everything but the otherNodeID column as a list 108 | 109 | #Make the start and end times DatetimeIndex objects so it is easier to work with 110 | def __convertToDatetimeIndex(_list): 111 | return pd.to_datetime(_list, unit='s', utc=True) 112 | _newStartTime = _df['startTimeUnix'].apply(__convertToDatetimeIndex) 113 | _newEndTime = _df['endTimeUnix'].apply(__convertToDatetimeIndex) 114 | 115 | #Let's now create the final dataframe 116 | _resultsDf = pd.DataFrame({'nodeID': self.__nodeID, 117 | 'otherNodeID': _df['otherNodeID'], 118 | 'nodeType': _df['nodeType'].apply(lambda x: int(x[0])), #convert the list to a single value 119 | 'startTimes': _newStartTime, 120 | 'endTimes': _newEndTime}) 121 | 122 | self.__result = _resultsDf 123 | 124 | def get_Results(self) -> DataFrame: 125 | ''' 126 | @desc 127 | This method returns the results of the SMA in the form of a DataFrame table once it is executed. 128 | @return 129 | A DataFrame table containing the results of the SMA. 130 | ''' 131 | return self.__result 132 | 133 | def __init__(self, 134 | _modelLogPath: str): 135 | ''' 136 | @desc 137 | Constructor 138 | @param[in] _modelLogPath 139 | Path to the log file of the model 140 | ''' 141 | self.__logFile = _modelLogPath 142 | self.__nodeID = self.__logFile.split('/')[-1].split('_')[-1].split('.')[0] #get the nodeID from the log file name 143 | self.__result = None 144 | 145 | def init_SMAFovTimeBased(**_kwargs) -> ISMA: 146 | ''' 147 | @desc 148 | Initializes the SMAFovTimeBased class 149 | @param[in] _kwargs 150 | Keyworded arguments that are passed to the constructor of the SMAFovTimeBased class. 151 | It should have the following (key, value) pairs: 152 | @key modelLogPath 153 | Path to the log file of the model 154 | @return 155 | An instance of the SMAFovTimeBased class 156 | ''' 157 | #check if the keyworded arguments has the key 'modelLogPath' 158 | if 'modelLogPath' not in _kwargs: 159 | raise Exception('[Simulator Exception] The keyworded argument modelLogPath is missing') 160 | 161 | #create an instance of the SMAPowerBasic class 162 | sma = SMAFovTimeBased(_kwargs['modelLogPath']) 163 | return sma 164 | 165 | -------------------------------------------------------------------------------- /src/analytics/summarizers/isummarizers.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 11 Jul 2023 7 | @desc 8 | This implements the summarizer interface for processing the outputs of SMA(s) or other summarizers. 9 | ''' 10 | 11 | from abc import ABC, abstractmethod 12 | 13 | 14 | class ISummarizer(ABC): 15 | ''' 16 | This serves as an interface implementation for the summarizer. 17 | Each summarizer implementation should inherit from this interface. 18 | The summarizer is responsible for summarizing the results obtained after processing the outputs of one or multiple SMAs and/or existing summarizers. 19 | It takes the output of SMAs and existing summarizer implementations as input, and then analyzes the inputs to generate a summary of the results in dictionary format. 20 | ''' 21 | 22 | @property 23 | @abstractmethod 24 | def iName(self) -> 'str': 25 | """ 26 | @type 27 | str 28 | @desc 29 | A string representing the name of the summarizer class. For example, summarizerlatency 30 | Note that the name should exactly match to your class name. 31 | """ 32 | pass 33 | 34 | 35 | @property 36 | @abstractmethod 37 | def supportedSMANames(self) -> 'list[str]': 38 | ''' 39 | @type 40 | String 41 | @desc 42 | supportedSMANames gives the list of name of the SMAs, the output of which this SMA can process. 43 | ''' 44 | pass 45 | 46 | @property 47 | @abstractmethod 48 | def supportedSummarizerNames(self) -> 'list[str]': 49 | ''' 50 | @type 51 | List of String 52 | @desc 53 | supportedSummarizerNames gives the list of name of the Summarizers, the output of which this Summarizer can process. 54 | ''' 55 | pass 56 | 57 | @abstractmethod 58 | def call_APIs( 59 | self, 60 | _apiName: str, 61 | **_kwargs): 62 | ''' 63 | This method acts as an API interface of the summarizer. 64 | An API offered by the summarizer can be invoked through this method. 65 | @param[in] _apiName 66 | Name of the API. Each summarizer should have a list of the API names. 67 | @param[in] _kwargs 68 | Keyworded arguments that are passed to the corresponding API handler 69 | @return 70 | The API return 71 | ''' 72 | pass 73 | 74 | @abstractmethod 75 | def Execute(self): 76 | """ 77 | This method executes the tasks that needed to be performed by the summarizer. 78 | """ 79 | pass 80 | 81 | @abstractmethod 82 | def get_Results(self) -> 'dict': 83 | ''' 84 | @desc 85 | This method returns the results of the in the form of a dictionary where the key is the name of the metric and the value is the results. 86 | @return 87 | The results of the summarizer in the form of a dictionary where the key is the name of the metric and the value is the results. 88 | ''' 89 | pass -------------------------------------------------------------------------------- /src/analytics/summarizers/summarizerloraradiodevice.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 14 Jul 2023 7 | @desc 8 | This module processes the outputs of the two LoraRadioDevice SMAs and calculates the following metrics: 9 | 10 | 1. numFramesDroppedMTU: Number of frames dropped due to MTU 11 | 2. numFramesDroppedTxBusy: Number of frames dropped due to TX busy 12 | 3. numFramesDroppedRX: Number of frames dropped due to RX busy 13 | 4. numFramesCollided: Number of frames collided 14 | 5. average/minimum/maximum PLRTX (Packet Loss Ratio TX) 15 | 6. average/minimum/maximum PERTX (Packet Error Rate TX) 16 | ''' 17 | from src.analytics.summarizers.isummarizers import ISummarizer 18 | from pandas import DataFrame 19 | import pandas as pd 20 | 21 | class SummarizerLoraRadioDevice(ISummarizer): 22 | @property 23 | def iName(self) -> 'str': 24 | """ 25 | @type 26 | str 27 | @desc 28 | A string representing the name of the summarizer class. For example, summarizerlatency 29 | Note that the name should exactly match to your class name. 30 | """ 31 | return self.__class__.__name__ 32 | 33 | 34 | @property 35 | def supportedSMANames(self) -> 'list[str]': 36 | ''' 37 | @type 38 | String 39 | @desc 40 | supportedSMANames gives the list of name of the SMAs, the output of which this SMA can process. 41 | ''' 42 | return ['SMALoraRadioDeviceRx', 'SMALoraRadioDeviceTx'] 43 | 44 | @property 45 | def supportedSummarizerNames(self) -> 'list[str]': 46 | ''' 47 | @type 48 | List of String 49 | @desc 50 | supportedSummarizerNames gives the list of name of the Summarizers, the output of which this Summarizer can process. 51 | ''' 52 | return [] 53 | 54 | def call_APIs( 55 | self, 56 | _apiName: str, 57 | **_kwargs): 58 | ''' 59 | This method acts as an API interface of the summarizer. 60 | An API offered by the summarizer can be invoked through this method. 61 | @param[in] _apiName 62 | Name of the API. Each summarizer should have a list of the API names. 63 | @param[in] _kwargs 64 | Keyworded arguments that are passed to the corresponding API handler 65 | @return 66 | The API return 67 | ''' 68 | pass 69 | 70 | def Execute(self): 71 | """ 72 | This method executes the tasks that needed to be performed by the summarizer. 73 | """ 74 | self.__results = {} 75 | 76 | _txResults = self.__txSMA.get_Results() 77 | #_txResults columns are frameId,sourceAddress,frameSize,payloadSize,mtuDrop,busyDrop,noValidChannelDrop,instanceIDs, 78 | #destinationNodeIDs,destinationRadioIDs,snrs,secondsToTransmits,plrs,pers,timestamp,nodeID 79 | self.__results['numFramesDroppedMTU'] = _txResults['mtuDrop'].sum() 80 | self.__results['numFramesDroppedTxBusy'] = _txResults['busyDrop'].sum() 81 | 82 | #_txResults['plrs'] is a list of lists. We need to explode it to get a list of all the plrs 83 | _allPLRs = _txResults['plrs'].explode() 84 | self.__results['avgPLRTX'] = _allPLRs.mean() 85 | self.__results['minPLRTX'] = _allPLRs.min() 86 | self.__results['maxPLRTX'] = _allPLRs.max() 87 | 88 | _allPERs = _txResults['pers'].explode() 89 | self.__results['avgPERTX'] = _allPERs.mean() 90 | self.__results['minPERTX'] = _allPERs.min() 91 | self.__results['maxPERTX'] = _allPERs.max() 92 | 93 | #frameID, collision, collisionFrameIDs, plrDrop. perDrop, txBusyDrop, crbwDrop, nodeID, timestamp 94 | _rxResults = self.__rxSMA.get_Results() 95 | self.__results['numFramesDroppedRX'] = _rxResults['plrDrop'].sum() + _rxResults['perDrop'].sum() + _rxResults['txBusyDrop'].sum() 96 | self.__results['numFramesCollided'] = _rxResults['collision'].sum() 97 | 98 | _totalNumFrames = _rxResults['frameID'].count() 99 | self.__results['PLRRX'] = _rxResults['plrDrop'].sum() / _totalNumFrames 100 | self.__results['PERRX'] = _rxResults['perDrop'].sum() / _totalNumFrames 101 | 102 | def get_Results(self) -> 'dict': 103 | ''' 104 | @desc 105 | This method returns the results of the in the form of a dictionary where the key is the name of the metric and the value is the results. 106 | @return 107 | The results of the summarizer in the form of a dictionary where the key is the name of the metric and the value is the results. 108 | ''' 109 | return self.__results 110 | 111 | def __init__(self, 112 | _rxSMA: 'iSMA', 113 | _txSMA: 'iSMA'): 114 | """ 115 | @desc 116 | The constructor of the class 117 | @param[in] _rxSMA 118 | The LoraRadioDeviceRx SMA(s) 119 | @param[in] _txSMA 120 | The LoraRadioDeviceTx SMA(s) 121 | """ 122 | self.__rxSMA = _rxSMA 123 | self.__txSMA = _txSMA 124 | self.__results = None 125 | 126 | def init_SummarizerLoraRadioDevice(**kwargs): 127 | ''' 128 | @desc 129 | Initializes the init_SummarizerLoraRadioDevice class 130 | @param[in] _kwargs 131 | Keyworded arguments that are passed to the constructor of the init_SummarizerLoraRadioDevice class. 132 | It should have the following (key, value) pairs: 133 | @key _rxSMA 134 | The LoraRadioDeviceRx SMA which are storing the data. See smaloraradiodevicerx.py 135 | @key _txSMA 136 | The LoraRadioDeviceTx SMA which are storing the data. See smaloraradiodevicetx.py 137 | @return 138 | An instance of the init_SummarizerLoraRadioDevice class 139 | ''' 140 | _rxSMA = kwargs['_rxSMA'] 141 | _txSMA = kwargs['_txSMA'] 142 | 143 | return SummarizerLoraRadioDevice(_rxSMA, _txSMA) -------------------------------------------------------------------------------- /src/analytics/summarizers/summarizermultiplepower.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 14 Jul 2023 7 | @desc 8 | SummarizerMultiplePower is a summarizer designed to consolidate the results of multiple power models. 9 | It calculates and presents the following metrics: 10 | 11 | 1. percentCharging: The percentage of time when the battery was charging 12 | 2. averagePowerGeneration: The average power generation 13 | 3. averageNumberOfTimesWhenBatteryWasEmpty: The average number of times when the battery was empty 14 | 4. averageBatteryLevel: The average battery level 15 | 5. maximumComponent: The most common component that consumed the maximum power 16 | ''' 17 | from src.analytics.summarizers.isummarizers import ISummarizer 18 | import pandas as pd 19 | 20 | class SummarizerMultiplePower(ISummarizer): 21 | @property 22 | def iName(self) -> 'str': 23 | """ 24 | @type 25 | str 26 | @desc 27 | A string representing the name of the summarizer class. For example, summarizerlatency 28 | Note that the name should exactly match to your class name. 29 | """ 30 | return self.__class__.__name__ 31 | 32 | @property 33 | def supportedSMANames(self) -> 'list[str]': 34 | ''' 35 | @type 36 | String 37 | @desc 38 | supportedSMANames gives the list of name of the SMAs, the output of which this SMA can process. 39 | ''' 40 | return [] 41 | 42 | @property 43 | def supportedSummarizerNames(self) -> 'list[str]': 44 | ''' 45 | @type 46 | List of String 47 | @desc 48 | supportedSummarizerNames gives the list of name of the Summarizers, the output of which this Summarizer can process. 49 | ''' 50 | return [] 51 | 52 | def call_APIs( 53 | self, 54 | _apiName: str, 55 | **_kwargs): 56 | ''' 57 | This method acts as an API interface of the summarizer. 58 | An API offered by the summarizer can be invoked through this method. 59 | @param[in] _apiName 60 | Name of the API. Each summarizer should have a list of the API names. 61 | @param[in] _kwargs 62 | Keyworded arguments that are passed to the corresponding API handler 63 | @return 64 | The API return 65 | ''' 66 | pass 67 | 68 | def Execute(self): 69 | """ 70 | This method executes the tasks that needed to be performed by the summarizer. 71 | """ 72 | #The power metrics are the following: 73 | #percentCharging, averagePowerGeneration, averagePowerConsumption, numberOfTimesWhenBatteryWasEmpty, averageBatteryLevel, 74 | # averagePowerConsumptionByComponent, maxComponent, numberOfDenials 75 | 76 | #let's create a dataframe where the columns are the power metrics and the rows are the power models 77 | _listOfDicts = [] 78 | for _powerModel in self.__powerSummarizers: 79 | _listOfDicts.append(_powerModel.get_Results()) 80 | _df = pd.DataFrame(_listOfDicts) 81 | 82 | outDict = {} 83 | outDict['percentCharging'] = _df['percentCharging'].mean() 84 | outDict['averagePowerGeneration'] = _df['averagePowerGeneration'].mean() 85 | outDict['avgNumberOfTimesWhenBatteryWasEmpty'] = _df['numberOfTimesWhenBatteryWasEmpty'].mean() 86 | outDict['averageBatteryLevel'] = _df['averageBatteryLevel'].mean() 87 | outDict['mostCommonMax'] = _df['maxComponent'].mode()[0] 88 | self.__results = outDict 89 | 90 | def get_Results(self) -> 'dict': 91 | ''' 92 | @desc 93 | This method returns the results 94 | @return 95 | The results of the summarizer in the form of a dictionary where the key is the name of the metric and the value is the results. 96 | ''' 97 | return self.__results 98 | 99 | def __init__(self, 100 | _powerSummarizers: 'List[ISummarizer]'): 101 | ''' 102 | @desc 103 | Constructor of the class 104 | @param[in] _powerModelResult 105 | The result of the power model 106 | ''' 107 | self.__powerSummarizers = _powerSummarizers 108 | self.__results = {} 109 | 110 | def init_SummarizerMultiplePower(**_kwargs): 111 | """ 112 | @desc 113 | Initializes the SummarizerMultiplePower class 114 | @param[in] _kwargs 115 | Keyworded arguments that are passed to the constructor of the SummarizerMultiplePower class 116 | @key _powerSummarizers 117 | List of power summarizers 118 | @return 119 | An instance of the SummarizerMultiplePower class 120 | """ 121 | 122 | if '_powerSummarizers' in _kwargs: 123 | return SummarizerMultiplePower(_powerSummarizers=_kwargs['_powerSummarizers']) 124 | else: 125 | raise Exception('[Simulator Exception] SummarizerMultiplePower: No _powerSummarizers provided') -------------------------------------------------------------------------------- /src/global_schedulers/iglobalscheduler.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created by: Om Chabra 3 | Created on: 27 Jul 2023 4 | @desc 5 | This is a generic interface for a global scheduler. 6 | In the real world, a global scheduler (likely in the cloud) will run a simulation of the network and 7 | perform some algorithm to determine some desired results and then transmit this 8 | information to the nodes. 9 | 10 | Here, what we are doing is that we will first run a simulation of the network and 11 | while doing so will collect information about the network and run some algorithm. 12 | 13 | We then have two options: 14 | 1. Pickle this information and rerun the simulation with the pickled information. 15 | 2. Use the ManagerParallel's API "call_ModelAPIsByModelName" to call a model and directly send the information to the nodes 16 | ''' 17 | 18 | from abc import ABC, abstractmethod 19 | from pandas import DataFrame 20 | 21 | class IGlobalScheduler(ABC): 22 | @abstractmethod 23 | def call_APIs( 24 | self, 25 | _apiName: str, 26 | **_kwargs): 27 | ''' 28 | This method acts as an API interface of the SMA. 29 | An API offered by the SMA can be invoked through this method. 30 | @param[in] _apiName 31 | Name of the API. Each SMA should have a list of the API names. 32 | @param[in] _kwargs 33 | Keyworded arguments that are passed to the corresponding API handler 34 | @return 35 | The API return 36 | ''' 37 | pass 38 | 39 | @abstractmethod 40 | def Execute(self): 41 | """ 42 | This method executes the tasks that needed to be performed by the SMA. 43 | """ 44 | pass 45 | 46 | @abstractmethod 47 | def save_Schedule(self): 48 | """ 49 | This method saves the schedule to a file. 50 | """ 51 | pass 52 | 53 | @abstractmethod 54 | def setup_Simulation(self): 55 | """ 56 | This method setups the simulation. 57 | """ 58 | pass -------------------------------------------------------------------------------- /src/models/imodel.py: -------------------------------------------------------------------------------- 1 | """ 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 27 Sep 2022 7 | 8 | This module includes the interface definition of the model. 9 | """ 10 | from abc import ABC, abstractmethod 11 | from enum import Enum 12 | 13 | class EModelTag(Enum): 14 | """ 15 | An enum listing the tags of model implementation. 16 | Each model has a model tag. To know more, please refer to the documentation. 17 | """ 18 | POWER = 0 19 | ORBITAL = 1 20 | VIEWOFNODE = 2 21 | BASICLORARADIO = 3 22 | DATAGENERATOR = 4 23 | DATASTORE = 5 24 | ISL = 6 25 | MAC = 7 26 | ADACS = 8 27 | IMAGING = 9 28 | IMAGINGRADIO = 10 29 | COMPUTE = 11 30 | SCHEDULER = 12 31 | 32 | class IModel(ABC): 33 | ''' 34 | This is an interface implementation for the models. 35 | For the details on the definition and functionalities please refer to the documentation guide. 36 | ''' 37 | 38 | @property 39 | @abstractmethod 40 | def iName(self) -> str: 41 | """ 42 | @type 43 | str 44 | @desc 45 | A string representing the name of the model class. For example, ModelPower 46 | Note that the name should exactly match to your class name. 47 | """ 48 | pass 49 | 50 | @property 51 | @abstractmethod 52 | def modelTag(self) -> EModelTag: 53 | """ 54 | @type 55 | EModelTag 56 | @desc 57 | The model tag for the implemented model 58 | """ 59 | pass 60 | 61 | @property 62 | @abstractmethod 63 | def ownerNode(self): 64 | """ 65 | @type 66 | INode 67 | @desc 68 | Instance of the owner node that incorporates this model instance. 69 | The subclass (implementing a model) should keep a private variable holding the owner node instance. 70 | This method can return that variable. 71 | """ 72 | pass 73 | @property 74 | @abstractmethod 75 | def supportedNodeClasses(self) -> 'list[str]': 76 | ''' 77 | @type 78 | List of string 79 | @desc 80 | A model may not support all the node implementation. 81 | supportedNodeClasses gives the list of names of the node implementation classes that it supports. 82 | For example, if a model supports only the SatBasic and SatAdvanced, the list should be ['SatBasic', 'SatAdvanced'] 83 | If the model supports all the node implementations, just keep the list EMPTY. 84 | ''' 85 | pass 86 | 87 | @property 88 | @abstractmethod 89 | def dependencyModelClasses(self) -> 'list[list[str]]': 90 | ''' 91 | @type 92 | Nested list of string 93 | @desc 94 | dependencyModelClasses gives the nested list of name of the model implementations that this model has dependency on. 95 | For example, if a model has dependency on the ModelPower and ModelOrbitalBasic, the list should be [['ModelPower'], ['ModelOrbitalBasic']]. 96 | Now, if the model can work with EITHER of the ModelOrbitalBasic OR ModelOrbitalAdvanced, the these two should come under one sublist looking like [['ModelPower'], ['ModelOrbitalBasic', 'ModelOrbitalAdvanced']]. 97 | So each exclusively dependent model should be in a separate sublist and all the models that can work with either of the dependent models should be in the same sublist. 98 | If your model does not have any dependency, just keep the list EMPTY. 99 | ''' 100 | pass 101 | 102 | @abstractmethod 103 | def call_APIs( 104 | self, 105 | _apiName: str, 106 | **_kwargs): 107 | ''' 108 | This method acts as an API interface of the model. 109 | An API offered by the model can be invoked through this method. 110 | @param[in] _apiName 111 | Name of the API. Each model should have a list of the API names. 112 | @param[in] _kwargs 113 | Keyworded arguments that are passed to the corresponding API handler 114 | @return 115 | The API return 116 | ''' 117 | pass 118 | 119 | @abstractmethod 120 | def Execute(self): 121 | """ 122 | This method executes the tasks that needed to be performed by the model. 123 | """ 124 | pass -------------------------------------------------------------------------------- /src/models/models_radio/modelaggregatorradio.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 22 June 2023 7 | @desc 8 | This module is an extension of the original LoraRadioModel so that we can distinguish between the two lora radios. 9 | This is used for the aggregator node in the network, i.e., satellite in case of direct-to-satellite communication. 10 | ''' 11 | 12 | from src.models.models_radio.modelloraradio import ModelLoraRadio 13 | from src.models.imodel import EModelTag, IModel 14 | from src.nodes.inode import INode 15 | from src.simlogging.ilogger import ILogger 16 | from src.models.models_radio.modelloraradio import ModelLoraRadio 17 | 18 | class ModelAggregatorRadio(ModelLoraRadio): 19 | _modeltag = EModelTag.BASICLORARADIO 20 | 21 | @property 22 | def iName(self) -> str: 23 | """ 24 | @type 25 | str 26 | @desc 27 | A string representing the name of the model class. For example, ModelPower 28 | Note that the name should exactly match to your class name. 29 | """ 30 | return self.__class__.__name__ 31 | 32 | ##REST IS SAME AS BEFORE 33 | 34 | def init_ModelAggregatorRadio( 35 | _ownernodeins: INode, 36 | _loggerins: ILogger, 37 | _modelArgs) -> IModel: 38 | 39 | 40 | assert _ownernodeins is not None 41 | assert _loggerins is not None 42 | assert _modelArgs is not None 43 | 44 | if 'radioID' in _modelArgs: 45 | _radioId = _modelArgs.radioId 46 | else: 47 | _radioId = _ownernodeins.nodeID 48 | 49 | _queueSize = -1 50 | if 'queue_size' in _modelArgs: 51 | _queueSize = _modelArgs.queueSize 52 | 53 | _selfCtrl = True 54 | if 'self_ctrl' in _modelArgs: 55 | _selfCtrl = _modelArgs.self_ctrl 56 | 57 | _radioPhySetup = None 58 | if 'radio_physetup' in _modelArgs: 59 | _radioPhySetup = _modelArgs.radio_physetup 60 | 61 | return ModelAggregatorRadio(_ownernodeins, 62 | _loggerins, 63 | _radioId, 64 | _radioPhySetup, 65 | _queueSize, 66 | _selfCtrl) -------------------------------------------------------------------------------- /src/models/models_radio/modeldownlinkradio.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 22 June 2023 7 | 8 | @desc 9 | This module is an extension of the original LoraRadioModel so that we can distinguish between multiple radio models 10 | This one is for the sat communicating with ground stations and sending beacon messages 11 | ''' 12 | 13 | from src.models.imodel import IModel 14 | from src.nodes.inode import INode 15 | from src.simlogging.ilogger import ILogger 16 | from src.models.models_radio.modelloraradio import ModelLoraRadio 17 | 18 | class ModelDownlinkRadio(ModelLoraRadio): 19 | @property 20 | def iName(self) -> str: 21 | """ 22 | @type 23 | str 24 | @desc 25 | A string representing the name of the model class. For example, ModelPower 26 | Note that the name should exactly match to your class name. 27 | """ 28 | return self.__class__.__name__ 29 | 30 | ##REST IS SAME AS BEFORE 31 | 32 | def init_ModelDownlinkRadio( 33 | _ownernodeins: INode, 34 | _loggerins: ILogger, 35 | _modelArgs) -> IModel: 36 | 37 | 38 | assert _ownernodeins is not None 39 | assert _loggerins is not None 40 | assert _modelArgs is not None 41 | 42 | if 'radioID' in _modelArgs: 43 | _radioId = _modelArgs.radioId 44 | else: 45 | _radioId = _ownernodeins.nodeID 46 | 47 | _queueSize = -1 48 | if 'queue_size' in _modelArgs: 49 | _queueSize = _modelArgs.queueSize 50 | 51 | _selfCtrl = True 52 | if 'self_ctrl' in _modelArgs: 53 | _selfCtrl = _modelArgs.self_ctrl 54 | 55 | _radioPhySetup = None 56 | if 'radio_physetup' in _modelArgs: 57 | _radioPhySetup = _modelArgs.radio_physetup 58 | 59 | return ModelDownlinkRadio(_ownernodeins, 60 | _loggerins, 61 | _radioId, 62 | _radioPhySetup, 63 | _queueSize, 64 | _selfCtrl) -------------------------------------------------------------------------------- /src/models/network/address.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | ''' 5 | class Address(): 6 | def __init__(self, _address): 7 | self.__address = _address 8 | 9 | def get_Address(self): 10 | return self.__address 11 | 12 | def __str__(self) -> str: 13 | return str(self.__address) 14 | 15 | def __eq__(self, _other): 16 | return self.__address == _other.get_Address() -------------------------------------------------------------------------------- /src/models/network/channel.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 06 Jan 2023 7 | @desc 8 | This module implements the base network channel class. 9 | ''' 10 | 11 | from abc import ABC, abstractmethod 12 | 13 | class Channel(ABC): 14 | ''' 15 | This class abstracts out the base functionalities of a network channel. 16 | A radio device uses channel to communicate with other radio devices. 17 | ''' 18 | 19 | @abstractmethod 20 | def add_Device( 21 | self, 22 | _radio) -> bool: 23 | ''' 24 | @desc 25 | Add the radio device to the channel 26 | @param[in] _radio 27 | The radio device instance to add 28 | @return 29 | True: If the device has been added 30 | False: Otherwise 31 | ''' 32 | pass 33 | 34 | @abstractmethod 35 | def get_NumDevices(self) -> int: 36 | ''' 37 | @desc 38 | Get the number of devices part of this channel 39 | @return 40 | Number of devices part of this channel 41 | ''' 42 | pass 43 | 44 | @abstractmethod 45 | def get_Devices(self) -> list: 46 | ''' 47 | @desc 48 | Get the list of radio devices that are part of this channel 49 | @return 50 | List of radio devices 51 | ''' 52 | pass -------------------------------------------------------------------------------- /src/models/network/data/genericdata.py: -------------------------------------------------------------------------------- 1 | """ 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 17 March 2023 7 | 8 | This is a generic data model that can be used to represent any type of data unit generated at the application layer. 9 | """ 10 | 11 | from dataclasses import dataclass, field 12 | from src.utils import Time 13 | import threading 14 | 15 | @dataclass() 16 | class GenericData: 17 | # Time when the data is created 18 | creationTime: Time 19 | 20 | # Node ID of the source where data was generated 21 | sourceNodeID: int 22 | 23 | # size of the data payload in bytes 24 | size: int 25 | 26 | # This works like a counter to generate a new ID for each frame in incremental manner 27 | gloablDataIDCounter: int = field(init=False, default=0) 28 | 29 | # Unique ID of this data unit 30 | id: int = field(init=False) 31 | 32 | def __post_init__(self) -> None: 33 | 34 | with threading.Lock(): 35 | self.id = GenericData.gloablDataIDCounter 36 | GenericData.gloablDataIDCounter += 1 -------------------------------------------------------------------------------- /src/models/network/data/image.py: -------------------------------------------------------------------------------- 1 | """ 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 26 June 2023 7 | 8 | This model represents an image which a satellite might take 9 | """ 10 | 11 | from dataclasses import dataclass, field 12 | from src.utils import Time 13 | from src.models.network.data.genericdata import GenericData 14 | 15 | @dataclass 16 | class Image(GenericData): 17 | pass 18 | #TODO: Add image data (i.e. location of image, etc 19 | -------------------------------------------------------------------------------- /src/models/network/data/sensorappdata.py: -------------------------------------------------------------------------------- 1 | """ 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 26 June 2023 7 | 8 | This model represents a sensor app data from say a sensor node 9 | """ 10 | 11 | from dataclasses import dataclass 12 | from src.models.network.data.genericdata import GenericData 13 | 14 | @dataclass 15 | class SensorAppData(GenericData): 16 | pass 17 | #Same as GenericData. 18 | #TODO: Add more fields if required -------------------------------------------------------------------------------- /src/models/network/frame.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 03 Apr 2023 7 | @desc 8 | This module implements frame class that is used to exchange data between two radio devices 9 | ''' 10 | 11 | from dataclasses import dataclass,field 12 | from src.utils import Time 13 | from src.models.network.address import Address 14 | import threading 15 | 16 | @dataclass 17 | class Frame: 18 | # Unique ID of the frame 19 | id: int = field(init=False, default=0) 20 | 21 | # it works as an incrementing counter to generate unique ID for each frame instance 22 | globalFrameIDCounter: int = field(init=False, default=0) 23 | 24 | # Source adress of the frame 25 | source: Address 26 | 27 | # size of the frame in bytes 28 | size: int 29 | 30 | # payload of the frame in string 31 | payloadString: str = "" 32 | 33 | # When the frame is being transmitted, each device will get it's own instance of the frame. 34 | # This instance ID will be used to identify the frame instance 35 | instanceID: int = 0 36 | 37 | def __post_init__(self) -> None: 38 | 39 | with threading.Lock(): 40 | self.id = Frame.globalFrameIDCounter 41 | Frame.globalFrameIDCounter += 1 42 | 43 | self.__startTransmissionTime: 'Time | None' = None 44 | self.__endTransmissionTime: 'Time | None' = None 45 | self.__PLR = 0.0 46 | self.__PER = 0.0 47 | self.__collidedIDs: 'list[int]' = [] 48 | self.__RSSI = 0.0 49 | 50 | def set_startTransmissionTime(self, time: 'Time') -> None: 51 | self.__startTransmissionTime = time 52 | 53 | def set_endTransmissionTime(self, time: 'Time') -> None: 54 | self.__endTransmissionTime = time 55 | 56 | def get_startTransmissionTime(self) -> 'Time | None': 57 | return self.__startTransmissionTime 58 | 59 | def get_endTransmissionTime(self) -> 'Time | None': 60 | return self.__endTransmissionTime 61 | 62 | def set_startReceptionTime(self, time: 'Time') -> None: 63 | self.__startReceptionTime = time 64 | 65 | def set_endReceptionTime(self, time: 'Time') -> None: 66 | self.__endReceptionTime = time 67 | 68 | def get_startReceptionTime(self) -> 'Time | None': 69 | return self.__startReceptionTime 70 | 71 | def get_endReceptionTime(self) -> 'Time | None': 72 | return self.__endReceptionTime 73 | 74 | def set_PLR(self, PLR: float) -> None: 75 | self.__PLR = PLR 76 | 77 | def get_PLR(self) -> float: 78 | return self.__PLR 79 | 80 | def set_PER(self, PER: float) -> None: 81 | self.__PER = PER 82 | 83 | def get_PER(self) -> float: 84 | return self.__PER 85 | 86 | def set_CR(self, CR: float) -> None: 87 | self.__CR = CR 88 | 89 | def get_CR(self) -> float: 90 | return self.__CR 91 | 92 | def set_BW(self, BW: int) -> None: 93 | self.__BW = BW 94 | 95 | def get_BW(self) -> int: 96 | return self.__BW 97 | 98 | def set_RSSI(self, RSSI: float) -> None: 99 | self.__RSSI = RSSI 100 | 101 | def get_RSSI(self) -> float: 102 | return self.__RSSI 103 | 104 | def set_SNR(self, SNR: float) -> None: 105 | self.__SNR = SNR 106 | 107 | def get_SNR(self) -> float: 108 | return self.__SNR 109 | 110 | def add_collidedID(self, collidedID: int) -> None: 111 | self.__collidedIDs.append(collidedID) 112 | 113 | def get_collidedIDs(self) -> list: 114 | return self.__collidedIDs 115 | 116 | def __str__(self) -> str: 117 | return f"Frame({self.size}, {self.payloadString}, {self.__startTransmissionTime}, {self.__endTransmissionTime})" 118 | 119 | def __repr__(self) -> str: 120 | return self.__str__() -------------------------------------------------------------------------------- /src/models/network/imaging/imagingchannel.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 01 Feb 2023 7 | @desc 8 | This module implements the base network channel class. 9 | ''' 10 | 11 | from src.models.network.channel import Channel 12 | from src.models.network.radiodevice import RadioDevice 13 | 14 | class ImagingChannel(Channel): 15 | ''' 16 | This class implements the LoRa channel inheriting the base channel class 17 | ''' 18 | def __init__(self) -> None: 19 | super().__init__() 20 | self.__devices: 'list[RadioDevice]' = [] 21 | 22 | def add_Device( 23 | self, 24 | _radio) -> bool: 25 | ''' 26 | @desc 27 | Add the radio device to the channel 28 | @param[in] _radio 29 | The radio device instance to add 30 | @return 31 | True: If the device has been added 32 | False: Otherwise 33 | ''' 34 | if len(self.__devices) >= 2: 35 | raise Exception("Imaging channel is only for two devices") 36 | self.__devices.append(_radio) 37 | 38 | def get_NumDevices(self) -> int: 39 | ''' 40 | @desc 41 | Get the number of devices part of this channel 42 | @return 43 | Number of devices part of this channel 44 | ''' 45 | return len(self.__devices) 46 | 47 | def get_Devices(self) -> 'list': 48 | ''' 49 | @desc 50 | Get the list of radio devices that are part of this channel 51 | @return 52 | List of radio devices 53 | ''' 54 | return self.__devices -------------------------------------------------------------------------------- /src/models/network/isl/islchannel.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 22 May 2023 7 | @desc 8 | This module implements the ISL channel class. 9 | ''' 10 | 11 | from src.models.network.channel import Channel 12 | from src.models.network.isl.islradiodevice import ISLRadioDevice 13 | class ISLChannel(Channel): 14 | ''' 15 | This class implements the ISL channel inheriting the base channel class 16 | ''' 17 | def __init__(self) -> None: 18 | super().__init__() 19 | self.__devices: 'list[ISLRadioDevice]' = [] 20 | 21 | def add_Device( 22 | self, 23 | _radio) -> bool: 24 | ''' 25 | @desc 26 | Add the radio device to the channel 27 | @param[in] _radio 28 | The radio device instance to add 29 | @return 30 | True: If the device has been added 31 | False: Otherwise 32 | ''' 33 | self.__devices.append(_radio) 34 | 35 | def get_NumDevices(self) -> int: 36 | ''' 37 | @desc 38 | Get the number of devices part of this channel 39 | @return 40 | Number of devices part of this channel 41 | ''' 42 | return len(self.__devices) 43 | 44 | def get_Devices(self) -> list(): 45 | ''' 46 | @desc 47 | Get the list of radio devices that are part of this channel 48 | @return 49 | List of radio devices 50 | ''' 51 | return self.__devices -------------------------------------------------------------------------------- /src/models/network/isl/isllink.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 22 May 2023 7 | @desc 8 | This module implements the laser link between two sats. 9 | ''' 10 | import math 11 | 12 | from src.models.network.link import Link 13 | 14 | class ISLLink(Link): 15 | def __init__(self, _src, _dstn, _distance): 16 | ''' 17 | @desc 18 | Constructor 19 | @param 20 | _src: Source radio device 21 | _dstn: Destination radio device 22 | _distance: Distance between source and destination 23 | ''' 24 | self.__src: 'LoraRadioDevice' = _src 25 | self.__dstn: 'LoraRadioDevice' = _dstn 26 | self.__distance: float = _distance 27 | 28 | self.__SNR = None #SNR - avoids recalculation 29 | 30 | def get_Src(self) -> 'RadioDevice': 31 | ''' 32 | @desc 33 | Get the source radio device of the link. 34 | @return 35 | Src radio device 36 | ''' 37 | return self.__src 38 | 39 | def get_Dstn(self) -> 'RadioDevice': 40 | ''' 41 | @desc 42 | Get the destination radio device of the link. 43 | @return 44 | Src radio device 45 | ''' 46 | return self.__dstn 47 | 48 | def get_BER(self): 49 | ''' 50 | @desc 51 | Uses the table above to calculate the BER based on the SNR and SF 52 | @return 53 | BER from 0 to 1 54 | ''' 55 | _ber = self.__src.get_PhySetup()['BER'] 56 | return _ber 57 | 58 | def get_PropagationLoss(self) -> float: 59 | ''' 60 | @desc 61 | Get the Propagation Loss of the link in dB 62 | @return 63 | Free space Propagation Loss in dB 64 | ''' 65 | #We don't have a link model for ISL yet 66 | #Datarate is passed by the user 67 | return None 68 | 69 | def get_ReceivedSignalStrength(self) -> float: 70 | ''' 71 | @desc 72 | This method calculates the received signal strength at the receiver based on 73 | the Phy layer setups of transmitter and receiver. 74 | @return 75 | Received signal strength in dB 76 | ''' 77 | #Same as above 78 | return None 79 | 80 | def get_SNR(self) -> float: 81 | ''' 82 | @desc 83 | This method calculates the signal to noise ratio at the reciver end 84 | @param _txPhySetup 85 | Phy layer setup of the transmitter 86 | @param _rxPhySetup 87 | Phy layer setup of the receiver 88 | @param _distance 89 | Distance between the transmitter and receiver 90 | @return 91 | signal to noise ratio 92 | ''' 93 | #Same as above 94 | return None 95 | 96 | 97 | def get_PLR(self) -> float: 98 | ''' 99 | @desc 100 | This method caculates the packet loss rate of the LoRa link. 101 | @return 102 | The normalized packet loss rate 103 | ''' 104 | #Same as above 105 | _plr = 0.0 106 | return _plr 107 | 108 | def get_TimeOnAir( 109 | self, 110 | _frameLength: int)->float: 111 | ''' 112 | @desc 113 | Calculates the time on air for LoRa frame given the modulation config setup and frame length. 114 | @param _frameLength 115 | Length of the frame in bytes 116 | @return 117 | Time on the air in msec 118 | ''' 119 | _radioPhySetup = self.__src.get_PhySetup() 120 | _datarate = _radioPhySetup['datarate'] 121 | 122 | return _frameLength / _datarate * 1000 # convert to msec 123 | 124 | def get_PropagationDelay(self, **kwargs) -> float: 125 | ''' 126 | @desc 127 | Get the Propagation Delay of the link 128 | 129 | @return 130 | Propagation delay in seconds 131 | ''' 132 | return self.__distance / 3e8 133 | 134 | def get_PERFromBER( 135 | self, 136 | allowed_bits_wrong: int, 137 | _size: int) -> float: 138 | """ 139 | @desc 140 | Get the packet error rate (PER) of the frame based on the bit error rate (BER) 141 | @param[in] allowed_bits_wrong 142 | Number of bits that are allowed to be wrong 143 | @param[in] _size 144 | Size of a frame in bytes 145 | @return 146 | PER of the frame from 0 to 1 147 | """ 148 | # convert the size from bytes to bits 149 | _size = _size*8 150 | 151 | # get the bit error rate for this link 152 | _ber = self.get_BER() 153 | 154 | if not 0 <= _ber <= 1: 155 | raise ValueError("BER must be between 0 and 1") 156 | if allowed_bits_wrong < 0 or allowed_bits_wrong > _size: 157 | raise ValueError("Number of allowed bits wrong must be non-negative and less than or equal to the frame size") 158 | 159 | #now we have to use the binomial distribution to calculate the PER 160 | #P(X >= allowed_bits_wrong) = 1 - P(X < allowed_bits_wrong) 161 | prob = 1 162 | p = _ber 163 | q = 1 - p 164 | for _idx in range(allowed_bits_wrong+1): 165 | prob -= math.comb(_size, _idx) * (p ** _idx) * (q ** (_size - _idx)) 166 | 167 | return prob 168 | 169 | 170 | def get_DopplerShift(self, 171 | **kwargs)-> float: 172 | ''' 173 | @desc 174 | Get the doppler shift at the current time for a link between satelliote and ground 175 | @param 176 | kwargs: Keyworded arguments 177 | _frequency: center frequency 178 | _velocity: relative velocity of the satellite (+ve for approaching, -ve for receding) (m/s) 179 | @return 180 | Doppler shift in Hz 181 | ''' 182 | _frequency = kwargs.get('_frequency', None) 183 | _velocity = kwargs.get('_velocity', None) # You can use the API's in the satellite orbit helper model to get the relative velocity 184 | 185 | if _frequency is None: 186 | raise ValueError('Frequency is not provided for calculating the doppler shift') 187 | if _velocity is None: 188 | raise ValueError('Velocity is not provided for calculating the doppler shift') 189 | 190 | return (3e8/(3e8 + _velocity)) * _frequency - _frequency -------------------------------------------------------------------------------- /src/models/network/link.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 06 Jan 2023 7 | @desc 8 | This module implements the base wireless link between two radio devices. 9 | ''' 10 | 11 | from abc import ABC, abstractmethod 12 | from src.models.network.radiodevice import RadioDevice 13 | 14 | class Link(ABC): 15 | ''' 16 | This class abstracts out the concept of wireless link between two radio devices. 17 | ''' 18 | 19 | def get_Src(self) -> RadioDevice: 20 | ''' 21 | @desc 22 | Get the source radio device of the link. 23 | @return 24 | Src radio device 25 | ''' 26 | pass 27 | 28 | def get_Dstn(self) -> RadioDevice: 29 | ''' 30 | @desc 31 | Get the destination radio device of the link. 32 | @return 33 | Src radio device 34 | ''' 35 | pass 36 | 37 | def get_PropagationLoss(self) -> float: 38 | ''' 39 | @desc 40 | Get the Propagation Loss of the link 41 | @return 42 | Propagation Loss 43 | ''' 44 | pass 45 | 46 | def get_PropagationDelay(self) -> float: 47 | ''' 48 | @desc 49 | Get the datarate of thr channel 50 | @return 51 | Data rate 52 | ''' 53 | pass 54 | 55 | def get_TimeOnAir( 56 | self, 57 | _frameLength: int)->float: 58 | ''' 59 | @desc 60 | Calculates the time on air for frame given the modulation config setup and frame length. 61 | @param _frameLength 62 | Length of the frame in bytes 63 | @return 64 | Time on the air in msec 65 | ''' 66 | pass -------------------------------------------------------------------------------- /src/models/network/lora/lorachannel.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 01 Feb 2023 7 | @desc 8 | This module implements the base network channel class. 9 | ''' 10 | 11 | from src.models.network.channel import Channel 12 | from src.models.network.lora.loraradiodevice import LoraRadioDevice 13 | class LoraChannel(Channel): 14 | ''' 15 | This class implements the LoRa channel inheriting the base channel class 16 | ''' 17 | def __init__(self) -> None: 18 | super().__init__() 19 | self.__devices: 'list[LoraRadioDevice]' = [] 20 | 21 | def add_Device( 22 | self, 23 | _radio) -> bool: 24 | ''' 25 | @desc 26 | Add the radio device to the channel 27 | @param[in] _radio 28 | The radio device instance to add 29 | @return 30 | True: If the device has been added 31 | False: Otherwise 32 | ''' 33 | self.__devices.append(_radio) 34 | 35 | def get_NumDevices(self) -> int: 36 | ''' 37 | @desc 38 | Get the number of devices part of this channel 39 | @return 40 | Number of devices part of this channel 41 | ''' 42 | return len(self.__devices) 43 | 44 | def get_Devices(self) -> list: 45 | ''' 46 | @desc 47 | Get the list of radio devices that are part of this channel 48 | @return 49 | List of radio devices 50 | ''' 51 | return self.__devices -------------------------------------------------------------------------------- /src/models/network/lora/loraframe.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 26 July 2023 7 | @desc 8 | This module implements a Lora Frame 9 | ''' 10 | from src.models.network.frame import Frame 11 | 12 | class LoraFrame(Frame): 13 | def set_SF(self, sf: int) -> None: 14 | self.__SF = sf 15 | def get_SF(self) -> int: 16 | return self.__SF 17 | -------------------------------------------------------------------------------- /src/models/network/macdata/genericmac.py: -------------------------------------------------------------------------------- 1 | """ 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 26 June 2023 7 | 8 | This model is a superclass for all the MAC unit 9 | """ 10 | 11 | from dataclasses import dataclass, field 12 | from src.utils import Time 13 | import threading 14 | 15 | @dataclass 16 | class GenericMAC: 17 | # Time when the data unit was created 18 | creationTime: Time 19 | 20 | # ID of the node which created the data unit 21 | sourceRadioID: int 22 | 23 | # Size of the data unit in bytes 24 | size: int 25 | 26 | # ID of the node intended to receive the data unit. -1 if broadcast 27 | intendedRadioID: int 28 | 29 | # Sequence number of the data unit 30 | sequenceNumber: int 31 | 32 | # Incremented every time a new data unit is created 33 | globalMACIDCounter: int = field(init=False, default=0) 34 | 35 | # Unique ID of the MAC unit 36 | id : int = field(init=False) 37 | 38 | @property 39 | def maxsize(self): 40 | # Override this property in the subclass 41 | return 0 42 | 43 | def __post_init__(self) -> None: 44 | with threading.Lock(): 45 | self.id = GenericMAC.globalMACIDCounter 46 | GenericMAC.globalMACIDCounter += 1 47 | 48 | if self.size > self.maxsize: 49 | raise Exception("Size of the MAC unit cannot be greater than the max size") 50 | -------------------------------------------------------------------------------- /src/models/network/macdata/macack.py: -------------------------------------------------------------------------------- 1 | """ 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 26 June 2023 7 | 8 | This model represents an ack data unit. 9 | """ 10 | 11 | from dataclasses import dataclass, field 12 | from src.models.network.macdata.genericmac import GenericMAC 13 | 14 | @dataclass 15 | class MACAck(GenericMAC): 16 | receivedMACDataID: int 17 | 18 | maxsize: int = field(init=False, default=255-4) # 255 bytes - 4 bytes for header 19 | -------------------------------------------------------------------------------- /src/models/network/macdata/macbeacon.py: -------------------------------------------------------------------------------- 1 | """ 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 26 June 2023 7 | 8 | This model represents a beacon data unit which a satellite node broadcasts to all the nodes in its range. 9 | """ 10 | 11 | from dataclasses import dataclass, field 12 | from src.models.network.macdata.genericmac import GenericMAC 13 | 14 | @dataclass 15 | class MACBeacon(GenericMAC): 16 | # TODO: match the beacon data unit with the data from tinygs 17 | numDevicesInView: int = 0 18 | 19 | maxsize: int = field(init=False, default=255-4) # 255 bytes - 4 bytes for header 20 | -------------------------------------------------------------------------------- /src/models/network/macdata/macbulkack.py: -------------------------------------------------------------------------------- 1 | """ 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 26 June 2023 7 | 8 | This model represents an ack data unit. 9 | """ 10 | 11 | from dataclasses import dataclass, field 12 | from src.models.network.macdata.genericmac import GenericMAC 13 | 14 | @dataclass 15 | class MACBulkAck(GenericMAC): 16 | receivedMACDataIDs: int 17 | 18 | maxsize: int = field(init=False, default=255-4) # 255 bytes - 4 bytes for header -------------------------------------------------------------------------------- /src/models/network/macdata/maccontrol.py: -------------------------------------------------------------------------------- 1 | """ 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 26 June 2023 7 | 8 | This model represents an ack data unit. 9 | """ 10 | 11 | from dataclasses import dataclass, field 12 | from src.models.network.macdata.genericmac import GenericMAC 13 | 14 | @dataclass 15 | class MACControl(GenericMAC): 16 | numPacketsToSend: int 17 | 18 | maxsize: int = field(init=False, default=255-4) # 255 bytes - 4 bytes for header -------------------------------------------------------------------------------- /src/models/network/macdata/macdata.py: -------------------------------------------------------------------------------- 1 | """ 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 26 June 2023 7 | 8 | This model represents a mac layer packet which holds data 9 | """ 10 | 11 | from dataclasses import dataclass, field 12 | from src.models.network.macdata.genericmac import GenericMAC 13 | 14 | @dataclass 15 | class MACData(GenericMAC): 16 | dataPayloadString: str 17 | 18 | maxsize: int = field(init=False, default=255-4) # 255 bytes - 4 bytes for header 19 | -------------------------------------------------------------------------------- /src/models/network/radiodevice.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 06 Jan 2023 7 | @desc 8 | This module implements the base network radio device. 9 | ''' 10 | 11 | from abc import ABC, abstractmethod 12 | from src.models.network.channel import Channel 13 | from src.nodes.inode import INode 14 | 15 | class RadioDevice(ABC): 16 | ''' 17 | This class abstract out the base functionalities of a radio device. 18 | A radio device enables communication between nodes. 19 | ''' 20 | @abstractmethod 21 | def get_OwnerNode(self) -> INode: 22 | ''' 23 | @desc 24 | Get the the owner node of the radio 25 | @return 26 | Owner node object 27 | ''' 28 | pass 29 | 30 | @abstractmethod 31 | def get_Address(self): 32 | ''' 33 | @desc 34 | Get the unique address of the radio device 35 | @return 36 | Address of the radio device 37 | ''' 38 | pass 39 | 40 | @abstractmethod 41 | def get_Channels(self) -> 'list[Channel]': 42 | ''' 43 | @desc 44 | Get the channels that this node part of 45 | @return 46 | List of channels 47 | ''' 48 | pass 49 | 50 | @abstractmethod 51 | def is_P2P(self) -> bool: 52 | ''' 53 | @desc 54 | Get to know whether it is a P2P channel 55 | @return 56 | True: If P2P 57 | False: Otherwise 58 | ''' 59 | pass 60 | 61 | @abstractmethod 62 | def is_Broadcast(self) -> bool: 63 | ''' 64 | @desc 65 | Get to know whether it is a broadcast channel 66 | @return 67 | True: If broadcast 68 | False: Otherwise 69 | ''' 70 | pass 71 | 72 | @abstractmethod 73 | def is_Multicast(self) -> bool: 74 | ''' 75 | @desc 76 | Get to know whether it is a multicast channel 77 | @return 78 | True: If Multicast 79 | False: Otherwise 80 | ''' 81 | pass 82 | 83 | @abstractmethod 84 | def is_LinkUp(self): 85 | ''' 86 | @desc 87 | Get to know whether any link of the channel is up 88 | @return 89 | True: If any link is up 90 | False: Otherwise 91 | ''' 92 | pass 93 | 94 | @abstractmethod 95 | def get_MTU(self) -> int: 96 | ''' 97 | @desc 98 | Get maximum transmission unit (MTU) length in bytes. 99 | @return 100 | MTU in bytes 101 | ''' 102 | pass 103 | 104 | @abstractmethod 105 | def get_PhySetup(self) -> 'dict': 106 | ''' 107 | @desc 108 | Get the phy layer setup of the radio device, e.g., antenna configuration, transmission power, frequency, bandwidth, and so on. 109 | @return 110 | A dictionary where key is the name of phy layer parameter. 111 | For example, 112 | { 113 | "txpower": 20.0, 114 | "frequency": 470.0 115 | } 116 | ''' 117 | pass 118 | 119 | @abstractmethod 120 | def is_TxBusy(self) -> bool: 121 | ''' 122 | @desc 123 | Check whether the radio is already busy in transmitting packet 124 | @return 125 | True: If the radio is busy 126 | False: Otherwise 127 | ''' 128 | pass 129 | 130 | @abstractmethod 131 | def is_RxBusy(self) -> bool: 132 | ''' 133 | @desc 134 | Check whether the radio is already busy in receiving packet 135 | @return 136 | True: If the radio is busy 137 | False: Otherwise 138 | ''' 139 | pass 140 | 141 | @abstractmethod 142 | def send( 143 | self, 144 | _payloadSize: int, 145 | _payload: str, 146 | _channelIndex: int) -> bool: 147 | ''' 148 | @desc 149 | This method is used to transfer a frame from this radio device on a given channel 150 | @param[in] _payloadSize 151 | Size of the paylod in bytes 152 | @param[in] _payload 153 | The payload to be sent in the frame. If it's a object, serialize to string before passing it 154 | @param[in] _channelIndex 155 | The index of the channel 156 | @return 157 | True: If the package transmission was successful 158 | False: Otherwise 159 | ''' 160 | pass 161 | 162 | @abstractmethod 163 | def receive(self, _frame) -> bool: 164 | ''' 165 | @desc 166 | This is used to receive any frame from other radios 167 | @param[in] _frame 168 | The frame to receive 169 | @return 170 | True: If the reception is successful 171 | False: Otherwise 172 | ''' 173 | pass 174 | 175 | @abstractmethod 176 | def set_ReceiveCallBack(self, _cbMethod): 177 | ''' 178 | @desc 179 | This methods sets a receive callback for the packet reception event 180 | @param[in] _cbMethod 181 | Method to be call backed 182 | ''' 183 | pass -------------------------------------------------------------------------------- /src/nodes/inode.py: -------------------------------------------------------------------------------- 1 | """ 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 27 Sep 2022 7 | 8 | This module includes the interface definition of the node. 9 | """ 10 | 11 | from abc import ABC, abstractmethod 12 | from enum import Enum 13 | 14 | from src.models.imodel import EModelTag, IModel 15 | from src.utils import Location 16 | from src.utils import Time 17 | 18 | class ENodeType(Enum): 19 | """ 20 | An enum listing the types of the type. 21 | Each node implementation should have a type. 22 | """ 23 | SAT = 0 24 | GS = 1 25 | IOTDEVICE = 2 26 | 27 | class INode(ABC): 28 | """ 29 | This is an interface implementation for the nodes. 30 | Each node implementation such as satellite, ground station, terminal, among others should inherit this interface 31 | """ 32 | 33 | @property 34 | @abstractmethod 35 | def iName(self) -> str: 36 | """ 37 | @type 38 | str 39 | @desc 40 | A string representing the name of the node class. For example, NodeSatellite 41 | Note that the name should exactly match to your class name. 42 | """ 43 | pass 44 | 45 | @property 46 | @abstractmethod 47 | def nodeType(self) -> ENodeType: 48 | """ 49 | @type 50 | ENodeType 51 | @desc 52 | The node type for the implemented node class 53 | """ 54 | pass 55 | 56 | @property 57 | @abstractmethod 58 | def nodeID(self) -> int: 59 | """ 60 | @type 61 | int 62 | @desc 63 | The ID of a node in the topology. It basically distinguishes a node from another node. 64 | """ 65 | pass 66 | 67 | @property 68 | @abstractmethod 69 | def topologyID(self) -> int: 70 | """ 71 | @type 72 | int 73 | @desc 74 | The ID of the topology that the node instance is part of 75 | """ 76 | pass 77 | 78 | @abstractmethod 79 | def get_Position( 80 | self, 81 | _time: Time) -> Location: 82 | """ 83 | @desc 84 | This method returns the position of the node at the time provided in the argument. 85 | The implementing class can keep location as a private variable and return it through this method. 86 | @param[in] _time 87 | The time at which the location is being looked for 88 | @return 89 | Location of the node, if available 90 | otherwise, none 91 | """ 92 | pass 93 | 94 | @property 95 | @abstractmethod 96 | def managerInstance(self): 97 | """ 98 | @type 99 | Manager class 100 | @desc 101 | Manager instance of the simulator that is holding this node instance 102 | """ 103 | pass 104 | 105 | @property 106 | @abstractmethod 107 | def timestamp(self) -> Time: 108 | """ 109 | @type 110 | Time 111 | @desc 112 | Current timestamp of the node instance 113 | """ 114 | pass 115 | 116 | @property 117 | @abstractmethod 118 | def simStartTime(self) -> Time: 119 | """ 120 | @type 121 | Time 122 | @desc 123 | Start timestamp of the node instance for simulation 124 | """ 125 | pass 126 | 127 | @property 128 | @abstractmethod 129 | def simEndTime(self) -> Time: 130 | """ 131 | @type 132 | Time 133 | @desc 134 | End timestamp of the node instance for simulation 135 | """ 136 | pass 137 | 138 | @property 139 | @abstractmethod 140 | def deltaTime(self) -> float: 141 | """ 142 | @type 143 | Float 144 | @desc 145 | time granularity for the simulation of this node (in seconds). 146 | Time gap between two simulation epochs 147 | """ 148 | pass 149 | 150 | @abstractmethod 151 | def Execute(self) -> bool: 152 | """ 153 | @desc 154 | This method executes the models of the node instance one by one. 155 | This is one time execution of the models. 156 | @return 157 | True: If the execution is successful 158 | False: Otherwise 159 | """ 160 | pass 161 | 162 | @abstractmethod 163 | def ExecuteCntd(self): 164 | """ 165 | @desc 166 | This method executes the models of the node instance one by one continuously until 167 | it reaches simulation end time. 168 | """ 169 | pass 170 | 171 | @abstractmethod 172 | def add_Models( 173 | self, 174 | _modelsToAdd: 'list[IModel]'): 175 | """ 176 | @desc 177 | This method adds a model to the node 178 | @param[in] _modelsToAdd 179 | The list of models to be added 180 | """ 181 | pass 182 | 183 | @abstractmethod 184 | def has_ModelWithTag( 185 | self, 186 | _modelTag: EModelTag) -> IModel: 187 | """ 188 | @desc 189 | This method checks whether this node instance has a model implemented having the provided modeltag. 190 | If so, it returns the model. 191 | @param[in] _modelTag 192 | Tag of the model that is being looked for 193 | @return 194 | Instance of the model if it was found. 195 | Otherwise, None 196 | """ 197 | pass 198 | 199 | @abstractmethod 200 | def has_ModelWithName( 201 | self, 202 | _modelName: str) -> IModel: 203 | """ 204 | @desc 205 | This method checks whether this node instance has a model implemented having the provided model implementation name (iName). 206 | If so, it returns the model. 207 | @param[in] _modelName 208 | Implementation name (iName) of the model that is being looked for 209 | @return 210 | Instance of the model if it was found. 211 | Otherwise, None 212 | """ 213 | pass 214 | 215 | @abstractmethod 216 | def update_Position( 217 | self, 218 | _newLocation: Location, 219 | _time: Time): 220 | """ 221 | @desc 222 | This method updates the position of the node against a time provided in the argument 223 | @param[in] _newLocation 224 | New location of the node 225 | @param[in] _time 226 | The time when location is captured 227 | """ 228 | pass 229 | 230 | @abstractmethod 231 | def add_ManagerInstance(_managerIns): 232 | ''' 233 | @desc 234 | Adds manager instance to this node instance 235 | @param[in] _managerIns 236 | Manager instance as IManager 237 | ''' 238 | pass -------------------------------------------------------------------------------- /src/nodes/itopology.py: -------------------------------------------------------------------------------- 1 | """ 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 27 Sep 2022 7 | 8 | This module includes the interface implementation of topology. 9 | """ 10 | 11 | from abc import ABC, abstractmethod 12 | from src.nodes.inode import INode, ENodeType 13 | 14 | class ITopology(ABC): 15 | """ 16 | This is an interface for the node topology. Learn more about topology from README.md 17 | """ 18 | 19 | @property 20 | @abstractmethod 21 | def id(self) -> int: 22 | ''' 23 | @type 24 | Integer 25 | @desc 26 | ID of the topology. Each topology should have an unique ID 27 | ''' 28 | pass 29 | 30 | @property 31 | @abstractmethod 32 | def name(self) -> str: 33 | ''' 34 | @type 35 | String 36 | @desc 37 | Name of the topology 38 | ''' 39 | pass 40 | 41 | @abstractmethod 42 | def add_Node( 43 | self, 44 | _node: INode): 45 | ''' 46 | @desc 47 | Adds the node given in the argument to the list 48 | @param[in] _node 49 | Node to be added to the list 50 | ''' 51 | pass 52 | 53 | @abstractmethod 54 | def get_Node( 55 | self, 56 | _nodeId: int) -> INode: 57 | ''' 58 | @desc 59 | Get a node from this topology with node id. 60 | @param[in] _nodeId 61 | ID of the node that is being looked for 62 | @return 63 | INode instance of the node 64 | ''' 65 | pass 66 | 67 | @abstractmethod 68 | def get_NodesOfAType( 69 | self, 70 | _nodeType: ENodeType) -> 'list[INode]': 71 | ''' 72 | @desc 73 | Get the list of all nodes of a type provided in the argument 74 | @param[in] _nodeType 75 | Type of the node 76 | @return 77 | List of the nodes 78 | ''' 79 | pass 80 | 81 | @property 82 | @abstractmethod 83 | def nodes(self) -> 'list[INode]': 84 | ''' 85 | @type 86 | List of INode 87 | @desc 88 | All the nodes of this topology instance 89 | ''' 90 | pass 91 | -------------------------------------------------------------------------------- /src/nodes/topology.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 12 Oct 2022 7 | @desc 8 | This module implements the Topology class that inherits the ITopology 9 | ''' 10 | from io import StringIO 11 | 12 | from src.nodes.inode import ENodeType, INode 13 | from src.nodes.itopology import ITopology 14 | 15 | class Topology(ITopology): 16 | ''' 17 | Topology class that holds the nodes. It inherits the ITopology interface. 18 | ''' 19 | __nodes: 'list[INode]' 20 | __id: int 21 | __name: str 22 | 23 | @property 24 | def id(self) -> int: 25 | ''' 26 | @type 27 | Integer 28 | @desc 29 | ID of the topology. Each topology should have an unique ID 30 | ''' 31 | return self.__id 32 | 33 | @property 34 | def name(self) -> str: 35 | ''' 36 | @type 37 | String 38 | @desc 39 | Name of the topology 40 | ''' 41 | return self.__name 42 | 43 | def add_Node( 44 | self, 45 | _node: INode): 46 | ''' 47 | @desc 48 | Adds the node given in the argument to the list 49 | @param[in] _node 50 | Node to be added to the list 51 | ''' 52 | if(_node is not None): 53 | self.__nodes.append(_node) 54 | if _node.nodeID not in self.__nodeIDToNodeMap: 55 | self.__nodeIDToNodeMap[_node.nodeID] = _node 56 | else: 57 | raise Exception("Node ID already exists in the topology") 58 | 59 | def get_Node( 60 | self, 61 | _nodeId: int) -> INode: 62 | ''' 63 | @desc 64 | Get a node from this topology with node id. 65 | @param[in] _nodeId 66 | ID of the node that is being looked for 67 | @return 68 | INode instance of the node. None if not found 69 | ''' 70 | return self.__nodeIDToNodeMap.get(_nodeId, None) 71 | 72 | def get_NodesOfAType( 73 | self, 74 | _nodeType: ENodeType) -> 'list[INode]': 75 | ''' 76 | @desc 77 | Get the list of all nodes of a type provided in the argument 78 | @param[in] _nodeType 79 | Type of the node 80 | @return 81 | List of the nodes 82 | ''' 83 | _ret: 'list[INode]' = [] 84 | for _node in self.__nodes: 85 | if(_node.nodeType == _nodeType): 86 | _ret.append(_node) 87 | return _ret 88 | 89 | @property 90 | def nodes(self) -> 'list[INode]': 91 | ''' 92 | @type 93 | List of INode 94 | @desc 95 | All the nodes of this topology instance 96 | ''' 97 | return self.__nodes 98 | 99 | def __init__( 100 | self, 101 | _name: str, 102 | _id: int) -> None: 103 | ''' 104 | @desc 105 | Constructor of the topology 106 | @param[in] _name 107 | Name of the topology 108 | @param[in] _id 109 | ID of the topology 110 | ''' 111 | self.__name = _name 112 | self.__id = _id 113 | self.__nodes = [] 114 | self.__nodeIDToNodeMap = {} 115 | 116 | def __str__(self) -> str: 117 | ''' 118 | @desc 119 | Overriding the __str__() method 120 | ''' 121 | _string = "".join(["Topology ID: ", str(self.__id), ", ", 122 | "Topology name: ", self.__name, ", ", 123 | "Number of nodes: ", str(len(self.__nodes)), "\n"]) 124 | 125 | _stringIOObject = StringIO(_string) 126 | for _node in self.__nodes: 127 | _stringIOObject.write(_node.__str__()) 128 | 129 | return _stringIOObject.getvalue() -------------------------------------------------------------------------------- /src/sim/README.md: -------------------------------------------------------------------------------- 1 | # Working with the simulator 2 | 3 | ## Creation and execution 4 | The [`Simulator`](/src/sim/simulator.py) class serves as a comprehensive interface to our simulator, providing a seamless experience from creation to execution, including runtime control. When provided with the [config file](/configs/README.md) as input, the `Simulator` class orchestrates the simulation environment and delegates the environment to the manager for executing the simulation. Users can effortlessly utilize the simulator by creating an instance of this class. 5 | 6 | ```Python 7 | from src.sim.simulator import Simulator 8 | 9 | _configFilePath = "configs/config.json" 10 | 11 | _sim = Simulator(_configFilePath) 12 | _sim.execute() 13 | ``` 14 | 15 | Internally, the `Simulator` class relies on two core classes: [`Orchestrator`](/src/sim/orchestrator.py) and [`Manager`](/src/sim/imanager.py). 16 | 17 | The `Orchestrator` class is responsible for creating the simulation environment. Its main tasks include reading the config file, creating nodes with the specified models, resolving model dependencies, and allocating resources (e.g., threads, containers, virtual machines) to the nodes. To achieve this, the `Orchestrator` class refers to the [nodeinits](/src/sim/nodeinits.py) and [modelinits](/src/sim/modelinits.py) files to find the appropriate initialization methods for creating node and model instances based on the configuration in the config file. 18 | 19 | On the other hand, the `Manager` class takes the simulation environment created by the `Orchestrator` class and executes the operations of the nodes by invoking their `Execute()` method. The `Manager` class handles the runtime operation of the simulator. 20 | 21 | Please take a look at the [class diagram](/figs/Class_diagram.pdf) for better understanding. 22 | 23 | ## Calling runtime APIs 24 | The simulator offers the option of calling runtime APIs through the `call_RuntimeAPIs()` method of `Simulator` class. To enable this, you need to execute the simulator in a separate thread. Thn, in a different thread, you can invoke `call_RuntimeAPIs()`. 25 | ```Python 26 | from src.sim.simulator import Simulator 27 | import threading 28 | 29 | _configFilePath = "configs/config.json" 30 | 31 | _sim = Simulator(_configFilePath) 32 | 33 | #run execute method in a separate thread 34 | _thread_sim = threading.Thread(target=_sim.execute) 35 | 36 | #call runtime APIs 37 | _sim.call_RuntimeAPIs(...) 38 | 39 | ``` -------------------------------------------------------------------------------- /src/sim/imanager.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 19 Oct 2022 7 | @desc 8 | This module implements the manager interface for the simulator 9 | ''' 10 | 11 | from abc import ABC, abstractmethod 12 | from enum import Enum 13 | 14 | class EManagerReqType(Enum): 15 | ''' 16 | An enum listing the request types that a manager handles 17 | ''' 18 | GET_TOPOLOGIES = 0 19 | GET_TIMESTEPLENGTH = 1 20 | 21 | class IManager(ABC): 22 | ''' 23 | Interface of the simulation runtime manager. 24 | It handles the simulation in the runtime. 25 | ''' 26 | @abstractmethod 27 | def req_Manager( 28 | self, 29 | _reqType: EManagerReqType, 30 | **_kwargs): 31 | ''' 32 | @desc 33 | Send a request to the manager through this method 34 | @param[in] _reqType 35 | Type of the request 36 | @param[in] _kwargs 37 | Keyworded arguments to be passed to the request handler function. 38 | Take a look at the request handler function definition to know the keyworded argument lists pertinent to that 39 | @return 40 | Returns the results (if any) 41 | ''' 42 | pass 43 | 44 | 45 | def run_Sim(self): 46 | ''' 47 | @desc 48 | This method is called to run the simulation. 49 | ''' 50 | pass 51 | 52 | -------------------------------------------------------------------------------- /src/sim/loggerinits.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 31 Oct 2022 7 | @desc 8 | In this module, we list the initialization methods for different logger class implementations. 9 | Initialization method must be written in the same module as where class implementation is written. 10 | The initialization method must be added in the dictionary below as the value against the key as the name of class 11 | The prototype of the initialization method goes below. 12 | 13 | init_LoggerFile(__loglevel : ELogType, __logGeneratorName : str, __simsetupDetails) -> ILogger: 14 | @desc 15 | This method initializes an instance of LoggerCmd class and returns 16 | @param[in] __loglevel 17 | Depending on the log level of a logger it handles the log message type 18 | For example, if logLevel = LOGERROR, it handles log messages of LOGERROR type 19 | @param[in] __logGeneratorName 20 | Name of the log generator. It could be the name of the instance that generates the log message for this logger 21 | @param[in] __simsetupDetails 22 | It's a converted JSON object containing the node related info. 23 | The JSON object must have the literals as follows (values are given as example). 24 | { 25 | give example of the literals that your initialization methods look for 26 | } 27 | @return 28 | Logger class instance 29 | ''' 30 | 31 | from src.simlogging.ilogger import ELogType 32 | 33 | # import the logger classes here 34 | from src.simlogging.loggercmd import init_LoggerCmd 35 | from src.simlogging.loggerfile import init_LoggerFile 36 | from src.simlogging.loggerfilechunkwise import init_LoggerFileChunkwise 37 | 38 | 39 | loggerInitDictionary = { 40 | "LoggerCmd" : init_LoggerCmd, 41 | "LoggerFile": init_LoggerFile, 42 | "LoggerFileChunkwise": init_LoggerFileChunkwise 43 | } 44 | 45 | loggerTypeDictionary = { 46 | "error" : ELogType.LOGERROR, 47 | "warn" : ELogType.LOGWARN, 48 | "debug" : ELogType.LOGDEBUG, 49 | "info" : ELogType.LOGINFO, 50 | "logic" : ELogType.LOGLOGIC, 51 | "all" : ELogType.LOGALL 52 | } -------------------------------------------------------------------------------- /src/sim/modelinits.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 31 Oct 2022 7 | @desc 8 | In this module, we list the initialization methods for different model class implementations. 9 | Initialization method must be written in the same module as where class implementation is written. 10 | The initialization method must be added in the dictionary below as the value against the key as the name of the class 11 | The prototype of the initialization method goes below. 12 | 13 | init_Classname(_ownernodeins:INode, _loggerins:ILogger, _modelArgs) -> IModel 14 | Here, 15 | @desc 16 | This method initializes an instance of ModelOrbit class 17 | @param[in] _ownernodeins 18 | Instance of the owner node that incorporates this model instance 19 | @param[in] _loggerins 20 | Logger instance 21 | @param[in] _modelArgs 22 | It's a converted JSON object containing the model related info. 23 | The JSON object must have the literals as follows (values are given as example). 24 | { 25 | "tle_1": "1 50985U 22002B 22290.71715197 .00032099 00000+0 13424-2 0 9994", 26 | "tle_2": "2 50985 97.4784 357.5505 0011839 353.6613 6.4472 15.23462773 42039", 27 | } 28 | @return 29 | Instance of the model class 30 | ''' 31 | 32 | # import the node class here 33 | from src.models.models_orbital.modelorbit import init_ModelOrbit 34 | from src.models.models_orbital.modelorbitonefullupdate import init_ModelOrbitOneFullUpdate 35 | from src.models.models_orbital.modelfixedorbit import init_ModelFixedOrbit 36 | 37 | from src.models.models_fov.modelhelperfov import init_ModelHelperFoV 38 | from src.models.models_fov.modelfovtimebased import init_ModelFovTimeBased 39 | 40 | from src.models.models_power.modelpower import init_ModelPower 41 | 42 | from src.models.models_radio.modelisl import init_ModelISL 43 | 44 | from src.models.models_radio.modelloraradio import init_ModelLoraRadio 45 | from src.models.models_radio.modelaggregatorradio import init_ModelAggregatorRadio 46 | from src.models.models_radio.modeldownlinkradio import init_ModelDownlinkRadio 47 | from src.models.models_radio.modelimagingradio import init_ModelImagingRadio 48 | 49 | from src.models.models_data.modeldatagenerator import init_ModelDataGenerator 50 | from src.models.models_data.modeldatarelay import init_ModelDataRelay 51 | from src.models.models_data.modeldatastore import init_ModelDataStore 52 | 53 | from src.models.models_mac.modelmacttnc import init_ModelMACTTnC 54 | from src.models.models_mac.modelmacgateway import init_ModelMACgateway 55 | from src.models.models_mac.modelmaciot import init_ModelMACiot 56 | from src.models.models_mac.modelmacgs import init_ModelMACgs 57 | 58 | from src.models.models_scheduling.modelcompute import init_ModelCompute 59 | from src.models.models_scheduling.modeledgecompute import init_ModelEdgeCompute 60 | 61 | from src.models.models_tumbling.modeladacs import init_ModelADACS 62 | 63 | from src.models.models_imaging.modelimaginglogicbased import init_ModelImagingLogicBased 64 | 65 | modelInitDictionary = { 66 | "ModelOrbit" : init_ModelOrbit, 67 | "ModelOrbitOneFullUpdate": init_ModelOrbitOneFullUpdate, 68 | "ModelFixedOrbit": init_ModelFixedOrbit, 69 | 70 | "ModelHelperFoV": init_ModelHelperFoV, 71 | "ModelFovTimeBased": init_ModelFovTimeBased, 72 | 73 | "ModelDataGenerator": init_ModelDataGenerator, 74 | 75 | "ModelPower": init_ModelPower, 76 | 77 | "ModelISL": init_ModelISL, 78 | 79 | "ModelLoraRadio": init_ModelLoraRadio, 80 | "ModelAggregatorRadio": init_ModelAggregatorRadio, 81 | "ModelDownlinkRadio": init_ModelDownlinkRadio, 82 | "ModelImagingRadio": init_ModelImagingRadio, 83 | 84 | "ModelDataStore": init_ModelDataStore, 85 | "ModelDataRelay": init_ModelDataRelay, 86 | 87 | "ModelMACTTnC": init_ModelMACTTnC, 88 | "ModelMACgateway": init_ModelMACgateway, 89 | "ModelMACiot": init_ModelMACiot, 90 | "ModelMACgs": init_ModelMACgs, 91 | 92 | "ModelCompute": init_ModelCompute, 93 | "ModelEdgeCompute": init_ModelEdgeCompute, 94 | 95 | "ModelADACS": init_ModelADACS, 96 | 97 | "ModelImagingLogicBased": init_ModelImagingLogicBased 98 | } -------------------------------------------------------------------------------- /src/sim/nodeinits.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 31 Oct 2022 7 | @desc 8 | In this module, we list the initialization methods for different node class implementations. 9 | Initialization method must be written in the same module as where class implementation is written. 10 | The initialization method must be added in the dictionary below as the value against the key as "iname" (implementation name) of the class 11 | The prototype of the initialization method goes below. 12 | 13 | init_ClassName(__nodeDetails, __timeDetails, __topologyID, __logger, _managerInstance) -> INode 14 | Here, 15 | @param[in] __nodeDetails 16 | It's a converted JSON object containing the node related info. 17 | The JSON object must have the literals as follows (values are given as example). 18 | { 19 | give example of the literals that your initialization methods look for 20 | } 21 | @param[in] __timeDetails 22 | It's a converted JSON object containing the simulation timing related info. 23 | The JOSN object must have the literals as follows (values are given as example). 24 | { 25 | give example of the literals that your initialization methods look for 26 | } 27 | @param[in] __topologyID 28 | The ID of the topology that the node instance is part of 29 | @param[in] __logger 30 | Logger instance 31 | @return 32 | Created instance of the class 33 | ''' 34 | 35 | # import the node class here 36 | from src.nodes.satellitebasic import init_SatelliteBasic 37 | from src.nodes.gsbasic import init_GSBasic 38 | from src.nodes.iotbasic import init_IoTBasic 39 | 40 | 41 | nodeInitDictionary = { 42 | "SatelliteBasic" : init_SatelliteBasic, 43 | "GSBasic": init_GSBasic, 44 | "IoTBasic": init_IoTBasic 45 | } -------------------------------------------------------------------------------- /src/sim/simulator.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 07 Nov 2022 7 | @desc 8 | This module implements the simulator class. It's the face of simulation pipeline. 9 | ''' 10 | 11 | from src.sim.orchestrator import Orchestrator 12 | from src.sim.imanager import IManager 13 | from src.sim.managerparallel import ManagerParallel 14 | 15 | 16 | class Simulator(): 17 | ''' 18 | This is the entry class to our simulation pipeline. 19 | It invokes the orchestrator and hands over the simulation environment to the manager. 20 | ''' 21 | _configFilePath: str 22 | _orchestrator: Orchestrator 23 | _manager: IManager 24 | 25 | def __init__( 26 | self, 27 | _configfilepath: str, 28 | _numWorkers: int = 1) -> None: 29 | ''' 30 | @desc 31 | Constructor of the simulator class. 32 | @param[in] _configfilepath 33 | File path to the configuration file 34 | @param[in] _numWorkers 35 | Number of workers to be used for parallel execution 36 | ''' 37 | self.__configFilePath = _configfilepath 38 | 39 | # invoke the orchestrator to create the simulation environment 40 | self.__orchestrator = Orchestrator(self.__configFilePath) 41 | self.__orchestrator.create_SimEnv() 42 | __simEnv = self.__orchestrator.get_SimEnv() 43 | 44 | # hand over the simulation environment to the manager 45 | self.__manager = ManagerParallel( 46 | topologies = __simEnv[0], 47 | numOfSimSteps = __simEnv[1], 48 | numOfWorkers = _numWorkers 49 | ) 50 | 51 | def call_RuntimeAPIs(self, 52 | _api: str, 53 | **_kwargs): 54 | ''' 55 | This method acts as a runtime API interface of the manager. 56 | An API offered by the manager can be invoked through this method in runtime. 57 | @param[in] _api 58 | Name of the API. Each model should have a list of the API names. 59 | @param[in] _kwargs 60 | Keyworded arguments that are passed to the corresponding API handler 61 | @return 62 | The API return 63 | ''' 64 | 65 | _ret = None 66 | # check that manager is not None 67 | if(self.__manager is None): 68 | raise Exception("[Simulator]: Manager is not initialized") 69 | 70 | #check that the API name is not None 71 | if(_api is None): 72 | raise Exception("[Simulator]: API name needs to be provided") 73 | 74 | # call the API from the manager 75 | try: 76 | _ret = self.__manager.call_APIs(_api, **_kwargs) 77 | except Exception as e: 78 | raise Exception(f"[Simulator]: The API call returned an exception: {e}") 79 | 80 | return _ret 81 | 82 | def execute(self): 83 | ''' 84 | @desc 85 | Executes the simulation 86 | ''' 87 | self.__manager.run_Sim() -------------------------------------------------------------------------------- /src/simlogging/ilogger.py: -------------------------------------------------------------------------------- 1 | """ 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 29 Sep 2022 7 | 8 | This module includes the interface definition of the logger. 9 | """ 10 | 11 | from enum import Enum 12 | from abc import ABC, abstractmethod 13 | from src.utils import Time 14 | 15 | class ELogType(Enum): 16 | ''' 17 | An enum listing the types of log. 18 | Each log message should have one of these types 19 | ''' 20 | LOGERROR = 0 21 | LOGWARN = 1 22 | LOGINFO = 2 23 | LOGDEBUG = 3 24 | LOGLOGIC = 4 25 | LOGALL = 5 26 | 27 | class ILogger(ABC): 28 | ''' 29 | This is an interface implementation of the logger. 30 | ''' 31 | @property 32 | @abstractmethod 33 | def logTypeLevel(self) -> ELogType: 34 | ''' 35 | @type 36 | ELogType 37 | @desc 38 | Depending on the log type level of a logger it handles the log message type 39 | For example, if logTypeLevel = LOGERROR, it handles log messages of LOGERROR type 40 | ''' 41 | pass 42 | 43 | @abstractmethod 44 | def write_Log( 45 | self, 46 | _message: str, 47 | _logType: ELogType, 48 | _timeStamp: Time, 49 | _modelName: str) -> bool: 50 | ''' 51 | @desc 52 | This method writes log message passed in the argument 53 | @param[in] _message 54 | Log message in string format 55 | @param[in] _logType 56 | Type of the log message 57 | @param [in] _timeStamp 58 | Time stamp for the log message 59 | @param [in] _modelName 60 | Name of the instance that is writing the log message 61 | ''' 62 | pass -------------------------------------------------------------------------------- /src/simlogging/loggercmd.py: -------------------------------------------------------------------------------- 1 | """ 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 30 Sep 2022 7 | 8 | This module implements a logger that uses python print function for logging 9 | """ 10 | 11 | from src.simlogging.ilogger import ELogType, ILogger 12 | from src.utils import Time 13 | 14 | class LoggerCmd(ILogger): 15 | ''' 16 | This class inherits the ILogger interface. 17 | It writes the log in cmd interface. 18 | ''' 19 | __loggeneratorname: str 20 | __logTypeLevel: ELogType 21 | 22 | def write_Log( 23 | self, 24 | _message: str, 25 | _logType: ELogType, 26 | _timeStamp: Time = None, 27 | _modelName: str = None,) -> bool: 28 | ''' 29 | @desc 30 | This method writes log message passed in the argument 31 | @param[in] _message 32 | Log message in string format 33 | @param[in] _logType 34 | Type of the log message 35 | @param [in] _timeStamp 36 | Time stamp for the log message 37 | @param[in] _modelName 38 | Name of the model that generates the log message 39 | ''' 40 | _ret = False 41 | #check whether the log type of the message can be handled by this logger instance 42 | if (self.__logTypeLevel == ELogType.LOGALL or 43 | self.__logTypeLevel == _logType): 44 | _logMessage = "".join(["[", _logType.__str__(), "]", ", ", 45 | self.__loggeneratorname, ", ", 46 | (_timeStamp.to_str() if _timeStamp is not None else "NTA"), ", ", 47 | (_modelName if _modelName is not None else "NMA"), ": ", 48 | _message , "\n"]) 49 | print(_logMessage) 50 | _ret = True 51 | return _ret 52 | 53 | def __init__( 54 | self, 55 | _logLevel: ELogType, 56 | _logGeneratorName: str) -> None: 57 | ''' 58 | @desc 59 | Constructor of the class. 60 | @param[in] _logLevel 61 | Depending on the log level of a logger it handles the log message type 62 | For example, if logLevel = LOGERROR, it handles log messages of LOGERROR type 63 | @param[in] _logGeneratorName 64 | Name of the log generator. It could be the name of the instance that generates the log message for this logger 65 | ''' 66 | self.__logTypeLevel = _logLevel 67 | self.__loggeneratorname = _logGeneratorName 68 | 69 | @property 70 | def logTypeLevel(self) -> ELogType: 71 | ''' 72 | @type 73 | ELogType 74 | @desc 75 | Depending on the log type level of a logger it handles the log message type 76 | For example, if logTypeLevel = LOGERROR, it handles log messages of LOGERROR type 77 | ''' 78 | return self.__logTypeLevel 79 | 80 | def init_LoggerCmd( 81 | _loglevel: ELogType, 82 | _logGeneratorName: str, 83 | __simsetupDetails) -> ILogger: 84 | ''' 85 | @desc 86 | This method initializes an instance of LoggerCmd class and returns 87 | @param[in] _loglevel 88 | Depending on the log level of a logger it handles the log message type 89 | For example, if logLevel = LOGERROR, it handles log messages of LOGERROR type 90 | @param[in] _logGeneratorName 91 | Name of the log generator. It could be the name of the instance that generates the log message for this logger 92 | @param[in] _simsetupDetails 93 | It's a converted JSON object containing the node related info. 94 | The JSON object must have the literals as follows (values are given as example). 95 | { 96 | for this class no info is required 97 | } 98 | ''' 99 | assert _loglevel is not None 100 | assert _logGeneratorName != "" 101 | 102 | return LoggerCmd(_loglevel, _logGeneratorName) 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /src/simlogging/loggerfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 30 Sep 2022 7 | 8 | This module implements a logger that uses a separate file for each instance creator to dump its log 9 | """ 10 | 11 | from src.simlogging.ilogger import ELogType, ILogger 12 | from src.utils import Time 13 | import os 14 | 15 | class LoggerFile(ILogger): 16 | ''' 17 | This class inherits the ILogger interface. 18 | It writes the log in a dedicated file for each instance. 19 | ''' 20 | __fileExtension = '.log' 21 | __filePath: str 22 | __logTypeLevel: ELogType 23 | 24 | def write_Log( 25 | self, 26 | _message: str, 27 | _logType: ELogType, 28 | _timeStamp: Time = None, 29 | _modelName: str = None ) -> bool: 30 | ''' 31 | @desc 32 | This method writes log message passed in the argument 33 | @param[in] _message 34 | Log message in string format 35 | @param[in] _logType 36 | Type of the log message 37 | @param [in] _timeStamp 38 | Time stamp for the log message 39 | @param[in] _modelName 40 | Name of the model that generates the log message 41 | ''' 42 | _ret = False 43 | 44 | #print(_message, _logType, _timeStamp, _modelName) 45 | #print(self.__logTypeLevel, _logType, self.__logTypeLevel.value, _logType.value, self.__logTypeLevel.value >= _logType.value) 46 | 47 | #check whether the log type of the message can be handled by this logger instance 48 | if (self.__logTypeLevel == ELogType.LOGALL or 49 | self.__logTypeLevel.value >= _logType.value): 50 | #check whether log directory exists 51 | if(os.path.isfile(self.__filePath)): 52 | try: 53 | with open(self.__filePath, "a") as _file: 54 | 55 | _logmessage = "".join(["[", _logType.__str__(), "]", ", ", 56 | (_timeStamp.to_str() if _timeStamp is not None else "NTA"), ", ", 57 | (_modelName if _modelName is not None else "NMA"), ", \"", 58 | _message , "\" \n"]) 59 | 60 | _file.write(_logmessage) 61 | 62 | _ret = True 63 | 64 | except: 65 | raise Exception(f"[Simulator Exception] Couldn't open the log file at {self.__filePath}") 66 | else: 67 | raise Exception(f"[Simulator Exception] Couldn't find the log file at {self.__filePath}" ) 68 | 69 | return _ret 70 | 71 | def __init__( 72 | self, 73 | _logLevel: ELogType, 74 | _logGeneratorName: str, 75 | _logDir: str) -> None: 76 | ''' 77 | @desc 78 | Constructor of the class. 79 | @param[in] _logLevel 80 | Depending on the log level of a logger it handles the log message type 81 | For example, if logLevel = LOGERROR, it handles log messages of LOGERROR type 82 | @param[in] _logGeneratorName 83 | Name of the log generator. It could be the name of the instance that generates the log message for this logger 84 | @param[in] _logDir 85 | Path to the directory where the log will be saved 86 | ''' 87 | self.__logTypeLevel = _logLevel 88 | self.__filePath = _logDir + "/" + "Log_" + _logGeneratorName + self.__fileExtension 89 | 90 | # check whether the log directory exists. If not, create one 91 | if(not os.path.isdir(_logDir)): 92 | os.mkdir(_logDir) # let it throw exception if it can't create the directory 93 | 94 | # create the file 95 | try: 96 | __file = open (self.__filePath, "w") 97 | __file.write("logLevel, timestamp, modelName, message\n") 98 | __file.close() 99 | except: 100 | raise Exception("[Simulator Exception] Couldn't create the log file.") 101 | 102 | @property 103 | def logTypeLevel(self) -> ELogType: 104 | ''' 105 | @type 106 | ELogType 107 | @desc 108 | Depending on the log type level of a logger it handles the log message type 109 | For example, if logTypeLevel = LOGERROR, it handles log messages of LOGERROR type 110 | ''' 111 | return self.__logTypeLevel 112 | 113 | def init_LoggerFile( 114 | _loglevel: ELogType, 115 | _logGeneratorName: str, 116 | _simsetupDetails) -> ILogger: 117 | ''' 118 | @desc 119 | This method initializes an instance of LoggerCmd class and returns 120 | @param[in] _loglevel 121 | Depending on the log level of a logger it handles the log message type 122 | For example, if logLevel = LOGERROR, it handles log messages of LOGERROR type 123 | @param[in] _logGeneratorName 124 | Name of the log generator. It could be the name of the instance that generates the log message for this logger 125 | @param[in] _simsetupDetails 126 | It's a converted JSON object containing the node related info. 127 | The JSON object must have the literals as follows (values are given as example). 128 | { 129 | "logfolder": "C:\\spacesim\logs" 130 | } 131 | ''' 132 | assert _loglevel is not None 133 | assert _logGeneratorName != "" 134 | assert _simsetupDetails is not None 135 | assert _simsetupDetails.logfolder != "" 136 | 137 | return LoggerFile( 138 | _loglevel, 139 | _logGeneratorName, 140 | _simsetupDetails.logfolder) 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /src/test/README.md: -------------------------------------------------------------------------------- 1 | # Unit test 2 | We use the [*unittest*](https://docs.python.org/3/library/unittest.html) framework provided by Python. The recommend practice is writing a dedicated unit test class in a module for each newly implemented node, model, and any other significant class. The test class incorporates the corresponding test cases. You can find some example test classes [here](/src/test/). If you need a specific *config* file for your test class setup, please add the same under [test config folder](/configs/testconfigs/) having the test class name in the file name. 3 | 4 | Once your test class is ready, you can create a test suite for it in the [*runtests.py*](/runtests.py) file. The test suite may include multiple relevant test modules. The objective here is having single point for running all the unit tests and customize the test environment. One can just run the [*runtests.py*](/runtests.py) to validate that things are not broken. -------------------------------------------------------------------------------- /src/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/CosmicBeats-Simulator/dc3fff419f454bd66975fade54dbd7985d336181/src/test/__init__.py -------------------------------------------------------------------------------- /src/test/test_datageneration.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 17 March 2023 7 | @desc 8 | This module implements the test cases for the data generation model 9 | Since this is a random process, we will check that over many timesteps, 10 | the expected number of frames generated approximately matches the expected value (set to 5 per second in this case) 11 | ''' 12 | 13 | import unittest 14 | from src.sim.orchestrator import Orchestrator 15 | from src.sim.imanager import IManager, EManagerReqType 16 | from src.sim.managerparallel import ManagerParallel 17 | from src.models.imodel import IModel, EModelTag 18 | from src.nodes.itopology import ITopology 19 | from src.nodes.inode import ENodeType 20 | from src.models.network.frame import Frame 21 | import os 22 | import numpy as np 23 | 24 | class testdatagenerationmodel(unittest.TestCase): 25 | def setUp(self) -> None: 26 | _orchestrator = Orchestrator(os.path.join(os.getcwd(), "configs/testconfigs/config_testgenerator.json")) 27 | _orchestrator.create_SimEnv() 28 | _simEnv = _orchestrator.get_SimEnv() 29 | 30 | # hand over the simulation environment to the manager 31 | self.__manager = ManagerParallel(topologies = _simEnv[0], numOfSimSteps = _simEnv[1], numOfWorkers = 1) 32 | 33 | self.__topologies = self.__manager.req_Manager(EManagerReqType.GET_TOPOLOGIES) 34 | self.__model = self.__topologies[0].nodes[0].has_ModelWithTag(EModelTag.DATAGENERATOR) 35 | 36 | def test_basic(self) -> None: 37 | #Since this is a random process, let's check that the expected number of frames generated is 5 per second 38 | 39 | _dataQueue = self.__model.call_APIs("get_Queue") 40 | self.assertEqual(_dataQueue.qsize(), 0) 41 | 42 | _atEachStep = [0] #list of number of frames generated at each step (expected 5*500 = 2500 frames) 43 | for i in range(0, 500): 44 | self.__model.Execute() 45 | _atEachStep.append(_dataQueue.qsize()) 46 | print(_atEachStep[-1]) 47 | 48 | #let's find the average difference between the number of frames generated at each step 49 | _diff = [_atEachStep[i+1] - _atEachStep[i] for i in range(0, len(_atEachStep)-1)] 50 | _avgDiff = sum(_diff)/len(_diff) 51 | self.assertAlmostEqual(_avgDiff, 5, delta=0.5) 52 | 53 | #let's also test that the max holds true 54 | #we expect the max to be 3000 frames. Let's add extra frames and check that the max holds true 55 | for i in range(0, 500): 56 | self.__model.Execute() 57 | 58 | self.assertEqual(self.__model.call_APIs("get_QueueSize"), 3000) -------------------------------------------------------------------------------- /src/test/test_imaginglink.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 23 Dec 2022 7 | @desc 8 | Let's test if the imaginglink class matches the data on page 6 at: 9 | https://digitalcommons.usu.edu/cgi/viewcontent.cgi?article=4405&context=smallsat 10 | ''' 11 | 12 | import unittest 13 | import os 14 | from src.sim.orchestrator import Orchestrator 15 | from src.sim.imanager import EManagerReqType 16 | from src.sim.managerparallel import ManagerParallel 17 | from src.models.imodel import EModelTag 18 | from src.models.network.imaging.imaginglink import ImagingLink 19 | 20 | class testimagingradiomodel(unittest.TestCase): 21 | def setUp(self) -> None: 22 | _orchestrator = Orchestrator(os.path.join(os.getcwd(), "configs/testconfigs/config_testimaginglink.json")) 23 | _orchestrator.create_SimEnv() 24 | _simEnv = _orchestrator.get_SimEnv() 25 | 26 | # hand over the simulation environment to the manager 27 | self.__manager = ManagerParallel(topologies = _simEnv[0], numOfSimSteps = _simEnv[1], numOfWorkers = 1) 28 | 29 | self.__topologies = self.__manager.req_Manager(EManagerReqType.GET_TOPOLOGIES) 30 | 31 | #Ignore the first satellite. It's not used in this test 32 | self.__sat = self.__topologies[0].get_Node(1) 33 | self.__gs = self.__topologies[0].get_Node(2) 34 | 35 | #Let's get the radio models 36 | self.__satModel = self.__sat.has_ModelWithTag(EModelTag.IMAGINGRADIO) 37 | self.__gsModel = self.__gs.has_ModelWithTag(EModelTag.IMAGINGRADIO) 38 | 39 | #Get the radio devices 40 | self.__satRadio = self.__satModel.call_APIs("get_RadioDevice") 41 | self.__gsRadio = self.__gsModel.call_APIs("get_RadioDevice") 42 | 43 | def nextStep(self) -> None: 44 | self.__manager.call_APIs("run_OneStep") 45 | 46 | def test_linkquality(self) -> None: 47 | #Let's test the link quality. This is the first column of the table 48 | _satLocation = self.__sat.get_Position() 49 | _gsLocation = self.__gs.get_Position() 50 | _distance = _satLocation.get_distance(_gsLocation) 51 | 52 | self.assertEqual(_distance, 1408*1000) 53 | 54 | _link = ImagingLink(self.__satRadio, self.__gsRadio, _distance) 55 | 56 | _desiredFSPL = 173.5 57 | self.assertAlmostEqual(_link.get_PropagationLoss(), _desiredFSPL, 0) 58 | 59 | _desiredSNR = 10.7 + .9 60 | _diff = abs(_link.get_SNR() - _desiredSNR) 61 | print(_link.get_SNR(), _desiredSNR) 62 | print(_link.get_SNR(), _desiredSNR) 63 | self.assertLessEqual(_diff, 1.1) 64 | 65 | #Let's test the data rate. 66 | _sizeInBytes = 64000/8 67 | _timeToTransmit = _link.get_TimeOnAir(_sizeInBytes) #this is msec 68 | _Bps = _sizeInBytes / (_timeToTransmit/1000) #this is bytes per second 69 | _Mbps = _Bps / 1000000 * 8 70 | 71 | #Take the uncoded data rate of one channel * coding rate * number of channels 72 | _desiredMbps = 228 * .75 * 6 73 | _diff = abs(_Mbps - _desiredMbps) 74 | self.assertLessEqual(_diff, 5) #5 Mbps error is acceptable 75 | 76 | @unittest.skip("This test must be verified by hand") 77 | def test_linkoverdistance(self): 78 | #Let's make the same plot as on page 9 of the paper 79 | _rates = [] 80 | _start = 2200*1000 81 | _end = 600*1000 82 | _delta = 1000 83 | _rng = range(_start, _end, -_delta) 84 | for _distance in _rng: 85 | _link = ImagingLink(self.__satRadio, self.__gsRadio, _distance) 86 | _sizeInBytes = 64000/8 87 | _timeToTransmit = _link.get_TimeOnAir(_sizeInBytes) #this is msec 88 | _Bps = _sizeInBytes / (_timeToTransmit/1000) #this is bytes per second 89 | _Mbps = _Bps / 1000000 * 8 90 | _rates.append(_Mbps) 91 | 92 | import matplotlib.pyplot as plt 93 | _kmVals = [x/1000 for x in _rng] 94 | plt.plot(_kmVals, _rates) 95 | plt.grid() 96 | #flip the x axis 97 | plt.gca().invert_xaxis() 98 | #Don't make the x axis scientific notation 99 | plt.ticklabel_format(style='plain') 100 | plt.xticks(rotation=45) 101 | 102 | plt.xlabel("Distance (km)") 103 | plt.ylabel("Mbps") 104 | plt.title("Data Rate Across All Channels vs Distance") 105 | plt.tight_layout() 106 | plt.savefig("test_linkoverdistance.png") -------------------------------------------------------------------------------- /src/test/test_imagingradiomodel.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 27 July 2023 7 | @desc 8 | Just some test cases for the ImagingRadio model. 9 | These are very similar to the lora ones. 10 | ''' 11 | import os 12 | import unittest 13 | from src.models.network.frame import Frame 14 | from src.sim.orchestrator import Orchestrator 15 | from src.sim.imanager import IManager, EManagerReqType 16 | from src.sim.managerparallel import ManagerParallel 17 | from src.models.imodel import IModel, EModelTag 18 | 19 | class testimagingradiomodel(unittest.TestCase): 20 | def setUp(self) -> None: 21 | _orchestrator = Orchestrator(os.path.join(os.getcwd(), "configs/testconfigs/config_testimaginglink.json")) 22 | _orchestrator.create_SimEnv() 23 | _simEnv = _orchestrator.get_SimEnv() 24 | 25 | # hand over the simulation environment to the manager 26 | self.__manager = ManagerParallel(topologies = _simEnv[0], numOfSimSteps = _simEnv[1], numOfWorkers = 1) 27 | 28 | self.__topologies = self.__manager.req_Manager(EManagerReqType.GET_TOPOLOGIES) 29 | self.__models = [] 30 | self.__models.append(self.__topologies[0].nodes[0].has_ModelWithTag(EModelTag.IMAGINGRADIO)) 31 | self.__models.append(self.__topologies[0].nodes[1].has_ModelWithTag(EModelTag.IMAGINGRADIO)) 32 | self.__models.append(self.__topologies[0].nodes[2].has_ModelWithTag(EModelTag.IMAGINGRADIO)) 33 | 34 | self.__rxQueues = [i.call_APIs("get_RxQueue") for i in self.__models] 35 | self.__txQueues = [i.call_APIs("get_TxQueue") for i in self.__models] 36 | 37 | self.__topologies[0].nodes[0].has_ModelWithTag(EModelTag.ORBITAL).Execute() 38 | 39 | def test_basic(self) -> None: 40 | # Let's just check that if we transmit from node 0 to node 1, we get a packet in the queue of node 1 41 | self.assertEqual(self.__rxQueues[0].qsize(), 0) 42 | self.assertEqual(self.__rxQueues[1].qsize(), 0) 43 | self.assertEqual(self.__rxQueues[2].qsize(), 0) 44 | 45 | _sentFrame = Frame(0, 100, payloadString="Test") 46 | self.__models[0].call_APIs("send_Packet", _packet=_sentFrame) 47 | 48 | self.__manager.call_APIs("run_OneStep") 49 | self.__manager.call_APIs("run_OneStep") 50 | self.__manager.call_APIs("run_OneStep") 51 | self.__manager.call_APIs("run_OneStep") 52 | 53 | self.assertEqual(self.__txQueues[0].qsize(), 0) 54 | self.assertEqual(self.__rxQueues[0].qsize(), 0) 55 | self.assertEqual(self.__rxQueues[1].qsize(), 0) 56 | self.assertEqual(self.__rxQueues[2].qsize(), 1) 57 | 58 | _receivedAtGS = self.__models[2].call_APIs("get_ReceivedPacket") 59 | self.assertEqual(_receivedAtGS, _sentFrame) 60 | 61 | self.assertEqual(self.__txQueues[0].qsize(), 0) 62 | self.assertEqual(self.__rxQueues[0].qsize(), 0) 63 | self.assertEqual(self.__rxQueues[1].qsize(), 0) 64 | self.assertEqual(self.__rxQueues[2].qsize(), 0) 65 | 66 | def test_collision(self) -> None: 67 | #Let's also just check that collision handling works 68 | _sentFrame = Frame(0, 1000, payloadString="Test") 69 | _sentFrame2 = Frame(0, 1000, payloadString="Test2") 70 | 71 | self.__models[0].call_APIs("send_Packet", _packet=_sentFrame) 72 | self.__models[1].call_APIs("send_Packet", _packet=_sentFrame2) 73 | 74 | self.__manager.call_APIs("run_OneStep") 75 | self.__manager.call_APIs("run_OneStep") 76 | self.__manager.call_APIs("run_OneStep") 77 | self.__manager.call_APIs("run_OneStep") 78 | 79 | self.assertEqual(self.__txQueues[0].qsize(), 0) 80 | self.assertEqual(self.__txQueues[1].qsize(), 0) 81 | self.assertEqual(self.__rxQueues[0].qsize(), 0) 82 | self.assertEqual(self.__rxQueues[1].qsize(), 0) 83 | self.assertEqual(self.__rxQueues[2].qsize(), 0) 84 | 85 | self.assertEqual(self.__models[2].call_APIs("get_ReceivedPacket"), None) -------------------------------------------------------------------------------- /src/test/test_isl.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 5 May 2023 7 | @desc 8 | Just some test cases for the ISL class 9 | ''' 10 | 11 | import unittest 12 | from src.sim.orchestrator import Orchestrator 13 | from src.sim.imanager import EManagerReqType 14 | from src.sim.managerparallel import ManagerParallel 15 | from src.models.imodel import EModelTag 16 | from src.models.network.frame import Frame 17 | import os 18 | from src.models.network.address import Address 19 | 20 | class testisl(unittest.TestCase): 21 | def setUp(self) -> None: 22 | _orchestrator = Orchestrator(os.path.join(os.getcwd(), "configs/testconfigs/config_testisl.json")) 23 | _orchestrator.create_SimEnv() 24 | _simEnv = _orchestrator.get_SimEnv() 25 | 26 | # hand over the simulation environment to the manager 27 | self.__manager = ManagerParallel(topologies = _simEnv[0], numOfSimSteps = _simEnv[1], numOfWorkers = 1) 28 | 29 | self.__topologies = self.__manager.req_Manager(EManagerReqType.GET_TOPOLOGIES) 30 | 31 | self.__models = [] 32 | self.__models.append(self.__topologies[0].nodes[0].has_ModelWithTag(EModelTag.ISL)) 33 | self.__models.append(self.__topologies[0].nodes[1].has_ModelWithTag(EModelTag.ISL)) 34 | self.__models.append(self.__topologies[0].nodes[2].has_ModelWithTag(EModelTag.ISL)) 35 | 36 | self.__rxQueues = [i.call_APIs("get_RxQueue") for i in self.__models] 37 | 38 | self.__topologies[0].nodes[0].has_ModelWithTag(EModelTag.ORBITAL).Execute() 39 | self.__topologies[0].nodes[1].has_ModelWithTag(EModelTag.ORBITAL).Execute() 40 | self.__topologies[0].nodes[2].has_ModelWithTag(EModelTag.ORBITAL).Execute() 41 | 42 | def nextStep(self) -> None: 43 | self.__manager.call_APIs("run_OneStep") 44 | 45 | def test_basic(self) -> None: 46 | # Let's just check that if we transmit from node 0 to node 1 and node2, we get a packet in the queue of node 1 and node 2 47 | self.assertEqual(self.__rxQueues[0].qsize(), 0) 48 | self.assertEqual(self.__rxQueues[1].qsize(), 0) 49 | self.assertEqual(self.__rxQueues[2].qsize(), 0) 50 | 51 | self.nextStep() 52 | #so at this point, the orbital model has executed and the ISL model has executed for 18:30 and now it is 18:31 53 | 54 | self.__models[0].call_APIs("send_Packet", _packet=Frame(0, 100, payloadString="Test"), _destAddr=Address(2)) #(addresses are 1-indexed) 55 | self.nextStep() 56 | self.nextStep() 57 | 58 | self.assertEqual(self.__rxQueues[0].qsize(), 0) 59 | self.assertEqual(self.__rxQueues[1].qsize(), 1) 60 | self.assertEqual(self.__rxQueues[2].qsize(), 0) 61 | 62 | _frame = Frame(0, 100, payloadString="Test") 63 | self.__models[1].call_APIs("send_Packet", _packet=_frame, _destAddr=Address(1)) 64 | self.nextStep() 65 | self.nextStep() 66 | 67 | self.assertEqual(self.__rxQueues[0].qsize(), 1) 68 | self.assertEqual(self.__rxQueues[1].qsize(), 1) 69 | self.assertEqual(self.__rxQueues[2].qsize(), 0) 70 | 71 | _receivedFrame = self.__models[0].call_APIs("get_ReceivedPacket") 72 | self.assertEqual(_receivedFrame, _frame) -------------------------------------------------------------------------------- /src/test/test_location.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 27 July 2023 7 | @desc 8 | Let's just test the location class 9 | ''' 10 | import unittest 11 | from src.utils import Location 12 | 13 | class TestLocation(unittest.TestCase): 14 | def test_ToLatLong(self): 15 | _loc = Location(6254.834*1000, 6862.875*1000, 6448.296*1000) 16 | _lat, _long, _alt = _loc.to_lat_long() 17 | 18 | _desiredLat = 34.8793622 19 | _desiredLong = 47.6539135 20 | _desiredAlt = 4933808.18 21 | 22 | self.assertAlmostEqual(_desiredLat, _lat, 2) 23 | self.assertAlmostEqual(_desiredLong, _long, 2) 24 | self.assertAlmostEqual(_desiredAlt, _alt, -2) -------------------------------------------------------------------------------- /src/test/test_loggerfile.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 12 Oct 2022 7 | @desc 8 | We conduct the unit test here for FileLogger class 9 | ''' 10 | 11 | import unittest 12 | import os 13 | from src.simlogging.loggerfile import LoggerFile 14 | from src.simlogging.ilogger import ELogType 15 | 16 | class TestLoggerFile(unittest.TestCase): 17 | 18 | def setUp(self): 19 | self.__logger = LoggerFile(ELogType.LOGALL, "TestFileLogger", os.getcwd()) 20 | 21 | def test_WriteLog(self): 22 | for i in range(1, 500): 23 | __result = self.__logger.write_Log("Test log", ELogType.LOGDEBUG) 24 | if(i%10 == 0): 25 | self.assertTrue(__result) 26 | 27 | def tearDown(self) -> None: 28 | _path = os.path.join(os.getcwd(), "Log_TestFileLogger.log") 29 | if os.path.isfile(_path): 30 | os.remove(_path) 31 | -------------------------------------------------------------------------------- /src/test/test_loggerfilechunkwise.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 18 Jul 2023 7 | @desc 8 | We conduct the unit test here for LoggerFileChunkWise class 9 | ''' 10 | 11 | import unittest 12 | import os 13 | from src.simlogging.loggerfilechunkwise import LoggerFileChunkwise 14 | from src.simlogging.ilogger import ELogType 15 | 16 | class TestLoggerFile(unittest.TestCase): 17 | 18 | def setUp(self): 19 | self.__logger = LoggerFileChunkwise(ELogType.LOGALL, "TestFileLogger", os.getcwd(), 400) 20 | 21 | def test_WriteLog(self): 22 | for i in range(1, 500): 23 | __result = self.__logger.write_Log("Test log", ELogType.LOGDEBUG) 24 | if(i%10 == 0): 25 | # The max chunk size is 400 characters and each log message length is 40 characters. So, after 10 writes, the chunk should be dumped in the file. 26 | self.assertTrue(__result) 27 | 28 | def tearDown(self) -> None: 29 | _path = os.path.join(os.getcwd(), "Log_TestFileLogger.log") 30 | if os.path.isfile(_path): 31 | os.remove(_path) 32 | -------------------------------------------------------------------------------- /src/test/test_loralink.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 23 Dec 2022 7 | @desc 8 | Let's test if the lora matches the expected values. See page 31 of the following document: 9 | https://forum.nasaspaceflight.com/index.php?action=dlattach;topic=47072.0;attach=1538105;sess=0 10 | ''' 11 | 12 | import unittest 13 | from src.sim.orchestrator import Orchestrator 14 | from src.sim.imanager import IManager, EManagerReqType 15 | from src.sim.managerparallel import ManagerParallel 16 | from src.models.imodel import IModel, EModelTag 17 | import os 18 | from src.models.network.lora.loraradiodevice import LoraRadioDevice 19 | from src.models.network.lora.loralink import LoraLink 20 | 21 | class testloraradiomodel(unittest.TestCase): 22 | def setUp(self) -> None: 23 | _orchestrator = Orchestrator(os.path.join(os.getcwd(), "configs/testconfigs/config_testloralink.json")) 24 | _orchestrator.create_SimEnv() 25 | _simEnv = _orchestrator.get_SimEnv() 26 | 27 | # hand over the simulation environment to the manager 28 | self.__manager = ManagerParallel(topologies = _simEnv[0], numOfSimSteps = _simEnv[1], numOfWorkers = 1) 29 | 30 | self.__topologies = self.__manager.req_Manager(EManagerReqType.GET_TOPOLOGIES) 31 | 32 | self.__sat = self.__topologies[0].get_Node(1) 33 | self.__gs = self.__topologies[0].get_Node(2) 34 | 35 | self.__satModel = self.__sat.has_ModelWithTag(EModelTag.BASICLORARADIO) 36 | self.__gsModel = self.__gs.has_ModelWithTag(EModelTag.BASICLORARADIO) 37 | 38 | #Get the radio devices 39 | self.__satRadio = self.__satModel.call_APIs("get_RadioDevice") 40 | self.__gsRadio = self.__gsModel.call_APIs("get_RadioDevice") 41 | 42 | def nextStep(self) -> None: 43 | self.__manager.call_APIs("run_OneStep") 44 | 45 | def test_linkquality(self) -> None: 46 | #Let's test the link quality from the satellite to the ground station (col 3 in the table) 47 | _satLocation = self.__sat.get_Position() 48 | _gsLocation = self.__gs.get_Position() 49 | _distance = _satLocation.get_distance(_gsLocation) 50 | 51 | self.assertEqual(_distance, 637*1000) 52 | 53 | _link = LoraLink(self.__satRadio, self.__gsRadio, _distance) 54 | 55 | _desiredFSPL = 131.33 56 | _calculatedFSPL = _link.get_PropagationLoss() 57 | _diff = abs(_desiredFSPL - _calculatedFSPL) 58 | self.assertLessEqual(_diff, 1) 59 | 60 | _desiredRSSI = -138.25 61 | _calculatedRSSI = _link.get_ReceivedSignalStrength() 62 | _diff = abs(_desiredRSSI - _calculatedRSSI) 63 | self.assertLessEqual(_diff, 1) 64 | print("SNR:", _link.get_SNR()) 65 | 66 | 67 | #Try the other way around (col 1 in the table) 68 | _link = LoraLink(self.__gsRadio, self.__satRadio, _distance) 69 | 70 | _desiredFSPL = 131.99 71 | _calculatedFSPL = _link.get_PropagationLoss() 72 | _diff = abs(_desiredFSPL - _calculatedFSPL) 73 | self.assertLessEqual(_diff, 1) 74 | 75 | _desiredRSSI = -138.25 76 | _calculatedRSSI = _link.get_ReceivedSignalStrength() 77 | print(_desiredRSSI, _calculatedRSSI) 78 | _diff = abs(_desiredRSSI - _calculatedRSSI) 79 | self.assertLessEqual(_diff, 1) 80 | 81 | print("SNR:", _link.get_SNR()) 82 | -------------------------------------------------------------------------------- /src/test/test_maclayer.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: 30 July 2023 7 | @desc 8 | Let's test the mac layer protocol out. 9 | This won't be an exhaustive test, but more to check if it's not completely broken. 10 | ''' 11 | 12 | from src.sim.simulator import Simulator 13 | import os 14 | import unittest 15 | import time 16 | 17 | class testmaclayer(unittest.TestCase): 18 | def setUp(self) -> None: 19 | _simulator = Simulator(os.path.join(os.getcwd(), "configs/testconfigs/config_testmaclayer.json")) 20 | _simulator.execute() 21 | #Delete the simulator once it's done. We need the logs to be flushed to the file 22 | del _simulator 23 | 24 | def string_IsInFile(self, _string: str, _file: str) -> bool: 25 | with open(_file, "r") as _f: 26 | _lines = _f.readlines() 27 | for _line in _lines: 28 | if _string in _line: 29 | return True 30 | return False 31 | 32 | def test_output(self): 33 | time.sleep(1) #Wait for all the logs to be written to the file 34 | #There should be a folder called "macLayerTestLogs" in the current directory 35 | #Let's check that it exists 36 | self.assertTrue(os.path.isdir(os.path.join(os.getcwd(), "macLayerTestLogs"))) 37 | 38 | #Now let's check that there are 3 files in the folder 39 | self.assertEqual(len(os.listdir(os.path.join(os.getcwd(), "macLayerTestLogs"))), 3) 40 | 41 | #Let's check that the files are named correctly 42 | _gsFile = "Log_Constln1_0_GS_3.log" 43 | _gsFullPath = os.path.join(os.getcwd(), "macLayerTestLogs", _gsFile) 44 | 45 | _iotFile = "Log_Constln1_0_IoT_2.log" 46 | _iotFullPath = os.path.join(os.getcwd(), "macLayerTestLogs", _iotFile) 47 | 48 | _satFile = "Log_Constln1_0_SAT_1.log" 49 | _satFullPath = os.path.join(os.getcwd(), "macLayerTestLogs", _satFile) 50 | 51 | self.assertTrue(os.path.isfile(_gsFullPath)) 52 | self.assertTrue(os.path.isfile(_iotFullPath)) 53 | self.assertTrue(os.path.isfile(_satFullPath)) 54 | 55 | #Now let's check that the files have the correct data in them 56 | #Let's check that the satellite sent a beacon 57 | _desiredString = "Sending beacon" 58 | self.assertTrue(self.string_IsInFile("Sending beacon", _satFullPath)) 59 | 60 | #Now, let's check that the iot device received the beacon 61 | _desiredString = "Beacons received" 62 | self.assertTrue(self.string_IsInFile(_desiredString, _iotFullPath)) 63 | 64 | #Check that the iot device sent a packet 65 | _desiredString = "Transmitting" 66 | self.assertTrue(self.string_IsInFile(_desiredString, _iotFullPath)) 67 | 68 | #Check that the satellite received the packet 69 | _desiredString = "Received MACData" 70 | self.assertTrue(self.string_IsInFile(_desiredString, _satFullPath)) 71 | 72 | #Check that the satellite sent an ack 73 | _desiredString = "Sending ACK with" 74 | self.assertTrue(self.string_IsInFile(_desiredString, _satFullPath)) 75 | 76 | #Check that the iot device received the ack 77 | _desiredString = "Ack received" 78 | self.assertTrue(self.string_IsInFile(_desiredString, _iotFullPath)) 79 | 80 | #Check that the satellite received a control packet from the ground station 81 | _desiredString = "Received control packet" 82 | self.assertTrue(self.string_IsInFile(_desiredString, _satFullPath)) 83 | 84 | #Check that the gs received data from the satellite 85 | _desiredString = "Received MACData" 86 | self.assertTrue(self.string_IsInFile(_desiredString, _gsFullPath)) 87 | 88 | #Check that the sat received a bulk ack from the gs 89 | _desiredString = "Received ack MACBulkAck" 90 | self.assertTrue(self.string_IsInFile(_desiredString, _satFullPath)) 91 | 92 | os.remove(_gsFullPath) 93 | os.remove(_iotFullPath) 94 | os.remove(_satFullPath) 95 | os.rmdir(os.path.join(os.getcwd(), "macLayerTestLogs")) -------------------------------------------------------------------------------- /src/test/test_modelhelpfov.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 23 Dec 2022 7 | @desc 8 | We conduct the unit test of the field of view helper model here. 9 | As the functionality of this model demands a mature simulation setup, we set up the same here. 10 | ''' 11 | 12 | import unittest 13 | from src.sim.orchestrator import Orchestrator 14 | from src.sim.imanager import IManager, EManagerReqType 15 | from src.sim.managerparallel import ManagerParallel 16 | from src.models.imodel import IModel, EModelTag 17 | from src.nodes.itopology import ITopology 18 | from src.nodes.inode import ENodeType 19 | 20 | class TestModelHelperFoV(unittest.TestCase): 21 | def setUp(self) -> None: 22 | _orchestrator = Orchestrator("configs/testconfigs/config_testmodelhelperfov.json") 23 | _orchestrator.create_SimEnv() 24 | _simEnv = _orchestrator.get_SimEnv() 25 | 26 | # hand over the simulation environment to the manager 27 | self.__manager = ManagerParallel(topologies = _simEnv[0], numOfSimSteps = _simEnv[1], numOfWorkers = 1) 28 | 29 | # run the orbital model to update the position of a satellite 30 | self.__topologies = self.__manager.req_Manager(EManagerReqType.GET_TOPOLOGIES) 31 | self.__topologies[0].nodes[0].has_ModelWithTag(EModelTag.ORBITAL).Execute() 32 | self.__topologies[0].nodes[1].has_ModelWithTag(EModelTag.ORBITAL).Execute() 33 | 34 | def test_get_View(self): 35 | _desiredResult = [[3, -19.279868415728256], 36 | [4, -6.47781766442099], 37 | [5, -15.249999282440179], 38 | [6, 1.3183448569488105], 39 | [7, -13.591257313017305], 40 | [8, -27.306696675612574], 41 | [9, -0.49842836843193666], 42 | [10, 4.103873098364186], 43 | [11, 3.8902260059766824], 44 | [12, -25.92891116120493], 45 | [13, -53.57128636153784], 46 | [14, -51.71924016388395]] 47 | #_result is a list of nodes that are visible from the node with id 0 48 | _result = self.__topologies[0].nodes[0].has_ModelWithTag(EModelTag.VIEWOFNODE).call_APIs( 49 | "get_View", 50 | _isDownView = True, 51 | _targetNodeTypes = [ENodeType.GS], 52 | _myTime = None, 53 | _myLocation = None) 54 | for i in range(len(_desiredResult)): 55 | if _desiredResult[i][1] > 0: 56 | self.assertIn(_desiredResult[i][0], _result) 57 | 58 | _desiredResult = [[1, -6.47781766442099], [2, -60.858417791668614]] 59 | _result = self.__topologies[0].nodes[3].has_ModelWithTag(EModelTag.VIEWOFNODE).call_APIs( 60 | "get_View", 61 | _isDownView = False, 62 | _targetNodeTypes = [ENodeType.SAT], 63 | _myTime = None, 64 | _myLocation = None) 65 | 66 | for i in range(len(_desiredResult)): 67 | if _desiredResult[i][1] > 0: 68 | self.assertIn(_desiredResult[i][0], _result) -------------------------------------------------------------------------------- /src/test/test_orchestrator.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 18 Oct 2022 7 | @desc 8 | We conduct the unit test of the orchestrator class here 9 | ''' 10 | 11 | import unittest 12 | from src.sim.orchestrator import Orchestrator 13 | from src.sim.imanager import IManager 14 | from src.models.imodel import IModel, EModelTag 15 | 16 | class TestOrchestrator(unittest.TestCase): 17 | def setUp(self) -> None: 18 | self.__orchestrator = Orchestrator("configs/testconfigs/config_testorchestrator.json") 19 | self.__orchestrator.create_SimEnv() 20 | 21 | 22 | def test_CreateSimEnv(self): 23 | __simEnv = self.__orchestrator.get_SimEnv() 24 | self.assertEqual(len(__simEnv[0]), 1) 25 | self.assertEqual(len(__simEnv[0][0].nodes), 3) 26 | self.assertEqual(__simEnv[0][0].nodes[0].iName, "SatelliteBasic") 27 | self.assertEqual(__simEnv[0][0].nodes[0].nodeID, 1) 28 | self.assertEqual(__simEnv[0][0].nodes[1].nodeID, 2) 29 | 30 | def test_ModelCreation(self): 31 | __simEnv = self.__orchestrator.get_SimEnv() 32 | self.assertTrue(__simEnv[0][0].nodes[0].has_ModelWithTag(EModelTag.ORBITAL) is not None) 33 | self.assertTrue(__simEnv[0][0].nodes[2].has_ModelWithTag(EModelTag.VIEWOFNODE) is not None) 34 | 35 | -------------------------------------------------------------------------------- /src/test/test_power.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Om Chabra 6 | Created on: July 11, 2023 7 | @desc 8 | We test out the power consumption of the system. 9 | ''' 10 | 11 | import os 12 | import unittest 13 | from src.sim.orchestrator import Orchestrator 14 | from src.sim.imanager import EManagerReqType 15 | from src.sim.managerparallel import ManagerParallel 16 | from src.models.imodel import EModelTag 17 | 18 | #for sunlight test: 19 | from skyfield.api import load, EarthSatellite 20 | 21 | class testpower(unittest.TestCase): 22 | def setUp(self) -> None: 23 | _orchestrator = Orchestrator(os.path.join(os.getcwd(), "configs/testconfigs/config_testpower.json")) 24 | _orchestrator.create_SimEnv() 25 | _simEnv = _orchestrator.get_SimEnv() 26 | 27 | # hand over the simulation environment to the manager 28 | self.__manager = ManagerParallel(topologies = _simEnv[0], numOfSimSteps = _simEnv[1], numOfWorkers = 1) 29 | 30 | self.__topologies = self.__manager.req_Manager(EManagerReqType.GET_TOPOLOGIES) 31 | self.__models = [] 32 | self.__models.append(self.__topologies[0].nodes[0].has_ModelWithTag(EModelTag.POWER)) 33 | 34 | def test_basic(self) -> None: 35 | 36 | #let's check that the power model is working 37 | #let's first check that the amount of power is correct when loaded 38 | currPower = 7030*0.001*3600 39 | self.assertEqual(self.__models[0].call_APIs("get_AvailableEnergy"), currPower) 40 | 41 | #let's check that if we use transmit power for one second, the amount of power is correct 42 | ret = self.__models[0].call_APIs("consume_Energy", _tag="TXRADIO", _duration=1) 43 | self.assertTrue(ret) 44 | currPower -= 532 * 1 * 0.001 45 | self.assertAlmostEquals(self.__models[0].call_APIs("get_AvailableEnergy"), currPower) 46 | 47 | self.__manager.call_APIs("run_OneStep") #the satellite should be in the sun, so the power should be the maximum 48 | 49 | #satellite should gain some power 50 | currPower += 1667.667 * 5 * 0.001 51 | currPower = min(currPower, 7030*0.001*3600) 52 | #but also lose some power due to necessary operations 53 | currPower -= (190 + 133 + 532 + 266) * 0.001 * 5 54 | 55 | #check that the power is within 1% of the expected value 56 | self.assertEqual(round(self.__models[0].call_APIs("get_AvailableEnergy")), round(currPower)) 57 | 58 | #let's check that if we use transmit power for 1 million seconds, no power is consumed and the function returns false 59 | ret = self.__models[0].call_APIs("consume_Energy", _tag="TXRADIO", _duration=1000000) 60 | self.assertFalse(ret) 61 | self.assertEqual(round(self.__models[0].call_APIs("get_AvailableEnergy")), round(currPower)) 62 | 63 | def test_Sunlight(self): 64 | #Let's check that the sunlight calculations are correct 65 | #Going off of: https://rhodesmill.org/skyfield/earth-satellites.html 66 | eph = load('dependencies/de440s.bsp') 67 | ts = load.timescale() 68 | 69 | tle_1 = "1 50985U 22002B 22290.71715197 .00032099 00000+0 13424-2 0 9994" 70 | tle_2 = "2 50985 97.4784 357.5505 0011839 353.6613 6.4472 15.23462773 42039" 71 | satellite = EarthSatellite(tle_1, tle_2, 'samplesat', ts) 72 | 73 | times = ts.utc(2022, 11, 14, 12, 0, range(0, 2*60*60, 5)) 74 | sunlits = satellite.at(times).is_sunlit(eph) 75 | #Now, let's check the model orbit and see if the results match 76 | modelOrbit = self.__topologies[0].nodes[0].has_ModelWithTag(EModelTag.ORBITAL) 77 | 78 | for i in range(0, len(sunlits)): 79 | self.__manager.call_APIs("run_OneStep") 80 | _modelSunlit = modelOrbit.call_APIs("in_Sunlight") 81 | 82 | #The sunlit might be off by a a timestep, so let's check if it's within 1 timestep 83 | _sunlitCorrect = sunlits[i] == _modelSunlit or sunlits[i-1] == _modelSunlit or sunlits[i+1] == _modelSunlit 84 | self.assertTrue(_sunlitCorrect) 85 | -------------------------------------------------------------------------------- /src/test/test_satellitebasic.py: -------------------------------------------------------------------------------- 1 | ''' 2 | // Copyright (c) Microsoft Corporation. 3 | // Licensed under the MIT license. 4 | 5 | Created by: Tusher Chakraborty 6 | Created on: 13 Oct 2022 7 | @desc 8 | We conduct the unit test here for SatelliteBasic class 9 | ''' 10 | 11 | from pyexpat import model 12 | import unittest 13 | from src.models.imodel import EModelTag 14 | from src.simlogging.ilogger import ELogType, ILogger 15 | from src.simlogging.loggercmd import LoggerCmd 16 | from src.nodes.satellitebasic import SatelliteBasic 17 | from src.utils import Location, Time 18 | from src.models.models_orbital.modelorbit import ModelOrbit 19 | 20 | class TestSatelliteBasic(unittest.TestCase): 21 | 22 | def setUp(self) -> None: 23 | _logger = LoggerCmd(ELogType.LOGALL, 'SatelliteBasicTest') 24 | self.__time = Time().from_str("2022-10-11 12:00:00") 25 | self.__endtime = Time().from_str("2022-10-11 12:00:30") 26 | _tlelines = ["1 50985U 22002B 22290.71715197 .00032099 00000+0 13424-2 0 9994", "2 50985 97.4784 357.5505 0011839 353.6613 6.4472 15.23462773 42039"] 27 | self.__sat = SatelliteBasic(0 , 0, _tlelines[0], _tlelines[1], 10, self.__time.copy(), self.__endtime.copy(), _logger) 28 | 29 | _models = list() 30 | _models.append(ModelOrbit(self.__sat, _logger)) 31 | 32 | self.__sat.add_Models(_models) 33 | 34 | self.__sat.ExecuteCntd() 35 | 36 | 37 | def test_OrbitModel(self): 38 | _testTime = Time().from_str("2022-10-11 12:00:30") 39 | _testTargetLoc = Location(4254212.590504601, -1566798.705286896, 5166824.198105468) 40 | _testRetLocation = self.__sat.get_Position(_testTime) 41 | 42 | #Within 10m (way less than the error in the model) 43 | self.assertAlmostEquals(_testRetLocation.x, _testTargetLoc.x, delta=10) 44 | self.assertAlmostEquals(_testRetLocation.y, _testTargetLoc.y, delta=10) 45 | self.assertAlmostEquals(_testRetLocation.z, _testTargetLoc.z, delta=10) 46 | 47 | def test_HasModel(self): 48 | _retModel = self.__sat.has_ModelWithTag(EModelTag.ORBITAL) 49 | self.assertTrue(_retModel.modelTag == EModelTag.ORBITAL) 50 | 51 | 52 | def test_Position(self): 53 | _position = Location().from_lat_long(1.0, 2.0, 3.0) 54 | _time = Time().from_str("2022-10-11 12:00:30") 55 | 56 | self.__sat.update_Position(_position, _time) 57 | 58 | _lat, _long, _elv = self.__sat.get_Position(_time).to_lat_long() 59 | 60 | self.assertEqual(_lat, 1.0) 61 | self.assertEqual(_long, 2.0) 62 | self.assertEqual(_elv, 3.0) --------------------------------------------------------------------------------