55from concurrent import futures
66from datetime import timedelta
77import warnings
8- from clouddrift .haversine import distance , bearing
8+ from clouddrift .haversine import distance , bearing , position_from_distance_and_bearing
99from clouddrift .dataformat import unpack_ragged
1010
1111
@@ -375,6 +375,207 @@ def segment(
375375 return np .concatenate (segment_sizes )
376376
377377
378+ def position_from_velocity (
379+ u : np .ndarray ,
380+ v : np .ndarray ,
381+ time : np .ndarray ,
382+ x_origin : float ,
383+ y_origin : float ,
384+ coord_system : Optional [str ] = "spherical" ,
385+ integration_scheme : Optional [str ] = "forward" ,
386+ time_axis : Optional [int ] = - 1 ,
387+ ) -> Tuple [np .ndarray , np .ndarray ]:
388+ """Compute positions from arrays of velocities and time and a pair of origin
389+ coordinates.
390+
391+ The units of the result are degrees if ``coord_system == "spherical"`` (default).
392+ If ``coord_system == "cartesian"``, the units of the result are equal to the
393+ units of the input velocities multiplied by the units of the input time.
394+ For example, if the input velocities are in meters per second and the input
395+ time is in seconds, the units of the result will be meters.
396+
397+ Integration scheme can take one of three values:
398+
399+ 1. "forward" (default): integration from x[i] to x[i+1] is performed
400+ using the velocity at x[i].
401+ 2. "backward": integration from x[i] to x[i+1] is performed using the
402+ velocity at x[i+1].
403+ 3. "centered": integration from x[i] to x[i+1] is performed using the
404+ arithmetic average of the velocities at x[i] and x[i+1]. Note that
405+ this method introduces some error due to the averaging.
406+
407+ u, v, and time can be multi-dimensional arrays. If the time axis, along
408+ which the finite differencing is performed, is not the last one (i.e.
409+ x.shape[-1]), use the ``time_axis`` optional argument to specify along which
410+ axis should the differencing be done. ``x``, ``y``, and ``time`` must have
411+ the same shape.
412+
413+ This function will not do any special handling of longitude ranges. If the
414+ integrated trajectory crosses the antimeridian (dateline) in either direction, the
415+ longitude values will not be adjusted to stay in any specific range such
416+ as [-180, 180] or [0, 360]. If you need your longitudes to be in a specific
417+ range, recast the resulting longitude from this function using the function
418+ :func:`clouddrift.sphere.recast_lon`.
419+
420+ Parameters
421+ ----------
422+ u : np.ndarray
423+ An array of eastward velocities.
424+ v : np.ndarray
425+ An array of northward velocities.
426+ time : np.ndarray
427+ An array of time values.
428+ x_origin : float
429+ Origin x-coordinate or origin longitude.
430+ y_origin : float
431+ Origin y-coordinate or origin latitude.
432+ coord_system : str, optional
433+ The coordinate system of the input. Can be "spherical" or "cartesian".
434+ Default is "spherical".
435+ integration_scheme : str, optional
436+ The difference scheme to use for computing the position. Can be
437+ "forward" or "backward". Default is "forward".
438+ time_axis : int, optional
439+ The axis of the time array. Default is -1, which corresponds to the
440+ last axis.
441+
442+ Returns
443+ -------
444+ x : np.ndarray
445+ An array of zonal displacements or longitudes.
446+ y : np.ndarray
447+ An array of meridional displacements or latitudes.
448+
449+ Examples
450+ --------
451+
452+ Simple integration on a plane, using the forward scheme by default:
453+
454+ >>> import numpy as np
455+ >>> from clouddrift.analysis import position_from_velocity
456+ >>> u = np.array([1., 2., 3., 4.])
457+ >>> v = np.array([1., 1., 1., 1.])
458+ >>> time = np.array([0., 1., 2., 3.])
459+ >>> x, y = position_from_velocity(u, v, time, 0, 0, coord_system="cartesian")
460+ >>> x
461+ array([0., 1., 3., 6.])
462+ >>> y
463+ array([0., 1., 2., 3.])
464+
465+ As above, but using centered scheme:
466+
467+ >>> x, y = position_from_velocity(u, v, time, 0, 0, coord_system="cartesian", integration_scheme="centered")
468+ >>> x
469+ array([0., 1.5, 4., 7.5])
470+ >>> y
471+ array([0., 1., 2., 3.])
472+
473+ Simple integration on a sphere (default):
474+
475+ >>> u = np.array([1., 2., 3., 4.])
476+ >>> v = np.array([1., 1., 1., 1.])
477+ >>> time = np.array([0., 1., 2., 3.]) * 1e5
478+ >>> x, y = position_from_velocity(u, v, time, 0, 0)
479+ >>> x
480+ array([0. , 0.89839411, 2.69584476, 5.39367518])
481+ >>> y
482+ array([0. , 0.89828369, 1.79601515, 2.69201609])
483+
484+ Integrating across the antimeridian (dateline) by default does not
485+ recast the resulting longitude:
486+
487+ >>> u = np.array([1., 1.])
488+ >>> v = np.array([0., 0.])
489+ >>> time = np.array([0, 1e5])
490+ >>> x, y = position_from_velocity(u, v, time, 179.5, 0)
491+ >>> x
492+ array([179.5 , 180.3983205])
493+ >>> y
494+ array([0., 0.])
495+
496+ Use the ``clouddrift.sphere.recast_lon`` function to recast the longitudes
497+ to the desired range:
498+
499+ >>> from clouddrift.sphere import recast_lon
500+ >>> recast_lon(x, -180)
501+ array([ 179.5 , -179.6016795])
502+
503+ Raises
504+ ------
505+ ValueError
506+ If the input arrays do not have the same shape.
507+ If the time axis is outside of the valid range ([-1, N-1]).
508+ If the input coordinate system is not "spherical" or "cartesian".
509+ If the input integration scheme is not "forward", "backward", or "centered"
510+
511+ See Also
512+ --------
513+ :func:`velocity_from_position`
514+ """
515+ # Positions and time arrays must have the same shape.
516+ if not u .shape == v .shape == time .shape :
517+ raise ValueError ("u, v, and time must have the same shape." )
518+
519+ # time_axis must be in valid range
520+ if time_axis < - 1 or time_axis > len (u .shape ) - 1 :
521+ raise ValueError (
522+ f"time_axis ({ time_axis } ) is outside of the valid range ([-1,"
523+ f" { len (x .shape ) - 1 } ])."
524+ )
525+
526+ # Nominal order of axes on input, i.e. (0, 1, 2, ..., N-1)
527+ target_axes = list (range (len (u .shape )))
528+
529+ # If time_axis is not the last one, transpose the inputs
530+ if time_axis != - 1 and time_axis < len (u .shape ) - 1 :
531+ target_axes .append (target_axes .pop (target_axes .index (time_axis )))
532+
533+ # Reshape the inputs to ensure the time axis is last (fast-varying)
534+ u_ = np .transpose (u , target_axes )
535+ v_ = np .transpose (v , target_axes )
536+ time_ = np .transpose (time , target_axes )
537+
538+ x = np .zeros (u_ .shape , dtype = u .dtype )
539+ y = np .zeros (v_ .shape , dtype = v .dtype )
540+
541+ dt = np .diff (time_ )
542+
543+ if integration_scheme .lower () == "forward" :
544+ x [..., 1 :] = np .cumsum (u_ [..., :- 1 ] * dt , axis = - 1 )
545+ y [..., 1 :] = np .cumsum (v_ [..., :- 1 ] * dt , axis = - 1 )
546+ elif integration_scheme .lower () == "backward" :
547+ x [..., 1 :] = np .cumsum (u_ [1 :] * dt , axis = - 1 )
548+ y [..., 1 :] = np .cumsum (v_ [1 :] * dt , axis = - 1 )
549+ elif integration_scheme .lower () == "centered" :
550+ x [..., 1 :] = np .cumsum (0.5 * (u_ [..., :- 1 ] + u_ [..., 1 :]) * dt , axis = - 1 )
551+ y [..., 1 :] = np .cumsum (0.5 * (v_ [..., :- 1 ] + v_ [..., 1 :]) * dt , axis = - 1 )
552+ else :
553+ raise ValueError (
554+ 'integration_scheme must be "forward", "backward", or "centered".'
555+ )
556+
557+ if coord_system .lower () == "cartesian" :
558+ x += x_origin
559+ y += y_origin
560+ elif coord_system .lower () == "spherical" :
561+ dx = np .diff (x )
562+ dy = np .diff (y )
563+ distances = np .sqrt (dx ** 2 + dy ** 2 )
564+ bearings = np .arctan2 (dy , dx )
565+ x [..., 0 ], y [..., 0 ] = x_origin , y_origin
566+ for n in range (distances .shape [- 1 ]):
567+ y [..., n + 1 ], x [..., n + 1 ] = position_from_distance_and_bearing (
568+ y [..., n ], x [..., n ], distances [..., n ], bearings [..., n ]
569+ )
570+ else :
571+ raise ValueError ('coord_system must be "spherical" or "cartesian".' )
572+
573+ if target_axes == list (range (len (u .shape ))):
574+ return x , y
575+ else :
576+ return np .transpose (x , target_axes ), np .transpose (y , target_axes )
577+
578+
378579def velocity_from_position (
379580 x : np .ndarray ,
380581 y : np .ndarray ,
@@ -394,9 +595,9 @@ def velocity_from_position(
394595 units of seconds, the resulting velocity is in the units of meters per
395596 second. Otherwise, if coord_system == "cartesian", the units of the
396597 resulting velocity correspond to the units of the input. For example,
397- if Easting and Northing are in the units of kilometers and time is in
398- the units of hours, the resulting velocity is in the units of kilometers
399- per hour.
598+ if zonal and meridional displacements are in the units of kilometers and
599+ time is in the units of hours, the resulting velocity is in the units of
600+ kilometers per hour.
400601
401602 x, y, and time can be multi-dimensional arrays. If the time axis, along
402603 which the finite differencing is performed, is not the last one (i.e.
@@ -421,16 +622,39 @@ def velocity_from_position(
421622 case of a centered difference scheme, the start and end boundary points are
422623 evaluated using the forward and backward difference scheme, respectively.
423624
424- Args:
425- x (array_like): An N-d array of x-positions (longitude in degrees or easting in any unit)
426- y (array_like): An N-d array of y-positions (latitude in degrees or northing in any unit)
427- time (array_like): An N-d array of times as floating point values (in any unit)
428- coord_system (str, optional): Coordinate system that x and y arrays are in; possible values are "spherical" (default) or "cartesian".
429- difference_scheme (str, optional): Difference scheme to use; possible values are "forward", "backward", and "centered".
430- time_axis (int, optional): Axis along which to differentiate (default is -1)
625+ Parameters
626+ ----------
627+ x : array_like
628+ An N-d array of x-positions (longitude in degrees or zonal displacement in any unit)
629+ y : array_like
630+ An N-d array of y-positions (latitude in degrees or meridional displacement in any unit)
631+ time : array_like
632+ An N-d array of times as floating point values (in any unit)
633+ coord_system : str, optional
634+ Coordinate system that x and y arrays are in; possible values are "spherical" (default) or "cartesian".
635+ difference_scheme : str, optional
636+ Difference scheme to use; possible values are "forward", "backward", and "centered".
637+ time_axis : int, optional)
638+ Axis along which to differentiate (default is -1)
431639
432- Returns:
433- out (Tuple[xr.DataArray[float], xr.DataArray[float]]): Arrays of x- and y-velocities
640+ Returns
641+ -------
642+ u : np.ndarray
643+ Zonal velocity
644+ v : np.ndarray
645+ Meridional velocity
646+
647+ Raises
648+ ------
649+ ValueError
650+ If x, y, and time do not have the same shape.
651+ If time_axis is outside of the valid range.
652+ If coord_system is not "spherical" or "cartesian".
653+ If difference_scheme is not "forward", "backward", or "centered".
654+
655+ See Also
656+ --------
657+ :function:`position_from_velocity`
434658 """
435659
436660 # Positions and time arrays must have the same shape.
@@ -536,10 +760,14 @@ def velocity_from_position(
536760
537761 elif coord_system == "spherical" :
538762 # Inner values
539- distances = distance (y_ [..., :- 2 ], x_ [..., :- 2 ], y_ [..., 2 :], x_ [..., 2 :])
540- bearings = bearing (y_ [..., :- 2 ], x_ [..., :- 2 ], y_ [..., 2 :], x_ [..., 2 :])
541- dx [..., 1 :- 1 ] = distances * np .cos (bearings ) / 2
542- dy [..., 1 :- 1 ] = distances * np .sin (bearings ) / 2
763+ y1 = (y_ [..., :- 2 ] + y_ [..., 1 :- 1 ]) / 2
764+ x1 = (x_ [..., :- 2 ] + x_ [..., 1 :- 1 ]) / 2
765+ y2 = (y_ [..., 2 :] + y_ [..., 1 :- 1 ]) / 2
766+ x2 = (x_ [..., 2 :] + x_ [..., 1 :- 1 ]) / 2
767+ distances = distance (y1 , x1 , y2 , x2 )
768+ bearings = bearing (y1 , x1 , y2 , x2 )
769+ dx [..., 1 :- 1 ] = distances * np .cos (bearings )
770+ dy [..., 1 :- 1 ] = distances * np .sin (bearings )
543771
544772 # Boundary values
545773 distance1 = distance (y_ [..., 0 ], x_ [..., 0 ], y_ [..., 1 ], x_ [..., 1 ])
0 commit comments