pycontrails 0.54.2__cp310-cp310-win_amd64.whl → 0.54.3__cp310-cp310-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.
- pycontrails/_version.py +2 -2
- pycontrails/core/aircraft_performance.py +17 -3
- pycontrails/core/flight.py +3 -1
- pycontrails/core/rgi_cython.cp310-win_amd64.pyd +0 -0
- pycontrails/datalib/ecmwf/variables.py +1 -0
- pycontrails/datalib/landsat.py +5 -8
- pycontrails/datalib/sentinel.py +7 -11
- pycontrails/ext/bada.py +3 -2
- pycontrails/ext/synthetic_flight.py +3 -2
- pycontrails/models/accf.py +40 -19
- pycontrails/models/apcemm/apcemm.py +2 -1
- pycontrails/models/cocip/cocip.py +1 -2
- pycontrails/models/cocipgrid/cocip_grid.py +25 -20
- pycontrails/models/dry_advection.py +50 -54
- pycontrails/models/ps_model/__init__.py +2 -1
- pycontrails/models/ps_model/ps_aircraft_params.py +3 -2
- pycontrails/models/ps_model/ps_grid.py +187 -1
- pycontrails/models/ps_model/ps_model.py +4 -7
- pycontrails/models/ps_model/ps_operational_limits.py +39 -52
- pycontrails/physics/geo.py +149 -0
- pycontrails/physics/jet.py +141 -11
- pycontrails/physics/static/iata-cargo-load-factors-20241115.csv +71 -0
- pycontrails/physics/static/iata-passenger-load-factors-20241115.csv +71 -0
- {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/METADATA +9 -9
- {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/RECORD +29 -27
- {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/WHEEL +1 -1
- {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/LICENSE +0 -0
- {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/NOTICE +0 -0
- {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/top_level.txt +0 -0
|
@@ -333,6 +333,7 @@ def ps_nominal_grid(
|
|
|
333
333
|
q_fuel: float = JetA.q_fuel,
|
|
334
334
|
mach_number: float | None = None,
|
|
335
335
|
maxiter: int = PSGridParams.maxiter,
|
|
336
|
+
engine_deterioration_factor: float = PSGridParams.engine_deterioration_factor,
|
|
336
337
|
) -> xr.Dataset:
|
|
337
338
|
"""Calculate the nominal performance grid for a given aircraft type.
|
|
338
339
|
|
|
@@ -359,6 +360,9 @@ def ps_nominal_grid(
|
|
|
359
360
|
The Mach number. If None (default), the PS design Mach number is used.
|
|
360
361
|
maxiter : int, optional
|
|
361
362
|
Passed into :func:`scipy.optimize.newton`.
|
|
363
|
+
engine_deterioration_factor : float, optional
|
|
364
|
+
The engine deterioration factor,
|
|
365
|
+
by default :attr:`PSGridParams.engine_deterioration_factor`.
|
|
362
366
|
|
|
363
367
|
Returns
|
|
364
368
|
-------
|
|
@@ -428,7 +432,7 @@ def ps_nominal_grid(
|
|
|
428
432
|
|
|
429
433
|
air_pressure = level * 100.0
|
|
430
434
|
|
|
431
|
-
aircraft_engine_params = ps_model.load_aircraft_engine_params()
|
|
435
|
+
aircraft_engine_params = ps_model.load_aircraft_engine_params(engine_deterioration_factor)
|
|
432
436
|
|
|
433
437
|
try:
|
|
434
438
|
atyp_param = aircraft_engine_params[aircraft_type]
|
|
@@ -503,3 +507,185 @@ def ps_nominal_grid(
|
|
|
503
507
|
coords=coords,
|
|
504
508
|
attrs=attrs,
|
|
505
509
|
)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _newton_mach(
|
|
513
|
+
mach_number: ArrayOrFloat,
|
|
514
|
+
perf: _PerfVariables,
|
|
515
|
+
aircraft_mass: ArrayOrFloat,
|
|
516
|
+
headwind: ArrayOrFloat,
|
|
517
|
+
cost_index: ArrayOrFloat,
|
|
518
|
+
) -> ArrayOrFloat:
|
|
519
|
+
"""Approximate the derivative of the cost of a segment based on mach number.
|
|
520
|
+
|
|
521
|
+
This is used to find the mach number at which cost in minimized.
|
|
522
|
+
"""
|
|
523
|
+
perf.mach_number = mach_number + 1e-4
|
|
524
|
+
tas = units.mach_number_to_tas(perf.mach_number, perf.air_temperature)
|
|
525
|
+
groundspeed = tas - headwind
|
|
526
|
+
ff1 = _nominal_perf(aircraft_mass, perf).fuel_flow
|
|
527
|
+
eccf1 = (cost_index + ff1 * 60) / groundspeed
|
|
528
|
+
|
|
529
|
+
perf.mach_number = mach_number - 1e-4
|
|
530
|
+
tas = units.mach_number_to_tas(perf.mach_number, perf.air_temperature)
|
|
531
|
+
groundspeed = tas - headwind
|
|
532
|
+
ff2 = _nominal_perf(aircraft_mass, perf).fuel_flow
|
|
533
|
+
eccf2 = (cost_index + ff2 * 60) / groundspeed
|
|
534
|
+
return eccf1 - eccf2
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def ps_nominal_optimize_mach(
|
|
538
|
+
aircraft_type: str,
|
|
539
|
+
aircraft_mass: ArrayOrFloat,
|
|
540
|
+
cost_index: ArrayOrFloat,
|
|
541
|
+
level: ArrayOrFloat,
|
|
542
|
+
*,
|
|
543
|
+
air_temperature: ArrayOrFloat | None = None,
|
|
544
|
+
northward_wind: ArrayOrFloat | None = None,
|
|
545
|
+
eastward_wind: ArrayOrFloat | None = None,
|
|
546
|
+
sin_a: ArrayOrFloat | None = None,
|
|
547
|
+
cos_a: ArrayOrFloat | None = None,
|
|
548
|
+
q_fuel: float = JetA.q_fuel,
|
|
549
|
+
engine_deterioration_factor: float = PSGridParams.engine_deterioration_factor,
|
|
550
|
+
) -> xr.Dataset:
|
|
551
|
+
"""Calculate the nominal optimal mach number for a given aircraft type.
|
|
552
|
+
|
|
553
|
+
This function is similar to the :class:`ps_nominal_grid` method, but rather than
|
|
554
|
+
maximizing engine efficiecy by adjusting aircraft, we are minimizing cost by adjusting
|
|
555
|
+
mach number.
|
|
556
|
+
|
|
557
|
+
Parameters
|
|
558
|
+
----------
|
|
559
|
+
aircraft_type : str
|
|
560
|
+
The aircraft type.
|
|
561
|
+
aircraft_mass: ArrayOrFloat
|
|
562
|
+
The aircraft mass, [:math:`kg`].
|
|
563
|
+
cost_index: ArrayOrFloat
|
|
564
|
+
The cost index, [:math:`kg/min`], or non-fuel cost of one minute of flight time
|
|
565
|
+
level : ArrayOrFloat
|
|
566
|
+
The pressure level, [:math:`hPa`]. If a :class:`numpy.ndarray` is passed, it is
|
|
567
|
+
assumed to be one dimensional and the same length as the``aircraft_mass`` argument.
|
|
568
|
+
air_temperature : ArrayOrFloat | None, optional
|
|
569
|
+
The ambient air temperature, [:math:`K`]. If None (default), the ISA
|
|
570
|
+
temperature is computed from the ``level`` argument. If a :class:`numpy.ndarray`
|
|
571
|
+
is passed, it is assumed to be one dimensional and the same length as the
|
|
572
|
+
``aircraft_mass`` argument.
|
|
573
|
+
air_temperature : ArrayOrFloat | None, optional
|
|
574
|
+
northward_wind: ArrayOrFloat | None = None, optional
|
|
575
|
+
The northward component of winds, [:math:`m/s`]. If None (default) assumed to be
|
|
576
|
+
zero.
|
|
577
|
+
eastward_wind: ArrayOrFloat | None = None, optional
|
|
578
|
+
The eastward component of winds, [:math:`m/s`]. If None (default) assumed to be
|
|
579
|
+
zero.
|
|
580
|
+
sin_a: ArrayOrFloat | None = None, optional
|
|
581
|
+
The sine between the true bearing of flight and the longitudinal axis. Must be
|
|
582
|
+
specified if wind data is provided. Will be ignored if wind data is not provided.
|
|
583
|
+
cos_a: ArrayOrFloat | None = None, optional
|
|
584
|
+
The cosine between the true bearing of flight and the longitudinal axis. Must be
|
|
585
|
+
specified if wind data is provided. Will be ignored if wind data is not provided.
|
|
586
|
+
q_fuel : float, optional
|
|
587
|
+
The fuel heating value, by default :attr:`JetA.q_fuel`.
|
|
588
|
+
engine_deterioration_factor : float, optional
|
|
589
|
+
The engine deterioration factor,
|
|
590
|
+
by default :attr:`PSGridParams.engine_deterioration_factor`.
|
|
591
|
+
|
|
592
|
+
Returns
|
|
593
|
+
-------
|
|
594
|
+
xr.Dataset
|
|
595
|
+
The nominal performance grid. The grid is indexed by altitude.
|
|
596
|
+
Contains the following variables:
|
|
597
|
+
|
|
598
|
+
- ``"mach_number"``: The mach number that minimizes segment cost
|
|
599
|
+
- ``"fuel_flow"`` : Fuel flow rate, [:math:`kg/s`]
|
|
600
|
+
- ``"engine_efficiency"`` : Engine efficiency
|
|
601
|
+
- ``"aircraft_mass"`` : Aircraft mass,
|
|
602
|
+
[:math:`kg`]
|
|
603
|
+
|
|
604
|
+
Raises
|
|
605
|
+
------
|
|
606
|
+
KeyError
|
|
607
|
+
If "aircraft_type" is not supported by the PS model.
|
|
608
|
+
ValueError
|
|
609
|
+
If wind data is provided without segment angles.
|
|
610
|
+
"""
|
|
611
|
+
dims = ("level",)
|
|
612
|
+
coords = {"level": level}
|
|
613
|
+
aircraft_engine_params = ps_model.load_aircraft_engine_params(engine_deterioration_factor)
|
|
614
|
+
try:
|
|
615
|
+
atyp_param = aircraft_engine_params[aircraft_type]
|
|
616
|
+
except KeyError as exc:
|
|
617
|
+
msg = (
|
|
618
|
+
f"The aircraft type {aircraft_type} is not currently supported by the PS model. "
|
|
619
|
+
f"Available aircraft types are: {list(aircraft_engine_params)}"
|
|
620
|
+
)
|
|
621
|
+
raise KeyError(msg) from exc
|
|
622
|
+
|
|
623
|
+
if air_temperature is None:
|
|
624
|
+
altitude_m = units.pl_to_m(level)
|
|
625
|
+
air_temperature = units.m_to_T_isa(altitude_m)
|
|
626
|
+
|
|
627
|
+
headwind: ArrayOrFloat
|
|
628
|
+
if northward_wind is not None and eastward_wind is not None:
|
|
629
|
+
if sin_a is None or cos_a is None:
|
|
630
|
+
msg = "Segment angles must be provide if wind data is specified"
|
|
631
|
+
raise ValueError(msg)
|
|
632
|
+
headwind = -(northward_wind * cos_a + eastward_wind * sin_a)
|
|
633
|
+
else:
|
|
634
|
+
headwind = 0.0 # type: ignore
|
|
635
|
+
|
|
636
|
+
min_mach = ps_operational_limits.minimum_mach_num(
|
|
637
|
+
air_pressure=level * 100.0,
|
|
638
|
+
aircraft_mass=aircraft_mass,
|
|
639
|
+
atyp_param=atyp_param,
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
max_mach = ps_operational_limits.maximum_mach_num(
|
|
643
|
+
altitude_ft=units.pl_to_ft(level),
|
|
644
|
+
air_pressure=level * 100.0,
|
|
645
|
+
aircraft_mass=aircraft_mass,
|
|
646
|
+
air_temperature=air_temperature,
|
|
647
|
+
theta=np.full_like(aircraft_mass, 0.0),
|
|
648
|
+
atyp_param=atyp_param,
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
x0 = (min_mach + max_mach) / 2.0 # type: ignore
|
|
652
|
+
|
|
653
|
+
perf = _PerfVariables(
|
|
654
|
+
atyp_param=atyp_param,
|
|
655
|
+
air_pressure=level * 100.0,
|
|
656
|
+
air_temperature=air_temperature,
|
|
657
|
+
mach_number=x0,
|
|
658
|
+
q_fuel=q_fuel,
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
opt_mach = scipy.optimize.newton(
|
|
662
|
+
func=_newton_mach,
|
|
663
|
+
args=(perf, aircraft_mass, headwind, cost_index),
|
|
664
|
+
x0=x0,
|
|
665
|
+
tol=1e-4,
|
|
666
|
+
disp=False,
|
|
667
|
+
).clip(min=min_mach, max=max_mach)
|
|
668
|
+
|
|
669
|
+
perf.mach_number = opt_mach
|
|
670
|
+
output = _nominal_perf(aircraft_mass, perf)
|
|
671
|
+
|
|
672
|
+
engine_efficiency = output.engine_efficiency
|
|
673
|
+
fuel_flow = output.fuel_flow
|
|
674
|
+
|
|
675
|
+
attrs = {
|
|
676
|
+
"aircraft_type": aircraft_type,
|
|
677
|
+
"q_fuel": q_fuel,
|
|
678
|
+
"wingspan": atyp_param.wing_span,
|
|
679
|
+
"n_engine": atyp_param.n_engine,
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return xr.Dataset(
|
|
683
|
+
{
|
|
684
|
+
"mach_number": (dims, opt_mach),
|
|
685
|
+
"aircraft_mass": (dims, aircraft_mass),
|
|
686
|
+
"engine_efficiency": (dims, engine_efficiency),
|
|
687
|
+
"fuel_flow": (dims, fuel_flow),
|
|
688
|
+
},
|
|
689
|
+
coords=coords,
|
|
690
|
+
attrs=attrs,
|
|
691
|
+
)
|
|
@@ -51,13 +51,6 @@ class PSFlightParams(AircraftPerformanceParams):
|
|
|
51
51
|
#: efficiency to always exceed this value.
|
|
52
52
|
eta_over_eta_b_min: float | None = 0.5
|
|
53
53
|
|
|
54
|
-
#: Account for "in-service" engine deterioration between maintenance cycles.
|
|
55
|
-
#: Default value is set to +2.5% increase in fuel consumption.
|
|
56
|
-
# Reference:
|
|
57
|
-
# Gurrola Arrieta, M.D.J., Botez, R.M. and Lasne, A., 2024. An Engine Deterioration Model for
|
|
58
|
-
# Predicting Fuel Consumption Impact in a Regional Aircraft. Aerospace, 11(6), p.426.
|
|
59
|
-
engine_deterioration_factor: float = 0.025
|
|
60
|
-
|
|
61
54
|
|
|
62
55
|
class PSFlight(AircraftPerformance):
|
|
63
56
|
"""Simulate aircraft performance using Poll-Schumann (PS) model.
|
|
@@ -70,6 +63,10 @@ class PSFlight(AircraftPerformance):
|
|
|
70
63
|
Poll & Schumann (2022). An estimation method for the fuel burn and other performance
|
|
71
64
|
characteristics of civil transport aircraft. Part 3 Generalisation to cover climb,
|
|
72
65
|
descent and holding. Aero. J., submitted.
|
|
66
|
+
|
|
67
|
+
See Also
|
|
68
|
+
--------
|
|
69
|
+
pycontrails.physics.jet.aircraft_load_factor
|
|
73
70
|
"""
|
|
74
71
|
|
|
75
72
|
name = "PSFlight"
|
|
@@ -350,49 +350,47 @@ def max_usable_lift_coefficient(
|
|
|
350
350
|
|
|
351
351
|
|
|
352
352
|
def minimum_mach_num(
|
|
353
|
-
air_pressure:
|
|
354
|
-
aircraft_mass:
|
|
353
|
+
air_pressure: ArrayOrFloat,
|
|
354
|
+
aircraft_mass: ArrayOrFloat,
|
|
355
355
|
atyp_param: PSAircraftEngineParams,
|
|
356
|
-
) ->
|
|
356
|
+
) -> ArrayOrFloat:
|
|
357
357
|
"""
|
|
358
358
|
Calculate minimum mach number to avoid stall.
|
|
359
359
|
|
|
360
360
|
Parameters
|
|
361
361
|
----------
|
|
362
|
-
air_pressure :
|
|
362
|
+
air_pressure : ArrayOrFloat
|
|
363
363
|
Ambient pressure, [:math:`Pa`]
|
|
364
|
-
aircraft_mass :
|
|
364
|
+
aircraft_mass : ArrayOrFloat
|
|
365
365
|
Aircraft mass at each waypoint, [:math:`kg`]
|
|
366
366
|
atyp_param : PSAircraftEngineParams
|
|
367
367
|
Extracted aircraft and engine parameters.
|
|
368
368
|
|
|
369
369
|
Returns
|
|
370
370
|
-------
|
|
371
|
-
|
|
372
|
-
|
|
371
|
+
ArrayOrFloat
|
|
372
|
+
Minimum mach number to avoid stall.
|
|
373
373
|
"""
|
|
374
374
|
|
|
375
375
|
def excess_mass(
|
|
376
|
-
mach_number:
|
|
377
|
-
air_pressure:
|
|
378
|
-
aircraft_mass:
|
|
376
|
+
mach_number: ArrayOrFloat,
|
|
377
|
+
air_pressure: ArrayOrFloat,
|
|
378
|
+
aircraft_mass: ArrayOrFloat,
|
|
379
379
|
mach_num_des: float,
|
|
380
380
|
c_l_do: float,
|
|
381
381
|
wing_surface_area: float,
|
|
382
|
-
) ->
|
|
382
|
+
) -> ArrayOrFloat:
|
|
383
383
|
amass_max = max_allowable_aircraft_mass(
|
|
384
384
|
air_pressure,
|
|
385
385
|
mach_number,
|
|
386
386
|
mach_num_des,
|
|
387
387
|
c_l_do,
|
|
388
388
|
wing_surface_area,
|
|
389
|
-
1e10,
|
|
389
|
+
1e10, # clipped to this value which we want to ignore
|
|
390
390
|
)
|
|
391
|
-
if amass_max < 0:
|
|
392
|
-
return np.nan
|
|
393
391
|
return amass_max - aircraft_mass
|
|
394
392
|
|
|
395
|
-
m = scipy.optimize.
|
|
393
|
+
m = scipy.optimize.newton(
|
|
396
394
|
excess_mass,
|
|
397
395
|
args=(
|
|
398
396
|
air_pressure,
|
|
@@ -401,21 +399,22 @@ def minimum_mach_num(
|
|
|
401
399
|
atyp_param.c_l_do,
|
|
402
400
|
atyp_param.wing_surface_area,
|
|
403
401
|
),
|
|
404
|
-
x0=0.
|
|
405
|
-
x1=0.
|
|
406
|
-
|
|
402
|
+
x0=np.full_like(air_pressure, 0.4),
|
|
403
|
+
x1=np.full_like(air_pressure, 0.5),
|
|
404
|
+
tol=1e-4,
|
|
405
|
+
)
|
|
407
406
|
|
|
408
407
|
return m
|
|
409
408
|
|
|
410
409
|
|
|
411
410
|
def maximum_mach_num(
|
|
412
|
-
altitude_ft:
|
|
413
|
-
air_pressure:
|
|
414
|
-
aircraft_mass:
|
|
415
|
-
air_temperature:
|
|
416
|
-
theta:
|
|
411
|
+
altitude_ft: ArrayOrFloat,
|
|
412
|
+
air_pressure: ArrayOrFloat,
|
|
413
|
+
aircraft_mass: ArrayOrFloat,
|
|
414
|
+
air_temperature: ArrayOrFloat,
|
|
415
|
+
theta: ArrayOrFloat,
|
|
417
416
|
atyp_param: PSAircraftEngineParams,
|
|
418
|
-
) ->
|
|
417
|
+
) -> ArrayOrFloat:
|
|
419
418
|
r"""
|
|
420
419
|
Return the maximum mach number at the current operating conditions.
|
|
421
420
|
|
|
@@ -424,23 +423,23 @@ def maximum_mach_num(
|
|
|
424
423
|
|
|
425
424
|
Parameters
|
|
426
425
|
----------
|
|
427
|
-
altitude_ft :
|
|
426
|
+
altitude_ft : ArrayOrFloat
|
|
428
427
|
Altitude, [:math:`ft`]
|
|
429
|
-
air_pressure :
|
|
428
|
+
air_pressure : ArrayOrFloat
|
|
430
429
|
Ambient pressure, [:math:`Pa`]
|
|
431
|
-
aircraft_mass :
|
|
430
|
+
aircraft_mass : ArrayOrFloat
|
|
432
431
|
Aircraft mass at each waypoint, [:math:`kg`]
|
|
433
|
-
air_temperature :
|
|
432
|
+
air_temperature : ArrayOrFloat
|
|
434
433
|
Array of ambient temperature, [:math: `K`]
|
|
435
|
-
theta :
|
|
434
|
+
theta : ArrayOrFloat
|
|
436
435
|
Climb (positive value) or descent (negative value) angle, [:math:`\deg`]
|
|
437
436
|
atyp_param : PSAircraftEngineParams
|
|
438
437
|
Extracted aircraft and engine parameters.
|
|
439
438
|
|
|
440
439
|
Returns
|
|
441
440
|
-------
|
|
442
|
-
|
|
443
|
-
Maximum
|
|
441
|
+
ArrayOrFloat
|
|
442
|
+
Maximum mach number given thrust limiations.
|
|
444
443
|
"""
|
|
445
444
|
# Max speed ignoring thrust limits
|
|
446
445
|
mach_num_op_lim = max_mach_number_by_altitude(
|
|
@@ -451,27 +450,15 @@ def maximum_mach_num(
|
|
|
451
450
|
atyp_param.p_inf_co,
|
|
452
451
|
)
|
|
453
452
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
)
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
# Numerically solve for the speed where drag == max thrust
|
|
464
|
-
try:
|
|
465
|
-
m_max = scipy.optimize.root_scalar(
|
|
466
|
-
get_excess_thrust_available,
|
|
467
|
-
args=(air_temperature, air_pressure, aircraft_mass, theta, atyp_param),
|
|
468
|
-
x0=mach_num_op_lim,
|
|
469
|
-
x1=mach_num_op_lim - 0.05,
|
|
470
|
-
).root
|
|
471
|
-
except ValueError:
|
|
472
|
-
return np.nan
|
|
473
|
-
|
|
474
|
-
return m_max
|
|
453
|
+
max_mach = scipy.optimize.newton(
|
|
454
|
+
func=get_excess_thrust_available,
|
|
455
|
+
args=(air_temperature, air_pressure, aircraft_mass, theta, atyp_param),
|
|
456
|
+
x0=mach_num_op_lim,
|
|
457
|
+
x1=mach_num_op_lim - 0.01,
|
|
458
|
+
tol=1e-4,
|
|
459
|
+
).clip(max=mach_num_op_lim)
|
|
460
|
+
|
|
461
|
+
return max_mach
|
|
475
462
|
|
|
476
463
|
|
|
477
464
|
# ----------------
|
pycontrails/physics/geo.py
CHANGED
|
@@ -855,6 +855,155 @@ def advect_level(
|
|
|
855
855
|
return (level * 100.0 + (dt_s * dp_dt)) / 100.0
|
|
856
856
|
|
|
857
857
|
|
|
858
|
+
def advect_longitude_and_latitude_near_poles(
|
|
859
|
+
longitude: npt.NDArray[np.floating],
|
|
860
|
+
latitude: npt.NDArray[np.floating],
|
|
861
|
+
u_wind: npt.NDArray[np.floating],
|
|
862
|
+
v_wind: npt.NDArray[np.floating],
|
|
863
|
+
dt: npt.NDArray[np.timedelta64] | np.timedelta64,
|
|
864
|
+
) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
|
|
865
|
+
r"""Advect a particle near the poles.
|
|
866
|
+
|
|
867
|
+
This function calculates the longitude and latitude of a particle after time ``dt``
|
|
868
|
+
caused by advection due to wind near the poles (above 80 degrees North and South).
|
|
869
|
+
|
|
870
|
+
Automatically wrap over the antimeridian if necessary.
|
|
871
|
+
|
|
872
|
+
Parameters
|
|
873
|
+
----------
|
|
874
|
+
longitude : npt.NDArray[np.floating]
|
|
875
|
+
Original longitude, [:math:`\deg`]
|
|
876
|
+
latitude : npt.NDArray[np.floating]
|
|
877
|
+
Original latitude, [:math:`\deg`]
|
|
878
|
+
u_wind : npt.NDArray[np.floating]
|
|
879
|
+
Wind speed in the longitudinal direction, [:math:`m s^{-1}`]
|
|
880
|
+
v_wind : npt.NDArray[np.floating]
|
|
881
|
+
Wind speed in the latitudinal direction, [:math:`m s^{-1}`]
|
|
882
|
+
dt : npt.NDArray[np.timedelta64] | np.timedelta64
|
|
883
|
+
Advection timestep
|
|
884
|
+
|
|
885
|
+
Returns
|
|
886
|
+
-------
|
|
887
|
+
tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]
|
|
888
|
+
New longitude and latitude values, [:math:`\deg`]
|
|
889
|
+
|
|
890
|
+
Notes
|
|
891
|
+
-----
|
|
892
|
+
Near the poles, the longitude and latitude is converted to a 2-D Cartesian-like coordinate
|
|
893
|
+
system to avoid numerical instabilities and singularities caused by convergence of meridians.
|
|
894
|
+
|
|
895
|
+
See Also
|
|
896
|
+
--------
|
|
897
|
+
advect_longitude
|
|
898
|
+
advect_latitude
|
|
899
|
+
advect_horizontal
|
|
900
|
+
"""
|
|
901
|
+
# Determine hemisphere sign (1 for Northern Hemisphere, -1 for Southern Hemisphere)
|
|
902
|
+
hemisphere_sign = np.where(latitude > 0.0, 1.0, -1.0)
|
|
903
|
+
|
|
904
|
+
# Convert longitude and latitude to radians
|
|
905
|
+
sin_lon_rad = np.sin(units.degrees_to_radians(longitude))
|
|
906
|
+
cos_lon_rad = np.cos(units.degrees_to_radians(longitude))
|
|
907
|
+
|
|
908
|
+
# Convert longitude and latitude to 2-D Cartesian-like coordinate system, [:math:`\deg`]
|
|
909
|
+
polar_radius = 90.0 - np.abs(latitude)
|
|
910
|
+
x_cartesian = sin_lon_rad * polar_radius
|
|
911
|
+
y_cartesian = -cos_lon_rad * polar_radius * hemisphere_sign
|
|
912
|
+
|
|
913
|
+
# Convert winds from eastward and northward direction (u, v) to (X, Y), [:math:`\deg s^{-1}`]
|
|
914
|
+
x_wind = units.radians_to_degrees(
|
|
915
|
+
(u_wind * cos_lon_rad - v_wind * sin_lon_rad * hemisphere_sign) / constants.radius_earth
|
|
916
|
+
)
|
|
917
|
+
y_wind = units.radians_to_degrees(
|
|
918
|
+
(u_wind * sin_lon_rad * hemisphere_sign + v_wind * cos_lon_rad) / constants.radius_earth
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
# Advect contrails in 2-D Cartesian-like plane, [:math:`\deg`]
|
|
922
|
+
dtype = np.result_type(latitude, v_wind)
|
|
923
|
+
dt_s = units.dt_to_seconds(dt, dtype)
|
|
924
|
+
x_cartesian_new = x_cartesian + dt_s * x_wind
|
|
925
|
+
y_cartesian_new = y_cartesian + dt_s * y_wind
|
|
926
|
+
|
|
927
|
+
# Convert `y_cartesian_new` back to `latitude`, [:math:`\deg`]
|
|
928
|
+
dist_squared = x_cartesian_new**2 + y_cartesian_new**2
|
|
929
|
+
new_latitude = (90.0 - np.sqrt(dist_squared)) * hemisphere_sign
|
|
930
|
+
|
|
931
|
+
# Convert `x_cartesian_new` back to `longitude`, [:math:`\deg`]
|
|
932
|
+
new_lon_rad = np.arctan2(y_cartesian_new, x_cartesian_new)
|
|
933
|
+
|
|
934
|
+
new_longitude = np.where(
|
|
935
|
+
(x_wind == 0.0) & (y_wind == 0.0),
|
|
936
|
+
longitude,
|
|
937
|
+
90.0 + units.radians_to_degrees(new_lon_rad) * hemisphere_sign,
|
|
938
|
+
)
|
|
939
|
+
# new_longitude = 90.0 + units.radians_to_degrees(new_lon_rad) * hemisphere_sign
|
|
940
|
+
new_longitude = (new_longitude + 180.0) % 360.0 - 180.0 # wrap antimeridian
|
|
941
|
+
return new_longitude, new_latitude
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def advect_horizontal(
|
|
945
|
+
longitude: npt.NDArray[np.floating],
|
|
946
|
+
latitude: npt.NDArray[np.floating],
|
|
947
|
+
u_wind: npt.NDArray[np.floating],
|
|
948
|
+
v_wind: npt.NDArray[np.floating],
|
|
949
|
+
dt: npt.NDArray[np.timedelta64] | np.timedelta64,
|
|
950
|
+
) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
|
|
951
|
+
r"""Advect a particle in the horizontal plane.
|
|
952
|
+
|
|
953
|
+
This function calls :func:`advect_longitude` and :func:`advect_latitude` when
|
|
954
|
+
the position is far from the poles (<= 80.0 degrees). When the position is near
|
|
955
|
+
the poles (> 80.0 degrees), :func:`advect_longitude_and_latitude_near_poles`
|
|
956
|
+
is used instead.
|
|
957
|
+
|
|
958
|
+
Parameters
|
|
959
|
+
----------
|
|
960
|
+
longitude : npt.NDArray[np.floating]
|
|
961
|
+
Original longitude, [:math:`\deg`]
|
|
962
|
+
latitude : npt.NDArray[np.floating]
|
|
963
|
+
Original latitude, [:math:`\deg`]
|
|
964
|
+
u_wind : npt.NDArray[np.floating]
|
|
965
|
+
Wind speed in the longitudinal direction, [:math:`m s^{-1}`]
|
|
966
|
+
v_wind : npt.NDArray[np.floating]
|
|
967
|
+
Wind speed in the latitudinal direction, [:math:`m s^{-1}`]
|
|
968
|
+
dt : npt.NDArray[np.timedelta64] | np.timedelta64
|
|
969
|
+
Advection timestep
|
|
970
|
+
|
|
971
|
+
Returns
|
|
972
|
+
-------
|
|
973
|
+
tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]
|
|
974
|
+
New longitude and latitude values, [:math:`\deg`]
|
|
975
|
+
"""
|
|
976
|
+
near_poles = np.abs(latitude) > 80.0
|
|
977
|
+
|
|
978
|
+
longitude_out = np.empty_like(longitude)
|
|
979
|
+
latitude_out = np.empty_like(latitude)
|
|
980
|
+
|
|
981
|
+
# Use simple spherical advection if position is far from the poles (<= 80.0 degrees)
|
|
982
|
+
cond = ~near_poles
|
|
983
|
+
lon_cond = longitude[cond]
|
|
984
|
+
lat_cond = latitude[cond]
|
|
985
|
+
u_wind_cond = u_wind[cond]
|
|
986
|
+
v_wind_cond = v_wind[cond]
|
|
987
|
+
dt_cond = dt if isinstance(dt, np.timedelta64) else dt[cond]
|
|
988
|
+
longitude_out[cond] = advect_longitude(lon_cond, lat_cond, u_wind_cond, dt_cond)
|
|
989
|
+
latitude_out[cond] = advect_latitude(lat_cond, v_wind_cond, dt_cond)
|
|
990
|
+
|
|
991
|
+
# And use Cartesian-like advection if position is near the poles (> 80.0 degrees)
|
|
992
|
+
cond = near_poles
|
|
993
|
+
lon_cond = longitude[cond]
|
|
994
|
+
lat_cond = latitude[cond]
|
|
995
|
+
u_wind_cond = u_wind[cond]
|
|
996
|
+
v_wind_cond = v_wind[cond]
|
|
997
|
+
dt_cond = dt if isinstance(dt, np.timedelta64) else dt[cond]
|
|
998
|
+
lon_out_cond, lat_out_cond = advect_longitude_and_latitude_near_poles(
|
|
999
|
+
lon_cond, lat_cond, u_wind_cond, v_wind_cond, dt_cond
|
|
1000
|
+
)
|
|
1001
|
+
longitude_out[cond] = lon_out_cond
|
|
1002
|
+
latitude_out[cond] = lat_out_cond
|
|
1003
|
+
|
|
1004
|
+
return longitude_out, latitude_out
|
|
1005
|
+
|
|
1006
|
+
|
|
858
1007
|
# ---------------
|
|
859
1008
|
# Grid properties
|
|
860
1009
|
# ---------------
|