Ejemplo n.º 1
0
def orientation_polygon_along_equator(polygon):
    """Rotate the polygon such that its major axis is aligned along the equator."""
    
    polygon_centroid = polygon.get_boundary_centroid()
    
    # Rotate polygon so its centroid in on equator (nearest point on equator to original centroid).
    rotation_to_equator = get_rotation_to_equator(polygon_centroid)
    equator_polygon = rotation_to_equator * polygon
    equator_polygon_centroid = rotation_to_equator * polygon_centroid
    
    # Tessellate polygon on equator so we have enough points to do PCA analysis of polygon's boundary.
    tesselated_equator_polygon = equator_polygon.to_tessellated(math.radians(1))
    major_axis_orientation_angle_radians = get_major_axis_orientation_angle(
            equator_polygon_centroid.to_lat_lon(),
            tesselated_equator_polygon.to_lat_lon_list())
    
    # Rotate polygon such that major axis is aligned with equator.
    reorient_major_axis = pygplates.FiniteRotation(
            equator_polygon_centroid,
            -major_axis_orientation_angle_radians)
    oriented_polygon = reorient_major_axis * equator_polygon
    
    #print 'Rotation to equator %s' % rotation_to_equator
    #print 'Rotation to reorient major axis %s' % reorient_major_axis
    
    composed_rotation =  reorient_major_axis * rotation_to_equator
    
    return oriented_polygon,composed_rotation
Ejemplo n.º 2
0
def get_rotation_to_equator(point):
    """Get rotation from 'point' to nearest position on equator."""
    
    rotate_from_north_to_point = pygplates.FiniteRotation(pygplates.PointOnSphere.north_pole, point)
    
    if rotate_from_north_to_point.represents_identity_rotation():
        # Point coincides with North Pole, so just choose any rotation pole on the equator to rotate point with.
        # The point could end up anywhere along equator.
        rotate_from_north_to_point_pole = pygplates.PointOnSphere(0, 0)
        rotate_from_north_to_point_angle = 0.5 * math.pi # Rotate 90 degrees from North Pole to equator.
    else:
        rotate_from_north_to_point_pole, rotate_from_north_to_point_angle = rotate_from_north_to_point.get_euler_pole_and_angle()
    
    return pygplates.FiniteRotation(
            rotate_from_north_to_point_pole,
            0.5 * math.pi - rotate_from_north_to_point_angle)
Ejemplo n.º 3
0
def orientation_polygon_along_meridian(polygon):

    median_longitude = np.median(polygon.to_lat_lon_array()[:,1])

    reorient_major_axis = pygplates.FiniteRotation(
            pygplates.PointOnSphere(90,0),
            -np.radians(median_longitude))
    oriented_polygon = reorient_major_axis * polygon

    return oriented_polygon,reorient_major_axis
