pycontrails 0.58.0__cp314-cp314-macosx_10_13_x86_64.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,701 @@
|
|
|
1
|
+
"""Support for the Poll-Schumann (PS) theoretical aircraft performance over a grid."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import warnings
|
|
7
|
+
from typing import Any, overload
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import numpy.typing as npt
|
|
11
|
+
import scipy.optimize
|
|
12
|
+
import xarray as xr
|
|
13
|
+
|
|
14
|
+
from pycontrails.core.aircraft_performance import (
|
|
15
|
+
AircraftPerformanceGrid,
|
|
16
|
+
AircraftPerformanceGridData,
|
|
17
|
+
AircraftPerformanceGridParams,
|
|
18
|
+
)
|
|
19
|
+
from pycontrails.core.flight import Flight
|
|
20
|
+
from pycontrails.core.fuel import JetA
|
|
21
|
+
from pycontrails.core.met import MetDataset
|
|
22
|
+
from pycontrails.core.met_var import AirTemperature, MetVariable
|
|
23
|
+
from pycontrails.core.vector import GeoVectorDataset
|
|
24
|
+
from pycontrails.models.ps_model import ps_model, ps_operational_limits
|
|
25
|
+
from pycontrails.models.ps_model.ps_aircraft_params import PSAircraftEngineParams
|
|
26
|
+
from pycontrails.physics import units
|
|
27
|
+
from pycontrails.utils.types import ArrayOrFloat
|
|
28
|
+
|
|
29
|
+
# mypy: disable-error-code = type-var
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclasses.dataclass
|
|
33
|
+
class PSGridParams(AircraftPerformanceGridParams):
|
|
34
|
+
"""Parameters for :class:`PSGrid`."""
|
|
35
|
+
|
|
36
|
+
#: Passed into :func:`ps_nominal_grid`
|
|
37
|
+
maxiter: int = 10
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PSGrid(AircraftPerformanceGrid):
|
|
41
|
+
"""Compute nominal Poll-Schumann aircraft performance over a grid.
|
|
42
|
+
|
|
43
|
+
For a given aircraft type, altitude, aircraft mass, air temperature, and
|
|
44
|
+
mach number, the PS model computes a theoretical engine efficiency and fuel
|
|
45
|
+
flow rate for an aircraft under cruise conditions. Letting the aircraft mass
|
|
46
|
+
vary and fixing the other parameters, the engine efficiency curve attains a
|
|
47
|
+
single maximum at a particular aircraft mass. By solving this implicit
|
|
48
|
+
equation, the PS model can be used to compute the aircraft mass that
|
|
49
|
+
maximizes engine efficiency for a given set of parameters. This is the
|
|
50
|
+
"nominal" aircraft mass computed by this model.
|
|
51
|
+
|
|
52
|
+
This nominal aircraft mass is not always realizable. For example, the maximum
|
|
53
|
+
engine efficiency may be attained at an aircraft mass that is less than the
|
|
54
|
+
operating empty mass of the aircraft. This model determines the minimum and
|
|
55
|
+
maximum possible aircraft mass for a given set of parameters using a simple
|
|
56
|
+
heuristic. The nominal aircraft mass is then clipped to this range.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
name = "PSGrid"
|
|
60
|
+
long_name = "Poll-Schumann Aircraft Performance evaluated at arbitrary points"
|
|
61
|
+
met_variables: tuple[MetVariable, ...] = (AirTemperature,)
|
|
62
|
+
default_params = PSGridParams
|
|
63
|
+
|
|
64
|
+
met: MetDataset
|
|
65
|
+
source: GeoVectorDataset
|
|
66
|
+
|
|
67
|
+
@overload
|
|
68
|
+
def eval(self, source: GeoVectorDataset, **params: Any) -> GeoVectorDataset: ...
|
|
69
|
+
|
|
70
|
+
@overload
|
|
71
|
+
def eval(self, source: MetDataset | None = ..., **params: Any) -> MetDataset: ...
|
|
72
|
+
|
|
73
|
+
def eval(
|
|
74
|
+
self, source: GeoVectorDataset | MetDataset | None = None, **params: Any
|
|
75
|
+
) -> GeoVectorDataset | MetDataset:
|
|
76
|
+
"""Evaluate the PS model over a :class:`MetDataset` or :class:`GeoVectorDataset`.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
source : GeoVectorDataset | MetDataset | None, optional
|
|
81
|
+
The source data to use for the evaluation. If None, the source is taken
|
|
82
|
+
from the :attr:`met` attribute of the :class:`PSGrid` instance.
|
|
83
|
+
The aircraft type is taken from ``source.attrs["aircraft_type"]``. If this field
|
|
84
|
+
is not present, ``params["aircraft_type"]`` is used instead. See the
|
|
85
|
+
static CSV file :file:`ps-aircraft-params-20240524.csv` for a list of supported
|
|
86
|
+
aircraft types.
|
|
87
|
+
**params : Any
|
|
88
|
+
Override the default parameters of the :class:`PSGrid` instance.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
GeoVectorDataset | MetDataset
|
|
93
|
+
The source data with the following variables added:
|
|
94
|
+
|
|
95
|
+
- aircraft_mass
|
|
96
|
+
- fuel_flow
|
|
97
|
+
- engine_efficiency
|
|
98
|
+
|
|
99
|
+
Raises
|
|
100
|
+
------
|
|
101
|
+
NotImplementedError
|
|
102
|
+
If "true_airspeed" or "aircraft_mass" fields are included in
|
|
103
|
+
:attr:`source`.
|
|
104
|
+
|
|
105
|
+
See Also
|
|
106
|
+
--------
|
|
107
|
+
:func:`ps_nominal_grid`
|
|
108
|
+
"""
|
|
109
|
+
self.update_params(**params)
|
|
110
|
+
self.set_source(source)
|
|
111
|
+
self.require_source_type((GeoVectorDataset, MetDataset))
|
|
112
|
+
self.set_source_met()
|
|
113
|
+
|
|
114
|
+
# Check some assumptions
|
|
115
|
+
if "true_airspeed" in self.source or "true_airspeed" in self.source.attrs:
|
|
116
|
+
msg = "PSGrid currently only supports setting a 'mach_number' parameter."
|
|
117
|
+
raise NotImplementedError(msg)
|
|
118
|
+
if self.get_source_param("aircraft_mass", set_attr=False) is not None:
|
|
119
|
+
msg = "The 'aircraft_mass' parameter must be None."
|
|
120
|
+
raise NotImplementedError(msg)
|
|
121
|
+
if isinstance(self.source, Flight):
|
|
122
|
+
warnings.warn(
|
|
123
|
+
"The 'PSGrid' model is not intended to support 'Flight' objects as 'source' "
|
|
124
|
+
"data. Instead, use the 'PSFlight' model."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Extract the relevant source data
|
|
128
|
+
try:
|
|
129
|
+
aircraft_type = self.source.attrs["aircraft_type"]
|
|
130
|
+
except KeyError:
|
|
131
|
+
aircraft_type = self.params["aircraft_type"]
|
|
132
|
+
self.source.attrs["aircraft_type"] = aircraft_type
|
|
133
|
+
|
|
134
|
+
fuel = self.source.attrs.get("fuel", self.params["fuel"])
|
|
135
|
+
q_fuel = fuel.q_fuel
|
|
136
|
+
mach_number = self.get_source_param("mach_number", set_attr=False)
|
|
137
|
+
|
|
138
|
+
if isinstance(self.source, MetDataset):
|
|
139
|
+
if "fuel_flow" in self.source:
|
|
140
|
+
msg = "PSGrid doesn't support custom 'fuel_flow' values."
|
|
141
|
+
raise NotImplementedError(msg)
|
|
142
|
+
if "engine_efficiency" in self.source:
|
|
143
|
+
msg = "PSGrid doesn't support custom 'engine_efficiency' values."
|
|
144
|
+
raise NotImplementedError(msg)
|
|
145
|
+
|
|
146
|
+
ds = ps_nominal_grid(
|
|
147
|
+
aircraft_type,
|
|
148
|
+
air_temperature=self.source.data["air_temperature"],
|
|
149
|
+
q_fuel=q_fuel,
|
|
150
|
+
mach_number=mach_number,
|
|
151
|
+
maxiter=self.params["maxiter"],
|
|
152
|
+
)
|
|
153
|
+
return MetDataset(ds)
|
|
154
|
+
|
|
155
|
+
air_temperature = self.source["air_temperature"]
|
|
156
|
+
ds = ps_nominal_grid(
|
|
157
|
+
aircraft_type,
|
|
158
|
+
level=self.source.level,
|
|
159
|
+
air_temperature=air_temperature,
|
|
160
|
+
q_fuel=q_fuel,
|
|
161
|
+
mach_number=mach_number,
|
|
162
|
+
maxiter=self.params["maxiter"],
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Set the source data
|
|
166
|
+
self.source.setdefault("aircraft_mass", ds["aircraft_mass"])
|
|
167
|
+
self.source.setdefault("fuel_flow", ds["fuel_flow"])
|
|
168
|
+
self.source.setdefault("engine_efficiency", ds["engine_efficiency"])
|
|
169
|
+
mach_number = self.source.attrs.setdefault("mach_number", ds.attrs["mach_number"])
|
|
170
|
+
self.source["true_airspeed"] = units.mach_number_to_tas(mach_number, air_temperature)
|
|
171
|
+
self.source.attrs.setdefault("wingspan", ds.attrs["wingspan"])
|
|
172
|
+
self.source.attrs.setdefault("n_engine", ds.attrs["n_engine"])
|
|
173
|
+
|
|
174
|
+
return self.source
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclasses.dataclass
|
|
178
|
+
class _PerfVariables:
|
|
179
|
+
atyp_param: PSAircraftEngineParams
|
|
180
|
+
air_pressure: npt.NDArray[np.floating] | float
|
|
181
|
+
air_temperature: npt.NDArray[np.floating] | float
|
|
182
|
+
mach_number: npt.NDArray[np.floating] | float
|
|
183
|
+
q_fuel: float
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _nominal_perf(aircraft_mass: ArrayOrFloat, perf: _PerfVariables) -> AircraftPerformanceGridData:
|
|
187
|
+
"""Compute nominal Poll-Schumann aircraft performance."""
|
|
188
|
+
|
|
189
|
+
atyp_param = perf.atyp_param
|
|
190
|
+
air_pressure = perf.air_pressure
|
|
191
|
+
air_temperature = perf.air_temperature
|
|
192
|
+
mach_number = perf.mach_number
|
|
193
|
+
q_fuel = perf.q_fuel
|
|
194
|
+
|
|
195
|
+
# Using np.float32 here avoids scalar promotion to float64 via numpy 2.0 and NEP50
|
|
196
|
+
# In other words, the dtype of the perf variables is maintained
|
|
197
|
+
theta = np.float32(0.0)
|
|
198
|
+
dv_dt = np.float32(0.0)
|
|
199
|
+
|
|
200
|
+
rn = ps_model.reynolds_number(
|
|
201
|
+
atyp_param.wing_surface_area, mach_number, air_temperature, air_pressure
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
c_lift = ps_model.lift_coefficient(
|
|
205
|
+
atyp_param.wing_surface_area, aircraft_mass, air_pressure, mach_number, theta
|
|
206
|
+
)
|
|
207
|
+
c_f = ps_model.skin_friction_coefficient(rn)
|
|
208
|
+
c_drag_0 = ps_model.zero_lift_drag_coefficient(c_f, atyp_param.psi_0)
|
|
209
|
+
e_ls = ps_model.oswald_efficiency_factor(c_drag_0, atyp_param)
|
|
210
|
+
c_drag_w = ps_model.wave_drag_coefficient(mach_number, c_lift, atyp_param)
|
|
211
|
+
c_drag = ps_model.airframe_drag_coefficient(
|
|
212
|
+
c_drag_0, c_drag_w, c_lift, e_ls, atyp_param.wing_aspect_ratio
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
thrust = ps_model.thrust_force(aircraft_mass, c_lift, c_drag, dv_dt, theta)
|
|
216
|
+
|
|
217
|
+
c_t = ps_model.engine_thrust_coefficient(
|
|
218
|
+
thrust, mach_number, air_pressure, atyp_param.wing_surface_area
|
|
219
|
+
)
|
|
220
|
+
c_t_eta_b = ps_model.thrust_coefficient_at_max_efficiency(
|
|
221
|
+
mach_number, atyp_param.m_des, atyp_param.c_t_des
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Always correct thrust coefficients in the gridded case
|
|
225
|
+
# (In the flight case, this correction is governed by a model parameter)
|
|
226
|
+
c_t_available = ps_operational_limits.max_available_thrust_coefficient(
|
|
227
|
+
air_temperature, mach_number, c_t_eta_b, atyp_param
|
|
228
|
+
)
|
|
229
|
+
np.clip(c_t, 0.0, c_t_available, out=c_t)
|
|
230
|
+
|
|
231
|
+
engine_efficiency = ps_model.overall_propulsion_efficiency(
|
|
232
|
+
mach_number, c_t, c_t_eta_b, atyp_param
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
fuel_flow = ps_model.fuel_mass_flow_rate(
|
|
236
|
+
air_pressure,
|
|
237
|
+
air_temperature,
|
|
238
|
+
mach_number,
|
|
239
|
+
c_t,
|
|
240
|
+
engine_efficiency,
|
|
241
|
+
atyp_param.wing_surface_area,
|
|
242
|
+
q_fuel,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return AircraftPerformanceGridData(
|
|
246
|
+
fuel_flow=fuel_flow,
|
|
247
|
+
engine_efficiency=engine_efficiency,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _newton_func(aircraft_mass: ArrayOrFloat, perf: _PerfVariables) -> ArrayOrFloat:
|
|
252
|
+
"""Approximate the derivative of the engine efficiency with respect to mass.
|
|
253
|
+
|
|
254
|
+
This is used to find the mass at which the engine efficiency is maximized.
|
|
255
|
+
"""
|
|
256
|
+
eta1 = _nominal_perf(aircraft_mass + 0.5, perf).engine_efficiency
|
|
257
|
+
eta2 = _nominal_perf(aircraft_mass - 0.5, perf).engine_efficiency
|
|
258
|
+
return eta1 - eta2
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _min_mass(
|
|
262
|
+
oem: float,
|
|
263
|
+
lf: float,
|
|
264
|
+
mpm: float,
|
|
265
|
+
reserve_fuel: float,
|
|
266
|
+
) -> float:
|
|
267
|
+
"""Calculate the minimum mass given OEM, LF, MPM, and reserve fuel."""
|
|
268
|
+
return oem + lf * mpm + reserve_fuel
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _estimate_mass_extremes(
|
|
272
|
+
atyp_param: PSAircraftEngineParams,
|
|
273
|
+
perf: _PerfVariables,
|
|
274
|
+
n_iter: int = 3,
|
|
275
|
+
) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
|
|
276
|
+
"""Calculate the minimum and maximum mass for a given aircraft type."""
|
|
277
|
+
|
|
278
|
+
oem = atyp_param.amass_oew # operating empty mass
|
|
279
|
+
lf = 0.7 # load factor
|
|
280
|
+
mpm = atyp_param.amass_mpl # max payload mass
|
|
281
|
+
mtow = atyp_param.amass_mtow # max takeoff mass
|
|
282
|
+
|
|
283
|
+
min_mass = _min_mass(oem, lf, mpm, 0.0) # no reserve fuel
|
|
284
|
+
for _ in range(n_iter):
|
|
285
|
+
# Estimate the fuel required to cruise at 35,000 ft for 90 minutes.
|
|
286
|
+
# This is used to compute the reserve fuel.
|
|
287
|
+
ff = _nominal_perf(min_mass, perf).fuel_flow
|
|
288
|
+
reserve_fuel = ff * 60.0 * 90.0 # 90 minutes
|
|
289
|
+
min_mass = _min_mass(oem, lf, mpm, reserve_fuel)
|
|
290
|
+
|
|
291
|
+
# Crude: Assume 2x the fuel flow of cruise for climb
|
|
292
|
+
# Compute the maximum weight at cruise by assuming a 20 minute climb
|
|
293
|
+
ff = _nominal_perf(mtow, perf).fuel_flow
|
|
294
|
+
max_mass = mtow - 2.0 * ff * 60.0 * 20.0
|
|
295
|
+
|
|
296
|
+
return min_mass, max_mass # type: ignore[return-value]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _parse_variables(
|
|
300
|
+
level: npt.NDArray[np.floating] | None,
|
|
301
|
+
air_temperature: xr.DataArray | npt.NDArray[np.floating] | None,
|
|
302
|
+
) -> tuple[
|
|
303
|
+
tuple[str],
|
|
304
|
+
dict[str, npt.NDArray[np.floating]],
|
|
305
|
+
npt.NDArray[np.floating],
|
|
306
|
+
npt.NDArray[np.floating],
|
|
307
|
+
]:
|
|
308
|
+
"""Parse the level and air temperature arguments.
|
|
309
|
+
|
|
310
|
+
Returns a tuple of ``(dims, coords, air_pressure, air_temperature)``.
|
|
311
|
+
"""
|
|
312
|
+
if isinstance(air_temperature, xr.DataArray):
|
|
313
|
+
if level is not None:
|
|
314
|
+
msg = "If 'air_temperature' is a DataArray, 'level' must be None"
|
|
315
|
+
raise ValueError(msg)
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
pressure_da = air_temperature["air_pressure"]
|
|
319
|
+
except KeyError as exc:
|
|
320
|
+
msg = "An 'air_pressure' coordinate must be present in 'air_temperature'"
|
|
321
|
+
raise KeyError(msg) from exc
|
|
322
|
+
|
|
323
|
+
air_temperature, pressure_da = xr.broadcast(air_temperature, pressure_da)
|
|
324
|
+
return ( # type: ignore[return-value]
|
|
325
|
+
air_temperature.dims,
|
|
326
|
+
air_temperature.coords,
|
|
327
|
+
np.asarray(pressure_da),
|
|
328
|
+
np.asarray(air_temperature),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
if level is None:
|
|
332
|
+
msg = "The 'level' argument must be provided"
|
|
333
|
+
raise ValueError(msg)
|
|
334
|
+
|
|
335
|
+
air_pressure = level * 100.0
|
|
336
|
+
if air_temperature is None:
|
|
337
|
+
altitude_m = units.pl_to_m(level)
|
|
338
|
+
air_temperature = units.m_to_T_isa(altitude_m)
|
|
339
|
+
return ("level",), {"level": level}, air_pressure, air_temperature
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def ps_nominal_grid(
|
|
343
|
+
aircraft_type: str,
|
|
344
|
+
*,
|
|
345
|
+
level: npt.NDArray[np.floating] | None = None,
|
|
346
|
+
air_temperature: xr.DataArray | npt.NDArray[np.floating] | None = None,
|
|
347
|
+
q_fuel: float = JetA.q_fuel,
|
|
348
|
+
mach_number: float | None = None,
|
|
349
|
+
maxiter: int = PSGridParams.maxiter,
|
|
350
|
+
engine_deterioration_factor: float = PSGridParams.engine_deterioration_factor,
|
|
351
|
+
) -> xr.Dataset:
|
|
352
|
+
"""Calculate the nominal performance grid for a given aircraft type.
|
|
353
|
+
|
|
354
|
+
This function is similar to the :class:`PSGrid` model, but it doesn't require
|
|
355
|
+
meteorological data. Instead, the ambient air temperature can be computed from
|
|
356
|
+
the ISA model or passed as an argument.
|
|
357
|
+
|
|
358
|
+
Parameters
|
|
359
|
+
----------
|
|
360
|
+
aircraft_type : str
|
|
361
|
+
The aircraft type.
|
|
362
|
+
level : npt.NDArray[np.floating] | None, optional
|
|
363
|
+
The pressure level, [:math:`hPa`]. If None, the ``air_temperature``
|
|
364
|
+
argument must be a :class:`xarray.DataArray` with an ``air_pressure`` coordinate.
|
|
365
|
+
air_temperature : xr.DataArray | npt.NDArray[np.floating] | None, optional
|
|
366
|
+
The ambient air temperature, [:math:`K`]. If None (default), the ISA
|
|
367
|
+
temperature is computed from the ``level`` argument. If a :class:`xarray.DataArray`,
|
|
368
|
+
an ``air_pressure`` coordinate must be present and the ``level`` argument must be None
|
|
369
|
+
to avoid ambiguity. If a :class:`numpy.ndarray` is passed, it is assumed to be 1
|
|
370
|
+
dimensional with the same shape as the ``level`` argument.
|
|
371
|
+
q_fuel : float, optional
|
|
372
|
+
The fuel heating value, by default :attr:`JetA.q_fuel`
|
|
373
|
+
mach_number : float | None, optional
|
|
374
|
+
The Mach number. If None (default), the PS design Mach number is used.
|
|
375
|
+
maxiter : int, optional
|
|
376
|
+
Passed into :func:`scipy.optimize.newton`.
|
|
377
|
+
engine_deterioration_factor : float, optional
|
|
378
|
+
The engine deterioration factor,
|
|
379
|
+
by default :attr:`PSGridParams.engine_deterioration_factor`.
|
|
380
|
+
|
|
381
|
+
Returns
|
|
382
|
+
-------
|
|
383
|
+
xr.Dataset
|
|
384
|
+
The nominal performance grid. The grid is indexed by altitude and Mach number.
|
|
385
|
+
Contains the following variables:
|
|
386
|
+
|
|
387
|
+
- ``"fuel_flow"`` : Fuel flow rate, [:math:`kg/s`]
|
|
388
|
+
- ``"engine_efficiency"`` : Engine efficiency
|
|
389
|
+
- ``"aircraft_mass"`` : Aircraft mass at which the engine efficiency is maximized,
|
|
390
|
+
[:math:`kg`]
|
|
391
|
+
|
|
392
|
+
Raises
|
|
393
|
+
------
|
|
394
|
+
KeyError
|
|
395
|
+
If "aircraft_type" is not supported by the PS model.
|
|
396
|
+
|
|
397
|
+
See Also
|
|
398
|
+
--------
|
|
399
|
+
ps_nominal_optimize_mach
|
|
400
|
+
|
|
401
|
+
Examples
|
|
402
|
+
--------
|
|
403
|
+
>>> level = np.arange(200, 300, 10, dtype=float)
|
|
404
|
+
|
|
405
|
+
>>> # Compute nominal aircraft performance assuming ISA conditions
|
|
406
|
+
>>> # and the design Mach number
|
|
407
|
+
>>> perf = ps_nominal_grid("A320", level=level)
|
|
408
|
+
>>> perf.attrs["mach_number"]
|
|
409
|
+
0.753
|
|
410
|
+
|
|
411
|
+
>>> perf.to_dataframe().round({"aircraft_mass": 0, "engine_efficiency": 3, "fuel_flow": 3})
|
|
412
|
+
aircraft_mass engine_efficiency fuel_flow
|
|
413
|
+
level
|
|
414
|
+
200.0 58416.0 0.301 0.576
|
|
415
|
+
210.0 61618.0 0.301 0.604
|
|
416
|
+
220.0 64830.0 0.301 0.633
|
|
417
|
+
230.0 68026.0 0.301 0.663
|
|
418
|
+
240.0 71188.0 0.301 0.695
|
|
419
|
+
250.0 71775.0 0.301 0.703
|
|
420
|
+
260.0 71766.0 0.300 0.708
|
|
421
|
+
270.0 71752.0 0.300 0.715
|
|
422
|
+
280.0 71736.0 0.299 0.722
|
|
423
|
+
290.0 71717.0 0.298 0.730
|
|
424
|
+
|
|
425
|
+
>>> # Now compute it for a higher Mach number
|
|
426
|
+
>>> perf = ps_nominal_grid("A320", level=level, mach_number=0.78)
|
|
427
|
+
>>> perf.to_dataframe().round({"aircraft_mass": 0, "engine_efficiency": 3, "fuel_flow": 3})
|
|
428
|
+
aircraft_mass engine_efficiency fuel_flow
|
|
429
|
+
level
|
|
430
|
+
200.0 58473.0 0.307 0.601
|
|
431
|
+
210.0 60626.0 0.307 0.621
|
|
432
|
+
220.0 63818.0 0.307 0.651
|
|
433
|
+
230.0 66994.0 0.307 0.682
|
|
434
|
+
240.0 70130.0 0.307 0.714
|
|
435
|
+
250.0 71703.0 0.307 0.733
|
|
436
|
+
260.0 71690.0 0.306 0.739
|
|
437
|
+
270.0 71673.0 0.306 0.747
|
|
438
|
+
280.0 71653.0 0.305 0.756
|
|
439
|
+
290.0 71631.0 0.304 0.766
|
|
440
|
+
"""
|
|
441
|
+
dims, coords, air_pressure, air_temperature = _parse_variables(level, air_temperature)
|
|
442
|
+
|
|
443
|
+
aircraft_engine_params = ps_model.load_aircraft_engine_params(engine_deterioration_factor)
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
atyp_param = aircraft_engine_params[aircraft_type]
|
|
447
|
+
except KeyError as exc:
|
|
448
|
+
msg = (
|
|
449
|
+
f"The aircraft type {aircraft_type} is not currently supported by the PS model. "
|
|
450
|
+
f"Available aircraft types are: {list(aircraft_engine_params)}"
|
|
451
|
+
)
|
|
452
|
+
raise KeyError(msg) from exc
|
|
453
|
+
|
|
454
|
+
mach_number = mach_number or atyp_param.m_des
|
|
455
|
+
|
|
456
|
+
perf = _PerfVariables(
|
|
457
|
+
atyp_param=atyp_param,
|
|
458
|
+
air_pressure=air_pressure,
|
|
459
|
+
air_temperature=air_temperature,
|
|
460
|
+
mach_number=mach_number,
|
|
461
|
+
q_fuel=q_fuel,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
min_mass, max_mass = _estimate_mass_extremes(atyp_param, perf)
|
|
465
|
+
|
|
466
|
+
mass_allowed = ps_operational_limits.max_allowable_aircraft_mass(
|
|
467
|
+
air_pressure,
|
|
468
|
+
mach_number=mach_number,
|
|
469
|
+
mach_num_des=atyp_param.m_des,
|
|
470
|
+
c_l_do=atyp_param.c_l_do,
|
|
471
|
+
wing_surface_area=atyp_param.wing_surface_area,
|
|
472
|
+
amass_mtow=atyp_param.amass_mtow,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
min_mass.clip(max=mass_allowed, out=min_mass) # type: ignore[call-overload]
|
|
476
|
+
max_mass.clip(max=mass_allowed, out=max_mass) # type: ignore[call-overload]
|
|
477
|
+
|
|
478
|
+
x0 = np.full_like(air_temperature, (max_mass + min_mass) / 2.0)
|
|
479
|
+
|
|
480
|
+
# Choose aircraft mass to maximize engine efficiency
|
|
481
|
+
# This is the critical step of the calculation
|
|
482
|
+
aircraft_mass = scipy.optimize.newton(
|
|
483
|
+
func=_newton_func,
|
|
484
|
+
args=(perf,),
|
|
485
|
+
x0=x0,
|
|
486
|
+
tol=80.0, # use roughly the weight of a passenger as a tolerance
|
|
487
|
+
disp=False,
|
|
488
|
+
maxiter=maxiter,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# scipy.optimize.newton promotes float32 to float64
|
|
492
|
+
aircraft_mass = aircraft_mass.astype(x0.dtype, copy=False)
|
|
493
|
+
|
|
494
|
+
aircraft_mass.clip(min=min_mass, max=max_mass, out=aircraft_mass)
|
|
495
|
+
|
|
496
|
+
output = _nominal_perf(aircraft_mass, perf)
|
|
497
|
+
|
|
498
|
+
engine_efficiency = output.engine_efficiency
|
|
499
|
+
fuel_flow = output.fuel_flow
|
|
500
|
+
|
|
501
|
+
attrs = {
|
|
502
|
+
"aircraft_type": aircraft_type,
|
|
503
|
+
"mach_number": mach_number,
|
|
504
|
+
"q_fuel": q_fuel,
|
|
505
|
+
"wingspan": atyp_param.wing_span,
|
|
506
|
+
"n_engine": atyp_param.n_engine,
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return xr.Dataset(
|
|
510
|
+
{
|
|
511
|
+
"aircraft_mass": (dims, aircraft_mass),
|
|
512
|
+
"engine_efficiency": (dims, engine_efficiency),
|
|
513
|
+
"fuel_flow": (dims, fuel_flow),
|
|
514
|
+
},
|
|
515
|
+
coords=coords,
|
|
516
|
+
attrs=attrs,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _newton_mach(
|
|
521
|
+
mach_number: ArrayOrFloat,
|
|
522
|
+
perf: _PerfVariables,
|
|
523
|
+
aircraft_mass: ArrayOrFloat,
|
|
524
|
+
headwind: ArrayOrFloat,
|
|
525
|
+
cost_index: ArrayOrFloat,
|
|
526
|
+
) -> ArrayOrFloat:
|
|
527
|
+
"""Approximate the derivative of the cost of a segment based on mach number.
|
|
528
|
+
|
|
529
|
+
This is used to find the mach number at which cost in minimized.
|
|
530
|
+
"""
|
|
531
|
+
perf.mach_number = mach_number + 1e-4
|
|
532
|
+
tas = units.mach_number_to_tas(perf.mach_number, perf.air_temperature)
|
|
533
|
+
groundspeed = tas - headwind
|
|
534
|
+
ff1 = _nominal_perf(aircraft_mass, perf).fuel_flow
|
|
535
|
+
eccf1 = (cost_index + ff1 * 60) / groundspeed
|
|
536
|
+
|
|
537
|
+
perf.mach_number = mach_number - 1e-4
|
|
538
|
+
tas = units.mach_number_to_tas(perf.mach_number, perf.air_temperature)
|
|
539
|
+
groundspeed = tas - headwind
|
|
540
|
+
ff2 = _nominal_perf(aircraft_mass, perf).fuel_flow
|
|
541
|
+
eccf2 = (cost_index + ff2 * 60) / groundspeed
|
|
542
|
+
return eccf1 - eccf2
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def ps_nominal_optimize_mach(
|
|
546
|
+
aircraft_type: str,
|
|
547
|
+
aircraft_mass: ArrayOrFloat,
|
|
548
|
+
cost_index: ArrayOrFloat,
|
|
549
|
+
level: ArrayOrFloat,
|
|
550
|
+
*,
|
|
551
|
+
air_temperature: ArrayOrFloat | None = None,
|
|
552
|
+
northward_wind: ArrayOrFloat | None = None,
|
|
553
|
+
eastward_wind: ArrayOrFloat | None = None,
|
|
554
|
+
sin_a: ArrayOrFloat | None = None,
|
|
555
|
+
cos_a: ArrayOrFloat | None = None,
|
|
556
|
+
q_fuel: float = JetA.q_fuel,
|
|
557
|
+
engine_deterioration_factor: float = PSGridParams.engine_deterioration_factor,
|
|
558
|
+
) -> xr.Dataset:
|
|
559
|
+
"""Calculate the nominal optimal mach number for a given aircraft type.
|
|
560
|
+
|
|
561
|
+
This function is similar to the :class:`ps_nominal_grid` method, but rather than
|
|
562
|
+
maximizing engine efficiency by adjusting aircraft, we are minimizing cost by adjusting
|
|
563
|
+
mach number.
|
|
564
|
+
|
|
565
|
+
Parameters
|
|
566
|
+
----------
|
|
567
|
+
aircraft_type : str
|
|
568
|
+
The aircraft type.
|
|
569
|
+
aircraft_mass: ArrayOrFloat
|
|
570
|
+
The aircraft mass, [:math:`kg`].
|
|
571
|
+
cost_index: ArrayOrFloat
|
|
572
|
+
The cost index, [:math:`kg/min`], or non-fuel cost of one minute of flight time
|
|
573
|
+
level : ArrayOrFloat
|
|
574
|
+
The pressure level, [:math:`hPa`]. If a :class:`numpy.ndarray` is passed, it is
|
|
575
|
+
assumed to be one dimensional and the same length as the``aircraft_mass`` argument.
|
|
576
|
+
air_temperature : ArrayOrFloat | None, optional
|
|
577
|
+
The ambient air temperature, [:math:`K`]. If None (default), the ISA
|
|
578
|
+
temperature is computed from the ``level`` argument. If a :class:`numpy.ndarray`
|
|
579
|
+
is passed, it is assumed to be one dimensional and the same length as the
|
|
580
|
+
``aircraft_mass`` argument.
|
|
581
|
+
air_temperature : ArrayOrFloat | None, optional
|
|
582
|
+
northward_wind: ArrayOrFloat | None = None, optional
|
|
583
|
+
The northward component of winds, [:math:`m/s`]. If None (default) assumed to be
|
|
584
|
+
zero.
|
|
585
|
+
eastward_wind: ArrayOrFloat | None = None, optional
|
|
586
|
+
The eastward component of winds, [:math:`m/s`]. If None (default) assumed to be
|
|
587
|
+
zero.
|
|
588
|
+
sin_a: ArrayOrFloat | None = None, optional
|
|
589
|
+
The sine between the true bearing of flight and the longitudinal axis. Must be
|
|
590
|
+
specified if wind data is provided. Will be ignored if wind data is not provided.
|
|
591
|
+
cos_a: ArrayOrFloat | None = None, optional
|
|
592
|
+
The cosine between the true bearing of flight and the longitudinal axis. Must be
|
|
593
|
+
specified if wind data is provided. Will be ignored if wind data is not provided.
|
|
594
|
+
q_fuel : float, optional
|
|
595
|
+
The fuel heating value, by default :attr:`JetA.q_fuel`.
|
|
596
|
+
engine_deterioration_factor : float, optional
|
|
597
|
+
The engine deterioration factor,
|
|
598
|
+
by default :attr:`PSGridParams.engine_deterioration_factor`.
|
|
599
|
+
|
|
600
|
+
Returns
|
|
601
|
+
-------
|
|
602
|
+
xr.Dataset
|
|
603
|
+
The nominal performance grid. The grid is indexed by altitude.
|
|
604
|
+
Contains the following variables:
|
|
605
|
+
|
|
606
|
+
- ``"mach_number"``: The mach number that minimizes segment cost
|
|
607
|
+
- ``"fuel_flow"`` : Fuel flow rate, [:math:`kg/s`]
|
|
608
|
+
- ``"engine_efficiency"`` : Engine efficiency
|
|
609
|
+
- ``"aircraft_mass"`` : Aircraft mass, [:math:`kg`]
|
|
610
|
+
|
|
611
|
+
Raises
|
|
612
|
+
------
|
|
613
|
+
KeyError
|
|
614
|
+
If "aircraft_type" is not supported by the PS model.
|
|
615
|
+
ValueError
|
|
616
|
+
If wind data is provided without segment angles.
|
|
617
|
+
|
|
618
|
+
See Also
|
|
619
|
+
--------
|
|
620
|
+
ps_nominal_grid
|
|
621
|
+
"""
|
|
622
|
+
dims = ("level",)
|
|
623
|
+
coords = {"level": level}
|
|
624
|
+
aircraft_engine_params = ps_model.load_aircraft_engine_params(engine_deterioration_factor)
|
|
625
|
+
try:
|
|
626
|
+
atyp_param = aircraft_engine_params[aircraft_type]
|
|
627
|
+
except KeyError as exc:
|
|
628
|
+
msg = (
|
|
629
|
+
f"The aircraft type {aircraft_type} is not currently supported by the PS model. "
|
|
630
|
+
f"Available aircraft types are: {list(aircraft_engine_params)}"
|
|
631
|
+
)
|
|
632
|
+
raise KeyError(msg) from exc
|
|
633
|
+
|
|
634
|
+
if air_temperature is None:
|
|
635
|
+
altitude_m = units.pl_to_m(level)
|
|
636
|
+
air_temperature = units.m_to_T_isa(altitude_m)
|
|
637
|
+
|
|
638
|
+
if northward_wind is not None and eastward_wind is not None:
|
|
639
|
+
if sin_a is None or cos_a is None:
|
|
640
|
+
msg = "Segment angles must be provide if wind data is specified"
|
|
641
|
+
raise ValueError(msg)
|
|
642
|
+
headwind = -(northward_wind * cos_a + eastward_wind * sin_a)
|
|
643
|
+
else:
|
|
644
|
+
headwind = 0.0
|
|
645
|
+
|
|
646
|
+
min_mach = ps_operational_limits.minimum_mach_num(
|
|
647
|
+
air_pressure=level * 100.0,
|
|
648
|
+
aircraft_mass=aircraft_mass,
|
|
649
|
+
atyp_param=atyp_param,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
max_mach = ps_operational_limits.maximum_mach_num(
|
|
653
|
+
altitude_ft=units.pl_to_ft(level),
|
|
654
|
+
air_pressure=level * 100.0,
|
|
655
|
+
aircraft_mass=aircraft_mass,
|
|
656
|
+
air_temperature=air_temperature,
|
|
657
|
+
theta=np.full_like(aircraft_mass, 0.0),
|
|
658
|
+
atyp_param=atyp_param,
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
x0 = (min_mach + max_mach) / 2.0 # type: ignore
|
|
662
|
+
|
|
663
|
+
perf = _PerfVariables(
|
|
664
|
+
atyp_param=atyp_param,
|
|
665
|
+
air_pressure=level * 100.0,
|
|
666
|
+
air_temperature=air_temperature,
|
|
667
|
+
mach_number=x0,
|
|
668
|
+
q_fuel=q_fuel,
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
opt_mach = scipy.optimize.newton(
|
|
672
|
+
func=_newton_mach,
|
|
673
|
+
args=(perf, aircraft_mass, headwind, cost_index),
|
|
674
|
+
x0=x0,
|
|
675
|
+
tol=1e-4,
|
|
676
|
+
disp=False,
|
|
677
|
+
).clip(min=min_mach, max=max_mach)
|
|
678
|
+
|
|
679
|
+
perf.mach_number = opt_mach
|
|
680
|
+
output = _nominal_perf(aircraft_mass, perf)
|
|
681
|
+
|
|
682
|
+
engine_efficiency = output.engine_efficiency
|
|
683
|
+
fuel_flow = output.fuel_flow
|
|
684
|
+
|
|
685
|
+
attrs = {
|
|
686
|
+
"aircraft_type": aircraft_type,
|
|
687
|
+
"q_fuel": q_fuel,
|
|
688
|
+
"wingspan": atyp_param.wing_span,
|
|
689
|
+
"n_engine": atyp_param.n_engine,
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return xr.Dataset(
|
|
693
|
+
{
|
|
694
|
+
"mach_number": (dims, opt_mach),
|
|
695
|
+
"aircraft_mass": (dims, aircraft_mass),
|
|
696
|
+
"engine_efficiency": (dims, engine_efficiency),
|
|
697
|
+
"fuel_flow": (dims, fuel_flow),
|
|
698
|
+
},
|
|
699
|
+
coords=coords,
|
|
700
|
+
attrs=attrs,
|
|
701
|
+
)
|