pycontrails 0.52.2__cp310-cp310-macosx_11_0_arm64.whl → 0.52.3__cp310-cp310-macosx_11_0_arm64.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.52.2'
16
- __version_tuple__ = version_tuple = (0, 52, 2)
15
+ __version__ = version = '0.52.3'
16
+ __version_tuple__ = version_tuple = (0, 52, 3)
@@ -215,7 +215,8 @@ class PycontrailsRegularGridInterpolator(scipy.interpolate.RegularGridInterpolat
215
215
 
216
216
  if ndim == 1:
217
217
  # np.interp could be better ... although that may also promote the dtype
218
- return rgi_cython.evaluate_linear_1d(values, indices, norm_distances, out)
218
+ # 1-d view is required for evaluate_linear_1d
219
+ return rgi_cython.evaluate_linear_1d(values, indices[0, :], norm_distances[0, :], out)
219
220
 
220
221
  msg = f"Invalid number of dimensions: {ndim}"
221
222
  raise ValueError(msg)
pycontrails/core/met.py CHANGED
@@ -9,7 +9,15 @@ import pathlib
9
9
  import typing
10
10
  import warnings
11
11
  from abc import ABC, abstractmethod
12
- from collections.abc import Hashable, Iterable, Iterator, Mapping, MutableMapping, Sequence
12
+ from collections.abc import (
13
+ Generator,
14
+ Hashable,
15
+ Iterable,
16
+ Iterator,
17
+ Mapping,
18
+ MutableMapping,
19
+ Sequence,
20
+ )
13
21
  from contextlib import ExitStack
14
22
  from datetime import datetime
15
23
  from typing import (
@@ -1502,6 +1510,7 @@ class MetDataArray(MetBase):
1502
1510
  bounds_error: bool = ...,
1503
1511
  fill_value: float | np.float64 | None = ...,
1504
1512
  localize: bool = ...,
1513
+ lowmem: bool = ...,
1505
1514
  indices: interpolation.RGIArtifacts | None = ...,
1506
1515
  return_indices: Literal[False] = ...,
1507
1516
  ) -> npt.NDArray[np.float64]: ...
@@ -1518,6 +1527,7 @@ class MetDataArray(MetBase):
1518
1527
  bounds_error: bool = ...,
1519
1528
  fill_value: float | np.float64 | None = ...,
1520
1529
  localize: bool = ...,
1530
+ lowmem: bool = ...,
1521
1531
  indices: interpolation.RGIArtifacts | None = ...,
1522
1532
  return_indices: Literal[True],
1523
1533
  ) -> tuple[npt.NDArray[np.float64], interpolation.RGIArtifacts]: ...
@@ -1533,6 +1543,7 @@ class MetDataArray(MetBase):
1533
1543
  bounds_error: bool = False,
1534
1544
  fill_value: float | np.float64 | None = np.nan,
1535
1545
  localize: bool = False,
1546
+ lowmem: bool = False,
1536
1547
  indices: interpolation.RGIArtifacts | None = None,
1537
1548
  return_indices: bool = False,
1538
1549
  ) -> npt.NDArray[np.float64] | tuple[npt.NDArray[np.float64], interpolation.RGIArtifacts]:
@@ -1540,7 +1551,9 @@ class MetDataArray(MetBase):
1540
1551
 
1541
1552
  Zero dimensional coordinates are reshaped to 1D arrays.
1542
1553
 
1543
- Method automatically loads underlying :attr:`data` into memory.
1554
+ If ``lowmem == False``, method automatically loads underlying :attr:`data` into
1555
+ memory. Otherwise, method iterates through smaller subsets of :attr:`data` and releases
1556
+ subsets from memory once interpolation against each subset is finished.
1544
1557
 
1545
1558
  If ``method == "nearest"``, the out array will have the same ``dtype`` as
1546
1559
  the underlying :attr:`data`.
@@ -1586,10 +1599,18 @@ class MetDataArray(MetBase):
1586
1599
  localize: bool, optional
1587
1600
  Experimental. If True, downselect gridded data to smallest bounding box containing
1588
1601
  all points. By default False.
1602
+ lowmem: bool, optional
1603
+ Experimental. If True, iterate through points binned by the time coordinate of the
1604
+ grided data, and downselect gridded data to the smallest bounding box containing
1605
+ each binned set of point *before loading into memory*. This can significantly reduce
1606
+ memory consumption with large numbers of points at the cost of increased runtime.
1607
+ By default False.
1589
1608
  indices: tuple | None, optional
1590
1609
  Experimental. See :func:`interpolation.interp`. None by default.
1591
1610
  return_indices: bool, optional
1592
1611
  Experimental. See :func:`interpolation.interp`. False by default.
1612
+ Note that values returned differ when ``lowmem=True`` and ``lowmem=False``,
1613
+ so output should only be re-used in calls with the same ``lowmem`` value.
1593
1614
 
1594
1615
  Returns
1595
1616
  -------
@@ -1632,10 +1653,29 @@ class MetDataArray(MetBase):
1632
1653
  >>> level = np.linspace(200, 300, 10)
1633
1654
  >>> time = pd.date_range("2022-03-01T14", periods=10, freq="5min")
1634
1655
  >>> mda.interpolate(longitude, latitude, level, time)
1656
+ array([220.44347694, 223.08900738, 225.74338924, 228.41642088,
1657
+ 231.10858599, 233.54857391, 235.71504913, 237.86478872,
1658
+ 239.99274623, 242.10792167])
1659
+
1660
+ >>> # Can easily switch to alternative low-memory implementation
1661
+ >>> mda.interpolate(longitude, latitude, level, time, lowmem=True)
1635
1662
  array([220.44347694, 223.08900738, 225.74338924, 228.41642088,
1636
1663
  231.10858599, 233.54857391, 235.71504913, 237.86478872,
1637
1664
  239.99274623, 242.10792167])