def get_mid_ocean_ridges(shared_boundary_sections,
                         rotation_model,
                         reconstruction_time,
                         time_step,
                         sampling=2.0):
    """ Get tessellated points along a mid ocean ridge"""

    shifted_mor_points = []

    for shared_boundary_section in shared_boundary_sections:
        # The shared sub-segments contribute either to the ridges or to the subduction zones.
        if shared_boundary_section.get_feature().get_feature_type(
        ) == pygplates.FeatureType.create_gpml('MidOceanRidge'):
            # Ignore zero length segments - they don't have a direction.
            spreading_feature = shared_boundary_section.get_feature()

            # Find the stage rotation of the spreading feature in the frame of reference of its
            # geometry at the current reconstruction time (the MOR is currently actively spreading).
            # The stage pole can then be directly geometrically compared to the *reconstructed* spreading geometry.
            stage_rotation = separate_ridge_transform_segments.get_stage_rotation_for_reconstructed_geometry(
                spreading_feature, rotation_model, reconstruction_time)
            if not stage_rotation:
                # Skip current feature - it's not a spreading feature.
                continue

            # Get the stage pole of the stage rotation.
            # Note that the stage rotation is already in frame of reference of the *reconstructed* geometry at the spreading time.
            stage_pole, _ = stage_rotation.get_euler_pole_and_angle()

            # One way rotates left and the other right, but don't know which - doesn't matter in our example though.
            rotate_slightly_off_mor_one_way = pygplates.FiniteRotation(
                stage_pole, np.radians(0.01))
            rotate_slightly_off_mor_opposite_way = rotate_slightly_off_mor_one_way.get_inverse(
            )

            # Iterate over the shared sub-segments.
            for shared_sub_segment in shared_boundary_section.get_shared_sub_segments(
            ):

                # Tessellate MOR section.
                mor_points = pygplates.MultiPointOnSphere(
                    shared_sub_segment.get_resolved_geometry().to_tessellated(
                        np.radians(sampling)))

                # NOTE temporary hack to avoid seed points at ridge trench intersections
                for point in mor_points.get_points()[1:-1]:
                    # Append shifted geometries (one with points rotated one way and the other rotated the opposite way).
                    shifted_mor_points.append(rotate_slightly_off_mor_one_way *
                                              point)
                    shifted_mor_points.append(
                        rotate_slightly_off_mor_opposite_way * point)

    #print shifted_mor_points
    return shifted_mor_points
    def _create_node(self, node_centre_lon, node_centre_lat,
                     node_half_width_degrees, is_north_hemisphere):

        # Create the points of the polygon bounding the current quad tree node.

        bounding_polygon_points = []

        left_lon = node_centre_lon - node_half_width_degrees

        right_lon = node_centre_lon + node_half_width_degrees

        bottom_lat = node_centre_lat - node_half_width_degrees

        top_lat = node_centre_lat + node_half_width_degrees

        # Northern and southern hemispheres handled separately.

        if is_north_hemisphere:

            # Northern hemisphere.

            left_boundary = pygplates.PolylineOnSphere([(0, left_lon),
                                                        (90, left_lon)])

            right_boundary = pygplates.PolylineOnSphere([(0, right_lon),
                                                         (90, right_lon)])

            # Midpoint of small circle arc bounding the bottom of quad tree node.

            bottom_mid_point = pygplates.PointOnSphere(
                bottom_lat, 0.5 * (left_lon + right_lon))

            # Find the great circle (rotation) that passes through the bottom midpoint (and is oriented towards North pole).

            bottom_great_circle_rotation_axis = pygplates.Vector3D.cross(
                bottom_mid_point.to_xyz(),
                pygplates.Vector3D.cross(
                    pygplates.PointOnSphere.north_pole.to_xyz(),
                    bottom_mid_point.to_xyz())).to_normalised()

            bottom_great_circle_rotation = pygplates.FiniteRotation(
                bottom_great_circle_rotation_axis.to_xyz(), 0.5 * math.pi)

            # Intersect great circle bottom boundary with left and right boundaries to find bottom-left and bottom-right points.

            # The bottom boundary is actually a small circle (due to lat/lon grid), but since we need to use *great* circle arcs

            # in our geometries we need to be a bit loose with our bottom boundary otherwise it will go inside the quad tree node.

            bottom_boundary = pygplates.PolylineOnSphere([
                bottom_great_circle_rotation * bottom_mid_point,
                bottom_mid_point,
                bottom_great_circle_rotation.get_inverse() * bottom_mid_point
            ])

            _, _, bottom_left_point = pygplates.GeometryOnSphere.distance(
                bottom_boundary, left_boundary, return_closest_positions=True)

            _, _, bottom_right_point = pygplates.GeometryOnSphere.distance(
                bottom_boundary, right_boundary, return_closest_positions=True)

            bounding_polygon_points.append(bottom_left_point)

            bounding_polygon_points.append(bottom_right_point)

            bounding_polygon_points.append(
                pygplates.PointOnSphere(top_lat, right_lon))

            bounding_polygon_points.append(
                pygplates.PointOnSphere(top_lat, left_lon))

        else:

            # Southern hemisphere.

            left_boundary = pygplates.PolylineOnSphere([(0, left_lon),
                                                        (-90, left_lon)])

            right_boundary = pygplates.PolylineOnSphere([(0, right_lon),
                                                         (-90, right_lon)])

            # Midpoint of small circle arc bounding the top of quad tree node.

            top_mid_point = pygplates.PointOnSphere(
                top_lat, 0.5 * (left_lon + right_lon))

            # Find the great circle (rotation) that passes through the top midpoint (and is oriented towards North pole).

            top_great_circle_rotation_axis = pygplates.Vector3D.cross(
                top_mid_point.to_xyz(),
                pygplates.Vector3D.cross(
                    pygplates.PointOnSphere.north_pole.to_xyz(),
                    top_mid_point.to_xyz())).to_normalised()

            top_great_circle_rotation = pygplates.FiniteRotation(
                top_great_circle_rotation_axis.to_xyz(), 0.5 * math.pi)

            # Intersect great circle top boundary with left and right boundaries to find top-left and top-right points.

            # The top boundary is actually a small circle (due to lat/lon grid), but since we need to use *great* circle arcs

            # in our geometries we need to be a bit loose with our top boundary otherwise it will go inside the quad tree node.

            top_boundary = pygplates.PolylineOnSphere([
                top_great_circle_rotation * top_mid_point, top_mid_point,
                top_great_circle_rotation.get_inverse() * top_mid_point
            ])

            _, _, top_left_point = pygplates.GeometryOnSphere.distance(
                top_boundary, left_boundary, return_closest_positions=True)

            _, _, top_right_point = pygplates.GeometryOnSphere.distance(
                top_boundary, right_boundary, return_closest_positions=True)

            bounding_polygon_points.append(top_left_point)

            bounding_polygon_points.append(top_right_point)

            bounding_polygon_points.append(
                pygplates.PointOnSphere(bottom_lat, right_lon))

            bounding_polygon_points.append(
                pygplates.PointOnSphere(bottom_lat, left_lon))

        bounding_polygon = pygplates.PolygonOnSphere(bounding_polygon_points)

        return QuadTreeNode(bounding_polygon)
