Module gplately.ptt.diagnose_rotations

Copyright (C) 2020 The University of Sydney, Australia

This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, version 2, as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program; if not, write to Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

Expand source code
"""
    Copyright (C) 2020 The University of Sydney, Australia
    
    This program is free software; you can redistribute it and/or modify it under
    the terms of the GNU General Public License, version 2, as published by
    the Free Software Foundation.
    
    This program is distributed in the hope that it will be useful, but WITHOUT
    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
    for more details.
    
    You should have received a copy of the GNU General Public License along
    with this program; if not, write to Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
"""

from __future__ import print_function

import argparse
import os.path
import sys

import pygplates

# The default is useful when the rotations were loaded from a PLATES rotation file
# that stored rotation lat/lon/angle to 2 decimal places of accuracy.
DEFAULT_ROTATION_THRESHOLD_DEGREES = 0.01


def diagnose_rotations(
    rotation_features, rotation_threshold_degrees=DEFAULT_ROTATION_THRESHOLD_DEGREES
):
    """
    Diagnose one or more rotation files to check for inconsistencies.

    'rotation_features' can be a rotation feature collection, or rotation filename, or rotation feature, or
    sequence of rotation features, or a sequence (eg, list or tuple) of any combination of those four types.

    'rotation_threshold_degrees' allows two rotations to compare equal if their rotation latitude, longitude or angle
    differ by less than the specified amount (in degrees). The default value is useful for some PLATES rotation files
    that are typically accurate to 2 decimal places (or threshold of 0.01).
    Specifing 'None' is equivalent to having no threshold (see pygplates.FiniteRotation.are_equal).
    """

    # Use helper class to convert 'rotation_features' argument to a list of features.
    rotation_features = pygplates.FeaturesFunctionArgument(rotation_features)
    rotation_feature_sequence = rotation_features.get_features()

    # Make sure threshold is a number.
    if rotation_threshold_degrees is not None:
        rotation_threshold_degrees = float(rotation_threshold_degrees)

    # A 'dict' to map each moving plate to a list of total reconstruction poles
    # (one per moving/fixed plate pair)
    total_reconstruction_poles_by_moving_plate = {}

    # Get the moving/fixed total reconstruction poles.
    for rotation_feature in rotation_feature_sequence:
        total_reconstruction_pole = rotation_feature.get_total_reconstruction_pole()
        # If the current feature is a valid rotation feature...
        if total_reconstruction_pole:
            (
                fixed_plate_id,
                moving_plate_id,
                rotation_sequence,
            ) = total_reconstruction_pole
            # Each moving plate has a list of total reconstruction poles.
            total_reconstruction_poles = (
                total_reconstruction_poles_by_moving_plate.setdefault(
                    moving_plate_id, []
                )
            )
            total_reconstruction_poles.append(total_reconstruction_pole)

    # Iterate over the moving plates.
    for (
        moving_plate_id,
        total_reconstruction_poles,
    ) in total_reconstruction_poles_by_moving_plate.items():
        moving_plate_time_samples = []

        # Iterate over the total reconstruction poles associated with the current moving plate.
        for total_reconstruction_pole in total_reconstruction_poles:
            fixed_plate_id = total_reconstruction_pole[0]
            rotation_sequence = total_reconstruction_pole[2]

            # Skip moving/fixed plates 999 since they are used as comments in PLATES rotation file.
            if moving_plate_id == 999 and fixed_plate_id == 999:
                continue

            time_samples = rotation_sequence.get_enabled_time_samples()
            if not time_samples:
                print(
                    "Moving_pid({0}), fixed_pid({1}): no enabled poles in sequence.".format(
                        moving_plate_id, fixed_plate_id
                    )
                )
                continue
            if len(time_samples) == 1:
                print(
                    "Moving_pid({0}), fixed_pid({1}): only one enabled pole in sequence.".format(
                        moving_plate_id, fixed_plate_id
                    )
                )
                continue
            times = [time_sample.get_time() for time_sample in time_samples]
            if sorted(times) != times:
                print(
                    "Moving_pid({0}), fixed_pid({1}): times of enabled samples not monotonically increasing between {2} and {3}.".format(
                        moving_plate_id, fixed_plate_id, times[0], times[-1]
                    )
                )
                continue

            moving_plate_time_samples.append((fixed_plate_id, time_samples))

        # Go through all rotation sequences with the same moving plate (and potentially different
        # fixed plates) to see if there's any overlap in their time ranges and to see if two
        # adjacent rotation sequences have matching rotations (or crossovers if fixed plates differ)
        # at the joint time.
        for index1 in range(len(moving_plate_time_samples) - 1):
            for index2 in range(index1 + 1, len(moving_plate_time_samples)):
                fixed_plate_id1 = moving_plate_time_samples[index1][0]
                fixed_plate_id2 = moving_plate_time_samples[index2][0]
                fixed_plate_time_samples1 = moving_plate_time_samples[index1][1]
                fixed_plate_time_samples2 = moving_plate_time_samples[index2][1]
                time_range1 = (
                    pygplates.GeoTimeInstant(fixed_plate_time_samples1[0].get_time()),
                    pygplates.GeoTimeInstant(fixed_plate_time_samples1[-1].get_time()),
                )
                time_range2 = (
                    pygplates.GeoTimeInstant(fixed_plate_time_samples2[0].get_time()),
                    pygplates.GeoTimeInstant(fixed_plate_time_samples2[-1].get_time()),
                )

                if time_range1[0] > time_range2[0]:
                    if time_range1[0] < time_range2[1]:
                        print(
                            "Moving_pid({0}), fixed_pid1({1}), fixed_pid2({2}): sequences overlap between {3} and {4}.".format(
                                moving_plate_id,
                                fixed_plate_id1,
                                fixed_plate_id2,
                                time_range1[0],
                                time_range2[1],
                            )
                        )
                    elif time_range1[0] == time_range2[1]:
                        # Sequence at 'index1' is *older* than sequence at 'index2' and abuts it.
                        if fixed_plate_id1 == fixed_plate_id2:
                            # Check that both rotations are equal.
                            if not pygplates.FiniteRotation.are_equal(
                                fixed_plate_time_samples1[0]
                                .get_value()
                                .get_finite_rotation(),
                                fixed_plate_time_samples2[-1]
                                .get_value()
                                .get_finite_rotation(),
                                rotation_threshold_degrees,
                            ):
                                print(
                                    "Moving_pid({0}), fixed_pid({1}): two different rotations at time {2}.".format(
                                        moving_plate_id, fixed_plate_id1, time_range1[0]
                                    )
                                )
                        else:
                            # We have a crossover (different fixed plates).
                            # Check that the crossover is synchronised.
                            # Implementation follows that in pygplates.synchronise_crossovers():
                            #  1. Disable crossover in older rotation sequence,
                            #  2. Calculate rotation relative to older crossover's fixed plate,
                            #  3. Compare calculated rotation with previous rotation,
                            #  4. Re-enable crossover in older rotation sequence.
                            fixed_plate_time_samples1[0].set_disabled()
                            rotation_model = pygplates.RotationModel(
                                rotation_feature_sequence
                            )
                            if not pygplates.FiniteRotation.are_equal(
                                rotation_model.get_rotation(
                                    time_range1[0],
                                    moving_plate_id,
                                    anchor_plate_id=fixed_plate_id1,
                                ),
                                fixed_plate_time_samples1[0]
                                .get_value()
                                .get_finite_rotation(),
                                rotation_threshold_degrees,
                            ):
                                print(
                                    "Moving_pid({0}), young_fixed_pid({1}), old_fixed_pid({2}): "
                                    "crossover at time {3} needs fixing.".format(
                                        moving_plate_id,
                                        fixed_plate_id2,
                                        fixed_plate_id1,
                                        time_range1[0],
                                    )
                                )
                            fixed_plate_time_samples1[0].set_enabled()
                else:  # time_range1[0] <= time_range2[0] ...
                    if time_range2[0] < time_range1[1]:
                        print(
                            "Moving_pid({0}), fixed_pid1({1}), fixed_pid2({2}): sequences overlap between {3} and {4}.".format(
                                moving_plate_id,
                                fixed_plate_id1,
                                fixed_plate_id2,
                                time_range2[0],
                                time_range1[1],
                            )
                        )
                    elif time_range2[0] == time_range1[1]:
                        # Sequence at 'index1' is *younger* than sequence at 'index2' and abuts it.
                        if fixed_plate_id1 == fixed_plate_id2:
                            # Check that both rotations are equal.
                            if not pygplates.FiniteRotation.are_equal(
                                fixed_plate_time_samples1[-1]
                                .get_value()
                                .get_finite_rotation(),
                                fixed_plate_time_samples2[0]
                                .get_value()
                                .get_finite_rotation(),
                                rotation_threshold_degrees,
                            ):
                                print(
                                    "Moving_pid({0}), fixed_pid({1}): two different rotations at time {2}.".format(
                                        moving_plate_id, fixed_plate_id1, time_range2[0]
                                    )
                                )
                        else:
                            # We have a crossover (different fixed plates).
                            # Check that the crossover is synchronised.
                            # Implementation follows that in pygplates.synchronise_crossovers():
                            #  1. Disable crossover in older rotation sequence,
                            #  2. Calculate rotation relative to older crossover's fixed plate,
                            #  3. Compare calculated rotation with previous rotation,
                            #  4. Re-enable crossover in older rotation sequence.
                            fixed_plate_time_samples2[0].set_disabled()
                            rotation_model = pygplates.RotationModel(
                                rotation_feature_sequence
                            )
                            if not pygplates.FiniteRotation.are_equal(
                                rotation_model.get_rotation(
                                    time_range2[0],
                                    moving_plate_id,
                                    anchor_plate_id=fixed_plate_id2,
                                ),
                                fixed_plate_time_samples2[0]
                                .get_value()
                                .get_finite_rotation(),
                                rotation_threshold_degrees,
                            ):
                                print(
                                    "Moving_pid({0}), young_fixed_pid({1}), old_fixed_pid({2}): "
                                    "crossover at time {3} needs fixing.".format(
                                        moving_plate_id,
                                        fixed_plate_id1,
                                        fixed_plate_id2,
                                        time_range2[0],
                                    )
                                )
                            fixed_plate_time_samples2[0].set_enabled()


