def connect_features(cls, catchment: Catchment, nexus: Nexus, is_catchment_upstream: bool): """ Make the connections on both sides between this catchment and nexus. Set the connections for this catchment and nexus pair. How the upstream/downstream relationship exists is indicated by another parameter. E.g., if ``is_catchment_upstream`` is ``True``, then the catchment's ::attribute:`Catchment.outflow` property is set to this nexus, and the catchment is added to the nexus's ::attribute:`Nexus.contributing_catchments` tuple (or the tuple is created with the catchment placed in it). Other properties, in particular those on the other "side" of each entity from where the two are connected, are not altered. Note that, for ::class:`Nexus` properties, duplication is avoided, but otherwise the catchment will be added to a collection property (potentially with the collection object being created). However, for ::class`Catchment` properties, which are each an individual reference, the reference will be always be set. No check is performed as to whether the property currently references ``None``. Parameters ---------- catchment : Catchment The upstream/downstream catchment in the connected pair. nexus : Nexus The upstream/downstream (with this being opposite the state of ``catchment``) nexus in the connected pair. is_catchment_upstream : bool Whether ``catchment`` is connected upstream of ``nexus``. """ if is_catchment_upstream: # Add catchment to nexus's collection of contributing, accounting for it being in there or the collection # needing to be created if nexus.contributing_catchments is None: nexus._contributing_catchments = (catchment, ) elif catchment.id not in [ cat.id for cat in nexus.contributing_catchments ]: nexus._contributing_catchments = nexus.contributing_catchments + ( catchment, ) # Add nexus as catchment's outflow catchment._outflow = nexus else: # Add catchment to nexus's collection of receiving, accounting for it being in there or the collection # needing to be created if nexus.receiving_catchments is None: nexus._receiving_catchments = (catchment, ) elif catchment.id not in [ cat.id for cat in nexus.receiving_catchments ]: nexus._receiving_catchments = nexus.receiving_catchments + ( catchment, ) # Add nexus as catchment's inflow catchment._inflow = nexus
def nexus(): """ Nexus object to test """ nexus_id = 'nex-test' catchment_id_receiving0 = 'cat-hymod-receive0' catchment_id_receiving1 = 'cat-hymod-receive1' catchment_id_contributing0 = 'cat-hymod-contribute0' catchment_id_contributing1 = 'cat-hymod-contribute1' data_path = _current_dir.joinpath('data') data_file = data_path.joinpath('example_realization_config.json') with open(data_file) as fp: data = json.load(fp) params = {} receiving_catchments = (Catchment(catchment_id_receiving0, params), Catchment(catchment_id_receiving1, params)) #tuple contributing_catchments = [ Catchment(catchment_id_contributing0, params), Catchment(catchment_id_contributing1, params) ] #list location = HydroLocation(nexus_id, (0, 0), HydroLocationType.UNDEFINED, None) nexus = Nexus(nexus_id, location, receiving_catchments, contributing_catchments) receiving_catchments[0]._inflow = nexus receiving_catchments[1]._inflow = nexus contributing_catchments[0]._outflow = nexus contributing_catchments[1]._outflow = nexus yield nexus
def hydrofabric_graph(self) -> Dict[str, Union[Catchment, Nexus]]: """ Lazily get the hydrofabric object graph as a dictionary of the elements by their ids. Using data in ::attribute:`catchment_geodataframe` and ::attribute:`nexus_geodataframe`, method generates (when necessary) a hydrofabric object graph of associated ::class:`Catchment` and ::class:`Nexus` objects, including all the inter-object relationships. These are collected in dictionary data structure keyed by the ``id`` property of each object. Once that is created, the backing attribute is set to this dictionary for subsequent reuse, and the dictionary is returned. Returns ------- Dict[str, Union[Catchment, Nexus]] A dictionary of the nodes of the graph keyed by each's string id value. """ if self._hydrofabric_graph is None: # Keys of nexus id to lists of catchment ids for the catchments receiving water from this nexus nexus_receiving_cats = dict() # Keys of nexus id to lists of catchment ids for the catchments contributing water to this nexus nexus_contrib_cats = dict() known_catchment_ids = set() known_nexus_ids = set() cat_to = dict() cat_from = dict() for cat_id in self.catchment_geodataframe.index: known_catchment_ids.add(cat_id) # TODO: do we need to account for more than one downstream? to_nex_id = self.catchment_geodataframe.loc[cat_id][ 'toid'].strip() known_nexus_ids.add(to_nex_id) cat_to[cat_id] = to_nex_id if to_nex_id in nexus_contrib_cats: nexus_contrib_cats[to_nex_id].add(cat_id) else: nexus_contrib_cats[to_nex_id] = {cat_id} # TODO: do we need to account for contained/containing/conjoined? for nex_id in self.nexus_geodataframe.index: known_nexus_ids.add(nex_id) to_cats = self.nexus_geodataframe.loc[nex_id]['toid'] # Handle the first one with conditional check separate, to optimize later ones first_cat_id = to_cats.split(',')[0].strip() if nex_id in nexus_receiving_cats and to_cats: nexus_receiving_cats[nex_id].add(first_cat_id) else: nexus_receiving_cats[nex_id] = {first_cat_id} known_catchment_ids.add(first_cat_id) cat_from[first_cat_id] = nex_id # Now add any remaining for cat_id in to_cats[1:]: clean_cat_id = cat_id.strip() nexus_receiving_cats[nex_id].add(clean_cat_id) known_catchment_ids.add(clean_cat_id) cat_from[cat_id] = nex_id hf = dict() # Create the catchments first, just without any upstream/downstream connections for cat_id in known_catchment_ids: # TODO: do params need to be something different? hf[cat_id] = Catchment(catchment_id=cat_id, params=dict()) # Create the nexuses next, applying the right collections of catchments for contrib and receiv for nex_id in known_nexus_ids: contributing = set() for cid in nexus_contrib_cats[nex_id]: contributing.add(hf[cid]) receiving = set() for cid in nexus_receiving_cats[nex_id]: receiving.add(hf[cid]) hf[nex_id] = Nexus( nexus_id=nex_id, hydro_location=HydroLocation(realized_nexus=nex_id), receiving_catchments=list(receiving), contributing_catchments=list(contributing)) # Now go back and apply the right to/from relationships for catchments for cat_id, nex_id in cat_to.items(): hf[cat_id]._outflow = hf[nex_id] for cat_id, nex_id in cat_from.items(): hf[cat_id]._inflow = hf[nex_id] # TODO: again, do we need to worry about contained/containing/conjoined? # Finally ... self._hydrofabric_graph = hf return self._hydrofabric_graph
def get_subset_hydrofabric( self, subset: SubsetDefinition) -> 'MappedGraphHydrofabric': """ Derive a hydrofabric object from this one with only entities included in a given subset. Parameters ---------- subset : SubsetDefinition Subset describing which catchments/nexuses from this instance may be included in the produced hydrofabric. Returns ------- GeoJsonHydrofabric A hydrofabric object that is a subset of this instance as defined by the given param. """ new_graph: Dict[str, Union[Catchment, Nexus]] = dict() new_graph_roots = set() # TODO: consider changing to implement via Pickle and copy module later graph_features_stack = list(self.roots) already_seen = set() # keep track of new graph entities where we can't immediately link to one (or more) of their parents unlinked_to_parent = defaultdict(set) # Keep track of catchments with related nested catchments to handle at the end have_nested_catchments = set() while len(graph_features_stack) > 0: feature_id = graph_features_stack.pop() if feature_id in already_seen: continue else: already_seen.add(feature_id) old_cat = self._hydrofabric_graph[feature_id] # add ids for all downstream connected features to graph_features_stack for later processing graph_features_stack.extend( self.get_ids_of_connected(old_cat, upstream=False, downstream=True)) subset_copy_of_feature = None # Assume False means feature is a Nexus is_catchment = False # If feature is in subset, make the start of a deep copy. # Note that the deep copy's upstream refs are handled in next step, and downstream refs are handled during # creation of the subset copy of the downstream object if feature_id in subset.catchment_ids: is_catchment = True subset_copy_of_feature = Catchment( feature_id, params=dict(), realization=old_cat.realization) # Must track and handle (later) contained_catchments, containing_catchment, and conjoined_catchments if old_cat.containing_catchment is not None: if old_cat.containing_catchment.id in subset.catchment_ids: have_nested_catchments.add(feature_id) graph_features_stack.append( old_cat.containing_catchment.id) if old_cat.contained_catchments: contained_ids = [ c.id for c in old_cat.contained_catchments if c.id in subset.catchment_ids ] have_nested_catchments.update(contained_ids) graph_features_stack.extend(contained_ids) if old_cat.conjoined_catchments: conjoined_ids = [ c.id for c in old_cat.conjoined_catchments if c.id in subset.catchment_ids ] have_nested_catchments.update(conjoined_ids) graph_features_stack.extend(conjoined_ids) elif feature_id in subset.nexus_ids: subset_copy_of_feature = Nexus( feature_id, hydro_location=old_cat._hydro_location) # Will be None when not in subset, so ... if subset_copy_of_feature is not None: # add to new_graph new_graph[feature_id] = subset_copy_of_feature # Get the ids of parents in the subset parent_ids = self.get_ids_of_connected(old_cat, upstream=True, downstream=False) pids_in_subset = [ pid for pid in parent_ids if (pid in subset.catchment_ids or pid in subset.nexus_ids) ] # If old feature does not have any parents that will be in the subset, this is a new root if len(pids_in_subset) == 0: new_graph_roots.add(feature_id) # For each parent in the subset, set up ref to new copy of parent and its ref down to feature's new copy for pid in pids_in_subset: # if parent copy in new graph (i.e., the copy exists) if pid in new_graph: # set upstream connections to parent copy and downstream connection of parent copy to this if is_catchment: self.connect_features( catchment=subset_copy_of_feature, nexus=new_graph[pid], is_catchment_upstream=False) else: self.connect_features(catchment=new_graph[pid], nexus=subset_copy_of_feature, is_catchment_upstream=True) # If parent copy not in new graph yet (i.e., does not exist), add to collection to deal with later else: unlinked_to_parent[feature_id].add(pid) # Now deal with any previously unlinked parents for feature_id in unlinked_to_parent: new_cat = new_graph[feature_id] if isinstance(new_cat, Catchment): for pid in unlinked_to_parent[feature_id]: self.connect_features(catchment=new_cat, nexus=new_graph[pid], is_catchment_upstream=False) else: for pid in unlinked_to_parent[feature_id]: self.connect_features(catchment=new_graph[pid], nexus=new_cat, is_catchment_upstream=True) # Also deal with any catchment conjoined, containing, or contained collections for cid in subset.catchment_ids: old_cat = self._hydrofabric_graph[cid] new_cat = new_graph[cid] if old_cat.containing_catchment is not None and old_cat.containing_catchment.id in subset.catchment_ids: new_cat._containing_catchment = new_graph[ old_cat.containing_catchment.id] if old_cat.contained_catchments: for contained_id in [ c.id for c in old_cat.contained_catchments if c.id in subset.catchment_ids ]: if contained_id not in [ c.id for c in new_cat.contained_catchments ]: new_cat.contained_catchments = new_cat.contained_catchments + ( new_graph[contained_id], ) if old_cat.conjoined_catchments: for conjoined_id in [ c.id for c in old_cat.conjoined_catchments if c.id in subset.catchment_ids ]: if conjoined_id not in [ c.id for c in new_cat.conjoined_catchments ]: new_cat.conjoined_catchments = new_cat.conjoined_catchments + ( new_graph[conjoined_id], ) return MappedGraphHydrofabric(hydrofabric_object_graph=new_graph, roots=frozenset(new_graph_roots), graph_creator=self)