Ejemplo n.º 6
0
def warp_subduction_segment(tessellated_line,
                            rotation_model,
                            subducting_plate_id,
                            overriding_plate_id,
                            subduction_polarity,
                            time,
                            end_time,
                            time_step,
                            dip_angle_radians,
                            subducting_plate_disappearance_time=-1,
                            use_small_circle_path=False):

    # We need to reverse the subducting_normal vector direction if overriding plate is to
    # the right of the subducting line since great circle arc normal is always to the left.
    if subduction_polarity == 'Left':
        subducting_normal_reversal = 1
    else:
        subducting_normal_reversal = -1

    # tesselate the line, and create an array with the same length as the tesselated points
    # with zero as the starting depth for each point at t=0
    points = [point for point in tessellated_line]
    point_depths = [0. for point in points]

    # Need at least two points for a polyline. Otherwise, return None for all results
    if len(points) < 2:
        points = None; point_depths = None; polyline = None
        return points, point_depths, polyline

    polyline = pygplates.PolylineOnSphere(points)

    warped_polylines = []

    # Add original unwarped polyline first.
    warped_polylines.append(polyline)

    #warped_end_time = time - warped_time_interval
    warped_end_time = end_time
    if warped_end_time < 0:
        warped_end_time = 0

    # iterate over each time in the range defined by the input parameters
    for warped_time in np.arange(time, warped_end_time-time_step,-time_step):

        #if warped_time<=23. and subducting_plate_id==902:
        #    if overriding_plate_id in [224,0,101]:
        #        print('forcing Farallon to become Cocos where overriding plate id is %d' % overriding_plate_id)
        #        subducting_plate_id = 909
        #    else:
        #        print('forcing Farallon to become Nazca where overriding plate id is %d' % overriding_plate_id)
        #        subducting_plate_id = 911
        if warped_time<=subducting_plate_disappearance_time:
            print('Using %0.2f to %0.2f Ma stage pole for plate %d' % (subducting_plate_disappearance_time+time_step, subducting_plate_disappearance_time, subducting_plate_id))
            stage_rotation = rotation_model.get_rotation(subducting_plate_disappearance_time+time_step, 
                                                         subducting_plate_id, 
                                                         subducting_plate_disappearance_time)

        else:
            # the stage rotation that describes the motion of the subducting plate,
            # with respect to the fixed plate for the rotation model
            stage_rotation = rotation_model.get_rotation(warped_time-time_step, subducting_plate_id, warped_time)

        if use_small_circle_path:
            stage_pole, stage_pole_angle_radians = stage_rotation.get_euler_pole_and_angle()

        # get velocity vectors at each point along polyline
        relative_velocity_vectors = pygplates.calculate_velocities(
                points,
                stage_rotation,
                time_step,
                pygplates.VelocityUnits.kms_per_my)

        # Get subducting normals for each segment of tessellated polyline.
        # Also add an imaginary normal prior to first and post last points
        # (makes its easier to later calculate average normal at tessellated points).
        # The number of normals will be one greater than the number of points.
        subducting_normals = []
        subducting_normals.append(None) # Imaginary segment prior to first point.
        for segment in polyline.get_segments():
            if segment.is_zero_length():
                subducting_normals.append(None)
            else:
                # The normal to the subduction zone in the direction of subduction (towards overriding plate).
                subducting_normals.append(
                    subducting_normal_reversal * segment.get_great_circle_normal())
        subducting_normals.append(None) # Imaginary segment after to last point.

        # get vectors of normals and parallels for each segment, use these 
        # to get a normal and parallel at each point location
        normals = []
        parallels = []            
        for point_index in range(len(points)):
            prev_normal = subducting_normals[point_index]
            next_normal = subducting_normals[point_index + 1]

            if prev_normal is None and next_normal is None:
                # Skip point altogether (both adjoining segments are zero length).
                continue

            if prev_normal is None:
                normal = next_normal
            elif next_normal is None:
                normal = prev_normal
            else:
                normal = (prev_normal + next_normal).to_normalised()

            parallel = pygplates.Vector3D.cross(point.to_xyz(), normal).to_normalised()

            normals.append(normal)
            parallels.append(parallel)

        # iterate over each point to determine the incremented position 
        # based on plate motion and subduction dip
        warped_points = []
        warped_point_depths = []
        for point_index, point in enumerate(points):
            normal = normals[point_index]
            parallel = parallels[point_index]

            velocity = relative_velocity_vectors[point_index]
            if velocity.is_zero_magnitude():
                # Point hasn't moved.
                warped_points.append(point)
                warped_point_depths.append(point_depths[point_index])
                continue

            # reconstruct the tracked point from position at current time to
            # position at the next time step
            normal_angle = pygplates.Vector3D.angle_between(velocity, normal)
            parallel_angle = pygplates.Vector3D.angle_between(velocity, parallel)

            # Trench parallel and normal components of velocity.
            velocity_normal = np.cos(normal_angle) * velocity.get_magnitude()
            velocity_parallel = np.cos(parallel_angle) * velocity.get_magnitude()

            normal_vector = normal.to_normalised() * velocity_normal
            parallel_vector = parallel.to_normalised() * velocity_parallel

            # Adjust velocity based on subduction vertical dip angle.
            velocity_dip = parallel_vector + np.cos(dip_angle_radians) * normal_vector

            #deltaZ is the amount that this point increases in depth within the time step
            deltaZ = np.sin(dip_angle_radians) * velocity.get_magnitude()

            # Should be 90 degrees always.
            #print np.degrees(np.arccos(pygplates.Vector3D.dot(normal_vector, parallel_vector)))

            if use_small_circle_path:
                # Rotate original stage pole by the same angle that effectively
                # rotates the velocity vector to the dip velocity vector.
                dip_stage_pole_rotate = pygplates.FiniteRotation(
                        point,
                        pygplates.Vector3D.angle_between(velocity_dip, velocity))
                dip_stage_pole = dip_stage_pole_rotate * stage_pole
            else:
                # Get the unnormalised vector perpendicular to both the point and velocity vector.
                dip_stage_pole_x, dip_stage_pole_y, dip_stage_pole_z = pygplates.Vector3D.cross(
                        point.to_xyz(), velocity_dip).to_xyz()

                # PointOnSphere requires a normalised (ie, unit length) vector (x, y, z).
                dip_stage_pole = pygplates.PointOnSphere(
                        dip_stage_pole_x, dip_stage_pole_y, dip_stage_pole_z, normalise=True)


            # Get angle that velocity will rotate seed point along great circle arc 
            # over 'time_step' My (if velocity in Kms / My).
            dip_stage_angle_radians = velocity_dip.get_magnitude() * (
                    time_step / pygplates.Earth.mean_radius_in_kms)

            if use_small_circle_path:
                # Increase rotation angle to adjust for fact that we're moving a
                # shorter distance with small circle (compared to great circle).
                dip_stage_angle_radians /= np.abs(np.sin(
                        pygplates.Vector3D.angle_between(
                                dip_stage_pole.to_xyz(), point.to_xyz())))
                # Use same sign as original stage rotation.
                if stage_pole_angle_radians < 0:
                    dip_stage_angle_radians = -dip_stage_angle_radians

            # get the stage rotation that describes the lateral motion of the 
            # point taking the dip into account
            dip_stage_rotation = pygplates.FiniteRotation(dip_stage_pole, dip_stage_angle_radians)

            # increment the point long,lat and depth
            warped_point = dip_stage_rotation * point
            warped_points.append(warped_point)
            warped_point_depths.append(point_depths[point_index] + deltaZ)

        # finished warping all points in polyline
        # --> increment the polyline for this time step
        warped_polyline = pygplates.PolylineOnSphere(warped_points)
        warped_polylines.append(warped_polyline)

        # For next warping iteration.
        points = warped_points
        polyline = warped_polyline
        point_depths = warped_point_depths
        
    return points, point_depths, polyline
