diff --git a/setup.py b/setup.py index 65566be..822b887 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def find_version(*path_parts): version=find_version("src", "nsdlib", "version.py"), license="MIT", description="Network source detection library", - url="https://github.com/damianfraszczak/nclib", + url="https://github.com/damianfraszczak/nsdlib", author="Damian Frąszczak, Edyta Frąszczak", author_email="damian.fraszczak@wat.edu.pl", classifiers=[ @@ -41,7 +41,7 @@ def find_version(*path_parts): "Programming Language :: Python", "Programming Language :: Python :: 3", ], - keywords="node_importance centrality_measures centrality complex-networks", + keywords="propagation-source-detection outbreaks-detection propagation-reconstruction complex-networks", install_requires=[ "networkx>=3.0", "numpy", diff --git a/src/nsdlib/algorithms/evaluation/jordan_center.py b/src/nsdlib/algorithms/evaluation/jordan_center.py index 8a5baf8..6f46e86 100644 --- a/src/nsdlib/algorithms/evaluation/jordan_center.py +++ b/src/nsdlib/algorithms/evaluation/jordan_center.py @@ -13,7 +13,7 @@ def jordan_center(network: Graph) -> Dict[int, float]: ---------- - [1] L. Ying and K. Zhu, "On the Universality of Jordan Centers for Estimating Infection Sources - in Tree Networks" IEEE Transactions of Information Theory, 2017 + in Tree Networks" IEEE Transactions of Information Theory, 2014 - [2] L. Ying and K. Zhu, "Diffusion Source Localization in Large Networks" Synthesis Lectures on Communication Networks, 2018 diff --git a/src/nsdlib/common/models.py b/src/nsdlib/common/models.py index 34c36b8..fbf3d68 100644 --- a/src/nsdlib/common/models.py +++ b/src/nsdlib/common/models.py @@ -13,25 +13,38 @@ NODE_TYPE = Union[int, str] +@dataclass +class SelectionAlgorithm: + selection_method: Optional[NodeEvaluationAlgorithm] = None + selection_threshold: Optional[float] = None + + def __post_init__(self): + if self.selection_threshold is not None and not ( + 0 <= self.selection_threshold <= 1 + ): + raise ValueError("selection_threshold must be None or between 0 and 1.") + if self.selection_method and self.selection_threshold: + raise ValueError( + "selection_method and selection_threshold cannot be used together." + ) + + @dataclass class SourceDetectionConfig: """Source detection configuration.""" - # for None, only one with the highest score will be selected - selection_threshold: Optional[float] = None node_evaluation_algorithm: NodeEvaluationAlgorithm = ( NodeEvaluationAlgorithm.CENTRALITY_DEGREE ) + selection_algorithm: Optional[SelectionAlgorithm] = None outbreaks_detection_algorithm: Optional[OutbreaksDetectionAlgorithm] = None propagation_reconstruction_algorithm: Optional[ PropagationReconstructionAlgorithm ] = None def __post_init__(self): - if self.selection_threshold is not None and not ( - 0 <= self.selection_threshold <= 1 - ): - raise ValueError("selection_threshold must be None or between 0 and 1.") + if not self.selection_algorithm: + self.selection_algorithm = SelectionAlgorithm() @dataclass diff --git a/src/nsdlib/source_detection.py b/src/nsdlib/source_detection.py index a93ce1b..a71e4db 100644 --- a/src/nsdlib/source_detection.py +++ b/src/nsdlib/source_detection.py @@ -34,7 +34,7 @@ def detect_sources(self, IG: Graph, G: Graph) -> SourceDetectionResult: outbreaks = self._detect_outbreaks(IG) scores_in_outbreaks = self._evaluate_outbreaks(outbreaks) global_scores = self._get_global_scores(scores_in_outbreaks) - detected_sources = self._select_sources(scores_in_outbreaks) + detected_sources = self._select_sources(IG, scores_in_outbreaks) return SourceDetectionResult( config=self.config, G=G, @@ -102,10 +102,41 @@ def _evaluate_outbreaks( ) return scores - def _select_sources(self, outbreaks_evaluation: List[Dict[NODE_TYPE, float]]): + def _select_sources( + self, IG: Graph, outbreaks_evaluation: List[Dict[NODE_TYPE, float]] + ): sources = [] for outbreak_evaluation in outbreaks_evaluation: - if self.config.selection_threshold is None: + if self.config.selection_algorithm.selection_method: + max_score = max(outbreak_evaluation.values()) + nodes_with_higher_score = [ + node + for node, score in outbreak_evaluation.items() + if score == max_score + ] + if len(nodes_with_higher_score) == 1: + sources.append(nodes_with_higher_score[0]) + else: + outbreak_nodes = list(outbreak_evaluation.keys()) + subgraph = IG.subgraph(outbreak_nodes) + selection_evaluation = evaluate_nodes_cached( + network=subgraph, + evaluation_alg=self.config.selection_algorithm.selection_method, + ) + filtered_second_evaluation = { + node: selection_evaluation[node] + for node in nodes_with_higher_score + } + max_second_score = max(filtered_second_evaluation.values()) + sources.extend( + [ + node + for node, score in filtered_second_evaluation.items() + if score == max_second_score + ] + ) + + elif self.config.selection_algorithm.selection_threshold is None: sources.append(max(outbreak_evaluation, key=outbreak_evaluation.get)) else: outbreaks_evaluation_normalized = normalize_dict_values( @@ -116,9 +147,11 @@ def _select_sources(self, outbreaks_evaluation: List[Dict[NODE_TYPE, float]]): [ node for node, evaluation in outbreaks_evaluation_normalized.items() - if evaluation >= self.config.selection_threshold + if evaluation + >= self.config.selection_algorithm.selection_threshold ] ) + return sources diff --git a/src/nsdlib/taxonomies.py b/src/nsdlib/taxonomies.py index 74a8134..7346958 100644 --- a/src/nsdlib/taxonomies.py +++ b/src/nsdlib/taxonomies.py @@ -20,6 +20,7 @@ class NodeEvaluationAlgorithm(Enum): CENTRALITY_DECAY = "decay_centrality" CENTRALITY_DEGREE = "degree_centrality" CENTRALITY_DIFFUSION_DEGREE = "diffusion_degree_centrality" + CENTRALITY_ECCENTRICITY = "eccentricity_centrality" CENTRALITY_EIGENVECTOR = "eigenvector_centrality" CENTRALITY_ENTROPY = "entropy_centrality" CENTRALITY_GEODESTIC_K_PATH = "geodestic_k_path_centrality"