__description__ = """Diagnose one or more rotation files to check for inconsistencies.

NOTE: Separate the positional and optional arguments with '--' (workaround for bug in argparse module).
For example...

%(prog)s input_rotations1.rot input_rotations2.rot
"""


def add_arguments(parser: argparse.ArgumentParser):
    """add command line argument parser"""

    parser.formatter_class = argparse.RawDescriptionHelpFormatter
    parser.description = __description__

    parser.set_defaults(func=main)

    def parse_positive_number(value_string):
        try:
            value = float(value_string)
        except ValueError:
            raise argparse.ArgumentTypeError("%s is not a number" % value_string)

        if value < 0:
            raise argparse.ArgumentTypeError("%g is not a positive number" % value)

        return value

    parser.add_argument(
        "-t",
        "--rotation_threshold_degrees",
        type=parse_positive_number,
        default=DEFAULT_ROTATION_THRESHOLD_DEGREES,
        help="Two rotations differ if either the rotation latitude, longitude or angle differ by "
        "the specified amount (in degrees). The default (0.01 degrees) is useful for some "
        "PLATES rotation files that are typically accurate to 2 decimal places.",
    )

    parser.add_argument(
        "rotation_filenames",
        type=str,
        nargs="+",
        metavar="rotation_filename",
        help="One or more rotation filenames.",
    )