Ejemplo n.º 7
0
def extract_plate_pair_stage_rotations(
        rotation_feature_collections,
        plate_pair_filter=None):
    # Docstring in numpydoc format...
    """Calculate stage rotations between consecutive finite rotations in each specified plate pair.
    
    Parameters
    ----------
    rotation_feature_collections : sequence of (str, or sequence of pygplates.Feature, or pygplates.FeatureCollection, or pygplates.Feature)
        A sequence of rotation feature collections.
        Each collection in the sequence can be a rotation filename, or a sequence (eg, list of tuple) or features, or
        a feature collection, or even a single feature.
    plate_pair_filter : Filter function accepting accepting 3 arguments (moving_plate_id, fixed_plate_id, rotation_sequence), or sequence of 2-tuple (moving_plate_id, fixed_plate_id), optional
        Optional filtering of plate pairs to apply operation to.
        Filter function (callable) accepting 3 arguments (), or
        a sequence of (moving, fixed) plate pairs to limit operation to.
    
    Returns
    -------
    list of pygplates.FeatureCollection
        The modified feature collections.
        Returned list is same length as ``rotation_feature_collections``.
    
    Notes
    -----
    The results are returned as a list of pygplates.FeatureCollection (one per input rotation feature collection).
    
    Note that only the rotation features satisfying ``plate_pair_filter`` (if specified) are returned.
    So if a returned feature collection is empty then it means all of its features were filtered out.
    """
    
    if plate_pair_filter is None:
        plate_pair_filter = _all_filter
    # If caller specified a sequence of moving/fixed plate pairs then use them, otherwise it's a filter function (callable).
    elif hasattr(plate_pair_filter, '__iter__'):
        plate_pair_filter = _is_in_plate_pair_sequence(plate_pair_filter)
    # else ...'plate_pair_filter' is a callable...
    
    output_rotation_feature_collections = []
    for rotation_feature_collection in rotation_feature_collections:
        # Create an empty output rotation feature collection for each input rotation feature collection.
        # We'll only add output features when an input feature is modified.
        output_rotation_feature_collection = []
        output_rotation_feature_collections.append(output_rotation_feature_collection)
        
        for rotation_feature in rotation_feature_collection:
            # Get the rotation feature information.
            total_reconstruction_pole = rotation_feature.get_total_reconstruction_pole()
            if not total_reconstruction_pole:
                # Not a rotation feature.
                continue
        
            fixed_plate_id, moving_plate_id, rotation_sequence = total_reconstruction_pole
            # We're only interested in rotation features with matching moving/fixed plate IDs.
            if not plate_pair_filter(fixed_plate_id, moving_plate_id, rotation_sequence):
                continue
            
            # Clone the input feature before we start making modifications.
            # Otherwise we'll be modifying the input rotation feature (which the caller might not expect).
            output_rotation_feature = rotation_feature.clone()
            
            _, _, output_rotation_sequence = output_rotation_feature.get_total_reconstruction_pole()
            
            # Get the enabled rotation samples - ignore the disabled samples.
            output_enabled_rotation_samples = output_rotation_sequence.get_enabled_time_samples()
            if not output_enabled_rotation_samples:
                # No time samples are enabled.
                continue
            
            prev_finite_rotation = output_enabled_rotation_samples[0].get_value().get_finite_rotation()
            # Replace first finite rotation with identity rotation.
            # This is the stage rotation of first finite rotation (which has no previous finite rotation).
            output_enabled_rotation_samples[0].get_value().set_finite_rotation(pygplates.FiniteRotation())
            
            for rotation_sample_index in range(1, len(output_enabled_rotation_samples)):
                time_sample = output_enabled_rotation_samples[rotation_sample_index]
                finite_rotation = time_sample.get_value().get_finite_rotation()
                
                # The finite rotation at current time tc is composed of finite rotation at
                # previous (younger) time tp and stage rotation between them:
                #   R(0->tc) = R(tp->tc) * R(0->tp)
                # The stage rotation from tp->tc:
                #   R(tp->tc) = R(0->tc) * inverse[R(0->tp)]
                stage_rotation = finite_rotation * prev_finite_rotation.get_inverse()
                
                # Replace current finite rotation with stage rotation.
                time_sample.get_value().set_finite_rotation(stage_rotation)
                
                prev_finite_rotation = finite_rotation
            
            output_rotation_feature_collection.append(output_rotation_feature)
    
    # Return our output feature collections as a list of pygplates.FeatureCollection.
    return [pygplates.FeatureCollection(rotation_feature_collection)
        for rotation_feature_collection in output_rotation_feature_collections]
