pycontrails 0.58.0__cp314-cp314-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/__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 +2931 -0
- pycontrails/core/met_var.py +387 -0
- pycontrails/core/models.py +1321 -0
- pycontrails/core/polygon.py +549 -0
- pycontrails/core/rgi_cython.cp314-win_amd64.pyd +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 +757 -0
- pycontrails/datalib/himawari/__init__.py +27 -0
- pycontrails/datalib/himawari/header_struct.py +266 -0
- pycontrails/datalib/himawari/himawari.py +667 -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.58.0.dist-info/METADATA +180 -0
- pycontrails-0.58.0.dist-info/RECORD +122 -0
- pycontrails-0.58.0.dist-info/WHEEL +5 -0
- pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
- pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
- pycontrails-0.58.0.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Tools for searching for low Earth orbit satellite imagery."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import pathlib
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from pycontrails.core import Flight
|
|
10
|
+
from pycontrails.utils import dependencies
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import geojson
|
|
14
|
+
except ModuleNotFoundError as exc:
|
|
15
|
+
dependencies.raise_module_not_found_error(
|
|
16
|
+
name="datalib.leo_utils module",
|
|
17
|
+
package_name="geojson",
|
|
18
|
+
module_not_found_error=exc,
|
|
19
|
+
pycontrails_optional_package="sat",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_path_to_static = pathlib.Path(__file__).parent / "static"
|
|
23
|
+
ROI_QUERY_FILENAME = _path_to_static / "bq_roi_query.sql"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
#: GeoJSON polygon that covers the entire globe.
|
|
27
|
+
GLOBAL_EXTENT = geojson.dumps(
|
|
28
|
+
geojson.Polygon([[(-180, -90), (180, -90), (180, 90), (-180, 90), (-180, -90)]])
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclasses.dataclass
|
|
33
|
+
class ROI:
|
|
34
|
+
"""Spatiotemporal region of interest."""
|
|
35
|
+
|
|
36
|
+
#: Start time
|
|
37
|
+
start_time: np.datetime64
|
|
38
|
+
|
|
39
|
+
#: End time
|
|
40
|
+
end_time: np.datetime64
|
|
41
|
+
|
|
42
|
+
#: GeoJSON representation of spatial ROI.
|
|
43
|
+
extent: str
|
|
44
|
+
|
|
45
|
+
def __post_init__(self) -> None:
|
|
46
|
+
"""Validate region of interest."""
|
|
47
|
+
if self.start_time > self.end_time:
|
|
48
|
+
msg = "start_time must be before end_time"
|
|
49
|
+
raise ValueError(msg)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
decoded = geojson.Feature(geometry=geojson.loads(self.extent))
|
|
53
|
+
except Exception as exc:
|
|
54
|
+
msg = "extent cannot be converted to GeoJSON structure"
|
|
55
|
+
raise ValueError(msg) from exc
|
|
56
|
+
if not decoded.is_valid:
|
|
57
|
+
msg = "extent is not valid GeoJSON"
|
|
58
|
+
raise ValueError(msg)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def track_to_geojson(flight: Flight) -> str:
|
|
62
|
+
"""Convert ground track to GeoJSON string, splitting at antimeridian crossings.
|
|
63
|
+
|
|
64
|
+
Coordinates contain longitude and latitude only (no altitude coordinate)
|
|
65
|
+
and are padded to terminate and restart exactly at the antimeridian when
|
|
66
|
+
antimeridian crossings are encountered.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
flight : Flight
|
|
71
|
+
Flight with ground track to convert to GeoJSON string.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
str
|
|
76
|
+
String encoding of a GeoJSON MultiLineString containing ground track split at
|
|
77
|
+
antimeridian crossings.
|
|
78
|
+
|
|
79
|
+
See Also
|
|
80
|
+
--------
|
|
81
|
+
:meth:`Flight.to_geojson_multilinestring`
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
# Logic assumes longitudes are between -180 and 180.
|
|
85
|
+
# Raise an error if this is not the case.
|
|
86
|
+
if np.abs(flight["longitude"]).max() > 180.0:
|
|
87
|
+
msg = "Flight longitudes must be between -180 and 180."
|
|
88
|
+
raise ValueError(msg)
|
|
89
|
+
|
|
90
|
+
# Get feature collection containing a single multilinestring
|
|
91
|
+
# split at antimeridian crossings
|
|
92
|
+
fc = flight.to_geojson_multilinestring(split_antimeridian=True)
|
|
93
|
+
|
|
94
|
+
# Extract multilinestring
|
|
95
|
+
mls = fc["features"][0]["geometry"]
|
|
96
|
+
|
|
97
|
+
# Strip altitude coordinates
|
|
98
|
+
coords = [[[c[0], c[1]] for c in linestring] for linestring in mls["coordinates"]]
|
|
99
|
+
|
|
100
|
+
# No padding required if no antimeridian crossings were encountered
|
|
101
|
+
if len(coords) == 1:
|
|
102
|
+
return geojson.dumps(geojson.MultiLineString(coords))
|
|
103
|
+
|
|
104
|
+
# Pad at crossings
|
|
105
|
+
for i in range(len(coords) - 1):
|
|
106
|
+
x0 = coords[i][-1][0]
|
|
107
|
+
x1 = coords[i + 1][0][0]
|
|
108
|
+
if abs(x0) == 180.0 and abs(x1) == 180.0:
|
|
109
|
+
continue
|
|
110
|
+
y0 = coords[i][-1][1]
|
|
111
|
+
y1 = coords[i + 1][0][1]
|
|
112
|
+
xl = 180.0 * np.sign(x0)
|
|
113
|
+
xr = 180.0 * np.sign(x1)
|
|
114
|
+
w0 = np.abs(xr - x1)
|
|
115
|
+
w1 = np.abs(xl - x0)
|
|
116
|
+
yc = (w0 * y0 + w1 * y1) / (w0 + w1)
|
|
117
|
+
if abs(x0) < 180.0:
|
|
118
|
+
coords[i].append([xl, yc])
|
|
119
|
+
if abs(x1) < 180.0:
|
|
120
|
+
coords[i + 1].insert(0, [xr, yc])
|
|
121
|
+
|
|
122
|
+
# Encode as string
|
|
123
|
+
return geojson.dumps(geojson.MultiLineString(coords))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def query(table: str, roi: ROI, columns: list[str], extra_filters: str = "") -> pd.DataFrame:
|
|
127
|
+
"""Find satellite imagery within region of interest.
|
|
128
|
+
|
|
129
|
+
This function requires access to the
|
|
130
|
+
`Google BigQuery API <https://cloud.google.com/bigquery?hl=en>`__
|
|
131
|
+
and uses the `BigQuery python library <https://cloud.google.com/python/docs/reference/bigquery/latest/index.html>`__.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
table : str
|
|
136
|
+
Name of BigQuery table to query
|
|
137
|
+
roi : ROI
|
|
138
|
+
Region of interest
|
|
139
|
+
columns : list[str]
|
|
140
|
+
Columns to return from Google
|
|
141
|
+
`BigQuery table <https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=cloud_storage_geo_index&t=landsat_index&page=table&_ga=2.90807450.1051800793.1716904050-255800408.1705955196>`__.
|
|
142
|
+
extra_filters : str, optional
|
|
143
|
+
Additional selection filters, injected verbatim into constructed query.
|
|
144
|
+
|
|
145
|
+
Returns
|
|
146
|
+
-------
|
|
147
|
+
pd.DataFrame
|
|
148
|
+
Query results in pandas DataFrame
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
from google.cloud import bigquery
|
|
152
|
+
except ModuleNotFoundError as exc:
|
|
153
|
+
dependencies.raise_module_not_found_error(
|
|
154
|
+
name="landsat module",
|
|
155
|
+
package_name="google-cloud-bigquery",
|
|
156
|
+
module_not_found_error=exc,
|
|
157
|
+
pycontrails_optional_package="landsat",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if len(columns) == 0:
|
|
161
|
+
msg = "At least column must be provided."
|
|
162
|
+
raise ValueError(msg)
|
|
163
|
+
|
|
164
|
+
start_time = pd.Timestamp(roi.start_time).strftime("%Y-%m-%d %H:%M:%S")
|
|
165
|
+
end_time = pd.Timestamp(roi.end_time).strftime("%Y-%m-%d %H:%M:%S")
|
|
166
|
+
extent = roi.extent.replace('"', "'")
|
|
167
|
+
|
|
168
|
+
client = bigquery.Client()
|
|
169
|
+
with open(ROI_QUERY_FILENAME) as f:
|
|
170
|
+
query_str = f.read().format(
|
|
171
|
+
table=table,
|
|
172
|
+
columns=",".join(columns),
|
|
173
|
+
start_time=start_time,
|
|
174
|
+
end_time=end_time,
|
|
175
|
+
geojson_str=extent,
|
|
176
|
+
extra_filters=extra_filters,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
result = client.query(query_str)
|
|
180
|
+
return result.to_dataframe()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def intersect(
|
|
184
|
+
table: str, flight: Flight, columns: list[str], extra_filters: str = ""
|
|
185
|
+
) -> pd.DataFrame:
|
|
186
|
+
"""Find satellite imagery intersecting with flight track.
|
|
187
|
+
|
|
188
|
+
This function will return all scenes with a bounding box that includes flight waypoints
|
|
189
|
+
both before and after the sensing time.
|
|
190
|
+
|
|
191
|
+
This function requires access to the
|
|
192
|
+
`Google BigQuery API <https://cloud.google.com/bigquery?hl=en>`__
|
|
193
|
+
and uses the `BigQuery python library <https://cloud.google.com/python/docs/reference/bigquery/latest/index.html>`__.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
table : str
|
|
198
|
+
Name of BigQuery table to query
|
|
199
|
+
flight : Flight
|
|
200
|
+
Flight for intersection
|
|
201
|
+
columns : list[str]
|
|
202
|
+
Columns to return from Google
|
|
203
|
+
`BigQuery table <https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=cloud_storage_geo_index&t=landsat_index&page=table&_ga=2.90807450.1051800793.1716904050-255800408.1705955196>`__.
|
|
204
|
+
extra_filters : str, optional
|
|
205
|
+
Additional selection filters, injected verbatim into constructed query.
|
|
206
|
+
|
|
207
|
+
Returns
|
|
208
|
+
-------
|
|
209
|
+
pd.DataFrame
|
|
210
|
+
Query results in pandas DataFrame
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
# create ROI with time span between flight start and end
|
|
214
|
+
# and spatial extent set to flight track
|
|
215
|
+
extent = track_to_geojson(flight)
|
|
216
|
+
roi = ROI(start_time=flight["time"].min(), end_time=flight["time"].max(), extent=extent)
|
|
217
|
+
|
|
218
|
+
# first pass: query for intersections with ROI
|
|
219
|
+
# requires additional columns for final intersection with flight
|
|
220
|
+
required_columns = set(["sensing_time", "west_lon", "east_lon", "south_lat", "north_lat"])
|
|
221
|
+
queried_columns = list(required_columns.union(set(columns)))
|
|
222
|
+
candidates = query(table, roi, queried_columns, extra_filters)
|
|
223
|
+
|
|
224
|
+
if len(candidates) == 0: # already know there are no intersections
|
|
225
|
+
return candidates[columns]
|
|
226
|
+
|
|
227
|
+
# second pass: keep images with where flight waypoints
|
|
228
|
+
# bounding sensing time are both within bounding box
|
|
229
|
+
flight_data = flight.dataframe
|
|
230
|
+
|
|
231
|
+
def intersects(scene: pd.Series) -> bool:
|
|
232
|
+
if scene["west_lon"] <= scene["east_lon"]: # scene does not span antimeridian
|
|
233
|
+
bbox_data = flight_data[
|
|
234
|
+
flight_data["longitude"].between(scene["west_lon"], scene["east_lon"])
|
|
235
|
+
& flight_data["latitude"].between(scene["south_lat"], scene["north_lat"])
|
|
236
|
+
]
|
|
237
|
+
else: # scene spans antimeridian
|
|
238
|
+
bbox_data = flight_data[
|
|
239
|
+
(
|
|
240
|
+
flight_data["longitude"]
|
|
241
|
+
> scene["west_lon"] | flight.data["longitude"]
|
|
242
|
+
< scene["east_lon"]
|
|
243
|
+
)
|
|
244
|
+
& flight_data["latitude"].between(scene["south_lat"], scene["north_lat"])
|
|
245
|
+
]
|
|
246
|
+
sensing_time = pd.Timestamp(scene["sensing_time"]).tz_localize(None)
|
|
247
|
+
return bbox_data["time"].min() <= sensing_time and bbox_data["time"].max() >= sensing_time
|
|
248
|
+
|
|
249
|
+
mask = candidates.apply(intersects, axis="columns")
|
|
250
|
+
return candidates[columns][mask]
|