pycontrails 0.50.0__cp311-cp311-win_amd64.whl → 0.50.1__cp311-cp311-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 CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.50.0'
16
- __version_tuple__ = version_tuple = (0, 50, 0)
15
+ __version__ = version = '0.50.1'
16
+ __version_tuple__ = version_tuple = (0, 50, 1)
@@ -787,6 +787,13 @@ class Flight(GeoVectorDataset):
787
787
  Waypoints are resampled according to the frequency ``freq``. Values for :attr:`data`
788
788
  columns ``longitude``, ``latitude``, and ``altitude`` are interpolated.
789
789
 
790
+ Resampled waypoints will include all multiples of ``freq`` between the flight
791
+ start and end time. For example, when resampling to a frequency of 1 minute,
792
+ a flight that starts at 2020/1/1 00:00:59 and ends at 2020/1/1 00:01:01
793
+ will return a single waypoint at 2020/1/1 00:01:00, whereas a flight that
794
+ starts at 2020/1/1 00:01:01 and ends at 2020/1/1 00:01:59 will return an empty
795
+ flight.
796
+
790
797
  Parameters
791
798
  ----------
792
799
  freq : str, optional
@@ -1349,8 +1356,8 @@ class Flight(GeoVectorDataset):
1349
1356
  >>> # Intersect and attach
1350
1357
  >>> fl["air_temperature"] = fl.intersect_met(met['air_temperature'])
1351
1358
  >>> fl["air_temperature"]
1352
- array([235.94658, 235.95767, 235.96873, ..., 234.59918, 234.60388,
1353
- 234.60846], dtype=float32)
1359
+ array([235.94657007, 235.95766965, 235.96873412, ..., 234.59917962,
1360
+ 234.60387402, 234.60845312])
1354
1361
 
1355
1362
  >>> # Length (in meters) of waypoints whose temperature exceeds 236K
1356
1363
  >>> fl.length_met("air_temperature", threshold=236)
