pycontrails 0.41.0__cp39-cp39-macosx_11_0_arm64.whl → 0.42.2__cp39-cp39-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-39-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
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""ATC Flight Plan Parser."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def to_atc_plan(plan: dict[str, Any]) -> str:
|
|
8
|
+
"""Write dictionary from :func:`parse_atc_plan` as ATC flight plan string.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
plan: dict[str, Any]
|
|
13
|
+
Dictionary representation of ATC flight plan returned from :func:`parse_atc_plan`.
|
|
14
|
+
|
|
15
|
+
Returns
|
|
16
|
+
-------
|
|
17
|
+
str
|
|
18
|
+
ATC flight plan string conforming to ICAO Doc 4444-ATM/501
|
|
19
|
+
|
|
20
|
+
See Also
|
|
21
|
+
--------
|
|
22
|
+
:func:`parse_atc_plan`
|
|
23
|
+
"""
|
|
24
|
+
ret = f'(FPL-{plan["callsign"]}-{plan["flight_rules"]}'
|
|
25
|
+
ret += f'{plan["type_of_flight"]}\n'
|
|
26
|
+
ret += "-"
|
|
27
|
+
if "number_aircraft" in plan and plan["number_aircraft"] <= 10:
|
|
28
|
+
ret += plan["number_aircraft"]
|
|
29
|
+
ret += f'{plan["type_of_aircraft"]}/{plan["wake_category"]}-'
|
|
30
|
+
ret += f'{plan["equipment"]}/{plan["transponder"]}\n'
|
|
31
|
+
ret += f'-{plan["departure_icao"]}{plan["time"]}\n'
|
|
32
|
+
ret += f'-{plan["speed_type"]}{plan["speed"]}{plan["level_type"]}'
|
|
33
|
+
ret += f'{plan["level"]} {plan["route"]}\n'
|
|
34
|
+
if "destination_icao" in plan and "duration" in plan:
|
|
35
|
+
ret += f'-{plan["destination_icao"]}{plan["duration"]}'
|
|
36
|
+
if "alt_icao" in plan:
|
|
37
|
+
ret += f' {plan["alt_icao"]}'
|
|
38
|
+
if "second_alt_icao" in plan:
|
|
39
|
+
ret += f' {plan["second_alt_icao"]}'
|
|
40
|
+
ret += "\n"
|
|
41
|
+
ret += f'-{plan["other_info"]})\n'
|
|
42
|
+
if "supplementary_info" in plan:
|
|
43
|
+
ret += " ".join([f"{i[0]}/{i[1]}" for i in plan["supplementary_info"].items()])
|
|
44
|
+
|
|
45
|
+
if ret[-1] == "\n":
|
|
46
|
+
ret = ret[:-1]
|
|
47
|
+
|
|
48
|
+
return ret
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_atc_plan(atc_plan: str) -> dict[str, str]:
|
|
52
|
+
"""Parse an ATC flight plan string into a dictionary.
|
|
53
|
+
|
|
54
|
+
The route string is not converted to lat/lon in this process.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
atc_plan : str
|
|
59
|
+
An ATC flight plan string conforming to ICAO Doc 4444-ATM/501 (Appendix 2)
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
dict[str, str]
|
|
64
|
+
A dictionary consisting of parsed components of the ATC flight plan.
|
|
65
|
+
A full ATC plan will contain the keys:
|
|
66
|
+
|
|
67
|
+
- ``callsign``: ICAO flight callsign
|
|
68
|
+
- ``flight_rules``: Flight rules ("I", "V", "Y", "Z")
|
|
69
|
+
- ``type_of_flight``: Type of flight ("S", "N", "G", "M", "X")
|
|
70
|
+
- ``number_aircraft``: The number of aircraft, if more than one
|
|
71
|
+
- ``type_of_aircraft``: ICAO aircraft type
|
|
72
|
+
- ``wake_category``: Wake turbulence category
|
|
73
|
+
- ``equipment``: Radiocommunication, navigation and approach aid equipment and capabilities
|
|
74
|
+
- ``transponder``: Surveillance equipment and capabilities
|
|
75
|
+
- ``departure_icao``: ICAO departure airport
|
|
76
|
+
- ``time``: Estimated off-block (departure) time (UTC)
|
|
77
|
+
- ``speed_type``: Speed units ("K": km / hr, "N": knots)
|
|
78
|
+
- ``speed``: Cruise true airspeed in ``speed_type`` units
|
|
79
|
+
- ``level_type``: Level units ("F", "S", "A", "M")
|
|
80
|
+
- ``level``: Cruise level
|
|
81
|
+
- ``route``: Route string
|
|
82
|
+
- ``destination_icao``: ICAO destination airport
|
|
83
|
+
- ``duration``: The total estimated elapsed time for the flight plan
|
|
84
|
+
- ``alt_icao``: ICAO alternate destination airport
|
|
85
|
+
- ``second_alt_icao``: ICAO second alternate destination airport
|
|
86
|
+
- ``other_info``: Other information
|
|
87
|
+
- ``supplementary_info``: Supplementary information
|
|
88
|
+
|
|
89
|
+
References
|
|
90
|
+
----------
|
|
91
|
+
- https://applications.icao.int/tools/ATMiKIT/story_content/external_files/story_content/external_files/DOC%204444_PANS%20ATM_en.pdf
|
|
92
|
+
|
|
93
|
+
See Also
|
|
94
|
+
--------
|
|
95
|
+
:func:`to_atc_plan`
|
|
96
|
+
""" # noqa: E501
|
|
97
|
+
atc_plan = atc_plan.replace("\r", "")
|
|
98
|
+
atc_plan = atc_plan.replace("\n", "")
|
|
99
|
+
atc_plan = atc_plan.upper()
|
|
100
|
+
atc_plan = atc_plan.strip()
|
|
101
|
+
|
|
102
|
+
if len(atc_plan) == 0:
|
|
103
|
+
raise ValueError("Empty or invalid flight plan")
|
|
104
|
+
|
|
105
|
+
atc_plan = atc_plan.replace("(FPL", "")
|
|
106
|
+
atc_plan = atc_plan.replace(")", "")
|
|
107
|
+
atc_plan = atc_plan.replace("--", "-")
|
|
108
|
+
|
|
109
|
+
basic = atc_plan.split("-")
|
|
110
|
+
|
|
111
|
+
flightplan: dict[str, Any] = {}
|
|
112
|
+
|
|
113
|
+
# Callsign
|
|
114
|
+
if len(basic) > 1:
|
|
115
|
+
flightplan["callsign"] = basic[1]
|
|
116
|
+
|
|
117
|
+
# Flight Rules
|
|
118
|
+
if len(basic) > 2:
|
|
119
|
+
flightplan["flight_rules"] = basic[2][0]
|
|
120
|
+
flightplan["type_of_flight"] = basic[2][1]
|
|
121
|
+
|
|
122
|
+
# Aircraft
|
|
123
|
+
if len(basic) > 3:
|
|
124
|
+
aircraft = basic[3].split("/")
|
|
125
|
+
matches = re.match("(\d{1})(\S{3,4})", aircraft[0])
|
|
126
|
+
if matches:
|
|
127
|
+
groups = matches.groups()
|
|
128
|
+
else:
|
|
129
|
+
groups = ()
|
|
130
|
+
|
|
131
|
+
if matches and len(groups) > 2:
|
|
132
|
+
flightplan["number_aircraft"] = groups[1]
|
|
133
|
+
flightplan["type_of_aircraft"] = groups[2]
|
|
134
|
+
else:
|
|
135
|
+
flightplan["type_of_aircraft"] = aircraft[0]
|
|
136
|
+
|
|
137
|
+
if len(aircraft) > 1:
|
|
138
|
+
flightplan["wake_category"] = aircraft[1]
|
|
139
|
+
|
|
140
|
+
# Equipment
|
|
141
|
+
if len(basic) > 4:
|
|
142
|
+
equip = basic[4].split("/")
|
|
143
|
+
flightplan["equipment"] = equip[0]
|
|
144
|
+
if len(equip) > 1:
|
|
145
|
+
flightplan["transponder"] = equip[1]
|
|
146
|
+
|
|
147
|
+
# Dep. airport info
|
|
148
|
+
if len(basic) > 5:
|
|
149
|
+
matches = re.match("(\D*)(\d*)", basic[5])
|
|
150
|
+
if matches:
|
|
151
|
+
groups = matches.groups()
|
|
152
|
+
else:
|
|
153
|
+
groups = ()
|
|
154
|
+
|
|
155
|
+
if len(groups) > 0:
|
|
156
|
+
flightplan["departure_icao"] = groups[0]
|
|
157
|
+
if len(groups) > 1:
|
|
158
|
+
flightplan["time"] = groups[1]
|
|
159
|
+
|
|
160
|
+
# Speed and route info
|
|
161
|
+
if len(basic) > 6:
|
|
162
|
+
matches = re.match("(\D*)(\d*)(\D*)(\d*)", basic[6])
|
|
163
|
+
if matches:
|
|
164
|
+
groups = matches.groups()
|
|
165
|
+
else:
|
|
166
|
+
groups = ()
|
|
167
|
+
|
|
168
|
+
# match speed and level
|
|
169
|
+
if len(groups) > 0:
|
|
170
|
+
flightplan["speed_type"] = groups[0]
|
|
171
|
+
if len(groups) > 1:
|
|
172
|
+
flightplan["speed"] = groups[1]
|
|
173
|
+
if len(groups) > 2:
|
|
174
|
+
flightplan["level_type"] = groups[2]
|
|
175
|
+
if len(groups) > 3:
|
|
176
|
+
flightplan["level"] = groups[3]
|
|
177
|
+
|
|
178
|
+
flightplan["route"] = basic[6][len("".join(groups)) :].strip()
|
|
179
|
+
else:
|
|
180
|
+
flightplan["route"] = basic[6].strip()
|
|
181
|
+
|
|
182
|
+
# Dest. airport info
|
|
183
|
+
if len(basic) > 7:
|
|
184
|
+
matches = re.match("(\D{4})(\d{4})", basic[7])
|
|
185
|
+
if matches:
|
|
186
|
+
groups = matches.groups()
|
|
187
|
+
else:
|
|
188
|
+
groups = ()
|
|
189
|
+
|
|
190
|
+
if len(groups) > 0:
|
|
191
|
+
flightplan["destination_icao"] = groups[0]
|
|
192
|
+
if len(groups) > 1:
|
|
193
|
+
flightplan["duration"] = groups[1]
|
|
194
|
+
|
|
195
|
+
matches = re.match("(\D{4})(\d{4})(\s{1})(\D{4})", basic[7])
|
|
196
|
+
if matches:
|
|
197
|
+
groups = matches.groups()
|
|
198
|
+
else:
|
|
199
|
+
groups = ()
|
|
200
|
+
|
|
201
|
+
if len(groups) > 3:
|
|
202
|
+
flightplan["alt_icao"] = groups[3]
|
|
203
|
+
|
|
204
|
+
matches = re.match("(\D{4})(\d{4})(\s{1})(\D{4})(\s{1})(\D{4})", basic[7])
|
|
205
|
+
if matches:
|
|
206
|
+
groups = matches.groups()
|
|
207
|
+
else:
|
|
208
|
+
groups = ()
|
|
209
|
+
|
|
210
|
+
if len(groups) > 5:
|
|
211
|
+
flightplan["second_alt_icao"] = groups[5]
|
|
212
|
+
|
|
213
|
+
# Other info
|
|
214
|
+
if len(basic) > 8:
|
|
215
|
+
flightplan["other_info"] = basic[8]
|
|
216
|
+
|
|
217
|
+
# Supl. Info
|
|
218
|
+
if len(basic) > 9:
|
|
219
|
+
sup_match = re.findall("(\D{1}[\/]{1})", basic[9])
|
|
220
|
+
if len(sup_match) > 0:
|
|
221
|
+
suplInfo = {}
|
|
222
|
+
for i in range(len(sup_match) - 1):
|
|
223
|
+
this_key = sup_match[i]
|
|
224
|
+
this_idx = basic[9].find(this_key)
|
|
225
|
+
|
|
226
|
+
next_key = sup_match[i + 1]
|
|
227
|
+
next_idx = basic[9].find(next_key)
|
|
228
|
+
|
|
229
|
+
val = basic[9][this_idx + 2 : next_idx - 1]
|
|
230
|
+
suplInfo[this_key[0]] = val
|
|
231
|
+
|
|
232
|
+
last_key = sup_match[-1]
|
|
233
|
+
last_idx = basic[9].find(last_key)
|
|
234
|
+
suplInfo[last_key[0]] = basic[9][last_idx + 2 :]
|
|
235
|
+
|
|
236
|
+
flightplan["supplementary_info"] = suplInfo
|
|
237
|
+
|
|
238
|
+
return flightplan
|
|
@@ -74,7 +74,7 @@ class PycontrailsRegularGridInterpolator(scipy.interpolate.RegularGridInterpolat
|
|
|
74
74
|
self.bounds_error = bounds_error
|
|
75
75
|
self.fill_value = fill_value
|
|
76
76
|
|
|
77
|
-
def _prepare_xi_simple(self, xi: npt.NDArray[np.float64]) -> npt.NDArray[np.bool_]
|
|
77
|
+
def _prepare_xi_simple(self, xi: npt.NDArray[np.float64]) -> npt.NDArray[np.bool_]:
|
|
78
78
|
"""Run looser version of :meth:`_prepare_xi`.
|
|
79
79
|
|
|
80
80
|
Parameters
|
|
@@ -84,12 +84,9 @@ class PycontrailsRegularGridInterpolator(scipy.interpolate.RegularGridInterpolat
|
|
|
84
84
|
|
|
85
85
|
Returns
|
|
86
86
|
-------
|
|
87
|
-
npt.NDArray[np.bool_]
|
|
87
|
+
npt.NDArray[np.bool_]
|
|
88
88
|
A 1-dimensional Boolean array indicating which points are out of bounds.
|
|
89
|
-
If ``bounds_error`` is ``True``, this will be ``
|
|
90
|
-
no points are out of bounds. (This is the same convention as
|
|
91
|
-
:meth:`scipy.interpolate.RegularGridInterpolator._prepare_xi`). If
|
|
92
|
-
every point is in bounds, this is set to ``None``.
|
|
89
|
+
If ``bounds_error`` is ``True``, this will be all ``False``.
|
|
93
90
|
"""
|
|
94
91
|
|
|
95
92
|
if self.bounds_error:
|
|
@@ -99,12 +96,9 @@ class PycontrailsRegularGridInterpolator(scipy.interpolate.RegularGridInterpolat
|
|
|
99
96
|
if not (np.all(p >= g0) and np.all(p <= g1)):
|
|
100
97
|
raise ValueError(f"One of the requested xi is out of bounds in dimension {i}")
|
|
101
98
|
|
|
102
|
-
return
|
|
99
|
+
return np.zeros(xi.shape[0], dtype=bool)
|
|
103
100
|
|
|
104
|
-
|
|
105
|
-
if not np.any(out_of_bounds):
|
|
106
|
-
return None
|
|
107
|
-
return out_of_bounds
|
|
101
|
+
return self._find_out_of_bounds(xi.T)
|
|
108
102
|
|
|
109
103
|
def __call__(
|
|
110
104
|
self, xi: npt.NDArray[np.float64], method: str | None = None
|
|
@@ -137,7 +131,9 @@ class PycontrailsRegularGridInterpolator(scipy.interpolate.RegularGridInterpolat
|
|
|
137
131
|
return self._set_out_of_bounds(out, out_of_bounds)
|
|
138
132
|
|
|
139
133
|
def _set_out_of_bounds(
|
|
140
|
-
self,
|
|
134
|
+
self,
|
|
135
|
+
out: npt.NDArray[np.float_],
|
|
136
|
+
out_of_bounds: npt.NDArray[np.bool_],
|
|
141
137
|
) -> npt.NDArray[np.float_]:
|
|
142
138
|
"""Set out-of-bounds values to the fill value.
|
|
143
139
|
|
|
@@ -145,7 +141,7 @@ class PycontrailsRegularGridInterpolator(scipy.interpolate.RegularGridInterpolat
|
|
|
145
141
|
----------
|
|
146
142
|
out : npt.NDArray[np.float_]
|
|
147
143
|
Values from interpolation. This is modified in-place.
|
|
148
|
-
out_of_bounds : npt.NDArray[np.bool_]
|
|
144
|
+
out_of_bounds : npt.NDArray[np.bool_]
|
|
149
145
|
A 1-dimensional Boolean array indicating which points are out of bounds.
|
|
150
146
|
|
|
151
147
|
Returns
|
|
@@ -153,7 +149,7 @@ class PycontrailsRegularGridInterpolator(scipy.interpolate.RegularGridInterpolat
|
|
|
153
149
|
out : npt.NDArray[np.float_]
|
|
154
150
|
A reference to the ``out`` array.
|
|
155
151
|
"""
|
|
156
|
-
if
|
|
152
|
+
if self.fill_value is not None and np.any(out_of_bounds):
|
|
157
153
|
out[out_of_bounds] = self.fill_value
|
|
158
154
|
|
|
159
155
|
return out
|
|
@@ -557,7 +553,7 @@ class RGIArtifacts:
|
|
|
557
553
|
|
|
558
554
|
xi_indices: npt.NDArray[np.int64]
|
|
559
555
|
norm_distances: npt.NDArray[np.float64]
|
|
560
|
-
out_of_bounds: npt.NDArray[np.bool_]
|
|
556
|
+
out_of_bounds: npt.NDArray[np.bool_]
|
|
561
557
|
|
|
562
558
|
|
|
563
559
|
# ------------------------------------------------------------------------------
|
pycontrails/core/met.py
CHANGED
|
@@ -416,7 +416,7 @@ class MetBase(ABC, Generic[XArrayType]):
|
|
|
416
416
|
>>> variables = "air_temperature", "specific_humidity"
|
|
417
417
|
>>> levels = [200, 300]
|
|
418
418
|
>>> era5 = ERA5(times, variables, levels)
|
|
419
|
-
>>> mds = era5.open_metdataset(
|
|
419
|
+
>>> mds = era5.open_metdataset()
|
|
420
420
|
>>> mds.variables["level"].values # faster access than mds.data["level"]
|
|
421
421
|
array([200., 300.])
|
|
422
422
|
|
|
@@ -606,7 +606,7 @@ class MetDataset(MetBase):
|
|
|
606
606
|
>>> era5 = ERA5(time, variables, pressure_levels)
|
|
607
607
|
|
|
608
608
|
>>> # Open directly as `MetDataset`
|
|
609
|
-
>>> met = era5.open_metdataset(
|
|
609
|
+
>>> met = era5.open_metdataset()
|
|
610
610
|
>>> # Use `data` attribute to access `xarray` object
|
|
611
611
|
>>> assert isinstance(met.data, xr.Dataset)
|
|
612
612
|
|
|
@@ -992,7 +992,7 @@ class MetDataset(MetBase):
|
|
|
992
992
|
>>> variables = ["air_temperature", "specific_humidity"]
|
|
993
993
|
>>> levels = [250, 200]
|
|
994
994
|
>>> era5 = ERA5(time=times, variables=variables, pressure_levels=levels)
|
|
995
|
-
>>> met = era5.open_metdataset(
|
|
995
|
+
>>> met = era5.open_metdataset()
|
|
996
996
|
>>> met.to_vector(transfer_attrs=False)
|
|
997
997
|
GeoVectorDataset [6 keys x 4152960 length, 1 attributes]
|
|
998
998
|
Keys: longitude, latitude, level, time, air_temperature, ..., specific_humidity
|
|
@@ -1507,7 +1507,7 @@ class MetDataArray(MetBase):
|
|
|
1507
1507
|
>>> variables = "air_temperature"
|
|
1508
1508
|
>>> levels = [200, 250, 300]
|
|
1509
1509
|
>>> era5 = ERA5(times, variables, levels)
|
|
1510
|
-
>>> met = era5.open_metdataset(
|
|
1510
|
+
>>> met = era5.open_metdataset()
|
|
1511
1511
|
>>> mda = met["air_temperature"]
|
|
1512
1512
|
|
|
1513
1513
|
>>> # Interpolation at a grid point agrees with value
|
|
@@ -1779,7 +1779,7 @@ class MetDataArray(MetBase):
|
|
|
1779
1779
|
>>> from pprint import pprint
|
|
1780
1780
|
>>> from pycontrails.datalib.ecmwf import ERA5
|
|
1781
1781
|
>>> era5 = ERA5("2022-03-01", variables="air_temperature", pressure_levels=250)
|
|
1782
|
-
>>> mda = era5.open_metdataset(
|
|
1782
|
+
>>> mda = era5.open_metdataset()["air_temperature"]
|
|
1783
1783
|
>>> mda.shape
|
|
1784
1784
|
(1440, 721, 1, 1)
|
|
1785
1785
|
|
pycontrails/core/models.py
CHANGED
|
@@ -566,6 +566,10 @@ class Model(ABC):
|
|
|
566
566
|
try:
|
|
567
567
|
# This case is when self.source is a subgrid of self.met
|
|
568
568
|
# The call to .sel will raise a KeyError if this is not the case
|
|
569
|
+
|
|
570
|
+
# XXX: Sometimes this hangs when using dask!
|
|
571
|
+
# This issue is somewhat similar to
|
|
572
|
+
# https://github.com/pydata/xarray/issues/4406
|
|
569
573
|
self.source[met_key] = da.sel(self.source.coords)
|
|
570
574
|
|
|
571
575
|
except KeyError:
|
|
Binary file
|