diff --git a/HISTORY.rst b/HISTORY.rst index ce9664a..405c127 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,5 +1,8 @@ Release History +0.8.0 ( ?????????????? ) +++++++++++++++++++++++++ + 0.7.3 (October, 6, 2020) ++++++++++++++++++++++++ * update to python 3.8 and change tests for compatibility diff --git a/pyprom/__init__.py b/pyprom/__init__.py index 889c38b..26ef622 100644 --- a/pyprom/__init__.py +++ b/pyprom/__init__.py @@ -6,7 +6,7 @@ PyProm: This library includes tools for surface network analysis. """ -version_info = (0, 7, 3) +version_info = (0, 8, 0) __name__ = 'pyProm' __doc__ = 'A python surface network analysis script' __author__ = 'Marc Howes' diff --git a/pyprom/dataload.py b/pyprom/dataload.py index b61a6da..f5f4eee 100644 --- a/pyprom/dataload.py +++ b/pyprom/dataload.py @@ -8,8 +8,8 @@ import os import numpy import logging -import gdal -import osr +from osgeo import gdal +from osgeo import osr from .lib.datamap import ProjectionDataMap @@ -88,6 +88,7 @@ def __init__(self, filename, epsg_alias="WGS84"): # Create target Spatial Reference for converting coordinates. target = osr.SpatialReference() target.ImportFromEPSG(epsg_code) + target.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER) transform = osr.CoordinateTransformation(spatialRef, target) # create a reverse transform for translating back # into Native GDAL coordinates diff --git a/pyprom/feature_discovery.py b/pyprom/feature_discovery.py index 13b6bce..367ada6 100644 --- a/pyprom/feature_discovery.py +++ b/pyprom/feature_discovery.py @@ -382,7 +382,9 @@ def edge_feature_analysis(self, x, y, perimeter, if multipoint: pts = multipoint.points # this gets the closest single highPerimeterNeighborhood point to our midpoint - highPerimeterNeighborhoods.append([high_perimeter_neighborhood_shortest_path(mid, pts, highPerimeter, self.datamap)]) + highPerimeterNeighborhoods.append( + (high_perimeter_neighborhood_shortest_path(mid, pts, highPerimeter, self.datamap),) + ) else: # just use the regular highPerimeterNeighborhoods if not a multipoint highPerimeterNeighborhoods = highPerimeter diff --git a/pyprom/lib/containers/linker.py b/pyprom/lib/containers/linker.py index 8ca92cb..4c023cc 100644 --- a/pyprom/lib/containers/linker.py +++ b/pyprom/lib/containers/linker.py @@ -151,6 +151,17 @@ def linkers_to_summits_connected_via_saddle(self, excludeSelf=True, if _linker_ok(linker, skipDisqualified, {}) and self._help_exclude_self(linker, excludeSelf)] + def linker_other_side_of_saddle(self): + """ + Much faster, but less robust than linkers_to_summits_connected_via_saddle + Uses linker ids. + :return: list of linkers to summits connected to the saddle this + linker links + :rtype: list(:class:`Linker`) + """ + + return [x for x in self.saddle.summits if x.id != self.id and not x.disqualified] + def add_to_remote_saddle_and_summit(self, ignoreDuplicates=True): """ Safely adds this linker to the remote diff --git a/pyprom/lib/containers/spot_elevation.py b/pyprom/lib/containers/spot_elevation.py index b3ef09e..94a2b37 100644 --- a/pyprom/lib/containers/spot_elevation.py +++ b/pyprom/lib/containers/spot_elevation.py @@ -37,7 +37,14 @@ def __init__(self, spotElevationList): """ super(SpotElevationContainer, self).__init__() self.points = spotElevationList - self.fast_lookup = {point.id: point for point in self.points} + self.fast_lookup = self.generate_fast_lookup() + + def generate_fast_lookup(self): + """ + Produces a fast lookup dict of this Container. + :return: {id: SpotElevation} fast lookup dict. + """ + return {point.id: point for point in self.points} @property def lowest(self): diff --git a/pyprom/lib/locations/saddle.py b/pyprom/lib/locations/saddle.py index 803584c..798ca74 100644 --- a/pyprom/lib/locations/saddle.py +++ b/pyprom/lib/locations/saddle.py @@ -83,7 +83,9 @@ def __init__(self, latitude, longitude, elevation, *args, **kwargs): elevation, *args, **kwargs) self.multipoint = kwargs.get('multipoint', []) self.highPerimeterNeighborhoods = kwargs.get('highPerimeterNeighborhoods', []) - self.id = kwargs.get('id', 'sa:' + randomString()) + self.id = kwargs.get('id') + if not self.id: + self.id = 'sa:' + randomString() # List of linkers to summits self.summits = [] # If this is set, this saddle has spun out another @@ -375,11 +377,11 @@ def from_dict(cls, saddleDict, datamap=None): hsx = [] for hs in hss: hsx.append(tuple(hs)) - highPerimeterNeighborhoods.append(hsx) + highPerimeterNeighborhoods.append(tuple(hsx)) return cls(lat, long, elevation, multipoint=multipoint, - highPerimeterNeighborhoods=highPerimeterNeighborhoods, + highPerimeterNeighborhoods=tuple(highPerimeterNeighborhoods), edge=edge, edgePoints=edgePoints, id=id, diff --git a/pyprom/lib/locations/spot_elevation.py b/pyprom/lib/locations/spot_elevation.py index 9bed921..bda0598 100644 --- a/pyprom/lib/locations/spot_elevation.py +++ b/pyprom/lib/locations/spot_elevation.py @@ -42,7 +42,9 @@ def __init__(self, latitude, longitude, elevation, *args, **kwargs): self.elevation = elevation self.edgeEffect = kwargs.get('edge', False) self.edgePoints = kwargs.get('edgePoints', []) - self.id = kwargs.get('id', 'se:' + randomString()) + self.id = kwargs.get('id') + if not self.id: + self.id = 'se:' + randomString() def to_dict(self): """ diff --git a/pyprom/lib/locations/summit.py b/pyprom/lib/locations/summit.py index 95ba5df..35d967e 100644 --- a/pyprom/lib/locations/summit.py +++ b/pyprom/lib/locations/summit.py @@ -53,7 +53,9 @@ def __init__(self, latitude, longitude, elevation, *args, **kwargs): super(Summit, self).__init__(latitude, longitude, elevation, *args, **kwargs) self.multipoint = kwargs.get('multipoint', []) - self.id = kwargs.get('id', 'su:' + randomString()) + self.id = kwargs.get('id') + if not self.id: + self.id = 'su:' + randomString() # saddles contains a list of linker objects linking this summit to a # saddle. These are populated by :class:`Walk` self.saddles = list() diff --git a/pyprom/lib/logic/basin_saddle_finder.py b/pyprom/lib/logic/basin_saddle_finder.py index 6249c20..147c429 100644 --- a/pyprom/lib/logic/basin_saddle_finder.py +++ b/pyprom/lib/logic/basin_saddle_finder.py @@ -64,7 +64,7 @@ def disqualify_basin_saddles(self): while features: # loop over features if root is None: _, root = features.popitem() - if root.disqualified or root.edgeEffect: + if root.disqualified: root = None continue stack = [root] # stack of features to explore. @@ -73,12 +73,12 @@ def disqualify_basin_saddles(self): cycleMembers = {} while stack: z = stack.pop() - if z.disqualified or z.edgeEffect: + if z.disqualified: features.pop(z.id, None) continue zEexploredNbrs = exploredNbrs[z.id] for nbr in z.feature_neighbors(): - if nbr.disqualified or nbr.edgeEffect: + if nbr.disqualified: features.pop(nbr.id, None) continue if nbr.id not in exploredNbrs: # new node diff --git a/pyprom/lib/logic/breadth_first_search.py b/pyprom/lib/logic/breadth_first_search.py new file mode 100644 index 0000000..a6a7cea --- /dev/null +++ b/pyprom/lib/logic/breadth_first_search.py @@ -0,0 +1,36 @@ + +""" +pyProm: Copyright 2020. +This software is distributed under a license that is described in +the LICENSE file that accompanies it. +This library contains logic for performing breadth first search on +contiguous point sets on a cartesian grid. +""" +from collections import defaultdict + +class BreadthFirstSearch: + def __init__(self, + pointList=None, + pointIndex=None, + datamap=None) + """ + :param list pointList: list(tuple(x,y,z)) + """ + self.datamap = datamap + self.points = [] + + if pointList and pointIndex: + self.points = pointList + self.pointIndex = pointIndex + return + + if pointIndex: + self.pointIndex = pointIndex + self.points = [p for x, _y in self.pointIndex.items() + for y, p in _y.items()] + + if pointList: + self.points = pointList + self.pointIndex = defaultdict(dict) + for point in self.points: + self.pointIndex[point[0]][point[1]] = point \ No newline at end of file diff --git a/pyprom/lib/logic/contiguous_neighbors.py b/pyprom/lib/logic/contiguous_neighbors.py index e8744b9..2548069 100644 --- a/pyprom/lib/logic/contiguous_neighbors.py +++ b/pyprom/lib/logic/contiguous_neighbors.py @@ -31,7 +31,7 @@ def contiguous_neighbors(points): stack = [points.pop()] lookup[stack[0][0]][stack[0][1]] = None neighbors = list([stack[0]]) - neighborsList.append(neighbors) + neighborsList.append(tuple(neighbors)) while stack: # Grab a point from the stack. point = stack.pop() diff --git a/pyprom/lib/logic/internal_saddle_network.py b/pyprom/lib/logic/internal_saddle_network.py index 657caf5..69b690e 100644 --- a/pyprom/lib/logic/internal_saddle_network.py +++ b/pyprom/lib/logic/internal_saddle_network.py @@ -210,8 +210,8 @@ def generate_child_saddles(self): self.saddle.longitude, self.saddle.elevation) - newSaddle.highPerimeterNeighborhoods = [[link.local], - [link.remote]] + newSaddle.highPerimeterNeighborhoods = [(link.local,), + (link.remote,)] if self.saddle.edgeEffect: newSaddle.parent = self.saddle diff --git a/pyprom/lib/logic/parentage.py b/pyprom/lib/logic/parentage.py new file mode 100644 index 0000000..8056362 --- /dev/null +++ b/pyprom/lib/logic/parentage.py @@ -0,0 +1,242 @@ +""" +pyProm: Copyright 2020. +This software is distributed under a license that is described in +the LICENSE file that accompanies it. +This library contains a class for manipulating a pyProm Prominence Island +""" + +from collections import deque + + +# Use static values as they are a bit more performant and we'll be using them a lot. +LOCAL_LINKER = 0 +LOWEST_SADDLE = 1 +HIGHEST_SUMMIT = 2 + +class ProminenceIslandParentFinder: + + def __init__(self, summit): + self.summit = summit + self.queue = deque() + self.candidate_key_col = None + self.candidate_parent = None + + self.candidate_offmap_saddles = set() + + + def find_parent(self): + for local_linker in self.summit.saddles: + if local_linker.disqualified: + continue + # we pass a tuple around since it's lightweight and disposable. + obj = ( + local_linker, # Linker between this summit and the next saddle. + None, # lowest saddle seen + None, # highest summit seen + ) + self.queue.append(obj) + self.breadth_search() + + offmap_saddles = [] + if self.candidate_key_col: + for candidate_offmap_saddle in self.candidate_offmap_saddles: + if candidate_offmap_saddle.elevation > self.candidate_key_col.elevation: + offmap_saddles.append(candidate_offmap_saddle) + else: + offmap_saddles = self.candidate_offmap_saddles + + return self.candidate_key_col, self.candidate_parent, offmap_saddles + + def breadth_search(self): + """ + the breadth search function, consumes the current queue until exhausted. + Its the responsibility of the caller to iterate and replenish the queue. + """ + while self.queue: + obj = self.queue.pop() + saddle = obj[LOCAL_LINKER].saddle + # This will fail if we encounter a runoff or some other oddball scenarios. + ############## + try: + next_linker = obj[LOCAL_LINKER].linker_other_side_of_saddle()[0] + except: + # keep track of any low saddles that lead up to an edge. + # We'll cull any invalid ones at the caller + if saddle.edgeEffect: + if obj[LOWEST_SADDLE]: + self.candidate_offmap_saddles.add(obj[LOWEST_SADDLE]) + else: + print(f"BAD: {saddle}") + continue + ################# + summit_under_test = next_linker.summit + + if summit_under_test.edgeEffect: + if obj[LOWEST_SADDLE]: + self.candidate_offmap_saddles.add(obj[LOWEST_SADDLE]) + + + # lowest seen already lower than the candidate? bail. + if self.candidate_key_col and obj[LOWEST_SADDLE].elevation < self.candidate_key_col.elevation: + continue + + # have we seen a highest summit, and are we at a lower saddle? + if obj[HIGHEST_SUMMIT] and saddle.elevation < obj[LOWEST_SADDLE].elevation: + if not self.candidate_key_col: + self.candidate_key_col = obj[LOWEST_SADDLE] + self.candidate_parent = obj[HIGHEST_SUMMIT] + continue + + # If we have an existing candidate col and we're the higher one, use us. + if self.candidate_key_col and self.candidate_key_col.elevation < obj[LOWEST_SADDLE].elevation: + self.candidate_key_col = obj[LOWEST_SADDLE] + self.candidate_parent = obj[HIGHEST_SUMMIT] + continue + + # If we have an existing candidate col and we're the same height, use the one with the taller summit + elif self.candidate_key_col and self.candidate_key_col.elevation == obj[LOWEST_SADDLE].elevation: + if self.candidate_parent.elevation < obj[HIGHEST_SUMMIT].elevation: + self.candidate_key_col = obj[LOWEST_SADDLE] + self.candidate_parent = obj[HIGHEST_SUMMIT] + continue + + # + # add logic for handling map edges here. + # + + # check for low saddle. + if not obj[LOWEST_SADDLE]: + lowest = saddle + elif obj[LOWEST_SADDLE] and saddle.elevation < obj[LOWEST_SADDLE].elevation: + lowest = saddle + else: + lowest = obj[LOWEST_SADDLE] + + # if we found a summit taller than anything we've seen, mark it. + if obj[HIGHEST_SUMMIT] and summit_under_test.elevation > obj[HIGHEST_SUMMIT].elevation: + highest = summit_under_test + elif not obj[HIGHEST_SUMMIT] and summit_under_test.elevation > self.summit.elevation: + highest = summit_under_test + # otherwise pass along what we already have. + else: + highest = obj[HIGHEST_SUMMIT] + + # Alright, we're done here, queue up the next summits and add them to the next ring. + for next_saddle_linker in summit_under_test.saddles: + if next_saddle_linker.id == next_linker.id or next_saddle_linker.disqualified: + continue + self.queue.appendleft((next_saddle_linker, lowest, highest)) + + +#### +""" +Troublesome scenarios: Equal height, +Should we track equal heights along a path and use the low point between them as the col? +Edge, we need to track edges. +""" + + +class LineParentFinder: + + def __init__(self, summit): + self.summit = summit + self.queue = deque() + self.candidate_key_col = None + self.candidate_parent = None + + self.candidate_offmap_saddles = set() + + def find_parent(self): + for local_linker in self.summit.saddles: + if local_linker.disqualified: + continue + # we pass a tuple around since it's lightweight and disposable. + obj = ( + local_linker, # Linker between this summit and the next saddle. + None, # lowest saddle seen + ) + self.queue.append(obj) + self.breadth_search() + + offmap_saddles = [] + if self.candidate_key_col: + for candidate_offmap_saddle in self.candidate_offmap_saddles: + if candidate_offmap_saddle.elevation > self.candidate_key_col.elevation: + offmap_saddles.append(candidate_offmap_saddle) + else: + offmap_saddles = self.candidate_offmap_saddles + + return self.candidate_key_col, self.candidate_parent, offmap_saddles + + + def breadth_search(self): + """ + the breadth search function, consumes the current queue until exhausted. + Its the responsibility of the caller to iterate and replenish the queue. + """ + while self.queue: + obj = self.queue.pop() + saddle = obj[LOCAL_LINKER].saddle + # This will fail if we encounter a runoff or some other oddball scenarios. + ############## + try: + next_linker = obj[LOCAL_LINKER].linker_other_side_of_saddle()[0] + except: + # keep track of any low saddles that lead up to an edge. + # We'll cull any invalid ones at the caller + if saddle.edgeEffect: + if obj[LOWEST_SADDLE]: + self.candidate_offmap_saddles.add(obj[LOWEST_SADDLE]) + else: + print(f"BAD: {saddle}") + continue + ################# + summit_under_test = next_linker.summit + + if summit_under_test.edgeEffect: + if obj[LOWEST_SADDLE]: + self.candidate_offmap_saddles.add(obj[LOWEST_SADDLE]) + + viable_saddle = obj[LOWEST_SADDLE] if obj[LOWEST_SADDLE] else saddle + # lowest seen already lower than the candidate? bail. + if self.candidate_key_col and viable_saddle.elevation < self.candidate_key_col.elevation: + continue + + if summit_under_test.elevation > self.summit.elevation: + + if not self.candidate_key_col: + self.candidate_key_col = viable_saddle + self.candidate_parent = summit_under_test + continue + + # If we have an existing candidate col and we're the higher one, use us. + if self.candidate_key_col and self.candidate_key_col.elevation < viable_saddle.elevation: + self.candidate_key_col = viable_saddle + self.candidate_parent = summit_under_test + continue + + # If we have an existing candidate col and we're the same height, use the one with the taller summit + elif self.candidate_key_col and self.candidate_key_col.elevation == viable_saddle.elevation: + if self.candidate_parent.elevation < summit_under_test.elevation: + self.candidate_key_col = viable_saddle + self.candidate_parent = summit_under_test + continue + + + # + # add logic for handling map edges here. + # + + # check for low saddle. + if not obj[LOWEST_SADDLE]: + lowest = saddle + elif obj[LOWEST_SADDLE] and saddle.elevation < obj[LOWEST_SADDLE].elevation: + lowest = saddle + else: + lowest = obj[LOWEST_SADDLE] + + # Alright, we're done here, queue up the next summits and add them to the next ring. + for next_saddle_linker in summit_under_test.saddles: + if next_saddle_linker.id == next_linker.id or next_saddle_linker.disqualified: + continue + self.queue.appendleft((next_saddle_linker, lowest)) \ No newline at end of file diff --git a/pyprom/lib/logic/summit_domain_walk.py b/pyprom/lib/logic/summit_domain_walk.py index a56460b..4d3d83f 100644 --- a/pyprom/lib/logic/summit_domain_walk.py +++ b/pyprom/lib/logic/summit_domain_walk.py @@ -289,7 +289,7 @@ def generate_synthetic_saddles(self, saddle): middleSpotElevation.longitude, saddle.elevation) - highPerimeterNeighborhoods = [[hs0], [hs1]] + highPerimeterNeighborhoods = [(hs0,), (hs1,)] else: newSaddle = Saddle(saddle.latitude, saddle.longitude, diff --git a/pyprom/lib/logic/tuple_funcs.py b/pyprom/lib/logic/tuple_funcs.py index eb0aecd..8330433 100644 --- a/pyprom/lib/logic/tuple_funcs.py +++ b/pyprom/lib/logic/tuple_funcs.py @@ -19,4 +19,4 @@ def highest(points): highest.append(gridPoint) elif gridPoint[2] == high: highest.append(gridPoint) - return highest \ No newline at end of file + return tuple(highest) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5d5b16a..7af44df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ cbor dijkstar +lxml fastkml numpy gdal==3.5.3