def extract_plate_pair_stage_rotations(rotation_feature_collections,
                                       plate_pair_filter=None):
    # Docstring in numpydoc format...
    """Calculate stage rotations between consecutive finite rotations in each specified plate pair.
    
    The results are returned as a list of pygplates.FeatureCollection (one per input rotation feature collection).
    
    Parameters
    ----------
    rotation_feature_collections : sequence of (str, or sequence of pygplates.Feature, or pygplates.FeatureCollection, or pygplates.Feature)
        A sequence of rotation feature collections.
        Each collection in the sequence can be a rotation filename, or a sequence (eg, list of tuple) or features, or
        a feature collection, or even a single feature.
    plate_pair_filter : Filter function accepting accepting 3 arguments (moving_plate_id, fixed_plate_id, rotation_sequence), or sequence of 2-tuple (moving_plate_id, fixed_plate_id), optional
        Optional filtering of plate pairs to apply operation to.
        Filter function (callable) accepting 3 arguments (), or
        a sequence of (moving, fixed) plate pairs to limit operation to.
    
    Returns
    -------
    list of pygplates.FeatureCollection
        The modified feature collections.
        Returned list is same length as ``rotation_feature_collections``.
    """

    # Convert each feature collection into a list of features for easier manipulation.
    rotation_feature_collections = [
        list(pygplates.FeatureCollection(rotation_feature_collection))
        for rotation_feature_collection in rotation_feature_collections
    ]

    if plate_pair_filter is None:
        plate_pair_filter = _all_filter
    # If caller specified a sequence of moving/fixed plate pairs then use them, otherwise it's a filter function (callable).
    elif hasattr(plate_pair_filter, '__iter__'):
        plate_pair_filter = _is_in_plate_pair_sequence(plate_pair_filter)
    # else ...'plate_pair_filter' is a callable...

    for rotation_feature_collection in rotation_feature_collections:
        for rotation_feature in rotation_feature_collection:
            # Get the rotation feature information.
            total_reconstruction_pole = rotation_feature.get_total_reconstruction_pole(
            )
            if not total_reconstruction_pole:
                # Not a rotation feature.
                continue

            fixed_plate_id, moving_plate_id, rotation_sequence = total_reconstruction_pole
            # We're only interested in rotation features with matching moving/fixed plate IDs.
            if not plate_pair_filter(fixed_plate_id, moving_plate_id,
                                     rotation_sequence):
                continue

            # Get the enabled rotation samples - ignore the disabled samples.
            enabled_rotation_samples = rotation_sequence.get_enabled_time_samples(
            )
            if len(enabled_rotation_samples) < 2:
                # Need at least two time samples to find a stage rotation (between them).
                continue

            prev_finite_rotation = enabled_rotation_samples[0].get_value(
            ).get_finite_rotation()
            # Replace first finite rotation with identity rotation.
            # This is the stage rotation of first finite rotation (which has no previous finite rotation).
            enabled_rotation_samples[0].get_value().set_finite_rotation(
                pygplates.FiniteRotation())

            for rotation_sample_index in range(1,
                                               len(enabled_rotation_samples)):
                time_sample = enabled_rotation_samples[rotation_sample_index]
                finite_rotation = time_sample.get_value().get_finite_rotation()

                # The finite rotation at current time tc is composed of finite rotation at
                # previous (younger) time tp and stage rotation between them:
                #   R(0->tc) = R(tp->tc) * R(0->tp)
                # The stage rotation from tp->tc:
                #   R(tp->tc) = R(0->tc) * inverse[R(0->tp)]
                stage_rotation = finite_rotation * prev_finite_rotation.get_inverse(
                )

                # Replace current finite rotation with stage rotation.
                time_sample.get_value().set_finite_rotation(stage_rotation)

                prev_finite_rotation = finite_rotation

    # Return our (potentially) modified feature collections as a list of pygplates.FeatureCollection.
    return [
        pygplates.FeatureCollection(rotation_feature_collection)
        for rotation_feature_collection in rotation_feature_collections
    ]