def main(args):
    diagnose_rotations(args.rotation_filenames, args.rotation_threshold_degrees)


if __name__ == "__main__":
    # Check the imported pygplates version.
    required_version = pygplates.Version(4)
    if (
        not hasattr(pygplates, "Version")
        or pygplates.Version.get_imported_version() < required_version
    ):
        print(
            "{0}: Error - imported pygplates version {1} but version {2} or greater is required".format(
                os.path.basename(__file__),
                pygplates.Version.get_imported_version(),
                required_version,
            ),
            file=sys.stderr,
        )
        sys.exit(1)

    # The command-line parser.
    parser = argparse.ArgumentParser(
        description=__description__,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )

    # add arguments
    add_arguments(parser)

    # Parse command-line options.
    args = parser.parse_args()

    # call main function
    main(args)

Functions

def add_arguments(parser: argparse.ArgumentParser)

add command line argument parser

Expand source code
def add_arguments(parser: argparse.ArgumentParser):
    """add command line argument parser"""

    parser.formatter_class = argparse.RawDescriptionHelpFormatter
    parser.description = __description__

    parser.set_defaults(func=main)

    def parse_positive_number(value_string):
        try:
            value = float(value_string)
        except ValueError:
            raise argparse.ArgumentTypeError("%s is not a number" % value_string)

        if value < 0:
            raise argparse.ArgumentTypeError("%g is not a positive number" % value)

        return value

    parser.add_argument(
        "-t",
        "--rotation_threshold_degrees",
        type=parse_positive_number,
        default=DEFAULT_ROTATION_THRESHOLD_DEGREES,
        help="Two rotations differ if either the rotation latitude, longitude or angle differ by "
        "the specified amount (in degrees). The default (0.01 degrees) is useful for some "
        "PLATES rotation files that are typically accurate to 2 decimal places.",
    )

    parser.add_argument(
        "rotation_filenames",
        type=str,
        nargs="+",
        metavar="rotation_filename",
        help="One or more rotation filenames.",
    )
