cusfpredict 0.2.0__tar.gz → 0.3.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.
@@ -0,0 +1,3 @@
1
+ # Include the license file
2
+ include LICENSE.txt
3
+
@@ -0,0 +1,222 @@
1
+ Metadata-Version: 2.4
2
+ Name: cusfpredict
3
+ Version: 0.3.0
4
+ Summary: Python Wrapper for the CUSF High-Altitude Balloon Predictor
5
+ Keywords: horus balloon prediction gfs
6
+ Classifier: Intended Audience :: Developers
7
+ Classifier: Programming Language :: Python :: 3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: fastkml
11
+ Requires-Dist: requests
12
+ Requires-Dist: numpy
13
+ Requires-Dist: pytz
14
+ Requires-Dist: shapely
15
+ Requires-Dist: python-dateutil
16
+ Requires-Dist: xarray
17
+ Requires-Dist: cfgrib>=0.9.8
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: keywords
22
+ Dynamic: license-file
23
+ Dynamic: requires-dist
24
+ Dynamic: summary
25
+
26
+ # CUSF Standalone Predictor - Python Wrapper
27
+ This is a semi-fork of the [CUSF Standalone Predictor](https://github.com/jonsowman/cusf-standalone-predictor/), which provides a Python wrapper around the predictor binary, and provides a means of gathering the requisite wind data.
28
+
29
+ 2018-02 Update: Wind downloader updated to use the [NOMADS GRIB Filter](http://nomads.ncep.noaa.gov/txt_descriptions/grib_filter_doc.shtml), as the OpenDAP interface stopped working. As such, we no longer require PyDAP, but we do now require GDAL to read in the GRIB2 files.
30
+
31
+ 2021-03 Update: We have dropped GDAL in favour of cfgrib.
32
+
33
+ 2026-06 Update: A range of updates for support of newer libraries (e.g. numpy, fastkml), and better packaging. The minimum supported Python version is now 3.9.
34
+
35
+ ## 1. System Dependencies
36
+ The Python package installs its Python dependencies automatically. You still need the system libraries used by the wind-data reader and the standalone C predictor.
37
+
38
+ On a Raspbian/Ubuntu/Debian system, you can get most of the required dependencies using:
39
+ ```
40
+ $ sudo apt-get install git cmake build-essential libglib2.0-dev python3-numpy python3-requests python3-dateutil python3-pip libeccodes-data libeccodes0 libgeos-dev libatlas-base-dev
41
+ ```
42
+
43
+ On macOS with Homebrew:
44
+ ```
45
+ $ brew install cmake glib eccodes geos
46
+ ```
47
+
48
+ ## 2. Install the Python Wrapper
49
+
50
+ ### From PyPI
51
+ Install into a virtual environment:
52
+ ```
53
+ $ python3 -m venv venv
54
+ $ source venv/bin/activate
55
+ $ python -m pip install --upgrade pip
56
+ $ python -m pip install cusfpredict
57
+ ```
58
+
59
+ Python 2 is not supported.
60
+
61
+ ### From Source, Editable Mode (Only required if you are experimenting with changes in this library)
62
+ For development, clone this repository and install it in editable mode:
63
+ ```
64
+ $ git clone https://github.com/darksidelemm/cusf_predictor_wrapper.git
65
+ $ cd cusf_predictor_wrapper
66
+ $ python3 -m venv venv
67
+ $ source venv/bin/activate
68
+ $ python -m pip install --upgrade pip
69
+ $ python -m pip install -e .
70
+ ```
71
+
72
+ If you only want to install the source checkout without editable mode, use:
73
+ ```
74
+ $ python -m pip install .
75
+ ```
76
+
77
+ If Python dependency installation fails, the same dependencies are listed in `requirements.txt` and can be installed explicitly:
78
+ ```
79
+ $ python -m pip install -r requirements.txt
80
+ ```
81
+
82
+ The package depends on Shapely and ecCodes-backed GRIB readers. If installation fails with missing headers or missing shared libraries, install the system dependencies above and retry.
83
+
84
+
85
+ ## 3. Building the Predictor Binary
86
+ The predictor itself is a binary (`pred`), which is built separately using CMake. The Python package does not currently build or install this binary for you.
87
+
88
+ From within the cusf_predictor_wrapper directory, run the following to build the predictor binary:
89
+
90
+ ```
91
+ $ cmake -S src -B src/build
92
+ $ cmake --build src/build
93
+ ```
94
+
95
+ The `pred` binary then needs to be copied somewhere useful. For the example scripts in `apps`, copy it into that directory:
96
+ ```
97
+ $ cp src/build/pred apps/
98
+ ```
99
+
100
+ If you are building this utility for use with chasemapper, then you should copy `pred` into the chasemapper directory:
101
+ ```
102
+ $ cp src/build/pred ~/chasemapper/
103
+ ```
104
+
105
+ A pre-compiled Windows binary of the predictor is available here: http://rfhead.net/horus/cusf_standalone_predictor.zip
106
+ Use at your own risk!
107
+
108
+
109
+ ## 4. Getting Wind Data
110
+ The predictor binary uses a custom wind data format, extracted from NOAA's Global Forecast System wind models. The `cusfpredict.gfs` Python module pulls down and formats the relevant data from NOAA's [NOMADS](http://nomads.ncep.noaa.gov) server.
111
+
112
+ If you are using this library with ChaseMapper, you will need to adjust the download command in the [chasemapper configuration file](https://github.com/projecthorus/chasemapper/blob/master/horusmapper.cfg.example#L135).
113
+
114
+ An example of running it is as follows:
115
+ ```
116
+ $ python3 -m cusfpredict.gfs --lat=-33 --lon=139 --latdelta=10 --londelta=10 -f 24 -m 0p50 -o gfs
117
+ ```
118
+
119
+ The command line arguments are as follows:
120
+ ```
121
+ Area of interest:
122
+ --lat Latitude (Decimal Degrees) of the centre of the area to gather.
123
+ --lon Longitude (Decimal Degrees) of the centre of the area to gather.
124
+ --latdelta Gather data from lat+/-latdelta
125
+ --londelta Gather data from lon+/-londelta
126
+
127
+ Time of interest:
128
+ -f X Gather data up to X hours into the future, from the start of the most recent model. (Note that this can be up to 8 hours in the past.) Make sure you get enough for the flight!
129
+
130
+ GFS Model Choice:
131
+ -m <model> Choose between either:
132
+ 0p50 - 0.5 Degree Spatial, 3-hour Time Resolution
133
+ 0p25_1hr - 0.25 Degree Spatial, 1-hour Time Resolution (default)
134
+
135
+ Other settings:
136
+ -v Verbose output
137
+ -o output_dir (Where to save the gfs data to, defaults to ./gfs/)
138
+ ```
139
+
140
+ The higher resolution wind model you choose, the larger the amount of data to download, and the longer it will take. It also increases the prediction calculation time (though not significantly).
141
+
142
+ `wind_grabber.sh` is an example script to automatically grab wind data first to a temporary directory, and then to the final gfs directory. This could be run from a cronjob to keep the wind data up-to-date.
143
+
144
+ New wind models become available approximately every 6 hours, approximately 4 hours after the model's nominal time (i.e. the 00Z model becomes available around 04Z). Information on the status of the GFS model generation is available here: http://www.nco.ncep.noaa.gov/pmb/nwprod/prodstat_new/
145
+
146
+ ## 5. Using the Predictor
147
+ (Note: This section is intended for users within to run predictions from within their own software. If you are just installing this library for use with chasemapper, you can skip all of this!)
148
+
149
+ The basic usage of the predictor from within Python is as follows:
150
+ ```
151
+ import datetime
152
+ from cusfpredict.predict import Predictor
153
+
154
+ pred = Predictor(bin_path='./pred', gfs_path='./gfs')
155
+
156
+ flight_path = pred.predict(
157
+ launch_lat=-34.9499,
158
+ launch_lon=138.5194,
159
+ launch_alt=0.0,
160
+ ascent_rate=5.0,
161
+ descent_rate=5.0,
162
+ burst_alt=30000,
163
+ launch_time=datetime.datetime.now(datetime.timezone.utc)
164
+ )
165
+
166
+ ```
167
+
168
+ Note that the launch time is a datetime object interpreted as UTC, so make sure you convert your launch time as appropriate.
169
+
170
+ The output is a list-of-lists, containing entries of [utctimestamp, lat, lon, alt], i.e.:
171
+
172
+ ```
173
+ >>> flight_path
174
+ [[1516702953, -34.9471, 138.517, 250.0], [1516703003, -34.9436, 138.514, 500.0], <etc>, [1516703053, -34.9415, 138.513, 750.0]]
175
+ ```
176
+
177
+ There is also a command-line utility, `predict.py`, which allows performing predictions with launch parameter variations:
178
+ ```
179
+ usage: predict.py [-h] [-a ASCENTRATE] [-d DESCENTRATE] [-b BURSTALT]
180
+ [--launchalt LAUNCHALT] [--latitude LATITUDE]
181
+ [--longitude LONGITUDE] [--time TIME] [-o OUTPUT]
182
+ [--altitude_deltas ALTITUDE_DELTAS]
183
+ [--time_deltas TIME_DELTAS] [--absolute]
184
+
185
+ optional arguments:
186
+ -h, --help show this help message and exit
187
+ -a ASCENTRATE, --ascentrate ASCENTRATE
188
+ Ascent Rate (m/s). Default 5m/s
189
+ -d DESCENTRATE, --descentrate DESCENTRATE
190
+ Descent Rate (m/s). Default 5m/s
191
+ -b BURSTALT, --burstalt BURSTALT
192
+ Burst Altitude (m). Default 30000m
193
+ --launchalt LAUNCHALT
194
+ Launch Altitude (m). Default 0m
195
+ --latitude LATITUDE Launch Latitude (dd.dddd)
196
+ --longitude LONGITUDE
197
+ Launch Longitude (dd.dddd)
198
+ --time TIME Launch Time (string, UTC). Default = Now
199
+ -o OUTPUT, --output OUTPUT
200
+ Output KML File. Default = prediction.kml
201
+ --altitude_deltas ALTITUDE_DELTAS
202
+ Comma-delimited list of altitude deltas. (metres).
203
+ --time_deltas TIME_DELTAS
204
+ Comma-delimited list of time deltas. (hours)
205
+ --absolute Show absolute altitudes for tracks and placemarks.
206
+ ```
207
+
208
+ For example, to predict a radiosonde launch from Adelaide Airport (5m/s ascent, 26km burst, 7.5m/s descent), but to look at what happens if the burst altitude is higher or lower than usual:
209
+ ```
210
+ $ python3 predict.py --latitude=-34.9499 --longitude=138.5194 -a 5 -d 7.5 -b 26000 --time "2018-01-27 11:15Z" --altitude_deltas="-2000,0,2000"
211
+ Running using GFS Model: gfs20180127-00z
212
+ 2018-01-27T11:15:00+00:00 5.0/24000.0/7.5 - Landing: -34.8585, 138.9600 at 2018-01-27T13:03:33
213
+ 2018-01-27T11:15:00+00:00 5.0/26000.0/7.5 - Landing: -34.8587, 138.8870 at 2018-01-27T13:11:01
214
+ 2018-01-27T11:15:00+00:00 5.0/28000.0/7.5 - Landing: -34.8598, 138.7990 at 2018-01-27T13:18:22
215
+ KML written to prediction.kml
216
+ ```
217
+
218
+ A few other example scripts are located in the 'apps' directory:
219
+ * basic_usage.py - Example showing how to write a predicted flight path out to a KML file
220
+ * sonde_predict.py - A more complex example, where predictions for the next week's of radiosonde flights are run and written to a KML file.
221
+
222
+
@@ -0,0 +1,197 @@
1
+ # CUSF Standalone Predictor - Python Wrapper
2
+ This is a semi-fork of the [CUSF Standalone Predictor](https://github.com/jonsowman/cusf-standalone-predictor/), which provides a Python wrapper around the predictor binary, and provides a means of gathering the requisite wind data.
3
+
4
+ 2018-02 Update: Wind downloader updated to use the [NOMADS GRIB Filter](http://nomads.ncep.noaa.gov/txt_descriptions/grib_filter_doc.shtml), as the OpenDAP interface stopped working. As such, we no longer require PyDAP, but we do now require GDAL to read in the GRIB2 files.
5
+
6
+ 2021-03 Update: We have dropped GDAL in favour of cfgrib.
7
+
8
+ 2026-06 Update: A range of updates for support of newer libraries (e.g. numpy, fastkml), and better packaging. The minimum supported Python version is now 3.9.
9
+
10
+ ## 1. System Dependencies
11
+ The Python package installs its Python dependencies automatically. You still need the system libraries used by the wind-data reader and the standalone C predictor.
12
+
13
+ On a Raspbian/Ubuntu/Debian system, you can get most of the required dependencies using:
14
+ ```
15
+ $ sudo apt-get install git cmake build-essential libglib2.0-dev python3-numpy python3-requests python3-dateutil python3-pip libeccodes-data libeccodes0 libgeos-dev libatlas-base-dev
16
+ ```
17
+
18
+ On macOS with Homebrew:
19
+ ```
20
+ $ brew install cmake glib eccodes geos
21
+ ```
22
+
23
+ ## 2. Install the Python Wrapper
24
+
25
+ ### From PyPI
26
+ Install into a virtual environment:
27
+ ```
28
+ $ python3 -m venv venv
29
+ $ source venv/bin/activate
30
+ $ python -m pip install --upgrade pip
31
+ $ python -m pip install cusfpredict
32
+ ```
33
+
34
+ Python 2 is not supported.
35
+
36
+ ### From Source, Editable Mode (Only required if you are experimenting with changes in this library)
37
+ For development, clone this repository and install it in editable mode:
38
+ ```
39
+ $ git clone https://github.com/darksidelemm/cusf_predictor_wrapper.git
40
+ $ cd cusf_predictor_wrapper
41
+ $ python3 -m venv venv
42
+ $ source venv/bin/activate
43
+ $ python -m pip install --upgrade pip
44
+ $ python -m pip install -e .
45
+ ```
46
+
47
+ If you only want to install the source checkout without editable mode, use:
48
+ ```
49
+ $ python -m pip install .
50
+ ```
51
+
52
+ If Python dependency installation fails, the same dependencies are listed in `requirements.txt` and can be installed explicitly:
53
+ ```
54
+ $ python -m pip install -r requirements.txt
55
+ ```
56
+
57
+ The package depends on Shapely and ecCodes-backed GRIB readers. If installation fails with missing headers or missing shared libraries, install the system dependencies above and retry.
58
+
59
+
60
+ ## 3. Building the Predictor Binary
61
+ The predictor itself is a binary (`pred`), which is built separately using CMake. The Python package does not currently build or install this binary for you.
62
+
63
+ From within the cusf_predictor_wrapper directory, run the following to build the predictor binary:
64
+
65
+ ```
66
+ $ cmake -S src -B src/build
67
+ $ cmake --build src/build
68
+ ```
69
+
70
+ The `pred` binary then needs to be copied somewhere useful. For the example scripts in `apps`, copy it into that directory:
71
+ ```
72
+ $ cp src/build/pred apps/
73
+ ```
74
+
75
+ If you are building this utility for use with chasemapper, then you should copy `pred` into the chasemapper directory:
76
+ ```
77
+ $ cp src/build/pred ~/chasemapper/
78
+ ```
79
+
80
+ A pre-compiled Windows binary of the predictor is available here: http://rfhead.net/horus/cusf_standalone_predictor.zip
81
+ Use at your own risk!
82
+
83
+
84
+ ## 4. Getting Wind Data
85
+ The predictor binary uses a custom wind data format, extracted from NOAA's Global Forecast System wind models. The `cusfpredict.gfs` Python module pulls down and formats the relevant data from NOAA's [NOMADS](http://nomads.ncep.noaa.gov) server.
86
+
87
+ If you are using this library with ChaseMapper, you will need to adjust the download command in the [chasemapper configuration file](https://github.com/projecthorus/chasemapper/blob/master/horusmapper.cfg.example#L135).
88
+
89
+ An example of running it is as follows:
90
+ ```
91
+ $ python3 -m cusfpredict.gfs --lat=-33 --lon=139 --latdelta=10 --londelta=10 -f 24 -m 0p50 -o gfs
92
+ ```
93
+
94
+ The command line arguments are as follows:
95
+ ```
96
+ Area of interest:
97
+ --lat Latitude (Decimal Degrees) of the centre of the area to gather.
98
+ --lon Longitude (Decimal Degrees) of the centre of the area to gather.
99
+ --latdelta Gather data from lat+/-latdelta
100
+ --londelta Gather data from lon+/-londelta
101
+
102
+ Time of interest:
103
+ -f X Gather data up to X hours into the future, from the start of the most recent model. (Note that this can be up to 8 hours in the past.) Make sure you get enough for the flight!
104
+
105
+ GFS Model Choice:
106
+ -m <model> Choose between either:
107
+ 0p50 - 0.5 Degree Spatial, 3-hour Time Resolution
108
+ 0p25_1hr - 0.25 Degree Spatial, 1-hour Time Resolution (default)
109
+
110
+ Other settings:
111
+ -v Verbose output
112
+ -o output_dir (Where to save the gfs data to, defaults to ./gfs/)
113
+ ```
114
+
115
+ The higher resolution wind model you choose, the larger the amount of data to download, and the longer it will take. It also increases the prediction calculation time (though not significantly).
116
+
117
+ `wind_grabber.sh` is an example script to automatically grab wind data first to a temporary directory, and then to the final gfs directory. This could be run from a cronjob to keep the wind data up-to-date.
118
+
119
+ New wind models become available approximately every 6 hours, approximately 4 hours after the model's nominal time (i.e. the 00Z model becomes available around 04Z). Information on the status of the GFS model generation is available here: http://www.nco.ncep.noaa.gov/pmb/nwprod/prodstat_new/
120
+
121
+ ## 5. Using the Predictor
122
+ (Note: This section is intended for users within to run predictions from within their own software. If you are just installing this library for use with chasemapper, you can skip all of this!)
123
+
124
+ The basic usage of the predictor from within Python is as follows:
125
+ ```
126
+ import datetime
127
+ from cusfpredict.predict import Predictor
128
+
129
+ pred = Predictor(bin_path='./pred', gfs_path='./gfs')
130
+
131
+ flight_path = pred.predict(
132
+ launch_lat=-34.9499,
133
+ launch_lon=138.5194,
134
+ launch_alt=0.0,
135
+ ascent_rate=5.0,
136
+ descent_rate=5.0,
137
+ burst_alt=30000,
138
+ launch_time=datetime.datetime.now(datetime.timezone.utc)
139
+ )
140
+
141
+ ```
142
+
143
+ Note that the launch time is a datetime object interpreted as UTC, so make sure you convert your launch time as appropriate.
144
+
145
+ The output is a list-of-lists, containing entries of [utctimestamp, lat, lon, alt], i.e.:
146
+
147
+ ```
148
+ >>> flight_path
149
+ [[1516702953, -34.9471, 138.517, 250.0], [1516703003, -34.9436, 138.514, 500.0], <etc>, [1516703053, -34.9415, 138.513, 750.0]]
150
+ ```
151
+
152
+ There is also a command-line utility, `predict.py`, which allows performing predictions with launch parameter variations:
153
+ ```
154
+ usage: predict.py [-h] [-a ASCENTRATE] [-d DESCENTRATE] [-b BURSTALT]
155
+ [--launchalt LAUNCHALT] [--latitude LATITUDE]
156
+ [--longitude LONGITUDE] [--time TIME] [-o OUTPUT]
157
+ [--altitude_deltas ALTITUDE_DELTAS]
158
+ [--time_deltas TIME_DELTAS] [--absolute]
159
+
160
+ optional arguments:
161
+ -h, --help show this help message and exit
162
+ -a ASCENTRATE, --ascentrate ASCENTRATE
163
+ Ascent Rate (m/s). Default 5m/s
164
+ -d DESCENTRATE, --descentrate DESCENTRATE
165
+ Descent Rate (m/s). Default 5m/s
166
+ -b BURSTALT, --burstalt BURSTALT
167
+ Burst Altitude (m). Default 30000m
168
+ --launchalt LAUNCHALT
169
+ Launch Altitude (m). Default 0m
170
+ --latitude LATITUDE Launch Latitude (dd.dddd)
171
+ --longitude LONGITUDE
172
+ Launch Longitude (dd.dddd)
173
+ --time TIME Launch Time (string, UTC). Default = Now
174
+ -o OUTPUT, --output OUTPUT
175
+ Output KML File. Default = prediction.kml
176
+ --altitude_deltas ALTITUDE_DELTAS
177
+ Comma-delimited list of altitude deltas. (metres).
178
+ --time_deltas TIME_DELTAS
179
+ Comma-delimited list of time deltas. (hours)
180
+ --absolute Show absolute altitudes for tracks and placemarks.
181
+ ```
182
+
183
+ For example, to predict a radiosonde launch from Adelaide Airport (5m/s ascent, 26km burst, 7.5m/s descent), but to look at what happens if the burst altitude is higher or lower than usual:
184
+ ```
185
+ $ python3 predict.py --latitude=-34.9499 --longitude=138.5194 -a 5 -d 7.5 -b 26000 --time "2018-01-27 11:15Z" --altitude_deltas="-2000,0,2000"
186
+ Running using GFS Model: gfs20180127-00z
187
+ 2018-01-27T11:15:00+00:00 5.0/24000.0/7.5 - Landing: -34.8585, 138.9600 at 2018-01-27T13:03:33
188
+ 2018-01-27T11:15:00+00:00 5.0/26000.0/7.5 - Landing: -34.8587, 138.8870 at 2018-01-27T13:11:01
189
+ 2018-01-27T11:15:00+00:00 5.0/28000.0/7.5 - Landing: -34.8598, 138.7990 at 2018-01-27T13:18:22
190
+ KML written to prediction.kml
191
+ ```
192
+
193
+ A few other example scripts are located in the 'apps' directory:
194
+ * basic_usage.py - Example showing how to write a predicted flight path out to a KML file
195
+ * sonde_predict.py - A more complex example, where predictions for the next week's of radiosonde flights are run and written to a KML file.
196
+
197
+
@@ -5,4 +5,4 @@
5
5
  # Copyright 2020 Mark Jessop <vk5qi@rfhead.net>
6
6
  #
7
7
 
8
- __version__ = "0.2.0"
8
+ __version__ = "0.3.0"
@@ -10,6 +10,7 @@
10
10
  # [ ] Use HTTP Range requests instead of using the GRIB filter.
11
11
  #
12
12
  import sys
13
+ import glob
13
14
  import os.path
14
15
  from os import remove
15
16
  import shutil
@@ -475,7 +476,8 @@ def main():
475
476
  _wind = parse_grib_to_dict(os.path.join(_temp_dir, 'temp.grib'))
476
477
  # Remove GRIB and index file.
477
478
  remove(os.path.join(_temp_dir, 'temp.grib'))
478
- remove(os.path.join(_temp_dir, 'temp.grib.90c91.idx'))
479
+ for _entry in glob.glob(os.path.join(_temp_dir, "*.idx")):
480
+ remove(_entry)
479
481
 
480
482
  if _wind is not None:
481
483
  (_filename, _text) = wind_dict_to_cusf(_wind, output_dir=_temp_dir)
@@ -10,6 +10,11 @@ import datetime
10
10
  import os.path
11
11
  from shapely.geometry import Point, LineString
12
12
 
13
+ try:
14
+ from fastkml.enums import AltitudeMode
15
+ except ImportError:
16
+ AltitudeMode = None
17
+
13
18
  def available_gfs(gfs_path='./gfs'):
14
19
  """ Determine the time extent of the GFS dataset """
15
20
 
@@ -30,8 +35,8 @@ def available_gfs(gfs_path='./gfs'):
30
35
 
31
36
  _timestamps.sort()
32
37
 
33
- start_time = datetime.datetime.utcfromtimestamp(_timestamps[0])
34
- end_time = datetime.datetime.utcfromtimestamp(_timestamps[-1])
38
+ start_time = datetime.datetime.fromtimestamp(_timestamps[0], datetime.timezone.utc)
39
+ end_time = datetime.datetime.fromtimestamp(_timestamps[-1], datetime.timezone.utc)
35
40
 
36
41
  return (start_time, end_time)
37
42
 
@@ -56,6 +61,38 @@ def gfs_model_age(gfs_path="./gfs"):
56
61
  # Geometry and KML related stuff
57
62
  ns = '{http://www.opengis.net/kml/2.2}'
58
63
 
64
+ def _kml_altitude_mode(altitude_mode):
65
+ """Return an altitude mode value compatible with the installed fastkml."""
66
+
67
+ if AltitudeMode is None or not isinstance(altitude_mode, str):
68
+ return altitude_mode
69
+
70
+ for mode in AltitudeMode:
71
+ if altitude_mode in (mode.name, mode.value):
72
+ return mode
73
+
74
+ return altitude_mode
75
+
76
+
77
+ def _kml_geometry(geometry, **kwargs):
78
+ """Create a fastkml geometry across old and new fastkml APIs."""
79
+
80
+ if hasattr(fastkml.geometry, "create_kml_geometry"):
81
+ if "altitude_mode" in kwargs:
82
+ kwargs["altitude_mode"] = _kml_altitude_mode(kwargs["altitude_mode"])
83
+ return fastkml.geometry.create_kml_geometry(geometry, ns=ns, **kwargs)
84
+
85
+ return fastkml.geometry.Geometry(ns=ns, geometry=geometry, **kwargs)
86
+
87
+
88
+ def _set_placemark_geometry(placemark, geometry, **kwargs):
89
+ kml_geometry = _kml_geometry(geometry, **kwargs)
90
+
91
+ if hasattr(fastkml.geometry, "create_kml_geometry"):
92
+ placemark.kml_geometry = kml_geometry
93
+ else:
94
+ placemark.geometry = kml_geometry
95
+
59
96
  def flight_path_to_linestring(flight_path):
60
97
  ''' Convert a predicted flight path to a LineString geometry object '''
61
98
 
@@ -105,9 +142,9 @@ def flight_path_to_geometry(flight_path,
105
142
  name=comment,
106
143
  styles=[flight_track_style])
107
144
 
108
- flight_line.geometry = fastkml.geometry.Geometry(
109
- ns=ns,
110
- geometry=flight_path_to_linestring(flight_path),
145
+ _set_placemark_geometry(
146
+ flight_line,
147
+ flight_path_to_linestring(flight_path),
111
148
  altitude_mode=altitude_mode,
112
149
  extrude=True,
113
150
  tessellate=True)
@@ -136,9 +173,9 @@ def flight_path_landing_placemark(flight_path,
136
173
  description="",
137
174
  styles=[flight_style])
138
175
 
139
- flight_placemark.geometry = fastkml.geometry.Geometry(
140
- ns=ns,
141
- geometry=Point(flight_path[-1][2], flight_path[-1][1], flight_path[-1][3]),
176
+ _set_placemark_geometry(
177
+ flight_placemark,
178
+ Point(flight_path[-1][2], flight_path[-1][1], flight_path[-1][3]),
142
179
  altitude_mode='clampToGround')
143
180
 
144
181
  return flight_placemark
@@ -175,9 +212,9 @@ def flight_path_burst_placemark(flight_path,
175
212
  current_index = i
176
213
 
177
214
 
178
- flight_placemark.geometry = fastkml.geometry.Geometry(
179
- ns=ns,
180
- geometry=Point(flight_path[current_index][2], flight_path[current_index][1], flight_path[current_index][3]),
215
+ _set_placemark_geometry(
216
+ flight_placemark,
217
+ Point(flight_path[current_index][2], flight_path[current_index][1], flight_path[current_index][3]),
181
218
  altitude_mode=altitude_mode)
182
219
 
183
220
  return flight_placemark
@@ -200,8 +237,10 @@ def write_flight_path_kml(flight_data,
200
237
  for _flight in flight_data:
201
238
  kml_doc.append(_flight)
202
239
 
240
+ kml_root.append(kml_doc)
241
+
203
242
  with open(filename,'w') as kml_file:
204
- kml_str = kml_doc.to_string()
243
+ kml_str = kml_root.to_string()
205
244
  if kml_hack:
206
245
  kml_str = kml_str.replace('kml:','').replace(':kml','')
207
246
  kml_file.write(kml_str)
@@ -0,0 +1,222 @@
1
+ Metadata-Version: 2.4
2
+ Name: cusfpredict
3
+ Version: 0.3.0
4
+ Summary: Python Wrapper for the CUSF High-Altitude Balloon Predictor
5
+ Keywords: horus balloon prediction gfs
6
+ Classifier: Intended Audience :: Developers
7
+ Classifier: Programming Language :: Python :: 3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: fastkml
11
+ Requires-Dist: requests
12
+ Requires-Dist: numpy
13
+ Requires-Dist: pytz
14
+ Requires-Dist: shapely
15
+ Requires-Dist: python-dateutil
16
+ Requires-Dist: xarray
17
+ Requires-Dist: cfgrib>=0.9.8
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: keywords
22
+ Dynamic: license-file
23
+ Dynamic: requires-dist
24
+ Dynamic: summary
25
+
26
+ # CUSF Standalone Predictor - Python Wrapper
27
+ This is a semi-fork of the [CUSF Standalone Predictor](https://github.com/jonsowman/cusf-standalone-predictor/), which provides a Python wrapper around the predictor binary, and provides a means of gathering the requisite wind data.
28
+
29
+ 2018-02 Update: Wind downloader updated to use the [NOMADS GRIB Filter](http://nomads.ncep.noaa.gov/txt_descriptions/grib_filter_doc.shtml), as the OpenDAP interface stopped working. As such, we no longer require PyDAP, but we do now require GDAL to read in the GRIB2 files.
30
+
31
+ 2021-03 Update: We have dropped GDAL in favour of cfgrib.
32
+
33
+ 2026-06 Update: A range of updates for support of newer libraries (e.g. numpy, fastkml), and better packaging. The minimum supported Python version is now 3.9.
34
+
35
+ ## 1. System Dependencies
36
+ The Python package installs its Python dependencies automatically. You still need the system libraries used by the wind-data reader and the standalone C predictor.
37
+
38
+ On a Raspbian/Ubuntu/Debian system, you can get most of the required dependencies using:
39
+ ```
40
+ $ sudo apt-get install git cmake build-essential libglib2.0-dev python3-numpy python3-requests python3-dateutil python3-pip libeccodes-data libeccodes0 libgeos-dev libatlas-base-dev
41
+ ```
42
+
43
+ On macOS with Homebrew:
44
+ ```
45
+ $ brew install cmake glib eccodes geos
46
+ ```
47
+
48
+ ## 2. Install the Python Wrapper
49
+
50
+ ### From PyPI
51
+ Install into a virtual environment:
52
+ ```
53
+ $ python3 -m venv venv
54
+ $ source venv/bin/activate
55
+ $ python -m pip install --upgrade pip
56
+ $ python -m pip install cusfpredict
57
+ ```
58
+
59
+ Python 2 is not supported.
60
+
61
+ ### From Source, Editable Mode (Only required if you are experimenting with changes in this library)
62
+ For development, clone this repository and install it in editable mode:
63
+ ```
64
+ $ git clone https://github.com/darksidelemm/cusf_predictor_wrapper.git
65
+ $ cd cusf_predictor_wrapper
66
+ $ python3 -m venv venv
67
+ $ source venv/bin/activate
68
+ $ python -m pip install --upgrade pip
69
+ $ python -m pip install -e .
70
+ ```
71
+
72
+ If you only want to install the source checkout without editable mode, use:
73
+ ```
74
+ $ python -m pip install .
75
+ ```
76
+
77
+ If Python dependency installation fails, the same dependencies are listed in `requirements.txt` and can be installed explicitly:
78
+ ```
79
+ $ python -m pip install -r requirements.txt
80
+ ```
81
+
82
+ The package depends on Shapely and ecCodes-backed GRIB readers. If installation fails with missing headers or missing shared libraries, install the system dependencies above and retry.
83
+
84
+
85
+ ## 3. Building the Predictor Binary
86
+ The predictor itself is a binary (`pred`), which is built separately using CMake. The Python package does not currently build or install this binary for you.
87
+
88
+ From within the cusf_predictor_wrapper directory, run the following to build the predictor binary:
89
+
90
+ ```
91
+ $ cmake -S src -B src/build
92
+ $ cmake --build src/build
93
+ ```
94
+
95
+ The `pred` binary then needs to be copied somewhere useful. For the example scripts in `apps`, copy it into that directory:
96
+ ```
97
+ $ cp src/build/pred apps/
98
+ ```
99
+
100
+ If you are building this utility for use with chasemapper, then you should copy `pred` into the chasemapper directory:
101
+ ```
102
+ $ cp src/build/pred ~/chasemapper/
103
+ ```
104
+
105
+ A pre-compiled Windows binary of the predictor is available here: http://rfhead.net/horus/cusf_standalone_predictor.zip
106
+ Use at your own risk!
107
+
108
+
109
+ ## 4. Getting Wind Data
110
+ The predictor binary uses a custom wind data format, extracted from NOAA's Global Forecast System wind models. The `cusfpredict.gfs` Python module pulls down and formats the relevant data from NOAA's [NOMADS](http://nomads.ncep.noaa.gov) server.
111
+
112
+ If you are using this library with ChaseMapper, you will need to adjust the download command in the [chasemapper configuration file](https://github.com/projecthorus/chasemapper/blob/master/horusmapper.cfg.example#L135).
113
+
114
+ An example of running it is as follows:
115
+ ```
116
+ $ python3 -m cusfpredict.gfs --lat=-33 --lon=139 --latdelta=10 --londelta=10 -f 24 -m 0p50 -o gfs
117
+ ```
118
+
119
+ The command line arguments are as follows:
120
+ ```
121
+ Area of interest:
122
+ --lat Latitude (Decimal Degrees) of the centre of the area to gather.
123
+ --lon Longitude (Decimal Degrees) of the centre of the area to gather.
124
+ --latdelta Gather data from lat+/-latdelta
125
+ --londelta Gather data from lon+/-londelta
126
+
127
+ Time of interest:
128
+ -f X Gather data up to X hours into the future, from the start of the most recent model. (Note that this can be up to 8 hours in the past.) Make sure you get enough for the flight!
129
+
130
+ GFS Model Choice:
131
+ -m <model> Choose between either:
132
+ 0p50 - 0.5 Degree Spatial, 3-hour Time Resolution
133
+ 0p25_1hr - 0.25 Degree Spatial, 1-hour Time Resolution (default)
134
+
135
+ Other settings:
136
+ -v Verbose output
137
+ -o output_dir (Where to save the gfs data to, defaults to ./gfs/)
138
+ ```
139
+
140
+ The higher resolution wind model you choose, the larger the amount of data to download, and the longer it will take. It also increases the prediction calculation time (though not significantly).
141
+
142
+ `wind_grabber.sh` is an example script to automatically grab wind data first to a temporary directory, and then to the final gfs directory. This could be run from a cronjob to keep the wind data up-to-date.
143
+
144
+ New wind models become available approximately every 6 hours, approximately 4 hours after the model's nominal time (i.e. the 00Z model becomes available around 04Z). Information on the status of the GFS model generation is available here: http://www.nco.ncep.noaa.gov/pmb/nwprod/prodstat_new/
145
+
146
+ ## 5. Using the Predictor
147
+ (Note: This section is intended for users within to run predictions from within their own software. If you are just installing this library for use with chasemapper, you can skip all of this!)
148
+
149
+ The basic usage of the predictor from within Python is as follows:
150
+ ```
151
+ import datetime
152
+ from cusfpredict.predict import Predictor
153
+
154
+ pred = Predictor(bin_path='./pred', gfs_path='./gfs')
155
+
156
+ flight_path = pred.predict(
157
+ launch_lat=-34.9499,
158
+ launch_lon=138.5194,
159
+ launch_alt=0.0,
160
+ ascent_rate=5.0,
161
+ descent_rate=5.0,
162
+ burst_alt=30000,
163
+ launch_time=datetime.datetime.now(datetime.timezone.utc)
164
+ )
165
+
166
+ ```
167
+
168
+ Note that the launch time is a datetime object interpreted as UTC, so make sure you convert your launch time as appropriate.
169
+
170
+ The output is a list-of-lists, containing entries of [utctimestamp, lat, lon, alt], i.e.:
171
+
172
+ ```
173
+ >>> flight_path
174
+ [[1516702953, -34.9471, 138.517, 250.0], [1516703003, -34.9436, 138.514, 500.0], <etc>, [1516703053, -34.9415, 138.513, 750.0]]
175
+ ```
176
+
177
+ There is also a command-line utility, `predict.py`, which allows performing predictions with launch parameter variations:
178
+ ```
179
+ usage: predict.py [-h] [-a ASCENTRATE] [-d DESCENTRATE] [-b BURSTALT]
180
+ [--launchalt LAUNCHALT] [--latitude LATITUDE]
181
+ [--longitude LONGITUDE] [--time TIME] [-o OUTPUT]
182
+ [--altitude_deltas ALTITUDE_DELTAS]
183
+ [--time_deltas TIME_DELTAS] [--absolute]
184
+
185
+ optional arguments:
186
+ -h, --help show this help message and exit
187
+ -a ASCENTRATE, --ascentrate ASCENTRATE
188
+ Ascent Rate (m/s). Default 5m/s
189
+ -d DESCENTRATE, --descentrate DESCENTRATE
190
+ Descent Rate (m/s). Default 5m/s
191
+ -b BURSTALT, --burstalt BURSTALT
192
+ Burst Altitude (m). Default 30000m
193
+ --launchalt LAUNCHALT
194
+ Launch Altitude (m). Default 0m
195
+ --latitude LATITUDE Launch Latitude (dd.dddd)
196
+ --longitude LONGITUDE
197
+ Launch Longitude (dd.dddd)
198
+ --time TIME Launch Time (string, UTC). Default = Now
199
+ -o OUTPUT, --output OUTPUT
200
+ Output KML File. Default = prediction.kml
201
+ --altitude_deltas ALTITUDE_DELTAS
202
+ Comma-delimited list of altitude deltas. (metres).
203
+ --time_deltas TIME_DELTAS
204
+ Comma-delimited list of time deltas. (hours)
205
+ --absolute Show absolute altitudes for tracks and placemarks.
206
+ ```
207
+
208
+ For example, to predict a radiosonde launch from Adelaide Airport (5m/s ascent, 26km burst, 7.5m/s descent), but to look at what happens if the burst altitude is higher or lower than usual:
209
+ ```
210
+ $ python3 predict.py --latitude=-34.9499 --longitude=138.5194 -a 5 -d 7.5 -b 26000 --time "2018-01-27 11:15Z" --altitude_deltas="-2000,0,2000"
211
+ Running using GFS Model: gfs20180127-00z
212
+ 2018-01-27T11:15:00+00:00 5.0/24000.0/7.5 - Landing: -34.8585, 138.9600 at 2018-01-27T13:03:33
213
+ 2018-01-27T11:15:00+00:00 5.0/26000.0/7.5 - Landing: -34.8587, 138.8870 at 2018-01-27T13:11:01
214
+ 2018-01-27T11:15:00+00:00 5.0/28000.0/7.5 - Landing: -34.8598, 138.7990 at 2018-01-27T13:18:22
215
+ KML written to prediction.kml
216
+ ```
217
+
218
+ A few other example scripts are located in the 'apps' directory:
219
+ * basic_usage.py - Example showing how to write a predicted flight path out to a KML file
220
+ * sonde_predict.py - A more complex example, where predictions for the next week's of radiosonde flights are run and written to a KML file.
221
+
222
+
@@ -0,0 +1,20 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ setup.py
6
+ ./cusfpredict/__init__.py
7
+ ./cusfpredict/gfs.py
8
+ ./cusfpredict/predict.py
9
+ ./cusfpredict/reader.py
10
+ ./cusfpredict/utils.py
11
+ cusfpredict/__init__.py
12
+ cusfpredict/gfs.py
13
+ cusfpredict/predict.py
14
+ cusfpredict/reader.py
15
+ cusfpredict/utils.py
16
+ cusfpredict.egg-info/PKG-INFO
17
+ cusfpredict.egg-info/SOURCES.txt
18
+ cusfpredict.egg-info/dependency_links.txt
19
+ cusfpredict.egg-info/requires.txt
20
+ cusfpredict.egg-info/top_level.txt
@@ -0,0 +1,8 @@
1
+ fastkml
2
+ requests
3
+ numpy
4
+ pytz
5
+ shapely
6
+ python-dateutil
7
+ xarray
8
+ cfgrib>=0.9.8
@@ -0,0 +1 @@
1
+ cusfpredict
@@ -0,0 +1,20 @@
1
+ [tool.poetry]
2
+ name = "cusfpredict"
3
+ version = "0.3.0"
4
+ description = "CUSF Predictor Wrapper & GFS Downloader"
5
+ authors = ["Mark Jessop"]
6
+
7
+ [tool.poetry.dependencies]
8
+ python = "^3.9" # last actively maintained version of Python
9
+ fastkml = "^1"
10
+ requests = "^2.25.1"
11
+ numpy = "^2.0.0"
12
+ pytz = "^2025.1"
13
+ shapely = "^2.0.0"
14
+ python-dateutil = "^2.8.1"
15
+ xarray = "*"
16
+ cfgrib = "^0.9.8"
17
+
18
+ [build-system]
19
+ requires = ["setuptools>=64", "wheel"]
20
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,44 @@
1
+ import os
2
+ import re
3
+ from setuptools import setup, find_packages
4
+
5
+ regexp = re.compile(r".*__version__ = [\'\"](.*?)[\'\"]", re.S)
6
+
7
+ init_file = os.path.join(os.path.dirname(__file__), "cusfpredict", "__init__.py")
8
+ with open(init_file, "r") as f:
9
+ module_content = f.read()
10
+ match = regexp.match(module_content)
11
+ if match:
12
+ version = match.group(1)
13
+ else:
14
+ raise RuntimeError(f"Cannot find __version__ in {init_file}")
15
+
16
+
17
+ with open("README.md", "r") as f:
18
+ readme = f.read()
19
+
20
+
21
+ with open("requirements.txt", "r") as f:
22
+ requirements = []
23
+ for line in f.read().split("\n"):
24
+ line = line.strip()
25
+ if line and not line.startswith("#"):
26
+ requirements.append(line)
27
+
28
+
29
+ if __name__ == "__main__":
30
+ setup(
31
+ name="cusfpredict",
32
+ description="Python Wrapper for the CUSF High-Altitude Balloon Predictor",
33
+ long_description=readme,
34
+ long_description_content_type="text/markdown",
35
+ version=version,
36
+ install_requires=requirements,
37
+ keywords=["horus balloon prediction gfs"],
38
+ package_dir={"": "."},
39
+ packages=find_packages("."),
40
+ classifiers=[
41
+ "Intended Audience :: Developers",
42
+ "Programming Language :: Python :: 3.9"
43
+ ]
44
+ )
@@ -1,18 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: cusfpredict
3
- Version: 0.2.0
4
- Summary: CUSF Predictor Wrapper & GFS Downloader
5
- Author: Mark Jessop
6
- Requires-Python: >=3.6,<4.0
7
- Classifier: Programming Language :: Python :: 3
8
- Classifier: Programming Language :: Python :: 3.6
9
- Classifier: Programming Language :: Python :: 3.7
10
- Classifier: Programming Language :: Python :: 3.8
11
- Requires-Dist: cfgrib (>=0.9.8,<0.10.0)
12
- Requires-Dist: fastkml (>=0.11,<0.12)
13
- Requires-Dist: numpy (>=1.17,<2.0)
14
- Requires-Dist: python-dateutil (>=2.8.1,<3.0.0)
15
- Requires-Dist: pytz (>=2020.5,<2021.0)
16
- Requires-Dist: requests (>=2.25.1,<3.0.0)
17
- Requires-Dist: shapely (>=1.7.1,<2.0.0)
18
- Requires-Dist: xarray (>=0.16.2,<0.17.0)
@@ -1,22 +0,0 @@
1
- [tool.poetry]
2
- name = "cusfpredict"
3
- version = "0.2.0"
4
- description = "CUSF Predictor Wrapper & GFS Downloader"
5
- authors = ["Mark Jessop"]
6
-
7
- [tool.poetry.dependencies]
8
- python = "^3.6"
9
- fastkml = "^0.11"
10
- requests = "^2.25.1"
11
- numpy = "^1.17"
12
- pytz = "^2020.5"
13
- shapely = "^1.7.1"
14
- python-dateutil = "^2.8.1"
15
- xarray = "^0.16.2"
16
- cfgrib = "^0.9.8"
17
-
18
- [tool.poetry.dev-dependencies]
19
-
20
- [build-system]
21
- requires = ["setuptools","poetry>=0.12"]
22
- build-backend = "poetry.masonry.api"
@@ -1,37 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from setuptools import setup
3
-
4
- packages = \
5
- ['cusfpredict']
6
-
7
- package_data = \
8
- {'': ['*']}
9
-
10
- install_requires = \
11
- ['cfgrib>=0.9.8,<0.10.0',
12
- 'fastkml>=0.11,<0.12',
13
- 'numpy>=1.17,<2.0',
14
- 'python-dateutil>=2.8.1,<3.0.0',
15
- 'pytz>=2020.5,<2021.0',
16
- 'requests>=2.25.1,<3.0.0',
17
- 'shapely>=1.7.1,<2.0.0',
18
- 'xarray>=0.16.2,<0.17.0']
19
-
20
- setup_kwargs = {
21
- 'name': 'cusfpredict',
22
- 'version': '0.2.0',
23
- 'description': 'CUSF Predictor Wrapper & GFS Downloader',
24
- 'long_description': None,
25
- 'author': 'Mark Jessop',
26
- 'author_email': None,
27
- 'maintainer': None,
28
- 'maintainer_email': None,
29
- 'url': None,
30
- 'packages': packages,
31
- 'package_data': package_data,
32
- 'install_requires': install_requires,
33
- 'python_requires': '>=3.6,<4.0',
34
- }
35
-
36
-
37
- setup(**setup_kwargs)
File without changes