pycontrails 0.52.1__cp310-cp310-win_amd64.whl → 0.52.3__cp310-cp310-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pycontrails might be problematic. Click here for more details.
- pycontrails/_version.py +2 -2
- pycontrails/core/aircraft_performance.py +115 -19
- pycontrails/core/fleet.py +11 -10
- pycontrails/core/flight.py +98 -57
- pycontrails/core/interpolation.py +2 -1
- pycontrails/core/met.py +179 -2
- pycontrails/core/models.py +25 -16
- pycontrails/core/rgi_cython.cp310-win_amd64.pyd +0 -0
- pycontrails/core/vector.py +25 -28
- pycontrails/models/cocip/cocip.py +116 -48
- pycontrails/models/cocip/cocip_params.py +21 -0
- pycontrails/models/cocip/output_formats.py +22 -6
- pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +1 -1
- {pycontrails-0.52.1.dist-info → pycontrails-0.52.3.dist-info}/METADATA +76 -76
- {pycontrails-0.52.1.dist-info → pycontrails-0.52.3.dist-info}/RECORD +19 -19
- {pycontrails-0.52.1.dist-info → pycontrails-0.52.3.dist-info}/WHEEL +1 -1
- {pycontrails-0.52.1.dist-info → pycontrails-0.52.3.dist-info}/LICENSE +0 -0
- {pycontrails-0.52.1.dist-info → pycontrails-0.52.3.dist-info}/NOTICE +0 -0
- {pycontrails-0.52.1.dist-info → pycontrails-0.52.3.dist-info}/top_level.txt +0 -0
pycontrails/_version.py
CHANGED
|
@@ -9,6 +9,7 @@ from typing import Any, Generic, NoReturn, overload
|
|
|
9
9
|
|
|
10
10
|
import numpy as np
|
|
11
11
|
import numpy.typing as npt
|
|
12
|
+
from overrides import overrides
|
|
12
13
|
|
|
13
14
|
from pycontrails.core import flight, fuel
|
|
14
15
|
from pycontrails.core.flight import Flight
|
|
@@ -39,6 +40,19 @@ class AircraftPerformanceParams(ModelParams):
|
|
|
39
40
|
#: The default value of 3 is sufficient for most cases.
|
|
40
41
|
n_iter: int = 3
|
|
41
42
|
|
|
43
|
+
#: Experimental. If True, fill waypoints below the lowest altitude met
|
|
44
|
+
#: level with ISA temperature when interpolating "air_temperature" or "t".
|
|
45
|
+
#: If the ``met`` data is not provided, the entire air temperature array
|
|
46
|
+
#: is approximated with the ISA temperature. Enabling this does NOT
|
|
47
|
+
#: remove any NaN values in the ``met`` data itself.
|
|
48
|
+
fill_low_altitude_with_isa_temperature: bool = False
|
|
49
|
+
|
|
50
|
+
#: Experimental. If True, fill waypoints below the lowest altitude met
|
|
51
|
+
#: level with zero wind when computing true airspeed. In other words,
|
|
52
|
+
#: approximate low-altitude true airspeed with the ground speed. Enabling
|
|
53
|
+
#: this does NOT remove any NaN values in the ``met`` data itself.
|
|
54
|
+
fill_low_altitude_with_zero_wind: bool = False
|
|
55
|
+
|
|
42
56
|
|
|
43
57
|
class AircraftPerformance(Model):
|
|
44
58
|
"""
|
|
@@ -104,6 +118,23 @@ class AircraftPerformance(Model):
|
|
|
104
118
|
Flight trajectory with aircraft performance data.
|
|
105
119
|
"""
|
|
106
120
|
|
|
121
|
+
@overrides
|
|
122
|
+
def set_source_met(self, *args: Any, **kwargs: Any) -> None:
|
|
123
|
+
fill_with_isa = self.params["fill_low_altitude_with_isa_temperature"]
|
|
124
|
+
if fill_with_isa and (self.met is None or "air_temperature" not in self.met):
|
|
125
|
+
if "air_temperature" in self.source:
|
|
126
|
+
_fill_low_altitude_with_isa_temperature(self.source, 0.0)
|
|
127
|
+
else:
|
|
128
|
+
self.source["air_temperature"] = self.source.T_isa()
|
|
129
|
+
fill_with_isa = False # we've just filled it
|
|
130
|
+
|
|
131
|
+
super().set_source_met(*args, **kwargs)
|
|
132
|
+
if not fill_with_isa:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
met_level_0 = self.met.data["level"][-1].item() # type: ignore[union-attr]
|
|
136
|
+
_fill_low_altitude_with_isa_temperature(self.source, met_level_0)
|
|
137
|
+
|
|
107
138
|
def simulate_fuel_and_performance(
|
|
108
139
|
self,
|
|
109
140
|
*,
|
|
@@ -426,27 +457,41 @@ class AircraftPerformance(Model):
|
|
|
426
457
|
on :attr:`source`, this is returned directly. Otherwise, it is calculated
|
|
427
458
|
using :meth:`Flight.segment_true_airspeed`.
|
|
428
459
|
"""
|
|
460
|
+
tas = self.source.get("true_airspeed")
|
|
461
|
+
fill_with_groundspeed = self.params["fill_low_altitude_with_zero_wind"]
|
|
462
|
+
|
|
463
|
+
if tas is not None:
|
|
464
|
+
if not fill_with_groundspeed:
|
|
465
|
+
return tas
|
|
466
|
+
cond = np.isnan(tas)
|
|
467
|
+
tas[cond] = self.source.segment_groundspeed()[cond]
|
|
468
|
+
return tas
|
|
469
|
+
|
|
470
|
+
met_incomplete = (
|
|
471
|
+
self.met is None or "eastward_wind" not in self.met or "northward_wind" not in self.met
|
|
472
|
+
)
|
|
473
|
+
if met_incomplete:
|
|
474
|
+
if fill_with_groundspeed:
|
|
475
|
+
tas = self.source.segment_groundspeed()
|
|
476
|
+
self.source["true_airspeed"] = tas
|
|
477
|
+
return tas
|
|
478
|
+
msg = (
|
|
479
|
+
"Cannot compute 'true_airspeed' without 'eastward_wind' and 'northward_wind' "
|
|
480
|
+
"met data. Either include met data in the model constructor, define "
|
|
481
|
+
"'true_airspeed' data on the flight, or set "
|
|
482
|
+
"'fill_low_altitude_with_zero_wind' to True."
|
|
483
|
+
)
|
|
484
|
+
raise ValueError(msg)
|
|
429
485
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
except KeyError:
|
|
433
|
-
pass
|
|
434
|
-
|
|
435
|
-
if not isinstance(self.source, Flight):
|
|
436
|
-
raise TypeError("Model source must be a Flight to calculate true airspeed.")
|
|
437
|
-
|
|
438
|
-
# Two step fallback: try to find u_wind and v_wind.
|
|
439
|
-
try:
|
|
440
|
-
u = interpolate_met(self.met, self.source, "eastward_wind", **self.interp_kwargs)
|
|
441
|
-
v = interpolate_met(self.met, self.source, "northward_wind", **self.interp_kwargs)
|
|
486
|
+
u = interpolate_met(self.met, self.source, "eastward_wind", **self.interp_kwargs)
|
|
487
|
+
v = interpolate_met(self.met, self.source, "northward_wind", **self.interp_kwargs)
|
|
442
488
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
) from exc
|
|
489
|
+
if fill_with_groundspeed:
|
|
490
|
+
met_level_max = self.met.data["level"][-1].item() # type: ignore[union-attr]
|
|
491
|
+
cond = self.source.level > met_level_max
|
|
492
|
+
# We DON'T overwrite the original u and v arrays already attached to the source
|
|
493
|
+
u = np.where(cond, 0.0, u)
|
|
494
|
+
v = np.where(cond, 0.0, v)
|
|
450
495
|
|
|
451
496
|
out = self.source.segment_true_airspeed(u, v)
|
|
452
497
|
self.source["true_airspeed"] = out
|
|
@@ -543,3 +588,54 @@ class AircraftPerformanceGridData(Generic[ArrayOrFloat]):
|
|
|
543
588
|
|
|
544
589
|
#: Engine efficiency, [:math:`0-1`]
|
|
545
590
|
engine_efficiency: ArrayOrFloat
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _fill_low_altitude_with_isa_temperature(vector: GeoVectorDataset, met_level_max: float) -> None:
|
|
594
|
+
"""Fill low-altitude NaN values in ``air_temperature`` with ISA values.
|
|
595
|
+
|
|
596
|
+
The ``air_temperature`` param is assumed to have been computed by
|
|
597
|
+
interpolating against a gridded air temperature field that did not
|
|
598
|
+
necessarily extend to the surface. This function fills points below the
|
|
599
|
+
lowest altitude in the gridded data with ISA temperature values.
|
|
600
|
+
|
|
601
|
+
This function operates in-place and modifies the ``air_temperature`` field.
|
|
602
|
+
|
|
603
|
+
Parameters
|
|
604
|
+
----------
|
|
605
|
+
vector : GeoVectorDataset
|
|
606
|
+
GeoVectorDataset instance associated with the ``air_temperature`` data.
|
|
607
|
+
met_level_max : float
|
|
608
|
+
The maximum level in the met data, [:math:`hPa`].
|
|
609
|
+
"""
|
|
610
|
+
air_temperature = vector["air_temperature"]
|
|
611
|
+
is_nan = np.isnan(air_temperature)
|
|
612
|
+
low_alt = vector.level > met_level_max
|
|
613
|
+
cond = is_nan & low_alt
|
|
614
|
+
|
|
615
|
+
t_isa = vector.T_isa()
|
|
616
|
+
air_temperature[cond] = t_isa[cond]
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _fill_low_altitude_tas_with_true_groundspeed(fl: Flight, met_level_max: float) -> None:
|
|
620
|
+
"""Fill low-altitude NaN values in ``true_airspeed`` with ground speed.
|
|
621
|
+
|
|
622
|
+
The ``true_airspeed`` param is assumed to have been computed by
|
|
623
|
+
interpolating against a gridded wind field that did not necessarily
|
|
624
|
+
extend to the surface. This function fills points below the lowest
|
|
625
|
+
altitude in the gridded data with ground speed values.
|
|
626
|
+
|
|
627
|
+
This function operates in-place and modifies the ``true_airspeed`` field.
|
|
628
|
+
|
|
629
|
+
Parameters
|
|
630
|
+
----------
|
|
631
|
+
fl : Flight
|
|
632
|
+
Flight instance associated with the ``true_airspeed`` data.
|
|
633
|
+
met_level_max : float
|
|
634
|
+
The maximum level in the met data, [:math:`hPa`].
|
|
635
|
+
"""
|
|
636
|
+
tas = fl["true_airspeed"]
|
|
637
|
+
is_nan = np.isnan(tas)
|
|
638
|
+
low_alt = fl.level > met_level_max
|
|
639
|
+
cond = is_nan & low_alt
|
|
640
|
+
|
|
641
|
+
tas[cond] = fl.segment_groundspeed()[cond]
|
pycontrails/core/fleet.py
CHANGED
|
@@ -226,28 +226,29 @@ class Fleet(Flight):
|
|
|
226
226
|
return len(self.fl_attrs)
|
|
227
227
|
|
|
228
228
|
def to_flight_list(self, copy: bool = True) -> list[Flight]:
|
|
229
|
-
"""De-concatenate merged waypoints into a list of Flight instances.
|
|
229
|
+
"""De-concatenate merged waypoints into a list of :class:`Flight` instances.
|
|
230
230
|
|
|
231
231
|
Any global :attr:`attrs` are lost.
|
|
232
232
|
|
|
233
233
|
Parameters
|
|
234
234
|
----------
|
|
235
235
|
copy : bool, optional
|
|
236
|
-
If True, make copy of each
|
|
236
|
+
If True, make copy of each :class:`Flight` instance.
|
|
237
237
|
|
|
238
238
|
Returns
|
|
239
239
|
-------
|
|
240
240
|
list[Flight]
|
|
241
|
-
List of Flights in the same order as was passed into the
|
|
241
|
+
List of Flights in the same order as was passed into the ``Fleet`` instance.
|
|
242
242
|
"""
|
|
243
|
-
|
|
244
|
-
# Avoid self.dataframe to purposely drop global attrs
|
|
245
|
-
tmp = pd.DataFrame(self.data, copy=copy)
|
|
246
|
-
grouped = tmp.groupby("flight_id", sort=False)
|
|
247
|
-
|
|
243
|
+
indices = self.dataframe.groupby("flight_id", sort=False).indices
|
|
248
244
|
return [
|
|
249
|
-
Flight(
|
|
250
|
-
|
|
245
|
+
Flight(
|
|
246
|
+
data=VectorDataDict({k: v[idx] for k, v in self.data.items()}),
|
|
247
|
+
attrs=self.fl_attrs[flight_id],
|
|
248
|
+
copy=copy,
|
|
249
|
+
fuel=self.fuel,
|
|
250
|
+
)
|
|
251
|
+
for flight_id, idx in indices.items()
|
|
251
252
|
]
|
|
252
253
|
|
|
253
254
|
###################################
|
pycontrails/core/flight.py
CHANGED
|
@@ -954,28 +954,13 @@ class Flight(GeoVectorDataset):
|
|
|
954
954
|
# STEP 3: Set the time index, and sort it
|
|
955
955
|
df = df.set_index("time", verify_integrity=True).sort_index()
|
|
956
956
|
|
|
957
|
-
# STEP 4:
|
|
958
|
-
#
|
|
959
|
-
#
|
|
960
|
-
#
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
# would instead wrap the other way). For this flights spanning the
|
|
964
|
-
# antimeridian, we translate them to a common "chart" away from the
|
|
965
|
-
# antimeridian (see variable `shift`), then apply the interpolation,
|
|
966
|
-
# then shift back to their original position.
|
|
967
|
-
lon = df["longitude"].to_numpy()
|
|
968
|
-
sign_ = np.sign(lon)
|
|
969
|
-
min_pos = np.min(lon[sign_ == 1.0], initial=np.inf)
|
|
970
|
-
max_neg = np.max(lon[sign_ == -1.0], initial=-np.inf)
|
|
971
|
-
|
|
972
|
-
if (180.0 - min_pos) + (180.0 + max_neg) < 180.0 and min_pos < np.inf and max_neg > -np.inf:
|
|
973
|
-
# In this case, we believe the flight crosses the antimeridian
|
|
974
|
-
shift = min_pos
|
|
975
|
-
# So we shift the longitude "chart"
|
|
957
|
+
# STEP 4: handle antimeridian crossings
|
|
958
|
+
# For flights spanning the antimeridian, we translate them to a
|
|
959
|
+
# common "chart" away from the antimeridian (see variable `shift`),
|
|
960
|
+
# then apply the interpolation, then shift back to their original position.
|
|
961
|
+
shift = self._antimeridian_shift()
|
|
962
|
+
if shift is not None:
|
|
976
963
|
df["longitude"] = (df["longitude"] - shift) % 360.0
|
|
977
|
-
else:
|
|
978
|
-
shift = None
|
|
979
964
|
|
|
980
965
|
# STEP 5: Resample flight to freq
|
|
981
966
|
# Save altitudes to copy over - these just get rounded down in time.
|
|
@@ -1189,19 +1174,12 @@ class Flight(GeoVectorDataset):
|
|
|
1189
1174
|
"""
|
|
1190
1175
|
|
|
1191
1176
|
# Check if flight crosses antimeridian line
|
|
1177
|
+
# If it does, shift longitude chart to remove jump
|
|
1192
1178
|
lon_ = self["longitude"]
|
|
1193
1179
|
lat_ = self["latitude"]
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
max_neg = np.max(lon_[sign_ == -1.0], initial=-np.inf)
|
|
1197
|
-
|
|
1198
|
-
if (180.0 - min_pos) + (180.0 + max_neg) < 180.0 and min_pos < np.inf and max_neg > -np.inf:
|
|
1199
|
-
# In this case, we believe the flight crosses the antimeridian
|
|
1200
|
-
shift = min_pos
|
|
1201
|
-
# So we shift the longitude "chart"
|
|
1180
|
+
shift = self._antimeridian_shift()
|
|
1181
|
+
if shift is not None:
|
|
1202
1182
|
lon_ = (lon_ - shift) % 360.0
|
|
1203
|
-
else:
|
|
1204
|
-
shift = None
|
|
1205
1183
|
|
|
1206
1184
|
# Make a fake flight that flies at constant height so distance is just
|
|
1207
1185
|
# distance traveled across groud
|
|
@@ -1262,6 +1240,55 @@ class Flight(GeoVectorDataset):
|
|
|
1262
1240
|
|
|
1263
1241
|
return lat, lon, seg_idx
|
|
1264
1242
|
|
|
1243
|
+
def _antimeridian_shift(self) -> float | None:
|
|
1244
|
+
"""Determine shift required for resampling trajectories that cross antimeridian.
|
|
1245
|
+
|
|
1246
|
+
Because flights sometimes span more than 180 degree longitude (for example,
|
|
1247
|
+
when flight-level winds favor travel in a specific direction, typically eastward),
|
|
1248
|
+
antimeridian crossings cannot reliably be detected by looking only at minimum
|
|
1249
|
+
and maximum longitudes.
|
|
1250
|
+
|
|
1251
|
+
Instead, this function checks each flight segment for an antimeridian crossing,
|
|
1252
|
+
and if it finds one returns the coordinate of a meridian that is not crossed by
|
|
1253
|
+
the flight.
|
|
1254
|
+
|
|
1255
|
+
Returns
|
|
1256
|
+
-------
|
|
1257
|
+
float | None
|
|
1258
|
+
Longitude shift for handling antimeridian crossings, or None if the
|
|
1259
|
+
flight does not cross the antimeridian.
|
|
1260
|
+
"""
|
|
1261
|
+
|
|
1262
|
+
# logic for detecting crossings is consistent with _antimeridian_crossing,
|
|
1263
|
+
# but implementation is separate to keep performance costs as low as possible
|
|
1264
|
+
lon = self["longitude"]
|
|
1265
|
+
if np.any(np.isnan(lon)):
|
|
1266
|
+
warnings.warn("Anti-meridian crossings can't be reliably detected with nan longitudes")
|
|
1267
|
+
|
|
1268
|
+
s1 = (lon >= -180) & (lon <= -90)
|
|
1269
|
+
s2 = (lon <= 180) & (lon >= 90)
|
|
1270
|
+
jump12 = s1[:-1] & s2[1:] # westward
|
|
1271
|
+
jump21 = s2[:-1] & s1[1:] # eastward
|
|
1272
|
+
if not np.any(jump12 | jump21):
|
|
1273
|
+
return None
|
|
1274
|
+
|
|
1275
|
+
# separate flight into segments that are east and west of crossings
|
|
1276
|
+
net_westward = np.insert(np.cumsum(jump12.astype(int) - jump21.astype(int)), 0, 0)
|
|
1277
|
+
max_westward = net_westward.max()
|
|
1278
|
+
if max_westward - net_westward.min() > 1:
|
|
1279
|
+
msg = "Cannot handle consecutive antimeridian crossings in the same direction"
|
|
1280
|
+
raise ValueError(msg)
|
|
1281
|
+
east = (net_westward == 0) if max_westward == 1 else (net_westward == -1)
|
|
1282
|
+
|
|
1283
|
+
# shift must be between maximum longitude east of crossings
|
|
1284
|
+
# and minimum longitude west of crossings
|
|
1285
|
+
shift_min = np.nanmax(lon[east])
|
|
1286
|
+
shift_max = np.nanmin(lon[~east])
|
|
1287
|
+
if shift_min >= shift_max:
|
|
1288
|
+
msg = "Cannot handle flight that spans more than 360 degrees longitude"
|
|
1289
|
+
raise ValueError(msg)
|
|
1290
|
+
return (shift_min + shift_max) / 2
|
|
1291
|
+
|
|
1265
1292
|
def _geodesic_interpolation(self, geodesic_threshold: float) -> pd.DataFrame | None:
|
|
1266
1293
|
"""Geodesic interpolate between large gaps between waypoints.
|
|
1267
1294
|
|
|
@@ -1506,25 +1533,25 @@ class Flight(GeoVectorDataset):
|
|
|
1506
1533
|
|
|
1507
1534
|
>>> # Build flight
|
|
1508
1535
|
>>> df = pd.DataFrame()
|
|
1509
|
-
>>> df[
|
|
1510
|
-
>>> df[
|
|
1511
|
-
>>> df[
|
|
1512
|
-
>>> df[
|
|
1513
|
-
>>> fl = Flight(df).resample_and_fill(
|
|
1536
|
+
>>> df["time"] = pd.date_range("2022-03-01T00", "2022-03-01T03", periods=11)
|
|
1537
|
+
>>> df["longitude"] = np.linspace(-20, 20, 11)
|
|
1538
|
+
>>> df["latitude"] = np.linspace(-20, 20, 11)
|
|
1539
|
+
>>> df["altitude"] = np.linspace(9500, 10000, 11)
|
|
1540
|
+
>>> fl = Flight(df).resample_and_fill("10s")
|
|
1514
1541
|
|
|
1515
1542
|
>>> # Intersect and attach
|
|
1516
|
-
>>> fl["air_temperature"] = fl.intersect_met(met[
|
|
1543
|
+
>>> fl["air_temperature"] = fl.intersect_met(met["air_temperature"])
|
|
1517
1544
|
>>> fl["air_temperature"]
|
|
1518
|
-
array([235.94657007, 235.
|
|
1545
|
+
array([235.94657007, 235.55745645, 235.56709768, ..., 234.59917962,
|
|
1519
1546
|
234.60387402, 234.60845312])
|
|
1520
1547
|
|
|
1521
1548
|
>>> # Length (in meters) of waypoints whose temperature exceeds 236K
|
|
1522
1549
|
>>> fl.length_met("air_temperature", threshold=236)
|
|
1523
|
-
np.float64(
|
|
1550
|
+
np.float64(3589705.998...)
|
|
1524
1551
|
|
|
1525
1552
|
>>> # Proportion (with respect to distance) of waypoints whose temperature exceeds 236K
|
|
1526
1553
|
>>> fl.proportion_met("air_temperature", threshold=236)
|
|
1527
|
-
np.float64(0.
|
|
1554
|
+
np.float64(0.576...)
|
|
1528
1555
|
"""
|
|
1529
1556
|
if key not in self.data:
|
|
1530
1557
|
raise KeyError(f"Column {key} does not exist in data.")
|
|
@@ -1591,10 +1618,30 @@ class Flight(GeoVectorDataset):
|
|
|
1591
1618
|
:class:`matplotlib.axes.Axes`
|
|
1592
1619
|
Plot
|
|
1593
1620
|
"""
|
|
1594
|
-
|
|
1621
|
+
kwargs.setdefault("legend", False)
|
|
1622
|
+
ax = self.dataframe.plot(x="longitude", y="latitude", **kwargs)
|
|
1595
1623
|
ax.set(xlabel="longitude", ylabel="latitude")
|
|
1596
1624
|
return ax
|
|
1597
1625
|
|
|
1626
|
+
def plot_profile(self, **kwargs: Any) -> matplotlib.axes.Axes:
|
|
1627
|
+
"""Plot flight trajectory time-altitude values.
|
|
1628
|
+
|
|
1629
|
+
Parameters
|
|
1630
|
+
----------
|
|
1631
|
+
**kwargs : Any
|
|
1632
|
+
Additional plot properties to passed to `pd.DataFrame.plot`
|
|
1633
|
+
|
|
1634
|
+
Returns
|
|
1635
|
+
-------
|
|
1636
|
+
:class:`matplotlib.axes.Axes`
|
|
1637
|
+
Plot
|
|
1638
|
+
"""
|
|
1639
|
+
kwargs.setdefault("legend", False)
|
|
1640
|
+
df = self.dataframe.assign(altitude_ft=self.altitude_ft)
|
|
1641
|
+
ax = df.plot(x="time", y="altitude_ft", **kwargs)
|
|
1642
|
+
ax.set(xlabel="time", ylabel="altitude_ft")
|
|
1643
|
+
return ax
|
|
1644
|
+
|
|
1598
1645
|
|
|
1599
1646
|
def _return_linestring(data: dict[str, npt.NDArray[np.float64]]) -> list[list[float]]:
|
|
1600
1647
|
"""Return list of coordinates for geojson constructions.
|
|
@@ -1631,18 +1678,14 @@ def _antimeridian_index(longitude: pd.Series, crs: str = "EPSG:4326") -> list[in
|
|
|
1631
1678
|
|
|
1632
1679
|
Returns
|
|
1633
1680
|
-------
|
|
1634
|
-
int
|
|
1635
|
-
|
|
1681
|
+
list[int]
|
|
1682
|
+
Indices after jump, or empty list of flight does not cross antimeridian.
|
|
1636
1683
|
|
|
1637
1684
|
Raises
|
|
1638
1685
|
------
|
|
1639
1686
|
ValueError
|
|
1640
1687
|
CRS is not supported.
|
|
1641
|
-
Flight crosses antimeridian several times.
|
|
1642
1688
|
"""
|
|
1643
|
-
# FIXME: This logic here is somewhat outdated - the _interpolate_altitude
|
|
1644
|
-
# method handles this somewhat more reliably
|
|
1645
|
-
# This function should get updated to follow the logic there.
|
|
1646
1689
|
# WGS84
|
|
1647
1690
|
if crs in ["EPSG:4326"]:
|
|
1648
1691
|
l1 = (-180.0, -90.0)
|
|
@@ -1878,7 +1921,7 @@ def _altitude_interpolation_climb_descend_middle(
|
|
|
1878
1921
|
s = pd.Series(altitude)
|
|
1879
1922
|
|
|
1880
1923
|
# Check to see if we have gaps greater than two hours
|
|
1881
|
-
step_threshold =
|
|
1924
|
+
step_threshold = np.timedelta64(2, "h") / freq
|
|
1882
1925
|
step_groups = na_group_size > step_threshold
|
|
1883
1926
|
if np.any(step_groups):
|
|
1884
1927
|
# If there are gaps greater than two hours, step through one by one
|
|
@@ -2214,16 +2257,14 @@ def segment_rocd(
|
|
|
2214
2257
|
if air_temperature is None:
|
|
2215
2258
|
return out
|
|
2216
2259
|
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
T_isa = units.m_to_T_isa(altitude_m)
|
|
2260
|
+
altitude_m = units.ft_to_m(altitude_ft)
|
|
2261
|
+
T_isa = units.m_to_T_isa(altitude_m)
|
|
2220
2262
|
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
return T_correction * out
|
|
2263
|
+
T_correction = np.empty_like(altitude_ft)
|
|
2264
|
+
T_correction[:-1] = (air_temperature[:-1] + air_temperature[1:]) / (T_isa[:-1] + T_isa[1:])
|
|
2265
|
+
T_correction[-1] = np.nan
|
|
2266
|
+
|
|
2267
|
+
return T_correction * out
|
|
2227
2268
|
|
|
2228
2269
|
|
|
2229
2270
|
def _resample_to_freq(df: pd.DataFrame, freq: str) -> tuple[pd.DataFrame, pd.DatetimeIndex]:
|
|
@@ -215,7 +215,8 @@ class PycontrailsRegularGridInterpolator(scipy.interpolate.RegularGridInterpolat
|
|
|
215
215
|
|
|
216
216
|
if ndim == 1:
|
|
217
217
|
# np.interp could be better ... although that may also promote the dtype
|
|
218
|
-
|
|
218
|
+
# 1-d view is required for evaluate_linear_1d
|
|
219
|
+
return rgi_cython.evaluate_linear_1d(values, indices[0, :], norm_distances[0, :], out)
|
|
219
220
|
|
|
220
221
|
msg = f"Invalid number of dimensions: {ndim}"
|
|
221
222
|
raise ValueError(msg)
|