def diagnose_rotations(rotation_features, rotation_threshold_degrees=0.01)

Diagnose one or more rotation files to check for inconsistencies.

'rotation_features' can be a rotation feature collection, or rotation filename, or rotation feature, or sequence of rotation features, or a sequence (eg, list or tuple) of any combination of those four types.

'rotation_threshold_degrees' allows two rotations to compare equal if their rotation latitude, longitude or angle differ by less than the specified amount (in degrees). The default value is useful for some PLATES rotation files that are typically accurate to 2 decimal places (or threshold of 0.01). Specifing 'None' is equivalent to having no threshold (see pygplates.FiniteRotation.are_equal).

Expand source code
def diagnose_rotations(
    rotation_features, rotation_threshold_degrees=DEFAULT_ROTATION_THRESHOLD_DEGREES
):
    """
    Diagnose one or more rotation files to check for inconsistencies.

    'rotation_features' can be a rotation feature collection, or rotation filename, or rotation feature, or
    sequence of rotation features, or a sequence (eg, list or tuple) of any combination of those four types.

    'rotation_threshold_degrees' allows two rotations to compare equal if their rotation latitude, longitude or angle
    differ by less than the specified amount (in degrees). The default value is useful for some PLATES rotation files
    that are typically accurate to 2 decimal places (or threshold of 0.01).
    Specifing 'None' is equivalent to having no threshold (see pygplates.FiniteRotation.are_equal).
    """

    # Use helper class to convert 'rotation_features' argument to a list of features.
    rotation_features = pygplates.FeaturesFunctionArgument(rotation_features)
    rotation_feature_sequence = rotation_features.get_features()

    # Make sure threshold is a number.
    if rotation_threshold_degrees is not None:
        rotation_threshold_degrees = float(rotation_threshold_degrees)

    # A 'dict' to map each moving plate to a list of total reconstruction poles
    # (one per moving/fixed plate pair)
    total_reconstruction_poles_by_moving_plate = {}

    # Get the moving/fixed total reconstruction poles.
    for rotation_feature in rotation_feature_sequence:
        total_reconstruction_pole = rotation_feature.get_total_reconstruction_pole()
        # If the current feature is a valid rotation feature...
        if total_reconstruction_pole:
            (
                fixed_plate_id,
                moving_plate_id,
                rotation_sequence,
            ) = total_reconstruction_pole
            # Each moving plate has a list of total reconstruction poles.
            total_reconstruction_poles = (
                total_reconstruction_poles_by_moving_plate.setdefault(
                    moving_plate_id, []
                )
            )
            total_reconstruction_poles.append(total_reconstruction_pole)

    # Iterate over the moving plates.
    for (
        moving_plate_id,
        total_reconstruction_poles,
    ) in total_reconstruction_poles_by_moving_plate.items():
        moving_plate_time_samples = []

        # Iterate over the total reconstruction poles associated with the current moving plate.
        for total_reconstruction_pole in total_reconstruction_poles:
            fixed_plate_id = total_reconstruction_pole[0]
            rotation_sequence = total_reconstruction_pole[2]

            # Skip moving/fixed plates 999 since they are used as comments in PLATES rotation file.
            if moving_plate_id == 999 and fixed_plate_id == 999:
                continue

            time_samples = rotation_sequence.get_enabled_time_samples()
            if not time_samples:
                print(
                    "Moving_pid({0}), fixed_pid({1}): no enabled poles in sequence.".format(
                        moving_plate_id, fixed_plate_id
                    )
                )
                continue
            if len(time_samples) == 1:
                print(
                    "Moving_pid({0}), fixed_pid({1}): only one enabled pole in sequence.".format(
                        moving_plate_id, fixed_plate_id
                    )
                )
                continue
            times = [time_sample.get_time() for time_sample in time_samples]
            if sorted(times) != times:
                print(
                    "Moving_pid({0}), fixed_pid({1}): times of enabled samples not monotonically increasing between {2} and {3}.".format(
                        moving_plate_id, fixed_plate_id, times[0], times[-1]
                    )
                )
                continue

            moving_plate_time_samples.append((fixed_plate_id, time_samples))

        # Go through all rotation sequences with the same moving plate (and potentially different
        # fixed plates) to see if there's any overlap in their time ranges and to see if two
        # adjacent rotation sequences have matching rotations (or crossovers if fixed plates differ)
        # at the joint time.
        for index1 in range(len(moving_plate_time_samples) - 1):
            for index2 in range(index1 + 1, len(moving_plate_time_samples)):
                fixed_plate_id1 = moving_plate_time_samples[index1][0]
                fixed_plate_id2 = moving_plate_time_samples[index2][0]
                fixed_plate_time_samples1 = moving_plate_time_samples[index1][1]
                fixed_plate_time_samples2 = moving_plate_time_samples[index2][1]
                time_range1 = (
                    pygplates.GeoTimeInstant(fixed_plate_time_samples1[0].get_time()),
                    pygplates.GeoTimeInstant(fixed_plate_time_samples1[-1].get_time()),
                )
                time_range2 = (
                    pygplates.GeoTimeInstant(fixed_plate_time_samples2[0].get_time()),
                    pygplates.GeoTimeInstant(fixed_plate_time_samples2[-1].get_time()),
                )

                if time_range1[0] > time_range2[0]:
                    if time_range1[0] < time_range2[1]:
                        print(
                            "Moving_pid({0}), fixed_pid1({1}), fixed_pid2({2}): sequences overlap between {3} and {4}.".format(
                                moving_plate_id,
                                fixed_plate_id1,
                                fixed_plate_id2,
                                time_range1[0],
                                time_range2[1],
                            )
                        )
                    elif time_range1[0] == time_range2[1]:
                        # Sequence at 'index1' is *older* than sequence at 'index2' and abuts it.
                        if fixed_plate_id1 == fixed_plate_id2:
                            # Check that both rotations are equal.
                            if not pygplates.FiniteRotation.are_equal(
                                fixed_plate_time_samples1[0]
                                .get_value()
                                .get_finite_rotation(),
                                fixed_plate_time_samples2[-1]
                                .get_value()
                                .get_finite_rotation(),
                                rotation_threshold_degrees,
                            ):
                                print(
                                    "Moving_pid({0}), fixed_pid({1}): two different rotations at time {2}.".format(
                                        moving_plate_id, fixed_plate_id1, time_range1[0]
                                    )
                                )
                        else:
                            # We have a crossover (different fixed plates).
                            # Check that the crossover is synchronised.
                            # Implementation follows that in pygplates.synchronise_crossovers():
                            #  1. Disable crossover in older rotation sequence,
                            #  2. Calculate rotation relative to older crossover's fixed plate,
                            #  3. Compare calculated rotation with previous rotation,
                            #  4. Re-enable crossover in older rotation sequence.
                            fixed_plate_time_samples1[0].set_disabled()
                            rotation_model = pygplates.RotationModel(
                                rotation_feature_sequence
                            )
                            if not pygplates.FiniteRotation.are_equal(
                                rotation_model.get_rotation(
                                    time_range1[0],
                                    moving_plate_id,
                                    anchor_plate_id=fixed_plate_id1,
                                ),
                                fixed_plate_time_samples1[0]
                                .get_value()
                                .get_finite_rotation(),
                                rotation_threshold_degrees,
                            ):
                                print(
                                    "Moving_pid({0}), young_fixed_pid({1}), old_fixed_pid({2}): "
                                    "crossover at time {3} needs fixing.".format(
                                        moving_plate_id,
                                        fixed_plate_id2,
                                        fixed_plate_id1,
                                        time_range1[0],
                                    )
                                )
                            fixed_plate_time_samples1[0].set_enabled()
                else:  # time_range1[0] <= time_range2[0] ...
                    if time_range2[0] < time_range1[1]:
                        print(
                            "Moving_pid({0}), fixed_pid1({1}), fixed_pid2({2}): sequences overlap between {3} and {4}.".format(
                                moving_plate_id,
                                fixed_plate_id1,
                                fixed_plate_id2,
                                time_range2[0],
                                time_range1[1],
                            )
                        )
                    elif time_range2[0] == time_range1[1]:
                        # Sequence at 'index1' is *younger* than sequence at 'index2' and abuts it.
                        if fixed_plate_id1 == fixed_plate_id2:
                            # Check that both rotations are equal.
                            if not pygplates.FiniteRotation.are_equal(
                                fixed_plate_time_samples1[-1]
                                .get_value()
                                .get_finite_rotation(),
                                fixed_plate_time_samples2[0]
                                .get_value()
                                .get_finite_rotation(),
                                rotation_threshold_degrees,
                            ):
                                print(
                                    "Moving_pid({0}), fixed_pid({1}): two different rotations at time {2}.".format(
                                        moving_plate_id, fixed_plate_id1, time_range2[0]
                                    )
                                )
                        else:
                            # We have a crossover (different fixed plates).
                            # Check that the crossover is synchronised.
                            # Implementation follows that in pygplates.synchronise_crossovers():
                            #  1. Disable crossover in older rotation sequence,
                            #  2. Calculate rotation relative to older crossover's fixed plate,
                            #  3. Compare calculated rotation with previous rotation,
                            #  4. Re-enable crossover in older rotation sequence.
                            fixed_plate_time_samples2[0].set_disabled()
                            rotation_model = pygplates.RotationModel(
                                rotation_feature_sequence
                            )
                            if not pygplates.FiniteRotation.are_equal(
                                rotation_model.get_rotation(
                                    time_range2[0],
                                    moving_plate_id,
                                    anchor_plate_id=fixed_plate_id2,
                                ),
                                fixed_plate_time_samples2[0]
                                .get_value()
                                .get_finite_rotation(),
                                rotation_threshold_degrees,
                            ):
                                print(
                                    "Moving_pid({0}), young_fixed_pid({1}), old_fixed_pid({2}): "
                                    "crossover at time {3} needs fixing.".format(
                                        moving_plate_id,
                                        fixed_plate_id1,
                                        fixed_plate_id2,
                                        time_range2[0],
                                    )
                                )
                            fixed_plate_time_samples2[0].set_enabled()
def main(args)
Expand source code
def main(args):
    diagnose_rotations(args.rotation_filenames, args.rotation_threshold_degrees)