pycontrails 0.54.2__cp313-cp313-win_amd64.whl → 0.54.3__cp313-cp313-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.

Files changed (29) hide show
  1. pycontrails/_version.py +2 -2
  2. pycontrails/core/aircraft_performance.py +17 -3
  3. pycontrails/core/flight.py +3 -1
  4. pycontrails/core/rgi_cython.cp313-win_amd64.pyd +0 -0
  5. pycontrails/datalib/ecmwf/variables.py +1 -0
  6. pycontrails/datalib/landsat.py +5 -8
  7. pycontrails/datalib/sentinel.py +7 -11
  8. pycontrails/ext/bada.py +3 -2
  9. pycontrails/ext/synthetic_flight.py +3 -2
  10. pycontrails/models/accf.py +40 -19
  11. pycontrails/models/apcemm/apcemm.py +2 -1
  12. pycontrails/models/cocip/cocip.py +1 -2
  13. pycontrails/models/cocipgrid/cocip_grid.py +25 -20
  14. pycontrails/models/dry_advection.py +50 -54
  15. pycontrails/models/ps_model/__init__.py +2 -1
  16. pycontrails/models/ps_model/ps_aircraft_params.py +3 -2
  17. pycontrails/models/ps_model/ps_grid.py +187 -1
  18. pycontrails/models/ps_model/ps_model.py +4 -7
  19. pycontrails/models/ps_model/ps_operational_limits.py +39 -52
  20. pycontrails/physics/geo.py +149 -0
  21. pycontrails/physics/jet.py +141 -11
  22. pycontrails/physics/static/iata-cargo-load-factors-20241115.csv +71 -0
  23. pycontrails/physics/static/iata-passenger-load-factors-20241115.csv +71 -0
  24. {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/METADATA +9 -9
  25. {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/RECORD +29 -27
  26. {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/WHEEL +1 -1
  27. {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/LICENSE +0 -0
  28. {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/NOTICE +0 -0
  29. {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: float,
354
- aircraft_mass: float,
353
+ air_pressure: ArrayOrFloat,
354
+ aircraft_mass: ArrayOrFloat,
355
355
  atyp_param: PSAircraftEngineParams,
356
- ) -> float:
356
+ ) -> ArrayOrFloat:
357
357
  """
358
358
  Calculate minimum mach number to avoid stall.
359
359
 
360
360
  Parameters
361
361
  ----------
362
- air_pressure : float
362
+ air_pressure : ArrayOrFloat
363
363
  Ambient pressure, [:math:`Pa`]
364
- aircraft_mass : float
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
- float
372
- Maximum usable lift coefficient.
371
+ ArrayOrFloat
372
+ Minimum mach number to avoid stall.
373
373
  """
374
374
 
375
375
  def excess_mass(
376
- mach_number: float,
377
- air_pressure: float,
378
- aircraft_mass: float,
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
- ) -> float:
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.root_scalar(
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.5,
405
- x1=0.6,
406
- ).root
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: float,
413
- air_pressure: float,
414
- aircraft_mass: float,
415
- air_temperature: float,
416
- theta: float,
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
- ) -> float:
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 : float
426
+ altitude_ft : ArrayOrFloat
428
427
  Altitude, [:math:`ft`]
429
- air_pressure : float
428
+ air_pressure : ArrayOrFloat
430
429
  Ambient pressure, [:math:`Pa`]
431
- aircraft_mass : float
430
+ aircraft_mass : ArrayOrFloat
432
431
  Aircraft mass at each waypoint, [:math:`kg`]
433
- air_temperature : npt.NDArray[np.float64]
432
+ air_temperature : ArrayOrFloat
434
433
  Array of ambient temperature, [:math: `K`]
435
- theta : float | npt.NDArray[np.float64]
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
- float
443
- Maximum usable lift coefficient.
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
- # If the max mach number ignoring thrust limits is possible, return that value
455
- if (
456
- get_excess_thrust_available(
457
- mach_num_op_lim, air_temperature, air_pressure, aircraft_mass, theta, atyp_param
458
- )
459
- > 0
460
- ):
461
- return mach_num_op_lim
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
  # ----------------
@@ -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
  # ---------------