├── .gitignore ├── DIRECTORY_STRUCTURE.md ├── LICENSE ├── README.md ├── docs ├── Makefile ├── detailed │ └── io │ │ ├── rinex2.md │ │ └── rinex3.md ├── make.bat └── source │ ├── api.rst │ ├── conf.py │ ├── index.rst │ ├── modules │ ├── rinex2.rst │ ├── rinex3.rst │ └── rinexnav.rst │ └── usage.rst ├── pygnsslab_logo.png ├── pyproject.toml ├── requirements.txt ├── setup.py ├── src ├── __init__.py └── pygnsslab │ ├── __init__.py │ └── io │ ├── rinex2 │ ├── __init__.py │ ├── example_usage.py │ ├── metadata.py │ ├── reader.py │ ├── utils.py │ └── writer.py │ ├── rinex3 │ ├── __init__.py │ ├── example_usage.py │ ├── metadata.py │ ├── reader.py │ ├── utils.py │ └── writer.py │ ├── rinexnav │ ├── __init__.py │ ├── example_usage.py │ ├── metadata.py │ ├── reader.py │ └── utils.py │ └── sp3 │ ├── __init__.py │ ├── example_usage.py │ ├── metadata.py │ ├── reader.py │ ├── utils.py │ └── writer.py └── tests ├── rinex2 ├── test_output.ipynb └── test_output.py ├── rinexnav ├── test_rinexnav.ipynb └── test_rinexnav.py └── sp3 └── test_sp3.ipynb /.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 | # User defined 30 | data/ 31 | update_directory_structure.py 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # UV 102 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | #uv.lock 106 | 107 | # poetry 108 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 109 | # This is especially recommended for binary packages to ensure reproducibility, and is more 110 | # commonly ignored for libraries. 111 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 112 | #poetry.lock 113 | 114 | # pdm 115 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 116 | #pdm.lock 117 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 118 | # in version control. 119 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 120 | .pdm.toml 121 | .pdm-python 122 | .pdm-build/ 123 | 124 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 125 | __pypackages__/ 126 | 127 | # Celery stuff 128 | celerybeat-schedule 129 | celerybeat.pid 130 | 131 | # SageMath parsed files 132 | *.sage.py 133 | 134 | # Environments 135 | .env 136 | .venv 137 | env/ 138 | venv/ 139 | ENV/ 140 | env.bak/ 141 | venv.bak/ 142 | 143 | # Spyder project settings 144 | .spyderproject 145 | .spyproject 146 | 147 | # Rope project settings 148 | .ropeproject 149 | 150 | # mkdocs documentation 151 | /site 152 | 153 | # mypy 154 | .mypy_cache/ 155 | .dmypy.json 156 | dmypy.json 157 | 158 | # Pyre type checker 159 | .pyre/ 160 | 161 | # pytype static type analyzer 162 | .pytype/ 163 | 164 | # Cython debug symbols 165 | cython_debug/ 166 | 167 | # PyCharm 168 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 169 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 170 | # and can be added to the global gitignore or merged into this file. For a more nuclear 171 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 172 | #.idea/ 173 | 174 | # Ruff stuff: 175 | .ruff_cache/ 176 | 177 | # PyPI configuration file 178 | .pypirc 179 | -------------------------------------------------------------------------------- /DIRECTORY_STRUCTURE.md: -------------------------------------------------------------------------------- 1 | # Project Directory Structure 2 | 3 | ``` 4 | . 5 | │ └─── .gitignore 6 | │ └─── DIRECTORY_STRUCTURE.md 7 | │ └─── LICENSE 8 | │ └─── README.md 9 | │ └─── pygnsslab_logo.png 10 | │ └─── pyproject.toml 11 | │ └─── requirements.txt 12 | │ └─── setup.py 13 | │ └───src 14 | │ │ └─── __init__.py 15 | │ │ └───pygnsslab 16 | │ │ │ └─── __init__.py 17 | │ │ │ └───io 18 | │ │ │ │ └───rinex2 19 | │ │ │ │ │ └─── __init__.py 20 | │ │ │ │ │ └─── example_usage.py 21 | │ │ │ │ │ └─── metadata.py 22 | │ │ │ │ │ └─── reader.py 23 | │ │ │ │ │ └─── utils.py 24 | │ │ │ │ │ └─── writer.py 25 | │ │ │ │ └───rinex3 26 | │ │ │ │ │ └─── __init__.py 27 | │ │ │ │ │ └─── example_usage.py 28 | │ │ │ │ │ └─── metadata.py 29 | │ │ │ │ │ └─── reader.py 30 | │ │ │ │ │ └─── utils.py 31 | │ │ │ │ │ └─── writer.py 32 | │ │ │ │ └───rinexnav 33 | │ │ │ │ │ └─── __init__.py 34 | │ │ │ │ │ └─── example_usage.py 35 | │ │ │ │ │ └─── metadata.py 36 | │ │ │ │ │ └─── reader.py 37 | │ │ │ │ │ └─── utils.py 38 | │ │ │ │ └───sp3 39 | │ │ │ │ │ └─── __init__.py 40 | │ │ │ │ │ └─── example_usage.py 41 | │ │ │ │ │ └─── metadata.py 42 | │ │ │ │ │ └─── reader.py 43 | │ │ │ │ │ └─── utils.py 44 | │ │ │ │ │ └─── writer.py 45 | │ └───tests 46 | │ │ └───rinex2 47 | │ │ │ └─── test_output.ipynb 48 | │ │ │ └─── test_output.py 49 | │ │ └───rinexnav 50 | │ │ │ └─── test_rinexnav.ipynb 51 | │ │ │ └─── test_rinexnav.py 52 | │ │ └───sp3 53 | │ │ │ └─── test_sp3.ipynb 54 | ``` 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 PyGnssLab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PyGNSSLab Logo 2 | 3 | # pygnsslab 4 | Open-source Python-based GNSS software. 5 | 6 | # PyGNSSLab 7 | **PyGNSSLab** is an open-source project focused on developing modular, Python-based GNSS tools and libraries. Our aim is to provide accurate, reliable, and extensible solutions for GNSS data processing — from basic RINEX handling to advanced PPP-AR techniques. 8 | 9 | ## 🔍 Key Features 10 | 11 | - 🛰️ RINEX reading & conversion tools 12 | - 📡 PPP and PPP-AR processing engines 13 | - 🧪 Real-time GNSS data stream support 14 | - 🧰 Modular structure for research & development 15 | - 📦 Ready-to-use APIs and command-line tools 16 | 17 | ## 💡 Who is it for? 18 | 19 | - Geodesists & Earth scientists 20 | - Researchers working on GNSS positioning 21 | - Developers creating custom GNSS pipelines 22 | - Students learning GNSS with Python 23 | 24 | ## 📁 Modules 25 | 26 | This organization hosts multiple standalone and integrated modules: 27 | - **RINEX2**: A comprehensive parser for RINEX 2.x observation files, supporting: 28 | - Fast reading of observation data 29 | - Metadata extraction 30 | - Export to Parquet format for efficient storage 31 | - JSON metadata output 32 | 33 | - [Example usage](src/pygnsslab/io/rinex2/example_usage.py) 34 | - [Test output](tests/rinex2/test_output.py) 35 | 36 | - **RINEX3**: Modern RINEX 3.x file handler with features for: 37 | - Multi-GNSS observation data parsing 38 | - Complete metadata extraction 39 | - Parquet file conversion 40 | - Standardized JSON metadata output 41 | 42 | - [Example usage](src/pygnsslab/io/rinex3/example_usage.py) 43 | 44 | - **Coming Soon**: 45 | - SPP Capabilities 46 | - PPP/PPP-AR Processing Engine 47 | - Real-time Data Stream Handler 48 | - Troposphere & Ionosphere Models 49 | - Orbit & Clock Products Interface 50 | 51 | ## 🚀 Getting Started 52 | 53 | ### Installation 54 | 55 | Follow these steps to get PyGNSSLab set up on your local machine: 56 | 57 | 1. **Clone the repository:** 58 | ```bash 59 | git clone https://github.com/PyGNSSLab/pygnsslab.git 60 | cd pygnsslab 61 | ``` 62 | 63 | 2. **Create and activate a virtual environment (Recommended):** 64 | ```bash 65 | python -m venv venv 66 | # On Windows 67 | .\venv\Scripts\activate 68 | # On macOS/Linux 69 | source venv/bin/activate 70 | ``` 71 | 72 | 3. **Install the required dependencies:** 73 | ```bash 74 | pip install -r requirements.txt 75 | ``` 76 | 77 | Now you're ready to use the PyGNSSLab tools and libraries! 78 | 79 | ## 📜 License 80 | 81 | All repositories under PyGNSSLab are released under the **MIT License** unless otherwise specified. 82 | 83 | --- 84 | 85 | > Made with 🛰️ by the GNSS community, for the GNSS community. 86 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/detailed/io/rinex2.md: -------------------------------------------------------------------------------- 1 | # pygnsslab.io.rinex2 Module Documentation 2 | 3 | ## Overview 4 | 5 | The `pygnsslab.io.rinex2` module is a Python package designed for parsing, processing, and manipulating RINEX 2 observation files. RINEX (Receiver Independent Exchange Format) is a standardized format for GNSS (Global Navigation Satellite System) data. This module provides a comprehensive set of tools for extracting metadata and observation data from RINEX 2 files, converting between different time formats, and saving processed data to more efficient formats. 6 | 7 | The module is part of the larger `pygnsslab` package and is located at `./src/pygnsslab/io/rinex2`. 8 | 9 | ## Module Structure 10 | 11 | The `pygnsslab.io.rinex2` module consists of the following Python files: 12 | 13 | - `__init__.py`: Main module initialization that exports key functionality 14 | - `reader.py`: Contains the `Rinex2Reader` class for parsing RINEX 2 files 15 | - `writer.py`: Functions for saving observation data to parquet format 16 | - `metadata.py`: Functions for handling RINEX metadata 17 | - `utils.py`: Utility functions for time conversions and file operations 18 | - `example_usage.py`: Example script demonstrating how to use the module 19 | 20 | ## Key Components 21 | 22 | ### Rinex2Reader 23 | 24 | The `Rinex2Reader` class is the primary interface for reading and parsing RINEX 2 observation files. 25 | 26 | #### Usage 27 | 28 | ```python 29 | from pygnsslab.io.rinex2 import Rinex2Reader 30 | 31 | # Initialize the reader with a RINEX 2 file 32 | rinex_reader = Rinex2Reader("path/to/rinex_file.o") 33 | 34 | # Access metadata 35 | metadata = rinex_reader.metadata 36 | 37 | # Access observation data as a pandas DataFrame 38 | obs_data = rinex_reader.get_obs_data() 39 | 40 | # Save observation data directly to Parquet (convenience method) 41 | rinex_reader.to_parquet("output.parquet") 42 | ``` 43 | 44 | #### Input 45 | 46 | - RINEX 2 observation files (typically with extensions `.obs`, `.o`, `.yyo`, `.yyO`, `.yyN`, or `.yyG`) 47 | 48 | #### Output 49 | 50 | - `metadata`: Dictionary containing header information from the RINEX file 51 | - `obs_data`: Pandas DataFrame containing observation data 52 | 53 | ### Metadata Handling 54 | 55 | The `metadata.py` module provides functions for extracting, formatting, and saving header metadata from RINEX 2 files. 56 | 57 | #### Key Functions 58 | 59 | - `extract_metadata(rinex_header)`: Extracts and formats metadata from RINEX header 60 | - `save_metadata(metadata, output_file)`: Saves metadata to a JSON file 61 | - `load_metadata(metadata_file)`: Loads metadata from a JSON file 62 | - `compare_metadata(metadata1, metadata2)`: Compares two metadata dictionaries and returns their differences 63 | 64 | #### Usage 65 | 66 | ```python 67 | from pygnsslab.io.rinex2.metadata import extract_metadata, save_metadata, load_metadata, compare_metadata 68 | 69 | # Extract metadata from RINEX header 70 | metadata = extract_metadata(rinex_header) 71 | 72 | # Save metadata to a JSON file 73 | save_metadata(metadata, "output_metadata.json") 74 | 75 | # Load metadata from a JSON file 76 | loaded_metadata = load_metadata("metadata.json") 77 | 78 | # Compare two metadata dictionaries 79 | differences = compare_metadata(metadata1, metadata2) 80 | ``` 81 | 82 | ### Data Writing 83 | 84 | The `writer.py` module provides functions for saving RINEX observation data to parquet format and creating summary reports. 85 | 86 | #### Key Functions 87 | 88 | - `write_to_parquet(obs_data, output_file, metadata=None, metadata_file=None)`: Writes observation data to a parquet file and optionally saves metadata 89 | - `write_obs_summary(obs_data, output_file)`: Writes a summary of observation data to a text file 90 | 91 | #### Usage 92 | 93 | ```python 94 | from pygnsslab.io.rinex2.writer import write_to_parquet, write_obs_summary 95 | 96 | # Write observation data to parquet format 97 | parquet_file, metadata_file = write_to_parquet(obs_data, "output.parquet", metadata) 98 | 99 | # Write observation data summary 100 | summary_file = write_obs_summary(obs_data, "summary.txt") 101 | ``` 102 | 103 | ### Utility Functions 104 | 105 | The `utils.py` module provides utility functions for time conversions and file operations. 106 | 107 | #### Time Conversion Functions 108 | 109 | - `datetime_to_mjd(dt)`: Converts a datetime object to Modified Julian Date (MJD) 110 | - `datetime_to_jd(dt)`: Converts a datetime object to Julian Date 111 | - `mjd_to_datetime(mjd)`: Converts Modified Julian Date to datetime object 112 | - `jd_to_datetime(jd)`: Converts Julian Date to datetime object 113 | - `time_to_seconds_of_day(time_obj)`: Converts a time object to seconds of day 114 | 115 | #### File Operations 116 | 117 | - `is_rinex2_file(filename)`: Checks if a file is a RINEX 2 observation file 118 | - `find_rinex2_files(directory)`: Finds all RINEX 2 observation files in a directory 119 | 120 | #### Usage 121 | 122 | ```python 123 | from pygnsslab.io.rinex2.utils import datetime_to_mjd, is_rinex2_file, find_rinex2_files 124 | import datetime 125 | 126 | # Convert datetime to MJD 127 | dt = datetime.datetime.now() 128 | mjd = datetime_to_mjd(dt) 129 | 130 | # Check if a file is a RINEX 2 file 131 | is_rinex2 = is_rinex2_file("path/to/file.o") 132 | 133 | # Find all RINEX 2 files in a directory 134 | rinex_files = find_rinex2_files("path/to/directory") 135 | ``` 136 | 137 | ## Data Formats 138 | 139 | ### RINEX 2 Format 140 | 141 | The RINEX 2 format is an older standard format for GNSS data, with some notable differences from the newer RINEX 3 format. RINEX 2 observation files contain: 142 | 143 | 1. **Header Section**: Contains metadata about the observation session, including station information, receiver and antenna details, and observation types. 144 | 2. **Observation Section**: Contains the actual observation data from different satellites. 145 | 146 | Key differences from RINEX 3: 147 | - Observation types are defined globally rather than per constellation 148 | - Two-digit year format is commonly used 149 | - Limited support for multiple GNSS constellations 150 | - Different formatting for epoch headers and observation data 151 | 152 | ### Metadata Structure 153 | 154 | The extracted metadata from RINEX 2 files is structured as a Python dictionary with the following key fields: 155 | 156 | - `rinexVersion`: RINEX version number 157 | - `fileType`: Type of RINEX file (usually 'O' for observation) 158 | - `stationName`: Name of the station 159 | - `observer`: Name of the observer 160 | - `agency`: Agency or institution 161 | - `approxPos`: Approximate position of the station (X, Y, Z coordinates) 162 | - `receiverNumber`: Receiver number 163 | - `receiverType`: Receiver type 164 | - `receiverVersion`: Receiver firmware version 165 | - `antennaNumber`: Antenna number 166 | - `antennaType`: Antenna type 167 | - `observationTypes`: Dictionary of observation types by constellation 168 | - In RINEX 2, observation types are stored globally but converted to a constellation-based format for compatibility with RINEX 3 processing code 169 | - Original global list is preserved under the 'ALL' key 170 | - `firstObsTime`: Time of first observation 171 | - `lastObsTime`: Time of last observation 172 | - `interval`: Observation interval in seconds 173 | - `leapSeconds`: Number of leap seconds 174 | - `antennaDelta`: Antenna delta (height, east, north) 175 | - `wavelengthFactors`: Wavelength factors information 176 | 177 | ### Observation Data Structure 178 | 179 | The observation data is structured as a pandas DataFrame with the following columns: 180 | 181 | - `date`: Observation date 182 | - `epoch_sec`: Seconds of day 183 | - `mjd`: Modified Julian Date 184 | - `constellation`: GNSS constellation (G=GPS, R=GLONASS, E=Galileo, C=BeiDou, J=QZSS, S=SBAS) 185 | - `sat`: Satellite identifier 186 | - `channel`: Signal channel/frequency 187 | - `range`: Pseudorange measurement 188 | - `phase`: Carrier phase measurement 189 | - `doppler`: Doppler measurement 190 | - `snr`: Signal-to-noise ratio 191 | 192 | This structure is designed to be compatible with the RINEX 3 data format for easier integration with existing processing pipelines. 193 | 194 | ## Example Usage 195 | 196 | ### Basic Example 197 | 198 | ```python 199 | from pygnsslab.io.rinex2 import Rinex2Reader 200 | from pygnsslab.io.rinex2.writer import write_to_parquet 201 | import os 202 | 203 | # Path to RINEX 2 file 204 | rinex_file = "path/to/rinex_file.o" 205 | 206 | # Read RINEX 2 file 207 | rinex_reader = Rinex2Reader(rinex_file) 208 | 209 | # Get metadata and observations 210 | metadata = rinex_reader.metadata 211 | obs_data = rinex_reader.get_obs_data() 212 | 213 | # Save to parquet format 214 | output_dir = "output" 215 | os.makedirs(output_dir, exist_ok=True) 216 | base_name = os.path.splitext(os.path.basename(rinex_file))[0] 217 | parquet_file = os.path.join(output_dir, f"{base_name}.parquet") 218 | metadata_file = os.path.join(output_dir, f"{base_name}_metadata.json") 219 | 220 | # Write to parquet with metadata 221 | write_to_parquet(obs_data, parquet_file, metadata, metadata_file) 222 | ``` 223 | 224 | ### Processing Multiple Files 225 | 226 | ```python 227 | from pygnsslab.io.rinex2 import Rinex2Reader 228 | from pygnsslab.io.rinex2.writer import write_to_parquet 229 | from pygnsslab.io.rinex2.utils import find_rinex2_files 230 | import os 231 | 232 | # Find all RINEX 2 files in a directory 233 | rinex_dir = "path/to/rinex_directory" 234 | rinex_files = find_rinex2_files(rinex_dir) 235 | 236 | # Process each file 237 | output_dir = "output" 238 | os.makedirs(output_dir, exist_ok=True) 239 | 240 | for rinex_file in rinex_files: 241 | # Extract base name 242 | base_name = os.path.splitext(os.path.basename(rinex_file))[0] 243 | 244 | # Read RINEX 2 file 245 | print(f"Processing {rinex_file}...") 246 | rinex_reader = Rinex2Reader(rinex_file) 247 | 248 | # Get metadata and observations 249 | metadata = rinex_reader.metadata 250 | obs_data = rinex_reader.get_obs_data() 251 | 252 | # Convert datetime objects to ISO format strings for JSON serialization 253 | if metadata.get('firstObsTime'): 254 | metadata['firstObsTime'] = metadata['firstObsTime'].isoformat() 255 | if metadata.get('lastObsTime'): 256 | metadata['lastObsTime'] = metadata['lastObsTime'].isoformat() 257 | 258 | # Save to parquet format 259 | parquet_file = os.path.join(output_dir, f"{base_name}.parquet") 260 | metadata_file = os.path.join(output_dir, f"{base_name}_metadata.json") 261 | 262 | # Write to parquet with metadata 263 | write_to_parquet(obs_data, parquet_file, metadata, metadata_file) 264 | 265 | print(f"Saved to {parquet_file} and {metadata_file}") 266 | ``` 267 | 268 | ### Comparing Metadata from Multiple Files 269 | 270 | ```python 271 | from pygnsslab.io.rinex2 import Rinex2Reader 272 | from pygnsslab.io.rinex2.metadata import compare_metadata 273 | import os 274 | 275 | # Paths to RINEX 2 files 276 | rinex_file1 = "path/to/rinex_file1.o" 277 | rinex_file2 = "path/to/rinex_file2.o" 278 | 279 | # Read RINEX 2 files 280 | rinex_reader1 = Rinex2Reader(rinex_file1) 281 | rinex_reader2 = Rinex2Reader(rinex_file2) 282 | 283 | # Get metadata 284 | metadata1 = rinex_reader1.metadata 285 | metadata2 = rinex_reader2.metadata 286 | 287 | # Convert datetime objects to strings for comparison 288 | for metadata in [metadata1, metadata2]: 289 | if metadata.get('firstObsTime'): 290 | metadata['firstObsTime'] = metadata['firstObsTime'].isoformat() 291 | if metadata.get('lastObsTime'): 292 | metadata['lastObsTime'] = metadata['lastObsTime'].isoformat() 293 | 294 | # Compare metadata 295 | differences = compare_metadata(metadata1, metadata2) 296 | 297 | # Print differences 298 | if differences: 299 | print("Differences found in metadata:") 300 | for key, diff in differences.items(): 301 | print(f" {key}: {diff}") 302 | else: 303 | print("No differences found in metadata") 304 | ``` 305 | 306 | ## Advanced Usage 307 | 308 | ### Filtering Observation Data 309 | 310 | ```python 311 | from pygnsslab.io.rinex2 import Rinex2Reader 312 | import pandas as pd 313 | 314 | # Read RINEX 2 file 315 | rinex_reader = Rinex2Reader("path/to/rinex_file.o") 316 | obs_data = rinex_reader.get_obs_data() 317 | 318 | # Filter by constellation (e.g., GPS only) 319 | gps_data = obs_data[obs_data['constellation'] == 'G'] 320 | 321 | # Filter by satellite 322 | sat_data = obs_data[obs_data['sat'] == 'G01'] 323 | 324 | # Filter by time range 325 | start_mjd = 59000.0 326 | end_mjd = 59001.0 327 | time_filtered_data = obs_data[(obs_data['mjd'] >= start_mjd) & (obs_data['mjd'] <= end_mjd)] 328 | 329 | # Filter by signal quality (SNR > 30) 330 | quality_data = obs_data[obs_data['snr'] > 30] 331 | 332 | # Filter by observation type (has valid pseudorange) 333 | range_data = obs_data[obs_data['range'].notna()] 334 | ``` 335 | 336 | ### Analyzing Observation Data 337 | 338 | ```python 339 | import matplotlib.pyplot as plt 340 | from pygnsslab.io.rinex2 import Rinex2Reader 341 | 342 | # Read RINEX 2 file 343 | rinex_reader = Rinex2Reader("path/to/rinex_file.o") 344 | obs_data = rinex_reader.get_obs_data() 345 | 346 | # Group by constellation and count satellites 347 | sat_counts = obs_data.groupby('constellation')['sat'].nunique() 348 | print("Satellites by constellation:") 349 | print(sat_counts) 350 | 351 | # Plot satellite counts 352 | sat_counts.plot(kind='bar') 353 | plt.title('Number of Satellites by Constellation') 354 | plt.xlabel('Constellation') 355 | plt.ylabel('Number of Satellites') 356 | plt.grid(True) 357 | plt.savefig('satellite_counts.png') 358 | 359 | # Analyze SNR distribution 360 | plt.figure() 361 | obs_data['snr'].hist(bins=20) 362 | plt.title('SNR Distribution') 363 | plt.xlabel('SNR') 364 | plt.ylabel('Frequency') 365 | plt.grid(True) 366 | plt.savefig('snr_distribution.png') 367 | 368 | # Analyze time coverage 369 | obs_data['mjd_rounded'] = obs_data['mjd'].round(5) # Round to avoid floating point issues 370 | unique_epochs = obs_data['mjd_rounded'].unique() 371 | print(f"Number of unique epochs: {len(unique_epochs)}") 372 | print(f"First epoch: {unique_epochs.min()}") 373 | print(f"Last epoch: {unique_epochs.max()}") 374 | print(f"Approximate interval: {(unique_epochs[1:] - unique_epochs[:-1]).mean() * 86400:.1f} seconds") 375 | ``` 376 | 377 | ### Converting Between RINEX 2 and RINEX 3 Format Data 378 | 379 | The module's DataFrame structure is designed to be compatible with the RINEX 3 module, allowing for easy conversion between formats. 380 | 381 | ```python 382 | from pygnsslab.io.rinex2 import Rinex2Reader 383 | from pygnsslab.io.rinex3.writer import write_to_parquet as write_rinex3 384 | 385 | # Read RINEX 2 file 386 | rinex2_reader = Rinex2Reader("path/to/rinex_file.o") 387 | obs_data = rinex2_reader.get_obs_data() 388 | metadata = rinex2_reader.metadata 389 | 390 | # Convert datetime objects to strings for JSON serialization 391 | if metadata.get('firstObsTime'): 392 | metadata['firstObsTime'] = metadata['firstObsTime'].isoformat() 393 | if metadata.get('lastObsTime'): 394 | metadata['lastObsTime'] = metadata['lastObsTime'].isoformat() 395 | 396 | # Save in RINEX 3 compatible format 397 | output_file = "output_rinex3_format.parquet" 398 | metadata_file = "output_rinex3_metadata.json" 399 | 400 | # Use the RINEX 3 writer 401 | write_rinex3(obs_data, output_file, metadata, metadata_file) 402 | ``` 403 | 404 | ## Handling RINEX 2 Specific Features 405 | 406 | ### Wavelength Factors 407 | 408 | RINEX 2 files may contain wavelength factor information, which is stored in the metadata dictionary. 409 | 410 | ```python 411 | from pygnsslab.io.rinex2 import Rinex2Reader 412 | 413 | # Read RINEX 2 file 414 | rinex_reader = Rinex2Reader("path/to/rinex_file.o") 415 | metadata = rinex_reader.metadata 416 | 417 | # Access wavelength factors 418 | wavelength_factors = metadata.get('wavelengthFactors', []) 419 | 420 | for wf in wavelength_factors: 421 | print(f"L1 factor: {wf['L1']}") 422 | print(f"L2 factor: {wf['L2']}") 423 | if wf['satellites']: 424 | print(f"Applies to satellites: {', '.join(wf['satellites'])}") 425 | else: 426 | print("Applies to all satellites") 427 | ``` 428 | 429 | ### Two-Digit Year Handling 430 | 431 | RINEX 2 files often use two-digit years, which are handled by the parser according to the following convention: 432 | - Years 00-79 are interpreted as 2000-2079 433 | - Years 80-99 are interpreted as 1980-1999 434 | 435 | ```python 436 | from pygnsslab.io.rinex2 import Rinex2Reader 437 | 438 | # Read RINEX 2 file with two-digit year 439 | rinex_reader = Rinex2Reader("path/to/rinex_file.99o") # Year 1999 440 | metadata = rinex_reader.metadata 441 | 442 | # Access first observation time (correctly interpreted as 1999) 443 | first_obs = metadata.get('firstObsTime') 444 | print(f"First observation time: {first_obs}") 445 | ``` 446 | 447 | ## Limitations and Considerations 448 | 449 | 1. **Memory Usage**: Processing large RINEX files can be memory-intensive, especially when loading the entire observation dataset into a pandas DataFrame. 450 | 451 | 2. **File Compatibility**: The module is designed specifically for RINEX 2 format. It may not work correctly with RINEX 1, RINEX 3, or other specialized variations. 452 | 453 | 3. **Missing Data**: RINEX files may contain missing or corrupted data. The module handles these cases by setting corresponding values to None in the DataFrame. 454 | 455 | 4. **Performance**: The current implementation prioritizes readability and correctness over performance. For very large files or high-performance applications, additional optimizations may be necessary. 456 | 457 | 5. **Two-Digit Year Ambiguity**: The RINEX 2 standard uses two-digit years which can be ambiguous. The parser follows the convention that years 00-79 are interpreted as 2000-2079 and years 80-99 are interpreted as 1980-1999. 458 | 459 | 6. **Constellation Compatibility**: RINEX 2 was developed primarily for GPS observations, with limited support for other GNSS constellations. The module attempts to handle multi-constellation RINEX 2 files, but some specialized constellation-specific data may not be properly parsed. 460 | 461 | ## Differences Between RINEX 2 and RINEX 3 Handling 462 | 463 | The `pygnsslab.io.rinex2` module is designed to work alongside the `pygnsslab.io.rinex3` module, with several key differences in how data is processed: 464 | 465 | 1. **Observation Types**: 466 | - RINEX 2: Observation types are defined globally for all satellites in the header 467 | - RINEX 3: Observation types are defined per constellation 468 | 469 | 2. **Time Format**: 470 | - RINEX 2: Often uses two-digit years with specific epoch line formatting 471 | - RINEX 3: Uses four-digit years with a different epoch line format 472 | 473 | 3. **Multi-Constellation Support**: 474 | - RINEX 2: Limited built-in support for multiple constellations 475 | - RINEX 3: Full support for multiple GNSS constellations 476 | 477 | 4. **Compatibility Layer**: 478 | - The module converts RINEX 2 observation types to a RINEX 3-like structure where they are organized by constellation 479 | - Original global observation type list is preserved under the 'ALL' key in metadata 480 | 481 | ## Conclusion 482 | 483 | The `pygnsslab.io.rinex2` module provides a comprehensive set of tools for working with RINEX 2 observation files. It enables users to extract metadata, process observation data, convert between different time formats, and save processed data to more efficient file formats. The module is designed to work seamlessly with the RINEX 3 module, allowing for consistent processing of different RINEX version files. -------------------------------------------------------------------------------- /docs/detailed/io/rinex3.md: -------------------------------------------------------------------------------- 1 | # pygnsslab.io.rinex3 Module Documentation 2 | 3 | ## Overview 4 | 5 | The `pygnsslab.io.rinex3` module is a Python package designed for parsing, processing, and manipulating RINEX 3 observation files. RINEX (Receiver Independent Exchange Format) is a standard format for GNSS (Global Navigation Satellite System) data. This module provides tools for extracting metadata and observation data from RINEX 3 files, converting between different time formats, and saving processed data to more efficient formats. 6 | 7 | ## Module Structure 8 | 9 | The `pygnsslab.io.rinex3` module consists of the following Python files: 10 | 11 | - `__init__.py`: Main module initialization that exports key functionality 12 | - `reader.py`: Contains the `Rinex3Reader` class for parsing RINEX 3 files 13 | - `writer.py`: Functions for saving observation data to parquet format 14 | - `metadata.py`: Functions for handling RINEX metadata 15 | - `utils.py`: Utility functions for time conversions and file operations 16 | - `example_usage.py`: Example script demonstrating how to use the module 17 | 18 | ## Installation 19 | 20 | The module is part of the `pygnsslab` package and is located at `./src/pygnsslab/io/rinex3`. To use it, you need to install the `pygnsslab` package. 21 | 22 | ## Key Components 23 | 24 | ### Rinex3Reader 25 | 26 | The `Rinex3Reader` class is the primary interface for reading and parsing RINEX 3 observation files. 27 | 28 | #### Usage 29 | 30 | ```python 31 | from pygnsslab.io.rinex3 import Rinex3Reader 32 | 33 | # Initialize the reader with a RINEX 3 file 34 | rinex_reader = Rinex3Reader("path/to/rinex_file.rnx") 35 | 36 | # Access metadata 37 | metadata = rinex_reader.metadata 38 | 39 | # Access observation data as a pandas DataFrame 40 | obs_data = rinex_reader.get_obs_data() 41 | ``` 42 | 43 | #### Input 44 | 45 | - RINEX 3 observation files (typically with extensions `.rnx`, `.crx`, or `.obs`) 46 | 47 | #### Output 48 | 49 | - `metadata`: Dictionary containing header information from the RINEX file 50 | - `obs_data`: Pandas DataFrame containing observation data 51 | 52 | ### Metadata Handling 53 | 54 | The `metadata.py` module provides functions for extracting, formatting, and saving header metadata from RINEX 3 files. 55 | 56 | #### Key Functions 57 | 58 | - `extract_metadata(rinex_header)`: Extracts and formats metadata from RINEX header 59 | - `save_metadata(metadata, output_file)`: Saves metadata to a JSON file 60 | - `load_metadata(metadata_file)`: Loads metadata from a JSON file 61 | - `compare_metadata(metadata1, metadata2)`: Compares two metadata dictionaries and returns their differences 62 | 63 | #### Usage 64 | 65 | ```python 66 | from pygnsslab.io.rinex3.metadata import extract_metadata, save_metadata, load_metadata, compare_metadata 67 | 68 | # Extract metadata from RINEX header 69 | metadata = extract_metadata(rinex_header) 70 | 71 | # Save metadata to a JSON file 72 | save_metadata(metadata, "output_metadata.json") 73 | 74 | # Load metadata from a JSON file 75 | loaded_metadata = load_metadata("metadata.json") 76 | 77 | # Compare two metadata dictionaries 78 | differences = compare_metadata(metadata1, metadata2) 79 | ``` 80 | 81 | ### Data Writing 82 | 83 | The `writer.py` module provides functions for saving RINEX observation data to parquet format and creating summary reports. 84 | 85 | #### Key Functions 86 | 87 | - `write_to_parquet(obs_data, output_file, metadata=None, metadata_file=None)`: Writes observation data to a parquet file and optionally saves metadata 88 | - `write_obs_summary(obs_data, output_file)`: Writes a summary of observation data to a text file 89 | 90 | #### Usage 91 | 92 | ```python 93 | from pygnsslab.io.rinex3.writer import write_to_parquet, write_obs_summary 94 | 95 | # Write observation data to parquet format 96 | parquet_file, metadata_file = write_to_parquet(obs_data, "output.parquet", metadata) 97 | 98 | # Write observation data summary 99 | summary_file = write_obs_summary(obs_data, "summary.txt") 100 | ``` 101 | 102 | ### Utility Functions 103 | 104 | The `utils.py` module provides utility functions for time conversions and file operations. 105 | 106 | #### Time Conversion Functions 107 | 108 | - `datetime_to_mjd(dt)`: Converts a datetime object to Modified Julian Date (MJD) 109 | - `datetime_to_jd(dt)`: Converts a datetime object to Julian Date 110 | - `mjd_to_datetime(mjd)`: Converts Modified Julian Date to datetime object 111 | - `jd_to_datetime(jd)`: Converts Julian Date to datetime object 112 | - `time_to_seconds_of_day(time_obj)`: Converts a time object to seconds of day 113 | 114 | #### File Operations 115 | 116 | - `is_rinex3_file(filename)`: Checks if a file is a RINEX 3 observation file 117 | - `find_rinex3_files(directory)`: Finds all RINEX 3 observation files in a directory 118 | 119 | #### Usage 120 | 121 | ```python 122 | from pygnsslab.io.rinex3.utils import datetime_to_mjd, is_rinex3_file, find_rinex3_files 123 | import datetime 124 | 125 | # Convert datetime to MJD 126 | dt = datetime.datetime.now() 127 | mjd = datetime_to_mjd(dt) 128 | 129 | # Check if a file is a RINEX 3 file 130 | is_rinex3 = is_rinex3_file("path/to/file.rnx") 131 | 132 | # Find all RINEX 3 files in a directory 133 | rinex_files = find_rinex3_files("path/to/directory") 134 | ``` 135 | 136 | ## Data Formats 137 | 138 | ### RINEX 3 Format 139 | 140 | The RINEX 3 format is a standard format for GNSS data. RINEX 3 observation files contain: 141 | 142 | 1. **Header Section**: Contains metadata about the observation session, including station information, receiver and antenna details, and observation types. 143 | 2. **Observation Section**: Contains the actual observation data from different satellites and constellations. 144 | 145 | ### Metadata Structure 146 | 147 | The extracted metadata from RINEX 3 files is structured as a Python dictionary with the following key fields: 148 | 149 | - `rinexVersion`: RINEX version number 150 | - `stationName`: Name of the station 151 | - `approxPos`: Approximate position of the station (X, Y, Z coordinates) 152 | - `receiverNumber`: Receiver number 153 | - `receiverType`: Receiver type 154 | - `receiverVersion`: Receiver firmware version 155 | - `antennaNumber`: Antenna number 156 | - `antennaType`: Antenna type 157 | - `observationTypes`: Dictionary of observation types by constellation 158 | 159 | ### Observation Data Structure 160 | 161 | The observation data is structured as a pandas DataFrame with the following columns: 162 | 163 | - `date`: Observation date 164 | - `epoch_sec`: Seconds of day 165 | - `mjd`: Modified Julian Date 166 | - `constellation`: GNSS constellation (G=GPS, R=GLONASS, E=Galileo, C=BeiDou, J=QZSS, S=SBAS) 167 | - `sat`: Satellite identifier 168 | - `channel`: Signal channel/frequency 169 | - `range`: Pseudorange measurement 170 | - `phase`: Carrier phase measurement 171 | - `doppler`: Doppler measurement 172 | - `snr`: Signal-to-noise ratio 173 | 174 | ## Example Usage 175 | 176 | ### Basic Example 177 | 178 | ```python 179 | from pygnsslab.io.rinex3 import Rinex3Reader 180 | from pygnsslab.io.rinex3.writer import write_to_parquet 181 | import os 182 | 183 | # Path to RINEX 3 file 184 | rinex_file = "path/to/rinex_file.rnx" 185 | 186 | # Read RINEX 3 file 187 | rinex_reader = Rinex3Reader(rinex_file) 188 | 189 | # Get metadata and observations 190 | metadata = rinex_reader.metadata 191 | obs_data = rinex_reader.get_obs_data() 192 | 193 | # Save to parquet format 194 | output_dir = "output" 195 | os.makedirs(output_dir, exist_ok=True) 196 | base_name = os.path.splitext(os.path.basename(rinex_file))[0] 197 | parquet_file = os.path.join(output_dir, f"{base_name}.parquet") 198 | metadata_file = os.path.join(output_dir, f"{base_name}_metadata.json") 199 | 200 | # Write to parquet with metadata 201 | write_to_parquet(obs_data, parquet_file, metadata, metadata_file) 202 | ``` 203 | 204 | ### Processing Multiple Files 205 | 206 | ```python 207 | from pygnsslab.io.rinex3 import Rinex3Reader 208 | from pygnsslab.io.rinex3.writer import write_to_parquet 209 | from pygnsslab.io.rinex3.utils import find_rinex3_files 210 | import os 211 | 212 | # Find all RINEX 3 files in a directory 213 | rinex_dir = "path/to/rinex_directory" 214 | rinex_files = find_rinex3_files(rinex_dir) 215 | 216 | # Process each file 217 | output_dir = "output" 218 | os.makedirs(output_dir, exist_ok=True) 219 | 220 | for rinex_file in rinex_files: 221 | # Extract base name 222 | base_name = os.path.splitext(os.path.basename(rinex_file))[0] 223 | 224 | # Read RINEX 3 file 225 | print(f"Processing {rinex_file}...") 226 | rinex_reader = Rinex3Reader(rinex_file) 227 | 228 | # Get metadata and observations 229 | metadata = rinex_reader.metadata 230 | obs_data = rinex_reader.get_obs_data() 231 | 232 | # Save to parquet format 233 | parquet_file = os.path.join(output_dir, f"{base_name}.parquet") 234 | metadata_file = os.path.join(output_dir, f"{base_name}_metadata.json") 235 | 236 | # Write to parquet with metadata 237 | write_to_parquet(obs_data, parquet_file, metadata, metadata_file) 238 | 239 | print(f"Saved to {parquet_file} and {metadata_file}") 240 | ``` 241 | 242 | ### Comparing Metadata from Multiple Files 243 | 244 | ```python 245 | from pygnsslab.io.rinex3 import Rinex3Reader 246 | from pygnsslab.io.rinex3.metadata import compare_metadata 247 | import os 248 | 249 | # Paths to RINEX 3 files 250 | rinex_file1 = "path/to/rinex_file1.rnx" 251 | rinex_file2 = "path/to/rinex_file2.rnx" 252 | 253 | # Read RINEX 3 files 254 | rinex_reader1 = Rinex3Reader(rinex_file1) 255 | rinex_reader2 = Rinex3Reader(rinex_file2) 256 | 257 | # Get metadata 258 | metadata1 = rinex_reader1.metadata 259 | metadata2 = rinex_reader2.metadata 260 | 261 | # Compare metadata 262 | differences = compare_metadata(metadata1, metadata2) 263 | 264 | # Print differences 265 | if differences: 266 | print("Differences found in metadata:") 267 | for key, diff in differences.items(): 268 | print(f" {key}: {diff}") 269 | else: 270 | print("No differences found in metadata") 271 | ``` 272 | 273 | ## Advanced Usage 274 | 275 | ### Filtering Observation Data 276 | 277 | ```python 278 | from pygnsslab.io.rinex3 import Rinex3Reader 279 | import pandas as pd 280 | 281 | # Read RINEX 3 file 282 | rinex_reader = Rinex3Reader("path/to/rinex_file.rnx") 283 | obs_data = rinex_reader.get_obs_data() 284 | 285 | # Filter by constellation (e.g., GPS only) 286 | gps_data = obs_data[obs_data['constellation'] == 'G'] 287 | 288 | # Filter by satellite 289 | sat_data = obs_data[obs_data['sat'] == 'G01'] 290 | 291 | # Filter by time range 292 | start_mjd = 59000.0 293 | end_mjd = 59001.0 294 | time_filtered_data = obs_data[(obs_data['mjd'] >= start_mjd) & (obs_data['mjd'] <= end_mjd)] 295 | 296 | # Filter by signal quality (SNR > 30) 297 | quality_data = obs_data[obs_data['snr'] > 30] 298 | ``` 299 | 300 | ### Analyzing Observation Data 301 | 302 | ```python 303 | import matplotlib.pyplot as plt 304 | from pygnsslab.io.rinex3 import Rinex3Reader 305 | 306 | # Read RINEX 3 file 307 | rinex_reader = Rinex3Reader("path/to/rinex_file.rnx") 308 | obs_data = rinex_reader.get_obs_data() 309 | 310 | # Group by constellation and count satellites 311 | sat_counts = obs_data.groupby('constellation')['sat'].nunique() 312 | print("Satellites by constellation:") 313 | print(sat_counts) 314 | 315 | # Plot satellite counts 316 | sat_counts.plot(kind='bar') 317 | plt.title('Number of Satellites by Constellation') 318 | plt.xlabel('Constellation') 319 | plt.ylabel('Number of Satellites') 320 | plt.grid(True) 321 | plt.savefig('satellite_counts.png') 322 | 323 | # Analyze SNR distribution 324 | plt.figure() 325 | obs_data['snr'].hist(bins=20) 326 | plt.title('SNR Distribution') 327 | plt.xlabel('SNR') 328 | plt.ylabel('Frequency') 329 | plt.grid(True) 330 | plt.savefig('snr_distribution.png') 331 | ``` 332 | 333 | ## Limitations and Considerations 334 | 335 | 1. **Memory Usage**: Processing large RINEX files can be memory-intensive, especially when loading the entire observation dataset into a pandas DataFrame. 336 | 337 | 2. **File Compatibility**: The module is designed specifically for RINEX 3 format. It may not work correctly with RINEX 2 or other variations. 338 | 339 | 3. **Missing Data**: RINEX files may contain missing or corrupted data. The module handles these cases by setting corresponding values to None in the DataFrame. 340 | 341 | 4. **Performance**: The current implementation prioritizes readability and correctness over performance. For very large files or high-performance applications, additional optimizations may be necessary. 342 | 343 | ## Conclusion 344 | 345 | The `pygnsslab.io.rinex3` module provides a comprehensive set of tools for working with RINEX 3 observation files. It enables users to extract metadata, process observation data, and convert between different time formats and file formats. With its intuitive API and versatile functionality, it serves as a valuable resource for GNSS data processing and analysis. -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | modules/rinex2 8 | modules/rinex3 9 | modules/rinexnav 10 | 11 | Detailed Module Documentation 12 | ----------------------------- 13 | 14 | This section contains the auto-generated API documentation for each module. -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.insert(0, os.path.abspath('../../src')) 4 | 5 | # Configuration file for the Sphinx documentation builder. 6 | # 7 | # For the full list of built-in configuration values, see the documentation: 8 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 9 | 10 | # -- Project information ----------------------------------------------------- 11 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 12 | 13 | project = 'pygnsslab' 14 | copyright = '2025, Cemali Altuntas, PyGNSSLab' 15 | author = 'Cemali Altuntas, PyGNSSLab' 16 | release = '0.1.0' 17 | 18 | # -- General configuration --------------------------------------------------- 19 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 20 | 21 | extensions = [ 22 | 'sphinx.ext.autodoc', 23 | 'sphinx.ext.napoleon', # Support for Google and NumPy style docstrings 24 | 'sphinx.ext.viewcode', # Add links to source code 25 | 'sphinx.ext.intersphinx', # Link to other projects' documentation 26 | # Add 'myst_parser' if you want to use Markdown 27 | ] 28 | 29 | # Configuration for intersphinx: refer to the Python standard library. 30 | intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} 31 | 32 | # Napoleon settings (optional, if you want to customize) 33 | napoleon_google_docstring = True 34 | napoleon_numpy_docstring = True 35 | napoleon_include_init_with_doc = False 36 | napoleon_include_private_with_doc = False 37 | napoleon_include_special_with_doc = True 38 | napoleon_use_admonition_for_examples = False 39 | napoleon_use_admonition_for_notes = False 40 | napoleon_use_admonition_for_references = False 41 | napoleon_use_ivar = False 42 | napoleon_use_param = True 43 | napoleon_use_rtype = True 44 | 45 | templates_path = ['_templates'] 46 | exclude_patterns = [] 47 | 48 | 49 | 50 | # -- Options for HTML output ------------------------------------------------- 51 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 52 | 53 | html_theme = 'sphinx_rtd_theme' # Use the Read the Docs theme 54 | html_static_path = ['_static'] 55 | 56 | # Add any paths that contain custom static files (such as style sheets) here, 57 | # relative to this directory. They are copied after the builtin static files, 58 | # so a file named "default.css" will overwrite the builtin "default.css". 59 | # html_static_path = ['_static'] 60 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. pygnsslab documentation master file, created by 2 | sphinx-quickstart on Fri Apr 25 15:30:31 2025. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | pygnsslab documentation 7 | ======================= 8 | 9 | Add your content using ``reStructuredText`` syntax. See the 10 | `reStructuredText `_ 11 | documentation for details. 12 | 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | :caption: Contents: 17 | 18 | usage 19 | api 20 | 21 | Indices and tables 22 | ================== 23 | 24 | -------------------------------------------------------------------------------- /docs/source/modules/rinex2.rst: -------------------------------------------------------------------------------- 1 | pygnsslab.io.rinex2 2 | ===================== 3 | 4 | .. automodule:: pygnsslab.io.rinex2 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/modules/rinex3.rst: -------------------------------------------------------------------------------- 1 | pygnsslab.io.rinex3 2 | ===================== 3 | 4 | .. automodule:: pygnsslab.io.rinex3 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/modules/rinexnav.rst: -------------------------------------------------------------------------------- 1 | pygnsslab.io.rinexnav 2 | ======================= 3 | 4 | .. automodule:: pygnsslab.io.rinexnav 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | This section will contain usage examples and tutorials. -------------------------------------------------------------------------------- /pygnsslab_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyGnssLab/pygnsslab/ed124951cf3db268a9fc6c63392a308b84680220/pygnsslab_logo.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45", "wheel"] 3 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pandas 3 | matplotlib 4 | setuptools 5 | pytest 6 | pyarrow 7 | sphinx 8 | sphinx-rtd-theme 9 | myst-parser -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | # Read requirements from requirements.txt 4 | with open("requirements.txt", "r") as f: 5 | requirements = [line.strip() for line in f if line.strip() and not line.startswith("#")] 6 | 7 | setup( 8 | name="pygnsslab", 9 | version="0.1.0", 10 | packages=find_packages(where="src"), 11 | package_dir={"": "src"}, 12 | install_requires=requirements, 13 | ) -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | pygnsslab - A Python package for GNSS data processing and analysis 3 | """ 4 | -------------------------------------------------------------------------------- /src/pygnsslab/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyGnssLab/pygnsslab/ed124951cf3db268a9fc6c63392a308b84680220/src/pygnsslab/__init__.py -------------------------------------------------------------------------------- /src/pygnsslab/io/rinex2/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | RINEX 2 parser module for reading observation files. 3 | Extracts header metadata and observation data. 4 | """ 5 | 6 | from .reader import Rinex2Reader 7 | from .writer import write_to_parquet 8 | 9 | __all__ = ['Rinex2Reader', 'write_to_parquet'] -------------------------------------------------------------------------------- /src/pygnsslab/io/rinex2/example_usage.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example script demonstrating how to use the RINEX 2 parser module. 3 | """ 4 | 5 | import os 6 | import json 7 | import logging 8 | import pandas as pd 9 | from pathlib import Path 10 | from datetime import datetime 11 | from pygnsslab.io.rinex2.reader import Rinex2Reader 12 | from pygnsslab.io.rinex2.writer import write_to_parquet 13 | from pygnsslab.io.rinex2.utils import find_rinex2_files 14 | 15 | def process_rinex_file(rinex_file, output_dir): 16 | """ 17 | Process a RINEX 2 file, extracting metadata and observation data. 18 | 19 | Parameters: 20 | ----------- 21 | rinex_file : str 22 | Path to the RINEX 2 file 23 | output_dir : str 24 | Path to the output directory 25 | 26 | Returns: 27 | -------- 28 | tuple 29 | (metadata_file_path, parquet_file_path) 30 | """ 31 | # Create output directory if it doesn't exist 32 | os.makedirs(output_dir, exist_ok=True) 33 | 34 | # Get the base filename without extension 35 | base_name = Path(rinex_file).stem 36 | 37 | # Create output file paths 38 | metadata_file = os.path.join(output_dir, f"{base_name}_metadata.json") 39 | parquet_file = os.path.join(output_dir, f"{base_name}_observations.parquet") 40 | 41 | # Read RINEX 2 file 42 | print(f"Reading RINEX file: {rinex_file}") 43 | rinex_reader = Rinex2Reader(rinex_file) 44 | 45 | # Get metadata and observations 46 | metadata = rinex_reader.metadata 47 | obs_data = rinex_reader.get_obs_data() 48 | 49 | # Convert datetime objects to ISO format strings 50 | if metadata.get('firstObsTime'): 51 | metadata['firstObsTime'] = metadata['firstObsTime'].isoformat() 52 | if metadata.get('lastObsTime'): 53 | metadata['lastObsTime'] = metadata['lastObsTime'].isoformat() 54 | 55 | # Save metadata to JSON 56 | with open(metadata_file, 'w') as f: 57 | json.dump(metadata, f, indent=2) 58 | print(f"Metadata saved to: {metadata_file}") 59 | 60 | # Save observations to parquet 61 | write_to_parquet(obs_data, parquet_file, metadata, metadata_file) 62 | print(f"Observations saved to: {parquet_file}") 63 | 64 | return metadata_file, parquet_file 65 | 66 | def print_metadata_summary(metadata_file): 67 | """ 68 | Print a summary of the metadata. 69 | 70 | Parameters: 71 | ----------- 72 | metadata_file : str 73 | Path to the metadata JSON file 74 | """ 75 | with open(metadata_file, 'r') as f: 76 | metadata = json.load(f) 77 | 78 | print("\nRINEX Metadata Summary:") 79 | print(f" RINEX Version: {metadata.get('rinexVersion', 'N/A')}") 80 | print(f" Station Name: {metadata.get('stationName', 'N/A')}") 81 | print(f" Receiver Type: {metadata.get('receiverType', 'N/A')}") 82 | print(f" Antenna Type: {metadata.get('antennaType', 'N/A')}") 83 | 84 | # Print observation types 85 | print("\nObservation Types:") 86 | for constellation, obs_types in metadata.get('observationTypes', {}).items(): 87 | # Join observation types with commas and wrap at 60 characters 88 | obs_str = ', '.join(obs_types) 89 | # Split into lines of max 60 characters 90 | obs_lines = [obs_str[i:i+60] for i in range(0, len(obs_str), 60)] 91 | print(f" {constellation}: {obs_lines[0]}") 92 | for line in obs_lines[1:]: 93 | print(f" {line}") 94 | 95 | # Print time information 96 | print("\nTime Information:") 97 | print(f" First Observation: {metadata.get('firstObsTime', 'N/A')}") 98 | print(f" Last Observation: {metadata.get('lastObsTime', 'N/A')}") 99 | print(f" Interval: {metadata.get('interval', 'N/A')} seconds") 100 | 101 | def main(): 102 | """ 103 | Main function to demonstrate the RINEX 2 parser. 104 | """ 105 | # Example file path (replace with an actual file path) 106 | rinex_file = os.path.join("data", "obs", "ajac0010.22o") 107 | 108 | # Check if the file exists 109 | if not os.path.exists(rinex_file): 110 | print(f"File not found: {rinex_file}") 111 | print("Please provide a valid RINEX 2 observation file path.") 112 | return 113 | 114 | # Process the file 115 | output_dir = "data/jsonpqt" 116 | metadata_file, parquet_file = process_rinex_file(rinex_file, output_dir) 117 | 118 | # Print metadata summary 119 | print_metadata_summary(metadata_file) 120 | 121 | # Alternatively, process all RINEX 2 files in a directory 122 | # rinex_dir = "rinex_data" 123 | # rinex_files = find_rinex2_files(rinex_dir) 124 | # for rinex_file in rinex_files: 125 | # process_rinex_file(rinex_file, output_dir) 126 | 127 | if __name__ == "__main__": 128 | main() -------------------------------------------------------------------------------- /src/pygnsslab/io/rinex2/metadata.py: -------------------------------------------------------------------------------- 1 | """ 2 | Metadata handling module for RINEX 2 files. 3 | Provides functions to extract, format, and save header metadata. 4 | """ 5 | 6 | import json 7 | import os 8 | from pathlib import Path 9 | 10 | def extract_metadata(rinex_header): 11 | """ 12 | Extract and format metadata from RINEX header. 13 | 14 | Parameters: 15 | ----------- 16 | rinex_header : dict 17 | Dictionary containing RINEX header information 18 | 19 | Returns: 20 | -------- 21 | dict 22 | Formatted metadata dictionary 23 | """ 24 | metadata = { 25 | 'rinexVersion': rinex_header.get('rinexVersion'), 26 | 'stationName': rinex_header.get('stationName'), 27 | 'approxPos': rinex_header.get('approxPos'), 28 | 'receiverNumber': rinex_header.get('receiverNumber'), 29 | 'receiverType': rinex_header.get('receiverType'), 30 | 'receiverVersion': rinex_header.get('receiverVersion'), 31 | 'antennaNumber': rinex_header.get('antennaNumber'), 32 | 'antennaType': rinex_header.get('antennaType'), 33 | 'observationTypes': dict(rinex_header.get('observationTypes', {})), 34 | 'firstObsTime': rinex_header.get('firstObsTime'), 35 | 'lastObsTime': rinex_header.get('lastObsTime'), 36 | 'interval': rinex_header.get('interval') 37 | } 38 | 39 | # Convert datetime objects to strings for JSON serialization 40 | if metadata['firstObsTime']: 41 | metadata['firstObsTime'] = metadata['firstObsTime'].isoformat() 42 | if metadata['lastObsTime']: 43 | metadata['lastObsTime'] = metadata['lastObsTime'].isoformat() 44 | 45 | return metadata 46 | 47 | def save_metadata(metadata, output_file): 48 | """ 49 | Save metadata to a JSON file. 50 | 51 | Parameters: 52 | ----------- 53 | metadata : dict 54 | Metadata dictionary 55 | output_file : str 56 | Path to the output JSON file 57 | 58 | Returns: 59 | -------- 60 | str 61 | Path to the output JSON file 62 | """ 63 | # Create output directory if it doesn't exist 64 | output_path = Path(output_file) 65 | output_dir = output_path.parent 66 | os.makedirs(output_dir, exist_ok=True) 67 | 68 | # Write metadata to JSON 69 | with open(output_file, 'w') as f: 70 | json.dump(metadata, f, indent=2) 71 | 72 | return output_file 73 | 74 | def load_metadata(metadata_file): 75 | """ 76 | Load metadata from a JSON file. 77 | 78 | Parameters: 79 | ----------- 80 | metadata_file : str 81 | Path to the metadata JSON file 82 | 83 | Returns: 84 | -------- 85 | dict 86 | Metadata dictionary 87 | """ 88 | with open(metadata_file, 'r') as f: 89 | metadata = json.load(f) 90 | 91 | return metadata 92 | 93 | def compare_metadata(metadata1, metadata2): 94 | """ 95 | Compare two metadata dictionaries and return their differences. 96 | 97 | Parameters: 98 | ----------- 99 | metadata1 : dict 100 | First metadata dictionary 101 | metadata2 : dict 102 | Second metadata dictionary 103 | 104 | Returns: 105 | -------- 106 | dict 107 | Dictionary of differences 108 | """ 109 | differences = {} 110 | 111 | # Compare simple key-value pairs 112 | for key in set(metadata1.keys()) | set(metadata2.keys()): 113 | if key == 'observationTypes': 114 | continue # Handle separately 115 | 116 | if key not in metadata1: 117 | differences[key] = {'in_metadata1': None, 'in_metadata2': metadata2[key]} 118 | elif key not in metadata2: 119 | differences[key] = {'in_metadata1': metadata1[key], 'in_metadata2': None} 120 | elif metadata1[key] != metadata2[key]: 121 | differences[key] = {'in_metadata1': metadata1[key], 'in_metadata2': metadata2[key]} 122 | 123 | # Compare observation types 124 | if 'observationTypes' in metadata1 and 'observationTypes' in metadata2: 125 | obs_types1 = metadata1['observationTypes'] 126 | obs_types2 = metadata2['observationTypes'] 127 | 128 | obs_diff = {} 129 | 130 | # Check constellations in both 131 | for const in set(obs_types1.keys()) | set(obs_types2.keys()): 132 | if const not in obs_types1: 133 | obs_diff[const] = {'in_metadata1': None, 'in_metadata2': obs_types2[const]} 134 | elif const not in obs_types2: 135 | obs_diff[const] = {'in_metadata1': obs_types1[const], 'in_metadata2': None} 136 | elif set(obs_types1[const]) != set(obs_types2[const]): 137 | # Find differences in observation types 138 | only_in_1 = set(obs_types1[const]) - set(obs_types2[const]) 139 | only_in_2 = set(obs_types2[const]) - set(obs_types1[const]) 140 | 141 | obs_diff[const] = { 142 | 'only_in_metadata1': list(only_in_1) if only_in_1 else None, 143 | 'only_in_metadata2': list(only_in_2) if only_in_2 else None 144 | } 145 | 146 | if obs_diff: 147 | differences['observationTypes'] = obs_diff 148 | 149 | return differences -------------------------------------------------------------------------------- /src/pygnsslab/io/rinex2/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for RINEX 2 processing. 3 | """ 4 | 5 | import datetime 6 | import numpy as np 7 | import os 8 | from pathlib import Path 9 | 10 | def datetime_to_mjd(dt): 11 | """ 12 | Convert a datetime object to Modified Julian Date (MJD). 13 | 14 | Parameters: 15 | ----------- 16 | dt : datetime.datetime 17 | Datetime object to convert 18 | 19 | Returns: 20 | -------- 21 | float 22 | Modified Julian Date 23 | """ 24 | # Convert to Julian date first 25 | jd = datetime_to_jd(dt) 26 | # Convert Julian date to Modified Julian Date 27 | mjd = jd - 2400000.5 28 | 29 | return mjd 30 | 31 | def datetime_to_jd(dt): 32 | """ 33 | Convert a datetime object to Julian date. 34 | 35 | Parameters: 36 | ----------- 37 | dt : datetime.datetime 38 | Datetime object to convert 39 | 40 | Returns: 41 | -------- 42 | float 43 | Julian date 44 | """ 45 | # Extract components 46 | year, month, day = dt.year, dt.month, dt.day 47 | hour, minute, second = dt.hour, dt.minute, dt.second 48 | microsecond = dt.microsecond 49 | 50 | # Calculate decimal day 51 | decimal_day = day + (hour / 24.0) + (minute / 1440.0) + ((second + microsecond / 1e6) / 86400.0) 52 | 53 | # Calculate Julian date 54 | if month <= 2: 55 | year -= 1 56 | month += 12 57 | 58 | A = int(year / 100) 59 | B = 2 - A + int(A / 4) 60 | 61 | jd = int(365.25 * (year + 4716)) + int(30.6001 * (month + 1)) + decimal_day + B - 1524.5 62 | 63 | return jd 64 | 65 | def mjd_to_datetime(mjd): 66 | """ 67 | Convert Modified Julian Date (MJD) to datetime object. 68 | 69 | Parameters: 70 | ----------- 71 | mjd : float 72 | Modified Julian Date 73 | 74 | Returns: 75 | -------- 76 | datetime.datetime 77 | Datetime object 78 | """ 79 | # Convert MJD to Julian date 80 | jd = mjd + 2400000.5 81 | 82 | # Convert Julian date to datetime 83 | return jd_to_datetime(jd) 84 | 85 | def jd_to_datetime(jd): 86 | """ 87 | Convert Julian date to datetime object. 88 | 89 | Parameters: 90 | ----------- 91 | jd : float 92 | Julian date 93 | 94 | Returns: 95 | -------- 96 | datetime.datetime 97 | Datetime object 98 | """ 99 | jd = jd + 0.5 # Shift by 0.5 day 100 | 101 | # Calculate integer and fractional parts 102 | F, I = np.modf(jd) 103 | I = int(I) 104 | 105 | A = np.trunc((I - 1867216.25) / 36524.25) 106 | B = I + 1 + A - np.trunc(A / 4) 107 | 108 | C = B + 1524 109 | D = np.trunc((C - 122.1) / 365.25) 110 | E = np.trunc(365.25 * D) 111 | G = np.trunc((C - E) / 30.6001) 112 | 113 | # Calculate day, month, year 114 | day = C - E + F - np.trunc(30.6001 * G) 115 | 116 | if G < 13.5: 117 | month = G - 1 118 | else: 119 | month = G - 13 120 | 121 | if month > 2.5: 122 | year = D - 4716 123 | else: 124 | year = D - 4715 125 | 126 | # Calculate time 127 | day_frac = day - int(day) 128 | hours = int(day_frac * 24) 129 | minutes = int((day_frac * 24 - hours) * 60) 130 | seconds = ((day_frac * 24 - hours) * 60 - minutes) * 60 131 | 132 | # Create datetime object 133 | try: 134 | dt = datetime.datetime( 135 | int(year), 136 | int(month), 137 | int(day), 138 | hours, 139 | minutes, 140 | int(seconds), 141 | int((seconds - int(seconds)) * 1e6) 142 | ) 143 | return dt 144 | except ValueError: 145 | # Handling edge cases 146 | if int(day) == 0: 147 | # Handle case where day is calculated as 0 148 | month = int(month) - 1 149 | if month == 0: 150 | month = 12 151 | year = int(year) - 1 152 | days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] 153 | # Adjust for leap year 154 | if month == 2 and (year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)): 155 | day = 29 156 | else: 157 | day = days_in_month[month] 158 | else: 159 | day = int(day) 160 | 161 | return datetime.datetime( 162 | int(year), 163 | int(month), 164 | day, 165 | hours, 166 | minutes, 167 | int(seconds), 168 | int((seconds - int(seconds)) * 1e6) 169 | ) 170 | 171 | def time_to_seconds_of_day(time_obj): 172 | """ 173 | Convert a time object to seconds of day. 174 | 175 | Parameters: 176 | ----------- 177 | time_obj : datetime.time 178 | Time object to convert 179 | 180 | Returns: 181 | -------- 182 | float 183 | Seconds of day 184 | """ 185 | return time_obj.hour * 3600 + time_obj.minute * 60 + time_obj.second + time_obj.microsecond / 1e6 186 | 187 | def is_rinex2_file(filename): 188 | """ 189 | Check if a file is a RINEX 2 observation file. 190 | 191 | Parameters: 192 | ----------- 193 | filename : str 194 | Path to the file 195 | 196 | Returns: 197 | -------- 198 | bool 199 | True if the file is a RINEX 2 observation file, False otherwise 200 | """ 201 | if not os.path.exists(filename): 202 | return False 203 | 204 | # Check file extension 205 | ext = Path(filename).suffix.lower() 206 | if ext not in ['.obs', '.o', '.yyo', '.yyO', '.yyN', '.yyG']: 207 | # Not a standard RINEX 2 observation file extension 208 | return False 209 | 210 | # Check file content 211 | try: 212 | with open(filename, 'r') as f: 213 | # Read first few lines 214 | header_lines = [f.readline() for _ in range(10)] 215 | 216 | # Check for RINEX 2 version marker 217 | for line in header_lines: 218 | if "RINEX VERSION / TYPE" in line and line.strip().startswith("2"): 219 | return True 220 | except: 221 | # If there's an error reading the file, assume it's not a valid RINEX file 222 | return False 223 | 224 | return False 225 | 226 | def find_rinex2_files(directory): 227 | """ 228 | Find all RINEX 2 observation files in a directory. 229 | 230 | Parameters: 231 | ----------- 232 | directory : str 233 | Path to the directory 234 | 235 | Returns: 236 | -------- 237 | list 238 | List of paths to RINEX 2 observation files 239 | """ 240 | rinex_files = [] 241 | 242 | for root, _, files in os.walk(directory): 243 | for file in files: 244 | filepath = os.path.join(root, file) 245 | if is_rinex2_file(filepath): 246 | rinex_files.append(filepath) 247 | 248 | return rinex_files -------------------------------------------------------------------------------- /src/pygnsslab/io/rinex2/writer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Writer module for saving RINEX 2 observation data to parquet format. 3 | """ 4 | 5 | import os 6 | import json 7 | import pandas as pd 8 | from pathlib import Path 9 | 10 | def write_to_parquet(obs_data, output_file, metadata=None, metadata_file=None): 11 | """ 12 | Write observation data to a parquet file and optionally save metadata to a JSON file. 13 | 14 | Parameters: 15 | ----------- 16 | obs_data : pandas.DataFrame 17 | DataFrame containing observation data 18 | output_file : str 19 | Path to the output parquet file 20 | metadata : dict, optional 21 | Metadata dictionary to save 22 | metadata_file : str, optional 23 | Path to the output metadata JSON file. If None but metadata is provided, 24 | a default name will be created based on output_file 25 | 26 | Returns: 27 | -------- 28 | tuple 29 | (parquet_file_path, metadata_file_path) 30 | """ 31 | # Create output directory if it doesn't exist 32 | output_path = Path(output_file) 33 | output_dir = output_path.parent 34 | os.makedirs(output_dir, exist_ok=True) 35 | 36 | # Ensure data has the required columns 37 | required_columns = ['date', 'epoch_sec', 'mjd', 'constellation', 'sat', 38 | 'channel', 'range', 'phase', 'doppler', 'snr'] 39 | 40 | # Create empty columns if they don't exist 41 | for col in required_columns: 42 | if col not in obs_data.columns: 43 | obs_data[col] = None 44 | 45 | # Write observation data to parquet 46 | obs_data.to_parquet(output_file, index=False) 47 | 48 | # Write metadata to JSON if provided 49 | metadata_path = None 50 | if metadata: 51 | if not metadata_file: 52 | # Create default metadata filename 53 | metadata_file = str(output_path.with_suffix('.json')) 54 | 55 | with open(metadata_file, 'w') as f: 56 | json.dump(metadata, f, indent=2) 57 | 58 | metadata_path = metadata_file 59 | 60 | return output_file, metadata_path 61 | 62 | def write_obs_summary(obs_data, output_file): 63 | """ 64 | Write a summary of observation data to a text file. 65 | 66 | Parameters: 67 | ----------- 68 | obs_data : pandas.DataFrame 69 | DataFrame containing observation data 70 | output_file : str 71 | Path to the output summary file 72 | 73 | Returns: 74 | -------- 75 | str 76 | Path to the output summary file 77 | """ 78 | # Create output directory if it doesn't exist 79 | output_path = Path(output_file) 80 | output_dir = output_path.parent 81 | os.makedirs(output_dir, exist_ok=True) 82 | 83 | # Generate summary statistics 84 | summary = {} 85 | 86 | # Count observations by constellation 87 | constellation_counts = obs_data['constellation'].value_counts().to_dict() 88 | summary['observations_by_constellation'] = constellation_counts 89 | 90 | # Count satellites by constellation 91 | sat_counts = obs_data.groupby('constellation')['sat'].nunique().to_dict() 92 | summary['satellites_by_constellation'] = sat_counts 93 | 94 | # Get date range 95 | if 'date' in obs_data.columns and not obs_data['date'].empty: 96 | min_date = obs_data['date'].min() 97 | max_date = obs_data['date'].max() 98 | summary['date_range'] = {'start': min_date, 'end': max_date} 99 | 100 | # Write summary to file 101 | with open(output_file, 'w') as f: 102 | f.write("RINEX 2 Observation Data Summary\n") 103 | f.write("================================\n\n") 104 | 105 | f.write("Observations by Constellation:\n") 106 | for const, count in summary.get('observations_by_constellation', {}).items(): 107 | f.write(f" {const}: {count}\n") 108 | 109 | f.write("\nSatellites by Constellation:\n") 110 | for const, count in summary.get('satellites_by_constellation', {}).items(): 111 | f.write(f" {const}: {count}\n") 112 | 113 | if 'date_range' in summary: 114 | f.write("\nDate Range:\n") 115 | f.write(f" Start: {summary['date_range']['start']}\n") 116 | f.write(f" End: {summary['date_range']['end']}\n") 117 | 118 | return output_file -------------------------------------------------------------------------------- /src/pygnsslab/io/rinex3/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | RINEX 3 parser module for reading observation files. 3 | Extracts header metadata and observation data. 4 | """ 5 | 6 | from .reader import Rinex3Reader 7 | from .writer import write_to_parquet 8 | 9 | __all__ = ['Rinex3Reader', 'write_to_parquet'] -------------------------------------------------------------------------------- /src/pygnsslab/io/rinex3/example_usage.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example script demonstrating how to use the RINEX 3 parser module. 3 | """ 4 | 5 | import os 6 | import json 7 | import logging 8 | import pandas as pd 9 | from pathlib import Path 10 | from datetime import datetime 11 | from pygnsslab.io.rinex3.reader import Rinex3Reader 12 | from pygnsslab.io.rinex3.writer import write_to_parquet 13 | from pygnsslab.io.rinex3.utils import find_rinex3_files 14 | 15 | def process_rinex_file(rinex_file, output_dir): 16 | """ 17 | Process a RINEX 3 file, extracting metadata and observation data. 18 | 19 | Parameters: 20 | ----------- 21 | rinex_file : str 22 | Path to the RINEX 3 file 23 | output_dir : str 24 | Path to the output directory 25 | 26 | Returns: 27 | -------- 28 | tuple 29 | (metadata_file_path, parquet_file_path) 30 | """ 31 | # Create output directory if it doesn't exist 32 | os.makedirs(output_dir, exist_ok=True) 33 | 34 | # Get the base filename without extension 35 | base_name = Path(rinex_file).stem 36 | 37 | # Create output file paths 38 | metadata_file = os.path.join(output_dir, f"{base_name}_metadata.json") 39 | parquet_file = os.path.join(output_dir, f"{base_name}_observations.parquet") 40 | 41 | # Read RINEX 3 file 42 | print(f"Reading RINEX file: {rinex_file}") 43 | rinex_reader = Rinex3Reader(rinex_file) 44 | 45 | # Get metadata and observations 46 | metadata = rinex_reader.metadata 47 | obs_data = rinex_reader.get_obs_data() 48 | 49 | # Save metadata to JSON 50 | with open(metadata_file, 'w') as f: 51 | json.dump(metadata, f, indent=2) 52 | print(f"Metadata saved to: {metadata_file}") 53 | 54 | # Save observations to parquet 55 | write_to_parquet(obs_data, parquet_file, metadata, metadata_file) 56 | print(f"Observations saved to: {parquet_file}") 57 | 58 | return metadata_file, parquet_file 59 | 60 | def print_metadata_summary(metadata_file): 61 | """ 62 | Print a summary of the metadata. 63 | 64 | Parameters: 65 | ----------- 66 | metadata_file : str 67 | Path to the metadata JSON file 68 | """ 69 | with open(metadata_file, 'r') as f: 70 | metadata = json.load(f) 71 | 72 | print("\nRINEX Metadata Summary:") 73 | print(f" RINEX Version: {metadata.get('rinexVersion', 'N/A')}") 74 | print(f" Station Name: {metadata.get('stationName', 'N/A')}") 75 | print(f" Receiver Type: {metadata.get('receiverType', 'N/A')}") 76 | print(f" Antenna Type: {metadata.get('antennaType', 'N/A')}") 77 | 78 | # Print observation types 79 | print("\nObservation Types:") 80 | for constellation, obs_types in metadata.get('observationTypes', {}).items(): 81 | print(f" {constellation}: {len(obs_types)} types") 82 | 83 | def main(): 84 | """ 85 | Main function to demonstrate the RINEX 3 parser. 86 | """ 87 | # Example file path (replace with an actual file path) 88 | rinex_file = os.path.join("data","obs","PTLD00AUS_R_20220010000_01D_30S_MO.rnx") 89 | 90 | # Check if the file exists 91 | if not os.path.exists(rinex_file): 92 | print(f"File not found: {rinex_file}") 93 | print("Please provide a valid RINEX 3 observation file path.") 94 | return 95 | 96 | # Process the file 97 | output_dir = "data/jsonpqt" 98 | metadata_file, parquet_file = process_rinex_file(rinex_file, output_dir) 99 | 100 | # Print metadata summary 101 | print_metadata_summary(metadata_file) 102 | 103 | # Alternatively, process all RINEX 3 files in a directory 104 | # rinex_dir = "rinex_data" 105 | # rinex_files = find_rinex3_files(rinex_dir) 106 | # for rinex_file in rinex_files: 107 | # process_rinex_file(rinex_file, output_dir) 108 | 109 | if __name__ == "__main__": 110 | main() -------------------------------------------------------------------------------- /src/pygnsslab/io/rinex3/metadata.py: -------------------------------------------------------------------------------- 1 | """ 2 | Metadata handling module for RINEX 3 files. 3 | Provides functions to extract, format, and save header metadata. 4 | """ 5 | 6 | import json 7 | import os 8 | from pathlib import Path 9 | 10 | def extract_metadata(rinex_header): 11 | """ 12 | Extract and format metadata from RINEX header. 13 | 14 | Parameters: 15 | ----------- 16 | rinex_header : dict 17 | Dictionary containing RINEX header information 18 | 19 | Returns: 20 | -------- 21 | dict 22 | Formatted metadata dictionary 23 | """ 24 | metadata = { 25 | 'rinexVersion': rinex_header.get('rinexVersion'), 26 | 'stationName': rinex_header.get('stationName'), 27 | 'approxPos': rinex_header.get('approxPos'), 28 | 'receiverNumber': rinex_header.get('receiverNumber'), 29 | 'receiverType': rinex_header.get('receiverType'), 30 | 'receiverVersion': rinex_header.get('receiverVersion'), 31 | 'antennaNumber': rinex_header.get('antennaNumber'), 32 | 'antennaType': rinex_header.get('antennaType'), 33 | 'observationTypes': dict(rinex_header.get('observationTypes', {})) 34 | } 35 | 36 | return metadata 37 | 38 | def save_metadata(metadata, output_file): 39 | """ 40 | Save metadata to a JSON file. 41 | 42 | Parameters: 43 | ----------- 44 | metadata : dict 45 | Metadata dictionary 46 | output_file : str 47 | Path to the output JSON file 48 | 49 | Returns: 50 | -------- 51 | str 52 | Path to the output JSON file 53 | """ 54 | # Create output directory if it doesn't exist 55 | output_path = Path(output_file) 56 | output_dir = output_path.parent 57 | os.makedirs(output_dir, exist_ok=True) 58 | 59 | # Write metadata to JSON 60 | with open(output_file, 'w') as f: 61 | json.dump(metadata, f, indent=2) 62 | 63 | return output_file 64 | 65 | def load_metadata(metadata_file): 66 | """ 67 | Load metadata from a JSON file. 68 | 69 | Parameters: 70 | ----------- 71 | metadata_file : str 72 | Path to the metadata JSON file 73 | 74 | Returns: 75 | -------- 76 | dict 77 | Metadata dictionary 78 | """ 79 | with open(metadata_file, 'r') as f: 80 | metadata = json.load(f) 81 | 82 | return metadata 83 | 84 | def compare_metadata(metadata1, metadata2): 85 | """ 86 | Compare two metadata dictionaries and return their differences. 87 | 88 | Parameters: 89 | ----------- 90 | metadata1 : dict 91 | First metadata dictionary 92 | metadata2 : dict 93 | Second metadata dictionary 94 | 95 | Returns: 96 | -------- 97 | dict 98 | Dictionary of differences 99 | """ 100 | differences = {} 101 | 102 | # Compare simple key-value pairs 103 | for key in set(metadata1.keys()) | set(metadata2.keys()): 104 | if key == 'observationTypes': 105 | continue # Handle separately 106 | 107 | if key not in metadata1: 108 | differences[key] = {'in_metadata1': None, 'in_metadata2': metadata2[key]} 109 | elif key not in metadata2: 110 | differences[key] = {'in_metadata1': metadata1[key], 'in_metadata2': None} 111 | elif metadata1[key] != metadata2[key]: 112 | differences[key] = {'in_metadata1': metadata1[key], 'in_metadata2': metadata2[key]} 113 | 114 | # Compare observation types 115 | if 'observationTypes' in metadata1 and 'observationTypes' in metadata2: 116 | obs_types1 = metadata1['observationTypes'] 117 | obs_types2 = metadata2['observationTypes'] 118 | 119 | obs_diff = {} 120 | 121 | # Check constellations in both 122 | for const in set(obs_types1.keys()) | set(obs_types2.keys()): 123 | if const not in obs_types1: 124 | obs_diff[const] = {'in_metadata1': None, 'in_metadata2': obs_types2[const]} 125 | elif const not in obs_types2: 126 | obs_diff[const] = {'in_metadata1': obs_types1[const], 'in_metadata2': None} 127 | elif set(obs_types1[const]) != set(obs_types2[const]): 128 | # Find differences in observation types 129 | only_in_1 = set(obs_types1[const]) - set(obs_types2[const]) 130 | only_in_2 = set(obs_types2[const]) - set(obs_types1[const]) 131 | 132 | obs_diff[const] = { 133 | 'only_in_metadata1': list(only_in_1) if only_in_1 else None, 134 | 'only_in_metadata2': list(only_in_2) if only_in_2 else None 135 | } 136 | 137 | if obs_diff: 138 | differences['observationTypes'] = obs_diff 139 | 140 | return differences -------------------------------------------------------------------------------- /src/pygnsslab/io/rinex3/reader.py: -------------------------------------------------------------------------------- 1 | """ 2 | RINEX 3 reader module for parsing observation files. 3 | """ 4 | 5 | import os 6 | import re 7 | import datetime 8 | import json 9 | import numpy as np 10 | import pandas as pd 11 | from collections import defaultdict 12 | 13 | class Rinex3Reader: 14 | """ 15 | Class for reading and parsing RINEX 3 observation files. 16 | """ 17 | 18 | def __init__(self, filename): 19 | """ 20 | Initialize the Rinex3Reader with a RINEX 3 observation file. 21 | 22 | Parameters: 23 | ----------- 24 | filename : str 25 | Path to the RINEX 3 observation file 26 | """ 27 | self.filename = filename 28 | self.header = {} 29 | self.obs_data = [] 30 | self.metadata = {} 31 | self._parse_file() 32 | 33 | def _parse_file(self): 34 | """ 35 | Parse the RINEX 3 file, extracting header information and observation data. 36 | """ 37 | if not os.path.exists(self.filename): 38 | raise FileNotFoundError(f"RINEX file not found: {self.filename}") 39 | 40 | # Read the entire file 41 | with open(self.filename, 'r') as f: 42 | lines = f.readlines() 43 | 44 | # Determine where the header ends (look for END OF HEADER marker) 45 | header_end_idx = 0 46 | for i, line in enumerate(lines): 47 | if "END OF HEADER" in line: 48 | header_end_idx = i 49 | break 50 | 51 | # Extract header and observation sections 52 | header_lines = lines[:header_end_idx + 1] 53 | observation_lines = lines[header_end_idx + 1:] 54 | 55 | # Parse header 56 | self._parse_header(header_lines) 57 | 58 | # Parse observations 59 | self._parse_observations(observation_lines) 60 | 61 | def _parse_header(self, header_lines): 62 | """ 63 | Parse the header section of the RINEX file. 64 | 65 | Parameters: 66 | ----------- 67 | header_lines : list 68 | List of strings containing the header section 69 | """ 70 | # Initialize header data structures 71 | self.header = { 72 | 'rinexVersion': None, 73 | 'stationName': None, 74 | 'approxPos': [None, None, None], 75 | 'receiverNumber': None, 76 | 'receiverType': None, 77 | 'receiverVersion': None, 78 | 'antennaNumber': None, 79 | 'antennaType': None, 80 | 'observationTypes': defaultdict(list) 81 | } 82 | 83 | # Parse each header line 84 | for line in header_lines: 85 | label = line[60:].strip() 86 | 87 | # RINEX Version / Type 88 | if "RINEX VERSION / TYPE" in label: 89 | self.header['rinexVersion'] = line[:9].strip() 90 | 91 | # Marker Name 92 | elif "MARKER NAME" in label: 93 | self.header['stationName'] = line[:60].strip() 94 | 95 | # Approx Position XYZ 96 | elif "APPROX POSITION XYZ" in label: 97 | try: 98 | x = float(line[:14].strip()) 99 | y = float(line[14:28].strip()) 100 | z = float(line[28:42].strip()) 101 | self.header['approxPos'] = [x, y, z] 102 | except ValueError: 103 | pass 104 | 105 | # Receiver info 106 | elif "REC # / TYPE / VERS" in label: 107 | self.header['receiverNumber'] = line[:20].strip() 108 | self.header['receiverType'] = line[20:40].strip() 109 | self.header['receiverVersion'] = line[40:60].strip() 110 | 111 | # Antenna info 112 | elif "ANT # / TYPE" in label: 113 | self.header['antennaNumber'] = line[:20].strip() 114 | self.header['antennaType'] = line[20:40].strip() 115 | 116 | # Observation types 117 | elif "SYS / # / OBS TYPES" in label: 118 | constellation = line[0].strip() 119 | if not constellation: 120 | # Continuation line 121 | last_constellation = list(self.header['observationTypes'].keys())[-1] 122 | obs_types = [line[i:i+4].strip() for i in range(7, 60, 4) if line[i:i+4].strip() and i+4 <= 60] 123 | self.header['observationTypes'][last_constellation].extend(obs_types) 124 | else: 125 | # New constellation 126 | obs_types = [line[i:i+4].strip() for i in range(7, 60, 4) if line[i:i+4].strip() and i+4 <= 60] 127 | self.header['observationTypes'][constellation] = obs_types 128 | 129 | # Convert metadata to the required format 130 | self.metadata = self.header.copy() 131 | 132 | def _parse_observations(self, observation_lines): 133 | """ 134 | Parse the observation section of the RINEX file. 135 | 136 | Parameters: 137 | ----------- 138 | observation_lines : list 139 | List of strings containing the observation section 140 | """ 141 | # Preallocation - start with a reasonable capacity 142 | initial_capacity = 100000 143 | 144 | # Data arrays for each column 145 | dates = [] 146 | epoch_secs = [] 147 | mjds = [] 148 | constellations = [] 149 | sats = [] 150 | channels = [] 151 | ranges = [] 152 | phases = [] 153 | dopplers = [] 154 | snrs = [] 155 | 156 | i = 0 157 | 158 | while i < len(observation_lines): 159 | line = observation_lines[i] 160 | 161 | # Skip empty lines 162 | if not line.strip(): 163 | i += 1 164 | continue 165 | 166 | # Check if this is an epoch line (starts with ">") 167 | if line.startswith(">"): 168 | try: 169 | # Parse epoch information 170 | year = int(line[2:6]) 171 | month = int(line[7:9]) 172 | day = int(line[10:12]) 173 | hour = int(line[13:15]) 174 | minute = int(line[16:18]) 175 | second = float(line[19:30]) 176 | epoch_flag = int(line[31:32]) 177 | num_sats = int(line[32:35]) 178 | 179 | # Skip special epochs (flags > 1) 180 | if epoch_flag > 1: 181 | i += 1 182 | while i < len(observation_lines) and not observation_lines[i].startswith(">"): 183 | i += 1 184 | continue 185 | 186 | epoch_datetime = datetime.datetime(year, month, day, hour, minute, int(second)) 187 | epoch_date = epoch_datetime.date().isoformat() 188 | epoch_sec = hour * 3600 + minute * 60 + second 189 | 190 | # Calculate Modified Julian Date (MJD) 191 | jd = self._datetime_to_jd(epoch_datetime) 192 | mjd = jd - 2400000.5 193 | 194 | # Move to next line to start processing satellites 195 | i += 1 196 | 197 | # Parse and process satellites 198 | sat_count = 0 199 | sat_lines = [] 200 | 201 | # Collect satellite lines (could span multiple lines) 202 | while sat_count < num_sats and i < len(observation_lines): 203 | line = observation_lines[i] 204 | if line.startswith(">"): 205 | break 206 | 207 | # If first character is a constellation identifier, it's a satellite line 208 | if line and line[0] in "GRECJS": 209 | sat_lines.append(line) 210 | sat_count += 1 211 | 212 | i += 1 213 | 214 | # Process each satellite 215 | for sat_line in sat_lines: 216 | # Extract satellite ID (first 3 characters) 217 | if len(sat_line) >= 3: 218 | sat_id = sat_line[0:3].strip() 219 | if not sat_id: 220 | continue 221 | 222 | constellation = sat_id[0] 223 | 224 | # Get observation types for this constellation 225 | obs_types = self.header['observationTypes'].get(constellation, []) 226 | num_obs = len(obs_types) 227 | 228 | if num_obs == 0: 229 | continue 230 | 231 | # Initialize data structures 232 | unique_channels = {} # Maps channel code to index 233 | channel_data = {} # Stores observation data by channel 234 | 235 | # Process this satellite's observations 236 | for obs_idx in range(num_obs): 237 | # Calculate position in the line 238 | # Each observation takes 16 characters (14 for value, 2 for LLI and signal strength) 239 | # First 3 characters are for satellite ID 240 | line_idx = obs_idx // 5 # 5 observations per line 241 | pos_in_line = obs_idx % 5 242 | 243 | start_pos = 3 + pos_in_line * 16 # Starting at position 3 (after satellite ID) 244 | 245 | # Check if we need to read from next lines 246 | curr_line = sat_line 247 | if line_idx > 0: 248 | # Need to read from continuation lines 249 | line_offset = i - len(sat_lines) + line_idx 250 | if line_offset < len(observation_lines): 251 | curr_line = observation_lines[line_offset] 252 | else: 253 | continue 254 | 255 | # Skip if line is too short 256 | if len(curr_line) <= start_pos: 257 | continue 258 | 259 | # Extract observation value 260 | end_pos = start_pos + 14 261 | if end_pos > len(curr_line): 262 | end_pos = len(curr_line) 263 | 264 | obs_str = curr_line[start_pos:end_pos].strip() 265 | obs_value = None 266 | if obs_str: 267 | try: 268 | obs_value = float(obs_str) 269 | except ValueError: 270 | obs_value = None 271 | 272 | # Skip if no valid observation 273 | if obs_value is None: 274 | continue 275 | 276 | # Extract SNR (signal strength) 277 | snr_value = None 278 | if end_pos + 2 <= len(curr_line): 279 | snr_str = curr_line[end_pos:end_pos+2].strip() 280 | if snr_str: 281 | try: 282 | snr_value = float(snr_str) 283 | except ValueError: 284 | snr_value = None 285 | 286 | # Get observation type and channel 287 | obs_type = obs_types[obs_idx] 288 | 289 | if len(obs_type) >= 2: 290 | # First character is observation type (C, L, D, S) 291 | obs_code = obs_type[0] 292 | # Rest is the channel code 293 | channel_code = obs_type[1:] 294 | 295 | # Get or create channel index 296 | if channel_code not in unique_channels: 297 | unique_channels[channel_code] = len(unique_channels) 298 | channel_data[channel_code] = { 299 | 'range': None, 300 | 'phase': None, 301 | 'doppler': None, 302 | 'snr': None 303 | } 304 | 305 | # Map observation type to appropriate category 306 | if obs_code == 'C': # Pseudorange 307 | channel_data[channel_code]['range'] = obs_value 308 | elif obs_code == 'L': # Carrier phase 309 | channel_data[channel_code]['phase'] = obs_value 310 | elif obs_code == 'D': # Doppler 311 | channel_data[channel_code]['doppler'] = obs_value 312 | elif obs_code == 'S': # Signal strength 313 | channel_data[channel_code]['snr'] = obs_value 314 | 315 | # If we have a separate SNR value, use it regardless of observation type 316 | if snr_value is not None: 317 | channel_data[channel_code]['snr'] = snr_value 318 | 319 | # Add data for each channel to our lists 320 | for channel_code, data in channel_data.items(): 321 | dates.append(epoch_date) 322 | epoch_secs.append(epoch_sec) 323 | mjds.append(mjd) 324 | constellations.append(constellation) 325 | sats.append(sat_id) 326 | channels.append(channel_code) 327 | ranges.append(data['range']) 328 | phases.append(data['phase']) 329 | dopplers.append(data['doppler']) 330 | snrs.append(data['snr']) 331 | 332 | except Exception as e: 333 | # Handle parsing errors 334 | print(f"Error parsing epoch: {str(e)}") 335 | i += 1 336 | else: 337 | i += 1 338 | 339 | # Create final DataFrame 340 | self.obs_data = pd.DataFrame({ 341 | 'date': dates, 342 | 'epoch_sec': epoch_secs, 343 | 'mjd': mjds, 344 | 'constellation': constellations, 345 | 'sat': sats, 346 | 'channel': channels, 347 | 'range': ranges, 348 | 'phase': phases, 349 | 'doppler': dopplers, 350 | 'snr': snrs 351 | }) 352 | 353 | 354 | # Note: This method is no longer used with the improved parsing approach 355 | 356 | def _datetime_to_jd(self, dt): 357 | """ 358 | Convert a datetime object to Julian date. 359 | 360 | Parameters: 361 | ----------- 362 | dt : datetime.datetime 363 | Datetime object to convert 364 | 365 | Returns: 366 | -------- 367 | float 368 | Julian date 369 | """ 370 | # Extract components 371 | year, month, day = dt.year, dt.month, dt.day 372 | hour, minute, second = dt.hour, dt.minute, dt.second 373 | microsecond = dt.microsecond 374 | 375 | # Calculate decimal day 376 | decimal_day = day + (hour / 24.0) + (minute / 1440.0) + ((second + microsecond / 1e6) / 86400.0) 377 | 378 | # Calculate Julian date 379 | if month <= 2: 380 | year -= 1 381 | month += 12 382 | 383 | A = int(year / 100) 384 | B = 2 - A + int(A / 4) 385 | 386 | jd = int(365.25 * (year + 4716)) + int(30.6001 * (month + 1)) + decimal_day + B - 1524.5 387 | 388 | return jd 389 | 390 | def get_metadata_json(self): 391 | """ 392 | Get the metadata in JSON format. 393 | 394 | Returns: 395 | -------- 396 | str 397 | Metadata in JSON format 398 | """ 399 | return json.dumps(self.metadata) 400 | 401 | def get_obs_data(self): 402 | """ 403 | Get the observation data as a DataFrame. 404 | 405 | Returns: 406 | -------- 407 | pandas.DataFrame 408 | Observation data 409 | """ 410 | return self.obs_data -------------------------------------------------------------------------------- /src/pygnsslab/io/rinex3/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for RINEX 3 processing. 3 | """ 4 | 5 | import datetime 6 | import numpy as np 7 | import os 8 | from pathlib import Path 9 | 10 | def datetime_to_mjd(dt): 11 | """ 12 | Convert a datetime object to Modified Julian Date (MJD). 13 | 14 | Parameters: 15 | ----------- 16 | dt : datetime.datetime 17 | Datetime object to convert 18 | 19 | Returns: 20 | -------- 21 | float 22 | Modified Julian Date 23 | """ 24 | # Convert to Julian date first 25 | jd = datetime_to_jd(dt) 26 | # Convert Julian date to Modified Julian Date 27 | mjd = jd - 2400000.5 28 | 29 | return mjd 30 | 31 | def datetime_to_jd(dt): 32 | """ 33 | Convert a datetime object to Julian date. 34 | 35 | Parameters: 36 | ----------- 37 | dt : datetime.datetime 38 | Datetime object to convert 39 | 40 | Returns: 41 | -------- 42 | float 43 | Julian date 44 | """ 45 | # Extract components 46 | year, month, day = dt.year, dt.month, dt.day 47 | hour, minute, second = dt.hour, dt.minute, dt.second 48 | microsecond = dt.microsecond 49 | 50 | # Calculate decimal day 51 | decimal_day = day + (hour / 24.0) + (minute / 1440.0) + ((second + microsecond / 1e6) / 86400.0) 52 | 53 | # Calculate Julian date 54 | if month <= 2: 55 | year -= 1 56 | month += 12 57 | 58 | A = int(year / 100) 59 | B = 2 - A + int(A / 4) 60 | 61 | jd = int(365.25 * (year + 4716)) + int(30.6001 * (month + 1)) + decimal_day + B - 1524.5 62 | 63 | return jd 64 | 65 | def mjd_to_datetime(mjd): 66 | """ 67 | Convert Modified Julian Date (MJD) to datetime object. 68 | 69 | Parameters: 70 | ----------- 71 | mjd : float 72 | Modified Julian Date 73 | 74 | Returns: 75 | -------- 76 | datetime.datetime 77 | Datetime object 78 | """ 79 | # Convert MJD to Julian date 80 | jd = mjd + 2400000.5 81 | 82 | # Convert Julian date to datetime 83 | return jd_to_datetime(jd) 84 | 85 | def jd_to_datetime(jd): 86 | """ 87 | Convert Julian date to datetime object. 88 | 89 | Parameters: 90 | ----------- 91 | jd : float 92 | Julian date 93 | 94 | Returns: 95 | -------- 96 | datetime.datetime 97 | Datetime object 98 | """ 99 | jd = jd + 0.5 # Shift by 0.5 day 100 | 101 | # Calculate integer and fractional parts 102 | F, I = np.modf(jd) 103 | I = int(I) 104 | 105 | A = np.trunc((I - 1867216.25) / 36524.25) 106 | B = I + 1 + A - np.trunc(A / 4) 107 | 108 | C = B + 1524 109 | D = np.trunc((C - 122.1) / 365.25) 110 | E = np.trunc(365.25 * D) 111 | G = np.trunc((C - E) / 30.6001) 112 | 113 | # Calculate day, month, year 114 | day = C - E + F - np.trunc(30.6001 * G) 115 | 116 | if G < 13.5: 117 | month = G - 1 118 | else: 119 | month = G - 13 120 | 121 | if month > 2.5: 122 | year = D - 4716 123 | else: 124 | year = D - 4715 125 | 126 | # Calculate time 127 | day_frac = day - int(day) 128 | hours = int(day_frac * 24) 129 | minutes = int((day_frac * 24 - hours) * 60) 130 | seconds = ((day_frac * 24 - hours) * 60 - minutes) * 60 131 | 132 | # Create datetime object 133 | try: 134 | dt = datetime.datetime( 135 | int(year), 136 | int(month), 137 | int(day), 138 | hours, 139 | minutes, 140 | int(seconds), 141 | int((seconds - int(seconds)) * 1e6) 142 | ) 143 | return dt 144 | except ValueError: 145 | # Handling edge cases 146 | if int(day) == 0: 147 | # Handle case where day is calculated as 0 148 | month = int(month) - 1 149 | if month == 0: 150 | month = 12 151 | year = int(year) - 1 152 | days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] 153 | # Adjust for leap year 154 | if month == 2 and (year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)): 155 | day = 29 156 | else: 157 | day = days_in_month[month] 158 | else: 159 | day = int(day) 160 | 161 | return datetime.datetime( 162 | int(year), 163 | int(month), 164 | day, 165 | hours, 166 | minutes, 167 | int(seconds), 168 | int((seconds - int(seconds)) * 1e6) 169 | ) 170 | 171 | def time_to_seconds_of_day(time_obj): 172 | """ 173 | Convert a time object to seconds of day. 174 | 175 | Parameters: 176 | ----------- 177 | time_obj : datetime.time 178 | Time object to convert 179 | 180 | Returns: 181 | -------- 182 | float 183 | Seconds of day 184 | """ 185 | return time_obj.hour * 3600 + time_obj.minute * 60 + time_obj.second + time_obj.microsecond / 1e6 186 | 187 | def is_rinex3_file(filename): 188 | """ 189 | Check if a file is a RINEX 3 observation file. 190 | 191 | Parameters: 192 | ----------- 193 | filename : str 194 | Path to the file 195 | 196 | Returns: 197 | -------- 198 | bool 199 | True if the file is a RINEX 3 observation file, False otherwise 200 | """ 201 | if not os.path.exists(filename): 202 | return False 203 | 204 | # Check file extension 205 | ext = Path(filename).suffix.lower() 206 | if ext not in ['.rnx', '.crx', '.obs']: 207 | # Not a standard RINEX observation file extension 208 | return False 209 | 210 | # Check file content 211 | try: 212 | with open(filename, 'r') as f: 213 | # Read first few lines 214 | header_lines = [f.readline() for _ in range(10)] 215 | 216 | # Check for RINEX 3 version marker 217 | for line in header_lines: 218 | if "RINEX VERSION / TYPE" in line and line.strip().startswith("3"): 219 | return True 220 | except: 221 | # If there's an error reading the file, assume it's not a valid RINEX file 222 | return False 223 | 224 | return False 225 | 226 | def find_rinex3_files(directory): 227 | """ 228 | Find all RINEX 3 observation files in a directory. 229 | 230 | Parameters: 231 | ----------- 232 | directory : str 233 | Path to the directory 234 | 235 | Returns: 236 | -------- 237 | list 238 | List of paths to RINEX 3 observation files 239 | """ 240 | rinex_files = [] 241 | 242 | for root, _, files in os.walk(directory): 243 | for file in files: 244 | filepath = os.path.join(root, file) 245 | if is_rinex3_file(filepath): 246 | rinex_files.append(filepath) 247 | 248 | return rinex_files -------------------------------------------------------------------------------- /src/pygnsslab/io/rinex3/writer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Writer module for saving RINEX observation data to parquet format. 3 | """ 4 | 5 | import os 6 | import json 7 | import pandas as pd 8 | from pathlib import Path 9 | 10 | def write_to_parquet(obs_data, output_file, metadata=None, metadata_file=None): 11 | """ 12 | Write observation data to a parquet file and optionally save metadata to a JSON file. 13 | 14 | Parameters: 15 | ----------- 16 | obs_data : pandas.DataFrame 17 | DataFrame containing observation data 18 | output_file : str 19 | Path to the output parquet file 20 | metadata : dict, optional 21 | Metadata dictionary to save 22 | metadata_file : str, optional 23 | Path to the output metadata JSON file. If None but metadata is provided, 24 | a default name will be created based on output_file 25 | 26 | Returns: 27 | -------- 28 | tuple 29 | (parquet_file_path, metadata_file_path) 30 | """ 31 | # Create output directory if it doesn't exist 32 | output_path = Path(output_file) 33 | output_dir = output_path.parent 34 | os.makedirs(output_dir, exist_ok=True) 35 | 36 | # Ensure data has the required columns 37 | required_columns = ['date', 'epoch_sec', 'mjd', 'constellation', 'sat', 38 | 'channel', 'range', 'phase', 'doppler', 'snr'] 39 | 40 | # Create empty columns if they don't exist 41 | for col in required_columns: 42 | if col not in obs_data.columns: 43 | obs_data[col] = None 44 | 45 | # Write observation data to parquet 46 | obs_data.to_parquet(output_file, index=False) 47 | 48 | # Write metadata to JSON if provided 49 | metadata_path = None 50 | if metadata: 51 | if not metadata_file: 52 | # Create default metadata filename 53 | metadata_file = str(output_path.with_suffix('.json')) 54 | 55 | with open(metadata_file, 'w') as f: 56 | json.dump(metadata, f, indent=2) 57 | 58 | metadata_path = metadata_file 59 | 60 | return output_file, metadata_path 61 | 62 | def write_obs_summary(obs_data, output_file): 63 | """ 64 | Write a summary of observation data to a text file. 65 | 66 | Parameters: 67 | ----------- 68 | obs_data : pandas.DataFrame 69 | DataFrame containing observation data 70 | output_file : str 71 | Path to the output summary file 72 | 73 | Returns: 74 | -------- 75 | str 76 | Path to the output summary file 77 | """ 78 | # Create output directory if it doesn't exist 79 | output_path = Path(output_file) 80 | output_dir = output_path.parent 81 | os.makedirs(output_dir, exist_ok=True) 82 | 83 | # Generate summary statistics 84 | summary = {} 85 | 86 | # Count observations by constellation 87 | constellation_counts = obs_data['constellation'].value_counts().to_dict() 88 | summary['observations_by_constellation'] = constellation_counts 89 | 90 | # Count satellites by constellation 91 | sat_counts = obs_data.groupby('constellation')['sat'].nunique().to_dict() 92 | summary['satellites_by_constellation'] = sat_counts 93 | 94 | # Get date range 95 | if 'date' in obs_data.columns and not obs_data['date'].empty: 96 | min_date = obs_data['date'].min() 97 | max_date = obs_data['date'].max() 98 | summary['date_range'] = {'start': min_date, 'end': max_date} 99 | 100 | # Write summary to file 101 | with open(output_file, 'w') as f: 102 | f.write("RINEX Observation Data Summary\n") 103 | f.write("==============================\n\n") 104 | 105 | f.write("Observations by Constellation:\n") 106 | for const, count in summary.get('observations_by_constellation', {}).items(): 107 | f.write(f" {const}: {count}\n") 108 | 109 | f.write("\nSatellites by Constellation:\n") 110 | for const, count in summary.get('satellites_by_constellation', {}).items(): 111 | f.write(f" {const}: {count}\n") 112 | 113 | if 'date_range' in summary: 114 | f.write("\nDate Range:\n") 115 | f.write(f" Start: {summary['date_range']['start']}\n") 116 | f.write(f" End: {summary['date_range']['end']}\n") 117 | 118 | return output_file -------------------------------------------------------------------------------- /src/pygnsslab/io/rinexnav/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | RINEX Navigation File Reader Module for PyGNSSLab. 3 | 4 | This module handles the reading of RINEX Navigation files (versions 2, 3, and 4), 5 | and provides a unified interface for accessing navigation data across different RINEX versions. 6 | """ 7 | 8 | from .reader import ( 9 | read_rinex_nav, 10 | RinexNavReader, 11 | Rinex2NavReader, 12 | Rinex3NavReader 13 | ) 14 | 15 | __all__ = [ 16 | 'read_rinex_nav', 17 | 'RinexNavReader', 18 | 'Rinex2NavReader', 19 | 'Rinex3NavReader' 20 | ] -------------------------------------------------------------------------------- /src/pygnsslab/io/rinexnav/example_usage.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example usage of the RINEX Navigation reader module. 3 | 4 | This script demonstrates how to use the RINEX Navigation reader to read navigation data 5 | from files of different RINEX versions and how to process the data. 6 | """ 7 | 8 | import os 9 | import sys 10 | import numpy as np 11 | import pandas as pd 12 | import matplotlib.pyplot as plt 13 | from datetime import datetime, timedelta 14 | 15 | # Add the parent directory to the path 16 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..'))) 17 | 18 | from pygnsslab.io.rinexnav.reader import read_rinex_nav 19 | from pygnsslab.io.rinexnav.utils import ( 20 | gps_time_to_datetime, 21 | datetime_to_gps_time, 22 | interpolate_orbit, 23 | compute_sv_clock_correction 24 | ) 25 | 26 | def read_and_display_rinex2_nav(filename): 27 | """ 28 | Read a RINEX 2 navigation file and display basic information. 29 | 30 | Parameters 31 | ---------- 32 | filename : str 33 | Path to the RINEX 2 navigation file. 34 | """ 35 | print(f"\nReading RINEX 2 Navigation file: {filename}") 36 | 37 | # Read the file 38 | nav_reader = read_rinex_nav(filename) 39 | 40 | # Display header information 41 | print("\nHeader Information:") 42 | for key, value in nav_reader.header.items(): 43 | print(f" {key}: {value}") 44 | 45 | # Display data summary 46 | print("\nData Summary:") 47 | for sys, df in nav_reader.data.items(): 48 | if not df.empty: 49 | print(f" System {sys}: {len(df)} records") 50 | print(f" PRNs: {sorted(df['PRN'].unique())}") 51 | 52 | # Display time span 53 | min_time = df['Epoch'].min() 54 | max_time = df['Epoch'].max() 55 | print(f" Time span: {min_time} to {max_time}") 56 | 57 | # Display first record 58 | print("\nFirst record:") 59 | display_record(df.iloc[0]) 60 | 61 | def read_and_display_rinex3_nav(filename): 62 | """ 63 | Read a RINEX 3 navigation file and display basic information. 64 | 65 | Parameters 66 | ---------- 67 | filename : str 68 | Path to the RINEX 3 navigation file. 69 | """ 70 | print(f"\nReading RINEX 3 Navigation file: {filename}") 71 | 72 | # Read the file 73 | nav_reader = read_rinex_nav(filename) 74 | 75 | # Display header information 76 | print("\nHeader Information:") 77 | for key, value in nav_reader.header.items(): 78 | if key not in ['ionospheric_corr', 'time_system_corr']: 79 | print(f" {key}: {value}") 80 | 81 | # Display ionospheric correction if available 82 | if 'ionospheric_corr' in nav_reader.header: 83 | print("\nIonospheric Correction:") 84 | for corr_type, values in nav_reader.header['ionospheric_corr'].items(): 85 | print(f" {corr_type}: {values}") 86 | 87 | # Display time system correction if available 88 | if 'time_system_corr' in nav_reader.header: 89 | print("\nTime System Correction:") 90 | for corr_type, values in nav_reader.header['time_system_corr'].items(): 91 | print(f" {corr_type}: {values}") 92 | 93 | # Display data summary 94 | print("\nData Summary:") 95 | for sys, df in nav_reader.data.items(): 96 | if not df.empty: 97 | print(f"\nSystem {sys}: {len(df)} records") 98 | print(f" PRNs: {sorted(df['PRN'].unique())}") 99 | 100 | # Display time span 101 | min_time = df['Epoch'].min() 102 | max_time = df['Epoch'].max() 103 | print(f" Time span: {min_time} to {max_time}") 104 | 105 | # Display first record 106 | print("\nFirst record for system", sys) 107 | display_record(df.iloc[0]) 108 | 109 | def display_record(record): 110 | """ 111 | Display a navigation record in a readable format. 112 | 113 | Parameters 114 | ---------- 115 | record : pd.Series 116 | Navigation record as a pandas Series. 117 | """ 118 | # Display basic information 119 | print(f" PRN: {record['PRN']}") 120 | print(f" Epoch: {record['Epoch']}") 121 | print(f" SV Clock Bias: {record['SV_clock_bias']:.12e} s") 122 | print(f" SV Clock Drift: {record['SV_clock_drift']:.12e} s/s") 123 | print(f" SV Clock Drift Rate: {record['SV_clock_drift_rate']:.12e} s/s²") 124 | 125 | # Display orbital parameters if they exist 126 | for param in ['IODE', 'e', 'sqrt_A', 'Toe']: 127 | if param in record and not pd.isna(record[param]): 128 | if param == 'sqrt_A': 129 | print(f" {param}: {record[param]:.12e} m^(1/2)") 130 | elif param == 'e': 131 | print(f" {param}: {record[param]:.12e} (dimensionless)") 132 | elif param == 'Toe': 133 | print(f" {param}: {record[param]}") 134 | else: 135 | print(f" {param}: {record[param]}") 136 | 137 | def plot_satellite_orbit(nav_data, sat_system, prn, start_time, duration_hours): 138 | """ 139 | Plot the satellite orbit over a specified time period. 140 | 141 | Parameters 142 | ---------- 143 | nav_data : dict of pd.DataFrame 144 | Navigation data dictionary where keys are satellite systems. 145 | sat_system : str 146 | Satellite system identifier (e.g., 'G' for GPS). 147 | prn : int 148 | Satellite PRN number. 149 | start_time : datetime 150 | Start time for the plot. 151 | duration_hours : float 152 | Duration of the plot in hours. 153 | """ 154 | if sat_system not in nav_data: 155 | print(f"No data available for satellite system {sat_system}") 156 | return 157 | 158 | # Get dataframe for the specified satellite system 159 | df = nav_data[sat_system] 160 | 161 | # Ensure the data contains the specified PRN 162 | if prn not in df['PRN'].values: 163 | print(f"No data available for satellite {sat_system}{prn}") 164 | return 165 | 166 | # Check if satellite system is supported for interpolation 167 | supported_systems = ['G'] # Currently only GPS is supported 168 | if sat_system not in supported_systems: 169 | print(f"Interpolation for satellite system {sat_system} is not supported yet. Skipping orbit plotting.") 170 | return 171 | 172 | # Create time points for interpolation 173 | num_points = int(duration_hours * 12) # One point every 5 minutes 174 | times = [start_time + timedelta(hours=duration_hours*i/num_points) for i in range(num_points+1)] 175 | 176 | # Interpolate the orbit at each time point 177 | positions = [] 178 | for t in times: 179 | try: 180 | pos = interpolate_orbit(df, t, sat_system, prn) 181 | # Check for NaN values in the position 182 | if np.isnan(pos['X']) or np.isnan(pos['Y']) or np.isnan(pos['Z']): 183 | continue 184 | positions.append((t, pos['X'], pos['Y'], pos['Z'])) 185 | except Exception as e: 186 | print(f"Error interpolating at time {t}: {e}") 187 | 188 | # Extract coordinates for plotting 189 | if positions: 190 | t_vals, x_vals, y_vals, z_vals = zip(*positions) 191 | 192 | # Create the plot 193 | plt.figure(figsize=(12, 10)) 194 | 195 | # 3D plot 196 | ax = plt.subplot(111, projection='3d') 197 | ax.plot(x_vals, y_vals, z_vals, 'b-', linewidth=2) 198 | 199 | # Plot Earth (simplified as a sphere) 200 | u, v = np.mgrid[0:2*np.pi:20j, 0:np.pi:10j] 201 | earth_radius = 6371000 # Earth's radius in meters 202 | x = earth_radius * np.cos(u) * np.sin(v) 203 | y = earth_radius * np.sin(u) * np.sin(v) 204 | z = earth_radius * np.cos(v) 205 | ax.plot_surface(x, y, z, color='g', alpha=0.2) 206 | 207 | # Add starting point marker 208 | ax.scatter([x_vals[0]], [y_vals[0]], [z_vals[0]], color='r', s=100, label='Start') 209 | 210 | # Set labels and title 211 | ax.set_xlabel('X (m)') 212 | ax.set_ylabel('Y (m)') 213 | ax.set_zlabel('Z (m)') 214 | ax.set_title(f'Orbit of Satellite {sat_system}{prn} over {duration_hours} hours') 215 | 216 | # Set equal aspect ratio 217 | max_range = max([max(x_vals) - min(x_vals), max(y_vals) - min(y_vals), max(z_vals) - min(z_vals)]) 218 | mid_x = (max(x_vals) + min(x_vals)) * 0.5 219 | mid_y = (max(y_vals) + min(y_vals)) * 0.5 220 | mid_z = (max(z_vals) + min(z_vals)) * 0.5 221 | ax.set_xlim(mid_x - max_range/2, mid_x + max_range/2) 222 | ax.set_ylim(mid_y - max_range/2, mid_y + max_range/2) 223 | ax.set_zlim(mid_z - max_range/2, mid_z + max_range/2) 224 | 225 | plt.legend() 226 | plt.tight_layout() 227 | plt.savefig(f'satellite_{sat_system}{prn}_orbit.png', dpi=300) 228 | plt.close() 229 | 230 | print(f"Orbit plot saved as 'satellite_{sat_system}{prn}_orbit.png'") 231 | 232 | # Plot clock correction 233 | plt.figure(figsize=(10, 6)) 234 | 235 | clock_corrections = [] 236 | for t in times: 237 | try: 238 | clock_corr = compute_sv_clock_correction(df, t, sat_system, prn) 239 | clock_corrections.append(clock_corr) 240 | except Exception as e: 241 | print(f"Error computing clock correction at time {t}: {e}") 242 | clock_corrections.append(np.nan) 243 | 244 | # Remove NaN values for plotting 245 | valid_times = [] 246 | valid_corrections = [] 247 | for i, corr in enumerate(clock_corrections): 248 | if not np.isnan(corr): 249 | valid_times.append(times[i]) 250 | valid_corrections.append(corr) 251 | 252 | if valid_corrections: 253 | # Convert to nanoseconds for better visualization 254 | clock_corrections_ns = [c * 1e9 for c in valid_corrections] 255 | 256 | # Create time axis in hours from start 257 | time_hours = [(t - start_time).total_seconds()/3600 for t in valid_times] 258 | 259 | plt.plot(time_hours, clock_corrections_ns, 'b-', linewidth=2) 260 | plt.xlabel('Time (hours from start)') 261 | plt.ylabel('Clock Correction (ns)') 262 | plt.title(f'Clock Correction of Satellite {sat_system}{prn} over {duration_hours} hours') 263 | plt.grid(True) 264 | plt.tight_layout() 265 | plt.savefig(f'satellite_{sat_system}{prn}_clock_correction.png', dpi=300) 266 | plt.close() 267 | 268 | print(f"Clock correction plot saved as 'satellite_{sat_system}{prn}_clock_correction.png'") 269 | else: 270 | print("No valid clock corrections calculated for the specified time range.") 271 | else: 272 | print("No valid positions calculated for the specified time range.") 273 | 274 | def compute_dop_values(nav_data, receiver_pos, time, visible_sats=None): 275 | """ 276 | Compute Dilution of Precision (DOP) values. 277 | 278 | Parameters 279 | ---------- 280 | nav_data : dict of pd.DataFrame 281 | Navigation data dictionary where keys are satellite systems. 282 | receiver_pos : tuple of float 283 | Receiver position (X, Y, Z) in ECEF coordinates. 284 | time : datetime 285 | Time for DOP computation. 286 | visible_sats : list of tuple, optional 287 | List of visible satellites as (system, prn) tuples. 288 | If None, all satellites in the navigation data are used. 289 | 290 | Returns 291 | ------- 292 | dict 293 | Dictionary containing DOP values (GDOP, PDOP, HDOP, VDOP, TDOP). 294 | """ 295 | # Currently supported satellite systems for interpolation 296 | supported_systems = ['G'] # Currently only GPS is supported 297 | 298 | if visible_sats is None: 299 | # Use all satellites in the navigation data from supported systems 300 | visible_sats = [] 301 | for sys, df in nav_data.items(): 302 | if sys in supported_systems: 303 | for prn in df['PRN'].unique(): 304 | visible_sats.append((sys, prn)) 305 | 306 | if not visible_sats: 307 | print(f"No satellites from supported systems ({', '.join(supported_systems)}) found in navigation data.") 308 | return None 309 | 310 | # Compute satellite positions 311 | sat_positions = [] 312 | 313 | for sys, prn in visible_sats: 314 | if sys not in supported_systems: 315 | continue # Skip unsupported satellite systems 316 | 317 | if sys in nav_data and prn in nav_data[sys]['PRN'].values: 318 | try: 319 | pos = interpolate_orbit(nav_data[sys], time, sys, prn) 320 | # Check for NaN values in the position 321 | if np.isnan(pos['X']) or np.isnan(pos['Y']) or np.isnan(pos['Z']): 322 | continue 323 | sat_positions.append((sys, prn, pos['X'], pos['Y'], pos['Z'])) 324 | except Exception as e: 325 | print(f"Error interpolating position for {sys}{prn} at {time}: {e}") 326 | 327 | if len(sat_positions) < 4: 328 | print(f"Insufficient satellites ({len(sat_positions)}) for DOP computation.") 329 | return None 330 | 331 | # Compute line-of-sight unit vectors 332 | rx, ry, rz = receiver_pos 333 | G_matrix = [] 334 | 335 | for _, _, sx, sy, sz in sat_positions: 336 | # Vector from receiver to satellite 337 | dx = sx - rx 338 | dy = sy - ry 339 | dz = sz - rz 340 | 341 | # Range 342 | range_val = np.sqrt(dx**2 + dy**2 + dz**2) 343 | 344 | # Unit vector components 345 | ax = dx / range_val 346 | ay = dy / range_val 347 | az = dz / range_val 348 | 349 | # Add to G matrix with clock term 350 | G_matrix.append([ax, ay, az, 1]) 351 | 352 | # Compute DOP from G matrix 353 | G = np.array(G_matrix) 354 | try: 355 | Q = np.linalg.inv(G.T @ G) 356 | 357 | # Extract DOP values 358 | GDOP = np.sqrt(np.trace(Q)) 359 | PDOP = np.sqrt(Q[0,0] + Q[1,1] + Q[2,2]) 360 | HDOP = np.sqrt(Q[0,0] + Q[1,1]) 361 | VDOP = np.sqrt(Q[2,2]) 362 | TDOP = np.sqrt(Q[3,3]) 363 | 364 | return { 365 | 'GDOP': GDOP, 366 | 'PDOP': PDOP, 367 | 'HDOP': HDOP, 368 | 'VDOP': VDOP, 369 | 'TDOP': TDOP 370 | } 371 | except np.linalg.LinAlgError as e: 372 | print(f"Linear algebra error in DOP computation: {e}") 373 | return None 374 | 375 | def verify_data_integrity(nav_data, sat_system): 376 | """ 377 | Verify the integrity of navigation data by checking for required parameters. 378 | 379 | Parameters 380 | ---------- 381 | nav_data : dict of pd.DataFrame 382 | Navigation data dictionary where keys are satellite systems. 383 | sat_system : str 384 | Satellite system identifier (e.g., 'G' for GPS). 385 | 386 | Returns 387 | ------- 388 | list 389 | List of valid PRNs with complete data for orbit calculations. 390 | """ 391 | if sat_system not in nav_data: 392 | print(f"No data available for satellite system {sat_system}") 393 | return [] 394 | 395 | # Get dataframe for the specified satellite system 396 | df = nav_data[sat_system] 397 | 398 | # List of required parameters for orbit calculations 399 | required_params = ['sqrt_A', 'e', 'M0', 'omega', 'OMEGA', 'i0', 'Delta_n', 400 | 'Cuc', 'Cus', 'Crc', 'Crs', 'Cic', 'Cis', 'IDOT', 'OMEGA_DOT', 'Toe'] 401 | 402 | valid_prns = [] 403 | 404 | # Check all unique PRNs in the dataframe 405 | for prn in df['PRN'].unique(): 406 | # Get all records for this PRN 407 | prn_data = df[df['PRN'] == prn] 408 | 409 | # Check if there's at least one complete record 410 | for _, row in prn_data.iterrows(): 411 | is_valid = True 412 | for param in required_params: 413 | if param not in row or pd.isna(row[param]): 414 | is_valid = False 415 | break 416 | 417 | if is_valid: 418 | valid_prns.append(prn) 419 | break 420 | 421 | return valid_prns 422 | 423 | def main(): 424 | """Main function to demonstrate the usage of the RINEX Navigation reader.""" 425 | # Example usage with RINEX 2 file 426 | rinex2_file = "data/nav/ajac0010.22n" # Replace with actual path 427 | 428 | try: 429 | # Read RINEX 2 navigation file 430 | nav_reader_v2 = read_rinex_nav(rinex2_file) 431 | read_and_display_rinex2_nav(rinex2_file) 432 | 433 | # Verify data integrity and find valid satellites 434 | if 'G' in nav_reader_v2.data: 435 | valid_prns = verify_data_integrity(nav_reader_v2.data, 'G') 436 | print(f"\nValid GPS satellites in RINEX 2 file with complete orbital data: {valid_prns}") 437 | 438 | if valid_prns: 439 | prn_to_plot = valid_prns[0] # Choose the first valid PRN 440 | 441 | # Find a suitable start time (with valid data) 442 | # Use a time within the data range 443 | all_epochs = sorted(nav_reader_v2.data['G']['Epoch'].unique()) 444 | if all_epochs: 445 | # Choose a time that's not at the very beginning or end 446 | start_idx = len(all_epochs) // 4 # 25% into the data 447 | start_time = all_epochs[start_idx] 448 | 449 | print(f"\nPlotting orbit for GPS satellite PRN {prn_to_plot} starting at {start_time}") 450 | plot_satellite_orbit(nav_reader_v2.data, 'G', prn_to_plot, start_time, 12) # Reduced to 12 hours 451 | 452 | # Example of DOP computation 453 | # Receiver position in ECEF coordinates (example: near Greenwich) 454 | receiver_pos = (3980000, 0, 4970000) 455 | print(f"\nComputing DOP values at {start_time}") 456 | dop_values = compute_dop_values(nav_reader_v2.data, receiver_pos, start_time) 457 | if dop_values: 458 | print("\nDOP Values:") 459 | for key, value in dop_values.items(): 460 | print(f" {key}: {value:.2f}") 461 | else: 462 | print("Could not compute DOP values.") 463 | else: 464 | print("No GPS epoch data available for plotting.") 465 | else: 466 | print("No GPS satellites with complete orbital data found in the RINEX 2 file.") 467 | else: 468 | print("No GPS data found in the RINEX 2 file.") 469 | 470 | except Exception as e: 471 | print(f"Error processing RINEX 2 file: {e}") 472 | 473 | # Example usage with RINEX 3 file 474 | rinex3_file = "data/nav/PTLD00AUS_R_20220010000_01D_30S_MN.rnx" # Replace with actual path 475 | 476 | try: 477 | # Read RINEX 3 navigation file 478 | nav_reader_v3 = read_rinex_nav(rinex3_file) 479 | read_and_display_rinex3_nav(rinex3_file) 480 | 481 | # Verify data integrity and find valid satellites 482 | if 'G' in nav_reader_v3.data: 483 | valid_prns = verify_data_integrity(nav_reader_v3.data, 'G') 484 | print(f"\nValid GPS satellites in RINEX 3 file with complete orbital data: {valid_prns}") 485 | 486 | if valid_prns: 487 | prn_to_plot = valid_prns[0] # Choose the first valid PRN 488 | 489 | # Find a suitable start time (with valid data) 490 | all_epochs = sorted(nav_reader_v3.data['G']['Epoch'].unique()) 491 | if all_epochs: 492 | # Choose a time that's not at the very beginning or end 493 | start_idx = len(all_epochs) // 4 # 25% into the data 494 | start_time = all_epochs[start_idx] 495 | 496 | print(f"\nPlotting orbit for GPS satellite PRN {prn_to_plot} from RINEX 3 file starting at {start_time}") 497 | plot_satellite_orbit(nav_reader_v3.data, 'G', prn_to_plot, start_time, 12) # Reduced to 12 hours 498 | else: 499 | print("No GPS epoch data available for plotting from RINEX 3.") 500 | else: 501 | print("No GPS satellites with complete orbital data found in the RINEX 3 file.") 502 | else: 503 | print("No GPS data found in the RINEX 3 file.") 504 | 505 | # Print a note about unsupported satellite systems 506 | unsupported_systems = [sys for sys in nav_reader_v3.data.keys() if sys != 'G'] 507 | if unsupported_systems: 508 | print(f"\nNote: Interpolation for satellite systems {', '.join(unsupported_systems)} is not yet supported.") 509 | print("Only GPS (G) satellites can be used for orbit plotting and DOP calculations.") 510 | 511 | except Exception as e: 512 | print(f"Error processing RINEX 3 file: {e}") 513 | 514 | if __name__ == "__main__": 515 | main() -------------------------------------------------------------------------------- /src/pygnsslab/io/rinexnav/metadata.py: -------------------------------------------------------------------------------- 1 | """ 2 | Metadata for RINEX Navigation files. 3 | 4 | This module provides metadata and constants related to RINEX Navigation files 5 | and GNSS systems. 6 | """ 7 | 8 | from typing import Dict, List, Tuple 9 | 10 | # GNSS System Identifiers 11 | GNSS_SYSTEMS = { 12 | 'G': 'GPS', 13 | 'R': 'GLONASS', 14 | 'E': 'Galileo', 15 | 'C': 'BeiDou', 16 | 'J': 'QZSS', 17 | 'I': 'IRNSS/NavIC', 18 | 'S': 'SBAS' 19 | } 20 | 21 | # File Type Identifiers 22 | FILE_TYPES = { 23 | 'N': 'GPS Navigation Data', 24 | 'G': 'GLONASS Navigation Data', 25 | 'H': 'GEO Navigation Data', 26 | 'B': 'SBAS Broadcast Data', 27 | 'L': 'GALILEO Navigation Data', 28 | 'P': 'Mixed GNSS Navigation Data', 29 | 'Q': 'QZSS Navigation Data', 30 | 'C': 'BDS Navigation Data', 31 | 'I': 'IRNSS Navigation Data' 32 | } 33 | 34 | # RINEX Version Compatibility 35 | RINEX_VERSIONS = { 36 | 2.0: ["GPS", "GLONASS"], 37 | 2.1: ["GPS", "GLONASS"], 38 | 2.11: ["GPS", "GLONASS"], 39 | 2.12: ["GPS", "GLONASS"], 40 | 3.0: ["GPS", "GLONASS", "Galileo", "SBAS", "QZSS"], 41 | 3.01: ["GPS", "GLONASS", "Galileo", "SBAS", "QZSS"], 42 | 3.02: ["GPS", "GLONASS", "Galileo", "SBAS", "QZSS", "BeiDou"], 43 | 3.03: ["GPS", "GLONASS", "Galileo", "SBAS", "QZSS", "BeiDou", "IRNSS"], 44 | 3.04: ["GPS", "GLONASS", "Galileo", "SBAS", "QZSS", "BeiDou", "IRNSS"], 45 | 4.0: ["GPS", "GLONASS", "Galileo", "SBAS", "QZSS", "BeiDou", "IRNSS"] 46 | } 47 | 48 | # Navigation Data Record Structure by GNSS System 49 | # Key: System identifier 50 | # Value: List of parameter names in order of appearance 51 | NAV_DATA_STRUCTURE = { 52 | 'G': [ # GPS Navigation Message 53 | 'IODE', 'Crs', 'Delta_n', 'M0', 54 | 'Cuc', 'e', 'Cus', 'sqrt_A', 55 | 'Toe', 'Cic', 'OMEGA', 'Cis', 56 | 'i0', 'Crc', 'omega', 'OMEGA_DOT', 57 | 'IDOT', 'L2_codes', 'GPS_week', 'L2_P_flag', 58 | 'SV_accuracy', 'SV_health', 'TGD', 'IODC', 59 | 'Transmission_time', 'Fit_interval' 60 | ], 61 | 'R': [ # GLONASS Navigation Message 62 | 'Tau', 'Gamma', 'tb', 'X', 63 | 'X_velocity', 'X_acceleration', 'health', 'Y', 64 | 'Y_velocity', 'Y_acceleration', 'frequency_number', 'Z', 65 | 'Z_velocity', 'Z_acceleration', 'Age' 66 | ], 67 | 'E': [ # Galileo Navigation Message 68 | 'IODnav', 'Crs', 'Delta_n', 'M0', 69 | 'Cuc', 'e', 'Cus', 'sqrt_A', 70 | 'Toe', 'Cic', 'OMEGA', 'Cis', 71 | 'i0', 'Crc', 'omega', 'OMEGA_DOT', 72 | 'IDOT', 'Data_sources', 'GAL_week', 'SISA', 73 | 'SV_health', 'BGD_E5a_E1', 'BGD_E5b_E1', 'Transmission_time' 74 | ], 75 | 'C': [ # BeiDou Navigation Message 76 | 'AODE', 'Crs', 'Delta_n', 'M0', 77 | 'Cuc', 'e', 'Cus', 'sqrt_A', 78 | 'Toe', 'Cic', 'OMEGA', 'Cis', 79 | 'i0', 'Crc', 'omega', 'OMEGA_DOT', 80 | 'IDOT', 'spare1', 'BDT_week', 'spare2', 81 | 'SV_accuracy', 'SatH1', 'TGD1', 'TGD2', 82 | 'Transmission_time', 'AODC' 83 | ], 84 | 'J': [ # QZSS Navigation Message (similar to GPS) 85 | 'IODE', 'Crs', 'Delta_n', 'M0', 86 | 'Cuc', 'e', 'Cus', 'sqrt_A', 87 | 'Toe', 'Cic', 'OMEGA', 'Cis', 88 | 'i0', 'Crc', 'omega', 'OMEGA_DOT', 89 | 'IDOT', 'L2_codes', 'GPS_week', 'L2_P_flag', 90 | 'SV_accuracy', 'SV_health', 'TGD', 'IODC', 91 | 'Transmission_time', 'Fit_interval' 92 | ], 93 | 'I': [ # IRNSS Navigation Message 94 | 'IODEC', 'Crs', 'Delta_n', 'M0', 95 | 'Cuc', 'e', 'Cus', 'sqrt_A', 96 | 'Toe', 'Cic', 'OMEGA', 'Cis', 97 | 'i0', 'Crc', 'omega', 'OMEGA_DOT', 98 | 'IDOT', 'spare', 'IRNSS_week', 'spare', 99 | 'SV_accuracy', 'SV_health', 'TGD', 'spare', 100 | 'Transmission_time', 'spare' 101 | ], 102 | 'S': [ # SBAS Navigation Message 103 | 'spare', 'Crs', 'Delta_n', 'M0', 104 | 'Cuc', 'e', 'Cus', 'sqrt_A', 105 | 'Toe', 'Cic', 'OMEGA', 'Cis', 106 | 'i0', 'Crc', 'omega', 'OMEGA_DOT', 107 | 'IDOT', 'spare', 'GPS_week', 'spare', 108 | 'SV_accuracy', 'SV_health', 'spare', 'spare', 109 | 'Transmission_time', 'spare' 110 | ] 111 | } 112 | 113 | # Physical Constants 114 | PHYSICAL_CONSTANTS = { 115 | 'c': 299792458.0, # Speed of light in vacuum (m/s) 116 | 'mu_earth': 3.986005e14, # Earth's gravitational constant (m^3/s^2) 117 | 'omega_e': 7.2921151467e-5, # Earth's rotation rate (rad/s) 118 | 'F': -4.442807633e-10, # Relativistic correction constant (-2*sqrt(mu)/(c^2)) 119 | 'earth_radius': 6371000 # Earth's mean radius (m) 120 | } 121 | 122 | # Time System Offsets 123 | TIME_SYSTEM_OFFSETS = { 124 | # (From, To): Offset in seconds 125 | ('GPST', 'UTC'): lambda week, tow: leap_seconds(week, tow) - 19, # GPS Time to UTC 126 | ('GLST', 'UTC'): lambda week, tow: 0, # GLONASS Time (UTC-based) to UTC 127 | ('GST', 'UTC'): lambda week, tow: leap_seconds(week, tow) - 19, # Galileo System Time to UTC 128 | ('BDT', 'UTC'): lambda week, tow: leap_seconds(week, tow) - 33, # BeiDou Time to UTC 129 | ('QZSST', 'UTC'): lambda week, tow: leap_seconds(week, tow) - 19, # QZSS Time to UTC 130 | ('IRNWT', 'UTC'): lambda week, tow: leap_seconds(week, tow) - 19, # IRNSS Time to UTC 131 | } 132 | 133 | # Epoch reference times for different GNSS systems 134 | EPOCH_REFERENCES = { 135 | 'G': (1980, 1, 6, 0, 0, 0), # GPS: January 6, 1980 00:00:00 UTC 136 | 'R': (1996, 1, 1, 0, 0, 0), # GLONASS: January 1, 1996 00:00:00 UTC 137 | 'E': (1999, 8, 22, 0, 0, 0), # Galileo: August 22, 1999 00:00:00 UTC 138 | 'C': (2006, 1, 1, 0, 0, 0), # BeiDou: January 1, 2006 00:00:00 UTC 139 | 'J': (1980, 1, 6, 0, 0, 0), # QZSS: Same as GPS 140 | 'I': (1980, 1, 6, 0, 0, 0), # IRNSS: Same as GPS 141 | 'S': (1980, 1, 6, 0, 0, 0) # SBAS: Same as GPS 142 | } 143 | 144 | def leap_seconds(gps_week, tow): 145 | """ 146 | Get the number of leap seconds for a given GPS time. 147 | 148 | Parameters 149 | ---------- 150 | gps_week : int 151 | GPS week number. 152 | tow : float 153 | Time of week in seconds. 154 | 155 | Returns 156 | ------- 157 | int 158 | Number of leap seconds. 159 | 160 | Notes 161 | ----- 162 | GPS time is ahead of UTC by a number of leap seconds that increases over time. 163 | This function provides a rough estimate based on known leap second insertions. 164 | For precise applications, use an up-to-date leap second table. 165 | """ 166 | # Convert GPS week and ToW to seconds since GPS epoch 167 | seconds_since_gps_epoch = gps_week * 604800 + tow 168 | 169 | # Define leap second insertion times in seconds since GPS epoch 170 | leap_second_insertions = [ 171 | (46828800, 1), # July 1, 1981 172 | (78364801, 2), # July 1, 1982 173 | (109900802, 3), # July 1, 1983 174 | (173059203, 4), # July 1, 1985 175 | (252028804, 5), # January 1, 1988 176 | (315187205, 6), # January 1, 1990 177 | (346723206, 7), # January 1, 1991 178 | (393984007, 8), # July 1, 1992 179 | (425520008, 9), # July 1, 1993 180 | (457056009, 10), # July 1, 1994 181 | (504489610, 11), # January 1, 1996 182 | (551750411, 12), # July 1, 1997 183 | (599184012, 13), # January 1, 1999 184 | (820108813, 14), # January 1, 2006 185 | (914803214, 15), # January 1, 2009 186 | (1025136015, 16), # July 1, 2012 187 | (1119744016, 17), # July 1, 2015 188 | (1167264017, 18), # January 1, 2017 189 | # Add more as they occur 190 | ] 191 | 192 | # Find the number of leap seconds at the given time 193 | leap_seconds = 0 194 | for insertion_time, num_leaps in leap_second_insertions: 195 | if seconds_since_gps_epoch >= insertion_time: 196 | leap_seconds = num_leaps 197 | else: 198 | break 199 | 200 | return leap_seconds -------------------------------------------------------------------------------- /src/pygnsslab/io/rinexnav/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for RINEX Navigation file handling. 3 | 4 | This module provides helper functions for working with RINEX navigation data. 5 | """ 6 | 7 | import numpy as np 8 | import pandas as pd 9 | from datetime import datetime, timedelta 10 | from typing import Dict, List, Tuple, Union, Optional 11 | 12 | def gps_time_to_datetime(week: int, seconds: float) -> datetime: 13 | """ 14 | Convert GPS time (week number + seconds of week) to datetime. 15 | 16 | Parameters 17 | ---------- 18 | week : int 19 | GPS week number. 20 | seconds : float 21 | Seconds of the GPS week. 22 | 23 | Returns 24 | ------- 25 | datetime 26 | Equivalent datetime object in UTC. 27 | """ 28 | gps_epoch = datetime(1980, 1, 6, 0, 0, 0) 29 | delta = timedelta(weeks=week, seconds=seconds) 30 | return gps_epoch + delta 31 | 32 | def datetime_to_gps_time(dt: datetime) -> Tuple[int, float]: 33 | """ 34 | Convert datetime to GPS time (week number + seconds of week). 35 | 36 | Parameters 37 | ---------- 38 | dt : datetime 39 | Datetime object in UTC. 40 | 41 | Returns 42 | ------- 43 | Tuple[int, float] 44 | GPS week number and seconds of the week. 45 | """ 46 | gps_epoch = datetime(1980, 1, 6, 0, 0, 0) 47 | delta = dt - gps_epoch 48 | 49 | # Total seconds since GPS epoch 50 | total_seconds = delta.total_seconds() 51 | 52 | # GPS week number 53 | week = int(total_seconds / (7 * 24 * 3600)) 54 | 55 | # Seconds of the week 56 | sow = total_seconds % (7 * 24 * 3600) 57 | 58 | return week, sow 59 | 60 | def interpolate_orbit(nav_data: pd.DataFrame, time: datetime, 61 | sat_system: str = 'G', prn: int = 1) -> Dict[str, float]: 62 | """ 63 | Interpolate satellite position at a given time using navigation data. 64 | 65 | Parameters 66 | ---------- 67 | nav_data : pd.DataFrame 68 | Navigation data DataFrame for a specific satellite system. 69 | time : datetime 70 | Time at which to interpolate the satellite position. 71 | sat_system : str, optional 72 | Satellite system identifier (default: 'G' for GPS). 73 | prn : int, optional 74 | Satellite PRN number (default: 1). 75 | 76 | Returns 77 | ------- 78 | Dict[str, float] 79 | Dictionary containing interpolated position (X, Y, Z) and velocity (VX, VY, VZ). 80 | 81 | Notes 82 | ----- 83 | This is a simplified version that works for GPS satellites. 84 | Different algorithms are needed for other GNSS systems. 85 | """ 86 | if sat_system not in ['G', 'J']: # Only GPS and QZSS supported for now 87 | raise ValueError(f"Interpolation for {sat_system} not supported yet.") 88 | 89 | # Filter navigation data for the specific satellite 90 | sat_data = nav_data[nav_data['PRN'] == prn].sort_values('Epoch') 91 | 92 | if sat_data.empty: 93 | raise ValueError(f"No navigation data found for {sat_system}{prn:02d}") 94 | 95 | # Find the closest epoch before the requested time 96 | closest_row = sat_data[sat_data['Epoch'] <= time].iloc[-1] 97 | 98 | # Extract orbit parameters 99 | mu = 3.986005e14 # Earth's gravitational constant (m^3/s^2) 100 | omega_e = 7.2921151467e-5 # Earth's rotation rate (rad/s) 101 | 102 | # Check for NaN values in key parameters 103 | required_params = ['sqrt_A', 'e', 'M0', 'omega', 'OMEGA', 'i0', 'Delta_n', 'Cuc', 'Cus', 'Crc', 'Crs', 'Cic', 'Cis', 'IDOT', 'OMEGA_DOT'] 104 | for param in required_params: 105 | if param not in closest_row or pd.isna(closest_row[param]): 106 | raise ValueError(f"Missing or NaN value for {param} in navigation data") 107 | 108 | # Semi-major axis 109 | A = closest_row['sqrt_A'] ** 2 110 | 111 | # Computed mean motion 112 | n0 = np.sqrt(mu / (A**3)) 113 | 114 | # Time difference from epoch 115 | toe = closest_row['Toe'] 116 | if isinstance(toe, float): 117 | # Toe is in seconds of GPS week 118 | # Try to get the GPS week - different satellite systems might use different column names 119 | week = None 120 | for week_col in ['GPS_week', 'GAL_week', 'BDT_week']: 121 | if week_col in closest_row and not pd.isna(closest_row[week_col]): 122 | try: 123 | # Ensure the week is an integer 124 | week = int(float(closest_row[week_col])) 125 | break 126 | except (ValueError, TypeError): 127 | # Skip if value can't be converted to int 128 | continue 129 | 130 | if week is None: 131 | # If no week column is found, use current week as fallback 132 | # This is not accurate but better than failing completely 133 | current_week, _ = datetime_to_gps_time(datetime.now()) 134 | week = current_week 135 | 136 | toe_datetime = gps_time_to_datetime(week, toe) 137 | else: 138 | # Toe is already a datetime 139 | toe_datetime = toe 140 | 141 | dt = (time - toe_datetime).total_seconds() 142 | 143 | # Corrected mean motion 144 | n = n0 + closest_row['Delta_n'] 145 | 146 | # Mean anomaly at time t 147 | M = closest_row['M0'] + n * dt 148 | 149 | # Eccentric anomaly (iterative solution) 150 | e = closest_row['e'] 151 | E = M 152 | for _ in range(10): # Usually converges within a few iterations 153 | E_next = M + e * np.sin(E) 154 | if abs(E_next - E) < 1e-12: 155 | break 156 | E = E_next 157 | 158 | # True anomaly 159 | v = np.arctan2(np.sqrt(1 - e**2) * np.sin(E), np.cos(E) - e) 160 | 161 | # Argument of latitude 162 | phi = v + closest_row['omega'] 163 | 164 | # Second harmonic perturbations 165 | du = closest_row['Cuc'] * np.cos(2*phi) + closest_row['Cus'] * np.sin(2*phi) # Argument of latitude correction 166 | dr = closest_row['Crc'] * np.cos(2*phi) + closest_row['Crs'] * np.sin(2*phi) # Radius correction 167 | di = closest_row['Cic'] * np.cos(2*phi) + closest_row['Cis'] * np.sin(2*phi) 168 | 169 | # Corrected argument of latitude 170 | u = phi + du 171 | 172 | # Corrected radius 173 | r = A * (1 - e * np.cos(E)) + dr 174 | 175 | # Corrected inclination 176 | i = closest_row['i0'] + di + closest_row['IDOT'] * dt 177 | 178 | # Positions in orbital plane 179 | x_prime = r * np.cos(u) 180 | y_prime = r * np.sin(u) 181 | 182 | # Corrected longitude of ascending node 183 | Omega = closest_row['OMEGA'] + (closest_row['OMEGA_DOT'] - omega_e) * dt - omega_e * toe 184 | 185 | # Earth-fixed coordinates 186 | X = x_prime * np.cos(Omega) - y_prime * np.cos(i) * np.sin(Omega) 187 | Y = x_prime * np.sin(Omega) + y_prime * np.cos(i) * np.cos(Omega) 188 | Z = y_prime * np.sin(i) 189 | 190 | # Velocity calculations (simplified) 191 | # Time derivative of eccentric anomaly 192 | E_dot = n / (1 - e * np.cos(E)) 193 | 194 | # Time derivative of argument of latitude 195 | phi_dot = np.sqrt(1 - e**2) * E_dot / (1 - e * np.cos(E)) 196 | 197 | # Time derivative of corrected argument of latitude 198 | u_dot = phi_dot * (1 + 2 * (closest_row['Cus'] * np.cos(2*phi) - closest_row['Cuc'] * np.sin(2*phi))) 199 | 200 | # Time derivative of corrected radius 201 | r_dot = A * e * np.sin(E) * E_dot + 2 * phi_dot * (closest_row['Crs'] * np.cos(2*phi) - closest_row['Crc'] * np.sin(2*phi)) 202 | 203 | # Time derivatives in orbital plane 204 | x_prime_dot = r_dot * np.cos(u) - r * u_dot * np.sin(u) 205 | y_prime_dot = r_dot * np.sin(u) + r * u_dot * np.cos(u) 206 | 207 | # Time derivative of longitude of ascending node 208 | Omega_dot = closest_row['OMEGA_DOT'] - omega_e 209 | 210 | # Time derivative of corrected inclination 211 | i_dot = closest_row['IDOT'] + 2 * phi_dot * (closest_row['Cis'] * np.cos(2*phi) - closest_row['Cic'] * np.sin(2*phi)) 212 | 213 | # Earth-fixed velocity components 214 | VX = (x_prime_dot * np.cos(Omega) - y_prime_dot * np.cos(i) * np.sin(Omega) - 215 | y_prime * np.sin(i) * i_dot * np.sin(Omega) - 216 | (x_prime * np.sin(Omega) + y_prime * np.cos(i) * np.cos(Omega)) * Omega_dot) 217 | 218 | VY = (x_prime_dot * np.sin(Omega) + y_prime_dot * np.cos(i) * np.cos(Omega) + 219 | y_prime * np.sin(i) * i_dot * np.cos(Omega) + 220 | (x_prime * np.cos(Omega) - y_prime * np.cos(i) * np.sin(Omega)) * Omega_dot) 221 | 222 | VZ = y_prime_dot * np.sin(i) + y_prime * np.cos(i) * i_dot 223 | 224 | return { 225 | 'X': X, 226 | 'Y': Y, 227 | 'Z': Z, 228 | 'VX': VX, 229 | 'VY': VY, 230 | 'VZ': VZ 231 | } 232 | 233 | def compute_sv_clock_correction(nav_data: pd.DataFrame, time: datetime, 234 | sat_system: str = 'G', prn: int = 1) -> float: 235 | """ 236 | Compute satellite clock correction at a given time. 237 | 238 | Parameters 239 | ---------- 240 | nav_data : pd.DataFrame 241 | Navigation data DataFrame for a specific satellite system. 242 | time : datetime 243 | Time at which to compute the clock correction. 244 | sat_system : str, optional 245 | Satellite system identifier (default: 'G' for GPS). 246 | prn : int, optional 247 | Satellite PRN number (default: 1). 248 | 249 | Returns 250 | ------- 251 | float 252 | Clock correction value in seconds. 253 | """ 254 | # Filter navigation data for the specific satellite 255 | sat_data = nav_data[nav_data['PRN'] == prn].sort_values('Epoch') 256 | 257 | if sat_data.empty: 258 | raise ValueError(f"No navigation data found for {sat_system}{prn:02d}") 259 | 260 | # Find the closest epoch before the requested time 261 | closest_row = sat_data[sat_data['Epoch'] <= time].iloc[-1] 262 | 263 | # Check for NaN values in key parameters 264 | required_params = ['SV_clock_bias', 'SV_clock_drift', 'SV_clock_drift_rate'] 265 | for param in required_params: 266 | if param not in closest_row or pd.isna(closest_row[param]): 267 | raise ValueError(f"Missing or NaN value for {param} in navigation data") 268 | 269 | # Get clock parameters 270 | a0 = closest_row['SV_clock_bias'] 271 | a1 = closest_row['SV_clock_drift'] 272 | a2 = closest_row['SV_clock_drift_rate'] 273 | 274 | # Time difference from epoch 275 | toc = closest_row['Epoch'] # Time of clock parameters 276 | dt = (time - toc).total_seconds() 277 | 278 | # Compute clock correction 279 | dt_corr = a0 + a1 * dt + a2 * dt**2 280 | 281 | # For GPS satellites, apply relativistic correction 282 | if sat_system in ['G', 'J']: 283 | # Check if we have the required orbital parameters 284 | if 'e' in closest_row and 'sqrt_A' in closest_row and not pd.isna(closest_row['e']) and not pd.isna(closest_row['sqrt_A']): 285 | # Get orbital parameters 286 | e = closest_row['e'] 287 | A = closest_row['sqrt_A']**2 288 | 289 | # Compute eccentric anomaly for relativistic correction 290 | # (Simplified approach - for full implementation, use the same E as in interpolate_orbit) 291 | mu = 3.986005e14 # Earth's gravitational constant 292 | n0 = np.sqrt(mu / (A**3)) 293 | 294 | if 'Toe' in closest_row and not pd.isna(closest_row['Toe']) and 'M0' in closest_row and not pd.isna(closest_row['M0']): 295 | toe = closest_row['Toe'] 296 | if isinstance(toe, float): 297 | # Toe is in seconds of GPS week 298 | # Try to get the GPS week - different satellite systems might use different column names 299 | week = None 300 | for week_col in ['GPS_week', 'GAL_week', 'BDT_week']: 301 | if week_col in closest_row and not pd.isna(closest_row[week_col]): 302 | try: 303 | # Ensure the week is an integer 304 | week = int(float(closest_row[week_col])) 305 | break 306 | except (ValueError, TypeError): 307 | # Skip if value can't be converted to int 308 | continue 309 | 310 | if week is None: 311 | # If no week column is found, use current week as fallback 312 | # This is not accurate but better than failing completely 313 | current_week, _ = datetime_to_gps_time(datetime.now()) 314 | week = current_week 315 | 316 | toe_datetime = gps_time_to_datetime(week, toe) 317 | else: 318 | # Toe is already a datetime 319 | toe_datetime = toe 320 | 321 | dt_toe = (time - toe_datetime).total_seconds() 322 | M = closest_row['M0'] + n0 * dt_toe 323 | 324 | # Iterative solution for eccentric anomaly 325 | E = M 326 | for _ in range(10): 327 | E_next = M + e * np.sin(E) 328 | if abs(E_next - E) < 1e-12: 329 | break 330 | E = E_next 331 | 332 | # Relativistic correction 333 | F = -2 * np.sqrt(mu) / (3e8**2) 334 | rel_corr = F * e * np.sqrt(A) * np.sin(E) 335 | 336 | # Add relativistic correction to the clock correction 337 | dt_corr += rel_corr 338 | 339 | return dt_corr 340 | 341 | def detect_rinex_version(filename: str) -> float: 342 | """ 343 | Detect the RINEX version from a navigation file. 344 | 345 | Parameters 346 | ---------- 347 | filename : str 348 | Path to the RINEX navigation file. 349 | 350 | Returns 351 | ------- 352 | float 353 | RINEX version number. 354 | 355 | Raises 356 | ------ 357 | ValueError 358 | If the RINEX version cannot be determined. 359 | """ 360 | with open(filename, 'r') as f: 361 | first_line = f.readline() 362 | 363 | try: 364 | version = float(first_line[:9].strip()) 365 | return version 366 | except ValueError: 367 | raise ValueError(f"Cannot determine RINEX version from file: {filename}") -------------------------------------------------------------------------------- /src/pygnsslab/io/sp3/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | SP3 (Standard Product 3) file I/O module. 3 | 4 | This module provides functionality to read and write SP3 format files, 5 | which contain precise GNSS satellite orbit and clock information. 6 | 7 | The module supports both SP3-c and SP3-d formats. 8 | """ 9 | 10 | from .reader import SP3Reader 11 | from .writer import SP3Writer, write_sp3_file 12 | from .metadata import SP3Header, SP3Epoch, SP3SatelliteRecord 13 | from .utils import identify_sp3_version 14 | 15 | __all__ = [ 16 | 'SP3Reader', 17 | 'SP3Writer', 18 | 'write_sp3_file', 19 | 'SP3Header', 20 | 'SP3Epoch', 21 | 'SP3SatelliteRecord', 22 | 'identify_sp3_version' 23 | ] -------------------------------------------------------------------------------- /src/pygnsslab/io/sp3/example_usage.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example usage of the SP3 module. 3 | 4 | This module demonstrates how to read, manipulate, and write SP3 files. 5 | """ 6 | 7 | from datetime import datetime, timedelta 8 | import os 9 | import sys 10 | import matplotlib.pyplot as plt 11 | import numpy as np 12 | 13 | # Add the parent directory to the path so we can import the module 14 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) 15 | 16 | from pygnsslab.io.sp3.reader import SP3Reader 17 | from pygnsslab.io.sp3.writer import write_sp3_file 18 | from pygnsslab.io.sp3.metadata import SP3Header, SP3Epoch, SP3SatelliteRecord 19 | 20 | 21 | def read_sp3_file(file_path): 22 | """ 23 | Read an SP3 file and print basic information. 24 | 25 | Parameters 26 | ---------- 27 | file_path : str 28 | Path to the SP3 file. 29 | """ 30 | # Read the SP3 file 31 | print(f"Reading SP3 file: {file_path}") 32 | reader = SP3Reader(file_path) 33 | 34 | # Print header information 35 | header = reader.header 36 | print("\nHeader Information:") 37 | print(f" Version: SP3-{header.version}") 38 | print(f" File Type: {header.file_type}") 39 | print(f" Time: {header.time}") 40 | print(f" Epoch Count: {header.epoch_count}") 41 | print(f" Satellite Count: {header.num_satellites}") 42 | if header.satellite_ids: 43 | satellite_ids_str = ', '.join(header.satellite_ids[:5]) 44 | if len(header.satellite_ids) > 5: 45 | satellite_ids_str += "..." 46 | print(f" Satellites: {satellite_ids_str}") 47 | else: 48 | print(" Satellites: None") 49 | 50 | # Print information about the first epoch 51 | if reader.epochs: 52 | first_epoch = reader.epochs[0] 53 | print("\nFirst Epoch Information:") 54 | print(f" Time: {first_epoch.time}") 55 | print(f" Number of Satellites: {len(first_epoch.satellites)}") 56 | 57 | # Print information about the first satellite in the first epoch 58 | if first_epoch.satellites: 59 | first_sat_id = list(first_epoch.satellites.keys())[0] 60 | first_sat = first_epoch.satellites[first_sat_id] 61 | print(f"\nSatellite {first_sat_id} Information:") 62 | print(f" Position (X, Y, Z): ({first_sat.x}, {first_sat.y}, {first_sat.z}) km") 63 | print(f" Clock: {first_sat.clock} µs") 64 | 65 | if first_sat.x_vel is not None: 66 | print(f" Velocity (X, Y, Z): ({first_sat.x_vel}, {first_sat.y_vel}, {first_sat.z_vel}) dm/s") 67 | 68 | # Print summary 69 | print("\nSummary:") 70 | print(f" Number of Epochs: {len(reader.epochs)}") 71 | if len(reader.epochs) > 1: 72 | time_diff = reader.epochs[1].time - reader.epochs[0].time 73 | print(f" Epoch Interval: {time_diff.total_seconds()} seconds") 74 | 75 | return reader 76 | 77 | 78 | def plot_satellite_positions(reader, satellite_id, title=None): 79 | """ 80 | Plot the positions of a satellite over time. 81 | 82 | Parameters 83 | ---------- 84 | reader : SP3Reader 85 | The SP3 reader object. 86 | satellite_id : str 87 | The satellite ID to plot. 88 | title : str, optional 89 | The title for the plot. 90 | """ 91 | positions = reader.get_satellite_positions(satellite_id) 92 | if not positions: 93 | print(f"No position data for satellite {satellite_id}") 94 | return 95 | 96 | # Extract data for plotting 97 | times = [pos[0] for pos in positions] 98 | x_coords = [pos[1] for pos in positions] 99 | y_coords = [pos[2] for pos in positions] 100 | z_coords = [pos[3] for pos in positions] 101 | 102 | # Create a figure with 3 subplots 103 | fig, axs = plt.subplots(3, 1, figsize=(10, 12)) 104 | 105 | # Convert datetime to hours from the first epoch for x-axis 106 | t0 = times[0] 107 | hours = [(t - t0).total_seconds() / 3600 for t in times] 108 | 109 | # Plot X coordinate 110 | axs[0].plot(hours, x_coords, 'r-') 111 | axs[0].set_ylabel('X Coordinate (km)') 112 | axs[0].set_title(f'Satellite {satellite_id} Position - X Coordinate') 113 | axs[0].grid(True) 114 | 115 | # Plot Y coordinate 116 | axs[1].plot(hours, y_coords, 'g-') 117 | axs[1].set_ylabel('Y Coordinate (km)') 118 | axs[1].set_title(f'Satellite {satellite_id} Position - Y Coordinate') 119 | axs[1].grid(True) 120 | 121 | # Plot Z coordinate 122 | axs[2].plot(hours, z_coords, 'b-') 123 | axs[2].set_xlabel('Time (hours from start)') 124 | axs[2].set_ylabel('Z Coordinate (km)') 125 | axs[2].set_title(f'Satellite {satellite_id} Position - Z Coordinate') 126 | axs[2].grid(True) 127 | 128 | # Add a main title if provided 129 | if title: 130 | fig.suptitle(title, fontsize=16) 131 | 132 | plt.tight_layout() 133 | plt.show(block=True) 134 | 135 | 136 | def plot_satellite_3d_orbit(reader, satellite_id, title=None): 137 | """ 138 | Create a 3D plot of the satellite orbit. 139 | 140 | Parameters 141 | ---------- 142 | reader : SP3Reader 143 | The SP3 reader object. 144 | satellite_id : str 145 | The satellite ID to plot. 146 | title : str, optional 147 | The title for the plot. 148 | """ 149 | positions = reader.get_satellite_positions(satellite_id) 150 | if not positions: 151 | print(f"No position data for satellite {satellite_id}") 152 | return 153 | 154 | # Extract data for plotting 155 | x_coords = [pos[1] for pos in positions] 156 | y_coords = [pos[2] for pos in positions] 157 | z_coords = [pos[3] for pos in positions] 158 | 159 | # Create a figure 160 | fig = plt.figure(figsize=(10, 8)) 161 | ax = fig.add_subplot(111, projection='3d') 162 | 163 | # Plot orbit 164 | ax.plot(x_coords, y_coords, z_coords, 'b-', label=f'Satellite {satellite_id} Orbit') 165 | 166 | # Plot the Earth (simplified as a sphere) 167 | # Earth radius in km 168 | r_earth = 6371.0 169 | 170 | # Create a sphere 171 | u, v = np.mgrid[0:2*np.pi:20j, 0:np.pi:10j] 172 | x = r_earth * np.cos(u) * np.sin(v) 173 | y = r_earth * np.sin(u) * np.sin(v) 174 | z = r_earth * np.cos(v) 175 | 176 | # Plot Earth with low alpha for transparency 177 | ax.plot_surface(x, y, z, color='blue', alpha=0.1) 178 | 179 | # Set labels and title 180 | ax.set_xlabel('X (km)') 181 | ax.set_ylabel('Y (km)') 182 | ax.set_zlabel('Z (km)') 183 | 184 | if title: 185 | ax.set_title(title) 186 | else: 187 | ax.set_title(f'Satellite {satellite_id} Orbit') 188 | 189 | # Set axis limits to be equal for proper sphere rendering 190 | max_range = max( 191 | max(x_coords) - min(x_coords), 192 | max(y_coords) - min(y_coords), 193 | max(z_coords) - min(z_coords) 194 | ) / 2.0 195 | 196 | mid_x = (max(x_coords) + min(x_coords)) / 2 197 | mid_y = (max(y_coords) + min(y_coords)) / 2 198 | mid_z = (max(z_coords) + min(z_coords)) / 2 199 | 200 | ax.set_xlim(mid_x - max_range, mid_x + max_range) 201 | ax.set_ylim(mid_y - max_range, mid_y + max_range) 202 | ax.set_zlim(mid_z - max_range, mid_z + max_range) 203 | 204 | plt.tight_layout() 205 | plt.show() 206 | 207 | 208 | def create_sample_sp3_file(output_file_path): 209 | """ 210 | Create a sample SP3 file with some test data. 211 | 212 | Parameters 213 | ---------- 214 | output_file_path : str 215 | Path to the output SP3 file. 216 | """ 217 | # Create header 218 | header = SP3Header( 219 | version='c', # SP3-c format 220 | file_type='P', # Position only 221 | time=datetime(2023, 1, 1, 0, 0, 0), 222 | epoch_count=24, # 24 epochs 223 | data_used="ORBIT", 224 | coordinate_system="IGS14", 225 | orbit_type="FIT", 226 | agency="TEST", 227 | gps_week=2245, 228 | seconds_of_week=0.0, 229 | epoch_interval=3600.0, # 1 hour 230 | mjd=59945, 231 | fractional_day=0.0, 232 | num_satellites=3, 233 | satellite_ids=["G01", "G02", "G03"], 234 | satellite_accuracy={"G01": 7, "G02": 6, "G03": 7}, 235 | file_system="G", 236 | time_system="GPS", 237 | comments=["This is a sample SP3 file", "Created for demonstration purposes"] 238 | ) 239 | 240 | # Create epochs with satellite data 241 | epochs = [] 242 | for i in range(24): # 24 hours 243 | epoch_time = header.time + timedelta(hours=i) 244 | epoch = SP3Epoch(time=epoch_time) 245 | 246 | # Angular position around Earth (simplified circular orbit) 247 | angle_g01 = 2 * np.pi * i / 12 # 12-hour orbit for G01 248 | angle_g02 = 2 * np.pi * i / 11 # 11-hour orbit for G02 249 | angle_g03 = 2 * np.pi * i / 10 # 10-hour orbit for G03 250 | 251 | # Orbital radius in km (simplified) 252 | radius_g01 = 26000.0 253 | radius_g02 = 26500.0 254 | radius_g03 = 27000.0 255 | 256 | # Satellite positions in Earth-centered coordinates 257 | # G01 258 | x_g01 = radius_g01 * np.cos(angle_g01) 259 | y_g01 = radius_g01 * np.sin(angle_g01) 260 | z_g01 = 5000.0 * np.sin(angle_g01 * 2) # Add some Z variation 261 | 262 | # G02 263 | x_g02 = radius_g02 * np.cos(angle_g02) 264 | y_g02 = radius_g02 * np.sin(angle_g02) 265 | z_g02 = 5500.0 * np.sin(angle_g02 * 2) # Add some Z variation 266 | 267 | # G03 268 | x_g03 = radius_g03 * np.cos(angle_g03) 269 | y_g03 = radius_g03 * np.sin(angle_g03) 270 | z_g03 = 6000.0 * np.sin(angle_g03 * 2) # Add some Z variation 271 | 272 | # Add satellite records to the epoch 273 | epoch.satellites["G01"] = SP3SatelliteRecord( 274 | sat_id="G01", 275 | x=x_g01, 276 | y=y_g01, 277 | z=z_g01, 278 | clock=100.0 + i * 0.1, # Simulated clock bias 279 | x_sdev=5.0, 280 | y_sdev=5.0, 281 | z_sdev=5.0, 282 | clock_sdev=10.0 283 | ) 284 | 285 | epoch.satellites["G02"] = SP3SatelliteRecord( 286 | sat_id="G02", 287 | x=x_g02, 288 | y=y_g02, 289 | z=z_g02, 290 | clock=200.0 + i * 0.2, # Simulated clock bias 291 | x_sdev=5.0, 292 | y_sdev=5.0, 293 | z_sdev=5.0, 294 | clock_sdev=10.0 295 | ) 296 | 297 | epoch.satellites["G03"] = SP3SatelliteRecord( 298 | sat_id="G03", 299 | x=x_g03, 300 | y=y_g03, 301 | z=z_g03, 302 | clock=300.0 + i * 0.3, # Simulated clock bias 303 | x_sdev=5.0, 304 | y_sdev=5.0, 305 | z_sdev=5.0, 306 | clock_sdev=10.0 307 | ) 308 | 309 | epochs.append(epoch) 310 | 311 | # Write the SP3 file 312 | write_sp3_file(output_file_path, header, epochs) 313 | print(f"Created sample SP3 file: {output_file_path}") 314 | 315 | 316 | def main(show_plots=False): 317 | """ 318 | Main function to demonstrate the SP3 module functionality. 319 | 320 | Parameters 321 | ---------- 322 | show_plots : bool, optional 323 | Whether to show the plots, by default False 324 | """ 325 | # Set the path to the example SP3 file 326 | # You should replace this with the path to your actual SP3 file 327 | sp3_file_path = "data/sp3/COD0MGXFIN_20220010000_01D_05M_ORB.SP3" 328 | 329 | # Create the directory if it doesn't exist 330 | os.makedirs(os.path.dirname(sp3_file_path), exist_ok=True) 331 | 332 | # Create a sample SP3 file 333 | create_sample_sp3_file(sp3_file_path) 334 | 335 | # Read the SP3 file 336 | reader = read_sp3_file(sp3_file_path) 337 | 338 | # Plot satellite positions and orbits if requested 339 | if show_plots: 340 | # Plot satellite positions 341 | plot_satellite_positions(reader, "G01", "Satellite G01 Position Over Time") 342 | 343 | # Plot satellite 3D orbit 344 | plot_satellite_3d_orbit(reader, "G01", "Satellite G01 Orbit") 345 | 346 | 347 | if __name__ == "__main__": 348 | # Set show_plots to False to avoid blocking when running as a module 349 | main(show_plots=False) -------------------------------------------------------------------------------- /src/pygnsslab/io/sp3/metadata.py: -------------------------------------------------------------------------------- 1 | """ 2 | SP3 metadata classes. 3 | 4 | This module contains classes to represent SP3 file metadata, including 5 | header information, epochs, and satellite records. 6 | """ 7 | 8 | from dataclasses import dataclass, field 9 | from datetime import datetime 10 | from typing import Dict, List, Optional, Set, Tuple 11 | 12 | 13 | @dataclass 14 | class SP3Header: 15 | """Class representing the header of an SP3 file.""" 16 | version: str # 'c' or 'd' 17 | file_type: str # 'P'=position only, 'V'=position and velocity, etc. 18 | time: datetime # Start time 19 | epoch_count: int # Number of epochs 20 | data_used: str # Data used 21 | coordinate_system: str # Coordinate system 22 | orbit_type: str # Orbit type 23 | agency: str # Agency providing the data 24 | gps_week: int # GPS week 25 | seconds_of_week: float # Seconds of week 26 | epoch_interval: float # Interval between epochs in seconds 27 | mjd: int # Modified Julian Date 28 | fractional_day: float # Fractional part of the day 29 | num_satellites: int # Number of satellites 30 | satellite_ids: List[str] = field(default_factory=list) # List of satellite IDs 31 | satellite_accuracy: Dict[str, int] = field(default_factory=dict) # Dictionary of satellite accuracy values 32 | file_system: str = "" # File system 33 | time_system: str = "" # Time system 34 | base_pos_vel: List[float] = field(default_factory=list) # Base position/velocity 35 | base_clock: List[float] = field(default_factory=list) # Base clock 36 | custom_parameters: List[str] = field(default_factory=list) # Custom parameters 37 | comments: List[str] = field(default_factory=list) # Comments 38 | 39 | 40 | @dataclass 41 | class SP3SatelliteRecord: 42 | """Class representing a satellite record in an SP3 file.""" 43 | sat_id: str # Satellite ID (e.g., 'G01') 44 | x: float # X coordinate in km 45 | y: float # Y coordinate in km 46 | z: float # Z coordinate in km 47 | clock: float # Clock in microseconds 48 | x_sdev: Optional[float] = None # Standard deviation of X in m 49 | y_sdev: Optional[float] = None # Standard deviation of Y in m 50 | z_sdev: Optional[float] = None # Standard deviation of Z in m 51 | clock_sdev: Optional[float] = None # Standard deviation of clock in 1E-12 s 52 | x_vel: Optional[float] = None # X velocity in dm/s 53 | y_vel: Optional[float] = None # Y velocity in dm/s 54 | z_vel: Optional[float] = None # Z velocity in dm/s 55 | clock_rate: Optional[float] = None # Clock rate in 1E-14 s/s 56 | x_vel_sdev: Optional[float] = None # Standard deviation of X velocity 57 | y_vel_sdev: Optional[float] = None # Standard deviation of Y velocity 58 | z_vel_sdev: Optional[float] = None # Standard deviation of Z velocity 59 | clock_rate_sdev: Optional[float] = None # Standard deviation of clock rate 60 | correlation: Optional[Dict[str, float]] = None # Correlation information 61 | event_flag: Optional[int] = None # Event flag 62 | clock_event_flag: Optional[int] = None # Clock event flag 63 | 64 | 65 | @dataclass 66 | class SP3Epoch: 67 | """Class representing an epoch in an SP3 file.""" 68 | time: datetime # Epoch time 69 | satellites: Dict[str, SP3SatelliteRecord] = field(default_factory=dict) # Dictionary of satellite records -------------------------------------------------------------------------------- /src/pygnsslab/io/sp3/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for SP3 file handling. 3 | 4 | This module provides utility functions for processing SP3 files, 5 | including format detection and conversion helpers. 6 | """ 7 | 8 | import re 9 | from datetime import datetime, timedelta 10 | from typing import Tuple 11 | 12 | 13 | def identify_sp3_version(first_line: str) -> str: 14 | """ 15 | Identify the version of an SP3 file from its first line. 16 | 17 | Parameters 18 | ---------- 19 | first_line : str 20 | The first line of the SP3 file. 21 | 22 | Returns 23 | ------- 24 | str 25 | The SP3 version ('c' or 'd'). 26 | 27 | Raises 28 | ------ 29 | ValueError 30 | If the first line is not a valid SP3 header line. 31 | """ 32 | if not first_line.startswith('#'): 33 | raise ValueError("Not a valid SP3 file: header must start with '#'") 34 | 35 | version = first_line[1] # Second character is the version 36 | 37 | if version not in ['c', 'd']: 38 | raise ValueError(f"Unsupported SP3 version: {version}") 39 | 40 | return version 41 | 42 | 43 | def parse_sp3_datetime(year: int, month: int, day: int, hour: int, minute: int, second: float) -> datetime: 44 | """ 45 | Parse SP3 date and time components into a datetime object. 46 | 47 | Parameters 48 | ---------- 49 | year : int 50 | Year. 51 | month : int 52 | Month. 53 | day : int 54 | Day. 55 | hour : int 56 | Hour. 57 | minute : int 58 | Minute. 59 | second : float 60 | Second (can include fractional part). 61 | 62 | Returns 63 | ------- 64 | datetime 65 | The parsed datetime object. 66 | """ 67 | int_second = int(second) 68 | microsecond = int((second - int_second) * 1_000_000) 69 | 70 | return datetime(year, month, day, hour, minute, int_second, microsecond) 71 | 72 | 73 | def compute_mjd(dt: datetime) -> Tuple[int, float]: 74 | """ 75 | Compute Modified Julian Date and fractional day from a datetime. 76 | 77 | Parameters 78 | ---------- 79 | dt : datetime 80 | The datetime object. 81 | 82 | Returns 83 | ------- 84 | Tuple[int, float] 85 | The MJD and fractional day. 86 | """ 87 | # Julian date at January 1, 4713 BCE at noon 88 | jd = (dt.toordinal() + 1721425.5 + 89 | (dt.hour - 12) / 24.0 + 90 | dt.minute / 1440.0 + 91 | dt.second / 86400.0 + 92 | dt.microsecond / 86400000000.0) 93 | 94 | # Modified Julian Date (MJD = JD - 2400000.5) 95 | mjd = jd - 2400000.5 96 | 97 | # Extract integer and fractional parts 98 | mjd_int = int(mjd) 99 | mjd_frac = mjd - mjd_int 100 | 101 | return mjd_int, mjd_frac 102 | 103 | 104 | def gps_time_from_datetime(dt: datetime) -> Tuple[int, float]: 105 | """ 106 | Convert datetime to GPS week and seconds of week. 107 | 108 | Parameters 109 | ---------- 110 | dt : datetime 111 | The datetime object. 112 | 113 | Returns 114 | ------- 115 | Tuple[int, float] 116 | The GPS week and seconds of week. 117 | """ 118 | # GPS time epoch: January 6, 1980 00:00:00 UTC 119 | gps_epoch = datetime(1980, 1, 6) 120 | 121 | # Calculate time difference 122 | delta = dt - gps_epoch 123 | 124 | # Calculate GPS week 125 | gps_week = delta.days // 7 126 | 127 | # Calculate seconds of week 128 | seconds_of_week = (delta.days % 7) * 86400 + delta.seconds + delta.microseconds / 1_000_000 129 | 130 | return gps_week, seconds_of_week 131 | 132 | 133 | def datetime_from_gps_time(week: int, sow: float) -> datetime: 134 | """ 135 | Convert GPS week and seconds of week to datetime. 136 | 137 | Parameters 138 | ---------- 139 | week : int 140 | GPS week number. 141 | sow : float 142 | Seconds of week. 143 | 144 | Returns 145 | ------- 146 | datetime 147 | The corresponding datetime object. 148 | """ 149 | # GPS time epoch: January 6, 1980 00:00:00 UTC 150 | gps_epoch = datetime(1980, 1, 6) 151 | 152 | # Calculate time from GPS epoch 153 | delta = timedelta(weeks=week, seconds=sow) 154 | 155 | return gps_epoch + delta 156 | 157 | 158 | def parse_satellite_id(sat_id: str) -> Tuple[str, int]: 159 | """ 160 | Parse a satellite ID into system and number. 161 | 162 | Parameters 163 | ---------- 164 | sat_id : str 165 | The satellite ID (e.g., 'G01'). 166 | 167 | Returns 168 | ------- 169 | Tuple[str, int] 170 | The satellite system and PRN/slot number. 171 | """ 172 | if len(sat_id) < 2: 173 | raise ValueError(f"Invalid satellite ID: {sat_id}") 174 | 175 | system = sat_id[0] 176 | number = int(sat_id[1:]) 177 | 178 | return system, number 179 | 180 | 181 | def format_satellite_id(system: str, number: int) -> str: 182 | """ 183 | Format a satellite system and number into a satellite ID. 184 | 185 | Parameters 186 | ---------- 187 | system : str 188 | The satellite system (e.g., 'G'). 189 | number : int 190 | The PRN/slot number. 191 | 192 | Returns 193 | ------- 194 | str 195 | The formatted satellite ID (e.g., 'G01'). 196 | """ 197 | return f"{system}{number:02d}" -------------------------------------------------------------------------------- /src/pygnsslab/io/sp3/writer.py: -------------------------------------------------------------------------------- 1 | """ 2 | SP3 file writer module. 3 | 4 | This module provides functionality to write SP3 format files, 5 | supporting both SP3-c and SP3-d formats. 6 | """ 7 | 8 | from datetime import datetime 9 | from pathlib import Path 10 | from typing import Dict, List, Optional, TextIO, Union 11 | 12 | from .metadata import SP3Epoch, SP3Header, SP3SatelliteRecord 13 | from .utils import compute_mjd, gps_time_from_datetime 14 | 15 | 16 | class SP3Writer: 17 | """ 18 | Writer for SP3 (Standard Product 3) precise orbit files. 19 | 20 | This class can write both SP3-c and SP3-d format files. 21 | """ 22 | 23 | def __init__(self, header: SP3Header, epochs: List[SP3Epoch]): 24 | """ 25 | Initialize the SP3 writer. 26 | 27 | Parameters 28 | ---------- 29 | header : SP3Header 30 | Header information for the SP3 file. 31 | epochs : List[SP3Epoch] 32 | List of epochs with satellite data. 33 | """ 34 | self.header = header 35 | self.epochs = epochs 36 | 37 | def write(self, file_path: Union[str, Path]): 38 | """ 39 | Write SP3 data to a file. 40 | 41 | Parameters 42 | ---------- 43 | file_path : Union[str, Path] 44 | Path to the output file. 45 | """ 46 | with open(file_path, 'w') as file: 47 | self._write_header(file) 48 | self._write_epochs(file) 49 | file.write("**EOF **\n") 50 | 51 | def _write_header(self, file: TextIO): 52 | """ 53 | Write the SP3 header to the file. 54 | 55 | Parameters 56 | ---------- 57 | file : TextIO 58 | Open file handle for writing. 59 | """ 60 | h = self.header 61 | dt = h.time 62 | 63 | # First line 64 | file.write(f"#{h.version}{h.file_type}{dt.year:4d} {dt.month:2d} {dt.day:2d} " 65 | f"{dt.hour:2d} {dt.minute:2d} {dt.second:11.8f} " 66 | f"{h.epoch_count:6d} {h.data_used:5s}{h.coordinate_system:5s}" 67 | f"{h.orbit_type:3s} {h.agency:4s}{h.num_satellites:2d}\n") 68 | 69 | # Second line - if GPS week/SOW not provided, calculate them 70 | if h.gps_week == 0 and h.seconds_of_week == 0: 71 | gps_week, sow = gps_time_from_datetime(dt) 72 | else: 73 | gps_week, sow = h.gps_week, h.seconds_of_week 74 | 75 | # If MJD not provided, calculate it 76 | if h.mjd == 0 and h.fractional_day == 0: 77 | mjd, frac = compute_mjd(dt) 78 | else: 79 | mjd, frac = h.mjd, h.fractional_day 80 | 81 | file.write(f"##{gps_week:4d} {sow:15.8f} {h.epoch_interval:14.8f}" 82 | f"{mjd:5d}{frac:15.13f}\n") 83 | 84 | # Satellite IDs 85 | for i in range(0, len(h.satellite_ids), 17): 86 | sat_block = h.satellite_ids[i:i+17] 87 | line = "+" 88 | for sat in sat_block: 89 | line += f" {sat:3s}" 90 | line += " " * (3 * (17 - len(sat_block)) + 1) # Pad to fixed width 91 | file.write(f"{line}\n") 92 | 93 | # Satellite accuracy 94 | for i in range(0, len(h.satellite_ids), 17): 95 | sat_block = h.satellite_ids[i:i+17] 96 | line = "++" 97 | for sat in sat_block: 98 | acc = h.satellite_accuracy.get(sat, 0) 99 | line += f" {acc:2d}" 100 | line += " " * (3 * (17 - len(sat_block)) + 1) # Pad to fixed width 101 | file.write(f"{line}\n") 102 | 103 | # File and time system 104 | file.write(f"%c {h.file_system} cc{h.time_system}ccc cccc cccc cccc cccc " 105 | f"ccccc ccccc ccccc ccccc\n") 106 | 107 | # Additional fixed format lines for the header 108 | file.write("%c cccc cccc cccc cccc ccccc ccccc ccccc ccccc\n") 109 | file.write("%f 1.2500000 1.025000000 0.00000000000 0.000000000000000\n") 110 | file.write("%f 0.0000000 0.000000000 0.00000000000 0.000000000000000\n") 111 | file.write("%i 0 0 0 0 0 0 0 0 0\n") 112 | file.write("%i 0 0 0 0 0 0 0 0 0\n") 113 | 114 | # Write comments 115 | for comment in h.comments: 116 | file.write(f"/* {comment}\n") 117 | 118 | # End of header marker 119 | file.write("*\n") 120 | 121 | def _write_epochs(self, file: TextIO): 122 | """ 123 | Write epoch data to the file. 124 | 125 | Parameters 126 | ---------- 127 | file : TextIO 128 | Open file handle for writing. 129 | """ 130 | for epoch in self.epochs: 131 | dt = epoch.time 132 | 133 | # Write epoch line 134 | file.write(f"* {dt.year:4d} {dt.month:2d} {dt.day:2d} " 135 | f"{dt.hour:2d} {dt.minute:2d} {dt.second:11.8f}\n") 136 | 137 | # Write satellite positions 138 | for sat_id, sat in sorted(epoch.satellites.items()): 139 | self._write_satellite_record(file, sat) 140 | 141 | def _write_satellite_record(self, file: TextIO, sat: SP3SatelliteRecord): 142 | """ 143 | Write a satellite record to the file. 144 | 145 | Parameters 146 | ---------- 147 | file : TextIO 148 | Open file handle for writing. 149 | sat : SP3SatelliteRecord 150 | The satellite record to write. 151 | """ 152 | # Position record 153 | line = f"P{sat.sat_id:3s} {sat.x:14.6f} {sat.y:14.6f} {sat.z:14.6f} {sat.clock:14.6f}" 154 | 155 | # Add event flags if present 156 | if sat.event_flag is not None: 157 | line += f" {sat.event_flag:1d}" 158 | if sat.clock_event_flag is not None: 159 | line += f" {sat.clock_event_flag:1d}" 160 | 161 | file.write(f"{line}\n") 162 | 163 | # Position standard deviations if available 164 | if sat.x_sdev is not None and sat.y_sdev is not None and sat.z_sdev is not None and sat.clock_sdev is not None: 165 | file.write(f"EP{sat.sat_id:3s} {sat.x_sdev:5.0f} {sat.y_sdev:5.0f} " 166 | f"{sat.z_sdev:5.0f} {sat.clock_sdev:5.0f}\n") 167 | 168 | # Velocity record if available and if file type is 'V' 169 | if self.header.file_type == 'V' and sat.x_vel is not None and sat.y_vel is not None and sat.z_vel is not None: 170 | file.write(f"V{sat.sat_id:3s} {sat.x_vel:14.6f} {sat.y_vel:14.6f} " 171 | f"{sat.z_vel:14.6f} {sat.clock_rate or 0.0:14.6f}\n") 172 | 173 | # Velocity standard deviations if available 174 | if (sat.x_vel_sdev is not None and sat.y_vel_sdev is not None 175 | and sat.z_vel_sdev is not None and sat.clock_rate_sdev is not None): 176 | file.write(f"EV{sat.sat_id:3s} {sat.x_vel_sdev:5.0f} {sat.y_vel_sdev:5.0f} " 177 | f"{sat.z_vel_sdev:5.0f} {sat.clock_rate_sdev:5.0f}\n") 178 | 179 | 180 | def write_sp3_file(file_path: Union[str, Path], header: SP3Header, epochs: List[SP3Epoch]): 181 | """ 182 | Write SP3 data to a file. 183 | 184 | Parameters 185 | ---------- 186 | file_path : Union[str, Path] 187 | Path to the output file. 188 | header : SP3Header 189 | Header information for the SP3 file. 190 | epochs : List[SP3Epoch] 191 | List of epochs with satellite data. 192 | """ 193 | writer = SP3Writer(header, epochs) 194 | writer.write(file_path) -------------------------------------------------------------------------------- /tests/rinex2/test_output.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pandas as pd 3 | from pathlib import Path 4 | 5 | def test_read_rinex2_files(): 6 | # Define the base directory and file paths 7 | base_dir = Path(__file__).parent.parent.parent 8 | json_file = base_dir / "data" / "jsonpqt" / "ajac0010_metadata.json" 9 | parquet_file = base_dir / "data" / "jsonpqt" / "ajac0010_observations.parquet" 10 | 11 | # Read and print JSON metadata 12 | print("\n=== Reading JSON Metadata ===") 13 | with open(json_file, 'r') as f: 14 | metadata = json.load(f) 15 | print(json.dumps(metadata, indent=2)) 16 | 17 | # Read and print Parquet observations 18 | print("\n=== Reading Parquet Observations ===") 19 | df = pd.read_parquet(parquet_file) 20 | print("\nDataFrame Info:") 21 | print(df.info()) 22 | print("\nFirst few rows of the DataFrame:") 23 | print(df) 24 | 25 | if __name__ == "__main__": 26 | test_read_rinex2_files() 27 | -------------------------------------------------------------------------------- /tests/rinexnav/test_rinexnav.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Test for version 2 navigation file" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 6, 13 | "metadata": {}, 14 | "outputs": [ 15 | { 16 | "name": "stdout", 17 | "output_type": "stream", 18 | "text": [ 19 | "\n", 20 | " PRN Epoch Toc SV_clock_bias SV_clock_drift \\\n", 21 | "0 1 2002-01-01 00:00:00 2002-01-01 00:00:00 0.000469 1.000444e-11 \n", 22 | "1 1 2022-01-01 02:00:00 2022-01-01 02:00:00 0.000469 -1.000444e-11 \n", 23 | "2 1 2022-01-01 04:00:00 2022-01-01 04:00:00 0.000469 -1.000444e-11 \n", 24 | "3 1 2022-01-01 06:00:00 2022-01-01 06:00:00 0.000469 -1.000444e-11 \n", 25 | "4 1 2022-01-01 08:00:00 2022-01-01 08:00:00 0.000469 -1.000444e-11 \n", 26 | "\n", 27 | " SV_clock_drift_rate IODE Crs Delta_n M0 ... \\\n", 28 | "0 0.0 39.0 -141.12500 3.988380e-09 -0.624294 ... \n", 29 | "1 0.0 70.0 -137.78125 4.009810e-09 0.425960 ... \n", 30 | "2 0.0 71.0 -146.31250 4.069812e-09 1.476137 ... \n", 31 | "3 0.0 85.0 -162.96875 3.931592e-09 2.526152 ... \n", 32 | "4 0.0 89.0 -168.37500 3.989095e-09 -2.706783 ... \n", 33 | "\n", 34 | " IDOT L2_codes GPS_week L2_P_flag SV_accuracy SV_health \\\n", 35 | "0 -3.778729e-10 1.0 2190.0 0.0 2.0 0.0 \n", 36 | "1 -3.832303e-10 1.0 2190.0 0.0 2.0 0.0 \n", 37 | "2 -3.578720e-10 1.0 2190.0 0.0 2.0 0.0 \n", 38 | "3 -3.042984e-10 1.0 2190.0 0.0 2.0 0.0 \n", 39 | "4 -2.375099e-10 1.0 2190.0 0.0 2.0 0.0 \n", 40 | "\n", 41 | " TGD IODC Transmission_time Fit_interval \n", 42 | "0 5.122274e-09 39.0 518400.0 NaN \n", 43 | "1 5.122274e-09 70.0 518430.0 NaN \n", 44 | "2 5.122274e-09 71.0 525630.0 NaN \n", 45 | "3 5.122274e-09 85.0 532830.0 NaN \n", 46 | "4 5.122274e-09 89.0 540030.0 NaN \n", 47 | "\n", 48 | "[5 rows x 32 columns]\n", 49 | "Index(['PRN', 'Epoch', 'Toc', 'SV_clock_bias', 'SV_clock_drift',\n", 50 | " 'SV_clock_drift_rate', 'IODE', 'Crs', 'Delta_n', 'M0', 'Cuc', 'e',\n", 51 | " 'Cus', 'sqrt_A', 'Toe', 'Cic', 'OMEGA', 'Cis', 'i0', 'Crc', 'omega',\n", 52 | " 'OMEGA_DOT', 'IDOT', 'L2_codes', 'GPS_week', 'L2_P_flag', 'SV_accuracy',\n", 53 | " 'SV_health', 'TGD', 'IODC', 'Transmission_time', 'Fit_interval'],\n", 54 | " dtype='object')\n", 55 | "1.00044417195e-11\n", 56 | "\n" 57 | ] 58 | } 59 | ], 60 | "source": [ 61 | "import os\n", 62 | "import sys\n", 63 | "import tempfile\n", 64 | "import pandas as pd\n", 65 | "from datetime import datetime, timedelta\n", 66 | "from pathlib import Path\n", 67 | "\n", 68 | "from pygnsslab.io.rinexnav.reader import read_rinex_nav\n", 69 | "from pygnsslab.io.rinexnav.utils import (\n", 70 | " interpolate_orbit,\n", 71 | " compute_sv_clock_correction,\n", 72 | " detect_rinex_version\n", 73 | ")\n", 74 | "\n", 75 | "# file location\n", 76 | "# Define the base directory and file paths\n", 77 | "base_dir = Path.cwd().parent.parent\n", 78 | "file_location = base_dir / \"data\" / \"nav\" / \"ajac0010.22n\"\n", 79 | "\n", 80 | "# read the file (this is a dict of pandas DataFrames)\n", 81 | "nav_data = read_rinex_nav(file_location).to_dataframe()\n", 82 | "\n", 83 | "# print the type of the data\n", 84 | "print(type(nav_data))\n", 85 | "\n", 86 | "# get GPS data\n", 87 | "gps_data = nav_data['G']\n", 88 | "\n", 89 | "# print the first few rows of the data\n", 90 | "print(gps_data.head())\n", 91 | "\n", 92 | "# print the column names\n", 93 | "print(gps_data.columns)\n", 94 | "\n", 95 | "# print a specific SV_clock_drift value (e.g. the first row)\n", 96 | "print(gps_data['SV_clock_drift'].iloc[0])\n", 97 | "\n", 98 | "# print the type of the SV_clock_drift value\n", 99 | "print(type(gps_data['SV_clock_drift'].iloc[0]))\n" 100 | ] 101 | }, 102 | { 103 | "cell_type": "markdown", 104 | "metadata": {}, 105 | "source": [ 106 | "## Test for version 3 navigation file" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": 7, 112 | "metadata": {}, 113 | "outputs": [ 114 | { 115 | "name": "stdout", 116 | "output_type": "stream", 117 | "text": [ 118 | "\n", 119 | " PRN Epoch Toc SV_clock_bias SV_clock_drift \\\n", 120 | "0 31 2022-01-01 2022-01-01 -0.000158 -1.818989e-12 \n", 121 | "1 11 2022-01-01 2022-01-01 -0.000001 -3.387868e-11 \n", 122 | "2 12 2022-01-01 2022-01-01 -0.000149 -5.570655e-12 \n", 123 | "3 24 2022-01-01 2022-01-01 0.000277 7.958079e-13 \n", 124 | "4 18 2022-01-01 2022-01-01 0.000269 -7.048584e-12 \n", 125 | "\n", 126 | " SV_clock_drift_rate IODE Crs Delta_n M0 ... \\\n", 127 | "0 0.0 11.0 -17.09375 4.802343e-09 -1.730985 ... \n", 128 | "1 0.0 187.0 -167.34375 4.416613e-09 1.617921 ... \n", 129 | "2 0.0 176.0 171.09375 4.090528e-09 -1.949393 ... \n", 130 | "3 0.0 69.0 -10.96875 5.471299e-09 -0.311958 ... \n", 131 | "4 0.0 100.0 -162.00000 4.190532e-09 -0.429474 ... \n", 132 | "\n", 133 | " IDOT L2_codes GPS_week L2_P_flag SV_accuracy SV_health \\\n", 134 | "0 -4.960921e-10 1.0 2190.0 0.0 2.0 0.0 \n", 135 | "1 -3.496574e-10 1.0 2190.0 0.0 2.0 63.0 \n", 136 | "2 7.286018e-11 1.0 2190.0 0.0 2.8 0.0 \n", 137 | "3 -6.114540e-10 1.0 2190.0 0.0 2.0 0.0 \n", 138 | "4 -4.000167e-10 1.0 2190.0 0.0 2.0 0.0 \n", 139 | "\n", 140 | " TGD IODC Transmission_time Fit_interval \n", 141 | "0 -1.303852e-08 11.0 516102.0 4.0 \n", 142 | "1 -8.847564e-09 443.0 511218.0 4.0 \n", 143 | "2 -1.257285e-08 176.0 511218.0 4.0 \n", 144 | "3 2.328306e-09 69.0 511218.0 4.0 \n", 145 | "4 -8.381903e-09 868.0 518298.0 4.0 \n", 146 | "\n", 147 | "[5 rows x 32 columns]\n", 148 | "Index(['PRN', 'Epoch', 'Toc', 'SV_clock_bias', 'SV_clock_drift',\n", 149 | " 'SV_clock_drift_rate', 'IODE', 'Crs', 'Delta_n', 'M0', 'Cuc', 'e',\n", 150 | " 'Cus', 'sqrt_A', 'Toe', 'Cic', 'OMEGA', 'Cis', 'i0', 'Crc', 'omega',\n", 151 | " 'OMEGA_DOT', 'IDOT', 'L2_codes', 'GPS_week', 'L2_P_flag', 'SV_accuracy',\n", 152 | " 'SV_health', 'TGD', 'IODC', 'Transmission_time', 'Fit_interval'],\n", 153 | " dtype='object')\n", 154 | "-1.81898940355e-12\n", 155 | "\n" 156 | ] 157 | } 158 | ], 159 | "source": [ 160 | "import os\n", 161 | "import sys\n", 162 | "import tempfile\n", 163 | "import pandas as pd\n", 164 | "from datetime import datetime, timedelta\n", 165 | "from pathlib import Path\n", 166 | "\n", 167 | "from pygnsslab.io.rinexnav.reader import read_rinex_nav\n", 168 | "from pygnsslab.io.rinexnav.utils import (\n", 169 | " interpolate_orbit,\n", 170 | " compute_sv_clock_correction,\n", 171 | " detect_rinex_version\n", 172 | ")\n", 173 | "\n", 174 | "# file location\n", 175 | "# Define the base directory and file paths\n", 176 | "base_dir = Path.cwd().parent.parent\n", 177 | "file_location = base_dir / \"data\" / \"nav\" / \"PTLD00AUS_R_20220010000_01D_30S_MN.rnx\"\n", 178 | "\n", 179 | "# read the file (this is a dict of pandas DataFrames)\n", 180 | "nav_data = read_rinex_nav(file_location).to_dataframe()\n", 181 | "\n", 182 | "# print the type of the data\n", 183 | "print(type(nav_data))\n", 184 | "\n", 185 | "# get GPS data\n", 186 | "gps_data = nav_data['G']\n", 187 | "\n", 188 | "# print the first few rows of the data\n", 189 | "print(gps_data.head())\n", 190 | "\n", 191 | "# print the column names\n", 192 | "print(gps_data.columns)\n", 193 | "\n", 194 | "# print a specific SV_clock_drift value (e.g. the first row)\n", 195 | "print(gps_data['SV_clock_drift'].iloc[0])\n", 196 | "\n", 197 | "# print the type of the SV_clock_drift value\n", 198 | "print(type(gps_data['SV_clock_drift'].iloc[0]))\n" 199 | ] 200 | } 201 | ], 202 | "metadata": { 203 | "kernelspec": { 204 | "display_name": ".venv", 205 | "language": "python", 206 | "name": "python3" 207 | }, 208 | "language_info": { 209 | "codemirror_mode": { 210 | "name": "ipython", 211 | "version": 3 212 | }, 213 | "file_extension": ".py", 214 | "mimetype": "text/x-python", 215 | "name": "python", 216 | "nbconvert_exporter": "python", 217 | "pygments_lexer": "ipython3", 218 | "version": "3.12.0" 219 | } 220 | }, 221 | "nbformat": 4, 222 | "nbformat_minor": 2 223 | } 224 | -------------------------------------------------------------------------------- /tests/rinexnav/test_rinexnav.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test script for the RINEX Navigation reader module. 3 | 4 | This script tests the RINEX Navigation reader with sample files to verify functionality. 5 | """ 6 | 7 | import os 8 | import sys 9 | import tempfile 10 | import pandas as pd 11 | from datetime import datetime, timedelta 12 | 13 | # Add the parent directory to the path to import the pygnsslab module 14 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..'))) 15 | 16 | from pygnsslab.io.rinexnav.reader import read_rinex_nav 17 | from pygnsslab.io.rinexnav.utils import ( 18 | interpolate_orbit, 19 | compute_sv_clock_correction, 20 | detect_rinex_version 21 | ) 22 | 23 | def write_sample_file(content, file_extension=".txt"): 24 | """ 25 | Write sample content to a temporary file. 26 | 27 | Parameters 28 | ---------- 29 | content : str 30 | Content to write to the file. 31 | file_extension : str, optional 32 | File extension to use for the temporary file (default: ".txt"). 33 | 34 | Returns 35 | ------- 36 | str 37 | Path to the temporary file. 38 | """ 39 | with tempfile.NamedTemporaryFile(suffix=file_extension, delete=False, mode="w") as f: 40 | f.write(content) 41 | temp_path = f.name 42 | 43 | return temp_path 44 | 45 | def test_rinex2_reader(): 46 | """Test the RINEX 2 Navigation reader with sample data.""" 47 | print("Starting RINEX 2 Navigation reader test...") 48 | # Sample RINEX 2 Navigation data 49 | rinex2_content = """ 2.11 N: GPS NAV DATA RINEX VERSION / TYPE 50 | Converto v3.5.6 IGN 20220102 000706 UTC PGM / RUN BY / DATE 51 | Linux 2.6.32-573.12.1.x86_64|x86_64|gcc|Linux 64|=+ COMMENT 52 | END OF HEADER 53 | 1 02 1 1 0 0 0.0 4.691267386079D-04-1.000444171950D-11 0.000000000000D+00 54 | 3.900000000000D+01-1.411250000000D+02 3.988380292697D-09-6.242942382352D-01 55 | -7.363036274910D-06 1.121813920327D-02 4.695728421211D-06 5.153674995422D+03 56 | 5.184000000000D+05-3.166496753693D-08-1.036611240093D+00 1.955777406693D-07 57 | 9.864187694897D-01 2.997500000000D+02 8.840876015687D-01-8.133553386358D-09 58 | -3.778728718817D-10 1.000000000000D+00 2.190000000000D+03 0.000000000000D+00 59 | 2.000000000000D+00 0.000000000000D+00 5.122274160385D-09 3.900000000000D+01 60 | 5.184000000000D+05 61 | 1 02 1 1 2 0 0.0 4.690550267696D-04-1.000444171950D-11 0.000000000000D+00 62 | 7.000000000000D+01-1.377812500000D+02 4.009809817518D-09 4.259599915377D-01 63 | -7.105991244316D-06 1.121853594668D-02 4.127621650696D-06 5.153675922394D+03 64 | 5.256000000000D+05-8.381903171539D-08-1.036670036234D+00 1.229345798492D-07 65 | 9.864159884824D-01 3.122812500000D+02 8.840083845547D-01-8.161768150217D-09 66 | -3.832302530871D-10 1.000000000000D+00 2.190000000000D+03 0.000000000000D+00 67 | 2.000000000000D+00 0.000000000000D+00 5.122274160385D-09 7.000000000000D+01 68 | 5.184300000000D+05""" 69 | 70 | print("Writing sample RINEX 2 content to temporary file...") 71 | # Write to a temporary file 72 | rinex2_file = write_sample_file(rinex2_content, ".n") 73 | 74 | try: 75 | # Test version detection 76 | version = detect_rinex_version(rinex2_file) 77 | print(f"Detected RINEX version: {version}") 78 | assert 2.0 <= version < 3.0, f"Expected RINEX 2.x, got {version}" 79 | 80 | # Test reading the file 81 | print("Reading RINEX 2 navigation file...") 82 | nav_reader = read_rinex_nav(rinex2_file) 83 | 84 | # Check if header was parsed correctly 85 | print(f"Header version: {nav_reader.header['version']}") 86 | print(f"Header file_type: '{nav_reader.header['file_type']}'") 87 | 88 | assert nav_reader.header['version'] == 2.11, "Incorrect version in header" 89 | assert nav_reader.header['file_type'] == 'N', "Incorrect file type in header" 90 | assert 'program' in nav_reader.header, "Missing program in header" 91 | 92 | # Check if data was parsed correctly 93 | assert 'G' in nav_reader.data, "Missing GPS data" 94 | gps_data = nav_reader.data['G'] 95 | assert len(gps_data) == 2, f"Expected 2 records, got {len(gps_data)}" 96 | 97 | # Check the first record 98 | first_record = gps_data.iloc[0] 99 | assert first_record['PRN'] == 1, f"Expected PRN 1, got {first_record['PRN']}" 100 | assert first_record['Epoch'].year == 2002, f"Expected year 2002, got {first_record['Epoch'].year}" 101 | assert abs(first_record['SV_clock_bias'] - 4.691267386079e-04) < 1e-12, "Incorrect SV clock bias" 102 | 103 | # Test orbit interpolation 104 | test_time = first_record['Epoch'] + timedelta(hours=1) 105 | try: 106 | orbit = interpolate_orbit(gps_data, test_time, 'G', 1) 107 | print("Interpolated orbit:", orbit) 108 | assert 'X' in orbit and 'Y' in orbit and 'Z' in orbit, "Missing position components" 109 | assert 'VX' in orbit and 'VY' in orbit and 'VZ' in orbit, "Missing velocity components" 110 | except Exception as e: 111 | print(f"Warning: Orbit interpolation failed: {e}") 112 | 113 | # Test clock correction 114 | try: 115 | clock_corr = compute_sv_clock_correction(gps_data, test_time, 'G', 1) 116 | print(f"Clock correction: {clock_corr} seconds") 117 | except Exception as e: 118 | print(f"Warning: Clock correction failed: {e}") 119 | 120 | print("RINEX 2 Navigation reader test: PASSED") 121 | 122 | except Exception as e: 123 | print(f"RINEX 2 Navigation reader test: FAILED - {e}") 124 | 125 | finally: 126 | # Clean up 127 | if os.path.exists(rinex2_file): 128 | os.unlink(rinex2_file) 129 | 130 | def test_rinex3_reader(): 131 | """Test the RINEX 3 Navigation reader with sample data.""" 132 | print("Starting RINEX 3 Navigation reader test...") 133 | # Sample RINEX 3 Navigation data 134 | rinex3_content = """ 3.04 N: GNSS NAV DATA M: MIXED NAV DATA RINEX VERSION / TYPE 135 | Alloy 5.44 Receiver Operator 20220101 000000 UTC PGM / RUN BY / DATE 136 | GPSA .1211D-07 -.7451D-08 -.5960D-07 .1192D-06 IONOSPHERIC CORR 137 | GPSB .1167D+06 -.2458D+06 -.6554D+05 .1114D+07 IONOSPHERIC CORR 138 | GAL .8775D+02 .4180D+00 -.1074D-01 .0000D+00 IONOSPHERIC CORR 139 | QZSA .1118D-07 -.4470D-07 -.4172D-06 -.4768D-06 IONOSPHERIC CORR 140 | QZSB .9830D+05 -.3277D+05 -.1507D+07 -.8192D+07 IONOSPHERIC CORR 141 | GPUT -.1862645149D-08 -.621724894D-14 61440 2191 TIME SYSTEM CORR 142 | GAUT .1862645149D-08 -.888178420D-15 432000 2190 TIME SYSTEM CORR 143 | QZUT .2793967724D-08 .000000000D+00 94208 2191 TIME SYSTEM CORR 144 | GPGA .1979060471D-08 -.976996262D-14 518400 2190 TIME SYSTEM CORR 145 | 18 18 2185 7 LEAP SECONDS 146 | END OF HEADER 147 | G31 2022 01 01 00 00 00 -.157775823027D-03 -.181898940355D-11 .000000000000D+00 148 | .110000000000D+02 -.170937500000D+02 .480234289400D-08 -.173098544978D+01 149 | -.117905437946D-05 .104654430179D-01 .877678394318D-05 .515366246605D+04 150 | .518400000000D+06 .204890966415D-07 .211722260728D+01 -.141561031342D-06 151 | .954983160093D+00 .203406250000D+03 .359636360687D+00 -.795676000242D-08 152 | -.496092092798D-09 .100000000000D+01 .219000000000D+04 .000000000000D+00 153 | .200000000000D+01 .000000000000D+00 -.130385160446D-07 .110000000000D+02 154 | .516102000000D+06 .400000000000D+01 155 | C01 2021 12 31 23 00 00 -.285546178930D-03 .402868849392D-10 .000000000000D+00 156 | .100000000000D+01 .250171875000D+03 -.412517182996D-09 .329616550241D+00 157 | .806059688330D-05 .611470197327D-03 .322125852108D-04 .649341227531D+04 158 | .514800000000D+06 -.211410224438D-06 .302754416285D+01 -.167638063431D-06 159 | .794697272388D-01 -.992296875000D+03 -.992867610818D+00 .158042297382D-08 160 | -.118362073113D-08 .000000000000D+00 .834000000000D+03 .000000000000D+00 161 | .200000000000D+01 .000000000000D+00 -.580000003580D-08 -.102000000000D-07 162 | .514800000000D+06 .000000000000D+00 163 | R05 2021 12 31 23 45 00 .851778313518D-04 .909494701773D-12 .518370000000D+06 164 | -.215384257812D+05 -.146265029907D-01 -.279396772385D-08 .000000000000D+00 165 | -.136201572266D+05 .206674575806D+00 .279396772385D-08 .100000000000D+01 166 | -.692044921875D+03 -.358163070679D+01 -.186264514923D-08 .000000000000D+00""" 167 | 168 | print("Writing sample RINEX 3 content to temporary file...") 169 | # Write to a temporary file 170 | rinex3_file = write_sample_file(rinex3_content, ".nav") 171 | 172 | try: 173 | # Test version detection 174 | version = detect_rinex_version(rinex3_file) 175 | print(f"Detected RINEX version: {version}") 176 | assert 3.0 <= version < 4.0, f"Expected RINEX 3.x, got {version}" 177 | 178 | # Test reading the file 179 | print("Reading RINEX 3 navigation file...") 180 | nav_reader = read_rinex_nav(rinex3_file) 181 | 182 | # Check if header was parsed correctly 183 | print(f"Header file_type: '{nav_reader.header['file_type']}'") 184 | assert nav_reader.header['version'] == 3.04, "Incorrect version in header" 185 | assert nav_reader.header['file_type'] == 'N', "Incorrect file type in header" 186 | assert 'program' in nav_reader.header, "Missing program in header" 187 | assert 'ionospheric_corr' in nav_reader.header, "Missing ionospheric correction in header" 188 | assert 'time_system_corr' in nav_reader.header, "Missing time system correction in header" 189 | 190 | # Check if data was parsed correctly 191 | print(f"Systems in data: {list(nav_reader.data.keys())}") 192 | assert 'G' in nav_reader.data, "Missing GPS data" 193 | assert 'C' in nav_reader.data, "Missing BeiDou data" 194 | assert 'R' in nav_reader.data, "Missing GLONASS data" 195 | 196 | # Check GPS data 197 | gps_data = nav_reader.data['G'] 198 | assert len(gps_data) == 1, f"Expected 1 GPS record, got {len(gps_data)}" 199 | 200 | # Check BeiDou data 201 | bds_data = nav_reader.data['C'] 202 | assert len(bds_data) == 1, f"Expected 1 BeiDou record, got {len(bds_data)}" 203 | 204 | # Check GLONASS data 205 | glo_data = nav_reader.data['R'] 206 | assert len(glo_data) == 1, f"Expected 1 GLONASS record, got {len(glo_data)}" 207 | 208 | # Check GPS record 209 | gps_record = gps_data.iloc[0] 210 | assert gps_record['PRN'] == 31, f"Expected PRN 31, got {gps_record['PRN']}" 211 | assert gps_record['Epoch'].year == 2022, f"Expected year 2022, got {gps_record['Epoch'].year}" 212 | assert abs(gps_record['SV_clock_bias'] + 1.57775823027e-04) < 1e-12, "Incorrect SV clock bias" 213 | 214 | # Check BeiDou record 215 | bds_record = bds_data.iloc[0] 216 | assert bds_record['PRN'] == 1, f"Expected PRN 1, got {bds_record['PRN']}" 217 | 218 | # Check GLONASS record 219 | glo_record = glo_data.iloc[0] 220 | assert glo_record['PRN'] == 5, f"Expected PRN 5, got {glo_record['PRN']}" 221 | 222 | # Test orbit interpolation for GPS 223 | test_time = gps_record['Epoch'] + timedelta(hours=1) 224 | try: 225 | orbit = interpolate_orbit(gps_data, test_time, 'G', 31) 226 | print("Interpolated GPS orbit:", orbit) 227 | assert 'X' in orbit and 'Y' in orbit and 'Z' in orbit, "Missing position components" 228 | except Exception as e: 229 | print(f"Warning: GPS orbit interpolation failed: {e}") 230 | 231 | print("RINEX 3 Navigation reader test: PASSED") 232 | 233 | except Exception as e: 234 | print(f"RINEX 3 Navigation reader test: FAILED - {e}") 235 | import traceback 236 | traceback.print_exc() 237 | 238 | finally: 239 | # Clean up 240 | if os.path.exists(rinex3_file): 241 | os.unlink(rinex3_file) 242 | 243 | def main(): 244 | """Run all tests.""" 245 | print("Testing RINEX Navigation reader...") 246 | test_rinex2_reader() 247 | test_rinex3_reader() 248 | print("All tests completed.") 249 | 250 | if __name__ == "__main__": 251 | main() -------------------------------------------------------------------------------- /tests/sp3/test_sp3.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 15, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "name": "stdout", 10 | "output_type": "stream", 11 | "text": [ 12 | "\n", 13 | "\n", 14 | " 0 1 2 3 4\n", 15 | "0 2023-01-01 00:00:00 26000.000000 0.000000 0.000000 100.0\n", 16 | "1 2023-01-01 01:00:00 22516.660498 13000.000000 4330.127019 100.1\n", 17 | "2 2023-01-01 02:00:00 13000.000000 22516.660498 4330.127019 100.2\n", 18 | "3 2023-01-01 03:00:00 0.000000 26000.000000 0.000000 100.3\n", 19 | "4 2023-01-01 04:00:00 -13000.000000 22516.660498 -4330.127019 100.4\n", 20 | "5 2023-01-01 05:00:00 -22516.660498 13000.000000 -4330.127019 100.5\n", 21 | "6 2023-01-01 06:00:00 -26000.000000 0.000000 -0.000000 100.6\n", 22 | "7 2023-01-01 07:00:00 -22516.660498 -13000.000000 4330.127019 100.7\n", 23 | "8 2023-01-01 08:00:00 -13000.000000 -22516.660498 4330.127019 100.8\n", 24 | "9 2023-01-01 09:00:00 -0.000000 -26000.000000 0.000000 100.9\n", 25 | "10 2023-01-01 10:00:00 13000.000000 -22516.660498 -4330.127019 101.0\n", 26 | "11 2023-01-01 11:00:00 22516.660498 -13000.000000 -4330.127019 101.1\n", 27 | "12 2023-01-01 12:00:00 26000.000000 -0.000000 -0.000000 101.2\n", 28 | "13 2023-01-01 13:00:00 22516.660498 13000.000000 4330.127019 101.3\n", 29 | "14 2023-01-01 14:00:00 13000.000000 22516.660498 4330.127019 101.4\n", 30 | "15 2023-01-01 15:00:00 0.000000 26000.000000 0.000000 101.5\n", 31 | "16 2023-01-01 16:00:00 -13000.000000 22516.660498 -4330.127019 101.6\n", 32 | "17 2023-01-01 17:00:00 -22516.660498 13000.000000 -4330.127019 101.7\n", 33 | "18 2023-01-01 18:00:00 -26000.000000 0.000000 -0.000000 101.8\n", 34 | "19 2023-01-01 19:00:00 -22516.660498 -13000.000000 4330.127019 101.9\n", 35 | "20 2023-01-01 20:00:00 -13000.000000 -22516.660498 4330.127019 102.0\n", 36 | "21 2023-01-01 21:00:00 -0.000000 -26000.000000 0.000000 102.1\n", 37 | "22 2023-01-01 22:00:00 13000.000000 -22516.660498 -4330.127019 102.2\n", 38 | "23 2023-01-01 23:00:00 22516.660498 -13000.000000 -4330.127019 102.3\n" 39 | ] 40 | } 41 | ], 42 | "source": [ 43 | "from datetime import datetime, timedelta\n", 44 | "import os\n", 45 | "import sys\n", 46 | "import pandas as pd\n", 47 | "import matplotlib.pyplot as plt\n", 48 | "import numpy as np\n", 49 | "from pathlib import Path\n", 50 | "\n", 51 | "from pygnsslab.io.sp3.reader import SP3Reader\n", 52 | "from pygnsslab.io.sp3.writer import write_sp3_file\n", 53 | "from pygnsslab.io.sp3.metadata import SP3Header, SP3Epoch, SP3SatelliteRecord\n", 54 | "\n", 55 | "# file location\n", 56 | "# Define the base directory and file paths\n", 57 | "base_dir = Path.cwd().parent.parent\n", 58 | "file_location = base_dir / \"data\" / \"sp3\" / \"COD0MGXFIN_20220010000_01D_05M_ORB.SP3\"\n", 59 | "\n", 60 | "# read the Sp3 file\n", 61 | "reader = SP3Reader(file_location)\n", 62 | "\n", 63 | "# print type of reader\n", 64 | "print(type(reader))\n", 65 | "\n", 66 | "# print the including data in it\n", 67 | "print(type(reader.get_satellite_positions(\"G01\")))\n", 68 | "\n", 69 | "# convert list to pandas dataframe\n", 70 | "df = pd.DataFrame(reader.get_satellite_positions(\"G01\"))\n", 71 | "\n", 72 | "# print the dataframe\n", 73 | "print(df)\n", 74 | "\n", 75 | "\n", 76 | "\n" 77 | ] 78 | } 79 | ], 80 | "metadata": { 81 | "kernelspec": { 82 | "display_name": ".venv", 83 | "language": "python", 84 | "name": "python3" 85 | }, 86 | "language_info": { 87 | "codemirror_mode": { 88 | "name": "ipython", 89 | "version": 3 90 | }, 91 | "file_extension": ".py", 92 | "mimetype": "text/x-python", 93 | "name": "python", 94 | "nbconvert_exporter": "python", 95 | "pygments_lexer": "ipython3", 96 | "version": "3.12.0" 97 | } 98 | }, 99 | "nbformat": 4, 100 | "nbformat_minor": 2 101 | } 102 | --------------------------------------------------------------------------------