pycontrails 0.59.0__cp314-cp314-macosx_10_15_x86_64.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/__init__.py +70 -0
- pycontrails/_version.py +34 -0
- pycontrails/core/__init__.py +30 -0
- pycontrails/core/aircraft_performance.py +679 -0
- pycontrails/core/airports.py +228 -0
- pycontrails/core/cache.py +889 -0
- pycontrails/core/coordinates.py +174 -0
- pycontrails/core/fleet.py +483 -0
- pycontrails/core/flight.py +2185 -0
- pycontrails/core/flightplan.py +228 -0
- pycontrails/core/fuel.py +140 -0
- pycontrails/core/interpolation.py +702 -0
- pycontrails/core/met.py +2936 -0
- pycontrails/core/met_var.py +387 -0
- pycontrails/core/models.py +1321 -0
- pycontrails/core/polygon.py +549 -0
- pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
- pycontrails/core/vector.py +2249 -0
- pycontrails/datalib/__init__.py +12 -0
- pycontrails/datalib/_met_utils/metsource.py +746 -0
- pycontrails/datalib/ecmwf/__init__.py +73 -0
- pycontrails/datalib/ecmwf/arco_era5.py +345 -0
- pycontrails/datalib/ecmwf/common.py +114 -0
- pycontrails/datalib/ecmwf/era5.py +554 -0
- pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
- pycontrails/datalib/ecmwf/hres.py +804 -0
- pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
- pycontrails/datalib/ecmwf/ifs.py +287 -0
- pycontrails/datalib/ecmwf/model_levels.py +435 -0
- pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
- pycontrails/datalib/ecmwf/variables.py +268 -0
- pycontrails/datalib/geo_utils.py +261 -0
- pycontrails/datalib/gfs/__init__.py +28 -0
- pycontrails/datalib/gfs/gfs.py +656 -0
- pycontrails/datalib/gfs/variables.py +104 -0
- pycontrails/datalib/goes.py +764 -0
- pycontrails/datalib/gruan.py +343 -0
- pycontrails/datalib/himawari/__init__.py +27 -0
- pycontrails/datalib/himawari/header_struct.py +266 -0
- pycontrails/datalib/himawari/himawari.py +671 -0
- pycontrails/datalib/landsat.py +589 -0
- pycontrails/datalib/leo_utils/__init__.py +5 -0
- pycontrails/datalib/leo_utils/correction.py +266 -0
- pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
- pycontrails/datalib/leo_utils/search.py +250 -0
- pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
- pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
- pycontrails/datalib/leo_utils/vis.py +59 -0
- pycontrails/datalib/sentinel.py +650 -0
- pycontrails/datalib/spire/__init__.py +5 -0
- pycontrails/datalib/spire/exceptions.py +62 -0
- pycontrails/datalib/spire/spire.py +604 -0
- pycontrails/ext/bada.py +42 -0
- pycontrails/ext/cirium.py +14 -0
- pycontrails/ext/empirical_grid.py +140 -0
- pycontrails/ext/synthetic_flight.py +431 -0
- pycontrails/models/__init__.py +1 -0
- pycontrails/models/accf.py +425 -0
- pycontrails/models/apcemm/__init__.py +8 -0
- pycontrails/models/apcemm/apcemm.py +983 -0
- pycontrails/models/apcemm/inputs.py +226 -0
- pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
- pycontrails/models/apcemm/utils.py +437 -0
- pycontrails/models/cocip/__init__.py +29 -0
- pycontrails/models/cocip/cocip.py +2742 -0
- pycontrails/models/cocip/cocip_params.py +305 -0
- pycontrails/models/cocip/cocip_uncertainty.py +291 -0
- pycontrails/models/cocip/contrail_properties.py +1530 -0
- pycontrails/models/cocip/output_formats.py +2270 -0
- pycontrails/models/cocip/radiative_forcing.py +1260 -0
- pycontrails/models/cocip/radiative_heating.py +520 -0
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
- pycontrails/models/cocip/wake_vortex.py +396 -0
- pycontrails/models/cocip/wind_shear.py +120 -0
- pycontrails/models/cocipgrid/__init__.py +9 -0
- pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
- pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
- pycontrails/models/dry_advection.py +602 -0
- pycontrails/models/emissions/__init__.py +21 -0
- pycontrails/models/emissions/black_carbon.py +599 -0
- pycontrails/models/emissions/emissions.py +1353 -0
- pycontrails/models/emissions/ffm2.py +336 -0
- pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
- pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
- pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
- pycontrails/models/extended_k15.py +1327 -0
- pycontrails/models/humidity_scaling/__init__.py +37 -0
- pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
- pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
- pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
- pycontrails/models/issr.py +210 -0
- pycontrails/models/pcc.py +326 -0
- pycontrails/models/pcr.py +154 -0
- pycontrails/models/ps_model/__init__.py +18 -0
- pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
- pycontrails/models/ps_model/ps_grid.py +701 -0
- pycontrails/models/ps_model/ps_model.py +1000 -0
- pycontrails/models/ps_model/ps_operational_limits.py +525 -0
- pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
- pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
- pycontrails/models/sac.py +442 -0
- pycontrails/models/tau_cirrus.py +183 -0
- pycontrails/physics/__init__.py +1 -0
- pycontrails/physics/constants.py +117 -0
- pycontrails/physics/geo.py +1138 -0
- pycontrails/physics/jet.py +968 -0
- pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
- pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
- pycontrails/physics/thermo.py +551 -0
- pycontrails/physics/units.py +472 -0
- pycontrails/py.typed +0 -0
- pycontrails/utils/__init__.py +1 -0
- pycontrails/utils/dependencies.py +66 -0
- pycontrails/utils/iteration.py +13 -0
- pycontrails/utils/json.py +187 -0
- pycontrails/utils/temp.py +50 -0
- pycontrails/utils/types.py +163 -0
- pycontrails-0.59.0.dist-info/METADATA +179 -0
- pycontrails-0.59.0.dist-info/RECORD +123 -0
- pycontrails-0.59.0.dist-info/WHEEL +6 -0
- pycontrails-0.59.0.dist-info/licenses/LICENSE +178 -0
- pycontrails-0.59.0.dist-info/licenses/NOTICE +43 -0
- pycontrails-0.59.0.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""Support for accessing `GRUAN <https://www.gruan.org/>`_ data over FTP."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import ftplib
|
|
5
|
+
import functools
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from concurrent import futures
|
|
9
|
+
|
|
10
|
+
import xarray as xr
|
|
11
|
+
|
|
12
|
+
from pycontrails.core import cache
|
|
13
|
+
|
|
14
|
+
#: GRUAN FTP server address
|
|
15
|
+
FTP_SERVER = "ftp.ncdc.noaa.gov"
|
|
16
|
+
|
|
17
|
+
#: Base path for GRUAN data on the FTP server
|
|
18
|
+
FTP_BASE_PATH = "/pub/data/gruan/processing/level2"
|
|
19
|
+
|
|
20
|
+
#: All available GRUAN products and sites on the FTP server as of 2025-10
|
|
21
|
+
#: This is simply the hardcoded output of :func:`available_sites` at that time to
|
|
22
|
+
#: avoid a lookup that changes infrequently.
|
|
23
|
+
AVAILABLE_PRODUCTS_TO_SITES = {
|
|
24
|
+
"RS-11G-GDP.1": ["SYO", "TAT", "NYA", "LIN"],
|
|
25
|
+
"RS41-EDT.1": ["LIN", "POT", "SNG"],
|
|
26
|
+
"RS92-GDP.1": ["BOU", "CAB", "LIN", "PAY", "POT", "SOD", "TAT"],
|
|
27
|
+
"RS92-GDP.2": [
|
|
28
|
+
"BAR",
|
|
29
|
+
"BEL",
|
|
30
|
+
"BOU",
|
|
31
|
+
"CAB",
|
|
32
|
+
"DAR",
|
|
33
|
+
"GRA",
|
|
34
|
+
"LAU",
|
|
35
|
+
"LIN",
|
|
36
|
+
"MAN",
|
|
37
|
+
"NAU",
|
|
38
|
+
"NYA",
|
|
39
|
+
"PAY",
|
|
40
|
+
"POT",
|
|
41
|
+
"REU",
|
|
42
|
+
"SGP",
|
|
43
|
+
"SOD",
|
|
44
|
+
"TAT",
|
|
45
|
+
"TEN",
|
|
46
|
+
"GVN",
|
|
47
|
+
],
|
|
48
|
+
"RS92-PROFILE-BETA.2": ["BOU", "CAB", "LIN", "POT", "SOD", "TAT"],
|
|
49
|
+
"RS92-PROFILE-BETA.3": ["BOU", "CAB", "LIN", "POT", "SOD", "TAT"],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def extract_gruan_time(filename: str) -> tuple[datetime.datetime, int]:
|
|
54
|
+
"""Extract launch time and revision number from a GRUAN filename.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
filename : str
|
|
59
|
+
GRUAN filename, e.g. "LIN-RS-01_2_RS92-GDP_002_20210125T132400_1-000-001.nc"
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
tuple[datetime.datetime, int]
|
|
64
|
+
Launch time as a datetime object and revision number as an integer.
|
|
65
|
+
"""
|
|
66
|
+
parts = filename.split("_")
|
|
67
|
+
if len(parts) != 6:
|
|
68
|
+
raise ValueError(f"Unexpected filename format: {filename}")
|
|
69
|
+
time_part = parts[4]
|
|
70
|
+
try:
|
|
71
|
+
time = datetime.datetime.strptime(time_part, "%Y%m%dT%H%M%S")
|
|
72
|
+
except ValueError as e:
|
|
73
|
+
raise ValueError(f"Unexpected time segment: {time_part}") from e
|
|
74
|
+
|
|
75
|
+
revision_part = parts[5].removesuffix(".nc")
|
|
76
|
+
if not revision_part[-3:].isdigit():
|
|
77
|
+
raise ValueError(f"Unexpected revision segment: {revision_part}")
|
|
78
|
+
revision = int(revision_part[-3:])
|
|
79
|
+
|
|
80
|
+
return time, revision
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _fetch_product_tree(prod: str) -> dict[str, list[str]]:
|
|
84
|
+
result = {}
|
|
85
|
+
with ftplib.FTP(FTP_SERVER) as ftp:
|
|
86
|
+
ftp.login()
|
|
87
|
+
prod_path = f"{FTP_BASE_PATH}/{prod}"
|
|
88
|
+
versions = [v.split("/")[-1] for v in ftp.nlst(prod_path)]
|
|
89
|
+
|
|
90
|
+
for v in versions:
|
|
91
|
+
version_path = f"{prod_path}/{v}"
|
|
92
|
+
sites = [s.split("/")[-1] for s in ftp.nlst(version_path)]
|
|
93
|
+
|
|
94
|
+
key = f"{prod}.{int(v.split('-')[-1])}"
|
|
95
|
+
result[key] = sites
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@functools.cache
|
|
100
|
+
def available_sites() -> dict[str, list[str]]:
|
|
101
|
+
"""Get a list of available GRUAN sites for each supported product.
|
|
102
|
+
|
|
103
|
+
The :attr:`GRUAN.AVAILABLE` is a hardcoded snapshot of this data. The data returned
|
|
104
|
+
by this function does not change frequently, so it is cached for efficiency.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
dict[str, list[str]]
|
|
109
|
+
Mapping of product names to lists of available site identifiers.
|
|
110
|
+
"""
|
|
111
|
+
with ftplib.FTP(FTP_SERVER) as ftp:
|
|
112
|
+
ftp.login()
|
|
113
|
+
files = [p.split("/")[-1] for p in ftp.nlst(FTP_BASE_PATH)]
|
|
114
|
+
products = [p for p in files if "." not in p] # crude filter to exclude non-directories
|
|
115
|
+
|
|
116
|
+
# Compute each product tree in separate thread to speed up retrieval
|
|
117
|
+
# The FTP server only allows up to 5 connections from the same client
|
|
118
|
+
out = {}
|
|
119
|
+
with futures.ThreadPoolExecutor(max_workers=min(len(products), 5)) as tpe:
|
|
120
|
+
result = tpe.map(_fetch_product_tree, products)
|
|
121
|
+
for r in result:
|
|
122
|
+
out.update(r)
|
|
123
|
+
|
|
124
|
+
return out
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class GRUAN:
|
|
128
|
+
"""Access `GRUAN <https://www.gruan.org/>`_ data over anonymous FTP.
|
|
129
|
+
|
|
130
|
+
GRUAN is the Global Climate Observing System Reference Upper-Air Network. It provides
|
|
131
|
+
high-quality measurements of atmospheric variables from ground to stratosphere
|
|
132
|
+
through a global network of radiosonde stations.
|
|
133
|
+
|
|
134
|
+
.. versionadded:: 0.59.0
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
product : str
|
|
139
|
+
GRUAN data product. See :attr:`AVAILABLE` for available products. These currently
|
|
140
|
+
include:
|
|
141
|
+
- ``RS92-GDP.2``
|
|
142
|
+
- ``RS92-GDP.1``
|
|
143
|
+
- ``RS92-PROFILE-BETA.2``
|
|
144
|
+
- ``RS92-PROFILE-BETA.3``
|
|
145
|
+
- ``RS41-EDT.1``
|
|
146
|
+
- ``RS-11G-GDP.1``
|
|
147
|
+
site : str
|
|
148
|
+
GRUAN station identifier. See :attr:`AVAILABLE` for available sites for each product.
|
|
149
|
+
cachestore : cache.CacheStore | None, optional
|
|
150
|
+
Cache store to use for downloaded files. If not provided, a disk cache store
|
|
151
|
+
will be created in the user cache directory under ``gruan/``. Set to ``None``
|
|
152
|
+
to disable caching.
|
|
153
|
+
|
|
154
|
+
Notes
|
|
155
|
+
-----
|
|
156
|
+
The FTP files have the following hierarchy::
|
|
157
|
+
|
|
158
|
+
/pub/data/gruan/processing/level2/
|
|
159
|
+
{product-root}/
|
|
160
|
+
version-{NNN}/
|
|
161
|
+
{SITE}/
|
|
162
|
+
{YYYY}/
|
|
163
|
+
<filename>.nc
|
|
164
|
+
|
|
165
|
+
- {product-root} is the product name without the trailing version integer (e.g. ``RS92-GDP``)
|
|
166
|
+
- version-{NNN} zero-pads to three digits (suffix ``.2`` -> ``version-002``)
|
|
167
|
+
- {SITE} is the station code (e.g. ``LIN``)
|
|
168
|
+
- {YYYY} is launch year
|
|
169
|
+
- Filenames encode launch time and revision (parsed by :func:`extract_gruan_time`)
|
|
170
|
+
|
|
171
|
+
Discovery helpers methods:
|
|
172
|
+
|
|
173
|
+
- :attr:`AVAILABLE` or :func:`available_sites` -> products and sites
|
|
174
|
+
- :meth:`years` -> list available years for (product, site)
|
|
175
|
+
- :meth:`list_files` -> list available NetCDF files for the given year
|
|
176
|
+
- :meth:`get` -> download and open a single NetCDF file as an :class:`xarray.Dataset`
|
|
177
|
+
|
|
178
|
+
Typical workflow:
|
|
179
|
+
|
|
180
|
+
1. Inspect :attr:`AVAILABLE` (fast) or call :func:`available_sites` (live)
|
|
181
|
+
2. Instantiate ``GRUAN(product, site)``
|
|
182
|
+
3. Call ``years()``
|
|
183
|
+
4. Call ``list_files(year)``
|
|
184
|
+
5. Call ``get(filename)`` for an ``xarray.Dataset``
|
|
185
|
+
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
# Convenience access to available sites
|
|
189
|
+
available_sites = staticmethod(available_sites)
|
|
190
|
+
AVAILABLE = AVAILABLE_PRODUCTS_TO_SITES
|
|
191
|
+
|
|
192
|
+
__slots__ = ("_ftp", "cachestore", "product", "site")
|
|
193
|
+
|
|
194
|
+
__marker = object()
|
|
195
|
+
|
|
196
|
+
def __init__(
|
|
197
|
+
self,
|
|
198
|
+
product: str,
|
|
199
|
+
site: str,
|
|
200
|
+
cachestore: cache.CacheStore | None = __marker, # type: ignore[assignment]
|
|
201
|
+
) -> None:
|
|
202
|
+
known = AVAILABLE_PRODUCTS_TO_SITES
|
|
203
|
+
|
|
204
|
+
if product not in known:
|
|
205
|
+
known = available_sites() # perhaps AVAILABLE_PRODUCTS_TO_SITES is outdated
|
|
206
|
+
if product not in known:
|
|
207
|
+
raise ValueError(f"Unknown GRUAN product: {product}. Known products: {list(known)}")
|
|
208
|
+
self.product = product
|
|
209
|
+
|
|
210
|
+
if site not in known[product]:
|
|
211
|
+
known = available_sites() # perhaps AVAILABLE_PRODUCTS_TO_SITES is outdated
|
|
212
|
+
if site not in known[product]:
|
|
213
|
+
raise ValueError(
|
|
214
|
+
f"Unknown GRUAN site '{site}' for product '{product}'. "
|
|
215
|
+
f"Known sites: {known[product]}"
|
|
216
|
+
)
|
|
217
|
+
self.site = site
|
|
218
|
+
|
|
219
|
+
if cachestore is self.__marker:
|
|
220
|
+
cache_root = cache._get_user_cache_dir()
|
|
221
|
+
cache_dir = f"{cache_root}/gruan"
|
|
222
|
+
cachestore = cache.DiskCacheStore(cache_dir=cache_dir)
|
|
223
|
+
self.cachestore = cachestore
|
|
224
|
+
|
|
225
|
+
self._ftp: ftplib.FTP | None = None
|
|
226
|
+
|
|
227
|
+
def __repr__(self) -> str:
|
|
228
|
+
return f"GRUAN(product='{self.product}', site='{self.site}')"
|
|
229
|
+
|
|
230
|
+
def _connect(self) -> ftplib.FTP:
|
|
231
|
+
"""Connect to the GRUAN FTP server."""
|
|
232
|
+
if self._ftp is None or self._ftp.sock is None:
|
|
233
|
+
self._ftp = ftplib.FTP(FTP_SERVER)
|
|
234
|
+
self._ftp.login()
|
|
235
|
+
return self._ftp
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
self._ftp.pwd() # check if connection is still alive
|
|
239
|
+
except (*ftplib.all_errors, ConnectionError): # type: ignore[misc]
|
|
240
|
+
# If we encounter any error, reset the connection and retry
|
|
241
|
+
self._ftp = None
|
|
242
|
+
return self._connect()
|
|
243
|
+
return self._ftp
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def base_path_product(self) -> str:
|
|
247
|
+
"""Get the base path for GRUAN data product on the FTP server."""
|
|
248
|
+
product, version = self.product.rsplit(".")
|
|
249
|
+
return f"/pub/data/gruan/processing/level2/{product}/version-{version.zfill(3)}"
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def base_path_site(self) -> str:
|
|
253
|
+
"""Get the base path for GRUAN data site on the FTP server."""
|
|
254
|
+
return f"{self.base_path_product}/{self.site}"
|
|
255
|
+
|
|
256
|
+
def years(self) -> list[int]:
|
|
257
|
+
"""Get a list of available years for the selected product and site."""
|
|
258
|
+
ftp = self._connect()
|
|
259
|
+
ftp.cwd(self.base_path_site)
|
|
260
|
+
years = ftp.nlst()
|
|
261
|
+
return sorted(int(year) for year in years)
|
|
262
|
+
|
|
263
|
+
def list_files(self, year: int | None = None) -> list[str]:
|
|
264
|
+
"""List available files for a given year.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
year : int | None, optional
|
|
269
|
+
Year to list files for. If ``None``, list files for all available years. The later
|
|
270
|
+
may be time-consuming.
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
list[str]
|
|
275
|
+
List of available GRUAN filenames for the specified year.
|
|
276
|
+
"""
|
|
277
|
+
if year is None:
|
|
278
|
+
years = self.years()
|
|
279
|
+
return sorted(file for y in years for file in self.list_files(y))
|
|
280
|
+
|
|
281
|
+
path = f"{self.base_path_site}/{year}"
|
|
282
|
+
|
|
283
|
+
ftp = self._connect()
|
|
284
|
+
try:
|
|
285
|
+
ftp.cwd(path)
|
|
286
|
+
except ftplib.error_perm as e:
|
|
287
|
+
available = self.years()
|
|
288
|
+
if year not in available:
|
|
289
|
+
msg = f"No data available for year {year}. Available years are: {available}"
|
|
290
|
+
raise ValueError(msg) from e
|
|
291
|
+
raise
|
|
292
|
+
return sorted(ftp.nlst())
|
|
293
|
+
|
|
294
|
+
def get(self, filename: str) -> xr.Dataset:
|
|
295
|
+
"""Download a GRUAN dataset by filename.
|
|
296
|
+
|
|
297
|
+
Parameters
|
|
298
|
+
----------
|
|
299
|
+
filename : str
|
|
300
|
+
GRUAN filename to download, e.g. "LIN-RS-01_2_RS92-GDP_002_20210125T132400_1-000-001.nc"
|
|
301
|
+
|
|
302
|
+
Returns
|
|
303
|
+
-------
|
|
304
|
+
xr.Dataset
|
|
305
|
+
The GRUAN dataset retrieved from the FTP server. If caching is enabled,
|
|
306
|
+
the file is downloaded to the cache store and loaded from there on subsequent calls.
|
|
307
|
+
"""
|
|
308
|
+
if self.cachestore is None:
|
|
309
|
+
return self._get_no_cache(filename)
|
|
310
|
+
return self._get_with_cache(filename)
|
|
311
|
+
|
|
312
|
+
def _get_no_cache(self, filename: str) -> xr.Dataset:
|
|
313
|
+
t, _ = extract_gruan_time(filename)
|
|
314
|
+
path = f"{self.base_path_site}/{t.year}/{filename}"
|
|
315
|
+
|
|
316
|
+
ftp = self._connect()
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
# On windows, NamedTemporaryFile cannot be reopened while still open.
|
|
320
|
+
# After python 3.11 support is dropped, we can use delete_on_close=False
|
|
321
|
+
# in NamedTemporaryFile to streamline this.
|
|
322
|
+
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
|
323
|
+
ftp.retrbinary(f"RETR {path}", tmp.write)
|
|
324
|
+
return xr.load_dataset(tmp.name)
|
|
325
|
+
finally:
|
|
326
|
+
os.remove(tmp.name)
|
|
327
|
+
|
|
328
|
+
def _get_with_cache(self, filename: str) -> xr.Dataset:
|
|
329
|
+
if self.cachestore is None:
|
|
330
|
+
raise ValueError("Cachestore is not configured.")
|
|
331
|
+
|
|
332
|
+
lpath = self.cachestore.path(filename)
|
|
333
|
+
if self.cachestore.exists(lpath):
|
|
334
|
+
return xr.open_dataset(lpath)
|
|
335
|
+
|
|
336
|
+
t, _ = extract_gruan_time(filename)
|
|
337
|
+
path = f"{self.base_path_site}/{t.year}/{filename}"
|
|
338
|
+
|
|
339
|
+
ftp = self._connect()
|
|
340
|
+
with open(lpath, "wb") as f:
|
|
341
|
+
ftp.retrbinary(f"RETR {path}", f.write)
|
|
342
|
+
|
|
343
|
+
return xr.open_dataset(lpath)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Support for Himawari-8/9 satellite data access."""
|
|
2
|
+
|
|
3
|
+
from pycontrails.datalib.himawari.header_struct import (
|
|
4
|
+
HEADER_STRUCT_SCHEMA,
|
|
5
|
+
parse_himawari_header,
|
|
6
|
+
)
|
|
7
|
+
from pycontrails.datalib.himawari.himawari import (
|
|
8
|
+
HIMAWARI_8_9_SWITCH_DATE,
|
|
9
|
+
HIMAWARI_8_BUCKET,
|
|
10
|
+
HIMAWARI_9_BUCKET,
|
|
11
|
+
Himawari,
|
|
12
|
+
HimawariRegion,
|
|
13
|
+
extract_visualization,
|
|
14
|
+
to_true_color,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"HEADER_STRUCT_SCHEMA",
|
|
19
|
+
"HIMAWARI_8_9_SWITCH_DATE",
|
|
20
|
+
"HIMAWARI_8_BUCKET",
|
|
21
|
+
"HIMAWARI_9_BUCKET",
|
|
22
|
+
"Himawari",
|
|
23
|
+
"HimawariRegion",
|
|
24
|
+
"extract_visualization",
|
|
25
|
+
"parse_himawari_header",
|
|
26
|
+
"to_true_color",
|
|
27
|
+
]
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Support for parsing the Himawari-8/9 header structure.
|
|
2
|
+
|
|
3
|
+
See the latest user guide for details:
|
|
4
|
+
https://www.data.jma.go.jp/mscweb/en/himawari89/space_segment/hsd_sample/HS_D_users_guide_en_v13.pdf
|
|
5
|
+
|
|
6
|
+
If that link breaks, find the correct link here:
|
|
7
|
+
https://www.data.jma.go.jp/mscweb/en/himawari89/space_segment/sample_hisd.html
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import struct
|
|
11
|
+
from typing import Any, TypedDict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _HeaderBlock(TypedDict):
|
|
15
|
+
"""An individual Himawari header block."""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
fields: list[tuple[str, str, int, int, str | None]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
HEADER_STRUCT_SCHEMA: dict[int, _HeaderBlock] = {
|
|
22
|
+
1: {
|
|
23
|
+
"name": "basic_information",
|
|
24
|
+
"fields": [
|
|
25
|
+
("header_block_number", "I1", 1, 1, None),
|
|
26
|
+
("block_length", "I2", 2, 1, None),
|
|
27
|
+
("total_header_blocks", "I2", 2, 1, None),
|
|
28
|
+
("byte_order", "I1", 1, 1, None),
|
|
29
|
+
("satellite_name", "C", 1, 16, None),
|
|
30
|
+
("processing_center_name", "C", 1, 16, None),
|
|
31
|
+
("observation_area", "C", 1, 4, None),
|
|
32
|
+
("other_obs_info", "C", 1, 2, None),
|
|
33
|
+
("obs_timeline", "I2", 2, 1, None),
|
|
34
|
+
("obs_start_time", "R8", 8, 1, None),
|
|
35
|
+
("obs_end_time", "R8", 8, 1, None),
|
|
36
|
+
("file_creation_time", "R8", 8, 1, None),
|
|
37
|
+
("total_header_length", "I4", 4, 1, None),
|
|
38
|
+
("total_data_length", "I4", 4, 1, None),
|
|
39
|
+
("quality_flag_1", "I1", 1, 1, None),
|
|
40
|
+
("quality_flag_2", "I1", 1, 1, None),
|
|
41
|
+
("quality_flag_3", "I1", 1, 1, None),
|
|
42
|
+
("quality_flag_4", "I1", 1, 1, None),
|
|
43
|
+
("file_format_version", "C", 1, 32, None),
|
|
44
|
+
("file_name", "C", 1, 128, None),
|
|
45
|
+
("spare", "C", 40, 1, None),
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
2: {
|
|
49
|
+
"name": "data_information",
|
|
50
|
+
"fields": [
|
|
51
|
+
("header_block_number", "I1", 1, 1, None),
|
|
52
|
+
("block_length", "I2", 2, 1, None),
|
|
53
|
+
("bits_per_pixel", "I2", 2, 1, None),
|
|
54
|
+
("num_columns", "I2", 2, 1, None),
|
|
55
|
+
("num_lines", "I2", 2, 1, None),
|
|
56
|
+
("compression_flag", "I1", 1, 1, None),
|
|
57
|
+
("spare", "C", 40, 1, None),
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
3: {
|
|
61
|
+
"name": "projection_information",
|
|
62
|
+
"fields": [
|
|
63
|
+
("header_block_number", "I1", 1, 1, None),
|
|
64
|
+
("block_length", "I2", 2, 1, None),
|
|
65
|
+
("sub_lon", "R8", 8, 1, None),
|
|
66
|
+
("cfac", "I4", 4, 1, None),
|
|
67
|
+
("lfac", "I4", 4, 1, None),
|
|
68
|
+
("coff", "R4", 4, 1, None),
|
|
69
|
+
("loff", "R4", 4, 1, None),
|
|
70
|
+
("dist_from_earth_center", "R8", 8, 1, None),
|
|
71
|
+
("equatorial_radius", "R8", 8, 1, None),
|
|
72
|
+
("polar_radius", "R8", 8, 1, None),
|
|
73
|
+
("rec_minus_rpol_div_req_sq", "R8", 8, 1, None),
|
|
74
|
+
("rpol_sq_div_req_sq", "R8", 8, 1, None),
|
|
75
|
+
("req_sq_div_rpol_sq", "R8", 8, 1, None),
|
|
76
|
+
("coeff_for_sd", "R8", 8, 1, None),
|
|
77
|
+
("resampling_types", "I2", 2, 1, None),
|
|
78
|
+
("resampling_size", "I2", 2, 1, None),
|
|
79
|
+
("spare", "C", 40, 1, None),
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
4: {
|
|
83
|
+
"name": "navigation_information",
|
|
84
|
+
"fields": [
|
|
85
|
+
("header_block_number", "I1", 1, 1, None),
|
|
86
|
+
("block_length", "I2", 2, 1, None),
|
|
87
|
+
("nav_info_time", "R8", 8, 1, None),
|
|
88
|
+
("ssp_longitude", "R8", 8, 1, None),
|
|
89
|
+
("ssp_latitude", "R8", 8, 1, None),
|
|
90
|
+
("dist_from_earth_center_to_sat", "R8", 8, 1, None),
|
|
91
|
+
("nadir_longitude", "R8", 8, 1, None),
|
|
92
|
+
("nadir_latitude", "R8", 8, 1, None),
|
|
93
|
+
("sun_position", "R8", 8, 3, None),
|
|
94
|
+
("moon_position", "R8", 8, 3, None),
|
|
95
|
+
("spare", "C", 40, 1, None),
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
5: {
|
|
99
|
+
"name": "calibration_information",
|
|
100
|
+
"fields": [
|
|
101
|
+
("header_block_number", "I1", 1, 1, None),
|
|
102
|
+
("block_length", "I2", 2, 1, None),
|
|
103
|
+
("band_number", "I2", 2, 1, None),
|
|
104
|
+
("central_wavelength", "R8", 8, 1, None),
|
|
105
|
+
("valid_bits_per_pixel", "I2", 2, 1, None),
|
|
106
|
+
("count_error_pixels", "I2", 2, 1, None),
|
|
107
|
+
("count_outside_scan_area", "I2", 2, 1, None),
|
|
108
|
+
("gain", "R8", 8, 1, None),
|
|
109
|
+
("constant", "R8", 8, 1, None),
|
|
110
|
+
("c0", "R8", 8, 1, "IR-BANDS"),
|
|
111
|
+
("c1", "R8", 8, 1, "IR-BANDS"),
|
|
112
|
+
("c2", "R8", 8, 1, "IR-BANDS"),
|
|
113
|
+
("C0", "R8", 8, 1, "IR-BANDS"),
|
|
114
|
+
("C1", "R8", 8, 1, "IR-BANDS"),
|
|
115
|
+
("C2", "R8", 8, 1, "IR-BANDS"),
|
|
116
|
+
("speed_of_light", "R8", 8, 1, "IR-BANDS"),
|
|
117
|
+
("planck_constant", "R8", 8, 1, "IR-BANDS"),
|
|
118
|
+
("boltzmann_constant", "R8", 8, 1, "IR-BANDS"),
|
|
119
|
+
("spare", "C", 40, 1, "IR-BANDS"),
|
|
120
|
+
("coeff_c_prime", "R8", 8, 1, "NIR-BANDS"),
|
|
121
|
+
("spare", "C", 104, 1, "NIR-BANDS"),
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
6: {
|
|
125
|
+
"name": "inter_calibration_information",
|
|
126
|
+
"fields": [
|
|
127
|
+
("header_block_number", "I1", 1, 1, None),
|
|
128
|
+
("block_length", "I2", 2, 1, None),
|
|
129
|
+
("gsics_intercept", "R8", 8, 1, None),
|
|
130
|
+
("gsics_slope", "R8", 8, 1, None),
|
|
131
|
+
("gsics_quad", "R8", 8, 1, None),
|
|
132
|
+
("rad_bias_standard", "R8", 8, 1, None),
|
|
133
|
+
("uncert_rad_bias", "R8", 8, 1, None),
|
|
134
|
+
("rad_standard_scene", "R8", 8, 1, None),
|
|
135
|
+
("gsics_validity_start", "R8", 8, 1, None),
|
|
136
|
+
("gsics_validity_end", "R8", 8, 1, None),
|
|
137
|
+
("rad_validity_upper", "R4", 4, 1, None),
|
|
138
|
+
("rad_validity_lower", "R4", 4, 1, None),
|
|
139
|
+
("gsics_file_name", "C", 1, 128, None),
|
|
140
|
+
("spare", "C", 56, 1, None),
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
7: {
|
|
144
|
+
"name": "segment_information",
|
|
145
|
+
"fields": [
|
|
146
|
+
("header_block_number", "I1", 1, 1, None),
|
|
147
|
+
("block_length", "I2", 2, 1, None),
|
|
148
|
+
("total_segments", "I1", 1, 1, None),
|
|
149
|
+
("segment_seq_number", "I1", 1, 1, None),
|
|
150
|
+
("first_line_number", "I2", 2, 1, None),
|
|
151
|
+
("spare", "C", 40, 1, None),
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
8: {
|
|
155
|
+
"name": "navigation_correction_information",
|
|
156
|
+
"fields": [
|
|
157
|
+
("header_block_number", "I1", 1, 1, None),
|
|
158
|
+
("block_length", "I2", 2, 1, None),
|
|
159
|
+
("center_col_rot", "R4", 4, 1, None),
|
|
160
|
+
("center_line_rot", "R4", 4, 1, None),
|
|
161
|
+
("rot_correction", "R8", 8, 1, None),
|
|
162
|
+
("num_corr_data", "I2", 2, 1, None),
|
|
163
|
+
# The following fields are variable and depend on 'num_corr_data'
|
|
164
|
+
# These are not currently parsed
|
|
165
|
+
# ("line_after_rot", "I2", 2, 1, None),
|
|
166
|
+
# ("shift_amount_col", "R4", 4, 1, None),
|
|
167
|
+
# ("shift_amount_line", "R4", 4, 1, None),
|
|
168
|
+
# ("spare", "C", 40, 1, None),
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
9: {
|
|
172
|
+
"name": "observation_time_information",
|
|
173
|
+
"fields": [
|
|
174
|
+
("header_block_number", "I1", 1, 1, None),
|
|
175
|
+
("block_length", "I2", 2, 1, None),
|
|
176
|
+
("num_obs_times", "I2", 2, 1, None),
|
|
177
|
+
# The following fields are variable and depend on 'num_obs_times'
|
|
178
|
+
# These are not currently parsed
|
|
179
|
+
# ("line_number", "I2", 2, 1, None),
|
|
180
|
+
# ("obs_time", "R8", 8, 1, None),
|
|
181
|
+
# ("spare", "C", 40, 1, None),
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
10: {
|
|
185
|
+
"name": "error_information",
|
|
186
|
+
"fields": [
|
|
187
|
+
("header_block_number", "I1", 1, 1, None),
|
|
188
|
+
("block_length", "I4", 4, 1, None),
|
|
189
|
+
("num_error_data", "I2", 2, 1, None),
|
|
190
|
+
# The following fields are variable and depend on 'num_error_data'
|
|
191
|
+
# These are not currently parsed
|
|
192
|
+
# ("line_number", "I2", 2, 1, None),
|
|
193
|
+
# ("num_error_pixels", "I2", 2, 1, None),
|
|
194
|
+
# ("spare", "C", 40, 1, None),
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
11: {
|
|
198
|
+
"name": "spare",
|
|
199
|
+
"fields": [
|
|
200
|
+
("header_block_number", "I1", 1, 1, None),
|
|
201
|
+
("block_length", "I2", 2, 1, None),
|
|
202
|
+
("spare", "C", 256, 1, None),
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def parse_himawari_header(content: bytes) -> dict[str, dict[str, Any]]:
|
|
209
|
+
"""Parse the Himawari header data.
|
|
210
|
+
|
|
211
|
+
Skips variable-length fields and spares.
|
|
212
|
+
"""
|
|
213
|
+
out = {}
|
|
214
|
+
offset = 0
|
|
215
|
+
|
|
216
|
+
# everything is little-endian (see the byte_order field in block #1)
|
|
217
|
+
typ_map = {
|
|
218
|
+
"I1": "B",
|
|
219
|
+
"I2": "H",
|
|
220
|
+
"I4": "I",
|
|
221
|
+
"R4": "f",
|
|
222
|
+
"R8": "d",
|
|
223
|
+
"C": "s",
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for block_num, block_info in HEADER_STRUCT_SCHEMA.items():
|
|
227
|
+
offset_block_start = offset # blocks 8, 9, 10 are dynamic
|
|
228
|
+
block_data: dict[str, Any] = {}
|
|
229
|
+
block_name = block_info["name"]
|
|
230
|
+
fields = block_info["fields"]
|
|
231
|
+
block_length_value: int | None = None
|
|
232
|
+
|
|
233
|
+
for name, typ, size, count, cond in fields:
|
|
234
|
+
if block_num == 5 and cond: # deal with dynamic block 5
|
|
235
|
+
band_number = block_data["band_number"]
|
|
236
|
+
if cond == "IR-BANDS" and band_number <= 6:
|
|
237
|
+
continue
|
|
238
|
+
if cond == "NIR-BANDS" and band_number >= 7:
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
if name == "spare": # skip spare fields
|
|
242
|
+
offset += size * count
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
fmt = typ_map[typ]
|
|
246
|
+
if typ == "C":
|
|
247
|
+
raw = struct.unpack_from(f"{size * count}s", content, offset)[0]
|
|
248
|
+
value = raw.rstrip(b"\x00").decode("ascii", errors="ignore")
|
|
249
|
+
else:
|
|
250
|
+
value = struct.unpack_from(f"{count}{fmt}", content, offset)
|
|
251
|
+
if count == 1:
|
|
252
|
+
value = value[0]
|
|
253
|
+
|
|
254
|
+
block_data[name] = value
|
|
255
|
+
offset += size * count
|
|
256
|
+
|
|
257
|
+
if name == "block_length":
|
|
258
|
+
block_length_value = value
|
|
259
|
+
|
|
260
|
+
if block_length_value is None:
|
|
261
|
+
raise ValueError(f"Missing block_length in {block_name}")
|
|
262
|
+
offset = offset_block_start + block_length_value # only needed for blocks 8, 9, 10
|
|
263
|
+
|
|
264
|
+
out[block_name] = block_data
|
|
265
|
+
|
|
266
|
+
return out
|