diff --git a/fire/cli/niv/_netoversigt.py b/fire/cli/niv/_netoversigt.py index 819804f0..f161e4aa 100644 --- a/fire/cli/niv/_netoversigt.py +++ b/fire/cli/niv/_netoversigt.py @@ -1,4 +1,4 @@ -from collections import Counter +from collections import Counter, deque from typing import Dict, List, Set, Tuple import click @@ -108,60 +108,39 @@ def netgraf( net[til].add(fra) # Undersøg om der er nettet består af flere ikke-sammenhængende subnet. - # Skriv advarsel hvis ikke der er mindste et fastholdt punkt i hvert - # subnet. subnet = analyser_subnet(net) - if len(subnet) > 1: - fastholdte_i_subnet = [None for _ in subnet] - for i, subnet_ in enumerate(subnet): - for subnetpunkt in subnet_: - if subnetpunkt in fastholdte_punkter: - fastholdte_i_subnet[i] = subnetpunkt - continue - - if None in fastholdte_i_subnet: - fire.cli.print( - "ADVARSEL: Manglende fastholdt punkt i mindst et subnet! Forslag til fastholdte punkter i hvert subnet:", - bg="yellow", - fg="black", - ) - for i, subnet_ in enumerate(subnet): - if fastholdte_i_subnet[i]: - fire.cli.print( - f" Subnet {i}: {fastholdte_i_subnet[i]}", fg="green" - ) - else: - fire.cli.print(f" Subnet {i}: {subnet_[0]}", fg="red") - - # Tilføj forbindelser fra alle fastholdte punkter til det første fastholdte punkt - udgangspunkt = fastholdte_punkter[0] - for punkt in fastholdte_punkter: - if punkt != udgangspunkt: - net[udgangspunkt].add(punkt) - net[punkt].add(udgangspunkt) - - # Analysér netgraf forbundne_punkter = set() ensomme_punkter = set() - for punkt in alle_punkter: - if path_to_origin(net, udgangspunkt, punkt) is None: - ensomme_punkter.add(punkt) - else: - forbundne_punkter.add(punkt) - # Vi vil ikke have de kunstige forbindelser mellem fastholdte punkter med - # i output, så nu genopbygger vi nettet uden dem - net = {} - for punkt in alle_punkter: - net[punkt] = set() - for fra, til in zip(observationer["Fra"], observationer["Til"]): - net[fra].add(til) - net[til].add(fra) + fastholdte_i_subnet = [None for _ in subnet] + for i, subnet_ in enumerate(subnet): + for punkt in fastholdte_punkter: + if punkt in subnet_: + fastholdte_i_subnet[i] = punkt + forbundne_punkter.update(subnet_) + break + else: + ensomme_punkter.update(subnet_) - # De ensomme punkter skal heller ikke med i netgrafen + # De ensomme punkter skal ikke med i netgrafen for punkt in ensomme_punkter: net.pop(punkt, None) + # Skriv advarsel hvis ikke der er mindste et fastholdt punkt i hvert + # subnet. + + if None in fastholdte_i_subnet: + fire.cli.print( + "ADVARSEL: Manglende fastholdt punkt i mindst et subnet! Forslag til fastholdte punkter i hvert subnet:", + bg="yellow", + fg="black", + ) + for i, subnet_ in enumerate(subnet): + if fastholdte_i_subnet[i]: + fire.cli.print(f" Subnet {i}: {fastholdte_i_subnet[i]}", fg="green") + else: + fire.cli.print(f" Subnet {i}: {subnet_[0]}", fg="red") + # Nu kommer der noget grimt... # Tving alle rækker til at være lige lange, så vi kan lave en dataframe af dem max_antal_naboer = max([len(net[e]) for e in net]) @@ -181,72 +160,47 @@ def netgraf( return netf, ensomme -# ------------------------------------------------------------------------------ -# path_to_origin - eksempel: -# -# graph = { -# 'A': {'B', 'C'}, -# 'B': {'C', 'D'}, -# 'C': {'D'}, -# 'D': {'C'}, -# 'E': {'F'}, -# 'F': {'C'}, -# 'G': {} -# } -# -# print (path_to_origin (graph, 'A', 'C')) -# print (path_to_origin (graph, 'A', 'G')) -# ------------------------------------------------------------------------------ -def path_to_origin( - graph: Dict[str, Set[str]], start: str, origin: str, path: List[str] = [] -): - """ - Mikroskopisk backtracking netkonnektivitetstest. Baseret på et - essay af Pythonstifteren Guido van Rossum, publiceret 1998 på - https://www.python.org/doc/essays/graphs/. Koden er her - moderniseret fra Python 1.5 til 3.7 og modificeret til at - arbejde på dict-over-set (originalen brugte dict-over-list) - """ - path = path + [start] - if start == origin: - return path - if start not in graph: - return None - for node in graph[start]: - if node not in path: - newpath = path_to_origin(graph, node, origin, path) - if newpath: - return newpath - return None - - -def analyser_subnet(net): +def analyser_subnet(net: dict[set]) -> list[list]: """ Find selvstændige net i et større net - Med inspiration fra https://www.geeksforgeeks.org/connected-components-in-an-undirected-graph/ + Bruger breadth first search (BFS). + Baseret på materiale fra: https://www.geeksforgeeks.org/breadth-first-search-or-bfs-for-a-graph/ """ - def depth_first_search(net, visited, vertex, subnet): - visited[vertex] = True - subnet.append(vertex) - - for adjacent_vertex in net[vertex]: - if not visited[adjacent_vertex]: - net, visited, vertex, subnet = depth_first_search( - net, visited, adjacent_vertex, subnet - ) - - return net, visited, vertex, subnet - - visited = {vertex: False for vertex in net.keys()} - connected_vertices = [] - for vertex in net.keys(): - if not visited[vertex]: + # Funktion til breadth first search + def bfs(net, besøgt, startpunkt, subnet=[]): + # Initialiser kø + kø = deque() + + # Marker nuværende punkt som besøgt og føj til kø + besøgt[startpunkt] = True + kø.append(startpunkt) + + # Opbyg subnet + subnet.append(startpunkt) + + # Loop over køen + while kø: + # Fjern nuværende punkt fra køen + nuværende_punkt = kø.popleft() + + # Find naboer til nuværende punkt + # Hvis en nabo ikke har været besøgt, marker den da som besøgt og føj til kø + for nabo in net[nuværende_punkt]: + if not besøgt[nabo]: + besøgt[nabo] = True + kø.append(nabo) + + # Opbyg subnet + subnet.append(nabo) + + besøgt = {punkt: False for punkt in net.keys()} + liste_af_subnet = [] + for punkt in net.keys(): + if not besøgt[punkt]: subnet = [] - net, visited, vertex, subnet = depth_first_search( - net, visited, vertex, subnet - ) - connected_vertices.append(subnet) + bfs(net, besøgt, punkt, subnet) + liste_af_subnet.append(subnet) - return connected_vertices + return liste_af_subnet diff --git a/test/api/niv/test_netanalyse.py b/test/api/niv/test_netanalyse.py new file mode 100644 index 00000000..2cfcac93 --- /dev/null +++ b/test/api/niv/test_netanalyse.py @@ -0,0 +1,99 @@ +import pandas as pd + +from fire.cli.niv._netoversigt import ( + analyser_subnet, + netgraf, +) + + +def test_analyser_subnet(): + """Test at analyser_subnet finder det korrekte antal subnet""" + + disjunkt_net = {} + subnet = [i for i in range(3)] + for N_subnet in range(1, 5): + disjunkt_net.update({node: list(set(subnet) - {node}) for node in subnet}) + + liste_af_subnet = analyser_subnet(disjunkt_net) + + assert len(liste_af_subnet) == N_subnet + + # lav et kopi af disjunkt net og forbind alle subnet. + forbundet_net = {k: v for (k, v) in disjunkt_net.items()} + + # Forbind punkt "0" til første punkt i hvert subnet for at forbinde alle subnet + forbundet_net[0] = list( + set(forbundet_net[0] + [subn[0] for subn in liste_af_subnet]) + ) + + liste_af_subnet = analyser_subnet(forbundet_net) + assert len(liste_af_subnet) == 1 + + subnet = [i + 100 for i in subnet] + + +def test_netgraf(): + """ + Test at netgraf returnerer det forventede antal punkter + + Der testes for antal forbundne punkter, antal singulære punkter, og antal + kolonner i Netgeometri-dataframen (svarende til højeste antal naboer + 1). + """ + N = 20 + alle_punkter = tuple(str(i) for i in range(N)) + fastholdte = ( + "3", + "13", + ) + + # Observationer opbygges som en symmetrisk, rettet, cirkulær graf, for at + # efterligne en lukket nivellementspolygon. + obs = [ + {"Fra": str(i % N), "Til": str(j % N)} for i in range(N) for j in (i + 1, i - 1) + ] + observationer = pd.DataFrame.from_records(obs) + (net, singulære) = netgraf(observationer, alle_punkter, fastholdte) + + # Check ingen singulære + assert len(net) == N + assert len(singulære) == 0 + assert len(net.keys()) == 3 + + # Tilføj disjunkte net (singulære punkter) + obs.append({"Fra": "RMV", "Til": "SKG"}) + obs.append({"Fra": "SKG", "Til": "RMV"}) + obs.append({"Fra": "Hjemme", "Til": "Ude"}) + obs.append({"Fra": "Ude", "Til": "Hjemme"}) + + # ... og tilføj dem til alle_punkter + alle_punkter += ("RMV", "SKG", "Hjemme", "Ude") + + observationer = pd.DataFrame.from_records(obs) + (net, singulære) = netgraf(observationer, alle_punkter, fastholdte) + + assert len(net) == N + assert len(singulære) == 4 + assert len(net.keys()) == 3 + + # Fasthold nyt punkt + fastholdte += ("RMV",) + (net, singulære) = netgraf(observationer, alle_punkter, fastholdte) + + assert len(net) == N + 2 + assert len(singulære) == 2 + assert len(net.keys()) == 3 + + # Fra-Til har ikke en tilsvarende modsatrettet observation + obs.append({"Fra": "SKG", "Til": "Nord"}) + obs.append({"Fra": "SKG", "Til": "Syd"}) + obs.append({"Fra": "SKG", "Til": "Øst"}) + obs.append({"Fra": "SKG", "Til": "Vest"}) + + alle_punkter += ("Nord", "Syd", "Øst", "Vest") + + observationer = pd.DataFrame.from_records(obs) + (net, singulære) = netgraf(observationer, alle_punkter, fastholdte) + + assert len(net) == N + 6 + assert len(singulære) == 2 + assert len(net.keys()) == 6