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.

Files changed (29) hide show
  1. pycontrails/_version.py +2 -2
  2. pycontrails/core/datalib.py +1 -1
  3. pycontrails/core/flight.py +11 -11
  4. pycontrails/core/interpolation.py +29 -19
  5. pycontrails/core/met.py +192 -104
  6. pycontrails/core/models.py +29 -15
  7. pycontrails/core/rgi_cython.cp312-win_amd64.pyd +0 -0
  8. pycontrails/core/vector.py +14 -15
  9. pycontrails/datalib/gfs/gfs.py +1 -1
  10. pycontrails/datalib/spire/spire.py +23 -19
  11. pycontrails/ext/synthetic_flight.py +3 -1
  12. pycontrails/models/accf.py +6 -4
  13. pycontrails/models/cocip/cocip.py +48 -18
  14. pycontrails/models/cocip/cocip_params.py +13 -10
  15. pycontrails/models/cocip/output_formats.py +62 -52
  16. pycontrails/models/cocipgrid/cocip_grid.py +459 -275
  17. pycontrails/models/cocipgrid/cocip_grid_params.py +12 -18
  18. pycontrails/models/emissions/ffm2.py +10 -8
  19. pycontrails/models/pcc.py +1 -1
  20. pycontrails/models/ps_model/ps_aircraft_params.py +1 -1
  21. pycontrails/models/ps_model/static/{ps-aircraft-params-20231117.csv → ps-aircraft-params-20240209.csv} +12 -3
  22. pycontrails/utils/json.py +12 -10
  23. {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/METADATA +2 -2
  24. {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/RECORD +28 -29
  25. pycontrails/models/cocipgrid/cocip_time_handling.py +0 -342
  26. {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/LICENSE +0 -0
  27. {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/NOTICE +0 -0
  28. {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/WHEEL +0 -0
  29. {pycontrails-0.49.3.dist-info → pycontrails-0.49.5.dist-info}/top_level.txt +0 -0
@@ -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
- raise TypeError(f"Unable to handle input eval data {source}")
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
- if not hasattr(self, "source"):
520
- raise ValueError("Attribute `source` must be defined before calling `downselect_met`")
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(self.source, GeoVectorDataset):
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 = self.source.downselect_met(self.met, **kwargs, copy=False)
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
- raise TypeError(f"Unknown source type: {type(self.source)}")
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
- raise NotImplementedError(f"Unsupported q_method: {q_method}")
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
- raise KeyError(
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
- raise KeyError(
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
- raise ValueError(f"No variable key '{vector_key}' in 'vector' and 'met' is None")
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
- raise KeyError(f"No variable key '{met_key}' in 'met'.") from exc
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
- raise KeyError(f"No variable key '{met_key}' in 'met'.") from exc
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
- raise KeyError(f"No variable key '{met_key}' in 'met'.") from exc
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
- raise ValueError("Cubic spline interpolation requires data to span 50-1000 hPa.")
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
- raise ValueError(f"Invalid 'q_method' value '{q_method}'. Must be one of {available}.")
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
@@ -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
- self.data.update({key: np.full(self.size, scalar, dtype=np.float32)})
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
- variables = met.variables
1608
+ indexes = met.indexes
1611
1609
 
1612
1610
  lat_intersect = coordinates.intersect_domain(
1613
- variables["latitude"].values,
1611
+ indexes["latitude"].to_numpy(),
1614
1612
  self["latitude"],
1615
1613
  )
1616
1614
  lon_intersect = coordinates.intersect_domain(
1617
- variables["longitude"].values,
1615
+ indexes["longitude"].to_numpy(),
1618
1616
  self["longitude"],
1619
1617
  )
1620
1618
  level_intersect = coordinates.intersect_domain(
1621
- variables["level"].values,
1619
+ indexes["level"].to_numpy(),
1622
1620
  self.level,
1623
1621
  )
1624
1622
  time_intersect = coordinates.intersect_domain(
1625
- variables["time"].values,
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
- met.variables["longitude"].values,
1904
+ variables["longitude"].to_numpy(),
1906
1905
  self["longitude"],
1907
1906
  buffer=longitude_buffer,
1908
1907
  )
1909
1908
  lat_slice = coordinates.slice_domain(
1910
- met.variables["latitude"].values,
1909
+ variables["latitude"].to_numpy(),
1911
1910
  self["latitude"],
1912
1911
  buffer=latitude_buffer,
1913
1912
  )
1914
1913
  time_slice = coordinates.slice_domain(
1915
- met.variables["time"].values,
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
- met.variables["level"].values,
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 -8.0 ... 8.0 8.5 9.0 9.5
2047
- * latitude (latitude) float64 -10.0 -9.5 -9.0 -8.5 -8.0 ... 8.0 8.5 9.0 9.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],
@@ -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 = [-1],
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
- "icao_address",
203
- "tail_number",
204
- "aircraft_type_icao",
205
- "callsign",
206
- "timestamp",
207
- "longitude",
208
- "latitude",
209
- "altitude_baro",
210
- "on_ground",
211
- ]].groupby(["icao_address", "tail_number", "aircraft_type_icao", "callsign"], sort=False):
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
- "flight_id",
547
- "aircraft_type_icao",
548
- "timestamp",
549
- "altitude_baro",
550
- "on_ground",
551
- "speed",
552
- ]].groupby("flight_id", sort=False):
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 = np.timedelta64(1, "m")) -> Flight:
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.
@@ -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
- "latitude": p_settings["lat_bound"],
317
- "longitude": p_settings["lon_bound"],
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._calc_timesteps()
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
- buffer = 0, self.params["met_level_buffer"][1]
389
- met = self.source.downselect_met(self.met, copy=False, level_buffer=buffer)
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 _calc_timesteps(self) -> None:
442
- """Calculate :attr:`timesteps`."""
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.variables["time"].values[-1]:
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.variables["time"].values[-1]:
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
- rad.data = rad.data.diff("time", label="upper")
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
- shift_radiation_time = -np.timedelta64(30, "m")
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
- aircraft_performance_outputs = "wingspan", "engine_efficiency", "fuel_flow", "aircraft_mass"
1511
- if flight.ensure_vars(aircraft_performance_outputs, False):
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
- raise ValueError(
1516
- "An AircraftPerformance model parameter is required if the flight "
1517
- f"does not contain the following variables: {aircraft_performance_outputs}. "
1518
- "For example, instantiate the Cocip model with "
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
- "Sphere",
40
- "Solid column",
41
- "Hollow column",
42
- "Rough aggregate",
43
- "Rosette-6",
44
- "Plate",
45
- "Droxtal",
46
- "Myhre",
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
- "formation_time",
298
- "segment_length",
299
- "width",
300
- "tau_contrail",
301
- "rf_sw",
302
- "rf_lw",
303
- "rf_net",
304
- "ef",
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
- "flight_id",
862
- "segment_length",
863
- "true_airspeed",
864
- "fuel_flow",
865
- "engine_efficiency",
866
- "nvpm_ei_n",
867
- "sac",
868
- "persistent_1",
869
- ))
870
- contrails.ensure_vars((
871
- "flight_id",
872
- "segment_length",
873
- "air_temperature",
874
- "iwc",
875
- "r_ice_vol",
876
- "n_ice_per_m",
877
- "tau_contrail",
878
- "tau_cirrus",
879
- "width",
880
- "area_eff",
881
- "sdr",
882
- "rsr",
883
- "olr",
884
- "rf_sw",
885
- "rf_lw",
886
- "rf_net",
887
- "ef",
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
- "air_temperature",
925
- "specific_humidity",
926
- "specific_cloud_ice_water_content",
927
- "geopotential",
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
- "air_temperature",
1934
- "specific_humidity",
1935
- "specific_cloud_ice_water_content",
1936
- "geopotential",
1937
- "fraction_of_cloud_cover",
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: