-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathisopolate.py
1742 lines (1475 loc) · 105 KB
/
isopolate.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Copyright (C) 2015 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.
"""
##############################################################################
# You can either:
# (1) Import this module ('import isopolate') into your script and call the
# 'interpolate_isochrons()' function in your script, or
# (2) Run this script with command-line options:
# python isopolate.py ...
#
# For (1) see the docstring of interpolate_isochrons() for more details.
# For (2) run this script as 'python isopolate.py -h' to see more options.
##############################################################################
from __future__ import print_function
import argparse
import math
import sys
import os.path
import pygplates
# Required pygplates version.
PYGPLATES_VERSION_REQUIRED = pygplates.Version(8)
# The default interval spacing between interpolated isochrons.
DEFAULT_INTERPOLATE_RESOLUTION_DEGREES = 0.1
DEFAULT_INTERPOLATE_RESOLUTION_RADIANS = math.radians(DEFAULT_INTERPOLATE_RESOLUTION_DEGREES)
# The default value for the minimum latitude overlap when detecting if
# we can interpolate between two polylines.
DEFAULT_MINIMUM_LATITUDE_OVERLAP_DEGREES = 1
DEFAULT_MINIMUM_LATITUDE_OVERLAP_RADIANS = math.radians(DEFAULT_MINIMUM_LATITUDE_OVERLAP_DEGREES)
# Default values for the extra range of non-overlapping latitudes at the North and South
# (in stage rotation pole reference frame).
# For isochrons...
DEFAULT_MAXIMUM_ISOCHRON_LATITUDE_NON_OVERLAP_DEGREES = 5
DEFAULT_MAXIMUM_ISOCHRON_LATITUDE_NON_OVERLAP_RADIANS = math.radians(DEFAULT_MAXIMUM_ISOCHRON_LATITUDE_NON_OVERLAP_DEGREES)
# For COBs...
DEFAULT_MAXIMUM_COB_LATITUDE_NON_OVERLAP_DEGREES = 1
DEFAULT_MAXIMUM_COB_LATITUDE_NON_OVERLAP_RADIANS = math.radians(DEFAULT_MAXIMUM_COB_LATITUDE_NON_OVERLAP_DEGREES)
# The maximum distance between any corresponding pair of points (same latitude), in degrees,
# above which two polylines will not be interpolated.
DEFAULT_MAXIMUM_DISTANCE_THRESHOLD_DEGREES = 30
DEFAULT_MAXIMUM_DISTANCE_THRESHOLD_RADIANS = math.radians(DEFAULT_MAXIMUM_DISTANCE_THRESHOLD_DEGREES)
# The default value for the maximum difference in age between two features to be interpolated.
# If exceeded then no new features are interpolated between them.
DEFAULT_MAXIMUM_AGE_DIFFERENCE = 50
# The default value to join polylines if their end points are within a threshold distance of each other.
DEFAULT_JOIN_POLYLINES_THRESHOLD_DEGREES = 3.0
DEFAULT_JOIN_POLYLINES_THRESHOLD_RADIANS = math.radians(DEFAULT_JOIN_POLYLINES_THRESHOLD_DEGREES)
# Scalar coverages.
SCALAR_COVERAGE_SPREADING_ASYMMETRY = 'SpreadingAsymmetry'
SCALAR_COVERAGE_SPREADING_RATE = 'SpreadingRate'
SCALAR_COVERAGE_FULL_SPREADING_RATE = 'FullSpreadingRate'
SCALAR_COVERAGE_SPREADING_DIRECTION = 'SpreadingDirection'
SCALAR_COVERAGE_SPREADING_OBLIQUITY = 'SpreadingObliquity'
SCALAR_COVERAGE_AGE = 'Age'
# Scalar types.
SCALAR_TYPE_SPREADING_ASYMMETRY = pygplates.ScalarType.create_gpml(SCALAR_COVERAGE_SPREADING_ASYMMETRY)
SCALAR_TYPE_SPREADING_RATE = pygplates.ScalarType.create_gpml(SCALAR_COVERAGE_SPREADING_RATE)
SCALAR_TYPE_FULL_SPREADING_RATE = pygplates.ScalarType.create_gpml(SCALAR_COVERAGE_FULL_SPREADING_RATE)
SCALAR_TYPE_SPREADING_DIRECTION = pygplates.ScalarType.create_gpml(SCALAR_COVERAGE_SPREADING_DIRECTION)
SCALAR_TYPE_SPREADING_OBLIQUITY = pygplates.ScalarType.create_gpml(SCALAR_COVERAGE_SPREADING_OBLIQUITY)
SCALAR_TYPE_AGE = pygplates.ScalarType.create_gpml(SCALAR_COVERAGE_AGE)
# Known scalar coverages.
KNOWN_SCALAR_COVERAGES = set([
SCALAR_COVERAGE_SPREADING_ASYMMETRY,
SCALAR_COVERAGE_SPREADING_RATE,
SCALAR_COVERAGE_FULL_SPREADING_RATE,
SCALAR_COVERAGE_SPREADING_DIRECTION,
SCALAR_COVERAGE_SPREADING_OBLIQUITY,
SCALAR_COVERAGE_AGE])
COB_FEATURE_TYPE = pygplates.FeatureType.create_gpml('PassiveContinentalBoundary')
ISOCHRON_FEATURE_TYPE = pygplates.FeatureType.create_gpml('Isochron')
MID_OCEAN_RIDGE_FEATURE_TYPE = pygplates.FeatureType.create_gpml('MidOceanRidge')
#
# Private function.
#
# Returns a tuple of left and right plate ids from a 'feature', or None if not found.
# If feature does not have left/right plate ids then looks for reconstruction/conjugate plate ids,
# otherwise looks for plate/conjugate ids if feature came from a PLATES data file, otherwise returns None.
#
def _get_left_right_plate_ids(feature):
# Get left and right plate ids (if any).
left_plate_id = feature.get_left_plate(None)
right_plate_id = feature.get_right_plate(None)
# If missing either then attempt to get reconstruction/conjugate plate id.
if left_plate_id is not None and right_plate_id is not None:
return left_plate_id, right_plate_id
left_plate_id = feature.get_reconstruction_plate_id(None)
right_plate_id = feature.get_conjugate_plate_id(None)
# If missing either then attempt to get reconstruction/conjugate from the 'gpml:OldPlatesHeader' property.
if left_plate_id is not None and right_plate_id is not None:
return left_plate_id, right_plate_id
gpml_old_plates_header = feature.get_value(pygplates.PropertyName.create_gpml('oldPlatesHeader'))
if gpml_old_plates_header:
try:
left_plate_id = gpml_old_plates_header.get_plate_id_number()
right_plate_id = gpml_old_plates_header.get_conjugate_plate_id_number()
return left_plate_id, right_plate_id
except AttributeError:
# The property value type did not match the property name.
# This indicates the data does not conform to the GPlates Geological Information Model (GPGIM).
pass
#
# Private function.
#
# Remove from 'features' those features that do not have conjugate plate ids in the 'plate_pairs' list.
#
def _filter_plate_pairs(features, plate_pairs):
feature_index = 0
while feature_index < len(features):
feature = features[feature_index]
# Get left and right plate ids (in one form or another), otherwise remove the current feature.
left_right_plate_ids = _get_left_right_plate_ids(feature)
if left_right_plate_ids:
left_plate_id, right_plate_id = left_right_plate_ids
if (left_plate_id, right_plate_id) in plate_pairs:
# Keep current feature and move to next.
feature_index += 1
continue
if (right_plate_id, left_plate_id) in plate_pairs:
# Keep current feature and move to next.
feature_index += 1
continue
# Remove current feature.
del features[feature_index]
#
# Private function.
#
# Removes duplicate geometries from the list 'features'.
# This can reduce the number of features in the list.
#
def _remove_features_with_duplicate_geometries(features):
if len(features) == 1:
return
#
# Do N^2 search over pairs of features to test for duplicates.
#
# Iterate over all features except last feature.
# Using len() since some features are removed during iteration.
feature1_index = 0
while feature1_index < len(features) - 1:
feature1 = features[feature1_index]
# Get all geometries in 'feature1'.
feature1_geoms = feature1.get_all_geometries()
# Iterate over the remaining features (after 'feature1').
# Using len() since some features are removed during iteration.
feature2_index = feature1_index + 1
while feature2_index < len(features):
feature2 = features[feature2_index]
# Get all geometries in 'feature2'.
feature2_geoms = feature2.get_all_geometries()
# Compare the geometries of feature1 and feature2.
removed = False
for feature1_geom_index, feature1_geom in enumerate(feature1_geoms):
feature2_geom_index = 0
# Using len() since some geometries are removed during iteration.
while feature2_geom_index < len(feature2_geoms):
feature2_geom = feature2_geoms[feature2_geom_index]
# Test for duplicate geometries.
if feature1_geom == feature2_geom:
# Remove the feature 2 geometry (from the list).
del feature2_geoms[feature2_geom_index]
feature2_geom_index -= 1
removed = True
feature2_geom_index += 1
if removed:
# Replace 'feature2's geometries with the reduced set of geometries, or
# remove 'feature2' from the list if all its geometries have been removed.
if feature2_geoms:
feature2.set_geometry(feature2_geoms)
else:
del features[feature2_index]
feature2_index -= 1
feature2_index += 1
feature1_index += 1
#
# Private function.
#
# Joins adjacent geometries in the 'features' list and returns a new joined list.
#
def _join_adjacent_features(features, distance_threshold_radians, print_debug_output):
geometries = []
begin_time = None
end_time = float('inf')
# Extract all the geometries from the features and expand time range to include all features.
for feature in features:
feature_begin_time, feature_end_time = feature.get_valid_time()
# Some features erroneously have end time earlier than begin time.
# Ignore these features.
if feature_end_time > feature_begin_time:
if print_debug_output >= 1:
print(" ...skipping '{0}' "
"feature (id: {1}): begin time {2} later than end time {3}."
.format(feature.get_feature_type(), feature.get_feature_id(),
feature_begin_time, feature_end_time))
continue
# Expand time range to include the current feature.
# Note: All features should have the same begin time but can have differing end times.
if begin_time is None:
begin_time = feature_begin_time
if feature_end_time < end_time:
end_time = feature_end_time
# Get all geometries in 'feature'.
geometries.extend(feature.get_all_geometries())
# Join polylines that have end points closer than a distance threshold.
# Print debug message if not all geometries are polylines.
try:
joined_geometries = pygplates.PolylineOnSphere.join(
geometries, distance_threshold_radians, pygplates.PolylineConversion.raise_if_non_polyline)
except pygplates.GeometryTypeError:
if print_debug_output >= 1:
print(' ...skipping non-polyline geometries at time {0}'.format(begin_time))
# Try again but this time just ignore any geometry that's not a polyline.
joined_geometries = pygplates.PolylineOnSphere.join(
geometries, distance_threshold_radians, pygplates.PolylineConversion.ignore_non_polyline)
#print('joined begin time {0}, plate ids {1}'.format(begin_time, _get_left_right_plate_ids(feature)))
# Potentially reduced set of joined features.
joined_features = []
for joined_geometry in joined_geometries:
# Clone the properties in the first feature.
joined_feature = features[0].clone()
# Modify the time range and geometry.
joined_feature.set_valid_time(begin_time, end_time)
joined_feature.set_geometry(joined_geometry)
joined_features.append(joined_feature)
return joined_features
#
# Private function.
#
# Calculate a spreading asymmetry for each point in the young/old polyline.
#
# Both polylines should have the same number of points and they are expected to be
# from the output of 'pygplates.PolylineOnSphere.rotation_interpolate()' which
# keeps matching points latitude aligned (in coordinate system of stage rotation),
# except for the non-overlapping latitude regions at the ends.
#
# The stage rotation pole should be in the present day reference frame (since young/old polylines are present day).
#
# Also 'stage_pole_angle_radians' must not be zero (identity rotation).
#
def _calc_spreading_asymmetries(
young_polyline,
old_polyline,
stage_rotation_pole_for_present_day_geometry,
stage_pole_angle_radians):
asymmetries = []
# Store pole as a pygplates.Vector3D instead of a pygplates.PointOnSphere.
stage_rotation_vector_for_present_day_geometry = pygplates.Vector3D(stage_rotation_pole_for_present_day_geometry.to_xyz())
abs_stage_pole_angle_radians = abs(stage_pole_angle_radians)
for point_index in range(len(young_polyline)):
# The young point and the stage pole lie in a plane (also through origin of globe).
# We find the vector perpendicular to the young point and stage pole (this will help us below).
#
# The same applies to the old point (it has a plane and perpendicular vector).
#
# The angle between the young and old planes can then be calculated from the great circle arc
# angle between the young and the old perpendicular vectors. That is because the rotation
# about the stage pole vector at these perpendicular positions is a great circle (not a small circle).
#
# That angle represents the rotation angle about the stage pole that moves the young point
# onto the old points plane. In most cases the young and old points are latitude aligned
# and hence the young point will rotate on top of the old point but this won't be the
# case for the non-overlapping latitude regions at the ends of the polylines due to
# pygplates.PolylineOnSphere.rotation_interpolate().
young_vector_perp_pole = pygplates.Vector3D.cross(
pygplates.Vector3D.cross(stage_rotation_vector_for_present_day_geometry, young_polyline[point_index].to_xyz()),
stage_rotation_vector_for_present_day_geometry)
old_vector_perp_pole = pygplates.Vector3D.cross(
pygplates.Vector3D.cross(stage_rotation_vector_for_present_day_geometry, old_polyline[point_index].to_xyz()),
stage_rotation_vector_for_present_day_geometry)
try:
angle_between_young_and_old_points = pygplates.Vector3D.angle_between(
young_vector_perp_pole,
old_vector_perp_pole)
except pygplates.UnableToNormaliseZeroVectorError:
# Young or old point is coincident with the stage rotation pole so just set asymmetry to zero.
asymmetries.append(0)
continue
# Asymmetry is in the range [-1,1].
#
# The "min(..., 1)" clamps to the range [0,1].
# The "2 * () - 1" maps the range [0,1] to [-1,1].
asymmetry = 2 * min(angle_between_young_and_old_points / abs_stage_pole_angle_radians, 1) - 1
asymmetries.append(asymmetry)
return asymmetries
#
# Private function.
#
# Adds requested scalar coverages (if any) to an interpolated polyline and tessellates it (if requested).
#
# Any scalar types in 'output_scalar_types' are calculated at each point in the interpolated polyline
# and the returned geometry is then a coverage (ie, a geometry and scalar values).
# The length of 'spreading_asymmetries' must match the number of points in 'interpolated_polyline'.
# If 'output_scalar_types' is empty then 'spreading_asymmetries' is ignored.
#
# If 'tessellate_threshold_radians' is None then no tessellation is performed.
#
# The stage rotation should be in the present day reference frame (since interpolated polyline is present day).
#
def _add_tessellated_scalar_coverages(
interpolated_polyline,
tessellate_threshold_radians,
output_scalar_types,
spreading_asymmetries,
stage_rotation_for_present_day_geometry,
inverse_present_day_rotation,
interpolated_time,
stage_rotation_time_interval,
rotation_model,
interpolated_isochron_plate_id):
# If not outputting scalar coverages then just tessellate (if requested) and return.
if not output_scalar_types:
# Tessellate if requested.
if tessellate_threshold_radians:
return interpolated_polyline.to_tessellated(tessellate_threshold_radians)
return interpolated_polyline
scalar_coverages = {}
# Tessellate the interpolated polyline if requested.
if tessellate_threshold_radians:
tessellated_points = []
# We also need to tessellate the spreading asymmetries since its length
# must match the number of tessellated points.
tessellated_spreading_asymmetries = []
# Iterate over all polyline segments.
segments = interpolated_polyline.get_segments()
for segment_index in range(len(segments)):
segment = segments[segment_index]
tessellated_segment_points = segment.to_tessellated(tessellate_threshold_radians)
# Copy all points except the last one (it's a duplicate of the start point of next segment).
tessellated_points.extend(tessellated_segment_points[:-1])
# Add the spreading asymmetry associated with the segment's start point.
segment_start_asymmetry = spreading_asymmetries[segment_index]
tessellated_spreading_asymmetries.append(segment_start_asymmetry)
# Tessellate the spreading asymmetries except the last one
# (it's a duplicate of the asymmetry of the start point of next segment).
num_segment_sub_intervals = len(tessellated_segment_points) - 1
# Do we even need to tessellate ?
if num_segment_sub_intervals > 1:
inv_num_segment_sub_intervals = 1.0 / num_segment_sub_intervals
segment_end_asymmetry = spreading_asymmetries[segment_index + 1]
# Generate the asymmetries at the segment internal points (excludes segment end points).
for interval_index in range(1, num_segment_sub_intervals):
interpolate_ratio = interval_index * inv_num_segment_sub_intervals
tessellated_spreading_asymmetries.append(
(1 - interpolate_ratio) * segment_start_asymmetry +
interpolate_ratio * segment_end_asymmetry)
# Add the last point/asymmetry.
tessellated_points.append(interpolated_polyline[-1])
tessellated_spreading_asymmetries.append(spreading_asymmetries[-1])
# Replace the interpolated polyline with a tessellated version.
interpolated_polyline = pygplates.PolylineOnSphere(tessellated_points)
# Replace the spreading asymmetries with the tessellated versions.
spreading_asymmetries = tessellated_spreading_asymmetries
# Output spreading asymmetry scalar coverage (if requested) - per-point spreading asymmetry values.
if SCALAR_TYPE_SPREADING_ASYMMETRY in output_scalar_types:
scalar_coverages[SCALAR_TYPE_SPREADING_ASYMMETRY] = spreading_asymmetries
# Output spreading rate/direction/obliquity scalar coverage (if requested) - per-point spreading rate/direction values.
if (SCALAR_TYPE_SPREADING_RATE in output_scalar_types or
SCALAR_TYPE_FULL_SPREADING_RATE in output_scalar_types or
SCALAR_TYPE_SPREADING_DIRECTION in output_scalar_types or
SCALAR_TYPE_SPREADING_OBLIQUITY in output_scalar_types):
interpolated_points = interpolated_polyline.get_points()
# Calculate full spreading velocities at the points of the interpolated/tessellated polyline.
#
# First we calculate velocities at present day using the present day polyline point locations and the
# stage rotation in the present day reference frame.
full_spreading_velocity_vectors_at_present_day = pygplates.calculate_velocities(
interpolated_points,
stage_rotation_for_present_day_geometry,
stage_rotation_time_interval)
#
# Then reconstruct the points and velocities from present day to their reconstructed positions/directions at the
# interpolated isochron's birth time (time of crustal accretion) since we're recording a snapshot of crustal accretion.
#
# A small issue is there might be a non-zero finite rotation at present day, so we cannot assume the un-reconstructed
# isochron is the same as the isochron reconstructed to present day. We're working with isochron geometry at present day
# so we need to reverse reconstruct it to its un-reconstructed position 'inverse[R(0, A->Plate)] * geometry_present_day'
# before we can reconstruct it to its begin time (time of crustal accretion).
#
# geometry_moving_plate = R(0->begin_time, A->Plate) * geometry_present_day
# = R(begin_time, A->Plate) * inverse[R(0, A->Plate)] * geometry_present_day
#
# ...where '0->t' means reconstructing from its "present day" (not un-reconstructed) position to time 't'.
#
# Note: We can't just calculate 'R(0->begin_time, A->Plate)' in one call to 'rotation_model.get_rotation(interpolated_time, interpolated_isochron_plate_id, 0)'
# because explicitly setting the 'from_time' argument to '0' results in pyGPlates (versions < 0.27) assuming that the present day rotation is zero
# (and so it essentially just calculates 'R(begin_time, A->Plate)' instead of 'R(begin_time, A->Plate) * inverse[R(0, A->Plate)]').
# So to ensure the same results for pyGPlates versions before and after 0.27, we'll exclude the 'from_time' argument for 'R(begin_time, A->Plate)'
# so versions >= 0.27 don't calculate 'R(0->begin_time, A->Plate)' and explicitly include 'inverse[R(0, A->Plate)]' so that versions < 0.27 are accounted for.
#
#
interpolated_isochron_reconstruction = rotation_model.get_rotation(interpolated_time, interpolated_isochron_plate_id) * inverse_present_day_rotation
full_spreading_velocity_vectors = [interpolated_isochron_reconstruction * velocity
for velocity in full_spreading_velocity_vectors_at_present_day]
reconstructed_interpolated_points = [interpolated_isochron_reconstruction * point
for point in interpolated_points]
# Convert global 3D velocity vectors to local (magnitude, azimuth, inclination) tuples (one tuple per point).
full_spreading_velocities = pygplates.LocalCartesian.convert_from_geocentric_to_magnitude_azimuth_inclination(
reconstructed_interpolated_points,
full_spreading_velocity_vectors)
# Extract the 'magnitude' element of each velocity tuple (if requested) and adjust for asymmetry.
if SCALAR_TYPE_SPREADING_RATE in output_scalar_types:
# Convert asymmetry from range [-1,1] to [0,1] and multiply by the full spreading velocity.
spreading_rates = [0.5 * (spreading_asymmetries[point_index] + 1) * full_spreading_velocities[point_index][0]
for point_index in range(len(full_spreading_velocities))]
scalar_coverages[SCALAR_TYPE_SPREADING_RATE] = spreading_rates
# Extract the 'magnitude' element of each velocity tuple (if requested).
if SCALAR_TYPE_FULL_SPREADING_RATE in output_scalar_types:
full_spreading_rates = [velocity[0] for velocity in full_spreading_velocities]
scalar_coverages[SCALAR_TYPE_FULL_SPREADING_RATE] = full_spreading_rates
# Extract the 'azimuth' element of each velocity tuple (if requested).
if SCALAR_TYPE_SPREADING_DIRECTION in output_scalar_types:
spreading_directions = []
for velocity in full_spreading_velocities:
spreading_direction = math.degrees(velocity[1])
# Since spreading at crustal accretion is in two opposite directions,
# we pick the direction with the smaller azimuth (an azimuth is in the range [0, 360]).
# For example, for an azimuth of 225 and its opposite (225 - 180 = 45) we choose 45.
if spreading_direction > 180:
spreading_direction -= 180
spreading_directions.append(spreading_direction)
scalar_coverages[SCALAR_TYPE_SPREADING_DIRECTION] = spreading_directions
# Calculate deviation of spreading direction from isochron normal.
if SCALAR_TYPE_SPREADING_OBLIQUITY in output_scalar_types:
spreading_obliquities = []
# First calculate a normal for each arc segment (between two adjacent points)
# in the interpolated polyline.
segment_normals = []
for segment in interpolated_polyline.get_segments():
if segment.is_zero_length():
segment_normal = None
else:
segment_normal = segment.get_great_circle_normal()
segment_normals.append(segment_normal)
# For each isochron point calculate a spreading obliquity for its two adjoining arc segment normals and then
# average the two obliquities (if both are ridges, ie, less than 45 degrees), or take the one obliquity that
# is a ridge (if other is transform), or both are transforms so hard-wire obliquity to 90 degrees.
num_points = len(interpolated_points)
for point_index in range(num_points):
# Normal of segment before current point.
if point_index > 0:
prev_normal = segment_normals[point_index - 1]
else:
prev_normal = None
# Normal of segment after current point.
if point_index < num_points - 1:
next_normal = segment_normals[point_index]
else:
next_normal = None
#
# The angle between the velocity and normal vectors is in the range [0, 180].
#
# Obliquity is how much the small circle of spreading rotation pole (-/+ velocity_vector)
# deviates from the perpendicular line at the isochron point (-/+ normal).
# This is the minimum deviation of 'velocity_vector' and '-velocity_vector' from 'normal' and '-normal'.
# Obliquity angle is in range [0, 90].
#
velocity_vector = full_spreading_velocity_vectors_at_present_day[point_index]
# Calculate spreading obliquity for previous normal. If there is no previous normal or
# obliquity is larger than 45 degrees then set to None.
if prev_normal:
# Range [0,180].
spreading_obliquity_for_prev_normal = math.degrees(pygplates.Vector3D.angle_between(
velocity_vector,
prev_normal))
# Range [0,90].
if spreading_obliquity_for_prev_normal > 90:
spreading_obliquity_for_prev_normal = 180 - spreading_obliquity_for_prev_normal
# Exclude transform sections (obliquity larger than 45 degrees).
if spreading_obliquity_for_prev_normal > 45:
spreading_obliquity_for_prev_normal = None
else:
spreading_obliquity_for_prev_normal = None
# Calculate spreading obliquity for next normal. If there is no next normal or
# obliquity is larger than 45 degrees then set to None.
if next_normal:
# Range [0,180].
spreading_obliquity_for_next_normal = math.degrees(pygplates.Vector3D.angle_between(
velocity_vector,
next_normal))
# Range [0,90].
if spreading_obliquity_for_next_normal > 90:
spreading_obliquity_for_next_normal = 180 - spreading_obliquity_for_next_normal
# Exclude transform sections (obliquity larger than 45 degrees).
if spreading_obliquity_for_next_normal > 45:
spreading_obliquity_for_next_normal = None
else:
spreading_obliquity_for_next_normal = None
if (spreading_obliquity_for_prev_normal is not None and
spreading_obliquity_for_next_normal is not None):
# Both previous and next normals have ridge-like spreading obliquities.
spreading_obliquity = 0.5 * (spreading_obliquity_for_prev_normal + spreading_obliquity_for_next_normal)
elif spreading_obliquity_for_prev_normal is not None:
# Only previous normal has ridge-like spreading obliquity.
spreading_obliquity = spreading_obliquity_for_prev_normal
elif spreading_obliquity_for_next_normal is not None:
# Only next normal has ridge-like spreading obliquity.
spreading_obliquity = spreading_obliquity_for_next_normal
else:
# Is a transform, so hard-wire to 90 degrees.
spreading_obliquity = 90
spreading_obliquities.append(spreading_obliquity)
scalar_coverages[SCALAR_TYPE_SPREADING_OBLIQUITY] = spreading_obliquities
# Output age scalar coverage (if requested) - per-point age values.
if SCALAR_TYPE_AGE in output_scalar_types:
# Each point in the polyline has the same age.
scalar_coverages[SCALAR_TYPE_AGE] = [interpolated_time] * len(interpolated_polyline)
# Convert the geometry to a coverage which is a (geometry, scalar-coverages-dict) tuple.
return (interpolated_polyline, scalar_coverages)
#
# Private function.
#
# Write a GMT ".xy" feature containing an interpolated isochron polyline and associated
# per-point scalar values (one point and scalar values per line in the file).
#
def _write_coverage_to_xy_file(xy_file, coverage, output_scalar_types):
coverage_geometry, coverage_scalars = coverage
coverage_points = coverage_geometry.get_points()
# Get the points as a list of latitudes and a list of longitudes.
lat_lon_points = [point.to_lat_lon() for point in coverage_points]
latitudes, longitudes = zip(*lat_lon_points)
# A list of lists of scalar values.
# The first two lists are the longitudes and latitudes.
scalars_list = [longitudes, latitudes]
for output_scalar_type in output_scalar_types:
scalars = coverage_scalars[output_scalar_type]
# If asymmetries then convert from range [-1,1] to [0,100] when writing to '.xy'.
if output_scalar_type == SCALAR_TYPE_SPREADING_ASYMMETRY:
scalars = [50 * (scalar + 1) for scalar in scalars]
scalars_list.append(scalars)
# Determine the output line format string (which is '{0} {1} {2} ...').
line_format_string = ' '.join(['{{{0}}}'.format(i) for i in range(len(scalars_list))])
line_format_string += '\n'
# Write the start of the feature.
xy_file.write('>\n')
# Iterate over the points/scalars and write each point to a separate line.
for point_index in range(len(coverage_points)):
# The x, y, z, ... scalar values to write to the current line of the file.
line_scalars = [scalars[point_index] for scalars in scalars_list]
xy_file.write(line_format_string.format(*line_scalars))
def write_coverage_features_to_xy_file(
xy_filename,
coverage_features,
output_scalar_types,
rotation_model,
anchor_plate_id=0,
reconstruction_time=None,
print_debug_output=0):
"""write_coverage_features_to_xy_file(xy_filename, coverage_features, output_scalar_types, rotation_model, anchor_plate_id, reconstruction_time, print_debug_output)
Write a GMT ".xy" ascii file containing the interpolated isochron polylines and associated per-point scalar values (one point and scalar values per line in the file).
:param xy_filename: filename of '.xy' file
:type xy_filename: str
:param coverage_features: the features
:type coverage_features: a sequence of pygplates.Feature
:param output_scalar_types: list of scalar types (must match those of coverages in 'coverage_features')
:type output_scalar_types: list of pygplates.ScalarType
:param rotation_model: the rotation model
:type rotation_model: pygplates.RotationModel
:param anchor_plate_id: the closeness threshold, in radians, used to determine if two geometries are adjacent
:type anchor_plate_id: int - defaults to zero
:param reconstruction_time: the reconstruction time (or None if not reconstructing)
:type reconstruction_time: float or None - defaults to None
:param print_debug_output: the level at which to print debug output - zero means no debug output, \
one or more means debug output
:type print_debug_output: int - defaults to zero
"""
if reconstruction_time is None:
if print_debug_output >= 1:
print('Write features...')
with open(xy_filename, 'w') as xy_file:
for feature in coverage_features:
coverage = feature.get_geometry(coverage_return=pygplates.CoverageReturn.geometry_and_scalars)
if not coverage:
# Shouldn't be able to get here since all feature should have coverages as geometries.
continue
_write_coverage_to_xy_file(xy_file, coverage, output_scalar_types)
else:
if print_debug_output >= 1:
print('Exporting reconstructed features...')
# Reconstruct the features.
reconstructed_feature_geometries = []
pygplates.reconstruct(coverage_features, rotation_model, reconstructed_feature_geometries, reconstruction_time, anchor_plate_id)
with open(xy_filename, 'w') as xy_file:
for reconstructed_feature_geometry in reconstructed_feature_geometries:
# Get the coverage from the feature (contains scalars values and present-day geometry).
coverage = reconstructed_feature_geometry.get_feature().get_geometry(coverage_return=pygplates.CoverageReturn.geometry_and_scalars)
if not coverage:
# Shouldn't be able to get here since all feature should have coverages as geometries.
continue
# Replace the present day geometry with the reconstructed geometry.
# The coverage is a tuple of (geometry, scalar-values-dict).
coverage = (reconstructed_feature_geometry.get_reconstructed_geometry(), coverage[1])
_write_coverage_to_xy_file(xy_file, coverage, output_scalar_types)
def join_adjacent_features_with_same_type_and_begin_time_and_plate_ids(features, distance_threshold_radians, print_debug_output):
"""join_adjacent_features_with_same_type_and_begin_time_and_plate_ids(features, distance_threshold_radians)
Joins features that have adjacent geometries and have the same feature type, begin time and plate ids.
:param features: the features
:type features: a sequence of pygplates.Feature
:param distance_threshold_radians: the closeness threshold, in radians, used to determine if two geometries are adjacent
:type distance_threshold_radians: float
:param print_debug_output: the level at which to print debug output (0, 1, 2 or 3) - zero means no debug output
:type print_debug_output: int - defaults to zero
"""
if print_debug_output >= 1:
print('Join features...')
# Group features by (begin time, reconstruction plate id, conjugate plate id, feature type).
join_feature_groups = {}
for feature in features:
begin_time, end_time = feature.get_valid_time()
# Get left and right plate ids (in one form or another), otherwise skip the current feature.
left_right_plate_ids = _get_left_right_plate_ids(feature)
if not left_right_plate_ids:
if print_debug_output >= 1:
print(" ...skipping '{0}' "
"feature (id: {1}): No left/right or reconstruction/conjugate plate IDs and no "
"'gpml:OldPlatesHeader' property."
.format(feature.get_feature_type(), feature.get_feature_id()))
continue
left_plate_id, right_plate_id = left_right_plate_ids
join_feature_group = join_feature_groups.setdefault(
(begin_time, left_plate_id, right_plate_id, feature.get_feature_type()),
[])
join_feature_group.append(feature)
joined_features = []
# Iterate over the groups (lists) of features to join and attempt to join.
for join_feature_group in join_feature_groups.values():
# Join any adjacent features in the group - this potentially modifies 'join_feature_group'.
_remove_features_with_duplicate_geometries(join_feature_group)
joined_features.extend(
_join_adjacent_features(join_feature_group, distance_threshold_radians, print_debug_output))
return joined_features
def interpolate_isochrons(
rotation_model,
isochron_cob_ridge_features,
# Note: can be a distance spacing in radians, or a list of interpolation times (see docstring below)...
interpolate=DEFAULT_INTERPOLATE_RESOLUTION_RADIANS,
minimum_latitude_overlap_radians=DEFAULT_MINIMUM_LATITUDE_OVERLAP_RADIANS,
maximum_isochron_latitude_non_overlap_radians=DEFAULT_MAXIMUM_ISOCHRON_LATITUDE_NON_OVERLAP_RADIANS,
maximum_cob_latitude_non_overlap_radians=DEFAULT_MAXIMUM_COB_LATITUDE_NON_OVERLAP_RADIANS,
maximum_distance_threshold_radians=DEFAULT_MAXIMUM_DISTANCE_THRESHOLD_RADIANS,
maximum_age_difference=DEFAULT_MAXIMUM_AGE_DIFFERENCE,
join_polylines_threshold_radians=DEFAULT_JOIN_POLYLINES_THRESHOLD_RADIANS,
**kwargs):
"""interpolate_isochrons(rotation_model, isochron_cob_ridge_features, \
[interpolate=DEFAULT_INTERPOLATE_RESOLUTION_RADIANS], \
[minimum_latitude_overlap_radians=DEFAULT_MINIMUM_LATITUDE_OVERLAP_RADIANS], \
[maximum_isochron_latitude_non_overlap_radians=DEFAULT_MAXIMUM_ISOCHRON_LATITUDE_NON_OVERLAP_RADIANS], \
[maximum_cob_latitude_non_overlap_radians=DEFAULT_MAXIMUM_COB_LATITUDE_NON_OVERLAP_RADIANS], \
[maximum_distance_threshold_radians=DEFAULT_MAXIMUM_DISTANCE_THRESHOLD_RADIANS], \
[maximum_age_difference=DEFAULT_MAXIMUM_AGE_DIFFERENCE], \
[join_polylines_threshold_radians=DEFAULT_JOIN_POLYLINES_THRESHOLD_RADIANS], \
[\\*\\*kwargs]) \
Generate interpolated isochrons using isochrons, continental-oceanic boundaries and present-day ridges.
:param rotation_model: the rotation model
:type rotation_model: pygplates.RotationModel
:param isochron_cob_ridge_features: input features containing isochrons, continental-oceanic \
boundaries and ridge boundaries
:type isochron_cob_ridge_features: pygplates.FeatureCollection, or string (filename), or pygplates.Feature, \
or sequence of pygplates.Feature, or sequence of any combination of those four types
:param interpolate: if a single number then *interpolate* is the interval spacing, in radians, \
between input features at which to generate interpolated isochrons - otherwise if a sequence of \
numbers (eg, list or tuple) then *interpolate* is the sequence of times at which to generate \
interpolated isochrons - by default it is the single number DEFAULT_INTERPOLATE_RESOLUTION_RADIANS
:type interpolate: float or list of float
:param minimum_latitude_overlap_radians: required amount of latitude overlap of two input feature \
polyline geometries in order for them to be interpolated
:type minimum_latitude_overlap_radians: float
:param maximum_isochron_latitude_non_overlap_radians: allowed non-overlapping latitude region \
when the older of two input feature geometries being interpolated belongs to an isochron feature
:type maximum_isochron_latitude_non_overlap_radians: float
:param maximum_cob_latitude_non_overlap_radians: allowed non-overlapping latitude region \
when the older of two input feature geometries being interpolated belongs to a COB feature
:type maximum_cob_latitude_non_overlap_radians: float
:param maximum_distance_threshold_radians: maximum distance (in radians) between two input \
feature geometries above which they will not be interpolated
:type maximum_distance_threshold_radians: float
:param maximum_age_difference: maximum difference in age, in My, between two input feature \
geometries above which they will not be interpolated
:type maximum_age_difference: float
:param join_polylines_threshold_radians: the closeness threshold, in radians, used to determine if \
two polyline input feature geometries are adjacent and hence can be joined into a single geometry
:type join_polylines_threshold_radians: float
:returns: the interpolated features
:rtype: list of pygplates.Feature
:raises: OpenFileForReadingError if any file is not readable (when filenames specified)
:raises: FileFormatNotSupportedError if any file format (identified by the filename \
extensions) does not support reading (when filenames specified)
The following optional keyword arguments are supported by *kwargs*:
+-------------------------------------+-------+---------+-----------------------------------------------------------------------------+
| Name | Type | Default | Description |
+-------------------------------------+-------+---------+-----------------------------------------------------------------------------+
| tessellate_threshold_radians | float | None | The maximum tessellation angle, in radians, between adjacent points in |
| | | | each interpolated polyline. |
| | | | Note that this is *along* each polyline (not between polylines). |
| | | | If not specified (None) then there is no tessellation. |
+-------------------------------------+-------+---------+-----------------------------------------------------------------------------+
| plate_pairs | list | None | Optional list of plate pairs to restrict output to. |
| | | | This is a list of (int, int) tuples. |
+-------------------------------------+-------+---------+-----------------------------------------------------------------------------+
| print_debug_output | int | 0 | The level at which to print debug output (0, 1, 2 or 3). |
| | | | Zero means no debug output. |
+-------------------------------------+-------+---------+-----------------------------------------------------------------------------+
| output_scalar_spreading_asymmetry | bool | False | Store spreading asymmetry at each point in each interpolated isochron. |
| | | | This is in the range [-1,1] where 0 represents half-stage rotation, |
| | | | -1 represents spreading only on the conjugate flank and |
| | | | +1 represents spreading only on the flank containing the isochron. |
| | | | This will stored under the 'gpml:SpreadingAsymmetry' scalar type. |
+-------------------------------------+-------+---------+-----------------------------------------------------------------------------+
| output_scalar_spreading_rate | bool | False | Store spreading rate at each point in each interpolated isochron. |
| | | | This is the spreading rate relative to the mid-ocean ridge. |
| | | | This velocity magnitude is in Kms/My. |
| | | | This will stored under the 'gpml:SpreadingRate' scalar type. |
+-------------------------------------+-------+---------+-----------------------------------------------------------------------------+
| output_scalar_full_spreading_rate | bool | False | Store *full* spreading rate at each point in each interpolated isochron. |
| | | | This is the spreading rate relative to the conjugate plate. |
| | | | This velocity magnitude is in Kms/My. |
| | | | This will stored under the 'gpml:SpreadingRate' scalar type. |
+-------------------------------------+-------+---------+-----------------------------------------------------------------------------+
| output_scalar_spreading_direction | bool | False | Store spreading direction at each point in each interpolated isochron. |
| | | | This is an angle (in degrees) clockwise (East-wise) from North (0 to 180). |
| | | | The lowest azimuth of the two opposite-pointing directions of spreading |
| | | | at the time of crustal accretion. |
| | | | This will stored under the 'gpml:SpreadingDirection' scalar type. |
+-------------------------------------+-------+---------+-----------------------------------------------------------------------------+
| output_scalar_spreading_obliquity | bool | False | Store spreading obliquity at each point in each interpolated isochron. |
| | | | This is an angle from 0 to 90 degrees. |
| | | | The amount that the small circle of spreading rotation pole deviates from |
| | | | the perpendicular line at the ridge point at the time of crustal accretion. |
| | | | This will stored under the 'gpml:SpreadingObliquity' scalar type. |
+-------------------------------------+-------+---------+-----------------------------------------------------------------------------+
| output_scalar_age | bool | False | Store age at each point in each interpolated isochron. |
| | | | This is the interpolated isochron's age (in Ma). It is constant for all |
| | | | points within an isochron and is equal to the isochron's begin time. |
| | | | This will stored under the 'gpml:Age' scalar type. |
+-------------------------------------+-------+---------+-----------------------------------------------------------------------------+
The algorithms used in this function very closely match those used in the C++ program Intertec.
If *interpolate* is a single number then features are interpolated at regularly spaced intervals
of *interpolate* radians between input features. Most of these interpolated features are isochrons
with the boundaries being continental-oceanic boundaries (COB).
If *interpolate* is a sequence of numbers then features are interpolated at geological times
specified by these numbers. All of these interpolated features will be isochrons.
For more details on *minimum_latitude_overlap_radians*, *maximum_isochron_latitude_non_overlap_radians*,
*maximum_cob_latitude_non_overlap_radians* and *maximum_distance_threshold_radians*
please see the pygplates API documentation for the function pygplates.PolylineOnSphere.rotation_interpolate.
To interpolate isochrons with a spacing of 2 minutes (with a minimum required latitude
overlap of 1 degree and with an allowed latitude non-overlap of up to 3 degrees for isochrons
and up to 1 degree for COBs and with other parameters assuming default values):
::
interpolated_isochrons = interpolate_isochrons(
pygplates.RotationModel('rotations.rot'),
('cobs.gpml', 'isochrons.gpml', 'ridges.gpml'),
math.radians(2 / 60.0),
math.radians(1),
math.radians(3),
math.radians(1),
print_debug_output = 1)
...where *print_debug_output* is one of the optional keyword arguments shown in the *kwargs* table above.
To interpolate isochrons at times between 0Ma and 100Ma at 10My intervals (with a minimum
required latitude overlap of 1 degree and with an allowed latitude non-overlap of up to 3 degrees
for isochrons and up to 1 degree for COBs and with other parameters assuming default values):
::
interpolated_isochrons = interpolate_isochrons(
pygplates.RotationModel('rotations.rot'),
('cobs.gpml', 'isochrons.gpml', 'ridges.gpml'),
range(0, 101, 10),
math.radians(1),
math.radians(3),
math.radians(1),
print_debug_output = 1)
"""
# Check the imported pygplates version.
if not hasattr(pygplates, 'Version') or pygplates.Version.get_imported_version() < PYGPLATES_VERSION_REQUIRED:
raise RuntimeError('{0}: Error - imported pygplates version {1} but version {2} or greater is required'.format(
os.path.basename(__file__), pygplates.Version.get_imported_version(), PYGPLATES_VERSION_REQUIRED))
# Keyword arguments.
#
tessellate_threshold_radians = kwargs.pop('tessellate_threshold_radians', None)
plate_pairs = kwargs.pop('plate_pairs', None)
print_debug_output = kwargs.pop('print_debug_output', 0)
# Scalar coverages to output.
output_scalar_types = set()
if kwargs.pop('output_scalar_spreading_asymmetry', False):
output_scalar_types.add(SCALAR_TYPE_SPREADING_ASYMMETRY)
if kwargs.pop('output_scalar_spreading_rate', False):
output_scalar_types.add(SCALAR_TYPE_SPREADING_RATE)
if kwargs.pop('output_scalar_full_spreading_rate', False):
output_scalar_types.add(SCALAR_TYPE_FULL_SPREADING_RATE)
if kwargs.pop('output_scalar_spreading_direction', False):
output_scalar_types.add(SCALAR_TYPE_SPREADING_DIRECTION)
if kwargs.pop('output_scalar_spreading_obliquity', False):
output_scalar_types.add(SCALAR_TYPE_SPREADING_OBLIQUITY)
if kwargs.pop('output_scalar_age', False):
output_scalar_types.add(SCALAR_TYPE_AGE)
# Raise error if any unknown keyword arguments.
if kwargs:
raise TypeError('Unknown keyword arguments {0}'.format(kwargs.keys()))
# 'interpolate' is either a sequence of times or an interval spacing (in radians).
if hasattr(interpolate, '__iter__'):
# Sort sequence of interpolation times to make it easier to find input features
# whose times overlap them.
interpolation_times = sorted(float(time) for time in interpolate)
using_interpolation_times = True
else:
interpolate_interval_radians = float(interpolate)
using_interpolation_times = False
# Turn function argument into something more convenient for extracting features.
isochron_cob_ridge_features = pygplates.FeaturesFunctionArgument(isochron_cob_ridge_features)
if print_debug_output >= 1:
print('Read features...')
isochron_cob_ridge_features = isochron_cob_ridge_features.get_features()
# Filter the features if requested.
if plate_pairs is not None:
_filter_plate_pairs(isochron_cob_ridge_features, plate_pairs)
joined_isochron_cob_ridge_features = join_adjacent_features_with_same_type_and_begin_time_and_plate_ids(
isochron_cob_ridge_features,
join_polylines_threshold_radians,
print_debug_output)
if print_debug_output >= 1: