pycontrails 0.58.0__cp314-cp314-macosx_10_13_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.

Files changed (122) hide show
  1. pycontrails/__init__.py +70 -0
  2. pycontrails/_version.py +34 -0
  3. pycontrails/core/__init__.py +30 -0
  4. pycontrails/core/aircraft_performance.py +679 -0
  5. pycontrails/core/airports.py +228 -0
  6. pycontrails/core/cache.py +889 -0
  7. pycontrails/core/coordinates.py +174 -0
  8. pycontrails/core/fleet.py +483 -0
  9. pycontrails/core/flight.py +2185 -0
  10. pycontrails/core/flightplan.py +228 -0
  11. pycontrails/core/fuel.py +140 -0
  12. pycontrails/core/interpolation.py +702 -0
  13. pycontrails/core/met.py +2931 -0
  14. pycontrails/core/met_var.py +387 -0
  15. pycontrails/core/models.py +1321 -0
  16. pycontrails/core/polygon.py +549 -0
  17. pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
  18. pycontrails/core/vector.py +2249 -0
  19. pycontrails/datalib/__init__.py +12 -0
  20. pycontrails/datalib/_met_utils/metsource.py +746 -0
  21. pycontrails/datalib/ecmwf/__init__.py +73 -0
  22. pycontrails/datalib/ecmwf/arco_era5.py +345 -0
  23. pycontrails/datalib/ecmwf/common.py +114 -0
  24. pycontrails/datalib/ecmwf/era5.py +554 -0
  25. pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
  26. pycontrails/datalib/ecmwf/hres.py +804 -0
  27. pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
  28. pycontrails/datalib/ecmwf/ifs.py +287 -0
  29. pycontrails/datalib/ecmwf/model_levels.py +435 -0
  30. pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
  31. pycontrails/datalib/ecmwf/variables.py +268 -0
  32. pycontrails/datalib/geo_utils.py +261 -0
  33. pycontrails/datalib/gfs/__init__.py +28 -0
  34. pycontrails/datalib/gfs/gfs.py +656 -0
  35. pycontrails/datalib/gfs/variables.py +104 -0
  36. pycontrails/datalib/goes.py +757 -0
  37. pycontrails/datalib/himawari/__init__.py +27 -0
  38. pycontrails/datalib/himawari/header_struct.py +266 -0
  39. pycontrails/datalib/himawari/himawari.py +667 -0
  40. pycontrails/datalib/landsat.py +589 -0
  41. pycontrails/datalib/leo_utils/__init__.py +5 -0
  42. pycontrails/datalib/leo_utils/correction.py +266 -0
  43. pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
  44. pycontrails/datalib/leo_utils/search.py +250 -0
  45. pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
  46. pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
  47. pycontrails/datalib/leo_utils/vis.py +59 -0
  48. pycontrails/datalib/sentinel.py +650 -0
  49. pycontrails/datalib/spire/__init__.py +5 -0
  50. pycontrails/datalib/spire/exceptions.py +62 -0
  51. pycontrails/datalib/spire/spire.py +604 -0
  52. pycontrails/ext/bada.py +42 -0
  53. pycontrails/ext/cirium.py +14 -0
  54. pycontrails/ext/empirical_grid.py +140 -0
  55. pycontrails/ext/synthetic_flight.py +431 -0
  56. pycontrails/models/__init__.py +1 -0
  57. pycontrails/models/accf.py +425 -0
  58. pycontrails/models/apcemm/__init__.py +8 -0
  59. pycontrails/models/apcemm/apcemm.py +983 -0
  60. pycontrails/models/apcemm/inputs.py +226 -0
  61. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  62. pycontrails/models/apcemm/utils.py +437 -0
  63. pycontrails/models/cocip/__init__.py +29 -0
  64. pycontrails/models/cocip/cocip.py +2742 -0
  65. pycontrails/models/cocip/cocip_params.py +305 -0
  66. pycontrails/models/cocip/cocip_uncertainty.py +291 -0
  67. pycontrails/models/cocip/contrail_properties.py +1530 -0
  68. pycontrails/models/cocip/output_formats.py +2270 -0
  69. pycontrails/models/cocip/radiative_forcing.py +1260 -0
  70. pycontrails/models/cocip/radiative_heating.py +520 -0
  71. pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
  72. pycontrails/models/cocip/wake_vortex.py +396 -0
  73. pycontrails/models/cocip/wind_shear.py +120 -0
  74. pycontrails/models/cocipgrid/__init__.py +9 -0
  75. pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
  76. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  77. pycontrails/models/dry_advection.py +602 -0
  78. pycontrails/models/emissions/__init__.py +21 -0
  79. pycontrails/models/emissions/black_carbon.py +599 -0
  80. pycontrails/models/emissions/emissions.py +1353 -0
  81. pycontrails/models/emissions/ffm2.py +336 -0
  82. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  83. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  84. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  85. pycontrails/models/extended_k15.py +1327 -0
  86. pycontrails/models/humidity_scaling/__init__.py +37 -0
  87. pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
  88. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  89. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  90. pycontrails/models/issr.py +210 -0
  91. pycontrails/models/pcc.py +326 -0
  92. pycontrails/models/pcr.py +154 -0
  93. pycontrails/models/ps_model/__init__.py +18 -0
  94. pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
  95. pycontrails/models/ps_model/ps_grid.py +701 -0
  96. pycontrails/models/ps_model/ps_model.py +1000 -0
  97. pycontrails/models/ps_model/ps_operational_limits.py +525 -0
  98. pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
  99. pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
  100. pycontrails/models/sac.py +442 -0
  101. pycontrails/models/tau_cirrus.py +183 -0
  102. pycontrails/physics/__init__.py +1 -0
  103. pycontrails/physics/constants.py +117 -0
  104. pycontrails/physics/geo.py +1138 -0
  105. pycontrails/physics/jet.py +968 -0
  106. pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
  107. pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
  108. pycontrails/physics/thermo.py +551 -0
  109. pycontrails/physics/units.py +472 -0
  110. pycontrails/py.typed +0 -0
  111. pycontrails/utils/__init__.py +1 -0
  112. pycontrails/utils/dependencies.py +66 -0
  113. pycontrails/utils/iteration.py +13 -0
  114. pycontrails/utils/json.py +187 -0
  115. pycontrails/utils/temp.py +50 -0
  116. pycontrails/utils/types.py +163 -0
  117. pycontrails-0.58.0.dist-info/METADATA +180 -0
  118. pycontrails-0.58.0.dist-info/RECORD +122 -0
  119. pycontrails-0.58.0.dist-info/WHEEL +6 -0
  120. pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
  121. pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
  122. pycontrails-0.58.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,757 @@
