pycontrails 0.41.0__cp310-cp310-macosx_11_0_arm64.whl → 0.42.2__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 +2 -2
- pycontrails/core/airports.py +228 -0
- pycontrails/core/cache.py +4 -6
- pycontrails/core/datalib.py +13 -6
- pycontrails/core/fleet.py +72 -20
- pycontrails/core/flight.py +485 -134
- pycontrails/core/flightplan.py +238 -0
- pycontrails/core/interpolation.py +11 -15
- pycontrails/core/met.py +5 -5
- pycontrails/core/models.py +4 -0
- pycontrails/core/rgi_cython.cpython-310-darwin.so +0 -0
- pycontrails/core/vector.py +80 -63
- pycontrails/datalib/__init__.py +1 -1
- pycontrails/datalib/ecmwf/common.py +14 -19
- pycontrails/datalib/spire/__init__.py +19 -0
- pycontrails/datalib/spire/spire.py +739 -0
- pycontrails/ext/bada/__init__.py +6 -6
- pycontrails/ext/cirium/__init__.py +2 -2
- pycontrails/models/cocip/cocip.py +37 -39
- pycontrails/models/cocip/cocip_params.py +37 -30
- pycontrails/models/cocip/cocip_uncertainty.py +47 -58
- pycontrails/models/cocip/radiative_forcing.py +220 -193
- pycontrails/models/cocip/wake_vortex.py +96 -91
- pycontrails/models/cocip/wind_shear.py +2 -2
- pycontrails/models/emissions/emissions.py +1 -1
- pycontrails/models/humidity_scaling.py +266 -9
- pycontrails/models/issr.py +2 -2
- pycontrails/models/pcr.py +1 -1
- pycontrails/models/quantiles/era5_ensemble_quantiles.npy +0 -0
- pycontrails/models/quantiles/iagos_quantiles.npy +0 -0
- pycontrails/models/sac.py +7 -5
- pycontrails/physics/geo.py +5 -3
- pycontrails/physics/jet.py +66 -113
- pycontrails/utils/json.py +3 -3
- {pycontrails-0.41.0.dist-info → pycontrails-0.42.2.dist-info}/METADATA +4 -7
- {pycontrails-0.41.0.dist-info → pycontrails-0.42.2.dist-info}/RECORD +40 -34
- {pycontrails-0.41.0.dist-info → pycontrails-0.42.2.dist-info}/LICENSE +0 -0
- {pycontrails-0.41.0.dist-info → pycontrails-0.42.2.dist-info}/NOTICE +0 -0
- {pycontrails-0.41.0.dist-info → pycontrails-0.42.2.dist-info}/WHEEL +0 -0
- {pycontrails-0.41.0.dist-info → pycontrails-0.42.2.dist-info}/top_level.txt +0 -0
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
"""Support for humidity scaling methodologies
|
|
1
|
+
"""Support for humidity scaling methodologies."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import abc
|
|
6
6
|
import dataclasses
|
|
7
|
+
import functools
|
|
8
|
+
import pathlib
|
|
7
9
|
import warnings
|
|
8
10
|
from typing import Any, NoReturn, overload
|
|
9
11
|
|
|
10
12
|
import numpy as np
|
|
13
|
+
import numpy.typing as npt
|
|
11
14
|
import xarray as xr
|
|
12
15
|
from overrides import overrides
|
|
13
16
|
|
|
14
|
-
from pycontrails.core.met import MetDataset
|
|
17
|
+
from pycontrails.core.met import MetDataArray, MetDataset
|
|
15
18
|
from pycontrails.core.models import Model, ModelParams
|
|
16
19
|
from pycontrails.core.vector import GeoVectorDataset
|
|
17
20
|
from pycontrails.physics import constants, thermo, units
|
|
@@ -19,7 +22,7 @@ from pycontrails.utils.types import ArrayLike
|
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
def _rhi_over_q(air_temperature: ArrayLike, air_pressure: ArrayLike) -> ArrayLike:
|
|
22
|
-
"""Compute the quotient RHi / q
|
|
25
|
+
"""Compute the quotient ``RHi / q``."""
|
|
23
26
|
return air_pressure * (constants.R_v / constants.R_d) / thermo.e_sat_ice(air_temperature)
|
|
24
27
|
|
|
25
28
|
|
|
@@ -140,16 +143,18 @@ class HumidityScaling(Model):
|
|
|
140
143
|
"being applied more than once."
|
|
141
144
|
)
|
|
142
145
|
|
|
146
|
+
p: np.ndarray | xr.DataArray
|
|
143
147
|
if isinstance(self.source, GeoVectorDataset):
|
|
144
|
-
self.source.setdefault("air_pressure", self.source.air_pressure)
|
|
148
|
+
p = self.source.setdefault("air_pressure", self.source.air_pressure)
|
|
149
|
+
else:
|
|
150
|
+
p = self.source.data["air_pressure"]
|
|
145
151
|
|
|
146
152
|
q = self.source.data["specific_humidity"]
|
|
147
153
|
T = self.source.data["air_temperature"]
|
|
148
|
-
p = self.source.data["air_pressure"]
|
|
149
154
|
kwargs = {k: self.get_source_param(k) for k in self.scaler_specific_keys}
|
|
150
155
|
|
|
151
156
|
q, rhi = self.scale(q, T, p, **kwargs)
|
|
152
|
-
self.source.update(specific_humidity=q, rhi=rhi)
|
|
157
|
+
self.source.update(specific_humidity=q, rhi=rhi)
|
|
153
158
|
|
|
154
159
|
return self.source
|
|
155
160
|
|
|
@@ -264,7 +269,7 @@ class ExponentialBoostHumidityScaling(HumidityScaling):
|
|
|
264
269
|
rhi /= rhi_adj
|
|
265
270
|
|
|
266
271
|
# Find ISSRs
|
|
267
|
-
is_issr = rhi >= 1
|
|
272
|
+
is_issr = rhi >= 1.0
|
|
268
273
|
|
|
269
274
|
# Apply boosting to ISSRs
|
|
270
275
|
if isinstance(rhi, xr.DataArray):
|
|
@@ -440,7 +445,7 @@ def _calc_rhi_max(air_temperature: ArrayLike) -> ArrayLike:
|
|
|
440
445
|
p_ice = thermo.e_sat_ice(air_temperature)
|
|
441
446
|
p_liq = thermo.e_sat_liquid(air_temperature)
|
|
442
447
|
return xr.where(
|
|
443
|
-
air_temperature < 235,
|
|
448
|
+
air_temperature < 235.0,
|
|
444
449
|
1.67 + (1.45 - 1.67) * (air_temperature - 190.0) / (235.0 - 190.0),
|
|
445
450
|
p_liq / p_ice,
|
|
446
451
|
)
|
|
@@ -553,10 +558,262 @@ class HumidityScalingByLevel(HumidityScaling):
|
|
|
553
558
|
"attribute 'mid_troposphere_threshold'."
|
|
554
559
|
)
|
|
555
560
|
|
|
556
|
-
level = air_pressure / 100
|
|
561
|
+
level = air_pressure / 100.0
|
|
557
562
|
fp = [rhi_adj_stratosphere, rhi_adj_mid_troposphere]
|
|
558
563
|
rhi_adj = np.interp(level, xp=xp, fp=fp)
|
|
559
564
|
|
|
560
565
|
q = specific_humidity / rhi_adj
|
|
561
566
|
rhi = thermo.rhi(q, air_temperature, air_pressure)
|
|
562
567
|
return q, rhi
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
@functools.cache
|
|
571
|
+
def _load_iagos_quantiles() -> npt.NDArray[np.float64]:
|
|
572
|
+
path = pathlib.Path(__file__).parent / "quantiles" / "iagos_quantiles.npy"
|
|
573
|
+
# FIXME: Recompute to avoid the divide by 100.0 here
|
|
574
|
+
return np.load(path, allow_pickle=False) / 100.0
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
@functools.cache
|
|
578
|
+
def _load_era5_ensemble_quantiles() -> npt.NDArray[np.float64]:
|
|
579
|
+
path = pathlib.Path(__file__).parent / "quantiles" / "era5_ensemble_quantiles.npy"
|
|
580
|
+
# FIXME: Recompute to avoid the divide by 100.0 here
|
|
581
|
+
return np.load(path, allow_pickle=False) / 100.0
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def quantile_rhi_map(era5_rhi: npt.NDArray[np.float_], member: int) -> npt.NDArray[np.float64]:
|
|
585
|
+
"""Map ERA5-derived RHi to it's corresponding IAGOS quantile via histogram matching.
|
|
586
|
+
|
|
587
|
+
This matching is performed on a **single** ERA5 ensemble member.
|
|
588
|
+
|
|
589
|
+
Parameters
|
|
590
|
+
----------
|
|
591
|
+
era5_rhi : npt.NDArray[np.float_]
|
|
592
|
+
ERA5-derived RHi values for the given ensemble member.
|
|
593
|
+
member : int
|
|
594
|
+
The ERA5 ensemble member to use. Must be in the range ``[0, 10)``.
|
|
595
|
+
|
|
596
|
+
Returns
|
|
597
|
+
-------
|
|
598
|
+
npt.NDArray[np.float64]
|
|
599
|
+
The IAGOS quantiles corresponding to the ERA5-derived RHi values.
|
|
600
|
+
"""
|
|
601
|
+
|
|
602
|
+
era5_quantiles = _load_era5_ensemble_quantiles() # shape (801, 10)
|
|
603
|
+
era5_quantiles = era5_quantiles[:, member] # shape (801,)
|
|
604
|
+
iagos_quantiles = _load_iagos_quantiles() # shape (801,)
|
|
605
|
+
|
|
606
|
+
return np.interp(era5_rhi, era5_quantiles, iagos_quantiles)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def recalibrate_rhi(
|
|
610
|
+
era5_rhi_all_members: npt.NDArray[np.float_], member: int
|
|
611
|
+
) -> npt.NDArray[np.float_]:
|
|
612
|
+
"""Recalibrate ERA5-derived RHi values to IAGOS quantiles.
|
|
613
|
+
|
|
614
|
+
This recalibration requires values for **all** ERA5 ensemble members.
|
|
615
|
+
|
|
616
|
+
Parameters
|
|
617
|
+
----------
|
|
618
|
+
era5_rhi_all_members : npt.NDArray[np.float_]
|
|
619
|
+
ERA5-derived RHi values for all ensemble members. This array should have shape ``(n, 10)``.
|
|
620
|
+
member : int
|
|
621
|
+
The ERA5 ensemble member to use. Must be in the range ``[0, 10)``.
|
|
622
|
+
|
|
623
|
+
Returns
|
|
624
|
+
-------
|
|
625
|
+
npt.NDArray[np.float_]
|
|
626
|
+
The recalibrated RHi values. This is an array of shape ``(n,)``.
|
|
627
|
+
|
|
628
|
+
References
|
|
629
|
+
----------
|
|
630
|
+
:cite:`eckelCalibratedProbabilisticQuantitative1998`
|
|
631
|
+
"""
|
|
632
|
+
|
|
633
|
+
n_members = 10
|
|
634
|
+
assert era5_rhi_all_members.shape[1] == n_members
|
|
635
|
+
|
|
636
|
+
# Perform histogram matching on the given ensemble member
|
|
637
|
+
recalibrated_rhi = quantile_rhi_map(era5_rhi_all_members[:, member], member)
|
|
638
|
+
|
|
639
|
+
# Perform histogram matching on all other ensemble members
|
|
640
|
+
# Add up the results into a single 'ensemble_mean_rhi' array
|
|
641
|
+
ensemble_mean_rhi: npt.NDArray[np.float_] = 0.0 # type: ignore[assignment]
|
|
642
|
+
for r in range(n_members):
|
|
643
|
+
if r == member:
|
|
644
|
+
ensemble_mean_rhi += recalibrated_rhi
|
|
645
|
+
else:
|
|
646
|
+
ensemble_mean_rhi += quantile_rhi_map(era5_rhi_all_members[:, r], r)
|
|
647
|
+
|
|
648
|
+
# Divide by the number of ensemble members to get the mean
|
|
649
|
+
ensemble_mean_rhi /= n_members
|
|
650
|
+
|
|
651
|
+
eckel_a = -0.005213832567192828
|
|
652
|
+
eckel_c = 2.7859172756970354
|
|
653
|
+
|
|
654
|
+
out = (ensemble_mean_rhi - eckel_a) + eckel_c * (recalibrated_rhi - ensemble_mean_rhi)
|
|
655
|
+
return out.astype(era5_rhi_all_members.dtype)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
@dataclasses.dataclass
|
|
659
|
+
class HistogramMatchingWithEckelParams(ModelParams):
|
|
660
|
+
"""Parameters for :class:`HistogramMatchingWithEckel`.
|
|
661
|
+
|
|
662
|
+
.. warning::
|
|
663
|
+
Experimental. This may change or be removed in a future release.
|
|
664
|
+
"""
|
|
665
|
+
|
|
666
|
+
#: A length-10 list of ERA5 ensemble members.
|
|
667
|
+
#: Each element is a :class:`MetDataArray` holding specific humidity
|
|
668
|
+
#: values for a single ensemble member. If None, a ValueError will be
|
|
669
|
+
#: raised at model instantiation time. The order of the list must be
|
|
670
|
+
#: consistent with the order of the ERA5 ensemble members.
|
|
671
|
+
ensemble_specific_humidity: list[MetDataArray] | None = None
|
|
672
|
+
|
|
673
|
+
#: The specific member used. Must be in the range [0, 10). If None,
|
|
674
|
+
#: a ValueError will be raised at model instantiation time.
|
|
675
|
+
member: int | None = None
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
class HistogramMatchingWithEckel(HumidityScaling):
|
|
679
|
+
"""Scale humidity by histogram matching to IAGOS RHi quantiles.
|
|
680
|
+
|
|
681
|
+
This method also applies the Eckel scaling to the recalibrated RHi values.
|
|
682
|
+
|
|
683
|
+
Unlike other specific humidity scaling methods, this method requires met data
|
|
684
|
+
and performs interpolation at evaluation time.
|
|
685
|
+
|
|
686
|
+
.. warning::
|
|
687
|
+
Experimental. This may change or be removed in a future release.
|
|
688
|
+
|
|
689
|
+
References
|
|
690
|
+
----------
|
|
691
|
+
:cite:`eckelCalibratedProbabilisticQuantitative1998`
|
|
692
|
+
"""
|
|
693
|
+
|
|
694
|
+
name = "histogram_matching_with_eckel"
|
|
695
|
+
long_name = "IAGOS RHi histogram matching with Eckel scaling"
|
|
696
|
+
formula = "era5_quantiles -> iagos_quantiles -> recalibrated_rhi"
|
|
697
|
+
default_params = HistogramMatchingWithEckelParams
|
|
698
|
+
|
|
699
|
+
n_members = 10 # hard-coded elsewhere
|
|
700
|
+
|
|
701
|
+
def __init__(
|
|
702
|
+
self,
|
|
703
|
+
met: MetDataset | None = None,
|
|
704
|
+
params: dict[str, Any] | None = None,
|
|
705
|
+
**params_kwargs: Any,
|
|
706
|
+
) -> None:
|
|
707
|
+
super().__init__(met, params, **params_kwargs)
|
|
708
|
+
|
|
709
|
+
# Some very crude validation
|
|
710
|
+
member = self.params["member"]
|
|
711
|
+
assert member in range(self.n_members)
|
|
712
|
+
self.member: int = member
|
|
713
|
+
|
|
714
|
+
ensemble_specific_humidity = self.params["ensemble_specific_humidity"]
|
|
715
|
+
assert len(ensemble_specific_humidity) == self.n_members
|
|
716
|
+
for member, mda in enumerate(ensemble_specific_humidity):
|
|
717
|
+
try:
|
|
718
|
+
assert mda.data["number"] == member
|
|
719
|
+
except KeyError:
|
|
720
|
+
pass
|
|
721
|
+
|
|
722
|
+
self.ensemble_specific_humidity: list[MetDataArray] = ensemble_specific_humidity
|
|
723
|
+
|
|
724
|
+
@overload
|
|
725
|
+
def eval(self, source: GeoVectorDataset, **params: Any) -> GeoVectorDataset:
|
|
726
|
+
...
|
|
727
|
+
|
|
728
|
+
@overload
|
|
729
|
+
def eval(self, source: MetDataset, **params: Any) -> NoReturn:
|
|
730
|
+
...
|
|
731
|
+
|
|
732
|
+
@overload
|
|
733
|
+
def eval(self, source: None = ..., **params: Any) -> NoReturn:
|
|
734
|
+
...
|
|
735
|
+
|
|
736
|
+
def eval(
|
|
737
|
+
self, source: GeoVectorDataset | MetDataset | None = None, **params: Any
|
|
738
|
+
) -> GeoVectorDataset | MetDataset:
|
|
739
|
+
"""Scale specific humidity by histogram matching to IAGOS RHi quantiles.
|
|
740
|
+
|
|
741
|
+
This method assumes ``source`` is equipped with the following variables:
|
|
742
|
+
|
|
743
|
+
- air_temperature
|
|
744
|
+
- specific_humidity: Humidity values for the :attr:`member` ERA5 ensemble member.
|
|
745
|
+
"""
|
|
746
|
+
|
|
747
|
+
self.update_params(params)
|
|
748
|
+
self.set_source(source)
|
|
749
|
+
self.source = self.require_source_type(GeoVectorDataset)
|
|
750
|
+
|
|
751
|
+
if "rhi" in self.source:
|
|
752
|
+
warnings.warn(
|
|
753
|
+
"Variable 'rhi' already found on source to be scaled. This "
|
|
754
|
+
"is unexpected and may be the result of humidity scaling "
|
|
755
|
+
"being applied more than once."
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
# Create a 2D array of specific humidity values for all ensemble members
|
|
759
|
+
# The specific humidity values for the current member are taken from the source
|
|
760
|
+
# This matches patterns used in other humidity scaling methods
|
|
761
|
+
# The remaining values are interpolated from the ERA5 ensemble members
|
|
762
|
+
q = self.source.data["specific_humidity"]
|
|
763
|
+
q2d = np.empty((len(self.source), self.n_members), dtype=q.dtype)
|
|
764
|
+
|
|
765
|
+
for member, mda in enumerate(self.ensemble_specific_humidity):
|
|
766
|
+
if member == self.member:
|
|
767
|
+
q2d[:, member] = q
|
|
768
|
+
else:
|
|
769
|
+
q2d[:, member] = self.source.intersect_met(mda, **self.interp_kwargs)
|
|
770
|
+
|
|
771
|
+
p = self.source.setdefault("air_pressure", self.source.air_pressure)
|
|
772
|
+
T = self.source.data["air_temperature"]
|
|
773
|
+
|
|
774
|
+
q, rhi = self.scale(q2d, T, p)
|
|
775
|
+
self.source.update(specific_humidity=q, rhi=rhi)
|
|
776
|
+
|
|
777
|
+
return self.source
|
|
778
|
+
|
|
779
|
+
@overrides
|
|
780
|
+
def scale( # type: ignore[override]
|
|
781
|
+
self,
|
|
782
|
+
specific_humidity: npt.NDArray[np.float_],
|
|
783
|
+
air_temperature: npt.NDArray[np.float_],
|
|
784
|
+
air_pressure: npt.NDArray[np.float_],
|
|
785
|
+
**kwargs: Any,
|
|
786
|
+
) -> tuple[npt.NDArray[np.float_], npt.NDArray[np.float_]]:
|
|
787
|
+
"""Scale specific humidity values via histogram matching and Eckel scaling.
|
|
788
|
+
|
|
789
|
+
Unlike the method on the base class, the method assumes each of the input
|
|
790
|
+
arrays are :class:`np.ndarray` and not :class:`xr.DataArray` objects.
|
|
791
|
+
|
|
792
|
+
Parameters
|
|
793
|
+
----------
|
|
794
|
+
specific_humidity : npt.NDArray[np.float_]
|
|
795
|
+
A 2D array of specific humidity values for all ERA5 ensemble members.
|
|
796
|
+
The shape of this array must be ``(n, 10)``, where ``n`` is the number
|
|
797
|
+
of observations and ``10`` is the number of ERA5 ensemble members.
|
|
798
|
+
air_temperature : npt.NDArray[np.float_]
|
|
799
|
+
A 1D array of air temperature values with shape ``(n,)``.
|
|
800
|
+
air_pressure : npt.NDArray[np.float_]
|
|
801
|
+
A 1D array of air pressure values with shape ``(n,)``.
|
|
802
|
+
kwargs: Any
|
|
803
|
+
Unused, kept for compatibility with the base class.
|
|
804
|
+
|
|
805
|
+
Returns
|
|
806
|
+
-------
|
|
807
|
+
specific_humidity : npt.NDArray[np.float_]
|
|
808
|
+
The recalibrated specific humidity values. A 1D array with shape ``(n,)``.
|
|
809
|
+
rhi : npt.NDArray[np.float_]
|
|
810
|
+
The recalibrated RHi values. A 1D array with shape ``(n,)``.
|
|
811
|
+
"""
|
|
812
|
+
|
|
813
|
+
rhi_over_q = _rhi_over_q(air_temperature, air_pressure)
|
|
814
|
+
rhi = rhi_over_q[:, np.newaxis] * specific_humidity
|
|
815
|
+
|
|
816
|
+
recalibrated_rhi = recalibrate_rhi(rhi, self.member)
|
|
817
|
+
recalibrated_q = recalibrated_rhi / rhi_over_q
|
|
818
|
+
|
|
819
|
+
return recalibrated_q, recalibrated_rhi
|
pycontrails/models/issr.py
CHANGED
|
@@ -51,7 +51,7 @@ class ISSR(Model):
|
|
|
51
51
|
>>> variables = ["air_temperature", "specific_humidity"]
|
|
52
52
|
>>> pressure_levels = [200, 250, 300]
|
|
53
53
|
>>> era5 = ERA5(time, variables, pressure_levels)
|
|
54
|
-
>>> met = era5.open_metdataset(
|
|
54
|
+
>>> met = era5.open_metdataset()
|
|
55
55
|
|
|
56
56
|
>>> # Instantiate and run model
|
|
57
57
|
>>> scaling = ConstantHumidityScaling(rhi_adj=0.98)
|
|
@@ -136,7 +136,7 @@ class ISSR(Model):
|
|
|
136
136
|
rhi=self.source.data.get("rhi", None), # if rhi already known, pass it in
|
|
137
137
|
rhi_threshold=self.params["rhi_threshold"],
|
|
138
138
|
)
|
|
139
|
-
self.source["issr"] = issr_
|
|
139
|
+
self.source["issr"] = issr_
|
|
140
140
|
|
|
141
141
|
# Tag output with additional attrs when source is MetDataset
|
|
142
142
|
if isinstance(self.source, MetDataset):
|
pycontrails/models/pcr.py
CHANGED
|
@@ -97,7 +97,7 @@ class PCR(Model):
|
|
|
97
97
|
sac_model.eval(self.source)
|
|
98
98
|
|
|
99
99
|
pcr_ = _pcr_from_issr_and_sac(self.source.data["issr"], self.source.data["sac"])
|
|
100
|
-
self.source["pcr"] = pcr_
|
|
100
|
+
self.source["pcr"] = pcr_
|
|
101
101
|
# Tag output with additional attrs when source is MetDataset
|
|
102
102
|
if isinstance(self.source, MetDataset):
|
|
103
103
|
attrs = {**self.source["issr"].attrs, **self.source["sac"].attrs}
|
|
Binary file
|
|
Binary file
|
pycontrails/models/sac.py
CHANGED
|
@@ -139,11 +139,11 @@ class SAC(Model):
|
|
|
139
139
|
sac_ = sac(rh, rh_crit_sac)
|
|
140
140
|
|
|
141
141
|
# Attaching some intermediate artifacts onto the source
|
|
142
|
-
self.source["G"] = G
|
|
143
|
-
self.source["T_sat_liquid"] = T_sat_liquid_
|
|
144
|
-
self.source["rh"] = rh
|
|
145
|
-
self.source["rh_critical_sac"] = rh_crit_sac
|
|
146
|
-
self.source["sac"] = sac_
|
|
142
|
+
self.source["G"] = G
|
|
143
|
+
self.source["T_sat_liquid"] = T_sat_liquid_
|
|
144
|
+
self.source["rh"] = rh
|
|
145
|
+
self.source["rh_critical_sac"] = rh_crit_sac
|
|
146
|
+
self.source["sac"] = sac_
|
|
147
147
|
|
|
148
148
|
# Tag output with additional attrs when source is MetDataset
|
|
149
149
|
if isinstance(self.source, MetDataset):
|
|
@@ -438,6 +438,8 @@ def T_critical_sac(
|
|
|
438
438
|
# We only apply Newton's method at points with rh bounded below 1 (scipy will
|
|
439
439
|
# raise an error if Newton's method is not converging well).
|
|
440
440
|
filt = relative_humidity < 0.999
|
|
441
|
+
if not np.any(filt):
|
|
442
|
+
return T_LM
|
|
441
443
|
|
|
442
444
|
U_filt = relative_humidity[filt]
|
|
443
445
|
T_LM_filt = T_LM[filt]
|
pycontrails/physics/geo.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
|
+
import numpy.typing as npt
|
|
6
7
|
import xarray as xr
|
|
7
8
|
|
|
8
9
|
from pycontrails.physics import constants, units
|
|
@@ -32,11 +33,12 @@ def haversine(lons0: ArrayLike, lats0: ArrayLike, lons1: ArrayLike, lats1: Array
|
|
|
32
33
|
|
|
33
34
|
Notes
|
|
34
35
|
-----
|
|
35
|
-
This formula does not take into account the non-spheroidal (ellipsoidal) shape of the Earth
|
|
36
|
+
This formula does not take into account the non-spheroidal (ellipsoidal) shape of the Earth.
|
|
37
|
+
Originally referenced from https://andrew.hedges.name/experiments/haversine/.
|
|
36
38
|
|
|
37
39
|
References
|
|
38
40
|
----------
|
|
39
|
-
|
|
41
|
+
- :cite:`CalculateDistanceBearing`
|
|
40
42
|
|
|
41
43
|
See Also
|
|
42
44
|
--------
|
|
@@ -833,7 +835,7 @@ def advect_level(
|
|
|
833
835
|
return (level * 100.0 + (dt_s * velocity)) / 100.0
|
|
834
836
|
|
|
835
837
|
|
|
836
|
-
def _dt_to_float_seconds(dt: np.ndarray | np.timedelta64, dtype:
|
|
838
|
+
def _dt_to_float_seconds(dt: np.ndarray | np.timedelta64, dtype: npt.DTypeLike) -> np.ndarray:
|
|
837
839
|
"""Convert a time delta to seconds as a float with specified ``dtype`` precision.
|
|
838
840
|
|
|
839
841
|
Parameters
|