From 62e6ad6d7f0182b9ebbcb015328c90f75f447e21 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= <pmlpm@posteo.de>
Date: Sun, 12 May 2024 00:35:16 +0200
Subject: [PATCH] Added method to check for overlapping segments.

---
 src/topupopt/data/gis/utils.py |  83 ++++++++++++++++--
 tests/test_esipp_problem.py    | 151 ++++++++++++++++++++++++++++++++-
 tests/test_gis_utils.py        |  24 +++++-
 3 files changed, 248 insertions(+), 10 deletions(-)

diff --git a/src/topupopt/data/gis/utils.py b/src/topupopt/data/gis/utils.py
index 0c21992..4197447 100644
--- a/src/topupopt/data/gis/utils.py
+++ b/src/topupopt/data/gis/utils.py
@@ -13,6 +13,7 @@ from numpy import float64, int64
 from geopandas import GeoDataFrame, read_file
 from shapely.geometry import Point, LineString
 import contextily as cx
+from shapely import intersects
 
 # local, internal
 
@@ -1232,15 +1233,87 @@ def convert_edge_path(
 # *****************************************************************************
 # *****************************************************************************
 
-def create_edge_geometry(network: MultiDiGraph, edge_key) -> LineString:
+def create_edge_geometry(
+        network: MultiDiGraph, 
+        edge_key,
+        x_key = osm.KEY_OSMNX_X,
+        y_key = osm.KEY_OSMNX_Y) -> LineString:
     "Returns a newly-created geometry for a given edge."
     
     return LineString(
-        [(network.nodes[edge_key[0]][osm.KEY_OSMNX_X],
-          network.nodes[edge_key[0]][osm.KEY_OSMNX_Y]),
-         (network.nodes[edge_key[1]][osm.KEY_OSMNX_X],
-          network.nodes[edge_key[1]][osm.KEY_OSMNX_Y])]
+        [(network.nodes[edge_key[0]][x_key],
+          network.nodes[edge_key[0]][y_key]),
+         (network.nodes[edge_key[1]][x_key],
+          network.nodes[edge_key[1]][y_key])]
         )
 
 # *****************************************************************************
 # *****************************************************************************
+
+def find_overlapping_edges(
+        network: MultiDiGraph,
+        excluded_edges: list = None
+        ) -> list:
+    """
+    Returns a list of key pairs for edges whose geometries overlap.
+
+    Parameters
+    ----------
+    network : MultiDiGraph
+        The object describing the network.
+    excluded_edges : list, optional
+        A list of edges that should not be considered. The default is None, in
+        which case all edges in the network object will be considered.
+
+    Returns
+    -------
+    list
+        A list containing key pairs for overlapping edges.
+
+    """
+    # check if there are excluded edges
+    if type(excluded_edges) == type(None):
+        excluded_edges = list()
+    # initialise the list of edges to check
+    edges = list(
+        edge_key 
+        for edge_key in network.edges(keys=True)
+        if edge_key not in excluded_edges
+        )
+    visited_edges = []
+    out = []
+    # for each edge
+    for edge_key in edges:
+        # remove the current edge so it is not considered again
+        visited_edges.append(edge_key)
+        # for each other edge
+        for other_edge_key in edges:
+            # for each other edge
+            # skip edges having nodes in common
+            # this will also skip identical edges
+            if edge_key[0] in other_edge_key[0:2] or edge_key[1] in other_edge_key[0:2]:
+                # has nodes in common, skip
+                continue
+            # skip edges that have already been considered in the first loop
+            if other_edge_key in visited_edges:
+                # this edge has already been tested against all other edges
+                continue
+            # first edge                
+            if osm.KEY_OSMNX_GEOMETRY in network.edges[edge_key]:
+                first_geo = network.edges[edge_key][osm.KEY_OSMNX_GEOMETRY]
+            else:
+                first_geo = create_edge_geometry(network, edge_key)
+            # second edge
+            if osm.KEY_OSMNX_GEOMETRY in network.edges[other_edge_key]:
+                second_geo = network.edges[other_edge_key][osm.KEY_OSMNX_GEOMETRY] 
+            else:
+                second_geo = create_edge_geometry(network, other_edge_key)
+            # check if they intersect
+            if intersects(first_geo, second_geo):
+                # they do, add tuple of the edges to the output
+                out.append((edge_key, other_edge_key))
+    # return tuples of overlapping edges
+    return out
+
+# *****************************************************************************
+# *****************************************************************************
diff --git a/tests/test_esipp_problem.py b/tests/test_esipp_problem.py
index eb71285..c69bbc9 100644
--- a/tests/test_esipp_problem.py
+++ b/tests/test_esipp_problem.py
@@ -3639,8 +3639,6 @@ class TestESIPPProblem:
             
     # *************************************************************************
     # *************************************************************************
-    
-    # TODO: trigger error with static losses
         
     def test_direct_imp_exp_network(self):
         
@@ -8153,7 +8151,7 @@ class TestESIPPProblem:
             solver_options={},
             perform_analysis=False,
             plot_results=False,  # True,
-            print_solver_output=True,
+            print_solver_output=False,
             time_frame=tf,
             networks={"mynet": mynet},
             static_losses_mode=True,  # just to reach a line,
@@ -8196,6 +8194,153 @@ class TestESIPPProblem:
         assert math.isclose(pyo.value(ipp.instance.var_capex), 10.0, abs_tol=1e-3)
         # the objective function
         assert math.isclose(pyo.value(ipp.instance.obj_f), -1.193236715e+01, abs_tol=1e-3)
+        
+        
+    # *************************************************************************
+    # *************************************************************************
+    
+    def test_problem_with_reverse_tree_network(self):
+        
+        # assessment
+        q = 0
+        tf = EconomicTimeFrame(
+            discount_rate=3.5/100,
+            reporting_periods={q: (0,)},
+            reporting_period_durations={q: (365 * 24 * 3600,)},
+            time_intervals={q: (0,)},
+            time_interval_durations={q: (1,)},
+        )
+
+        # 2 nodes: one import, one regular
+        mynet = Network()
+
+        # export node
+        node_EXP = "thatexpnode"
+        mynet.add_export_node(
+            node_key=node_EXP,
+            prices={
+                qpk: ResourcePrice(prices=1.0, volumes=None)
+                for qpk in tf.qpk()
+            },
+        )
+
+        # node A
+        node_A = "thatnodea"
+        mynet.add_source_sink_node(
+            node_key=node_A,
+            base_flow={(q, 0): -0.50},
+        )
+        # node B
+        node_B = "thatnodeb"
+        mynet.add_source_sink_node(
+            node_key=node_B,
+            base_flow={(q, 0): -0.25},
+        )
+        # node C
+        node_C = "thatnodec"
+        mynet.add_source_sink_node(
+            node_key=node_C,
+            base_flow={(q, 0): -1.25},
+        )
+        
+        list_imp_arcs = [
+            (node_A, node_EXP), # AE
+            (node_B, node_EXP), # BE
+            (node_C, node_EXP), # CE
+            ]
+        for i, node_pair in enumerate(list_imp_arcs):
+        
+            # import arcs: AE, BE, CE
+            new_arc = Arcs(
+                name="arc_"+str(node_pair),
+                efficiency=None,
+                efficiency_reverse=None,
+                static_loss=None,
+                capacity=[2],
+                minimum_cost=[6],
+                specific_capacity_cost=i,
+                capacity_is_instantaneous=False,
+                validate=False,
+            )
+            mynet.add_directed_arc(*node_pair, arcs=new_arc)        
+        
+        # arcs: AB, BA, BC, CB, AC, CA
+        
+        list_other_arcs = [
+            (node_A, node_B), # AB
+            (node_B, node_A), # BA
+            (node_B, node_C), # BC
+            (node_C, node_B), # CB
+            (node_A, node_C), # AC
+            (node_C, node_A), # CA
+            ]
+        
+        for node_pair in list_other_arcs:
+            # arc
+            new_arc_tech = Arcs(
+                name="any",
+                efficiency=None,
+                efficiency_reverse=None,
+                static_loss=None,
+                capacity=[3],
+                minimum_cost=[2],
+                specific_capacity_cost=0,
+                capacity_is_instantaneous=False,
+                validate=False,
+            )
+            mynet.add_directed_arc(*node_pair, arcs=new_arc_tech)
+
+        # identify node types
+        mynet.identify_node_types()
+
+        # no sos, regular time intervals
+        ipp = self.build_solve_ipp(
+            solver_options={},
+            perform_analysis=False,
+            plot_results=False,  # True,
+            print_solver_output=False,
+            time_frame=tf,
+            networks={"mynet": mynet},
+            static_losses_mode=True,  # just to reach a line,
+            mandatory_arcs=[],
+            max_number_parallel_arcs={},
+            simplify_problem=True,
+        )
+
+        assert ipp.has_peak_total_assessments()
+        assert ipp.results["Problem"][0]["Number of constraints"] == 61 
+        assert ipp.results["Problem"][0]["Number of variables"] == 53 
+        assert ipp.results["Problem"][0]["Number of nonzeros"] == 143
+        
+        # *********************************************************************
+        # *********************************************************************
+
+        # validation
+
+        # only the IA arc should be installed
+        true_imp_arcs_selected = [True, False, False]
+        for node_pair, true_arc_decision in zip(list_imp_arcs, true_imp_arcs_selected):
+            assert (
+                true_arc_decision
+                in ipp.networks["mynet"]
+                .edges[(*node_pair, 0)][Network.KEY_ARC_TECH]
+                .options_selected
+            )
+        # only two arcs between A, B and C can be installed
+        arcs_selected = tuple(
+            1
+            for node_pair in list_other_arcs
+            if True in ipp.networks["mynet"]
+            .edges[(*node_pair, 0)][Network.KEY_ARC_TECH]
+            .options_selected
+            )
+        assert sum(arcs_selected) == 2
+        # the network must be tree-shaped
+        assert ipp.networks["mynet"].has_tree_topology()
+        # capex
+        assert math.isclose(pyo.value(ipp.instance.var_capex), 10.0, abs_tol=1e-3)
+        # the objective function
+        assert math.isclose(pyo.value(ipp.instance.obj_f), -(10+(-11.93236715+10)), abs_tol=1e-3)
 
 # *****************************************************************************
 # *****************************************************************************
\ No newline at end of file
diff --git a/tests/test_gis_utils.py b/tests/test_gis_utils.py
index d2edd78..b561546 100644
--- a/tests/test_gis_utils.py
+++ b/tests/test_gis_utils.py
@@ -2650,15 +2650,35 @@ class TestGisUtils:
         edge_key = (0, 1)
         G.add_node(0, x=0, y=0)
         G.add_node(1, x=1, y=1)
-        G.add_edge(0, 1, length=2**0.5)
+        k1 = G.add_edge(0, 1, length=2**0.5)
         first_geo = gis_utils.create_edge_geometry(G, edge_key)
         edge_key = (2, 3)
         G.add_node(2, x=0, y=1)
         G.add_node(3, x=1, y=0)
-        G.add_edge(2, 3, length=2**0.5)
+        k2 = G.add_edge(2, 3, length=2**0.5)
         second_geo = gis_utils.create_edge_geometry(G, edge_key)
         assert intersects(first_geo, second_geo)
         
+        # test finding overlapping edges
+        ol_edges = gis_utils.find_overlapping_edges(G)
+        assert len(ol_edges) == 1
+        assert ((0,1,k1),(2,3,k2)) in ol_edges
+        # no overlapping edges if there is only one node
+        ol_edges = gis_utils.find_overlapping_edges(G, excluded_edges=[(0,1,k1)])
+        assert len(ol_edges) == 0
+        
+        # add fifth node
+        G.add_node(4, x=0.5, y=1)
+        k3 = G.add_edge(3, 4, length=(0.5**2+1**2)**0.5)
+        ol_edges = gis_utils.find_overlapping_edges(G)
+        assert len(ol_edges) == 2
+        assert ((0,1,k1),(2,3,k2)) in ol_edges
+        assert ((0,1,k1),(3,4,k3)) in ol_edges
+        # fewer overlapping edges
+        ol_edges = gis_utils.find_overlapping_edges(G, excluded_edges=[(2,3,k2)])
+        assert len(ol_edges) == 1
+        assert ((0,1,k1),(3,4,k3)) in ol_edges
+        
     # *************************************************************************
     # *************************************************************************
 
-- 
GitLab