1638
1665
  """
1666
+ if lowmem:
1667
+ return self._interp_lowmem(
1668
+ longitude,
1669
+ latitude,
1670
+ level,
1671
+ time,
1672
+ method=method,
1673
+ bounds_error=bounds_error,
1674
+ fill_value=fill_value,
1675
+ indices=indices,
1676
+ return_indices=return_indices,
1677
+ )
1678
+
1639
1679
  # Load if necessary
1640
1680
  if not self.in_memory:
1641
1681
  self._check_memory("Interpolation over")
@@ -1660,6 +1700,100 @@ class MetDataArray(MetBase):
1660
1700
  return_indices=return_indices,
1661
1701
  )
1662
1702
 
1703
+ def _interp_lowmem(
1704
+ self,
1705
+ longitude: float | npt.NDArray[np.float64],
1706
+ latitude: float | npt.NDArray[np.float64],
1707
+ level: float | npt.NDArray[np.float64],
1708
+ time: np.datetime64 | npt.NDArray[np.datetime64],
1709
+ *,
1710
+ method: str = "linear",
1711
+ bounds_error: bool = False,
1712
+ fill_value: float | np.float64 | None = np.nan,
1713
+ minimize_memory: bool = False,
1714
+ indices: interpolation.RGIArtifacts | None = None,
1715
+ return_indices: bool = False,
1716
+ ) -> npt.NDArray[np.float64] | tuple[npt.NDArray[np.float64], interpolation.RGIArtifacts]:
1717
+ """Interpolate values against underlying DataArray.
1718
+
1719
+ This method is used by :meth:`interpolate` when ``lowmem=True``.
1720
+ Parameters and return types are identical to :meth:`interpolate`, except
1721
+ that the ``localize`` keyword argument is omitted.
1722
+ """
1723
+ # Convert all inputs to 1d arrays
1724
+ # Not validating against ndim >= 2
1725
+ longitude, latitude, level, time = np.atleast_1d(longitude, latitude, level, time)
1726
+
1727
+ if bounds_error:
1728
+ _lowmem_boundscheck(time, self.data)
1729
+
1730
+ # Create buffers for holding interpolation output
1731
+ # Use np.full rather than np.empty so points not covered
1732
+ # by masks are filled with correct out-of-bounds values.
1733
+ out = np.full(longitude.shape, fill_value, dtype=self.data.dtype)
1734
+ if return_indices:
1735
+ rgi_artifacts = interpolation.RGIArtifacts(
1736
+ xi_indices=np.full((4, longitude.size), -1, dtype=np.int64),
1737
+ norm_distances=np.full((4, longitude.size), np.nan, dtype=np.float64),
1738
+ out_of_bounds=np.full((longitude.size,), True, dtype=np.bool_),
1739
+ )
1740
+
1741
+ # Iterate over portions of points between adjacent time steps in gridded data
1742
+ for mask in _lowmem_masks(time, self.data["time"].values):
1743
+ if mask is None or not np.any(mask):
1744
+ continue
1745
+
1746
+ lon_sl = longitude[mask]
1747
+ lat_sl = latitude[mask]
1748
+ lev_sl = level[mask]
1749
+ t_sl = time[mask]
1750
+ if indices is not None:
1751
+ indices_sl = interpolation.RGIArtifacts(
1752
+ xi_indices=indices.xi_indices[:, mask],
1753
+ norm_distances=indices.norm_distances[:, mask],
1754
+ out_of_bounds=indices.out_of_bounds[mask],
1755
+ )
1756
+ else:
1757
+ indices_sl = None
1758
+
1759
+ coords = {"longitude": lon_sl, "latitude": lat_sl, "level": lev_sl, "time": t_sl}
1760
+ if any(np.all(np.isnan(coord)) for coord in coords.values()):
1761
+ continue
1762
+ da = interpolation._localize(self.data, coords)
1763
+ if not da._in_memory:
1764
+ logger.debug(
1765
+ "Loading %s MB subset of %s into memory.",
1766
+ round(da.nbytes / 1_000_000, 2),
1767
+ da.name,
1768
+ )
1769
+ da.load()
1770
+
1771
+ tmp = interpolation.interp(
1772
+ longitude=lon_sl,
1773
+ latitude=lat_sl,
1774
+ level=lev_sl,
1775
+ time=t_sl,
1776
+ da=da,
1777
+ method=method,
1778
+ bounds_error=bounds_error,
1779
+ fill_value=fill_value,
1780
+ localize=False, # would be no-op; da is localized already
1781
+ indices=indices_sl,
1782
+ return_indices=return_indices,
1783
+ )
1784
+
1785
+ if return_indices:
1786
+ out[mask], rgi_sl = tmp
1787
+ rgi_artifacts.xi_indices[:, mask] = rgi_sl.xi_indices
1788
+ rgi_artifacts.norm_distances[:, mask] = rgi_sl.norm_distances
1789
+ rgi_artifacts.out_of_bounds[mask] = rgi_sl.out_of_bounds
1790
+ else:
1791
+ out[mask] = tmp
1792
+
1793
+ if return_indices:
1794
+ return out, rgi_artifacts
1795
+ return out
1796
+
1663
1797
  def _check_memory(self, msg_start: str) -> None:
1664
1798
  """Check the memory usage of the underlying data.
1665
1799
 
@@ -2656,3 +2790,46 @@ def _add_vertical_coords(data: XArrayType) -> XArrayType:
2656
2790
  data.coords["altitude"] = data.coords["altitude"].astype(dtype, copy=False)
2657
2791
 
2658
2792
  return data
