pycontrails 0.56.0__cp310-cp310-win_amd64.whl → 0.57.0__cp310-cp310-win_amd64.whl
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.
Potentially problematic release.
This version of pycontrails might be problematic. Click here for more details.
- pycontrails/_version.py +3 -3
- pycontrails/core/rgi_cython.cp310-win_amd64.pyd +0 -0
- pycontrails/datalib/geo_utils.py +261 -0
- pycontrails/datalib/gfs/gfs.py +58 -64
- pycontrails/datalib/goes.py +193 -399
- pycontrails/datalib/himawari/__init__.py +27 -0
- pycontrails/datalib/himawari/header_struct.py +266 -0
- pycontrails/datalib/himawari/himawari.py +654 -0
- {pycontrails-0.56.0.dist-info → pycontrails-0.57.0.dist-info}/METADATA +2 -2
- {pycontrails-0.56.0.dist-info → pycontrails-0.57.0.dist-info}/RECORD +14 -10
- {pycontrails-0.56.0.dist-info → pycontrails-0.57.0.dist-info}/WHEEL +0 -0
- {pycontrails-0.56.0.dist-info → pycontrails-0.57.0.dist-info}/licenses/LICENSE +0 -0
- {pycontrails-0.56.0.dist-info → pycontrails-0.57.0.dist-info}/licenses/NOTICE +0 -0
- {pycontrails-0.56.0.dist-info → pycontrails-0.57.0.dist-info}/top_level.txt +0 -0
pycontrails/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.57.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 57, 0)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g7b8b60b87'
|
|
Binary file
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Tooling and support for GEO satellites."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import numpy.typing as npt
|
|
5
|
+
import xarray as xr
|
|
6
|
+
|
|
7
|
+
from pycontrails.utils import dependencies
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parallax_correct(
|
|
11
|
+
longitude: npt.NDArray[np.floating],
|
|
12
|
+
latitude: npt.NDArray[np.floating],
|
|
13
|
+
altitude: npt.NDArray[np.floating],
|
|
14
|
+
goes_da: xr.DataArray,
|
|
15
|
+
) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
|
|
16
|
+
r"""Apply parallax correction to WGS84 geodetic coordinates based on satellite perspective.
|
|
17
|
+
|
|
18
|
+
This function considers the ray from the satellite to the points of interest and finds
|
|
19
|
+
the intersection of this ray with the WGS84 ellipsoid. The intersection point is then
|
|
20
|
+
returned as the corrected longitude and latitude coordinates.
|
|
21
|
+
|
|
22
|
+
::
|
|
23
|
+
|
|
24
|
+
@ satellite
|
|
25
|
+
\
|
|
26
|
+
\
|
|
27
|
+
\
|
|
28
|
+
\
|
|
29
|
+
\
|
|
30
|
+
* aircraft
|
|
31
|
+
\
|
|
32
|
+
\
|
|
33
|
+
x parallax corrected aircraft
|
|
34
|
+
------------------------- surface
|
|
35
|
+
|
|
36
|
+
If the point of interest is not visible from the satellite (ie, on the opposite side of the
|
|
37
|
+
earth), the function returns nan for the corrected coordinates.
|
|
38
|
+
|
|
39
|
+
This function requires the :mod:`pyproj` package to be installed.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
longitude : npt.NDArray[np.floating]
|
|
44
|
+
A 1D array of longitudes in degrees.
|
|
45
|
+
latitude : npt.NDArray[np.floating]
|
|
46
|
+
A 1D array of latitudes in degrees.
|
|
47
|
+
altitude : npt.NDArray[np.floating]
|
|
48
|
+
A 1D array of altitudes in meters.
|
|
49
|
+
goes_da : xr.DataArray
|
|
50
|
+
DataArray containing the GOES projection information. Only the ``goes_imager_projection``
|
|
51
|
+
field of the :attr:`xr.DataArray.attrs` is used.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]
|
|
56
|
+
A tuple containing the corrected longitude and latitude coordinates.
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
goes_imager_projection = goes_da.attrs["goes_imager_projection"]
|
|
60
|
+
sat_lon = goes_imager_projection["longitude_of_projection_origin"]
|
|
61
|
+
sat_lat = goes_imager_projection["latitude_of_projection_origin"]
|
|
62
|
+
sat_alt = goes_imager_projection["perspective_point_height"]
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
import pyproj
|
|
66
|
+
except ModuleNotFoundError as exc:
|
|
67
|
+
dependencies.raise_module_not_found_error(
|
|
68
|
+
name="parallax_correct function",
|
|
69
|
+
package_name="pyproj",
|
|
70
|
+
module_not_found_error=exc,
|
|
71
|
+
pycontrails_optional_package="pyproj",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Convert from WGS84 to ECEF coordinates
|
|
75
|
+
ecef_crs = pyproj.CRS("EPSG:4978")
|
|
76
|
+
wgs84_crs = pyproj.CRS("EPSG:4326")
|
|
77
|
+
transformer = pyproj.Transformer.from_crs(wgs84_crs, ecef_crs, always_xy=True)
|
|
78
|
+
|
|
79
|
+
p0 = np.array(transformer.transform([sat_lon], [sat_lat], [sat_alt]))
|
|
80
|
+
p1 = np.array(transformer.transform(longitude, latitude, altitude))
|
|
81
|
+
|
|
82
|
+
# Major and minor axes of the ellipsoid
|
|
83
|
+
a = ecef_crs.ellipsoid.semi_major_metre # type: ignore[union-attr]
|
|
84
|
+
b = ecef_crs.ellipsoid.semi_minor_metre # type: ignore[union-attr]
|
|
85
|
+
intersection = _intersection_with_ellipsoid(p0, p1, a, b)
|
|
86
|
+
|
|
87
|
+
# Convert back to WGS84 coordinates
|
|
88
|
+
inv_transformer = pyproj.Transformer.from_crs(ecef_crs, wgs84_crs, always_xy=True)
|
|
89
|
+
return inv_transformer.transform(*intersection)[:2] # final coord is (close to) 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _intersection_with_ellipsoid(
|
|
93
|
+
p0: npt.NDArray[np.floating],
|
|
94
|
+
p1: npt.NDArray[np.floating],
|
|
95
|
+
a: float,
|
|
96
|
+
b: float,
|
|
97
|
+
) -> npt.NDArray[np.floating]:
|
|
98
|
+
"""Find the intersection of a line with the surface of an ellipsoid."""
|
|
99
|
+
# Calculate the direction vector
|
|
100
|
+
px, py, pz = p0
|
|
101
|
+
v = p1 - p0
|
|
102
|
+
vx, vy, vz = v
|
|
103
|
+
|
|
104
|
+
# The line between p0 and p1 in parametric form is p(t) = p0 + t * v
|
|
105
|
+
# We need to find t such that p(t) lies on the ellipsoid
|
|
106
|
+
# x^2 / a^2 + y^2 / a^2 + z^2 / b^2 = 1
|
|
107
|
+
# (px + t * vx)^2 / a^2 + (py + t * vy)^2 / a^2 + (pz + t * vz)^2 / b^2 = 1
|
|
108
|
+
# Rearranging gives a quadratic in t
|
|
109
|
+
|
|
110
|
+
# Calculate the coefficients of this quadratic equation
|
|
111
|
+
A = vx**2 / a**2 + vy**2 / a**2 + vz**2 / b**2
|
|
112
|
+
B = 2 * (px * vx / a**2 + py * vy / a**2 + pz * vz / b**2)
|
|
113
|
+
C = px**2 / a**2 + py**2 / a**2 + pz**2 / b**2 - 1.0
|
|
114
|
+
|
|
115
|
+
# Calculate the discriminant
|
|
116
|
+
D = B**2 - 4 * A * C
|
|
117
|
+
sqrtD = np.sqrt(D, where=D >= 0, out=np.full_like(D, np.nan))
|
|
118
|
+
|
|
119
|
+
# Calculate the two possible solutions for t
|
|
120
|
+
t0 = (-B + sqrtD) / (2.0 * A)
|
|
121
|
+
t1 = (-B - sqrtD) / (2.0 * A)
|
|
122
|
+
|
|
123
|
+
# Calculate the intersection points
|
|
124
|
+
intersection0 = p0 + t0 * v
|
|
125
|
+
intersection1 = p0 + t1 * v
|
|
126
|
+
|
|
127
|
+
# Pick the intersection point that is closer to the aircraft (p1)
|
|
128
|
+
d0 = np.linalg.norm(intersection0 - p1, axis=0)
|
|
129
|
+
d1 = np.linalg.norm(intersection1 - p1, axis=0)
|
|
130
|
+
out = np.where(d0 < d1, intersection0, intersection1)
|
|
131
|
+
|
|
132
|
+
# Fill the points in which the aircraft is not visible by the satellite with nan
|
|
133
|
+
# This occurs when the earth is between the satellite and the aircraft
|
|
134
|
+
# In other words, we can check for t0 < 1 (or t1 < 1)
|
|
135
|
+
opposite_side = t0 < 1.0
|
|
136
|
+
out[:, opposite_side] = np.nan
|
|
137
|
+
|
|
138
|
+
return out
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def to_ash(da: xr.DataArray, convention: str = "SEVIRI") -> npt.NDArray[np.float32]:
|
|
142
|
+
"""Compute 3d RGB array for the ASH color scheme.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
da : xr.DataArray
|
|
147
|
+
DataArray of GOES data with appropriate bands.
|
|
148
|
+
convention : str, optional
|
|
149
|
+
Convention for color space.
|
|
150
|
+
|
|
151
|
+
- SEVIRI convention requires bands C11, C14, C15.
|
|
152
|
+
Used in :cite:`kulikSatellitebasedDetectionContrails2019`.
|
|
153
|
+
- Standard convention requires bands C11, C13, C14, C15
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
npt.NDArray[np.float32]
|
|
158
|
+
3d RGB array with ASH color scheme according to convention.
|
|
159
|
+
|
|
160
|
+
References
|
|
161
|
+
----------
|
|
162
|
+
- `Ash RGB quick guide (the color space and color interpretations) <https://rammb.cira.colostate.edu/training/visit/quick_guides/GOES_Ash_RGB.pdf>`_
|
|
163
|
+
- :cite:`SEVIRIRGBCal`
|
|
164
|
+
- :cite:`kulikSatellitebasedDetectionContrails2019`
|
|
165
|
+
|
|
166
|
+
Examples
|
|
167
|
+
--------
|
|
168
|
+
>>> from pycontrails.datalib.goes import GOES
|
|
169
|
+
>>> goes = GOES(region="M2", bands=("C11", "C14", "C15"))
|
|
170
|
+
>>> da = goes.get("2022-10-03 04:34:00")
|
|
171
|
+
>>> rgb = to_ash(da)
|
|
172
|
+
>>> rgb.shape
|
|
173
|
+
(500, 500, 3)
|
|
174
|
+
|
|
175
|
+
>>> rgb[0, 0, :]
|
|
176
|
+
array([0.0127004 , 0.22793579, 0.3930847 ], dtype=float32)
|
|
177
|
+
"""
|
|
178
|
+
if convention == "standard":
|
|
179
|
+
if not np.all(np.isin([11, 13, 14, 15], da["band_id"])):
|
|
180
|
+
msg = "DataArray must contain bands 11, 13, 14, and 15 for standard ash"
|
|
181
|
+
raise ValueError(msg)
|
|
182
|
+
c11 = da.sel(band_id=11).values # 8.44
|
|
183
|
+
c13 = da.sel(band_id=13).values # 10.33
|
|
184
|
+
c14 = da.sel(band_id=14).values # 11.19
|
|
185
|
+
c15 = da.sel(band_id=15).values # 12.27
|
|
186
|
+
|
|
187
|
+
red = c15 - c13
|
|
188
|
+
green = c14 - c11
|
|
189
|
+
blue = c13
|
|
190
|
+
|
|
191
|
+
elif convention in ("SEVIRI", "MIT"): # retain MIT for backwards compatibility
|
|
192
|
+
if not np.all(np.isin([11, 14, 15], da["band_id"])):
|
|
193
|
+
msg = "DataArray must contain bands 11, 14, and 15 for SEVIRI ash"
|
|
194
|
+
raise ValueError(msg)
|
|
195
|
+
c11 = da.sel(band_id=11).values # 8.44
|
|
196
|
+
c14 = da.sel(band_id=14).values # 11.19
|
|
197
|
+
c15 = da.sel(band_id=15).values # 12.27
|
|
198
|
+
|
|
199
|
+
red = c15 - c14
|
|
200
|
+
green = c14 - c11
|
|
201
|
+
blue = c14
|
|
202
|
+
|
|
203
|
+
else:
|
|
204
|
+
raise ValueError("Convention must be either 'SEVIRI' or 'standard'")
|
|
205
|
+
|
|
206
|
+
# See colostate pdf for slightly wider values
|
|
207
|
+
red = _clip_and_scale(red, -4.0, 2.0)
|
|
208
|
+
green = _clip_and_scale(green, -4.0, 5.0)
|
|
209
|
+
blue = _clip_and_scale(blue, 243.0, 303.0)
|
|
210
|
+
return np.dstack([red, green, blue])
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _clip_and_scale(
|
|
214
|
+
arr: npt.NDArray[np.floating], low: float, high: float
|
|
215
|
+
) -> npt.NDArray[np.floating]:
|
|
216
|
+
"""Clip array and rescale to the interval [0, 1].
|
|
217
|
+
|
|
218
|
+
Array is first clipped to the interval [low, high] and then linearly rescaled
|
|
219
|
+
to the interval [0, 1] so that::
|
|
220
|
+
|
|
221
|
+
low -> 0
|
|
222
|
+
high -> 1
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
arr : npt.NDArray[np.floating]
|
|
227
|
+
Array to clip and scale.
|
|
228
|
+
low : float
|
|
229
|
+
Lower clipping bound.
|
|
230
|
+
high : float
|
|
231
|
+
Upper clipping bound.
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
npt.NDArray[np.floating]
|
|
236
|
+
Clipped and scaled array.
|
|
237
|
+
"""
|
|
238
|
+
return (arr.clip(low, high) - low) / (high - low)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _coarsen_then_concat(da1: xr.DataArray, da2: xr.DataArray) -> xr.DataArray:
|
|
242
|
+
"""Concatenate two DataArrays, averaging da2 to da1's resolution.
|
|
243
|
+
|
|
244
|
+
This function is hacky and should not be used publicly. It is used in goes.py
|
|
245
|
+
and himawari.py to combine data from different resolutions.
|
|
246
|
+
|
|
247
|
+
The function assumes that da2 has exactly twice the resolution of da1 in both
|
|
248
|
+
the x and y dimensions.
|
|
249
|
+
"""
|
|
250
|
+
da2 = da2.coarsen(x=2, y=2, boundary="exact").mean() # type: ignore[attr-defined]
|
|
251
|
+
|
|
252
|
+
# Gut check
|
|
253
|
+
np.testing.assert_allclose(da1["x"], da2["x"], atol=2e-5)
|
|
254
|
+
np.testing.assert_allclose(da1["y"], da2["y"], atol=2e-5)
|
|
255
|
+
|
|
256
|
+
# Assign the coarser coords to the coarsened coords to account for any small differences
|
|
257
|
+
da2["x"] = da1["x"]
|
|
258
|
+
da2["y"] = da1["y"]
|
|
259
|
+
|
|
260
|
+
# Finally, combine the datasets
|
|
261
|
+
return xr.concat([da1, da2], dim="band_id", coords="different", compat="equals")
|
pycontrails/datalib/gfs/gfs.py
CHANGED
|
@@ -15,7 +15,6 @@ import logging
|
|
|
15
15
|
import pathlib
|
|
16
16
|
import sys
|
|
17
17
|
import warnings
|
|
18
|
-
from collections.abc import Callable
|
|
19
18
|
from datetime import datetime
|
|
20
19
|
from typing import TYPE_CHECKING, Any
|
|
21
20
|
|
|
@@ -41,9 +40,8 @@ from pycontrails.datalib.gfs.variables import (
|
|
|
41
40
|
from pycontrails.utils import dependencies, temp
|
|
42
41
|
from pycontrails.utils.types import DatetimeLike
|
|
43
42
|
|
|
44
|
-
# optional imports
|
|
45
43
|
if TYPE_CHECKING:
|
|
46
|
-
import
|
|
44
|
+
import s3fs
|
|
47
45
|
|
|
48
46
|
logger = logging.getLogger(__name__)
|
|
49
47
|
|
|
@@ -125,10 +123,10 @@ class GFSForecast(metsource.MetDataSource):
|
|
|
125
123
|
- `GFS Documentation <https://www.emc.ncep.noaa.gov/emc/pages/numerical_forecast_systems/gfs/documentation.php>`_
|
|
126
124
|
"""
|
|
127
125
|
|
|
128
|
-
__slots__ = ("cache_download", "cachestore", "
|
|
126
|
+
__slots__ = ("cache_download", "cachestore", "forecast_time", "fs", "grid", "show_progress")
|
|
129
127
|
|
|
130
|
-
#:
|
|
131
|
-
|
|
128
|
+
#: s3fs filesystem for anonymous access to GFS bucket
|
|
129
|
+
fs: s3fs.S3FileSystem | None
|
|
132
130
|
|
|
133
131
|
#: Lat / Lon grid spacing. One of [0.25, 0.5, 1]
|
|
134
132
|
grid: float
|
|
@@ -153,26 +151,6 @@ class GFSForecast(metsource.MetDataSource):
|
|
|
153
151
|
show_progress: bool = False,
|
|
154
152
|
cache_download: bool = False,
|
|
155
153
|
) -> None:
|
|
156
|
-
try:
|
|
157
|
-
import boto3
|
|
158
|
-
except ModuleNotFoundError as e:
|
|
159
|
-
dependencies.raise_module_not_found_error(
|
|
160
|
-
name="GFSForecast class",
|
|
161
|
-
package_name="boto3",
|
|
162
|
-
module_not_found_error=e,
|
|
163
|
-
pycontrails_optional_package="gfs",
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
try:
|
|
167
|
-
import botocore
|
|
168
|
-
except ModuleNotFoundError as e:
|
|
169
|
-
dependencies.raise_module_not_found_error(
|
|
170
|
-
name="GFSForecast class",
|
|
171
|
-
package_name="botocore",
|
|
172
|
-
module_not_found_error=e,
|
|
173
|
-
pycontrails_optional_package="gfs",
|
|
174
|
-
)
|
|
175
|
-
|
|
176
154
|
# inputs
|
|
177
155
|
self.paths = paths
|
|
178
156
|
if cachestore is self.__marker:
|
|
@@ -196,13 +174,10 @@ class GFSForecast(metsource.MetDataSource):
|
|
|
196
174
|
self.variables = metsource.parse_variables(variables, self.supported_variables)
|
|
197
175
|
self.grid = metsource.parse_grid(grid, (0.25, 0.5, 1))
|
|
198
176
|
|
|
199
|
-
#
|
|
200
|
-
|
|
201
|
-
self.client = boto3.client(
|
|
202
|
-
"s3", config=botocore.client.Config(signature_version=botocore.UNSIGNED)
|
|
203
|
-
)
|
|
177
|
+
# s3 filesystem (created on first download)
|
|
178
|
+
self.fs = None
|
|
204
179
|
|
|
205
|
-
# set specific forecast time
|
|
180
|
+
# set specific forecast time if requested, otherwise compute from timesteps
|
|
206
181
|
if forecast_time is not None:
|
|
207
182
|
forecast_time_pd = pd.to_datetime(forecast_time)
|
|
208
183
|
if forecast_time_pd.hour % 6:
|
|
@@ -492,10 +467,35 @@ class GFSForecast(metsource.MetDataSource):
|
|
|
492
467
|
ds.to_netcdf(cache_path)
|
|
493
468
|
|
|
494
469
|
def _make_download(self, aws_key: str, target: str, filename: str) -> None:
|
|
470
|
+
"""Download a single GRIB file using s3fs.
|
|
471
|
+
|
|
472
|
+
Parameters
|
|
473
|
+
----------
|
|
474
|
+
aws_key : str
|
|
475
|
+
Key under GFS bucket forecast path.
|
|
476
|
+
target : str
|
|
477
|
+
Local filename to write.
|
|
478
|
+
filename : str
|
|
479
|
+
Original filename (used for progress label).
|
|
480
|
+
"""
|
|
481
|
+
# Lazily import s3fs and create filesystem if needed
|
|
482
|
+
if self.fs is None:
|
|
483
|
+
try:
|
|
484
|
+
import s3fs
|
|
485
|
+
except ModuleNotFoundError as exc:
|
|
486
|
+
dependencies.raise_module_not_found_error(
|
|
487
|
+
name="GFSForecast class",
|
|
488
|
+
package_name="s3fs",
|
|
489
|
+
module_not_found_error=exc,
|
|
490
|
+
pycontrails_optional_package="gfs",
|
|
491
|
+
)
|
|
492
|
+
self.fs = s3fs.S3FileSystem(anon=True)
|
|
493
|
+
|
|
494
|
+
s3_path = f"s3://{GFS_FORECAST_BUCKET}/{aws_key}"
|
|
495
495
|
if self.show_progress:
|
|
496
|
-
_download_with_progress(self.
|
|
496
|
+
_download_with_progress(self.fs, s3_path, target, filename)
|
|
497
497
|
else:
|
|
498
|
-
self.
|
|
498
|
+
self.fs.get(s3_path, target)
|
|
499
499
|
|
|
500
500
|
def _open_gfs_dataset(self, filepath: str | pathlib.Path, t: datetime) -> xr.Dataset:
|
|
501
501
|
"""Open GFS grib file for one forecast timestep.
|
|
@@ -615,30 +615,20 @@ class GFSForecast(metsource.MetDataSource):
|
|
|
615
615
|
return met.MetDataset(ds, **kwargs)
|
|
616
616
|
|
|
617
617
|
|
|
618
|
-
def _download_with_progress(
|
|
619
|
-
|
|
620
|
-
) -> None:
|
|
621
|
-
"""Download with `tqdm` progress bar.
|
|
618
|
+
def _download_with_progress(fs: s3fs.S3FileSystem, s3_path: str, target: str, label: str) -> None:
|
|
619
|
+
"""Download with tqdm progress bar using s3fs.
|
|
622
620
|
|
|
623
621
|
Parameters
|
|
624
622
|
----------
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
filename : str
|
|
632
|
-
Local filename to download to
|
|
623
|
+
fs : s3fs.S3FileSystem
|
|
624
|
+
Filesystem instance.
|
|
625
|
+
s3_path : str
|
|
626
|
+
Full s3 path (s3://bucket/key).
|
|
627
|
+
target : str
|
|
628
|
+
Local file path to write.
|
|
633
629
|
label : str
|
|
634
|
-
Progress label
|
|
635
|
-
|
|
636
|
-
Raises
|
|
637
|
-
------
|
|
638
|
-
ModuleNotFoundError
|
|
639
|
-
Raises if tqdm can't be found
|
|
630
|
+
Progress bar label.
|
|
640
631
|
"""
|
|
641
|
-
|
|
642
632
|
try:
|
|
643
633
|
from tqdm import tqdm
|
|
644
634
|
except ModuleNotFoundError as e:
|
|
@@ -649,14 +639,18 @@ def _download_with_progress(
|
|
|
649
639
|
pycontrails_optional_package="gfs",
|
|
650
640
|
)
|
|
651
641
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
642
|
+
# get object size via simple info call
|
|
643
|
+
info = fs.info(s3_path)
|
|
644
|
+
filesize = info.get("Size") or info.get("size")
|
|
645
|
+
|
|
646
|
+
with (
|
|
647
|
+
fs.open(s3_path, "rb") as fsrc,
|
|
648
|
+
open(target, "wb") as fdst,
|
|
649
|
+
tqdm(total=filesize, unit="B", unit_scale=True, desc=label) as t,
|
|
650
|
+
):
|
|
651
|
+
# stream in chunks
|
|
652
|
+
chunk = fsrc.read(1024 * 1024)
|
|
653
|
+
while chunk:
|
|
654
|
+
fdst.write(chunk)
|
|
655
|
+
t.update(len(chunk))
|
|
656
|
+
chunk = fsrc.read(1024 * 1024)
|