diff --git a/.gitignore b/.gitignore index 1c2c799..5ab9116 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ *.pyc _dumpster test.py -pyProm.egg* \ No newline at end of file +pyProm.egg* +docs/_build 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..c88009b 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 @@ -87,6 +87,7 @@ def __init__(self, filename, epsg_alias="WGS84"): if spatialRef.IsProjected: # Create target Spatial Reference for converting coordinates. target = osr.SpatialReference() + target.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER) target.ImportFromEPSG(epsg_code) transform = osr.CoordinateTransformation(spatialRef, target) # create a reverse transform for translating back diff --git a/pyprom/domain_map.py b/pyprom/domain_map.py index ed9233d..76d233b 100644 --- a/pyprom/domain_map.py +++ b/pyprom/domain_map.py @@ -26,6 +26,7 @@ from .lib.containers.summit_domain import SummitDomain from .lib.logic.basin_saddle_finder import BasinSaddleFinder from .lib.logic.summit_domain_walk import Walk +from .lib.contexts.manager import FeatureContextManager from .lib.constants import DOMAIN_EXTENSION from . import version_info @@ -39,12 +40,15 @@ class DomainMap: required to calculate the surface network. """ - def __init__(self, data, + def __init__(self, + data, summits=SummitsContainer([]), saddles=SaddlesContainer([]), runoffs=RunoffsContainer([]), summit_domains=[], - linkers=[]): + linkers=[], + context=None + ): """ A DomainMap consumes either a :class:`pyprom.lib.datamap.DataMap` object or a :class:`pyprom.dataload.Loader` child object. @@ -73,6 +77,7 @@ def __init__(self, data, self.runoffs = runoffs self.linkers = linkers self.summit_domains = summit_domains + self.context = context self.extent = 'LL: {}\n LR: {}\n UL: {}\n UR: {}\n'.format( self.datamap.lower_left, self.datamap.lower_right, @@ -93,10 +98,11 @@ def run(self, sparse=False, superSparse=False, rebuildSaddles=False): :param bool superSparse: just do feature discovery :param bool rebuildSaddles: command AnalyzeData to rebuild saddles """ - # Expunge any existing saddles, runoffs, summits, and linkers + # Expunge any existing saddles, runoffs, summits, and contexts self.saddles = SaddlesContainer([]) self.summits = SummitsContainer([]) self.runoffs = RunoffsContainer([]) + self.context = None self.linkers = list() # Find Features self.summits, self.saddles, self.runoffs =\ @@ -223,9 +229,6 @@ def to_dict(self): """ Returns dict representation of this :class:`DomainMap` - :param bool noWalkPath: exclude - :class:`pyprom.lib.containers.walkpath.WalkPath` from member - :class:`pyprom.lib.containers.linker.Linker` :return: dict() representation of :class:`DomainMap` :rtype: dict() """ diff --git a/pyprom/feature_discovery.py b/pyprom/feature_discovery.py index ce3fb89..877d307 100644 --- a/pyprom/feature_discovery.py +++ b/pyprom/feature_discovery.py @@ -14,11 +14,9 @@ from timeit import default_timer from datetime import timedelta from math import floor -from .lib.locations.gridpoint import GridPoint from .lib.locations.saddle import Saddle from .lib.locations.summit import Summit from .lib.locations.runoff import Runoff -from .lib.locations.base_gridpoint import BaseGridPoint from .lib.containers.saddles import SaddlesContainer from .lib.containers.summits import SummitsContainer from .lib.containers.runoffs import RunoffsContainer @@ -27,6 +25,7 @@ from .lib.logic.contiguous_neighbors import contiguous_neighbors, touching_neighborhoods from .lib.logic.shortest_path_by_points import high_perimeter_neighborhood_shortest_path from .lib.logic.tuple_funcs import highest +from .lib.contexts.manager import FeatureContextManager from .lib.constants import METERS_TO_FEET @@ -50,6 +49,7 @@ def __init__(self, datamap): self.max_y = self.datamap.max_y self.max_x = self.datamap.max_x self.explored = defaultdict(dict) + self.context = FeatureContextManager([], [], []) def run(self, rebuildSaddles=True): """ @@ -66,7 +66,7 @@ def run(self, rebuildSaddles=True): """ _, _, _ = self.analyze() # Corners also are runoffs. - self.runoffObjects.extend(make_corner_runoffs(self.datamap)) + self.runoffObjects.extend(make_corner_runoffs(self.datamap, self.context)) if rebuildSaddles: self.logger.info("Rebuilding Saddles") @@ -93,10 +93,14 @@ def analyze(self): index = 0 start = default_timer() then = start + current_x = 0 # Iterate through numpy grid, and keep track of GridPoint coordinates. while not iterator.finished: x, y = iterator.multi_index # core storage is always in metric. + if current_x != x: + del self.explored[current_x] + current_x = x if self.datamap.unit == "FEET": self.elevation = float(METERS_TO_FEET * iterator[0]) else: @@ -127,10 +131,13 @@ def analyze(self): for result in results: if isinstance(result, Summit): self.summitObjects.append(result) + self.context.add_summit(result) if isinstance(result, Runoff): self.runoffObjects.append(result) + self.context.add_saddle(result) elif isinstance(result, Saddle): self.saddleObjects.append(result) + self.context.add_saddle(result) # Reset variables, and go to next gridpoint. iterator.iternext() # Free some memory. @@ -378,7 +385,7 @@ 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 @@ -426,7 +433,7 @@ def edge_feature_analysis(self, x, y, perimeter, returnable_features.append(saddle) return returnable_features -def make_corner_runoffs(datamap): +def make_corner_runoffs(datamap, ctx): """ Dumb function for generating single point corner runoffs. @@ -448,4 +455,7 @@ def make_corner_runoffs(datamap): datamap.get(0, datamap.max_y)) rur.edgePoints.append((0, datamap.max_y, datamap.get(0, datamap.max_y))) - return [rll, rlr, rul, rur] + corners = [rll, rlr, rul, rur] + for corner in corners: + ctx.add_saddle(corner) + return corners diff --git a/pyprom/lib/containers/linker.py b/pyprom/lib/containers/linker.py index 8ca92cb..52ca759 100644 --- a/pyprom/lib/containers/linker.py +++ b/pyprom/lib/containers/linker.py @@ -151,6 +151,18 @@ 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..cc7ce78 100644 --- a/pyprom/lib/containers/spot_elevation.py +++ b/pyprom/lib/containers/spot_elevation.py @@ -37,7 +37,15 @@ 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/contexts/__init__.py b/pyprom/lib/contexts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyprom/lib/contexts/exceptions.py b/pyprom/lib/contexts/exceptions.py new file mode 100644 index 0000000..42a106d --- /dev/null +++ b/pyprom/lib/contexts/exceptions.py @@ -0,0 +1,26 @@ +""" +pyProm: Copyright 2020. + +This software is distributed under a license that is described in +the LICENSE file that accompanies it. +""" + +class SaddleContextException(Exception): + pass + +class SummitContextException(Exception): + pass + +class LinkageException(Exception): + + def __init__(self, added_saddle, added_summit, recorded_saddle, recorded_summit, duplicate): + self.added_saddle = added_saddle + self.added_summit = added_summit + self.recorded_saddle = recorded_saddle + self.recorded_summit = recorded_summit + self.duplicate = duplicate + msg = lambda x: f"{'OK' if x else 'FAILED'}," + super().__init__(f"added_saddle: {msg(self.added_saddle)} added_summit: {msg(self.added_summit)} recorded_saddle: {msg(self.recorded_saddle)} recorded_summit: {msg(self.recorded_summit)} duplicate: {self.duplicate}") + + + diff --git a/pyprom/lib/contexts/feature_context.py b/pyprom/lib/contexts/feature_context.py new file mode 100644 index 0000000..5b21bb3 --- /dev/null +++ b/pyprom/lib/contexts/feature_context.py @@ -0,0 +1,41 @@ +""" +pyProm: Copyright 2020. + +This software is distributed under a license that is described in +the LICENSE file that accompanies it. +""" + +from ..util import randomString + +class FeatureContext: + + def __init__(self, feature, manager, id=None, disabled=False): + """ + A Base Feature Context. Basically sets up id, disabled. + """ + + self.manager = manager + self.id = id + if not self.id: + self.id = randomString() + self._feature = feature + self._disabled = disabled + + @property + def disabled(self): + return self._disabled + + def disable(self): + """ + Disable this feature + """ + self._disabled = True + self.manager.disabled_tracker[self._feature.id] = True + + def enable(self): + """ + Enable this feature + """ + self._disabled = False + if self.manager.disabled_tracker.get(self._feature.id): + del self.manager.disabled_tracker[self._feature.id] diff --git a/pyprom/lib/contexts/manager.py b/pyprom/lib/contexts/manager.py new file mode 100644 index 0000000..1299e55 --- /dev/null +++ b/pyprom/lib/contexts/manager.py @@ -0,0 +1,397 @@ +""" +pyProm: Copyright 2020. + +This software is distributed under a license that is described in +the LICENSE file that accompanies it. +""" + +from collections import defaultdict + +from .saddle_context import SaddleContext +from .summit_context import SummitContext +from .exceptions import SummitContextException, SaddleContextException +from ..containers.saddles import SaddlesContainer +from ..locations.runoff import Runoff +from ..util import randomString + +class FeatureContextManager: + + def __init__(self, summits, saddles, runoffs, *args, **kwargs): + """ + a feature context creates a context for all first class features + (Saddles, Summits, Runoffs). This context controls whether those features + have certain parameters, like if they are disabled, or what neighbors they have. + + :param summits: list of Summits + :param saddles: list of Saddles + :param runoffs: list of Runoffs + :param args: + :param kwargs: + """ + + self.id = kwargs.get('id') + if not self.id: + self.id = randomString() + + self._saddles = [] + self._summits = [] + self.summit_lookup = {} + self.saddle_lookup = {} + + for s in summits: + self.add_summit(s) + for s in saddles: + self.add_saddle(s) + for r in runoffs: + self.add_saddle(r) + + self.disabled_tracker = {} + self.basin_saddle_tracker = defaultdict(list) + + self.saddle_to_summit_tracker = defaultdict(dict) # {'saddle.id': {summit.id: bool, ...}} + self.summit_to_saddle_tracker = defaultdict(dict) + + def get_ctx(self, feature): + """ + Returns feature context on remote object corresponding to this Context Manager. + :param feature: Saddle, Summit, Runoff + :return: SaddleContext, SummitContext + """ + return feature.contexts.get(self.id, None) + + def add_summit(self, summit, disabled=False): + """ + Add a Summit to this context. + :param summit: Summit + """ + added = self._add_feature(summit, self.summit_lookup, self._summits, disabled=disabled) + if added: + sc = SummitContext(self, summit, [], [], disabled=disabled) + summit.contexts[self.id] = sc + return added + + @property + def summits(self): + """ + List of all summits. + """ + return self._summits + + def add_saddle(self, saddle, disabled=False): + """ + Add a Saddle to this context. + :param saddle: Saddle/Runoff + """ + added = self._add_feature(saddle, self.saddle_lookup, self._saddles, disabled=disabled) + if added: + sc = SaddleContext(self, saddle, [], [], disabled=disabled) + saddle.contexts[self.id] = sc + return added + + @property + def saddles_exact(self): + """ + list of all explicit saddle objects. + """ + return [x for x in self._saddles if not isinstance(x, Runoff)] + + @property + def saddles(self): + """ + list of saddles/runoffs + """ + return self._saddles + + @property + def runoffs(self): + """ + List of Runoffs + """ + return [x for x in self._saddles if isinstance(x, Runoff)] + + + def link_saddle_summit(self, saddle, summit, disable_duplicate=True): + """ + Links tracks Saddles/Summits. Note that disabled + Saddles/Summit/Runoffs can still be linked. + :param saddle: Saddle/Runoff + :param summit: Summit + :param disable_duplicate: bool. if 1 saddle links to 1 summit > 1 times, + disable saddle. + :return bool, bool, bool, bool, bool: added_saddle, added_summit, + recorded_saddle, recorded_summit, purged_duplicate. + """ + + if not self.saddle_lookup.get(saddle.id): + raise SaddleContextException(f"Couldn't find Saddle {saddle.id} in this context.") + if not self.summit_lookup.get(summit.id): + raise SummitContextException(f"Couldn't find Summit {summit.id} in this context.") + + # Update manager records. + recorded_saddle, recorded_summit, duplicate = self.record_saddle_summit_linkage(saddle, summit) + + if duplicate and disable_duplicate: + # de-link + a, b = self.record_saddle_summit_delinkage(saddle, summit) + # reach into feature contexts and remove pairing. + self.get_ctx(saddle)._remove_summit(summit) + self.get_ctx(summit)._remove_saddle(saddle) + # disable that saddle. + self.disable_feature(saddle) + return False, False, False, False, True + + # reach into feature contexts and update. + added_saddle = self.get_ctx(saddle)._add_summit(summit) + added_summit = self.get_ctx(summit)._add_saddle(saddle) + + return added_saddle, added_summit, recorded_saddle, recorded_summit, duplicate + + def delink_saddle_summit(self, saddle, summit): + """ + Delinks tracked Saddles/Summits + :param saddle: Saddle/Runoff + :param summit: Summit + :return: bool saddle, bool summit + """ + + if not self.saddle_lookup[saddle.id]: + raise SaddleContextException("Couldn't find Saddle {saddle.id} in this context.") + if not self.summit_lookup[summit.id]: + raise SummitContextException("Couldn't find Summit {summit.id} in this context.") + + # reach into feature contexts and update. + self.get_ctx(saddle)._remove_summit(summit) + self.get_ctx(summit)._remove_saddle(saddle) + + # Update manager records. + dedsaddle, dedsummit = self.record_saddle_summit_delinkage(saddle, summit) + if not dedsaddle and dedsummit: + return False + return True + + def check_linkage_trackers(self, saddle_id, summit_id): + """ + Checks internal tracker object for saddle/summit linkage. + + :param saddle_id: Saddle id + :param summit_id: Summit id + :return bool, bool: Saddle tracker OK, Summit tracker OK + """ + return (self.saddle_to_summit_tracker[saddle_id].get(summit_id, False), + self.summit_to_saddle_tracker[summit_id].get(saddle_id, False)) + + def record_saddle_summit_linkage(self, saddle, summit): + """ + Records the linkage between a saddle and a summit in the context manager. + Local tracking must be handled by the caller. + :param saddle: Saddle/Runoff + :param summit: Summit + :returns bool, bool, bool: Bool if added saddle, Bool if added summit, Bool if duplicate + """ + added_saddle = False + added_summit = False + + saddle_tracker_exist, summit_tracker_exist =\ + self.check_linkage_trackers(saddle.id, summit.id) + + # if this saddle-summit is already linked, we have a duplicate. Report it as such. + if saddle_tracker_exist and summit_tracker_exist: + return False, False, True + + if not saddle_tracker_exist: + self.saddle_to_summit_tracker[saddle.id][summit.id] = True + added_summit = True + if not summit_tracker_exist: + self.summit_to_saddle_tracker[summit.id][saddle.id] = True + added_saddle = True + return added_saddle, added_summit, False + + def record_saddle_summit_delinkage(self, saddle, summit): + """ + Unrecords the linkage between a saddle and a summit in the context manager. + Local tracking must be handled by the caller. + :param saddle: Saddle/Runoff + :param summit: Summit + :returns: Bool if removed saddle, Bool if removed summit + """ + removed_saddle = False + removed_summit = False + saddle_tracker_exist, summit_tracker_exist =\ + self.check_linkage_trackers(saddle.id, summit.id) + + if saddle_tracker_exist: + del self.saddle_to_summit_tracker[saddle.id][summit.id] + removed_summit = True + if summit_tracker_exist: + del self.summit_to_saddle_tracker[summit.id][saddle.id] + removed_saddle = True + return removed_saddle, removed_summit + + def remove_summit(self, summit): + """ + Remove a Summit from this context. Cleans up remote SummitContexts as well. + :param summit: Summit + """ + # de-link summit from any saddles. + if self.summit_to_saddle_tracker[summit.id]: + for saddle_id in self.summit_to_saddle_tracker[summit.id].keys(): + self.delink_saddle_summit(self.saddle_lookup[saddle_id], summit) + # remove from disabled tracker if it exists. + if self.is_disabled(summit.id): + del self.disabled_tracker[summit.id] + # remove summit from lookup, list and the remote context. + self._remove_feature(summit, self.summit_lookup, self._summits) + + def remove_saddle(self, saddle): + """ + Remove a Saddle from this context. Cleans up remote SaddleContexts as well. + :param saddle: Saddle + """ + #de-link from any summits. Cleans up remote SaddleContexts as well. + if self.saddle_to_summit_tracker[saddle.id]: + for summit_id in self.saddle_to_summit_tracker[saddle.id].keys(): + self.delink_saddle_summit(saddle, self.summit_lookup[summit_id]) + # remove from disabled tracker if it exists. + if self.is_disabled(saddle.id): + del self.disabled_tracker[saddle.id] + # remove saddle from lookup, list and the remote context. + self._remove_feature(saddle, self.saddle_lookup, self._saddles) + + def disable_feature(self, feature): + """ + Disables feature From Tracker and in Feature Context. + :param feature: Saddle/Summit/Runoff object to disable + """ + self.disabled_tracker[feature.id] = True + self.get_ctx(feature)._disabled = True + + def enable_feature(self, feature): + """ + Enables Feature in Tracker and in Feature Context. + :param feature: Saddle/Summit/Runoff object to enable + """ + if self.is_disabled(feature.id): + del self.disabled_tracker[self.feature.id] + self.get_ctx(feature)._disabled = False + + def is_disabled(self, feature_id): + """ + See if a feature is disabled. + :param feature_id: string ID of feature. + :return: bool + """ + return self.disabled_tracker.get(feature_id, False) + + def _populate_dicts(self, feature_list, destination_dict): + """ + basic lookup population function + """ + for feature in feature_list: + destination_dict[feature.id] = feature + return destination_dict + + def _add_feature(self, feature, feature_lookup, feature_list, disabled=False): + """ + Adds the feature to the feature list item, and the lookup. + this DOES NOT create the feature context on the feature itself. That burden + is on the caller. + + :param feature: + :param feature_lookup: + :param feature_list: + :param disabled: + :return: + """ + + if not feature_lookup.get(feature.id): + feature_list.append(feature) + feature_lookup[feature.id] = feature + if disabled: + self.disabled_tracker[feature.id] = True + return True + return False + + def _remove_feature(self, feature, feature_lookup, feature_list): + """ + Removes the feature from the feature list item, and the lookup. + This DOES remove the context on the feature. + + :param feature: + :param feature_lookup: + :param feature_list: + """ + if feature_lookup.get(feature.id): + feature_list.remove(feature) + del feature_lookup[feature.id] + del feature.contexts[self.id] + return True + return False + + def to_dict(self): + """ + :return: dict representation of this object. + """ + to_dict = { + 'id': self.id, + 'summit_ids': [s.id for s in self._summits], + 'saddle_ids': [s.id for s in self._saddles], + 'disabled_tracker': self.disabled_tracker, + 'saddle_to_summit_tracker': self.saddle_to_summit_tracker + #'basin_saddle_tracker': self.basin_saddle_tracker + } + return to_dict + + @classmethod + def from_dict(cls, from_dict, saddle_container, summit_container, runoff_container): + """ + :param from_dict: + :param saddle_container: + :param summit_container: + :param runoff_container: + :return: + """ + + combined_container = SaddlesContainer(saddle_container.points + runoff_container.points) + + id = from_dict['id'] + + # build feature lists + summits = [summit_container.by_id(summit_id) for summit_id in from_dict['summit_ids']] + saddles = [combined_container.by_id(saddle_id) for saddle_id in from_dict['saddle_ids']] + + # build summit_to_saddle_tracker + summit_to_saddle_tracker = defaultdict(dict) + for saddle_id, summit_ids in from_dict['saddle_to_summit_tracker'].items(): + for summit_id, val in summit_ids.items(): + summit_to_saddle_tracker[summit_id][saddle_id] = val + + # basin_saddle_tracker = from_dict['basin_saddle_tracker'] + + # this will build out contexts which will be disposed of. + context = cls(summits, saddles, [], id=id) + context.disabled_tracker = from_dict['disabled_tracker'] + + # make sure this is a defaultdict. + context.saddle_to_summit_tracker = defaultdict(dict) + for sad, sum_dict in from_dict['saddle_to_summit_tracker'].items(): + context.saddle_to_summit_tracker[sad]=sum_dict + + context.summit_to_saddle_tracker = summit_to_saddle_tracker + + # rebuild our contexts. + + for saddle in context._saddles: + saddle.contexts[context.id] = SaddleContext(context, saddle, [context.summit_lookup[sum] for sum, val in context.saddle_to_summit_tracker[saddle.id].items() if val], [], disabled=context.disabled_tracker.get(saddle.id, False)) + + for summit in context._summits: + summit.contexts[context.id] = SummitContext(context, summit, [context.saddle_lookup[sad] for sad, val in context.summit_to_saddle_tracker[summit.id].items() if val], [], disabled=context.disabled_tracker.get(summit.id, False)) + + return context + + + def __repr__(self): + """ + :return: String representation of this object + """ + return f" Summits:{len(self._summits)} " \ + f"Saddles:{len(self._saddles)}" \ No newline at end of file diff --git a/pyprom/lib/contexts/saddle_context.py b/pyprom/lib/contexts/saddle_context.py new file mode 100644 index 0000000..20c6e03 --- /dev/null +++ b/pyprom/lib/contexts/saddle_context.py @@ -0,0 +1,143 @@ +""" +pyProm: Copyright 2020. + +This software is distributed under a license that is described in +the LICENSE file that accompanies it. +""" + +from .feature_context import FeatureContext +from .exceptions import LinkageException + +class SaddleContext(FeatureContext): + + def __init__(self, manager, saddle, summits, neighbor_saddles, id=None, disabled=False): + """ + A SummitContext. + """ + super().__init__(saddle, manager, id=id, disabled=disabled) + self._summits = summits + self._summits_lookup = {x.id:x for x in summits} + self._neighbor_saddles = neighbor_saddles + + @property + def summits(self): + return self._summits + + @property + def saddle(self): + """ + saddle is immutable. + :return: + """ + return self._feature + + @property + def neighbor_saddles(self): + return self._neighbor_saddles + + def _add_summit(self, summit): + """ + Adds Summit to this context. In effect, this links that Summit to that Saddle. + -- + This makes no effort to change any states on the manager and therefore should + only be called from the manager. + + :param summit: Summit + :return: bool indicating if successful + """ + if not self._summits_lookup.get(summit.id): + self._summits.append(summit) + self._summits_lookup[summit.id] = summit + return True + return False + + def _remove_summit(self, summit): + """ + Removes Summit from this context. In effect, this unlinks a Summit from this Saddle. + -- + This makes no effort to change any states on the manager and therefore should + only be called from the manager. + + :param summit: Summit + :return: bool indicating if successful + """ + if self._summits_lookup.get(summit.id): + self._summits.remove(summit) + del self._summits_lookup[summit.id] + return True + return False + + def link_summit(self, summit, disable_duplicate=True): + """ + Link a Summit to this Saddle. + Both Saddle and Summit must be tracked by context manager or this will fail. + :param summit: Summit to link. + :return bool, bool: Success, duplicate + """ + + # Raises Exception if saddle or summit isn't in the context. + added_saddle, added_summit, recorded_saddle, recorded_summit, duplicate\ + = self.manager.link_saddle_summit(self.saddle, summit, disable_duplicate) + + if duplicate: + # if this is a duplicate, then success means we added nothing. + return not (added_saddle or added_summit or recorded_saddle or recorded_summit), duplicate + + # Bool for everything added (Success indicator), bool for duplicate. + return (added_saddle and added_summit and recorded_saddle and recorded_summit), duplicate + + def remove_summit(self, summit): + """ + UnLink a Summit from this Saddle + Both Saddle and Summit must be tracked by context manager or this will fail. + :param summit: Summit to de-link + :return bool: Success indicator + """ + + # Raises Exception if saddle or summit isn't in the context. + return self.manager.delink_saddle_summit(self.saddle, summit) + + def feature_neighbors(self): + """ + Returns our directly connected non disabled neighbors, in this case, our Summits. + :return: + """ + if not self.disabled: + return self._summits + + # @classmethod + # def from_dict_and_attach(cls, saddle_context_dict, manager): + # """ + # Create this object from dictionary representation. + # + # This function requires a saddle/summit lookup dict + # in order to create the correct associations. + # Therefore those saddles/summits must already exist + # in memory for this to work. + # + # :param saddle_context_dict: dict representation of this object. + # :param saddle_lookup: {id: Saddle} lookup dict. + # :param summit_lookup: {id: Summit} lookup dict. + # :return: a new SaddleContext object. + # :rtype: :class:`SaddleContext` + # """ + # + # # manager, saddle, summits, neighbor_saddles + # + # return cls(manager) + # + # def to_dict(self): + # """ + # Produces a dictionary of this object. uses feature IDs. + # :return: dict representation of this object. + # """ + # to_dict = { + # 'summits': [x.id for x in self.summits], + # 'saddle': self.saddle.id, + # 'id': self.id, + # 'disabled': self.disabled, + # } + # return to_dict + + def __repr__(self): + return f" {self.saddle}: {len(self.summits)} Summits" diff --git a/pyprom/lib/contexts/summit_context.py b/pyprom/lib/contexts/summit_context.py new file mode 100644 index 0000000..12a57d8 --- /dev/null +++ b/pyprom/lib/contexts/summit_context.py @@ -0,0 +1,114 @@ +""" +pyProm: Copyright 2020. + +This software is distributed under a license that is described in +the LICENSE file that accompanies it. +""" + +from .feature_context import FeatureContext +from .exceptions import LinkageException + +class SummitContext(FeatureContext): + + def __init__(self, manager, summit, saddles, neighbor_summits, id=None, disabled=False): + """ + A SummitContext. + """ + super().__init__(summit, manager, id=id, disabled=disabled) + self._saddles = saddles + self._saddles_lookup = {x.id:x for x in saddles} + self._neighbor_summits = neighbor_summits + + + @property + def summit(self): + """ + summit is immutable. + :return: + """ + return self._feature + + @property + def saddles(self): + return self._saddles + + @property + def neighbor_summits(self): + return self._neighbor_summits + + def feature_neighbors(self): + """ + Returns our directly connected neighbors, in this case, our Saddles. + :return: + """ + if not self.disabled: + return self._saddles + + + def _add_saddle(self, saddle): + """ + Adds Saddle to this context. In effect, this links this Saddle to that Summit + -- + This makes no effort to change any states on the manager and therefore should + only be called from the manager. + + :param saddle: Saddle + :return: bool if added + """ + if not self._saddles_lookup.get(saddle.id): + self._saddles.append(saddle) + self._saddles_lookup[saddle.id] = saddle + return True + return False + + def _remove_saddle(self, saddle): + """ + Removes Saddle from this context. In effect, this unlinks this Saddle from that Summit + -- + This makes no effort to change any states on the manager and therefore should + only be called from the manager. + + :param saddle: Saddle + :return: bool indicating if removed + """ + if self._saddles_lookup.get(saddle.id): + self._saddles.remove(saddle) + del self._saddles_lookup[saddle.id] + return True + return False + + def link_saddle(self, saddle, disable_duplicate=True): + """ + Link a Saddle to this Summit. + Both Saddle and Summit must be tracked by context manager or this will fail. + :param saddle: Saddle to link. + :return bool, bool: Success, duplicate + """ + + # Raises Exception if saddle or summit isn't in the context. + added_saddle, added_summit, recorded_saddle, recorded_summit, duplicate\ + = self.manager.link_saddle_summit(saddle, self.summit, disable_duplicate) + + if duplicate: + # if this is a duplicate, then success means we added nothing. + return not (added_saddle or added_summit or recorded_saddle or recorded_summit), duplicate + + # Bool for everything added (Success indicator), bool for duplicate. + return (added_saddle and added_summit and recorded_saddle and recorded_summit), duplicate + + def remove_saddle(self, saddle): + """ + UnLink a Saddle to this Summit. + Both Saddle and Summit must be tracked by context manager or this will fail. + :param saddle: Saddle to link. + :return bool: Success indicator + """ + + # Raises Exception if saddle or summit isn't in the context. + return self.manager.delink_saddle_summit(saddle, self.summit) + + def __repr__(self): + return f" {self.summit}: {len(self.saddles)} Saddles" + + + 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..0818703 100644 --- a/pyprom/lib/locations/spot_elevation.py +++ b/pyprom/lib/locations/spot_elevation.py @@ -21,7 +21,7 @@ class SpotElevation(BaseCoordinate): SpotElevation is intended to be inherited from. Effectively it's a Latitude/Longitude coordinate with an elevation """ - __slots__ = ['elevation', 'edgeEffect', 'edgePoints', 'id'] + __slots__ = ['elevation', 'edgeEffect', 'edgePoints', 'id', 'contexts'] def __init__(self, latitude, longitude, elevation, *args, **kwargs): """ @@ -42,7 +42,10 @@ 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') + self.contexts = kwargs.get('contexts', {}) + 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..b558f7e --- /dev/null +++ b/pyprom/lib/logic/breadth_first_search.py @@ -0,0 +1,38 @@ +""" +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 + 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..288b8cf --- /dev/null +++ b/pyprom/lib/logic/parentage.py @@ -0,0 +1,245 @@ +""" +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/pyprom/tests/contexts/__init__.py b/pyprom/tests/contexts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyprom/tests/contexts/manager.py b/pyprom/tests/contexts/manager.py new file mode 100644 index 0000000..79d191b --- /dev/null +++ b/pyprom/tests/contexts/manager.py @@ -0,0 +1,738 @@ +""" +pyProm: Copyright 2020. + +This software is distributed under a license that is described in +the LICENSE file that accompanies it. +""" + +import unittest +import cbor +from pyprom.lib.locations.saddle import Saddle +from pyprom.lib.locations.summit import Summit +from pyprom.lib.locations.runoff import Runoff + +from pyprom.lib.containers.saddles import SaddlesContainer +from pyprom.lib.containers.summits import SummitsContainer +from pyprom.lib.containers.runoffs import RunoffsContainer + +from pyprom.lib.contexts.manager import FeatureContextManager + +class ManagerTests(unittest.TestCase): + """Test Context Manager""" + + def setUp(self): + self.saddle1 = Saddle(1, 2, 3) + self.saddle2 = Saddle(2, 3, 4) + self.summit1 = Summit(1, 2, 3) + self.summit2 = Summit(2, 3, 4) + self.runoff1 = Runoff(1, 2, 3) + self.runoff2 = Runoff(2, 3, 4) + + # empty manager. + self.manager = FeatureContextManager([], [], []) + + # Saddles. + + def testAddSaddleOK(self): + """ + Add an active Saddle to this context and check for all expected attributes. + :return: + """ + self.manager.add_saddle(self.saddle1) + self.assertEqual(self.manager.saddle_lookup[self.saddle1.id], self.saddle1) + self.assertEqual(self.manager.saddle_lookup.get(self.saddle2.id), None) + self.assertIn(self.saddle1, self.manager._saddles) + self.assertEqual(len(self.manager._saddles), 1) + self.assertEqual(self.manager.is_disabled(self.saddle1.id), False) + self.assertIsNotNone(self.saddle1.contexts[self.manager.id]) + self.assertEqual(self.saddle1.contexts[self.manager.id].saddle, self.saddle1) + self.assertEqual(self.saddle1.contexts[self.manager.id].summits, []) + self.assertEqual(self.saddle1.contexts[self.manager.id].disabled, False) + + def testAddSaddleDisabled(self): + """ + Add a disabled Saddle to this context and check for all expected attributes. + :return: + """ + self.manager.add_saddle(self.saddle1, disabled=True) + self.assertEqual(self.manager.saddle_lookup[self.saddle1.id], self.saddle1) + self.assertEqual(self.manager.saddle_lookup.get(self.saddle2.id), None) + self.assertIn(self.saddle1, self.manager._saddles) + self.assertEqual(len(self.manager._saddles), 1) + self.assertEqual(self.manager.is_disabled(self.saddle1.id), True) + self.assertIsNotNone(self.saddle1.contexts[self.manager.id]) + self.assertEqual(self.saddle1.contexts[self.manager.id].saddle, self.saddle1) + self.assertEqual(self.saddle1.contexts[self.manager.id].summits, []) + self.assertEqual(self.saddle1.contexts[self.manager.id].disabled, True) + + def testRemoveSaddleOK(self): + """ + Add then remove an active Saddle to this context and check for all expected attributes. + :return: + """ + self.manager.add_saddle(self.saddle1) + self.assertEqual(self.manager.saddle_lookup[self.saddle1.id], self.saddle1) + self.assertIn(self.saddle1, self.manager._saddles) + self.assertEqual(len(self.manager._saddles), 1) + self.assertEqual(self.manager.is_disabled(self.saddle1.id), False) + + self.manager.remove_saddle(self.saddle1) + + self.assertEqual(self.manager.saddle_lookup.get(self.saddle1.id), None) + self.assertNotIn(self.saddle1, self.manager._saddles) + self.assertEqual(len(self.manager._saddles), 0) + self.assertEqual(self.manager.is_disabled(self.saddle1.id), False) + + self.assertIsNone(self.saddle1.contexts.get(self.manager.id)) + + + def testRemoveSaddleDisabled(self): + """ + Add then remove disabled Saddle to this context and check for all expected attributes. + """ + self.manager.add_saddle(self.saddle1, disabled=True) + self.assertEqual(self.manager.saddle_lookup[self.saddle1.id], self.saddle1) + self.assertIn(self.saddle1, self.manager._saddles) + self.assertEqual(len(self.manager._saddles), 1) + self.assertEqual(self.manager.is_disabled(self.saddle1.id), True) + + self.manager.remove_saddle(self.saddle1) + + self.assertEqual(self.manager.saddle_lookup.get(self.saddle1.id), None) + self.assertNotIn(self.saddle1, self.manager._saddles) + self.assertEqual(len(self.manager._saddles), 0) + self.assertEqual(self.manager.is_disabled(self.saddle1.id), False) + self.assertIsNone(self.saddle1.contexts.get(self.manager.id)) + + def testDisableSaddle(self): + """ + Add Saddle then disable it + """ + self.manager.add_saddle(self.saddle1) + self.assertEqual(self.manager.saddle_lookup[self.saddle1.id], self.saddle1) + self.assertIn(self.saddle1, self.manager._saddles) + self.assertEqual(len(self.manager._saddles), 1) + self.assertEqual(self.manager.is_disabled(self.saddle1.id), False) + self.assertIsNotNone(self.saddle1.contexts.get(self.manager.id)) + self.assertFalse(self.saddle1.contexts[self.manager.id].disabled) + # Perform action. + self.saddle1.contexts[self.manager.id].disable() + + self.assertEqual(self.manager.is_disabled(self.saddle1.id), True) + self.assertEqual(self.saddle1.contexts[self.manager.id].disabled, True) + + def testEnableSaddle(self): + """ + Add Disabled Saddle then enable it + """ + self.manager.add_saddle(self.saddle1, disabled=True) + self.assertEqual(self.manager.saddle_lookup[self.saddle1.id], self.saddle1) + self.assertIn(self.saddle1, self.manager._saddles) + self.assertEqual(len(self.manager._saddles), 1) + self.assertEqual(self.manager.is_disabled(self.saddle1.id), True) + self.assertIsNotNone(self.saddle1.contexts.get(self.manager.id)) + self.assertTrue(self.saddle1.contexts[self.manager.id].disabled) + # Perform action. + self.saddle1.contexts[self.manager.id].enable() + + self.assertEqual(self.manager.is_disabled(self.saddle1.id), False) + self.assertEqual(self.saddle1.contexts[self.manager.id].disabled, False) + + + + + # Summits. + + + + + + + def testAddSummitOK(self): + """ + Add an active Summit to this context and check for all expected attributes. + :return: + """ + self.manager.add_summit(self.summit1) + self.assertEqual(self.manager.summit_lookup[self.summit1.id], self.summit1) + self.assertEqual(self.manager.summit_lookup.get(self.saddle2.id), None) + self.assertIn(self.summit1, self.manager._summits) + self.assertEqual(len(self.manager._summits), 1) + self.assertEqual(self.manager.is_disabled(self.summit1.id), False) + self.assertIsNotNone(self.summit1.contexts[self.manager.id]) + self.assertEqual(self.summit1.contexts[self.manager.id].summit, self.summit1) + self.assertEqual(self.summit1.contexts[self.manager.id].saddles, []) + self.assertEqual(self.summit1.contexts[self.manager.id].disabled, False) + + def testAddSummitDisabled(self): + """ + Add a disabled Summit to this context and check for all expected attributes. + :return: + """ + self.manager.add_summit(self.summit1, disabled=True) + self.assertEqual(self.manager.summit_lookup[self.summit1.id], self.summit1) + self.assertEqual(self.manager.summit_lookup.get(self.saddle2.id), None) + self.assertIn(self.summit1, self.manager._summits) + self.assertEqual(len(self.manager._summits), 1) + self.assertEqual(self.manager.is_disabled(self.summit1.id), True) + self.assertIsNotNone(self.summit1.contexts[self.manager.id]) + self.assertEqual(self.summit1.contexts[self.manager.id].summit, self.summit1) + self.assertEqual(self.summit1.contexts[self.manager.id].saddles, []) + self.assertEqual(self.summit1.contexts[self.manager.id].disabled, True) + + + def testDisableSummit(self): + """ + Add Summit then disable it + """ + self.manager.add_summit(self.summit1) + self.assertEqual(self.manager.summit_lookup[self.summit1.id], self.summit1) + self.assertIn(self.summit1, self.manager._summits) + self.assertEqual(len(self.manager._summits), 1) + self.assertEqual(self.manager.is_disabled(self.summit1.id), False) + self.assertIsNotNone(self.summit1.contexts.get(self.manager.id)) + self.assertFalse(self.summit1.contexts[self.manager.id].disabled) + # Perform action. + self.summit1.contexts[self.manager.id].disable() + + self.assertEqual(self.manager.is_disabled(self.summit1.id), True) + self.assertEqual(self.summit1.contexts[self.manager.id].disabled, True) + + def testEnableSummit(self): + """ + Add Disabled Summit then enable it + """ + self.manager.add_summit(self.summit1, disabled=True) + self.assertEqual(self.manager.summit_lookup[self.summit1.id], self.summit1) + self.assertIn(self.summit1, self.manager._summits) + self.assertEqual(len(self.manager._summits), 1) + self.assertEqual(self.manager.is_disabled(self.summit1.id), True) + self.assertIsNotNone(self.summit1.contexts.get(self.manager.id)) + self.assertTrue(self.summit1.contexts[self.manager.id].disabled) + # Perform action. + self.summit1.contexts[self.manager.id].enable() + + self.assertEqual(self.manager.is_disabled(self.summit1.id), False) + self.assertEqual(self.summit1.contexts[self.manager.id].disabled, False) + + def testFeatureList(self): + """ + + make sure feature lists work. + """ + self.manager.add_summit(self.summit1) + self.manager.add_saddle(self.saddle1) + self.manager.add_saddle(self.runoff1) + + self.assertEqual(self.manager.summits, [self.summit1]) + self.assertEqual(self.manager.saddles, [self.saddle1, self.runoff1]) + self.assertEqual(self.manager.saddles_exact, [self.saddle1]) + self.assertEqual(self.manager.runoffs, [self.runoff1]) + + + # Runoffs. + + + def testAddRunoffOK(self): + """ + Add an active Runoff to this context and check for all expected attributes. + :return: + """ + self.manager.add_saddle(self.runoff1) + self.assertEqual(self.manager.saddle_lookup[self.runoff1.id], self.runoff1) + self.assertEqual(self.manager.saddle_lookup.get(self.saddle2.id), None) + self.assertIn(self.runoff1, self.manager._saddles) + self.assertEqual(len(self.manager._saddles), 1) + self.assertEqual(self.manager.is_disabled(self.runoff1.id), False) + self.assertIsNotNone(self.runoff1.contexts[self.manager.id]) + self.assertEqual(self.runoff1.contexts[self.manager.id].saddle, self.runoff1) + self.assertEqual(self.runoff1.contexts[self.manager.id].summits, []) + self.assertEqual(self.runoff1.contexts[self.manager.id].disabled, False) + + def testAddRunoffDisabled(self): + """ + Add a disabled Runoff to this context and check for all expected attributes. + :return: + """ + self.manager.add_saddle(self.runoff1, disabled=True) + self.assertEqual(self.manager.saddle_lookup[self.runoff1.id], self.runoff1) + self.assertEqual(self.manager.saddle_lookup.get(self.saddle2.id), None) + self.assertIn(self.runoff1, self.manager._saddles) + self.assertEqual(len(self.manager._saddles), 1) + self.assertEqual(self.manager.is_disabled(self.runoff1.id), True) + self.assertIsNotNone(self.runoff1.contexts[self.manager.id]) + self.assertEqual(self.runoff1.contexts[self.manager.id].saddle, self.runoff1) + self.assertEqual(self.runoff1.contexts[self.manager.id].summits, []) + self.assertEqual(self.runoff1.contexts[self.manager.id].disabled, True) + + + def testDisableRunoff(self): + """ + Add Runoff then disable it + """ + self.manager.add_saddle(self.runoff1) + self.assertEqual(self.manager.saddle_lookup[self.runoff1.id], self.runoff1) + self.assertIn(self.runoff1, self.manager._saddles) + self.assertEqual(len(self.manager._saddles), 1) + self.assertEqual(self.manager.is_disabled(self.runoff1.id), False) + self.assertIsNotNone(self.runoff1.contexts.get(self.manager.id)) + self.assertFalse(self.runoff1.contexts[self.manager.id].disabled) + # Perform action. + self.runoff1.contexts[self.manager.id].disable() + + self.assertEqual(self.manager.is_disabled(self.runoff1.id), True) + self.assertEqual(self.runoff1.contexts[self.manager.id].disabled, True) + + def testEnableRunoff(self): + """ + Add Disabled Runoff then enable it + """ + self.manager.add_saddle(self.runoff1, disabled=True) + self.assertEqual(self.manager.saddle_lookup[self.runoff1.id], self.runoff1) + self.assertIn(self.runoff1, self.manager._saddles) + self.assertEqual(len(self.manager._saddles), 1) + self.assertEqual(self.manager.is_disabled(self.runoff1.id), True) + self.assertIsNotNone(self.runoff1.contexts.get(self.manager.id)) + self.assertTrue(self.runoff1.contexts[self.manager.id].disabled) + # Perform action. + self.runoff1.contexts[self.manager.id].enable() + + self.assertEqual(self.manager.is_disabled(self.runoff1.id), False) + self.assertEqual(self.runoff1.contexts[self.manager.id].disabled, False) + + + # Linkages + + def testLinkSaddleSummitCreate(self): + """ + Simple Saddle Summit linkage creation test. + """ + + # add saddle/summit to manager + self.manager.add_saddle(self.saddle1) + self.manager.add_summit(self.summit1) + + # link saddle1 from summit1 + self.manager.get_ctx(self.summit1).link_saddle(self.saddle1) + + # asserts + sad1_ctx = self.manager.get_ctx(self.saddle1) + sum1_ctx = self.manager.get_ctx(self.summit1) + + # right summits from saddle? + self.assertEqual(len(sad1_ctx.summits), 1) + self.assertEqual(sad1_ctx.summits[0], self.summit1) + + # right saddles from summit? + self.assertEqual(len(sum1_ctx.saddles), 1) + self.assertEqual(sum1_ctx.saddles[0], self.saddle1) + + # Tracked in manager? + self.assertEqual(self.manager.summit_to_saddle_tracker[self.summit1.id][self.saddle1.id], True) + self.assertEqual(self.manager.saddle_to_summit_tracker[self.saddle1.id][self.summit1.id], True) + + def testDeLinkSaddleSummit(self): + """ + Simple Saddle Summit destruction test. + """ + + # add saddle/summit to manager + self.manager.add_saddle(self.saddle1) + self.manager.add_summit(self.summit1) + + # link saddle1 from summit1 + self.manager.get_ctx(self.summit1).link_saddle(self.saddle1) + + #de-link + self.manager.get_ctx(self.summit1).remove_saddle(self.saddle1) + + # asserts + sad1_ctx = self.manager.get_ctx(self.saddle1) + sum1_ctx = self.manager.get_ctx(self.summit1) + + # right summits from saddle? + self.assertEqual(len(sad1_ctx.summits), 0) + + # right saddles from summit? + self.assertEqual(len(sum1_ctx.saddles), 0) + + # Tracked in manager? + self.assertIsNone(self.manager.summit_to_saddle_tracker[self.summit1.id].get(self.saddle1.id)) + self.assertIsNone(self.manager.saddle_to_summit_tracker[self.saddle1.id].get(self.summit1.id)) + + def testLinkRunoffSummitCreate(self): + """ + Simple Runoff Summit linkage creation test. + """ + + # add saddle/summit to manager + self.manager.add_saddle(self.runoff1) + self.manager.add_summit(self.summit1) + + # link saddle1 from summit1 + self.manager.get_ctx(self.summit1).link_saddle(self.runoff1) + + # asserts + sad1_ctx = self.manager.get_ctx(self.runoff1) + sum1_ctx = self.manager.get_ctx(self.summit1) + + # right summits from saddle? + self.assertEqual(len(sad1_ctx.summits), 1) + self.assertEqual(sad1_ctx.summits[0], self.summit1) + + # right saddles from summit? + self.assertEqual(len(sum1_ctx.saddles), 1) + self.assertEqual(sum1_ctx.saddles[0], self.runoff1) + + # Tracked in manager? + self.assertEqual(self.manager.summit_to_saddle_tracker[self.summit1.id][self.runoff1.id], True) + self.assertEqual(self.manager.saddle_to_summit_tracker[self.runoff1.id][self.summit1.id], True) + + def testDeLinkRunoffSummit(self): + """ + Simple Runoff Summit destruction test. + """ + + # add saddle/summit to manager + self.manager.add_saddle(self.runoff1) + self.manager.add_summit(self.summit1) + + # link saddle1 from summit1 + self.manager.get_ctx(self.summit1).link_saddle(self.runoff1) + + #de-link + self.manager.get_ctx(self.summit1).remove_saddle(self.runoff1) + + # asserts + sad1_ctx = self.manager.get_ctx(self.runoff1) + sum1_ctx = self.manager.get_ctx(self.summit1) + + # right summits from saddle? + self.assertEqual(len(sad1_ctx.summits), 0) + + # right saddles from summit? + self.assertEqual(len(sum1_ctx.saddles), 0) + + # Tracked in manager? + self.assertIsNone(self.manager.summit_to_saddle_tracker[self.summit1.id].get(self.runoff1.id)) + self.assertIsNone(self.manager.saddle_to_summit_tracker[self.runoff1.id].get(self.summit1.id)) + + def testLinkSummitSaddleCreate(self): + """ + Simple Saddle Summit linkage creation test. + """ + + # add saddle/summit to manager + self.manager.add_saddle(self.saddle1) + self.manager.add_summit(self.summit1) + + # link summit1 from saddle1 + self.manager.get_ctx(self.saddle1).link_summit(self.summit1) + + # asserts + sad1_ctx = self.manager.get_ctx(self.saddle1) + sum1_ctx = self.manager.get_ctx(self.summit1) + + # right summits from saddle? + self.assertEqual(len(sad1_ctx.summits), 1) + self.assertEqual(sad1_ctx.summits[0], self.summit1) + + # right saddles from summit? + self.assertEqual(len(sum1_ctx.saddles), 1) + self.assertEqual(sum1_ctx.saddles[0], self.saddle1) + + # Tracked in manager? + self.assertEqual(self.manager.summit_to_saddle_tracker[self.summit1.id][self.saddle1.id], True) + self.assertEqual(self.manager.saddle_to_summit_tracker[self.saddle1.id][self.summit1.id], True) + + def testDeLinkSummitSaddle(self): + """ + Simple Saddle Summit linkage destruction test. + """ + + # add saddle/summit to manager + self.manager.add_saddle(self.saddle1) + self.manager.add_summit(self.summit1) + + # link saddle1 from summit1 + self.manager.get_ctx(self.saddle1).link_summit(self.summit1) + + #de-link + self.manager.get_ctx(self.saddle1).remove_summit(self.summit1) + + # asserts + sad1_ctx = self.manager.get_ctx(self.saddle1) + sum1_ctx = self.manager.get_ctx(self.summit1) + + # right summits from saddle? + self.assertEqual(len(sad1_ctx.summits), 0) + + # right saddles from summit? + self.assertEqual(len(sum1_ctx.saddles), 0) + + # Tracked in manager? + self.assertIsNone(self.manager.summit_to_saddle_tracker[self.summit1.id].get(self.saddle1.id)) + self.assertIsNone(self.manager.saddle_to_summit_tracker[self.saddle1.id].get(self.summit1.id)) + + def testDuplicateLinkWithRemovalFromSummitSide(self): + """ + This test demonstrates the functionality which automatically + disables a Saddle when a duplicate link is created. from the Summits + perspective + """ + + # add saddle/summit to manager + self.manager.add_saddle(self.saddle1) + self.manager.add_summit(self.summit1) + + + # Links saddles to summits + self.manager.get_ctx(self.summit1).link_saddle(self.saddle1) + ok, dupe = self.manager.get_ctx(self.summit1).link_saddle(self.saddle1) + + sad1_ctx = self.manager.get_ctx(self.saddle1) + sum1_ctx = self.manager.get_ctx(self.summit1) + + self.assertTrue(ok) + self.assertTrue(dupe) + + # right summits from saddle? + self.assertEqual(len(sad1_ctx.summits), 0) + + # right saddles from summit? + self.assertEqual(len(sum1_ctx.saddles), 0) + + # Tracked in manager? + self.assertIsNone(self.manager.summit_to_saddle_tracker[self.summit1.id].get(self.saddle1.id)) + self.assertIsNone(self.manager.saddle_to_summit_tracker[self.saddle1.id].get(self.summit1.id)) + + # disabled? + self.assertTrue(sad1_ctx.disabled) + self.assertTrue(self.manager.disabled_tracker[self.saddle1.id]) + + + def testDuplicateLinkWithRemovalFromSaddleSide(self): + """ + This test demonstrates the functionality which automatically + disables a Saddle when a duplicate link is created. from the Saddles + perspective + """ + + # add saddle/summit to manager + self.manager.add_saddle(self.saddle1) + self.manager.add_summit(self.summit1) + + # Links summits to saddles + ok, dupe = self.manager.get_ctx(self.saddle1).link_summit(self.summit1) + self.assertTrue(ok) + self.assertFalse(dupe) + + ok, dupe = self.manager.get_ctx(self.saddle1).link_summit(self.summit1) + + sad1_ctx = self.manager.get_ctx(self.saddle1) + sum1_ctx = self.manager.get_ctx(self.summit1) + + self.assertTrue(ok) + self.assertTrue(dupe) + + # right summits from saddle? + self.assertEqual(len(sad1_ctx.summits), 0) + + # right saddles from summit? + self.assertEqual(len(sum1_ctx.saddles), 0) + + # Tracked in manager? + self.assertIsNone(self.manager.summit_to_saddle_tracker[self.summit1.id].get(self.saddle1.id)) + self.assertIsNone(self.manager.saddle_to_summit_tracker[self.saddle1.id].get(self.summit1.id)) + + # disabled? + self.assertTrue(sad1_ctx.disabled) + self.assertTrue(self.manager.disabled_tracker[self.saddle1.id]) + + def testDuplicateLinkWithoutRemovalFromSummitSide(self): + """ + Test duplicate links with disable_duplicate=False. + + This should Not create a duplicate link, + it will preserve the existing link and will not + disable the saddle + """ + + # add saddle/summit to manager + self.manager.add_saddle(self.saddle1) + self.manager.add_summit(self.summit1) + + # Links saddles to summits + ok, dupe = self.manager.get_ctx(self.summit1).link_saddle(self.saddle1) + self.assertTrue(ok) + self.assertFalse(dupe) + + ok, dupe = self.manager.get_ctx(self.summit1).link_saddle(self.saddle1, disable_duplicate=False) + + sad1_ctx = self.manager.get_ctx(self.saddle1) + sum1_ctx = self.manager.get_ctx(self.summit1) + + self.assertTrue(ok) + self.assertTrue(dupe) + + # right summits from saddle? + self.assertEqual(len(sad1_ctx.summits), 1) + self.assertEqual(sad1_ctx.summits[0], self.summit1) + + # right saddles from summit? + self.assertEqual(len(sum1_ctx.saddles), 1) + self.assertEqual(sum1_ctx.saddles[0], self.saddle1) + + # Tracked in manager? + self.assertEqual(self.manager.summit_to_saddle_tracker[self.summit1.id].get(self.saddle1.id), True) + self.assertEqual(self.manager.saddle_to_summit_tracker[self.saddle1.id].get(self.summit1.id), True) + + # disabled? + self.assertFalse(sad1_ctx.disabled) + self.assertFalse(self.manager.disabled_tracker[self.saddle1.id]) + + def testDuplicateLinkWithoutRemovalFromSaddleSide(self): + """ + Test duplicate links with disable_duplicate=False. + + This should Not create a duplicate link, + it will preserve the existing link and will not + disable the saddle + """ + + # add saddle/summit to manager + self.manager.add_saddle(self.saddle1) + self.manager.add_summit(self.summit1) + + # Links saddles to summits + ok, dupe = self.manager.get_ctx(self.saddle1).link_summit(self.summit1) + self.assertTrue(ok) + self.assertFalse(dupe) + + ok, dupe = self.manager.get_ctx(self.saddle1).link_summit(self.summit1, disable_duplicate=False) + + sad1_ctx = self.manager.get_ctx(self.saddle1) + sum1_ctx = self.manager.get_ctx(self.summit1) + + self.assertTrue(ok) + self.assertTrue(dupe) + + # right summits from saddle? + self.assertEqual(len(sad1_ctx.summits), 1) + self.assertEqual(sad1_ctx.summits[0], self.summit1) + + # right saddles from summit? + self.assertEqual(len(sum1_ctx.saddles), 1) + self.assertEqual(sum1_ctx.saddles[0], self.saddle1) + + # Tracked in manager? + self.assertEqual(self.manager.summit_to_saddle_tracker[self.summit1.id].get(self.saddle1.id), True) + self.assertEqual(self.manager.saddle_to_summit_tracker[self.saddle1.id].get(self.summit1.id), True) + + # disabled? + self.assertFalse(sad1_ctx.disabled) + self.assertFalse(self.manager.disabled_tracker[self.saddle1.id]) + + +class ManagerLoadSaveTests(unittest.TestCase): + """Test Context Manager ability to load/save to/from dict""" + + def setUp(self): + self.saddle1 = Saddle(1, 2, 3) + self.saddle2 = Saddle(2, 3, 4) + self.summit1 = Summit(1, 2, 3) + self.summit2 = Summit(2, 3, 4) + self.runoff1 = Runoff(1, 2, 3) + self.runoff2 = Runoff(2, 3, 4) + + self.summits = SummitsContainer([self.summit1, self.summit2]) + self.saddles = SaddlesContainer([self.saddle1, self.saddle2]) + self.runoffs = RunoffsContainer([self.runoff1, self.runoff2]) + + + self.manager = FeatureContextManager(self.summits.points, + self.saddles.points, + self.runoffs.points) + + def test_to_from_dict(self): + """ + Test exporting to dict and importing. + + This uses to_dict to generate a dict of this context, + then cbors the result, uncbors, and loads with from_dict + + + """ + #Links + self.manager.get_ctx(self.saddle1).link_summit(self.summit1) + self.manager.get_ctx(self.saddle1).link_summit(self.summit2) + self.manager.get_ctx(self.runoff1).link_summit(self.summit1) + self.manager.get_ctx(self.runoff2).link_summit(self.summit1) + self.manager.get_ctx(self.saddle2).link_summit(self.summit1) + self.manager.get_ctx(self.saddle2).link_summit(self.summit2) + + self.manager.get_ctx(self.saddle2).disable() + self.manager.get_ctx(self.runoff2).disable() + + # Export! + ctx_dict = self.manager.to_dict() + + # serialize/deserialize + cbored_ctx_dict = cbor.loads(cbor.dumps(ctx_dict)) + + # blow away old context references, but stash objects for later comparison. + oldecontexts={} + for i in self.saddles.points + self.summits.points + self.runoffs.points: + oldecontexts[i.id] = i.contexts[self.manager.id] + i.contexts = {} + + #reload! + new = FeatureContextManager.from_dict(cbored_ctx_dict, self.saddles, self.summits, self.runoffs) + + # Do our members have this context? + for i in self.saddles.points + self.summits.points + self.runoffs.points: + self.assertTrue(new.id in i.contexts.keys()) + + # saddles populated? + for saddle in self.saddles: + self.assertIn(saddle, new._saddles) + self.assertEqual(saddle, new.saddle_lookup[saddle.id]) + + # runoffs populated? + for runoff in self.runoffs: + self.assertIn(runoff, new._saddles) + self.assertEqual(runoff, new.saddle_lookup[runoff.id]) + + self.assertEqual(len(new._saddles), 4) + + # summits populated? + for summit in self.summits: + self.assertIn(summit, new._summits) + self.assertEqual(summit, new.summit_lookup[summit.id]) + + # our disabled resources still disabled? + self.assertTrue(new.get_ctx(self.saddle2).disabled) + self.assertTrue(new.get_ctx(self.runoff2).disabled) + + # link records match? + self.assertDictEqual(new.summit_to_saddle_tracker, self.manager.summit_to_saddle_tracker) + self.assertDictEqual(new.saddle_to_summit_tracker, self.manager.saddle_to_summit_tracker) + + # disabled tracker match? + self.assertDictEqual(new.disabled_tracker, self.manager.disabled_tracker) + + # Are our links in contexts as expected? + self.assertEqual(new.get_ctx(self.saddle1).summits, oldecontexts[self.saddle1.id].summits) + self.assertEqual(new.get_ctx(self.saddle2).summits, oldecontexts[self.saddle2.id].summits) + self.assertEqual(new.get_ctx(self.summit1).saddles, oldecontexts[self.summit1.id].saddles) + self.assertEqual(new.get_ctx(self.summit2).saddles, oldecontexts[self.summit2.id].saddles) + self.assertEqual(new.get_ctx(self.runoff1).summits, oldecontexts[self.runoff1.id].summits) + self.assertEqual(new.get_ctx(self.runoff2).summits, oldecontexts[self.runoff2.id].summits) + + # disabled state as expected? + self.assertEqual(new.get_ctx(self.saddle1).disabled, oldecontexts[self.saddle1.id].disabled) + self.assertEqual(new.get_ctx(self.saddle2).disabled, oldecontexts[self.saddle2.id].disabled) + self.assertEqual(new.get_ctx(self.summit1).disabled, oldecontexts[self.summit1.id].disabled) + self.assertEqual(new.get_ctx(self.summit2).disabled, oldecontexts[self.summit2.id].disabled) + self.assertEqual(new.get_ctx(self.runoff1).disabled, oldecontexts[self.runoff1.id].disabled) + self.assertEqual(new.get_ctx(self.runoff2).disabled, oldecontexts[self.runoff2.id].disabled) diff --git a/pyprom/tests/testDivideTree.py b/pyprom/tests/testDivideTree.py index fa32eb5..aac294f 100644 --- a/pyprom/tests/testDivideTree.py +++ b/pyprom/tests/testDivideTree.py @@ -9,7 +9,7 @@ from pyprom.tests.getData import gettestzip from pyprom.dataload import GDALLoader from pyprom.domain_map import DomainMap -from pyprom.dividetree import DivideTree +from lib.logic.parentage import ProminenceIslandParentFinder class DivideTreeTests(unittest.TestCase): @@ -21,26 +21,77 @@ def setUp(self): """ gettestzip() datafile = GDALLoader('/tmp/N44W072.hgt') - datamap = datafile.datamap - self.someslice = datamap.subset(2494, 2240, 700, 800) # this is - # the one we want in the end... - # self.someslice = datamap.subset(2594, 2340, 500, 500) - # this is fine too apparently... (or not) - # self.someslice = datamap.subset(2694, 2440, 400, 400) - self.domain = DomainMap(self.someslice) - self.domain.run(sparse=True) - # self.domain.run() - - def testDivideTreeInitial(self): + + self.domain = DomainMap.read("/home/mhowes/Elements_TEMP/pyprom_results/080/N44W072_full_run_edge_cycle2.dom", datafile.datamap) + + + # datamap = datafile.datamap + # self.someslice = datamap.subset(2494, 2240, 700, 800) # this is + # # the one we want in the end... + # # self.someslice = datamap.subset(2594, 2340, 500, 500) + # # this is fine too apparently... (or not) + # # self.someslice = datamap.subset(2694, 2440, 400, 400) + # self.domain = DomainMap(self.someslice) + # self.domain.run(sparse=True) + # # self.domain.run() + + # def testDivideTreeInitial(self): + # """ + # Test divide tree initial + # """ + # # self.domain.disqualify_lower_linkers() + # # self.domain.mark_redundant_linkers() + # import logging + # logging.basicConfig(level=logging.DEBUG) + # self.domain.detect_basin_saddles() + # dt = ProminenceIsland(domain=self.domain) + # w = self.domain.summits.highest[0] + # dt.localProminentRegion(w) + # print(w.lprBoundary) + # + # def testDivideTreeOther(self): + # """ + # Test divide tree initial + # """ + # # self.domain.disqualify_lower_linkers() + # # self.domain.mark_redundant_linkers() + # import logging + # logging.basicConfig(level=logging.DEBUG) + # self.domain.detect_basin_saddles() + # dt = ProminenceIsland(domain=self.domain) + # lpr = dt.run() + # # w = self.domain.summits.highest[0] + # # dt.localProminentRegion(w) + # print(lpr) + + # def testDivideTreeOther(self): + # """ + # Test divide tree initial + # """ + # # x = ProminenceIslandFinder(self.domain.summits.highest[0]) + # # foo = x.find_prominence_island_and_sub_islands() + # # print("hi") + # #sumsum = [x for x in self.domain.summits if x.id == 'su:FpzutWlK3EBA'][0] + # + # #sumsum = self.domain.summits.elevationRange(5685,5686)[0] # jefferson + # sumsum = [x for x in self.domain.summits if x.id == 'su:kAiZHl2Utprc'][0] #lafayette + # x = ProminenceIslandParentFinder(sumsum) + # a,b =x.find_parent2() + # print (a,b) + + + def testDivideTreeOther(self): """ Test divide tree initial """ - # self.domain.disqualify_lower_linkers() - # self.domain.mark_redundant_linkers() - import logging - logging.basicConfig(level=logging.DEBUG) - self.domain.detect_basin_saddles() - dt = DivideTree(domain=self.domain) - w = self.domain.summits.highest[0] - dt.localProminentRegion(w) - print(w.lprBoundary) + # x = ProminenceIslandFinder(self.domain.summits.highest[0]) + # foo = x.find_prominence_island_and_sub_islands() + # print("hi") + #sumsum = [x for x in self.domain.summits if x.id == 'su:FpzutWlK3EBA'][0] #near clay + #sumsum = self.domain.summits.elevationRange(5685,5686)[0] # jefferson + #sumsum = [x for x in self.domain.summits if x.id == 'su:kAiZHl2Utprc'][0] #lafayette + #sumsum = [x for x in self.domain.summits if x.id == 'su:QrdGWnTKqeFw'][0]# sableshoulder + sumsum = [x for x in self.domain.summits if x.id == 'su:usnWYNyeYf9d'][0] #washington + x = ProminenceIslandParentFinder(sumsum) + a,b,c,d =x.find_parent() + print (a,b,c) \ No newline at end of file diff --git a/pyprom/tests/testSummitDomainWalk.py b/pyprom/tests/testSummitDomainWalk.py new file mode 100644 index 0000000..6861806 --- /dev/null +++ b/pyprom/tests/testSummitDomainWalk.py @@ -0,0 +1,81 @@ +""" +pyProm: Copyright 2017. + +This software is distributed under a license that is described in +the LICENSE file that accompanies it. +""" + +from __future__ import division +import unittest +from pyprom.tests.getData import gettestzip +from pyprom.dataload import GDALLoader +from pyprom.domain_map import DomainMap +from pyprom.lib.logic.summit_domain_walk import Walk +from pyprom.lib.locations.saddle import Saddle +from pyprom.lib.logic.internal_saddle_network import InternalSaddleNetwork +import logging +logging.basicConfig(level=logging.DEBUG) + +class Walk2AziscohosTests(unittest.TestCase): + """ + Test The Walk Features of feature_discovery + """ + + @classmethod + def setUpClass(cls): + """ + Set up shared resources + """ + logging.basicConfig(level=logging.DEBUG) + gettestzip() + datafile = GDALLoader('/tmp/N44W072.hgt') + cls.datamap = datafile.datamap + #cls.aziscohos= cls.datamap.subset(0,0,50,50) #todo: remove + cls.aziscohos = cls.datamap.subset(622, 3275, 457, 325) + cls.aziscohosVicinity = DomainMap(cls.aziscohos) + cls.aziscohosVicinity.run(superSparse=True, rebuildSaddles=False) + #cls.masterSaddlesDict = saddles.to_dict() + + + # def setUp(self): + # """ + # Set Up Tests. + # """ + # self.aziscohosSaddle = [x for x in self.aziscohosVicinity.saddles.multipoints if len(x.multipoint) > 1000][0] + + def test_walk2_intro(self): + aziscohosSaddle = [x for x in self.aziscohosVicinity.saddles.multipoints if len(x.multipoint) > 1000][0] + #self.aziscohosVicinity.walkSingleSaddle2(aziscohosSaddle) + #hs0 = aziscohosSaddle.highPerimeterNeighborhoods[0] + #isn = InternalSaddleNetwork(aziscohosSaddle, self.aziscohosVicinity.datamap) + #cs = isn.generate_child_saddles() + + + walker = Walk(self.aziscohosVicinity) + #doms = walker.climb_points(aziscohosSaddle.highPerimeterNeighborhoods[0].to_tuples()) + + + + saddles, runoffs, linkers, summitDomains = walker.climb_from_saddles() + print("yolo") + + + # def test_HLE(self): + # #aziscohosSaddle = [x for x in self.aziscohosVicinity.saddles.multipoints if len(x.multipoint) > 1000][0] + # + # + # #self.aziscohosVicinity.walkSingleSaddle2(aziscohosSaddle) + # #hs0 = aziscohosSaddle.highPerimeterNeighborhoods[0] + # #isn = InternalSaddleNetwork(aziscohosSaddle, self.aziscohosVicinity.datamap) + # #cs = isn.generate_child_saddles() + # + # walker = Walk(self.aziscohosVicinity) + # walker.climb_from_saddles() + # + # #walker = Walk(self.aziscohosVicinity) + # #doms = walker.climb_points(aziscohosSaddle.highPerimeterNeighborhoods[0].to_tuples()) + # + # + # + # #saddles, linkers, summitDomains = walker.climb_from_saddles() + # print("yolo") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index bf6ea76..23391da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,11 @@ cbor dijkstar +lxml fastkml -numpy == 1.16.4 -gdal==3.0.4 +numpy +gdal==3.4.1 geopy scipy shapely utm -geopy \ No newline at end of file +geopy