@@ -2063,11 +2070,9 @@ def _resample_to_freq(df: pd.DataFrame, freq: str) -> tuple[pd.DataFrame, pd.Dat
2063
2070
 
2064
2071
  # Manually create a new index that includes all the original index values
2065
2072
  # and the resampled-to-freq index values.
2066
- t0 = df.index[0]
2073
+ t0 = df.index[0].ceil(freq)
2067
2074
  t1 = df.index[-1]
2068
- t = pd.date_range(t0, t1, freq=freq, name="time").floor(freq)
2069
- if t[0] < t0:
2070
- t = t[1:]
2075
+ t = pd.date_range(t0, t1, freq=freq, name="time")
2071
2076
 
2072
2077
  concat_arr = np.concatenate([df.index, t])
2073
2078
  concat_arr = np.unique(concat_arr)
@@ -71,7 +71,9 @@ class PycontrailsRegularGridInterpolator(scipy.interpolate.RegularGridInterpolat
71
71
 
72
72
  self.grid = points
73
73
  self.values = values
74
- self.method = method
74
+ # TODO: consider supporting updated tensor-product spline methods
75
+ # see https://github.com/scipy/scipy/releases/tag/v1.13.0
76
+ self.method = _pick_method(scipy.__version__, method)
75
77
  self.bounds_error = bounds_error
76
78
  self.fill_value = fill_value
77
79
 
@@ -219,6 +221,42 @@ class PycontrailsRegularGridInterpolator(scipy.interpolate.RegularGridInterpolat
219
221
  raise ValueError(msg)
220
222
 
221
223
 
224
+ def _pick_method(scipy_version: str, method: str) -> str:
225
+ """Select an interpolation method.
226
+
227
+ For scipy versions 1.13.0 and later, fall back on legacy implementations
228
+ of tensor-product spline methods. The default implementations in 1.13.0
229
+ and later are incompatible with this class.
230
+
231
+ Parameters
232
+ ----------
233
+ scipy_version : str
234
+ scipy version (major.minor.patch)
235
+
236
+ method : str
237
+ Interpolation method. Passed into :class:`scipy.interpolate.RegularGridInterpolator`
238
+ as-is unless ``scipy_version`` is 1.13.0 or later and ``method`` is ``"slinear"``,
239
+ ``"cubic"``, or ``"quintic"``. In this case, ``"_legacy"`` is appended to ``method``.
240
+
241
+ Returns
242
+ -------
243
+ str
244
+ Interpolation method adjusted for compatibility with this class.
245
+ """
246
+ try:
247
+ version = scipy_version.split(".")
248
+ major = int(version[0])
249
+ minor = int(version[1])
250
+ except (IndexError, ValueError) as exc:
251
+ msg = f"Failed to parse major and minor version from {scipy_version}"
252
+ raise ValueError(msg) from exc
253
+
254
+ reimplemented_methods = ["slinear", "cubic", "quintic"]
255
+ if major > 1 or (major == 1 and minor >= 13) and method in reimplemented_methods:
256
+ return method + "_legacy"
257
+ return method
258
+
259
+
222
260
  def _floatize_time(
223
261
  time: npt.NDArray[np.datetime64], offset: np.datetime64
224
262
  ) -> npt.NDArray[np.float64]:
pycontrails/core/met.py CHANGED
@@ -674,13 +674,10 @@ class MetDataset(MetBase):
674
674
  >>> da = mda.data # Underlying `xarray` object
675
675
 
676
676
  >>> # Check out a few values
677
- >>> da[5:10, 5:10, 1, 1].values
678
- array([[224.0896 , 224.41374, 224.75946, 225.16237, 225.60507],
679
- [224.09457, 224.42038, 224.76526, 225.16817, 225.61089],
680
- [224.10037, 224.42618, 224.77106, 225.17314, 225.61586],
681
- [224.10617, 224.43282, 224.7777 , 225.17812, 225.62166],
682
- [224.11115, 224.44028, 224.7835 , 225.18393, 225.62663]],
683
- dtype=float32)
677
+ >>> da[5:8, 5:8, 1, 1].values
678
+ array([[224.08959005, 224.41374427, 224.75945349],
679
+ [224.09456429, 224.42037658, 224.76525676],
680
+ [224.10036756, 224.42617985, 224.77106004]])
684
681
 
685
682
  >>> # Mean temperature over entire array
686
683
  >>> da.mean().load().item()
@@ -1618,15 +1615,15 @@ class MetDataArray(MetBase):
1618
1615
 
1619
1616
  >>> # Interpolation at a grid point agrees with value
1620
1617
  >>> mda.interpolate(1, 2, 300, np.datetime64('2022-03-01T14:00'))
1621
- array([241.91974], dtype=float32)
1618
+ array([241.91972984])
1622
1619
 
1623
1620
  >>> da = mda.data
1624
1621
  >>> da.sel(longitude=1, latitude=2, level=300, time=np.datetime64('2022-03-01T14')).item()
1625
- 241.91974
1622
+ 241.9197298421629
1626
1623
 
1627
1624
  >>> # Interpolation off grid
1628
1625
  >>> mda.interpolate(1.1, 2.1, 290, np.datetime64('2022-03-01 13:10'))
1629
- array([239.83794], dtype=float32)
1626
+ array([239.83793798])
1630
1627
 
1631
1628
  >>> # Interpolate along path
1632
1629
  >>> longitude = np.linspace(1, 2, 10)
@@ -1634,8 +1631,9 @@ class MetDataArray(MetBase):
1634
1631
  >>> level = np.linspace(200, 300, 10)
1635
1632
  >>> time = pd.date_range("2022-03-01T14", periods=10, freq="5min")
1636
1633
  >>> mda.interpolate(longitude, latitude, level, time)
1637
- array([220.44348, 223.089 , 225.7434 , 228.41643, 231.10858, 233.54858,
1638
- 235.71506, 237.86479, 239.99275, 242.10793], dtype=float32)
1634
+ array([220.44347694, 223.08900738, 225.74338924, 228.41642088,
1635
+ 231.10858599, 233.54857391, 235.71504913, 237.86478872,
1636
+ 239.99274623, 242.10792167])
1639
1637
  """
1640
1638
  # Load if necessary
1641
1639
  if not self.in_memory:
@@ -1694,12 +1694,14 @@ class GeoVectorDataset(VectorDataset):
1694
1694
 
1695
1695
  >>> # Intersect
1696
1696
  >>> fl.intersect_met(met['air_temperature'], method='nearest')
1697
- array([231.6297 , 230.72604, 232.2432 , 231.88339, 231.0643 , 231.59073,
1698
- 231.65126, 231.93065, 232.03345, 231.65955], dtype=float32)
1697
+ array([231.62969892, 230.72604651, 232.24318771, 231.88338483,
1698
+ 231.06429438, 231.59073409, 231.65125393, 231.93064004,
1699
+ 232.03344087, 231.65954432])
1699
1700
 
1700
1701
  >>> fl.intersect_met(met['air_temperature'], method='linear')
1701
- array([225.77794, 225.13908, 226.23122, 226.31831, 225.56102, 225.81192,
1702
- 226.03194, 226.22057, 226.0377 , 225.63226], dtype=float32)
1702
+ array([225.77794552, 225.13908414, 226.231218 , 226.31831528,
1703
+ 225.56102321, 225.81192149, 226.03192642, 226.22056121,
1704
+ 226.03770174, 225.63226188])
1703
1705
 
1704
1706
  >>> # Interpolate and attach to `Flight` instance
1705
1707
  >>> for key in met:
@@ -1708,11 +1710,11 @@ class GeoVectorDataset(VectorDataset):
1708
1710
  >>> # Show the final three columns of the dataframe
1709
1711
  >>> fl.dataframe.iloc[:, -3:].head()
1710
1712
  time air_temperature specific_humidity
1711
- 0 2022-03-01 00:00:00 225.777939 0.000132
1713
+ 0 2022-03-01 00:00:00 225.777946 0.000132
1712
1714
  1 2022-03-01 00:13:20 225.139084 0.000132
1713
- 2 2022-03-01 00:26:40 226.231216 0.000107
1714
- 3 2022-03-01 00:40:00 226.318314 0.000171
1715
- 4 2022-03-01 00:53:20 225.561020 0.000109
1715
+ 2 2022-03-01 00:26:40 226.231218 0.000107
1716
+ 3 2022-03-01 00:40:00 226.318315 0.000171
1717
+ 4 2022-03-01 00:53:20 225.561022 0.000109
1716
1718
 
1717
1719
  """
1718
1720
  # Override use_indices in certain situations
@@ -50,7 +50,7 @@ except ModuleNotFoundError as exc:
50
50
 
51
51
 
52
52
  #: Default channels to use if none are specified. These are the channels
53
- #: required by the MIT ash color scheme.
53
+ #: required by the SEVIRI (MIT) ash color scheme.
54
54
  DEFAULT_CHANNELS = "C11", "C14", "C15"
55
55
 
56
56
  #: The time at which the GOES scan mode changed from mode 3 to mode 6. This
@@ -203,10 +203,10 @@ def gcs_goes_path(
203
203
  GOES Region of interest.
204
204
  channels : str | Iterable[str]
205
205
  Set of channels or bands for CMIP data. The 16 possible channels are
206
- represented by the strings "C01" to "C16". For the MIT ash color scheme,
206
+ represented by the strings "C01" to "C16". For the SEVIRI ash color scheme,
207
207
  set ``channels=("C11", "C14", "C15")``. For the true color scheme,
208
208
  set ``channels=("C01", "C02", "C03")``. By default, the channels
209
- required by the MIT ash color scheme are used.
209
+ required by the SEVIRI ash color scheme are used.
210
210
 
211
211
  Returns
212
212
  -------
@@ -306,10 +306,10 @@ class GOES:
306
306
 
307
307
  channels : str | set[str] | None
308
308
  Set of channels or bands for CMIP data. The 16 possible channels are
309
- represented by the strings "C01" to "C16". For the MIT ash color scheme,
309
+ represented by the strings "C01" to "C16". For the SEVIRI ash color scheme,
310
310
  set ``channels=("C11", "C14", "C15")``. For the true color scheme,
311
311
  set ``channels=("C01", "C02", "C03")``. By default, the channels
312
- required by the MIT ash color scheme are used. The channels must have
312
+ required by the SEVIRI ash color scheme are used. The channels must have
313
313
  a common horizontal resolution. The resolutions are:
314
314
 
315
315
  - C01: 1.0 km
@@ -585,7 +585,7 @@ def _concat_c02(ds1: XArrayType, ds2: XArrayType) -> XArrayType:
585
585
  def extract_goes_visualization(
586
586
  da: xr.DataArray,
587
587
  color_scheme: str = "ash",
588
- ash_convention: str = "MIT",
588
+ ash_convention: str = "SEVIRI",
589
589
  gamma: float = 2.2,
590
590
  ) -> tuple[npt.NDArray[np.float32], ccrs.Geostationary, tuple[float, float, float, float]]:
591
591
  """Extract artifacts for visualizing GOES data with the given color scheme.
@@ -597,7 +597,7 @@ def extract_goes_visualization(
597
597
  required by :func:`to_ash`.
598
598
  color_scheme : str = {"ash", "true"}
599
599
  Color scheme to use for visualization.
600
- ash_convention : str = {"MIT", "standard"}
600
+ ash_convention : str = {"SEVIRI", "standard"}
601
601
  Passed into :func:`to_ash`. Only used if ``color_scheme="ash"``.
602
602
  gamma : float = 2.2
603
603
  Passed into :func:`to_true_color`. Only used if ``color_scheme="true"``.
@@ -672,17 +672,18 @@ def to_true_color(da: xr.DataArray, gamma: float = 2.2) -> npt.NDArray[np.float3
672
672
  return np.dstack([red, green, blue])
673
673
 
674
674
 
675
- def to_ash(da: xr.DataArray, convention: str = "MIT") -> npt.NDArray[np.float32]:
675
+ def to_ash(da: xr.DataArray, convention: str = "SEVIRI") -> npt.NDArray[np.float32]:
676
676
  """Compute 3d RGB array for the ASH color scheme.
677
677
 
678
678
  Parameters
679
679
  ----------
680
680
  da : xr.DataArray
681
681
  DataArray of GOES data with appropriate channels.
682
- convention : str = {"MIT", "standard"}
682
+ convention : str = {"SEVIRI", "standard"}
683
683
  Convention for color space.
684
684
 
685
- - MIT convention requires channels C11, C14, C15
685
+ - SEVIRI convention requires channels C11, C14, C15.
686
+ Used in :cite:`kulikSatellitebasedDetectionContrails2019`.
686
687
  - Standard convention requires channels C11, C13, C14, C15
687
688
 
688
689
  Returns
@@ -693,6 +694,7 @@ def to_ash(da: xr.DataArray, convention: str = "MIT") -> npt.NDArray[np.float32]
693
694
  References
694
695
  ----------
695
696
  - `Ash RGB quick guide (the color space and color interpretations) <https://rammb.cira.colostate.edu/training/visit/quick_guides/GOES_Ash_RGB.pdf>`_
697
+ - :cite:`SEVIRIRGBCal`
696
698
  - :cite:`kulikSatellitebasedDetectionContrails2019`
697
699
 
698
700
  Examples
@@ -716,7 +718,7 @@ def to_ash(da: xr.DataArray, convention: str = "MIT") -> npt.NDArray[np.float32]
716
718
  green = c14 - c11
717
719
  blue = c13
718
720
 
719
- elif convention == "MIT":
721
+ elif convention in ["SEVIRI", "MIT"]: # retain MIT for backwards compatibility
720
722
  c11 = da.sel(band_id=11).values # 8.44
721
723
  c14 = da.sel(band_id=14).values # 11.19
722
724
  c15 = da.sel(band_id=15).values # 12.27
@@ -726,7 +728,7 @@ def to_ash(da: xr.DataArray, convention: str = "MIT") -> npt.NDArray[np.float32]
726
728
  blue = c14
727
729
 
728
730
  else:
729
- raise ValueError("Convention must be either 'MIT' or 'standard'")
731
+ raise ValueError("Convention must be either 'SEVIRI' or 'standard'")
730
732
 
731
733
  # See colostate pdf for slightly wider values
732
734
  red = _clip_and_scale(red, -4.0, 2.0)
@@ -26,6 +26,7 @@ from pycontrails.models.cocip import (
26
26
  contrail_properties,
27
27
  radiative_forcing,
28
28
  radiative_heating,
29
+ unterstrasser_wake_vortex,
29
30
  wake_vortex,
30
31
  wind_shear,
31
32
  )
@@ -838,9 +839,9 @@ class Cocip(Model):
838
839
  T_critical_sac = self._sac_flight["T_critical_sac"]
839
840
 
840
841
  # Flight performance parameters
841
- fuel_dist = (
842
- self._sac_flight.get_data_or_attr("fuel_flow") / self._sac_flight["true_airspeed"]
843
- )
842
+ fuel_flow = self._sac_flight.get_data_or_attr("fuel_flow")
843
+ true_airspeed = self._sac_flight["true_airspeed"]
844
+ fuel_dist = fuel_flow / true_airspeed
844
845
 
845
846
  nvpm_ei_n = self._sac_flight.get_data_or_attr("nvpm_ei_n")
846
847
  ei_h2o = self._sac_flight.fuel.ei_h2o
@@ -890,11 +891,27 @@ class Cocip(Model):
890
891
  air_temperature, air_pressure, air_pressure_1
891
892
  )
892
893
  iwc_1 = contrail_properties.iwc_post_wake_vortex(iwc, iwc_ad)
894
+
895
+ if self.params["unterstrasser_ice_survival_fraction"]:
896
+ wingspan = self._sac_flight.get_data_or_attr("wingspan")
897
+ rhi_0 = thermo.rhi(specific_humidity, air_temperature, air_pressure)
898
+ f_surv = unterstrasser_wake_vortex.ice_particle_number_survival_fraction(
899
+ air_temperature,
900
+ rhi_0,
901
+ ei_h2o,
902
+ wingspan,
903
+ true_airspeed,
904
+ fuel_flow,
905
+ nvpm_ei_n,
906
+ 0.5 * depth, # Taking the mid-point of the contrail plume
907
+ )
908
+ else:
909
+ f_surv = contrail_properties.ice_particle_survival_fraction(iwc, iwc_1)
910
+
893
911
  n_ice_per_m_1 = contrail_properties.ice_particle_number(
894
912
  nvpm_ei_n=nvpm_ei_n,
895
913
  fuel_dist=fuel_dist,
896
- iwc=iwc,
897
- iwc_1=iwc_1,
914
+ f_surv=f_surv,
898
915
  air_temperature=air_temperature,
899
916
  T_crit_sac=T_critical_sac,
900
917
  min_ice_particle_number_nvpm_ei_n=self.params["min_ice_particle_number_nvpm_ei_n"],
@@ -138,12 +138,12 @@ class CocipParams(ModelParams):
138
138
 
139
139
  #: Add additional metric of ATR20 and global yearly mean RF to model output.
140
140
  #: These are not standard CoCiP outputs but based on the derivation used
141
- #: in the first supplement to :cite:`yin_predicting_2023`. ATR20 is defined
141
+ #: in the first supplement to :cite:`yinPredictingClimateImpact2023`. ATR20 is defined
142
142
  #: as the average temperature response over a 20 year horizon.
143
143
  compute_atr20: bool = False
144
144
 
145
145
  #: Constant factor used to convert global- and year-mean RF, [:math:`W m^{-2}`],
146
- #: to ATR20, [:math:`K`], given by :cite:`yin_predicting_2023`.
146
+ #: to ATR20, [:math:`K`], given by :cite:`yinPredictingClimateImpact2023`.
147
147
  global_rf_to_atr20_factor: float = 0.0151
148
148
 
149
149
  # ----------------
@@ -184,6 +184,15 @@ class CocipParams(ModelParams):
184
184
  #: :attr:`radiative_heating_effects` is enabled.
185
185
  max_depth: float = 1500.0
186
186
 
187
+ #: Experimental. Improved ice crystal number survival fraction in the wake vortex phase.
188
+ #: Implement :cite:`unterstrasserPropertiesYoungContrails2016`, who developed a
189
+ #: parametric model that estimates the survival fraction of the contrail ice crystal
190
+ #: number after the wake vortex phase based on the results from large eddy simulations.
191
+ #: This replicates Fig. 4 of :cite:`karcherFormationRadiativeForcing2018`.
192
+ #:
193
+ #: .. versionadded:: 0.50.1
194
+ unterstrasser_ice_survival_fraction: bool = False
195
+
187
196
  #: Experimental. Radiative heating effects on contrail cirrus properties.
188
197
  #: Terrestrial and solar radiances warm the contrail ice particles and cause
189
198
  #: convective turbulence. This effect is expected to enhance vertical mixing
@@ -204,8 +204,7 @@ def iwc_post_wake_vortex(
204
204
  def ice_particle_number(
205
205
  nvpm_ei_n: npt.NDArray[np.float64],
206
206
  fuel_dist: npt.NDArray[np.float64],
207
- iwc: npt.NDArray[np.float64],
208
- iwc_1: npt.NDArray[np.float64],
207
+ f_surv: npt.NDArray[np.float64],
209
208
  air_temperature: npt.NDArray[np.float64],
210
209
  T_crit_sac: npt.NDArray[np.float64],
211
210
  min_ice_particle_number_nvpm_ei_n: float,
@@ -223,11 +222,8 @@ def ice_particle_number(
223
222
  black carbon number emissions index, [:math:`kg^{-1}`]
224
223
  fuel_dist : npt.NDArray[np.float64]
225
224
  fuel consumption of the flight segment per distance traveled, [:math:`kg m^{-1}`]
226
- iwc : npt.NDArray[np.float64]
227
- initial ice water content at each flight waypoint before the wake vortex
228
- phase, [:math:`kg_{H_{2}O}/kg_{air}`]
229
- iwc_1 : npt.NDArray[np.float64]
230
- ice water content after the wake vortex phase, [:math:`kg_{H_{2}O}/kg_{air}`]
225
+ f_surv : npt.NDArray[np.float64]
226
+ Fraction of contrail ice particle number that survive the wake vortex phase.
231
227
  air_temperature : npt.NDArray[np.float64]
232
228
  ambient temperature for each waypoint, [:math:`K`]
233
229
  T_crit_sac : npt.NDArray[np.float64]
@@ -241,7 +237,6 @@ def ice_particle_number(
241
237
  npt.NDArray[np.float64]
242
238
  initial number of ice particles per distance after the wake vortex phase, [:math:`# m^{-1}`]
243
239
  """
244
- f_surv = ice_particle_survival_factor(iwc, iwc_1)
245
240
  f_activation = ice_particle_activation_rate(air_temperature, T_crit_sac)
246
241
  nvpm_ei_n_activated = nvpm_ei_n * f_activation
247
242
  return fuel_dist * np.maximum(nvpm_ei_n_activated, min_ice_particle_number_nvpm_ei_n) * f_surv
@@ -289,7 +284,7 @@ def ice_particle_activation_rate(
289
284
  return -0.661 * np.exp(d_temp) + 1.0
290
285
 
291
286
 
292
- def ice_particle_survival_factor(
287
+ def ice_particle_survival_fraction(
293
288
  iwc: npt.NDArray[np.float64], iwc_1: npt.NDArray[np.float64]
294
289
  ) -> npt.NDArray[np.float64]:
295
290
  """
@@ -0,0 +1,403 @@
1
+ """Wave-vortex downwash functions from Unterstrasser (2016).
2
+
3
+ Notes
4
+ -----
5
+ :cite:`unterstrasserPropertiesYoungContrails2016` provides a parameterized model of the
6
+ survival fraction of the contrail ice crystal number ``f_surv`` during the wake-vortex phase.
7
+ The model was developed based on output from large eddy simulations, and improves agreement with
8
+ LES outputs relative to the default survival fraction parameterization used in CoCiP.
9
+
10
+ For comparison, CoCiP assumes that ``f_surv`` is equal to the change in the contrail ice water
11
+ content (by mass) before and after the wake vortex phase. However, for larger (smaller) ice
12
+ particles, their survival fraction by number could be smaller (larger) than their survival fraction
13
+ by mass. This is particularly important in the "soot-poor" scenario, for example, in cleaner
14
+ lean-burn engines where their soot emissions can be 3-4 orders of magnitude lower than conventional
15
+ RQL engines.
16
+
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import numpy as np
22
+ import numpy.typing as npt
23
+
24
+ from pycontrails.models.cocip.wake_vortex import wake_vortex_separation
25
+ from pycontrails.physics import constants, thermo
26
+
27
+
28
+ def ice_particle_number_survival_fraction(
29
+ air_temperature: npt.NDArray[np.float64],
30
+ rhi_0: npt.NDArray[np.float64],
31
+ ei_h2o: npt.NDArray[np.float64] | float,
32
+ wingspan: npt.NDArray[np.float64] | float,
33
+ true_airspeed: npt.NDArray[np.float64],
34
+ fuel_flow: npt.NDArray[np.float64],
35
+ aei_n: npt.NDArray[np.float64],
36
+ z_desc: npt.NDArray[np.float64],
37
+ ) -> npt.NDArray[np.float64]:
38
+ r"""
39
+ Calculate fraction of ice particle number surviving the wake vortex phase and required inputs.
40
+
41
+ This implementation is based on the work of :cite:`unterstrasserPropertiesYoungContrails2016`
42
+ and is an improved estimation compared with
43
+ :func:`contrail_properties.ice_particle_survival_fraction`.
44
+
45
+ Parameters
46
+ ----------
47
+ air_temperature : npt.NDArray[np.float64]
48
+ ambient temperature for each waypoint, [:math:`K`]
49
+ rhi_0: npt.NDArray[np.float64]
50
+ Relative humidity with respect to ice at the flight waypoint
51
+ ei_h2o : npt.NDArray[np.float64] | float
52
+ Emission index of water vapor, [:math:`kg \ kg^{-1}`]
53
+ wingspan : npt.NDArray[np.float64] | float
54
+ aircraft wingspan, [:math:`m`]
55
+ true_airspeed : npt.NDArray[np.float64]
56
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
57
+ fuel_flow : npt.NDArray[np.float64]
58
+ Fuel mass flow rate, [:math:`kg s^{-1}`]
59
+ aei_n : npt.NDArray[np.float64]
60
+ Apparent ice crystal number emissions index at contrail formation, [:math:`kg^{-1}`]
61
+ z_desc : npt.NDArray[np.float64]
62
+ Final vertical displacement of the wake vortex, ``dz_max`` in :mod:`wake_vortex.py`,
63
+ [:math:`m`].
64
+
65
+ Returns
66
+ -------
67
+ npt.NDArray[np.float64]
68
+ Fraction of contrail ice particle number that survive the wake vortex phase.
69
+
70
+ References
71
+ ----------
72
+ - :cite:`unterstrasserPropertiesYoungContrails2016`
73
+
74
+ Notes
75
+ -----
76
+ - See eq. (3), (9), and (10) in :cite:`unterstrasserPropertiesYoungContrails2016`.
77
+ - For consistency in CoCiP, ``z_desc`` should be calculated using :func:`dz_max` instead of
78
+ using :func:`z_desc_length_scale`.
79
+ """
80
+ # Length scales
81
+ z_atm = z_atm_length_scale(air_temperature, rhi_0)
82
+ rho_emit = emitted_water_vapour_concentration(ei_h2o, wingspan, true_airspeed, fuel_flow)
83
+ z_emit = z_emit_length_scale(rho_emit, air_temperature)
84
+ z_total = z_total_length_scale(aei_n, z_atm, z_emit, z_desc)
85
+ return _survival_fraction_from_length_scale(z_total)
86
+
87
+
88
+ def z_total_length_scale(
89
+ aei_n: npt.NDArray[np.float64],
90
+ z_atm: npt.NDArray[np.float64],
91
+ z_emit: npt.NDArray[np.float64],
92
+ z_desc: npt.NDArray[np.float64],
93
+ ) -> npt.NDArray[np.float64]:
94
+ """
95
+ Calculate the total length-scale effect of the wake vortex downwash.
96
+
97
+ Parameters
98
+ ----------
99
+ aei_n : npt.NDArray[np.float64]
100
+ Apparent ice crystal number emissions index at contrail formation, [:math:`kg^{-1}`]
101
+ z_atm : npt.NDArray[np.float64]
102
+ Length-scale effect of ambient supersaturation on the ice crystal mass budget, [:math:`m`]
103
+ z_emit : npt.NDArray[np.float64]
104
+ Length-scale effect of water vapour emissions on the ice crystal mass budget, [:math:`m`]
105
+ z_desc : npt.NDArray[np.float64]
106
+ Final vertical displacement of the wake vortex, `dz_max` in `wake_vortex.py`, [:math:`m`]
107
+
108
+ Returns
109
+ -------
110
+ npt.NDArray[np.float64]
111
+ Total length-scale effect of the wake vortex downwash, [:math:`m`]
112
+ """
113
+ alpha_base = (aei_n / 2.8e14) ** (-0.18)
114
+ alpha_atm = 1.7 * alpha_base
115
+ alpha_emit = 1.15 * alpha_base
116
+
117
+ z_total = alpha_atm * z_atm + alpha_emit * z_emit - 0.6 * z_desc
118
+
119
+ z_total.clip(min=0.0, out=z_total)
120
+ return z_total
121
+
122
+
123
+ def z_atm_length_scale(
124
+ air_temperature: npt.NDArray[np.float64],
125
+ rhi_0: npt.NDArray[np.float64],
126
+ *,
127
+ n_iter: int = 10,
128
+ ) -> npt.NDArray[np.float64]:
129
+ """Calculate the length-scale effect of ambient supersaturation on the ice crystal mass budget.
130
+
131
+ Parameters
132
+ ----------
133
+ air_temperature : npt.NDArray[np.float64]
134
+ Ambient temperature for each waypoint, [:math:`K`].
135
+ rhi_0 : npt.NDArray[np.float64]
136
+ Relative humidity with respect to ice at the flight waypoint.
137
+ n_iter : int
138
+ Number of iterations, set to 10 as default where ``z_atm`` is accurate to within +-1 m.
139
+
140
+ Returns
141
+ -------
142
+ npt.NDArray[np.float64]
143
+ The effect of the ambient supersaturation on the ice crystal mass budget,
144
+ provided as a length scale equivalent, [:math:`m`].
145
+
146
+ Notes
147
+ -----
148
+ - See eq. (5) in :cite:`unterstrasserPropertiesYoungContrails2016`.
149
+ """
150
+ # Only perform operation when the ambient condition is supersaturated w.r.t. ice
151
+ issr = rhi_0 > 1.0
152
+
153
+ rhi_issr = rhi_0[issr]
154
+ air_temperature_issr = air_temperature[issr]
155
+
156
+ # Solve non-linear equation numerically using the bisection method
157
+ # Did not use scipy functions because it is unstable when dealing with np.arrays
158
+ z_1 = np.zeros_like(rhi_issr)
159
+ z_2 = np.full_like(rhi_issr, 1000.0)
160
+ lhs = rhi_issr * thermo.e_sat_ice(air_temperature_issr) / air_temperature_issr
161
+
162
+ dry_adiabatic_lapse_rate = constants.g / constants.c_pd
163
+ for _ in range(n_iter):
164
+ z_est = 0.5 * (z_1 + z_2)
165
+ rhs = (thermo.e_sat_ice(air_temperature_issr + dry_adiabatic_lapse_rate * z_est)) / (
166
+ air_temperature_issr + dry_adiabatic_lapse_rate * z_est
167
+ )
168
+ z_1[lhs > rhs] = z_est[lhs > rhs]
169
+ z_2[lhs < rhs] = z_est[lhs < rhs]
170
+
171
+ out = np.zeros_like(rhi_0)
172
+ out[issr] = 0.5 * (z_1 + z_2)
173
+ return out
174
+
175
+
176
+ def emitted_water_vapour_concentration(
177
+ ei_h2o: npt.NDArray[np.float64] | float,
178
+ wingspan: npt.NDArray[np.float64] | float,
179
+ true_airspeed: npt.NDArray[np.float64],
180
+ fuel_flow: npt.NDArray[np.float64],
181
+ ) -> npt.NDArray[np.float64]:
182
+ r"""
183
+ Calculate aircraft-emitted water vapour concentration in the plume.
184
+
185
+ Parameters
186
+ ----------
187
+ ei_h2o : npt.NDArray[np.float64] | float
188
+ Emission index of water vapor, [:math:`kg \ kg^{-1}`]
189
+ wingspan : npt.NDArray[np.float64] | float
190
+ aircraft wingspan, [:math:`m`]
191
+ true_airspeed : npt.NDArray[np.float64]
192
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
193
+ fuel_flow : npt.NDArray[np.float64]
194
+ Fuel mass flow rate, [:math:`kg s^{-1}`]
195
+
196
+ Returns
197
+ -------
198
+ npt.NDArray[np.float64]
199
+ Aircraft-emitted water vapour concentration in the plume, [:math:`kg m^{-3}`]
200
+
201
+ Notes
202
+ -----
203
+ - See eq. (6) and (A8) in :cite:`unterstrasserPropertiesYoungContrails2016`.
204
+ """
205
+ h2o_per_dist = (ei_h2o * fuel_flow) / true_airspeed
206
+ area_p = plume_area(wingspan)
207
+ return h2o_per_dist / area_p
208
+
209
+
210
+ def z_emit_length_scale(
211
+ rho_emit: npt.NDArray[np.float64], air_temperature: npt.NDArray[np.float64], *, n_iter: int = 10
212
+ ) -> npt.NDArray[np.float64]:
213
+ """Calculate the length-scale effect of water vapour emissions on the ice crystal mass budget.
214
+
215
+ Parameters
216
+ ----------
217
+ rho_emit : npt.NDArray[np.float64] | float
218
+ Aircraft-emitted water vapour concentration in the plume, [:math:`kg m^{-3}`]
219
+ air_temperature : npt.NDArray[np.float64]
220
+ ambient temperature for each waypoint, [:math:`K`]
221
+ n_iter : int
222
+ Number of iterations, set to 10 as default where ``z_emit`` is accurate to within +-1 m.
223
+
224
+ Returns
225
+ -------
226
+ npt.NDArray[np.float64]
227
+ The effect of the aircraft water vapour emission on the ice crystal mass budget,
228
+ provided as a length scale equivalent, [:math:`m`]
229
+
230
+ Notes
231
+ -----
232
+ - See eq. (7) in :cite:`unterstrasserPropertiesYoungContrails2016`.
233
+ """
234
+ # Solve non-linear equation numerically using the bisection method
235
+ # Did not use scipy functions because it is unstable when dealing with np.arrays
236
+ z_1 = np.zeros_like(rho_emit)
237
+ z_2 = np.full_like(rho_emit, 1000.0)
238
+
239
+ lhs = (thermo.e_sat_ice(air_temperature) / (constants.R_v * air_temperature)) + rho_emit
240
+
241
+ dry_adiabatic_lapse_rate = constants.g / constants.c_pd
242
+ for _ in range(n_iter):
243
+ z_est = 0.5 * (z_1 + z_2)
244
+ rhs = thermo.e_sat_ice(air_temperature + dry_adiabatic_lapse_rate * z_est) / (
245
+ constants.R_v * (air_temperature + dry_adiabatic_lapse_rate * z_est)
246
+ )
247
+ z_1[lhs > rhs] = z_est[lhs > rhs]
248
+ z_2[lhs < rhs] = z_est[lhs < rhs]
249
+
250
+ return 0.5 * (z_1 + z_2)
251
+
252
+
253
+ def plume_area(wingspan: npt.NDArray[np.float64] | float) -> npt.NDArray[np.float64] | float:
254
+ """Calculate area of the wake-vortex plume.
255
+
256
+ Parameters
257
+ ----------
258
+ wingspan : npt.NDArray[np.float64] | float
259
+ aircraft wingspan, [:math:`m`]
260
+
261
+ Returns
262
+ -------
263
+ float
264
+ Area of two wake-vortex plumes, [:math:`m^{2}`]
265
+
266
+ Notes
267
+ -----
268
+ - See eq. (A6) and (A7) in :cite:`unterstrasserPropertiesYoungContrails2016`.
269
+ """
270
+ r_plume = 1.5 + 0.314 * wingspan
271
+ return 2.0 * 2.0 * np.pi * r_plume**2
272
+
273
+
274
+ def z_desc_length_scale(
275
+ wingspan: npt.NDArray[np.float64] | float,
276
+ air_temperature: npt.NDArray[np.float64],
277
+ air_pressure: npt.NDArray[np.float64],
278
+ true_airspeed: npt.NDArray[np.float64],
279
+ aircraft_mass: npt.NDArray[np.float64],
280
+ dT_dz: npt.NDArray[np.float64],
281
+ ) -> npt.NDArray[np.float64]:
282
+ """Calculate the final vertical displacement of the wake vortex.
283
+
284
+ Parameters
285
+ ----------
286
+ wingspan : npt.NDArray[np.float64] | float
287
+ aircraft wingspan, [:math:`m`]
288
+ air_temperature : npt.NDArray[np.float64]
289
+ ambient temperature for each waypoint, [:math:`K`]
290
+ air_pressure : npt.NDArray[np.float64]
291
+ pressure altitude at each waypoint, [:math:`Pa`]
292
+ true_airspeed : npt.NDArray[np.float64]
293
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
294
+ aircraft_mass : npt.NDArray[np.float64] | float
295
+ aircraft mass for each waypoint, [:math:`kg`]
296
+ dT_dz : npt.NDArray[np.float64]
297
+ potential temperature gradient, [:math:`K m^{-1}`]
298
+
299
+ Returns
300
+ -------
301
+ npt.NDArray[np.float64]
302
+ Final vertical displacement of the wake vortex, [:math:`m`]
303
+
304
+ Notes
305
+ -----
306
+ - See eq. (4) in :cite:`unterstrasserPropertiesYoungContrails2016`.
307
+ """
308
+ gamma_0 = _initial_wake_vortex_circulation(
309
+ wingspan, air_temperature, air_pressure, true_airspeed, aircraft_mass
310
+ )
311
+ n_bv = thermo.brunt_vaisala_frequency(air_pressure, air_temperature, dT_dz)
312
+ return ((8.0 * gamma_0) / (np.pi * n_bv)) ** 0.5
313
+
314
+
315
+ def _initial_wake_vortex_circulation(
316
+ wingspan: npt.NDArray[np.float64] | float,
317
+ air_temperature: npt.NDArray[np.float64],
318
+ air_pressure: npt.NDArray[np.float64],
319
+ true_airspeed: npt.NDArray[np.float64],
320
+ aircraft_mass: npt.NDArray[np.float64],
321
+ ) -> npt.NDArray[np.float64]:
322
+ """Calculate initial wake vortex circulation.
323
+
324
+ Parameters
325
+ ----------
326
+ wingspan : npt.NDArray[np.float64] | float
327
+ aircraft wingspan, [:math:`m`]
328
+ air_temperature : npt.NDArray[np.float64]
329
+ ambient temperature for each waypoint, [:math:`K`]
330
+ air_pressure : npt.NDArray[np.float64]
331
+ pressure altitude at each waypoint, [:math:`Pa`]
332
+ true_airspeed : npt.NDArray[np.float64]
333
+ true airspeed for each waypoint, [:math:`m s^{-1}`]
334
+ aircraft_mass : npt.NDArray[np.float64] | float
335
+ aircraft mass for each waypoint, [:math:`kg`]
336
+
337
+ Returns
338
+ -------
339
+ npt.NDArray[np.float64]
340
+ Initial wake vortex circulation, [:math:`m^{2} s^{-1}`]
341
+
342
+ Notes
343
+ -----
344
+ - This is a measure of the strength/intensity of the wake vortex circulation.
345
+ - See eq. (A1) in :cite:`unterstrasserPropertiesYoungContrails2016`.
346
+ """
347
+ b_0 = wake_vortex_separation(wingspan)
348
+ rho_air = thermo.rho_d(air_temperature, air_pressure)
349
+ return (constants.g * aircraft_mass) / (rho_air * b_0 * true_airspeed)
350
+
351
+
352
+ def _survival_fraction_from_length_scale(
353
+ z_total: npt.NDArray[np.float64],
354
+ ) -> npt.NDArray[np.float64]:
355
+ """
356
+ Calculate fraction of ice particle number surviving the wake vortex phase.
357
+
358
+ Parameters
359
+ ----------
360
+ z_total : npt.NDArray[np.float64]
361
+ Total length-scale effect of the wake vortex downwash, [:math:`m`]
362
+
363
+ Returns
364
+ -------
365
+ npt.NDArray[np.float64]
366
+ Fraction of ice particle number surviving the wake vortex phase
367
+ """
368
+ f_surv = 0.45 + (1.19 / np.pi) * np.arctan(-1.35 + (z_total / 100.0))
369
+ np.clip(f_surv, 0.0, 1.0, out=f_surv)
370
+ return f_surv
371
+
372
+
373
+ def initial_contrail_depth(
374
+ z_desc: npt.NDArray[np.float64],
375
+ f_surv: npt.NDArray[np.float64],
376
+ ) -> npt.NDArray[np.float64]:
377
+ """Calculate initial contrail depth using :cite:`unterstrasserPropertiesYoungContrails2016`.
378
+
379
+ Parameters
380
+ ----------
381
+ z_desc : npt.NDArray[np.float64]
382
+ Final vertical displacement of the wake vortex, ``dz_max`` in :mod:`wake_vortex.py`,
383
+ [:math:`m`].
384
+ f_surv : npt.NDArray[np.float64]
385
+ Fraction of contrail ice particle number that survive the wake vortex phase.
386
+ See :func:`ice_particle_survival_fraction`.
387
+
388
+ Returns
389
+ -------
390
+ npt.NDArray[np.float64]
391
+ Initial contrail depth, [:math:`m`]
392
+
393
+ Notes
394
+ -----
395
+ - See eq. (12), and (13) in :cite:`unterstrasserPropertiesYoungContrails2016`.
396
+ - For consistency in CoCiP, `z_desc` should be calculated using :func:`dz_max` instead of
397
+ using :func:`z_desc_length_scale`.
398
+ """
399
+ return z_desc * np.where(
400
+ f_surv <= 0.2,
401
+ 6.0 * f_surv,
402
+ 0.15 * f_surv + (6.0 - 0.15) * 0.2,
403
+ )
@@ -1,4 +1,25 @@
1
- """Wave-vortex downwash functions."""
1
+ """Wave-vortex downwash functions.
2
+
3
+ This module includes equations from the original CoCiP model
4
+ :cite:`schumannContrailCirrusPrediction2012`. An alternative set of equations based on
5
+ :cite:`unterstrasserPropertiesYoungContrails2016` is available in
6
+ :py:mod:`unterstrasser_wake_vortex`.
7
+
8
+ Unterstrasser Notes
9
+ -------------------
10
+
11
+ Improved estimation of the survival fraction of the contrail ice crystal number ``f_surv``
12
+ during the wake-vortex phase. This is a parameterised model that is developed based on
13
+ outputs provided by large eddy simulations.
14
+
15
+ For comparison, CoCiP assumes that ``f_surv`` is equal to the change in the contrail ice water
16
+ content (by mass) before and after the wake vortex phase. However, for larger (smaller) ice
17
+ particles, their survival fraction by number could be smaller (larger) than their survival fraction
18
+ by mass. This is particularly important in the "soot-poor" scenario, for example, in cleaner
19
+ lean-burn engines where their soot emissions can be 3-4 orders of magnitude lower than conventional
20
+ RQL engines.
21
+
22
+ """
2
23
 
3
24
  from __future__ import annotations
4
25
 
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any, NoReturn, TypeVar, overload
11
11
  import numpy as np
12
12
  import numpy.typing as npt
13
13
  import pandas as pd
14
+ import xarray as xr
14
15
 
15
16
  import pycontrails
16
17
  from pycontrails.core import models
@@ -20,7 +21,7 @@ from pycontrails.models import humidity_scaling, sac
20
21
  from pycontrails.models.cocip import cocip, contrail_properties, wake_vortex, wind_shear
21
22
  from pycontrails.models.cocipgrid.cocip_grid_params import CocipGridParams
22
23
  from pycontrails.models.emissions import Emissions
23
- from pycontrails.physics import geo, thermo, units
24
+ from pycontrails.physics import constants, geo, thermo, units
24
25
  from pycontrails.utils import dependencies
25
26
 
26
27
  if TYPE_CHECKING:
@@ -122,6 +123,13 @@ class CocipGrid(models.Model):
122
123
  msg = "Parameter 'radiative_heating_effects' is not yet implemented in CocipGrid"
123
124
  raise NotImplementedError(msg)
124
125
 
126
+ if self.params["unterstrasser_ice_survival_fraction"]:
127
+ msg = (
128
+ "Parameter 'unterstrasser_ice_survival_fraction' is not "
129
+ "yet implemented in CocipGrid"
130
+ )
131
+ raise NotImplementedError(msg)
132
+
125
133
  self._target_dtype = np.result_type(*self.met.data.values())
126
134
 
127
135
  @overload
@@ -304,23 +312,76 @@ class CocipGrid(models.Model):
304
312
  ``time_end``, new slices are selected from the larger ``self.met`` and
305
313
  ``self.rad`` data. The slicing only occurs in the time domain.
306
314
 
315
+ The end of currently-used ``met`` and ``rad`` will be used as the start
316
+ of newly-selected met slices when possible to avoid losing and re-loading
317
+ already-loaded met data.
318
+
307
319
  If ``self.params["downselect_met"]`` is True, :func:`_downselect_met` has
308
320
  already performed a spatial downselection of the met data.
309
321
  """
310
- if met is None or time_end > met.indexes["time"].to_numpy()[-1]:
322
+
323
+ if met is None:
311
324
  # idx is the first index at which self.met.variables["time"].to_numpy() >= time_end
312
325
  idx = np.searchsorted(self.met.indexes["time"].to_numpy(), time_end)
313
326
  sl = slice(max(0, idx - 1), idx + 1)
314
327
  logger.debug("Select met slice %s", sl)
315
328
  met = MetDataset(self.met.data.isel(time=sl), copy=False)
316
329
 
317
- if rad is None or time_end > rad.indexes["time"].to_numpy()[-1]:
330
+ elif time_end > met.indexes["time"].to_numpy()[-1]:
331
+ current_times = met.indexes["time"].to_numpy()
332
+ all_times = self.met.indexes["time"].to_numpy()
333
+ # idx is the first index at which all_times >= time_end
334
+ idx = np.searchsorted(all_times, time_end)
335
+ sl = slice(max(0, idx - 1), idx + 1)
336
+
337
+ # case 1: cannot re-use end of current met as start of new met
338
+ if current_times[-1] != all_times[sl.start]:
339
+ logger.debug("Select met slice %s", sl)
340
+ met = MetDataset(self.met.data.isel(time=sl), copy=False)
341
+ # case 2: can re-use end of current met plus one step of new met
342
+ elif sl.start < all_times.size - 1:
343
+ sl = slice(sl.start + 1, sl.stop)
344
+ logger.debug("Reuse end of met and select met slice %s", sl)
345
+ met = MetDataset(
346
+ xr.concat((met.data.isel(time=[-1]), self.met.data.isel(time=sl)), dim="time"),
347
+ copy=False,
348
+ )
349
+ # case 3: can re-use end of current met and nothing else
350
+ else:
351
+ logger.debug("Reuse end of met")
352
+ met = MetDataset(met.data.isel(time=[-1]), copy=False)
353
+
354
+ if rad is None:
318
355
  # idx is the first index at which self.rad.variables["time"].to_numpy() >= time_end
319
356
  idx = np.searchsorted(self.rad.indexes["time"].to_numpy(), time_end)
320
357
  sl = slice(max(0, idx - 1), idx + 1)
321
358
  logger.debug("Select rad slice %s", sl)
322
359
  rad = MetDataset(self.rad.data.isel(time=sl), copy=False)
323
360
 
361
+ elif time_end > rad.indexes["time"].to_numpy()[-1]:
362
+ current_times = rad.indexes["time"].to_numpy()
363
+ all_times = self.rad.indexes["time"].to_numpy()
364
+ # idx is the first index at which all_times >= time_end
365
+ idx = np.searchsorted(all_times, time_end)
366
+ sl = slice(max(0, idx - 1), idx + 1)
367
+
368
+ # case 1: cannot re-use end of current rad as start of new rad
369
+ if current_times[-1] != all_times[sl.start]:
370
+ logger.debug("Select rad slice %s", sl)
371
+ rad = MetDataset(self.rad.data.isel(time=sl), copy=False)
372
+ # case 2: can re-use end of current rad plus one step of new rad
373
+ elif sl.start < all_times.size - 1:
374
+ sl = slice(sl.start + 1, sl.stop)
375
+ logger.debug("Reuse end of rad and select rad slice %s", sl)
376
+ rad = MetDataset(
377
+ xr.concat((rad.data.isel(time=[-1]), self.rad.data.isel(time=sl)), dim="time"),
378
+ copy=False,
379
+ )
380
+ # case 3: can re-use end of current rad and nothing else
381
+ else:
382
+ logger.debug("Reuse end of rad")
383
+ rad = MetDataset(rad.data.isel(time=[-1]), copy=False)
384
+
324
385
  return met, rad
325
386
 
326
387
  def _attach_verbose_outputs_evolution(self, contrail_list: list[GeoVectorDataset]) -> None:
@@ -398,6 +459,17 @@ class CocipGrid(models.Model):
398
459
  nominal_segment_length=segment_length,
399
460
  attrs=attrs,
400
461
  )
462
+
463
+ if self.params["compute_atr20"]:
464
+ self.source["global_yearly_mean_rf_per_m"] = (
465
+ self.source["ef_per_m"].data
466
+ / constants.surface_area_earth
467
+ / constants.seconds_per_year
468
+ )
469
+ self.source["atr20_per_m"] = (
470
+ self.params["global_rf_to_atr20_factor"]
471
+ * self.source["global_yearly_mean_rf_per_m"].data
472
+ )
401
473
  else:
402
474
  self.source = result_merge_source(
403
475
  result=summary,
@@ -406,6 +478,18 @@ class CocipGrid(models.Model):
406
478
  nominal_segment_length=segment_length,
407
479
  attrs=attrs,
408
480
  )
481
+
482
+ if self.params["compute_atr20"]:
483
+ self.source["global_yearly_mean_rf_per_m"] = (
484
+ self.source["ef_per_m"]
485
+ / constants.surface_area_earth
486
+ / constants.seconds_per_year
487
+ )
488
+ self.source["atr20_per_m"] = (
489
+ self.params["global_rf_to_atr20_factor"]
490
+ * self.source["global_yearly_mean_rf_per_m"]
491
+ )
492
+
409
493
  return self.source
410
494
 
411
495
  # ---------------------------
@@ -1459,11 +1543,11 @@ def find_initial_persistent_contrails(
1459
1543
  air_pressure_1=air_pressure_1,
1460
1544
  )
1461
1545
  iwc_1 = contrail_properties.iwc_post_wake_vortex(iwc, iwc_ad)
1546
+ f_surv = contrail_properties.ice_particle_survival_fraction(iwc, iwc_1)
1462
1547
  n_ice_per_m = contrail_properties.ice_particle_number(
1463
1548
  nvpm_ei_n=nvpm_ei_n,
1464
1549
  fuel_dist=fuel_dist,
1465
- iwc=iwc,
1466
- iwc_1=iwc_1,
1550
+ f_surv=f_surv,
1467
1551
  air_temperature=air_temperature,
1468
1552
  T_crit_sac=T_crit_sac,
1469
1553
  min_ice_particle_number_nvpm_ei_n=params["min_ice_particle_number_nvpm_ei_n"],
@@ -2241,6 +2325,14 @@ def _contrail_grid_variable_attrs() -> dict[str, dict[str, str]]:
2241
2325
  "long_name": "Ice water content after the wake vortex phase",
2242
2326
  "units": "kg_h2o / kg_air",
2243
2327
  },
2328
+ "global_yearly_mean_rf_per_m": {
2329
+ "long_name": "Global yearly mean RF per meter of flight trajectory",
2330
+ "units": "W / m**2 / m",
2331
+ },
2332
+ "atr20_per_m": {
2333
+ "long_name": "Average Temperature Response over a 20 year horizon",
2334
+ "units": "K / m",
2335
+ },
2244
2336
  }
2245
2337
 
2246
2338
 
@@ -2249,7 +2341,12 @@ def _supported_verbose_outputs_formation() -> set[str]:
2249
2341
 
2250
2342
  Uses output of :func:`_contrail_grid_variable_attrs` as a source of truth.
2251
2343
  """
2252
- return set(_contrail_grid_variable_attrs()) - {"contrail_age", "ef_per_m"}
2344
+ return set(_contrail_grid_variable_attrs()) - {
2345
+ "contrail_age",
2346
+ "ef_per_m",
2347
+ "global_yearly_mean_rf_per_m",
2348
+ "atr20_per_m",
2349
+ }
2253
2350
 
2254
2351
 
2255
2352
  def _warn_not_wrap(met: MetDataset) -> None:
@@ -59,7 +59,7 @@ class ISSR(Model):
59
59
  >>> out1 = model.eval()
60
60
  >>> issr1 = out1["issr"]
61
61
  >>> issr1.proportion # Get proportion of values with ice supersaturation
62
- 0.114140
62
+ 0.11414134603859523
63
63
 
64
64
  >>> # Run with a lower threshold
65
65
  >>> out2 = model.eval(rhi_threshold=0.95)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pycontrails
3
- Version: 0.50.0
3
+ Version: 0.50.1
4
4
  Summary: Python library for modeling aviation climate impacts
5
5
  Author-email: Breakthrough Energy <py@contrails.org>
6
6
  License: Apache-2.0
@@ -1,5 +1,5 @@
1
1
  pycontrails/__init__.py,sha256=8WUs6hZAAIH1yKfwYJ8UEqNsk5voRyLmmD3Dje9DZaE,2055
2
- pycontrails/_version.py,sha256=b7AfuH4OcP5xNu-o3fLCv9MgMbM00aNaiiWCwRH5eV8,429
2
+ pycontrails/_version.py,sha256=iOkMTC9BGn2KBWKt1YcKpLAm1DBaIq115ye0ZvuYj54,429
3
3
  pycontrails/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  pycontrails/core/__init__.py,sha256=eNypVTz1kHBSKAJX3CgfKw-VKrMRRkKTutmjSlrfeUs,870
5
5
  pycontrails/core/aircraft_performance.py,sha256=ikeJmdvFRDa1RdfR-JKfhQbiiIzL0c2vzcBmobmoxMs,22511
@@ -8,18 +8,18 @@ pycontrails/core/cache.py,sha256=NNrFNUvmDEkeS0d1VpDNWmF225a8u2d-TGcpRgQd9Qs,288
8
8
  pycontrails/core/coordinates.py,sha256=cb8RprpoSgRTFbAXTPNfuUHVnOxyV3zZ0Ac88P5YbBw,5465
9
9
  pycontrails/core/datalib.py,sha256=u2dNc8HxkeJd1jMPTZsOTEagTI0wQkzF1ComctmxJO8,23993
10
10
  pycontrails/core/fleet.py,sha256=WKF_s_gRXHmB9b1OW7RUkM1TzfvVD8Ab0Md-qKRwkzs,16544
11
- pycontrails/core/flight.py,sha256=ehxpo-Ofr4unQpeSR1ZXmbFGClyW628cA000oJZ_Jp8,77974
11
+ pycontrails/core/flight.py,sha256=u3if3cTTWtSIi2eVaq3OyhMbthcTNrVbWVypegAp3sg,78382
12
12
  pycontrails/core/flightplan.py,sha256=cpMZ6VCYbfwh3vnew2XgVEHnqBx1NzeAhrTVCvlbbss,7569
13
13
  pycontrails/core/fuel.py,sha256=06YUDhvC8Rx6KbUXRB9qLTsJX2V7tLbzjwAfDH0R6l8,4472
14
- pycontrails/core/interpolation.py,sha256=pDSQx8b3UjwWzKRKn_yKd3h2XsUA_ARRnZjIaH6sf60,24719
15
- pycontrails/core/met.py,sha256=0Nn03FZ43l10um5RlDQ3_gH1sHq_H59q-PpTb7xARPY,95114
14
+ pycontrails/core/interpolation.py,sha256=Sp9s17i1QQx3jZwnwvt3vo6enWwlkYwTVKCE27N2Wdk,26214
15
+ pycontrails/core/met.py,sha256=J7m0fJNJrFfcXP4d0ctf_ZK4eDa2kT9kWFHfwnEECo8,94924
16
16
  pycontrails/core/met_var.py,sha256=JzB7UhBLQyU4TuKZqemhpBHA6Dbt89BPYO2sYBLMkL4,9504
17
17
  pycontrails/core/models.py,sha256=VS-ct4xkojJIuqdPpT1ke1ZetNzv10nNx_Z_XalZyeo,40175
18
18
  pycontrails/core/polygon.py,sha256=ukcYh4TzGxz-ggipYe1m6DR50aEBgQhxWkEtjoTqC3w,18227
19
- pycontrails/core/rgi_cython.cp311-win_amd64.pyd,sha256=mAAHBlOJSW9K4PjGODsrnYbw-fFPwMQCzXIEyjpRQo4,258560
20
- pycontrails/core/vector.py,sha256=xYZDm9VjGlZGWVGqG6u5k0fwT8Ir9U5wThreB1wKOXQ,73854
19
+ pycontrails/core/rgi_cython.cp311-win_amd64.pyd,sha256=-0fMQ4TpMhIrFkMWhQVJtiT4ILu0n8Rk8tUUKfD3aPE,258560
20
+ pycontrails/core/vector.py,sha256=MF0oWX0Ghp_G41A712VbU1GEKRNU_Pj9PtzMREPG5Z8,73916
21
21
  pycontrails/datalib/__init__.py,sha256=WnXqgv20SrHZLjFZ9qpQEwRnf0QvfZU-YvMqh7PAUwg,246
22
- pycontrails/datalib/goes.py,sha256=Ky7elxQJyKsk6dlJ22wsOtpaZn34LaEQCqqdX3M4a5s,26932
22
+ pycontrails/datalib/goes.py,sha256=NVk6QFfHbGpOINlKlG6PR1Hn_LCmzP7k20VJQZXpVG8,27124
23
23
  pycontrails/datalib/ecmwf/__init__.py,sha256=MZBpXU2ZD4GOJX9Pr43_N-8TLF-KtV6aZQ5hADmGFCE,1262
24
24
  pycontrails/datalib/ecmwf/arco_era5.py,sha256=aWI69KhTfR42mZYssXCrB43lJelcV85GC8FPZDPxMak,20835
25
25
  pycontrails/datalib/ecmwf/common.py,sha256=XWPlWPQskuTMHBxWeKvRSuYWHRcKeiJAxfl5lScyTPc,3806
@@ -39,23 +39,24 @@ pycontrails/ext/synthetic_flight.py,sha256=CxKDAX8Jao9VXuy_eRksS8zqIaMYgiv9az5DW
39
39
  pycontrails/models/__init__.py,sha256=TKhrXe1Pu1-mV1gctx8cUAMrVxCCAtBkbZi9olfWq8s,34
40
40
  pycontrails/models/accf.py,sha256=72-2oRCSM98cwHxmKmO5ECaRPbiRE010jX5nTQDfHCs,12965
41
41
  pycontrails/models/dry_advection.py,sha256=z4XbEa0ihmcKyCrdaYbWEOYQ4yt9KfmpJ7jYhZ6g5lU,16622
42
- pycontrails/models/issr.py,sha256=uDMEFjQH_wh3jcQexMgFFq3-XrM7DEz4kZcgLSGqAM4,7550
42
+ pycontrails/models/issr.py,sha256=mqRKm106kz8um1cbRblxLRZDJanvqa5Q1BI2sqK5pkQ,7561
43
43
  pycontrails/models/pcc.py,sha256=M5KhtRgdCP9pfDFgui7ibbijtRBTjx3QOJL_m1tQYfs,11443
44
44
  pycontrails/models/pcr.py,sha256=G_0yR5PsCMeJBP6tZFi3M7A6Wcq8s71UvosdA7ozUkI,5502
45
45
  pycontrails/models/sac.py,sha256=Cj4Hi3324wjqLQ7I2CPVkrIh2Fq5W5pKpGrwhYADoHI,16420
46
46
  pycontrails/models/tau_cirrus.py,sha256=eXt3yRrcFBaZNNeH6ZOuU4XEZU2rOfrLKEOC7f0_Ywo,5194
47
47
  pycontrails/models/cocip/__init__.py,sha256=X_MlkJzQ-henQ0xGq-1bfCMajH6s9tlK5QLnN7yfQ68,929
48
- pycontrails/models/cocip/cocip.py,sha256=0nDEdSEjvWE4Klx_x2cIutjlwOmLrgWQ2bhDE65xoTE,99388
49
- pycontrails/models/cocip/cocip_params.py,sha256=L-DAw34JgD39UV6xjRIQt2CjpIRQRqzwNxU37WjNKrw,11107
48
+ pycontrails/models/cocip/cocip.py,sha256=s9j5UhPCaaxiJZDXUvQ2KnEgvQz2pMrRHlWKZijwRIw,100140
49
+ pycontrails/models/cocip/cocip_params.py,sha256=T4IseK6KtY4hG3BuGZBtFgM90HCYecUXsb_QVEK6uGo,11670
50
50
  pycontrails/models/cocip/cocip_uncertainty.py,sha256=lksROIRLt-jxEdjJTiLP9Da-FYt1GjZKaIxwDaH2Ytg,12069
51
- pycontrails/models/cocip/contrail_properties.py,sha256=dRq5o-YuC-s487Fs54OHpAuS8LkxM-HATNVclHKUhL0,57362
51
+ pycontrails/models/cocip/contrail_properties.py,sha256=u6SvucHC6VtF2kujfSVFTfv0263t5uYpNOUJZAroEzc,57111
52
52
  pycontrails/models/cocip/output_formats.py,sha256=sYGYosAL6BQvntKXdeomAph-K6EKOgsI2VQynLEHxnM,79228
53
53
  pycontrails/models/cocip/radiative_forcing.py,sha256=SYmQ8lL8gpWbf6he2C9mKSjODtytbFcdnMdBM-LtBKE,46206
54
54
  pycontrails/models/cocip/radiative_heating.py,sha256=N7FTR20luERmokprdqMOl-d8-cTYZZ2ZSsTdxZnLHfs,19368
55
- pycontrails/models/cocip/wake_vortex.py,sha256=d3oLkTWIocwYb99fl6t99LtEsbKkdsupAfoUY-GmbTI,13783
55
+ pycontrails/models/cocip/unterstrasser_wake_vortex.py,sha256=Ymz-uO9vVhLIFwT9yuF5g1g3hcT-XWdryLsebSBqoVU,14976
56
+ pycontrails/models/cocip/wake_vortex.py,sha256=r3FM4egyGohRF0qGD3pFWBJppUQ_3GhtO_g7L74HmjU,14817
56
57
  pycontrails/models/cocip/wind_shear.py,sha256=Dm181EsiCBJWTnRTZ3ZI3YXscBRnhA6ANnKer004b2Q,3980
57
58
  pycontrails/models/cocipgrid/__init__.py,sha256=OYSdZ1Htbr_IP7N_HuOAj1Pa_KLHtdEeJfXP-cN-gnU,271
58
- pycontrails/models/cocipgrid/cocip_grid.py,sha256=mcz8_G4YyiyqkuAAZiwWZk-NOtcxKJY57pVkhSunHSQ,92278
59
+ pycontrails/models/cocipgrid/cocip_grid.py,sha256=0KWf3Wm8GZOOIy-D86Y8bC-2MIjKbVkhevXMG-eF85s,96680
59
60
  pycontrails/models/cocipgrid/cocip_grid_params.py,sha256=ZpN00VEmeRYaeZhvSfVjnEjrgn6XdClf1eqJC8Ytcuw,6013
60
61
  pycontrails/models/emissions/__init__.py,sha256=BXzV2pBps8j3xbaF1n9uPdVVLI5MBIGYx8xqDJezYIE,499
61
62
  pycontrails/models/emissions/black_carbon.py,sha256=9DRqB487pH8Iq83FXggA5mPLYEAA8NpsKx24f8uTEF4,20828
@@ -85,9 +86,9 @@ pycontrails/utils/iteration.py,sha256=En2YY4NiNwCNtAVO8HL6tv9byBGKs8MKSI7R8P-gZy
85
86
  pycontrails/utils/json.py,sha256=xCv71CKVZNHk4MyoYC-hl7dXObXXbI7P8gcNCn3AUoU,6172
86
87
  pycontrails/utils/temp.py,sha256=5XXqQoEfWjz1OrhoOBZD5vkkCFeuq9LpZkyhc38gIeY,1159
87
88
  pycontrails/utils/types.py,sha256=f8FNSSzW-1ISgJ0zDtCwZgpuzpt26sUABiHHusS414s,4725
88
- pycontrails-0.50.0.dist-info/LICENSE,sha256=HVr8JnZfTaA-12BfKUQZi5hdrB3awOwLWs5X_ga5QzA,10353
89
- pycontrails-0.50.0.dist-info/METADATA,sha256=AEbrq0M0Zhb8ttYIrKZnhxUdo_3OYubbVuzmFHAGEf0,8528
90
- pycontrails-0.50.0.dist-info/NOTICE,sha256=qYeNEp8OjDK5jSW3hTlr9LQRjZeEhXQm0zDei5UFaYs,1969
91
- pycontrails-0.50.0.dist-info/WHEEL,sha256=nSybvzWlmdJnHiUQSY-d7V1ycwEVUTqXiTvr2eshg44,102
92
- pycontrails-0.50.0.dist-info/top_level.txt,sha256=Z8J1R_AiBAyCVjNw6jYLdrA68PrQqTg0t3_Yek_IZ0Q,29
93
- pycontrails-0.50.0.dist-info/RECORD,,
89
+ pycontrails-0.50.1.dist-info/LICENSE,sha256=HVr8JnZfTaA-12BfKUQZi5hdrB3awOwLWs5X_ga5QzA,10353
90
+ pycontrails-0.50.1.dist-info/METADATA,sha256=N03Ih7BucWyRsdXMP6kHazTHfGYPfkm2e6FAs_5NnHM,8528
91
+ pycontrails-0.50.1.dist-info/NOTICE,sha256=qYeNEp8OjDK5jSW3hTlr9LQRjZeEhXQm0zDei5UFaYs,1969
92
+ pycontrails-0.50.1.dist-info/WHEEL,sha256=nSybvzWlmdJnHiUQSY-d7V1ycwEVUTqXiTvr2eshg44,102
93
+ pycontrails-0.50.1.dist-info/top_level.txt,sha256=Z8J1R_AiBAyCVjNw6jYLdrA68PrQqTg0t3_Yek_IZ0Q,29
94
+ pycontrails-0.50.1.dist-info/RECORD,,