2793
+
2794
+
2795
+ def _lowmem_boundscheck(time: npt.NDArray[np.datetime64], da: xr.DataArray) -> None:
2796
+ """Extra bounds check required with low-memory interpolation strategy.
2797
+
2798
+ Because the main loop in `_interp_lowmem` processes points between time steps
2799
+ in gridded data, it will never encounter points that are out-of-bounds in time
2800
+ and may fail to produce requested out-of-bounds errors.
2801
+ """
2802
+ da_time = da["time"].to_numpy()
2803
+ if not np.all((time >= da_time.min()) & (time <= da_time.max())):
2804
+ axis = da.get_axis_num("time")
2805
+ msg = f"One of the requested xi is out of bounds in dimension {axis}"
2806
+ raise ValueError(msg)
2807
+
2808
+
2809
+ def _lowmem_masks(
2810
+ time: npt.NDArray[np.datetime64], t_met: npt.NDArray[np.datetime64]
2811
+ ) -> Generator[npt.NDArray[np.bool_], None, None]:
2812
+ """Generate sequence of masks for low-memory interpolation."""
2813
+ t_met_max = t_met.max()
2814
+ t_met_min = t_met.min()
2815
+ inbounds = (time >= t_met_min) & (time <= t_met_max)
2816
+ if not np.any(inbounds):
2817
+ return
2818
+
2819
+ earliest = np.nanmin(time)
2820
+ istart = 0 if earliest < t_met_min else np.flatnonzero(t_met <= earliest).max()
2821
+ latest = np.nanmax(time)
2822
+ iend = t_met.size - 1 if latest > t_met_max else np.flatnonzero(t_met >= latest).min()
2823
+ if istart == iend:
2824
+ yield inbounds
2825
+ return
2826
+
2827
+ # Sequence of masks covers elements in time in the interval [t_met[istart], t_met[iend]].
2828
+ # The first iteration masks elements in the interval [t_met[istart], t_met[istart+1]]
2829
+ # (inclusive of both endpoints).
2830
+ # Subsequent iterations mask elements in the interval (t_met[i], t_met[i+1]]
2831
+ # (inclusive of right endpoint only).
2832
+ for i in range(istart, iend):
2833
+ mask = ((time >= t_met[i]) if i == istart else (time > t_met[i])) & (time <= t_met[i + 1])
2834
+ if np.any(mask):
2835
+ yield mask
@@ -148,7 +148,6 @@ class Cocip(Model):
148
148
 
149
149
  This implementation is regression tested against
150
150
  results from :cite:`teohAviationContrailClimate2022`.
151
- See `tests/benchmark/north-atlantic-study/validate.py`.
152
151
 
153
152
  **Outputs**
154
153
 
@@ -549,6 +548,8 @@ class Cocip(Model):
549
548
  verbose_outputs = self.params["verbose_outputs"]
550
549
 
551
550
  interp_kwargs = self.interp_kwargs
551
+ if self.params["preprocess_lowmem"]:
552
+ interp_kwargs["lowmem"] = True
552
553
  interpolate_met(met, self.source, "air_temperature", **interp_kwargs)
553
554
  interpolate_met(met, self.source, "specific_humidity", **interp_kwargs)
554
555
  interpolate_met(met, self.source, "eastward_wind", "u_wind", **interp_kwargs)
@@ -750,6 +751,8 @@ class Cocip(Model):
750
751
 
751
752
  # get full met grid or flight data interpolated to the pressure level `p_dz`
752
753
  interp_kwargs = self.interp_kwargs
754
+ if self.params["preprocess_lowmem"]:
755
+ interp_kwargs["lowmem"] = True
753
756
  air_temperature_lower = interpolate_met(
754
757
  met,
755
758
  self._sac_flight,
@@ -861,6 +864,8 @@ class Cocip(Model):
861
864
 
862
865
  # get met post wake vortex along initial contrail
863
866
  interp_kwargs = self.interp_kwargs
867
+ if self.params["preprocess_lowmem"]:
868
+ interp_kwargs["lowmem"] = True
864
869
  air_temperature_1 = interpolate_met(met, contrail_1, "air_temperature", **interp_kwargs)
865
870
  interpolate_met(met, contrail_1, "specific_humidity", **interp_kwargs)
866
871
 
@@ -952,11 +957,14 @@ class Cocip(Model):
952
957
  )
953
958
  logger.debug("None are filtered out!")
954
959
 
955
- def _simulate_contrail_evolution(self) -> None:
956
- """Simulate contrail evolution."""
957
- # Calculate all properties for "downwash_contrail" which is
958
- # a contrail representation of the waypoints of the downwash flight.
959
- # The downwash_contrail has already been filtered for initial persistent waypoints.
960
+ def _process_downwash_flight(self) -> tuple[MetDataset | None, MetDataset | None]:
961
+ """Create and calculate properties of contrails created by downwash vortex.
962
+
963
+ ``_downwash_contrail`` is a contrail representation of the waypoints of
964
+ ``_downwash_flight``, which has already been filtered for initial persistent waypoints.
965
+
966
+ Returns MetDatasets for subsequent use if ``preprocess_lowmem=False``.
967
+ """
960
968
  self._downwash_contrail = self._create_downwash_contrail()