1
+ """Support for GOES access and analysis.
2
+
3
+ Resources
4
+ ---------
5
+
6
+ - `GOES 16/18 on GCP notes <https://console.cloud.google.com/marketplace/product/noaa-public/goes>`_
7
+ - `GOES on AWS notes <https://docs.opendata.aws/noaa-goes16/cics-readme.html>`_
8
+ - `Scan Mode information and timing <https://www.ospo.noaa.gov/Operations/GOES/16/GOES-16%20Scan%20Mode%206.html>`_
9
+ - `Current position of the MESO1 sector <https://www.ospo.noaa.gov/Operations/GOES/east/meso1-img.html>`_
10
+ - `Current position of the MESO2 sector <https://www.ospo.noaa.gov/Operations/GOES/east/meso2-img.html>`_
11
+ - `Historical Mesoscale regions <https://qcweb.ssec.wisc.edu/web/meso_search/>`_
12
+ - `Real time GOES data quality <https://qcweb.ssec.wisc.edu/web/abi_quality_scores/>`_
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import datetime
18
+ import enum
19
+ import os
20
+ import tempfile
21
+ import warnings
22
+ from collections.abc import Iterable
23
+ from typing import TYPE_CHECKING, Any
24
+
25
+ import numpy as np
26
+ import numpy.typing as npt
27
+ import pandas as pd
28
+ import xarray as xr
29
+
30
+ from pycontrails.core import cache
31
+ from pycontrails.datalib import geo_utils
32
+ from pycontrails.datalib.geo_utils import (
33
+ parallax_correct, # noqa: F401, keep for backwards compatibility
34
+ to_ash, # keep for backwards compatibility
35
+ )
36
+ from pycontrails.utils import dependencies
37
+
38
+ if TYPE_CHECKING:
39
+ import cartopy.crs
40
+
41
+ try:
42
+ import gcsfs
43
+ except ModuleNotFoundError as exc:
44
+ dependencies.raise_module_not_found_error(
45
+ name="goes module",
46
+ package_name="gcsfs",
47
+ module_not_found_error=exc,
48
+ pycontrails_optional_package="sat",
49
+ )
50
+
51
+
52
+ #: Default bands to use if none are specified. These are the bands
53
+ #: required by the SEVIRI (MIT) ash color scheme.
54
+ DEFAULT_BANDS = "C11", "C14", "C15"
55
+
56
+ #: The time at which the GOES scan mode changed from mode 3 to mode 6. This
57
+ #: is used to determine the scan time resolution.
58
+ #: See `GOES ABI scan information <https://www.goes-r.gov/users/abiScanModeInfo.html>`_.
59
+ GOES_SCAN_MODE_CHANGE = datetime.datetime(2019, 4, 2, 16)
60
+
61
+ #: The date at which GOES-19 data started being available. This is used to
62
+ #: determine the source (GOES-16 or GOES-19) of requested. In particular,
63
+ #: Mesoscale images are only available for GOES-East from GOES-19 after this date.
64
+ #: See the `NOAA press release <https://www.noaa.gov/news-release/noaas-goes-19-satellite-now-operational-providing-critical-new-data-to-forecasters>`_.
65
+ GOES_16_19_SWITCH_DATE = datetime.datetime(2025, 4, 4)
66
+
67
+ #: The GCS bucket for GOES-East data before ``GOES_16_19_SWITCH_DATE``.
68
+ GOES_16_BUCKET = "gcp-public-data-goes-16"
69
+
70
+ #: The GCS bucket for GOES-West data. Note that GOES-17 has degraded data quality
71
+ #: and is not recommended for use. This bucket isn't used by the ``GOES`` handler by default.
72
+ GOES_18_BUCKET = "gcp-public-data-goes-18"
73
+
74
+ #: The GCS bucket for GOES-East data after ``GOES_16_19_SWITCH_DATE``.
75
+ GOES_19_BUCKET = "gcp-public-data-goes-19"
76
+
77
+
78
+ class GOESRegion(enum.Enum):
79
+ """GOES Region of interest.
80
+
81
+ Uses the following conventions.
82
+
83
+ - F: Full Disk
84
+ - C: CONUS
85
+ - M1: Mesoscale 1
86
+ - M2: Mesoscale 2
87
+ """
88
+
89
+ F = enum.auto()
90
+ C = enum.auto()
91
+ M1 = enum.auto()
92
+ M2 = enum.auto()
93
+
94
+
95
+ def _check_time_resolution(t: datetime.datetime, region: GOESRegion) -> datetime.datetime:
96
+ """Confirm request t is at GOES scan time resolution."""
97
+ if t.second != 0 or t.microsecond != 0:
98
+ raise ValueError(
99
+ "Time must be at GOES scan time resolution. Seconds or microseconds not supported"
100
+ )
101
+
102
+ if region == GOESRegion.F:
103
+ # Full Disk: Scan times are available every 10 minutes after
104
+ # 2019-04-02 and every 15 minutes before
105
+ if t >= GOES_SCAN_MODE_CHANGE:
106
+ if t.minute % 10:
107
+ raise ValueError(
108
+ f"Time must be at GOES scan time resolution for {region}. "
109
+ f"After {GOES_SCAN_MODE_CHANGE}, time should be a multiple of 10 minutes."
110
+ )
111
+ elif t.minute % 15:
112
+ raise ValueError(
113
+ f"Time must be at GOES scan time resolution for {region}. "
114
+ f"Before {GOES_SCAN_MODE_CHANGE}, time should be a multiple of 15 minutes."
115
+ )
116
+ return t
117
+
118
+ if region == GOESRegion.C:
119
+ # CONUS: Scan times are every 5 minutes
120
+ if t.minute % 5:
121
+ raise ValueError(
122
+ f"Time must be at GOES scan time resolution for {region}. "
123
+ "Time should be a multiple of 5 minutes."
124
+ )
125
+ return t
126
+
127
+ return t
128
+
129
+
130
+ def _parse_bands(bands: str | Iterable[str] | None) -> set[str]:
131
+ """Check that the bands are valid and return as a set."""
132
+ if bands is None:
133
+ return set(DEFAULT_BANDS)
134
+
135
+ if isinstance(bands, str):
136
+ bands = (bands,)
137
+
138
+ available = {f"C{i:02d}" for i in range(1, 17)}
139
+ bands = {c.upper() for c in bands}
140
+ if not bands.issubset(available):
141
+ raise ValueError(f"Bands must be in {sorted(available)}")
142
+ return bands
143
+
144
+
145
+ def _check_band_resolution(bands: Iterable[str]) -> None:
146
+ """Confirm request bands have a common horizontal resolution."""
147
+ # https://www.goes-r.gov/spacesegment/abi.html
148
+ res = {
149
+ "C01": 1.0,
150
+ "C02": 1.0, # XXX: this actually has a resolution of 0.5 km, but we coarsen it to 1 km
151
+ "C03": 1.0,
152
+ "C04": 2.0,
153
+ "C05": 1.0,
154
+ "C06": 2.0,
155
+ "C07": 2.0,
156
+ "C08": 2.0,
157
+ "C09": 2.0,
158
+ "C10": 2.0,
159
+ "C11": 2.0,
160
+ "C12": 2.0,
161
+ "C13": 2.0,
162
+ "C14": 2.0,
163
+ "C15": 2.0,
164
+ "C16": 2.0,
165
+ }
166
+
167
+ found_res = {b: res[b] for b in bands}
168
+ unique_res = set(found_res.values())
169
+ if len(unique_res) > 1:
170
+ b0, r0 = found_res.popitem()
171
+ b1, r1 = next((b, r) for b, r in found_res.items() if r != r0)
172
+ raise ValueError(
173
+ "Bands must have a common horizontal resolution. "
174
+ f"Band {b0} has resolution {r0} km and band {b1} has resolution {r1} km."
175
+ )
176
+
177
+
178
+ def _parse_region(region: GOESRegion | str) -> GOESRegion:
179
+ """Parse region from string."""
180
+ if isinstance(region, GOESRegion):
181
+ return region
182
+
183
+ region = region.upper().replace(" ", "").replace("_", "")
184
+
185
+ if region in ("F", "FULL", "FULLDISK"):
186
+ return GOESRegion.F
187
+ if region in ("C", "CONUS", "CONTINENTAL"):
188
+ return GOESRegion.C
189
+ if region in ("M1", "MESO1", "MESOSCALE1"):
190
+ return GOESRegion.M1
191
+ if region in ("M2", "MESO2", "MESOSCALE2"):
192
+ return GOESRegion.M2
193
+ raise ValueError(f"Region must be one of {GOESRegion._member_names_} or their abbreviations")
194
+
195
+
196
+ def gcs_goes_path(
197
+ time: datetime.datetime,
198
+ region: GOESRegion,
199
+ bands: str | Iterable[str] | None = None,
200
+ bucket: str | None = None,
201
+ fs: gcsfs.GCSFileSystem | None = None,
202
+ ) -> list[str]:
203
+ """Return GCS paths to GOES data at the given time for the given region and bands.
204
+
205
+ Presently only supported for GOES data whose scan time minute coincides with
206
+ the minute of the time parameter.
207
+
208
+ Parameters
209
+ ----------
210
+ time : datetime.datetime
211
+ Time of GOES data. This should be a timezone-naive datetime object or an
212
+ ISO 8601 formatted string.
213
+ region : GOESRegion
214
+ GOES Region of interest.
215
+ bands : str | Iterable[str] | None, optional
216
+ Set of bands or bands for CMIP data. The 16 possible bands are
217
+ represented by the strings "C01" to "C16". For the SEVIRI ash color scheme,
218
+ set ``bands=("C11", "C14", "C15")``. For the true color scheme,
219
+ set ``bands=("C01", "C02", "C03")``. By default, the bands
220
+ required by the SEVIRI ash color scheme are used.
221
+ bucket : str | None
222
+ GCS bucket for GOES data. If None, the bucket is automatically
223
+ set to ``GOES_16_BUCKET`` if ``time`` is before
224
+ ``GOES_16_19_SWITCH_DATE`` and ``GOES_19_BUCKET`` otherwise.
225
+ fs : gcsfs.GCSFileSystem | None
226
+ GCS file system instance. If None, a default anonymous instance is created.
227
+
228
+ Returns
229
+ -------
230
+ list[str]
231
+ List of GCS paths to GOES data.
232
+
233
+ Examples
234
+ --------
235
+ >>> from pprint import pprint
236
+ >>> t = datetime.datetime(2023, 4, 3, 2, 10)
237
+
238
+ >>> paths = gcs_goes_path(t, GOESRegion.F, bands=("C11", "C12", "C13"))
239
+ >>> pprint(paths)
240
+ ['gcp-public-data-goes-16/ABI-L2-CMIPF/2023/093/02/OR_ABI-L2-CMIPF-M6C11_G16_s20230930210203_e20230930219511_c20230930219586.nc',
241
+ 'gcp-public-data-goes-16/ABI-L2-CMIPF/2023/093/02/OR_ABI-L2-CMIPF-M6C12_G16_s20230930210203_e20230930219516_c20230930219596.nc',
242
+ 'gcp-public-data-goes-16/ABI-L2-CMIPF/2023/093/02/OR_ABI-L2-CMIPF-M6C13_G16_s20230930210203_e20230930219523_c20230930219586.nc']
243
+
244
+ >>> paths = gcs_goes_path(t, GOESRegion.C, bands=("C11", "C12", "C13"))
245
+ >>> pprint(paths)
246
+ ['gcp-public-data-goes-16/ABI-L2-CMIPC/2023/093/02/OR_ABI-L2-CMIPC-M6C11_G16_s20230930211170_e20230930213543_c20230930214055.nc',
247
+ 'gcp-public-data-goes-16/ABI-L2-CMIPC/2023/093/02/OR_ABI-L2-CMIPC-M6C12_G16_s20230930211170_e20230930213551_c20230930214045.nc',
248
+ 'gcp-public-data-goes-16/ABI-L2-CMIPC/2023/093/02/OR_ABI-L2-CMIPC-M6C13_G16_s20230930211170_e20230930213557_c20230930214065.nc']
249
+
250
+ >>> t = datetime.datetime(2023, 4, 3, 2, 11)
251
+ >>> paths = gcs_goes_path(t, GOESRegion.M1, bands="C01")
252
+ >>> pprint(paths)
253
+ ['gcp-public-data-goes-16/ABI-L2-CMIPM/2023/093/02/OR_ABI-L2-CMIPM1-M6C01_G16_s20230930211249_e20230930211309_c20230930211386.nc']
254
+
255
+ >>> t = datetime.datetime(2025, 5, 4, 3, 2)
256
+ >>> paths = gcs_goes_path(t, GOESRegion.M2, bands="C01")
257
+ >>> pprint(paths)
258
+ ['gcp-public-data-goes-19/ABI-L2-CMIPM/2025/124/03/OR_ABI-L2-CMIPM2-M6C01_G19_s20251240302557_e20251240303014_c20251240303092.nc']
259
+
260
+ """
261
+ time = _check_time_resolution(time, region)
262
+ year = time.strftime("%Y")
263
+ yday = time.strftime("%j")
264
+ hour = time.strftime("%H")
265
+
266
+ sensor = "ABI" # Advanced Baseline Imager
267
+ level = "L2" # Level 2
268
+ product_name = "CMIP" # Cloud and Moisture Imagery
269
+ product = f"{sensor}-{level}-{product_name}{region.name[0]}"
270
+
271
+ if bucket is None:
272
+ bucket = GOES_16_BUCKET if time < GOES_16_19_SWITCH_DATE else GOES_19_BUCKET
273
+ else:
274
+ bucket = bucket.removeprefix("gs://")
275
+
276
+ path_prefix = f"gs://{bucket}/{product}/{year}/{yday}/{hour}/"
277
+
278
+ # https://www.goes-r.gov/users/abiScanModeInfo.html
279
+ mode = "M6" if time >= GOES_SCAN_MODE_CHANGE else "M3"
280
+
281
+ # Example name pattern
282
+ # OR_ABI-L1b-RadF-M3C02_G16_s20171671145342_e20171671156109_c20171671156144.nc
283
+ time_str = time.strftime("%Y%j%H%M")
284
+ if region == GOESRegion.F:
285
+ time_str = time_str[:-1] # might not work before 2019-04-02?
286
+ elif region == GOESRegion.C:
287
+ # Very crude -- assuming scan time ends with 1 or 6
288
+ if time_str.endswith("0"):
289
+ time_str = f"{time_str[:-1]}1"
290
+ elif time_str.endswith("5"):
291
+ time_str = f"{time_str[:-1]}6"
292
+
293
+ name_prefix = f"OR_{product[:-1]}{region.name}-{mode}"
294
+
295
+ try:
296
+ satellite_number = int(bucket[-2:]) # 16 or 18 or 19 -- this may fail for custom buckets
297
+ except (ValueError, IndexError) as exc:
298
+ msg = f"Bucket name {bucket} does not end with a valid satellite number."
299
+ raise ValueError(msg) from exc
300
+ name_suffix = f"_G{satellite_number}_s{time_str}*"
301
+
302
+ bands = _parse_bands(bands)
303
+
304
+ # It's faster to run a single glob with C?? then running a glob for
305
+ # each band. The downside is that we have to filter the results.
306
+ rpath = f"{path_prefix}{name_prefix}C??{name_suffix}"
307
+
308
+ fs = fs or gcsfs.GCSFileSystem(token="anon")
309
+ rpaths = fs.glob(rpath)
310
+
311
+ out = [r for r in rpaths if _extract_band_from_rpath(r) in bands]
312
+ if not out:
313
+ raise RuntimeError(f"No data found for {time} in {region} for bands {bands}")
314
+ return out
315
+
316
+
317
+ def _extract_band_from_rpath(rpath: str) -> str:
318
+ # Split at the separator between product name and mode
319
+ # This works for both M3 and M6
320
+ sep = "-M"
321
+ suffix = rpath.split(sep, maxsplit=1)[1]
322
+ return suffix[1:4]
323
+
324
+
325
+ class GOES:
326
+ """Support for GOES-16 data access via GCP.
327
+
328
+ This interface requires the ``gcsfs`` package.
329
+
330
+ Parameters
331
+ ----------
332
+ region : GOESRegion | str, optional
333
+ GOES Region of interest. Uses the following conventions.
334
+
335
+ - F: Full Disk
336
+ - C: CONUS
337
+ - M1: Mesoscale 1
338
+ - M2: Mesoscale 2
339
+
340
+ By default, Full Disk (F) is used.
341
+
342
+ bands : str | Iterable[str] | None
343
+ Set of bands or bands for CMIP data. The 16 possible bands are
344
+ represented by the strings "C01" to "C16". For the SEVIRI ash color scheme,
345
+ set ``bands=("C11", "C14", "C15")``. For the true color scheme,
346
+ set ``bands=("C01", "C02", "C03")``. By default, the bands
347
+ required by the SEVIRI ash color scheme are used. The bands must have
348
+ a common horizontal resolution. The resolutions are:
349
+
350
+ - C01: 1.0 km
351
+ - C02: 0.5 km (treated as 1.0 km)
352
+ - C03: 1.0 km
353
+ - C04: 2.0 km
354
+ - C05: 1.0 km
355
+ - C06 - C16: 2.0 km
356
+
357
+ cachestore : cache.CacheStore | None, optional
358
+ Cache store for GOES data. If None, data is downloaded directly into
359
+ memory. By default, a :class:`cache.DiskCacheStore` is used.
360
+ bucket : str | None, optional
361
+ GCP bucket for GOES data. If None, the default option, the bucket is automatically
362
+ set to ``GOES_16_BUCKET`` if the requested time is before
363
+ ``GOES_16_19_SWITCH_DATE`` and ``GOES_19_BUCKET`` otherwise.
364
+ The satellite number used for filename construction is derived from the
365
+ last two characters of this bucket name.
366
+
367
+ See Also
368
+ --------
369
+ GOESRegion
370
+ gcs_goes_path
371
+
372
+ Examples
373
+ --------
374
+ >>> goes = GOES(region="M1", bands=("C11", "C14"))
375
+ >>> da = goes.get("2021-04-03 02:10:00")
376
+ >>> da.shape
377
+ (2, 500, 500)
378
+
379
+ >>> da.dims
380
+ ('band_id', 'y', 'x')
381
+
382
+ >>> da.band_id.values
383
+ array([11, 14], dtype=int32)
384
+
385
+ >>> # Print out a sample of the data
386
+ >>> da.sel(band_id=11).isel(x=slice(0, 50, 10), y=slice(0, 50, 10)).values
387
+ array([[266.8644 , 265.50812, 271.5592 , 271.45486, 272.75897],
388
+ [250.53697, 273.28064, 273.80225, 270.77673, 274.8977 ],
389
+ [272.8633 , 272.65466, 271.5592 , 274.01093, 273.12415],
390
+ [274.16742, 274.11523, 276.5148 , 273.85443, 270.51593],
391
+ [274.84555, 275.15854, 272.60248, 270.67242, 272.23734]],
392
+ dtype=float32)
393
+
394
+ >>> # The data has been cached locally
395
+ >>> assert goes.cachestore.listdir()
396
+
397
+ >>> # Download GOES data directly into memory by setting cachestore=None
398
+ >>> goes = GOES(region="M2", bands=("C11", "C12", "C13"), cachestore=None)
399
+ >>> da = goes.get("2021-04-03 02:10:00")
400
+
401
+ >>> da.shape
402
+ (3, 500, 500)
403
+
404
+ >>> da.dims
405
+ ('band_id', 'y', 'x')
406
+
407
+ >>> da.band_id.values
408
+ array([11, 12, 13], dtype=int32)
409
+
410
+ >>> da.attrs["long_name"]
411
+ 'ABI L2+ Cloud and Moisture Imagery brightness temperature'
412
+
413
+ >>> da.sel(band_id=11).values
414
+ array([[251.31944, 249.59802, 249.65018, ..., 270.30725, 270.51593,
415
+ 269.83777],
416
+ [250.53697, 249.0242 , 249.12854, ..., 270.15076, 270.30725,
417
+ 269.73346],
418
+ [249.1807 , 249.33719, 251.99757, ..., 270.15076, 270.20294,
419
+ 268.7945 ],
420
+ ...,
421
+ [277.24512, 277.29727, 277.45377, ..., 274.42822, 274.11523,
422
+ 273.7501 ],
423
+ [277.24512, 277.45377, 278.18408, ..., 274.6369 , 274.01093,
424
+ 274.06308],
425
+ [276.8278 , 277.14078, 277.7146 , ..., 274.6369 , 273.9066 ,
426
+ 274.16742]], shape=(500, 500), dtype=float32)
427
+
428
+ """
429
+
430
+ __marker = object()
431
+
432
+ def __init__(
433
+ self,
434
+ region: GOESRegion | str = GOESRegion.F,
435
+ bands: str | Iterable[str] | None = None,
436
+ *,
437
+ channels: str | Iterable[str] | None = None, # deprecated alias for bands
438
+ cachestore: cache.CacheStore | None = __marker, # type: ignore[assignment]
439
+ bucket: str | None = None,
440
+ goes_bucket: str | None = None, # deprecated alias for bucket
441
+ ) -> None:
442
+ if channels is not None:
443
+ if bands is not None:
444
+ raise ValueError("Only one of channels or bands should be specified")
445
+ warnings.warn(
446
+ "The 'channels' parameter is deprecated and will be removed in a future release. "
447
+ "Use 'bands' instead.",
448
+ DeprecationWarning,
449
+ stacklevel=2,
450
+ )
451
+ bands = channels
452
+ if goes_bucket is not None:
453
+ if bucket is not None:
454
+ raise ValueError("Only one of goes_bucket or bucket should be specified")
455
+ warnings.warn(
456
+ "The 'goes_bucket' parameter is deprecated and will be removed in a future release."
457
+ "Use 'bucket' instead.",
458
+ DeprecationWarning,
459
+ stacklevel=2,
460
+ )
461
+ bucket = goes_bucket
462
+
463
+ self.region = _parse_region(region)
464
+ self.bands = _parse_bands(bands)
465
+ _check_band_resolution(self.bands)
466
+
467
+ self.bucket = bucket
468
+ self.fs = gcsfs.GCSFileSystem(token="anon")
469
+
470
+ if cachestore is self.__marker:
471
+ cache_root = cache._get_user_cache_dir()
472
+ cache_dir = f"{cache_root}/goes"
473
+ cachestore = cache.DiskCacheStore(cache_dir=cache_dir)
474
+ self.cachestore = cachestore
475
+
476
+ def __repr__(self) -> str:
477
+ """Return string representation."""
478
+ return (
479
+ f"GOES(region='{self.region.name}', bands={sorted(self.bands)}, bucket={self.bucket})"
480
+ )
481
+
482
+ def gcs_goes_path(self, time: datetime.datetime, bands: set[str] | None = None) -> list[str]:
483
+ """Return GCS paths to GOES data at given time.
484
+
485
+ Presently only supported for GOES data whose scan time minute coincides with
486
+ the minute of the time parameter.
487
+
488
+ Parameters
489
+ ----------
490
+ time : datetime.datetime
491
+ Time of GOES data.
492
+ bands : set[str] | None
493
+ Set of bands or bands for CMIP data. If None, the :attr:`bands`
494
+ attribute is used.
495
+
496
+ Returns
497
+ -------
498
+ list[str]
499
+ List of GCS paths to GOES data.
500
+ """
501
+ bands = bands or self.bands
502
+ return gcs_goes_path(time, self.region, bands, bucket=self.bucket, fs=self.fs)
503
+
504
+ def _lpaths(self, time: datetime.datetime) -> dict[str, str]:
505
+ """Construct names for local netcdf files using the :attr:`cachestore`.
506
+
507
+ Returns dictionary of the form ``{band: local_path}``.
508
+ """
509
+ if not self.cachestore:
510
+ raise ValueError("cachestore must be set to use _lpaths")
511
+
512
+ t_str = time.strftime("%Y%m%d%H%M")
513
+
514
+ out = {}
515
+ for band in self.bands:
516
+ if self.bucket:
517
+ name = f"{self.bucket}_{self.region.name}_{t_str}_{band}.nc"
518
+ else:
519
+ name = f"{self.region.name}_{t_str}_{band}.nc"
520
+
521
+ lpath = self.cachestore.path(name)
522
+ out[band] = lpath
523
+
524
+ return out
525
+
526
+ def get(self, time: datetime.datetime | str) -> xr.DataArray:
527
+ """Return GOES data at given time.
528
+
529
+ Parameters
530
+ ----------
531
+ time : datetime.datetime | str
532
+ Time of GOES data. This should be a timezone-naive datetime object
533
+ or an ISO 8601 formatted string.
534
+
535
+ Returns
536
+ -------
537
+ xr.DataArray
538
+ DataArray of GOES data with coordinates:
539
+
540
+ - band_id: Channel or band ID
541
+ - x: GOES x-coordinate
542
+ - y: GOES y-coordinate
543
+ """
544
+ t = pd.Timestamp(time).to_pydatetime()
545
+
546
+ if self.cachestore is not None:
547
+ return self._get_with_cache(t)
548
+ return self._get_without_cache(t)
549
+
550
+ def _get_with_cache(self, time: datetime.datetime) -> xr.DataArray:
551
+ """Download the GOES data to the :attr:`cachestore` at the given time."""
552
+ if self.cachestore is None:
553
+ raise ValueError("cachestore must be set to use _get_with_cache")
554
+
555
+ lpaths = self._lpaths(time)
556
+ bands_needed = {c for c, lpath in lpaths.items() if not self.cachestore.exists(lpath)}
557
+
558
+ if bands_needed:
559
+ rpaths = self.gcs_goes_path(time, bands_needed)
560
+ for rpath in rpaths:
561
+ band = _extract_band_from_rpath(rpath)
562
+ lpath = lpaths[band]
563
+ self.fs.get(rpath, lpath)
564
+
565
+ # Deal with the different spatial resolutions
566
+ kwargs = {
567
+ "concat_dim": "band",
568
+ "combine": "nested",
569
+ "data_vars": ["CMI"],
570
+ "compat": "override",
571
+ "coords": "minimal",
572
+ }
573
+ if len(lpaths) > 1 and "C02" in lpaths: # xr.open_mfdataset fails after pop if only 1 file
574
+ lpath02 = lpaths.pop("C02")
575
+ ds = xr.open_mfdataset(lpaths.values(), **kwargs).swap_dims(band="band_id") # type: ignore[arg-type]
576
+ da1 = ds.reset_coords()["CMI"]
577
+ da2 = xr.open_dataset(lpath02).reset_coords()["CMI"].expand_dims(band_id=[2])
578
+ da = (
579
+ geo_utils._coarsen_then_concat(da1, da2)
580
+ .sortby("band_id")
581
+ .assign_coords(t=ds["t"].values)
582
+ )
583
+ else:
584
+ ds = xr.open_mfdataset(lpaths.values(), **kwargs).swap_dims(band="band_id") # type: ignore[arg-type]
585
+ da = ds["CMI"].sortby("band_id")
586
+
587
+ # Attach some useful attrs -- only using goes_imager_projection currently
588
+ da.attrs["goes_imager_projection"] = ds.goes_imager_projection.attrs
589
+ da.attrs["geospatial_lat_lon_extent"] = ds.geospatial_lat_lon_extent.attrs
590
+
591
+ return da
592
+
593
+ def _get_without_cache(self, time: datetime.datetime) -> xr.DataArray:
594
+ """Download the GOES data into memory at the given time."""
595
+ rpaths = self.gcs_goes_path(time)
596
+
597
+ # Load into memory
598
+ data = self.fs.cat(rpaths)
599
+
600
+ da_dict = {}
601
+ for rpath, init_bytes in data.items():
602
+ band = _extract_band_from_rpath(rpath)
603
+ ds = _load_via_tempfile(init_bytes)
604
+
605
+ da = ds["CMI"]
606
+ da = da.expand_dims(band_id=ds["band_id"].values)
607
+ da_dict[band] = da
608
+
609
+ if len(da_dict) > 1 and "C02" in da_dict: # xr.concat fails after pop if only 1 file
610
+ da2 = da_dict.pop("C02")
611
+ da1 = xr.concat(da_dict.values(), dim="band_id", coords="different", compat="equals")
612
+ da = geo_utils._coarsen_then_concat(da1, da2)
613
+ else:
614
+ da = xr.concat(da_dict.values(), dim="band_id", coords="different", compat="equals")
615
+
616
+ da = da.sortby("band_id")
617
+
618
+ # Attach some useful attrs -- only using goes_imager_projection currently
619
+ da.attrs["goes_imager_projection"] = ds.goes_imager_projection.attrs
620
+ da.attrs["geospatial_lat_lon_extent"] = ds.geospatial_lat_lon_extent.attrs
621
+
622
+ return da
623
+
624
+
625
+ def _load_via_tempfile(data: bytes) -> xr.Dataset:
626
+ """Load xarray dataset via temporary file."""
627
+ with tempfile.NamedTemporaryFile(delete=False) as tmp:
628
+ tmp.write(data)
629
+ try:
630
+ return xr.load_dataset(tmp.name)
631
+ finally:
632
+ os.remove(tmp.name)
633
+
634
+
635
+ def _cartopy_crs(proj_info: dict[str, Any]) -> cartopy.crs.Geostationary:
636
+ try:
637
+ from cartopy import crs as ccrs
638
+ except ModuleNotFoundError as exc:
639
+ dependencies.raise_module_not_found_error(
640
+ name="GOES visualization",
641
+ package_name="cartopy",
642
+ module_not_found_error=exc,
643
+ pycontrails_optional_package="sat",
644
+ )
645
+
646
+ globe = ccrs.Globe(
647
+ semimajor_axis=proj_info["semi_major_axis"],
648
+ semiminor_axis=proj_info["semi_minor_axis"],
649
+ )
650
+ return ccrs.Geostationary(
651
+ central_longitude=proj_info["longitude_of_projection_origin"],
652
+ satellite_height=proj_info["perspective_point_height"],
653
+ sweep_axis=proj_info["sweep_angle_axis"],
654
+ globe=globe,
655
+ )
656
+
657
+
658
+ def extract_visualization(
659
+ da: xr.DataArray,
660
+ color_scheme: str = "ash",
661
+ ash_convention: str = "SEVIRI",
662
+ gamma: float = 2.2,
663
+ ) -> tuple[npt.NDArray[np.float32], cartopy.crs.Geostationary, tuple[float, float, float, float]]:
664
+ """Extract artifacts for visualizing GOES data with the given color scheme.
665
+
666
+ Parameters
667
+ ----------
668
+ da : xr.DataArray
669
+ DataArray of GOES data as returned by :meth:`GOES.get`. Must have the bands
670
+ required by :func:`to_ash`.
671
+ color_scheme : str
672
+ Color scheme to use for visualization. Must be one of {"true", "ash"}.
673
+ If "true", the ``da`` must contain bands C01, C02, and C03.
674
+ If "ash", the ``da`` must contain bands C11, C14, and C15 (SEVIRI convention)
675
+ or bands C11, C13, C14, and C15 (standard convention).
676
+ ash_convention : str
677
+ Passed into :func:`to_ash`. Only used if ``color_scheme="ash"``. Must be one
678
+ of {"SEVIRI", "standard"}. By default, "SEVIRI" is used.
679
+ gamma : float
680
+ Passed into :func:`to_true_color`. Only used if ``color_scheme="true"``. By
681
+ default, 2.2 is used.
682
+
683
+ Returns
684
+ -------
685
+ rgb : npt.NDArray[np.float32]
686
+ 3D RGB array of shape ``(height, width, 3)``. Any nan values are replaced with 0.
687
+ src_crs : cartopy.crs.Geostationary
688
+ The Geostationary projection built from the GOES metadata.
689
+ src_extent : tuple[float, float, float, float]
690
+ Extent of GOES data in the Geostationary projection
691
+ """
692
+ proj_info = da.attrs["goes_imager_projection"]
693
+ h = proj_info["perspective_point_height"]
694
+
695
+ src_crs = _cartopy_crs(proj_info)
696
+
697
+ if color_scheme == "true":
698
+ rgb = to_true_color(da, gamma)
699
+ elif color_scheme == "ash":
700
+ rgb = to_ash(da, ash_convention)
701
+ else:
702
+ raise ValueError(f"Color scheme must be 'true' or 'ash', not '{color_scheme}'")
703
+
704
+ np.nan_to_num(rgb, copy=False)
705
+
706
+ x = da["x"].values
707
+ y = da["y"].values
708
+
709
+ # Multiply extremes by the satellite height
710
+ src_extent = h * x.min(), h * x.max(), h * y.min(), h * y.max()
711
+
712
+ return rgb, src_crs, src_extent
713
+
714
+
715
+ extract_goes_visualization = extract_visualization # keep for backwards compatibility
716
+
717
+
718
+ def to_true_color(da: xr.DataArray, gamma: float = 2.2) -> npt.NDArray[np.float32]:
719
+ """Compute 3d RGB array for the true color scheme.
720
+
721
+ Parameters
722
+ ----------
723
+ da : xr.DataArray
724
+ DataArray of GOES data with bands C01, C02, C03.
725
+ gamma : float, optional
726
+ Gamma correction for the RGB bands.
727
+
728
+ Returns
729
+ -------
730
+ npt.NDArray[np.float32]
731
+ 3d RGB array with true color scheme.
732
+
733
+ References
734
+ ----------
735
+ - `Unidata's true color recipe <https://unidata.github.io/python-gallery/examples/mapping_GOES16_TrueColor.html>`_
736
+ """
737
+ if not np.all(np.isin([1, 2, 3], da["band_id"])):
738
+ msg = "DataArray must contain bands 1, 2, and 3 for true color"
739
+ raise ValueError(msg)
740
+
741
+ red = da.sel(band_id=2).values
742
+ veggie = da.sel(band_id=3).values
743
+ blue = da.sel(band_id=1).values
744
+
745
+ red = geo_utils._clip_and_scale(red, 0.0, 1.0)
746
+ veggie = geo_utils._clip_and_scale(veggie, 0.0, 1.0)
747
+ blue = geo_utils._clip_and_scale(blue, 0.0, 1.0)
748
+
749
+ red = red ** (1 / gamma)
750
+ veggie = veggie ** (1 / gamma)
751
+ blue = blue ** (1 / gamma)
752
+
753
+ # Calculate synthetic green band
754
+ green = 0.45 * red + 0.1 * veggie + 0.45 * blue
755
+ green = geo_utils._clip_and_scale(green, 0.0, 1.0)
756
+
757
+ return np.dstack([red, green, blue])