pycontrails 0.58.0__cp314-cp314-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/__init__.py +70 -0
- pycontrails/_version.py +34 -0
- pycontrails/core/__init__.py +30 -0
- pycontrails/core/aircraft_performance.py +679 -0
- pycontrails/core/airports.py +228 -0
- pycontrails/core/cache.py +889 -0
- pycontrails/core/coordinates.py +174 -0
- pycontrails/core/fleet.py +483 -0
- pycontrails/core/flight.py +2185 -0
- pycontrails/core/flightplan.py +228 -0
- pycontrails/core/fuel.py +140 -0
- pycontrails/core/interpolation.py +702 -0
- pycontrails/core/met.py +2931 -0
- pycontrails/core/met_var.py +387 -0
- pycontrails/core/models.py +1321 -0
- pycontrails/core/polygon.py +549 -0
- pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
- pycontrails/core/vector.py +2249 -0
- pycontrails/datalib/__init__.py +12 -0
- pycontrails/datalib/_met_utils/metsource.py +746 -0
- pycontrails/datalib/ecmwf/__init__.py +73 -0
- pycontrails/datalib/ecmwf/arco_era5.py +345 -0
- pycontrails/datalib/ecmwf/common.py +114 -0
- pycontrails/datalib/ecmwf/era5.py +554 -0
- pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
- pycontrails/datalib/ecmwf/hres.py +804 -0
- pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
- pycontrails/datalib/ecmwf/ifs.py +287 -0
- pycontrails/datalib/ecmwf/model_levels.py +435 -0
- pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
- pycontrails/datalib/ecmwf/variables.py +268 -0
- pycontrails/datalib/geo_utils.py +261 -0
- pycontrails/datalib/gfs/__init__.py +28 -0
- pycontrails/datalib/gfs/gfs.py +656 -0
- pycontrails/datalib/gfs/variables.py +104 -0
- pycontrails/datalib/goes.py +757 -0
- pycontrails/datalib/himawari/__init__.py +27 -0
- pycontrails/datalib/himawari/header_struct.py +266 -0
- pycontrails/datalib/himawari/himawari.py +667 -0
- pycontrails/datalib/landsat.py +589 -0
- pycontrails/datalib/leo_utils/__init__.py +5 -0
- pycontrails/datalib/leo_utils/correction.py +266 -0
- pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
- pycontrails/datalib/leo_utils/search.py +250 -0
- pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
- pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
- pycontrails/datalib/leo_utils/vis.py +59 -0
- pycontrails/datalib/sentinel.py +650 -0
- pycontrails/datalib/spire/__init__.py +5 -0
- pycontrails/datalib/spire/exceptions.py +62 -0
- pycontrails/datalib/spire/spire.py +604 -0
- pycontrails/ext/bada.py +42 -0
- pycontrails/ext/cirium.py +14 -0
- pycontrails/ext/empirical_grid.py +140 -0
- pycontrails/ext/synthetic_flight.py +431 -0
- pycontrails/models/__init__.py +1 -0
- pycontrails/models/accf.py +425 -0
- pycontrails/models/apcemm/__init__.py +8 -0
- pycontrails/models/apcemm/apcemm.py +983 -0
- pycontrails/models/apcemm/inputs.py +226 -0
- pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
- pycontrails/models/apcemm/utils.py +437 -0
- pycontrails/models/cocip/__init__.py +29 -0
- pycontrails/models/cocip/cocip.py +2742 -0
- pycontrails/models/cocip/cocip_params.py +305 -0
- pycontrails/models/cocip/cocip_uncertainty.py +291 -0
- pycontrails/models/cocip/contrail_properties.py +1530 -0
- pycontrails/models/cocip/output_formats.py +2270 -0
- pycontrails/models/cocip/radiative_forcing.py +1260 -0
- pycontrails/models/cocip/radiative_heating.py +520 -0
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
- pycontrails/models/cocip/wake_vortex.py +396 -0
- pycontrails/models/cocip/wind_shear.py +120 -0
- pycontrails/models/cocipgrid/__init__.py +9 -0
- pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
- pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
- pycontrails/models/dry_advection.py +602 -0
- pycontrails/models/emissions/__init__.py +21 -0
- pycontrails/models/emissions/black_carbon.py +599 -0
- pycontrails/models/emissions/emissions.py +1353 -0
- pycontrails/models/emissions/ffm2.py +336 -0
- pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
- pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
- pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
- pycontrails/models/extended_k15.py +1327 -0
- pycontrails/models/humidity_scaling/__init__.py +37 -0
- pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
- pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
- pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
- pycontrails/models/issr.py +210 -0
- pycontrails/models/pcc.py +326 -0
- pycontrails/models/pcr.py +154 -0
- pycontrails/models/ps_model/__init__.py +18 -0
- pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
- pycontrails/models/ps_model/ps_grid.py +701 -0
- pycontrails/models/ps_model/ps_model.py +1000 -0
- pycontrails/models/ps_model/ps_operational_limits.py +525 -0
- pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
- pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
- pycontrails/models/sac.py +442 -0
- pycontrails/models/tau_cirrus.py +183 -0
- pycontrails/physics/__init__.py +1 -0
- pycontrails/physics/constants.py +117 -0
- pycontrails/physics/geo.py +1138 -0
- pycontrails/physics/jet.py +968 -0
- pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
- pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
- pycontrails/physics/thermo.py +551 -0
- pycontrails/physics/units.py +472 -0
- pycontrails/py.typed +0 -0
- pycontrails/utils/__init__.py +1 -0
- pycontrails/utils/dependencies.py +66 -0
- pycontrails/utils/iteration.py +13 -0
- pycontrails/utils/json.py +187 -0
- pycontrails/utils/temp.py +50 -0
- pycontrails/utils/types.py +163 -0
- pycontrails-0.58.0.dist-info/METADATA +180 -0
- pycontrails-0.58.0.dist-info/RECORD +122 -0
- pycontrails-0.58.0.dist-info/WHEEL +6 -0
- pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
- pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
- pycontrails-0.58.0.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""ECWMF IFS forecast data access."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import pathlib
|
|
7
|
+
import sys
|
|
8
|
+
import warnings
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
if sys.version_info >= (3, 12):
|
|
13
|
+
from typing import override
|
|
14
|
+
else:
|
|
15
|
+
from typing_extensions import override
|
|
16
|
+
|
|
17
|
+
LOG = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import pandas as pd
|
|
21
|
+
import xarray as xr
|
|
22
|
+
|
|
23
|
+
from pycontrails.core import met
|
|
24
|
+
from pycontrails.datalib._met_utils import metsource
|
|
25
|
+
from pycontrails.datalib.ecmwf.variables import ECMWF_VARIABLES
|
|
26
|
+
from pycontrails.physics import constants
|
|
27
|
+
from pycontrails.utils.types import DatetimeLike
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class IFS(metsource.MetDataSource):
|
|
31
|
+
"""
|
|
32
|
+
ECMWF Integrated Forecasting System (IFS) data source.
|
|
33
|
+
|
|
34
|
+
.. warning::
|
|
35
|
+
|
|
36
|
+
This data source is not fully implemented.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
time : metsource.TimeInput | None
|
|
41
|
+
The time range for data retrieval, either a single datetime or (start, end) datetime range.
|
|
42
|
+
Input must be a single datetime-like or tuple of datetime-like
|
|
43
|
+
(datetime, :class:`pandas.Timestamp`, :class:`numpy.datetime64`)
|
|
44
|
+
specifying the (start, end) of the date range, inclusive.
|
|
45
|
+
If None, all time coordinates will be loaded.
|
|
46
|
+
variables : metsource.VariableInput
|
|
47
|
+
Variable name (i.e. "air_temperature", ["air_temperature, relative_humidity"])
|
|
48
|
+
See :attr:`pressure_level_variables` for the list of available variables.
|
|
49
|
+
pressure_levels : metsource.PressureLevelInput, optional
|
|
50
|
+
Pressure level bounds for data (min, max), in hPa (mbar)
|
|
51
|
+
Set to -1 for to download surface level parameters.
|
|
52
|
+
Defaults to -1.
|
|
53
|
+
paths : str | list[str] | pathlib.Path | list[pathlib.Path] | None, optional
|
|
54
|
+
UNSUPPORTED FOR IFS
|
|
55
|
+
forecast_path: str | pathlib.Path | None, optional
|
|
56
|
+
Path to local forecast files.
|
|
57
|
+
Defaults to None
|
|
58
|
+
forecast_date: DatetimeLike, optional
|
|
59
|
+
Forecast date to load specific netcdf files.
|
|
60
|
+
Defaults to None
|
|
61
|
+
|
|
62
|
+
Notes
|
|
63
|
+
-----
|
|
64
|
+
This takes an average pressure of the model level to create
|
|
65
|
+
pressure level dimensions.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
__slots__ = ("forecast_date", "forecast_path")
|
|
69
|
+
|
|
70
|
+
#: Root path of IFS data
|
|
71
|
+
forecast_path: pathlib.Path
|
|
72
|
+
|
|
73
|
+
#: Forecast datetime of IFS forecast
|
|
74
|
+
forecast_date: pd.Timestamp
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
time: metsource.TimeInput | None,
|
|
79
|
+
variables: metsource.VariableInput,
|
|
80
|
+
pressure_levels: metsource.PressureLevelInput = -1,
|
|
81
|
+
paths: str | list[str] | pathlib.Path | list[pathlib.Path] | None = None,
|
|
82
|
+
grid: float | None = None,
|
|
83
|
+
forecast_path: str | pathlib.Path | None = None,
|
|
84
|
+
forecast_date: DatetimeLike | None = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
self.paths = paths # TODO: this is currently unused
|
|
87
|
+
self.grid = grid # TODO: this is currently unused
|
|
88
|
+
|
|
89
|
+
# path to forecast files
|
|
90
|
+
if forecast_path is None:
|
|
91
|
+
raise ValueError("Forecast path input is required for IFS")
|
|
92
|
+
self.forecast_path = pathlib.Path(forecast_path)
|
|
93
|
+
|
|
94
|
+
# TODO: automatically select a forecast_date from input time range?
|
|
95
|
+
self.forecast_date = pd.to_datetime(forecast_date).to_pydatetime()
|
|
96
|
+
|
|
97
|
+
# parse inputs
|
|
98
|
+
self.timesteps = metsource.parse_timesteps(time, freq="3h")
|
|
99
|
+
self.pressure_levels = metsource.parse_pressure_levels(pressure_levels, None)
|
|
100
|
+
self.variables = metsource.parse_variables(variables, self.supported_variables)
|
|
101
|
+
|
|
102
|
+
def __repr__(self) -> str:
|
|
103
|
+
base = super().__repr__()
|
|
104
|
+
return f"{base}\n\tForecast date: {self.forecast_date}"
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def supported_variables(self) -> list[met.MetVariable]:
|
|
108
|
+
"""IFS parameters available.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
list[MetVariable] | None
|
|
113
|
+
List of MetVariable available in datasource
|
|
114
|
+
"""
|
|
115
|
+
return ECMWF_VARIABLES
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def supported_pressure_levels(self) -> None:
|
|
119
|
+
"""IFS does not provide constant pressure levels and instead uses model levels.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
list[int]
|
|
124
|
+
"""
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
@override
|
|
128
|
+
def open_metdataset(
|
|
129
|
+
self,
|
|
130
|
+
dataset: xr.Dataset | None = None,
|
|
131
|
+
xr_kwargs: dict[str, Any] | None = None,
|
|
132
|
+
**kwargs: Any,
|
|
133
|
+
) -> met.MetDataset:
|
|
134
|
+
xr_kwargs = xr_kwargs or {}
|
|
135
|
+
|
|
136
|
+
# short-circuit dataset or file paths if provided
|
|
137
|
+
if self.paths is not None or dataset is not None:
|
|
138
|
+
raise NotImplementedError("IFS input paths or input dataset is not supported")
|
|
139
|
+
|
|
140
|
+
# load / merge datasets
|
|
141
|
+
ds = self._open_ifs_dataset(**xr_kwargs)
|
|
142
|
+
|
|
143
|
+
# drop ancillary vars
|
|
144
|
+
ds = ds.drop_vars(names=["hyai", "hybi"])
|
|
145
|
+
|
|
146
|
+
# downselect dataset if only a subset of times, pressure levels, or variables are requested
|
|
147
|
+
if self.timesteps:
|
|
148
|
+
ds = ds.sel(time=self.timesteps)
|
|
149
|
+
else:
|
|
150
|
+
# set timesteps from dataset "time" coordinates
|
|
151
|
+
# np.datetime64 doesn't covert to list[datetime] unless its unit is us
|
|
152
|
+
self.timesteps = ds["time"].values.astype("datetime64[us]").tolist()
|
|
153
|
+
|
|
154
|
+
# downselect hyam/hybm coefficients by the "lev" coordinate
|
|
155
|
+
# (this is a 1-indexed verison of nhym)
|
|
156
|
+
ds["hyam"] = ds["hyam"][dict(nhym=(ds["lev"] - 1).astype(int))]
|
|
157
|
+
ds["hybm"] = ds["hybm"][dict(nhym=(ds["lev"] - 1).astype(int))]
|
|
158
|
+
|
|
159
|
+
# calculate air_pressure (Pa) by hybrid sigma pressure
|
|
160
|
+
ds["air_pressure"] = ds["hyam"] + (ds["hybm"] * ds["surface_pressure"])
|
|
161
|
+
ds["air_pressure"].attrs["units"] = "Pa"
|
|
162
|
+
ds["air_pressure"].attrs["long_name"] = "Air pressure"
|
|
163
|
+
|
|
164
|
+
# calculate virtual temperature (t_virtual)
|
|
165
|
+
# the temperature at which dry air would have the same density
|
|
166
|
+
# as the moist air at a given pressure
|
|
167
|
+
ds["t_virtual"] = ds["t"] * (1 + ds["q"] * ((constants.R_v / constants.R_d) - 1))
|
|
168
|
+
ds["t_virtual"].attrs["units"] = "K"
|
|
169
|
+
ds["t_virtual"].attrs["long_name"] = "Virtual Temperature"
|
|
170
|
+
|
|
171
|
+
# calculate geopotential
|
|
172
|
+
if "z" in self.variable_shortnames:
|
|
173
|
+
ds["z"] = self._calc_geopotential(ds)
|
|
174
|
+
|
|
175
|
+
# take the mean of the air pressure to create quasi-gridded level coordinate
|
|
176
|
+
ds = ds.assign_coords(
|
|
177
|
+
{"level": ("lev", (ds["air_pressure"].mean(dim=["time", "lat", "lon"]) / 100).values)}
|
|
178
|
+
)
|
|
179
|
+
ds = ds.swap_dims({"lev": "level"})
|
|
180
|
+
ds = ds.drop_vars(names=["lev"])
|
|
181
|
+
|
|
182
|
+
# rename dimensions
|
|
183
|
+
ds = ds.rename({"lat": "latitude", "lon": "longitude"})
|
|
184
|
+
|
|
185
|
+
# downselect variables
|
|
186
|
+
ds = ds[self.variable_shortnames]
|
|
187
|
+
|
|
188
|
+
# TODO: fix this correctly
|
|
189
|
+
if "level" not in ds.dims:
|
|
190
|
+
ds = ds.expand_dims({"level": [-1]})
|
|
191
|
+
|
|
192
|
+
# harmonize variable names
|
|
193
|
+
ds = met.standardize_variables(ds, self.variables)
|
|
194
|
+
|
|
195
|
+
self.set_metadata(ds)
|
|
196
|
+
return met.MetDataset(ds, **kwargs)
|
|
197
|
+
|
|
198
|
+
@override
|
|
199
|
+
def set_metadata(self, ds: xr.Dataset | met.MetDataset) -> None:
|
|
200
|
+
ds.attrs.update(
|
|
201
|
+
provider="ECMWF",
|
|
202
|
+
dataset="IFS",
|
|
203
|
+
product="forecast",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
@override
|
|
207
|
+
def download_dataset(self, times: list[datetime]) -> None:
|
|
208
|
+
raise NotImplementedError("IFS download is not supported")
|
|
209
|
+
|
|
210
|
+
@override
|
|
211
|
+
def cache_dataset(self, dataset: xr.Dataset) -> None:
|
|
212
|
+
raise NotImplementedError("IFS dataset caching not supported")
|
|
213
|
+
|
|
214
|
+
@override
|
|
215
|
+
def create_cachepath(self, t: datetime) -> str:
|
|
216
|
+
raise NotImplementedError("IFS download is not supported")
|
|
217
|
+
|
|
218
|
+
def _open_ifs_dataset(self, **xr_kwargs: Any) -> xr.Dataset:
|
|
219
|
+
# get the path to each IFS file for each forecast date
|
|
220
|
+
date_str = self.forecast_date.strftime("%Y%m%d")
|
|
221
|
+
path_full = f"{self.forecast_path}/FC_{date_str}_00_144.nc"
|
|
222
|
+
path_fl = f"{self.forecast_path}/FC_{date_str}_00_144_fl.nc"
|
|
223
|
+
path_surface = f"{self.forecast_path}/FC_{date_str}_00_144_sur.nc"
|
|
224
|
+
path_rad = f"{self.forecast_path}/FC_{date_str}_00_144_rad.nc"
|
|
225
|
+
|
|
226
|
+
# load each dataset
|
|
227
|
+
LOG.debug(f"Loading IFS forecast date {date_str}")
|
|
228
|
+
|
|
229
|
+
# load each dataset
|
|
230
|
+
xr_kwargs.setdefault("chunks", metsource.DEFAULT_CHUNKS)
|
|
231
|
+
xr_kwargs.setdefault("engine", metsource.NETCDF_ENGINE)
|
|
232
|
+
ds_full = xr.open_dataset(path_full, **xr_kwargs)
|
|
233
|
+
ds_fl = xr.open_dataset(path_fl, **xr_kwargs)
|
|
234
|
+
ds_surface = xr.open_dataset(path_surface, **xr_kwargs)
|
|
235
|
+
ds_rad = xr.open_dataset(path_rad, **xr_kwargs)
|
|
236
|
+
|
|
237
|
+
# calculate surface pressure from ln(surface pressure) var, squeeze out "lev" dim
|
|
238
|
+
ds_full["surface_pressure"] = np.exp(ds_full["lnsp"]).squeeze()
|
|
239
|
+
ds_full["surface_pressure"].attrs["units"] = "Pa"
|
|
240
|
+
ds_full["surface_pressure"].attrs["long_name"] = "Surface air pressure"
|
|
241
|
+
|
|
242
|
+
# swap dim names for consistency
|
|
243
|
+
ds_full = ds_full.drop_vars(names=["lnsp", "lev"])
|
|
244
|
+
ds_full = ds_full.rename({"lev_2": "lev"})
|
|
245
|
+
|
|
246
|
+
# drop vars so all datasets can merge
|
|
247
|
+
ds_fl = ds_fl.drop_vars(names=["hyai", "hybi", "hyam", "hybm"])
|
|
248
|
+
|
|
249
|
+
# merge all datasets using the "ds_fl" dimensions as the join keys
|
|
250
|
+
return xr.merge([ds_fl, ds_full, ds_surface, ds_rad], join="left") # order matters!
|
|
251
|
+
|
|
252
|
+
def _calc_geopotential(self, ds: xr.Dataset) -> xr.DataArray:
|
|
253
|
+
warnings.warn(
|
|
254
|
+
"The geopotential calculation implementation may assume the underlying grid "
|
|
255
|
+
"starts at ground level. This may not be the case for IFS data. It may be "
|
|
256
|
+
"better to use geometric height (altitude) instead of geopotential for downstream "
|
|
257
|
+
"applications (tau cirrus, etc.).",
|
|
258
|
+
UserWarning,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# TODO: this could be done via a mapping on the "lev" dimension
|
|
262
|
+
# groupby("lev")
|
|
263
|
+
|
|
264
|
+
z_level = ds["z"].copy()
|
|
265
|
+
p_level = ds["surface_pressure"].copy()
|
|
266
|
+
geopotential = xr.zeros_like(ds["t"])
|
|
267
|
+
|
|
268
|
+
geopotential.attrs["standard_name"] = "geopotential"
|
|
269
|
+
geopotential.attrs["units"] = "m**2 s**-2"
|
|
270
|
+
geopotential.attrs["long_name"] = "Geopotential"
|
|
271
|
+
|
|
272
|
+
# iterate through level layers from the bottom up
|
|
273
|
+
for k in ds["lev"][::-1]:
|
|
274
|
+
d_log_p = np.log(p_level / ds["air_pressure"].loc[dict(lev=k)])
|
|
275
|
+
|
|
276
|
+
denom = p_level - ds["air_pressure"].loc[dict(lev=k)]
|
|
277
|
+
alpha = 1 - d_log_p * ds["air_pressure"].loc[dict(lev=k)] / denom
|
|
278
|
+
|
|
279
|
+
geopotential.loc[dict(lev=k)] = (
|
|
280
|
+
z_level + ds["t_virtual"].loc[dict(lev=k)] * alpha * constants.R_d
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Update values for next loop
|
|
284
|
+
z_level = geopotential.loc[dict(lev=k)].copy()
|
|
285
|
+
p_level = ds["air_pressure"].loc[dict(lev=k)].copy()
|
|
286
|
+
|
|
287
|
+
return geopotential
|