pycontrails 0.49.3__cp312-cp312-win_amd64.whl → 0.49.5__cp312-cp312-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/datalib.py +1 -1
- pycontrails/core/flight.py +11 -11
- pycontrails/core/interpolation.py +29 -19
- pycontrails/core/met.py +192 -104
- pycontrails/core/models.py +29 -15
- pycontrails/core/rgi_cython.cp312-win_amd64.pyd +0 -0
- pycontrails/core/vector.py +14 -15
- pycontrails/datalib/gfs/gfs.py +1 -1
- pycontrails/datalib/spire/spire.py +23 -19
- pycontrails/ext/synthetic_flight.py +3 -1
- pycontrails/models/accf.py +6 -4
- pycontrails/models/cocip/cocip.py +48 -18
- pycontrails/models/cocip/cocip_params.py +13 -10
- pycontrails/models/cocip/output_formats.py +62 -52
- pycontrails/models/cocipgrid/cocip_grid.py +459 -275
- pycontrails/models/cocipgrid/cocip_grid_params.py +12 -18
- pycontrails/models/emissions/ffm2.py +10 -8
- pycontrails/models/pcc.py +1 -1
- pycontrails/models/ps_model/ps_aircraft_params.py +1 -1
- pycontrails/models/ps_model/static/{ps-aircraft-params-20231117.csv → ps-aircraft-params-20240209.csv} +12 -3
- pycontrails/utils/json.py +12 -10
- {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/METADATA +2 -2
- {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/RECORD +28 -29
- pycontrails/models/cocipgrid/cocip_time_handling.py +0 -342
- {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/LICENSE +0 -0
- {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/NOTICE +0 -0
- {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/WHEEL +0 -0
- {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/top_level.txt +0 -0
pycontrails/core/models.py
CHANGED
|
@@ -447,7 +447,8 @@ class Model(ABC):
|
|
|
447
447
|
|
|
448
448
|
# Raise error if source is not a MetDataset or GeoVectorDataset
|
|
449
449
|
if not isinstance(source, (MetDataset, GeoVectorDataset)):
|
|
450
|
-
|
|
450
|
+
msg = f"Unknown source type: {type(source)}"
|
|
451
|
+
raise TypeError(msg)
|
|
451
452
|
|
|
452
453
|
if self.params["copy_source"]:
|
|
453
454
|
source = source.copy()
|
|
@@ -516,11 +517,14 @@ class Model(ABC):
|
|
|
516
517
|
TypeError
|
|
517
518
|
Raised if :attr:`met` is not a :class:`MetDataset`.
|
|
518
519
|
"""
|
|
519
|
-
|
|
520
|
-
|
|
520
|
+
try:
|
|
521
|
+
source = self.source
|
|
522
|
+
except AttributeError as exc:
|
|
523
|
+
msg = "Attribute 'source' must be defined before calling 'downselect_met'."
|
|
524
|
+
raise AttributeError(msg) from exc
|
|
521
525
|
|
|
522
526
|
# TODO: This could be generalized for a MetDataset source
|
|
523
|
-
if not isinstance(
|
|
527
|
+
if not isinstance(source, GeoVectorDataset):
|
|
524
528
|
msg = "Attribute 'source' must be a GeoVectorDataset"
|
|
525
529
|
raise TypeError(msg)
|
|
526
530
|
|
|
@@ -543,7 +547,7 @@ class Model(ABC):
|
|
|
543
547
|
}
|
|
544
548
|
kwargs = {k: v for k, v in buffers.items() if v is not None}
|
|
545
549
|
|
|
546
|
-
self.met =
|
|
550
|
+
self.met = source.downselect_met(self.met, **kwargs, copy=False)
|
|
547
551
|
|
|
548
552
|
def set_source_met(
|
|
549
553
|
self,
|
|
@@ -614,7 +618,8 @@ class Model(ABC):
|
|
|
614
618
|
continue
|
|
615
619
|
|
|
616
620
|
if not isinstance(self.source, MetDataset):
|
|
617
|
-
|
|
621
|
+
msg = f"Unknown source type: {type(self.source)}"
|
|
622
|
+
raise TypeError(msg)
|
|
618
623
|
|
|
619
624
|
da = self.met.data[met_key].reset_coords(drop=True)
|
|
620
625
|
try:
|
|
@@ -770,7 +775,8 @@ def _interp_grid_to_grid(
|
|
|
770
775
|
interped = da.interp(coords, **interp_kwargs).load().astype(da.dtype, copy=False)
|
|
771
776
|
return interped.assign_coords(level=level0)
|
|
772
777
|
|
|
773
|
-
|
|
778
|
+
msg = f"Unsupported q_method: {q_method}"
|
|
779
|
+
raise NotImplementedError(msg)
|
|
774
780
|
|
|
775
781
|
|
|
776
782
|
def _raise_missing_met_var(var: MetVariable | Sequence[MetVariable]) -> NoReturn:
|
|
@@ -786,15 +792,17 @@ def _raise_missing_met_var(var: MetVariable | Sequence[MetVariable]) -> NoReturn
|
|
|
786
792
|
KeyError
|
|
787
793
|
"""
|
|
788
794
|
if isinstance(var, MetVariable):
|
|
789
|
-
|
|
795
|
+
msg = (
|
|
790
796
|
f"Variable `{var.standard_name}` not found. Either pass parameter `met`"
|
|
791
797
|
f"in model constructor, or define `{var.standard_name}` data on input data."
|
|
792
798
|
)
|
|
799
|
+
raise KeyError(msg)
|
|
793
800
|
missing_keys = [v.standard_name for v in var]
|
|
794
|
-
|
|
801
|
+
msg = (
|
|
795
802
|
f"One of `{missing_keys}` is required. Either pass parameter `met`"
|
|
796
803
|
f"in model constructor, or define one of `{missing_keys}` data on input data."
|
|
797
804
|
)
|
|
805
|
+
raise KeyError(msg)
|
|
798
806
|
|
|
799
807
|
|
|
800
808
|
def interpolate_met(
|
|
@@ -847,7 +855,8 @@ def interpolate_met(
|
|
|
847
855
|
return out
|
|
848
856
|
|
|
849
857
|
if met is None:
|
|
850
|
-
|
|
858
|
+
msg = f"No variable key '{vector_key}' in 'vector' and 'met' is None"
|
|
859
|
+
raise KeyError(msg)
|
|
851
860
|
|
|
852
861
|
if met_key in ("q", "specific_humidity") and q_method is not None:
|
|
853
862
|
mda, log_applied = _extract_q(met, met_key, q_method)
|
|
@@ -859,7 +868,8 @@ def interpolate_met(
|
|
|
859
868
|
try:
|
|
860
869
|
mda = met[met_key]
|
|
861
870
|
except KeyError as exc:
|
|
862
|
-
|
|
871
|
+
msg = f"No variable key '{met_key}' in 'met'."
|
|
872
|
+
raise KeyError(msg) from exc
|
|
863
873
|
|
|
864
874
|
out = vector.intersect_met(mda, **interp_kwargs)
|
|
865
875
|
|
|
@@ -890,7 +900,8 @@ def _extract_q(met: MetDataset, met_key: str, q_method: str) -> tuple[MetDataArr
|
|
|
890
900
|
try:
|
|
891
901
|
return met[met_key], False
|
|
892
902
|
except KeyError as exc:
|
|
893
|
-
|
|
903
|
+
msg = f"No variable key '{met_key}' in 'met'."
|
|
904
|
+
raise KeyError(msg) from exc
|
|
894
905
|
|
|
895
906
|
try:
|
|
896
907
|
return met["log_specific_humidity"], True
|
|
@@ -904,7 +915,8 @@ def _extract_q(met: MetDataset, met_key: str, q_method: str) -> tuple[MetDataArr
|
|
|
904
915
|
try:
|
|
905
916
|
return met[met_key], False
|
|
906
917
|
except KeyError as exc:
|
|
907
|
-
|
|
918
|
+
msg = f"No variable key '{met_key}' in 'met'."
|
|
919
|
+
raise KeyError(msg) from exc
|
|
908
920
|
|
|
909
921
|
|
|
910
922
|
def _prepare_q(
|
|
@@ -972,7 +984,8 @@ def _prepare_q_cubic_spline(
|
|
|
972
984
|
da: xr.DataArray, level: npt.NDArray[np.float64]
|
|
973
985
|
) -> tuple[MetDataArray, npt.NDArray[np.float64]]:
|
|
974
986
|
if da["level"][0] < 50.0 or da["level"][-1] > 1000.0:
|
|
975
|
-
|
|
987
|
+
msg = "Cubic spline interpolation requires data to span 50-1000 hPa."
|
|
988
|
+
raise ValueError(msg)
|
|
976
989
|
ppoly = _load_spline()
|
|
977
990
|
|
|
978
991
|
da = da.assign_coords(level=ppoly(da["level"]))
|
|
@@ -1038,7 +1051,8 @@ def raise_invalid_q_method_error(q_method: str) -> NoReturn:
|
|
|
1038
1051
|
``q_method`` is not one of ``None``, ``"log-q-log-p"``, or ``"cubic-spline"``.
|
|
1039
1052
|
"""
|
|
1040
1053
|
available = None, "log-q-log-p", "cubic-spline"
|
|
1041
|
-
|
|
1054
|
+
msg = f"Invalid 'q_method' value '{q_method}'. Must be one of {available}."
|
|
1055
|
+
raise ValueError(msg)
|
|
1042
1056
|
|
|
1043
1057
|
|
|
1044
1058
|
@functools.cache
|
|
Binary file
|
pycontrails/core/vector.py
CHANGED
|
@@ -956,10 +956,8 @@ class VectorDataset:
|
|
|
956
956
|
continue
|
|
957
957
|
|
|
958
958
|
min_dtype = np.min_scalar_type(scalar)
|
|
959
|
-
if np.can_cast(min_dtype, np.float32)
|
|
960
|
-
|
|
961
|
-
else:
|
|
962
|
-
self.data.update({key: np.full(self.size, scalar)})
|
|
959
|
+
dtype = np.float32 if np.can_cast(min_dtype, np.float32) else None
|
|
960
|
+
self.data.update({key: np.full(self.size, scalar, dtype=dtype)})
|
|
963
961
|
|
|
964
962
|
def broadcast_numeric_attrs(
|
|
965
963
|
self, ignore_keys: str | Iterable[str] | None = None, overwrite: bool = False
|
|
@@ -1607,22 +1605,22 @@ class GeoVectorDataset(VectorDataset):
|
|
|
1607
1605
|
npt.NDArray[np.bool_]
|
|
1608
1606
|
True if point is inside the bounding box defined by ``met``.
|
|
1609
1607
|
"""
|
|
1610
|
-
|
|
1608
|
+
indexes = met.indexes
|
|
1611
1609
|
|
|
1612
1610
|
lat_intersect = coordinates.intersect_domain(
|
|
1613
|
-
|
|
1611
|
+
indexes["latitude"].to_numpy(),
|
|
1614
1612
|
self["latitude"],
|
|
1615
1613
|
)
|
|
1616
1614
|
lon_intersect = coordinates.intersect_domain(
|
|
1617
|
-
|
|
1615
|
+
indexes["longitude"].to_numpy(),
|
|
1618
1616
|
self["longitude"],
|
|
1619
1617
|
)
|
|
1620
1618
|
level_intersect = coordinates.intersect_domain(
|
|
1621
|
-
|
|
1619
|
+
indexes["level"].to_numpy(),
|
|
1622
1620
|
self.level,
|
|
1623
1621
|
)
|
|
1624
1622
|
time_intersect = coordinates.intersect_domain(
|
|
1625
|
-
|
|
1623
|
+
indexes["time"].to_numpy(),
|
|
1626
1624
|
self["time"],
|
|
1627
1625
|
)
|
|
1628
1626
|
|
|
@@ -1901,18 +1899,19 @@ class GeoVectorDataset(VectorDataset):
|
|
|
1901
1899
|
MetDataset | MetDataArray
|
|
1902
1900
|
Copy of downselected MetDataset or MetDataArray.
|
|
1903
1901
|
"""
|
|
1902
|
+
variables = met.indexes
|
|
1904
1903
|
lon_slice = coordinates.slice_domain(
|
|
1905
|
-
|
|
1904
|
+
variables["longitude"].to_numpy(),
|
|
1906
1905
|
self["longitude"],
|
|
1907
1906
|
buffer=longitude_buffer,
|
|
1908
1907
|
)
|
|
1909
1908
|
lat_slice = coordinates.slice_domain(
|
|
1910
|
-
|
|
1909
|
+
variables["latitude"].to_numpy(),
|
|
1911
1910
|
self["latitude"],
|
|
1912
1911
|
buffer=latitude_buffer,
|
|
1913
1912
|
)
|
|
1914
1913
|
time_slice = coordinates.slice_domain(
|
|
1915
|
-
|
|
1914
|
+
variables["time"].to_numpy(),
|
|
1916
1915
|
self["time"],
|
|
1917
1916
|
buffer=time_buffer,
|
|
1918
1917
|
)
|
|
@@ -1922,7 +1921,7 @@ class GeoVectorDataset(VectorDataset):
|
|
|
1922
1921
|
level_slice = slice(None)
|
|
1923
1922
|
else:
|
|
1924
1923
|
level_slice = coordinates.slice_domain(
|
|
1925
|
-
|
|
1924
|
+
variables["level"].to_numpy(),
|
|
1926
1925
|
self.level,
|
|
1927
1926
|
buffer=level_buffer,
|
|
1928
1927
|
)
|
|
@@ -2043,8 +2042,8 @@ def vector_to_lon_lat_grid(
|
|
|
2043
2042
|
>>> da = ds["foo"]
|
|
2044
2043
|
>>> da.coords
|
|
2045
2044
|
Coordinates:
|
|
2046
|
-
* longitude (longitude) float64 -10.0 -9.5 -9.0 -8.5
|
|
2047
|
-
* latitude (latitude) float64 -10.0 -9.5 -9.0 -8.5
|
|
2045
|
+
* longitude (longitude) float64 320B -10.0 -9.5 -9.0 -8.5 ... 8.0 8.5 9.0 9.5
|
|
2046
|
+
* latitude (latitude) float64 320B -10.0 -9.5 -9.0 -8.5 ... 8.0 8.5 9.0 9.5
|
|
2048
2047
|
|
|
2049
2048
|
>>> da.values.round(2)
|
|
2050
2049
|
array([[2.23, 0.67, 1.29, ..., 4.66, 3.91, 1.93],
|
pycontrails/datalib/gfs/gfs.py
CHANGED
|
@@ -134,7 +134,7 @@ class GFSForecast(datalib.MetDataSource):
|
|
|
134
134
|
self,
|
|
135
135
|
time: datalib.TimeInput | None,
|
|
136
136
|
variables: datalib.VariableInput,
|
|
137
|
-
pressure_levels: datalib.PressureLevelInput =
|
|
137
|
+
pressure_levels: datalib.PressureLevelInput = -1,
|
|
138
138
|
paths: str | list[str] | pathlib.Path | list[pathlib.Path] | None = None,
|
|
139
139
|
grid: float = 0.25,
|
|
140
140
|
forecast_time: DatetimeLike | None = None,
|
|
@@ -198,17 +198,19 @@ def identify_flights(messages: pd.DataFrame) -> pd.Series:
|
|
|
198
198
|
index=messages.index,
|
|
199
199
|
)
|
|
200
200
|
|
|
201
|
-
for idx, gp in messages[
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
201
|
+
for idx, gp in messages[
|
|
202
|
+
[
|
|
203
|
+
"icao_address",
|
|
204
|
+
"tail_number",
|
|
205
|
+
"aircraft_type_icao",
|
|
206
|
+
"callsign",
|
|
207
|
+
"timestamp",
|
|
208
|
+
"longitude",
|
|
209
|
+
"latitude",
|
|
210
|
+
"altitude_baro",
|
|
211
|
+
"on_ground",
|
|
212
|
+
]
|
|
213
|
+
].groupby(["icao_address", "tail_number", "aircraft_type_icao", "callsign"], sort=False):
|
|
212
214
|
# minimum # of messages > TRAJECTORY_MINIMUM_MESSAGES
|
|
213
215
|
if len(gp) < TRAJECTORY_MINIMUM_MESSAGES:
|
|
214
216
|
logger.debug(f"Message {idx} group too small to create flight ids")
|
|
@@ -542,14 +544,16 @@ def validate_flights(messages: pd.DataFrame) -> pd.Series:
|
|
|
542
544
|
index=messages.index,
|
|
543
545
|
)
|
|
544
546
|
|
|
545
|
-
for _, gp in messages[
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
547
|
+
for _, gp in messages[
|
|
548
|
+
[
|
|
549
|
+
"flight_id",
|
|
550
|
+
"aircraft_type_icao",
|
|
551
|
+
"timestamp",
|
|
552
|
+
"altitude_baro",
|
|
553
|
+
"on_ground",
|
|
554
|
+
"speed",
|
|
555
|
+
]
|
|
556
|
+
].groupby("flight_id", sort=False):
|
|
553
557
|
# save flight ids
|
|
554
558
|
valid.loc[gp.index] = is_valid_trajectory(gp)
|
|
555
559
|
|
|
@@ -162,7 +162,7 @@ class SyntheticFlight:
|
|
|
162
162
|
msg += f"\nmax_queue_size: {self.max_queue_size} min_n_waypoints: {self.min_n_waypoints}"
|
|
163
163
|
return msg
|
|
164
164
|
|
|
165
|
-
def __call__(self, timestep: np.timedelta64 =
|
|
165
|
+
def __call__(self, timestep: np.timedelta64 | None = None) -> Flight:
|
|
166
166
|
"""Create random flight within `bounds` at a constant altitude.
|
|
167
167
|
|
|
168
168
|
BADA4 data is used to determine flight speed at a randomly chosen altitude within
|
|
@@ -183,6 +183,8 @@ class SyntheticFlight:
|
|
|
183
183
|
Flight
|
|
184
184
|
Random `Flight` instance constrained by bounds.
|
|
185
185
|
"""
|
|
186
|
+
if timestep is None:
|
|
187
|
+
timestep = np.timedelta64(1, "m")
|
|
186
188
|
# Building flights with `u_wind` and `v_wind` involved in the true airspeed calculation is
|
|
187
189
|
# slow. BUT, we can do it in a vectorized way. So we maintain a short queue that gets
|
|
188
190
|
# repeatedly replenished.
|
pycontrails/models/accf.py
CHANGED
|
@@ -312,10 +312,12 @@ class ACCF(Model):
|
|
|
312
312
|
)
|
|
313
313
|
|
|
314
314
|
if p_settings["lat_bound"] and p_settings["lon_bound"]:
|
|
315
|
-
ws.reduce_domain(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
315
|
+
ws.reduce_domain(
|
|
316
|
+
{
|
|
317
|
+
"latitude": p_settings["lat_bound"],
|
|
318
|
+
"longitude": p_settings["lon_bound"],
|
|
319
|
+
}
|
|
320
|
+
)
|
|
319
321
|
|
|
320
322
|
self.ds = ws.get_xarray()
|
|
321
323
|
self.variable_names = ws.variable_names
|
|
@@ -302,7 +302,7 @@ class Cocip(Model):
|
|
|
302
302
|
self,
|
|
303
303
|
met: MetDataset,
|
|
304
304
|
rad: MetDataset,
|
|
305
|
-
params: dict[str, Any] =
|
|
305
|
+
params: dict[str, Any] | None = None,
|
|
306
306
|
**params_kwargs: Any,
|
|
307
307
|
) -> None:
|
|
308
308
|
# call Model init
|
|
@@ -379,15 +379,14 @@ class Cocip(Model):
|
|
|
379
379
|
self.source = self.require_source_type(Flight)
|
|
380
380
|
return_flight_list = isinstance(self.source, Fleet) and isinstance(source, Sequence)
|
|
381
381
|
|
|
382
|
-
self.
|
|
382
|
+
self._set_timesteps()
|
|
383
383
|
|
|
384
384
|
# Downselect met for CoCiP initialization
|
|
385
385
|
# We only need to buffer in the negative vertical direction,
|
|
386
386
|
# which is the positive direction for level
|
|
387
387
|
logger.debug("Downselect met for Cocip initialization")
|
|
388
|
-
|
|
389
|
-
met = self.source.downselect_met(self.met,
|
|
390
|
-
# Add tau_cirrus if it doesn't exist already.
|
|
388
|
+
level_buffer = 0, self.params["met_level_buffer"][1]
|
|
389
|
+
met = self.source.downselect_met(self.met, level_buffer=level_buffer, copy=False)
|
|
391
390
|
met = add_tau_cirrus(met)
|
|
392
391
|
|
|
393
392
|
# Prepare flight for model
|
|
@@ -438,8 +437,11 @@ class Cocip(Model):
|
|
|
438
437
|
|
|
439
438
|
return self.source
|
|
440
439
|
|
|
441
|
-
def
|
|
442
|
-
"""
|
|
440
|
+
def _set_timesteps(self) -> None:
|
|
441
|
+
"""Set the :attr:`timesteps` based on the ``source`` time range.
|
|
442
|
+
|
|
443
|
+
This method is called in :meth:`eval` before the flight is processed.
|
|
444
|
+
"""
|
|
443
445
|
if isinstance(self.source, Fleet):
|
|
444
446
|
# time not sorted in Fleet instance
|
|
445
447
|
tmin = self.source["time"].min()
|
|
@@ -1011,11 +1013,11 @@ class Cocip(Model):
|
|
|
1011
1013
|
np.timedelta64(0, "ns"),
|
|
1012
1014
|
time_end - latest_contrail["time"].max(),
|
|
1013
1015
|
)
|
|
1014
|
-
if time_end > met.
|
|
1016
|
+
if time_end > met.indexes["time"].to_numpy()[-1]:
|
|
1015
1017
|
logger.debug("Downselect met at time_end %s within Cocip evolution", time_end)
|
|
1016
1018
|
met = latest_contrail.downselect_met(self.met, **buffers, copy=False)
|
|
1017
1019
|
met = add_tau_cirrus(met)
|
|
1018
|
-
if time_end > rad.
|
|
1020
|
+
if time_end > rad.indexes["time"].to_numpy()[-1]:
|
|
1019
1021
|
logger.debug("Downselect rad at time_end %s within Cocip evolution", time_end)
|
|
1020
1022
|
rad = latest_contrail.downselect_met(self.rad, **buffers, copy=False)
|
|
1021
1023
|
|
|
@@ -1446,20 +1448,46 @@ def _process_rad(rad: MetDataset) -> MetDataset:
|
|
|
1446
1448
|
)
|
|
1447
1449
|
raise ValueError(msg) from exc
|
|
1448
1450
|
if radiation_accumulated:
|
|
1451
|
+
|
|
1452
|
+
# Don't assume that radiation data is uniformly spaced in time
|
|
1453
|
+
# Instead, infer the appropriate time shift
|
|
1454
|
+
time_diff = rad.data["time"].diff("time", label="upper")
|
|
1455
|
+
time_shift = -time_diff / 2
|
|
1456
|
+
|
|
1449
1457
|
# Keep the original attrs -- we need these later on
|
|
1450
1458
|
old_attrs = {k: v.attrs for k, v in rad.data.items()}
|
|
1451
1459
|
|
|
1460
|
+
# Also need to keep dataset-level attrs, which are lost
|
|
1461
|
+
# when dividing a Dataset by a DataArray
|
|
1462
|
+
old_rad_attrs = rad.data.attrs
|
|
1463
|
+
|
|
1452
1464
|
# NOTE: Taking the diff will remove the first time step
|
|
1453
1465
|
# This is typically what we want (forecast step 0 is all zeros)
|
|
1454
1466
|
# But, if the data has been downselected for a particular Flight / Fleet,
|
|
1455
1467
|
# we lose the first time step of the data.
|
|
1456
|
-
|
|
1468
|
+
#
|
|
1469
|
+
# Other portions of the code convert HRES accumulated fluxes (J/m2)
|
|
1470
|
+
# to averaged fluxes (W/m2) assuming that accumulations are over
|
|
1471
|
+
# one hour. For those conversions to work correctly, must normalize
|
|
1472
|
+
# accumulations by number of hours between steps
|
|
1473
|
+
time_diff_h = time_diff / np.timedelta64(1, "h")
|
|
1474
|
+
rad.data = rad.data.diff("time", label="upper") / time_diff_h
|
|
1475
|
+
rad.data.attrs = old_rad_attrs
|
|
1457
1476
|
|
|
1458
1477
|
# Add back the original attrs
|
|
1459
1478
|
for k, v in rad.data.items():
|
|
1460
1479
|
v.attrs = old_attrs[k]
|
|
1461
1480
|
|
|
1462
|
-
|
|
1481
|
+
# Short-circuit to avoid idiot check
|
|
1482
|
+
rad.data = rad.data.assign_coords({"time": rad.data["time"] + time_shift})
|
|
1483
|
+
if np.unique(time_shift).size == 1:
|
|
1484
|
+
rad.data["time"].attrs["shift_radiation_time"] = str(time_shift.values[0])
|
|
1485
|
+
else:
|
|
1486
|
+
rad.data["time"].attrs["shift_radiation_time"] = "variable"
|
|
1487
|
+
return rad
|
|
1488
|
+
|
|
1489
|
+
else:
|
|
1490
|
+
shift_radiation_time = -np.timedelta64(30, "m")
|
|
1463
1491
|
|
|
1464
1492
|
elif dataset == "ERA5" and product == "ensemble":
|
|
1465
1493
|
shift_radiation_time = -np.timedelta64(90, "m")
|
|
@@ -1507,17 +1535,19 @@ def _eval_aircraft_performance(
|
|
|
1507
1535
|
If ``aircraft_performance`` is None
|
|
1508
1536
|
"""
|
|
1509
1537
|
|
|
1510
|
-
|
|
1511
|
-
|
|
1538
|
+
ap_vars = {"wingspan", "engine_efficiency", "fuel_flow", "aircraft_mass"}
|
|
1539
|
+
missing = ap_vars.difference(flight).difference(flight.attrs)
|
|
1540
|
+
if not missing:
|
|
1512
1541
|
return flight
|
|
1513
1542
|
|
|
1514
1543
|
if aircraft_performance is None:
|
|
1515
|
-
|
|
1516
|
-
"An AircraftPerformance model parameter is required if the flight "
|
|
1517
|
-
f"
|
|
1518
|
-
"
|
|
1519
|
-
"'Cocip(..., aircraft_performance=PSFlight(...))'."
|
|
1544
|
+
msg = (
|
|
1545
|
+
f"An AircraftPerformance model parameter is required if the flight does not contain "
|
|
1546
|
+
f"the following variables: {ap_vars}. This flight is missing: {missing}. "
|
|
1547
|
+
"Instantiate the Cocip model with an AircraftPerformance model. "
|
|
1548
|
+
"For example, 'Cocip(..., aircraft_performance=PSFlight(...))'."
|
|
1520
1549
|
)
|
|
1550
|
+
raise ValueError(msg)
|
|
1521
1551
|
|
|
1522
1552
|
return aircraft_performance.eval(source=flight, copy_source=False)
|
|
1523
1553
|
|
|
@@ -35,16 +35,18 @@ def _habit_distributions() -> npt.NDArray[np.float32]:
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def _habits() -> npt.NDArray[np.str_]:
|
|
38
|
-
return np.array(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
return np.array(
|
|
39
|
+
[
|
|
40
|
+
"Sphere",
|
|
41
|
+
"Solid column",
|
|
42
|
+
"Hollow column",
|
|
43
|
+
"Rough aggregate",
|
|
44
|
+
"Rosette-6",
|
|
45
|
+
"Plate",
|
|
46
|
+
"Droxtal",
|
|
47
|
+
"Myhre",
|
|
48
|
+
]
|
|
49
|
+
)
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
@dataclasses.dataclass
|
|
@@ -184,6 +186,7 @@ class CocipParams(ModelParams):
|
|
|
184
186
|
#: Experimental. Radiative effects due to contrail-contrail overlapping
|
|
185
187
|
#: Account for change in local contrail shortwave and longwave radiative forcing
|
|
186
188
|
#: due to contrail-contrail overlapping.
|
|
189
|
+
#:
|
|
187
190
|
#: .. versionadded:: 0.45
|
|
188
191
|
contrail_contrail_overlapping: bool = False
|
|
189
192
|
|
|
@@ -293,16 +293,18 @@ def longitude_latitude_grid(
|
|
|
293
293
|
"""
|
|
294
294
|
# Ensure the required columns are included in `flight_waypoints`, `contrails` and `met`
|
|
295
295
|
flight_waypoints.ensure_vars(("segment_length", "ef"))
|
|
296
|
-
contrails.ensure_vars(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
296
|
+
contrails.ensure_vars(
|
|
297
|
+
(
|
|
298
|
+
"formation_time",
|
|
299
|
+
"segment_length",
|
|
300
|
+
"width",
|
|
301
|
+
"tau_contrail",
|
|
302
|
+
"rf_sw",
|
|
303
|
+
"rf_lw",
|
|
304
|
+
"rf_net",
|
|
305
|
+
"ef",
|
|
306
|
+
)
|
|
307
|
+
)
|
|
306
308
|
met.ensure_vars(
|
|
307
309
|
("air_temperature", "specific_humidity", "specific_cloud_ice_water_content", "geopotential")
|
|
308
310
|
)
|
|
@@ -857,35 +859,39 @@ def time_slice_statistics(
|
|
|
857
859
|
- ``mean_albedo_at_contrail_wypts``, [dimensionless]
|
|
858
860
|
"""
|
|
859
861
|
# Ensure the required columns are included in `flight_waypoints`, `contrails`, `met` and `rad`
|
|
860
|
-
flight_waypoints.ensure_vars(
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
862
|
+
flight_waypoints.ensure_vars(
|
|
863
|
+
(
|
|
864
|
+
"flight_id",
|
|
865
|
+
"segment_length",
|
|
866
|
+
"true_airspeed",
|
|
867
|
+
"fuel_flow",
|
|
868
|
+
"engine_efficiency",
|
|
869
|
+
"nvpm_ei_n",
|
|
870
|
+
"sac",
|
|
871
|
+
"persistent_1",
|
|
872
|
+
)
|
|
873
|
+
)
|
|
874
|
+
contrails.ensure_vars(
|
|
875
|
+
(
|
|
876
|
+
"flight_id",
|
|
877
|
+
"segment_length",
|
|
878
|
+
"air_temperature",
|
|
879
|
+
"iwc",
|
|
880
|
+
"r_ice_vol",
|
|
881
|
+
"n_ice_per_m",
|
|
882
|
+
"tau_contrail",
|
|
883
|
+
"tau_cirrus",
|
|
884
|
+
"width",
|
|
885
|
+
"area_eff",
|
|
886
|
+
"sdr",
|
|
887
|
+
"rsr",
|
|
888
|
+
"olr",
|
|
889
|
+
"rf_sw",
|
|
890
|
+
"rf_lw",
|
|
891
|
+
"rf_net",
|
|
892
|
+
"ef",
|
|
893
|
+
)
|
|
894
|
+
)
|
|
889
895
|
|
|
890
896
|
# Ensure that the waypoints are within `t_start` and `t_end`
|
|
891
897
|
is_in_time = flight_waypoints.dataframe["time"].between(t_start, t_end, inclusive="right")
|
|
@@ -920,12 +926,14 @@ def time_slice_statistics(
|
|
|
920
926
|
|
|
921
927
|
# Meteorology domain statistics
|
|
922
928
|
if met is not None:
|
|
923
|
-
met.ensure_vars(
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
+
met.ensure_vars(
|
|
930
|
+
(
|
|
931
|
+
"air_temperature",
|
|
932
|
+
"specific_humidity",
|
|
933
|
+
"specific_cloud_ice_water_content",
|
|
934
|
+
"geopotential",
|
|
935
|
+
)
|
|
936
|
+
)
|
|
929
937
|
met = met.downselect(spatial_bbox)
|
|
930
938
|
met_stats = meteorological_time_slice_statistics(t_end, contrails, met, humidity_scaling)
|
|
931
939
|
|
|
@@ -1929,13 +1937,15 @@ def natural_cirrus_properties_to_hi_res_grid(
|
|
|
1929
1937
|
relatively narrow contrails.
|
|
1930
1938
|
"""
|
|
1931
1939
|
# Ensure the required columns are included in `met`
|
|
1932
|
-
met.ensure_vars(
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1940
|
+
met.ensure_vars(
|
|
1941
|
+
(
|
|
1942
|
+
"air_temperature",
|
|
1943
|
+
"specific_humidity",
|
|
1944
|
+
"specific_cloud_ice_water_content",
|
|
1945
|
+
"geopotential",
|
|
1946
|
+
"fraction_of_cloud_cover",
|
|
1947
|
+
)
|
|
1948
|
+
)
|
|
1939
1949
|
|
|
1940
1950
|
# Ensure `met` only contains one time step, constraint can be relaxed in the future.
|
|
1941
1951
|
if len(met["time"].data) > 1:
|