pycontrails 0.47.3__cp39-cp39-win_amd64.whl → 0.48.1__cp39-cp39-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.

@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import warnings
5
6
  from dataclasses import dataclass
6
7
  from typing import Any, overload
7
8
 
@@ -9,7 +10,7 @@ import xarray as xr
9
10
 
10
11
  import pycontrails
11
12
  from pycontrails.core.flight import Flight
12
- from pycontrails.core.met import MetDataArray, MetDataset, standardize_variables
13
+ from pycontrails.core.met import MetDataset, standardize_variables
13
14
  from pycontrails.core.met_var import (
14
15
  AirTemperature,
15
16
  EastwardWind,
@@ -23,28 +24,46 @@ from pycontrails.core.vector import GeoVectorDataset
23
24
  from pycontrails.datalib import ecmwf
24
25
  from pycontrails.utils import dependencies
25
26
 
26
- WideBodyJets = {
27
- "A332",
28
- "A333",
29
- "A338",
30
- "A339",
31
- "A342",
32
- "A343",
33
- "A345",
34
- "A356",
35
- "A359",
36
- "A388",
37
- "B762",
38
- "B763",
39
- "B764",
40
- "B772",
41
- "B773",
42
- "B778",
43
- "B779",
44
- "B788",
45
- "B789",
46
- }
47
- RegionalJets = {"CRJ1", "CRJ2", "CRJ7", "CRJ8", "CRJ9", "CRJX", "E135", "E145", "E170", "E190"}
27
+
28
+ def wide_body_jets() -> set[str]:
29
+ """Return a set of wide body jets."""
30
+ return {
31
+ "A332",
32
+ "A333",
33
+ "A338",
34
+ "A339",
35
+ "A342",
36
+ "A343",
37
+ "A345",
38
+ "A356",
39
+ "A359",
40
+ "A388",
41
+ "B762",
42
+ "B763",
43
+ "B764",
44
+ "B772",
45
+ "B773",
46
+ "B778",
47
+ "B779",
48
+ "B788",
49
+ "B789",
50
+ }
51
+
52
+
53
+ def regional_jets() -> set[str]:
54
+ """Return a set of regional jets."""
55
+ return {
56
+ "CRJ1",
57
+ "CRJ2",
58
+ "CRJ7",
59
+ "CRJ8",
60
+ "CRJ9",
61
+ "CRJX",
62
+ "E135",
63
+ "E145",
64
+ "E170",
65
+ "E190",
66
+ }
48
67
 
49
68
 
50
69
  @dataclass
@@ -102,13 +121,16 @@ class ACCF(Model):
102
121
  """Compute Algorithmic Climate Change Functions (ACCF).
103
122
 
104
123
  This class is a wrapper over the DLR / UMadrid library
105
- `climaccf <https://github.com/dlr-pa/climaccf>`__,
106
- `DOI: 10.5281/zenodo.6977272 <https://doi.org/10.5281/zenodo.6977272>`__
124
+ `climaccf <https://github.com/dlr-pa/climaccf>`_,
125
+ `DOI: 10.5281/zenodo.6977272 <https://doi.org/10.5281/zenodo.6977272>`_
107
126
 
108
127
  Parameters
109
128
  ----------
110
129
  met : MetDataset
111
130
  Dataset containing "air_temperature" and "specific_humidity" variables
131
+ surface : MetDataset, optional
132
+ Dataset containing "surface_solar_downward_radiation" and
133
+ "top_net_thermal_radiation" variables
112
134
 
113
135
  References
114
136
  ----------
@@ -132,47 +154,32 @@ class ACCF(Model):
132
154
  sur_variables = (ecmwf.SurfaceSolarDownwardRadiation, ecmwf.TopNetThermalRadiation)
133
155
  default_params = ACCFParams
134
156
 
135
- short_vars = [v.short_name for v in met_variables + sur_variables]
157
+ short_vars = {v.short_name for v in (*met_variables, *sur_variables)}
136
158
 
137
- ds_met: xr.Dataset | None
138
- ds_sur: xr.Dataset | None
159
+ # This variable won't get used since we are not writing the output
160
+ # anywhere, but the library will complain if it's not defined
161
+ path_lib = "./"
139
162
 
140
163
  def __init__(
141
164
  self,
142
165
  met: MetDataset,
143
166
  surface: MetDataset | None = None,
144
- params: dict[str, Any] = {},
167
+ params: dict[str, Any] | None = None,
145
168
  **params_kwargs: Any,
146
169
  ) -> None:
147
170
  # Normalize ECMWF variables
148
171
  met = standardize_variables(met, self.met_variables)
149
172
 
150
- if surface:
151
- surface = standardize_variables(surface, self.sur_variables)
173
+ # Ignore humidity scaling warning
174
+ with warnings.catch_warnings():
175
+ warnings.filterwarnings("ignore", module="pycontrails.core.models")
176
+ super().__init__(met, params=params, **params_kwargs)
152
177
 
153
- # Surpress warning about humdity scaling because that should be eet
154
- # using ACCF config variables for this model
155
- try:
156
- del met.attrs["history"]
157
- except KeyError:
158
- pass
159
-
160
- source = met.attrs["met_source"]
161
- met.attrs["met_source"] = "not_ecmwf"
162
- super().__init__(met, params=params, **params_kwargs)
163
- if self.met:
164
- self.met.attrs["met_source"] = source
165
-
166
- self._update_accf_config()
167
178
  if surface:
168
- self.surface = surface.copy()
169
-
170
- # This variable won't get used since we are not writing the output
171
- # anywhere, but the library will complain if it's not defined
172
- self.path_lib = "./"
173
-
174
- self.ds_met = None
175
- self.ds_sur = None
179
+ surface = surface.copy()
180
+ surface = standardize_variables(surface, self.sur_variables)
181
+ surface.data = _rad_instantaneous_to_accumulated(surface.data)
182
+ self.surface = surface
176
183
 
177
184
  @overload
178
185
  def eval(self, source: Flight, **params: Any) -> Flight: ...
@@ -181,11 +188,11 @@ class ACCF(Model):
181
188
  def eval(self, source: GeoVectorDataset, **params: Any) -> GeoVectorDataset: ...
182
189
 
183
190
  @overload
184
- def eval(self, source: MetDataset | None = ..., **params: Any) -> MetDataArray: ...
191
+ def eval(self, source: MetDataset | None = ..., **params: Any) -> MetDataset: ...
185
192
 
186
193
  def eval(
187
194
  self, source: GeoVectorDataset | Flight | MetDataset | None = None, **params: Any
188
- ) -> GeoVectorDataset | Flight | MetDataArray:
195
+ ) -> GeoVectorDataset | Flight | MetDataset:
189
196
  """Evaluate accfs along flight trajectory or on meteorology grid.
190
197
 
191
198
  Parameters
@@ -225,32 +232,38 @@ class ACCF(Model):
225
232
  self.surface = self.source.downselect_met(self.surface)
226
233
 
227
234
  if isinstance(self.source, MetDataset):
228
- if self.source["longitude"].size > 1:
229
- hres = abs(self.source["longitude"].data[1] - self.source["longitude"].data[0])
230
- self.params["horizontal_resolution"] = float(hres)
231
- elif self.source["latitude"].size > 1:
232
- hres = abs(self.source["latitude"].data[1] - self.source["latitude"].data[0])
235
+ # Overwrite horizontal resolution to match met
236
+ longitude = self.source.data["longitude"].values
237
+ if longitude.size > 1:
238
+ hres = abs(longitude[1] - longitude[0])
233
239
  self.params["horizontal_resolution"] = float(hres)
234
240
 
241
+ else:
242
+ latitude = self.source.data["latitude"].values
243
+ if latitude.size > 1:
244
+ hres = abs(latitude[1] - latitude[0])
245
+ self.params["horizontal_resolution"] = float(hres)
246
+
247
+ p_settings = _get_accf_config(self.params)
248
+
235
249
  self.set_source_met()
236
- self._update_accf_config()
237
- self._generate_weather_store()
250
+ self._generate_weather_store(p_settings)
238
251
 
239
252
  # check aircraft type and set in config if needed
240
253
  if self.params["nox_ei"] != "TTV":
241
254
  if isinstance(self.source, Flight):
242
255
  ac = self.source.attrs["aircraft_type"]
243
- if ac in WideBodyJets:
244
- self.p_settings["ac_type"] = "wide-body"
245
- elif ac in RegionalJets:
246
- self.p_settings["ac_type"] = "regional"
256
+ if ac in wide_body_jets():
257
+ p_settings["ac_type"] = "wide-body"
258
+ elif ac in regional_jets():
259
+ p_settings["ac_type"] = "regional"
247
260
  else:
248
- self.p_settings["ac_type"] = "single-aisle"
261
+ p_settings["ac_type"] = "single-aisle"
249
262
  else:
250
- self.p_settings["ac_type"] = "wide-body"
263
+ p_settings["ac_type"] = "wide-body"
251
264
 
252
265
  clim_imp = GeTaCCFs(self)
253
- clim_imp.get_accfs(**self.p_settings)
266
+ clim_imp.get_accfs(**p_settings)
254
267
  aCCFs, _ = clim_imp.get_xarray()
255
268
 
256
269
  # assign ACCF outputs to source
@@ -259,66 +272,53 @@ class ACCF(Model):
259
272
  # skip met variables
260
273
  if key in self.short_vars:
261
274
  continue
262
- if not isinstance(key, str):
263
- continue
264
275
 
276
+ assert isinstance(key, str)
265
277
  if isinstance(self.source, GeoVectorDataset):
266
278
  self.source[key] = self.source.intersect_met(maCCFs[key])
267
279
  else:
268
- self.source[key] = (maCCFs.dim_order, arr.data)
269
-
270
- # Tag output with additional attrs when source is MetDataset
271
- if isinstance(self.source, MetDataset):
272
- attrs: dict[str, Any] = {
273
- "description": self.long_name,
274
- "pycontrails_version": pycontrails.__version__,
275
- }
276
- if self.met is not None:
277
- attrs["met_source"] = self.met.attrs.get("met_source", "unknown")
280
+ self.source[key] = arr
278
281
 
279
- self.source[key].data.attrs.update(attrs)
282
+ self.transfer_met_source_attrs()
283
+ self.source.attrs["pycontrails_version"] = pycontrails.__version__
280
284
 
281
- return self.source # type: ignore[return-value]
285
+ return self.source
282
286
 
283
- def _generate_weather_store(self) -> None:
287
+ def _generate_weather_store(self, p_settings: dict[str, Any]) -> None:
284
288
  from climaccf.weather_store import WeatherStore
285
289
 
286
290
  # The library does not call the coordinates by name, it just slices the
287
291
  # underlying data array, so we need to put them in the expected order.
288
292
  # It also needs variables to have the ECMWF short name
289
293
  if isinstance(self.met, MetDataset):
290
- mt = self.met.data.transpose("time", "level", "latitude", "longitude")
291
- if mt is None or isinstance(mt, xr.Dataset):
292
- self.ds_met = mt
293
-
294
- if self.ds_met:
295
- for var in self.ds_met.data_vars:
296
- matching_variable = [v for v in self.met_variables if var == v.standard_name]
297
- if matching_variable:
298
- self.ds_met = self.ds_met.rename({var: matching_variable[0].short_name})
294
+ ds_met = self.met.data.transpose("time", "level", "latitude", "longitude")
295
+ name_dict = {v.standard_name: v.short_name for v in self.met_variables}
296
+ ds_met = ds_met.rename(name_dict)
297
+ else:
298
+ ds_met = None
299
299
 
300
300
  if hasattr(self, "surface"):
301
- self.ds_sur = self.surface.data.squeeze().transpose("time", "latitude", "longitude")
302
- for var in self.ds_sur.data_vars:
303
- matching_variable = [v for v in self.sur_variables if var == v.standard_name]
304
- if matching_variable:
305
- self.ds_sur = self.ds_sur.rename({var: matching_variable[0].short_name})
301
+ ds_sur = self.surface.data.squeeze().transpose("time", "latitude", "longitude")
302
+ name_dict = {v.standard_name: v.short_name for v in self.sur_variables}
303
+ ds_sur = ds_sur.rename(name_dict)
306
304
  else:
307
- self.ds_sur = None
305
+ ds_sur = None
308
306
 
309
307
  ws = WeatherStore(
310
- self.ds_met,
311
- self.ds_sur,
312
- ll_resolution=self.p_settings["horizontal_resolution"],
313
- forecast_step=self.p_settings["forecast_step"],
308
+ ds_met,
309
+ ds_sur,
310
+ ll_resolution=p_settings["horizontal_resolution"],
311
+ forecast_step=p_settings["forecast_step"],
314
312
  )
315
- if self.p_settings["lat_bound"] and self.p_settings["lon_bound"]:
313
+
314
+ if p_settings["lat_bound"] and p_settings["lon_bound"]:
316
315
  ws.reduce_domain(
317
316
  {
318
- "latitude": self.p_settings["lat_bound"],
319
- "longitude": self.p_settings["lon_bound"],
317
+ "latitude": p_settings["lat_bound"],
318
+ "longitude": p_settings["lon_bound"],
320
319
  }
321
320
  )
321
+
322
322
  self.ds = ws.get_xarray()
323
323
  self.variable_names = ws.variable_names
324
324
  self.pre_variable_names = ws.pre_variable_names
@@ -329,51 +329,78 @@ class ACCF(Model):
329
329
  self.axes = ws.axes
330
330
  self.var_xr = ws.var_xr
331
331
 
332
- def _update_accf_config(self) -> None:
333
- # a good portion of these will get ignored since we are not producing an
334
- # output file, but the library will complain if they aren't defined
335
- self.p_settings = {
336
- "lat_bound": self.params["lat_bound"],
337
- "lon_bound": self.params["lon_bound"],
338
- "time_bound": None,
339
- "horizontal_resolution": self.params["horizontal_resolution"],
340
- "forecast_step": self.params["forecast_step"],
341
- "NOx_aCCF": True,
342
- "NOx&inverse_EIs": self.params["nox_ei"],
343
- "output_format": "netCDF",
344
- "mean": False,
345
- "std": False,
346
- "merged": self.params["merged"],
347
- "aCCF-V": self.params["accf_v"],
348
- "efficacy": self.params["efficacy"],
349
- "efficacy-option": self.params["efficacy_option"],
350
- "emission_scenario": self.params["emission_scenario"],
351
- "climate_indicator": self.params["climate_indicator"],
352
- "TimeHorizon": self.params["time_horizon"],
353
- "ac_type": "wide-body",
354
- "sep_ri_rw": self.params["sep_ri_rw"],
355
- "PMO": self.params["PMO"],
356
- "aCCF-scalingF": {
357
- "CH4": self.params["ch4_scaling"],
358
- "CO2": self.params["co2_scaling"],
359
- "Cont.": self.params["cont_scaling"],
360
- "H2O": self.params["h2o_scaling"],
361
- "O3": self.params["o3_scaling"],
362
- },
363
- "PCFA": self.params["pfca"],
364
- "PCFA-ISSR": {
365
- "rhi_threshold": self.params["issr_rhi_threshold"],
366
- "temp_threshold": self.params["issr_temp_threshold"],
367
- },
368
- "PCFA-SAC": {
369
- "EI_H2O": self.params["sac_ei_h2o"],
370
- "Q": self.params["sac_q"],
371
- "eta": self.params["sac_eta"],
372
- },
373
- "Chotspots": False,
374
- "hotspots_binary": True,
375
- "color": "Reds",
376
- "geojson": False,
377
- "save_path": "./",
378
- "save_format": "netCDF",
379
- }
332
+
333
+ def _get_accf_config(params: dict[str, Any]) -> dict[str, Any]:
334
+ # a good portion of these will get ignored since we are not producing an
335
+ # output file, but the library will complain if they aren't defined
336
+ return {
337
+ "lat_bound": params["lat_bound"],
338
+ "lon_bound": params["lon_bound"],
339
+ "time_bound": None,
340
+ "horizontal_resolution": params["horizontal_resolution"],
341
+ "forecast_step": params["forecast_step"],
342
+ "NOx_aCCF": True,
343
+ "NOx&inverse_EIs": params["nox_ei"],
344
+ "output_format": "netCDF",
345
+ "mean": False,
346
+ "std": False,
347
+ "merged": params["merged"],
348
+ "aCCF-V": params["accf_v"],
349
+ "efficacy": params["efficacy"],
350
+ "efficacy-option": params["efficacy_option"],
351
+ "emission_scenario": params["emission_scenario"],
352
+ "climate_indicator": params["climate_indicator"],
353
+ "TimeHorizon": params["time_horizon"],
354
+ "ac_type": "wide-body",
355
+ "sep_ri_rw": params["sep_ri_rw"],
356
+ "PMO": params["PMO"],
357
+ "aCCF-scalingF": {
358
+ "CH4": params["ch4_scaling"],
359
+ "CO2": params["co2_scaling"],
360
+ "Cont.": params["cont_scaling"],
361
+ "H2O": params["h2o_scaling"],
362
+ "O3": params["o3_scaling"],
363
+ },
364
+ "PCFA": params["pfca"],
365
+ "PCFA-ISSR": {
366
+ "rhi_threshold": params["issr_rhi_threshold"],
367
+ "temp_threshold": params["issr_temp_threshold"],
368
+ },
369
+ "PCFA-SAC": {
370
+ "EI_H2O": params["sac_ei_h2o"],
371
+ "Q": params["sac_q"],
372
+ "eta": params["sac_eta"],
373
+ },
374
+ "Chotspots": False,
375
+ "hotspots_binary": True,
376
+ "color": "Reds",
377
+ "geojson": False,
378
+ "save_path": "./",
379
+ "save_format": "netCDF",
380
+ }
381
+
382
+
383
+ def _rad_instantaneous_to_accumulated(ds: xr.Dataset) -> xr.Dataset:
384
+ """Convert instantaneous radiation to accumulated radiation."""
385
+
386
+ for name, da in ds.items():
387
+ try:
388
+ unit = da.attrs["units"]
389
+ except KeyError as e:
390
+ msg = (
391
+ f"Radiation data contains '{name}' variable "
392
+ "but units are not specified. Provide units in the "
393
+ f"rad['{name}'].attrs passed into ACCF."
394
+ )
395
+ raise KeyError(msg) from e
396
+
397
+ if unit == "J m**-2":
398
+ continue
399
+ if unit != "W m**-2":
400
+ msg = f"Unexpected units '{unit}' for '{name}'. Expected 'J m**-2' or 'W m**-2'."
401
+ raise ValueError(msg)
402
+
403
+ # Convert from W m**-2 to J m**-2
404
+ ds[name] = da.assign_attrs(units="J m**-2") * 3600.0
405
+
406
+ return ds