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.
- 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.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 +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 +6 -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,268 @@
|
|
|
1
|
+
"""ECMWF Parameter Support.
|
|
2
|
+
|
|
3
|
+
Sourced from the ECMWF Parameter DB:
|
|
4
|
+
https://apps.ecmwf.int/codes/grib/param-db
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pycontrails.core import met_var
|
|
10
|
+
from pycontrails.core.met import MetVariable
|
|
11
|
+
|
|
12
|
+
RelativeVorticity = MetVariable(
|
|
13
|
+
short_name="vo",
|
|
14
|
+
standard_name="atmosphere_upward_relative_vorticity",
|
|
15
|
+
long_name="Vorticity (relative)",
|
|
16
|
+
units="s**-1",
|
|
17
|
+
level_type="isobaricInhPa",
|
|
18
|
+
grib1_id=43,
|
|
19
|
+
ecmwf_id=138,
|
|
20
|
+
grib2_id=(0, 2, 12),
|
|
21
|
+
description=(
|
|
22
|
+
"Atmosphere upward relative vorticity is the vertical component of the 3D air vorticity"
|
|
23
|
+
" vector. The vertical component arises from horizontal velocity only. 'Relative' in this"
|
|
24
|
+
" context means the vorticity of the air relative to the rotating solid earth reference"
|
|
25
|
+
" frame, i.e. excluding the Earth's own rotation. In contrast, the quantity with standard"
|
|
26
|
+
" name atmosphere_upward_absolute_vorticity includes the Earth's rotation. 'Upward'"
|
|
27
|
+
" indicates a vector component which is positive when directed upward (negative downward)."
|
|
28
|
+
" A positive value of atmosphere_upward_relative_vorticity indicates anticlockwise rotation"
|
|
29
|
+
" when viewed from above."
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
PotentialVorticity = MetVariable(
|
|
34
|
+
short_name="pv",
|
|
35
|
+
standard_name="potential_vorticity",
|
|
36
|
+
long_name="Potential vorticity (K m^2 / kg s)",
|
|
37
|
+
units="K m**2 kg**-1 s**-1",
|
|
38
|
+
level_type="isobaricInhPa",
|
|
39
|
+
grib1_id=128,
|
|
40
|
+
ecmwf_id=60,
|
|
41
|
+
grib2_id=(0, 2, 14),
|
|
42
|
+
amip="pvu",
|
|
43
|
+
description=(
|
|
44
|
+
"Potential vorticity is a measure of the capacity for air to rotate in the atmosphere.If we"
|
|
45
|
+
" ignore the effects of heating and friction, potential vorticity is conserved following an"
|
|
46
|
+
" air parcel.It is used to look for places where large wind storms are likely to originate"
|
|
47
|
+
" and develop.Potential vorticity increases strongly above the tropopause and therefore, it"
|
|
48
|
+
" can also be used in studiesrelated to the stratosphere and stratosphere-troposphere"
|
|
49
|
+
" exchanges. Large wind storms develop when a columnof air in the atmosphere starts to"
|
|
50
|
+
" rotate. Potential vorticity is calculated from the wind, temperature andpressure across a"
|
|
51
|
+
" column of air in the atmosphere."
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
CloudAreaFractionInLayer = MetVariable(
|
|
56
|
+
short_name="cc",
|
|
57
|
+
standard_name="fraction_of_cloud_cover",
|
|
58
|
+
long_name="Cloud area fraction in atmosphere layer",
|
|
59
|
+
ecmwf_id=248,
|
|
60
|
+
level_type="isobaricInhPa",
|
|
61
|
+
grib2_id=(0, 6, 32),
|
|
62
|
+
units="[0 - 1]",
|
|
63
|
+
amip="cl",
|
|
64
|
+
description=(
|
|
65
|
+
"This parameter is the proportion of a grid box covered by cloud (liquid or ice) at a"
|
|
66
|
+
" specific pressure level."
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
SpecificCloudLiquidWaterContent = MetVariable(
|
|
71
|
+
short_name="clwc",
|
|
72
|
+
standard_name="specific_cloud_liquid_water_content",
|
|
73
|
+
long_name="Specific cloud liquid water content",
|
|
74
|
+
units="kg kg**-1",
|
|
75
|
+
level_type="isobaricInhPa",
|
|
76
|
+
ecmwf_id=246,
|
|
77
|
+
grib2_id=(0, 1, 83),
|
|
78
|
+
description=(
|
|
79
|
+
"This parameter is the mass of cloud liquid water droplets per kilogram of the total mass"
|
|
80
|
+
" of moist air. The 'total mass of moist air' is the sum of the dry air, water vapour,"
|
|
81
|
+
" cloud liquid, cloud ice, rain and falling snow. This parameter represents the average"
|
|
82
|
+
" value for a grid box."
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
SpecificCloudIceWaterContent = MetVariable(
|
|
88
|
+
short_name="ciwc",
|
|
89
|
+
standard_name="specific_cloud_ice_water_content",
|
|
90
|
+
long_name="Specific cloud ice water content",
|
|
91
|
+
units="kg kg**-1",
|
|
92
|
+
level_type="isobaricInhPa",
|
|
93
|
+
ecmwf_id=247,
|
|
94
|
+
grib2_id=(0, 1, 84),
|
|
95
|
+
description=(
|
|
96
|
+
"This parameter is the mass of cloud ice particles per kilogram of the total mass of moist"
|
|
97
|
+
" air. The 'total mass of moist air' is the sum of the dry air, water vapour, cloud liquid,"
|
|
98
|
+
" cloud ice, rain and falling snow. This parameter represents the average value for a grid"
|
|
99
|
+
" box."
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Override units and description on Relative humidity
|
|
104
|
+
RelativeHumidity = MetVariable(
|
|
105
|
+
short_name=met_var.RelativeHumidity.short_name,
|
|
106
|
+
standard_name=met_var.RelativeHumidity.standard_name,
|
|
107
|
+
long_name=met_var.RelativeHumidity.long_name,
|
|
108
|
+
units="%",
|
|
109
|
+
level_type=met_var.RelativeHumidity.level_type,
|
|
110
|
+
grib1_id=met_var.RelativeHumidity.grib1_id,
|
|
111
|
+
ecmwf_id=met_var.RelativeHumidity.ecmwf_id,
|
|
112
|
+
grib2_id=met_var.RelativeHumidity.grib2_id,
|
|
113
|
+
description=(
|
|
114
|
+
"This parameter is the water vapour pressure as a percentage of the value at which the air"
|
|
115
|
+
" becomes saturated "
|
|
116
|
+
"(the point at which water vapour begins to condense into liquid water or deposition into"
|
|
117
|
+
" ice)."
|
|
118
|
+
"For temperatures over 0°C (273.15 K) it is calculated for saturation over water. "
|
|
119
|
+
"At temperatures below -23°C it is calculated for saturation over ice. "
|
|
120
|
+
"Between -23°C and 0°C this parameter is calculated by interpolating between the ice and"
|
|
121
|
+
" water values using a quadratic function."
|
|
122
|
+
"See https://www.ecmwf.int/sites/default/files/elibrary/2016/17117-part-iv-physical-processes.pdf#subsection.7.4.2"
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
Divergence = MetVariable(
|
|
128
|
+
short_name="d",
|
|
129
|
+
standard_name="divergence_of_wind",
|
|
130
|
+
long_name="Divergence of wind",
|
|
131
|
+
units="s**-1",
|
|
132
|
+
level_type="isobaricInhPa",
|
|
133
|
+
ecmwf_id=155,
|
|
134
|
+
grib2_id=(0, 2, 13),
|
|
135
|
+
description=(
|
|
136
|
+
"This parameter is the horizontal divergence of velocity. It is the rate "
|
|
137
|
+
"at which air is spreading out horizontally from a point, per square metre. "
|
|
138
|
+
"This parameter is positive for air that is spreading out, or diverging, and "
|
|
139
|
+
"negative for air that is moving inward toward a point, or converging."
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
TOAIncidentSolarRadiation = MetVariable(
|
|
145
|
+
short_name="tisr",
|
|
146
|
+
standard_name="toa_incident_solar_radiation",
|
|
147
|
+
long_name="Top of atmosphere incident shortwave radiation",
|
|
148
|
+
units="J m**-2",
|
|
149
|
+
level_type="nominalTop",
|
|
150
|
+
ecmwf_id=212,
|
|
151
|
+
grib2_id=(192, 128, 212), # reference ECMWF
|
|
152
|
+
description="Top of atmosphere incident solar radiation. Accumulated field.",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
TopNetSolarRadiation = MetVariable(
|
|
156
|
+
short_name="tsr",
|
|
157
|
+
standard_name="top_net_solar_radiation",
|
|
158
|
+
long_name="Top of atmosphere net solar (shortwave) radiation",
|
|
159
|
+
units="J m**-2",
|
|
160
|
+
level_type="nominalTop",
|
|
161
|
+
ecmwf_id=178,
|
|
162
|
+
grib2_id=(0, 4, 1),
|
|
163
|
+
description=(
|
|
164
|
+
"This parameter is the incoming solar radiation (also known as shortwave radiation) "
|
|
165
|
+
"minus the outgoing solar radiation at the top of the atmosphere. "
|
|
166
|
+
"It is the amount of radiation passing through a horizontal plane. "
|
|
167
|
+
"The incoming solar radiation is the amount received from the Sun. "
|
|
168
|
+
"The outgoing solar radiation is the amount reflected and scattered by the Earth's"
|
|
169
|
+
" atmosphere and surface"
|
|
170
|
+
"See https://www.ecmwf.int/sites/default/files/elibrary/2015/18490-radiation-quantities-ecmwf-model-and-mars.pdf"
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
TopNetThermalRadiation = MetVariable(
|
|
175
|
+
short_name="ttr",
|
|
176
|
+
standard_name="top_net_thermal_radiation",
|
|
177
|
+
long_name="Top of atmosphere net thermal (longwave) radiation",
|
|
178
|
+
units="J m**-2",
|
|
179
|
+
level_type="nominalTop",
|
|
180
|
+
ecmwf_id=179,
|
|
181
|
+
grib2_id=(0, 5, 5),
|
|
182
|
+
description=(
|
|
183
|
+
"The thermal (also known as terrestrial or longwave) "
|
|
184
|
+
"radiation emitted to space at the top of the atmosphere is commonly known as the Outgoing"
|
|
185
|
+
" Longwave Radiation (OLR). "
|
|
186
|
+
"The top net thermal radiation (this parameter) is equal to the negative of OLR."
|
|
187
|
+
"See https://www.ecmwf.int/sites/default/files/elibrary/2015/18490-radiation-quantities-ecmwf-model-and-mars.pdf"
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
SurfaceSolarDownwardRadiation = MetVariable(
|
|
192
|
+
short_name="ssrd",
|
|
193
|
+
standard_name="surface_solar_downward_radiation",
|
|
194
|
+
long_name="Surface Solar Downward Radiation",
|
|
195
|
+
units="J m**-2",
|
|
196
|
+
level_type="surface",
|
|
197
|
+
ecmwf_id=169,
|
|
198
|
+
grib2_id=(0, 4, 7),
|
|
199
|
+
description=(
|
|
200
|
+
"This parameter is the amount of solar radiation (also known as shortwave radiation) that"
|
|
201
|
+
" reaches a horizontal plane at the surface of the Earth. This parameter comprises both"
|
|
202
|
+
" direct and diffuse solar radiation."
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
CloudAreaFraction = MetVariable(
|
|
207
|
+
short_name="tcc",
|
|
208
|
+
standard_name="total_cloud_cover",
|
|
209
|
+
long_name="Cloud area fraction (total)",
|
|
210
|
+
level_type="surface",
|
|
211
|
+
grib1_id=71,
|
|
212
|
+
ecmwf_id=164,
|
|
213
|
+
grib2_id=(192, 128, 164), # reference ECMWF
|
|
214
|
+
units="[0 - 1]",
|
|
215
|
+
amip="clt",
|
|
216
|
+
description=(
|
|
217
|
+
"This parameter is the proportion of a grid box covered by cloud (liquid or ice) for a"
|
|
218
|
+
" whole atmosphere column."
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
OzoneMassMixingRatio = MetVariable(
|
|
223
|
+
short_name="o3",
|
|
224
|
+
standard_name="mass_fraction_of_ozone_in_air",
|
|
225
|
+
long_name="Ozone mass mixing ratio",
|
|
226
|
+
level_type="isobaricInhPa",
|
|
227
|
+
units="kg kg**-1",
|
|
228
|
+
ecmwf_id=203,
|
|
229
|
+
description="This parameter is the mass of ozone per kilogram of air.",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
PRESSURE_LEVEL_VARIABLES = [
|
|
234
|
+
met_var.AirTemperature,
|
|
235
|
+
met_var.SpecificHumidity,
|
|
236
|
+
met_var.Geopotential,
|
|
237
|
+
met_var.EastwardWind,
|
|
238
|
+
met_var.NorthwardWind,
|
|
239
|
+
met_var.VerticalVelocity,
|
|
240
|
+
RelativeHumidity,
|
|
241
|
+
RelativeVorticity,
|
|
242
|
+
CloudAreaFractionInLayer,
|
|
243
|
+
SpecificCloudIceWaterContent,
|
|
244
|
+
SpecificCloudLiquidWaterContent,
|
|
245
|
+
PotentialVorticity,
|
|
246
|
+
]
|
|
247
|
+
SURFACE_VARIABLES = [
|
|
248
|
+
met_var.SurfacePressure,
|
|
249
|
+
TOAIncidentSolarRadiation,
|
|
250
|
+
TopNetSolarRadiation,
|
|
251
|
+
TopNetThermalRadiation,
|
|
252
|
+
CloudAreaFraction,
|
|
253
|
+
SurfaceSolarDownwardRadiation,
|
|
254
|
+
]
|
|
255
|
+
MODEL_LEVEL_VARIABLES = [
|
|
256
|
+
met_var.AirTemperature,
|
|
257
|
+
met_var.SpecificHumidity,
|
|
258
|
+
met_var.VerticalVelocity,
|
|
259
|
+
met_var.EastwardWind,
|
|
260
|
+
met_var.NorthwardWind,
|
|
261
|
+
RelativeVorticity,
|
|
262
|
+
Divergence,
|
|
263
|
+
CloudAreaFractionInLayer,
|
|
264
|
+
SpecificCloudIceWaterContent,
|
|
265
|
+
SpecificCloudLiquidWaterContent,
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
ECMWF_VARIABLES = PRESSURE_LEVEL_VARIABLES + SURFACE_VARIABLES
|
|
@@ -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")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""GFS Data Access."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pycontrails.datalib.gfs.gfs import GFS_FORECAST_BUCKET, GFSForecast
|
|
6
|
+
from pycontrails.datalib.gfs.variables import (
|
|
7
|
+
GFS_VARIABLES,
|
|
8
|
+
PRESSURE_LEVEL_VARIABLES,
|
|
9
|
+
SURFACE_VARIABLES,
|
|
10
|
+
CloudIceWaterMixingRatio,
|
|
11
|
+
TOAUpwardLongwaveRadiation,
|
|
12
|
+
TOAUpwardShortwaveRadiation,
|
|
13
|
+
TotalCloudCoverIsobaric,
|
|
14
|
+
Visibility,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"GFS_FORECAST_BUCKET",
|
|
19
|
+
"GFS_VARIABLES",
|
|
20
|
+
"PRESSURE_LEVEL_VARIABLES",
|
|
21
|
+
"SURFACE_VARIABLES",
|
|
22
|
+
"CloudIceWaterMixingRatio",
|
|
23
|
+
"GFSForecast",
|
|
24
|
+
"TOAUpwardLongwaveRadiation",
|
|
25
|
+
"TOAUpwardShortwaveRadiation",
|
|
26
|
+
"TotalCloudCoverIsobaric",
|
|
27
|
+
"Visibility",
|
|
28
|
+
]
|