961
969
  buffers = {
962
970
  f"{coord}_buffer": self.params[f"met_{coord}_buffer"]
@@ -971,6 +979,8 @@ class Cocip(Model):
971
979
  calc_timestep_geometry(self._downwash_contrail)
972
980
 
973
981
  interp_kwargs = self.interp_kwargs
982
+ if self.params["preprocess_lowmem"]:
983
+ interp_kwargs["lowmem"] = True
974
984
  calc_timestep_meteorology(self._downwash_contrail, met, self.params, **interp_kwargs)
975
985
  calc_shortwave_radiation(rad, self._downwash_contrail, **interp_kwargs)
976
986
  calc_outgoing_longwave_radiation(rad, self._downwash_contrail, **interp_kwargs)
@@ -985,6 +995,16 @@ class Cocip(Model):
985
995
  # Intersect with rad dataset
986
996
  calc_radiative_properties(self._downwash_contrail, self.params)
987
997
 
998
+ if self.params["preprocess_lowmem"]:
999
+ return None, None
1000
+ return met, rad
1001
+
1002
+ def _simulate_contrail_evolution(self) -> None:
1003
+ """Simulate contrail evolution."""
1004
+
1005
+ met, rad = self._process_downwash_flight()
1006
+ interp_kwargs = self.interp_kwargs
1007
+
988
1008
  contrail_contrail_overlapping = self.params["contrail_contrail_overlapping"]
989
1009
  if contrail_contrail_overlapping and not isinstance(self.source, Fleet):
990
1010
  warnings.warn("Contrail-Contrail Overlapping is only valid for Fleet mode.")
@@ -1022,22 +1042,7 @@ class Cocip(Model):
1022
1042
  continue
1023
1043
 
1024
1044
  # Update met, rad slices as needed
1025
- # We need to both interpolate latest_contrail, as well as the "contrail_2"
1026
- # created by calc_timestep_contrail_evolution. This "contrail_2" object
1027
- # has constant time at "time_end", hence the buffer we apply below.
1028
- # After the downwash_contrails is all used up, these updates are intended
1029
- # to happen once each hour
1030
- buffers["time_buffer"] = (
1031
- np.timedelta64(0, "ns"),
1032
- time_end - latest_contrail["time"].max(),
1033
- )
1034
- if time_end > met.indexes["time"].to_numpy()[-1]:
1035
- logger.debug("Downselect met at time_end %s within Cocip evolution", time_end)
1036
- met = latest_contrail.downselect_met(self.met, **buffers, copy=False)
1037
- met = add_tau_cirrus(met)
1038
- if time_end > rad.indexes["time"].to_numpy()[-1]:
1039
- logger.debug("Downselect rad at time_end %s within Cocip evolution", time_end)
1040
- rad = latest_contrail.downselect_met(self.rad, **buffers, copy=False)
1045
+ met, rad = self._maybe_downselect_met_rad(met, rad, latest_contrail, time_end)
1041
1046
 
1042
1047
  # Recalculate latest_contrail with new values
1043
1048
  # NOTE: We are doing a substantial amount of redundant computation here
@@ -1075,6 +1080,75 @@ class Cocip(Model):
1075
1080
 
1076
1081
  self.contrail_list.append(final_contrail)
1077
1082
 
1083
+ def _maybe_downselect_met_rad(
1084
+ self,
1085
+ met: MetDataset | None,
1086
+ rad: MetDataset | None,
1087
+ latest_contrail: GeoVectorDataset,
1088
+ time_end: np.datetime64,
1089
+ ) -> tuple[MetDataset, MetDataset]:
1090
+ """Downselect ``self.met`` and ``self.rad`` if necessary to cover ``time_end``.
1091
+
1092
+ If current ``met`` and ``rad`` slices to not include ``time_end``, new slices are selected
1093
+ from ``self.met`` and ``self.rad``. Downselection in space will cover
1094
+ - locations of current contrails (``latest_contrail``),
1095
+ - locations of additional contrails that will be loaded from ``self._downwash_flight``
1096
+ before the new slices expire,
1097
+ plus a user-defined buffer.
1098
+ """
1099
+ if met is None or time_end > met.indexes["time"].to_numpy()[-1]:
1100
+ logger.debug("Downselect met at time_end %s within Cocip evolution", time_end)
1101
+ met = self._definitely_downselect_met_or_rad(self.met, latest_contrail, time_end)
1102
+ met = add_tau_cirrus(met)
1103
+
1104
+ if rad is None or time_end > rad.indexes["time"].to_numpy()[-1]:
1105
+ logger.debug("Downselect rad at time_end %s within Cocip evolution", time_end)
1106
+ rad = self._definitely_downselect_met_or_rad(self.rad, latest_contrail, time_end)
1107
+
1108
+ return met, rad
1109
+
1110
+ def _definitely_downselect_met_or_rad(
1111
+ self, met: MetDataset, latest_contrail: GeoVectorDataset, time_end: np.datetime64
1112
+ ) -> MetDataset:
1113
+ """Perform downselection when required by :meth:`_maybe_downselect_met_rad`.
1114
+
1115
+ Downselects ``met`` (which should be one of ``self.met`` or ``self.rad``)
1116
+ to cover ``time_end``. Downselection in space covers
1117
+ - locations of current contrails (``latest_contrail``),
1118
+ - locations of additional contrails that will be loaded from ``self._downwash_flight``
1119
+ before the new slices expire,
1120
+ plus a user-defined buffer, as described in :meth:`_maybe_downselect_met_rad`.
1121
+ """
1122
+ # compute lookahead for future contrails from downwash_flight
1123
+ met_time = met.indexes["time"].to_numpy()
1124
+ mask = met_time >= time_end
1125
+ lookahead = np.min(met_time[mask]) if np.any(mask) else time_end
1126
+
1127
+ # create vector for downselection based on current + future contrails
1128
+ future_contrails = self._downwash_flight.filter(
1129
+ (self._downwash_flight["time"] >= time_end)
1130
+ & (self._downwash_flight["time"] <= lookahead),
1131
+ copy=False,
1132
+ )
1133
+ vector = GeoVectorDataset(
1134
+ {
1135
+ key: np.concat((latest_contrail[key], future_contrails[key]))
1136
+ for key in ("longitude", "latitude", "level", "time")
1137
+ }
1138
+ )
1139
+
1140
+ # compute time buffer to ensure downselection extends to time_end
1141
+ buffers = {
1142
+ f"{coord}_buffer": self.params[f"met_{coord}_buffer"]
1143
+ for coord in ("longitude", "latitude", "level")
1144
+ }
1145
+ buffers["time_buffer"] = (
1146
+ np.timedelta64(0, "ns"),
1147
+ max(np.timedelta64(0, "ns"), time_end - vector["time"].max()),
1148
+ )
1149
+
1150
+ return vector.downselect_met(met, **buffers, copy=False)
1151
+
1078
1152
  def _create_downwash_contrail(self) -> GeoVectorDataset:
1079
1153
  """Get Contrail representation of downwash flight."""
1080
1154
 
@@ -84,6 +84,27 @@ class CocipParams(ModelParams):
84
84
  #: Humidity scaling
85
85
  humidity_scaling: HumidityScaling | None = None
86
86
 
87
+ #: Experimental. If ``True``, attempt to reduce memory consumption during
88
+ #: aircraft performance and initial contrail formation/persistent calculations
89
+ #: by calling :meth:`MetDataArray.interpolate` with ``lowmem=True``.
90
+ #:
91
+ #: **IMPORTANT**:
92
+ #:
93
+ #: * Memory optimizations used when ``proprocess_lowmem=True`` are designed for
94
+ #: meteorology backed by dask arrays with a chunk size of 1 along
95
+ #: the time dimension. This option may degrade performance if dask if not used
96
+ #: or if chunks contain more than a single time step.
97
+ #: * The impact on runtime of setting ``preprocess_lowmem=True`` depends on how
98
+ #: meteorology data is chunked. Runtime is likely to increase if meteorology
99
+ #: data is chunked in time only, but may decrease if meteorology data is also
100
+ #: chunked in longitude, latitude, and level.
101
+ #: * Changes to data access patterns with ``preprocess_lowmem=True`` alter locations
102
+ #: where interpolation is in- vs out-of-bounds. As a consequence,
103
+ #: Cocip output with ``preprocess_lowmem=True`` is only guaranteed to match output
104
+ #: with ``preprocess_lowmem=False`` when run with ``interpolation_bounds_error=True``
105
+ #: to ensure no out-of-bounds interpolation occurs.
106
+ preprocess_lowmem: bool = False
107
+
87
108
  # --------------
88
109
  # Downselect met
89
110
  # --------------
@@ -24,7 +24,6 @@ import pathlib
24
24
  import warnings
25
25
  from collections.abc import Hashable
26
26
 
27
- import matplotlib.pyplot as plt
28
27
  import numpy as np
29
28
  import numpy.typing as npt
30
29
  import pandas as pd
@@ -2141,7 +2140,17 @@ def compare_cocip_with_goes(
2141
2140
  name="compare_cocip_with_goes function",
2142
2141
  package_name="cartopy",
2143
2142
  module_not_found_error=e,
2144
- pycontrails_optional_package="goes",
2143
+ pycontrails_optional_package="sat",
2144
+ )
2145
+
2146
+ try:
2147
+ import matplotlib.pyplot as plt
2148
+ except ModuleNotFoundError as e:
2149
+ dependencies.raise_module_not_found_error(
2150
+ name="compare_cocip_with_goes function",
2151
+ package_name="matplotlib",
2152
+ module_not_found_error=e,
2153
+ pycontrails_optional_package="vis",
2145
2154
  )
2146
2155
 
2147
2156
  # Round `time` to nearest GOES image time slice
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pycontrails
3
- Version: 0.52.2
3
+ Version: 0.52.3
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
@@ -27,92 +27,92 @@ Requires-Python: >=3.9
27
27
  Description-Content-Type: text/markdown
28
28
  License-File: LICENSE
29
29
  License-File: NOTICE
30
- Requires-Dist: dask >=2022.3
31
- Requires-Dist: numpy >=1.22
32
- Requires-Dist: overrides >=6.1
33
- Requires-Dist: pandas >=2.2
34
- Requires-Dist: scipy >=1.10
35
- Requires-Dist: xarray >=2022.3
30
+ Requires-Dist: dask>=2022.3
31
+ Requires-Dist: numpy>=1.22
32
+ Requires-Dist: overrides>=6.1
33
+ Requires-Dist: pandas>=2.2
34
+ Requires-Dist: scipy>=1.10
35
+ Requires-Dist: xarray>=2022.3
36
36
  Provides-Extra: complete
37
- Requires-Dist: pycontrails[ecmwf,gcp,gfs,jupyter,pyproj,sat,vis,zarr] ; extra == 'complete'
37
+ Requires-Dist: pycontrails[ecmwf,gcp,gfs,jupyter,pyproj,sat,vis,zarr]; extra == "complete"
38
38
  Provides-Extra: dev
39
- Requires-Dist: black[jupyter] ==24.4.2 ; extra == 'dev'
40
- Requires-Dist: dep-license ; extra == 'dev'
41
- Requires-Dist: fastparquet >=0.8 ; extra == 'dev'
42
- Requires-Dist: ipdb >=0.13 ; extra == 'dev'
43
- Requires-Dist: memory-profiler ; extra == 'dev'
44
- Requires-Dist: mypy >=1.8 ; extra == 'dev'
45
- Requires-Dist: mypy-extensions >=1.0 ; extra == 'dev'
46
- Requires-Dist: platformdirs >=3.0 ; extra == 'dev'
47
- Requires-Dist: pre-commit >=2.10 ; extra == 'dev'
48
- Requires-Dist: psutil ; extra == 'dev'
49
- Requires-Dist: pyarrow >=5.0 ; extra == 'dev'
50
- Requires-Dist: pytest >=8.2 ; extra == 'dev'
51
- Requires-Dist: pytest-cov >=2.11 ; extra == 'dev'
52
- Requires-Dist: requests >=2.25 ; extra == 'dev'
53
- Requires-Dist: ruff ==0.5.3 ; extra == 'dev'
54
- Requires-Dist: setuptools ; extra == 'dev'
39
+ Requires-Dist: black[jupyter]==24.4.2; extra == "dev"
40
+ Requires-Dist: dep-license; extra == "dev"
41
+ Requires-Dist: fastparquet>=0.8; extra == "dev"
42
+ Requires-Dist: ipdb>=0.13; extra == "dev"
43
+ Requires-Dist: memory-profiler; extra == "dev"
44
+ Requires-Dist: mypy>=1.8; extra == "dev"
45
+ Requires-Dist: mypy-extensions>=1.0; extra == "dev"
46
+ Requires-Dist: platformdirs>=3.0; extra == "dev"
47
+ Requires-Dist: pre-commit>=2.10; extra == "dev"
48
+ Requires-Dist: psutil; extra == "dev"
49
+ Requires-Dist: pyarrow>=5.0; extra == "dev"
50
+ Requires-Dist: pytest>=8.2; extra == "dev"
51
+ Requires-Dist: pytest-cov>=2.11; extra == "dev"
52
+ Requires-Dist: requests>=2.25; extra == "dev"
53
+ Requires-Dist: ruff==0.5.3; extra == "dev"
54
+ Requires-Dist: setuptools; extra == "dev"
55
55
  Provides-Extra: docs
56
- Requires-Dist: doc8 >=1.1 ; extra == 'docs'
57
- Requires-Dist: furo >=2023.3 ; extra == 'docs'
58
- Requires-Dist: myst-parser >=1.0 ; extra == 'docs'
59
- Requires-Dist: nb-clean >=3.2 ; extra == 'docs'
60
- Requires-Dist: nbsphinx >=0.9 ; extra == 'docs'
61
- Requires-Dist: nbval !=0.10.0,>=0.9.6 ; extra == 'docs'
62
- Requires-Dist: pytest-check-links >=0.8.0 ; extra == 'docs'
63
- Requires-Dist: sphinx >=4.2 ; extra == 'docs'
64
- Requires-Dist: sphinx-autobuild >=0.7 ; extra == 'docs'
65
- Requires-Dist: sphinxcontrib-bibtex >=2.2 ; extra == 'docs'
66
- Requires-Dist: sphinx-copybutton >=0.5 ; extra == 'docs'
67
- Requires-Dist: sphinxext.opengraph >=0.8 ; extra == 'docs'
56
+ Requires-Dist: doc8>=1.1; extra == "docs"
57
+ Requires-Dist: furo>=2023.3; extra == "docs"
58
+ Requires-Dist: myst-parser>=1.0; extra == "docs"
59
+ Requires-Dist: nb-clean>=3.2; extra == "docs"
60
+ Requires-Dist: nbsphinx>=0.9; extra == "docs"
61
+ Requires-Dist: nbval!=0.10.0,>=0.9.6; extra == "docs"
62
+ Requires-Dist: pytest-check-links>=0.8.0; extra == "docs"
63
+ Requires-Dist: sphinx>=4.2; extra == "docs"
64
+ Requires-Dist: sphinx-autobuild>=0.7; extra == "docs"
65
+ Requires-Dist: sphinxcontrib-bibtex>=2.2; extra == "docs"
66
+ Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
67
+ Requires-Dist: sphinxext.opengraph>=0.8; extra == "docs"
68
68
  Provides-Extra: ecmwf
69
- Requires-Dist: cdsapi >=0.4 ; extra == 'ecmwf'
70
- Requires-Dist: cfgrib >=0.9 ; extra == 'ecmwf'
71
- Requires-Dist: eccodes >=1.4 ; extra == 'ecmwf'
72
- Requires-Dist: ecmwf-api-client >=1.6 ; extra == 'ecmwf'
73
- Requires-Dist: netcdf4 >=1.6.1 ; extra == 'ecmwf'
74
- Requires-Dist: platformdirs >=3.0 ; extra == 'ecmwf'
75
- Requires-Dist: requests >=2.25 ; extra == 'ecmwf'
76
- Requires-Dist: lxml >=5.1.0 ; extra == 'ecmwf'
69
+ Requires-Dist: cdsapi>=0.4; extra == "ecmwf"
70
+ Requires-Dist: cfgrib>=0.9; extra == "ecmwf"
71
+ Requires-Dist: eccodes>=1.4; extra == "ecmwf"
72
+ Requires-Dist: ecmwf-api-client>=1.6; extra == "ecmwf"
73
+ Requires-Dist: netcdf4>=1.6.1; extra == "ecmwf"
74
+ Requires-Dist: platformdirs>=3.0; extra == "ecmwf"
75
+ Requires-Dist: requests>=2.25; extra == "ecmwf"
76
+ Requires-Dist: lxml>=5.1.0; extra == "ecmwf"
77
77
  Provides-Extra: gcp
78
- Requires-Dist: google-cloud-storage >=2.1 ; extra == 'gcp'
79
- Requires-Dist: platformdirs >=3.0 ; extra == 'gcp'
80
- Requires-Dist: tqdm >=4.61 ; extra == 'gcp'
78
+ Requires-Dist: google-cloud-storage>=2.1; extra == "gcp"
79
+ Requires-Dist: platformdirs>=3.0; extra == "gcp"
80
+ Requires-Dist: tqdm>=4.61; extra == "gcp"
81
81
  Provides-Extra: gfs
82
- Requires-Dist: boto3 >=1.20 ; extra == 'gfs'
83
- Requires-Dist: cfgrib >=0.9 ; extra == 'gfs'
84
- Requires-Dist: eccodes >=1.4 ; extra == 'gfs'
85
- Requires-Dist: platformdirs >=3.0 ; extra == 'gfs'
86
- Requires-Dist: tqdm >=4.61 ; extra == 'gfs'
82
+ Requires-Dist: boto3>=1.20; extra == "gfs"
83
+ Requires-Dist: cfgrib>=0.9; extra == "gfs"
84
+ Requires-Dist: eccodes>=1.4; extra == "gfs"
85
+ Requires-Dist: platformdirs>=3.0; extra == "gfs"
86
+ Requires-Dist: tqdm>=4.61; extra == "gfs"
87
87
  Provides-Extra: jupyter
88
- Requires-Dist: ipywidgets >=7.6 ; extra == 'jupyter'
89
- Requires-Dist: jupyterlab >=2.2 ; extra == 'jupyter'
88
+ Requires-Dist: ipywidgets>=7.6; extra == "jupyter"
89
+ Requires-Dist: jupyterlab>=2.2; extra == "jupyter"
90
90
  Provides-Extra: open3d
91
- Requires-Dist: open3d >=0.14 ; extra == 'open3d'
91
+ Requires-Dist: open3d>=0.14; extra == "open3d"
92
92
  Provides-Extra: pyproj
93
- Requires-Dist: pyproj >=3.5 ; extra == 'pyproj'
93
+ Requires-Dist: pyproj>=3.5; extra == "pyproj"
94
94
  Provides-Extra: sat
95
- Requires-Dist: cartopy >=0.22 ; extra == 'sat'
96
- Requires-Dist: db-dtypes >=1.2 ; extra == 'sat'
97
- Requires-Dist: gcsfs >=2022.3 ; extra == 'sat'
98
- Requires-Dist: geojson >=3.1 ; extra == 'sat'
99
- Requires-Dist: google-cloud-bigquery >=3.23 ; extra == 'sat'
100
- Requires-Dist: google-cloud-bigquery-storage >=2.25 ; extra == 'sat'
101
- Requires-Dist: pillow >=10.3 ; extra == 'sat'
102
- Requires-Dist: pyproj >=3.5 ; extra == 'sat'
103
- Requires-Dist: rasterio >=1.3 ; extra == 'sat'
104
- Requires-Dist: scikit-image >=0.18 ; extra == 'sat'
95
+ Requires-Dist: cartopy>=0.22; extra == "sat"
96
+ Requires-Dist: db-dtypes>=1.2; extra == "sat"
97
+ Requires-Dist: gcsfs>=2022.3; extra == "sat"
98
+ Requires-Dist: geojson>=3.1; extra == "sat"
99
+ Requires-Dist: google-cloud-bigquery>=3.23; extra == "sat"
100
+ Requires-Dist: google-cloud-bigquery-storage>=2.25; extra == "sat"
101
+ Requires-Dist: pillow>=10.3; extra == "sat"
102
+ Requires-Dist: pyproj>=3.5; extra == "sat"
103
+ Requires-Dist: rasterio>=1.3; extra == "sat"
104
+ Requires-Dist: scikit-image>=0.18; extra == "sat"
105
105
  Provides-Extra: vis
106
- Requires-Dist: matplotlib >=3.3 ; extra == 'vis'
107
- Requires-Dist: opencv-python-headless >=4.5 ; extra == 'vis'
108
- Requires-Dist: scikit-learn >=0.23 ; extra == 'vis'
109
- Requires-Dist: scikit-image >=0.18 ; extra == 'vis'
110
- Requires-Dist: seaborn >=0.11 ; extra == 'vis'
111
- Requires-Dist: shapely >=2.0 ; extra == 'vis'
106
+ Requires-Dist: matplotlib>=3.3; extra == "vis"
107
+ Requires-Dist: opencv-python-headless>=4.5; extra == "vis"
108
+ Requires-Dist: scikit-learn>=0.23; extra == "vis"
109
+ Requires-Dist: scikit-image>=0.18; extra == "vis"
110
+ Requires-Dist: seaborn>=0.11; extra == "vis"
111
+ Requires-Dist: shapely>=2.0; extra == "vis"
112
112
  Provides-Extra: zarr
113
- Requires-Dist: fsspec >=2022.7.1 ; extra == 'zarr'
114
- Requires-Dist: gcsfs >=2022.7.1 ; extra == 'zarr'
115
- Requires-Dist: zarr >=2.12 ; extra == 'zarr'
113
+ Requires-Dist: fsspec>=2022.7.1; extra == "zarr"
114
+ Requires-Dist: gcsfs>=2022.7.1; extra == "zarr"
115
+ Requires-Dist: zarr>=2.12; extra == "zarr"
116
116
 
117
117
  # pycontrails
118
118
 
@@ -1,24 +1,24 @@
1
- pycontrails-0.52.2.dist-info/RECORD,,
2
- pycontrails-0.52.2.dist-info/LICENSE,sha256=gJ-h7SFFD1mCfR6a7HILvEtodDT6Iig8bLXdgqR6ucA,10175
3
- pycontrails-0.52.2.dist-info/WHEEL,sha256=jPrF5oWulsOxop3iQfMuD22j-CtnUOYm5n_3r9ZbENY,109
4
- pycontrails-0.52.2.dist-info/NOTICE,sha256=gKI8DcN1WhiXB2SFRKDogcjONldGubTvBxiOYdC4CXU,1926
5
- pycontrails-0.52.2.dist-info/top_level.txt,sha256=Z8J1R_AiBAyCVjNw6jYLdrA68PrQqTg0t3_Yek_IZ0Q,29
6
- pycontrails-0.52.2.dist-info/METADATA,sha256=_6cclDjRduE1ZmJm15bLAx4BibdxxfKd2ElNj8CwyNY,9307
7
- pycontrails/_version.py,sha256=7hRRz1Yg5iw29YoTywrkjgawiL8eg5Gzlc-mhwWCdtM,413
1
+ pycontrails-0.52.3.dist-info/RECORD,,
2
+ pycontrails-0.52.3.dist-info/LICENSE,sha256=gJ-h7SFFD1mCfR6a7HILvEtodDT6Iig8bLXdgqR6ucA,10175
3
+ pycontrails-0.52.3.dist-info/WHEEL,sha256=jPrF5oWulsOxop3iQfMuD22j-CtnUOYm5n_3r9ZbENY,109
4
+ pycontrails-0.52.3.dist-info/NOTICE,sha256=gKI8DcN1WhiXB2SFRKDogcjONldGubTvBxiOYdC4CXU,1926
5
+ pycontrails-0.52.3.dist-info/top_level.txt,sha256=Z8J1R_AiBAyCVjNw6jYLdrA68PrQqTg0t3_Yek_IZ0Q,29
6
+ pycontrails-0.52.3.dist-info/METADATA,sha256=0OlDUND65RdPbrTL_Zt4hXkn_A3nPptiZ5vXLk1gfaU,9170
7
+ pycontrails/_version.py,sha256=oUlYgy_HesUiXOJT6LNUWojwIHRLzqLUsE1WSH4KNos,413
8
8
  pycontrails/__init__.py,sha256=O2T9kXCMhcELcMZz7HEnwiBhh4Gfcj-yG1HtrotOKHQ,2001
9
9
  pycontrails/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  pycontrails/core/vector.py,sha256=ZvI-hQylANy6X8cSBMPa51FnMkmkjASsPyKLflZQJOg,71571
11
11
  pycontrails/core/models.py,sha256=z06ggBjtWJyDXUexGtoaUgHJA6e7jajqPvZvMXvlSwk,39310
12
- pycontrails/core/interpolation.py,sha256=beTKgJKQDu07rqwfkAJFOZu83jQTKm1mOEC23EDiLdA,25519
12
+ pycontrails/core/interpolation.py,sha256=w3F6G1vmmBgBnjlL06G01CaRYpfm2wrQLbkwBhX_Xx8,25589
13
13
  pycontrails/core/fleet.py,sha256=wqYY_2xD9X-Og0_oxU8ZPqTHYDau9TOPLQcmEnB1kiQ,16140
14
- pycontrails/core/rgi_cython.cpython-310-darwin.so,sha256=aSxW1q0rXzh9Ma3gCgUJxui806x_JCiZBn1CLZ1gTiM,310832
14
+ pycontrails/core/rgi_cython.cpython-310-darwin.so,sha256=AOJG9IBpTc4WdZ7jaerl5iyoo1mrfuRu2gbUvOU7uCs,310832
15
15
  pycontrails/core/flight.py,sha256=EdB8_P8BELSyXx01CXw_js0zgv-bdsqj3_mt28lhKMU,85144
16
16
  pycontrails/core/fuel.py,sha256=kJZ3P1lPm1L6rdPREM55XQ-VfJ_pt35cP4sO2Nnvmjs,4332
17
17
  pycontrails/core/polygon.py,sha256=gosyZBX1XBKD2EcHycIZb7uM-xGs8rCfdpiSZlhc2Hc,18028
18
18
  pycontrails/core/cache.py,sha256=9TaHyLtCpDazgN5zEt0aa4xNgvNcMc4oO_6nEz0XhYE,27971
19
19
  pycontrails/core/__init__.py,sha256=x1z6x8w3sYmEqYcNWyWHuNkS9lPUPbHUoYJZs1K0q98,856
20
20
  pycontrails/core/flightplan.py,sha256=s7tHbjMFbHAJkSWV6hPkghuW6jDb1n5UhWAo9XbJ9z0,7349
21
- pycontrails/core/met.py,sha256=tUoo0RT9FBhUWp_IavbyUm-uze1GuWnms0XrYZtZ8Ds,93967
21
+ pycontrails/core/met.py,sha256=vQl2Xenr0Ar-UFofzHOnCDS1NjnP6w5PbupTXDZY6ZI,101325
22
22
  pycontrails/core/aircraft_performance.py,sha256=4KnLj0zK-mk8Oo3As1CXUkQWBQGMeDdrKi5TeOhOmUA,26107
23
23
  pycontrails/core/airports.py,sha256=aeyAXVkioIRomrP79UtNrxindL4f1DJyXFaojZCuBBw,6758
24
24
  pycontrails/core/met_var.py,sha256=EhrLGdrCAp8cKb-3Whd_ttLMZn4_zLMhE-QyFinESqo,9197
@@ -82,10 +82,10 @@ pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq,s
82
82
  pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq,sha256=pShCvNUo0NYtAHhT9IBRuj38X9jejdlKfv-ZoOKmtKI,35943
83
83
  pycontrails/models/cocip/radiative_forcing.py,sha256=ERuFcYMo0_1iiOricnZ8D4ext23bMnTCeZwg9vd6Vzs,44944
84
84
  pycontrails/models/cocip/wind_shear.py,sha256=p8d3iaNzxPA3MoxFEM1ZDKt0aticoD6U9cv0QmbuBzs,3860
85
- pycontrails/models/cocip/cocip.py,sha256=1ReioOLubxyRjXAAZYwer2g144312wZQc72XhmUEVNA,97188
86
- pycontrails/models/cocip/output_formats.py,sha256=bmQKyyuwbn92W1IvdJI3NY4WfEpN6dT8HbxU38B_nnM,83352
85
+ pycontrails/models/cocip/cocip.py,sha256=yJQqlOuPNq_XFxhX8UtAAi7djxrh9r5bbClRE8zP2cE,100109
86
+ pycontrails/models/cocip/output_formats.py,sha256=TfMHBKtj0BGwfXpu37vqOXS9K8oBXo1t8_XDJxdSHY4,83645
87
87
  pycontrails/models/cocip/__init__.py,sha256=jd-9Tq20s1kwQBlxsYfZLi3hlT5MnWOY2XsPazq1fgE,962
88
- pycontrails/models/cocip/cocip_params.py,sha256=R4bewge3xLgWYbBbGwd8e8r0NlaFx2IaQPZEfiqJZRI,11392
88
+ pycontrails/models/cocip/cocip_params.py,sha256=kKTeF1vVQr361XBR79q4mQHYI7UUQ6C5Ik5Z5pJDtag,12703
89
89
  pycontrails/models/cocip/wake_vortex.py,sha256=i_OF193KK5BCMdVCgK0_4Aqn55f6rnL4WDWEac8um-w,14421
90
90
  pycontrails/models/cocip/cocip_uncertainty.py,sha256=-3ICEbrhB6eQiYIqpEahzhf12AwV7ge-yvj_NaOqW3g,11891
91
91
  pycontrails/models/cocip/radiative_heating.py,sha256=YRpwfXgFnf89iuJiIM96q-jbdcMAwlX8QLsADTKMABE,18848