Mage.py 5.41 KiB
# Mage.py -- Pamhyr MAGE checkers
# Copyright (C) 2023  INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

# -*- coding: utf-8 -*-

import time

from queue import Queue
from tools import flatten, timer

from PyQt5.QtCore import QCoreApplication

from Checker.Checker import AbstractModelChecker, STATUS

_translate = QCoreApplication.translate

class MageNetworkGraphChecker(AbstractModelChecker):
    def __init__(self, connectivity = True):
        super(MageNetworkGraphChecker, self).__init__()

        self._mode_conn = connectivity

        if connectivity:
            mode = "connectivity"
        else:
            mode = "cycle"

        self._name = _translate("Checker", f"Mage network graph {mode} checker")
        self._description = _translate("Checker", "Check if the network graph is valid")

    @timer
    def _connectivity(self, summary, status, graph):
        # Keep only enabled edges
        edges = list(
            filter(
                lambda e: e.is_enable(),
                graph.edges()
            )
        )
        # Get all related nodes
        nodes = list(
            set(
                flatten(
                    map(
                        lambda e: [e.node1, e.node2],
                        edges
                    )
                )
            )
        )

        # Visite graph
        q = Queue()
        for node in nodes:
            if graph.is_upstream_node(node):
                q.put(node)
                break           # We take only one node

        if q.qsize() == 0:
            summary = "no_upstream_node"
            status = STATUS.ERROR
            return summary, status

        visited = set()
        while q.qsize() != 0:
            current = q.get()
            if current is None:
                continue

            # Cut potential infinite loop on graph cycle
            if current in visited:
                continue

            # Get next node(s) to visite
            nexts = flatten(
                map(
                    lambda e: [e.node1, e.node2],
                    filter(
                        lambda e: e.node1 == current or e.node2 == current,
                        edges
                    )
                )
            )

            for n in nexts:
                q.put(n)

            # Visited node
            visited.add(current)

        if len(visited) != len(nodes):
            summary = "network_connectivity"
            status = STATUS.ERROR
            return summary, status

        return summary, status

    @timer
    def _cycle(self, summary, status, graph):
        # Keep only enabled edges
        edges = list(
            filter(
                lambda e: e.is_enable(),
                graph.edges()
            )
        )

        for edge in edges:
            # Visite graph starting from EDGE source node (INITIAL)
            q = Queue()
            initial = edge.node1
            q.put(initial)

            visited = set()
            while q.qsize() != 0:
                current = q.get()
                if current is None:
                    continue

                # Cut potential infinite loop on subgraph cycle
                if current in visited:
                    continue

                related_edges = list(
                    filter(
                        lambda e: e.node1 == current,
                        edges
                    )
                )

                # Get next node(s) to visite
                nexts = list(
                    map(
                        lambda e: e.node2,
                        related_edges
                    )
                )

                # The initial node cannot be visited a second time,
                # otherelse there is a cycle in the graph
                if initial in nexts:
                    summary = "cycle_detected"
                    status = STATUS.ERROR
                    return summary, status

                for n in nexts:
                    q.put(n)

                visited.add(current)

        return summary, status

    def run(self, study):
        summary = "ok"
        status = STATUS.OK

        if study is None:
            self._status = STATUS.ERROR
            self._summary = "invalid_study"
            return False

        river = study.river
        if river is None:
            self._status = STATUS.ERROR
            self._summary = "no_river_found"
            return False

        edges = list(filter(lambda e: e.is_enable(), river.edges()))
        if len(edges) == 0:
            self._status = STATUS.ERROR
            self._summary = "no_reach_defined"
            return False

        if self._mode_conn:
            summary, status = self._connectivity(summary, status, river)
        else:
            summary, status = self._cycle(summary, status, river)

        self._summary = summary
        self._status = status
        return True