gpstec 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gpstec-1.0.0/LICENSE +21 -0
- gpstec-1.0.0/PKG-INFO +157 -0
- gpstec-1.0.0/README.md +122 -0
- gpstec-1.0.0/gpstec/__init__.py +7 -0
- gpstec-1.0.0/gpstec/__main__.py +112 -0
- gpstec-1.0.0/gpstec/calibrate.py +175 -0
- gpstec-1.0.0/gpstec/config.py +85 -0
- gpstec-1.0.0/gpstec/constants.py +24 -0
- gpstec-1.0.0/gpstec/continuity.py +97 -0
- gpstec-1.0.0/gpstec/convert.py +77 -0
- gpstec-1.0.0/gpstec/data/station_template.toml +54 -0
- gpstec-1.0.0/gpstec/dcb.py +226 -0
- gpstec-1.0.0/gpstec/geometry.py +61 -0
- gpstec-1.0.0/gpstec/gim.py +156 -0
- gpstec-1.0.0/gpstec/orbits.py +215 -0
- gpstec-1.0.0/gpstec/pipeline.py +456 -0
- gpstec-1.0.0/gpstec/plots.py +398 -0
- gpstec-1.0.0/gpstec/products.py +154 -0
- gpstec-1.0.0/gpstec/rinex_obs.py +231 -0
- gpstec-1.0.0/gpstec/tec.py +196 -0
- gpstec-1.0.0/gpstec.egg-info/PKG-INFO +157 -0
- gpstec-1.0.0/gpstec.egg-info/SOURCES.txt +31 -0
- gpstec-1.0.0/gpstec.egg-info/dependency_links.txt +1 -0
- gpstec-1.0.0/gpstec.egg-info/entry_points.txt +2 -0
- gpstec-1.0.0/gpstec.egg-info/requires.txt +11 -0
- gpstec-1.0.0/gpstec.egg-info/top_level.txt +1 -0
- gpstec-1.0.0/pyproject.toml +57 -0
- gpstec-1.0.0/setup.cfg +4 -0
- gpstec-1.0.0/tests/test_cli.py +24 -0
- gpstec-1.0.0/tests/test_geometry.py +14 -0
- gpstec-1.0.0/tests/test_gim.py +33 -0
- gpstec-1.0.0/tests/test_midnight.py +51 -0
- gpstec-1.0.0/tests/test_plots.py +44 -0
gpstec-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dipanjan Banerjee
|
|
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.
|
gpstec-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gpstec
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Calibrated GPS total electron content (TEC) from NovAtel GPStation-6 and other dual-frequency receivers, with elevation cut-off and publication-quality plots
|
|
5
|
+
Author: Dipanjan Banerjee
|
|
6
|
+
Maintainer: Dipanjan Banerjee
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/dipanjanbanerjee/gpstec
|
|
9
|
+
Project-URL: Source, https://github.com/dipanjanbanerjee/gpstec
|
|
10
|
+
Project-URL: Issues, https://github.com/dipanjanbanerjee/gpstec/issues
|
|
11
|
+
Keywords: GPS,GNSS,TEC,VTEC,ionosphere,GPStation-6,NovAtel,RINEX,DCB,space weather,equatorial ionization anomaly
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: numpy>=1.24
|
|
27
|
+
Requires-Dist: pandas>=2.0
|
|
28
|
+
Requires-Dist: scipy>=1.10
|
|
29
|
+
Requires-Dist: matplotlib>=3.7
|
|
30
|
+
Requires-Dist: unlzw3>=0.2
|
|
31
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# gpstec
|
|
37
|
+
|
|
38
|
+
**Calibrated GPS total electron content (TEC) from NovAtel GPStation-6 and other dual-frequency receivers.**
|
|
39
|
+
|
|
40
|
+
[](https://github.com/dipanjanbanerjee/gpstec/actions/workflows/tests.yml)
|
|
41
|
+
[](https://pypi.org/project/gpstec/)
|
|
42
|
+
|
|
43
|
+
Website: https://dipanjanbanerjee.github.io/gpstec/
|
|
44
|
+
|
|
45
|
+
gpstec turns a raw GPStation-6 `.GPS` file (or any RINEX 2/3 observation file) into calibrated slant and vertical TEC, with a user-chosen elevation cut-off. It writes daily data files and publication-quality figures in one command:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
gpstec run -c station.toml -o results 2242_0_00_skbu.GPS
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
## Why
|
|
54
|
+
|
|
55
|
+
Many GPStation-6 users go through the chain `.GPS` → RTKLIB → RINEX → a closed calibration program → `.Cmn` files. That chain has three problems:
|
|
56
|
+
|
|
57
|
+
- **Wrong station position.** RTKCONV 2.4.2 writes latitude, longitude and height into the `APPROX POSITION XYZ` header field, which expects Earth-centred coordinates. A program that trusts the header then places the station near the centre of the Earth, so every elevation, pierce point and VTEC is wrong. gpstec always takes the position from your configuration file.
|
|
58
|
+
- **No elevation cut-off.** Low-elevation rays need a mapping function that fails near the equatorial anomaly. gpstec lets you choose the cut-off (50° by default) and keeps all lower data out of the products.
|
|
59
|
+
- **No way to check the calibration.** gpstec reports independent checks for every day: the CODE global ionosphere map, a midnight continuity test, and a calibration uncertainty band.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
Python 3.9 or newer:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
pip install gpstec
|
|
67
|
+
gpstec install-convbin # only needed for .GPS input: builds RTKLIB convbin (needs git, gcc, make)
|
|
68
|
+
gpstec init # writes station.toml: put your antenna position in it
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
On Ubuntu/Debian, `sudo apt install git build-essential` provides the build tools for `install-convbin`. On Windows, use the `convbin.exe` that comes with RTKLIB and set its path in `station.toml`.
|
|
72
|
+
|
|
73
|
+
## Use
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
gpstec run -c station.toml -o results /data/2242_*.GPS # a week of receiver files
|
|
77
|
+
gpstec run -c station.toml -o results skbu3590.22o # RINEX (.22n next to it)
|
|
78
|
+
gpstec run -c station.toml -o results skbu3590.22o --elev 40 # another elevation cut-off
|
|
79
|
+
gpstec --version
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Satellite bias files (CODE `.DCB`) and global ionosphere maps (CODE IONEX) are downloaded automatically into `dcb/` and `gim/`. If your network blocks the download, put the files there yourself. gpstec picks them up by date and prints the exact file names it needs.
|
|
83
|
+
|
|
84
|
+
## Method
|
|
85
|
+
|
|
86
|
+
1. **.GPS → RINEX** with RTKLIB `convbin -r nov`. The correct station position is written into the header.
|
|
87
|
+
2. **Geometry.** Satellite positions come from the broadcast ephemeris (or SP3 precise orbits with `--sp3`). Azimuth, elevation and pierce points are computed on a thin shell at 350 km.
|
|
88
|
+
3. **Slant TEC.**
|
|
89
|
+
- Code: `P4 = P2 − C1`.
|
|
90
|
+
- Carrier phase: `L4 = L1·λ1 − L2·λ2`.
|
|
91
|
+
- Each continuous satellite pass is levelled to the code with an elevation-weighted mean above 20° (Ciraolo et al., 2007).
|
|
92
|
+
- Passes that continue across midnight are levelled as one pass.
|
|
93
|
+
4. **Biases:** `STEC = STEC_measured + B_sat + B_rx`.
|
|
94
|
+
- `B_sat` comes from CODE monthly DCBs (P1−P2, plus P1−C1 for C1 receivers) or from Bias-SINEX files.
|
|
95
|
+
- `B_rx` is estimated for each day from that day's night-time data alone. It is a least-squares fit of `STEC = VTEC(t, pierce point)/M(el) − B_rx`, in which VTEC may vary north–south and east–west. That matters near the anomaly crest, where minimum-scatter methods drift.
|
|
96
|
+
- The receiver bias is never allowed to make VTEC negative.
|
|
97
|
+
5. **VTEC.** `VTEC = STEC · √(1 − (Re·cos E / (Re + h))²)`, using only satellites above the elevation cut-off.
|
|
98
|
+
6. **Station VTEC curve.** All satellites above the cut-off are fitted together as `V0(t) + Gn(t)·Δlat + Ge(t)·Δlon` with smooth splines. The curve therefore does not jump when a satellite rises or sets, and the horizontal gradients are saved as well.
|
|
99
|
+
|
|
100
|
+
## Checks you get for every day
|
|
101
|
+
|
|
102
|
+
- **CODE global ionosphere map** at the station, drawn on the plot. Its night-time difference from the station is saved, typically within 2–3 TECU.
|
|
103
|
+
- **Calibration uncertainty band:** the spread of independent receiver-bias estimates.
|
|
104
|
+
- **Midnight continuity.** Day X's file ends at 00:00:00 and the next file starts at 00:00:01, so the same satellites are seen at the same moment. gpstec compares them and splits any jump into the part caused by the receiver-bias change and the part left over from processing.
|
|
105
|
+
|
|
106
|
+

|
|
107
|
+
|
|
108
|
+
## Outputs
|
|
109
|
+
|
|
110
|
+
Per day, in `results/YYYY-MM-DD/`:
|
|
111
|
+
|
|
112
|
+
| File | Content |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `STAT_DATE_vtec_1min.csv` | UT, mean VTEC of satellites, std, satellite count, station VTEC, N/E gradients, GIM VTEC, uncertainty |
|
|
115
|
+
| `STAT_DATE.Std` | Gopi-style `.Std` (UT, station VTEC, std, latitude) |
|
|
116
|
+
| `STAT_DATE.Cmn` | Gopi-style `.Cmn` with correct geometry (all elevations ≥ 10°) |
|
|
117
|
+
| `STAT_DATE_vtec.pdf/.png` | daily figure |
|
|
118
|
+
| `STAT_DATE_pierce_points.pdf/.png` | pierce-point map coloured by VTEC |
|
|
119
|
+
| `STAT_DATE_calibration.json` | every bias, method, check and warning |
|
|
120
|
+
|
|
121
|
+
For multi-day runs, `results/` also gets:
|
|
122
|
+
- all days in one CSV;
|
|
123
|
+
- an overlay figure, a day × UT map and the midnight continuity figure;
|
|
124
|
+
- `calibration_summary.csv` and `midnight_continuity.csv`.
|
|
125
|
+
|
|
126
|
+
## Figures
|
|
127
|
+
|
|
128
|
+
The figures are made for papers:
|
|
129
|
+
- vector PDF with embedded, editable fonts, plus 300 dpi PNG;
|
|
130
|
+
- legends outside the data area;
|
|
131
|
+
- a colour-blind-safe palette.
|
|
132
|
+
|
|
133
|
+
The test suite checks automatically that no two pieces of text touch. In `station.toml` you can change:
|
|
134
|
+
- the formats (`png`, `pdf`, `svg`, `eps`, `tiff`);
|
|
135
|
+
- the width (`figure_width_in = 3.5` for one journal column);
|
|
136
|
+
- whether titles are drawn (`figure_titles = false` when the caption goes in the paper).
|
|
137
|
+
|
|
138
|
+

|
|
139
|
+
|
|
140
|
+
## Citing
|
|
141
|
+
|
|
142
|
+
If you use gpstec in published work, please cite it (see [CITATION.cff](CITATION.cff); GitHub shows a "Cite this repository" button):
|
|
143
|
+
|
|
144
|
+
> Banerjee, D. (2026). gpstec: calibrated GPS total electron content from GPStation-6 receivers (version 1.0.0). https://github.com/dipanjanbanerjee/gpstec
|
|
145
|
+
|
|
146
|
+
## References
|
|
147
|
+
|
|
148
|
+
- Ciraolo, L., Azpilicueta, F., Brunini, C., Meza, A., & Radicella, S. M. (2007). Calibration errors on experimental slant total electron content (TEC) determined with GPS. *Journal of Geodesy*, 81, 111–120.
|
|
149
|
+
- Ma, G., & Maruyama, T. (2003). Derivation of TEC and estimation of instrumental biases from GEONET in Japan. *Annales Geophysicae*, 21, 2083–2093.
|
|
150
|
+
- Schaer, S. (1999). *Mapping and predicting the Earth's ionosphere using the Global Positioning System*. PhD thesis, University of Bern.
|
|
151
|
+
- Seemala, G. K., & Valladares, C. E. (2011). Statistics of total electron content depletions observed over the South American continent for the year 2008. *Radio Science*, 46, RS5019.
|
|
152
|
+
|
|
153
|
+
## Author and license
|
|
154
|
+
|
|
155
|
+
Copyright © 2026 Dipanjan Banerjee. Released under the license in [LICENSE](LICENSE).
|
|
156
|
+
|
|
157
|
+
Bug reports and questions: [GitHub issues](https://github.com/dipanjanbanerjee/gpstec/issues).
|
gpstec-1.0.0/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# gpstec
|
|
2
|
+
|
|
3
|
+
**Calibrated GPS total electron content (TEC) from NovAtel GPStation-6 and other dual-frequency receivers.**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/dipanjanbanerjee/gpstec/actions/workflows/tests.yml)
|
|
6
|
+
[](https://pypi.org/project/gpstec/)
|
|
7
|
+
|
|
8
|
+
Website: https://dipanjanbanerjee.github.io/gpstec/
|
|
9
|
+
|
|
10
|
+
gpstec turns a raw GPStation-6 `.GPS` file (or any RINEX 2/3 observation file) into calibrated slant and vertical TEC, with a user-chosen elevation cut-off. It writes daily data files and publication-quality figures in one command:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
gpstec run -c station.toml -o results 2242_0_00_skbu.GPS
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
Many GPStation-6 users go through the chain `.GPS` → RTKLIB → RINEX → a closed calibration program → `.Cmn` files. That chain has three problems:
|
|
21
|
+
|
|
22
|
+
- **Wrong station position.** RTKCONV 2.4.2 writes latitude, longitude and height into the `APPROX POSITION XYZ` header field, which expects Earth-centred coordinates. A program that trusts the header then places the station near the centre of the Earth, so every elevation, pierce point and VTEC is wrong. gpstec always takes the position from your configuration file.
|
|
23
|
+
- **No elevation cut-off.** Low-elevation rays need a mapping function that fails near the equatorial anomaly. gpstec lets you choose the cut-off (50° by default) and keeps all lower data out of the products.
|
|
24
|
+
- **No way to check the calibration.** gpstec reports independent checks for every day: the CODE global ionosphere map, a midnight continuity test, and a calibration uncertainty band.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Python 3.9 or newer:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
pip install gpstec
|
|
32
|
+
gpstec install-convbin # only needed for .GPS input: builds RTKLIB convbin (needs git, gcc, make)
|
|
33
|
+
gpstec init # writes station.toml: put your antenna position in it
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
On Ubuntu/Debian, `sudo apt install git build-essential` provides the build tools for `install-convbin`. On Windows, use the `convbin.exe` that comes with RTKLIB and set its path in `station.toml`.
|
|
37
|
+
|
|
38
|
+
## Use
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
gpstec run -c station.toml -o results /data/2242_*.GPS # a week of receiver files
|
|
42
|
+
gpstec run -c station.toml -o results skbu3590.22o # RINEX (.22n next to it)
|
|
43
|
+
gpstec run -c station.toml -o results skbu3590.22o --elev 40 # another elevation cut-off
|
|
44
|
+
gpstec --version
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Satellite bias files (CODE `.DCB`) and global ionosphere maps (CODE IONEX) are downloaded automatically into `dcb/` and `gim/`. If your network blocks the download, put the files there yourself. gpstec picks them up by date and prints the exact file names it needs.
|
|
48
|
+
|
|
49
|
+
## Method
|
|
50
|
+
|
|
51
|
+
1. **.GPS → RINEX** with RTKLIB `convbin -r nov`. The correct station position is written into the header.
|
|
52
|
+
2. **Geometry.** Satellite positions come from the broadcast ephemeris (or SP3 precise orbits with `--sp3`). Azimuth, elevation and pierce points are computed on a thin shell at 350 km.
|
|
53
|
+
3. **Slant TEC.**
|
|
54
|
+
- Code: `P4 = P2 − C1`.
|
|
55
|
+
- Carrier phase: `L4 = L1·λ1 − L2·λ2`.
|
|
56
|
+
- Each continuous satellite pass is levelled to the code with an elevation-weighted mean above 20° (Ciraolo et al., 2007).
|
|
57
|
+
- Passes that continue across midnight are levelled as one pass.
|
|
58
|
+
4. **Biases:** `STEC = STEC_measured + B_sat + B_rx`.
|
|
59
|
+
- `B_sat` comes from CODE monthly DCBs (P1−P2, plus P1−C1 for C1 receivers) or from Bias-SINEX files.
|
|
60
|
+
- `B_rx` is estimated for each day from that day's night-time data alone. It is a least-squares fit of `STEC = VTEC(t, pierce point)/M(el) − B_rx`, in which VTEC may vary north–south and east–west. That matters near the anomaly crest, where minimum-scatter methods drift.
|
|
61
|
+
- The receiver bias is never allowed to make VTEC negative.
|
|
62
|
+
5. **VTEC.** `VTEC = STEC · √(1 − (Re·cos E / (Re + h))²)`, using only satellites above the elevation cut-off.
|
|
63
|
+
6. **Station VTEC curve.** All satellites above the cut-off are fitted together as `V0(t) + Gn(t)·Δlat + Ge(t)·Δlon` with smooth splines. The curve therefore does not jump when a satellite rises or sets, and the horizontal gradients are saved as well.
|
|
64
|
+
|
|
65
|
+
## Checks you get for every day
|
|
66
|
+
|
|
67
|
+
- **CODE global ionosphere map** at the station, drawn on the plot. Its night-time difference from the station is saved, typically within 2–3 TECU.
|
|
68
|
+
- **Calibration uncertainty band:** the spread of independent receiver-bias estimates.
|
|
69
|
+
- **Midnight continuity.** Day X's file ends at 00:00:00 and the next file starts at 00:00:01, so the same satellites are seen at the same moment. gpstec compares them and splits any jump into the part caused by the receiver-bias change and the part left over from processing.
|
|
70
|
+
|
|
71
|
+

|
|
72
|
+
|
|
73
|
+
## Outputs
|
|
74
|
+
|
|
75
|
+
Per day, in `results/YYYY-MM-DD/`:
|
|
76
|
+
|
|
77
|
+
| File | Content |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `STAT_DATE_vtec_1min.csv` | UT, mean VTEC of satellites, std, satellite count, station VTEC, N/E gradients, GIM VTEC, uncertainty |
|
|
80
|
+
| `STAT_DATE.Std` | Gopi-style `.Std` (UT, station VTEC, std, latitude) |
|
|
81
|
+
| `STAT_DATE.Cmn` | Gopi-style `.Cmn` with correct geometry (all elevations ≥ 10°) |
|
|
82
|
+
| `STAT_DATE_vtec.pdf/.png` | daily figure |
|
|
83
|
+
| `STAT_DATE_pierce_points.pdf/.png` | pierce-point map coloured by VTEC |
|
|
84
|
+
| `STAT_DATE_calibration.json` | every bias, method, check and warning |
|
|
85
|
+
|
|
86
|
+
For multi-day runs, `results/` also gets:
|
|
87
|
+
- all days in one CSV;
|
|
88
|
+
- an overlay figure, a day × UT map and the midnight continuity figure;
|
|
89
|
+
- `calibration_summary.csv` and `midnight_continuity.csv`.
|
|
90
|
+
|
|
91
|
+
## Figures
|
|
92
|
+
|
|
93
|
+
The figures are made for papers:
|
|
94
|
+
- vector PDF with embedded, editable fonts, plus 300 dpi PNG;
|
|
95
|
+
- legends outside the data area;
|
|
96
|
+
- a colour-blind-safe palette.
|
|
97
|
+
|
|
98
|
+
The test suite checks automatically that no two pieces of text touch. In `station.toml` you can change:
|
|
99
|
+
- the formats (`png`, `pdf`, `svg`, `eps`, `tiff`);
|
|
100
|
+
- the width (`figure_width_in = 3.5` for one journal column);
|
|
101
|
+
- whether titles are drawn (`figure_titles = false` when the caption goes in the paper).
|
|
102
|
+
|
|
103
|
+

|
|
104
|
+
|
|
105
|
+
## Citing
|
|
106
|
+
|
|
107
|
+
If you use gpstec in published work, please cite it (see [CITATION.cff](CITATION.cff); GitHub shows a "Cite this repository" button):
|
|
108
|
+
|
|
109
|
+
> Banerjee, D. (2026). gpstec: calibrated GPS total electron content from GPStation-6 receivers (version 1.0.0). https://github.com/dipanjanbanerjee/gpstec
|
|
110
|
+
|
|
111
|
+
## References
|
|
112
|
+
|
|
113
|
+
- Ciraolo, L., Azpilicueta, F., Brunini, C., Meza, A., & Radicella, S. M. (2007). Calibration errors on experimental slant total electron content (TEC) determined with GPS. *Journal of Geodesy*, 81, 111–120.
|
|
114
|
+
- Ma, G., & Maruyama, T. (2003). Derivation of TEC and estimation of instrumental biases from GEONET in Japan. *Annales Geophysicae*, 21, 2083–2093.
|
|
115
|
+
- Schaer, S. (1999). *Mapping and predicting the Earth's ionosphere using the Global Positioning System*. PhD thesis, University of Bern.
|
|
116
|
+
- Seemala, G. K., & Valladares, C. E. (2011). Statistics of total electron content depletions observed over the South American continent for the year 2008. *Radio Science*, 46, RS5019.
|
|
117
|
+
|
|
118
|
+
## Author and license
|
|
119
|
+
|
|
120
|
+
Copyright © 2026 Dipanjan Banerjee. Released under the license in [LICENSE](LICENSE).
|
|
121
|
+
|
|
122
|
+
Bug reports and questions: [GitHub issues](https://github.com/dipanjanbanerjee/gpstec/issues).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""gpstec: calibrated GPS total electron content from NovAtel GPStation-6 and other dual-frequency receivers.
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dipanjan Banerjee. See the LICENSE file for terms.
|
|
4
|
+
"""
|
|
5
|
+
__version__ = "1.0.0"
|
|
6
|
+
__author__ = "Dipanjan Banerjee"
|
|
7
|
+
__copyright__ = "Copyright (c) 2026 Dipanjan Banerjee"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Command line interface.
|
|
2
|
+
|
|
3
|
+
gpstec init write a station.toml template to edit
|
|
4
|
+
gpstec run -c station.toml -o results FILES...
|
|
5
|
+
gpstec install-convbin build RTKLIB convbin (needed for .GPS input)
|
|
6
|
+
gpstec --version
|
|
7
|
+
"""
|
|
8
|
+
import argparse
|
|
9
|
+
import glob
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from importlib import resources
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from . import __author__, __copyright__, __version__
|
|
18
|
+
|
|
19
|
+
TEMPLATE = "station_template.toml"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _init(path, force=False):
|
|
23
|
+
path = Path(path)
|
|
24
|
+
if path.exists() and not force:
|
|
25
|
+
print(f"{path} already exists (use --force to overwrite).")
|
|
26
|
+
return 1
|
|
27
|
+
text = resources.files("gpstec").joinpath("data", TEMPLATE).read_text(encoding="utf-8")
|
|
28
|
+
path.write_text(text, encoding="utf-8")
|
|
29
|
+
print(f"Wrote {path}. Edit the [station] block (name, lat, lon, height, utc_offset_h), then run:")
|
|
30
|
+
print(f" gpstec run -c {path} -o results your_file.GPS")
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _install_convbin(dest):
|
|
35
|
+
dest = Path(dest).expanduser()
|
|
36
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
for tool in ("git", "make", "gcc"):
|
|
38
|
+
if not shutil.which(tool):
|
|
39
|
+
print(f"'{tool}' is needed to build convbin. On Ubuntu/Debian: sudo apt install git build-essential")
|
|
40
|
+
return 1
|
|
41
|
+
src = Path.home() / ".cache" / "gpstec-rtklib"
|
|
42
|
+
if src.exists():
|
|
43
|
+
shutil.rmtree(src)
|
|
44
|
+
subprocess.run(["git", "clone", "--depth", "1", "https://github.com/rtklibexplorer/RTKLIB.git", str(src)],
|
|
45
|
+
check=True)
|
|
46
|
+
build = src / "app" / "consapp" / "convbin" / "gcc"
|
|
47
|
+
subprocess.run(["make", "-C", str(build)], check=True)
|
|
48
|
+
shutil.copy2(build / "convbin", dest / "convbin")
|
|
49
|
+
print(f"convbin installed to {dest / 'convbin'}")
|
|
50
|
+
if str(dest) not in os.environ.get("PATH", "").split(os.pathsep):
|
|
51
|
+
print(f"Make sure {dest} is on your PATH, or set convbin = \"{dest / 'convbin'}\" in station.toml.")
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main(argv=None):
|
|
56
|
+
ap = argparse.ArgumentParser(
|
|
57
|
+
prog="gpstec",
|
|
58
|
+
description="Calibrated GPS total electron content (TEC) from NovAtel GPStation-6 and other "
|
|
59
|
+
"dual-frequency receivers.",
|
|
60
|
+
epilog=f"{__copyright__}. https://github.com/dipanjanbanerjee/gpstec",
|
|
61
|
+
formatter_class=argparse.RawTextHelpFormatter)
|
|
62
|
+
ap.add_argument("-V", "--version", action="version",
|
|
63
|
+
version=f"gpstec {__version__}\n{__copyright__}\nAuthor: {__author__}")
|
|
64
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
65
|
+
|
|
66
|
+
i = sub.add_parser("init", help="write a station.toml configuration template")
|
|
67
|
+
i.add_argument("path", nargs="?", default="station.toml")
|
|
68
|
+
i.add_argument("--force", action="store_true", help="overwrite an existing file")
|
|
69
|
+
|
|
70
|
+
c = sub.add_parser("install-convbin", help="download and build RTKLIB convbin (Linux/macOS)")
|
|
71
|
+
c.add_argument("--dest", default="~/.local/bin", help="where to put convbin (default ~/.local/bin)")
|
|
72
|
+
|
|
73
|
+
r = sub.add_parser("run", help="process .GPS receiver files or RINEX files")
|
|
74
|
+
r.add_argument("files", nargs="+", help=".GPS receiver files or RINEX observation files (wildcards OK)")
|
|
75
|
+
r.add_argument("-c", "--config", help="TOML config file (station position, settings)")
|
|
76
|
+
r.add_argument("-o", "--out", default="tec_output", help="output folder")
|
|
77
|
+
r.add_argument("--nav", help="navigation file (if not next to the observation file)")
|
|
78
|
+
r.add_argument("--sp3", nargs="*", help="precise orbit SP3 file(s) instead of broadcast")
|
|
79
|
+
r.add_argument("--dcb", nargs="*", help="CODE P1P2/P1C1 .DCB or Bias-SINEX files")
|
|
80
|
+
r.add_argument("--elev", type=float, help="elevation cut for VTEC output (default 50)")
|
|
81
|
+
r.add_argument("--rx-bias", help="receiver bias: lsq, minstd, gim or a number in TECU")
|
|
82
|
+
r.add_argument("--no-cmn", action="store_true", help="skip writing the large .Cmn-style file")
|
|
83
|
+
a = ap.parse_args(argv)
|
|
84
|
+
|
|
85
|
+
if a.cmd == "init":
|
|
86
|
+
return _init(a.path, a.force)
|
|
87
|
+
if a.cmd == "install-convbin":
|
|
88
|
+
return _install_convbin(a.dest)
|
|
89
|
+
|
|
90
|
+
from . import config, pipeline # heavy imports only when processing
|
|
91
|
+
print(f"gpstec {__version__} · {__copyright__}")
|
|
92
|
+
station, settings = config.load(a.config)
|
|
93
|
+
if a.elev is not None:
|
|
94
|
+
settings.output_elevation = a.elev
|
|
95
|
+
if a.dcb:
|
|
96
|
+
settings.dcb_files = a.dcb
|
|
97
|
+
settings.sat_dcb = "file"
|
|
98
|
+
if a.rx_bias:
|
|
99
|
+
try:
|
|
100
|
+
settings.receiver_bias = float(a.rx_bias)
|
|
101
|
+
except ValueError:
|
|
102
|
+
settings.receiver_bias = a.rx_bias
|
|
103
|
+
files = []
|
|
104
|
+
for f in a.files:
|
|
105
|
+
files += sorted(glob.glob(f)) or [f]
|
|
106
|
+
pipeline.run([Path(f) for f in files], station, settings, a.out, nav=a.nav, sp3=a.sp3,
|
|
107
|
+
write_cmn=not a.no_cmn)
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
sys.exit(main())
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Removing the satellite and receiver code biases from leveled slant TEC.
|
|
2
|
+
|
|
3
|
+
Convention: STEC = STEC_raw + B_sat[prn] + B_rx (all in TECU)
|
|
4
|
+
|
|
5
|
+
Satellite biases come from CODE DCB files when available, otherwise they are
|
|
6
|
+
estimated from the data together with the receiver bias ("lsq").
|
|
7
|
+
|
|
8
|
+
Receiver bias:
|
|
9
|
+
minstd Pick the B_rx that makes the VTEC seen by different satellites at the
|
|
10
|
+
same moment agree best (smallest spread). This is the method used by
|
|
11
|
+
Gopi Seemala's GPS-TEC program (after Ma & Maruyama, 2003).
|
|
12
|
+
lsq Fit STEC_raw = VTEC(t, ipp)/mf - B_sat - B_rx by least squares, with a
|
|
13
|
+
VTEC that is linear in pierce-point lat/lon in 15-minute blocks. Gives
|
|
14
|
+
satellite and receiver biases with no external files (sum of satellite
|
|
15
|
+
biases = 0, the same convention CODE uses).
|
|
16
|
+
"""
|
|
17
|
+
import numpy as np
|
|
18
|
+
import pandas as pd
|
|
19
|
+
from scipy import sparse
|
|
20
|
+
from scipy.sparse.linalg import lsqr
|
|
21
|
+
|
|
22
|
+
from .orbits import gps_seconds
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def apply_sat_bias(d, sat_bias):
|
|
26
|
+
d = d.copy()
|
|
27
|
+
d["b_sat"] = d["prn"].map(sat_bias).astype(float)
|
|
28
|
+
missing = sorted(d.loc[d["b_sat"].isna(), "prn"].unique())
|
|
29
|
+
if missing:
|
|
30
|
+
print(f" warning: no satellite bias for PRN {missing}; those satellites are dropped")
|
|
31
|
+
d = d[d["b_sat"].notna()].copy()
|
|
32
|
+
d["stec_sat"] = d["stec_raw"] + d["b_sat"]
|
|
33
|
+
return d
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _sample(d, el_min, step_s=30):
|
|
37
|
+
s = d[d["el"] >= el_min]
|
|
38
|
+
t = gps_seconds(s["time"].to_numpy())
|
|
39
|
+
return s[np.isclose(t % step_s, 0, atol=0.01)]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def receiver_bias_minstd(d, el_min=30.0, step_s=30, search=(-150.0, 150.0)):
|
|
43
|
+
"""Receiver bias (TECU) minimising the spread of simultaneous VTEC values."""
|
|
44
|
+
s = _sample(d, el_min, step_s)
|
|
45
|
+
if s.empty:
|
|
46
|
+
raise ValueError(f"No data above {el_min} deg elevation to estimate the receiver bias")
|
|
47
|
+
epoch = s["time"].to_numpy()
|
|
48
|
+
_, inv = np.unique(epoch, return_inverse=True)
|
|
49
|
+
stec = s["stec_sat"].to_numpy()
|
|
50
|
+
mf = s["mf"].to_numpy()
|
|
51
|
+
nep = inv.max() + 1
|
|
52
|
+
cnt = np.bincount(inv, minlength=nep)
|
|
53
|
+
ok = cnt[inv] >= 2
|
|
54
|
+
|
|
55
|
+
def cost(b):
|
|
56
|
+
v = (stec + b) * mf
|
|
57
|
+
m = np.bincount(inv, v, nep) / np.maximum(cnt, 1)
|
|
58
|
+
r = (v - m[inv]) ** 2
|
|
59
|
+
var = np.bincount(inv, np.where(ok, r, 0.0), nep) / np.maximum(cnt, 1)
|
|
60
|
+
return np.sqrt(var[cnt >= 2]).mean()
|
|
61
|
+
|
|
62
|
+
grid = np.arange(search[0], search[1] + 1e-9, 1.0)
|
|
63
|
+
c = np.array([cost(b) for b in grid])
|
|
64
|
+
b0 = grid[c.argmin()]
|
|
65
|
+
fine = np.arange(b0 - 1.0, b0 + 1.0 + 1e-9, 0.01)
|
|
66
|
+
cf = np.array([cost(b) for b in fine])
|
|
67
|
+
return float(fine[cf.argmin()]), float(cf.min())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def sample_for_fit(d, el_min=30.0, step_s=30):
|
|
71
|
+
"""The small subset of columns/rows the bias fit needs (to combine many days)."""
|
|
72
|
+
return _sample(d, el_min, step_s)[["time", "prn", "el", "mf", "ipp_lat", "ipp_lon", "stec_raw"]].copy()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def biases_lsq(d, el_min=30.0, step_s=30, block_min=15.0, smooth=0.5, presampled=False):
|
|
76
|
+
"""Joint least-squares estimate of satellite and receiver biases (TECU).
|
|
77
|
+
|
|
78
|
+
Returns (sat_bias dict, receiver_bias, rms of fit).
|
|
79
|
+
"""
|
|
80
|
+
s = (d if presampled else _sample(d, el_min, step_s)).reset_index(drop=True)
|
|
81
|
+
if s.empty:
|
|
82
|
+
raise ValueError("No data for the bias fit")
|
|
83
|
+
t = gps_seconds(s["time"].to_numpy())
|
|
84
|
+
blk = ((t - t.min()) // (block_min * 60)).astype(int)
|
|
85
|
+
nb = blk.max() + 1
|
|
86
|
+
prns = np.sort(s["prn"].unique())
|
|
87
|
+
pidx = np.searchsorted(prns, s["prn"].to_numpy())
|
|
88
|
+
ns = len(prns)
|
|
89
|
+
lat0, lon0 = np.median(s["ipp_lat"]), np.median(s["ipp_lon"])
|
|
90
|
+
dlat = s["ipp_lat"].to_numpy() - lat0
|
|
91
|
+
dlon = s["ipp_lon"].to_numpy() - lon0
|
|
92
|
+
inv_mf = 1.0 / s["mf"].to_numpy()
|
|
93
|
+
y = s["stec_raw"].to_numpy()
|
|
94
|
+
n = len(s)
|
|
95
|
+
w = np.sin(np.radians(s["el"].to_numpy())) # elevation weighting
|
|
96
|
+
|
|
97
|
+
# unknown layout: [a0 (nb), a1 (nb), a2 (nb), b_sat (ns), b_rx]
|
|
98
|
+
rows = np.arange(n)
|
|
99
|
+
cols, vals, rr = [], [], []
|
|
100
|
+
for off, coef in ((0, inv_mf), (nb, inv_mf * dlat), (2 * nb, inv_mf * dlon)):
|
|
101
|
+
rr.append(rows); cols.append(off + blk); vals.append(coef)
|
|
102
|
+
rr.append(rows); cols.append(3 * nb + pidx); vals.append(-np.ones(n))
|
|
103
|
+
rr.append(rows); cols.append(np.full(n, 3 * nb + ns)); vals.append(-np.ones(n))
|
|
104
|
+
nun = 3 * nb + ns + 1
|
|
105
|
+
A = sparse.csr_matrix((np.concatenate(vals) * np.tile(w, 5), (np.concatenate(rr), np.concatenate(cols))), shape=(n, nun))
|
|
106
|
+
yw = y * w
|
|
107
|
+
|
|
108
|
+
# constraints: sum of satellite biases = 0 (strong), smooth VTEC/gradients in time (weak)
|
|
109
|
+
extra_r, extra_c, extra_v, extra_y = [], [], [], []
|
|
110
|
+
r0 = 0
|
|
111
|
+
big = 1e3
|
|
112
|
+
extra_r += [r0] * ns; extra_c += list(range(3 * nb, 3 * nb + ns)); extra_v += [big] * ns; extra_y.append(0.0); r0 += 1
|
|
113
|
+
for off in (0, nb, 2 * nb):
|
|
114
|
+
for kk in range(nb - 1):
|
|
115
|
+
extra_r += [r0, r0]; extra_c += [off + kk, off + kk + 1]; extra_v += [smooth, -smooth]; extra_y.append(0.0); r0 += 1
|
|
116
|
+
C = sparse.csr_matrix((extra_v, (extra_r, extra_c)), shape=(r0, nun))
|
|
117
|
+
AA = sparse.vstack([A, C]).tocsr()
|
|
118
|
+
yy = np.concatenate([yw, extra_y])
|
|
119
|
+
sol = lsqr(AA, yy, atol=1e-12, btol=1e-12, iter_lim=20000)[0]
|
|
120
|
+
resid = A @ sol - yw
|
|
121
|
+
sat = {int(p): float(b) for p, b in zip(prns, sol[3 * nb:3 * nb + ns])}
|
|
122
|
+
return sat, float(sol[-1]), float(np.sqrt(np.mean((resid / w) ** 2)))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def apply_receiver_bias(d, b_rx):
|
|
126
|
+
d = d.copy()
|
|
127
|
+
d["b_rx"] = b_rx
|
|
128
|
+
d["stec"] = d["stec_sat"] + b_rx
|
|
129
|
+
d["vtec"] = d["stec"] * d["mf"]
|
|
130
|
+
return d
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def receiver_bias_lsq_fixed_sats(d, sta_lat, sta_lon, el_min=20.0, step_s=30, block_min=15.0, smooth=0.5):
|
|
134
|
+
"""Receiver bias (TECU) by least squares when satellite biases are already applied.
|
|
135
|
+
|
|
136
|
+
Fits stec_sat = VTEC(t, ipp)/mf - B_rx, with VTEC linear in pierce-point offset
|
|
137
|
+
in 15-minute blocks. Uses the change of the mapping factor with elevation to
|
|
138
|
+
separate B_rx from VTEC; best on night data, where gradients are weak.
|
|
139
|
+
"""
|
|
140
|
+
s = _sample(d, el_min, step_s).reset_index(drop=True)
|
|
141
|
+
if len(s) < 50:
|
|
142
|
+
raise ValueError("Not enough data for the least-squares receiver bias")
|
|
143
|
+
t = gps_seconds(s["time"].to_numpy())
|
|
144
|
+
blk = ((t - t.min()) // (block_min * 60)).astype(int)
|
|
145
|
+
nb = blk.max() + 1
|
|
146
|
+
n = len(s)
|
|
147
|
+
r = np.arange(n)
|
|
148
|
+
inv = 1.0 / s["mf"].to_numpy()
|
|
149
|
+
dlat = s["ipp_lat"].to_numpy() - sta_lat
|
|
150
|
+
dlon = (s["ipp_lon"].to_numpy() - sta_lon) * np.cos(np.radians(sta_lat))
|
|
151
|
+
w = np.sin(np.radians(s["el"].to_numpy()))
|
|
152
|
+
rows = np.concatenate([r, r, r, r])
|
|
153
|
+
cols = np.concatenate([blk, nb + blk, 2 * nb + blk, np.full(n, 3 * nb)])
|
|
154
|
+
vals = np.concatenate([inv, inv * dlat, inv * dlon, -np.ones(n)]) * np.tile(w, 4)
|
|
155
|
+
nun = 3 * nb + 1
|
|
156
|
+
A = sparse.csr_matrix((vals, (rows, cols)), shape=(n, nun))
|
|
157
|
+
er, ec, ev = [], [], []
|
|
158
|
+
k = 0
|
|
159
|
+
for off in (0, nb, 2 * nb):
|
|
160
|
+
for j in range(nb - 1):
|
|
161
|
+
er += [k, k]; ec += [off + j, off + j + 1]; ev += [smooth, -smooth]; k += 1
|
|
162
|
+
C = sparse.csr_matrix((ev, (er, ec)), shape=(k, nun))
|
|
163
|
+
sol = lsqr(sparse.vstack([A, C]).tocsr(), np.r_[s["stec_sat"].to_numpy() * w, np.zeros(k)],
|
|
164
|
+
atol=1e-12, btol=1e-12, iter_lim=20000)[0]
|
|
165
|
+
return float(sol[-1])
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def receiver_bias_floor(d, el_min=10.0, pct=99.5):
|
|
169
|
+
"""Smallest receiver bias that keeps (almost) every satellite's VTEC >= 0.
|
|
170
|
+
|
|
171
|
+
VTEC = (stec_sat + B_rx) * mf >= 0 <=> B_rx >= -stec_sat. Using the 99.5th
|
|
172
|
+
percentile instead of the maximum ignores the noisiest 0.5 % of points.
|
|
173
|
+
"""
|
|
174
|
+
s = d[d["el"] >= el_min]
|
|
175
|
+
return float(np.percentile(-s["stec_sat"].to_numpy(), pct)) if len(s) else -np.inf
|