From 1c5e1addf84fa7c4bcdb3ee9a65def06bb2a3134 Mon Sep 17 00:00:00 2001 From: Sanat Kumar Gupta <123228827+SKG24@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:17:25 +0530 Subject: [PATCH 001/276] Added stochastic_variability.py file This example demonstrates the variability of stochastic community detection methods by analyzing the consistency of multiple partitions using similarity measures (NMI, VI, RI) on both random and structured graphs. --- .../stochastic_variability.py | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 doc/examples_sphinx-gallery/stochastic_variability.py diff --git a/doc/examples_sphinx-gallery/stochastic_variability.py b/doc/examples_sphinx-gallery/stochastic_variability.py new file mode 100644 index 000000000..b4217cb4d --- /dev/null +++ b/doc/examples_sphinx-gallery/stochastic_variability.py @@ -0,0 +1,96 @@ +""" +.. _tutorials-stochastic-variability: + +========================================================= +Stochastic Variability in Community Detection Algorithms +========================================================= + +This example demonstrates the variability of stochastic community detection methods by analyzing the consistency of multiple partitions using similarity measures (NMI, VI, RI) on both random and structured graphs. + +""" +# %% +# Import Libraries +import igraph as ig +import numpy as np +import matplotlib.pyplot as plt +import itertools + +# %% +# First, we generate a graph. +# Generates a random Erdos-Renyi graph (no clear community structure) +def generate_random_graph(n, p): + return ig.Graph.Erdos_Renyi(n=n, p=p) + +# %% +# Generates a clustered graph with clear communities using the Stochastic Block Model (SBM) +def generate_clustered_graph(n, clusters, intra_p, inter_p): + block_sizes = [n // clusters] * clusters + prob_matrix = [[intra_p if i == j else inter_p for j in range(clusters)] for i in range(clusters)] + return ig.Graph.SBM(sum(block_sizes), prob_matrix, block_sizes) + +# %% +# Computes pairwise similarity (NMI, VI, RI) between partitions +def compute_pairwise_similarity(partitions, method): + """Computes pairwise similarity measure between partitions.""" + scores = [] + for p1, p2 in itertools.combinations(partitions, 2): + scores.append(ig.compare_communities(p1, p2, method=method)) + return scores + +# %% +# Stochastic Community Detection +# Runs Louvain's method iteratively to generate partitions +# Computes similarity metrics: +def run_experiment(graph, iterations=50): + """Runs the stochastic method multiple times and collects community partitions.""" + partitions = [graph.community_multilevel().membership for _ in range(iterations)] + nmi_scores = compute_pairwise_similarity(partitions, method="nmi") + vi_scores = compute_pairwise_similarity(partitions, method="vi") + ri_scores = compute_pairwise_similarity(partitions, method="rand") + return nmi_scores, vi_scores, ri_scores + +# %% +# Parameters +n_nodes = 100 +p_random = 0.05 +clusters = 4 +p_intra = 0.3 # High intra-cluster connection probability +p_inter = 0.01 # Low inter-cluster connection probability + +# %% +# Generate graphs +random_graph = generate_random_graph(n_nodes, p_random) +clustered_graph = generate_clustered_graph(n_nodes, clusters, p_intra, p_inter) + +# %% +# Run experiments +nmi_random, vi_random, ri_random = run_experiment(random_graph) +nmi_clustered, vi_clustered, ri_clustered = run_experiment(clustered_graph) + +# %% +# Lets, plot the histograms +fig, axes = plt.subplots(3, 2, figsize=(12, 10)) +measures = [(nmi_random, nmi_clustered, "NMI"), (vi_random, vi_clustered, "VI"), (ri_random, ri_clustered, "RI")] +colors = ["red", "blue", "green"] + +for i, (random_scores, clustered_scores, measure) in enumerate(measures): + axes[i][0].hist(random_scores, bins=20, alpha=0.7, color=colors[i], edgecolor="black") + axes[i][0].set_title(f"Histogram of {measure} - Random Graph") + axes[i][0].set_xlabel(f"{measure} Score") + axes[i][0].set_ylabel("Frequency") + + axes[i][1].hist(clustered_scores, bins=20, alpha=0.7, color=colors[i], edgecolor="black") + axes[i][1].set_title(f"Histogram of {measure} - Clustered Graph") + axes[i][1].set_xlabel(f"{measure} Score") + +plt.tight_layout() +plt.show() + +# %% +# The results are plotted as histograms for random vs. clustered graphs, highlighting differences in detected community structures. +#The key reason for the inconsistency in random graphs and higher consistency in structured graphs is due to community structure strength: +#Random Graphs: Lack clear communities, leading to unstable partitions. Stochastic algorithms detect different structures across runs, resulting in low NMI, high VI, and inconsistent RI. +#Structured Graphs: Have well-defined communities, so detected partitions are more stable across multiple runs, leading to high NMI, low VI, and stable RI. + + +# %% From a3ac9c19e8a74bf5111dfcaec0058ef0246003f4 Mon Sep 17 00:00:00 2001 From: Sanat Kumar Gupta <123228827+SKG24@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:20:14 +0530 Subject: [PATCH 002/276] Update stochastic_variability.py --- doc/examples_sphinx-gallery/stochastic_variability.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/examples_sphinx-gallery/stochastic_variability.py b/doc/examples_sphinx-gallery/stochastic_variability.py index b4217cb4d..7c44f7add 100644 --- a/doc/examples_sphinx-gallery/stochastic_variability.py +++ b/doc/examples_sphinx-gallery/stochastic_variability.py @@ -91,6 +91,3 @@ def run_experiment(graph, iterations=50): #The key reason for the inconsistency in random graphs and higher consistency in structured graphs is due to community structure strength: #Random Graphs: Lack clear communities, leading to unstable partitions. Stochastic algorithms detect different structures across runs, resulting in low NMI, high VI, and inconsistent RI. #Structured Graphs: Have well-defined communities, so detected partitions are more stable across multiple runs, leading to high NMI, low VI, and stable RI. - - -# %% From dc52eb3c4dd0e3402fcb8685a18f73e77dfc568b Mon Sep 17 00:00:00 2001 From: Sanat Kumar Gupta <123228827+SKG24@users.noreply.github.com> Date: Thu, 27 Mar 2025 21:34:00 +0530 Subject: [PATCH 003/276] Update sg_execution_times.rst --- doc/source/sg_execution_times.rst | 87 ++++++++++++++++--------------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/doc/source/sg_execution_times.rst b/doc/source/sg_execution_times.rst index a63741754..65c6c041f 100644 --- a/doc/source/sg_execution_times.rst +++ b/doc/source/sg_execution_times.rst @@ -6,7 +6,7 @@ Computation times ================= -**00:10.013** total execution time for 25 files **from all galleries**: +**01:51.199** total execution time for 26 files **from all galleries**: .. container:: @@ -33,77 +33,80 @@ Computation times - Time - Mem (MB) * - :ref:`sphx_glr_tutorials_visualize_cliques.py` (``../examples_sphinx-gallery/visualize_cliques.py``) - - 00:02.970 + - 00:39.554 + - 0.0 + * - :ref:`sphx_glr_tutorials_visual_style.py` (``../examples_sphinx-gallery/visual_style.py``) + - 00:11.628 - 0.0 * - :ref:`sphx_glr_tutorials_ring_animation.py` (``../examples_sphinx-gallery/ring_animation.py``) - - 00:01.287 + - 00:09.870 - 0.0 - * - :ref:`sphx_glr_tutorials_cluster_contraction.py` (``../examples_sphinx-gallery/cluster_contraction.py``) - - 00:00.759 + * - :ref:`sphx_glr_tutorials_delaunay-triangulation.py` (``../examples_sphinx-gallery/delaunay-triangulation.py``) + - 00:09.261 - 0.0 * - :ref:`sphx_glr_tutorials_betweenness.py` (``../examples_sphinx-gallery/betweenness.py``) - - 00:00.735 - - 0.0 - * - :ref:`sphx_glr_tutorials_visual_style.py` (``../examples_sphinx-gallery/visual_style.py``) - - 00:00.711 - - 0.0 - * - :ref:`sphx_glr_tutorials_delaunay-triangulation.py` (``../examples_sphinx-gallery/delaunay-triangulation.py``) - - 00:00.504 + - 00:06.259 - 0.0 * - :ref:`sphx_glr_tutorials_configuration.py` (``../examples_sphinx-gallery/configuration.py``) - - 00:00.416 + - 00:05.379 - 0.0 - * - :ref:`sphx_glr_tutorials_online_user_actions.py` (``../examples_sphinx-gallery/online_user_actions.py``) - - 00:00.332 + * - :ref:`sphx_glr_tutorials_cluster_contraction.py` (``../examples_sphinx-gallery/cluster_contraction.py``) + - 00:04.307 - 0.0 * - :ref:`sphx_glr_tutorials_erdos_renyi.py` (``../examples_sphinx-gallery/erdos_renyi.py``) - - 00:00.313 + - 00:03.508 - 0.0 - * - :ref:`sphx_glr_tutorials_connected_components.py` (``../examples_sphinx-gallery/connected_components.py``) - - 00:00.216 + * - :ref:`sphx_glr_tutorials_bridges.py` (``../examples_sphinx-gallery/bridges.py``) + - 00:02.530 - 0.0 * - :ref:`sphx_glr_tutorials_complement.py` (``../examples_sphinx-gallery/complement.py``) - - 00:00.201 - - 0.0 - * - :ref:`sphx_glr_tutorials_generate_dag.py` (``../examples_sphinx-gallery/generate_dag.py``) - - 00:00.194 + - 00:02.393 - 0.0 * - :ref:`sphx_glr_tutorials_visualize_communities.py` (``../examples_sphinx-gallery/visualize_communities.py``) - - 00:00.176 + - 00:02.157 - 0.0 - * - :ref:`sphx_glr_tutorials_bridges.py` (``../examples_sphinx-gallery/bridges.py``) - - 00:00.169 + * - :ref:`sphx_glr_tutorials_stochastic_variability.py` (``../examples_sphinx-gallery/stochastic_variability.py``) + - 00:01.960 - 0.0 - * - :ref:`sphx_glr_tutorials_spanning_trees.py` (``../examples_sphinx-gallery/spanning_trees.py``) - - 00:00.161 + * - :ref:`sphx_glr_tutorials_online_user_actions.py` (``../examples_sphinx-gallery/online_user_actions.py``) + - 00:01.750 - 0.0 - * - :ref:`sphx_glr_tutorials_isomorphism.py` (``../examples_sphinx-gallery/isomorphism.py``) - - 00:00.153 + * - :ref:`sphx_glr_tutorials_connected_components.py` (``../examples_sphinx-gallery/connected_components.py``) + - 00:01.728 - 0.0 - * - :ref:`sphx_glr_tutorials_quickstart.py` (``../examples_sphinx-gallery/quickstart.py``) - - 00:00.142 + * - :ref:`sphx_glr_tutorials_isomorphism.py` (``../examples_sphinx-gallery/isomorphism.py``) + - 00:01.376 - 0.0 * - :ref:`sphx_glr_tutorials_minimum_spanning_trees.py` (``../examples_sphinx-gallery/minimum_spanning_trees.py``) - - 00:00.137 + - 00:01.135 + - 0.0 + * - :ref:`sphx_glr_tutorials_spanning_trees.py` (``../examples_sphinx-gallery/spanning_trees.py``) + - 00:01.120 + - 0.0 + * - :ref:`sphx_glr_tutorials_generate_dag.py` (``../examples_sphinx-gallery/generate_dag.py``) + - 00:00.939 + - 0.0 + * - :ref:`sphx_glr_tutorials_quickstart.py` (``../examples_sphinx-gallery/quickstart.py``) + - 00:00.902 - 0.0 * - :ref:`sphx_glr_tutorials_simplify.py` (``../examples_sphinx-gallery/simplify.py``) - - 00:00.079 + - 00:00.840 - 0.0 * - :ref:`sphx_glr_tutorials_bipartite_matching_maxflow.py` (``../examples_sphinx-gallery/bipartite_matching_maxflow.py``) - - 00:00.073 + - 00:00.674 - 0.0 - * - :ref:`sphx_glr_tutorials_articulation_points.py` (``../examples_sphinx-gallery/articulation_points.py``) - - 00:00.067 + * - :ref:`sphx_glr_tutorials_shortest_path_visualisation.py` (``../examples_sphinx-gallery/shortest_path_visualisation.py``) + - 00:00.609 - 0.0 - * - :ref:`sphx_glr_tutorials_topological_sort.py` (``../examples_sphinx-gallery/topological_sort.py``) - - 00:00.058 + * - :ref:`sphx_glr_tutorials_articulation_points.py` (``../examples_sphinx-gallery/articulation_points.py``) + - 00:00.396 - 0.0 * - :ref:`sphx_glr_tutorials_bipartite_matching.py` (``../examples_sphinx-gallery/bipartite_matching.py``) - - 00:00.058 + - 00:00.370 - 0.0 - * - :ref:`sphx_glr_tutorials_shortest_path_visualisation.py` (``../examples_sphinx-gallery/shortest_path_visualisation.py``) - - 00:00.052 + * - :ref:`sphx_glr_tutorials_topological_sort.py` (``../examples_sphinx-gallery/topological_sort.py``) + - 00:00.319 - 0.0 * - :ref:`sphx_glr_tutorials_maxflow.py` (``../examples_sphinx-gallery/maxflow.py``) - - 00:00.052 + - 00:00.234 - 0.0 From 97b7192b3a1c726259350e86efdfdef2ccc416ad Mon Sep 17 00:00:00 2001 From: Sanat Kumar Gupta <123228827+SKG24@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:43:00 +0530 Subject: [PATCH 004/276] Update stochastic_variability.py I have made the changes as per the review. --- .../stochastic_variability.py | 137 ++++++++++++------ 1 file changed, 89 insertions(+), 48 deletions(-) diff --git a/doc/examples_sphinx-gallery/stochastic_variability.py b/doc/examples_sphinx-gallery/stochastic_variability.py index 7c44f7add..944f82118 100644 --- a/doc/examples_sphinx-gallery/stochastic_variability.py +++ b/doc/examples_sphinx-gallery/stochastic_variability.py @@ -5,89 +5,130 @@ Stochastic Variability in Community Detection Algorithms ========================================================= -This example demonstrates the variability of stochastic community detection methods by analyzing the consistency of multiple partitions using similarity measures (NMI, VI, RI) on both random and structured graphs. +This example demonstrates the variability of stochastic community detection methods by analyzing the consistency of multiple partitions using similarity measures normalized mutual information (NMI), variation of information (VI), rand index (RI) on both random and structured graphs. """ # %% -# Import Libraries +# Import libraries import igraph as ig -import numpy as np import matplotlib.pyplot as plt import itertools # %% # First, we generate a graph. -# Generates a random Erdos-Renyi graph (no clear community structure) -def generate_random_graph(n, p): - return ig.Graph.Erdos_Renyi(n=n, p=p) +# Load the karate club network +karate = ig.Graph.Famous("Zachary") # %% -# Generates a clustered graph with clear communities using the Stochastic Block Model (SBM) -def generate_clustered_graph(n, clusters, intra_p, inter_p): - block_sizes = [n // clusters] * clusters - prob_matrix = [[intra_p if i == j else inter_p for j in range(clusters)] for i in range(clusters)] - return ig.Graph.SBM(sum(block_sizes), prob_matrix, block_sizes) +#For the random graph, we use an Erdős-Rényi :math:`G(n, m)` model, where 'n' is the number of nodes +#and 'm' is the number of edges. We set 'm' to match the edge count of the empirical (Karate Club) +#network to ensure structural similarity in terms of connectivity, making comparisons meaningful. +n_nodes = karate.vcount() +n_edges = karate.ecount() +#Generate an Erdős-Rényi graph with the same number of nodes and edges +random_graph = ig.Graph.Erdos_Renyi(n=n_nodes, m=n_edges) # %% -# Computes pairwise similarity (NMI, VI, RI) between partitions +# Now, lets plot the graph to visually understand them. + +# Create subplots +fig, axes = plt.subplots(1, 2, figsize=(12, 6)) + +# Karate Club Graph +layout_karate = karate.layout("fr") +ig.plot( + karate, layout=layout_karate, target=axes[0], vertex_size=30, vertex_color="lightblue", edge_width=1, + vertex_label=[str(v.index) for v in karate.vs], vertex_label_size=10 +) +axes[0].set_title("Karate Club Network") + +# Erdős-Rényi Graph +layout_random = random_graph.layout("fr") +ig.plot( + random_graph, layout=layout_random, target=axes[1], vertex_size=30, vertex_color="lightcoral", edge_width=1, + vertex_label=[str(v.index) for v in random_graph.vs], vertex_label_size=10 +) +axes[1].set_title("Erdős-Rényi Random Graph") +# %% +# Function to compute similarity between partitions def compute_pairwise_similarity(partitions, method): - """Computes pairwise similarity measure between partitions.""" - scores = [] + similarities = [] + for p1, p2 in itertools.combinations(partitions, 2): - scores.append(ig.compare_communities(p1, p2, method=method)) - return scores + similarity = ig.compare_communities(p1, p2, method=method) + similarities.append(similarity) + + return similarities # %% -# Stochastic Community Detection -# Runs Louvain's method iteratively to generate partitions -# Computes similarity metrics: +# We have used, stochastic community detection using the Louvain method, iteratively generating partitions and computing similarity metrics to assess stability. +# The Louvain method is a modularity maximization approach for community detection. +# Since exact modularity maximization is NP-hard, the algorithm employs a greedy heuristic that processes vertices in a random order. +# This randomness leads to variations in the detected communities across different runs, which is why results may differ each time the method is applied. def run_experiment(graph, iterations=50): - """Runs the stochastic method multiple times and collects community partitions.""" partitions = [graph.community_multilevel().membership for _ in range(iterations)] nmi_scores = compute_pairwise_similarity(partitions, method="nmi") vi_scores = compute_pairwise_similarity(partitions, method="vi") ri_scores = compute_pairwise_similarity(partitions, method="rand") return nmi_scores, vi_scores, ri_scores -# %% -# Parameters -n_nodes = 100 -p_random = 0.05 -clusters = 4 -p_intra = 0.3 # High intra-cluster connection probability -p_inter = 0.01 # Low inter-cluster connection probability - -# %% -# Generate graphs -random_graph = generate_random_graph(n_nodes, p_random) -clustered_graph = generate_clustered_graph(n_nodes, clusters, p_intra, p_inter) - # %% # Run experiments +nmi_karate, vi_karate, ri_karate = run_experiment(karate) nmi_random, vi_random, ri_random = run_experiment(random_graph) -nmi_clustered, vi_clustered, ri_clustered = run_experiment(clustered_graph) -# %% -# Lets, plot the histograms +# %% +# Lastly, lets plot probability density histograms to understand the result. fig, axes = plt.subplots(3, 2, figsize=(12, 10)) -measures = [(nmi_random, nmi_clustered, "NMI"), (vi_random, vi_clustered, "VI"), (ri_random, ri_clustered, "RI")] +measures = [ + (nmi_karate, nmi_random, "NMI", 0, 1), # Normalized Mutual Information (0-1, higher = more similar) + (vi_karate, vi_random, "VI", 0, None), # Variation of Information (0+, lower = more similar) + (ri_karate, ri_random, "RI", 0, 1), # Rand Index (0-1, higher = more similar) +] colors = ["red", "blue", "green"] -for i, (random_scores, clustered_scores, measure) in enumerate(measures): - axes[i][0].hist(random_scores, bins=20, alpha=0.7, color=colors[i], edgecolor="black") - axes[i][0].set_title(f"Histogram of {measure} - Random Graph") +for i, (karate_scores, random_scores, measure, lower, upper) in enumerate(measures): + # Karate Club histogram + axes[i][0].hist( + karate_scores, bins=20, alpha=0.7, color=colors[i], edgecolor="black", + density=True # Probability density + ) + axes[i][0].set_title(f"Probability Density of {measure} - Karate Club Network") axes[i][0].set_xlabel(f"{measure} Score") - axes[i][0].set_ylabel("Frequency") - - axes[i][1].hist(clustered_scores, bins=20, alpha=0.7, color=colors[i], edgecolor="black") - axes[i][1].set_title(f"Histogram of {measure} - Clustered Graph") + axes[i][0].set_ylabel("Density") + axes[i][0].set_xlim(lower, upper) # Set axis limits explicitly + + # Erdős-Rényi Graph histogram + axes[i][1].hist( + random_scores, bins=20, alpha=0.7, color=colors[i], edgecolor="black", + density=True + ) + axes[i][1].set_title(f"Probability Density of {measure} - Erdős-Rényi Graph") axes[i][1].set_xlabel(f"{measure} Score") + axes[i][1].set_xlim(lower, upper) # Set axis limits explicitly plt.tight_layout() plt.show() # %% -# The results are plotted as histograms for random vs. clustered graphs, highlighting differences in detected community structures. -#The key reason for the inconsistency in random graphs and higher consistency in structured graphs is due to community structure strength: -#Random Graphs: Lack clear communities, leading to unstable partitions. Stochastic algorithms detect different structures across runs, resulting in low NMI, high VI, and inconsistent RI. -#Structured Graphs: Have well-defined communities, so detected partitions are more stable across multiple runs, leading to high NMI, low VI, and stable RI. +# We have compared the probability density of NMI, VI, and RI for the Karate Club network (structured) and an Erdős-Rényi random graph. +# +# **NMI (Normalized Mutual Information):** +# +# - Karate Club Network: The distribution is concentrated near 1, indicating high similarity across multiple runs, suggesting stable community detection. +# - Erdős-Rényi Graph: The values are more spread out, with lower NMI scores, showing inconsistent partitions due to the lack of clear community structures. +# +# **VI (Variation of Information):** +# +# - Karate Club Network: The values are low and clustered, indicating stable partitioning with minor variations across runs. +# - Erdős-Rényi Graph: The distribution is broader and shifted toward higher VI values, meaning higher partition variability and less consistency. +# +# **RI (Rand Index):** +# +# - Karate Club Network: The RI values are high and concentrated near 1, suggesting consistent clustering results across multiple iterations. +# - Erdős-Rényi Graph: The distribution is more spread out, but with lower RI values, confirming unstable community detection. +# +# **Conclusion** +# +# The Karate Club Network exhibits strong, well-defined community structures, leading to consistent results across runs. +# The Erdős-Rényi Graph, being random, lacks clear communities, causing high variability in detected partitions. \ No newline at end of file From b6b07e98e16669b3def9cbb8b103561b93632662 Mon Sep 17 00:00:00 2001 From: Sanat Kumar Gupta <123228827+SKG24@users.noreply.github.com> Date: Wed, 2 Apr 2025 06:33:08 +0530 Subject: [PATCH 005/276] Delete doc/source/sg_execution_times.rst --- doc/source/sg_execution_times.rst | 112 ------------------------------ 1 file changed, 112 deletions(-) delete mode 100644 doc/source/sg_execution_times.rst diff --git a/doc/source/sg_execution_times.rst b/doc/source/sg_execution_times.rst deleted file mode 100644 index 65c6c041f..000000000 --- a/doc/source/sg_execution_times.rst +++ /dev/null @@ -1,112 +0,0 @@ - -:orphan: - -.. _sphx_glr_sg_execution_times: - - -Computation times -================= -**01:51.199** total execution time for 26 files **from all galleries**: - -.. container:: - - .. raw:: html - - - - - - - - .. list-table:: - :header-rows: 1 - :class: table table-striped sg-datatable - - * - Example - - Time - - Mem (MB) - * - :ref:`sphx_glr_tutorials_visualize_cliques.py` (``../examples_sphinx-gallery/visualize_cliques.py``) - - 00:39.554 - - 0.0 - * - :ref:`sphx_glr_tutorials_visual_style.py` (``../examples_sphinx-gallery/visual_style.py``) - - 00:11.628 - - 0.0 - * - :ref:`sphx_glr_tutorials_ring_animation.py` (``../examples_sphinx-gallery/ring_animation.py``) - - 00:09.870 - - 0.0 - * - :ref:`sphx_glr_tutorials_delaunay-triangulation.py` (``../examples_sphinx-gallery/delaunay-triangulation.py``) - - 00:09.261 - - 0.0 - * - :ref:`sphx_glr_tutorials_betweenness.py` (``../examples_sphinx-gallery/betweenness.py``) - - 00:06.259 - - 0.0 - * - :ref:`sphx_glr_tutorials_configuration.py` (``../examples_sphinx-gallery/configuration.py``) - - 00:05.379 - - 0.0 - * - :ref:`sphx_glr_tutorials_cluster_contraction.py` (``../examples_sphinx-gallery/cluster_contraction.py``) - - 00:04.307 - - 0.0 - * - :ref:`sphx_glr_tutorials_erdos_renyi.py` (``../examples_sphinx-gallery/erdos_renyi.py``) - - 00:03.508 - - 0.0 - * - :ref:`sphx_glr_tutorials_bridges.py` (``../examples_sphinx-gallery/bridges.py``) - - 00:02.530 - - 0.0 - * - :ref:`sphx_glr_tutorials_complement.py` (``../examples_sphinx-gallery/complement.py``) - - 00:02.393 - - 0.0 - * - :ref:`sphx_glr_tutorials_visualize_communities.py` (``../examples_sphinx-gallery/visualize_communities.py``) - - 00:02.157 - - 0.0 - * - :ref:`sphx_glr_tutorials_stochastic_variability.py` (``../examples_sphinx-gallery/stochastic_variability.py``) - - 00:01.960 - - 0.0 - * - :ref:`sphx_glr_tutorials_online_user_actions.py` (``../examples_sphinx-gallery/online_user_actions.py``) - - 00:01.750 - - 0.0 - * - :ref:`sphx_glr_tutorials_connected_components.py` (``../examples_sphinx-gallery/connected_components.py``) - - 00:01.728 - - 0.0 - * - :ref:`sphx_glr_tutorials_isomorphism.py` (``../examples_sphinx-gallery/isomorphism.py``) - - 00:01.376 - - 0.0 - * - :ref:`sphx_glr_tutorials_minimum_spanning_trees.py` (``../examples_sphinx-gallery/minimum_spanning_trees.py``) - - 00:01.135 - - 0.0 - * - :ref:`sphx_glr_tutorials_spanning_trees.py` (``../examples_sphinx-gallery/spanning_trees.py``) - - 00:01.120 - - 0.0 - * - :ref:`sphx_glr_tutorials_generate_dag.py` (``../examples_sphinx-gallery/generate_dag.py``) - - 00:00.939 - - 0.0 - * - :ref:`sphx_glr_tutorials_quickstart.py` (``../examples_sphinx-gallery/quickstart.py``) - - 00:00.902 - - 0.0 - * - :ref:`sphx_glr_tutorials_simplify.py` (``../examples_sphinx-gallery/simplify.py``) - - 00:00.840 - - 0.0 - * - :ref:`sphx_glr_tutorials_bipartite_matching_maxflow.py` (``../examples_sphinx-gallery/bipartite_matching_maxflow.py``) - - 00:00.674 - - 0.0 - * - :ref:`sphx_glr_tutorials_shortest_path_visualisation.py` (``../examples_sphinx-gallery/shortest_path_visualisation.py``) - - 00:00.609 - - 0.0 - * - :ref:`sphx_glr_tutorials_articulation_points.py` (``../examples_sphinx-gallery/articulation_points.py``) - - 00:00.396 - - 0.0 - * - :ref:`sphx_glr_tutorials_bipartite_matching.py` (``../examples_sphinx-gallery/bipartite_matching.py``) - - 00:00.370 - - 0.0 - * - :ref:`sphx_glr_tutorials_topological_sort.py` (``../examples_sphinx-gallery/topological_sort.py``) - - 00:00.319 - - 0.0 - * - :ref:`sphx_glr_tutorials_maxflow.py` (``../examples_sphinx-gallery/maxflow.py``) - - 00:00.234 - - 0.0 From 0c02648161a472477e6f2dd47e20f303809669c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 13:11:32 +0000 Subject: [PATCH 006/276] build(deps): bump pypa/cibuildwheel from 2.16.1 to 2.16.2 Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.16.1 to 2.16.2. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.16.1...v2.16.2) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d5fb40f1..59ffdea0b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: python-version: '3.8' - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.16.1 + uses: pypa/cibuildwheel@v2.16.2 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -38,7 +38,7 @@ jobs: CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.16.1 + uses: pypa/cibuildwheel@v2.16.2 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" @@ -63,7 +63,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.16.1 + uses: pypa/cibuildwheel@v2.16.2 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -88,7 +88,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.16.1 + uses: pypa/cibuildwheel@v2.16.2 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -155,7 +155,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.16.1 + uses: pypa/cibuildwheel@v2.16.2 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "python setup.py build_c_core" @@ -255,7 +255,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.16.1 + uses: pypa/cibuildwheel@v2.16.2 env: CIBW_BEFORE_BUILD: "python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From 3f60d008e13db3d722d7d7a3057a83c5e2fcba11 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 12 Oct 2023 15:28:33 +0200 Subject: [PATCH 007/276] chore: bumped version to 0.11.0 --- CHANGELOG.md | 5 +++-- src/igraph/version.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f37e0fcea..678b963ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # igraph Python interface changelog -## [develop] +## [0.11.0] - 2023-10-12 ### Added @@ -582,7 +582,8 @@ Please refer to the commit logs at https://github.com/igraph/python-igraph for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. -[develop]: https://github.com/igraph/python-igraph/compare/0.10.8...develop +[main]: https://github.com/igraph/python-igraph/compare/0.11.0...main +[0.11.0]: https://github.com/igraph/python-igraph/compare/0.10.8...0.11.0 [0.10.8]: https://github.com/igraph/python-igraph/compare/0.10.7...0.10.8 [0.10.7]: https://github.com/igraph/python-igraph/compare/0.10.6...0.10.7 [0.10.6]: https://github.com/igraph/python-igraph/compare/0.10.5...0.10.6 diff --git a/src/igraph/version.py b/src/igraph/version.py index 655891d8a..c1a3ca82e 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 10, 8) +__version_info__ = (0, 11, 0) __version__ = ".".join("{0}".format(x) for x in __version_info__) From 86dbb6880e3bd1783482dd717940f2e6e5b94acf Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 12 Oct 2023 15:56:22 +0200 Subject: [PATCH 008/276] doc: add switch to doc building script to ensure that the docs build from scratch --- scripts/mkdoc.sh | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index 4ff41d23a..3a65cc264 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -4,15 +4,22 @@ # # Usage: ./mkdoc.sh (makes API and tutorial docs) # ./mkdoc.sh -d (makes a Dash docset based on standalone docs, requires doc2dash) - +# +# Add -c to ensure that the documentation is built from scratch and no cached +# assets from previous builds are used. +# # Make sure we bail out on build errors set -e DOC2DASH=0 LINKCHECK=0 +CLEAN=0 -while getopts ":sjdl" OPTION; do +while getopts ":scjdl" OPTION; do case $OPTION in + c) + CLEAN=1 + ;; d) DOC2DASH=1 ;; @@ -66,10 +73,13 @@ rm -f dist/*.whl && .venv/bin/pip wheel -q -w dist . && .venv/bin/pip install -q echo "Patching modularized Graph methods" .venv/bin/python3 ${SCRIPTS_FOLDER}/patch_modularized_graph_methods.py - echo "Clean previous docs" rm -rf "${DOC_HTML_FOLDER}" +if [ "x$CLEAN" = "x1" ]; then + # This is generated by sphinx-gallery + rm -rf "${DOC_SOURCE_FOLDER}/tutorials" +fi if [ "x$LINKCHECK" = "x1" ]; then echo "Check for broken links" From 35ab0741fede81606855851b08bef4f4a7b5f2a6 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 12 Oct 2023 17:27:47 +0200 Subject: [PATCH 009/276] fix: fix plotting of null graphs with the Matplotlib backend --- src/igraph/drawing/matplotlib/graph.py | 2 +- .../cairo/baseline_images/graph_null.png | Bin 0 -> 2354 bytes tests/drawing/cairo/test_graph.py | 5 + .../baseline_images/test_graph/graph_null.png | Bin 0 -> 3326 bytes tests/drawing/matplotlib/test_graph.py | 8 + .../plotly/baseline_images/graph_null.json | 829 ++++++++++++++++++ tests/drawing/plotly/test_graph.py | 7 + tests/drawing/plotly/utils.py | 4 + 8 files changed, 854 insertions(+), 1 deletion(-) create mode 100644 tests/drawing/cairo/baseline_images/graph_null.png create mode 100644 tests/drawing/matplotlib/baseline_images/test_graph/graph_null.png create mode 100644 tests/drawing/plotly/baseline_images/graph_null.json diff --git a/src/igraph/drawing/matplotlib/graph.py b/src/igraph/drawing/matplotlib/graph.py index 139f122c6..73c5e6ab4 100644 --- a/src/igraph/drawing/matplotlib/graph.py +++ b/src/igraph/drawing/matplotlib/graph.py @@ -532,7 +532,7 @@ def _draw_vertices(self): art = VertexCollection( patches, - offsets=offsets, + offsets=offsets if offsets else None, offset_transform=self.axes.transData, match_original=True, transform=Affine2D(), diff --git a/tests/drawing/cairo/baseline_images/graph_null.png b/tests/drawing/cairo/baseline_images/graph_null.png new file mode 100644 index 0000000000000000000000000000000000000000..5cd2e6483a19e4188c8470b3f38165844681d00d GIT binary patch literal 2354 zcmeAS@N?(olHy`uVBq!ia0y~yV2S`?4kn<8Aq#&ukYY>nc6VX;4}uH!E}zW6!13JE z#WAE}&fBYoj0_4q2MiAEf3BG9F3kG3D*YLQ!`0h-3=O_+91gPtnI@#QFf5T&WXK%l xj)ul)Vi?T}qea1JNjO>(j+TT&pd|dpxIm13?So5OT!D=s22WQ%mvv4FO#o+eb=?2} literal 0 HcmV?d00001 diff --git a/tests/drawing/cairo/test_graph.py b/tests/drawing/cairo/test_graph.py index 2e5dd5832..7f99eae77 100644 --- a/tests/drawing/cairo/test_graph.py +++ b/tests/drawing/cairo/test_graph.py @@ -85,6 +85,11 @@ def test_graph_with_curved_edges(self): backend="cairo", ) + @image_comparison(baseline_images=["graph_null"]) + def test_null_graph(self): + g = Graph() + plot(g, backend="cairo", target=result_image_folder / "graph_null.png") + class ClusteringTestRunner(unittest.TestCase): @classmethod diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/graph_null.png b/tests/drawing/matplotlib/baseline_images/test_graph/graph_null.png new file mode 100644 index 0000000000000000000000000000000000000000..14b94a347e483ef389edca5918de3c12145e9a07 GIT binary patch literal 3326 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i1B%QlYbpRzmL;wcCBgY=CFO}lsSLh} zB?US8B{`W%3T3H9#hLke#(EZd2098EB_##LR{Hw6a0Pn#Md|vc8@H|o8p2uN5n0T@ zz@G@hj4SMyXEHGGWP7?ehE&XXd)<(cL4oIh!GZnHA1;zn;rdl}{)`R-L&JeeS)kTG zhZ}&TBr_8OLkbTE1H%ME1qKF(W21tj0Wz8xMl-@_$uL?fj@AgHwc%)OI9eNy)`p|C dA@ Date: Thu, 12 Oct 2023 17:32:05 +0200 Subject: [PATCH 010/276] chore: bumped version to 0.11.1 --- CHANGELOG.md | 9 ++++++++- setup.py | 2 +- src/igraph/version.py | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 678b963ed..75d30f177 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # igraph Python interface changelog +## [0.11.1] - 2023-10-12 + +### Fixed + +- Fixed plotting of null graphs with the Matplotlib backend. + ## [0.11.0] - 2023-10-12 ### Added @@ -582,7 +588,8 @@ Please refer to the commit logs at https://github.com/igraph/python-igraph for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. -[main]: https://github.com/igraph/python-igraph/compare/0.11.0...main +[main]: https://github.com/igraph/python-igraph/compare/0.11.1...main +[0.11.1]: https://github.com/igraph/python-igraph/compare/0.11.0...0.11.1 [0.11.0]: https://github.com/igraph/python-igraph/compare/0.10.8...0.11.0 [0.10.8]: https://github.com/igraph/python-igraph/compare/0.10.7...0.10.8 [0.10.7]: https://github.com/igraph/python-igraph/compare/0.10.6...0.10.7 diff --git a/setup.py b/setup.py index 5c57341f5..11d4cd967 100644 --- a/setup.py +++ b/setup.py @@ -1000,7 +1000,7 @@ def get_tag(self): "cairo": ["cairocffi>=1.2.0"], # Dependencies needed for plotting with Matplotlib "matplotlib": [ - "matplotlib>=3.5.0,<3.6.0; platform_python_implementation != 'PyPy'" + "matplotlib>=3.6.0; platform_python_implementation != 'PyPy'" ], # Dependencies needed for plotting with Plotly "plotly": ["plotly>=5.3.0"], diff --git a/src/igraph/version.py b/src/igraph/version.py index c1a3ca82e..679427be8 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 11, 0) +__version_info__ = (0, 11, 1) __version__ = ".".join("{0}".format(x) for x in __version_info__) From b70f1d2c96b55d8753714f5e0f51221391dceed0 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 12 Oct 2023 19:24:21 +0200 Subject: [PATCH 011/276] ci: increase test tolerance for Matplotlib tests to make sure they pass on aarch64 --- tests/drawing/matplotlib/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/drawing/matplotlib/utils.py b/tests/drawing/matplotlib/utils.py index b7ec998b4..36bd50f1f 100644 --- a/tests/drawing/matplotlib/utils.py +++ b/tests/drawing/matplotlib/utils.py @@ -71,7 +71,7 @@ def wrapper(*args, **kwargs): def image_comparison( baseline_images, - tol=0.01, + tol=0.025, remove_text=False, savefig_kwarg=None, # Default of mpl_test_settings fixture and cleanup too. From 2c1fde877e6e4718d993beb852b4e6ad04d23140 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 12 Oct 2023 19:24:54 +0200 Subject: [PATCH 012/276] chore: bumped version to 0.11.2 --- CHANGELOG.md | 6 +++--- src/igraph/version.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75d30f177..b31b3d20c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # igraph Python interface changelog -## [0.11.1] - 2023-10-12 +## [0.11.2] - 2023-10-12 ### Fixed @@ -588,8 +588,8 @@ Please refer to the commit logs at https://github.com/igraph/python-igraph for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. -[main]: https://github.com/igraph/python-igraph/compare/0.11.1...main -[0.11.1]: https://github.com/igraph/python-igraph/compare/0.11.0...0.11.1 +[main]: https://github.com/igraph/python-igraph/compare/0.11.2...main +[0.11.2]: https://github.com/igraph/python-igraph/compare/0.11.0...0.11.2 [0.11.0]: https://github.com/igraph/python-igraph/compare/0.10.8...0.11.0 [0.10.8]: https://github.com/igraph/python-igraph/compare/0.10.7...0.10.8 [0.10.7]: https://github.com/igraph/python-igraph/compare/0.10.6...0.10.7 diff --git a/src/igraph/version.py b/src/igraph/version.py index 679427be8..e9509d902 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 11, 1) +__version_info__ = (0, 11, 2) __version__ = ".".join("{0}".format(x) for x in __version_info__) From cc3dedb1f467912b8ffe869e97209a01edb35a92 Mon Sep 17 00:00:00 2001 From: Gwyn Ciesla Date: Tue, 24 Oct 2023 11:17:04 -0500 Subject: [PATCH 013/276] Include ctype.h, fixes build on Python 3.13 --- src/_igraph/convert.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 4283ff8ed..c05a4eba2 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -23,6 +23,7 @@ /************************ Conversion functions *************************/ #include +#include #include "attributes.h" #include "convert.h" #include "edgeseqobject.h" From fe4381e7ff1314b0e1227f2eb0cb32710e19bd1a Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 27 Oct 2023 18:55:39 +1100 Subject: [PATCH 014/276] Tutorial on DAG (#724) --- doc/examples_sphinx-gallery/generate_dag.py | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 doc/examples_sphinx-gallery/generate_dag.py diff --git a/doc/examples_sphinx-gallery/generate_dag.py b/doc/examples_sphinx-gallery/generate_dag.py new file mode 100644 index 000000000..7fb85b32d --- /dev/null +++ b/doc/examples_sphinx-gallery/generate_dag.py @@ -0,0 +1,45 @@ +""" + +.. _tutorials-dag: + +====================== +Directed Acyclic Graph +====================== + +This example demonstrates how to create a random directed acyclic graph (DAG), which is useful in a number of contexts including for Git commit history. +""" +import igraph as ig +import matplotlib.pyplot as plt +import random + + +# %% +# First, we set a random seed for reproducibility +random.seed(0) + +# %% +# First, we generate a random undirected graph without loops +g = ig.Graph.Erdos_Renyi(n=15, p=0.3, directed=False, loops=False) + +# %% +# Then we convert it to a DAG *in place* +g.to_directed(mode="acyclic") + +# %% +# We can print out a summary of the DAG +ig.summary(g) + + +# %% +# Finally, we can plot the graph using the Sugiyama layout from :meth:`igraph.Graph.layout_sugiyama`: +fig, ax = plt.subplots() +ig.plot( + g, + target=ax, + layout="sugiyama", + vertex_size=15, + vertex_color="grey", + edge_color="#222", + edge_width=1, +) +plt.show() From 487d56f0a159b60450d321f407e121d0027dd436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horv=C3=A1t?= Date: Fri, 27 Oct 2023 18:52:52 +0200 Subject: [PATCH 015/276] Update random DAG generation tutorial and mention that the sampling is uniform --- doc/examples_sphinx-gallery/generate_dag.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/examples_sphinx-gallery/generate_dag.py b/doc/examples_sphinx-gallery/generate_dag.py index 7fb85b32d..addbc661c 100644 --- a/doc/examples_sphinx-gallery/generate_dag.py +++ b/doc/examples_sphinx-gallery/generate_dag.py @@ -14,19 +14,19 @@ # %% -# First, we set a random seed for reproducibility +# First, we set a random seed for reproducibility. random.seed(0) # %% -# First, we generate a random undirected graph without loops -g = ig.Graph.Erdos_Renyi(n=15, p=0.3, directed=False, loops=False) +# First, we generate a random undirected graph with a fixed number of edges, without loops. +g = ig.Graph.Erdos_Renyi(n=15, m=30, directed=False, loops=False) # %% -# Then we convert it to a DAG *in place* +# Then we convert it to a DAG *in place*. This method samples DAGs with a given number of edges and vertices uniformly. g.to_directed(mode="acyclic") # %% -# We can print out a summary of the DAG +# We can print out a summary of the DAG. ig.summary(g) From a9f4553738a9cc2fbcc1c5aae50869515a48005d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 2 Nov 2023 13:12:48 +0100 Subject: [PATCH 016/276] feat: added Graph.__invalidate_cache(), closes #729 --- src/_igraph/graphobject.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 562d39e82..842cd84e3 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13420,6 +13420,12 @@ PyObject *igraphmodule_Graph___graph_as_capsule__(igraphmodule_GraphObject * return PyCapsule_New((void *)&self->g, 0, 0); } +PyObject *igraphmodule_Graph___invalidate_cache__(igraphmodule_GraphObject *self) +{ + igraph_invalidate_cache(&self->g); + Py_RETURN_NONE; +} + /** \ingroup python_interface_internal * \brief Returns the pointer of the encapsulated igraph graph as an ordinary * Python integer. This allows us to use igraph graphs with the Python ctypes @@ -17962,6 +17968,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /**********************/ /* INTERNAL FUNCTIONS */ /**********************/ + {"__graph_as_capsule", (PyCFunction) igraphmodule_Graph___graph_as_capsule__, METH_VARARGS | METH_KEYWORDS, @@ -17973,6 +17980,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "users, it is useful only in the case when the underlying igraph object\n" "must be passed to other C code through Python.\n\n"}, + {"__invalidate_cache", + (PyCFunction) igraphmodule_Graph___invalidate_cache__, + METH_NOARGS, + "__invalidate_cache()\n--\n\n" + "Invalidates the internal cache of the low-level C graph object that\n" + "the Python object wraps. This function should not be used directly\n" + "by igraph users, but it may be useful for benchmarking or debugging\n" + "purposes.", + }, + {"_raw_pointer", (PyCFunction) igraphmodule_Graph__raw_pointer, METH_NOARGS, From 90f6235b22a3e8c71718d96942c930ba9d253c6c Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 2 Nov 2023 13:13:36 +0100 Subject: [PATCH 017/276] chore: updated vendored igraph --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index c83046f4b..771a449d0 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit c83046f4b5f48425d07c6b9a18912f184d9edea5 +Subproject commit 771a449d09fcb4926d123975f35d2ba5797868da From 065e8f2cd8193a99d0eab32f42db1ba3b9fbf359 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 2 Nov 2023 13:22:15 +0100 Subject: [PATCH 018/276] fix: remove loops=... argument for is_bigraphical(), fixes #730 --- src/_igraph/igraphmodule.c | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 626a04396..cff5df44e 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -536,7 +536,7 @@ static PyObject* igraphmodule_i_is_graphical_or_bigraphical( PyObject *self, PyObject *args, PyObject *kwds, igraph_bool_t is_bigraphical ) { static char* kwlist_graphical[] = { "out_deg", "in_deg", "loops", "multiple", NULL }; - static char* kwlist_bigraphical[] = { "degrees1", "degrees2", "loops", "multiple", NULL }; + static char* kwlist_bigraphical[] = { "degrees1", "degrees2", "multiple", NULL }; PyObject *out_deg_o = 0, *in_deg_o = 0; PyObject *loops = Py_False, *multiple = Py_False; igraph_vector_int_t out_deg, in_deg; @@ -544,13 +544,21 @@ static PyObject* igraphmodule_i_is_graphical_or_bigraphical( int allowed_edge_types; igraph_error_t retval; - if (!PyArg_ParseTupleAndKeywords( - args, kwds, - is_bigraphical ? "OO|OO" : "O|OOO", - is_bigraphical ? kwlist_bigraphical : kwlist_graphical, - &out_deg_o, &in_deg_o, &loops, &multiple - )) - return NULL; + if (is_bigraphical) { + if (!PyArg_ParseTupleAndKeywords( + args, kwds, "OO|O", kwlist_bigraphical, + &out_deg_o, &in_deg_o, &multiple + )) { + return NULL; + } + } else { + if (!PyArg_ParseTupleAndKeywords( + args, kwds, "O|OOO", kwlist_graphical, + &out_deg_o, &in_deg_o, &loops, &multiple + )) { + return NULL; + } + } is_directed = (in_deg_o != 0 && in_deg_o != Py_None) || is_bigraphical; @@ -820,17 +828,16 @@ static PyMethodDef igraphmodule_methods[] = }, {"is_bigraphical", (PyCFunction)igraphmodule_is_bigraphical, METH_VARARGS | METH_KEYWORDS, - "is_bigraphical(degrees1, degrees2, loops=False, multiple=False)\n--\n\n" + "is_bigraphical(degrees1, degrees2, multiple=False)\n--\n\n" "Returns whether two sequences of integers can be the degree sequences of a\n" "bipartite graph.\n\n" - "The bipartite graph may or may not have multiple and loop edges, depending\n" + "The bipartite graph may or may not have multiple edges, depending\n" "on the allowed edge types in the remaining arguments.\n\n" "@param degrees1: the list of degrees in the first partition.\n" "@param degrees2: the list of degrees in the second partition.\n" - "@param loops: whether loop edges are allowed.\n" "@param multiple: whether multiple edges are allowed.\n" "@return: C{True} if there exists some bipartite graph that can realize the\n" - " given degree sequences with the given edge types, C{False} otherwise.\n" + " given degree sequences with or without multiple edges, C{False} otherwise.\n" }, {"is_graphical", (PyCFunction)igraphmodule_is_graphical, METH_VARARGS | METH_KEYWORDS, From 054fc9c4379ec40808c16d17732f66e4fa641ddc Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 2 Nov 2023 13:23:21 +0100 Subject: [PATCH 019/276] chore: updated changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b31b3d20c..a0fbeef3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # igraph Python interface changelog +## [main] + +### Added + +- Added `Graph.__invalidate_cache()` for debugging and benchmarking purposes. + +### Fixed + +- Removed incorrectly added `loops=...` argument of `Graph.is_bigraphical()`. + ## [0.11.2] - 2023-10-12 ### Fixed From 366092165c309d8ad708b642d40d7b75febb1a2f Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 4 Nov 2023 21:41:29 +1100 Subject: [PATCH 020/276] Bugfix for #731 --- src/igraph/drawing/matplotlib/edge.py | 5 +++++ ...ultigraph_with_curved_edges_undirected.png | Bin 0 -> 29139 bytes tests/drawing/matplotlib/test_graph.py | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 tests/drawing/matplotlib/baseline_images/test_graph/multigraph_with_curved_edges_undirected.png diff --git a/src/igraph/drawing/matplotlib/edge.py b/src/igraph/drawing/matplotlib/edge.py index 8e8adb9a2..c4b21cab1 100644 --- a/src/igraph/drawing/matplotlib/edge.py +++ b/src/igraph/drawing/matplotlib/edge.py @@ -410,6 +410,11 @@ def _compute_path_undirected(self, coordst, sizes, trans_inv, curved): voff *= sizes[1] / 2 path["vertices"].append(coordst[1] - voff) + # This is a dirty trick to make the facecolor work + # without making a separate Patch, which would be a little messy + path["codes"].extend(["CURVE4"] * 3) + path["vertices"].extend(path["vertices"][-2::-1]) + path = mpl.path.Path( path["vertices"], codes=[getattr(mpl.path.Path, x) for x in path["codes"]], diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/multigraph_with_curved_edges_undirected.png b/tests/drawing/matplotlib/baseline_images/test_graph/multigraph_with_curved_edges_undirected.png new file mode 100644 index 0000000000000000000000000000000000000000..2a5f6eed840ebc6d61ec5929f2b84337f3b0f89a GIT binary patch literal 29139 zcmeFZ_dnKu_&$8uMNwoVD_z1xb|FF~xsW81$V@1kjO-*MBbO~BA<>YLJ&R;yR`%X2 zd#~^L^#0tR`|-FR_n+|n!K1jw>-l<)^E}SuIL=pqn(FnVl+2VE4CW|aQC=N`A>qJa zi0>XEhrh%*KC^*;Bpt5Zb+~Q)(80;j&IEJA(80#S+QGuyh{Msu&feVGN>o5xK!l&e z%)!CN{-U6u<^R4xz}n7KkoW$WCw$3a8%1q<42H%K{SdvEO*hA2Y~}Fsmo=Q@=lYyt zG{&m-7UxfN3viU-cnma7(9@^9VY;Q2aZ7BC!AJ0%nF>G8H8P!#w^HvKXAwoxtEp+6 z2;lO6+ljemt{iQmefQnYp4@mXZaP1Tfu3q-#&4Iba>I31y8`@ zmAnm#NfhBfnHTvv;2(ne4`NPuf!E~!{`!AsdjdU@;ck6^td0+Sb z;B%Q;V)WR<#&~?f#V6|QcL))84n9ylV*7M`wyvi=T~w5LtYtuA&r_~I%|9hK=Vzjb zuN=p#z!*ZR-8b0TxJXI+@s=Wc)3e0xFC`vw+H{8+he{0lkNSpVBc)WYL}MfC3$u;Q zPf^?33IB*E&O7XPQryvkppGZh^JRG{5|sGG#iJFDGTU6mext8paXlp@&GPV9@slR9 zuv8p2{vU>*L4WS<7cWMAx3E7SwCJe)%)4^9Oyh0If4vsJ>6B-roo9BOEbNe9w1EZp z8xC5-#o-zjhnCAGtmiWBe7swg+bq+NW76O+&nafr8xww0CU$MECtWHI8|kV?6qb4- zPTEDpsn0{ox_tM-ldGrik^0Dc&!^nt^v1YvuZ;!rWsQFMPOa>uf#D20^g%1@VND?Y zN8KUMPdp7mILB)@)n{Qlm0MI%w5RQO0mWmx6$>nxXFGMqE(FO~cicRs~sdE8e| z9IkCs=(bZf`<+I1G+)Qsq%9Tye8hI`knbU)24mq13Mo57lPN{BOAS-)={Jj0l+=q% z(D3|gM|9m+|LiO$?T@}!^RH^Cj?)aQF-e+>opjY=VGCOkYf1R8;;tlapKmh zeZ^01=MethzImy}mr=J;uX0fFIh9wRhg;TQ?YyZ4PqkDl&s9I_v3IYn?0g0B-DFh$ z*tE*k4*ljQ9rtS!DorZvbct+)7vwoAUkaAV&-a%p?|!ACr1ZGM@7Uke+$^{zN#czO zJ|+Iny#1Yu+nP4DPWH2anjML$Q-_2KT*3x!+SSz+zf&t~XC>X_BllHbLV&uLY=@-qU-3*K922-2S4K{@m!dkseQPS+ zVY0=$wQ_-zg`G_GD5b?@t=Kg~*Q zAv$Gn6qQ!o#Vk5)qP0}x&OcW0AwPPE+|&K?&z&8&HM7=dDIOc%0gDpJoNm8bD;4wi ztt4?LZEp(DZq`)9@BhAHU_ID+oqcR)xtUu~aNbl)z;W-I zdr!V)O%pw(uUU8Al;ED7-tWka3s${~AEM4p<+z9n))-Y372`=ajD>#A1|2EaFE9P6 z&n+(Q6g{vXNYD4PuI@B*FvFCz)Q3Btx|=EgHf4#5h&ZpbUN2hsMx}iy@7%BpmDk@P zq4c%+=CbvEompLiMy$3RmN&r8-rl@5k2iX3LNqhR_$DR!W~sL5MOGKA?9Rf~fO3zr zr>nFT8v85JvoDUGqNSuCo8PRk+*+AhnQisy&t`&%^RtK&DzaNz|JVR+SUUcmLsC zf};@}1#H|U*9+HKpPXPPp=cxtmBj|v z?;nc{+FG57ejOS0balFBr&-1rwvY5mB}s3&?HMUTjrZ1*8N1dRMp3gCj_y43@HPwB zKSo()Yv^aI%XQH?es!i}eyP5Wz@_X(*q!fKsP&9mnT~`_f+7_5>CC5sjqmJxm5h}# zH7wf_HZorzrbOt~@lb7UZkEFS9~~XVwB(uJ4w2p4pxktS@ZiCMgpHMOt;gZnhBZ%$ zV#m3AYeU$KXG@g+ST4Mt9@~K1^3h~Duh>QBY^t9s23eQZ-rH2Lc7g+xR|2*+;`aPj*3u4)7g3-nFvz4kPw2~QYq z%4}k9yuM(fE1_a5Af82*c$j=j*4ZYgHrhXy9tz!Uu1u?}kwV z`l=&8f*US=4m5pT*Cgk2_nvC?Zm!jYln5)<}=mY`MHuH!qE*l#mg6-`m+ zbSPQ1Dm>huu1sDIWxHeskv`|GvU-s*ET&OljgcFiy$7bIqNYtOMzSt4@-Udmz7lcn zcpKu_*q$W+Y?tMSPuC}_q^v7`nPfAOcwO?e>)$7G%5`7bpJ_5wci;5F6=HYE{!q5K6T;T%e(6XjLeKj z$R(G4o|%FRvPwDb{Nm14R=;KnBjc?9^~(efFN`eLqATahJekksVuz_KFnh{J&B{iuuQVR>ir;ZD5M-sZ||S8ijX>gQ2^g%p)o5#L&N*C3Mt_v>P` z$+vPJ#sXgvgWaj`wpi2LFS{}516iBqI-3i2=nG%mhf%-^_T!C)=&oLi&zsKA zm&})=yD|=ue8GJFQL4-vY@{$G(YXhkOJk)wOAVz<^?ak_;|xy)&z$jrdw;tA?*{CP zwg|PzE(iy%rM>O> z@Pve46zpybx$BTPl+pUZeejK5CDL3RotRkuamsq3O<6G8d2u+YY#`${H(cx2kRSWM zbFKs&3;LeNyIZShKxkke?|sc|afLIM^0?#k%2d1G?pjat;UT4$&eBa2NuEukb>Xq+ zA2Yy}_*Oh%0;jrixw#V@XyI7hysd=Cv7s;IVhdgFGEEADfqm4@1t{j*Z~gb9_Ap~H zgv>PV)I6$Fu%su(;fgn*?4Afn52fA?G?q5eF4huu7EUsH&zjULN4Q1(+$ zAfJE!PWI`^%WA;50SQ4EofTg@V9WOrVn zV^BrGzR(`Jc)_x}CH*FQ@spW5sXF9NAE%!TY%KD}$R+`-yj@^*Kjh-tUGk&M^69Dx zgw;=qnQtL*)PI5y_q;;ahKRlC_4OH>nA@DknK7@uJFcFV*;p((=@fkO>Oi@>lS>Gf za?~rh+n|%89v3nSton)r@MQ?E?ula~vk`V=xY1c?Yn;69$RF1N0pwnRRawI#AhO|{ z(^ogFHtncsd?y)#2}B=B^bYxH(+1ddR#7Z37>BkdU-cXHyh_L1 zNvBAdIZbMzeTJ4DBaZ3v#u(Rzm<(2WSFV}dFl|n}0h^WGb7!%+scgVwn=9&k-=FWp zse$j_yy3{USb>Gy%U!3D*)Tx+ZlWoHu0bJ`T{iKu4|yvE6A{Z=xbQ6lW+DC)!>^go z$6Ro~c)!>IzvRT3f&1d6h;w2P)N3?km9iUrccYZ$<*jY4kYJ){B4c8rMCakdD3<@u zxUCISnmqF8_xsB*8z|hl&X+lfj3}G00*pjEwlP-Jb8WB@ro1KB^yarAp@~FSfvdx| z|A?Pe*p=(PfvA@LWO|{hwo0Q+kBq!F*IjYPB(Gk1R^C@6UTW>OFfiIzT+9k5)~uQ=P9sB^kHLALGQbl%^orGn6a zq5MG;R~y25|G1*&J!!Uylu#KXboygO{hrMHU4-K$!YYx>G*LudSoZKn)WoP7OvC8& zNDVFsDUp2Z1D>)zp?XI?oO8UmH z_m8|(OjXp9b-YWSM@`D)IpD4fVW=*UCxeowshJJ0RDA6vL+eErfjgUCbFz&d>;cF8 z70$7@37Yj53d^Uk+-h7RYN0iwr_MENefvN@T*LQZ3_1l?X%7$A{v}=V%=!H}^;`bv zMD%n;_BoyW2on~(<|{IJ^&<5wy-Rm$Myi#BE>OH5cz;X#Isq5C70evA+~c>aa^x;+ z=3z}qOIL(Fr28m?KHdpVYlv?D`GKQN@E%0!k^-}; zNkDgRnMPEpN$-}ebjBxCW{W3w5vxfAm`g*T+DJI@=`=5!>#VR* zHdB)O7VoJ$S>pkBgKm48^t!#dE}@bD0B+57{#J#@uEw<}+(%gE*0J>p)AoV^k#n>| zUiy-boHkZMin~98%{?oLNPR9h$?m%~$!uFZySyL5o3R0#s?7>OOs`$TIk$x>iU4;% z7P~`K6?<`b!P3gsZ*39(@s<#xVmftbf%uE(_L_>zs^-0Xi`YbKxlAtr8Vb#H zH%D{Hs3xh71lCSbl6nyv&| zk}{nZFHxMB`8WULXF35Av~S#3%rryy&Iw5Y@{sL-`}*+!d9OL;1wANqS?qqEmX-3T zdPW9#;59mPjg2bZ1=cq^KHpC`!WZccc?WXH-7f}20BvUTd#oI$+I*p;-=FO)vY)V9 z{w1Dk_?;XB0fdD7$eWukF0$ zg8`gyWe&&VsDL^$S($9**DICC&T^XXKf)yB1;_M^Rd z)4KD^>F<6t6J8|LiuM=rQ&4bfJc2kBK*zmQNkW75s55JMLj<+z2>hmk-mzB4on$i- zES_W4cM)?X`55q1!oV1A8$WR+j@I@cQ@}g%(%< zv9sYCVIS{iKWk07z6?p5d+o`5e*!QV+pp9!2)PjMbH*B?MPWUGPKoI3fGm#i)9Ume zbP%p-sHs&J+K$ACm?#n9VCWS zI3$5qKY~v7y07++$knJm+;9FqX|^dygqjQ1VFUfxrT3?h@ydP{uaC9%faoi+c$X>?9UWcE>eO4+V_lKZ*^zve z0vHel*dA1@rvl_-0I5?k3Os-q@cV;SD4@SqI3OCeQ5q@?$1(ze(nE!(@ctHKR{0t; z(HkqC>L};TsOsY|az`;CwK6>Ob*gJ1S`x9ykexVHy| z@OV1dy+gi?g%FMc6jCs->yr$>zp!n&Opy)6{*O@hW_RP-9EUNbN2ET#kC1jU1=CWf z*4GbbMeAOHnsi5-EAlpsg%au_U6BWeMt+1&EU5W=xAK@`zIwQM3J9?|i!i0)m&bQj ztvw+`oD4{hLmcMNcfa;>_iZtE3PNTa85M)j(-kv7@+5LXOjtg?m%>hcHJS8$65I^N z95z2gYbQvWiAS!Smho3ODQ$MlX}~_Tz#2aWP$F}5cu~TWRXXM{-C3d2Y!k!!l54JS zTzRSqkN!wZwp`87{J?Px;+61v|4ihujChD&k-ZVTw^5n2{yi%yl-17b%$3tiGaVCurTz`Z z+czx=`V-pDLeW%Bchlz2Saf){$EE#X1#|W1w(BIj-qHFVv5A*l&q}7H4g=1O)Lo~$ z`Cx7)Yk4npU7fKeyt2rmg3PF5cTM@;1w1aWS$01j^3XEY#!>jk6JYDyb{19orm8Xs zZGtgCV-rkU-(D*falhyjcuNMOO2X{-T_r~7v}6@Fg9#SwW;m1+FD0$i#OnFl(;1&S zXQr;ep^EH;t$7$@dct1!=wP1W^Nx2nl&|3MxJ3BR$Zy5x%rz`dXXT`u;9cRbl+^Ls za;ZudFj2pc`qF;5Pk)_Gk-v%$Ka~eLF)qqL_uLy*o@&^u^%7yJZRn=%LN4f3|7NbI z<8|+=lKOpYhQ*(c+TrK8m>4PYtBrm`uKjohj|0Y6BjD@fju6fruPHXluw@`C zykC2Bd!4c>@&yW*OZE+R_XVU#`w`jwI=aepJC29zh>HELh*`EiBO)Qk%Dzc8LR&`{ zCh??QmkEEyonY8DiKO}eh6`_cFMAtSe(lg}(BE5%ju?+f)nAUT*ztXwRTlNQb1(Oi zR)Mjl$+nM1=po;`>YtcBcY6lRI=>hy*(+^X${fC*IXeF9b*-k>{cAwNQB~{^;?F-e zW1+UchrL3VpXIm;k)WrQ=NI$&;TJkq*=~xs0=B1G>9p}XwMwiLGZS9ReTEG4E({r?+lMlYxqMYltsH>5%Ugl)=Rw{XXQixjeDF z@SP@IGBTBwpF^EX%yrEo(e!q9i%UiF?{#j8_-A`}WSiFCe|6}3_SZf4uWzr-wZ|wD+Kd7BP3-T@@6+}a^grcmh!(tQ%!voE zfr|t(7om|0*8NB10HH{ZMci(SS0c1cMBG*dZ6=hQ15hu}R@qNxJ~Nz&j=(_xr7Z%4 zgcK+YKr$)M{q2Fnq{QZ`T=*%vL!=wEvU^o9BTX>G0~C zD9hPGt~{GRUo^o6rFi@gdG$Y_9pMUD*Too5oWP)ZT2*4`xmY?Ob!p-`o$4DP1S;43 zPU?B?u8xk4rAbC=tlpJNrQ~M|wFflHedkFRROctf%+%o8>YL|*xL)gbwKmw!^ahTU zLMB{W=^UJH?;B7rIQ6?wcmX{tYE*p)#G1r$4T6S;W|m$8pa!)PM@vk<$F}9!8*vfi z8tQ)lRpH*>l--S>U~}fkb{~t>YC8ZHgw()fptN;uYv=>!;59FJk63U(YGw^Fr=}e2 z*ohaXtT-5664{K$n%a$qT}O+o$pYm3c#volhJ!iS4&eJ2faGwGndC6GD*2RKD0Bb) ze4nZk4o0;vqh3ru5Y|Myo6Vl%0OCju07s&9BA?OZv@ldF1VQwOmU|rf77L!^bnfqb+Lv7S(XAbu7$s-`OJ;`rB~ zdw|4-(E!e|g(MG)H%WJ#>keX(gmj!qB;>O6;9*mIEG*#|kl6Lvz;4-e+;`&2_k&`h z#Br_-P8O~JKC#$Qv3EY(9bU_RP)86uOTe+iw5pCxOxW+PcI28gFko_Wau9imP8P1> zUtftmU_8thcm+h9YaSjRXj=Wq6NgBNb?xf7 zW+4h=+}8V?dO?G%9$5qci=%*xQQt_Ej)fAB&MPSBh(Zw4Wzaj@VGT21fj}o>H>wB< z1_q*(IPLNxHZlaRWQyuxsIXpjmILA01uzpuip+NiMm7jmNM~R2*l{l37=DTF2VEk> zqPH*%P%vG9JVs*m_if3o$&_}upUlrd`@}(UNbjYH{RUN67f^g~KlE_saooTo6!h9r zfrd%Nv`Yk}(<2foB4Di@1 zkpCpspHUS81r5dMjbdZvu|P6;rMF$VrtRsbHy@3TwaU^av$rhOt{YW8t}K-Es#}!! z6?b8*luBT5{I&I2rj>t+cw8vVI8v<_YuV0lbNk7X69Y*i8M5!Gzb!I&d9d6j=2L8} z3iufW?iZ&D=4mu-=!8UIliZdL_vPkHzIxs8O!4{S&HXWw{QcT!vQnndW#TU$tB)8w zcAwnKQ6dCd4Rz?}8m>OlBh~Y-mGu5rq4t&ZGnvdwc?k~j=zrSUZUEM&{Be=Wz$g(? zZ`X#LXiRDbCH+aD&5rz_*kI8_;>bh`jj5%Oy(rJHL#{CB@_@D^e5Tm=SI#Qjev zSjp*A+%PMcd%oX;PKgUhR;5ycd5H^zsM0FlutUj4Av08@oiZ->2^!_4%S}?GUbh1! z1xQzdCl!fw%HgfN8 zg~5tGgClw&+CZ13ljUVtY^?U*Ff2YTA}p+G{B96ICGi)DXTGv5+G#E2S1BI82y`W3 zXe55SC;{h1a9fR#s*V(bfFC2J_n&2pUOnCIG^sWY)glldT$lH~NnB)^$G#hdoH}*r zi*<>k!(8|IgS$nA+?sh$RJONV_78WA+p%EU&w2@W`J>`()X5-45m0{2d(`@@)xvYw z{f)-Q7Tkrh&;zvre9;BBT*oU{qrFg2x&yE^uuUIeIsy~Boa=p zx>S6)#9UrG6K$wHV@3a31#t-Io`uaajUMxt=INqR?Qj4m1S<9~&6MvDIyjGetn=0W z@M`c9mh&T_fe)VzSXY%CI{}#}Vl+>EbCmuVFMexnuAwvA_$1vS8zYWN2M>?)502XK z**xHppS!gFp#DSFJsE5UOG9I0?_TcH=1YdIp;wd7KP}r%g)X8GK@@&Bp(CPjT>t~i($oX;nZK9DX6Jg zKd>3mN{q)C-z|5Ooay+SW}c!TX7SrUe|z>S5U-QHMa#|OE7wwPsa~8)O-h&m0x#{d z4?+5}Ql>N7ia-kht;7P3kLyBAqXFEBZ)`_?OoedaaU|JygXnHb3uo^lX6y*V+`lzt z!LkS_X-{;Eo7A7k)Do;SrcMj2k%X@)+x}PbQKx{~t0_TB2WFTSiP4mnj9=X_h?R(U zU+eN+TLrCA#STA~fI<}rQPndbJfsT;U^70t1F%kG-k*@&kA+b9rKS9(K=FHxi3l7q z%mh|Bir)}QeGJfKY!0)X!e!o&f2X(SD{hv4h65bXWQ(XVDDCMlPQcSKV434_4&m@be1!Z^(Q4F;T- zE*SA%y`>!BPULjFx(gtX`n%F>PD@2tO<~Xu_*W%?1QUm% zZM1sLkKIF2RaG^knJ-ctU`G(ln<-G2OkTi$h62?jg+^4D4{IbM**sX~LuHxd`me~u zPA@|miVc&-59|bu)mdrw)V#gbEKmCG#0!@9ViR<5G%y_}??EyF(-83+;9FSi8->kP z!HpRO^%l3DCrpQlV?RX1ik6&mKozF{`>Ce@LP3zk#5=U%H9*}s7(pATg4HvCaw4XR zARp9S=}B{+Ox8#QkSszQtU%Yi2OtEz0tYi#mj_DKSv_!FWvVpa2LN@Y=jw|iyzR@% zQ^0H0|Ad81+Q5L@;C8a|G9k6zbb^ng&Zha&`ibuT91~^rio}CTB4}26vlQrRa}`ct zM$d#uaAr&lSXb;)TJEm|OsQW(6I%&2%_gLL3~(+d{a79E#?j#kgt>H@*j@0vH0(S^ zG=4ybHRR;jBz+}>RmdG1cQKP7$|kGv1e;^=Qw!|@BPK)5o_qGmb2(4@{UZaXJ11rmuR|XXVx;75Iztd&O1(a=A26L4& zcqKp3=Soz z>X;Dpdh!Ij9q@YZuvK#<#ZSr(4f$C?$fQQbzi70gtULq^6 zv%6P=7ZD=?h5>WHduaLEKG{z+1$5AHtMa$k4a0Vay{W7u4#OuFDC5I#WdM`O&@Hpv zDB3XFyO*pS8317No#ux-T#1f7UktB?NH6_FHx@~etmY3(D3YC!l+;|y?x}lrBH!!1 z2ppUmu_x0S0ERG^T-UHH)CWhn6beE*O3Qg^$ucqUc{PkmA5iXrhCs~%J4x>a2Ep}z zeeLmz1dV=#Lw)nJil;Nxla^sW3$9A0E`j@#{yW4x1mZ9t2q#0Cg3cVynpxaR1<)pK z58T-p(A2ztB+qeVs=#ast0YQGE_T;V5K{H+=Dv0?bbA2*9wX&s70?XfYk*77^udFO zjsF?TB=EL|h*?095qUZfzCMfC8zTs1O|rYz{U`Kb8|1n;(4-|7|LSvK6AY>KW_172 z|F#8g9+lT%FVQm_0C?2^4gj@Tdv~Zr{)>|txp;I$0}UX)AsZD2*sf*M4{*zQd{8xv zMCn-Z=FNj(U_A8Ax8~waG3Fbk{~!j&J_v8CS-qNeA9nv3@KT1L;vvr!0P0ssNi>zV zwXD~H%8#Ri5-0E~%_X3{wie;oo?SZ%{N#tM@;C@lyTgUx13-mZHo%7q7M*fXj~@2> z&f>8#l;MAl>jno~Xl*;|6`Y{V?#gZOcGixBN`HIDPDLTda}0?zVwU~!28O+Ot zr*8%K(O&L{)9LYclwd?uvZx)gg%bbCk3Lty@Z8*hy^UU8uSZoJH+1q0z=+0TQ%S_z za4r(mhUzu;4bU2fi)TKkXiGHKgdIvJI_f2Da8~HTiQ!zci16)|p}O=WM|xVkdiFWp z!dE~EF?4Hz9t6_RJ^*>i?M6`-Ua8k4-wesGaXOHQct5H1^rHA?ct?R#*QxIbRFZ9* z{+X}%IcS9(Ce`+!1{Yy2qn{Ip3NoQhdR%`ZCT0W7x7!1HR$a-i=m#Q)PwWa zBs>^wt_b}c5U7Yp@)cf{8=o2Uwv`90De6YsFi{U{oCbu?u%+|dZk#9GBPv~KQx?Em-~c z%t_n9Nj3lJ`^7@bp9%;kZ{h3(zJI`3f`o}c_7MSvO$-ypqBk!9DG%DMSiGhjQ1Gad zug*7%AgWP<4XHQ2)NHZMMG>7W)-8CUa{5!(@$DUa1D``9Yl-t?}`oG0} z0ymZ$dvH!Nsj=))U$d@EO;G#~68qQTcD47NBNs5rDYrn8)&|`DAjj*yk`xp%{-ngx zp!l;B@4r-#oZndvKdwyB@I`(sH>TjF3ZoF$6Y!RJ+aD+E{JFS-8x}I_rAAiTW7oe? zHHOLuN;8hS4Sr%ccXy}H>ABK^79XDA2q`+x{7<0l$Gf*iW&P7QB3_qmi{?e#KQaeXj^f{o%coA9@#(|Fuh8;QpAXRa?v2qhn?z3%N%`PM9A zk0dbHIZH$4uAIggt7E63^wMOC^zlPBk2os7=}TChvpFwxsPMthV_WOuqH`#oSej_IV1ExOG(eW7-5?4p|Eeg z#z?>AEr?gwJ}~#@Tbu+95INK*|9)}<17N^9xX+J~>p$i7+-gs92HMaW>7Vng&V$~) z5VofL&mpp++qYd+Di|zW;O4l5Ogz8>MgCKSd9Z*`i4K{Hg+55+pgjA45(A8>3e*D` zKr1H;BaQ>&&{L!B5Kvi=TB`jpBQ>6cb5|ZXZ-n-%3|;{s2ON#Zi#yJCUg^2?bOrMU zJ`6_c1=BGSgUOa;-~4^J%CX?6m9Q7;({Rs8cZ=*yhM&OY+hgPj8k#CeGM5&&fJzWp zDojs&n%S~O#;B`yCoDA;5}vFfQ5ZH2!CFd7WYN3E)&opT6NvxSj=;Xnmv39enuH_e zaJL#P(zIFUI0&f^O2(peC@q0m{#`q41)Ft(vAZ0Z%MfzdPy-h{x(jT;g~tM0O(6fk)VD_^G>h|8OW(O@kP>8 z`RCC^4@P3s>LGK5+x5d`8N2&8CC?n7498)5XiLcin<5coc@GdLt{r&A+y@M=KxO9E+m=3t%7&@y;Eq5Ba<2k?K)-^qWTP?0A^7g1G-MW) zt*@)Ym*zub3E^BU8CFzO)O;dJsR35{=O#P4)hUtEx;)6g5$@-v3Sh$6H;=$raJ&DU zDwMlAz@ea5Ummrh!_Uzcs{0H&Xq14DHc)S7r}U-G0%zRo-&tJrz)vFn0$!}svqT1# z8>M=MHiqgIQ5r00fKpaflz!h~I}7~+$gPQtHVrFo*?0LDpTf#2PCXYcm@pK2e+x74)kb12Vhsec6%K;4LvaE z9}JHT5+RnHxKQejQBsFUDfD${+v5a(gs|$EZ6g*hyBTp^SUpJX#lK@ya~?ttFBoC?D952T zDR>hKk8afCP{XJn-`UjAfF$*6Z2Le~GQhDo?WgX7a(=-Gr6OQS6iCt5&n#}gDO=XE z2I8KQo}OOVkAKpksgRLP;*<|&Q5)4auf;>bc7dq%EDuFOeK3ea87;`1qH89_7MklY zr4H_eX3){ESy>4nH@!n6l=MlnG3o)3{nDV*YrHY`d(lMv)fDohlbzXT_QAj70=p_^ zV>TCPz>wbFUWeKUtfuYBlP8a`Ud(GLhnA|>U`rIS>Xo=STfh#K0|gRvkbx+eS!}TS zBR_(((rgrOz!W#-FR=Sg&aJj0O`R+H_3H+0)A*_g^LDdS)S?`;$#iNYcZneLF3_q; ziH1*{RRtFTUzs--Ub7#Jop+J2261kzfsCs2o?0clxBZB19Epziy7%GaV=q%~F^gOL zoRzfi1i7j`Mt92u7UnScZdlg+WEVECvoFpAcI{dnt4NdEH+VG#bwlX}C&U&dOEo;f zV;BTYED*z+?4By?2Coi-|6+c)kZFN)VM5J+_oCzIs`vm3AYTpBIKKe2DI6?aj@kHq zyOjQ=?W8r!DI!%GQs=J^J%jT^j#>V{605c_|kjzl=#7c0#P1cF$I!} zG58f%_{Iy1D`T^nnw_bl>Q7VoIEikFnHqCTOq$4!*oX)%+7no zYkmMnfyPrX5JAr?uQA+U$45L}Ij*YknjDMUm0~QfW!wczC6>E)pJnS4-FPj=J0h<> zr7fp1m0+PF9pkEcAVp{zZ5=8<1BGv`0Y!xvg&{S9BhW73u1zv@Q+KB7KYoV zZ#4R0!%xkXZ~u$8U+dCkJ+f2*En-AXz-fb2k18fdY1kHjfOzVHKTMDj%L7xt?*(0w zO_JLd$agvGtey>n<_dZ(Ausc8mN%`2;_f#o;hleJQmOr zFh}#~9yOc{p_A_uV(aYbxCxPwk^Pkf=otXJUcAS2hDCit3XP4Gv?^8pz@t?nq0=l6 zZLwFH|Mj_y7frke8`Dz~<%&u`@yLD;BBd~hxGw*?OnLox_Fr~=(#adl9QrN^EDTgM zs?4{Q2zieopnw;Qt{3D~w*og8BhOWv7||x_@XJ0F%i9C{iO~7^Bcd%U*Xw;fY8Qc4 zOJq2-kx&7bd?6?T?X1d#)B%SslW1glrUfgMkRdUgjm1#YOrdX6)EsHbsCfbUNx*tc zc1ql8B><=ZN+i#KI+p`(K4f7^lM2z`SG}SY*-`3ZkAW=k3+z*E@d`j1Xn~6i-s!c! zfAXv*7R6yG)JgK2X%=z**o;!0uaD@Z{=FGk{C{4R+Fvz;CQU@DwOQ+A8(kazN+wT_ zDxdx)vIi6F(?FbDm)g&6)cK-u99DL0Zl~i6YD=w`%JO>u7TqY+7T65VuFc$fK8{R( zlq3+pAyl|4OLp+a64oS5fV7}-L*mnLLt_Fj$|6ZGAArgw(A0BxYPsKtTXshP1RR%F zUxd`Xw|^-Qtel+U;^GJ)fDqlQi+l$X`&W^J9YyP&Ub- z@dBE385fn)&_>A1g(?c0A>Ff?n{nVd=3ucyS(ESp zm(r1sQ}qlQa5r9vNoB{x9Cq1(3Jv8kG-gTLYL-Jw5vkAK*1N!8o<3R2#wE66(VftgD~`M>|Fq#I!nq8w&gk#=upg@>Gg|%?PY-ZZ?JA!Q2iL zdR;+5q4*3wjX~83lObK#+&qCKNFPai)P8~imAun_y$?C7kWB$v2W;aYMBsAa?w4Dx z(%24kX1kR*m>YS1U^s`mNhNnz+QGGVhnY?(jn02N_55M4*;$J{x4q4eeP6u8pIufB zt}(+{kn2%EBo83-2`c?}@pI8QGjb=;DHCwvVC+U#-}f`0&oI5Ty~D2{&OvgQLgfg# z3l~F*VcFB2?d)1;9C3nobHNOGLDd(y+;l0rzj6)~K}XxhdxMUH41)ydpXumx(zycK z2C|qJC<~4y3L7&~>QamK*4R>cH;@qJcylMZ5ptU@@})_paN zA;-$8+M5`1;*r`=(@HO*IvO5)<_Z`DDgpYQq}ln)t$d>Jkn=^B@jgj=iP9ey&Ir2G+$xO7Jo$daOjo{9d=B~xLl++9u)mT$F66b>B z&Ku(oM?D#!)JAQC0Pk5l@KOKXI(oMYX+dySO^&pvMt*;kayO|#| za+c^6=zE|a9dK3$WKsTOEzbG!1~3%9-P<+i5lVKnjQXUD2hLRl9+rSBDlL`1cj57m z${VjyOXx6arub9hU^uR0%_W+sFe`D8{D;8^9K?J+ zaOQJK$V0F^avld$D6vcBOl6^(fAia0s{h*kxRX5Mf`WopUPNIvanSuY9-rGZTvHd` zC40gMoT9dJXTLCP&@9MFsHkGUp=NwAlO2vQSSpUNLeVVAeeEO^6eHQ5`>$_oZU*c^ z)9B4Cm7vVb@K`n@t^^`$O4zz;+OA!MeLRv3y$}SaD?4Bzl;=)I^-3KEuC|*Po057`0 zM)DI%9C&$zN?? z2zaAdb4!0;8&3hm>`9nii zg7YPCE|og-;Bly@2x+owO-YjKD8L4qLHG4r7!Jk_%s61xtwrKOCO`6LLN^vor8foJ zPjDQ@_RFc$0$F0z=~A`|?Slt`u1MGBv|sa@nhA7w1SUfd2KcYpy}+IV)qC8*kQ^TW z{b&fyc^EOUf`MubnoEQ(42uakDR66D!uZ!1sl2{$4#|!5eJ~{fHAWZ*Fk3?D39{3{ zdt*=oF~S)Mytm&S3y^O^TFfnwv>_f4gW)k^M@u3_k-oJliM7miUA0j#0w=Y9 zBU*_Ep5?@vi_0AzqI*^L9FQEt@Mp2JLQW@KvhK}Z*gQ6Oj>hkSd5Dn?>cS2GxQNtRNW&xH_5s zimL(3Q`;$+YpYNC6^W3=iFzhz+5S_M(-PrIX{bG+4bM&V#j@uD>XSIo-?UKM1&{?K zZ$J$EPfAYx4U=v11}MdVj@$24;5iKMgn=884pc}9F!dxQCVILy8w(Th6edCh+4TUN zkyh46@@<0m^*`wi*}qVGzfGs1AC#6a;ZkkiK}$T&ouW+e2v(j~>Ve%UVz)8m*Pe=} zGuYNILhUqY`IioQM?Q=|Oa2Yr^U;}{Fnn4oEjhx z!{%54tYI>l8m`L8LQS#&l#8008mtuPnmxr&_~1~Iya8P_-g!6xwn!QvRoqoziPND( z2Z5t73^{7iq)%OV{Ffi%1kq&oS3cAUY|qJChBW#f7m=-|nR#NLrf=)3lo z;|MlST{`ecA?*!XgE7!@^#I0;cCr#e*Zs~TM-iw2B3uW)d<-C$(lfZ1u*gU<3;kVG zQ~mc|=nee>t~umWJfI-|=e-2hW>W9D=s~BC`vUmP9i!}+@a8mqwaly&?Bz|*Om&H0$>B!WSp@lui65-jiF^dr0K$NMEw(CbIs%A>Qi zoH6kJc&(|4E$9HP;M#Mz4v-SnOU|%8Tr9U7{$^=k{dMmv#|fi`zn@U^95l#>+jQy| zq$X_YKaY4YWD^s*zb^Yf4xsDuoWDu!yu!f#^%E*Z)HDemAX+VDI99M9W;V8=&TDMH zc~z`FAe+gRSjTJs#eoCpz22oOjQ1nPQ`I-={7b|_Bg7DtS{STiq0l`UYR}?3@2X3( za|&88-`455E_T1RZUcys_aG8W{|o*j@ed3?yK@Ccf)zGJzbrqQWm>ps^Oyq?Bl12; z*cgEQZVdUy2;h>k1=o_@VH?H@nW5>&Z*(M7zPqm(LSrUC;Mh2jZj8F}%;~PP+L9jF ztj-)?+lw?dt~9BPk@G?>+?BZ=@oT(kOQ6qlU4{pfz@sqUj0Dn`%C+8cjiF(oJ!Df` zu~)+ttxFhvPDmvbLKG?5$!XZ&VVm#*_JBl{X#A^L)$n}znG=*;lF1r-MU+tyq;S}W zw(0qpF1PNVnWk%C%;%p-Ly{nR+t)8=jYhI|gnb zc)S98^HB0JKYIJ#LFafu27w3AUxTlMXk7%MXmRZAwIc%K>;V^v60V&7cS=^~e_eXd zHkYRuf*D+OwKz^3`eIP^4Db>>o~c=8J4lVAvXzUe=Nz+1)y}3#A|92o;L`(=D2O(Q z3UjiG14y$2Sn>Gh0{F2ucEC^@@sg#QK`tI(Y0US$z~Xq< zHkvyO#}3sPz6Fp3wbX{e3*RfF47QE(gV6SR`RWU-XdE^Jd~+wi0YJoGla^s*Vxw%$ zFuva4w{ixi7)x_$i)R2j(X$L*9j&mPRYv+VYOMz6?s>8)@I%^yiKTTt;m0Ov25X--Z=7F*9B!Qf2HUdSrV{hioX- zDr}9r=gOQez6G;fvSo?mZ|mNe+g{-$MDKEUO}D5Uyk@-ChJ=#-W_cR#S1T+Mmo`|ny8}+oyY)zhdLCvZp&Bo1i1a;Qh|2*xNDwTsZ=LToTjh&Cwq{PUw z45qa(06)vX%xA;3AN~D^hdd|=N-!`GR>fzO#|V%qxRH>^w!6qS1eXv7OEoh1ft4ML z+Hav(MGjT*gMS`noB**fk`Am57a;KyqXz`_LU*n_XBhaRu>gON+nt9UUK{?H+oXhy z&yW+wHxMg;zK$?x(N9h#ELE^&uw(!~ za^`vnSYk+oY=`BcQ(+D+k(xO(S1|B!xH%ketXYJfnU>%ZE+tMbPq~K)$rz`yDh15;@2y zg|j&g2Q1B5O=>`v@qjED;BHa@f?_}&P0-SF;erXsSOTJn84xn&4ZMOZ{RhsbNDUVN z9d@W~9W^@#%n2L+_+m5k4Lt6t^_Jjw=>@D5FxMbZ5(F7D9oi8#(fdEcf(C4d6u?6r zAis3NGlvdZ3y=dA1gaX~>AdEa7a=!9{ZF$14rqMxFR&n(FGe+#s0C=kM^XJ`AUq%j z{A18gO59n_X}G_)3&!@$c$j4})Mku)`>?5`Z2#-yEk>&!_!#wtfT#~F2?m)*siD$Aj(qf~DsYo*9wd7tv%a<}4glutpsfTv zsL*fnuA1HMqT}pO&`+Y#1VDX&i-F#e^BuZZVIO&(*hG89fo7X|DYHdQ&LcH#56yg?~Y{P zz*9ODYjd)u>^F|k@52R=0{0O|A0|gD4>T3hefsKwme4Vb>p1ce)xl$}QaoHC_={Xo zx=*es#eF_jmviS476}*>@_W>zA-Ph4xb~m6r<^ayIurD;WfXmkH4%P8er^In&6@-B z(N~D7ybuLTZj)1)_*qz8Q}(8C$al}gnQ8GK|I@ey3ec2aYXx-GQ237x&j_4Y89d`P!%J3{r$xs^*4*U{TQLk@o(CY$m>Xie)-d5|-vp%nwO zR)Bf7>~lb|5N^dSpM4s}p`U?QdI0Z+pdK9EFgVdpN0FgOX3YbI?|GqD;vmfsd`@O? zhhcM2`W4FIQ-TY%IBp*OzV`s2T;+UBIjh~U+CeJ^5YJ1FdN}PMpU%LA<|E0?&4M=_ zZ=8vc;vnZNEe#Y$DJIK{eny(KITCz|yei2b7y2)NCN*V?d7`Dv+_`i6ury*1HhJea zy3DduKVZtfJJb$ZLmMKh1v+ZSo~EL|T*tFvl1i~_5DY0a`Dm2z8q&kZkP;JS?4J(D zZI09XU~t5td}-!Vd95gSbe-^Z}R)rQ%{~MtO{AsqWfOus_g9K z)q6J-2M)@B(+8Dj!U;hw1^7t1%aw7z{V^>-247J%$(H)<<&95_7PisP-mKqPwX7Xa z6~2Ycd3ik>l68`xbwn*?6#GGA(Evigt(Vtbb>0LYdi!`V&9xQA&2GlUm!sd0joq8X z+qsVXQX_)|BcTamzcm9FPh83Kk-xDqQ++(arIodewTcDB&gTTsXDYGTY=98GkiT$L zBc_%!W_oC~$GenSpK3E9h*mUG$hm^%yOj^6zb_(0Ovj_Yatd*rxiIoUH9Dj_jM1_`V~V>sf}m&)F4jFl<7%mQzT9B^th=(6h_tNqEJp%u_C0EGrfa zq2$+$6ElmA_>8f#oLjfub!T?nm5f*1n%bhRrA(Lb4T~72f4;$L)ZS;of?z>4#sMd3 ze@ozyJXvucofvv+h8!x$pu*Y5SY^3Xb@V23c}gsaB{4*fFwFGx zy;~ZcW8TyZ@*Q2+d^bSY0#>^P&3CoulsVlN^=(?o5yF9H2ic=Zk)XuHI`^^IF4}64 z?i|gYbyG{&sfG2y%po!BmIl&|?Hd=FE^ROLP8V9@>PiOvtdFrY7RiZacgiRB>8sap zBX{JZ+%ex=7M-$n8S>|b6&bGbaE#-R^iyP>8%sqxgIdbm4U4A*BnX7ZOhqFlM(BRbGDs0d>+%r0t1KA;Y!F=7 z&Ei8GDk?%PH%{<9!%l6vA;EG6T^#%;O@Vdm$mu@G4NBCohTP&yRUt)sx{hg%zm&As z7WJH*UtL|zK}(cg1xrJg{JpH)4(viMAVOgi5SwBs6u;+Py~?9f=f3nfR)F$EYySxA zUWi$`&}7x%o7)oyhu-oS7Ybj@`pAz)%dn@^;+%}gKhOmMy+vWxm$oe7c+MaCN;uM7 z=yaapxAp<4wdZiyQ?;ryG3;`nq<<6m9kcWl=6)cS)Cwbq#@zSl4^H zI|ShptM`fXE%XyY8^IMpZ(2CC3!%zDWF_C1E_$i}tzdjYMT^XOG+4uQuV(qwU`-KX z)ft^GaQQ+|;Wk{i*M`ZJ=fQig^j5e&AEA|04OPq>c8t!b`vk;fut_Ic0I3olx$J$dz^; z?Z{5?dcaSdmyGkOF1&Q?A-f80fOdE>C2v#PKIjQUo%&9PRSm&s2hr0rgmp4XOK4Iw!#{;oOYStp$w79hMWo zMske^l8amppW4aUf#O!(T?ht|DKH0C^#6ksp@sz-yYrD`a@}rjIPy?F@fQrL0p_qk zKPIP~*kH&S>fl_M!=_g6@{&G-CDF#CN$%=3$bdXCwP-9a#kosYJ$1h0pVC!Z;yf>e zF@&@z!fZjU&(bt5!Zhv|e6E@}B(9XU!DE*uH&}G7z;YGtast>}ZWx}d?PGC=^M(EC z!}V?n;k&fvLY5zUxb<{V$EEW&ps%!?*qogU!FbMNq7+eufli7jWbf5DmBc0nF6)k% zoV1))(dtdUbApSIf}XG(pazk4dXL!>MSF0CYMvtxD;jA(Po|)MQfd$wN+j^4a|?;9 z+~7e}N~Hg$%nje19|{*GUmV^zGgGhus4;KAx!OZB%=j zy(QmvUNk^KW%0EHZF#D{rr*zFKdSZ^?RDNo<`u^AI zfa{Qsgu$RuJUYWwAOOx`K=wG6#D@;HWJ z<0>bvfL5!T$%2-`2~lu5#W0sPz8dV>pAym9e$TLcbK<9BQdbVfYuZ3q7R25xZPs zv?9$44tR*V4Ycn;yfoP8?)TO z+U4Iowud%RrT5f&wh@o5JvHEK54#u8ihuCfmh`^yOqv(&#k#RTn;-q0tf1D)eBBm! z^`*tYaU^W*eMc3mGxAo85Y%loO+~&%uzHWU_w3xoGdkDlu{G;Vx=xl1xQycz5*yY#h9J}Qh5?3pq5b?ViAb5mBdbXm}(NT4B zwa;ul^%^YQl=z(%uKkz|%sEvt?5kQeGpcH2AaVs&$#JnJ7B3)sw!BVG7IlHJHKpfT z=W*IH#cym*OO3Lz$nJj)*^qdoc~j@lOE{t*IPHFX?hJHRx#&7(`M4q-QRn9|9w^IE zyctxRd@Ba?#?IfbrSIXdoEB0UqW1ynSxbwlPlkdO#26-kF%lB45{9{cdtc^SQ&fvfUUfF?OdxfE8x9K|JGAb4lI#PwtSDa_B;cbiTQ$al-pmqviva< ze{*~DCUu6@sjH3*C2Mki(D+tl$8E3us+F7h%y@`AjGdo_1{Z^XrkF+rZV$=Lw^>?= z3Dr3u+zGdeSZ(%yKJHyuFh0#QFAnvW}#%&mlJj($n1`S z>-F9^XIrqxWW)E@b=Tiq^6p|QQyRs_E!je|Fh?Z4q3=APh9T$HTmeYsKB>t`ep((h z9@O=JA~73~X>_C}RP5SO;Im%cc$V})oQn>}fggCPc_CMQ1Q7!#>=Y&VxI`iLd{>fo zApja?x(M=3QU?C8IXwOttOF$gsKBz7v;sSjTQ3&?r#MoL@4n1 zd%eqPQQ=(MLeRKwetGR7wKOpmWlWgKSxuv8c!|T!;NJ8VAAwoSIH>s;ZgTjGTE1^R zH4F9;G|=r`D(ee8o9$CcuR_LF>R@5}2bvk`U@~VCnwiX;lk;P1ULl)EQgao+fJatC zqo6(j(o`_Jp8hA0s#d6hU*x8TFjd4oa4L=5@cB7YmKwkdwfu}gW6d$fo~|^cGu*;W z9;+^m=8tGbOQYBvT&PtajExvWScQRS9Q0KJc(vpli42f+t?vq!F}sdOI=RPrn7Mbb0VF zH}r=Y+b*Z=nh!7&`XRX%m=vIIDXp!29({i%mWV+wiO7(ZdSmuz4$kJl&+&(xu*sK< zez{8bj0^`y;X~aEt9t^>$3*Hfn67GMaRdWzOV^R<7ix+#MBAxvLJ5dswwwt15Ecp6 z@jL7=|5^_uX@s9iq@~?EHH92C9F+}b5o$RFzzXK?eWA3O-zsVRbp}7O%ydjRX>x>~ zP?lo4Tmpuov4@-xtdEmdby_E`>V8|SLD}n452b_+=84fT%_&n#8K}olazQ+d&U6U; zE&||4@v)mb@WIKS3KPghv_v;*IiPW{W!?9=D@>EilM|HrL(B5_y@1k3Hw_>Ch{@(P z1+7krq-c3m%8qfDGsxUss|mFTku~wui%7pzYA53&9Rj^}10p=~wl^p-%5!~e13idk zWBea0nzjsNILZUNFh@tI5}X-D{o%y8mw0Z$-!Rvj%Y`l;Tx&LW*zfYES5-U%BJ8lxPKG^4R z0V|Mo0T#Q3f$ELhF2|lZ!^y$`0DWa%d<7(b2(1L{8%9ziL_rx1EFo#b+FaFoscD*;vkPS~ulAQ=yhn(^qY@rAf zC2kxdE>Ka>?En!+!M=91?TSD|XYnBxm@%-K^pL!pd5H<@KPlWd%(SzL%Y3E)~tZ?}x464s1gtL7`wmr+#Jl9{HRxEF+6p8nwYDlo~lP=GGHkC1LZpKbZI3YnNG~!9(_H zA979&;`cF_ABUj}v%m`=l=YJn8syOu{-zI@u5bTQhH)&EFoR9LTb%nf-x{lw&hM)p zYL3Gr4Bn@*N-;>-70|uP$H#}m9c+ED4ww^zF7#P)g4i`&=a`f@I^QOCwN+K`n1)Q>n1L><NaVS03ck8~&c%OiGBN$29O!q8d*j6B?u14>Sz)nuVjLN*1|sMI0B)&4 zTB{27XF8}G>t4i@ZkF*#LTM1zQ+l4iwX*-QZ{5>k|K51lM|E{u=0;vHsf3>kkGvRZ za-R^A+Dh-Dzg+!{N!NyN8{abG%3!=v*3 z1#Jw^P6`K(BdhQJS_A%J?H*L!N$U=d!>F5WU6(o#Q*n5$J@NQSbql-gQRKj*BN*S&(?G0M!GdML{`S;8-@SR*j2^8L z*&GbX61qiGPaSzMeZ`M5^|w!bTE<})R+jSmo-X;an7#}PS2O~$CU~`b zeTzog+Eg6DlFRJ8p3l-%QH6Q;_oz+WvW*k>#?f7O$Y_eSv_%s@aT<|&!Q0j9;aGmc z;h3b+zn@KGx%itLNOy>k#|>BD?pKxlZQ z(R_B)!(>WpmNl22;^SMpYHC=@A1b~e+DB<}Qmk<%E0a~|b3WQQN`x-c4peysEl_?+wBOLCYBPc z6To?5(d^|MU&e=bajMJ~pD&=QJpRZVGy{pGeYPcz8~_kdxz)rInTL@h-)O+B_$R`LG2u{4E(u z1#}s_NN`%R6L&ddl80F<6lf16+*0%rk_`)gwL#bzqQX2fNAuD-mu8-r@%{zq5^l9M zG|9ym_Z0(*Tl*T-Q0-&;Z-*0fH=#KF)xk2vduIths;$j7Kf&816z;WAwdx)3j9Fo} zc?T{$dTwkX^0m_>7$4#sdisk2bnB-f%ID;pdg$-UlJ_{18=^)KB`a{ky84)#-Gq{T6FEZ4Ez^ z{)@3){`~=8r~dVTuNk+dX%40Mifae0MK5!YDSVe=FCO)fji_s?JP0pQ}Pp$nuR|M!poy&L~K7{+w3@O-}~8Nayz R4_M@2Y&6?&-N5mu{{eVOdiekV literal 0 HcmV?d00001 diff --git a/tests/drawing/matplotlib/test_graph.py b/tests/drawing/matplotlib/test_graph.py index 46c463249..4a83cda5a 100644 --- a/tests/drawing/matplotlib/test_graph.py +++ b/tests/drawing/matplotlib/test_graph.py @@ -155,6 +155,24 @@ def test_graph_with_curved_edges(self): ) ax.set_aspect(1.0) + @image_comparison(baseline_images=["multigraph_with_curved_edges_undirected"]) + def test_graph_with_curved_edges(self): + plt.close("all") + g = Graph.Ring(24, directed=False) + g.add_edges([(0, 1), (1, 2)]) + fig, ax = plt.subplots() + lo = g.layout("circle") + lo.scale(3) + plot( + g, + target=ax, + layout=lo, + vertex_size=15, + edge_arrow_size=5, + edge_arrow_width=5, + ) + ax.set_aspect(1.0) + @image_comparison(baseline_images=["graph_null"]) def test_null_graph(self): plt.close("all") From 1916d26d437c4ef07144516a0fdbeac00a10b254 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 17 Nov 2023 01:08:55 +0100 Subject: [PATCH 021/276] chore: updated C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 771a449d0..a8862c42d 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 771a449d09fcb4926d123975f35d2ba5797868da +Subproject commit a8862c42db657f52ad4b800f9f70941ccb1aee3b From 279d53e78ff990b65a5a83283196aa2c141c2744 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 17 Nov 2023 01:51:31 +0100 Subject: [PATCH 022/276] fix: fix test cases --- tests/test_isomorphism.py | 4 ++-- tests/test_structural.py | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/test_isomorphism.py b/tests/test_isomorphism.py index 3c1b62bbd..c1150da63 100644 --- a/tests/test_isomorphism.py +++ b/tests/test_isomorphism.py @@ -280,8 +280,8 @@ def testGetSubisomorphismsLAD(self): # Corner cases empty = Graph() - self.assertEqual([], g.get_subisomorphisms_lad(empty)) - self.assertEqual([], empty.get_subisomorphisms_lad(empty)) + self.assertEqual([[]], g.get_subisomorphisms_lad(empty)) + self.assertEqual([[]], empty.get_subisomorphisms_lad(empty)) def testSubisomorphicVF2(self): g = Graph.Lattice([3, 3], circular=False) diff --git a/tests/test_structural.py b/tests/test_structural.py index cde4f62aa..541a2c6c8 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -144,10 +144,6 @@ def testKnn(self): self.assertTrue(knn == [9.0] * 10) self.assertAlmostEqual(knnk[8], 9.0, places=6) - # knn works for simple graphs only -- self.g is not simple - self.assertRaises(InternalError, self.g.knn) - - # Okay, simplify it and then go on g = self.g.copy() g.simplify() @@ -158,6 +154,20 @@ def testKnn(self): self.assertAlmostEqual(knnk[1], 3, places=6) self.assertAlmostEqual(knnk[2], 7 / 3.0, places=6) + def testKnnNonSimple(self): + knn, knnk = self.gfull.knn() + self.assertTrue(knn == [9.0] * 10) + self.assertAlmostEqual(knnk[8], 9.0, places=6) + + # knn works for non-simple graphs as well + knn, knnk = self.g.knn() + diff = max(abs(a - b) for a, b in zip(knn, [17 / 5.0, 3, 4, 4])) + self.assertAlmostEqual(diff, 0.0, places=6) + self.assertEqual(len(knnk), 5) + self.assertAlmostEqual(knnk[1], 4, places=6) + self.assertAlmostEqual(knnk[2], 3, places=6) + self.assertAlmostEqual(knnk[4], 3.4, places=6) + def testDegree(self): self.assertTrue(self.gfull.degree() == [9] * 10) self.assertTrue(self.gempty.degree() == [0] * 10) From 29677eba30df72ddcb840d649a1cfa61cb3045a7 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 17 Nov 2023 13:31:40 +0100 Subject: [PATCH 023/276] chore: updated C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index a8862c42d..4e04d3943 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit a8862c42db657f52ad4b800f9f70941ccb1aee3b +Subproject commit 4e04d39438ac03bf177529e754a07b05beb0930b From f5a1f9f42b5740465f9f9536b4b6c01ccc224ef3 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 19 Nov 2023 12:30:35 +0100 Subject: [PATCH 024/276] chore: updated changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0fbeef3b..ca50e6098 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,16 @@ - Added `Graph.__invalidate_cache()` for debugging and benchmarking purposes. +### Changed + +- The C core of igraph was updated to version 0.10.8. + ### Fixed - Removed incorrectly added `loops=...` argument of `Graph.is_bigraphical()`. +- Fixed a bug in the Matplotlib graph drawing backend that filled the interior of undirected curved edges. + ## [0.11.2] - 2023-10-12 ### Fixed From e2149a6ba4c85aa26a9fbbc5740dbd56158d27b5 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 19 Nov 2023 12:37:16 +0100 Subject: [PATCH 025/276] chore: bumped version to 0.11.3 --- CHANGELOG.md | 5 +++-- src/igraph/version.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca50e6098..c186f5281 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # igraph Python interface changelog -## [main] +## [0.11.3] - 2023-11-19 ### Added @@ -604,7 +604,8 @@ Please refer to the commit logs at https://github.com/igraph/python-igraph for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. -[main]: https://github.com/igraph/python-igraph/compare/0.11.2...main +[main]: https://github.com/igraph/python-igraph/compare/0.11.3...main +[0.11.3]: https://github.com/igraph/python-igraph/compare/0.11.2...0.11.3 [0.11.2]: https://github.com/igraph/python-igraph/compare/0.11.0...0.11.2 [0.11.0]: https://github.com/igraph/python-igraph/compare/0.10.8...0.11.0 [0.10.8]: https://github.com/igraph/python-igraph/compare/0.10.7...0.10.8 diff --git a/src/igraph/version.py b/src/igraph/version.py index e9509d902..a75324513 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 11, 2) +__version_info__ = (0, 11, 3) __version__ = ".".join("{0}".format(x) for x in __version_info__) From c07ffbab48aa56410d066cea4c538653e384b0cc Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 7 Dec 2023 22:57:45 +0100 Subject: [PATCH 026/276] doc: fix documentation of 'neighbors' argument of Graph.vertex_connectivity(), closes #740 --- src/_igraph/graphobject.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 842cd84e3..85aeeadaf 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -15729,8 +15729,9 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " graph, therefore it is advised to set this to C{True}. The parameter\n" " is ignored if the connectivity between two given vertices is computed.\n" "@param neighbors: tells igraph what to do when the two vertices are\n" - " connected. C{\"error\"} raises an exception, C{\"infinity\"} returns\n" - " infinity, C{\"ignore\"} ignores the edge.\n" + " connected. C{\"error\"} raises an exception, C{\"negative\"} returns\n" + " a negative value, C{\"number_of_nodes\"} or C{\"nodes\"} returns the\n" + " number of nodes, or C{\"ignore\"} ignores the edge.\n" "@return: the vertex connectivity\n" }, From f61facb83d7faccbd8e98b4833112aac45fac41a Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 9 Dec 2023 16:57:42 +0100 Subject: [PATCH 027/276] fix: fixed graph-tool import for vector-valued vertex properties, closes #741 --- CHANGELOG.md | 7 +++++++ src/igraph/io/libraries.py | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c186f5281..6317bc16c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # igraph Python interface changelog +## [main] + +### Fixed + +- Fixed import of `graph-tool` graphs for vertex properties where each property + has a vector value. + ## [0.11.3] - 2023-11-19 ### Added diff --git a/src/igraph/io/libraries.py b/src/igraph/io/libraries.py index d47490292..52e47b8af 100644 --- a/src/igraph/io/libraries.py +++ b/src/igraph/io/libraries.py @@ -248,9 +248,12 @@ def _construct_graph_from_graph_tool(cls, g): # Node attributes for key, val in g.vertex_properties.items(): + # val.get_array() returns None for non-scalar types so use the slower + # way if this happens prop = val.get_array() + arr = prop if prop is not None else val for i in range(vcount): - graph.vs[i][key] = prop[i] + graph.vs[i][key] = arr[i] # Edges and edge attributes # NOTE: graph-tool is quite strongly typed, so each property is always From c47f4ccdc44704413199451278888ec456349d2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 13:54:02 +0000 Subject: [PATCH 028/276] build(deps): bump actions/setup-python from 4 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59ffdea0b..d7c93a671 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: submodules: true fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 name: Install Python with: python-version: '3.8' @@ -133,7 +133,7 @@ jobs: path: ~/local key: deps-cache-v2-${{ runner.os }}-${{ matrix.cmake_arch }}-llvm${{ env.LLVM_VERSION }} - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 name: Install Python with: python-version: '3.8' @@ -176,7 +176,7 @@ jobs: submodules: true fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 name: Install Python with: python-version: '3.11.2' @@ -226,7 +226,7 @@ jobs: submodules: true fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 name: Install Python with: python-version: '3.8' @@ -295,7 +295,7 @@ jobs: run: sudo apt install ninja-build cmake flex bison - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 name: Install Python with: python-version: '3.8' @@ -342,7 +342,7 @@ jobs: run: sudo apt install ninja-build cmake flex bison - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 name: Install Python with: python-version: '3.12' From ce2c436bc873f1c652f6e503b777a4f6545916e4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 13 Dec 2023 19:13:12 +0100 Subject: [PATCH 029/276] doc: clarify the effect of loop edges in Graph.similarity_inverse_log_weighted() --- src/_igraph/graphobject.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 85aeeadaf..5cef59ba8 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -15790,6 +15790,15 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Each vertex is assigned a weight which is 1 / log(degree). The\n" "log-weighted similarity of two vertices is the sum of the weights\n" "of their common neighbors.\n\n" + "Note that the presence of loop edges may yield counter-intuitive\n" + "results. A node with a loop edge is considered to be a neighbor of itself\n" + "I{twice} (because there are two edge stems incident on the node). Adding a\n" + "loop edge to a node may decrease its similarity to other nodes, but it may\n" + "also I{increase} it. For instance, if nodes A and B are connected but share\n" + "no common neighbors, their similarity is zero. However, if a loop edge is\n" + "added to B, then B itself becomes a common neighbor of A and B and thus the\n" + "similarity of A and B will be increased. Consider removing loop edges\n" + "explicitly before invoking this function using L{Graph.simplify()}.\n\n" "@param vertices: the vertices to be analysed. If C{None}, all vertices\n" " will be considered.\n" "@param mode: which neighbors should be considered for directed graphs.\n" From 755dd11ec9bf789fad79829c334983f4d7d20b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horv=C3=A1t?= Date: Fri, 12 Jan 2024 18:28:46 +0000 Subject: [PATCH 030/276] update install.rst --- doc/source/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/install.rst b/doc/source/install.rst index 5f8fc07ea..81bf35d5b 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -50,7 +50,7 @@ Package managers on Linux and other systems ------------------------------------------- |igraph|'s Python interface and its dependencies are included in several package management systems, including those of the most popular Linux distributions (Arch Linux, -Debian and Ubuntu, Fedora, GNU Guix, etc.) as well as some cross-platform systems like +Debian and Ubuntu, Fedora, etc.) as well as some cross-platform systems like NixPkgs or MacPorts. .. note:: |igraph| is updated quite often: if you need a more recent version than your From b901ce6908c6f812f8fd94a5ab180fdc13ffdf55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 13:40:01 +0000 Subject: [PATCH 031/276] build(deps): bump actions/cache from 3 to 4 Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d7c93a671..8451d601c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -121,14 +121,14 @@ jobs: - name: Cache installed C core id: cache-c-core - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: vendor/install key: C-core-cache-${{ runner.os }}-${{ matrix.cmake_arch }}-llvm${{ env.LLVM_VERSION }}-${{ hashFiles('.git/modules/**/HEAD') }} - name: Cache C core dependencies id: cache-c-deps - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/local key: deps-cache-v2-${{ runner.os }}-${{ matrix.cmake_arch }}-llvm${{ env.LLVM_VERSION }} @@ -233,13 +233,13 @@ jobs: - name: Cache installed C core id: cache-c-core - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: vendor/install key: C-core-cache-${{ runner.os }}-${{ matrix.cmake_arch }}-${{ hashFiles('.git/modules/**/HEAD') }} - name: Cache VCPKG - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: C:/vcpkg/installed/ key: vcpkg-${{ runner.os }}-${{ matrix.vcpkg_arch }} @@ -284,7 +284,7 @@ jobs: - name: Cache installed C core id: cache-c-core - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | vendor/install @@ -330,7 +330,7 @@ jobs: - name: Cache installed C core id: cache-c-core - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | vendor/build From 6720b0b4cd05f35454527b17469b4ddaafbf90cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 13:40:06 +0000 Subject: [PATCH 032/276] build(deps): bump mymindstorm/setup-emsdk from 12 to 14 Bumps [mymindstorm/setup-emsdk](https://github.com/mymindstorm/setup-emsdk) from 12 to 14. - [Release notes](https://github.com/mymindstorm/setup-emsdk/releases) - [Commits](https://github.com/mymindstorm/setup-emsdk/compare/v12...v14) --- updated-dependencies: - dependency-name: mymindstorm/setup-emsdk dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8451d601c..c778a9375 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -185,7 +185,7 @@ jobs: run: sudo apt install ninja-build cmake flex bison - - uses: mymindstorm/setup-emsdk@v12 + - uses: mymindstorm/setup-emsdk@v14 with: version: '3.1.45' actions-cache-folder: 'emsdk-cache' From 68d10b92bd4c9e5c0127617542d196a5fe696432 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 22 Jan 2024 19:31:07 +0100 Subject: [PATCH 033/276] ci: change name of workflow so it does not include a comma that confuses mymindstorm/setup-emsdk --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c778a9375..85334aa0a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,5 @@ -name: Build and test, upload to PyPI on release +# Name cannot contain commas because of setup-emsdk job +name: Build and test on: [push, pull_request] env: From f8da66d10aa61c48ea25fa976ab75db7fd6b9eee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Wed, 24 Jan 2024 11:35:02 +0000 Subject: [PATCH 034/276] chore: branding fix: IGraph -> igraph in the docs --- src/igraph/drawing/__init__.py | 2 +- src/igraph/drawing/cairo/plot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index 7bdbee231..e6d06b4bc 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -1,7 +1,7 @@ """ Drawing and plotting routines for igraph. -IGraph has two stable plotting backends at the moment: Cairo and Matplotlib. +igraph has two stable plotting backends at the moment: Cairo and Matplotlib. It also has experimental support for plotly. The Cairo backend is dependent on the C{pycairo} or C{cairocffi} libraries that diff --git a/src/igraph/drawing/cairo/plot.py b/src/igraph/drawing/cairo/plot.py index fb82c8542..ac98210db 100644 --- a/src/igraph/drawing/cairo/plot.py +++ b/src/igraph/drawing/cairo/plot.py @@ -1,7 +1,7 @@ """ Drawing and plotting routines for IGraph. -IGraph has two plotting backends at the moment: Cairo and Matplotlib. +igraph has two plotting backends at the moment: Cairo and Matplotlib. The Cairo backend is dependent on the C{pycairo} or C{cairocffi} libraries that provide Python bindings to the popular U{Cairo library}. From 73ce3ae981b191d09156d8543d6e075070695f91 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 24 Jan 2024 15:10:15 +0100 Subject: [PATCH 035/276] refactor: added type annotations to config object --- src/igraph/__init__.py | 2 +- src/igraph/configuration.py | 67 ++++++++++++++++++++++--------------- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 4ce6ebd24..c0997aa8c 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -1049,7 +1049,7 @@ def write(graph, filename, *args, **kwds): ############################################################## # Configuration singleton instance -config = init_configuration() +config: Configuration = init_configuration() """The main configuration object of igraph. Use this object to modify igraph's behaviour, typically when used in interactive mode. """ diff --git a/src/igraph/configuration.py b/src/igraph/configuration.py index 39788051c..6f507723e 100644 --- a/src/igraph/configuration.py +++ b/src/igraph/configuration.py @@ -11,6 +11,7 @@ import os.path from configparser import ConfigParser +from typing import IO, Optional, Sequence, Tuple, Union class Configuration: @@ -114,9 +115,6 @@ class Types: """Static class for the implementation of custom getter/setter functions for configuration keys""" - def __init__(self): - pass - @staticmethod def setboolean(obj, section, key, value): """Sets a boolean value in the given configuration object. @@ -169,7 +167,7 @@ def setfloat(obj, section, key, value): "float": {"getter": ConfigParser.getfloat, "setter": Types.setfloat}, } - _sections = ("general", "apps", "plotting", "remote", "shell") + _sections: Sequence[str] = ("general", "apps", "plotting", "remote", "shell") _definitions = { "general.shells": {"default": "IPythonShell,ClassicPythonShell"}, "general.verbose": {"default": True, "type": "boolean"}, @@ -184,10 +182,15 @@ def setfloat(obj, section, key, value): # The singleton instance we are using throughout other modules _instance = None + _filename: Optional[str] = None + """Name of the file that the configuration was loaded from, or ``None`` if + not known. + """ + def __init__(self, filename=None): """Creates a new configuration instance. - @param filename: file or file pointer to be read. Can be omitted. + @param filename: file or file-like object to be read. Can be omitted. """ self._config = ConfigParser() self._filename = None @@ -195,6 +198,7 @@ def __init__(self, filename=None): # Create default sections for sec in self._sections: self._config.add_section(sec) + # Create default values for name, definition in self._definitions.items(): if "default" in definition: @@ -204,7 +208,7 @@ def __init__(self, filename=None): self.load(filename) @property - def filename(self): + def filename(self) -> Optional[str]: """Returns the filename associated to the object. It is usually the name of the configuration file that was used when @@ -214,7 +218,7 @@ def filename(self): information.""" return self._filename - def _get(self, section, key): + def _get(self, section: str, key: str): """Internal function that returns the value of a given key in a given section.""" definition = self._definitions.get("%s.%s" % (section, key), {}) @@ -226,7 +230,7 @@ def _get(self, section, key): return getter(self._config, section, key) @staticmethod - def _item_to_section_key(item): + def _item_to_section_key(item: str) -> Tuple[str, str]: """Converts an item description to a section-key pair. @param item: the item to be converted @@ -241,7 +245,7 @@ def _item_to_section_key(item): section, key = "general", item return section, key - def __contains__(self, item): + def __contains__(self, item: str) -> bool: """Checks whether the given configuration item is set. @param item: the configuration key to check. @@ -250,7 +254,7 @@ def __contains__(self, item): section, key = self._item_to_section_key(item) return self._config.has_option(section, key) - def __getitem__(self, item): + def __getitem__(self, item: str): """Returns the given configuration item. @param item: the configuration key to retrieve. @@ -264,7 +268,7 @@ def __getitem__(self, item): else: return self._get(section, key) - def __setitem__(self, item, value): + def __setitem__(self, item: str, value): """Sets the given configuration item. @param item: the configuration key to set @@ -279,7 +283,7 @@ def __setitem__(self, item, value): setter = self._config.__class__.set return setter(self._config, section, key, value) - def __delitem__(self, item): + def __delitem__(self, item: str): """Deletes the given item from the configuration. If the item has a default value, the default value is written back instead @@ -292,14 +296,11 @@ def __delitem__(self, item): else: self._config.remove_option(section, key) - def has_key(self, item): + def has_key(self, item: str) -> bool: """Checks if the configuration has a given key. @param item: the key being sought""" - if "." in item: - section, key = item.split(".", 1) - else: - section, key = "general", item + section, key = self._item_to_section_key(item) return self._config.has_option(section, key) def load(self, stream=None): @@ -310,28 +311,40 @@ def load(self, stream=None): loaded. """ stream = stream or get_user_config_file() + if isinstance(stream, str): stream = open(stream, "r") file_was_open = True + else: + file_was_open = False + self._config.read_file(stream) - self._filename = getattr(stream, "name", None) + + filename = getattr(stream, "name", None) + self._filename = str(filename) if filename is not None else None + if file_was_open: stream.close() - def save(self, stream=None): + def save(self, stream: Optional[Union[str, IO[str]]]=None): """Saves the configuration. - @param stream: name of a file or a file object. The configuration will be saved - there. Can be omitted, in this case, the user-level configuration file will - be overwritten. + @param stream: name of a file or a file-like object. The configuration + will be saved there. Can be omitted, in this case, the user-level + configuration file will be overwritten. """ stream = stream or get_user_config_file() + if not hasattr(stream, "write") or not hasattr(stream, "close"): - stream = open(stream, "w") + stream = open(stream, "w") # type: ignore file_was_open = True - self._config.write(stream) + else: + file_was_open = False + + self._config.write(stream) # type: ignore + if file_was_open: - stream.close() + stream.close() # type: ignore @classmethod def instance(cls): @@ -347,12 +360,12 @@ def instance(cls): return cls._instance -def get_user_config_file(): +def get_user_config_file() -> str: """Returns the path where the user-level configuration file is stored""" return os.path.expanduser("~/.igraphrc") -def init(): +def init() -> Configuration: """Default mechanism to initiate igraph configuration This method loads the user-specific configuration file from the From 7fc470b1732c26e8d8a2500c6c4883a7ee34c4f3 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 26 Jan 2024 11:29:02 +0100 Subject: [PATCH 036/276] feat: added Graph.Prufer(), closes #750 --- CHANGELOG.md | 4 ++++ src/_igraph/graphobject.c | 47 +++++++++++++++++++++++++++++++++++++++ tests/test_conversion.py | 20 ++++++++++++++++- 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6317bc16c..cf3fe6078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [main] +### Added + +- Added `Graph.Prufer()` to construct a graph from a Prüfer sequence. + ### Fixed - Fixed import of `graph-tool` graphs for vertex properties where each property diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 5cef59ba8..9fdf8b289 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -3154,6 +3154,43 @@ PyObject *igraphmodule_Graph_Asymmetric_Preference(PyTypeObject * type, return (PyObject *) self; } + +/** \ingroup python_interface_graph + * \brief Generates a tree graph based on a Prufer sequenve + * \return a reference to the newly generated Python igraph object + * \sa igraph_from_prufer + */ +PyObject *igraphmodule_Graph_Prufer( + PyTypeObject * type, PyObject * args, PyObject * kwds +) { + igraphmodule_GraphObject *self; + igraph_t g; + PyObject *seq_o; + igraph_vector_int_t seq; + + static char *kwlist[] = { "seq", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &seq_o)) { + return NULL; + } + + if (igraphmodule_PyObject_to_vector_int_t(seq_o, &seq)) { + return NULL; + } + + if (igraph_from_prufer(&g, &seq)) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&seq); + return NULL; + } + + CREATE_GRAPH_FROM_TYPE(self, g, type); + + igraph_vector_int_destroy(&seq); + + return (PyObject *) self; +} + /** \ingroup python_interface_graph * \brief Generates a bipartite graph based on the Erdos-Renyi model * \return a reference to the newly generated Python igraph object @@ -14037,6 +14074,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param directed: whether to generate a directed graph.\n" "@param loops: whether loop edges are allowed.\n"}, + /* interface to igraph_from_prufer */ + {"Prufer", (PyCFunction) igraphmodule_Graph_Prufer, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Prufer(seq)\n--\n\n" + "Generates a tree from its Prufer sequence.\n\n" + "A Prufer sequence is a unique sequence of integers associated with a\n" + "labelled tree. A tree on M{n} vertices can be represented by a sequence\n" + "of M{n-2} integers, each between M{0} and M{n-1} (inclusive).\n\n" + "@param seq: the Prufer sequence as an iterable of integers\n"}, + /* interface to igraph_bipartite_game */ {"_Random_Bipartite", (PyCFunction) igraphmodule_Graph_Random_Bipartite, METH_VARARGS | METH_CLASS | METH_KEYWORDS, diff --git a/tests/test_conversion.py b/tests/test_conversion.py index 91838df3c..394f07cc7 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -172,6 +172,21 @@ def testGetSparseAdjacency(self): ) +class PruferTests(unittest.TestCase): + def testFromPrufer(self): + g = Graph.Prufer([3, 3, 3, 4]) + self.assertEqual(6, g.vcount()) + self.assertEqual(5, g.ecount()) + self.assertEqual( + [(0, 3), (1, 3), (2, 3), (3, 4), (4, 5)], + sorted(g.get_edgelist()) + ) + + def testToPrufer(self): + g = Graph([(0, 3), (1, 3), (2, 3), (3, 4), (4, 5)]) + self.assertEqual([3, 3, 3, 4], g.to_prufer()) + + def suite(): direction_suite = unittest.defaultTestLoader.loadTestsFromTestCase( DirectedUndirectedTests @@ -179,7 +194,10 @@ def suite(): representation_suite = unittest.defaultTestLoader.loadTestsFromTestCase( GraphRepresentationTests ) - return unittest.TestSuite([direction_suite, representation_suite]) + prufer_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + PruferTests + ) + return unittest.TestSuite([direction_suite, representation_suite, prufer_suite]) def test(): From f57bbb67886266758cd66ad8aa0bef542bc7cba8 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 26 Jan 2024 11:36:36 +0100 Subject: [PATCH 037/276] doc: fix docs of Graph.Adjacency() for the undirected case --- src/_igraph/graphobject.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 9fdf8b289..dc9161b3e 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13777,8 +13777,10 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param mode: the mode to be used. Possible values are:\n" "\n" " - C{\"directed\"} - the graph will be directed and a matrix\n" - " element gives the number of edges between two vertices.\n" - " - C{\"undirected\"} - alias to C{\"max\"} for convenience.\n" + " element specifies the number of edges between two vertices.\n" + " - C{\"undirected\"} - the graph will be undirected and a matrix\n" + " element specifies the number of edges between two vertices. The\n" + " input matrix must be symmetric.\n" " - C{\"max\"} - undirected graph will be created and the number of\n" " edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))}\n" " - C{\"min\"} - like C{\"max\"}, but with M{min(A(i,j), A(j,i))}\n" From 7f909293f9a82c378598dbf33ca104d26db383e0 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 26 Jan 2024 13:07:38 +0100 Subject: [PATCH 038/276] doc: also fix the doc of _construct_graph_from_weighted_adjacency() --- src/igraph/io/adjacency.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/igraph/io/adjacency.py b/src/igraph/io/adjacency.py index 5f4c3a5c4..ff83df5a6 100644 --- a/src/igraph/io/adjacency.py +++ b/src/igraph/io/adjacency.py @@ -15,9 +15,11 @@ def _construct_graph_from_adjacency(cls, matrix, mode="directed", loops="once"): - a pandas.DataFrame (column/row names must match, and will be used as vertex names). @param mode: the mode to be used. Possible values are: - - C{"directed"} - the graph will be directed and a matrix - element gives the number of edges between two vertex. - - C{"undirected"} - alias to C{"max"} for convenience. + - C{"directed"} - the graph will be directed and a matrix element + specifies the number of edges between two vertices. + - C{"undirected"} - the graph will be undirected and a matrix element + specifies the number of edges between two vertices. The matrix must + be symmetric. - C{"max"} - undirected graph will be created and the number of edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} - C{"min"} - like C{"max"}, but with M{min(A(i,j), A(j,i))} @@ -82,9 +84,11 @@ def _construct_graph_from_weighted_adjacency( - a scipy.sparse matrix (will be converted to a COO matrix, but not to a dense matrix) @param mode: the mode to be used. Possible values are: - - C{"directed"} - the graph will be directed and a matrix - element gives the number of edges between two vertex. - - C{"undirected"} - alias to C{"max"} for convenience. + - C{"directed"} - the graph will be directed and a matrix element + specifies the number of edges between two vertices. + - C{"undirected"} - the graph will be undirected and a matrix element + specifies the number of edges between two vertices. The matrix must + be symmetric. - C{"max"} - undirected graph will be created and the number of edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} - C{"min"} - like C{"max"}, but with M{min(A(i,j), A(j,i))} From 62a72bdd1d40135df04b763fb9f97989479dac73 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 26 Jan 2024 13:28:18 +0100 Subject: [PATCH 039/276] refactor: remove unneeded PyList type checks, Graph.Adjacency() now accepts Matrix instances --- src/_igraph/convert.c | 42 +++++++++++++++--- src/_igraph/convert.h | 13 ++++-- src/_igraph/graphobject.c | 89 +++++++++++++++++++------------------- src/_igraph/igraphmodule.c | 6 +-- src/igraph/datatypes.py | 4 ++ tests/test_conversion.py | 8 ++++ 6 files changed, 104 insertions(+), 58 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index c05a4eba2..82201f553 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -2594,10 +2594,10 @@ PyObject* igraphmodule_graph_list_t_to_PyList(igraph_graph_list_t *v, PyTypeObje * applicable. May be used in error messages. * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. */ -int igraphmodule_PyList_to_matrix_t( +int igraphmodule_PyObject_to_matrix_t( PyObject* o, igraph_matrix_t *m, const char *arg_name ) { - return igraphmodule_PyList_to_matrix_t_with_minimum_column_count(o, m, 0, arg_name); + return igraphmodule_PyObject_to_matrix_t_with_minimum_column_count(o, m, 0, arg_name); } /** @@ -2612,7 +2612,7 @@ int igraphmodule_PyList_to_matrix_t( * applicable. May be used in error messages. * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. */ -int igraphmodule_PyList_to_matrix_t_with_minimum_column_count( +int igraphmodule_PyObject_to_matrix_t_with_minimum_column_count( PyObject *o, igraph_matrix_t *m, int min_cols, const char *arg_name ) { Py_ssize_t nr, nc, n, i, j; @@ -2630,6 +2630,10 @@ int igraphmodule_PyList_to_matrix_t_with_minimum_column_count( } nr = PySequence_Size(o); + if (nr < 0) { + return 1; + } + nc = min_cols > 0 ? min_cols : 0; for (i = 0; i < nr; i++) { row = PySequence_GetItem(o, i); @@ -2644,18 +2648,30 @@ int igraphmodule_PyList_to_matrix_t_with_minimum_column_count( } n = PySequence_Size(row); Py_DECREF(row); + if (n < 0) { + return 1; + } if (n > nc) { nc = n; } } - igraph_matrix_init(m, nr, nc); + if (igraph_matrix_init(m, nr, nc)) { + igraphmodule_handle_igraph_error(); + return 1; + } + for (i = 0; i < nr; i++) { row = PySequence_GetItem(o, i); n = PySequence_Size(row); for (j = 0; j < n; j++) { item = PySequence_GetItem(row, j); + if (!item) { + igraph_matrix_destroy(m); + return 1; + } if (igraphmodule_PyObject_to_real_t(item, &value)) { + igraph_matrix_destroy(m); Py_DECREF(item); return 1; } @@ -2678,10 +2694,10 @@ int igraphmodule_PyList_to_matrix_t_with_minimum_column_count( * applicable. May be used in error messages. * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. */ -int igraphmodule_PyList_to_matrix_int_t( +int igraphmodule_PyObject_to_matrix_int_t( PyObject* o, igraph_matrix_int_t *m, const char* arg_name ) { - return igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count(o, m, 0, arg_name); + return igraphmodule_PyObject_to_matrix_int_t_with_minimum_column_count(o, m, 0, arg_name); } /** @@ -2696,7 +2712,7 @@ int igraphmodule_PyList_to_matrix_int_t( * applicable. May be used in error messages. * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. */ -int igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count( +int igraphmodule_PyObject_to_matrix_int_t_with_minimum_column_count( PyObject *o, igraph_matrix_int_t *m, int min_cols, const char* arg_name ) { Py_ssize_t nr, nc, n, i, j; @@ -2714,6 +2730,10 @@ int igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count( } nr = PySequence_Size(o); + if (nr < 0) { + return 1; + } + nc = min_cols > 0 ? min_cols : 0; for (i = 0; i < nr; i++) { row = PySequence_GetItem(o, i); @@ -2728,6 +2748,9 @@ int igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count( } n = PySequence_Size(row); Py_DECREF(row); + if (n < 0) { + return 1; + } if (n > nc) { nc = n; } @@ -2743,7 +2766,12 @@ int igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count( n = PySequence_Size(row); for (j = 0; j < n; j++) { item = PySequence_GetItem(row, j); + if (!item) { + igraph_matrix_int_destroy(m); + return 1; + } if (igraphmodule_PyObject_to_integer_t(item, &value)) { + igraph_matrix_int_destroy(m); Py_DECREF(item); return 1; } diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 006f0b652..3b35d2459 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -116,10 +116,15 @@ int igraphmodule_PyObject_to_edgelist( igraph_bool_t *list_is_owned ); -int igraphmodule_PyList_to_matrix_t(PyObject *o, igraph_matrix_t *m, const char *arg_name); -int igraphmodule_PyList_to_matrix_t_with_minimum_column_count(PyObject *o, igraph_matrix_t *m, int min_cols, const char *arg_name); -int igraphmodule_PyList_to_matrix_int_t(PyObject *o, igraph_matrix_int_t *m, const char *arg_name); -int igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count(PyObject *o, igraph_matrix_int_t *m, int min_cols, const char *arg_name); +int igraphmodule_PyObject_to_matrix_t( + PyObject *o, igraph_matrix_t *m, const char *arg_name); +int igraphmodule_PyObject_to_matrix_t_with_minimum_column_count( + PyObject *o, igraph_matrix_t *m, int min_cols, const char *arg_name); +int igraphmodule_PyObject_to_matrix_int_t( + PyObject *o, igraph_matrix_int_t *m, const char *arg_name); +int igraphmodule_PyObject_to_matrix_int_t_with_minimum_column_count( + PyObject *o, igraph_matrix_int_t *m, int min_cols, const char *arg_name); + PyObject* igraphmodule_strvector_t_to_PyList(igraph_strvector_t *v); int igraphmodule_PyList_to_strvector_t(PyObject* v, igraph_strvector_t *result); int igraphmodule_PyList_to_existing_strvector_t(PyObject* v, igraph_strvector_t *result); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index dc9161b3e..54e733817 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -1943,14 +1943,14 @@ PyObject *igraphmodule_Graph_Adjacency(PyTypeObject * type, igraphmodule_GraphObject *self; igraph_t g; igraph_matrix_t m; - PyObject *matrix, *mode_o = Py_None, *loops_o = Py_None; + PyObject *matrix_o, *mode_o = Py_None, *loops_o = Py_None; igraph_adjacency_t mode = IGRAPH_ADJ_DIRECTED; igraph_loops_t loops = IGRAPH_LOOPS_ONCE; static char *kwlist[] = { "matrix", "mode", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OO", kwlist, - &PyList_Type, &matrix, &mode_o, &loops_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, + &matrix_o, &mode_o, &loops_o)) return NULL; if (igraphmodule_PyObject_to_adjacency_t(mode_o, &mode)) @@ -1959,7 +1959,7 @@ PyObject *igraphmodule_Graph_Adjacency(PyTypeObject * type, if (igraphmodule_PyObject_to_loops_t(loops_o, &loops)) return NULL; - if (igraphmodule_PyList_to_matrix_t(matrix, &m, "matrix")) { + if (igraphmodule_PyObject_to_matrix_t(matrix_o, &m, "matrix")) { return NULL; } @@ -2281,9 +2281,8 @@ PyObject *igraphmodule_Graph_Establishment(PyTypeObject * type, char *kwlist[] = { "n", "k", "type_dist", "pref_matrix", "directed", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "nnO!O!|O", kwlist, - &n, &k, &PyList_Type, &type_dist, - &PyList_Type, &pref_matrix, &directed)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nnOO|O", kwlist, + &n, &k, &type_dist, &pref_matrix, &directed)) return NULL; if (n <= 0 || k <= 0) { @@ -2295,29 +2294,32 @@ PyObject *igraphmodule_Graph_Establishment(PyTypeObject * type, CHECK_SSIZE_T_RANGE(n, "vertex count"); CHECK_SSIZE_T_RANGE(k, "connection trials per set"); - types = PyList_Size(type_dist); - - if (igraphmodule_PyList_to_matrix_t(pref_matrix, &pm, "pref_matrix")) { + if (igraphmodule_PyObject_to_vector_t(type_dist, &td, 1)) { + PyErr_SetString(PyExc_ValueError, + "Error while converting type distribution vector"); return NULL; } + + if (igraphmodule_PyObject_to_matrix_t(pref_matrix, &pm, "pref_matrix")) { + igraph_vector_destroy(&td); + return NULL; + } + + types = igraph_vector_size(&td); + if (igraph_matrix_nrow(&pm) != igraph_matrix_ncol(&pm) || igraph_matrix_nrow(&pm) != types) { PyErr_SetString(PyExc_ValueError, "Preference matrix must have exactly the same rows and columns as the number of types"); - igraph_matrix_destroy(&pm); - return NULL; - } - if (igraphmodule_PyObject_to_vector_t(type_dist, &td, 1)) { - PyErr_SetString(PyExc_ValueError, - "Error while converting type distribution vector"); + igraph_vector_destroy(&td); igraph_matrix_destroy(&pm); return NULL; } if (igraph_establishment_game(&g, n, types, k, &td, &pm, PyObject_IsTrue(directed), 0)) { igraphmodule_handle_igraph_error(); - igraph_matrix_destroy(&pm); igraph_vector_destroy(&td); + igraph_matrix_destroy(&pm); return NULL; } @@ -2659,7 +2661,7 @@ PyObject *igraphmodule_Graph_Biadjacency(PyTypeObject * type, static char *kwlist[] = { "matrix", "directed", "mode", "multiple", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OOO", kwlist, &PyList_Type, &matrix_o, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, &matrix_o, &directed, &mode_o, &multiple)) return NULL; @@ -2670,7 +2672,7 @@ PyObject *igraphmodule_Graph_Biadjacency(PyTypeObject * type, return NULL; } - if (igraphmodule_PyList_to_matrix_t(matrix_o, &matrix, "matrix")) { + if (igraphmodule_PyObject_to_matrix_t(matrix_o, &matrix, "matrix")) { igraph_vector_bool_destroy(&vertex_types); return NULL; } @@ -2977,16 +2979,15 @@ PyObject *igraphmodule_Graph_Preference(PyTypeObject * type, { "n", "type_dist", "pref_matrix", "attribute", "directed", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "nO!O!|OOO", kwlist, - &n, &PyList_Type, &type_dist, - &PyList_Type, &pref_matrix, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nOO|OOO", kwlist, + &n, &type_dist, &pref_matrix, &attribute_key, &directed, &loops)) return NULL; CHECK_SSIZE_T_RANGE(n, "vertex count"); types = PyList_Size(type_dist); - if (igraphmodule_PyList_to_matrix_t(pref_matrix, &pm, "pref_matrix")) { + if (igraphmodule_PyObject_to_matrix_t(pref_matrix, &pm, "pref_matrix")) { return NULL; } if (igraphmodule_PyObject_float_to_vector_t(type_dist, &td)) { @@ -3070,18 +3071,18 @@ PyObject *igraphmodule_Graph_Asymmetric_Preference(PyTypeObject * type, char *kwlist[] = { "n", "type_dist_matrix", "pref_matrix", "attribute", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "nO!O!|OO", kwlist, - &n, &PyList_Type, &type_dist_matrix, - &PyList_Type, &pref_matrix, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nOO|OO", kwlist, + &n, &type_dist_matrix, + &pref_matrix, &attribute_key, &loops)) return NULL; CHECK_SSIZE_T_RANGE(n, "vertex count"); - if (igraphmodule_PyList_to_matrix_t(pref_matrix, &pm, "pref_matrix")) { + if (igraphmodule_PyObject_to_matrix_t(pref_matrix, &pm, "pref_matrix")) { return NULL; } - if (igraphmodule_PyList_to_matrix_t(type_dist_matrix, &td, "type_dist_matrix")) { + if (igraphmodule_PyObject_to_matrix_t(type_dist_matrix, &td, "type_dist_matrix")) { igraph_matrix_destroy(&pm); return NULL; } @@ -3375,15 +3376,15 @@ PyObject *igraphmodule_Graph_SBM(PyTypeObject * type, static char *kwlist[] = { "n", "pref_matrix", "block_sizes", "directed", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "nO!O!|OO", kwlist, - &n, &PyList_Type, &pref_matrix_o, - &PyList_Type, &block_sizes_o, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nOO|OO", kwlist, + &n, &pref_matrix_o, + &block_sizes_o, &directed_o, &loops_o)) return NULL; CHECK_SSIZE_T_RANGE(n, "vertex count"); - if (igraphmodule_PyList_to_matrix_t(pref_matrix_o, &pref_matrix, "pref_matrix")) { + if (igraphmodule_PyObject_to_matrix_t(pref_matrix_o, &pref_matrix, "pref_matrix")) { return NULL; } @@ -3746,8 +3747,8 @@ PyObject *igraphmodule_Graph_Weighted_Adjacency(PyTypeObject * type, static char *kwlist[] = { "matrix", "mode", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OO", kwlist, - &PyList_Type, &matrix, &mode_o, &loops_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, + &matrix, &mode_o, &loops_o)) return NULL; if (igraphmodule_PyObject_to_adjacency_t(mode_o, &mode)) @@ -3760,7 +3761,7 @@ PyObject *igraphmodule_Graph_Weighted_Adjacency(PyTypeObject * type, } else if (igraphmodule_PyObject_to_loops_t(loops_o, &loops)) return NULL; - if (igraphmodule_PyList_to_matrix_t(matrix, &m, "matrix")) { + if (igraphmodule_PyObject_to_matrix_t(matrix, &m, "matrix")) { return NULL; } @@ -6234,7 +6235,7 @@ PyObject *igraphmodule_Graph_permute_vertices(igraphmodule_GraphObject *self, igraphmodule_GraphObject *result_o; PyObject *list; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!", kwlist, &PyList_Type, &list)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &list)) return NULL; if (igraphmodule_PyObject_to_vector_int_t(list, &perm)) @@ -7790,7 +7791,7 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * } } else { use_seed = 1; - if (igraphmodule_PyList_to_matrix_t(seed_o, &m, "seed")) { + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { return NULL; } } @@ -7936,7 +7937,7 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel return NULL; } } else { - if (igraphmodule_PyList_to_matrix_t(seed_o, &m, "seed")) { + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { return NULL; } use_seed = 1; @@ -8005,7 +8006,7 @@ PyObject* igraphmodule_Graph_layout_drl(igraphmodule_GraphObject *self, return NULL; } } else { - if (igraphmodule_PyList_to_matrix_t(seed_o, &m, "seed")) { + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { return NULL; } use_seed = 1; @@ -8104,7 +8105,7 @@ PyObject return NULL; } } else { - if (igraphmodule_PyList_to_matrix_t(seed_o, &m, "seed")) { + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { return NULL; } use_seed = 1; @@ -8215,7 +8216,7 @@ PyObject *igraphmodule_Graph_layout_graphopt(igraphmodule_GraphObject *self, } } else { use_seed = 1; - if (igraphmodule_PyList_to_matrix_t(seed_o, &m, "seed")) { + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { return NULL; } } @@ -8324,7 +8325,7 @@ PyObject *igraphmodule_Graph_layout_mds(igraphmodule_GraphObject * self, PyErr_NoMemory(); return NULL; } - if (igraphmodule_PyList_to_matrix_t(dist_o, dist, "dist")) { + if (igraphmodule_PyObject_to_matrix_t(dist_o, dist, "dist")) { free(dist); return NULL; } @@ -8614,7 +8615,7 @@ PyObject *igraphmodule_Graph_layout_umap( } } else { use_seed = 1; - if (igraphmodule_PyList_to_matrix_t(seed_o, &m, "seed")) { + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { return NULL; } } @@ -9859,7 +9860,7 @@ PyObject *igraphmodule_Graph_isoclass(igraphmodule_GraphObject * self, char *kwlist[] = { "vertices", NULL }; if (!PyArg_ParseTupleAndKeywords - (args, kwds, "|O!", kwlist, &PyList_Type, &vids)) + (args, kwds, "|O", kwlist, &vids)) return NULL; if (vids) { diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index cff5df44e..97864f61e 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -355,10 +355,10 @@ PyObject* igraphmodule_community_to_membership(PyObject *self, igraph_vector_int_t result, csize, *csize_p = 0; Py_ssize_t nodes, steps; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!nn|O", kwlist, - &PyList_Type, &merges_o, &nodes, &steps, &return_csize)) return NULL; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "Onn|O", kwlist, + &merges_o, &nodes, &steps, &return_csize)) return NULL; - if (igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count(merges_o, &merges, 2, "merges")) { + if (igraphmodule_PyObject_to_matrix_int_t_with_minimum_column_count(merges_o, &merges, 2, "merges")) { return NULL; } diff --git a/src/igraph/datatypes.py b/src/igraph/datatypes.py index a8f26ed4c..a2446802d 100644 --- a/src/igraph/datatypes.py +++ b/src/igraph/datatypes.py @@ -184,6 +184,10 @@ def __isub__(self, other): for i in range(len(row)): row[i] -= other return self + + def __len__(self): + """Returns the number of rows in the matrix.""" + return len(self._data) def __ne__(self, other): """Checks whether a given matrix is not equal to another one""" diff --git a/tests/test_conversion.py b/tests/test_conversion.py index 394f07cc7..bbd9574c5 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -171,6 +171,14 @@ def testGetSparseAdjacency(self): np.all(g.get_adjacency_sparse() == np.array(g.get_adjacency().data)) ) + def testGetAdjacencyRoundtrip(self): + g = Graph.Tree(6, 3) + adj = g.get_adjacency() + g2 = Graph.Adjacency(adj, mode="undirected") + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(g.is_directed(), g2.is_directed()) + self.assertTrue(g.get_edgelist() == g2.get_edgelist()) + class PruferTests(unittest.TestCase): def testFromPrufer(self): From 53c7f590b4febe647b5a1f0058db78fa0f2496c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sun, 28 Jan 2024 11:14:58 +0000 Subject: [PATCH 040/276] docs: typo fix --- src/_igraph/graphobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 54e733817..fc8e62310 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -3157,7 +3157,7 @@ PyObject *igraphmodule_Graph_Asymmetric_Preference(PyTypeObject * type, /** \ingroup python_interface_graph - * \brief Generates a tree graph based on a Prufer sequenve + * \brief Generates a tree graph based on a Prufer sequence * \return a reference to the newly generated Python igraph object * \sa igraph_from_prufer */ From f4a3577348b887fb3bbd8e8dd0640a40dc1ae034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sun, 28 Jan 2024 11:20:44 +0000 Subject: [PATCH 041/276] docs: include 'diamond' vertex shape in tutorial --- doc/source/tutorial.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 49d113594..a552d4a25 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -792,8 +792,9 @@ Attribute name Keyword argument Purpose drawn first. --------------- ---------------------- ------------------------------------------ ``shape`` ``vertex_shape`` Shape of the vertex. Known shapes are: - ``rectangle``, ``circle``, ``hidden``, - ``triangle-up``, ``triangle-down``. + ``rectangle``, ``circle``, ``diamond``, + ``hidden``, ``triangle-up``, + ``triangle-down``. Several aliases are also accepted, see :data:`drawing.known_shapes`. --------------- ---------------------- ------------------------------------------ From dd69b1749fd07e8137e75060b5bcfffab8f29851 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 13:36:55 +0000 Subject: [PATCH 042/276] build(deps): bump pypa/cibuildwheel from 2.16.2 to 2.16.4 Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.16.2 to 2.16.4. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.16.2...v2.16.4) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85334aa0a..865d97278 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: python-version: '3.8' - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v2.16.4 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -39,7 +39,7 @@ jobs: CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v2.16.4 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" @@ -64,7 +64,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v2.16.4 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -89,7 +89,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v2.16.4 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -156,7 +156,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v2.16.4 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "python setup.py build_c_core" @@ -256,7 +256,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v2.16.4 env: CIBW_BEFORE_BUILD: "python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From 098584aa10704d86fc734937d3873f31cbfbcc32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Mon, 29 Jan 2024 22:26:39 +0000 Subject: [PATCH 043/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 4e04d3943..9d6f7a9d8 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 4e04d39438ac03bf177529e754a07b05beb0930b +Subproject commit 9d6f7a9d8eef9e7be36e03bf22177d093eaf0742 From 70c4f253aeb390eaa5e67fc44b8b92e55feefc2d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 30 Jan 2024 10:27:53 +0100 Subject: [PATCH 044/276] doc: add custom CSS to make tables wrap their text properly --- doc/source/_static/custom.css | 10 ++++++++++ doc/source/_static/other.css | 19 ------------------- doc/source/conf.py | 3 ++- 3 files changed, 12 insertions(+), 20 deletions(-) create mode 100644 doc/source/_static/custom.css delete mode 100644 doc/source/_static/other.css diff --git a/doc/source/_static/custom.css b/doc/source/_static/custom.css new file mode 100644 index 000000000..7c1a52022 --- /dev/null +++ b/doc/source/_static/custom.css @@ -0,0 +1,10 @@ +/* override table width restrictions */ +.wy-table-responsive table td, .wy-table-responsive table th { + white-space: normal; +} + +.wy-table-responsive { + margin-bottom: 24px; + max-width: 100%; + overflow: visible; +} diff --git a/doc/source/_static/other.css b/doc/source/_static/other.css deleted file mode 100644 index 54aa675fd..000000000 --- a/doc/source/_static/other.css +++ /dev/null @@ -1,19 +0,0 @@ -.highlight { - border-radius: 8px; -} - -.highlight pre { - padding: 8px; -} - -.navigation-header { - text-align: center; -} - -.bs-docs-section h1 { - display: none; -} - -.bs-docs-section h1.real { - display: block; -} diff --git a/doc/source/conf.py b/doc/source/conf.py index 17a25c204..1bfee14a2 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -136,7 +136,8 @@ def get_igraph_version(): # Inspired by pydoctor's RTD page itself # https://github.com/twisted/pydoctor/blob/master/docs/source/conf.py html_theme = "sphinx_rtd_theme" -html_static_path = [] +html_static_path = ["_static"] +html_css_files = ["custom.css"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". From bf32095c903c48da284beb8121da7a65c5afa4dd Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 30 Jan 2024 10:47:35 +0100 Subject: [PATCH 045/276] doc: clarify in the tutorial that `bbox` and `margin` works for Cairo only, closes #752 --- doc/source/tutorial.es.rst | 2 +- doc/source/tutorial.rst | 14 ++++++++------ doc/source/visualisation.rst | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/doc/source/tutorial.es.rst b/doc/source/tutorial.es.rst index 9ca6f88d5..5d2f59f05 100644 --- a/doc/source/tutorial.es.rst +++ b/doc/source/tutorial.es.rst @@ -543,7 +543,7 @@ Hmm, esto no es demasiado bonito hasta ahora. Una adición trivial sería usar l >>> color_dict = {"m": "blue", "f": "pink"} >>> g.vs["color"] = [color_dict[gender] for gender in g.vs["gender"]] >>> ig.plot(g, layout=layout, bbox=(300, 300), margin=20) # Cairo backend - >>> ig.plot(g, layout=layout, bbox=(300, 300), margin=20, target=ax) # matplotlib backend + >>> ig.plot(g, layout=layout, target=ax) # matplotlib backend Tenga en cuenta que aquí simplemente estamos reutilizando el objeto de diseño anterior, pero también hemos especificado que necesitamos un gráfico más pequeño (300 x 300 píxeles) y un margen mayor alrededor del grafo para que quepan las etiquetas (20 píxeles). El resultado es: diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index a552d4a25..e25523c66 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -705,11 +705,12 @@ from the ``label`` attribute by default and vertex colors are determined by the >>> color_dict = {"m": "blue", "f": "pink"} >>> g.vs["color"] = [color_dict[gender] for gender in g.vs["gender"]] >>> ig.plot(g, layout=layout, bbox=(300, 300), margin=20) # Cairo backend - >>> ig.plot(g, layout=layout, bbox=(300, 300), margin=20, target=ax) # matplotlib backend + >>> ig.plot(g, layout=layout, target=ax) # matplotlib backend -Note that we are simply re-using the previous layout object here, but we also specified -that we need a smaller plot (300 x 300 pixels) and a larger margin around the graph -to fit the labels (20 pixels). The result is: +Note that we are simply re-using the previous layout object here, but for the Cairo backend +we also specified that we need a smaller plot (300 x 300 pixels) and a larger margin around +the graph to fit the labels (20 pixels). These settings would be ignored for the Matplotlib +backend. The result is: .. figure:: figures/tutorial_social_network_2.png :alt: The visual representation of our social network - with names and genders @@ -869,7 +870,8 @@ Keyword argument Purpose ---------------- ---------------------------------------------------------------- ``bbox`` The bounding box of the plot. This must be a tuple containing the desired width and height of the plot. The default plot is - 600 pixels wide and 600 pixels high. + 600 pixels wide and 600 pixels high. Ignored for the + Matplotlib backend. ---------------- ---------------------------------------------------------------- ``layout`` The layout to be used. It can be an instance of :class:`~layout.Layout`, @@ -881,7 +883,7 @@ Keyword argument Purpose ``margin`` The top, right, bottom and left margins of the plot in pixels. This argument must be a list or tuple and its elements will be re-used if you specify a list or tuple with less than four - elements. + elements. Ignored for the Matplotlib backend. ================ ================================================================ Specifying colors in plots diff --git a/doc/source/visualisation.rst b/doc/source/visualisation.rst index 1963b85e4..7cad8bf9b 100644 --- a/doc/source/visualisation.rst +++ b/doc/source/visualisation.rst @@ -177,7 +177,7 @@ Plotting graphs in Jupyter notebooks |igraph| supports inline plots within a `Jupyter`_ notebook via both the `Cairo`_ and `matplotlib`_ backend. If you are calling `igraph.plot` from a notebook cell without a `matplotlib`_ axes, the image will be shown inline in the corresponding output cell. Use the `bbox` argument to scale the image while preserving the size of the vertices, text, and other artists. -For instance, to get a compact plot:: +For instance, to get a compact plot (using the Cairo backend only):: >>> ig.plot(g, bbox=(0, 0, 100, 100)) From 5a439f6fd08419428a5c5e6b5ec3686fffefe747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Tue, 30 Jan 2024 10:53:15 +0000 Subject: [PATCH 046/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 9d6f7a9d8..795992467 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 9d6f7a9d8eef9e7be36e03bf22177d093eaf0742 +Subproject commit 795992467202eccbe81910efef995d10f9f0c6b8 From c0d4755b0e56e4d456fbb731fde7b28213f08390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Tue, 30 Jan 2024 11:05:50 +0000 Subject: [PATCH 047/276] feat: is_biconnected() --- src/_igraph/graphobject.c | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index fc8e62310..d0c2aff54 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -1421,6 +1421,27 @@ PyObject *igraphmodule_Graph_is_connected(igraphmodule_GraphObject * self, Py_RETURN_FALSE; } +/** \ingroup python_interface_graph + * \brief Decides whether a graph is biconnected. + * \return Py_True if the graph is biconnected, Py_False otherwise + * \sa igraph_is_biconnected + */ +PyObject *igraphmodule_Graph_is_biconnected(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) +{ + igraph_bool_t res; + + if (igraph_is_biconnected(&self->g, &res)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (res) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} + /** \ingroup python_interface_graph * \brief Decides whether there is an edge from a given vertex to an other one. * \return Py_True if the vertices are directly connected, Py_False otherwise @@ -15275,6 +15296,20 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param mode: whether we should calculate strong or weak connectivity.\n" "@return: C{True} if the graph is connected, C{False} otherwise.\n"}, + /* interface to igraph_is_biconnected */ + {"is_biconnected", (PyCFunction) igraphmodule_Graph_is_biconnected, + METH_NOARGS, + "is_biconnected()\n--\n\n" + "Decides whether the graph is biconnected.\n\n" + "A graph is biconnected if it stays connected after the removal of\n" + "any single vertex.\n\n" + "Note that there are different conventions in use about whether to\n" + "consider a graph consisting of two connected vertices to be biconnected.\n" + "igraph does consider it biconnected.\n\n" + "@return: C{True} if it is biconnected, C{False} otherwise.\n" + "@rtype: boolean" + }, + /* interface to igraph_linegraph */ {"linegraph", (PyCFunction) igraphmodule_Graph_linegraph, METH_VARARGS | METH_KEYWORDS, From 52775558fcd0b9d46ec87a0ddc4284fdd87540f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Tue, 30 Jan 2024 11:25:11 +0000 Subject: [PATCH 048/276] tests: is_biconnected() --- tests/test_structural.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_structural.py b/tests/test_structural.py index 541a2c6c8..5ad20648c 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -242,6 +242,8 @@ class BiconnectedComponentTests(unittest.TestCase): g1 = Graph.Full(10) g2 = Graph(5, [(0, 1), (1, 2), (2, 3), (3, 4)]) g3 = Graph(6, [(0, 1), (1, 2), (2, 3), (3, 0), (2, 4), (2, 5), (4, 5)]) + g4 = Graph.Full(2) + g5 = Graph.Full(1) def testBiconnectedComponents(self): s = self.g1.biconnected_components() @@ -260,6 +262,14 @@ def testArticulationPoints(self): self.assertTrue(self.g1.articulation_points() == []) self.assertTrue(self.g2.cut_vertices() == [1, 2, 3]) self.assertTrue(self.g3.articulation_points() == [2]) + self.assertTrue(self.g4.articulation_points() == []) + + def testIsBiconnected(self): + self.assertTrue(self.g1.is_biconnected()) + self.assertFalse(self.g2.is_biconnected()) + self.assertFalse(self.g3.is_biconnected()) + self.assertTrue(self.g4.is_biconnected()) + self.assertFalse(self.g5.is_biconnected()) class CentralityTests(unittest.TestCase): From 6fdd2d88ec71fc9297bd49671376213a23023fc8 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 31 Jan 2024 21:20:48 +0100 Subject: [PATCH 049/276] doc: proper capitalization of Erdos-Renyi and Prufer in docstrings --- src/_igraph/graphobject.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index d0c2aff54..d6a669b60 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -3636,7 +3636,7 @@ PyObject *igraphmodule_Graph_Tree(PyTypeObject * type, * - directed is a bool that specifies if the edges should be directed. If so, they * point away from the root. * - method is one of: - * - 'Prufer' aka sample Pruefer sequences and convert to trees. + * - 'prufer' aka sample Prüfer sequences and convert to trees. * - 'lerw' aka loop-erased random walk on the complete graph to sample spanning * trees. * @@ -13946,7 +13946,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"Erdos_Renyi", (PyCFunction) igraphmodule_Graph_Erdos_Renyi, METH_VARARGS | METH_CLASS | METH_KEYWORDS, "Erdos_Renyi(n, p, m, directed=False, loops=False)\n--\n\n" - "Generates a graph based on the Erdos-Renyi model.\n\n" + "Generates a graph based on the Erdős-Rényi model.\n\n" "@param n: the number of vertices.\n" "@param p: the probability of edges. If given, C{m} must be missing.\n" "@param m: the number of edges. If given, C{p} must be missing.\n" @@ -14102,11 +14102,11 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"Prufer", (PyCFunction) igraphmodule_Graph_Prufer, METH_VARARGS | METH_CLASS | METH_KEYWORDS, "Prufer(seq)\n--\n\n" - "Generates a tree from its Prufer sequence.\n\n" - "A Prufer sequence is a unique sequence of integers associated with a\n" + "Generates a tree from its Prüfer sequence.\n\n" + "A Prüfer sequence is a unique sequence of integers associated with a\n" "labelled tree. A tree on M{n} vertices can be represented by a sequence\n" "of M{n-2} integers, each between M{0} and M{n-1} (inclusive).\n\n" - "@param seq: the Prufer sequence as an iterable of integers\n"}, + "@param seq: the Prüfer sequence as an iterable of integers\n"}, /* interface to igraph_bipartite_game */ {"_Random_Bipartite", (PyCFunction) igraphmodule_Graph_Random_Bipartite, @@ -14389,7 +14389,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param directed: whether the graph should be directed\n" "@param method: the generation method to be used. One of the following:\n" " \n" - " - C{\"prufer\"} -- samples Prufer sequences uniformly, then converts\n" + " - C{\"prufer\"} -- samples Prüfer sequences uniformly, then converts\n" " them to trees\n" " - C{\"lerw\"} -- performs a loop-erased random walk on the complete\n" " graph to uniformly sample its spanning trees (Wilson's algorithm).\n" @@ -15686,8 +15686,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { (PyCFunction) igraphmodule_Graph_to_prufer, METH_NOARGS, "to_prufer()\n--\n\n" - "Converts a tree graph into a Prufer sequence.\n\n" - "@return: the Prufer sequence as a list" + "Converts a tree graph into a Prüfer sequence.\n\n" + "@return: the Prüfer sequence as a list" }, // interface to igraph_transitivity_undirected From ce31917142201be7d6fafdbbf775a5418637fbc7 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 31 Jan 2024 21:56:03 +0100 Subject: [PATCH 050/276] ci: update cibuildwheel --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 865d97278..e9b7f0666 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: python-version: '3.8' - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.16.4 + uses: pypa/cibuildwheel@v2.16.5 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -39,7 +39,7 @@ jobs: CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.16.4 + uses: pypa/cibuildwheel@v2.16.5 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" @@ -64,7 +64,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.16.4 + uses: pypa/cibuildwheel@v2.16.5 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -89,7 +89,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.16.4 + uses: pypa/cibuildwheel@v2.16.5 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -156,7 +156,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.16.4 + uses: pypa/cibuildwheel@v2.16.5 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "python setup.py build_c_core" @@ -256,7 +256,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.16.4 + uses: pypa/cibuildwheel@v2.16.5 env: CIBW_BEFORE_BUILD: "python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From 063b3d48d89b8ffbf76655b87d6e4a923ce1d04d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Wed, 31 Jan 2024 21:45:32 +0000 Subject: [PATCH 051/276] docs: fix some typos --- src/_igraph/graphobject.c | 24 ++++++++++++------------ src/igraph/adjacency.py | 2 +- src/igraph/basic.py | 6 +++--- src/igraph/clustering.py | 8 ++++---- src/igraph/community.py | 8 ++++---- src/igraph/configuration.py | 4 ++-- src/igraph/formula.py | 4 ++-- src/igraph/layout.py | 2 +- src/igraph/statistics.py | 2 +- 9 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index d6a669b60..7a5f483cd 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -1925,8 +1925,8 @@ PyObject *igraphmodule_Graph_radius(igraphmodule_GraphObject * self, } /** \ingroup python_interface_graph - * \brief Converts a tree graph into a Prufer sequence - * \return the Prufer sequence as a Python object + * \brief Converts a tree graph into a Prüfer sequence + * \return the Prüfer sequence as a Python object * \sa igraph_to_prufer */ PyObject *igraphmodule_Graph_to_prufer(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) @@ -2024,7 +2024,7 @@ PyObject *igraphmodule_Graph_Atlas(PyTypeObject * type, PyObject * args) } /** \ingroup python_interface_graph - * \brief Generates a graph based on the Barabasi-Albert model + * \brief Generates a graph based on the Barabási-Albert model * This is intended to be a class method in Python, so the first argument * is the type object and not the Python igraph object (because we have * to allocate that in this method). @@ -3290,7 +3290,7 @@ PyObject *igraphmodule_Graph_Random_Bipartite(PyTypeObject * type, } /** \ingroup python_interface_graph - * \brief Generates a graph based on sort of a "windowed" Barabasi-Albert model + * \brief Generates a graph based on sort of a "windowed" Barabási-Albert model * \return a reference to the newly generated Python igraph object * \sa igraph_recent_degree_game */ @@ -13864,8 +13864,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_CLASS | METH_KEYWORDS, "Barabasi(n, m, outpref=False, directed=False, power=1,\n" " zero_appeal=1, implementation=\"psumtree\", start_from=None)\n--\n\n" - "Generates a graph based on the Barabasi-Albert model.\n\n" - "B{Reference}: Barabasi, A-L and Albert, R. 1999. Emergence of scaling\n" + "Generates a graph based on the Barabási-Albert model.\n\n" + "B{Reference}: Barabási, A-L and Albert, R. 1999. Emergence of scaling\n" "in random networks. I{Science}, 286 509-512.\n\n" "@param n: the number of vertices\n" "@param m: either the number of outgoing edges generated for\n" @@ -14084,7 +14084,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_CLASS | METH_KEYWORDS, "Preference(n, type_dist, pref_matrix, attribute=None, directed=False, loops=False)\n--\n\n" "Generates a graph based on vertex types and connection probabilities.\n\n" - "This is practically the nongrowing variant of L{Establishment}.\n" + "This is practically the non-growing variant of L{Establishment}.\n" "A given number of vertices are generated. Every vertex is assigned to a\n" "vertex type according to the given type probabilities. Finally, every\n" "vertex pair is evaluated and an edge is created between them with a\n" @@ -14140,7 +14140,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"SBM", (PyCFunction) igraphmodule_Graph_SBM, METH_VARARGS | METH_CLASS | METH_KEYWORDS, "SBM(n, pref_matrix, block_sizes, directed=False, loops=False)\n--\n\n" - "Generates a graph based on a stochastic blockmodel.\n\n" + "Generates a graph based on a stochastic block model.\n\n" "A given number of vertices are generated. Every vertex is assigned to a\n" "vertex type according to the given block sizes. Vertices of the same\n" "type will be assigned consecutive vertex IDs. Finally, every\n" @@ -14494,7 +14494,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "of the vertices.\n\n" "This coefficient is basically the correlation between the actual\n" "connectivity patterns of the vertices and the pattern expected from the\n" - "disribution of the vertex types.\n\n" + "distribution of the vertex types.\n\n" "See equation (21) in Newman MEJ: Mixing patterns in networks, Phys Rev E\n" "67:026126 (2003) for the proper definition. The actual calculation is\n" "performed using equation (26) in the same paper for directed graphs, and\n" @@ -14684,7 +14684,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "closeness(vertices=None, mode=\"all\", cutoff=None, weights=None, " "normalized=True)\n--\n\n" "Calculates the closeness centralities of given vertices in a graph.\n\n" - "The closeness centerality of a vertex measures how easily other\n" + "The closeness centrality of a vertex measures how easily other\n" "vertices can be reached from it (or the other way: how easily it\n" "can be reached from the other vertices). It is defined as the\n" "number of vertices minus one divided by the sum of\n" @@ -14717,7 +14717,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "harmonic_centrality(vertices=None, mode=\"all\", cutoff=None, weights=None, " "normalized=True)\n--\n\n" "Calculates the harmonic centralities of given vertices in a graph.\n\n" - "The harmonic centerality of a vertex measures how easily other\n" + "The harmonic centrality of a vertex measures how easily other\n" "vertices can be reached from it (or the other way: how easily it\n" "can be reached from the other vertices). It is defined as the\n" "mean inverse distance to all other vertices.\n\n" @@ -14744,7 +14744,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_KEYWORDS, "connected_components(mode=\"strong\")\n--\n\n" "Calculates the (strong or weak) connected components for a given graph.\n\n" - "Atttention: this function has a more convenient interface in class\n" + "Attention: this function has a more convenient interface in class\n" "L{Graph}, which wraps the result in a L{VertexClustering} object.\n" "It is advised to use that.\n" "@param mode: must be either C{\"strong\"} or C{\"weak\"}, depending on\n" diff --git a/src/igraph/adjacency.py b/src/igraph/adjacency.py index 9a55c1bc4..d94b246a3 100644 --- a/src/igraph/adjacency.py +++ b/src/igraph/adjacency.py @@ -128,7 +128,7 @@ def _get_adjlist(self, mode="out"): contains the neighbors of the given vertex. @param mode: if C{\"out\"}, returns the successors of the vertex. If - C{\"in\"}, returns the predecessors of the vertex. If C{\"all"\"}, both + C{\"in\"}, returns the predecessors of the vertex. If C{\"all\"}, both the predecessors and the successors will be returned. Ignored for undirected graphs. """ diff --git a/src/igraph/basic.py b/src/igraph/basic.py index 2944d3f2d..edc016d20 100644 --- a/src/igraph/basic.py +++ b/src/igraph/basic.py @@ -34,7 +34,7 @@ def _add_edges(graph, es, attributes=None): @param es: the list of edges to be added. Every edge is represented with a tuple containing the vertex IDs or names of the two endpoints. Vertices are enumerated from zero. - @param attributes: dict of sequences, all of length equal to the + @param attributes: dict of sequences, each of length equal to the number of edges to be added, containing the attributes of the new edges. """ @@ -92,7 +92,7 @@ def _add_vertices(graph, n, attributes=None): vertex to be added, or a sequence of strings, each corresponding to the name of a vertex to be added. Names will be assigned to the C{name} vertex attribute. - @param attributes: dict of sequences, all of length equal to the + @param attributes: dict of sequences, each of length equal to the number of vertices to be added, containing the attributes of the new vertices. If n is a string (so a single vertex is added), then the values of this dict are the attributes themselves, but if n=1 then @@ -136,7 +136,7 @@ def _delete_edges(graph, *args, **kwds): first positional argument is callable, an edge sequence is derived by calling L{EdgeSeq.select} with the same positional and keyword arguments. Edges in the derived edge sequence will be removed. - Otherwise the first positional argument is considered as follows: + Otherwise, the first positional argument is considered as follows: Deprecation notice: C{delete_edges(None)} has been replaced by C{delete_edges()} - with no arguments - since igraph 0.8.3. diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index b06e886e8..b45e6d9ce 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -452,7 +452,7 @@ def __plot__(self, backend, context, *args, **kwds): - C{True}: all the groups will be highlighted, the colors matching the corresponding color indices from the current palette - (see the C{palette} keyword argument of L{Graph.__plot__}. + (see the C{palette} keyword argument of L{Graph.__plot__}). - A dict mapping cluster indices or tuples of vertex indices to color names. The given clusters or vertex groups will be @@ -1151,7 +1151,7 @@ def __plot__(self, backend, context, *args, **kwds): - C{True}: all the clusters will be highlighted, the colors matching the corresponding color indices from the current palette - (see the C{palette} keyword argument of L{Graph.__plot__}. + (see the C{palette} keyword argument of L{Graph.__plot__}). - A dict mapping cluster indices or tuples of vertex indices to color names. The given clusters or vertex groups will be @@ -1371,7 +1371,7 @@ def _handle_mark_groups_arg_for_clustering(mark_groups, clustering): to clusters automatically. """ # Handle the case of mark_groups = True, mark_groups containing a list or - # tuple of cluster IDs, and and mark_groups yielding (cluster ID, color) + # tuple of cluster IDs, and mark_groups yielding (cluster ID, color) # pairs if mark_groups is True: group_iter = ((group, color) for color, group in enumerate(clustering)) @@ -1483,7 +1483,7 @@ def compare_communities(comm1, comm2, method="vi", remove_none=False): as a L{Clustering} object. @param method: the measure to use. C{"vi"} or C{"meila"} means the variation of information metric of Meila (2003), C{"nmi"} or C{"danon"} - means the normalized mutual information as defined by Danon et al (2005), + means the normalized mutual information as defined by Danon et al. (2005), C{"split-join"} means the split-join distance of van Dongen (2000), C{"rand"} means the Rand index of Rand (1971), C{"adjusted_rand"} means the adjusted Rand index of Hubert and Arabie (1985). diff --git a/src/igraph/community.py b/src/igraph/community.py index b890b97b2..e2dcb47da 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -46,7 +46,7 @@ def _community_infomap(graph, edge_weights=None, vertex_weights=None, trials=10) @param edge_weights: name of an edge attribute or a list containing edge weights. - @param vertex_weights: name of an vertex attribute or a list containing + @param vertex_weights: name of a vertex attribute or a list containing vertex weights. @param trials: the number of attempts to partition the network. @return: an appropriate L{VertexClustering} object with an extra attribute @@ -111,7 +111,7 @@ def _community_label_propagation(graph, weights=None, initial=None, fixed=None): Note that since ties are broken randomly, there is no guarantee that the algorithm returns the same community structure after each run. - In fact, they frequently differ. See the paper of Raghavan et al + In fact, they frequently differ. See the paper of Raghavan et al. on how to come up with an aggregated community structure. Also note that the community _labels_ (numbers) have no semantic meaning @@ -156,10 +156,10 @@ def _community_multilevel(graph, weights=None, return_levels=False, resolution=1 iteratively in a way that maximizes the vertices' local contribution to the overall modularity score. When a consensus is reached (i.e. no single move would increase the modularity score), every community in - the original graph is shrank to a single vertex (while keeping the + the original graph is shrunk to a single vertex (while keeping the total weight of the incident edges) and the process continues on the next level. The algorithm stops when it is not possible to increase - the modularity any more after shrinking the communities to vertices. + the modularity anymore after shrinking the communities to vertices. This algorithm is said to run almost in linear time on sparse graphs. diff --git a/src/igraph/configuration.py b/src/igraph/configuration.py index 6f507723e..d0202f541 100644 --- a/src/igraph/configuration.py +++ b/src/igraph/configuration.py @@ -125,7 +125,7 @@ def setboolean(obj, section, key, value): @param value: the value itself. C{0}, C{false}, C{no} and C{off} means false, C{1}, C{true}, C{yes} and C{on} means true, everything else results in a C{ValueError} being thrown. - Values are case insensitive + Values are case-insensitive """ value = str(value).lower() if value in ("0", "false", "no", "off"): @@ -234,7 +234,7 @@ def _item_to_section_key(item: str) -> Tuple[str, str]: """Converts an item description to a section-key pair. @param item: the item to be converted - @return: if C{item} contains a period (C{.}), it is splitted into two parts + @return: if C{item} contains a period (C{.}), it is split into two parts at the first period, then the two parts are returned, so the part before the period is the section. If C{item} does not contain a period, the section is assumed to be C{general}, and the second part of the returned diff --git a/src/igraph/formula.py b/src/igraph/formula.py index 8febe141a..c644b8d78 100644 --- a/src/igraph/formula.py +++ b/src/igraph/formula.py @@ -153,7 +153,7 @@ def construct_graph_from_formula(cls, formula=None, attr="name", simplify=True): + edges (vertex names): A->B - If you have may disconnected componnets, you can separate them + If you have many disconnected componnets, you can separate them with commas. You can also specify isolated vertices: >>> g = Graph.Formula("A--B, C--D, E--F, G--H, I, J, K") @@ -181,7 +181,7 @@ def construct_graph_from_formula(cls, formula=None, attr="name", simplify=True): @param formula: the formula itself @param attr: name of the vertex attribute where the vertex names will be stored - @param simplify: whether the simplify the constructed graph + @param simplify: whether to simplify the constructed graph @return: the constructed graph: """ diff --git a/src/igraph/layout.py b/src/igraph/layout.py index a846be7bd..f5b0a9181 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -38,7 +38,7 @@ class Layout: the vertices. It was particularly convenient for me to use the same layout for all of them, especially when I made figures for a paper. However, C{igraph} will of course refuse to draw a graph with a layout that has - less coordinates than the node count of the graph. + fewer coordinates than the node count of the graph. Layouts behave exactly like lists when they are accessed using the item index operator (C{[...]}). They can even be iterated through. Items diff --git a/src/igraph/statistics.py b/src/igraph/statistics.py index 98a53cc7f..e134f7751 100644 --- a/src/igraph/statistics.py +++ b/src/igraph/statistics.py @@ -530,7 +530,7 @@ def power_law_fit(data, xmin=None, method="auto", p_precision=0.01): size if n is small. - C{discrete}: exact maximum likelihood estimation when the - input comes from a discrete scale (see Clauset et al among the + input comes from a discrete scale (see Clauset et al. among the references). - C{auto}: exact maximum likelihood estimation where the continuous From cc632fff3b3e06d82fb92d7526910136b8ed1613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sat, 3 Feb 2024 22:43:12 +0000 Subject: [PATCH 052/276] chore: update C core to 0.10.9 --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 795992467..51dfb1403 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 795992467202eccbe81910efef995d10f9f0c6b8 +Subproject commit 51dfb14038eb534f66781f94263d9c04f2694294 From b85cf82d7879619492eeb868cfd55e810d40120a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sat, 3 Feb 2024 23:05:35 +0000 Subject: [PATCH 053/276] feat: Realize_Bipartite_Degree_Sequence() --- src/_igraph/graphobject.c | 86 +++++++++++++++++++++++++++++++++++++++ tests/test_generators.py | 61 +++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 7a5f483cd..5fe21c4c1 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2975,6 +2975,61 @@ PyObject *igraphmodule_Graph_Realize_Degree_Sequence(PyTypeObject *type, } +/** \ingroup python_interface_graph + * \brief Generates a graph with a specified degree sequence + * \return a reference to the newly generated Python igraph object + * \sa igraph_realize_bipartite_degree_sequence + */ +PyObject *igraphmodule_Graph_Realize_Bipartite_Degree_Sequence(PyTypeObject *type, + PyObject *args, PyObject *kwds) { + + igraph_vector_int_t degrees1, degrees2; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; + igraph_realize_degseq_t method = IGRAPH_REALIZE_DEGSEQ_SMALLEST; + PyObject *degrees1_o, *degrees2_o; + PyObject *edge_types_o = Py_None, *method_o = Py_None; + igraphmodule_GraphObject *self; + igraph_t g; + + static char *kwlist[] = { "degrees1", "degrees2", "allowed_edge_types", "method", NULL }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, + °rees1_o, °rees2_o, &edge_types_o, &method_o)) + return NULL; + + /* allowed edge types */ + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; + + /* methods */ + if (igraphmodule_PyObject_to_realize_degseq_t(method_o, &method)) + return NULL; + + /* First degree vector */ + if (igraphmodule_PyObject_to_vector_int_t(degrees1_o, °rees1)) + return NULL; + + /* Second degree vector */ + if (igraphmodule_PyObject_to_vector_int_t(degrees2_o, °rees2)) { + igraph_vector_int_destroy(°rees1); + return NULL; + } + + if (igraph_realize_bipartite_degree_sequence(&g, °rees1, °rees2, allowed_edge_types, method)) { + igraph_vector_int_destroy(°rees1); + igraph_vector_int_destroy(°rees2); + igraphmodule_handle_igraph_error(); + return NULL; + } + + igraph_vector_int_destroy(°rees1); + igraph_vector_int_destroy(°rees2); + + CREATE_GRAPH_FROM_TYPE(self, g, type); + + return (PyObject *) self; +} + + /** \ingroup python_interface_graph * \brief Generates a graph based on vertex types and connection preferences * \return a reference to the newly generated Python igraph object @@ -14247,6 +14302,37 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " See Horvát and Modes (2021) for details.\n" }, + {"Realize_Bipartite_Degree_Sequence", (PyCFunction) igraphmodule_Graph_Realize_Bipartite_Degree_Sequence, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Realize_Bipartite_Degree_Sequence(degrees1, degrees2, allowed_edge_types=\"simple\", method=\"smallest\")\n--\n\n" + "Generates a bipartite graph from the degree sequences of its partitions.\n" + "\n" + "This method implements a Havel-Hakimi style graph construction for biparite\n" + "graphs. In each step, the algorithm picks two vertices in a deterministic\n" + "manner and connects them. The way the vertices are picked is defined by the\n" + "C{method} parameter. The allowed edge types (i.e. whether multi-edges are allowed)\n" + "are specified in the C{allowed_edge_types} parameter. Self-loops are never created,\n" + "since a graph with self-loops is not bipartite.\n" + "\n" + "@param degrees1: the degrees of the first partition.\n" + "@param degrees2: the degrees of the second partition.\n" + "@param allowed_edge_types: controls whether multi-edges are allowed\n" + " during the generation process. Possible values are:\n" + "\n" + " - C{\"simple\"}: simple graphs (no multi-edges)\n" + " - C{\"multi\"}: multi-edges allowed\n" + "\n" + "@param method: controls how the vertices are selected during the generation\n" + " process. Possible values are:\n" + "\n" + " - C{smallest}: The vertex with smallest remaining degree first.\n" + " - C{largest}: The vertex with the largest remaining degree first.\n" + " - C{index}: The vertices are selected in order of their index.\n" + "\n" + " The smallest C{smallest} method is guaranteed to produce a connected graph,\n" + " if one exists." + }, + // interface to igraph_ring {"Ring", (PyCFunction) igraphmodule_Graph_Ring, METH_VARARGS | METH_CLASS | METH_KEYWORDS, diff --git a/tests/test_generators.py b/tests/test_generators.py index 31b8692bc..17205eb95 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -249,6 +249,67 @@ def testRealizeDegreeSequence(self): self.assertFalse(g.is_directed()) self.assertTrue(g.degree() == degrees) + def testRealizeBipartiteDegreeSequence(self): + deg1 = [2, 2] + deg2 = [1, 1, 2] + g = Graph.Realize_Bipartite_Degree_Sequence( + deg1, + deg2, + "simple", + "smallest", + ) + self.assertFalse(g.is_directed()) + self.assertTrue(g.is_connected()) + self.assertTrue(g.degree() == deg1 + deg2) + + g = Graph.Realize_Bipartite_Degree_Sequence( + deg1, + deg2, + "simple", + "largest", + ) + self.assertFalse(g.is_directed()) + self.assertTrue(g.degree() == deg1 + deg2) + + g = Graph.Realize_Bipartite_Degree_Sequence( + deg1, + deg2, + "simple", + "index", + ) + self.assertFalse(g.is_directed()) + self.assertTrue(g.degree() == deg1 + deg2) + + deg1 = [3, 1, 1] + deg2 = [2, 3] + self.assertRaises( + InternalError, + Graph.Realize_Bipartite_Degree_Sequence, + deg1, + deg2, + "simple", + "smallest", + ) + + self.assertRaises( + InternalError, + Graph.Realize_Bipartite_Degree_Sequence, + deg1, + deg2, + "simple", + "index", + ) + + g = Graph.Realize_Bipartite_Degree_Sequence( + deg1, + deg2, + "multi", + "smallest", + ) + self.assertFalse(g.is_directed()) + self.assertTrue(g.is_connected()) + self.assertTrue(g.degree() == deg1 + deg2) + def testKautz(self): g = Graph.Kautz(2, 2) deg_in = g.degree(mode="in") From a1708b882deaa5e19eeab7576812b95f4925a721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Sun, 4 Feb 2024 10:40:50 +0100 Subject: [PATCH 054/276] fix: make sure that degrees2 is not optional in Graph.Realize_Bipartite_Degree_Sequence --- src/_igraph/graphobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 5fe21c4c1..4315a6df0 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2992,7 +2992,7 @@ PyObject *igraphmodule_Graph_Realize_Bipartite_Degree_Sequence(PyTypeObject *typ igraph_t g; static char *kwlist[] = { "degrees1", "degrees2", "allowed_edge_types", "method", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OO", kwlist, °rees1_o, °rees2_o, &edge_types_o, &method_o)) return NULL; From 863ad514bd4067ef47c3eafb2c37bed44494e876 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 13 Feb 2024 17:13:46 +0100 Subject: [PATCH 055/276] chore: update C core to 0.10.10 --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 51dfb1403..2de2c37e3 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 51dfb14038eb534f66781f94263d9c04f2694294 +Subproject commit 2de2c37e353e15000d0608d2dd2f4b3c3595edb6 From 0982e7d506a45cbd74152d5911406726393a925f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 13 Feb 2024 17:18:49 +0100 Subject: [PATCH 056/276] chore: updated changelog [ci skip] --- CHANGELOG.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf3fe6078..03ca918b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,22 @@ # igraph Python interface changelog -## [main] +## [0.11.4] ### Added - Added `Graph.Prufer()` to construct a graph from a Prüfer sequence. +- Added `Graph.Bipartite_Degree_Sequence()` to construct a bipartite graph from + a bidegree sequence. + ### Fixed - Fixed import of `graph-tool` graphs for vertex properties where each property has a vector value. +- `Graph.Adjacency()` now accepts `Matrix` instances and other sequences as an + input, it is not limited to lists-of-lists-of-ints any more. + ## [0.11.3] - 2023-11-19 ### Added @@ -615,7 +621,8 @@ Please refer to the commit logs at https://github.com/igraph/python-igraph for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. -[main]: https://github.com/igraph/python-igraph/compare/0.11.3...main +[main]: https://github.com/igraph/python-igraph/compare/0.11.4...main +[0.11.4]: https://github.com/igraph/python-igraph/compare/0.11.3...0.11.4 [0.11.3]: https://github.com/igraph/python-igraph/compare/0.11.2...0.11.3 [0.11.2]: https://github.com/igraph/python-igraph/compare/0.11.0...0.11.2 [0.11.0]: https://github.com/igraph/python-igraph/compare/0.10.8...0.11.0 From 75d760d394865785308be4019120391eeb5979c1 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 13 Feb 2024 21:08:31 +0100 Subject: [PATCH 057/276] doc: clarify specification of same color for all vertices when plotting --- doc/source/sg_execution_times.rst | 109 ++++++++++++++++++++++++++++++ doc/source/tutorial.es.rst | 2 +- doc/source/tutorial.rst | 23 ++++++- 3 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 doc/source/sg_execution_times.rst diff --git a/doc/source/sg_execution_times.rst b/doc/source/sg_execution_times.rst new file mode 100644 index 000000000..a63741754 --- /dev/null +++ b/doc/source/sg_execution_times.rst @@ -0,0 +1,109 @@ + +:orphan: + +.. _sphx_glr_sg_execution_times: + + +Computation times +================= +**00:10.013** total execution time for 25 files **from all galleries**: + +.. container:: + + .. raw:: html + + + + + + + + .. list-table:: + :header-rows: 1 + :class: table table-striped sg-datatable + + * - Example + - Time + - Mem (MB) + * - :ref:`sphx_glr_tutorials_visualize_cliques.py` (``../examples_sphinx-gallery/visualize_cliques.py``) + - 00:02.970 + - 0.0 + * - :ref:`sphx_glr_tutorials_ring_animation.py` (``../examples_sphinx-gallery/ring_animation.py``) + - 00:01.287 + - 0.0 + * - :ref:`sphx_glr_tutorials_cluster_contraction.py` (``../examples_sphinx-gallery/cluster_contraction.py``) + - 00:00.759 + - 0.0 + * - :ref:`sphx_glr_tutorials_betweenness.py` (``../examples_sphinx-gallery/betweenness.py``) + - 00:00.735 + - 0.0 + * - :ref:`sphx_glr_tutorials_visual_style.py` (``../examples_sphinx-gallery/visual_style.py``) + - 00:00.711 + - 0.0 + * - :ref:`sphx_glr_tutorials_delaunay-triangulation.py` (``../examples_sphinx-gallery/delaunay-triangulation.py``) + - 00:00.504 + - 0.0 + * - :ref:`sphx_glr_tutorials_configuration.py` (``../examples_sphinx-gallery/configuration.py``) + - 00:00.416 + - 0.0 + * - :ref:`sphx_glr_tutorials_online_user_actions.py` (``../examples_sphinx-gallery/online_user_actions.py``) + - 00:00.332 + - 0.0 + * - :ref:`sphx_glr_tutorials_erdos_renyi.py` (``../examples_sphinx-gallery/erdos_renyi.py``) + - 00:00.313 + - 0.0 + * - :ref:`sphx_glr_tutorials_connected_components.py` (``../examples_sphinx-gallery/connected_components.py``) + - 00:00.216 + - 0.0 + * - :ref:`sphx_glr_tutorials_complement.py` (``../examples_sphinx-gallery/complement.py``) + - 00:00.201 + - 0.0 + * - :ref:`sphx_glr_tutorials_generate_dag.py` (``../examples_sphinx-gallery/generate_dag.py``) + - 00:00.194 + - 0.0 + * - :ref:`sphx_glr_tutorials_visualize_communities.py` (``../examples_sphinx-gallery/visualize_communities.py``) + - 00:00.176 + - 0.0 + * - :ref:`sphx_glr_tutorials_bridges.py` (``../examples_sphinx-gallery/bridges.py``) + - 00:00.169 + - 0.0 + * - :ref:`sphx_glr_tutorials_spanning_trees.py` (``../examples_sphinx-gallery/spanning_trees.py``) + - 00:00.161 + - 0.0 + * - :ref:`sphx_glr_tutorials_isomorphism.py` (``../examples_sphinx-gallery/isomorphism.py``) + - 00:00.153 + - 0.0 + * - :ref:`sphx_glr_tutorials_quickstart.py` (``../examples_sphinx-gallery/quickstart.py``) + - 00:00.142 + - 0.0 + * - :ref:`sphx_glr_tutorials_minimum_spanning_trees.py` (``../examples_sphinx-gallery/minimum_spanning_trees.py``) + - 00:00.137 + - 0.0 + * - :ref:`sphx_glr_tutorials_simplify.py` (``../examples_sphinx-gallery/simplify.py``) + - 00:00.079 + - 0.0 + * - :ref:`sphx_glr_tutorials_bipartite_matching_maxflow.py` (``../examples_sphinx-gallery/bipartite_matching_maxflow.py``) + - 00:00.073 + - 0.0 + * - :ref:`sphx_glr_tutorials_articulation_points.py` (``../examples_sphinx-gallery/articulation_points.py``) + - 00:00.067 + - 0.0 + * - :ref:`sphx_glr_tutorials_topological_sort.py` (``../examples_sphinx-gallery/topological_sort.py``) + - 00:00.058 + - 0.0 + * - :ref:`sphx_glr_tutorials_bipartite_matching.py` (``../examples_sphinx-gallery/bipartite_matching.py``) + - 00:00.058 + - 0.0 + * - :ref:`sphx_glr_tutorials_shortest_path_visualisation.py` (``../examples_sphinx-gallery/shortest_path_visualisation.py``) + - 00:00.052 + - 0.0 + * - :ref:`sphx_glr_tutorials_maxflow.py` (``../examples_sphinx-gallery/maxflow.py``) + - 00:00.052 + - 0.0 diff --git a/doc/source/tutorial.es.rst b/doc/source/tutorial.es.rst index 5d2f59f05..b5758c413 100644 --- a/doc/source/tutorial.es.rst +++ b/doc/source/tutorial.es.rst @@ -701,7 +701,7 @@ Especificación de colores en los gráficos ***Nombres de colores X11*** -Consulta la `lista de nombres de colores X11 `_ en Wikipedia para ver la lista completa. Los nombres de los colores no distinguen entre mayúsculas y minúsculas en |igraph|, por lo que "DarkBLue" puede escribirse también como "darkblue". +Consulta la `lista de nombres de colores X11 `_ en Wikipedia para ver la lista completa. Los nombres de los colores no distinguen entre mayúsculas y minúsculas en |igraph|, por lo que ``"DarkBLue"`` puede escribirse también como ``"darkblue"``. ***Especificación del color en la sintaxis CSS*** diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index e25523c66..ea2ce0e14 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -895,9 +895,9 @@ color (e.g., edge, vertex or label colors in the respective attributes): X11 color names See the `list of X11 color names `_ in Wikipedia for the complete list. Alternatively you can see the - keys of the igraph.drawing.colors.known_colors dictionary. Color - names are case insensitive in igraph so "DarkBlue" can be written as - "darkblue" as well. + keys of the ``igraph.drawing.colors.known_colors`` dictionary. Color + names are case insensitive in igraph so ``"DarkBlue"`` can be written as + ``"darkblue"`` as well. Color specification in CSS syntax This is a string according to one of the following formats (where *R*, *G* and @@ -913,6 +913,23 @@ Color specification in CSS syntax Lists or tuples of RGB values in the range 0-1 Example: ``(1.0, 0.5, 0)`` or ``[1.0, 0.5, 0]``. +Note that when specifying the same color for all vertices or edges, you can use +a string as-is but not the tuple or list syntax as tuples or lists would be +interpreted as if the *items* in the tuple are for individual vertices or +edges. So, this would work:: + + >>> ig.plot(g, vertex_color="green") + +But this would not, as it would treat the items in the tuple as palette indices +for the first, second and third vertoces:: + + >>> ig.plot(g, vertex_color=(1, 0, 0)) + +In this latter case, you need to expand the color specification for each vertex +explicitly:: + + >>> ig.plot(g, vertex_color=[(1, 0, 0)] * g.vcount()) + Saving plots ^^^^^^^^^^^^ From 9e0ddb14fc101018535b7022d46b9b453e1e0f50 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 13 Feb 2024 21:08:43 +0100 Subject: [PATCH 058/276] chore: bumped version to 0.11.4 --- src/igraph/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/igraph/version.py b/src/igraph/version.py index a75324513..4c4a3bf3a 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 11, 3) +__version_info__ = (0, 11, 4) __version__ = ".".join("{0}".format(x) for x in __version_info__) From f3a4fbce6dc5cfedd479f0ed0c41998551d192e2 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 14 Feb 2024 11:46:41 +0100 Subject: [PATCH 059/276] feat: add prefixattr=... argument to Graph.write_graphml(), fixes #759 --- src/_igraph/graphobject.c | 17 +++++++++++------ tests/test_foreign.py | 1 + 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 4315a6df0..488a55740 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -9728,18 +9728,19 @@ PyObject *igraphmodule_Graph_write_pajek(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_write_graphml(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - PyObject *fname = NULL; - static char *kwlist[] = { "f", NULL }; + PyObject *fname = NULL, *prefixattr_o = Py_True; + static char *kwlist[] = { "f", "prefixattr", NULL }; igraphmodule_filehandle_t fobj; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &fname)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &fname, &prefixattr_o)) return NULL; if (igraphmodule_filehandle_init(&fobj, fname, "w")) return NULL; - if (igraph_write_graph_graphml(&self->g, igraphmodule_filehandle_get(&fobj), - /*prefixattr=*/ 1)) { + if (igraph_write_graph_graphml( + &self->g, igraphmodule_filehandle_get(&fobj), PyObject_IsTrue(prefixattr_o) + )) { igraphmodule_handle_igraph_error(); igraphmodule_filehandle_destroy(&fobj); return NULL; @@ -16930,9 +16931,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_write_graph_edgelist */ {"write_graphml", (PyCFunction) igraphmodule_Graph_write_graphml, METH_VARARGS | METH_KEYWORDS, - "write_graphml(f)\n--\n\n" + "write_graphml(f, prefixattr=True)\n--\n\n" "Writes the graph to a GraphML file.\n\n" "@param f: the name of the file to be written or a Python file handle\n" + "@param prefixattr: whether attribute names in the written file should be\n" + " prefixed with C{g_}, C{v_} and C{e_} for graph, vertex and edge\n" + " attributes, respectively. This might be needed to ensure the uniqueness\n" + " of attribute identifiers in the written GraphML file.\n" }, /* interface to igraph_write_graph_leda */ {"write_leda", (PyCFunction) igraphmodule_Graph_write_leda, diff --git a/tests/test_foreign.py b/tests/test_foreign.py index 69657062e..72047dc1a 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -360,6 +360,7 @@ def testGraphML(self): self.assertTrue("name" in g.vertex_attributes()) g.write_graphml(tmpfname) + g.write_graphml(tmpfname, prefixattr=False) def testGraphMLz(self): with temporary_file( From ddd0fa6613906535d7b9c2b5b60ab81c521dc594 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 14 Feb 2024 11:47:44 +0100 Subject: [PATCH 060/276] chore: updated changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03ca918b0..32d6c5702 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # igraph Python interface changelog +## [master] + +### Added + +- Added a `prefixattr=...` keyword argument to `Graph.write_graphml()` that + allows the user to strip the `g_`, `v_` and `e_` prefixes from GraphML files + written by igraph. + ## [0.11.4] ### Added From 7825c7af1963d731c57057d12fa8df77f1c5d0f3 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 14 Feb 2024 11:52:59 +0100 Subject: [PATCH 061/276] refactor: Graph.are_connected() was renamed to Graph.are_adjacent() --- CHANGELOG.md | 6 ++++++ doc/source/analysis.rst | 2 +- src/_igraph/graphobject.c | 14 +++++++------- src/igraph/__init__.py | 8 ++++++++ tests/test_attributes.py | 2 +- tests/test_structural.py | 2 +- 6 files changed, 24 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32d6c5702..891995ccd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ allows the user to strip the `g_`, `v_` and `e_` prefixes from GraphML files written by igraph. +### Changed + +- `Graph.are_connected()` has now been renamed to `Graph.are_adjacent()`, + following up a similar change in the C core. The old name of the function + is deprecated but will be kept around until at least 0.12.0. + ## [0.11.4] ### Added diff --git a/doc/source/analysis.rst b/doc/source/analysis.rst index 943f719cc..bb911efad 100644 --- a/doc/source/analysis.rst +++ b/doc/source/analysis.rst @@ -63,7 +63,7 @@ To get the vertices at the two ends of an edge, use :attr:`Edge.source` and :att >>> v1, v2 = e.source, e.target Vice versa, to get the edge if from the source and target vertices, you can use :meth:`Graph.get_eid` or, for multiple pairs of source/targets, -:meth:`Graph.get_eids`. The boolean version, asking whether two vertices are directly connected, is :meth:`Graph.are_connected`. +:meth:`Graph.get_eids`. The boolean version, asking whether two vertices are directly connected, is :meth:`Graph.are_adjacent`. To get the edges incident on a vertex, you can use :meth:`Vertex.incident`, :meth:`Vertex.out_edges` and :meth:`Vertex.in_edges`. The three are equivalent on undirected graphs but not directed ones of course:: diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 488a55740..c79b4b479 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -1445,10 +1445,10 @@ PyObject *igraphmodule_Graph_is_biconnected(igraphmodule_GraphObject *self, PyOb /** \ingroup python_interface_graph * \brief Decides whether there is an edge from a given vertex to an other one. * \return Py_True if the vertices are directly connected, Py_False otherwise - * \sa igraph_are_connected + * \sa igraph_are_adjacent */ -PyObject *igraphmodule_Graph_are_connected(igraphmodule_GraphObject * self, - PyObject * args, PyObject * kwds) +PyObject *igraphmodule_Graph_are_adjacent(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) { static char *kwlist[] = { "v1", "v2", NULL }; PyObject *v1, *v2; @@ -1464,7 +1464,7 @@ PyObject *igraphmodule_Graph_are_connected(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_vid(v2, &idx2, &self->g)) return NULL; - if (igraph_are_connected(&self->g, idx1, idx2, &res)) + if (igraph_are_adjacent(&self->g, idx1, idx2, &res)) return igraphmodule_handle_igraph_error(); if (res) @@ -14554,10 +14554,10 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // STRUCTURAL PROPERTIES OF GRAPHS // ///////////////////////////////////// - // interface to igraph_are_connected - {"are_connected", (PyCFunction) igraphmodule_Graph_are_connected, + // interface to igraph_are_adjacent + {"are_adjacent", (PyCFunction) igraphmodule_Graph_are_adjacent, METH_VARARGS | METH_KEYWORDS, - "are_connected(v1, v2)\n--\n\n" + "are_adjacent(v1, v2)\n--\n\n" "Decides whether two given vertices are directly connected.\n\n" "@param v1: the ID or name of the first vertex\n" "@param v2: the ID or name of the second vertex\n" diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index c0997aa8c..7f0e328b7 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -948,6 +948,14 @@ def Incidence(cls, *args, **kwds): deprecated("Graph.Incidence() is deprecated; use Graph.Biadjacency() instead") return cls.Biadjacency(*args, **kwds) + def are_connected(self, *args, **kwds): + """Deprecated alias to L{Graph.are_adjacent()}.""" + deprecated( + "Graph.are_connected() is deprecated; use Graph.are_adjacent() " + "instead" + ) + return self.are_adjacent(*args, **kwds) + def get_incidence(self, *args, **kwds): """Deprecated alias to L{Graph.get_biadjacency()}.""" deprecated( diff --git a/tests/test_attributes.py b/tests/test_attributes.py index 4b6683d32..1299cada9 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -112,7 +112,7 @@ def testVertexNameIndexingBug196(self): g.add_vertices([a, b]) g.add_edges([(a, b)]) self.assertEqual(g.ecount(), 1) - self.assertTrue(g.are_connected(a, b)) + self.assertTrue(g.are_adjacent(a, b)) def testInvalidAttributeNames(self): g = Graph.Famous("bull") diff --git a/tests/test_structural.py b/tests/test_structural.py index 5ad20648c..bfda0f2e5 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -994,7 +994,7 @@ def testGetAllSimplePaths(self): self.assertEqual(15, path[-1]) curr = path[0] for next in path[1:]: - self.assertTrue(g.are_connected(curr, next)) + self.assertTrue(g.are_adjacent(curr, next)) curr = next def testPathLengthHist(self): From 9fc0372d638ceb158fb6c0211bec0ff3ccf7c592 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 19 Feb 2024 10:33:38 +0000 Subject: [PATCH 062/276] doc: clarify that we use natural logarithms for entropies --- src/igraph/clustering.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index b45e6d9ce..b1fca9ab4 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -1459,6 +1459,9 @@ def _ensure_list(obj): def compare_communities(comm1, comm2, method="vi", remove_none=False): """Compares two community structures using various distance measures. + For measures involving entropies (e.g., the variation of information metric), + igraph uses natural logarithms. + B{References} - Meila M: Comparing clusterings by the variation of information. In: From 12a376c1d4b4d239d0887b1f752c7c93c2cf0907 Mon Sep 17 00:00:00 2001 From: David R Connell Date: Wed, 6 Mar 2024 17:03:01 -0600 Subject: [PATCH 063/276] Update from deprecated PyCObject to PyCapsule --- src/_igraph/igraphmodule_api.h | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/src/_igraph/igraphmodule_api.h b/src/_igraph/igraphmodule_api.h index f7eeb932b..8ab364b50 100644 --- a/src/_igraph/igraphmodule_api.h +++ b/src/_igraph/igraphmodule_api.h @@ -56,25 +56,8 @@ extern "C" { /* Return -1 and set exception on error, 0 on success */ static int import_igraph(void) { - PyObject *c_api_object; - PyObject *module; - - module = PyImport_ImportModule("igraph._igraph"); - if (module == 0) - return -1; - - c_api_object = PyObject_GetAttrString(module, "_C_API"); - if (c_api_object == 0) { - Py_DECREF(module); - return -1; - } - - if (PyCObject_Check(c_api_object)) - PyIGraph_API = (void**)PyCObject_AsVoidPtr(c_api_object); - - Py_DECREF(c_api_object); - Py_DECREF(module); - return 0; + PyIGraph_API = (void **)PyCapsule_Import("igraph._igraph._C_API", 0); + return (PyIGraph_API != NULL) ? 0 : -1; } #endif From 833be5a1ba4a91d7788bd4af040a4ae42ce1a503 Mon Sep 17 00:00:00 2001 From: jpowell11 Date: Wed, 13 Mar 2024 18:30:18 -0500 Subject: [PATCH 064/276] Added clarification to the get_shortest_path() documentation --- src/_igraph/graphobject.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index c79b4b479..b10be661f 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -15122,6 +15122,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_KEYWORDS, "get_shortest_path(v, to, weights=None, mode=\"out\", output=\"vpath\", algorithm=\"auto\")\n--\n\n" "Calculates the shortest path from a source vertex to a target vertex in a graph.\n\n" + "This function only returns a single shortest path, to find all shortest paths consider using L{get_shortest_paths()}.\n\n" "@param v: the source vertex of the path\n" "@param to: the target vertex of the path\n" "@param weights: edge weights in a list or the name of an edge attribute\n" @@ -15139,7 +15140,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " algorithm automatically based on whether the graph has negative weights\n" " or not. C{\"dijkstra\"} uses Dijkstra's algorithm. C{\"bellman_ford\"}\n" " uses the Bellman-Ford algorithm. Ignored for unweighted graphs.\n" - "@return: see the documentation of the C{output} parameter.\n"}, + "@return: see the documentation of the C{output} parameter.\n" + "@see: L{get_shortest_paths()}\n"}, /* interface to igraph_get_shortest_paths */ {"get_shortest_paths", (PyCFunction) igraphmodule_Graph_get_shortest_paths, From 78c53f030a97327c30f02c42949ecb48797cd37e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Thu, 14 Mar 2024 17:03:48 +0100 Subject: [PATCH 065/276] doc: word wrapping in improved get_shortest_path() docs for the console --- src/_igraph/graphobject.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index b10be661f..44b617cf4 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -15122,7 +15122,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_KEYWORDS, "get_shortest_path(v, to, weights=None, mode=\"out\", output=\"vpath\", algorithm=\"auto\")\n--\n\n" "Calculates the shortest path from a source vertex to a target vertex in a graph.\n\n" - "This function only returns a single shortest path, to find all shortest paths consider using L{get_shortest_paths()}.\n\n" + "This function only returns a single shortest path. Consider using L{get_shortest_paths()}\n" + "to find all shortest paths between a source and one or more target vertices.\n\n" "@param v: the source vertex of the path\n" "@param to: the target vertex of the path\n" "@param weights: edge weights in a list or the name of an edge attribute\n" From 558612f4a02be015872ab58f1529d6e55915d793 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 14 Mar 2024 17:24:53 +0100 Subject: [PATCH 066/276] chore: updated contributors list --- .all-contributorsrc | 9 +++++++++ CONTRIBUTORS.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 78c931202..f8a0ea85d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -531,6 +531,15 @@ "contributions": [ "code" ] + }, + { + "login": "JDPowell648", + "name": "JDPowell648", + "avatar_url": "https://avatars.githubusercontent.com/u/41934552?v=4", + "profile": "https://github.com/JDPowell648", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 37b369f5d..5535a66ac 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -81,6 +81,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
szcf-weiya

💻
tristanlatr

💻 +
JDPowell648

📖 From 7fd469f0158d381bfc8fcda8b66dc0b9d213639f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:03:13 +0000 Subject: [PATCH 067/276] build(deps): bump pypa/cibuildwheel from 2.16.5 to 2.17.0 Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.16.5 to 2.17.0. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.16.5...v2.17.0) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e9b7f0666..f047f808a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: python-version: '3.8' - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@v2.17.0 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -39,7 +39,7 @@ jobs: CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@v2.17.0 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" @@ -64,7 +64,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@v2.17.0 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -89,7 +89,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@v2.17.0 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -156,7 +156,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@v2.17.0 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "python setup.py build_c_core" @@ -256,7 +256,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@v2.17.0 env: CIBW_BEFORE_BUILD: "python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From 7ce527528c94516cc05e86c5a8d5ce0cb1a71f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Monteiro=20de=20Moraes=20de=20Arruda=20Falc?= =?UTF-8?q?=C3=A3o?= Date: Wed, 20 Mar 2024 19:52:00 -0300 Subject: [PATCH 068/276] Bug fix in libraries.py The 'pop' function in line 58 is called wrongly, resulting in: TypeError: 'builtin_function_or_method' object is not subscriptable --- src/igraph/io/libraries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/igraph/io/libraries.py b/src/igraph/io/libraries.py index 52e47b8af..f35cc9545 100644 --- a/src/igraph/io/libraries.py +++ b/src/igraph/io/libraries.py @@ -55,7 +55,7 @@ def _export_graph_to_networkx( eattr["_igraph_index"] = i if multigraph and "_nx_multiedge_key" in eattr: - eattr["key"] = eattr.pop["_nx_multiedge_key"] + eattr["key"] = eattr.pop("_nx_multiedge_key") if vertex_attr_hashable in graph.vertex_attributes(): hashable_source = graph.vs[vertex_attr_hashable][edge.source] From 0be087a4ae781b17e1f29810804e4c2a12716efb Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 21 Mar 2024 08:35:29 +0100 Subject: [PATCH 069/276] chore: updated contributors list --- .all-contributorsrc | 45 +++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTORS.md | 7 +++++++ 2 files changed, 52 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index f8a0ea85d..6daaa80b3 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -540,6 +540,51 @@ "contributions": [ "doc" ] + }, + { + "login": "Adriankhl", + "name": "k.h.lai", + "avatar_url": "https://avatars.githubusercontent.com/u/16377650?v=4", + "profile": "https://github.com/Adriankhl", + "contributions": [ + "code" + ] + }, + { + "login": "gruebel", + "name": "Anton Grübel", + "avatar_url": "https://avatars.githubusercontent.com/u/33207684?v=4", + "profile": "https://github.com/gruebel", + "contributions": [ + "code" + ] + }, + { + "login": "flange-ipb", + "name": "flange-ipb", + "avatar_url": "https://avatars.githubusercontent.com/u/34936695?v=4", + "profile": "https://github.com/flange-ipb", + "contributions": [ + "code" + ] + }, + { + "login": "pmp-p", + "name": "Paul m. p. Peny", + "avatar_url": "https://avatars.githubusercontent.com/u/16009100?v=4", + "profile": "https://discuss.afpy.org/", + "contributions": [ + "code" + ] + }, + { + "login": "DavidRConnell", + "name": "David R. Connell", + "avatar_url": "https://avatars.githubusercontent.com/u/35470740?v=4", + "profile": "https://davidrconnell.github.io/", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5535a66ac..dc02942b4 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -82,6 +82,13 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
szcf-weiya

💻
tristanlatr

💻
JDPowell648

📖 +
k.h.lai

💻 +
Anton Grübel

💻 +
flange-ipb

💻 +
Paul m. p. Peny

💻 + + +
David R. Connell

💻 From 7ed868d8554f2e32a57c2b751935eb47b1b22025 Mon Sep 17 00:00:00 2001 From: Kreijstal Date: Sat, 20 Apr 2024 11:08:18 +0200 Subject: [PATCH 070/276] Linking with existing igraph on msys2 I've been thinking for a while if the msys2/mingw distribution should provide a package for python-igraph itself, but these are instructions to build from source, so it should suffice --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a94f33f5a..afa7e83e0 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,8 @@ where `[arch]` is either `Win32` for 32-bit builds or `x64` for 64-bit builds. Also, when building in MSYS2, you need to set the `SETUPTOOLS_USE_DISTUTILS` environment variable to `stdlib`; this is because MSYS2 uses a patched version of `distutils` that conflicts with `setuptools >= 60.0`. +Note: You need the following packages: +`$MINGW_PACKAGE_PREFIX-python-pip $MINGW_PACKAGE_PREFIX-python-setuptools $MINGW_PACKAGE_PREFIX-cc $MINGW_PACKAGE_PREFIX-cmake` ### Enabling GraphML @@ -158,8 +160,16 @@ the packaged igraph library instead of bringing its own copy. It is also useful on macOS if you want to link to the igraph library installed from Homebrew. -Due to the lack of support of `pkg-config` on Windows, it is currently not -possible to build against an external library on Windows. +Due to the lack of support of `pkg-config` on MSVC, it is currently not +possible to build against an external library on MSVC. + +In case you are already using a MSYS2/[MinGW](https://www.mingw-w64.org/) and already have +[mingw-w64-igraph](https://packages.msys2.org/base/mingw-w64-igraph) installed, +simply type: +``` +IGRAPH_USE_PKG_CONFIG=1 SETUPTOOLS_USE_DISTUTILS=stdlib pip install igraph +``` +to build. **Warning:** the Python interface is guaranteed to work only with the same version of the C core that is vendored inside the `vendor/source/igraph` From 96c6ff9418ebbc91b93b286f66987aa5f30a9f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Sat, 20 Apr 2024 14:35:01 +0200 Subject: [PATCH 071/276] doc: update README.md styling --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index afa7e83e0..45a127ca4 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,10 @@ where `[arch]` is either `Win32` for 32-bit builds or `x64` for 64-bit builds. Also, when building in MSYS2, you need to set the `SETUPTOOLS_USE_DISTUTILS` environment variable to `stdlib`; this is because MSYS2 uses a patched version of `distutils` that conflicts with `setuptools >= 60.0`. -Note: You need the following packages: -`$MINGW_PACKAGE_PREFIX-python-pip $MINGW_PACKAGE_PREFIX-python-setuptools $MINGW_PACKAGE_PREFIX-cc $MINGW_PACKAGE_PREFIX-cmake` + +> [!TIP] +> You need the following packages: +> `$MINGW_PACKAGE_PREFIX-python-pip $MINGW_PACKAGE_PREFIX-python-setuptools $MINGW_PACKAGE_PREFIX-cc $MINGW_PACKAGE_PREFIX-cmake` ### Enabling GraphML From 7507b87ae3d9c469a79904a398dae0d171f3632e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Tue, 23 Apr 2024 15:40:11 +0000 Subject: [PATCH 072/276] doc: clarify Weighted_Adjacency docs --- src/igraph/io/adjacency.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/igraph/io/adjacency.py b/src/igraph/io/adjacency.py index ff83df5a6..7f09ed167 100644 --- a/src/igraph/io/adjacency.py +++ b/src/igraph/io/adjacency.py @@ -78,6 +78,8 @@ def _construct_graph_from_weighted_adjacency( ): """Generates a graph from its weighted adjacency matrix. + Only edges with a non-zero weight are created. + @param matrix: the adjacency matrix. Possible types are: - a list of lists - a numpy 2D array or matrix (will be converted to list of lists) @@ -85,12 +87,12 @@ def _construct_graph_from_weighted_adjacency( to a dense matrix) @param mode: the mode to be used. Possible values are: - C{"directed"} - the graph will be directed and a matrix element - specifies the number of edges between two vertices. + specifies the weight of the corresponding edge. - C{"undirected"} - the graph will be undirected and a matrix element - specifies the number of edges between two vertices. The matrix must + specifies the weight of the corresponding edge. The matrix must be symmetric. - - C{"max"} - undirected graph will be created and the number of - edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} + - C{"max"} - undirected graph will be created and the weight of the + edge between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} - C{"min"} - like C{"max"}, but with M{min(A(i,j), A(j,i))} - C{"plus"} - like C{"max"}, but with M{A(i,j) + A(j,i)} - C{"upper"} - undirected graph with the upper right triangle of From 8a7c573e0935c947954c1c536031ce44b9748816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Mon, 29 Apr 2024 08:54:35 +0000 Subject: [PATCH 073/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 2de2c37e3..8b2d1c666 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 2de2c37e353e15000d0608d2dd2f4b3c3595edb6 +Subproject commit 8b2d1c666d0f53ffe65de5fd737866724e327d86 From 6c0098609c98932a44b66003b6a1de9e75d98414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Mon, 29 Apr 2024 09:15:29 +0000 Subject: [PATCH 074/276] chore: update C core (and pick up attempted fix for linking error) --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 8b2d1c666..45cb86394 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 8b2d1c666d0f53ffe65de5fd737866724e327d86 +Subproject commit 45cb86394a34021111fb6b071b48aa783930fdd9 From 8df1993999727fad969f6d0f1c1f5ff48ceec5d9 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 7 May 2024 15:13:22 +0200 Subject: [PATCH 075/276] chore: updated C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 45cb86394..9e7717014 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 45cb86394a34021111fb6b071b48aa783930fdd9 +Subproject commit 9e77170146f537ad44e81ca905548738a5a086a0 From e7b533b06959ebc50ecf4980c3c14e1422c38c1f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 7 May 2024 15:13:38 +0200 Subject: [PATCH 076/276] chore: updated pre-commit hooks --- .pre-commit-config.yaml | 13 +++---------- pyproject.toml | 5 ++--- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 516ea19ca..2b88275d5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,21 +1,14 @@ fail_fast: true repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.6.0 hooks: - - id: check-ast - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.275 + rev: v0.3.5 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - - - repo: https://github.com/psf/black - rev: 22.3.0 - hooks: - - id: black - exclude: ^doc/examples_sphinx-gallery/ - language_version: python3 + - id: ruff-format diff --git a/pyproject.toml b/pyproject.toml index 0d5932b7b..0ded74ac4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,5 @@ requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" [tool.ruff] -ignore = ["B905", "C901", "E402", "E501"] -line-length = 80 -select = ["B", "C", "E", "F", "W"] +lint.ignore = ["B905", "C901", "E402", "E501"] +lint.select = ["B", "C", "E", "F", "W"] From 28d78c1fd825d7e1a92838fdb9146787b44f5d03 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 7 May 2024 15:23:47 +0200 Subject: [PATCH 077/276] chore: bumped version to 0.11.5 --- CHANGELOG.md | 7 +++++-- src/igraph/version.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 891995ccd..f787db23f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # igraph Python interface changelog -## [master] +## [0.11.5] - 2024-05-07 ### Added @@ -14,6 +14,8 @@ following up a similar change in the C core. The old name of the function is deprecated but will be kept around until at least 0.12.0. +- The C core of igraph was updated to version 0.10.12. + ## [0.11.4] ### Added @@ -635,7 +637,8 @@ Please refer to the commit logs at https://github.com/igraph/python-igraph for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. -[main]: https://github.com/igraph/python-igraph/compare/0.11.4...main +[main]: https://github.com/igraph/python-igraph/compare/0.11.5...main +[0.11.5]: https://github.com/igraph/python-igraph/compare/0.11.4...0.11.5 [0.11.4]: https://github.com/igraph/python-igraph/compare/0.11.3...0.11.4 [0.11.3]: https://github.com/igraph/python-igraph/compare/0.11.2...0.11.3 [0.11.2]: https://github.com/igraph/python-igraph/compare/0.11.0...0.11.2 diff --git a/src/igraph/version.py b/src/igraph/version.py index 4c4a3bf3a..92c6ba6fb 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 11, 4) +__version_info__ = (0, 11, 5) __version__ = ".".join("{0}".format(x) for x in __version_info__) From d9e528c8bc81dfbdd2581c73a2918eff96345871 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 7 May 2024 19:33:00 +0200 Subject: [PATCH 078/276] chore: updated changelog based on Github suggestions --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f787db23f..b1d41e3d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,21 @@ - The C core of igraph was updated to version 0.10.12. +- Deprecated `PyCObject` API calls in the C code were replaced by calls to + `PyCapsule`, thanks to @DavidRConnell in + https://github.com/igraph/python-igraph/pull/763 + +- `get_shortest_path()` documentation was clarified by @JDPowell648 in + https://github.com/igraph/python-igraph/pull/764 + +- It is now possible to link to an existing igraph C core on MSYS2, thanks to + @Kreijstal in https://github.com/igraph/python-igraph/pull/770 + +### Fixed + +- Bugfix in the NetworkX graph conversion code by @rmmaf in + https://github.com/igraph/python-igraph/pull/767 + ## [0.11.4] ### Added From b656f940a1886f5bfc8f99e3d6b066563b3a0ebc Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 7 May 2024 19:36:36 +0200 Subject: [PATCH 079/276] chore: updated pre-commit hooks and contributors list --- .all-contributorsrc | 18 ++++++++++++++++++ .pre-commit-config.yaml | 1 + CONTRIBUTORS.md | 2 ++ README.md | 4 ++-- src/_igraph/graphobject.c | 2 +- src/igraph/datatypes.py | 2 +- tests/drawing/matplotlib/test_graph.py | 2 +- 7 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 6daaa80b3..68c701f9e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -585,6 +585,24 @@ "contributions": [ "code" ] + }, + { + "login": "rmmaf", + "name": "Rodrigo Monteiro de Moraes de Arruda Falcão", + "avatar_url": "https://avatars.githubusercontent.com/u/23747884?v=4", + "profile": "https://www.linkedin.com/in/rmmaf/", + "contributions": [ + "code" + ] + }, + { + "login": "Kreijstal", + "name": "Kreijstal", + "avatar_url": "https://avatars.githubusercontent.com/u/2415206?v=4", + "profile": "https://github.com/Kreijstal", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b88275d5..e67e6d125 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,7 @@ repos: rev: v4.6.0 hooks: - id: end-of-file-fixer + exclude: ^tests/drawing/plotly/baseline_images - id: trailing-whitespace - repo: https://github.com/charliermarsh/ruff-pre-commit diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index dc02942b4..f0945fc51 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -89,6 +89,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
David R. Connell

💻 +
Rodrigo Monteiro de Moraes de Arruda Falcão

💻 +
Kreijstal

💻 diff --git a/README.md b/README.md index 45a127ca4..6fb686d27 100644 --- a/README.md +++ b/README.md @@ -165,9 +165,9 @@ from Homebrew. Due to the lack of support of `pkg-config` on MSVC, it is currently not possible to build against an external library on MSVC. -In case you are already using a MSYS2/[MinGW](https://www.mingw-w64.org/) and already have +In case you are already using a MSYS2/[MinGW](https://www.mingw-w64.org/) and already have [mingw-w64-igraph](https://packages.msys2.org/base/mingw-w64-igraph) installed, -simply type: +simply type: ``` IGRAPH_USE_PKG_CONFIG=1 SETUPTOOLS_USE_DISTUTILS=stdlib pip install igraph ``` diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 44b617cf4..9045148c3 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2320,7 +2320,7 @@ PyObject *igraphmodule_Graph_Establishment(PyTypeObject * type, "Error while converting type distribution vector"); return NULL; } - + if (igraphmodule_PyObject_to_matrix_t(pref_matrix, &pm, "pref_matrix")) { igraph_vector_destroy(&td); return NULL; diff --git a/src/igraph/datatypes.py b/src/igraph/datatypes.py index a2446802d..b16e89cfe 100644 --- a/src/igraph/datatypes.py +++ b/src/igraph/datatypes.py @@ -184,7 +184,7 @@ def __isub__(self, other): for i in range(len(row)): row[i] -= other return self - + def __len__(self): """Returns the number of rows in the matrix.""" return len(self._data) diff --git a/tests/drawing/matplotlib/test_graph.py b/tests/drawing/matplotlib/test_graph.py index 4a83cda5a..a044d323e 100644 --- a/tests/drawing/matplotlib/test_graph.py +++ b/tests/drawing/matplotlib/test_graph.py @@ -156,7 +156,7 @@ def test_graph_with_curved_edges(self): ax.set_aspect(1.0) @image_comparison(baseline_images=["multigraph_with_curved_edges_undirected"]) - def test_graph_with_curved_edges(self): + def test_graph_with_curved_edges_undirected(self): plt.close("all") g = Graph.Ring(24, directed=False) g.add_edges([(0, 1), (1, 2)]) From 895804a83161ffe0710d5a91281f431971f446e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Fri, 10 May 2024 17:28:35 +0000 Subject: [PATCH 080/276] refactor: better error messages for non-existent attributes --- src/_igraph/attributes.c | 44 +++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 7079fa19f..d91eb84b5 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -1683,12 +1683,13 @@ igraph_error_t igraphmodule_i_attribute_get_type(const igraph_t *graph, int is_numeric, is_string, is_boolean; Py_ssize_t i, j; PyObject *o, *dict; + const char *attr_type_name; switch (elemtype) { - case IGRAPH_ATTRIBUTE_GRAPH: attrnum = ATTRHASH_IDX_GRAPH; break; - case IGRAPH_ATTRIBUTE_VERTEX: attrnum = ATTRHASH_IDX_VERTEX; break; - case IGRAPH_ATTRIBUTE_EDGE: attrnum = ATTRHASH_IDX_EDGE; break; - default: IGRAPH_ERROR("No such attribute type", IGRAPH_EINVAL); break; + case IGRAPH_ATTRIBUTE_GRAPH: attrnum = ATTRHASH_IDX_GRAPH; attr_type_name = "graph"; break; + case IGRAPH_ATTRIBUTE_VERTEX: attrnum = ATTRHASH_IDX_VERTEX; attr_type_name = "vertex"; break; + case IGRAPH_ATTRIBUTE_EDGE: attrnum = ATTRHASH_IDX_EDGE; attr_type_name = "edge"; break; + default: IGRAPH_ERROR("No such attribute type.", IGRAPH_EINVAL); break; } /* Get the attribute dict */ @@ -1697,12 +1698,12 @@ igraph_error_t igraphmodule_i_attribute_get_type(const igraph_t *graph, /* Check whether the attribute exists */ o = PyDict_GetItemString(dict, name); if (o == 0) { - IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + IGRAPH_ERRORF("No %s attribute named \"%s\" exists.", IGRAPH_EINVAL, attr_type_name, name); } /* Basic type check */ if (attrnum != ATTRHASH_IDX_GRAPH && !PyList_Check(o)) { - IGRAPH_ERROR("attribute hash type mismatch", IGRAPH_EINVAL); + IGRAPH_ERROR("Attribute hash type mismatch.", IGRAPH_EINVAL); } j = PyList_Size(o); @@ -1765,7 +1766,7 @@ igraph_error_t igraphmodule_i_get_boolean_graph_attr(const igraph_t *graph, attribute handler calls... hopefully :) Same applies for the other handlers. */ o = PyDict_GetItemString(dict, name); if (!o) { - IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + IGRAPH_ERRORF("No boolean graph attribute named \"%s\" exists.", IGRAPH_EINVAL, name); } IGRAPH_CHECK(igraph_vector_bool_resize(value, 1)); VECTOR(*value)[0] = PyObject_IsTrue(o); @@ -1781,7 +1782,7 @@ igraph_error_t igraphmodule_i_get_numeric_graph_attr(const igraph_t *graph, attribute handler calls... hopefully :) Same applies for the other handlers. */ o = PyDict_GetItemString(dict, name); if (!o) { - IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + IGRAPH_ERRORF("No numeric graph attribute named \"%s\" exists.", IGRAPH_EINVAL, name); } IGRAPH_CHECK(igraph_vector_resize(value, 1)); if (o == Py_None) { @@ -1806,7 +1807,7 @@ igraph_error_t igraphmodule_i_get_string_graph_attr(const igraph_t *graph, dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_GRAPH]; o = PyDict_GetItemString(dict, name); if (!o) { - IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + IGRAPH_ERRORF("No string graph attribute named \"%s\" exists.", IGRAPH_EINVAL, name); } IGRAPH_CHECK(igraph_strvector_resize(value, 1)); @@ -1853,7 +1854,9 @@ igraph_error_t igraphmodule_i_get_numeric_vertex_attr(const igraph_t *graph, dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; list = PyDict_GetItemString(dict, name); - if (!list) IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + if (!list) { + IGRAPH_ERRORF("No numeric vertex attribute named \"%s\" exists.", IGRAPH_EINVAL, name); + } if (igraph_vs_is_all(&vs)) { if (igraphmodule_PyObject_float_to_vector_t(list, &newvalue)) @@ -1895,8 +1898,9 @@ igraph_error_t igraphmodule_i_get_string_vertex_attr(const igraph_t *graph, dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; list = PyDict_GetItemString(dict, name); - if (!list) - IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + if (!list) { + IGRAPH_ERRORF("No string vertex attribute named \"%s\" exists.", IGRAPH_EINVAL, name); + } if (igraph_vs_is_all(&vs)) { if (igraphmodule_PyList_to_strvector_t(list, &newvalue)) @@ -1949,7 +1953,9 @@ igraph_error_t igraphmodule_i_get_boolean_vertex_attr(const igraph_t *graph, dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; list = PyDict_GetItemString(dict, name); - if (!list) IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + if (!list) { + IGRAPH_ERRORF("No boolean vertex attribute named \"%s\" exists.", IGRAPH_EINVAL, name); + } if (igraph_vs_is_all(&vs)) { if (igraphmodule_PyObject_to_vector_bool_t(list, &newvalue)) @@ -1985,7 +1991,9 @@ igraph_error_t igraphmodule_i_get_numeric_edge_attr(const igraph_t *graph, dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; list = PyDict_GetItemString(dict, name); - if (!list) IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + if (!list) { + IGRAPH_ERRORF("No numeric edge attribute named \"%s\" exists.", IGRAPH_EINVAL, name); + } if (igraph_es_is_all(&es)) { if (igraphmodule_PyObject_float_to_vector_t(list, &newvalue)) @@ -2025,7 +2033,9 @@ igraph_error_t igraphmodule_i_get_string_edge_attr(const igraph_t *graph, dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; list = PyDict_GetItemString(dict, name); - if (!list) IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + if (!list) { + IGRAPH_ERRORF("No string edge attribute named \"%s\" exists.", IGRAPH_EINVAL, name); + } if (igraph_es_is_all(&es)) { if (igraphmodule_PyList_to_strvector_t(list, &newvalue)) @@ -2077,7 +2087,9 @@ igraph_error_t igraphmodule_i_get_boolean_edge_attr(const igraph_t *graph, dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; list = PyDict_GetItemString(dict, name); - if (!list) IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); + if (!list) { + IGRAPH_ERRORF("No boolean edge attribute named \"%s\" exists.", IGRAPH_EINVAL, name); + } if (igraph_es_is_all(&es)) { if (igraphmodule_PyObject_to_vector_bool_t(list, &newvalue)) From ad2dbb5d4bcf59a6b3aaf824d9e9129da6d48738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Fri, 10 May 2024 17:30:29 +0000 Subject: [PATCH 081/276] chore: update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1d41e3d0..93329bec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # igraph Python interface changelog +### Changed + + - Error messages issued when an attribute is not found now mention the name and type of that attribute. + ## [0.11.5] - 2024-05-07 ### Added From d520c826f7f8e5cceb558df16ca2456d78e07967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sat, 18 May 2024 15:51:28 +0000 Subject: [PATCH 082/276] chore: add missing changelog entry on is_biconnected() --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93329bec9..a9036a3b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ - Added `Graph.Bipartite_Degree_Sequence()` to construct a bipartite graph from a bidegree sequence. +- Added `Graph.is_biconnected()` to check if a graph is biconnected. + ### Fixed - Fixed import of `graph-tool` graphs for vertex properties where each property From e5643fc6b1174ecc6cc805aa178a5aaf7210b746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sat, 18 May 2024 15:48:49 +0000 Subject: [PATCH 083/276] feat: add is_complete() --- CHANGELOG.md | 4 ++++ src/_igraph/graphobject.c | 29 +++++++++++++++++++++++++++++ src/igraph/adjacency.py | 2 +- tests/test_bipartite.py | 6 ++++++ tests/test_generators.py | 1 + 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9036a3b2..859944b4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # igraph Python interface changelog +### Added + + - Added `Graph.is_complete()` to test if there is a connection between all distinct pair of vertices. + ### Changed - Error messages issued when an attribute is not found now mention the name and type of that attribute. diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 9045148c3..92fc7def9 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -548,6 +548,25 @@ PyObject *igraphmodule_Graph_is_simple(igraphmodule_GraphObject* self, PyObject* } +/** \ingroup python_interface_graph + * \brief Checks whether an \c igraph.Graph object is a complete graph. + * \return \c True if the graph is complete, \c False otherwise. + * \sa igraph_is_complete + */ +PyObject *igraphmodule_Graph_is_complete(igraphmodule_GraphObject* self, PyObject* Py_UNUSED(_null)) { + igraph_bool_t res; + + if (igraph_is_complete(&self->g, &res)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (res) + Py_RETURN_TRUE; + Py_RETURN_FALSE; +} + + /** \ingroup python_interface_graph * \brief Determines whether a graph is a (directed or undirected) tree * \sa igraph_is_tree @@ -13625,6 +13644,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: C{True} if it is simple, C{False} otherwise.\n" "@rtype: boolean"}, + /* interface to igraph_is_complete */ + {"is_complete", (PyCFunction) igraphmodule_Graph_is_complete, + METH_NOARGS, + "is_complete()\n--\n\n" + "Checks whether the graph is complete, i.e. whether there is at least one\n" + "connection between all distinct pairs of vertices. In directed graphs,\n" + "ordered pairs are considered.\n\n" + "@return: C{True} if it is complete, C{False} otherwise.\n" + "@rtype: boolean"}, + /* interface to igraph_is_tree */ {"is_tree", (PyCFunction) igraphmodule_Graph_is_tree, METH_VARARGS | METH_KEYWORDS, diff --git a/src/igraph/adjacency.py b/src/igraph/adjacency.py index d94b246a3..b38889c81 100644 --- a/src/igraph/adjacency.py +++ b/src/igraph/adjacency.py @@ -140,7 +140,7 @@ def _get_biadjacency(graph, types="type", *args, **kwds): bipartite adjacency matrix is an M{n} times M{m} matrix, where M{n} and M{m} are the number of vertices in the two vertex classes. - @param types: an igraph vector containing the vertex types, or an + @param types: a vector containing the vertex types, or an attribute name. Anything that evalulates to C{False} corresponds to vertices of the first kind, everything else to the second kind. @return: the bipartite adjacency matrix and two lists in a triplet. The diff --git a/tests/test_bipartite.py b/tests/test_bipartite.py index 45cca2fc2..f3c5cc8c7 100644 --- a/tests/test_bipartite.py +++ b/tests/test_bipartite.py @@ -172,7 +172,9 @@ def testBipartiteProjection(self): g = Graph.Full_Bipartite(10, 5) g1, g2 = g.bipartite_projection() + self.assertTrue(g1.is_complete()) self.assertTrue(g1.isomorphic(Graph.Full(10))) + self.assertTrue(g2.is_complete()) self.assertTrue(g2.isomorphic(Graph.Full(5))) self.assertTrue(g.bipartite_projection(which=0).isomorphic(g1)) self.assertTrue(g.bipartite_projection(which=1).isomorphic(g2)) @@ -183,7 +185,9 @@ def testBipartiteProjection(self): self.assertTrue(g.bipartite_projection_size() == (10, 45, 5, 10)) g1, g2 = g.bipartite_projection(probe1=10) + self.assertTrue(g1.is_complete()) self.assertTrue(g1.isomorphic(Graph.Full(5))) + self.assertTrue(g2.is_complete()) self.assertTrue(g2.isomorphic(Graph.Full(10))) self.assertTrue(g.bipartite_projection(which=0).isomorphic(g2)) self.assertTrue(g.bipartite_projection(which=1).isomorphic(g1)) @@ -191,7 +195,9 @@ def testBipartiteProjection(self): self.assertTrue(g.bipartite_projection(which=True).isomorphic(g1)) g1, g2 = g.bipartite_projection(multiplicity=False) + self.assertTrue(g1.is_complete()) self.assertTrue(g1.isomorphic(Graph.Full(10))) + self.assertTrue(g2.is_complete()) self.assertTrue(g2.isomorphic(Graph.Full(5))) self.assertTrue(g.bipartite_projection(which=0).isomorphic(g1)) self.assertTrue(g.bipartite_projection(which=1).isomorphic(g2)) diff --git a/tests/test_generators.py b/tests/test_generators.py index 17205eb95..733a31c66 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -110,6 +110,7 @@ def testFull(self): g = Graph.Full(20, directed=True) el = g.get_edgelist() el.sort() + self.assertTrue(g.is_complete()) self.assertTrue( g.get_edgelist() == [(x, y) for x in range(20) for y in range(20) if x != y] ) From f990480267acaf06fe86a1ee6325df606e34567e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Tue, 21 May 2024 09:21:14 +0000 Subject: [PATCH 084/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 9e7717014..7bb245644 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 9e77170146f537ad44e81ca905548738a5a086a0 +Subproject commit 7bb245644d3dc2bbdaa3d7d38916218499422f82 From d89ccfa7e4c9af1e397670d7e80b130a4c7a9175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Tue, 21 May 2024 09:21:54 +0000 Subject: [PATCH 085/276] feat: add Chung_Lu() --- src/_igraph/convert.c | 14 ++++ src/_igraph/convert.h | 1 + src/_igraph/graphobject.c | 131 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 82201f553..a7711860e 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -514,6 +514,20 @@ int igraphmodule_PyObject_to_bliss_sh_t(PyObject *o, TRANSLATE_ENUM_WITH(bliss_sh_tt); } +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to an igraph \c igraph_chung_lu_t + */ +int igraphmodule_PyObject_to_chung_lu_t(PyObject *o, igraph_chung_lu_t *result) { + static igraphmodule_enum_translation_table_entry_t chung_lu_tt[] = { + {"original", IGRAPH_CHUNG_LU_ORIGINAL}, + {"grg", IGRAPH_CHUNG_LU_GRG}, + {"nr", IGRAPH_CHUNG_LU_NR}, + {0,0} + }; + TRANSLATE_ENUM_WITH(chung_lu_tt); +} + /** * \ingroup python_interface_conversion * \brief Converts a Python object to an igraph \c igraph_coloring_greedy_t diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 3b35d2459..54094684c 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -65,6 +65,7 @@ int igraphmodule_PyObject_to_attribute_combination_type_t(PyObject* o, int igraphmodule_PyObject_to_barabasi_algorithm_t(PyObject *o, igraph_barabasi_algorithm_t *result); int igraphmodule_PyObject_to_bliss_sh_t(PyObject *o, igraph_bliss_sh_t *result); +int igraphmodule_PyObject_to_chung_lu_t(PyObject *o, igraph_chung_lu_t *result); int igraphmodule_PyObject_to_coloring_greedy_t(PyObject *o, igraph_coloring_greedy_t *result); int igraphmodule_PyObject_to_community_comparison_t(PyObject *obj, igraph_community_comparison_t *result); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 92fc7def9..4bf150049 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2169,6 +2169,57 @@ PyObject *igraphmodule_Graph_Bipartite(PyTypeObject * type, return (PyObject *) self; } +/** \ingroup python_interface_graph + * \brief Generates a Chung-Lu random graph + * This is intended to be a class method in Python, so the first argument + * is the type object and not the Python igraph object (because we have + * to allocate that in this method). + * + * \return a reference to the newly generated Python igraph object + * \sa igraph_chung_lu_game + */ +PyObject *igraphmodule_Graph_Chung_Lu(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + igraphmodule_GraphObject *self; + igraph_t g; + igraph_vector_t outw, inw; + igraph_chung_lu_t var = IGRAPH_CHUNG_LU_ORIGINAL; + igraph_bool_t has_inw = false; + PyObject *weight_out = NULL, *weight_in = NULL, *loops = Py_True, *variant = NULL; + + static char *kwlist[] = { "out", "in_", "loops", "variant", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, + &weight_out, &weight_in, &loops, &variant)) + return NULL; + + if (igraphmodule_PyObject_to_chung_lu_t(variant, &var)) return NULL; + if (igraphmodule_PyObject_to_vector_t(weight_out, &outw, /* need_non_negative */ true)) return NULL; + if (weight_in) { + if (igraphmodule_PyObject_to_vector_t(weight_in, &inw, /* need_non_negative */ true)) { + igraph_vector_destroy(&outw); + return NULL; + } + has_inw=true; + } + + if (igraph_chung_lu_game(&g, &outw, has_inw ? &inw : NULL, PyObject_IsTrue(loops), var)) { + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(&outw); + if (has_inw) + igraph_vector_destroy(&inw); + return NULL; + } + + igraph_vector_destroy(&outw); + if (has_inw) + igraph_vector_destroy(&inw); + + CREATE_GRAPH_FROM_TYPE(self, g, type); + + return (PyObject *) self; +} + /** \ingroup python_interface_graph * \brief Generates a De Bruijn graph * \sa igraph_kautz @@ -14439,6 +14490,86 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " this is the case, also its orientation. Must be one of\n" " C{\"in\"}, C{\"out\"} and C{\"undirected\"}.\n"}, + /* interface to igraph_chung_lu_game */ + {"Chung_Lu", (PyCFunction) igraphmodule_Graph_Chung_Lu, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Chung_Lu(out, in_=None, loops=True, variant=\"original\")\n--\n\n" + "Generates a Chung-Lu random graph.\n\n" + "In the Chung-Lu model, each pair of vertices M{i} and M{j} is connected with\n" + "independent probability M{p_{ij} = w_i w_j / S}, where M{w_i} is a weight\n" + "associated with vertex M{i} and M{S = \\sum_k w_k} is the sum of weights.\n" + "In the directed variant, vertices have both out-weights, M{w^\\text{out}},\n" + "and in-weights, M{w^\\text{in}}, with equal sums,\n" + "M{S = \\sum_k w^\\text{out}_k = \\sum_k w^\\text{in}_k}. The connection\n" + "probability between M{i} and M{j} is M{p_{ij} = w^\\text{out}_i w^\\text{in}_j / S}.\n\n" + "This model is commonly used to create random graphs with a fixed I{expected}\n" + "degree sequence. The expected degree of vertex M{i} is approximately equal\n" + "to the weight M{w_i}. Specifically, if the graph is directed and self-loops\n" + "are allowed, then the expected out- and in-degrees are precisely M{w^\\text{out}}\n" + "and M{w^\\text{in}}. If self-loops are disallowed, then the expected out-\n" + "and in-degrees are M{w^\\text{out} (S - w^\\text{in}) / S} and\n" + "M{w^\\text{in} (S - w^\\text{out}) / S}, respectively. If the graph is\n" + "undirected, then the expected degrees with and without self-loops are\n" + "M{w (S + w) / S} and M{w (S - w) / S}, respectively.\n\n" + "A limitation of the original Chung-Lu model is that when some of the\n" + "weights are large, the formula for M{p_{ij}} yields values larger than 1.\n" + "Chung and Lu's original paper exludes the use of such weights. When\n" + "M{p_{ij} > 1}, this function simply issues a warning and creates\n" + "a connection between M{i} and M{j}. However, in this case the expected degrees\n" + "will no longer relate to the weights in the manner stated above. Thus the\n" + "original Chung-Lu model cannot produce certain (large) expected degrees.\n\n" + "The overcome this limitation, this function implements additional variants of\n" + "the model, with modified expressions for the connection probability M{p_{ij}}\n" + "between vertices M{i} and M{j}. Let M{q_{ij} = w_i w_j / S}, or\n" + "M{q_{ij} = w^out_i w^in_j / S} in the directed case. All model\n" + "variants become equivalent in the limit of sparse graphs where M{q_{ij}}\n" + "approaches zero. In the original Chung-Lu model, selectable by setting\n" + "C{variant} to C{\"original\"}, M{p_{ij} = min(q_{ij}, 1)}.\n" + "The C{\"grg\"} variant, often referred to a the generalized\n" + "random graph, uses M{p_{ij} = q_{ij} / (1 + q_{ij})}, and is equivalent\n" + "to a maximum entropy model (i.e. exponential random graph model) with\n" + "a constraint on expected degrees, see Park and Newman (2004), Section B,\n" + "setting M{exp(-\\Theta_{ij}) = w_i w_j / S}. This model is also\n" + "discussed by Britton, Deijfen and Martin-Löf (2006). By virtue of being\n" + "a degree-constrained maximum entropy model, it generates graphs having\n" + "the same degree sequence with the same probability.\n" + "A third variant can be requested with C{\"nr\"}, and uses\n" + "M{p_{ij} = 1 - exp(-q_{ij})}. This is the underlying simple graph\n" + "of a multigraph model introduced by Norros and Reittu (2006).\n" + "For a discussion of these three model variants, see Section 16.4 of\n" + "Bollobás, Janson, Riordan (2007), as well as Van Der Hofstad (2013).\n\n" + "B{References:}\n\n" + " - Chung F and Lu L: Connected components in a random graph with given degree sequences.\n" + " I{Annals of Combinatorics} 6, 125-145 (2002) U{https://doi.org/10.1007/PL00012580}\n" + " - Miller JC and Hagberg A: Efficient Generation of Networks with Given Expected Degrees (2011)\n" + " U{https://doi.org/10.1007/978-3-642-21286-4_10}\n" + " - Park J and Newman MEJ: Statistical mechanics of networks.\n" + " I{Physical Review E} 70, 066117 (2004). U{https://doi.org/10.1103/PhysRevE.70.066117}\n" + " - Britton T, Deijfen M, Martin-Löf A: Generating Simple Random Graphs with Prescribed Degree Distribution.\n" + " I{J Stat Phys} 124, 1377–1397 (2006). U{https://doi.org/10.1007/s10955-006-9168-x}\n" + " - Norros I and Reittu H: On a conditionally Poissonian graph process.\n" + " I{Advances in Applied Probability} 38, 59–75 (2006).\n" + " U{https://doi.org/10.1239/aap/1143936140}\n" + " - Bollobás B, Janson S, Riordan O: The phase transition in inhomogeneous random graphs.\n" + " I{Random Struct Algorithms} 31, 3–122 (2007). U{https://doi.org/10.1002/rsa.20168}\n" + " - Van Der Hofstad R: Critical behavior in inhomogeneous random graphs.\n" + " I{Random Struct Algorithms} 42, 480–508 (2013). U{https://doi.org/10.1002/rsa.20450}\n\n" + "@param out: the vertex weights (or out-weights). In sparse graphs\n" + " these will be approximately equal to the expected (out-)degrees.\n" + "@param in_: the vertex in-weights, approximately equal to the expected\n" + " in-degrees of the graph. If omitted, the generated graph will be\n" + " undirected.\n" + "@param loops: whether to allow the generation of self-loops.\n" + "@param variant: the model variant to be used. Let M{q_{ij}=w_i w_j / S},\n" + " where M{S = \\sum_k w_k}. The following variants are available:\n" + " \n" + " - C{\"original\"} -- the original Chung-Lu model with\n" + " M{p_{ij} = min(1, q_{ij})}.\n" + " - C{\"grg\"} -- generalized random graph, a maximum entropy model with\n" + " a soft constraint on degrees, M{p_{ij} = q_{ij} / (1 + q_{ij})}\n" + " - C{\"nr\"} -- Norros and Reittu's model, M{p_{ij} = 1 - exp(-q_{ij})}\n" + }, + /* interface to igraph_degree_sequence_game */ {"Degree_Sequence", (PyCFunction) igraphmodule_Graph_Degree_Sequence, METH_VARARGS | METH_CLASS | METH_KEYWORDS, From 1408a530e8f19a845d41bb0f2a2f7672b9de6914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Wed, 29 May 2024 10:06:38 +0000 Subject: [PATCH 086/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 7bb245644..392313cbc 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 7bb245644d3dc2bbdaa3d7d38916218499422f82 +Subproject commit 392313cbc54abfd27918c27b3ba8a9d4f9f544d3 From a98021ed4871e62d5208c4b17b391661e500bb9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sun, 2 Jun 2024 23:18:47 +0000 Subject: [PATCH 087/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 392313cbc..febc90642 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 392313cbc54abfd27918c27b3ba8a9d4f9f544d3 +Subproject commit febc90642002c6ea10a53d42d3ce005eed959477 From 7ed03140fc7d41d66cb591a93bebd159227a4c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sun, 9 Jun 2024 20:33:31 +0000 Subject: [PATCH 088/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index febc90642..78228dc28 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit febc90642002c6ea10a53d42d3ce005eed959477 +Subproject commit 78228dc28e8b29daf8c66f021afc9b05336d55bd From 8f3a3e47f8659024fabc8a5eea2375310c818e81 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 15 Jun 2024 20:02:41 +0200 Subject: [PATCH 089/276] doc: remove section about pycairo from Christoph Gohlke's page as pycairo now provides wheels for Windows --- doc/source/install.rst | 7 ------- 1 file changed, 7 deletions(-) diff --git a/doc/source/install.rst b/doc/source/install.rst index 81bf35d5b..91fcf6f18 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -174,13 +174,6 @@ you need to install Cairo headers using your package manager (Linux) or `homebre $ pip install pycairo -The Cairo project does not provide pre-compiled binaries for Windows, but Christoph Gohlke -maintains a site containing unofficial Windows binaries for several Python extension packages, -including Cairo. Therefore, the easiest way to install Cairo on Windows along with its Python bindings -is simply to download it from `Christoph's site `_. -Make sure you use an installer that is suitable for your Windows platform (32-bit or 64-bit) and the -version of Python you are using. - To check if Cairo is installed correctly on your system, run the following example:: >>> import igraph as ig From d6cea15e6ebaa79acadb40ef31939aab7f31cb6a Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 17 Jun 2024 16:12:04 +0200 Subject: [PATCH 090/276] ci: install setuptools explicitly --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f047f808a..9c6bdab2a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: - name: Build wheels (manylinux) uses: pypa/cibuildwheel@v2.17.0 env: - CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip wheel && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" # Skip tests for Python 3.10 onwards because SciPy does not have # 32-bit wheels for Linux @@ -41,7 +41,7 @@ jobs: - name: Build wheels (musllinux) uses: pypa/cibuildwheel@v2.17.0 env: - CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip wheel && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test-musl]' && python -m pytest -v tests" @@ -66,7 +66,7 @@ jobs: - name: Build wheels (manylinux) uses: pypa/cibuildwheel@v2.17.0 env: - CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip wheel && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 CIBW_BUILD: "*-manylinux_aarch64" @@ -91,7 +91,7 @@ jobs: - name: Build wheels (musllinux) uses: pypa/cibuildwheel@v2.17.0 env: - CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip wheel && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 CIBW_BUILD: "*-musllinux_aarch64" CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test-musl]' && python -m pytest -v tests" From 1b5ed56df591b004d0d6e7be6a75fca25502405f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 17 Jun 2024 16:30:53 +0200 Subject: [PATCH 091/276] ci: install setuptools explicitly for macOS and Windows as well --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9c6bdab2a..c6d634aff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -159,7 +159,7 @@ jobs: uses: pypa/cibuildwheel@v2.17.0 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" - CIBW_BEFORE_BUILD: "python setup.py build_c_core" + CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_ENVIRONMENT: "LDFLAGS=-L$HOME/local/lib" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_arch }} ${{ matrix.cmake_extra_args }} -DCMAKE_PREFIX_PATH=$HOME/local @@ -258,7 +258,7 @@ jobs: - name: Build wheels uses: pypa/cibuildwheel@v2.17.0 env: - CIBW_BEFORE_BUILD: "python setup.py build_c_core" + CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" CIBW_TEST_COMMAND: "cd /d {project} && pip install --prefer-binary \".[test]\" && python -m pytest tests" # Skip tests for Python 3.10 onwards because SciPy does not have From 655310610bbcd9cabc23ef6f91c7c068e6ccdd5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 14:31:28 +0000 Subject: [PATCH 092/276] build(deps): bump pypa/cibuildwheel from 2.17.0 to 2.19.1 Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.17.0 to 2.19.1. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.17.0...v2.19.1) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c6d634aff..760081073 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: python-version: '3.8' - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -39,7 +39,7 @@ jobs: CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" @@ -64,7 +64,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -89,7 +89,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -156,7 +156,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" @@ -256,7 +256,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.17.0 + uses: pypa/cibuildwheel@v2.19.1 env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From 86febe456c76b9a363777592ed6abc09d81b3f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sat, 22 Jun 2024 09:43:34 -0400 Subject: [PATCH 093/276] chore: update C core and adapt Graph.Chung_Lu to changes --- src/_igraph/convert.c | 2 +- src/_igraph/graphobject.c | 10 +++++----- vendor/source/igraph | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index a7711860e..44fe318cf 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -521,7 +521,7 @@ int igraphmodule_PyObject_to_bliss_sh_t(PyObject *o, int igraphmodule_PyObject_to_chung_lu_t(PyObject *o, igraph_chung_lu_t *result) { static igraphmodule_enum_translation_table_entry_t chung_lu_tt[] = { {"original", IGRAPH_CHUNG_LU_ORIGINAL}, - {"grg", IGRAPH_CHUNG_LU_GRG}, + {"maxent", IGRAPH_CHUNG_LU_MAXENT}, {"nr", IGRAPH_CHUNG_LU_NR}, {0,0} }; diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 4bf150049..31049e887 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -14495,8 +14495,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_CLASS | METH_KEYWORDS, "Chung_Lu(out, in_=None, loops=True, variant=\"original\")\n--\n\n" "Generates a Chung-Lu random graph.\n\n" - "In the Chung-Lu model, each pair of vertices M{i} and M{j} is connected with\n" - "independent probability M{p_{ij} = w_i w_j / S}, where M{w_i} is a weight\n" + "In the original Chung-Lu model, each pair of vertices M{i} and M{j} is connected\n" + "with independent probability M{p_{ij} = w_i w_j / S}, where M{w_i} is a weight\n" "associated with vertex M{i} and M{S = \\sum_k w_k} is the sum of weights.\n" "In the directed variant, vertices have both out-weights, M{w^\\text{out}},\n" "and in-weights, M{w^\\text{in}}, with equal sums,\n" @@ -14525,7 +14525,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "variants become equivalent in the limit of sparse graphs where M{q_{ij}}\n" "approaches zero. In the original Chung-Lu model, selectable by setting\n" "C{variant} to C{\"original\"}, M{p_{ij} = min(q_{ij}, 1)}.\n" - "The C{\"grg\"} variant, often referred to a the generalized\n" + "The C{\"grg\"} variant, sometimes referred to a the generalized\n" "random graph, uses M{p_{ij} = q_{ij} / (1 + q_{ij})}, and is equivalent\n" "to a maximum entropy model (i.e. exponential random graph model) with\n" "a constraint on expected degrees, see Park and Newman (2004), Section B,\n" @@ -14565,8 +14565,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " \n" " - C{\"original\"} -- the original Chung-Lu model with\n" " M{p_{ij} = min(1, q_{ij})}.\n" - " - C{\"grg\"} -- generalized random graph, a maximum entropy model with\n" - " a soft constraint on degrees, M{p_{ij} = q_{ij} / (1 + q_{ij})}\n" + " - C{\"maxent\"} -- maximum entropy model with fixed expected degrees\n" + " M{p_{ij} = q_{ij} / (1 + q_{ij})}\n" " - C{\"nr\"} -- Norros and Reittu's model, M{p_{ij} = 1 - exp(-q_{ij})}\n" }, diff --git a/vendor/source/igraph b/vendor/source/igraph index 78228dc28..ad645f8f9 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 78228dc28e8b29daf8c66f021afc9b05336d55bd +Subproject commit ad645f8f9bc0cb1530457a40a01828f03f584fd4 From 40d841665b9ebc59403c11e2116e8cb672513174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Fri, 28 Jun 2024 13:56:09 +0000 Subject: [PATCH 094/276] chore: update C core to 0.10.13 --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index ad645f8f9..e1d48f89a 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit ad645f8f9bc0cb1530457a40a01828f03f584fd4 +Subproject commit e1d48f89a08b7423761504d868aa1dd57c0e0557 From 2ca659a996102a36fd08f102f7af4f6970bf9c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Fri, 28 Jun 2024 17:27:59 +0000 Subject: [PATCH 095/276] chore: typo in changelog [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 859944b4f..47eb6a50a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Added - - Added `Graph.is_complete()` to test if there is a connection between all distinct pair of vertices. + - Added `Graph.is_complete()` to test if there is a connection between all distinct pairs of vertices. ### Changed From 64a48dd175ba0672a26502cab62a9cc951e8fafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Fri, 28 Jun 2024 18:01:45 +0000 Subject: [PATCH 096/276] chore: update changelog [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47eb6a50a..f93dfadfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Added + - Added `Graph.Chung_Lu()` for sampling from the Chung-Lu model as well as several related models. - Added `Graph.is_complete()` to test if there is a connection between all distinct pairs of vertices. ### Changed From 7b21e32e0a93151e53d21fa869abb84fc7c476bc Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 1 Jul 2024 18:21:15 +0200 Subject: [PATCH 097/276] fix: make sure to compile igrpah with LTO when available --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 11d4cd967..1610a5f1d 100644 --- a/setup.py +++ b/setup.py @@ -283,6 +283,9 @@ def _compile_in( for deps in "ARPACK BLAS GLPK GMP LAPACK".split(): args.append("-DIGRAPH_USE_INTERNAL_" + deps + "=ON") + # Use link-time optinization if available + args.append("-DIGRAPH_ENABLE_LTO=AUTO") + # -fPIC is needed on Linux so we can link to a static igraph lib from a # Python shared library args.append("-DCMAKE_POSITION_INDEPENDENT_CODE=ON") From f686b299439e455cd69cd23222dae9db8fde8f2e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 1 Jul 2024 20:10:58 +0200 Subject: [PATCH 098/276] ci: trying to switch to uv-based build backend for cibuildwheel --- .github/workflows/build.yml | 18 ++++++++++++++++++ pyproject.toml | 4 ++++ 2 files changed, 22 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 760081073..6c69e2a38 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,6 +29,9 @@ jobs: with: python-version: '3.8' + - uses: yezz123/setup-uv@v4 + name: Install uv + - name: Build wheels (manylinux) uses: pypa/cibuildwheel@v2.19.1 env: @@ -63,6 +66,9 @@ jobs: id: qemu uses: docker/setup-qemu-action@v3 + - uses: yezz123/setup-uv@v4 + name: Install uv + - name: Build wheels (manylinux) uses: pypa/cibuildwheel@v2.19.1 env: @@ -88,6 +94,9 @@ jobs: id: qemu uses: docker/setup-qemu-action@v3 + - uses: yezz123/setup-uv@v4 + name: Install uv + - name: Build wheels (musllinux) uses: pypa/cibuildwheel@v2.19.1 env: @@ -139,6 +148,9 @@ jobs: with: python-version: '3.8' + - uses: yezz123/setup-uv@v4 + name: Install uv + - name: Install OS dependencies if: steps.cache-c-core.outputs.cache-hit != 'true' || steps.cache-c-deps.outputs.cache-hit != 'true' # Only needed when building the C core or libomp run: @@ -182,6 +194,9 @@ jobs: with: python-version: '3.11.2' + - uses: yezz123/setup-uv@v4 + name: Install uv + - name: Install OS dependencies run: sudo apt install ninja-build cmake flex bison @@ -232,6 +247,9 @@ jobs: with: python-version: '3.8' + - uses: yezz123/setup-uv@v4 + name: Install uv + - name: Cache installed C core id: cache-c-core uses: actions/cache@v4 diff --git a/pyproject.toml b/pyproject.toml index 0ded74ac4..63ff71b8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,3 +5,7 @@ build-backend = "setuptools.build_meta" [tool.ruff] lint.ignore = ["B905", "C901", "E402", "E501"] lint.select = ["B", "C", "E", "F", "W"] + +[tool.cibuildwheel] +build-frontend = "build[uv]" + From 6c84efd878f593d1a2bf84752f67cf911251f00c Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 1 Jul 2024 20:11:27 +0200 Subject: [PATCH 099/276] doc: some doc fixes done while writing the tutorial for NetSci'24 --- doc/source/tutorial.rst | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index ea2ce0e14..7ba2de653 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -8,7 +8,7 @@ Tutorial ======== -This page is a detailed tutorial of |igraph|'s Python capabilities. To get an quick impression of what |igraph| can do, check out the :doc:`tutorials/quickstart`. If you have not installed |igraph| yet, follow the :doc:`install`. +This page is a detailed tutorial of |igraph|'s Python capabilities. To get an quick impression of what |igraph| can do, check out the :doc:`tutorials/quickstart`. If you have not installed |igraph| yet, follow the section titled :doc:`install`. .. note:: For the impatient reader, see the :doc:`tutorials/index` page for short, self-contained examples. @@ -16,7 +16,7 @@ This page is a detailed tutorial of |igraph|'s Python capabilities. To get an qu Starting |igraph| ================= -The most common way to use |igraph| is as a named import within a Python environment (e.g. a bare Python shell, a `IPython`_ shell, a `Jupyter`_ notebook or JupyterLab instance, `Google Colab `_, or an `IDE `_):: +The most common way to use |igraph| is as a named import within a Python environment (e.g. a bare Python shell, an `IPython`_ shell, a `Jupyter`_ notebook or JupyterLab instance, `Google Colab `_, or an `IDE `_):: $ python Python 3.9.6 (default, Jun 29 2021, 05:25:02) @@ -79,10 +79,9 @@ This means: **U** ndirected graph with **10** vertices and **2** edges, with the .. note:: - :meth:`Graph.summary` is similar to ``print`` but does not list the edges, which is - convenient for large graphs with millions of edges:: + |igraph| also has a :func:`igraph.summary()` function, which is similar to ``print()`` but it does not list the edges. This is convenient for large graphs with millions of edges:: - >>> summary(g) + >>> ig.summary(g) IGRAPH U--- 10 2 -- @@ -206,7 +205,7 @@ answer can quickly be given by checking the degree distributions of the two grap Setting and retrieving attributes ================================= -As mentioned above, in |igraph| each vertex and each edge have a numeric id from ``0`` upwards. Deleting vertices or edges can therefore cause reassignments of vertex and/or edge IDs. In addition to IDs, vertex and edges can have *attributes* such as a name, coordinates for plotting, metadata, and weights. The graph itself can have such attributes too (e.g. a name, which will show in ``print`` or :meth:`Graph.summary`). In a sense, every :class:`Graph`, vertex and edge can be used as a Python dictionary to store and retrieve these attributes. +As mentioned above, vertices and edges of a graph in |igraph| have numeric IDs from ``0`` upwards. Deleting vertices or edges can therefore cause reassignments of vertex and/or edge IDs. In addition to IDs, vertices and edges can have *attributes* such as a name, coordinates for plotting, metadata, and weights. The graph itself can have such attributes too (e.g. a name, which will show in ``print`` or :meth:`Graph.summary`). In a sense, every :class:`Graph`, vertex and edge can be used as a Python dictionary to store and retrieve these attributes. To demonstrate the use of attributes, let us create a simple social network:: @@ -604,6 +603,8 @@ Method name Short name Algorithm description ``layout_circle`` ``circle``, Deterministic layout that places the ``circular`` vertices on a circle ------------------------------------ --------------- --------------------------------------------- +``layout_davidson_harel`` ``dh`` Davidson-Harel simulated annealing algorithm +------------------------------------ --------------- --------------------------------------------- ``layout_drl`` ``drl`` The `Distributed Recursive Layout`_ algorithm for large graphs ------------------------------------ --------------- --------------------------------------------- @@ -612,6 +613,10 @@ Method name Short name Algorithm description ``layout_fruchterman_reingold_3d`` ``fr3d``, Fruchterman-Reingold force-directed algorithm ``fr_3d`` in three dimensions ------------------------------------ --------------- --------------------------------------------- +``layout_graphopt`` ``graphopt`` The GraphOpt algorithm for large graphs +------------------------------------ --------------- --------------------------------------------- +``layout_grid`` ``grid`` Regular grid layout +------------------------------------ --------------- --------------------------------------------- ``layout_kamada_kawai`` ``kk`` Kamada-Kawai force-directed algorithm ------------------------------------ --------------- --------------------------------------------- ``layout_kamada_kawai_3d`` ``kk3d``, Kamada-Kawai force-directed algorithm @@ -621,6 +626,8 @@ Method name Short name Algorithm description ``lgl``, large graphs ``large_graph`` ------------------------------------ --------------- --------------------------------------------- +``layout_mds`` ``mds`` Multidimensional scaling layout +------------------------------------ --------------- --------------------------------------------- ``layout_random`` ``random`` Places the vertices completely randomly ------------------------------------ --------------- --------------------------------------------- ``layout_random_3d`` ``random_3d`` Places the vertices completely randomly in 3D @@ -630,7 +637,7 @@ Method name Short name Algorithm description ------------------------------------ --------------- --------------------------------------------- ``layout_reingold_tilford_circular`` ``rt_circular`` Reingold-Tilford tree layout with a polar coordinate post-transformation, useful for - ``tree`` (almost) tree-like graphs + (almost) tree-like graphs ------------------------------------ --------------- --------------------------------------------- ``layout_sphere`` ``sphere``, Deterministic layout that places the vertices ``spherical``, evenly on the surface of a sphere @@ -652,9 +659,9 @@ are passed intact to the chosen layout method. For instance, the following two c completely equivalent:: >>> layout = g.layout_reingold_tilford(root=[2]) - >>> layout = g.layout("rt", [2]) + >>> layout = g.layout("rt", root=[2]) -Layout methods return a :class:`~layout.Layout` object which behaves mostly like a list of lists. +Layout methods return a :class:`~layout.Layout` object, which behaves mostly like a list of lists. Each list entry in a :class:`~layout.Layout` object corresponds to a vertex in the original graph and contains the vertex coordinates in the 2D or 3D space. :class:`~layout.Layout` objects also contain some useful methods to translate, scale or rotate the coordinates in a batch. From 8f4314176bd7f77958d861923ef0ef20c4143775 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 1 Jul 2024 20:14:49 +0200 Subject: [PATCH 100/276] Revert "ci: trying to switch to uv-based build backend for cibuildwheel" This reverts commit 702902c8386f9292b33989c57212a89a2e0e13e7. --- .github/workflows/build.yml | 18 ------------------ pyproject.toml | 4 ---- 2 files changed, 22 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c69e2a38..760081073 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,9 +29,6 @@ jobs: with: python-version: '3.8' - - uses: yezz123/setup-uv@v4 - name: Install uv - - name: Build wheels (manylinux) uses: pypa/cibuildwheel@v2.19.1 env: @@ -66,9 +63,6 @@ jobs: id: qemu uses: docker/setup-qemu-action@v3 - - uses: yezz123/setup-uv@v4 - name: Install uv - - name: Build wheels (manylinux) uses: pypa/cibuildwheel@v2.19.1 env: @@ -94,9 +88,6 @@ jobs: id: qemu uses: docker/setup-qemu-action@v3 - - uses: yezz123/setup-uv@v4 - name: Install uv - - name: Build wheels (musllinux) uses: pypa/cibuildwheel@v2.19.1 env: @@ -148,9 +139,6 @@ jobs: with: python-version: '3.8' - - uses: yezz123/setup-uv@v4 - name: Install uv - - name: Install OS dependencies if: steps.cache-c-core.outputs.cache-hit != 'true' || steps.cache-c-deps.outputs.cache-hit != 'true' # Only needed when building the C core or libomp run: @@ -194,9 +182,6 @@ jobs: with: python-version: '3.11.2' - - uses: yezz123/setup-uv@v4 - name: Install uv - - name: Install OS dependencies run: sudo apt install ninja-build cmake flex bison @@ -247,9 +232,6 @@ jobs: with: python-version: '3.8' - - uses: yezz123/setup-uv@v4 - name: Install uv - - name: Cache installed C core id: cache-c-core uses: actions/cache@v4 diff --git a/pyproject.toml b/pyproject.toml index 63ff71b8c..0ded74ac4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,3 @@ build-backend = "setuptools.build_meta" [tool.ruff] lint.ignore = ["B905", "C901", "E402", "E501"] lint.select = ["B", "C", "E", "F", "W"] - -[tool.cibuildwheel] -build-frontend = "build[uv]" - From 0faa5ce398c6f02dcc046c00f2a508b165842edb Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 1 Jul 2024 20:20:05 +0200 Subject: [PATCH 101/276] ci: trying to fix CentOS 7 deprecation --- .github/workflows/build.yml | 4 ++-- scripts/fixup_centos_7_in_ci.sh | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100755 scripts/fixup_centos_7_in_ci.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 760081073..22056bec2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: - name: Build wheels (manylinux) uses: pypa/cibuildwheel@v2.19.1 env: - CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "bash scripts/fixup_centos_7_in_ci.sh && yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" # Skip tests for Python 3.10 onwards because SciPy does not have # 32-bit wheels for Linux @@ -66,7 +66,7 @@ jobs: - name: Build wheels (manylinux) uses: pypa/cibuildwheel@v2.19.1 env: - CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "bash scripts/fixup_centos_7_in_ci.sh && yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 CIBW_BUILD: "*-manylinux_aarch64" diff --git a/scripts/fixup_centos_7_in_ci.sh b/scripts/fixup_centos_7_in_ci.sh new file mode 100755 index 000000000..bbc30fb5f --- /dev/null +++ b/scripts/fixup_centos_7_in_ci.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Workaround for cibuildwheel Docker images using CentOS 7 now that CentOS 7 +# is gone. + +sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo +sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo + From 5a6c1337046f6ad96e9324788e0bfd8c7bfc2126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Fri, 28 Jun 2024 16:42:07 +0000 Subject: [PATCH 102/276] feat: mean_degree() --- src/_igraph/graphobject.c | 31 +++++++++++++++++++++++++++++++ tests/test_structural.py | 7 +++++++ 2 files changed, 38 insertions(+) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 31049e887..f97c2a2f5 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -962,6 +962,29 @@ PyObject *igraphmodule_Graph_density(igraphmodule_GraphObject * self, return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); } +/** \ingroup python_interface_graph + * \brief Calculates the mean degree + * \return the mean degree + * \sa igraph_mean_degree + */ +PyObject *igraphmodule_Graph_mean_degree(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) +{ + char *kwlist[] = { "loops", NULL }; + igraph_real_t res; + PyObject *loops = Py_True; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &loops)) + return NULL; + + if (igraph_mean_degree(&self->g, &res, PyObject_IsTrue(loops))) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); +} + /** \ingroup python_interface_graph * \brief The maximum degree of some vertices in an \c igraph.Graph * \return the maxium degree as a Python object @@ -15075,6 +15098,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " assumes that there can't be any loops.\n" "@return: the density of the graph."}, + /* interface to igraph_mean_degree */ + {"mean_degree", (PyCFunction) igraphmodule_Graph_mean_degree, + METH_VARARGS | METH_KEYWORDS, + "mean_degree(loops=True)\n--\n\n" + "Calculates the mean degree of the graph.\n\n" + "@param loops: whether to consider self-loops during the calculation\n" + "@return: the mean degree of the graph."}, + /* interfaces to igraph_diameter */ {"diameter", (PyCFunction) igraphmodule_Graph_diameter, METH_VARARGS | METH_KEYWORDS, diff --git a/tests/test_structural.py b/tests/test_structural.py index bfda0f2e5..b9d90a7dd 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -24,6 +24,13 @@ def testDensity(self): self.assertAlmostEqual(7 / 16, self.gdir.density(True), places=5) self.assertAlmostEqual(1 / 7, self.tree.density(), places=5) + def testMeanDegree(self): + self.assertEqual(9.0, self.gfull.mean_degree()) + self.assertEqual(0.0, self.gempty.mean_degree()) + self.assertEqual(2.5, self.g.mean_degree()) + self.assertEqual(7 / 4, self.gdir.mean_degree()) + self.assertAlmostEqual(13 / 7, self.tree.mean_degree(), places=5) + def testDiameter(self): self.assertTrue(self.gfull.diameter() == 1) self.assertTrue(self.gempty.diameter(unconn=False) == inf) From 0b09b42c9c71ff3a236e394402525e32aeca22c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Fri, 28 Jun 2024 17:29:11 +0000 Subject: [PATCH 103/276] chore: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f93dfadfd..ddf5a8326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Added `Graph.Chung_Lu()` for sampling from the Chung-Lu model as well as several related models. - Added `Graph.is_complete()` to test if there is a connection between all distinct pairs of vertices. + - Added `Graph.mean_degree()` for a convenient way to compute the average degree of a graph. ### Changed From 145baf40f2210babe3417806580b0f71786bac47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Wed, 3 Jul 2024 09:28:29 +0000 Subject: [PATCH 104/276] refactor: rewire() now does 10 times the number of edges trials by default --- src/_igraph/graphobject.c | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index f97c2a2f5..7def34ffb 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -6432,14 +6432,19 @@ PyObject *igraphmodule_Graph_rewire(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "n", "mode", NULL }; - Py_ssize_t n = 1000; - PyObject *mode_o = Py_None; + PyObject *n_o = Py_None, *mode_o = Py_None; + igraph_integer_t n = 10 * igraph_ecount(&self->g); /* TODO overflow check */ igraph_rewiring_t mode = IGRAPH_REWIRING_SIMPLE; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nO", kwlist, &n, &mode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &n_o, &mode_o)) { return NULL; + } - CHECK_SSIZE_T_RANGE(n, "number of rewiring attempts"); + if (n_o != Py_None) { + if (igraphmodule_PyObject_to_integer_t(n_o, &n)) { + return NULL; + } + } if (igraphmodule_PyObject_to_rewiring_t(mode_o, &mode)) { return NULL; @@ -15810,12 +15815,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_rewire */ {"rewire", (PyCFunction) igraphmodule_Graph_rewire, METH_VARARGS | METH_KEYWORDS, - "rewire(n=1000, mode=\"simple\")\n--\n\n" + "rewire(n=None, mode=\"simple\")\n--\n\n" "Randomly rewires the graph while preserving the degree distribution.\n\n" - "Please note that the rewiring is done \"in-place\", so the original\n" - "graph will be modified. If you want to preserve the original graph,\n" - "use the L{copy} method before.\n\n" - "@param n: the number of rewiring trials.\n" + "The rewiring is done \"in-place\", so the original graph will be modified.\n" + "If you want to preserve the original graph, use the L{copy} method before\n" + "rewiring.\n\n" + "@param n: the number of rewiring trials. The default is 10 times the number\n" + " of edges.\n" "@param mode: the rewiring algorithm to use. It can either be C{\"simple\"} or\n" " C{\"loops\"}; the former does not create or destroy loop edges while the\n" " latter does.\n"}, From 8dee4a0a85a7e7386abb9d3170cdba8a6aad61c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Fri, 28 Jun 2024 16:31:59 +0000 Subject: [PATCH 105/276] feat: Graph.Hypercube() --- src/_igraph/graphobject.c | 47 +++++++++++++++++++++++++++++++++++++++ tests/test_generators.py | 5 +++++ 2 files changed, 52 insertions(+) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 7def34ffb..e0f921120 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2758,6 +2758,41 @@ PyObject *igraphmodule_Graph_Hexagonal_Lattice(PyTypeObject * type, return (PyObject *) self; } + +/** \ingroup python_interface_graph + * \brief Generates hypercube graph + * \return a reference to the newly generated Python igraph object + * \sa igraph_hypercube + */ +PyObject *igraphmodule_Graph_Hypercube(PyTypeObject * type, + PyObject * args, PyObject * kwds) +{ + Py_ssize_t n; + igraph_bool_t directed; + PyObject *o_directed = Py_False; + igraphmodule_GraphObject *self; + igraph_t g; + + static char *kwlist[] = { "n", "directed", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|O", kwlist, &n, &o_directed)) { + return NULL; + } + + CHECK_SSIZE_T_RANGE(n, "vertex count"); + + directed = PyObject_IsTrue(o_directed); + + if (igraph_hypercube(&g, n, directed)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + CREATE_GRAPH_FROM_TYPE(self, g, type); + + return (PyObject *) self; +} + /** \ingroup python_interface_graph * \brief Generates a bipartite graph from a bipartite adjacency matrix * \return a reference to the newly generated Python igraph object @@ -14205,6 +14240,18 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param mutual: whether to create all connections as mutual\n" " in case of a directed graph.\n"}, + /* interface to igraph_hypercube */ + {"Hypercube", (PyCFunction) igraphmodule_Graph_Hypercube, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Hypercube(n, directed=False)\n--\n\n" + "Generates an n-dimensional hypercube graph.\n\n" + "The hypercube graph M{Q_n} has M{2^n} vertices and M{2^{n-1} n} edges.\n" + "Two vertices are connected when the binary representations of their vertex\n" + "IDs differ in precisely one bit.\n" + "@param n: the dimension of the hypercube graph\n" + "@param directed: whether to create a directed graph; edges will point\n" + " from lower index vertices towards higher index ones."}, + /* interface to igraph_biadjacency */ {"_Biadjacency", (PyCFunction) igraphmodule_Graph_Biadjacency, METH_VARARGS | METH_CLASS | METH_KEYWORDS, diff --git a/tests/test_generators.py b/tests/test_generators.py index 733a31c66..30cc58d23 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -161,6 +161,11 @@ def testHexagonalLattice(self): g = Graph.Hexagonal_Lattice([2, 2], directed=True, mutual=True) self.assertEqual(sorted(g.get_edgelist()), sorted(el + [(y, x) for x, y in el])) + def testHypercube(self): + el = [(0, 1), (0, 2), (0, 4), (1, 3), (1, 5), (2, 3), (2, 6), (3, 7), (4, 5), (4, 6), (5, 7), (6, 7)] + g = Graph.Hypercube(3) + self.assertEqual(g.get_edgelist(), el) + def testLCF(self): g1 = Graph.LCF(12, (5, -5), 6) g2 = Graph.Famous("Franklin") From 1a5bcccd97603456f8a16bafea3be1501dfb7046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Fri, 28 Jun 2024 17:32:27 +0000 Subject: [PATCH 106/276] chore: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddf5a8326..4f50ecdd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Added + - Added `Graph.Hypercube()` for creating n-dimensional hypercube graphs. - Added `Graph.Chung_Lu()` for sampling from the Chung-Lu model as well as several related models. - Added `Graph.is_complete()` to test if there is a connection between all distinct pairs of vertices. - Added `Graph.mean_degree()` for a convenient way to compute the average degree of a graph. From bb0efcfc6b7a097b51a4ada534ab7ed034d5bca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Fri, 28 Jun 2024 17:55:30 +0000 Subject: [PATCH 107/276] feat: is_clique() and is_independent_vertex_set() --- src/_igraph/graphobject.c | 87 +++++++++++++++++++++++++++++++++++++++ tests/test_cliques.py | 4 ++ 2 files changed, 91 insertions(+) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index e0f921120..e33131b25 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -567,6 +567,77 @@ PyObject *igraphmodule_Graph_is_complete(igraphmodule_GraphObject* self, PyObjec } +/** \ingroup python_interface_graph + * \brief Checks whether a given vertex set forms a clique + */ +PyObject *igraphmodule_Graph_is_clique(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) +{ + PyObject *list = Py_None; + PyObject *directed = Py_False; + igraph_bool_t res; + igraph_vs_t vs; + + static char *kwlist[] = { "vertices", "directed", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &list, &directed)) { + return NULL; + } + + if (igraphmodule_PyObject_to_vs_t(list, &vs, &self->g, NULL, NULL)) { + return NULL; + } + + if (igraph_is_clique(&self->g, vs, PyObject_IsTrue(directed), &res)) { + igraphmodule_handle_igraph_error(); + igraph_vs_destroy(&vs); + return NULL; + } + + igraph_vs_destroy(&vs); + + if (res) + Py_RETURN_TRUE; + else + Py_RETURN_FALSE; +} + + +/** \ingroup python_interface_graph + * \brief Checks whether a the given vertices form an independent set + */ +PyObject *igraphmodule_Graph_is_independent_vertex_set(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) +{ + PyObject *list = Py_None; + igraph_bool_t res; + igraph_vs_t vs; + + static char *kwlist[] = { "vertices", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &list)) { + return NULL; + } + + if (igraphmodule_PyObject_to_vs_t(list, &vs, &self->g, NULL, NULL)) { + return NULL; + } + + if (igraph_is_independent_vertex_set(&self->g, vs, &res)) { + igraphmodule_handle_igraph_error(); + igraph_vs_destroy(&vs); + return NULL; + } + + igraph_vs_destroy(&vs); + + if (res) + Py_RETURN_TRUE; + else + Py_RETURN_FALSE; +} + + /** \ingroup python_interface_graph * \brief Determines whether a graph is a (directed or undirected) tree * \sa igraph_is_tree @@ -13768,6 +13839,22 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: C{True} if it is complete, C{False} otherwise.\n" "@rtype: boolean"}, + {"is_clique", (PyCFunction) igraphmodule_Graph_is_clique, + METH_VARARGS | METH_KEYWORDS, + "is_clique(vertices=None, directed=False)\n--\n\n" + "Decides whether a set of vertices is a clique, i.e. a fully connected subgraph.\n\n" + "@param vertices: a list of vertex IDs.\n" + "@param directed: whether to require mutual connections between vertex pairs\n" + " in directed graphs.\n" + "@return: C{True} is the given vertex set is a clique, C{False} if not.\n"}, + + {"is_independent_vertex_set", (PyCFunction) igraphmodule_Graph_is_independent_vertex_set, + METH_VARARGS | METH_KEYWORDS, + "is_independent_vertex_set(vertices=None)\n--\n\n" + "Decides whether no two vertices within a set are adjacent.\n\n" + "@param vertices: a list of vertex IDs.\n" + "@return: C{True} is the given vertices form an independent set, C{False} if not.\n"}, + /* interface to igraph_is_tree */ {"is_tree", (PyCFunction) igraphmodule_Graph_is_tree, METH_VARARGS | METH_KEYWORDS, diff --git a/tests/test_cliques.py b/tests/test_cliques.py index 3d63dd363..3c1c215e9 100644 --- a/tests/test_cliques.py +++ b/tests/test_cliques.py @@ -66,12 +66,14 @@ def testLargestCliques(self): self.assertEqual( sorted(map(sorted, self.g.largest_cliques())), [[1, 2, 3, 4], [1, 2, 4, 5]] ) + self.assertTrue(all(map(self.g.is_clique, self.g.largest_cliques()))) def testMaximalCliques(self): self.assertEqual( sorted(map(sorted, self.g.maximal_cliques())), [[0, 3, 4], [0, 4, 5], [1, 2, 3, 4], [1, 2, 4, 5]], ) + self.assertTrue(all(map(self.g.is_clique, self.g.maximal_cliques()))) self.assertEqual( sorted(map(sorted, self.g.maximal_cliques(min=4))), [[1, 2, 3, 4], [1, 2, 4, 5]], @@ -136,6 +138,7 @@ def testLargestIndependentVertexSets(self): self.assertEqual( self.g1.largest_independent_vertex_sets(), [(0, 3, 4), (2, 3, 4)] ) + self.assertTrue(all(map(self.g1.is_independent_vertex_set, self.g1.largest_independent_vertex_sets()))) def testMaximalIndependentVertexSets(self): self.assertEqual( @@ -152,6 +155,7 @@ def testMaximalIndependentVertexSets(self): (2, 4, 7, 8), ], ) + self.assertTrue(all(map(self.g2.is_independent_vertex_set, self.g2.maximal_independent_vertex_sets()))) def testIndependenceNumber(self): self.assertEqual(self.g2.independence_number(), 6) From b5b422a881cca594f4f3be838a0d3d02f65de091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Fri, 28 Jun 2024 17:56:32 +0000 Subject: [PATCH 108/276] chore: update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f50ecdd9..fc8c5dab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Added `Graph.Hypercube()` for creating n-dimensional hypercube graphs. - Added `Graph.Chung_Lu()` for sampling from the Chung-Lu model as well as several related models. - Added `Graph.is_complete()` to test if there is a connection between all distinct pairs of vertices. + - Added `Graph.is_clique()` to test if a set of vertices forms a clique. + - Added `Graph.is_independent_vertex_set()` to test if some vertices form an independent set. - Added `Graph.mean_degree()` for a convenient way to compute the average degree of a graph. ### Changed From 8e43032d17e6a060594c0256d715aa929ac1807c Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 8 Jul 2024 12:51:42 +0200 Subject: [PATCH 109/276] chore: updated changelog --- CHANGELOG.md | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc8c5dab8..e1cba8656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,34 @@ # igraph Python interface changelog +## [0.11.6] - 2024-07-08 + ### Added - Added `Graph.Hypercube()` for creating n-dimensional hypercube graphs. - - Added `Graph.Chung_Lu()` for sampling from the Chung-Lu model as well as several related models. - - Added `Graph.is_complete()` to test if there is a connection between all distinct pairs of vertices. + + - Added `Graph.Chung_Lu()` for sampling from the Chung-Lu model as well as + several related models. + + - Added `Graph.is_complete()` to test if there is a connection between all + distinct pairs of vertices. + - Added `Graph.is_clique()` to test if a set of vertices forms a clique. - - Added `Graph.is_independent_vertex_set()` to test if some vertices form an independent set. - - Added `Graph.mean_degree()` for a convenient way to compute the average degree of a graph. + + - Added `Graph.is_independent_vertex_set()` to test if some vertices form an + independent set. + + - Added `Graph.mean_degree()` for a convenient way to compute the average + degree of a graph. ### Changed - - Error messages issued when an attribute is not found now mention the name and type of that attribute. + - The C core of igraph was updated to version 0.10.13. + + - `Graph.rewire()` now attempts to perform edge swaps 10 times the number of + edges by default. + + - Error messages issued when an attribute is not found now mention the name + and type of that attribute. ## [0.11.5] - 2024-05-07 @@ -667,7 +684,8 @@ Please refer to the commit logs at https://github.com/igraph/python-igraph for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. -[main]: https://github.com/igraph/python-igraph/compare/0.11.5...main +[main]: https://github.com/igraph/python-igraph/compare/0.11.6...main +[0.11.6]: https://github.com/igraph/python-igraph/compare/0.11.5...0.11.6 [0.11.5]: https://github.com/igraph/python-igraph/compare/0.11.4...0.11.5 [0.11.4]: https://github.com/igraph/python-igraph/compare/0.11.3...0.11.4 [0.11.3]: https://github.com/igraph/python-igraph/compare/0.11.2...0.11.3 From bac56e0ffeec965e6182e341a0b79bda9c22e09d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 8 Jul 2024 12:51:58 +0200 Subject: [PATCH 110/276] fix: fix EOL in fixup_centos_7_in_ci.sh --- scripts/fixup_centos_7_in_ci.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/fixup_centos_7_in_ci.sh b/scripts/fixup_centos_7_in_ci.sh index bbc30fb5f..17a9f74e6 100755 --- a/scripts/fixup_centos_7_in_ci.sh +++ b/scripts/fixup_centos_7_in_ci.sh @@ -4,4 +4,3 @@ sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo - From 9d365148b815df908fa18f4effcafa56d49217da Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 8 Jul 2024 12:52:34 +0200 Subject: [PATCH 111/276] chore: bumped version to 0.11.6 --- src/igraph/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/igraph/version.py b/src/igraph/version.py index 92c6ba6fb..dfef0870c 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 11, 5) +__version_info__ = (0, 11, 6) __version__ = ".".join("{0}".format(x) for x in __version_info__) From 3f46d384e08a56cc01d6c316eee4b4328761fb58 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 8 Jul 2024 19:52:13 +0200 Subject: [PATCH 112/276] fix: update script that fixes the CentOS build --- scripts/fixup_centos_7_in_ci.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/fixup_centos_7_in_ci.sh b/scripts/fixup_centos_7_in_ci.sh index 17a9f74e6..74bc18173 100755 --- a/scripts/fixup_centos_7_in_ci.sh +++ b/scripts/fixup_centos_7_in_ci.sh @@ -4,3 +4,5 @@ sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo +sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/*.repo + From ec94854d99ebcc26714fec8560609c8e76d52f21 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 13:24:46 +0000 Subject: [PATCH 113/276] build(deps): bump pypa/cibuildwheel from 2.19.1 to 2.19.2 Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.19.1 to 2.19.2. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.19.1...v2.19.2) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 22056bec2..7d1eb9fd5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: python-version: '3.8' - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.19.1 + uses: pypa/cibuildwheel@v2.19.2 env: CIBW_BEFORE_BUILD: "bash scripts/fixup_centos_7_in_ci.sh && yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -39,7 +39,7 @@ jobs: CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.19.1 + uses: pypa/cibuildwheel@v2.19.2 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" @@ -64,7 +64,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.19.1 + uses: pypa/cibuildwheel@v2.19.2 env: CIBW_BEFORE_BUILD: "bash scripts/fixup_centos_7_in_ci.sh && yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -89,7 +89,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.19.1 + uses: pypa/cibuildwheel@v2.19.2 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -156,7 +156,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.19.1 + uses: pypa/cibuildwheel@v2.19.2 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" @@ -256,7 +256,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.19.1 + uses: pypa/cibuildwheel@v2.19.2 env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From cc5e7082b1c35c763ffd3d4c753cda6b7c7e1153 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 8 Jul 2024 20:41:53 +0200 Subject: [PATCH 114/276] ci: remove CentOS 7 specific hack from CI build --- .github/workflows/build.yml | 4 ++-- scripts/fixup_centos_7_in_ci.sh | 8 -------- 2 files changed, 2 insertions(+), 10 deletions(-) delete mode 100755 scripts/fixup_centos_7_in_ci.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7d1eb9fd5..b86dc0b30 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: - name: Build wheels (manylinux) uses: pypa/cibuildwheel@v2.19.2 env: - CIBW_BEFORE_BUILD: "bash scripts/fixup_centos_7_in_ci.sh && yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" # Skip tests for Python 3.10 onwards because SciPy does not have # 32-bit wheels for Linux @@ -66,7 +66,7 @@ jobs: - name: Build wheels (manylinux) uses: pypa/cibuildwheel@v2.19.2 env: - CIBW_BEFORE_BUILD: "bash scripts/fixup_centos_7_in_ci.sh && yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 CIBW_BUILD: "*-manylinux_aarch64" diff --git a/scripts/fixup_centos_7_in_ci.sh b/scripts/fixup_centos_7_in_ci.sh deleted file mode 100755 index 74bc18173..000000000 --- a/scripts/fixup_centos_7_in_ci.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -# Workaround for cibuildwheel Docker images using CentOS 7 now that CentOS 7 -# is gone. - -sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo -sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo -sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/*.repo - From e0ef1d779e2e6e89efb0f2854d89d0576d2f587f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Tue, 9 Jul 2024 08:54:15 +0000 Subject: [PATCH 115/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index e1d48f89a..ac22a920d 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit e1d48f89a08b7423761504d868aa1dd57c0e0557 +Subproject commit ac22a920d882ec9e2bd433c55e2da2fce0ebe529 From f999bb279b0fd6bf3245c09d31594c6ca9d1f671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Tue, 9 Jul 2024 18:43:14 +0000 Subject: [PATCH 116/276] doc: fix typos in Chung_Lu() docs --- src/_igraph/graphobject.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index e33131b25..6322969fa 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -14675,7 +14675,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "M{w (S + w) / S} and M{w (S - w) / S}, respectively.\n\n" "A limitation of the original Chung-Lu model is that when some of the\n" "weights are large, the formula for M{p_{ij}} yields values larger than 1.\n" - "Chung and Lu's original paper exludes the use of such weights. When\n" + "Chung and Lu's original paper excludes the use of such weights. When\n" "M{p_{ij} > 1}, this function simply issues a warning and creates\n" "a connection between M{i} and M{j}. However, in this case the expected degrees\n" "will no longer relate to the weights in the manner stated above. Thus the\n" @@ -14687,7 +14687,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "variants become equivalent in the limit of sparse graphs where M{q_{ij}}\n" "approaches zero. In the original Chung-Lu model, selectable by setting\n" "C{variant} to C{\"original\"}, M{p_{ij} = min(q_{ij}, 1)}.\n" - "The C{\"grg\"} variant, sometimes referred to a the generalized\n" + "The C{\"maxent\"} variant, sometimes referred to a the generalized\n" "random graph, uses M{p_{ij} = q_{ij} / (1 + q_{ij})}, and is equivalent\n" "to a maximum entropy model (i.e. exponential random graph model) with\n" "a constraint on expected degrees, see Park and Newman (2004), Section B,\n" From cdcf4ef0bd3db50e9e8929236a1d29603f0ff647 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 15 Jul 2024 13:09:57 +0200 Subject: [PATCH 117/276] fix: replace deprecated iteritems() method from Python 2, closes #789 --- src/igraph/clustering.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index b1fca9ab4..89b34b56e 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -1376,7 +1376,7 @@ def _handle_mark_groups_arg_for_clustering(mark_groups, clustering): if mark_groups is True: group_iter = ((group, color) for color, group in enumerate(clustering)) elif isinstance(mark_groups, dict): - group_iter = mark_groups.iteritems() + group_iter = mark_groups.items() elif hasattr(mark_groups, "__getitem__") and hasattr(mark_groups, "__len__"): # Lists, tuples try: @@ -1399,7 +1399,7 @@ def _handle_mark_groups_arg_for_clustering(mark_groups, clustering): # Iterators etc group_iter = mark_groups else: - group_iter = {}.iteritems() + group_iter = {}.items() def cluster_index_resolver(): for group, color in group_iter: From 130d055f114ba7be676e1f1df91296b96f5db0c4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 17 Jul 2024 11:39:00 +0200 Subject: [PATCH 118/276] chore: add some modifications to the Sphinx config file based on a notification from Read the Docs --- doc/source/conf.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/source/conf.py b/doc/source/conf.py index 1bfee14a2..334b52e6a 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -187,6 +187,14 @@ def get_igraph_version(): # Output file base name for HTML help builder. htmlhelp_basename = "igraphdoc" +# Integration with Read the Docs since RTD is not manipulating the Sphinx +# config files on its own any more. +# This is according to: +# https://about.readthedocs.com/blog/2024/07/addons-by-default/ +html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "") +html_context = {} +if os.environ.get("READTHEDOCS", "") == "True": + html_context["READTHEDOCS"] = True # -- Options for pydoctor ------------------------------------------------------ From 0479d0a9ba1052e3968b02663c376d07c52e36e2 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 21 Jul 2024 14:44:03 +0200 Subject: [PATCH 119/276] ci: update to upload-artifact@v4 --- .github/workflows/build.yml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b86dc0b30..8d1905c12 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,8 +45,9 @@ jobs: CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test-musl]' && python -m pytest -v tests" - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: + name: wheels-linux-${{ matrix.wheel_arch }} path: ./wheelhouse/*.whl build_wheel_linux_aarch64_manylinux: @@ -70,8 +71,9 @@ jobs: CIBW_ARCHS_LINUX: aarch64 CIBW_BUILD: "*-manylinux_aarch64" - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: + name: wheels-linux-aarch64-manylinux path: ./wheelhouse/*.whl build_wheel_linux_aarch64_musllinux: @@ -96,8 +98,9 @@ jobs: CIBW_BUILD: "*-musllinux_aarch64" CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test-musl]' && python -m pytest -v tests" - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: + name: wheels-linux-aarch64-musllinux path: ./wheelhouse/*.whl build_wheel_macos: @@ -163,8 +166,9 @@ jobs: CIBW_ENVIRONMENT: "LDFLAGS=-L$HOME/local/lib" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_arch }} ${{ matrix.cmake_extra_args }} -DCMAKE_PREFIX_PATH=$HOME/local - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: + name: wheels-macos-${{ matrix.wheel_arch }} path: ./wheelhouse/*.whl build_wheel_wasm: @@ -204,8 +208,9 @@ jobs: limit-access-to-actor: true wait-timeout-minutes: 5 - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: + name: wheels-wasm path: ./dist/*.whl build_wheel_win: @@ -270,8 +275,9 @@ jobs: IGRAPH_EXTRA_LIBRARIES: libxml2,lzma,zlib,iconv,charset IGRAPH_EXTRA_DYNAMIC_LIBRARIES: wsock32,ws2_32 - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: + name: wheels-win-${{ matrix.wheel_arch }} path: ./wheelhouse/*.whl build_sdist: @@ -312,8 +318,9 @@ jobs: pip install --prefer-binary cairocffi numpy scipy pandas networkx pytest pytest-timeout python -m pytest -v tests - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: + name: sdist path: dist/*.tar.gz # When updating 'runs-on', the ASan/UBSan library paths/versions must also be updated for LD_PRELOAD @@ -375,3 +382,4 @@ jobs: LSAN_OPTIONS: "suppressions=etc/lsan-suppr.txt:print_suppressions=false" run: | LD_PRELOAD=/lib/x86_64-linux-gnu/libasan.so.5:/lib/x86_64-linux-gnu/libubsan.so.1 python -m pytest --capture=sys tests + From 0afb1886a8c1867156072f7ad15d3a52b8414599 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:36:15 +0000 Subject: [PATCH 120/276] build(deps): bump pypa/cibuildwheel from 2.19.2 to 2.20.0 Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.19.2 to 2.20.0. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.19.2...v2.20.0) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8d1905c12..0ddaf3a57 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: python-version: '3.8' - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.19.2 + uses: pypa/cibuildwheel@v2.20.0 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -39,7 +39,7 @@ jobs: CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.19.2 + uses: pypa/cibuildwheel@v2.20.0 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" @@ -65,7 +65,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.19.2 + uses: pypa/cibuildwheel@v2.20.0 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -91,7 +91,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.19.2 + uses: pypa/cibuildwheel@v2.20.0 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -159,7 +159,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.19.2 + uses: pypa/cibuildwheel@v2.20.0 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" @@ -261,7 +261,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.19.2 + uses: pypa/cibuildwheel@v2.20.0 env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From 325995b432ee1d5da71c31727cfa3551f53a561f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Tue, 6 Aug 2024 12:38:57 +0200 Subject: [PATCH 121/276] Skip tests for Python 3.13 until SciPy starts publishing wheels --- .github/workflows/build.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0ddaf3a57..cdc66a3b6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,8 +35,9 @@ jobs: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" # Skip tests for Python 3.10 onwards because SciPy does not have - # 32-bit wheels for Linux - CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686" + # 32-bit wheels for Linux. Also skip tests for Python 3.13 because + # SciPy does not have wheels for Python 3.13 at all right now. + CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686 cp313-manylinux_*" - name: Build wheels (musllinux) uses: pypa/cibuildwheel@v2.20.0 @@ -164,6 +165,8 @@ jobs: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_ENVIRONMENT: "LDFLAGS=-L$HOME/local/lib" + # Skip tests for Python 3.13 because SciPy does not have wheels for Python 3.13 at all right now. + CIBW_TEST_SKIP: "cp313-*" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_arch }} ${{ matrix.cmake_extra_args }} -DCMAKE_PREFIX_PATH=$HOME/local - uses: actions/upload-artifact@v4 @@ -267,8 +270,9 @@ jobs: CIBW_BUILD: "*-${{ matrix.wheel_arch }}" CIBW_TEST_COMMAND: "cd /d {project} && pip install --prefer-binary \".[test]\" && python -m pytest tests" # Skip tests for Python 3.10 onwards because SciPy does not have - # 32-bit wheels for Windows - CIBW_TEST_SKIP: "cp310-win32 cp311-win32 cp312-win32" + # 32-bit wheels for Linux. Also skip tests for Python 3.13 because + # SciPy does not have wheels for Python 3.13 at all right now. + CIBW_TEST_SKIP: "cp310-win32 cp311-win32 cp312-win32 cp313-win32" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVCPKG_TARGET_TRIPLET=${{ matrix.vcpkg_arch }}-windows-static-md -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake -A ${{ matrix.cmake_arch }} IGRAPH_EXTRA_LIBRARY_PATH: C:/vcpkg/installed/${{ matrix.vcpkg_arch }}-windows-static-md/lib/ IGRAPH_STATIC_EXTENSION: True From eecbfce892a790338ad5215d2c282796de379637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Tue, 6 Aug 2024 13:31:31 +0200 Subject: [PATCH 122/276] ci: skip Python 3.13 tests completely on Windows (even on 64-bit) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cdc66a3b6..f7378cbd8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -272,7 +272,7 @@ jobs: # Skip tests for Python 3.10 onwards because SciPy does not have # 32-bit wheels for Linux. Also skip tests for Python 3.13 because # SciPy does not have wheels for Python 3.13 at all right now. - CIBW_TEST_SKIP: "cp310-win32 cp311-win32 cp312-win32 cp313-win32" + CIBW_TEST_SKIP: "cp310-win32 cp311-win32 cp312-win32 cp313-*" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVCPKG_TARGET_TRIPLET=${{ matrix.vcpkg_arch }}-windows-static-md -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake -A ${{ matrix.cmake_arch }} IGRAPH_EXTRA_LIBRARY_PATH: C:/vcpkg/installed/${{ matrix.vcpkg_arch }}-windows-static-md/lib/ IGRAPH_STATIC_EXTENSION: True From d8ff20de6dda6e087f006dd7957a9b8b63f0309f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 6 Aug 2024 12:57:52 +0200 Subject: [PATCH 123/276] ci: update to Pyodide 0.26.2 and fix build script --- .github/workflows/build.yml | 2 +- scripts/fix_pyodide_build.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f7378cbd8..d697545c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -195,7 +195,7 @@ jobs: - uses: mymindstorm/setup-emsdk@v14 with: - version: '3.1.45' + version: '3.1.52' actions-cache-folder: 'emsdk-cache' - name: Build wheel diff --git a/scripts/fix_pyodide_build.py b/scripts/fix_pyodide_build.py index 79d451969..46d190518 100644 --- a/scripts/fix_pyodide_build.py +++ b/scripts/fix_pyodide_build.py @@ -9,5 +9,5 @@ target_file = target_dir / "Emscripten.cmake" if not target_file.is_file(): - url = "https://raw.githubusercontent.com/pyodide/pyodide/main/pyodide-build/pyodide_build/tools/cmake/Modules/Platform/Emscripten.cmake" + url = "https://raw.githubusercontent.com/pyodide/pyodide-build/main/pyodide_build/tools/cmake/Modules/Platform/Emscripten.cmake" urlretrieve(url, str(target_file)) From aee757ecfb323db39a04a2bd1e309d5866391094 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 6 Aug 2024 13:29:55 +0200 Subject: [PATCH 124/276] ci: really update to Pyodide 0.26.2 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d697545c2..98784cd75 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -200,7 +200,7 @@ jobs: - name: Build wheel run: | - pip install pyodide-build==0.24.1 + pip install pyodide-build==0.26.2 python3 scripts/fix_pyodide_build.py pyodide build From 4b2bf7e9e1e5fcfcc0ee87269fa5b7bcf12140d0 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 7 Aug 2024 15:03:35 +0200 Subject: [PATCH 125/276] chore: fast-forward vendored igraph to tip of master --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index ac22a920d..28868a2fc 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit ac22a920d882ec9e2bd433c55e2da2fce0ebe529 +Subproject commit 28868a2fc7b7203d2b7197a58ca9a3bd7c7f5b9c From 3fb94e259130a4e9f0d777deaee68aeddfe7512a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sat, 17 Aug 2024 13:38:46 +0000 Subject: [PATCH 126/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 28868a2fc..33c01b0a1 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 28868a2fc7b7203d2b7197a58ca9a3bd7c7f5b9c +Subproject commit 33c01b0a1ee5cb014bbd1077431a76bac46b7d8c From 507c64458f472b80bf5c0b505accfdcfaa72e001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sat, 17 Aug 2024 13:39:43 +0000 Subject: [PATCH 127/276] feat: allow selecting specific integer programming feedback arc set problem formulations --- src/_igraph/convert.c | 2 ++ src/_igraph/graphobject.c | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 44fe318cf..3529d6921 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -596,6 +596,8 @@ int igraphmodule_PyObject_to_fas_algorithm_t(PyObject *o, {"exact", IGRAPH_FAS_EXACT_IP}, {"exact_ip", IGRAPH_FAS_EXACT_IP}, {"ip", IGRAPH_FAS_EXACT_IP}, + {"ip_ti", IGRAPH_FAS_EXACT_IP_TI}, + {"ip_cg", IGRAPH_FAS_EXACT_IP_CG}, {0,0} }; TRANSLATE_ENUM_WITH(fas_algorithm_tt); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 6322969fa..9d696c905 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -15442,8 +15442,12 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " breaking heuristic of Eades, Lin and Smyth, which is linear in the number\n" " of edges but not necessarily optimal; however, it guarantees that the\n" " number of edges to be removed is smaller than |E|/2 - |V|/6. C{\"ip\"} uses\n" - " an integer programming formulation which is guaranteed to yield an optimal\n" - " result, but is too slow for large graphs.\n" + " the most efficient available integer programming formulation which is guaranteed\n" + " to yield an optimal result. Specific integer programming formulations can be\n" + " selected using C{\"ip_ti\"} (using triangle inequalities) and C{\"ip_cg\"}\n" + " (a minimum set cover formulation using incremental constraint generation).\n" + " Note that the minimum feedback arc set problem is NP-hard, therefore all methods\n" + " that obtain exact optimal solutions are infeasibly slow on large graphs.\n" "@return: the IDs of the edges to be removed, in a list.\n\n" }, From 0a09dd1a03a569d24fce3f2c1ffda790e3543285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sat, 17 Aug 2024 16:43:42 +0000 Subject: [PATCH 128/276] feat: feedback_vertex_set() --- src/_igraph/convert.c | 13 +++++++++ src/_igraph/convert.h | 1 + src/_igraph/graphobject.c | 59 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 3529d6921..a96bc514d 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -603,6 +603,19 @@ int igraphmodule_PyObject_to_fas_algorithm_t(PyObject *o, TRANSLATE_ENUM_WITH(fas_algorithm_tt); } +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to an igraph \c igraph_fvs_algorithm_t + */ +int igraphmodule_PyObject_to_fvs_algorithm_t(PyObject *o, + igraph_fas_algorithm_t *result) { + static igraphmodule_enum_translation_table_entry_t fvs_algorithm_tt[] = { + {"ip", IGRAPH_FVS_EXACT_IP}, + {0,0} + }; + TRANSLATE_ENUM_WITH(fvs_algorithm_tt); +} + /** * \ingroup python_interface_conversion * \brief Converts a Python object to an igraph \c igraph_get_adjacency_t diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 54094684c..22f28eb75 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -72,6 +72,7 @@ int igraphmodule_PyObject_to_community_comparison_t(PyObject *obj, int igraphmodule_PyObject_to_connectedness_t(PyObject *o, igraph_connectedness_t *result); int igraphmodule_PyObject_to_degseq_t(PyObject *o, igraph_degseq_t *result); int igraphmodule_PyObject_to_fas_algorithm_t(PyObject *o, igraph_fas_algorithm_t *result); +int igraphmodule_PyObject_to_fvs_algorithm_t(PyObject *o, igraph_fas_algorithm_t *result); int igraphmodule_PyObject_to_get_adjacency_t(PyObject *o, igraph_get_adjacency_t *result); int igraphmodule_PyObject_to_laplacian_normalization_t(PyObject *o, igraph_laplacian_normalization_t *result); int igraphmodule_PyObject_to_layout_grid_t(PyObject *o, igraph_layout_grid_t *result); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 9d696c905..bdcfd3d8c 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -5558,6 +5558,48 @@ PyObject *igraphmodule_Graph_feedback_arc_set( } +/** \ingroup python_interface_graph + * \brief Calculates a feedback vertex set for a graph + * \return a list containing the indices in the chosen feedback vertex set + * \sa igraph_feedback_vertex_set + */ +PyObject *igraphmodule_Graph_feedback_vertex_set( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { + static char *kwlist[] = { "weights", "method", NULL }; + igraph_vector_t* weights = 0; + igraph_vector_int_t res; + igraph_fvs_algorithm_t algo = IGRAPH_FVS_EXACT_IP; + PyObject *weights_o = Py_None, *result_o = NULL, *algo_o = NULL; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &weights_o, &algo_o)) + return NULL; + + if (igraphmodule_PyObject_to_fvs_algorithm_t(algo_o, &algo)) + return NULL; + + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, + ATTRIBUTE_TYPE_VERTEX)) + return NULL; + + if (igraph_vector_int_init(&res, 0)) { + if (weights) { igraph_vector_destroy(weights); free(weights); } + } + + if (igraph_feedback_vertex_set(&self->g, &res, weights, algo)) { + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraph_vector_int_destroy(&res); + return NULL; + } + + if (weights) { igraph_vector_destroy(weights); free(weights); } + + result_o = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); + + return result_o; +} + + /** \ingroup python_interface_graph * \brief Calculates a single shortest path between a source and a target vertex * \return a list containing a single shortest path from the source to the target @@ -15451,6 +15493,23 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: the IDs of the edges to be removed, in a list.\n\n" }, + /* interface to igraph_feedback_vertex_set */ + {"feedback_vertex_set", (PyCFunction) igraphmodule_Graph_feedback_vertex_set, + METH_VARARGS | METH_KEYWORDS, + "feedback_vertex_set(weights=None, method=\"ip\")\n--\n\n" + "Calculates a minimum feedback vertex set.\n\n" + "A feedback vertex set is a set of edges whose removal makes the graph acyclic.\n" + "Finding a minimum feedback vertex set is an NP-hard problem both in directed\n" + "and undirected graphs.\n\n" + "@param weights: vertex weights to be used. Can be a sequence or iterable or\n" + " even a vertex attribute name. When given, the algorithm will strive to\n" + " remove lightweight vertices in order to minimize the total weight of the\n" + " feedback vertex set.\n" + "@param method: the algorithm to use. C{\"ip\"} uses an exact integer programming\n" + " approach, and is currently the only available method.\n" + "@return: the IDs of the vertices to be removed, in a list.\n\n" + }, + /* interface to igraph_get_shortest_path */ {"get_shortest_path", (PyCFunction) igraphmodule_Graph_get_shortest_path, METH_VARARGS | METH_KEYWORDS, From 86c73639ffedf750ae04a23a19c22860a1027efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Tue, 3 Sep 2024 01:44:50 +0200 Subject: [PATCH 129/276] fix: fix signature of igraphmodule_PyObject_to_fvs_algorithm_t --- src/_igraph/convert.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index a96bc514d..6ee49205c 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -608,7 +608,7 @@ int igraphmodule_PyObject_to_fas_algorithm_t(PyObject *o, * \brief Converts a Python object to an igraph \c igraph_fvs_algorithm_t */ int igraphmodule_PyObject_to_fvs_algorithm_t(PyObject *o, - igraph_fas_algorithm_t *result) { + igraph_fvs_algorithm_t *result) { static igraphmodule_enum_translation_table_entry_t fvs_algorithm_tt[] = { {"ip", IGRAPH_FVS_EXACT_IP}, {0,0} From 456e70609ca2a87bd268360d258b820cb2854ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Tue, 3 Sep 2024 01:46:53 +0200 Subject: [PATCH 130/276] fix: also fix signature here --- src/_igraph/convert.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 22f28eb75..873be1dc0 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -72,7 +72,7 @@ int igraphmodule_PyObject_to_community_comparison_t(PyObject *obj, int igraphmodule_PyObject_to_connectedness_t(PyObject *o, igraph_connectedness_t *result); int igraphmodule_PyObject_to_degseq_t(PyObject *o, igraph_degseq_t *result); int igraphmodule_PyObject_to_fas_algorithm_t(PyObject *o, igraph_fas_algorithm_t *result); -int igraphmodule_PyObject_to_fvs_algorithm_t(PyObject *o, igraph_fas_algorithm_t *result); +int igraphmodule_PyObject_to_fvs_algorithm_t(PyObject *o, igraph_fvs_algorithm_t *result); int igraphmodule_PyObject_to_get_adjacency_t(PyObject *o, igraph_get_adjacency_t *result); int igraphmodule_PyObject_to_laplacian_normalization_t(PyObject *o, igraph_laplacian_normalization_t *result); int igraphmodule_PyObject_to_layout_grid_t(PyObject *o, igraph_layout_grid_t *result); From 9716dfef300f496a5cadb4aae6aef5d5d6c5cbda Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 21 Aug 2024 10:36:14 +0200 Subject: [PATCH 131/276] fix: fix a potential memory leak in Graph.get_shortest_path_astar()'s heuristic function --- src/_igraph/graphobject.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index bdcfd3d8c..38ab0f5b5 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -5713,6 +5713,7 @@ igraph_error_t igraphmodule_i_Graph_get_shortest_path_astar_callback( to_o = igraphmodule_integer_t_to_PyObject(to); if (to_o == NULL) { /* Error in conversion, return 1 */ + Py_DECREF(from_o); return IGRAPH_FAILURE; } @@ -5727,9 +5728,11 @@ igraph_error_t igraphmodule_i_Graph_get_shortest_path_astar_callback( if (igraphmodule_PyObject_to_real_t(result_o, result)) { /* Error in conversion, return 1 */ + Py_DECREF(result_o); return IGRAPH_FAILURE; } + Py_DECREF(result_o); return IGRAPH_SUCCESS; } From cc4fbce775114f279898bc29b5283f5c4e364013 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 3 Sep 2024 10:46:43 +0200 Subject: [PATCH 132/276] ci: re-enable tests for Python 3.13 because SciPy now has wheels for 3.13 --- .github/workflows/build.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 98784cd75..797fdbb3c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,9 +35,8 @@ jobs: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" # Skip tests for Python 3.10 onwards because SciPy does not have - # 32-bit wheels for Linux. Also skip tests for Python 3.13 because - # SciPy does not have wheels for Python 3.13 at all right now. - CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686 cp313-manylinux_*" + # 32-bit wheels for Linux. + CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686" - name: Build wheels (musllinux) uses: pypa/cibuildwheel@v2.20.0 @@ -165,8 +164,6 @@ jobs: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_ENVIRONMENT: "LDFLAGS=-L$HOME/local/lib" - # Skip tests for Python 3.13 because SciPy does not have wheels for Python 3.13 at all right now. - CIBW_TEST_SKIP: "cp313-*" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_arch }} ${{ matrix.cmake_extra_args }} -DCMAKE_PREFIX_PATH=$HOME/local - uses: actions/upload-artifact@v4 @@ -270,9 +267,8 @@ jobs: CIBW_BUILD: "*-${{ matrix.wheel_arch }}" CIBW_TEST_COMMAND: "cd /d {project} && pip install --prefer-binary \".[test]\" && python -m pytest tests" # Skip tests for Python 3.10 onwards because SciPy does not have - # 32-bit wheels for Linux. Also skip tests for Python 3.13 because - # SciPy does not have wheels for Python 3.13 at all right now. - CIBW_TEST_SKIP: "cp310-win32 cp311-win32 cp312-win32 cp313-*" + # 32-bit wheels for Linux. + CIBW_TEST_SKIP: "cp310-win32 cp311-win32 cp312-win32 cp313-win32" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVCPKG_TARGET_TRIPLET=${{ matrix.vcpkg_arch }}-windows-static-md -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake -A ${{ matrix.cmake_arch }} IGRAPH_EXTRA_LIBRARY_PATH: C:/vcpkg/installed/${{ matrix.vcpkg_arch }}-windows-static-md/lib/ IGRAPH_STATIC_EXTENSION: True From 77698a00cc3964579d0d05639e615b516d78c54a Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 3 Sep 2024 12:00:13 +0200 Subject: [PATCH 133/276] ci: disable PyPy 3.8 as it is EOL --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 797fdbb3c..5c3651368 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,7 @@ on: [push, pull_request] env: CIBW_ENVIRONMENT_PASS_LINUX: PYTEST_TIMEOUT CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test]' && python -m pytest -v tests" - CIBW_SKIP: "cp36-* cp37-* pp37-*" + CIBW_SKIP: "cp36-* cp37-* pp37-* pp38-*" PYTEST_TIMEOUT: 60 MACOSX_DEPLOYMENT_TARGET: "10.9" From dd5ce269aef6807a9442b437da36b7a9542619ac Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 3 Sep 2024 12:12:55 +0200 Subject: [PATCH 134/276] ci: update Python to 3.12.1 when building with Pyodide --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c3651368..79b546fea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -184,7 +184,7 @@ jobs: - uses: actions/setup-python@v5 name: Install Python with: - python-version: '3.11.2' + python-version: '3.12.1' - name: Install OS dependencies run: From 72506cba1ca372ef6c36bca5b604f807e1687432 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 3 Sep 2024 12:20:03 +0200 Subject: [PATCH 135/276] ci: also try disabling PyPy 3.9 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 79b546fea..b3ddc6850 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,7 @@ on: [push, pull_request] env: CIBW_ENVIRONMENT_PASS_LINUX: PYTEST_TIMEOUT CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test]' && python -m pytest -v tests" - CIBW_SKIP: "cp36-* cp37-* pp37-* pp38-*" + CIBW_SKIP: "cp36-* cp37-* pp37-* pp38-* pp39-*" PYTEST_TIMEOUT: 60 MACOSX_DEPLOYMENT_TARGET: "10.9" From 3e8d9f2f8411d5296c660ded932598b8c90737fe Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 3 Sep 2024 13:52:07 +0200 Subject: [PATCH 136/276] ci: try to fix PyPy failures in a different way --- .github/workflows/build.yml | 2 +- pyproject.toml | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3ddc6850..a86043b91 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,7 @@ on: [push, pull_request] env: CIBW_ENVIRONMENT_PASS_LINUX: PYTEST_TIMEOUT CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test]' && python -m pytest -v tests" - CIBW_SKIP: "cp36-* cp37-* pp37-* pp38-* pp39-*" + CIBW_SKIP: "cp36-* cp37-* pp37-*" PYTEST_TIMEOUT: 60 MACOSX_DEPLOYMENT_TARGET: "10.9" diff --git a/pyproject.toml b/pyproject.toml index 0ded74ac4..ac22a60d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,10 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = [ + "wheel", + # pin setuptools: + # https://github.com/airspeed-velocity/asv/pull/1426#issuecomment-2290658198 + "setuptools>=64,<72.2.0" +] build-backend = "setuptools.build_meta" [tool.ruff] From 137b48399afbccd2b297ab59d167ad17069b30d5 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 3 Sep 2024 13:52:46 +0200 Subject: [PATCH 137/276] ci: fix Emscripten version for newer Pyodide --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a86043b91..eb0ab1eeb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -192,7 +192,7 @@ jobs: - uses: mymindstorm/setup-emsdk@v14 with: - version: '3.1.52' + version: '3.1.58' actions-cache-folder: 'emsdk-cache' - name: Build wheel From d9b11f809d6b5289c19163a79374df23b58bfac4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 3 Sep 2024 15:32:25 +0200 Subject: [PATCH 138/276] ci: skip SciPy tests for Python 3.13 on Linux/i686 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb0ab1eeb..69d4e92f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,7 +36,7 @@ jobs: CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" # Skip tests for Python 3.10 onwards because SciPy does not have # 32-bit wheels for Linux. - CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686" + CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686 cp313-manylinux_i686" - name: Build wheels (musllinux) uses: pypa/cibuildwheel@v2.20.0 From b7c9a4195757a28846b135906d299b3c65092383 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 3 Sep 2024 15:41:34 +0200 Subject: [PATCH 139/276] fix: Python 3.13 now provides PyLong_AsInt() so we need to rename ours --- src/_igraph/convert.c | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 6ee49205c..b02edcbd2 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -41,14 +41,18 @@ /** * \brief Converts a Python long to a C int - * + * * This is similar to PyLong_AsLong, but it checks for overflow first and * throws an exception if necessary. This variant is needed for enum conversions * because we assume that enums fit into an int. * + * Note that Python 3.13 also provides a PyLong_AsInt_OutArg() function, hence we need + * a different name for this function. The difference is that PyLong_AsInt_OutArg() + * needs an extra call to PyErr_Occurred() to disambiguate in case of errors. + * * Returns -1 if there was an error, 0 otherwise. */ -int PyLong_AsInt(PyObject* obj, int* result) { +int PyLong_AsInt_OutArg(PyObject* obj, int* result) { long dummy = PyLong_AsLong(obj); if (dummy < INT_MIN) { PyErr_SetString(PyExc_OverflowError, "long integer too small for conversion to C int"); @@ -92,7 +96,7 @@ int igraphmodule_PyObject_to_enum(PyObject *o, return 0; if (PyLong_Check(o)) - return PyLong_AsInt(o, result); + return PyLong_AsInt_OutArg(o, result); s = PyUnicode_CopyAsString(o); if (s == 0) { @@ -174,7 +178,7 @@ int igraphmodule_PyObject_to_enum_strict(PyObject *o, } if (PyLong_Check(o)) { - return PyLong_AsInt(o, result); + return PyLong_AsInt_OutArg(o, result); } s = PyUnicode_CopyAsString(o); @@ -410,15 +414,15 @@ int igraphmodule_PyObject_to_eigen_which_t(PyObject *o, } w->pos = w_pos_int; } else if (!strcasecmp(kv, "howmany")) { - if (PyLong_AsInt(value, &w->howmany)) { + if (PyLong_AsInt_OutArg(value, &w->howmany)) { return -1; } } else if (!strcasecmp(kv, "il")) { - if (PyLong_AsInt(value, &w->il)) { + if (PyLong_AsInt_OutArg(value, &w->il)) { return -1; } } else if (!strcasecmp(kv, "iu")) { - if (PyLong_AsInt(value, &w->iu)) { + if (PyLong_AsInt_OutArg(value, &w->iu)) { return -1; } } else if (!strcasecmp(kv, "vl")) { @@ -426,7 +430,7 @@ int igraphmodule_PyObject_to_eigen_which_t(PyObject *o, } else if (!strcasecmp(kv, "vu")) { w->vu = PyFloat_AsDouble(value); } else if (!strcasecmp(kv, "vestimate")) { - if (PyLong_AsInt(value, &w->vestimate)) { + if (PyLong_AsInt_OutArg(value, &w->vestimate)) { return -1; } } else if (!strcasecmp(kv, "balance")) { From 4e4ecd2c80384a9f3ddd505768aad147e4d1cfed Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 3 Sep 2024 15:43:25 +0200 Subject: [PATCH 140/276] doc: fix comments of PyLong_AsInt_OutArg() --- src/_igraph/convert.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index b02edcbd2..1964d40e1 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -41,13 +41,13 @@ /** * \brief Converts a Python long to a C int - * + * * This is similar to PyLong_AsLong, but it checks for overflow first and * throws an exception if necessary. This variant is needed for enum conversions * because we assume that enums fit into an int. * - * Note that Python 3.13 also provides a PyLong_AsInt_OutArg() function, hence we need - * a different name for this function. The difference is that PyLong_AsInt_OutArg() + * Note that Python 3.13 also provides a PyLong_AsInt() function, hence we need + * a different name for this function. The difference is that PyLong_AsInt() * needs an extra call to PyErr_Occurred() to disambiguate in case of errors. * * Returns -1 if there was an error, 0 otherwise. From c7644a887a639249d8fcacf014816744fd60109d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 3 Sep 2024 16:04:41 +0200 Subject: [PATCH 141/276] doc: fix typo --- src/_igraph/graphobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 38ab0f5b5..9faabab39 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -14732,7 +14732,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "variants become equivalent in the limit of sparse graphs where M{q_{ij}}\n" "approaches zero. In the original Chung-Lu model, selectable by setting\n" "C{variant} to C{\"original\"}, M{p_{ij} = min(q_{ij}, 1)}.\n" - "The C{\"maxent\"} variant, sometimes referred to a the generalized\n" + "The C{\"maxent\"} variant, sometimes referred to as the generalized\n" "random graph, uses M{p_{ij} = q_{ij} / (1 + q_{ij})}, and is equivalent\n" "to a maximum entropy model (i.e. exponential random graph model) with\n" "a constraint on expected degrees, see Park and Newman (2004), Section B,\n" From 29cd810af868416af4f16ae07f2306ad37937819 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 4 Sep 2024 15:45:43 +0200 Subject: [PATCH 142/276] chore: updated changelog and vendored igraph --- CHANGELOG.md | 22 ++++++++++++++++++++++ vendor/source/igraph | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1cba8656..5644c8f92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # igraph Python interface changelog +## [master] + +### Added + +- Added `Graph.feedback_vertex_set()` to calculate a feedback vertex set of the + graph. + +- Added new methods to `Graph.feedback_arc_set()` that allows the user to + select the specific integer problem formulation used by the underlying + solver. + +### Changed + +- Ensured compatibility with Python 3.13. + +- The C core of igraph was updated to version 0.10.14. + +### Fixed + +- Fixed a potential memory leak in the `Graph.get_shortest_path_astar()` heuristic + function callback + ## [0.11.6] - 2024-07-08 ### Added diff --git a/vendor/source/igraph b/vendor/source/igraph index 33c01b0a1..3dd336a4e 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 33c01b0a1ee5cb014bbd1077431a76bac46b7d8c +Subproject commit 3dd336a4e8114e0373b4bd9cf4d9837ffe9e703c From 23fd3b986d219e1469b6d72ecb4a9bbd707bf80a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:32:21 +0000 Subject: [PATCH 143/276] build(deps): bump pypa/cibuildwheel from 2.20.0 to 2.21.1 Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.20.0 to 2.21.1. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.20.0...v2.21.1) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 69d4e92f4..d080f8a32 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: python-version: '3.8' - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.20.0 + uses: pypa/cibuildwheel@v2.21.1 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -39,7 +39,7 @@ jobs: CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686 cp313-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.20.0 + uses: pypa/cibuildwheel@v2.21.1 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" @@ -65,7 +65,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.20.0 + uses: pypa/cibuildwheel@v2.21.1 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -91,7 +91,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.20.0 + uses: pypa/cibuildwheel@v2.21.1 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -159,7 +159,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.20.0 + uses: pypa/cibuildwheel@v2.21.1 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" @@ -261,7 +261,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.20.0 + uses: pypa/cibuildwheel@v2.21.1 env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From 98748ea0cab1da5311211d67a96f79ba0b7c0ee7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 13:07:01 +0000 Subject: [PATCH 144/276] build(deps): bump pypa/cibuildwheel from 2.21.1 to 2.21.2 Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.21.1 to 2.21.2. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.21.1...v2.21.2) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d080f8a32..42d2a15f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: python-version: '3.8' - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.21.1 + uses: pypa/cibuildwheel@v2.21.2 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -39,7 +39,7 @@ jobs: CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686 cp313-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.21.1 + uses: pypa/cibuildwheel@v2.21.2 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" @@ -65,7 +65,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.21.1 + uses: pypa/cibuildwheel@v2.21.2 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -91,7 +91,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.21.1 + uses: pypa/cibuildwheel@v2.21.2 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -159,7 +159,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.21.1 + uses: pypa/cibuildwheel@v2.21.2 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" @@ -261,7 +261,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.21.1 + uses: pypa/cibuildwheel@v2.21.2 env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From 4719c24657f849754139633a62fa4e8e849cc111 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:37:09 +0000 Subject: [PATCH 145/276] build(deps): bump pypa/cibuildwheel from 2.21.2 to 2.21.3 Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.21.2 to 2.21.3. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.21.2...v2.21.3) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 42d2a15f1..18784b018 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: python-version: '3.8' - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.21.2 + uses: pypa/cibuildwheel@v2.21.3 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -39,7 +39,7 @@ jobs: CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686 cp313-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.21.2 + uses: pypa/cibuildwheel@v2.21.3 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" @@ -65,7 +65,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.21.2 + uses: pypa/cibuildwheel@v2.21.3 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -91,7 +91,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.21.2 + uses: pypa/cibuildwheel@v2.21.3 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -159,7 +159,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.21.2 + uses: pypa/cibuildwheel@v2.21.3 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" @@ -261,7 +261,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.21.2 + uses: pypa/cibuildwheel@v2.21.3 env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From 0a761ad31cf9f3cf549d53c5a993a5b5d14dda9e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 24 Oct 2024 10:45:47 +0200 Subject: [PATCH 146/276] fix: pin down Sphinx to 8.0.2 until a new PyDoctor is released that is compatible with Sphinx 8.1 --- doc/source/requirements.txt | 1 + scripts/mkdoc.sh | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/requirements.txt b/doc/source/requirements.txt index 869964a7d..715f2c3d3 100644 --- a/doc/source/requirements.txt +++ b/doc/source/requirements.txt @@ -2,6 +2,7 @@ pip wheel requests>=2.28.1 +sphinx==8.0.2 sphinx-gallery>=0.14.0 sphinx-rtd-theme>=1.3.0 pydoctor>=23.4.0 diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index 3a65cc264..f4090eceb 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -51,11 +51,11 @@ if [ ! -d ".venv" ]; then # Install sphinx, matplotlib, pandas, scipy, wheel and pydoctor into the venv. # doc2dash is optional; it will be installed when -d is given - .venv/bin/pip install -U pip wheel sphinx matplotlib pandas scipy pydoctor sphinx-rtd-theme + .venv/bin/pip install -U pip wheel sphinx==8.0.2 matplotlib pandas scipy pydoctor sphinx-rtd-theme fi # Make sure that Sphinx, PyDoctor (and maybe doc2dash) are up-to-date in the virtualenv -.venv/bin/pip install -U sphinx pydoctor sphinx-gallery sphinxcontrib-jquery sphinx-rtd-theme +.venv/bin/pip install -U sphinx==8.0.2 pydoctor sphinx-gallery sphinxcontrib-jquery sphinx-rtd-theme if [ x$DOC2DASH = x1 ]; then .venv/bin/pip install -U doc2dash fi From 1eff3ef9815b7e139d465c7ad80fdf328df0021f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 24 Oct 2024 10:46:07 +0200 Subject: [PATCH 147/276] chore: bumped version to 0.11.7 --- CHANGELOG.md | 5 +++-- src/igraph/version.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5644c8f92..2aebb80bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # igraph Python interface changelog -## [master] +## [0.11.7] - 2024-10-24 ### Added @@ -706,7 +706,8 @@ Please refer to the commit logs at https://github.com/igraph/python-igraph for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. -[main]: https://github.com/igraph/python-igraph/compare/0.11.6...main +[main]: https://github.com/igraph/python-igraph/compare/0.11.7...main +[0.11.7]: https://github.com/igraph/python-igraph/compare/0.11.6...0.11.7 [0.11.6]: https://github.com/igraph/python-igraph/compare/0.11.5...0.11.6 [0.11.5]: https://github.com/igraph/python-igraph/compare/0.11.4...0.11.5 [0.11.4]: https://github.com/igraph/python-igraph/compare/0.11.3...0.11.4 diff --git a/src/igraph/version.py b/src/igraph/version.py index dfef0870c..129b73173 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 11, 6) +__version_info__ = (0, 11, 7) __version__ = ".".join("{0}".format(x) for x in __version_info__) From fbbf07abe77a77a3c9696c2ac32878b0cfa23c08 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 24 Oct 2024 10:46:48 +0200 Subject: [PATCH 148/276] chore: bumped version to 0.11.7 From ceff4833466009be01cb9a22211ecb318361185f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 24 Oct 2024 10:59:07 +0200 Subject: [PATCH 149/276] fix(rtd): build with Python 3.11 so we can get Sphinx 8 --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b323c94be..7de01065a 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -21,7 +21,7 @@ build: - zlib1g-dev tools: - python: "3.9" + python: "3.11" # You can also specify other tool versions: # nodejs: "16" # rust: "1.55" From 58b487064d73e5ad35f15efc51a5cd0d4e7a181a Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 24 Oct 2024 11:03:27 +0200 Subject: [PATCH 150/276] fix(rtd): fix version number of Python in scripts/rtd_prebuild.sh --- scripts/rtd_prebuild.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/rtd_prebuild.sh b/scripts/rtd_prebuild.sh index 9b41bab13..461cf2cc4 100755 --- a/scripts/rtd_prebuild.sh +++ b/scripts/rtd_prebuild.sh @@ -12,4 +12,4 @@ echo "Modularize pure Python modules" echo "NOTE: Patch pydoctor to trigger build-finished before RTD extension" # see https://www.sphinx-doc.org/en/master/extdev/appapi.html#sphinx.application.Sphinx.connect # see also https://github.com/readthedocs/readthedocs.org/pull/4054 - might or might not be exactly what we are seeing here -sed -i 's/on_build_finished)/on_build_finished, priority=490)/' /home/docs/checkouts/readthedocs.org/user_builds/igraph/envs/${READTHEDOCS_VERSION}/lib/python3.9/site-packages/pydoctor/sphinx_ext/build_apidocs.py +sed -i 's/on_build_finished)/on_build_finished, priority=490)/' /home/docs/checkouts/readthedocs.org/user_builds/igraph/envs/${READTHEDOCS_VERSION}/lib/python3.11/site-packages/pydoctor/sphinx_ext/build_apidocs.py From fac8f1a0d4b79e348e26b43299086d05452f25d3 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 24 Oct 2024 11:15:57 +0200 Subject: [PATCH 151/276] fix(rtd): okay, back to Sphinx 7 --- doc/source/requirements.txt | 2 +- scripts/mkdoc.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/requirements.txt b/doc/source/requirements.txt index 715f2c3d3..9044d840b 100644 --- a/doc/source/requirements.txt +++ b/doc/source/requirements.txt @@ -2,7 +2,7 @@ pip wheel requests>=2.28.1 -sphinx==8.0.2 +sphinx==7.4.7 sphinx-gallery>=0.14.0 sphinx-rtd-theme>=1.3.0 pydoctor>=23.4.0 diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index f4090eceb..db6487d98 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -51,11 +51,11 @@ if [ ! -d ".venv" ]; then # Install sphinx, matplotlib, pandas, scipy, wheel and pydoctor into the venv. # doc2dash is optional; it will be installed when -d is given - .venv/bin/pip install -U pip wheel sphinx==8.0.2 matplotlib pandas scipy pydoctor sphinx-rtd-theme + .venv/bin/pip install -U pip wheel sphinx==7.4.7 matplotlib pandas scipy pydoctor sphinx-rtd-theme fi # Make sure that Sphinx, PyDoctor (and maybe doc2dash) are up-to-date in the virtualenv -.venv/bin/pip install -U sphinx==8.0.2 pydoctor sphinx-gallery sphinxcontrib-jquery sphinx-rtd-theme +.venv/bin/pip install -U sphinx==7.4.7 pydoctor sphinx-gallery sphinxcontrib-jquery sphinx-rtd-theme if [ x$DOC2DASH = x1 ]; then .venv/bin/pip install -U doc2dash fi From d6019e8d017eab7011c8dd304cc3470ba7c785cf Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 25 Oct 2024 10:39:47 +0200 Subject: [PATCH 152/276] chore: bumped version to 0.11.8 --- CHANGELOG.md | 10 +++++++++- src/igraph/version.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aebb80bc..f040f80ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # igraph Python interface changelog +## [0.11.8] - 2024-10-25 + +### Fixed + +- Fixed documentation build on Read The Docs. No other changes compared to + 0.11.7. + ## [0.11.7] - 2024-10-24 ### Added @@ -706,7 +713,8 @@ Please refer to the commit logs at https://github.com/igraph/python-igraph for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. -[main]: https://github.com/igraph/python-igraph/compare/0.11.7...main +[main]: https://github.com/igraph/python-igraph/compare/0.11.8...main +[0.11.8]: https://github.com/igraph/python-igraph/compare/0.11.7...0.11.8 [0.11.7]: https://github.com/igraph/python-igraph/compare/0.11.6...0.11.7 [0.11.6]: https://github.com/igraph/python-igraph/compare/0.11.5...0.11.6 [0.11.5]: https://github.com/igraph/python-igraph/compare/0.11.4...0.11.5 diff --git a/src/igraph/version.py b/src/igraph/version.py index 129b73173..81a2aa780 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 11, 7) +__version_info__ = (0, 11, 8) __version__ = ".".join("{0}".format(x) for x in __version_info__) From f1967ec36f119cd2da8e4c147d70b1a9a5d0a852 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 28 Oct 2024 13:14:00 +0100 Subject: [PATCH 153/276] chore: drop support for Python 3.8 as it is now EOL --- .github/workflows/build.yml | 12 ++++++------ README.md | 2 +- setup.py | 9 +++------ src/_igraph/bfsiter.c | 3 --- src/_igraph/dfsiter.c | 3 --- src/_igraph/graphobject.c | 3 --- 6 files changed, 10 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 18784b018..80cb71f5c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,7 @@ on: [push, pull_request] env: CIBW_ENVIRONMENT_PASS_LINUX: PYTEST_TIMEOUT CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test]' && python -m pytest -v tests" - CIBW_SKIP: "cp36-* cp37-* pp37-*" + CIBW_SKIP: "cp36-* cp37-* cp38-* pp37-*" PYTEST_TIMEOUT: 60 MACOSX_DEPLOYMENT_TARGET: "10.9" @@ -27,7 +27,7 @@ jobs: - uses: actions/setup-python@v5 name: Install Python with: - python-version: '3.8' + python-version: '3.9' - name: Build wheels (manylinux) uses: pypa/cibuildwheel@v2.21.3 @@ -140,12 +140,12 @@ jobs: - uses: actions/setup-python@v5 name: Install Python with: - python-version: '3.8' + python-version: '3.9' - name: Install OS dependencies if: steps.cache-c-core.outputs.cache-hit != 'true' || steps.cache-c-deps.outputs.cache-hit != 'true' # Only needed when building the C core or libomp run: - brew install ninja autoconf automake libtool cmake + brew install ninja autoconf automake libtool - name: Install OpenMP library if: steps.cache-c-deps.outputs.cache-hit != 'true' @@ -235,7 +235,7 @@ jobs: - uses: actions/setup-python@v5 name: Install Python with: - python-version: '3.8' + python-version: '3.9' - name: Cache installed C core id: cache-c-core @@ -305,7 +305,7 @@ jobs: - uses: actions/setup-python@v5 name: Install Python with: - python-version: '3.8' + python-version: '3.9' - name: Build sdist run: | diff --git a/README.md b/README.md index 6fb686d27..1f7162788 100644 --- a/README.md +++ b/README.md @@ -302,7 +302,7 @@ faster than the first one as the C core does not need to be recompiled. We aim to keep up with the development cycle of Python and support all official Python versions that have not reached their end of life yet. Currently this -means that we support Python 3.8 to 3.12, inclusive. Please refer to [this +means that we support Python 3.9 to 3.13, inclusive. Please refer to [this page](https://devguide.python.org/versions/) for the status of Python branches and let us know if you encounter problems with `igraph` on any of the non-EOL Python versions. diff --git a/setup.py b/setup.py index 1610a5f1d..40d386d24 100644 --- a/setup.py +++ b/setup.py @@ -7,8 +7,8 @@ ########################################################################### # Check Python's version info and exit early if it is too old -if sys.version_info < (3, 8): - print("This module requires Python >= 3.8") +if sys.version_info < (3, 9): + print("This module requires Python >= 3.9") sys.exit(0) ########################################################################### @@ -916,8 +916,6 @@ def get_tag(self): bdist_wheel_abi3 = None # We are going to build an abi3 wheel if we are at least on CPython 3.9. -# This is because the C code contains conditionals for CPython 3.8 so we cannot -# use an abi3 wheel built with CPython 3.8 on CPython 3.9 should_build_abi3_wheel = ( bdist_wheel_abi3 and platform.python_implementation() == "CPython" @@ -1039,7 +1037,7 @@ def get_tag(self): "pydoctor>=23.4.0", ], }, - "python_requires": ">=3.8", + "python_requires": ">=3.9", "headers": headers, "platforms": "ALL", "keywords": [ @@ -1057,7 +1055,6 @@ def get_tag(self): "Operating System :: OS Independent", "Programming Language :: C", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c index cf7cafdfe..e0c976245 100644 --- a/src/_igraph/bfsiter.c +++ b/src/_igraph/bfsiter.c @@ -121,10 +121,7 @@ static int igraphmodule_BFSIter_traverse(igraphmodule_BFSIterObject *self, visitproc visit, void *arg) { RC_TRAVERSE("BFSIter", self); Py_VISIT(self->gref); -#if PY_VERSION_HEX >= 0x03090000 - // This was not needed before Python 3.9 (Python issue 35810 and 40217) Py_VISIT(Py_TYPE(self)); -#endif return 0; } diff --git a/src/_igraph/dfsiter.c b/src/_igraph/dfsiter.c index 6064a9862..e8273173c 100644 --- a/src/_igraph/dfsiter.c +++ b/src/_igraph/dfsiter.c @@ -122,10 +122,7 @@ static int igraphmodule_DFSIter_traverse(igraphmodule_DFSIterObject *self, visitproc visit, void *arg) { RC_TRAVERSE("DFSIter", self); Py_VISIT(self->gref); -#if PY_VERSION_HEX >= 0x03090000 - // This was not needed before Python 3.9 (Python issue 35810 and 40217) Py_VISIT(Py_TYPE(self)); -#endif return 0; } diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 9faabab39..95a29e5ff 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -141,10 +141,7 @@ int igraphmodule_Graph_traverse(igraphmodule_GraphObject * self, } } -#if PY_VERSION_HEX >= 0x03090000 - // This was not needed before Python 3.9 (Python issue 35810 and 40217) Py_VISIT(Py_TYPE(self)); -#endif return 0; } From aa299a2f5f0ebad6ef8870e162f6b4b4a421a492 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 28 Oct 2024 13:50:52 +0100 Subject: [PATCH 154/276] chore: also drop support for PyPy 3.8 --- .github/workflows/build.yml | 2 +- CHANGELOG.md | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 80cb71f5c..9d4ba187c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,7 @@ on: [push, pull_request] env: CIBW_ENVIRONMENT_PASS_LINUX: PYTEST_TIMEOUT CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test]' && python -m pytest -v tests" - CIBW_SKIP: "cp36-* cp37-* cp38-* pp37-*" + CIBW_SKIP: "cp36-* cp37-* cp38-* pp37-* pp38-*" PYTEST_TIMEOUT: 60 MACOSX_DEPLOYMENT_TARGET: "10.9" diff --git a/CHANGELOG.md b/CHANGELOG.md index f040f80ee..49918e543 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # igraph Python interface changelog +## [main] + +### Changed + +- Dropped support for Python 3.8 as it has now reached its end of life. + ## [0.11.8] - 2024-10-25 ### Fixed From 2abbca78c934d9d673ebc9e5c9540f72b16f5d2d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 28 Oct 2024 14:31:00 +0100 Subject: [PATCH 155/276] ci: move MACOSX_DEPLOYMENT_TARGET envvar to the macOS job --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9d4ba187c..47e78b36d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,6 @@ env: CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test]' && python -m pytest -v tests" CIBW_SKIP: "cp36-* cp37-* cp38-* pp37-* pp38-*" PYTEST_TIMEOUT: 60 - MACOSX_DEPLOYMENT_TARGET: "10.9" jobs: build_wheel_linux: @@ -108,6 +107,7 @@ jobs: runs-on: macos-latest env: LLVM_VERSION: "14.0.5" + MACOSX_DEPLOYMENT_TARGET: "10.9" strategy: matrix: include: From 60e11d95cf201dde764e5711e617572632d66fab Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 28 Oct 2024 14:31:21 +0100 Subject: [PATCH 156/276] doc: disambiguae comment in igraph_rng_Python_set_generator() --- src/_igraph/random.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/random.c b/src/_igraph/random.c index aef52c379..0552aba7d 100644 --- a/src/_igraph/random.c +++ b/src/_igraph/random.c @@ -112,7 +112,7 @@ PyObject* igraph_rng_Python_set_generator(PyObject* self, PyObject* object) { GET_FUNC("random"); new_state.random_func = func; GET_FUNC("gauss"); new_state.gauss_func = func; - /* construct the arguments of getrandbits(RNG_BITS) and randint(0, 2 ^ RNG_BITS-1) + /* construct the arguments of getrandbits(RNG_BITS) and randint(0, (2^RNG_BITS)-1) * in advance */ new_state.rng_bits_as_pyobject = PyLong_FromLong(RNG_BITS); if (new_state.rng_bits_as_pyobject == 0) { From b972656075db8f0303ff49285b073610789177ad Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 28 Oct 2024 14:42:57 +0100 Subject: [PATCH 157/276] doc: remove logo as it caused layout problems --- doc/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 334b52e6a..2dcdfa5fe 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -148,7 +148,7 @@ def get_igraph_version(): # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = "_static/logo-black.svg" +# html_logo = "_static/logo-black.svg" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 From 1a7429f11fe832a990a95226920e1138ca03464e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 29 Oct 2024 12:11:26 +0100 Subject: [PATCH 158/276] fix: pin setuptools < 72.2 only for PyPy --- pyproject.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ac22a60d4..d92754846 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,12 @@ requires = [ "wheel", # pin setuptools: # https://github.com/airspeed-velocity/asv/pull/1426#issuecomment-2290658198 - "setuptools>=64,<72.2.0" + # Most likely cause: + # https://github.com/pypa/distutils/issues/283 + # Workaround based on this commit: + # https://github.com/harfbuzz/uharfbuzz/commit/9b607bd06fb17fcb4abe3eab5c4f342ad08309d7 + "setuptools>=64,<72.2.0; platform_python_implementation == 'PyPy'", + "setuptools>=64; platform_python_implementation != 'PyPy'" ] build-backend = "setuptools.build_meta" From 6e3d722d326730f06c000de3329c026ec51c8388 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 29 Oct 2024 12:12:03 +0100 Subject: [PATCH 159/276] fix: tabs to spaces --- pyproject.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d92754846..d62d390de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,12 +3,12 @@ requires = [ "wheel", # pin setuptools: # https://github.com/airspeed-velocity/asv/pull/1426#issuecomment-2290658198 - # Most likely cause: - # https://github.com/pypa/distutils/issues/283 - # Workaround based on this commit: - # https://github.com/harfbuzz/uharfbuzz/commit/9b607bd06fb17fcb4abe3eab5c4f342ad08309d7 + # Most likely cause: + # https://github.com/pypa/distutils/issues/283 + # Workaround based on this commit: + # https://github.com/harfbuzz/uharfbuzz/commit/9b607bd06fb17fcb4abe3eab5c4f342ad08309d7 "setuptools>=64,<72.2.0; platform_python_implementation == 'PyPy'", - "setuptools>=64; platform_python_implementation != 'PyPy'" + "setuptools>=64; platform_python_implementation != 'PyPy'" ] build-backend = "setuptools.build_meta" From c8022ff12a07108af9b1e6393ce3b10fae678c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Tue, 5 Nov 2024 14:41:06 +0000 Subject: [PATCH 160/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 3dd336a4e..fb81bda14 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 3dd336a4e8114e0373b4bd9cf4d9837ffe9e703c +Subproject commit fb81bda143ad35aaa50cbc508f917671402c38f6 From 4ee238adea203a2d1b6e4a64961c75c4274ed59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Tue, 5 Nov 2024 19:46:59 +0000 Subject: [PATCH 161/276] chore: update C core again --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index fb81bda14..a662f79d4 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit fb81bda143ad35aaa50cbc508f917671402c38f6 +Subproject commit a662f79d46970bbcc2888e69dce5daa3fee8906e From a6da7b281329e47fe37225a8b4d5040a28cae737 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 6 Nov 2024 11:32:14 +0100 Subject: [PATCH 162/276] chore: updated vendored igraph to 0.10.15 --- CHANGELOG.md | 6 +++++- vendor/source/igraph | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49918e543..dea92015e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ - Dropped support for Python 3.8 as it has now reached its end of life. +- The C core of igraph was updated to version 0.10.15. + ## [0.11.8] - 2024-10-25 ### Fixed @@ -28,7 +30,9 @@ - Ensured compatibility with Python 3.13. -- The C core of igraph was updated to version 0.10.14. +- The C core of igraph was updated to an interim commit (3dd336a) between + version 0.10.13 and version 0.10.15. Earlier versions of this changelog + mistakenly marked this revision as version 0.10.14. ### Fixed diff --git a/vendor/source/igraph b/vendor/source/igraph index a662f79d4..635b432ef 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit a662f79d46970bbcc2888e69dce5daa3fee8906e +Subproject commit 635b432eff0a89580ac9bb98068d2fbc8ef374f2 From 81288fd98fad701bbdc902987c791bcac101743f Mon Sep 17 00:00:00 2001 From: Tim Bernhard Date: Wed, 13 Nov 2024 16:56:05 +0100 Subject: [PATCH 163/276] Explicitly require cmake as a build dependency --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d62d390de..c4915214f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,8 @@ requires = [ # Workaround based on this commit: # https://github.com/harfbuzz/uharfbuzz/commit/9b607bd06fb17fcb4abe3eab5c4f342ad08309d7 "setuptools>=64,<72.2.0; platform_python_implementation == 'PyPy'", - "setuptools>=64; platform_python_implementation != 'PyPy'" + "setuptools>=64; platform_python_implementation != 'PyPy'", + "cmake>=3.18" ] build-backend = "setuptools.build_meta" From ff0d89983e22d06e26eba6e153b1599ebb28f2ee Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 16 Nov 2024 12:10:14 +0100 Subject: [PATCH 164/276] chore: specify that we are now compatible with Python 3.13 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 40d386d24..73a31067b 100644 --- a/setup.py +++ b/setup.py @@ -1059,6 +1059,7 @@ def get_tag(self): "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3 :: Only", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Information Analysis", From c6ec1277bf0c1752373e2ea719f615b08a1701bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sun, 17 Nov 2024 16:53:22 +0000 Subject: [PATCH 165/276] test: increase tolerance for matplotlib backend image comparisons, fixes #805 --- tests/drawing/matplotlib/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/drawing/matplotlib/utils.py b/tests/drawing/matplotlib/utils.py index 36bd50f1f..f32ad3f73 100644 --- a/tests/drawing/matplotlib/utils.py +++ b/tests/drawing/matplotlib/utils.py @@ -71,7 +71,7 @@ def wrapper(*args, **kwargs): def image_comparison( baseline_images, - tol=0.025, + tol=4.0, remove_text=False, savefig_kwarg=None, # Default of mpl_test_settings fixture and cleanup too. From 50aa98df3cf4494f1aae02445328cef5e0d6a3d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:05:19 +0000 Subject: [PATCH 166/276] build(deps): bump pypa/cibuildwheel from 2.21.3 to 2.22.0 Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.21.3 to 2.22.0. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.21.3...v2.22.0) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 47e78b36d..8b5a2e70c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: python-version: '3.9' - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.21.3 + uses: pypa/cibuildwheel@v2.22.0 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -38,7 +38,7 @@ jobs: CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686 cp313-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.21.3 + uses: pypa/cibuildwheel@v2.22.0 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" @@ -64,7 +64,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.21.3 + uses: pypa/cibuildwheel@v2.22.0 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -90,7 +90,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.21.3 + uses: pypa/cibuildwheel@v2.22.0 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -159,7 +159,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.21.3 + uses: pypa/cibuildwheel@v2.22.0 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" @@ -261,7 +261,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.21.3 + uses: pypa/cibuildwheel@v2.22.0 env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From ea772d79e69f4aba8324bd4d2540704352bee8d9 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Mon, 23 Dec 2024 15:02:10 +0300 Subject: [PATCH 167/276] setup.py: import bdist_wheel from setuptools (#810) --- pyproject.toml | 1 - setup.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c4915214f..db932f633 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,5 @@ [build-system] requires = [ - "wheel", # pin setuptools: # https://github.com/airspeed-velocity/asv/pull/1426#issuecomment-2290658198 # Most likely cause: diff --git a/setup.py b/setup.py index 73a31067b..bbd5dbfb1 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ from setuptools import find_packages, setup, Command, Extension try: - from wheel.bdist_wheel import bdist_wheel + from setuptools.command.bdist_wheel import bdist_wheel except ImportError: bdist_wheel = None From 4b6425cd45569b28bb4d75eb39dd30dc07eb362e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 23 Dec 2024 13:04:03 +0100 Subject: [PATCH 168/276] chore: updated contributors list --- .all-contributorsrc | 9 +++ CONTRIBUTORS.md | 175 ++++++++++++++++++++++---------------------- 2 files changed, 98 insertions(+), 86 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 68c701f9e..f021a11fc 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -603,6 +603,15 @@ "contributions": [ "code" ] + }, + { + "login": "m1-s", + "name": "Michael Schneider", + "avatar_url": "https://avatars.githubusercontent.com/u/94642227?v=4", + "profile": "https://github.com/m1-s", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f0945fc51..88c897aed 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -6,92 +6,95 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Tamás Nepusz

💻

Fabio Zanini

💻

Kevin Zhu

💻

Gábor Csárdi

💻

Szabolcs Horvát

💻

Vincent Traag

💻

deeenes

💻

Seungoh Han

💻

Artem V L

💻

Yesung(Isaac) Lee

💻

John Boy

💻

Casper da Costa-Luis

💻

Alberto Alcolea

💻

Árpád Horváth

💻

ebraminio

💻

Fabian Witter

💻

Jan Katins

💻

Nick Eubank

💻

Peter Scott

💻

Sriram-Pattabiraman

💻

Sviatoslav

💻

Ah-Young Nho

💻

Frederik Harwath

💻

Navid Dianati

💻

abe-winter

💻

Alejandro Rivero

💻

Ariki

💻

Casper van Elteren

💻

Charles Tapley Hoyt

💻

Christoph Gohlke

💻

Christopher Falter

💻

FredInChina

💻

Friso van Vollenhoven

💻

Gabor Szarnyas

💻

Gao Fangshu

💻

Grzegorz Chilczuk

💻

Gwyn Ciesla

💻

Hong Xu

💻

Jay Smith

💻

MapleCCC

💻

Marco Köpcke

💻

Markus Elfring

💻

Martino Mensio

💻

Matas

💻

Mike Lissner

💻

Philipp A.

💻

Puneetha Pai

💻

S Murthy

💻

Scott Gigante

💻

Thierry Thomas

💻

Willem van den Boom

💻

Yisu Remy Wang

💻

YY Ahn

💻

kmankinen

💻

odidev

💻

sombreslames

💻

szcf-weiya

💻

tristanlatr

💻

JDPowell648

📖

k.h.lai

💻

Anton Grübel

💻

flange-ipb

💻

Paul m. p. Peny

💻

David R. Connell

💻

Rodrigo Monteiro de Moraes de Arruda Falcão

💻

Kreijstal

💻
Tamás Nepusz
Tamás Nepusz

💻
Fabio Zanini
Fabio Zanini

💻
Kevin Zhu
Kevin Zhu

💻
Gábor Csárdi
Gábor Csárdi

💻
Szabolcs Horvát
Szabolcs Horvát

💻
Vincent Traag
Vincent Traag

💻
deeenes
deeenes

💻
Seungoh Han
Seungoh Han

💻
Artem V L
Artem V L

💻
Yesung(Isaac) Lee
Yesung(Isaac) Lee

💻
John Boy
John Boy

💻
Casper da Costa-Luis
Casper da Costa-Luis

💻
Alberto Alcolea
Alberto Alcolea

💻
Árpád Horváth
Árpád Horváth

💻
ebraminio
ebraminio

💻
Fabian Witter
Fabian Witter

💻
Jan Katins
Jan Katins

💻
Nick Eubank
Nick Eubank

💻
Peter Scott
Peter Scott

💻
Sriram-Pattabiraman
Sriram-Pattabiraman

💻
Sviatoslav
Sviatoslav

💻
Ah-Young Nho
Ah-Young Nho

💻
Frederik Harwath
Frederik Harwath

💻
Navid Dianati
Navid Dianati

💻
abe-winter
abe-winter

💻
Alejandro Rivero
Alejandro Rivero

💻
Ariki
Ariki

💻
Casper van Elteren
Casper van Elteren

💻
Charles Tapley Hoyt
Charles Tapley Hoyt

💻
Christoph Gohlke
Christoph Gohlke

💻
Christopher Falter
Christopher Falter

💻
FredInChina
FredInChina

💻
Friso van Vollenhoven
Friso van Vollenhoven

💻
Gabor Szarnyas
Gabor Szarnyas

💻
Gao Fangshu
Gao Fangshu

💻
Grzegorz Chilczuk
Grzegorz Chilczuk

💻
Gwyn Ciesla
Gwyn Ciesla

💻
Hong Xu
Hong Xu

💻
Jay Smith
Jay Smith

💻
MapleCCC
MapleCCC

💻
Marco Köpcke
Marco Köpcke

💻
Markus Elfring
Markus Elfring

💻
Martino Mensio
Martino Mensio

💻
Matas
Matas

💻
Mike Lissner
Mike Lissner

💻
Philipp A.
Philipp A.

💻
Puneetha Pai
Puneetha Pai

💻
S Murthy
S Murthy

💻
Scott Gigante
Scott Gigante

💻
Thierry Thomas
Thierry Thomas

💻
Willem van den Boom
Willem van den Boom

💻
Yisu Remy Wang
Yisu Remy Wang

💻
YY Ahn
YY Ahn

💻
kmankinen
kmankinen

💻
odidev
odidev

💻
sombreslames
sombreslames

💻
szcf-weiya
szcf-weiya

💻
tristanlatr
tristanlatr

💻
JDPowell648
JDPowell648

📖
k.h.lai
k.h.lai

💻
Anton Grübel
Anton Grübel

💻
flange-ipb
flange-ipb

💻
Paul m. p. Peny
Paul m. p. Peny

💻
David R. Connell
David R. Connell

💻
Rodrigo Monteiro de Moraes de Arruda Falcão
Rodrigo Monteiro de Moraes de Arruda Falcão

💻
Kreijstal
Kreijstal

💻
Michael Schneider
Michael Schneider

💻
From 3ffe8358e7d0282a1bbcb8a69be5bef58c6e870b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 26 Dec 2024 18:40:48 +0100 Subject: [PATCH 169/276] ci: apparently bcrypt is now needed for libxml2 on Windows --- .github/workflows/build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b5a2e70c..9958409a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -272,7 +272,7 @@ jobs: IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVCPKG_TARGET_TRIPLET=${{ matrix.vcpkg_arch }}-windows-static-md -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake -A ${{ matrix.cmake_arch }} IGRAPH_EXTRA_LIBRARY_PATH: C:/vcpkg/installed/${{ matrix.vcpkg_arch }}-windows-static-md/lib/ IGRAPH_STATIC_EXTENSION: True - IGRAPH_EXTRA_LIBRARIES: libxml2,lzma,zlib,iconv,charset + IGRAPH_EXTRA_LIBRARIES: libxml2,lzma,zlib,iconv,charset,bcrypt IGRAPH_EXTRA_DYNAMIC_LIBRARIES: wsock32,ws2_32 - uses: actions/upload-artifact@v4 @@ -382,4 +382,3 @@ jobs: LSAN_OPTIONS: "suppressions=etc/lsan-suppr.txt:print_suppressions=false" run: | LD_PRELOAD=/lib/x86_64-linux-gnu/libasan.so.5:/lib/x86_64-linux-gnu/libubsan.so.1 python -m pytest --capture=sys tests - From bb26aa26d4e635e260c9cdfbaed566549ede5ee8 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 17 Jan 2025 21:50:15 +0100 Subject: [PATCH 170/276] ci: try using the new ARM runners from Github --- .github/workflows/build.yml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9958409a5..8cba2e82e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,18 +51,13 @@ jobs: build_wheel_linux_aarch64_manylinux: name: Build wheels on Linux (aarch64/manylinux) - runs-on: ubuntu-20.04 - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + runs-on: ubuntu-22.04-arm steps: - uses: actions/checkout@v4 with: submodules: true fetch-depth: 0 - - name: Set up QEMU - id: qemu - uses: docker/setup-qemu-action@v3 - - name: Build wheels (manylinux) uses: pypa/cibuildwheel@v2.22.0 env: @@ -77,18 +72,13 @@ jobs: build_wheel_linux_aarch64_musllinux: name: Build wheels on Linux (aarch64/musllinux) - runs-on: ubuntu-20.04 - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + runs-on: ubuntu-22.04-arm steps: - uses: actions/checkout@v4 with: submodules: true fetch-depth: 0 - - name: Set up QEMU - id: qemu - uses: docker/setup-qemu-action@v3 - - name: Build wheels (musllinux) uses: pypa/cibuildwheel@v2.22.0 env: From 2dee46c3a4dc005288d06671c1e3b4626d44288d Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Wed, 22 Jan 2025 14:36:09 +0100 Subject: [PATCH 171/276] Update graphobject.c - reverse cutoff, normalized args (#812) --- src/_igraph/graphobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 95a29e5ff..894e35d26 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -4912,7 +4912,7 @@ PyObject *igraphmodule_Graph_harmonic_centrality(igraphmodule_GraphObject * self return NULL; } if (igraph_harmonic_centrality_cutoff(&self->g, &res, vs, mode, weights, - PyFloat_AsDouble(cutoff_num), PyObject_IsTrue(normalized_o))) { + PyObject_IsTrue(normalized_o), PyFloat_AsDouble(cutoff_num))) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } From 1a96afa63bc5737175f6a5ab75e3e0cfa9f9d6c5 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 22 Jan 2025 14:37:06 +0100 Subject: [PATCH 172/276] chore: updated contributors list --- .all-contributorsrc | 18 ++++++++++++++++++ CONTRIBUTORS.md | 2 ++ 2 files changed, 20 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index f021a11fc..4cabb5284 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -612,6 +612,24 @@ "contributions": [ "code" ] + }, + { + "login": "aothms", + "name": "Thomas Krijnen", + "avatar_url": "https://avatars.githubusercontent.com/u/1096535?v=4", + "profile": "http://thomaskrijnen.com/", + "contributions": [ + "code" + ] + }, + { + "login": "GenieTim", + "name": "Tim Bernhard", + "avatar_url": "https://avatars.githubusercontent.com/u/8596965?v=4", + "profile": "https://github.com/GenieTim", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 88c897aed..f43ef7b6f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -93,6 +93,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Rodrigo Monteiro de Moraes de Arruda Falcão
Rodrigo Monteiro de Moraes de Arruda Falcão

💻 Kreijstal
Kreijstal

💻 Michael Schneider
Michael Schneider

💻 + Thomas Krijnen
Thomas Krijnen

💻 + Tim Bernhard
Tim Bernhard

💻 From ee3e94599b47133ed25995151725e6c86e4eb7ff Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 12 Feb 2025 22:26:43 +0100 Subject: [PATCH 173/276] ci: replace ubuntu-20.04 image with ubuntu-22.04 --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8cba2e82e..8ea75dd4b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ env: jobs: build_wheel_linux: name: Build wheels on Linux (${{ matrix.wheel_arch }}) - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: @@ -163,7 +163,7 @@ jobs: build_wheel_wasm: name: Build wheels for WebAssembly - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -317,7 +317,7 @@ jobs: # for the "Test" step below. build_with_sanitizer: name: Build with sanitizers for debugging purposes - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 env: IGRAPH_CMAKE_EXTRA_ARGS: -DFORCE_COLORED_OUTPUT=ON steps: From 09e4b0184f0cbe91849e1b7c239d9a0f9109815b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 13 Feb 2025 10:53:54 +0100 Subject: [PATCH 174/276] ci: trying to work around sanitizer issues in the new Ubuntu image --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ea75dd4b..b840f01bc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -317,7 +317,7 @@ jobs: # for the "Test" step below. build_with_sanitizer: name: Build with sanitizers for debugging purposes - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest env: IGRAPH_CMAKE_EXTRA_ARGS: -DFORCE_COLORED_OUTPUT=ON steps: @@ -371,4 +371,5 @@ jobs: ASAN_OPTIONS: "detect_stack_use_after_return=1" LSAN_OPTIONS: "suppressions=etc/lsan-suppr.txt:print_suppressions=false" run: | + sudo sysctl vm.mmap_rnd_bits=28 LD_PRELOAD=/lib/x86_64-linux-gnu/libasan.so.5:/lib/x86_64-linux-gnu/libubsan.so.1 python -m pytest --capture=sys tests From 51884536fd2eaa46914f392bd539a4044f18e39d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 13 Feb 2025 11:03:09 +0100 Subject: [PATCH 175/276] ci: enable PyPy builds explicitly in preparation for cibuildwheel 3 --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b840f01bc..cfce30ed8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,6 +33,7 @@ jobs: env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" + CIBW_ENABLE: pypy # Skip tests for Python 3.10 onwards because SciPy does not have # 32-bit wheels for Linux. CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686 cp313-manylinux_i686" @@ -64,6 +65,7 @@ jobs: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 CIBW_BUILD: "*-manylinux_aarch64" + CIBW_ENABLE: pypy - uses: actions/upload-artifact@v4 with: @@ -153,6 +155,7 @@ jobs: env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" + CIBW_ENABLE: pypy CIBW_ENVIRONMENT: "LDFLAGS=-L$HOME/local/lib" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_arch }} ${{ matrix.cmake_extra_args }} -DCMAKE_PREFIX_PATH=$HOME/local @@ -255,6 +258,7 @@ jobs: env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" + CIBW_ENABLE: pypy CIBW_TEST_COMMAND: "cd /d {project} && pip install --prefer-binary \".[test]\" && python -m pytest tests" # Skip tests for Python 3.10 onwards because SciPy does not have # 32-bit wheels for Linux. From 98b7741456a50ebcec982bc0770967fd55729473 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 13 Feb 2025 11:14:44 +0100 Subject: [PATCH 176/276] ci: fix libasan version in newer Ubuntu image --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cfce30ed8..567147b13 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -376,4 +376,4 @@ jobs: LSAN_OPTIONS: "suppressions=etc/lsan-suppr.txt:print_suppressions=false" run: | sudo sysctl vm.mmap_rnd_bits=28 - LD_PRELOAD=/lib/x86_64-linux-gnu/libasan.so.5:/lib/x86_64-linux-gnu/libubsan.so.1 python -m pytest --capture=sys tests + LD_PRELOAD=/lib/x86_64-linux-gnu/libasan.so.6:/lib/x86_64-linux-gnu/libubsan.so.1 python -m pytest --capture=sys tests From 0640de10cc21e30658827d54a893a732783f8d7e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 13 Feb 2025 11:34:40 +0100 Subject: [PATCH 177/276] ci: fix libasan version in newer Ubuntu image, really --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 567147b13..e2087dc4f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -376,4 +376,4 @@ jobs: LSAN_OPTIONS: "suppressions=etc/lsan-suppr.txt:print_suppressions=false" run: | sudo sysctl vm.mmap_rnd_bits=28 - LD_PRELOAD=/lib/x86_64-linux-gnu/libasan.so.6:/lib/x86_64-linux-gnu/libubsan.so.1 python -m pytest --capture=sys tests + LD_PRELOAD=/lib/x86_64-linux-gnu/libasan.so.8:/lib/x86_64-linux-gnu/libubsan.so.1 python -m pytest --capture=sys tests From fee96a095d2469c0ce7128ec258720b58ecd2334 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 9 Mar 2025 14:35:42 +0100 Subject: [PATCH 178/276] build(deps): bump pypa/cibuildwheel from 2.22.0 to 2.23.0 (#814) Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.22.0 to 2.23.0. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.22.0...v2.23.0) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e2087dc4f..5567ee31e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: python-version: '3.9' - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.22.0 + uses: pypa/cibuildwheel@v2.23.0 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -39,7 +39,7 @@ jobs: CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686 cp313-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.22.0 + uses: pypa/cibuildwheel@v2.23.0 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" @@ -60,7 +60,7 @@ jobs: fetch-depth: 0 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.22.0 + uses: pypa/cibuildwheel@v2.23.0 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -82,7 +82,7 @@ jobs: fetch-depth: 0 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.22.0 + uses: pypa/cibuildwheel@v2.23.0 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -151,7 +151,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.22.0 + uses: pypa/cibuildwheel@v2.23.0 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" @@ -254,7 +254,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.22.0 + uses: pypa/cibuildwheel@v2.23.0 env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From 4385f95307fac0ca2c3c53908184c8c4eed44716 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Mar 2025 12:29:36 +0100 Subject: [PATCH 179/276] build(deps): bump pypa/cibuildwheel from 2.23.0 to 2.23.1 (#817) Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.23.0 to 2.23.1. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/v2.23.1/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.23.0...v2.23.1) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5567ee31e..339919314 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: python-version: '3.9' - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.23.0 + uses: pypa/cibuildwheel@v2.23.1 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -39,7 +39,7 @@ jobs: CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686 cp313-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.23.0 + uses: pypa/cibuildwheel@v2.23.1 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" @@ -60,7 +60,7 @@ jobs: fetch-depth: 0 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.23.0 + uses: pypa/cibuildwheel@v2.23.1 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -82,7 +82,7 @@ jobs: fetch-depth: 0 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.23.0 + uses: pypa/cibuildwheel@v2.23.1 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -151,7 +151,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.23.0 + uses: pypa/cibuildwheel@v2.23.1 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" @@ -254,7 +254,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.23.0 + uses: pypa/cibuildwheel@v2.23.1 env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From 9504f47542780bae130c577c684c823c40a311bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horv=C3=A1t?= Date: Fri, 21 Mar 2025 08:49:02 +0000 Subject: [PATCH 180/276] chore: require copyright assignment in PR template (#816) --- .github/pull_request_template.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..686944ff0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,5 @@ + + + + +- [ ] By submitting this pull request, I assign the copyright of my contribution to _The igraph development team_. From 6575aaf91a538162afe4eb5456cc520f44a0f2f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horv=C3=A1t?= Date: Fri, 21 Mar 2025 08:49:32 +0000 Subject: [PATCH 181/276] docs: document negative/infinite order neighborhood (#815) * docs: document negative/infinite order neighborhood after the corresponding change in the C core * chore: update C core --- src/_igraph/graphobject.c | 6 ++++-- vendor/source/igraph | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 894e35d26..49e025003 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -15854,7 +15854,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param vertices: a single vertex ID or a list of vertex IDs, or\n" " C{None} meaning all the vertices in the graph.\n" "@param order: the order of the neighborhood, i.e. the maximum number of\n" - " steps to take from the seed vertex.\n" + " steps to take from the seed vertex. Negative values are interpreted as\n" + " an infinite order, i.e. no limit on the number of steps.\n" "@param mode: specifies how to take into account the direction of\n" " the edges if a directed graph is analyzed. C{\"out\"} means that\n" " only the outgoing edges are followed, so all vertices reachable\n" @@ -15882,7 +15883,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param vertices: a single vertex ID or a list of vertex IDs, or\n" " C{None} meaning all the vertices in the graph.\n" "@param order: the order of the neighborhood, i.e. the maximum number of\n" - " steps to take from the seed vertex.\n" + " steps to take from the seed vertex. Negative values are interpreted as\n" + " an infinite order, i.e. no limit on the number of steps.\n" "@param mode: specifies how to take into account the direction of\n" " the edges if a directed graph is analyzed. C{\"out\"} means that\n" " only the outgoing edges are followed, so all vertices reachable\n" diff --git a/vendor/source/igraph b/vendor/source/igraph index 635b432ef..662abbdfa 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 635b432eff0a89580ac9bb98068d2fbc8ef374f2 +Subproject commit 662abbdfa467a6436bad650c31a8360787d75379 From a242830f50466df6bcf3ab148bb36062c40d5859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sat, 22 Mar 2025 09:43:26 +0000 Subject: [PATCH 182/276] fix: build with vendored PLFIT by default instead of leaving this up to auto-detection --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bbd5dbfb1..18c5a7793 100644 --- a/setup.py +++ b/setup.py @@ -280,7 +280,7 @@ def _compile_in( args.append("-DIGRAPH_GRAPHML_SUPPORT:BOOL=OFF") # Build the Python interface with vendored libraries - for deps in "ARPACK BLAS GLPK GMP LAPACK".split(): + for deps in "ARPACK BLAS GLPK GMP LAPACK PLFIT".split(): args.append("-DIGRAPH_USE_INTERNAL_" + deps + "=ON") # Use link-time optinization if available From 3dafe701e80006ee0155514e97fe202d1e3b5872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sat, 22 Mar 2025 09:45:12 +0000 Subject: [PATCH 183/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 662abbdfa..10d4fc75f 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 662abbdfa467a6436bad650c31a8360787d75379 +Subproject commit 10d4fc75f146f5fc8ec17d99ab808c53a3c49dae From f04a3088c85c7c315dcb8883969a7ed33c36ef9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sun, 23 Mar 2025 21:55:15 +0000 Subject: [PATCH 184/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 10d4fc75f..fe6bdb78c 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 10d4fc75f146f5fc8ec17d99ab808c53a3c49dae +Subproject commit fe6bdb78c4a3269b542c6af7a167b9599f08173d From 18c0a8ffac8e4180a8c8a4c1398eb92749357963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sun, 23 Mar 2025 22:37:36 +0000 Subject: [PATCH 185/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index fe6bdb78c..fc6860b31 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit fe6bdb78c4a3269b542c6af7a167b9599f08173d +Subproject commit fc6860b31887b362e8ddbb07e44e6a12e9cef82d From 8f88b46c0a4b2555fb82e8b70743ee4f99c8f3b7 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 27 Mar 2025 14:46:06 +0100 Subject: [PATCH 186/276] fix: scripts/mkdoc.sh now allows overriding Python with the PYTHON envvar --- scripts/mkdoc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index db6487d98..696157fd6 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -47,7 +47,7 @@ cd ${ROOT_FOLDER} # Create a virtual environment if [ ! -d ".venv" ]; then - python3 -m venv .venv + ${PYTHON:-python3} -m venv .venv # Install sphinx, matplotlib, pandas, scipy, wheel and pydoctor into the venv. # doc2dash is optional; it will be installed when -d is given From 6f7597ff9e782529abefeea564eb63402a737a87 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 27 Mar 2025 14:46:27 +0100 Subject: [PATCH 187/276] fix: import _compare_communities from igraph._igraph on the top level --- src/igraph/clustering.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index 89b34b56e..36df961ad 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -5,7 +5,7 @@ from copy import deepcopy from io import StringIO -from igraph._igraph import GraphBase, community_to_membership +from igraph._igraph import GraphBase, community_to_membership, _compare_communities from igraph.configuration import Configuration from igraph.datatypes import UniqueIdGenerator from igraph.drawing.colors import ClusterColoringPalette @@ -1500,10 +1500,8 @@ def compare_communities(comm1, comm2, method="vi", remove_none=False): @return: the calculated measure. """ - import igraph._igraph - vec1, vec2 = _prepare_community_comparison(comm1, comm2, remove_none) - return igraph._igraph._compare_communities(vec1, vec2, method) + return _compare_communities(vec1, vec2, method) def split_join_distance(comm1, comm2, remove_none=False): From 47b1f193f3a9480e86c8fca71bb2af22f2159e26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 20:06:55 +0200 Subject: [PATCH 188/276] build(deps): bump pypa/cibuildwheel from 2.23.1 to 2.23.2 (#823) Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.23.1 to 2.23.2. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.23.1...v2.23.2) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 339919314..5fea2ee20 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: python-version: '3.9' - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.23.1 + uses: pypa/cibuildwheel@v2.23.2 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -39,7 +39,7 @@ jobs: CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686 cp313-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.23.1 + uses: pypa/cibuildwheel@v2.23.2 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" @@ -60,7 +60,7 @@ jobs: fetch-depth: 0 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.23.1 + uses: pypa/cibuildwheel@v2.23.2 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -82,7 +82,7 @@ jobs: fetch-depth: 0 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.23.1 + uses: pypa/cibuildwheel@v2.23.2 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -151,7 +151,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.23.1 + uses: pypa/cibuildwheel@v2.23.2 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" @@ -254,7 +254,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.23.1 + uses: pypa/cibuildwheel@v2.23.2 env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From 98a8d9d410f1db5520820557a173cfb8c3945589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Fri, 28 Mar 2025 21:26:35 +0000 Subject: [PATCH 189/276] chore: remove generated sg_execution_times.rst file --- doc/source/sg_execution_times.rst | 109 ------------------------------ 1 file changed, 109 deletions(-) delete mode 100644 doc/source/sg_execution_times.rst diff --git a/doc/source/sg_execution_times.rst b/doc/source/sg_execution_times.rst deleted file mode 100644 index a63741754..000000000 --- a/doc/source/sg_execution_times.rst +++ /dev/null @@ -1,109 +0,0 @@ - -:orphan: - -.. _sphx_glr_sg_execution_times: - - -Computation times -================= -**00:10.013** total execution time for 25 files **from all galleries**: - -.. container:: - - .. raw:: html - - - - - - - - .. list-table:: - :header-rows: 1 - :class: table table-striped sg-datatable - - * - Example - - Time - - Mem (MB) - * - :ref:`sphx_glr_tutorials_visualize_cliques.py` (``../examples_sphinx-gallery/visualize_cliques.py``) - - 00:02.970 - - 0.0 - * - :ref:`sphx_glr_tutorials_ring_animation.py` (``../examples_sphinx-gallery/ring_animation.py``) - - 00:01.287 - - 0.0 - * - :ref:`sphx_glr_tutorials_cluster_contraction.py` (``../examples_sphinx-gallery/cluster_contraction.py``) - - 00:00.759 - - 0.0 - * - :ref:`sphx_glr_tutorials_betweenness.py` (``../examples_sphinx-gallery/betweenness.py``) - - 00:00.735 - - 0.0 - * - :ref:`sphx_glr_tutorials_visual_style.py` (``../examples_sphinx-gallery/visual_style.py``) - - 00:00.711 - - 0.0 - * - :ref:`sphx_glr_tutorials_delaunay-triangulation.py` (``../examples_sphinx-gallery/delaunay-triangulation.py``) - - 00:00.504 - - 0.0 - * - :ref:`sphx_glr_tutorials_configuration.py` (``../examples_sphinx-gallery/configuration.py``) - - 00:00.416 - - 0.0 - * - :ref:`sphx_glr_tutorials_online_user_actions.py` (``../examples_sphinx-gallery/online_user_actions.py``) - - 00:00.332 - - 0.0 - * - :ref:`sphx_glr_tutorials_erdos_renyi.py` (``../examples_sphinx-gallery/erdos_renyi.py``) - - 00:00.313 - - 0.0 - * - :ref:`sphx_glr_tutorials_connected_components.py` (``../examples_sphinx-gallery/connected_components.py``) - - 00:00.216 - - 0.0 - * - :ref:`sphx_glr_tutorials_complement.py` (``../examples_sphinx-gallery/complement.py``) - - 00:00.201 - - 0.0 - * - :ref:`sphx_glr_tutorials_generate_dag.py` (``../examples_sphinx-gallery/generate_dag.py``) - - 00:00.194 - - 0.0 - * - :ref:`sphx_glr_tutorials_visualize_communities.py` (``../examples_sphinx-gallery/visualize_communities.py``) - - 00:00.176 - - 0.0 - * - :ref:`sphx_glr_tutorials_bridges.py` (``../examples_sphinx-gallery/bridges.py``) - - 00:00.169 - - 0.0 - * - :ref:`sphx_glr_tutorials_spanning_trees.py` (``../examples_sphinx-gallery/spanning_trees.py``) - - 00:00.161 - - 0.0 - * - :ref:`sphx_glr_tutorials_isomorphism.py` (``../examples_sphinx-gallery/isomorphism.py``) - - 00:00.153 - - 0.0 - * - :ref:`sphx_glr_tutorials_quickstart.py` (``../examples_sphinx-gallery/quickstart.py``) - - 00:00.142 - - 0.0 - * - :ref:`sphx_glr_tutorials_minimum_spanning_trees.py` (``../examples_sphinx-gallery/minimum_spanning_trees.py``) - - 00:00.137 - - 0.0 - * - :ref:`sphx_glr_tutorials_simplify.py` (``../examples_sphinx-gallery/simplify.py``) - - 00:00.079 - - 0.0 - * - :ref:`sphx_glr_tutorials_bipartite_matching_maxflow.py` (``../examples_sphinx-gallery/bipartite_matching_maxflow.py``) - - 00:00.073 - - 0.0 - * - :ref:`sphx_glr_tutorials_articulation_points.py` (``../examples_sphinx-gallery/articulation_points.py``) - - 00:00.067 - - 0.0 - * - :ref:`sphx_glr_tutorials_topological_sort.py` (``../examples_sphinx-gallery/topological_sort.py``) - - 00:00.058 - - 0.0 - * - :ref:`sphx_glr_tutorials_bipartite_matching.py` (``../examples_sphinx-gallery/bipartite_matching.py``) - - 00:00.058 - - 0.0 - * - :ref:`sphx_glr_tutorials_shortest_path_visualisation.py` (``../examples_sphinx-gallery/shortest_path_visualisation.py``) - - 00:00.052 - - 0.0 - * - :ref:`sphx_glr_tutorials_maxflow.py` (``../examples_sphinx-gallery/maxflow.py``) - - 00:00.052 - - 0.0 From ec27e895ac87eb2f028c0e16fab98da9832111d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Fri, 28 Mar 2025 22:16:02 +0000 Subject: [PATCH 190/276] docs: fix Dash docset build --- doc/source/icon.png | Bin 0 -> 2567 bytes doc/source/icon@2x.png | Bin 0 -> 3247 bytes scripts/mkdoc.sh | 6 ++++-- 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 doc/source/icon.png create mode 100644 doc/source/icon@2x.png diff --git a/doc/source/icon.png b/doc/source/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0735f005b968d8334bea9246a65033ea600a5437 GIT binary patch literal 2567 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 GCi@SL)7LBj literal 0 HcmV?d00001 diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index 696157fd6..31d6a35ac 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -104,17 +104,19 @@ if [ "x$DOC2DASH" = "x1" ]; then PWD=`pwd` # Output folder of sphinx (before Jekyll if requested) DOC_API_FOLDER=${ROOT_FOLDER}/doc/html/api - DOC2DASH=`which doc2dash 2>/dev/null || true` + DOC2DASH=.venv/bin/doc2dash DASH_FOLDER=${ROOT_FOLDER}/doc/dash if [ "x$DOC2DASH" != x ]; then echo "Generating Dash docset..." "$DOC2DASH" \ - --online-redirect-url "https://igraph.org/python/api/latest" \ + --online-redirect-url "https://python.igraph.org/en/latest/api/" \ --name "python-igraph" \ -d "${DASH_FOLDER}" \ -f \ -j \ -I "index.html" \ + --icon ${ROOT_FOLDER}/doc/source/icon.png \ + --icon-2x ${ROOT_FOLDER}/doc/source/icon@2x.png \ "${DOC_API_FOLDER}" DASH_READY=1 else From 9503678e76fd23adb64d0274ac812c2f3f232dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bea=20M=C3=A1rton?= Date: Tue, 1 Apr 2025 01:34:32 +0300 Subject: [PATCH 191/276] docs: add personalized PageRank gallery example --- .../personalized_pagerank.py | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 doc/examples_sphinx-gallery/personalized_pagerank.py diff --git a/doc/examples_sphinx-gallery/personalized_pagerank.py b/doc/examples_sphinx-gallery/personalized_pagerank.py new file mode 100644 index 000000000..2fd044612 --- /dev/null +++ b/doc/examples_sphinx-gallery/personalized_pagerank.py @@ -0,0 +1,96 @@ +""" +.. _tutorials-personalized_pagerank: + +=============================== +Personalized PageRank on a grid +=============================== + +This example demonstrates how to calculate and visualize personalized PageRank on a grid. We use the :meth:`igraph.Graph.personalized_pagerank` method, and demonstrate the effects on a grid graph. +""" + +# %% +# .. note:: +# +# The PageRank score of a vertex reflects the probability that a random walker will be at that vertex over the long run. At each step the walker has a 1 - damping chance to restart the walk and pick a starting vertex according to the probabilities defined in the reset vector. + +import igraph as ig +import matplotlib.cm as cm +import matplotlib.pyplot as plt +import numpy as np + +# %% +# We define a function that plots the graph on a Matplotlib axis, along with +# its personalized PageRank values. The function also generates a +# color bar on the side to see how the values change. +# We use `Matplotlib's Normalize class `_ +# to set the colors and ensure that our color bar range is correct. + + +def plot_pagerank(graph: ig.Graph, p_pagerank: list[float]): + """Plots personalized PageRank values on a grid graph with a colorbar. + + Parameters + ---------- + graph : ig.Graph + graph to plot + p_pagerank : list[float] + calculated personalized PageRank values + """ + # Create the axis for matplotlib + _, ax = plt.subplots(figsize=(8, 8)) + + # Create a matplotlib colormap + # coolwarm goes from blue (lowest value) to red (highest value) + cmap = cm.coolwarm + + # Normalize the PageRank values for colormap + normalized_pagerank = ig.rescale(p_pagerank) + + graph.vs["color"] = [cmap(pr) for pr in normalized_pagerank] + graph.vs["size"] = ig.rescale(p_pagerank, (20, 40)) + graph.es["color"] = "gray" + graph.es["width"] = 1.5 + + # Plot the graph + ig.plot(graph, target=ax, layout=graph.layout_grid()) + + # Add a colorbar + sm = cm.ScalarMappable(norm=plt.Normalize(min(p_pagerank), max(p_pagerank)), cmap=cmap) + plt.colorbar(sm, ax=ax, label="Personalized PageRank") + + plt.title("Graph with Personalized PageRank") + plt.axis("equal") + plt.show() + + +# %% +# First, we generate a graph, e.g. a Lattice Graph, which basically is a ``dim x dim`` grid: +dim = 5 +grid_size = (dim, dim) # dim rows, dim columns +g = ig.Graph.Lattice(dim=grid_size, circular=False) + +# %% +# Then we initialize the ``reset_vector`` (it's length should be equal to the number of vertices in the graph): +reset_vector = np.zeros(g.vcount()) + +# %% +# Then we set the nodes to prioritize, for example nodes with indices ``0`` and ``18``: +reset_vector[0] = 1 +reset_vector[18] = 0.65 + +# %% +# Then we calculate the personalized PageRank: +personalized_page_rank = g.personalized_pagerank(damping=0.85, reset=reset_vector) + +# %% +# Finally, we plot the graph with the personalized PageRank values: +plot_pagerank(g, personalized_page_rank) + + +# %% +# Alternatively, we can play around with the ``damping`` parameter: +personalized_page_rank = g.personalized_pagerank(damping=0.45, reset=reset_vector) + +# %% +# Here we can see the same plot with the new damping parameter: +plot_pagerank(g, personalized_page_rank) From e54abcc7ec0982af095580552deb43acc6888e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horv=C3=A1t?= Date: Tue, 8 Apr 2025 14:43:04 +0000 Subject: [PATCH 192/276] docs: fix a lot of outdated links (#824) --- .../cluster_contraction.py | 2 +- doc/source/install.rst | 4 +-- doc/source/visualisation.rst | 2 +- setup.py | 4 +-- src/_igraph/graphobject.c | 29 ++++++++++--------- src/igraph/__init__.py | 2 +- src/igraph/community.py | 18 ++++++------ src/igraph/drawing/__init__.py | 10 +++---- src/igraph/drawing/cairo/plot.py | 6 ++-- src/igraph/drawing/graph.py | 8 ++--- src/igraph/io/images.py | 2 +- tox.ini | 2 +- 12 files changed, 45 insertions(+), 44 deletions(-) diff --git a/doc/examples_sphinx-gallery/cluster_contraction.py b/doc/examples_sphinx-gallery/cluster_contraction.py index aa7cf38a7..af997dc5d 100644 --- a/doc/examples_sphinx-gallery/cluster_contraction.py +++ b/doc/examples_sphinx-gallery/cluster_contraction.py @@ -12,7 +12,7 @@ # %% # We begin by load the graph from file. The file containing this network can be -# downloaded `here `_. +# downloaded `here `_. g = ig.load("./lesmis/lesmis.gml") # %% diff --git a/doc/source/install.rst b/doc/source/install.rst index 91fcf6f18..65c985bb9 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -131,7 +131,7 @@ Fourth, call ``pip`` to compile and install the package from source:: $ pip install . Alternatively, you can call ``build`` or another PEP 517-compliant build frontend -to build an installable Python wheel. Here we use `pipx `_ +to build an installable Python wheel. Here we use `pipx `_ to invoke ``build`` in a separate virtualenv:: $ pipx run build @@ -140,7 +140,7 @@ Testing your installation ------------------------- Use ``tox`` or another standard test runner tool to run all the unit tests. -Here we use `pipx `_` to invoke ``tox``:: +Here we use `pipx `_ to invoke ``tox``:: $ pipx run tox diff --git a/doc/source/visualisation.rst b/doc/source/visualisation.rst index 7cad8bf9b..cf32dbd90 100644 --- a/doc/source/visualisation.rst +++ b/doc/source/visualisation.rst @@ -234,6 +234,6 @@ See the :ref:`tutorial ` for examples and a full list .. _matplotlib: https://matplotlib.org .. _Jupyter: https://jupyter.org/ .. _Cairo: https://www.cairographics.org -.. _graphviz: http://www.graphviz.org +.. _graphviz: https://www.graphviz.org .. _networkx: https://networkx.org/ .. _graph-tool: https://graph-tool.skewed.de/ diff --git a/setup.py b/setup.py index 18c5a7793..b61fe9881 100644 --- a/setup.py +++ b/setup.py @@ -953,7 +953,7 @@ def get_tag(self): Graph plotting functionality is provided by the Cairo library, so make sure you install the Python bindings of Cairo if you want to generate publication-quality graph plots. You can try either `pycairo -`_ or `cairocffi `_, +`_ or `cairocffi `_, ``cairocffi`` is recommended because there were bug reports affecting igraph graph plots in Jupyter notebooks when using ``pycairo`` (but not with ``cairocffi``). @@ -983,7 +983,7 @@ def get_tag(self): "Bug Tracker": "https://github.com/igraph/python-igraph/issues", "Changelog": "https://github.com/igraph/python-igraph/blob/main/CHANGELOG.md", "CI": "https://github.com/igraph/python-igraph/actions", - "Documentation": "https://igraph.readthedocs.io", + "Documentation": "https://python.igraph.org", "Source Code": "https://github.com/igraph/python-igraph", }, "ext_modules": [igraph_extension], diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 49e025003..b5565ab59 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -14530,7 +14530,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "the number of vertices in the graph, a list of shifts giving\n" "additional edges to a cycle backbone and another integer giving how\n" "many times the shifts should be performed. See\n" - "U{http://mathworld.wolfram.com/LCFNotation.html} for details.\n\n" + "U{https://mathworld.wolfram.com/LCFNotation.html} for details.\n\n" "@param n: the number of vertices\n" "@param shifts: the shifts in a list or tuple\n" "@param repeats: the number of repeats\n" @@ -16222,7 +16222,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " small-world networks. I{Nature} 393(6884):440-442, 1998.\n" " - Barrat A, Barthelemy M, Pastor-Satorras R and Vespignani A:\n" " The architecture of complex weighted networks. I{PNAS} 101, 3747 (2004).\n" - " U{http://arxiv.org/abs/cond-mat/0311416}.\n\n" + " U{https://arxiv.org/abs/cond-mat/0311416}.\n\n" "@param vertices: a list containing the vertex IDs which should be\n" " included in the result. C{None} means all of the vertices.\n" "@param mode: defines how to treat vertices with degree less than two.\n" @@ -16820,7 +16820,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "forces among the vertices and then the physical system is simulated\n" "until it reaches an equilibrium or the maximal number of iterations is\n" "reached.\n\n" - "See U{http://www.schmuhl.org/graphopt/} for the original graphopt.\n\n" + "See U{https://web.archive.org/web/20220611030748/http://www.schmuhl.org/graphopt/}\n" + "and U{https://sourceforge.net/projects/graphopt/} for the original graphopt.\n\n" "@param niter: the number of iterations to perform. Should be a couple\n" " of hundred in general.\n\n" "@param node_charge: the charge of the vertices, used to calculate electric\n" @@ -17135,7 +17136,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Read_DIMACS(f, directed=False)\n--\n\n" "Reads a graph from a file conforming to the DIMACS minimum-cost flow file format.\n\n" "For the exact description of the format, see\n" - "U{http://lpsolve.sourceforge.net/5.5/DIMACS.htm}\n\n" + "U{https://lpsolve.sourceforge.net/5.5/DIMACS.htm}\n\n" "Restrictions compared to the official description of the format:\n\n" " - igraph's DIMACS reader requires only three fields in an arc definition,\n" " describing the edge's source and target node and its capacity.\n" @@ -17171,7 +17172,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Read_GraphDB(f, directed=False)\n--\n\n" "Reads a GraphDB format file and creates a graph based on it.\n\n" "GraphDB is a binary format, used in the graph database for\n" - "isomorphism testing (see U{http://amalfi.dis.unina.it/graph/}).\n\n" + "isomorphism testing (see U{https://mivia.unisa.it/datasets/graph-database/arg-database/}).\n\n" "@param f: the name of the file or a Python file handle\n" "@param directed: whether the generated graph should be directed.\n"}, /* interface to igraph_read_graph_graphml */ @@ -17394,7 +17395,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "algorithm.\n\n" "Passing the permutation returned here to L{permute_vertices()} will\n" "transform the graph into its canonical form.\n\n" - "See U{http://www.tcs.hut.fi/Software/bliss/index.html} for more information\n" + "See U{https://users.aalto.fi/~tjunttil/bliss/} for more information\n" "about the BLISS algorithm and canonical permutations.\n\n" "@param sh: splitting heuristics for graph as a case-insensitive string,\n" " with the following possible values:\n\n" @@ -17420,7 +17421,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "count_automorphisms(sh=\"fl\", color=None)\n--\n\n" "Calculates the number of automorphisms of a graph using the BLISS isomorphism\n" "algorithm.\n\n" - "See U{http://www.tcs.hut.fi/Software/bliss/index.html} for more information\n" + "See U{https://users.aalto.fi/~tjunttil/bliss/} for more information\n" "about the BLISS algorithm and canonical permutations.\n\n" "@param sh: splitting heuristics for graph as a case-insensitive string,\n" " with the following possible values:\n\n" @@ -17471,7 +17472,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " sh1=\"fl\", sh2=None, color1=None, color2=None)\n--\n\n" "Checks whether the graph is isomorphic to another graph, using the\n" "BLISS isomorphism algorithm.\n\n" - "See U{http://www.tcs.hut.fi/Software/bliss/index.html} for more information\n" + "See U{https://users.aalto.fi/~tjunttil/bliss/} for more information\n" "about the BLISS algorithm.\n\n" "@param other: the other graph with which we want to compare the graph.\n" "@param color1: optional vector storing the coloring of the vertices of\n" @@ -18264,15 +18265,15 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "community_infomap(edge_weights=None, vertex_weights=None, trials=10)\n--\n\n" "Finds the community structure of the network according to the Infomap\n" "method of Martin Rosvall and Carl T. Bergstrom.\n\n" - "See U{http://www.mapequation.org} for a visualization of the algorithm\n" + "See U{https://www.mapequation.org} for a visualization of the algorithm\n" "or one of the references provided below.\n" "B{References}\n" " - M. Rosvall and C. T. Bergstrom: I{Maps of information flow reveal\n" " community structure in complex networks}. PNAS 105, 1118 (2008).\n" - " U{http://arxiv.org/abs/0707.0609}\n" + " U{https://arxiv.org/abs/0707.0609}\n" " - M. Rosvall, D. Axelsson and C. T. Bergstrom: I{The map equation}.\n" " I{Eur Phys J Special Topics} 178, 13 (2009).\n" - " U{http://arxiv.org/abs/0906.1405}\n" + " U{https://arxiv.org/abs/0906.1405}\n" "\n" "@param edge_weights: name of an edge attribute or a list containing\n" " edge weights.\n" @@ -18301,7 +18302,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "B{Reference}: Raghavan, U.N. and Albert, R. and Kumara, S. Near linear\n" "time algorithm to detect community structures in large-scale\n" "networks. I{Phys Rev E} 76:036106, 2007.\n" - "U{http://arxiv.org/abs/0709.2938}.\n" + "U{https://arxiv.org/abs/0709.2938}.\n" "\n" "@param weights: name of an edge attribute or a list containing\n" " edge weights\n" @@ -18356,7 +18357,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "\n" "B{Reference}: VD Blondel, J-L Guillaume, R Lambiotte and E Lefebvre: Fast\n" "unfolding of community hierarchies in large networks. J Stat Mech\n" - "P10008 (2008), U{http://arxiv.org/abs/0803.0476}\n" + "P10008 (2008), U{https://arxiv.org/abs/0803.0476}\n" "\n" "Attention: this function is wrapped in a more convenient syntax in the\n" "derived class L{Graph}. It is advised to use that instead of this version.\n\n" @@ -18495,7 +18496,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Attention: this function is wrapped in a more convenient syntax in the\n" "derived class L{Graph}. It is advised to use that instead of this version.\n\n" "B{Reference}: Pascal Pons, Matthieu Latapy: Computing communities in large\n" - "networks using random walks, U{http://arxiv.org/abs/physics/0512106}.\n\n" + "networks using random walks, U{https://arxiv.org/abs/physics/0512106}.\n\n" "@param weights: name of an edge attribute or a list containing\n" " edge weights\n" "@return: a tuple with the list of merges and the modularity scores corresponding\n" diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 7f0e328b7..0b1690849 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -860,7 +860,7 @@ def transitivity_avglocal_undirected(self, mode="nan", weights=None): networks. I{Nature} 393(6884):440-442, 1998. - Barrat A, Barthelemy M, Pastor-Satorras R and Vespignani A: The architecture of complex weighted networks. I{PNAS} 101, 3747 - (2004). U{http://arxiv.org/abs/cond-mat/0311416}. + (2004). U{https://arxiv.org/abs/cond-mat/0311416}. @param mode: defines how to treat vertices with degree less than two. If C{TRANSITIVITY_ZERO} or C{"zero"}, these vertices will have zero diff --git a/src/igraph/community.py b/src/igraph/community.py index e2dcb47da..0fdcdf154 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -37,12 +37,12 @@ def _community_infomap(graph, edge_weights=None, vertex_weights=None, trials=10) - M. Rosvall and C. T. Bergstrom: Maps of information flow reveal community structure in complex networks, I{PNAS} 105, 1118 (2008). - U{http://dx.doi.org/10.1073/pnas.0706851105}, - U{http://arxiv.org/abs/0707.0609}. + U{https://doi.org/10.1073/pnas.0706851105}, + U{https://arxiv.org/abs/0707.0609}. - M. Rosvall, D. Axelsson, and C. T. Bergstrom: The map equation, I{Eur Phys. J Special Topics} 178, 13 (2009). - U{http://dx.doi.org/10.1140/epjst/e2010-01179-1}, - U{http://arxiv.org/abs/0906.1405}. + U{https://doi.org/10.1140/epjst/e2010-01179-1}, + U{https://arxiv.org/abs/0906.1405}. @param edge_weights: name of an edge attribute or a list containing edge weights. @@ -125,7 +125,7 @@ def _community_label_propagation(graph, weights=None, initial=None, fixed=None): B{Reference}: Raghavan, U.N. and Albert, R. and Kumara, S. Near linear time algorithm to detect community structures in large-scale networks. - I{Phys Rev} E 76:036106, 2007. U{http://arxiv.org/abs/0709.2938}. + I{Phys Rev} E 76:036106, 2007. U{https://arxiv.org/abs/0709.2938}. @param weights: name of an edge attribute or a list containing edge weights @@ -165,7 +165,7 @@ def _community_multilevel(graph, weights=None, return_levels=False, resolution=1 B{Reference}: VD Blondel, J-L Guillaume, R Lambiotte and E Lefebvre: Fast unfolding of community hierarchies in large networks, I{J Stat Mech} - P10008 (2008). U{http://arxiv.org/abs/0803.0476} + P10008 (2008). U{https://arxiv.org/abs/0803.0476} @param weights: edge attribute name or a list containing edge weights @@ -271,11 +271,11 @@ def _community_spinglass(graph, *args, **kwds): - Reichardt J and Bornholdt S: Statistical mechanics of community detection. I{Phys Rev E} 74:016110 (2006). - U{http://arxiv.org/abs/cond-mat/0603718}. + U{https://arxiv.org/abs/cond-mat/0603718}. - Traag VA and Bruggeman J: Community detection in networks with positive and negative links. I{Phys Rev E} 80:036115 (2009). - U{http://arxiv.org/abs/0811.2329}. + U{https://arxiv.org/abs/0811.2329}. @keyword weights: edge weights to be used. Can be a sequence or iterable or even an edge attribute name. @@ -329,7 +329,7 @@ def _community_walktrap(graph, weights=None, steps=4): as a dendrogram. B{Reference}: Pascal Pons, Matthieu Latapy: Computing communities in large - networks using random walks, U{http://arxiv.org/abs/physics/0512106}. + networks using random walks, U{https://arxiv.org/abs/physics/0512106}. @param weights: name of an edge attribute or a list containing edge weights diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index e6d06b4bc..2725745f6 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -5,9 +5,9 @@ It also has experimental support for plotly. The Cairo backend is dependent on the C{pycairo} or C{cairocffi} libraries that -provide Python bindings to the popular U{Cairo library}. -This means that if you don't have U{pycairo} -or U{cairocffi} installed, you won't be able +provide Python bindings to the popular U{Cairo library}. +This means that if you don't have U{pycairo} +or U{cairocffi} installed, you won't be able to use the Cairo plotting backend. Whenever the documentation refers to the C{pycairo} library, you can safely replace it with C{cairocffi} as the two are API-compatible. @@ -21,8 +21,8 @@ If you do not want to (or cannot) install any of the dependencies outlined above, you can still save the graph to an SVG file and view it from -U{Mozilla Firefox} (free) or edit it in -U{Inkscape} (free), U{Skencil} +U{Mozilla Firefox} (free) or edit it in +U{Inkscape} (free), U{Skencil} (formerly known as Sketch, also free) or Adobe Illustrator. """ diff --git a/src/igraph/drawing/cairo/plot.py b/src/igraph/drawing/cairo/plot.py index ac98210db..f35db6ef1 100644 --- a/src/igraph/drawing/cairo/plot.py +++ b/src/igraph/drawing/cairo/plot.py @@ -6,7 +6,7 @@ The Cairo backend is dependent on the C{pycairo} or C{cairocffi} libraries that provide Python bindings to the popular U{Cairo library}. This means that if you don't have U{pycairo} -or U{cairocffi} installed, you won't be able +or U{cairocffi} installed, you won't be able to use the Cairo plotting backend. Whenever the documentation refers to the C{pycairo} library, you can safely replace it with C{cairocffi} as the two are API-compatible. @@ -17,8 +17,8 @@ If you do not want to (or cannot) install any of the dependencies outlined above, you can still save the graph to an SVG file and view it from -U{Mozilla Firefox} (free) or edit it in -U{Inkscape} (free), U{Skencil} +U{Mozilla Firefox} (free) or edit it in +U{Inkscape} (free), U{Skencil} (formerly known as Sketch, also free) or Adobe Illustrator. """ diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index ec326c48d..00f77f402 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -7,8 +7,8 @@ - Matplotlib axes (L{MatplotlibGraphDrawer}) It also contains routines to send an igraph graph directly to -(U{Cytoscape}) using the -(U{CytoscapeRPC plugin}), see +(U{Cytoscape}) using the +(U{CytoscapeRPC plugin}), see L{CytoscapeGraphDrawer}. L{CytoscapeGraphDrawer} can also fetch the current network from Cytoscape and convert it to igraph format. """ @@ -24,8 +24,8 @@ class CytoscapeGraphDrawer(AbstractXMLRPCDrawer, AbstractGraphDrawer): """Graph drawer that sends/receives graphs to/from Cytoscape using CytoscapeRPC. - This graph drawer cooperates with U{Cytoscape} - using U{CytoscapeRPC}. + This graph drawer cooperates with U{Cytoscape} + using U{CytoscapeRPC}. You need to install the CytoscapeRPC plugin first and start the XML-RPC server on a given port (port 9000 by default) from the appropriate Plugins submenu in Cytoscape. diff --git a/src/igraph/io/images.py b/src/igraph/io/images.py index 052be0500..202e39862 100644 --- a/src/igraph/io/images.py +++ b/src/igraph/io/images.py @@ -149,7 +149,7 @@ def _write_graph_to_svg( print('', file=f) print( - "", + "", file=f, ) print(file=f) diff --git a/tox.ini b/tox.ini index 221c56a55..e1b1944bb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,4 @@ -# Tox (http://tox.testrun.org/) is a tool for running tests +# Tox (https://tox.wiki) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. From 3bf77614b1107e2b0453e17db11dc44edb244fe3 Mon Sep 17 00:00:00 2001 From: Tim Bernhard Date: Thu, 10 Apr 2025 11:19:12 +0200 Subject: [PATCH 193/276] feat: implement simple cycle search Python binding (#806) --- CHANGELOG.md | 1 + src/_igraph/graphobject.c | 84 +++++++++++++++++++++++++++++++++++++++ tests/test_cycles.py | 19 +++++++++ 3 files changed, 104 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dea92015e..88c1c9d7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Dropped support for Python 3.8 as it has now reached its end of life. - The C core of igraph was updated to version 0.10.15. +- Added `Graph.simple_cycles()` to find simple cycles in the graph. ## [0.11.8] - 2024-10-25 diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index b5565ab59..0d2b05fab 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -7813,6 +7813,72 @@ PyObject *igraphmodule_Graph_minimum_cycle_basis( return result_o; } + +PyObject *igraphmodule_Graph_simple_cycles( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds +) { + PyObject *mode_o = Py_None; + PyObject *output_o = Py_None; + PyObject *min_cycle_length_o = Py_None; + PyObject *max_cycle_length_o = Py_None; + + // argument defaults: no cycle limits + igraph_integer_t mode = IGRAPH_OUT; + igraph_integer_t min_cycle_length = -1; + igraph_integer_t max_cycle_length = -1; + igraph_bool_t use_edges = false; + + static char *kwlist[] = { "mode", "min", "max", "output", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, &mode_o, &min_cycle_length_o, &max_cycle_length_o, &output_o)) + return NULL; + + if (mode_o != Py_None && igraphmodule_PyObject_to_integer_t(mode_o, &mode)) + return NULL; + + if (min_cycle_length_o != Py_None && igraphmodule_PyObject_to_integer_t(min_cycle_length_o, &min_cycle_length)) + return NULL; + + if (max_cycle_length_o != Py_None && igraphmodule_PyObject_to_integer_t(max_cycle_length_o, &max_cycle_length)) + return NULL; + + if (igraphmodule_PyObject_to_vpath_or_epath(output_o, &use_edges)) + return NULL; + + igraph_vector_int_list_t vertices; + if (igraph_vector_int_list_init(&vertices, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + igraph_vector_int_list_t edges; + if (igraph_vector_int_list_init(&edges, 0)) { + igraph_vector_int_list_destroy(&vertices); + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_simple_cycles( + &self->g, use_edges ? NULL : &vertices, use_edges ? &edges : NULL, mode, min_cycle_length, max_cycle_length + )) { + igraph_vector_int_list_destroy(&vertices); + igraph_vector_int_list_destroy(&edges); + igraphmodule_handle_igraph_error(); + return NULL; + } + + PyObject *result_o; + + if (use_edges) { + result_o = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&edges); + } else { + result_o = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&vertices); + } + igraph_vector_int_list_destroy(&edges); + igraph_vector_int_list_destroy(&vertices); + + return result_o; +} + /********************************************************************** * Graph layout algorithms * **********************************************************************/ @@ -16565,6 +16631,24 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " no guarantees are given about the ordering of edge IDs within cycles.\n" "@return: the cycle basis as a list of tuples containing edge IDs" }, + {"simple_cycles", (PyCFunction) igraphmodule_Graph_simple_cycles, + METH_VARARGS | METH_KEYWORDS, + "simple_cycles(mode=None, min=-1, max=-1, output=\"epath\")\n--\n\n" + "Finds simple cycles in a graph\n\n" + "@param mode: for directed graphs, specifies how the edge directions\n" + " should be taken into account. C{\"all\"} means that the edge directions\n" + " must be ignored, C{\"out\"} means that the edges must be oriented away\n" + " from the root, C{\"in\"} means that the edges must be oriented\n" + " towards the root. Ignored for undirected graphs.\n" + "@param min: the minimum number of vertices in a cycle\n" + " for it to be returned.\n" + "@param max: the maximum number of vertices in a cycle\n" + " for it to be considered.\n" + "@param output: determines what should be returned. If this is\n" + " C{\"vpath\"}, a list of tuples of vertex IDs will be returned. If this is\n" + " C{\"epath\"}, edge IDs are returned instead of vertex IDs.\n" + "@return: see the documentation of the C{output} parameter.\n" + }, /********************/ /* LAYOUT FUNCTIONS */ diff --git a/tests/test_cycles.py b/tests/test_cycles.py index 48a37f2f7..e3549121c 100644 --- a/tests/test_cycles.py +++ b/tests/test_cycles.py @@ -60,6 +60,25 @@ def test_fundamental_cycles(self): ] assert cycles == [[6, 7, 10], [8, 9, 10]] + def test_simple_cycles(self): + g = Graph( + [ + (0, 1), + (1, 2), + (2, 0), + (0, 0), + (0, 3), + (3, 4), + (4, 5), + (5, 0), + ] + ) + + vertices = g.simple_cycles(output="vpath") + edges = g.simple_cycles(output="epath") + assert len(vertices) == 3 + assert len(edges) == 3 + def test_minimum_cycle_basis(self): g = Graph( [ From f6fe8c9412a6c01b3e03a15f26820a0fb5e62116 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 20 Apr 2025 00:13:57 +0200 Subject: [PATCH 194/276] ci: trying to add Windows ARM64 builds --- .github/workflows/build.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5fea2ee20..6077b1094 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -208,16 +208,22 @@ jobs: build_wheel_win: name: Build wheels on Windows (${{ matrix.cmake_arch }}) - runs-on: windows-2019 strategy: matrix: include: - cmake_arch: Win32 wheel_arch: win32 vcpkg_arch: x86 + os: windows-2019 - cmake_arch: x64 wheel_arch: win_amd64 vcpkg_arch: x64 + os: windows-2019 + - cmake_arch: ARM64 + wheel_arch: win_arm64 + vcpkg_arch: arm64 + os: windows-11-arm + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 From 91225ea664b82f5db1f61b7a7723a1781ae64eb5 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 20 Apr 2025 00:21:33 +0200 Subject: [PATCH 195/276] ci: do not install Python in jobs where we are using cibuildwheel --- .github/workflows/build.yml | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6077b1094..83c921ef7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,11 +23,6 @@ jobs: submodules: true fetch-depth: 0 - - uses: actions/setup-python@v5 - name: Install Python - with: - python-version: '3.9' - - name: Build wheels (manylinux) uses: pypa/cibuildwheel@v2.23.2 env: @@ -129,11 +124,6 @@ jobs: path: ~/local key: deps-cache-v2-${{ runner.os }}-${{ matrix.cmake_arch }}-llvm${{ env.LLVM_VERSION }} - - uses: actions/setup-python@v5 - name: Install Python - with: - python-version: '3.9' - - name: Install OS dependencies if: steps.cache-c-core.outputs.cache-hit != 'true' || steps.cache-c-deps.outputs.cache-hit != 'true' # Only needed when building the C core or libomp run: @@ -231,11 +221,6 @@ jobs: submodules: true fetch-depth: 0 - - uses: actions/setup-python@v5 - name: Install Python - with: - python-version: '3.9' - - name: Cache installed C core id: cache-c-core uses: actions/cache@v4 @@ -267,7 +252,7 @@ jobs: CIBW_ENABLE: pypy CIBW_TEST_COMMAND: "cd /d {project} && pip install --prefer-binary \".[test]\" && python -m pytest tests" # Skip tests for Python 3.10 onwards because SciPy does not have - # 32-bit wheels for Linux. + # 32-bit wheels for Windows any more CIBW_TEST_SKIP: "cp310-win32 cp311-win32 cp312-win32 cp313-win32" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVCPKG_TARGET_TRIPLET=${{ matrix.vcpkg_arch }}-windows-static-md -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake -A ${{ matrix.cmake_arch }} IGRAPH_EXTRA_LIBRARY_PATH: C:/vcpkg/installed/${{ matrix.vcpkg_arch }}-windows-static-md/lib/ From 96660001b57b6538acd6e014e1af6441c20526f6 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 20 Apr 2025 10:50:26 +0200 Subject: [PATCH 196/276] ci: install liblzma in CI for Windows builds --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 83c921ef7..3888eec79 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -241,7 +241,7 @@ jobs: - name: Install VCPKG libraries run: | %VCPKG_INSTALLATION_ROOT%\vcpkg.exe integrate install - %VCPKG_INSTALLATION_ROOT%\vcpkg.exe install libxml2:${{ matrix.vcpkg_arch }}-windows-static-md + %VCPKG_INSTALLATION_ROOT%\vcpkg.exe install liblzma:${{ matrix.vcpkg_arch }}-windows-static-md libxml2:${{ matrix.vcpkg_arch }}-windows-static-md shell: cmd - name: Build wheels From 13869cf4f28226a0265844cbae12fe177f287ebf Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 20 Apr 2025 12:36:56 +0200 Subject: [PATCH 197/276] chore: extend tox.ini with Python 3.13 --- tox.ini | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index e1b1944bb..04fb5ca92 100644 --- a/tox.ini +++ b/tox.ini @@ -4,15 +4,16 @@ # and then run "tox" from this directory. [tox] -envlist = py38, py39, py310, py311, py312, pypy3 +envlist = py38, py39, py310, py311, py312, py313, pypy3 [gh-actions] python = 3.8: py38 3.9: py39 - 3.10: py310 - 3.11: py311 - 3.12: py312 + 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 pypy-3.7: pypy3 [testenv] From e5e6bc40d72d0a225c9913edff191f752a85012e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 20 Apr 2025 12:40:33 +0200 Subject: [PATCH 198/276] ci: do not attempt to compile NumPy in CI for Windows ARM64 --- .github/workflows/build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3888eec79..379bfe03a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -205,14 +205,17 @@ jobs: wheel_arch: win32 vcpkg_arch: x86 os: windows-2019 + test_extra: test - cmake_arch: x64 wheel_arch: win_amd64 vcpkg_arch: x64 os: windows-2019 + test_extra: test - cmake_arch: ARM64 wheel_arch: win_arm64 vcpkg_arch: arm64 os: windows-11-arm + test_extra: test-win-arm64 runs-on: ${{ matrix.os }} steps: @@ -250,7 +253,7 @@ jobs: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" CIBW_ENABLE: pypy - CIBW_TEST_COMMAND: "cd /d {project} && pip install --prefer-binary \".[test]\" && python -m pytest tests" + CIBW_TEST_COMMAND: "cd /d {project} && pip install --prefer-binary \".[${{ matrix.test_extra }}]\" && python -m pytest tests" # Skip tests for Python 3.10 onwards because SciPy does not have # 32-bit wheels for Windows any more CIBW_TEST_SKIP: "cp310-win32 cp311-win32 cp312-win32 cp313-win32" From fe1cdc21b61de93d6883b29348f655ed303e4118 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 20 Apr 2025 13:46:54 +0200 Subject: [PATCH 199/276] fix: added missing test-win-arm64 extra --- setup.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/setup.py b/setup.py index b61fe9881..a8d1c965a 100644 --- a/setup.py +++ b/setup.py @@ -1020,6 +1020,15 @@ def get_tag(self): "plotly>=5.3.0", "Pillow>=9; platform_python_implementation != 'PyPy'", ], + # Dependencies needed for testing on Windows ARM64; only those that are either + # pure Python or have Windows ARM64 wheels as we don't want to compile wheels + # in CI + "test-win-arm64": [ + "cairocffi>=1.2.0", + "networkx>=2.5", + "pytest>=7.0.1", + "pytest-timeout>=2.1.0", + ], # Dependencies needed for testing on musllinux; only those that are either # pure Python or have musllinux wheels as we don't want to compile wheels # in CI From 8ffb7031a3fca0094d66c37ba5c9289617cca518 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 21:15:08 +0200 Subject: [PATCH 200/276] build(deps): bump pypa/cibuildwheel from 2.23.2 to 2.23.3 (#827) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 379bfe03a..754f5fba1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: fetch-depth: 0 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.23.2 + uses: pypa/cibuildwheel@v2.23.3 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -34,7 +34,7 @@ jobs: CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686 cp313-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.23.2 + uses: pypa/cibuildwheel@v2.23.3 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" @@ -55,7 +55,7 @@ jobs: fetch-depth: 0 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.23.2 + uses: pypa/cibuildwheel@v2.23.3 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -77,7 +77,7 @@ jobs: fetch-depth: 0 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.23.2 + uses: pypa/cibuildwheel@v2.23.3 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -141,7 +141,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.23.2 + uses: pypa/cibuildwheel@v2.23.3 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" @@ -248,7 +248,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.23.2 + uses: pypa/cibuildwheel@v2.23.3 env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From 75610b3ebb66801155dbfc1156d28465c0141fd2 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 28 Apr 2025 16:02:42 +0200 Subject: [PATCH 201/276] chore: mkdoc.sh script reformatted, output is less verbose now --- scripts/mkdoc.sh | 91 ++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index 31d6a35ac..32ea358d2 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -17,27 +17,27 @@ CLEAN=0 while getopts ":scjdl" OPTION; do case $OPTION in - c) - CLEAN=1 - ;; - d) - DOC2DASH=1 - ;; - l) - LINKCHECK=1 - ;; - \?) - echo "Usage: $0 [-sjd]" - exit 1 - ;; - esac + c) + CLEAN=1 + ;; + d) + DOC2DASH=1 + ;; + l) + LINKCHECK=1 + ;; + \?) + echo "Usage: $0 [-sjd]" + exit 1 + ;; + esac done -shift $((OPTIND -1)) +shift $((OPTIND - 1)) -SCRIPTS_FOLDER=`dirname $0` +SCRIPTS_FOLDER=$(dirname $0) cd ${SCRIPTS_FOLDER}/.. -ROOT_FOLDER=`pwd` +ROOT_FOLDER=$(pwd) DOC_SOURCE_FOLDER=${ROOT_FOLDER}/doc/source DOC_HTML_FOLDER=${ROOT_FOLDER}/doc/html DOC_LINKCHECK_FOLDER=${ROOT_FOLDER}/doc/linkcheck @@ -51,17 +51,20 @@ if [ ! -d ".venv" ]; then # Install sphinx, matplotlib, pandas, scipy, wheel and pydoctor into the venv. # doc2dash is optional; it will be installed when -d is given - .venv/bin/pip install -U pip wheel sphinx==7.4.7 matplotlib pandas scipy pydoctor sphinx-rtd-theme + .venv/bin/pip install -q -U pip wheel sphinx==7.4.7 matplotlib pandas scipy pydoctor sphinx-rtd-theme +else + # Upgrade pip in the virtualenv + .venv/bin/pip install -q -U pip wheel fi # Make sure that Sphinx, PyDoctor (and maybe doc2dash) are up-to-date in the virtualenv -.venv/bin/pip install -U sphinx==7.4.7 pydoctor sphinx-gallery sphinxcontrib-jquery sphinx-rtd-theme +.venv/bin/pip install -q -U sphinx==7.4.7 pydoctor sphinx-gallery sphinxcontrib-jquery sphinx-rtd-theme if [ x$DOC2DASH = x1 ]; then - .venv/bin/pip install -U doc2dash + .venv/bin/pip install -U doc2dash fi echo "Removing existing igraph and python-igraph eggs from virtualenv..." -SITE_PACKAGES_DIR=`.venv/bin/python3 -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])'` +SITE_PACKAGES_DIR=$(.venv/bin/python3 -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])') rm -rf "${SITE_PACKAGES_DIR}"/igraph*.egg rm -rf "${SITE_PACKAGES_DIR}"/igraph*.egg-link rm -rf "${SITE_PACKAGES_DIR}"/python_igraph*.egg @@ -84,49 +87,47 @@ fi if [ "x$LINKCHECK" = "x1" ]; then echo "Check for broken links" .venv/bin/python -m sphinx \ - -T \ - -b linkcheck \ - -Dtemplates_path='' \ - -Dhtml_theme='alabaster' \ - ${DOC_SOURCE_FOLDER} ${DOC_LINKCHECK_FOLDER} + -T \ + -b linkcheck \ + -Dtemplates_path='' \ + -Dhtml_theme='alabaster' \ + ${DOC_SOURCE_FOLDER} ${DOC_LINKCHECK_FOLDER} fi - echo "Generating HTML documentation..." -.venv/bin/pip install -U sphinx-rtd-theme +.venv/bin/pip install -q -U sphinx-rtd-theme .venv/bin/python -m sphinx -T -b html ${DOC_SOURCE_FOLDER} ${DOC_HTML_FOLDER} echo "HTML documentation generated in ${DOC_HTML_FOLDER}" - # doc2dash if [ "x$DOC2DASH" = "x1" ]; then - PWD=`pwd` + PWD=$(pwd) # Output folder of sphinx (before Jekyll if requested) DOC_API_FOLDER=${ROOT_FOLDER}/doc/html/api DOC2DASH=.venv/bin/doc2dash DASH_FOLDER=${ROOT_FOLDER}/doc/dash if [ "x$DOC2DASH" != x ]; then - echo "Generating Dash docset..." - "$DOC2DASH" \ - --online-redirect-url "https://python.igraph.org/en/latest/api/" \ - --name "python-igraph" \ - -d "${DASH_FOLDER}" \ - -f \ - -j \ - -I "index.html" \ - --icon ${ROOT_FOLDER}/doc/source/icon.png \ - --icon-2x ${ROOT_FOLDER}/doc/source/icon@2x.png \ - "${DOC_API_FOLDER}" - DASH_READY=1 + echo "Generating Dash docset..." + "$DOC2DASH" \ + --online-redirect-url "https://python.igraph.org/en/latest/api/" \ + --name "python-igraph" \ + -d "${DASH_FOLDER}" \ + -f \ + -j \ + -I "index.html" \ + --icon ${ROOT_FOLDER}/doc/source/icon.png \ + --icon-2x ${ROOT_FOLDER}/doc/source/icon@2x.png \ + "${DOC_API_FOLDER}" + DASH_READY=1 else - echo "WARNING: doc2dash not installed, skipping Dash docset generation." - DASH_READY=0 + echo "WARNING: doc2dash not installed, skipping Dash docset generation." + DASH_READY=0 fi echo "" if [ "x${DASH_READY}" = x1 ]; then - echo "Dash docset generated in ${DASH_FOLDER}/python-igraph.docset" + echo "Dash docset generated in ${DASH_FOLDER}/python-igraph.docset" fi cd "$PWD" From 1c709fdf161753efbe4e65782cb3e07e75565e1f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 5 May 2025 21:07:35 +0200 Subject: [PATCH 202/276] refactor: remove unused igraphmodule_resolve_graph_weakref --- src/_igraph/common.c | 24 ------------------------ src/_igraph/common.h | 1 - 2 files changed, 25 deletions(-) diff --git a/src/_igraph/common.c b/src/_igraph/common.c index bf888adfc..a099ef030 100644 --- a/src/_igraph/common.c +++ b/src/_igraph/common.c @@ -46,27 +46,3 @@ PyObject* igraphmodule_unimplemented(PyObject* self, PyObject* args, PyObject* k PyErr_SetString(PyExc_NotImplementedError, "This method is unimplemented."); return NULL; } - -/** - * \ingroup python_interface - * \brief Resolves a weak reference to an \c igraph.Graph - * \return the \c igraph.Graph object or NULL if the weak reference is dead. - * Sets an exception in the latter case. - */ -PyObject* igraphmodule_resolve_graph_weakref(PyObject* ref) { - PyObject *o; - -#ifndef PYPY_VERSION - /* PyWeakref_Check is not implemented in PyPy yet */ - if (!PyWeakref_Check(ref)) { - PyErr_SetString(PyExc_TypeError, "weak reference expected"); - return NULL; - } -#endif /* PYPY_VERSION */ - o=PyWeakref_GetObject(ref); - if (o == Py_None) { - PyErr_SetString(PyExc_TypeError, "underlying graph has already been destroyed"); - return NULL; - } - return o; -} diff --git a/src/_igraph/common.h b/src/_igraph/common.h index 77514e2fd..fda85c948 100644 --- a/src/_igraph/common.h +++ b/src/_igraph/common.h @@ -51,5 +51,4 @@ #define ATTRIBUTE_TYPE_EDGE 2 PyObject* igraphmodule_unimplemented(PyObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_resolve_graph_weakref(PyObject* ref); #endif From 1c423c60c77499476f9ae51aad65fca9340acef6 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 5 May 2025 23:17:16 +0200 Subject: [PATCH 203/276] chore: updated vendored igraph --- src/_igraph/attributes.c | 179 +++++++++++++++---------- src/_igraph/convert.c | 65 ++++++++- src/_igraph/convert.h | 2 + src/_igraph/graphobject.c | 261 +++++++++++++++++++++++-------------- src/_igraph/igraphmodule.c | 8 +- src/igraph/__init__.py | 23 ++-- tests/test_atlas.py | 18 ++- tests/test_bipartite.py | 26 ++-- tests/test_foreign.py | 6 +- tests/test_generators.py | 92 ++++++------- tests/test_structural.py | 8 -- vendor/source/igraph | 2 +- 12 files changed, 423 insertions(+), 267 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index d91eb84b5..cb543030f 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -347,7 +347,9 @@ PyObject* igraphmodule_create_or_get_edge_attribute_values(const igraph_t* graph /* Attribute handlers for the Python interface */ /* Initialization */ -static igraph_error_t igraphmodule_i_attribute_init(igraph_t *graph, igraph_vector_ptr_t *attr) { +static igraph_error_t igraphmodule_i_attribute_init( + igraph_t *graph, const igraph_attribute_record_list_t *attr +) { igraphmodule_i_attribute_struct* attrs; igraph_integer_t i, n; @@ -368,27 +370,26 @@ static igraph_error_t igraphmodule_i_attribute_init(igraph_t *graph, igraph_vect PyObject *dict = attrs->attrs[0], *value; const char *s; - n = igraph_vector_ptr_size(attr); + n = igraph_attribute_record_list_size(attr); for (i = 0; i < n; i++) { - igraph_attribute_record_t *attr_rec; - attr_rec = VECTOR(*attr)[i]; + igraph_attribute_record_t *attr_rec = igraph_attribute_record_list_get_ptr(attr, i); switch (attr_rec->type) { case IGRAPH_ATTRIBUTE_NUMERIC: - value = PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[0]); + value = PyFloat_FromDouble((double)VECTOR(*attr_rec->value.as_vector)[0]); if (!value) { PyErr_PrintEx(0); } break; case IGRAPH_ATTRIBUTE_STRING: - s = igraph_strvector_get((igraph_strvector_t*)attr_rec->value, 0); + s = igraph_strvector_get(attr_rec->value.as_strvector, 0); value = PyUnicode_FromString(s ? s : ""); if (!value) { PyErr_PrintEx(0); } break; case IGRAPH_ATTRIBUTE_BOOLEAN: - value = VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[0] ? Py_True : Py_False; + value = VECTOR(*attr_rec->value.as_vector_bool)[0] ? Py_True : Py_False; Py_INCREF(value); break; default: @@ -515,7 +516,9 @@ static igraph_error_t igraphmodule_i_attribute_copy(igraph_t *to, const igraph_t } /* Adding vertices */ -static igraph_error_t igraphmodule_i_attribute_add_vertices(igraph_t *graph, igraph_integer_t nv, igraph_vector_ptr_t *attr) { +static igraph_error_t igraphmodule_i_attribute_add_vertices( + igraph_t *graph, igraph_integer_t nv, const igraph_attribute_record_list_t *attr +) { /* Extend the end of every value in the vertex hash with nv pieces of None */ PyObject *key, *value, *dict; igraph_integer_t i, j, k, num_attr_entries; @@ -531,7 +534,7 @@ static igraph_error_t igraphmodule_i_attribute_add_vertices(igraph_t *graph, igr return IGRAPH_SUCCESS; } - num_attr_entries = attr ? igraph_vector_ptr_size(attr) : 0; + num_attr_entries = attr ? igraph_attribute_record_list_size(attr) : 0; IGRAPH_VECTOR_BOOL_INIT_FINALLY(&added_attrs, num_attr_entries); dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; @@ -547,7 +550,7 @@ static igraph_error_t igraphmodule_i_attribute_add_vertices(igraph_t *graph, igr /* Check if we have specific values for the given attribute */ attr_rec = NULL; for (i = 0; i < num_attr_entries; i++) { - attr_rec = VECTOR(*attr)[i]; + attr_rec = igraph_attribute_record_list_get_ptr(attr, i); if (igraphmodule_PyObject_matches_attribute_record(key, attr_rec)) { VECTOR(added_attrs)[i] = 1; break; @@ -564,14 +567,14 @@ static igraph_error_t igraphmodule_i_attribute_add_vertices(igraph_t *graph, igr switch (attr_rec->type) { case IGRAPH_ATTRIBUTE_NUMERIC: - o = PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[i]); + o = PyFloat_FromDouble((double)VECTOR(*attr_rec->value.as_vector)[i]); break; case IGRAPH_ATTRIBUTE_STRING: - s = igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i); + s = igraph_strvector_get(attr_rec->value.as_strvector, i); o = PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: - o = VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; + o = VECTOR(*attr_rec->value.as_vector_bool)[i] ? Py_True : Py_False; Py_INCREF(o); break; default: @@ -620,7 +623,7 @@ static igraph_error_t igraphmodule_i_attribute_add_vertices(igraph_t *graph, igr continue; } - attr_rec = (igraph_attribute_record_t*)VECTOR(*attr)[k]; + attr_rec = igraph_attribute_record_list_get_ptr(attr, k); value = PyList_New(j + nv); if (!value) { @@ -637,14 +640,14 @@ static igraph_error_t igraphmodule_i_attribute_add_vertices(igraph_t *graph, igr PyObject *o = NULL; switch (attr_rec->type) { case IGRAPH_ATTRIBUTE_NUMERIC: - o = PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[i]); + o = PyFloat_FromDouble((double)VECTOR(*attr_rec->value.as_vector)[i]); break; case IGRAPH_ATTRIBUTE_STRING: - s = igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i); + s = igraph_strvector_get(attr_rec->value.as_strvector, i); o = PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: - o = VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; + o = VECTOR(*attr_rec->value.as_vector_bool)[i] ? Py_True : Py_False; Py_INCREF(o); break; default: @@ -739,7 +742,9 @@ static igraph_error_t igraphmodule_i_attribute_permute_vertices(const igraph_t * } /* Adding edges */ -static igraph_error_t igraphmodule_i_attribute_add_edges(igraph_t *graph, const igraph_vector_int_t *edges, igraph_vector_ptr_t *attr) { +static igraph_error_t igraphmodule_i_attribute_add_edges( + igraph_t *graph, const igraph_vector_int_t *edges, const igraph_attribute_record_list_t* attr +) { /* Extend the end of every value in the edge hash with ne pieces of None */ PyObject *key, *value, *dict; Py_ssize_t pos = 0; @@ -756,7 +761,7 @@ static igraph_error_t igraphmodule_i_attribute_add_edges(igraph_t *graph, const return IGRAPH_SUCCESS; } - num_attr_entries = attr ? igraph_vector_ptr_size(attr) : 0; + num_attr_entries = attr ? igraph_attribute_record_list_size(attr) : 0; IGRAPH_VECTOR_BOOL_INIT_FINALLY(&added_attrs, num_attr_entries); dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; @@ -772,7 +777,7 @@ static igraph_error_t igraphmodule_i_attribute_add_edges(igraph_t *graph, const /* Check if we have specific values for the given attribute */ attr_rec = NULL; for (i = 0; i < num_attr_entries; i++) { - attr_rec = VECTOR(*attr)[i]; + attr_rec = igraph_attribute_record_list_get_ptr(attr, i); if (igraphmodule_PyObject_matches_attribute_record(key, attr_rec)) { VECTOR(added_attrs)[i] = 1; break; @@ -788,14 +793,14 @@ static igraph_error_t igraphmodule_i_attribute_add_edges(igraph_t *graph, const PyObject *o; switch (attr_rec->type) { case IGRAPH_ATTRIBUTE_NUMERIC: - o = PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[i]); + o = PyFloat_FromDouble((double)VECTOR(*attr_rec->value.as_vector)[i]); break; case IGRAPH_ATTRIBUTE_STRING: - s = igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i); + s = igraph_strvector_get(attr_rec->value.as_strvector, i); o = PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: - o = VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; + o = VECTOR(*attr_rec->value.as_vector_bool)[i] ? Py_True : Py_False; Py_INCREF(o); break; default: @@ -837,7 +842,7 @@ static igraph_error_t igraphmodule_i_attribute_add_edges(igraph_t *graph, const if (VECTOR(added_attrs)[k]) { continue; } - attr_rec=(igraph_attribute_record_t*)VECTOR(*attr)[k]; + attr_rec = igraph_attribute_record_list_get_ptr(attr, k); value = PyList_New(j + ne); if (!value) { @@ -854,14 +859,14 @@ static igraph_error_t igraphmodule_i_attribute_add_edges(igraph_t *graph, const PyObject *o; switch (attr_rec->type) { case IGRAPH_ATTRIBUTE_NUMERIC: - o = PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[i]); + o = PyFloat_FromDouble((double)VECTOR(*attr_rec->value.as_vector)[i]); break; case IGRAPH_ATTRIBUTE_STRING: - s = igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i); + s = igraph_strvector_get(attr_rec->value.as_strvector, i); o = PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: - o = VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; + o = VECTOR(*attr_rec->value.as_vector_bool)[i] ? Py_True : Py_False; Py_INCREF(o); break; default: @@ -1768,15 +1773,15 @@ igraph_error_t igraphmodule_i_get_boolean_graph_attr(const igraph_t *graph, if (!o) { IGRAPH_ERRORF("No boolean graph attribute named \"%s\" exists.", IGRAPH_EINVAL, name); } - IGRAPH_CHECK(igraph_vector_bool_resize(value, 1)); - VECTOR(*value)[0] = PyObject_IsTrue(o); - return IGRAPH_SUCCESS; + return igraph_vector_bool_push_back(value, PyObject_IsTrue(o)); } /* Getting numeric graph attributes */ igraph_error_t igraphmodule_i_get_numeric_graph_attr(const igraph_t *graph, const char *name, igraph_vector_t *value) { PyObject *dict, *o, *result; + double num; + dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_GRAPH]; /* No error checking, if we get here, the type has already been checked by previous attribute handler calls... hopefully :) Same applies for the other handlers. */ @@ -1784,18 +1789,20 @@ igraph_error_t igraphmodule_i_get_numeric_graph_attr(const igraph_t *graph, if (!o) { IGRAPH_ERRORF("No numeric graph attribute named \"%s\" exists.", IGRAPH_EINVAL, name); } - IGRAPH_CHECK(igraph_vector_resize(value, 1)); + if (o == Py_None) { - VECTOR(*value)[0] = IGRAPH_NAN; - return IGRAPH_SUCCESS; + num = IGRAPH_NAN; + } else { + result = PyNumber_Float(o); + if (result) { + num = PyFloat_AsDouble(o); + Py_DECREF(result); + } else { + IGRAPH_ERROR("Internal error in PyNumber_Float", IGRAPH_EINVAL); + } } - result = PyNumber_Float(o); - if (result) { - VECTOR(*value)[0] = PyFloat_AsDouble(o); - Py_DECREF(result); - } else IGRAPH_ERROR("Internal error in PyFloat_AsDouble", IGRAPH_EINVAL); - return IGRAPH_SUCCESS; + return igraph_vector_push_back(value, num); } /* Getting string graph attributes */ @@ -1809,7 +1816,6 @@ igraph_error_t igraphmodule_i_get_string_graph_attr(const igraph_t *graph, if (!o) { IGRAPH_ERRORF("No string graph attribute named \"%s\" exists.", IGRAPH_EINVAL, name); } - IGRAPH_CHECK(igraph_strvector_resize(value, 1)); /* For Python 3.x, we simply call PyObject_Str, which produces a * Unicode string, then encode it into UTF-8, except when we @@ -1837,7 +1843,7 @@ igraph_error_t igraphmodule_i_get_string_graph_attr(const igraph_t *graph, IGRAPH_ERROR("Internal error in PyBytes_AsString", IGRAPH_EINVAL); } - IGRAPH_CHECK(igraph_strvector_set(value, 0, c_str)); + IGRAPH_CHECK(igraph_strvector_push_back(value, c_str)); Py_XDECREF(str); @@ -1859,16 +1865,19 @@ igraph_error_t igraphmodule_i_get_numeric_vertex_attr(const igraph_t *graph, } if (igraph_vs_is_all(&vs)) { - if (igraphmodule_PyObject_float_to_vector_t(list, &newvalue)) + if (igraphmodule_PyObject_float_to_vector_t(list, &newvalue)) { IGRAPH_ERROR("Internal error", IGRAPH_EINVAL); - igraph_vector_update(value, &newvalue); + } + IGRAPH_FINALLY(igraph_vector_destroy, &newvalue); + IGRAPH_CHECK(igraph_vector_append(value, &newvalue)); + IGRAPH_FINALLY_CLEAN(1); igraph_vector_destroy(&newvalue); } else { igraph_vit_t it; - igraph_integer_t i = 0; + igraph_integer_t i = igraph_vector_size(value); IGRAPH_CHECK(igraph_vit_create(graph, vs, &it)); IGRAPH_FINALLY(igraph_vit_destroy, &it); - IGRAPH_CHECK(igraph_vector_resize(value, IGRAPH_VIT_SIZE(it))); + IGRAPH_CHECK(igraph_vector_resize(value, i + IGRAPH_VIT_SIZE(it))); while (!IGRAPH_VIT_END(it)) { o = PyList_GetItem(list, (Py_ssize_t)IGRAPH_VIT_GET(it)); if (o != Py_None) { @@ -1903,16 +1912,19 @@ igraph_error_t igraphmodule_i_get_string_vertex_attr(const igraph_t *graph, } if (igraph_vs_is_all(&vs)) { - if (igraphmodule_PyList_to_strvector_t(list, &newvalue)) + if (igraphmodule_PyList_to_strvector_t(list, &newvalue)) { IGRAPH_ERROR("Internal error", IGRAPH_EINVAL); - igraph_strvector_destroy(value); - *value=newvalue; + } + IGRAPH_FINALLY(igraph_strvector_destroy, &newvalue); + IGRAPH_CHECK(igraph_strvector_append(value, &newvalue)); + IGRAPH_FINALLY_CLEAN(1); + igraph_strvector_destroy(&newvalue); } else { igraph_vit_t it; - igraph_integer_t i = 0; + igraph_integer_t i = igraph_strvector_size(value); IGRAPH_CHECK(igraph_vit_create(graph, vs, &it)); IGRAPH_FINALLY(igraph_vit_destroy, &it); - IGRAPH_CHECK(igraph_strvector_resize(value, IGRAPH_VIT_SIZE(it))); + IGRAPH_CHECK(igraph_strvector_resize(value, i + IGRAPH_VIT_SIZE(it))); while (!IGRAPH_VIT_END(it)) { igraph_integer_t v = IGRAPH_VIT_GET(it); char* str; @@ -1922,16 +1934,19 @@ igraph_error_t igraphmodule_i_get_string_vertex_attr(const igraph_t *graph, IGRAPH_ERROR("null element in PyList", IGRAPH_EINVAL); str = igraphmodule_PyObject_ConvertToCString(result); - if (str == 0) + if (str == 0) { IGRAPH_ERROR("error while calling igraphmodule_PyObject_ConvertToCString", IGRAPH_EINVAL); + } + IGRAPH_FINALLY(free, str); /* Note: this is a bit inefficient here, igraphmodule_PyObject_ConvertToCString * allocates a new string which could be copied into the string * vector straight away. Instead of that, the string vector makes * another copy. Probably the performance hit is not too severe. */ - igraph_strvector_set(value, i, str); + IGRAPH_CHECK(igraph_strvector_set(value, i, str)); free(str); + IGRAPH_FINALLY_CLEAN(1); IGRAPH_VIT_NEXT(it); i++; @@ -1958,16 +1973,19 @@ igraph_error_t igraphmodule_i_get_boolean_vertex_attr(const igraph_t *graph, } if (igraph_vs_is_all(&vs)) { - if (igraphmodule_PyObject_to_vector_bool_t(list, &newvalue)) + if (igraphmodule_PyObject_to_vector_bool_t(list, &newvalue)) { IGRAPH_ERROR("Internal error", IGRAPH_EINVAL); - igraph_vector_bool_update(value, &newvalue); + } + IGRAPH_FINALLY(igraph_vector_bool_destroy, &newvalue); + IGRAPH_CHECK(igraph_vector_bool_append(value, &newvalue)); + IGRAPH_FINALLY_CLEAN(1); igraph_vector_bool_destroy(&newvalue); } else { igraph_vit_t it; - igraph_integer_t i = 0; + igraph_integer_t i = igraph_vector_bool_size(value); IGRAPH_CHECK(igraph_vit_create(graph, vs, &it)); IGRAPH_FINALLY(igraph_vit_destroy, &it); - IGRAPH_CHECK(igraph_vector_bool_resize(value, IGRAPH_VIT_SIZE(it))); + IGRAPH_CHECK(igraph_vector_bool_resize(value, i + IGRAPH_VIT_SIZE(it))); while (!IGRAPH_VIT_END(it)) { o = PyList_GetItem(list, (Py_ssize_t)IGRAPH_VIT_GET(it)); VECTOR(*value)[i] = PyObject_IsTrue(o); @@ -1996,23 +2014,32 @@ igraph_error_t igraphmodule_i_get_numeric_edge_attr(const igraph_t *graph, } if (igraph_es_is_all(&es)) { - if (igraphmodule_PyObject_float_to_vector_t(list, &newvalue)) + if (igraphmodule_PyObject_float_to_vector_t(list, &newvalue)) { IGRAPH_ERROR("Internal error", IGRAPH_EINVAL); - igraph_vector_update(value, &newvalue); + } + IGRAPH_FINALLY(igraph_vector_destroy, &newvalue); + IGRAPH_CHECK(igraph_vector_append(value, &newvalue)); + IGRAPH_FINALLY_CLEAN(1); igraph_vector_destroy(&newvalue); } else { igraph_eit_t it; - igraph_integer_t i = 0; + igraph_integer_t i = igraph_vector_size(value); IGRAPH_CHECK(igraph_eit_create(graph, es, &it)); IGRAPH_FINALLY(igraph_eit_destroy, &it); - IGRAPH_CHECK(igraph_vector_resize(value, IGRAPH_EIT_SIZE(it))); + IGRAPH_CHECK(igraph_vector_resize(value, i + IGRAPH_EIT_SIZE(it))); while (!IGRAPH_EIT_END(it)) { o = PyList_GetItem(list, (Py_ssize_t)IGRAPH_EIT_GET(it)); if (o != Py_None) { result = PyNumber_Float(o); - VECTOR(*value)[i] = PyFloat_AsDouble(result); - Py_XDECREF(result); - } else VECTOR(*value)[i] = IGRAPH_NAN; + if (result) { + VECTOR(*value)[i] = PyFloat_AsDouble(result); + Py_XDECREF(result); + } else { + IGRAPH_ERROR("Internal error in PyNumber_Float", IGRAPH_EINVAL); + } + } else { + VECTOR(*value)[i] = IGRAPH_NAN; + } IGRAPH_EIT_NEXT(it); i++; } @@ -2038,13 +2065,16 @@ igraph_error_t igraphmodule_i_get_string_edge_attr(const igraph_t *graph, } if (igraph_es_is_all(&es)) { - if (igraphmodule_PyList_to_strvector_t(list, &newvalue)) + if (igraphmodule_PyList_to_strvector_t(list, &newvalue)) { IGRAPH_ERROR("Internal error", IGRAPH_EINVAL); - igraph_strvector_destroy(value); - *value=newvalue; + } + IGRAPH_FINALLY(igraph_strvector_destroy, &newvalue); + IGRAPH_CHECK(igraph_strvector_append(value, &newvalue)); + IGRAPH_FINALLY_CLEAN(1); + igraph_strvector_destroy(&newvalue); } else { igraph_eit_t it; - igraph_integer_t i = 0; + igraph_integer_t i = igraph_strvector_size(value); IGRAPH_CHECK(igraph_eit_create(graph, es, &it)); IGRAPH_FINALLY(igraph_eit_destroy, &it); IGRAPH_CHECK(igraph_strvector_resize(value, IGRAPH_EIT_SIZE(it))); @@ -2064,7 +2094,9 @@ igraph_error_t igraphmodule_i_get_string_edge_attr(const igraph_t *graph, * vector straight away. Instead of that, the string vector makes * another copy. Probably the performance hit is not too severe. */ - igraph_strvector_set(value, i, str); + IGRAPH_FINALLY(free, str); + IGRAPH_CHECK(igraph_strvector_set(value, i, str)); + IGRAPH_FINALLY_CLEAN(1); free(str); IGRAPH_EIT_NEXT(it); @@ -2092,16 +2124,19 @@ igraph_error_t igraphmodule_i_get_boolean_edge_attr(const igraph_t *graph, } if (igraph_es_is_all(&es)) { - if (igraphmodule_PyObject_to_vector_bool_t(list, &newvalue)) + if (igraphmodule_PyObject_to_vector_bool_t(list, &newvalue)) { IGRAPH_ERROR("Internal error", IGRAPH_EINVAL); - igraph_vector_bool_update(value, &newvalue); + } + IGRAPH_FINALLY(igraph_vector_bool_destroy, &newvalue); + IGRAPH_CHECK(igraph_vector_bool_append(value, &newvalue)); + IGRAPH_FINALLY_CLEAN(1); igraph_vector_bool_destroy(&newvalue); } else { igraph_eit_t it; - igraph_integer_t i=0; + igraph_integer_t i = igraph_vector_bool_size(value); IGRAPH_CHECK(igraph_eit_create(graph, es, &it)); IGRAPH_FINALLY(igraph_eit_destroy, &it); - IGRAPH_CHECK(igraph_vector_bool_resize(value, IGRAPH_EIT_SIZE(it))); + IGRAPH_CHECK(igraph_vector_bool_resize(value, i + IGRAPH_EIT_SIZE(it))); while (!IGRAPH_EIT_END(it)) { o = PyList_GetItem(list, (Py_ssize_t)IGRAPH_EIT_GET(it)); VECTOR(*value)[i] = PyObject_IsTrue(o); diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 1964d40e1..248c35387 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -710,6 +710,36 @@ int igraphmodule_PyObject_to_loops_t(PyObject *o, igraph_loops_t *result) { TRANSLATE_ENUM_WITH(loops_tt); } +/** + * \brief Converts a Python object to an igraph \c igraph_lpa_variant_t + */ +int igraphmodule_PyObject_to_lpa_variant_t(PyObject *o, igraph_lpa_variant_t *result) { + static igraphmodule_enum_translation_table_entry_t lpa_variant_tt[] = { + {"dominance", IGRAPH_LPA_DOMINANCE}, + {"retention", IGRAPH_LPA_RETENTION}, + {"fast", IGRAPH_LPA_FAST}, + {0,0} + }; + + TRANSLATE_ENUM_WITH(lpa_variant_tt); +} + +/** + * \brief Converts a Python object to an igraph \c igraph_mst_algorithm_t + */ +int igraphmodule_PyObject_to_mst_algorithm_t(PyObject *o, igraph_mst_algorithm_t *result) { + static igraphmodule_enum_translation_table_entry_t mst_algorithm_tt[] = { + {"auto", IGRAPH_MST_AUTOMATIC}, + {"automatic", IGRAPH_MST_AUTOMATIC}, + {"unweighted", IGRAPH_MST_UNWEIGHTED}, + {"prim", IGRAPH_MST_PRIM}, + {"kruskal", IGRAPH_MST_KRUSKAL}, + {0,0} + }; + + TRANSLATE_ENUM_WITH(mst_algorithm_tt); +} + /** * \ingroup python_interface_conversion * \brief Converts a Python object to an igraph \c igraph_random_walk_stuck_t @@ -1962,7 +1992,19 @@ int igraphmodule_attrib_to_vector_t(PyObject *o, igraphmodule_GraphObject *self, free(name); return 1; } - igraph_vector_init(result, n); + if (igraph_vector_init(result, 0)) { + igraphmodule_handle_igraph_error(); + free(name); + free(result); + return 1; + } + if (igraph_vector_reserve(result, n)) { + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(result); + free(name); + free(result); + return 1; + } if (attr_type == ATTRIBUTE_TYPE_VERTEX) { if (igraphmodule_i_get_numeric_vertex_attr(&self->g, name, igraph_vss_all(), result)) { @@ -2157,7 +2199,19 @@ int igraphmodule_attrib_to_vector_bool_t(PyObject *o, igraphmodule_GraphObject * free(name); return 1; } - igraph_vector_bool_init(result, n); + if (igraph_vector_bool_init(result, 0)) { + igraphmodule_handle_igraph_error(); + free(name); + free(result); + return 1; + } + if (igraph_vector_bool_reserve(result, n)) { + igraph_vector_bool_destroy(result); + igraphmodule_handle_igraph_error(); + free(name); + free(result); + return 1; + } if (attr_type == ATTRIBUTE_TYPE_VERTEX) { if (igraphmodule_i_get_boolean_vertex_attr(&self->g, name, igraph_vss_all(), result)) { @@ -2193,12 +2247,17 @@ int igraphmodule_attrib_to_vector_bool_t(PyObject *o, igraphmodule_GraphObject * n = igraph_vector_size(dummy); result = (igraph_vector_bool_t*)calloc(1, sizeof(igraph_vector_bool_t)); - igraph_vector_bool_init(result, n); if (result == 0) { igraph_vector_destroy(dummy); free(dummy); PyErr_NoMemory(); return 1; } + if (igraph_vector_bool_init(result, n)) { + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(dummy); free(dummy); + igraph_vector_bool_destroy(result); free(result); + return 1; + } for (i = 0; i < n; i++) { VECTOR(*result)[i] = (VECTOR(*dummy)[i] != 0 && VECTOR(*dummy)[i] == VECTOR(*dummy)[i]); diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 873be1dc0..525afbb5e 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -76,7 +76,9 @@ int igraphmodule_PyObject_to_fvs_algorithm_t(PyObject *o, igraph_fvs_algorithm_t int igraphmodule_PyObject_to_get_adjacency_t(PyObject *o, igraph_get_adjacency_t *result); int igraphmodule_PyObject_to_laplacian_normalization_t(PyObject *o, igraph_laplacian_normalization_t *result); int igraphmodule_PyObject_to_layout_grid_t(PyObject *o, igraph_layout_grid_t *result); +int igraphmodule_PyObject_to_lpa_variant_t(PyObject *o, igraph_lpa_variant_t *result); int igraphmodule_PyObject_to_loops_t(PyObject *o, igraph_loops_t *result); +int igraphmodule_PyObject_to_mst_algorithm_t(PyObject *o, igraph_mst_algorithm_t *result); int igraphmodule_PyObject_to_neimode_t(PyObject *o, igraph_neimode_t *result); int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, igraph_pagerank_algo_t *result); int igraphmodule_PyObject_to_edge_type_sw_t(PyObject *o, igraph_edge_type_sw_t *result); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 0d2b05fab..8c82bf79f 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2151,6 +2151,7 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, float power = 1.0f, zero_appeal = 1.0f; igraph_integer_t m = 1; igraph_vector_int_t outseq; + igraph_bool_t has_outseq = false; igraph_t *start_from = 0; igraph_barabasi_algorithm_t algo = IGRAPH_BARABASI_PSUMTREE; PyObject *m_obj = 0, *outpref = Py_False, *directed = Py_False; @@ -2176,36 +2177,36 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, CHECK_SSIZE_T_RANGE(n, "vertex count"); if (m_obj == 0) { - igraph_vector_int_init(&outseq, 0); m = 1; - } else if (m_obj != 0) { - /* let's check whether we have a constant out-degree or a list */ - if (PyLong_Check(m_obj)) { - if (igraphmodule_PyObject_to_integer_t(m_obj, &m)) { - return NULL; - } - igraph_vector_int_init(&outseq, 0); - } else if (PyList_Check(m_obj)) { - if (igraphmodule_PyObject_to_vector_int_t(m_obj, &outseq)) { - return NULL; - } - } else { - PyErr_SetString(PyExc_TypeError, "m must be an integer or a list of integers"); + } else if (PyLong_Check(m_obj)) { + if (igraphmodule_PyObject_to_integer_t(m_obj, &m)) { return NULL; } + } else if (PySequence_Check(m_obj)) { + if (igraphmodule_PyObject_to_vector_int_t(m_obj, &outseq)) { + return NULL; + } + has_outseq = true; + } else { + PyErr_SetString(PyExc_TypeError, "m must be an integer or a sequence of integers"); + return NULL; } if (igraph_barabasi_game(&g, n, power, m, - &outseq, PyObject_IsTrue(outpref), + has_outseq ? &outseq : NULL, PyObject_IsTrue(outpref), zero_appeal, PyObject_IsTrue(directed), algo, start_from)) { igraphmodule_handle_igraph_error(); - igraph_vector_int_destroy(&outseq); + if (has_outseq) { + igraph_vector_int_destroy(&outseq); + } return NULL; } - igraph_vector_int_destroy(&outseq); + if (has_outseq) { + igraph_vector_int_destroy(&outseq); + } CREATE_GRAPH_FROM_TYPE(self, g, type); @@ -2432,7 +2433,7 @@ PyObject *igraphmodule_Graph_Erdos_Renyi(PyTypeObject * type, retval = igraph_erdos_renyi_game_gnp(&g, n, p, PyObject_IsTrue(directed), PyObject_IsTrue(loops)); } else { /* GNM model */ - retval = igraph_erdos_renyi_game_gnm(&g, n, m, PyObject_IsTrue(directed), PyObject_IsTrue(loops)); + retval = igraph_erdos_renyi_game_gnm(&g, n, m, PyObject_IsTrue(directed), PyObject_IsTrue(loops), /* multiple = */ 0); } if (retval) { @@ -3518,7 +3519,8 @@ PyObject *igraphmodule_Graph_Random_Bipartite(PyTypeObject * type, } else { /* GNM model */ retval = igraph_bipartite_game_gnm( - &g, &vertex_types, n1, n2, m, PyObject_IsTrue(directed_o), neimode + &g, &vertex_types, n1, n2, m, PyObject_IsTrue(directed_o), neimode, + /* multiple = */ 0 ); } @@ -3554,6 +3556,7 @@ PyObject *igraphmodule_Graph_Recent_Degree(PyTypeObject * type, float power = 0.0f, zero_appeal = 0.0f; igraph_integer_t m = 0; igraph_vector_int_t outseq; + igraph_bool_t has_outseq = false; PyObject *m_obj, *outpref = Py_False, *directed = Py_False; char *kwlist[] = @@ -3573,24 +3576,28 @@ NULL }; if (igraphmodule_PyObject_to_integer_t(m_obj, &m)) { return NULL; } - igraph_vector_int_init(&outseq, 0); } else if (PyList_Check(m_obj)) { if (igraphmodule_PyObject_to_vector_int_t(m_obj, &outseq)) { // something bad happened during conversion return NULL; } + has_outseq = true; } - if (igraph_recent_degree_game(&g, n, power, window, m, &outseq, + if (igraph_recent_degree_game(&g, n, power, window, m, has_outseq ? &outseq : NULL, PyObject_IsTrue(outpref), zero_appeal, PyObject_IsTrue(directed))) { igraphmodule_handle_igraph_error(); - igraph_vector_int_destroy(&outseq); + if (has_outseq) { + igraph_vector_int_destroy(&outseq); + } return NULL; } - igraph_vector_int_destroy(&outseq); + if (has_outseq) { + igraph_vector_int_destroy(&outseq); + } CREATE_GRAPH_FROM_TYPE(self, g, type); @@ -4203,6 +4210,8 @@ PyObject *igraphmodule_Graph_authority_score( igraph_real_t value; igraph_vector_t res, *weights = 0; + /* scale is deprecated but kept for backward compatibility reasons */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO!O", kwlist, &weights_o, &scale_o, igraphmodule_ARPACKOptionsType, &arpack_options_o, &return_eigenvalue)) @@ -4214,7 +4223,7 @@ PyObject *igraphmodule_Graph_authority_score( ATTRIBUTE_TYPE_EDGE)) return NULL; arpack_options = (igraphmodule_ARPACKOptionsObject*)arpack_options_o; - if (igraph_hub_and_authority_scores(&self->g, NULL, &res, &value, PyObject_IsTrue(scale_o), + if (igraph_hub_and_authority_scores(&self->g, NULL, &res, &value, weights, igraphmodule_ARPACKOptions_get(arpack_options))) { igraphmodule_handle_igraph_error(); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -5471,6 +5480,8 @@ PyObject *igraphmodule_Graph_eigenvector_centrality( igraph_real_t value; igraph_vector_t *weights=0, res; + /* scale is deprecated but kept for backward compatibility reasons */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO!O", kwlist, &directed_o, &scale_o, &weights_o, igraphmodule_ARPACKOptionsType, @@ -5487,7 +5498,7 @@ PyObject *igraphmodule_Graph_eigenvector_centrality( arpack_options = (igraphmodule_ARPACKOptionsObject*)arpack_options_o; if (igraph_eigenvector_centrality(&self->g, &res, &value, - PyObject_IsTrue(directed_o), PyObject_IsTrue(scale_o), + PyObject_IsTrue(directed_o), weights, igraphmodule_ARPACKOptions_get(arpack_options))) { igraphmodule_handle_igraph_error(); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -6052,22 +6063,26 @@ PyObject *igraphmodule_Graph_get_all_simple_paths(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "v", "to", "cutoff", "mode", NULL }; - igraph_vector_int_t res; + static char *kwlist[] = { "v", "to", "minlen", "maxlen", "mode", NULL }; + igraph_vector_int_list_t res; igraph_neimode_t mode = IGRAPH_OUT; igraph_integer_t from; igraph_vs_t to; - igraph_integer_t cutoff; - PyObject *list, *from_o, *mode_o=Py_None, *to_o=Py_None, *cutoff_o=Py_None; + igraph_integer_t minlen, maxlen; + PyObject *list, *from_o, *mode_o = Py_None, *to_o = Py_None; + PyObject *minlen_o = Py_None, *maxlen_o = Py_None; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, &from_o, - &to_o, &cutoff_o, &mode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOOO", kwlist, &from_o, + &to_o, &minlen_o, &maxlen_o, &mode_o)) return NULL; if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return NULL; - if (igraphmodule_PyObject_to_integer_t(cutoff_o, &cutoff)) + if (igraphmodule_PyObject_to_integer_t(minlen_o, &minlen)) + return NULL; + + if (igraphmodule_PyObject_to_integer_t(maxlen_o, &maxlen)) return NULL; if (igraphmodule_PyObject_to_vid(from_o, &from, &self->g)) @@ -6076,24 +6091,24 @@ PyObject *igraphmodule_Graph_get_all_simple_paths(igraphmodule_GraphObject * if (igraphmodule_PyObject_to_vs_t(to_o, &to, &self->g, 0, 0)) return NULL; - if (igraph_vector_int_init(&res, 0)) { + if (igraph_vector_int_list_init(&res, 0)) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&to); return NULL; } - if (igraph_get_all_simple_paths(&self->g, &res, from, to, cutoff, mode)) { + if (igraph_get_all_simple_paths(&self->g, &res, from, to, minlen, maxlen, mode)) { igraphmodule_handle_igraph_error(); - igraph_vector_int_destroy(&res); + igraph_vector_int_list_destroy(&res); igraph_vs_destroy(&to); return NULL; } igraph_vs_destroy(&to); - list = igraphmodule_vector_int_t_to_PyList(&res); + list = igraphmodule_vector_int_list_t_to_PyList(&res); - igraph_vector_int_destroy(&res); + igraph_vector_int_list_destroy(&res); return list; } @@ -6114,6 +6129,8 @@ PyObject *igraphmodule_Graph_hub_score( igraph_real_t value; igraph_vector_t res, *weights = 0; + /* scale is deprecated but kept for backward compatibility reasons */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO!O", kwlist, &weights_o, &scale_o, igraphmodule_ARPACKOptionsType, &arpack_options, &return_eigenvalue)) @@ -6125,7 +6142,7 @@ PyObject *igraphmodule_Graph_hub_score( ATTRIBUTE_TYPE_EDGE)) return NULL; arpack_options = (igraphmodule_ARPACKOptionsObject*)arpack_options_o; - if (igraph_hub_and_authority_scores(&self->g, &res, NULL, &value, PyObject_IsTrue(scale_o), + if (igraph_hub_and_authority_scores(&self->g, &res, NULL, &value, weights, igraphmodule_ARPACKOptions_get(arpack_options))) { igraphmodule_handle_igraph_error(); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -6692,11 +6709,6 @@ PyObject *igraphmodule_Graph_distances( ); } - if (algorithm == IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_JOHNSON && mode != IGRAPH_OUT) { - PyErr_SetString(PyExc_ValueError, "Johnson's algorithm is supported for mode=\"out\" only"); - goto cleanup; - } - /* Call the C function */ switch (algorithm) { case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_DIJKSTRA: @@ -6708,7 +6720,7 @@ PyObject *igraphmodule_Graph_distances( break; case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_JOHNSON: - retval = igraph_distances_johnson(&self->g, &res, from_vs, to_vs, weights); + retval = igraph_distances_johnson(&self->g, &res, from_vs, to_vs, weights, mode); break; default: @@ -6765,25 +6777,35 @@ PyObject *igraphmodule_Graph_similarity_jaccard(igraphmodule_GraphObject * self, if (pairs_o == Py_None) { /* Case #1: vertices, returning matrix */ igraph_matrix_t res; - igraph_vs_t vs; + igraph_vs_t vs_from; + igraph_vs_t vs_to; igraph_bool_t return_single = false; - if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs, &self->g, &return_single, 0)) + if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs_from, &self->g, &return_single, 0)) return NULL; + /* TODO(ntamas): support separate vs_from and vs_to arguments */ + if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs_to, &self->g, &return_single, 0)) { + igraph_vs_destroy(&vs_from); + return NULL; + } + if (igraph_matrix_init(&res, 0, 0)) { - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); return igraphmodule_handle_igraph_error(); } - if (igraph_similarity_jaccard(&self->g, &res, vs, mode, PyObject_IsTrue(loops))) { + if (igraph_similarity_jaccard(&self->g, &res, vs_from, vs_to, mode, PyObject_IsTrue(loops))) { igraph_matrix_destroy(&res); - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); igraphmodule_handle_igraph_error(); return NULL; } - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); list = igraphmodule_matrix_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&res); @@ -6851,25 +6873,35 @@ PyObject *igraphmodule_Graph_similarity_dice(igraphmodule_GraphObject * self, if (pairs_o == Py_None) { /* Case #1: vertices, returning matrix */ igraph_matrix_t res; - igraph_vs_t vs; + igraph_vs_t vs_from; + igraph_vs_t vs_to; igraph_bool_t return_single = false; - if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs, &self->g, &return_single, 0)) + if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs_from, &self->g, &return_single, 0)) return NULL; + /* TODO(ntamas): support separate vs_from and vs_to arguments */ + if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs_to, &self->g, &return_single, 0)) { + igraph_vs_destroy(&vs_from); + return NULL; + } + if (igraph_matrix_init(&res, 0, 0)) { - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); return igraphmodule_handle_igraph_error(); } - if (igraph_similarity_dice(&self->g, &res, vs, mode, PyObject_IsTrue(loops))) { + if (igraph_similarity_dice(&self->g, &res, vs_from, vs_to, mode, PyObject_IsTrue(loops))) { igraph_matrix_destroy(&res); - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); igraphmodule_handle_igraph_error(); return NULL; } - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); list = igraphmodule_matrix_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&res); @@ -6962,12 +6994,16 @@ PyObject *igraphmodule_Graph_similarity_inverse_log_weighted( PyObject *igraphmodule_Graph_spanning_tree(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "weights", NULL }; + static char *kwlist[] = { "weights", "method", NULL }; igraph_vector_t* ws = 0; igraph_vector_int_t res; - PyObject *weights_o = Py_None, *result_o = NULL; + igraph_mst_algorithm_t method = IGRAPH_MST_AUTOMATIC; + PyObject *weights_o = Py_None, *result_o = NULL, *method_o = NULL; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &weights_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &weights_o, &method_o)) + return NULL; + + if (igraphmodule_PyObject_to_mst_algorithm_t(method_o, &method)) return NULL; if (igraph_vector_int_init(&res, 0)) { @@ -6980,14 +7016,19 @@ PyObject *igraphmodule_Graph_spanning_tree(igraphmodule_GraphObject * self, return NULL; } - if (igraph_minimum_spanning_tree(&self->g, &res, ws)) { - if (ws != 0) { igraph_vector_destroy(ws); free(ws); } + if (igraph_minimum_spanning_tree(&self->g, &res, ws, method)) { + if (ws != 0) { + igraph_vector_destroy(ws); free(ws); + } igraph_vector_int_destroy(&res); igraphmodule_handle_igraph_error(); return NULL; } - if (ws != 0) { igraph_vector_destroy(ws); free(ws); } + if (ws != 0) { + igraph_vector_destroy(ws); free(ws); + } + result_o = igraphmodule_vector_int_t_to_PyList(&res); igraph_vector_int_destroy(&res); return result_o; @@ -9184,13 +9225,14 @@ PyObject *igraphmodule_Graph_get_adjacency(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_get_biadjacency(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "types", NULL }; + static char *kwlist[] = { "types", "weights", NULL }; igraph_matrix_t matrix; igraph_vector_int_t row_ids, col_ids; igraph_vector_bool_t *types; - PyObject *matrix_o, *row_ids_o, *col_ids_o, *types_o; + igraph_vector_t *weights = 0; + PyObject *matrix_o, *row_ids_o, *col_ids_o, *types_o, *weights_o = Py_None; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &types_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &types_o)) return NULL; if (igraph_vector_int_init(&row_ids, 0)) @@ -9207,24 +9249,34 @@ PyObject *igraphmodule_Graph_get_biadjacency(igraphmodule_GraphObject * self, return NULL; } + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + igraph_vector_int_destroy(&row_ids); + igraph_vector_int_destroy(&col_ids); + if (types) { igraph_vector_bool_destroy(types); free(types); } + return NULL; + } + if (igraph_matrix_init(&matrix, 1, 1)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&row_ids); igraph_vector_int_destroy(&col_ids); if (types) { igraph_vector_bool_destroy(types); free(types); } + if (weights) { igraph_vector_destroy(weights); free(weights); } return NULL; } - if (igraph_get_biadjacency(&self->g, types, &matrix, &row_ids, &col_ids)) { + if (igraph_get_biadjacency(&self->g, types, weights, &matrix, &row_ids, &col_ids)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&row_ids); igraph_vector_int_destroy(&col_ids); if (types) { igraph_vector_bool_destroy(types); free(types); } + if (weights) { igraph_vector_destroy(weights); free(weights); } igraph_matrix_destroy(&matrix); return NULL; } if (types) { igraph_vector_bool_destroy(types); free(types); } + if (weights) { igraph_vector_destroy(weights); free(weights); } matrix_o = igraphmodule_matrix_t_to_PyList(&matrix, IGRAPHMODULE_TYPE_INT); igraph_matrix_destroy(&matrix); @@ -10130,7 +10182,7 @@ PyObject *igraphmodule_Graph_automorphism_group( if (igraphmodule_attrib_to_vector_int_t(color_o, self, &color, ATTRIBUTE_TYPE_VERTEX)) return NULL; - retval = igraph_automorphism_group(&self->g, color, &generators, sh, 0); + retval = igraph_automorphism_group_bliss(&self->g, color, &generators, sh, 0); if (color) { igraph_vector_int_destroy(color); free(color); } @@ -10176,7 +10228,7 @@ PyObject *igraphmodule_Graph_canonical_permutation( if (igraphmodule_attrib_to_vector_int_t(color_o, self, &color, ATTRIBUTE_TYPE_VERTEX)) return NULL; - retval = igraph_canonical_permutation(&self->g, color, &labeling, sh, 0); + retval = igraph_canonical_permutation_bliss(&self->g, color, &labeling, sh, 0); if (color) { igraph_vector_int_destroy(color); free(color); } @@ -10218,7 +10270,7 @@ PyObject *igraphmodule_Graph_count_automorphisms( if (igraphmodule_attrib_to_vector_int_t(color_o, self, &color, ATTRIBUTE_TYPE_VERTEX)) return NULL; - retval = igraph_count_automorphisms(&self->g, color, sh, &info); + retval = igraph_count_automorphisms_bliss(&self->g, color, sh, &info); if (color) { igraph_vector_int_destroy(color); free(color); } @@ -11170,18 +11222,18 @@ PyObject *igraphmodule_Graph_subisomorphic_lad(igraphmodule_GraphObject * self, { igraph_bool_t res = false; PyObject *o, *return_mapping=Py_False, *domains_o=Py_None, *induced=Py_False; - float time_limit = 0; igraphmodule_GraphObject *other; igraph_vector_int_list_t domains; igraph_vector_int_list_t* p_domains = 0; igraph_vector_int_t mapping, *map=0; - static char *kwlist[] = { "pattern", "domains", "induced", "time_limit", - "return_mapping", NULL }; + static char *kwlist[] = { + "pattern", "domains", "induced", "return_mapping", NULL + }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OOfO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OOO", kwlist, igraphmodule_GraphType, &o, &domains_o, &induced, - &time_limit, &return_mapping)) + &return_mapping)) return NULL; other=(igraphmodule_GraphObject*)o; @@ -11195,8 +11247,9 @@ PyObject *igraphmodule_Graph_subisomorphic_lad(igraphmodule_GraphObject * self, if (PyObject_IsTrue(return_mapping)) { if (igraph_vector_int_init(&mapping, 0)) { - if (p_domains) + if (p_domains) { igraph_vector_int_list_destroy(p_domains); + } igraphmodule_handle_igraph_error(); return NULL; } @@ -11204,9 +11257,10 @@ PyObject *igraphmodule_Graph_subisomorphic_lad(igraphmodule_GraphObject * self, } if (igraph_subisomorphic_lad(&other->g, &self->g, p_domains, &res, - map, 0, PyObject_IsTrue(induced), (igraph_integer_t) time_limit)) { - if (p_domains) + map, 0, PyObject_IsTrue(induced))) { + if (p_domains) { igraph_vector_int_list_destroy(p_domains); + } igraphmodule_handle_igraph_error(); return NULL; } @@ -11237,16 +11291,15 @@ PyObject *igraphmodule_Graph_get_subisomorphisms_lad( igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { PyObject *o, *domains_o=Py_None, *induced=Py_False, *result_o; - float time_limit = 0; igraphmodule_GraphObject *other; igraph_vector_int_list_t domains; igraph_vector_int_list_t* p_domains = 0; igraph_vector_int_list_t mappings; - static char *kwlist[] = { "pattern", "domains", "induced", "time_limit", NULL }; + static char *kwlist[] = { "pattern", "domains", "induced", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OOf", kwlist, - igraphmodule_GraphType, &o, &domains_o, &induced, &time_limit)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OO", kwlist, + igraphmodule_GraphType, &o, &domains_o, &induced)) return NULL; other=(igraphmodule_GraphObject*)o; @@ -11265,8 +11318,9 @@ PyObject *igraphmodule_Graph_get_subisomorphisms_lad( return NULL; } - if (igraph_subisomorphic_lad(&other->g, &self->g, p_domains, 0, 0, &mappings, - PyObject_IsTrue(induced), (igraph_integer_t) time_limit)) { + if (igraph_subisomorphic_lad( + &other->g, &self->g, p_domains, 0, 0, &mappings, PyObject_IsTrue(induced) + )) { igraphmodule_handle_igraph_error(); igraph_vector_int_list_destroy(&mappings); if (p_domains) @@ -13025,7 +13079,8 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject /* modularity = */ weights ? 0 : &q, /* membership = */ 0, PyObject_IsTrue(directed), - weights)) { + weights, + /* lengths = */ 0)) { igraphmodule_handle_igraph_error(); if (weights != 0) { igraph_vector_destroy(weights); free(weights); @@ -13278,17 +13333,23 @@ PyObject *igraphmodule_Graph_community_infomap(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_community_label_propagation( igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = { "weights", "initial", "fixed", NULL }; + static char *kwlist[] = { "weights", "initial", "fixed", "variant", NULL }; PyObject *weights_o = Py_None, *initial_o = Py_None, *fixed_o = Py_None; + PyObject *variant_o = NULL; PyObject *result_o; igraph_vector_int_t membership, *initial = 0; igraph_vector_t *ws = 0; igraph_vector_bool_t fixed; + igraph_lpa_variant_t variant = IGRAPH_LPA_DOMINANCE; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &weights_o, &initial_o, &fixed_o)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, &weights_o, &initial_o, &fixed_o, &variant_o)) { return NULL; } + if (igraphmodule_PyObject_to_lpa_variant_t(variant_o, &variant)) { + return NULL; + } + if (fixed_o != Py_None) { if (igraphmodule_PyObject_to_vector_bool_t(fixed_o, &fixed)) return NULL; @@ -13307,7 +13368,7 @@ PyObject *igraphmodule_Graph_community_label_propagation( igraph_vector_int_init(&membership, igraph_vcount(&self->g)); if (igraph_community_label_propagation(&self->g, &membership, - IGRAPH_OUT, ws, initial, (fixed_o != Py_None ? &fixed : 0))) { + IGRAPH_OUT, ws, initial, (fixed_o != Py_None ? &fixed : 0), variant)) { if (fixed_o != Py_None) igraph_vector_bool_destroy(&fixed); if (ws) { igraph_vector_destroy(ws); free(ws); } if (initial) { igraph_vector_int_destroy(initial); free(initial); } @@ -13392,15 +13453,16 @@ PyObject *igraphmodule_Graph_community_multilevel(igraphmodule_GraphObject *self */ PyObject *igraphmodule_Graph_community_optimal_modularity( igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"weights", NULL}; + static char *kwlist[] = {"weights", "resolution", NULL}; PyObject *weights_o = Py_None; igraph_real_t modularity; igraph_vector_int_t membership; igraph_vector_t* weights = 0; + double resolution = 1; PyObject *res; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, - &weights_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Od", kwlist, + &weights_o, &resolution)) return NULL; if (igraph_vector_int_init(&membership, igraph_vcount(&self->g))) { @@ -13414,8 +13476,8 @@ PyObject *igraphmodule_Graph_community_optimal_modularity( return NULL; } - if (igraph_community_optimal_modularity(&self->g, &modularity, - &membership, weights)) { + if (igraph_community_optimal_modularity(&self->g, weights, resolution, &modularity, + &membership)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&membership); if (weights != 0) { @@ -15096,8 +15158,6 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Calculates Kleinberg's authority score for the vertices of the graph\n\n" "@param weights: edge weights to be used. Can be a sequence or iterable or\n" " even an edge attribute name.\n" - "@param scale: whether to normalize the scores so that the largest one\n" - " is 1.\n" "@param arpack_options: an L{ARPACKOptions} object used to fine-tune\n" " the ARPACK eigenvector calculation. If omitted, the module-level\n" " variable called C{arpack_options} is used.\n" @@ -15515,8 +15575,6 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "components, and the eigenvector centrality calculated for each separately.\n\n" "@param directed: whether to consider edge directions in a directed\n" " graph. Ignored for undirected graphs.\n" - "@param scale: whether to normalize the centralities so the largest\n" - " one will always be 1.\n" "@param weights: edge weights given as a list or an edge attribute. If\n" " C{None}, all edges have equal weight.\n" "@param return_eigenvalue: whether to return the actual largest\n" @@ -15747,8 +15805,6 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Calculates Kleinberg's hub score for the vertices of the graph\n\n" "@param weights: edge weights to be used. Can be a sequence or iterable or\n" " even an edge attribute name.\n" - "@param scale: whether to normalize the scores so that the largest one\n" - " is 1.\n" "@param arpack_options: an L{ARPACKOptions} object used to fine-tune\n" " the ARPACK eigenvector calculation. If omitted, the module-level\n" " variable called C{arpack_options} is used.\n" @@ -16100,7 +16156,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "probability, given in the first argument.\n\n" "Please note that the rewiring is done \"in-place\", so the original\n" "graph will be modified. If you want to preserve the original graph,\n" - "use the L{copy} method before.\n\n" + "use the L{cop y} method before.\n\n" "@param prob: rewiring probability\n" "@param loops: whether the algorithm is allowed to create loop edges\n" "@param multiple: whether the algorithm is allowed to create multiple\n" @@ -16189,7 +16245,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_minimum_spanning_tree */ {"_spanning_tree", (PyCFunction) igraphmodule_Graph_spanning_tree, METH_VARARGS | METH_KEYWORDS, - "_spanning_tree(weights=None)\n--\n\n" + "_spanning_tree(weights=None, method=\"auto\")\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.spanning_tree()"}, @@ -18370,7 +18426,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"community_label_propagation", (PyCFunction) igraphmodule_Graph_community_label_propagation, METH_VARARGS | METH_KEYWORDS, - "community_label_propagation(weights=None, initial=None, fixed=None)\n--\n\n" + "community_label_propagation(weights=None, initial=None, fixed=None, variant=\"dominance\")\n--\n\n" "Finds the community structure of the graph according to the label\n" "propagation method of Raghavan et al.\n\n" "Initially, each vertex is assigned a different label. After that,\n" @@ -18400,6 +18456,9 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " It only makes sense if initial labels are also given. Unlabeled\n" " vertices cannot be fixed. Note that vertex attribute names are not\n" " accepted here.\n" + "@param variant: the variant of the algorithm to use: C{\"dominance\"}, \n" + " C{\"retention\"} or C{\"fast\"}. See the documentation of the C core\n" + " of igraph for details.\n" "@return: the resulting membership vector\n" }, {"community_leading_eigenvector", (PyCFunction) igraphmodule_Graph_community_leading_eigenvector, diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 97864f61e..f7ca0490a 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -142,12 +142,8 @@ static int igraphmodule_clear(PyObject *m) { return 0; } -static igraph_error_t igraphmodule_igraph_interrupt_hook(void* data) { - if (PyErr_CheckSignals()) { - IGRAPH_FINALLY_FREE(); - return IGRAPH_INTERRUPTED; - } - return IGRAPH_SUCCESS; +static igraph_bool_t igraphmodule_igraph_interrupt_hook() { + return PyErr_CheckSignals(); } igraph_error_t igraphmodule_igraph_progress_hook(const char* message, igraph_real_t percent, diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 0b1690849..b7f920cac 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -687,7 +687,7 @@ def es(self): ########################### # Paths/traversals - def get_all_simple_paths(self, v, to=None, cutoff=-1, mode="out"): + def get_all_simple_paths(self, v, to=None, minlen=0, maxlen=-1, mode="out"): """Calculates all the simple paths from a given node to some other nodes (or all of them) in a graph. @@ -703,7 +703,8 @@ def get_all_simple_paths(self, v, to=None, cutoff=-1, mode="out"): paths. This can be a single vertex ID, a list of vertex IDs, a single vertex name, a list of vertex names or a L{VertexSeq} object. C{None} means all the vertices. - @param cutoff: maximum length of path that is considered. If negative, + @param minlen: minimum length of path that is considered. + @param maxlen: maximum length of path that is considered. If negative, paths of all lengths are considered. @param mode: the directionality of the paths. C{\"in\"} means to calculate incoming paths, C{\"out\"} means to calculate outgoing paths, C{\"all\"} means @@ -712,14 +713,7 @@ def get_all_simple_paths(self, v, to=None, cutoff=-1, mode="out"): reachable node in the graph in a list. Note that in case of mode=C{\"in\"}, the vertices in a path are returned in reversed order! """ - paths = self._get_all_simple_paths(v, to, cutoff, mode) - prev = 0 - result = [] - for index, item in enumerate(paths): - if item < 0: - result.append(paths[prev:index]) - prev = index + 1 - return result + return self._get_all_simple_paths(v, to, minlen, maxlen, mode) def path_length_hist(self, directed=True): """Returns the path length histogram of the graph @@ -782,7 +776,7 @@ def dfs(self, vid, mode=OUT): return (vids, parents) - def spanning_tree(self, weights=None, return_tree=True): + def spanning_tree(self, weights=None, return_tree=True, method="auto"): """Calculates a minimum spanning tree for a graph. B{Reference}: Prim, R.C. Shortest connection networks and some @@ -795,11 +789,16 @@ def spanning_tree(self, weights=None, return_tree=True): the minimum spanning tree instead (when C{return_tree} is C{False}). The default is C{True} for historical reasons as this argument was introduced in igraph 0.6. + @param method: the algorithm to use. C{"auto"} means that the algorithm + is selected automatically. C{"prim"} means that Prim's algorithm is + used. C{"kruskal"} means that Kruskal's algorithm is used. + C{"unweighted"} assumes that the graph is unweighted even if weights + are provided. @return: the spanning tree as a L{Graph} object if C{return_tree} is C{True} or the IDs of the edges that constitute the spanning tree if C{return_tree} is C{False}. """ - result = GraphBase._spanning_tree(self, weights) + result = GraphBase._spanning_tree(self, weights, method) if return_tree: return self.subgraph_edges(result, delete_vertices=False) return result diff --git a/tests/test_atlas.py b/tests/test_atlas.py index 1f36f5f44..49f49531e 100644 --- a/tests/test_atlas.py +++ b/tests/test_atlas.py @@ -100,7 +100,14 @@ def testEigenvectorCentrality(self): def testHubScore(self): for idx, g in enumerate(self.__class__.graphs): try: - sc = g.hub_score() + if g.ecount() == 0: + with self.assertWarns(RuntimeWarning, msg="The graph has no edges"): + sc = g.hub_score() + elif not g.is_directed(): + with self.assertWarns(RuntimeWarning, msg="Hub and authority scores requested for undirected graph"): + sc = g.hub_score() + else: + sc = g.hub_score() except Exception as ex: self.assertTrue( False, @@ -126,7 +133,14 @@ def testHubScore(self): def testAuthorityScore(self): for idx, g in enumerate(self.__class__.graphs): try: - sc = g.authority_score() + if g.ecount() == 0: + with self.assertWarns(RuntimeWarning, msg="The graph has no edges"): + sc = g.authority_score() + elif not g.is_directed(): + with self.assertWarns(RuntimeWarning, msg="Hub and authority scores requested for undirected graph"): + sc = g.authority_score() + else: + sc = g.authority_score() except Exception as ex: self.assertTrue( False, diff --git a/tests/test_bipartite.py b/tests/test_bipartite.py index f3c5cc8c7..932838655 100644 --- a/tests/test_bipartite.py +++ b/tests/test_bipartite.py @@ -76,8 +76,8 @@ def testBiadjacency(self): ) ) self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) - self.assertListEqual(g.es["weight"], [1, 1, 1, 2]) - self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3)]) + self.assertListEqual(g.get_edgelist(), [(1, 2), (0, 3), (1, 3), (0, 4)]) + self.assertListEqual(g.es["weight"], [1, 1, 2, 1]) # Graph is not weighted when weighted=`str` g = Graph.Biadjacency([[0, 1, 1], [1, 2, 0]], weighted="some_attr_name") @@ -92,8 +92,8 @@ def testBiadjacency(self): ) ) self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) - self.assertListEqual(g.es["some_attr_name"], [1, 1, 1, 2]) - self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3)]) + self.assertListEqual(g.get_edgelist(), [(1, 2), (0, 3), (1, 3), (0, 4)]) + self.assertListEqual(g.es["some_attr_name"], [1, 1, 2, 1]) # Graph is not weighted when weighted="" g = Graph.Biadjacency([[0, 1, 1], [1, 2, 0]], weighted="") @@ -108,8 +108,8 @@ def testBiadjacency(self): ) ) self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) - self.assertListEqual(g.es[""], [1, 1, 1, 2]) - self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3)]) + self.assertListEqual(g.get_edgelist(), [(1, 2), (0, 3), (1, 3), (0, 4)]) + self.assertListEqual(g.es[""], [1, 1, 2, 1]) # Should work when directed=True and mode=out with weighted g = Graph.Biadjacency([[0, 1, 1], [1, 2, 0]], directed=True, weighted=True) @@ -117,8 +117,8 @@ def testBiadjacency(self): all((g.vcount() == 5, g.ecount() == 4, g.is_directed(), g.is_weighted())) ) self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) - self.assertListEqual(g.es["weight"], [1, 1, 1, 2]) - self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3)]) + self.assertListEqual(g.get_edgelist(), [(1, 2), (0, 3), (1, 3), (0, 4)]) + self.assertListEqual(g.es["weight"], [1, 1, 2, 1]) # Should work when directed=True and mode=in with weighted g = Graph.Biadjacency( @@ -128,8 +128,8 @@ def testBiadjacency(self): all((g.vcount() == 5, g.ecount() == 4, g.is_directed(), g.is_weighted())) ) self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) - self.assertListEqual(g.es["weight"], [1, 1, 1, 2]) - self.assertListEqual(sorted(g.get_edgelist()), [(2, 1), (3, 0), (3, 1), (4, 0)]) + self.assertListEqual(g.get_edgelist(), [(2, 1), (3, 0), (3, 1), (4, 0)]) + self.assertListEqual(g.es["weight"], [1, 1, 2, 1]) # Should work when directed=True and mode=all with weighted g = Graph.Biadjacency( @@ -139,11 +139,11 @@ def testBiadjacency(self): all((g.vcount() == 5, g.ecount() == 8, g.is_directed(), g.is_weighted())) ) self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) - self.assertListEqual(g.es["weight"], [1, 1, 1, 1, 1, 1, 2, 2]) self.assertListEqual( - sorted(g.get_edgelist()), - [(0, 3), (0, 4), (1, 2), (1, 3), (2, 1), (3, 0), (3, 1), (4, 0)], + g.get_edgelist(), + [(1, 2), (2, 1), (0, 3), (3, 0), (1, 3), (3, 1), (0, 4), (4, 0)] ) + self.assertListEqual(g.es["weight"], [1, 1, 1, 1, 2, 2, 1, 1]) def testBiadjacencyError(self): msg = "arguments weighted and multiple can not co-exist" diff --git a/tests/test_foreign.py b/tests/test_foreign.py index 72047dc1a..83664e5f5 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -215,14 +215,14 @@ def _testNCOLOrLGL(self, func, fname, can_be_reopened=True): self.assertTrue( "name" not in g.vertex_attributes() and "weight" in g.edge_attributes() ) - self.assertTrue(g.es["weight"] == [1, 2, 0, 3, 0]) + self.assertListEqual(g.es["weight"], [1.0, 2.0, 1.0, 3.0, 1.0]) g = func(fname, directed=False) self.assertTrue( "name" in g.vertex_attributes() and "weight" in g.edge_attributes() ) - self.assertTrue(g.vs["name"] == ["eggs", "spam", "ham", "bacon"]) - self.assertTrue(g.es["weight"] == [1, 2, 0, 3, 0]) + self.assertListEqual(g.vs["name"], ["eggs", "spam", "ham", "bacon"]) + self.assertListEqual(g.es["weight"], [1.0, 2.0, 1.0, 3.0, 1.0]) def testNCOL(self): with temporary_file( diff --git a/tests/test_generators.py b/tests/test_generators.py index 30cc58d23..3925eb7bd 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -481,33 +481,33 @@ def testAdjacencyNumPy(self): # ADJ_DIRECTED (default) g = Graph.Adjacency(mat) el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (2, 2), (3, 1)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (1, 0), (2, 2), (2, 2), (3, 1)]) # ADJ MIN g = Graph.Adjacency(mat, mode="min") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (2, 2), (2, 2)]) + self.assertListEqual(sorted(el), [(0, 1), (2, 2), (2, 2)]) # ADJ MAX g = Graph.Adjacency(mat, mode="max") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (1, 3), (2, 2), (2, 2)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (1, 3), (2, 2), (2, 2)]) # ADJ LOWER g = Graph.Adjacency(mat, mode="lower") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (2, 2), (2, 2), (1, 3)]) + self.assertListEqual(sorted(el), [(0, 1), (1, 3), (2, 2), (2, 2)]) # ADJ UPPER g = Graph.Adjacency(mat, mode="upper") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (2, 2), (2, 2)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (2, 2), (2, 2)]) @unittest.skipIf(np is None, "test case depends on NumPy") def testAdjacencyNumPyLoopHandling(self): @@ -518,64 +518,64 @@ def testAdjacencyNumPyLoopHandling(self): # ADJ_DIRECTED (default) g = Graph.Adjacency(mat) el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (2, 2), (3, 1)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (1, 0), (2, 2), (2, 2), (3, 1)]) # ADJ MIN g = Graph.Adjacency(mat, mode="min", loops="twice") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (2, 2)]) + self.assertListEqual(sorted(el), [(0, 1), (2, 2)]) # ADJ MAX g = Graph.Adjacency(mat, mode="max", loops="twice") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (1, 3), (2, 2)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (1, 3), (2, 2)]) # ADJ LOWER g = Graph.Adjacency(mat, mode="lower", loops="twice") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (2, 2), (1, 3)]) + self.assertListEqual(sorted(el), [(0, 1), (1, 3), (2, 2)]) # ADJ UPPER g = Graph.Adjacency(mat, mode="upper", loops="twice") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (2, 2)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (2, 2)]) # ADJ_DIRECTED (default) g = Graph.Adjacency(mat, loops=False) el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (3, 1)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (1, 0), (3, 1)]) # ADJ MIN g = Graph.Adjacency(mat, mode="min", loops=False) el = g.get_edgelist() - self.assertTrue(el == [(0, 1)]) + self.assertListEqual(sorted(el), [(0, 1)]) # ADJ MAX g = Graph.Adjacency(mat, mode="max", loops=False) el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (1, 3)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (1, 3)]) # ADJ LOWER g = Graph.Adjacency(mat, mode="lower", loops=False) el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (1, 3)]) + self.assertListEqual(sorted(el), [(0, 1), (1, 3)]) # ADJ UPPER g = Graph.Adjacency(mat, mode="upper", loops=False) el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2)]) @unittest.skipIf( (sparse is None) or (np is None), "test case depends on NumPy/SciPy" @@ -689,23 +689,23 @@ def testWeightedAdjacency(self): g = Graph.Weighted_Adjacency(mat, attr="w0") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 2.5, 1]) + self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2), (2, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 2.5]) g = Graph.Weighted_Adjacency(mat, mode="plus") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 3), (2, 2)]) - self.assertTrue(g.es["weight"] == [3, 2, 1, 2.5]) + self.assertListEqual(el, [(0, 1), (0, 2), (1, 3), (2, 2)]) + self.assertListEqual(g.es["weight"], [3, 2, 1, 2.5]) g = Graph.Weighted_Adjacency(mat, attr="w0", loops=False) el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 1]) + self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2]) g = Graph.Weighted_Adjacency(mat, attr="w0", loops="twice") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 1.25, 1]) + self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2), (2, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 1.25]) @unittest.skipIf(np is None, "test case depends on NumPy") def testWeightedAdjacencyNumPy(self): @@ -715,23 +715,23 @@ def testWeightedAdjacencyNumPy(self): g = Graph.Weighted_Adjacency(mat, attr="w0") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 2.5, 1]) + self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2), (2, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 2.5]) g = Graph.Weighted_Adjacency(mat, mode="plus") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 3), (2, 2)]) - self.assertTrue(g.es["weight"] == [3, 2, 1, 2.5]) + self.assertListEqual(el, [(0, 1), (0, 2), (1, 3), (2, 2)]) + self.assertListEqual(g.es["weight"], [3, 2, 1, 2.5]) g = Graph.Weighted_Adjacency(mat, attr="w0", loops=False) el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 1]) + self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2]) g = Graph.Weighted_Adjacency(mat, attr="w0", loops="twice") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 1.25, 1]) + self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2), (2, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 1.25]) @unittest.skipIf( (sparse is None) or (np is None), "test case depends on NumPy/SciPy" @@ -745,36 +745,36 @@ def testSparseWeightedAdjacency(self): el = g.get_edgelist() self.assertTrue(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 2.5, 1]) + self.assertListEqual(el, [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) + self.assertListEqual(g.es["w0"], [1, 2, 2, 2.5, 1]) g = Graph.Weighted_Adjacency(mat, mode="plus") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (2, 2), (1, 3)]) - self.assertTrue(g.es["weight"] == [3, 2, 2.5, 1]) + self.assertListEqual(el, [(0, 1), (0, 2), (2, 2), (1, 3)]) + self.assertListEqual(g.es["weight"], [3, 2, 2.5, 1]) g = Graph.Weighted_Adjacency(mat, mode="min") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (2, 2)]) - self.assertTrue(g.es["weight"] == [1, 2.5]) + self.assertListEqual(el, [(0, 1), (2, 2)]) + self.assertListEqual(g.es["weight"], [1, 2.5]) g = Graph.Weighted_Adjacency(mat, attr="w0", loops=False) el = g.get_edgelist() self.assertTrue(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 1]) + self.assertListEqual(el, [(0, 1), (0, 2), (1, 0), (3, 1)]) + self.assertListEqual(g.es["w0"], [1, 2, 2, 1]) g = Graph.Weighted_Adjacency(mat, attr="w0", loops="twice") el = g.get_edgelist() self.assertTrue(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) - self.assertTrue(g.es["w0"] == [1, 2, 2, 1.25, 1]) + self.assertListEqual(el, [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) + self.assertListEqual(g.es["w0"], [1, 2, 2, 1.25, 1]) @unittest.skipIf((np is None) or (pd is None), "test case depends on NumPy/Pandas") def testDataFrame(self): @@ -788,9 +788,9 @@ def testDataFrame(self): [["A", "blue"], ["B", "yellow"], ["C", "blue"]], columns=[0, "color"] ) g = Graph.DataFrame(edges, directed=True, vertices=vertices, use_vids=False) - self.assertTrue(g.vs["name"] == ["A", "B", "C"]) - self.assertTrue(g.vs["color"] == ["blue", "yellow", "blue"]) - self.assertTrue(g.es["weight"] == [0.4, 0.1]) + self.assertListEqual(g.vs["name"], ["A", "B", "C"]) + self.assertListEqual(g.vs["color"], ["blue", "yellow", "blue"]) + self.assertListEqual(g.es["weight"], [0.4, 0.1]) # Issue #347 edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) @@ -798,8 +798,8 @@ def testDataFrame(self): {"node": [1, 2, 3, 4, 5, 6], "label": ["1", "2", "3", "4", "5", "6"]} )[["node", "label"]] g = Graph.DataFrame(edges, directed=True, vertices=vertices, use_vids=False) - self.assertTrue(g.vs["name"] == [1, 2, 3, 4, 5, 6]) - self.assertTrue(g.vs["label"] == ["1", "2", "3", "4", "5", "6"]) + self.assertListEqual(g.vs["name"], [1, 2, 3, 4, 5, 6]) + self.assertListEqual(g.vs["label"], ["1", "2", "3", "4", "5", "6"]) # Vertex names edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) diff --git a/tests/test_structural.py b/tests/test_structural.py index b9d90a7dd..3d310dc53 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -824,14 +824,6 @@ def testDistances(self): g.distances(weights="weight", target=[2, 3], algorithm="johnson") == [row[2:4] for row in expected] ) - self.assertRaises( - ValueError, - g.distances, - weights="weight", - target=[2, 3], - algorithm="johnson", - mode="in", - ) def testGetShortestPath(self): g = Graph(4, [(0, 1), (0, 2), (1, 3), (3, 2), (2, 1)], directed=True) diff --git a/vendor/source/igraph b/vendor/source/igraph index fc6860b31..e101e1597 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit fc6860b31887b362e8ddbb07e44e6a12e9cef82d +Subproject commit e101e15975b6b07eaae33390005a5637d23debce From e6479b182fa4572d84410018fa9f46488bda3e06 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 11 May 2025 11:16:07 +0200 Subject: [PATCH 204/276] ci: do not invoke setup.py install --- .github/workflows/build.yml | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 754f5fba1..a37b152ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -126,8 +126,7 @@ jobs: - name: Install OS dependencies if: steps.cache-c-core.outputs.cache-hit != 'true' || steps.cache-c-deps.outputs.cache-hit != 'true' # Only needed when building the C core or libomp - run: - brew install ninja autoconf automake libtool + run: brew install ninja autoconf automake libtool - name: Install OpenMP library if: steps.cache-c-deps.outputs.cache-hit != 'true' @@ -167,16 +166,15 @@ jobs: - uses: actions/setup-python@v5 name: Install Python with: - python-version: '3.12.1' + python-version: "3.12.1" - name: Install OS dependencies - run: - sudo apt install ninja-build cmake flex bison + run: sudo apt install ninja-build cmake flex bison - uses: mymindstorm/setup-emsdk@v14 with: - version: '3.1.58' - actions-cache-folder: 'emsdk-cache' + version: "3.1.58" + actions-cache-folder: "emsdk-cache" - name: Build wheel run: | @@ -253,7 +251,7 @@ jobs: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" CIBW_ENABLE: pypy - CIBW_TEST_COMMAND: "cd /d {project} && pip install --prefer-binary \".[${{ matrix.test_extra }}]\" && python -m pytest tests" + CIBW_TEST_COMMAND: 'cd /d {project} && pip install --prefer-binary ".[${{ matrix.test_extra }}]" && python -m pytest tests' # Skip tests for Python 3.10 onwards because SciPy does not have # 32-bit wheels for Windows any more CIBW_TEST_SKIP: "cp310-win32 cp311-win32 cp312-win32 cp313-win32" @@ -287,23 +285,22 @@ jobs: - name: Install OS dependencies if: steps.cache-c-core.outputs.cache-hit != 'true' # Only needed when building the C core - run: - sudo apt install ninja-build cmake flex bison + run: sudo apt install ninja-build cmake flex bison - uses: actions/setup-python@v5 name: Install Python with: - python-version: '3.9' + python-version: "3.9" - name: Build sdist run: | python setup.py build_c_core python setup.py sdist - python setup.py install + pip install . - name: Test run: | - pip install --prefer-binary cairocffi numpy scipy pandas networkx pytest pytest-timeout + pip install '.[test]' python -m pytest -v tests - uses: actions/upload-artifact@v4 @@ -335,13 +332,12 @@ jobs: - name: Install OS dependencies if: steps.cache-c-core.outputs.cache-hit != 'true' # Only needed when building the C core - run: - sudo apt install ninja-build cmake flex bison + run: sudo apt install ninja-build cmake flex bison - uses: actions/setup-python@v5 name: Install Python with: - python-version: '3.12' + python-version: "3.12" - name: Install test dependencies run: | @@ -357,8 +353,7 @@ jobs: env: IGRAPH_USE_SANITIZERS: 1 run: | - # NOTE: install calls "build" first - python setup.py install + pip install . # Only pytest, and nothing else should be run in this section due to the presence of LD_PRELOAD. # The ASan/UBSan library versions need to be updated when switching to a newer Ubuntu/GCC. From 3e99b0ffcd5a0bac54e539f7be3def12f46d42a4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 11 Jun 2025 11:18:58 +0200 Subject: [PATCH 205/276] ci: reformatted yml, updated windows-2019 to windows-2022, bumped MACOSX_DEPLOYMENT_TARGET --- .github/workflows/build.yml | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 754f5fba1..264b66ce8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -94,7 +94,7 @@ jobs: runs-on: macos-latest env: LLVM_VERSION: "14.0.5" - MACOSX_DEPLOYMENT_TARGET: "10.9" + MACOSX_DEPLOYMENT_TARGET: "10.15" strategy: matrix: include: @@ -126,8 +126,7 @@ jobs: - name: Install OS dependencies if: steps.cache-c-core.outputs.cache-hit != 'true' || steps.cache-c-deps.outputs.cache-hit != 'true' # Only needed when building the C core or libomp - run: - brew install ninja autoconf automake libtool + run: brew install ninja autoconf automake libtool - name: Install OpenMP library if: steps.cache-c-deps.outputs.cache-hit != 'true' @@ -167,16 +166,15 @@ jobs: - uses: actions/setup-python@v5 name: Install Python with: - python-version: '3.12.1' + python-version: "3.12.1" - name: Install OS dependencies - run: - sudo apt install ninja-build cmake flex bison + run: sudo apt install ninja-build cmake flex bison - uses: mymindstorm/setup-emsdk@v14 with: - version: '3.1.58' - actions-cache-folder: 'emsdk-cache' + version: "3.1.58" + actions-cache-folder: "emsdk-cache" - name: Build wheel run: | @@ -204,12 +202,12 @@ jobs: - cmake_arch: Win32 wheel_arch: win32 vcpkg_arch: x86 - os: windows-2019 + os: windows-2022 test_extra: test - cmake_arch: x64 wheel_arch: win_amd64 vcpkg_arch: x64 - os: windows-2019 + os: windows-2022 test_extra: test - cmake_arch: ARM64 wheel_arch: win_arm64 @@ -253,7 +251,7 @@ jobs: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" CIBW_ENABLE: pypy - CIBW_TEST_COMMAND: "cd /d {project} && pip install --prefer-binary \".[${{ matrix.test_extra }}]\" && python -m pytest tests" + CIBW_TEST_COMMAND: 'cd /d {project} && pip install --prefer-binary ".[${{ matrix.test_extra }}]" && python -m pytest tests' # Skip tests for Python 3.10 onwards because SciPy does not have # 32-bit wheels for Windows any more CIBW_TEST_SKIP: "cp310-win32 cp311-win32 cp312-win32 cp313-win32" @@ -287,13 +285,12 @@ jobs: - name: Install OS dependencies if: steps.cache-c-core.outputs.cache-hit != 'true' # Only needed when building the C core - run: - sudo apt install ninja-build cmake flex bison + run: sudo apt install ninja-build cmake flex bison - uses: actions/setup-python@v5 name: Install Python with: - python-version: '3.9' + python-version: "3.9" - name: Build sdist run: | @@ -335,13 +332,12 @@ jobs: - name: Install OS dependencies if: steps.cache-c-core.outputs.cache-hit != 'true' # Only needed when building the C core - run: - sudo apt install ninja-build cmake flex bison + run: sudo apt install ninja-build cmake flex bison - uses: actions/setup-python@v5 name: Install Python with: - python-version: '3.12' + python-version: "3.12" - name: Install test dependencies run: | From 86d721e6b0d99c0c65be915251b96f8eeedb528b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 01:14:18 +0200 Subject: [PATCH 206/276] build(deps): bump pypa/cibuildwheel from 2.23.3 to 3.0.0 (#832) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tamas Nepusz --- .github/workflows/build.yml | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 264b66ce8..a0324dccb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,18 +5,13 @@ on: [push, pull_request] env: CIBW_ENVIRONMENT_PASS_LINUX: PYTEST_TIMEOUT CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test]' && python -m pytest -v tests" - CIBW_SKIP: "cp36-* cp37-* cp38-* pp37-* pp38-*" + CIBW_SKIP: "cp38-* pp38-*" PYTEST_TIMEOUT: 60 jobs: build_wheel_linux: - name: Build wheels on Linux (${{ matrix.wheel_arch }}) + name: Build wheels on Linux (x86_64) runs-on: ubuntu-22.04 - strategy: - fail-fast: false - matrix: - wheel_arch: [x86_64, i686] - steps: - uses: actions/checkout@v4 with: @@ -24,25 +19,22 @@ jobs: fetch-depth: 0 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.23.3 + uses: pypa/cibuildwheel@v3.0.0 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" - CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" + CIBW_BUILD: "*-manylinux_x86_64" CIBW_ENABLE: pypy - # Skip tests for Python 3.10 onwards because SciPy does not have - # 32-bit wheels for Linux. - CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686 cp313-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.23.3 + uses: pypa/cibuildwheel@v3.0.0 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" - CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" + CIBW_BUILD: "*-musllinux_x86_64" CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test-musl]' && python -m pytest -v tests" - uses: actions/upload-artifact@v4 with: - name: wheels-linux-${{ matrix.wheel_arch }} + name: wheels-linux-x86_64 path: ./wheelhouse/*.whl build_wheel_linux_aarch64_manylinux: @@ -55,7 +47,7 @@ jobs: fetch-depth: 0 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.23.3 + uses: pypa/cibuildwheel@v3.0.0 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -77,7 +69,7 @@ jobs: fetch-depth: 0 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.23.3 + uses: pypa/cibuildwheel@v3.0.0 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -140,7 +132,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.23.3 + uses: pypa/cibuildwheel@v3.0.0 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" @@ -246,7 +238,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.23.3 + uses: pypa/cibuildwheel@v3.0.0 env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From 1e4b0424c7a1cdff57e0e50de71ebd8b99171f11 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 17 Jun 2025 01:18:23 +0200 Subject: [PATCH 207/276] ci: simplify sanitizer test workflow --- .github/workflows/build.yml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a0324dccb..731bcbf38 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -331,22 +331,11 @@ jobs: with: python-version: "3.12" - - name: Install test dependencies - run: | - pip install --prefer-binary pytest pytest-timeout setuptools - - - name: Build C core - env: - IGRAPH_USE_SANITIZERS: 1 - run: | - python setup.py build_c_core - - name: Build and install Python extension env: IGRAPH_USE_SANITIZERS: 1 run: | - # NOTE: install calls "build" first - python setup.py install + pip install --prefer-binary '.[test]' # Only pytest, and nothing else should be run in this section due to the presence of LD_PRELOAD. # The ASan/UBSan library versions need to be updated when switching to a newer Ubuntu/GCC. From 75c49c6d298e9f2efe5761bf0b0101dd5d667a8c Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 18 Jun 2025 13:30:21 +0200 Subject: [PATCH 208/276] ci: uninstall matplotlib --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 731bcbf38..637ac9ad1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -336,6 +336,9 @@ jobs: IGRAPH_USE_SANITIZERS: 1 run: | pip install --prefer-binary '.[test]' + # Uninstall Matplotlib because Matplotlib-related tests cause false positives in the + # leak sanitizer checks + pip uninstall matplotlib # Only pytest, and nothing else should be run in this section due to the presence of LD_PRELOAD. # The ASan/UBSan library versions need to be updated when switching to a newer Ubuntu/GCC. From 0545b7324e7b59bd2bb8c0a49dbd2a3d035d8c6b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 19 Jun 2025 15:28:10 +0200 Subject: [PATCH 209/276] chore: updated contributors list --- .all-contributorsrc | 9 +++++++++ CONTRIBUTORS.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 4cabb5284..af4e6a3d4 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -630,6 +630,15 @@ "contributions": [ "code" ] + }, + { + "login": "BeaMarton13", + "name": "Bea Márton", + "avatar_url": "https://avatars.githubusercontent.com/u/204701577?v=4", + "profile": "https://github.com/BeaMarton13", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f43ef7b6f..5c5714306 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -95,6 +95,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Michael Schneider
Michael Schneider

💻 Thomas Krijnen
Thomas Krijnen

💻 Tim Bernhard
Tim Bernhard

💻 + Bea Márton
Bea Márton

💻 From 6d2fc69cab70d3a6418dd55eb1aaecf38e6fdbd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horv=C3=A1t?= Date: Thu, 19 Jun 2025 13:39:21 +0000 Subject: [PATCH 210/276] Edge betweenness-based community detection now computes modularity even for weighted graphs (#836) --- src/_igraph/graphobject.c | 67 +++++++++++++++++++------------------ src/igraph/community.py | 9 ++++- tests/test_decomposition.py | 6 +--- 3 files changed, 43 insertions(+), 39 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 8c82bf79f..d3056f941 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13048,16 +13048,18 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject PyObject *res, *qs, *ms; igraph_matrix_int_t merges; igraph_vector_t q; - igraph_vector_t *weights = 0; + igraph_vector_t *weights = NULL; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &directed, &weights_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &directed, &weights_o)) { return NULL; + } - if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) return NULL; + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } if (igraph_matrix_int_init(&merges, 0, 0)) { - if (weights != 0) { + if (weights) { igraph_vector_destroy(weights); free(weights); } return igraphmodule_handle_igraph_error(); @@ -13065,51 +13067,44 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject if (igraph_vector_init(&q, 0)) { igraph_matrix_int_destroy(&merges); - if (weights != 0) { + if (weights) { igraph_vector_destroy(weights); free(weights); } return igraphmodule_handle_igraph_error(); } if (igraph_community_edge_betweenness(&self->g, - /* removed_edges = */ 0, - /* edge_betweenness = */ 0, + /* removed_edges = */ NULL, + /* edge_betweenness = */ NULL, /* merges = */ &merges, - /* bridges = */ 0, - /* modularity = */ weights ? 0 : &q, - /* membership = */ 0, + /* bridges = */ NULL, + /* modularity = */ &q, + /* membership = */ NULL, PyObject_IsTrue(directed), weights, - /* lengths = */ 0)) { - igraphmodule_handle_igraph_error(); - if (weights != 0) { + /* lengths = */ NULL)) { + + igraph_vector_destroy(&q); + igraph_matrix_int_destroy(&merges); + if (weights) { igraph_vector_destroy(weights); free(weights); } - igraph_matrix_int_destroy(&merges); - igraph_vector_destroy(&q); - return NULL; + + return igraphmodule_handle_igraph_error();; } - if (weights != 0) { + if (weights) { igraph_vector_destroy(weights); free(weights); } - if (weights == 0) { - /* Calculate modularity vector only in the unweighted case as we don't - * calculate modularities for the weighted case */ - qs=igraphmodule_vector_t_to_PyList(&q, IGRAPHMODULE_TYPE_FLOAT); - igraph_vector_destroy(&q); - if (!qs) { - igraph_matrix_int_destroy(&merges); - return NULL; - } - } else { - qs = Py_None; - Py_INCREF(qs); - igraph_vector_destroy(&q); + qs = igraphmodule_vector_t_to_PyList(&q, IGRAPHMODULE_TYPE_FLOAT); + igraph_vector_destroy(&q); + if (!qs) { + igraph_matrix_int_destroy(&merges); + return NULL; } - ms=igraphmodule_matrix_int_t_to_PyList(&merges); + ms = igraphmodule_matrix_int_t_to_PyList(&merges); igraph_matrix_int_destroy(&merges); if (ms == NULL) { @@ -18531,12 +18526,18 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "is typically high. So we gradually remove the edge with the highest\n" "betweenness from the network and recalculate edge betweenness after every\n" "removal, as long as all edges are removed.\n\n" + "When edge weights are given, the ratio of betweenness and weight values\n" + "is used to choose which edges to remove first, as described in\n" + "M. E. J. Newman: Analysis of Weighted Networks (2004), Section C.\n" + "Thus, edges with large weights are treated as strong connections,\n" + "and will be removed later than weak connections having similar betweenness.\n" + "Weights are also used for calculating modularity.\n\n" "Attention: this function is wrapped in a more convenient syntax in the\n" "derived class L{Graph}. It is advised to use that instead of this version.\n\n" "@param directed: whether to take into account the directedness of the edges\n" " when we calculate the betweenness values.\n" "@param weights: name of an edge attribute or a list containing\n" - " edge weights.\n\n" + " edge weights. Higher weights indicate stronger connections.\n\n" "@return: a tuple with the merge matrix that describes the dendrogram\n" " and the modularity scores before each merge. The modularity scores\n" " use the weights if the original graph was weighted.\n" diff --git a/src/igraph/community.py b/src/igraph/community.py index 0fdcdf154..dfdcb3954 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -235,6 +235,13 @@ def _community_edge_betweenness(graph, clusters=None, directed=True, weights=Non separate components. The result of the clustering will be represented by a dendrogram. + When edge weights are given, the ratio of betweenness and weight values + is used to choose which edges to remove first, as described in + M. E. J. Newman: Analysis of Weighted Networks (2004), Section C. + Thus, edges with large weights are treated as strong connections, + and will be removed later than weak connections having similar betweenness. + Weights are also used for calculating modularity. + @param clusters: the number of clusters we would like to see. This practically defines the "level" where we "cut" the dendrogram to get the membership vector of the vertices. If C{None}, the dendrogram @@ -245,7 +252,7 @@ def _community_edge_betweenness(graph, clusters=None, directed=True, weights=Non @param directed: whether the directionality of the edges should be taken into account or not. @param weights: name of an edge attribute or a list containing - edge weights. + edge weights. Higher weights indicate stronger connections. @return: a L{VertexDendrogram} object, initally cut at the maximum modularity or at the desired number of clusters. """ diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index 0bfdd1b6a..336db7ded 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -263,11 +263,7 @@ def testEdgeBetweenness(self): g.es["weight"] = 1 g[0, 1] = g[1, 2] = g[2, 0] = g[3, 4] = 10 - # We need to specify the desired cluster count explicitly; this is - # because edge betweenness-based detection does not play well with - # modularity-based cluster count selection (the edge weights have - # different semantics) so we need to give igraph a hint - cl = g.community_edge_betweenness(weights="weight").as_clustering(n=2) + cl = g.community_edge_betweenness(weights="weight").as_clustering() self.assertMembershipsEqual(cl, [0, 0, 0, 1, 1]) self.assertAlmostEqual(cl.q, 0.2750, places=3) From a0cab35295e250045739b31868dafad21010d2cf Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 19 Jun 2025 15:46:02 +0200 Subject: [PATCH 211/276] ci: do not ask for confirmation when removing matplotlib --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 637ac9ad1..9f5572e79 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -338,7 +338,7 @@ jobs: pip install --prefer-binary '.[test]' # Uninstall Matplotlib because Matplotlib-related tests cause false positives in the # leak sanitizer checks - pip uninstall matplotlib + pip uninstall -y matplotlib # Only pytest, and nothing else should be run in this section due to the presence of LD_PRELOAD. # The ASan/UBSan library versions need to be updated when switching to a newer Ubuntu/GCC. From 027237bcc062224322a21aa3d059b3c1dbdc73f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bea=20M=C3=A1rton?= Date: Thu, 19 Jun 2025 17:58:35 +0300 Subject: [PATCH 212/276] add: ignore warnings on structural unittests (#834) --- tests/test_structural.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/test_structural.py b/tests/test_structural.py index 3d310dc53..c0f1bdb59 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -503,19 +503,28 @@ def testEigenvectorCentrality(self): def testAuthorityScore(self): g = Graph.Tree(15, 2, TREE_IN) - asc = g.authority_score() + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + asc = g.authority_score() self.assertAlmostEqual(max(asc), 1.0, places=3) # Smoke testing - g.authority_score(scale=False, return_eigenvalue=True) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + g.authority_score(scale=False, return_eigenvalue=True) def testHubScore(self): g = Graph.Tree(15, 2, TREE_IN) - hsc = g.hub_score() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + hsc = g.hub_score() self.assertAlmostEqual(max(hsc), 1.0, places=3) # Smoke testing - g.hub_score(scale=False, return_eigenvalue=True) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + g.hub_score(scale=False, return_eigenvalue=True) def testCoreness(self): g = Graph.Full(4) + Graph(4) + [(0, 4), (1, 5), (2, 6), (3, 7)] From bdad808181988f6a96f377e124f32400c7b6ee2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bea=20M=C3=A1rton?= Date: Tue, 24 Jun 2025 17:13:25 +0300 Subject: [PATCH 213/276] feat: expose fluid communities to python (#835) --- src/_igraph/graphobject.c | 56 ++++++++++++++++++++++++++++++++++++- src/igraph/__init__.py | 3 ++ src/igraph/community.py | 41 +++++++++++++++++++++++++++ tests/test_decomposition.py | 55 ++++++++++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index d3056f941..09f9c7176 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13743,6 +13743,38 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, return error ? NULL : Py_BuildValue("Nd", res, (double) quality); } + /** + * Fluid communities + */ +PyObject *igraphmodule_Graph_community_fluid_communities(igraphmodule_GraphObject *self, + PyObject *args, PyObject *kwds) { + static char *kwlist[] = {"no_of_communities", NULL}; + Py_ssize_t no_of_communities; + igraph_vector_int_t membership; + PyObject *result; + + // Parse the Python integer argument + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n", kwlist, &no_of_communities)) { + return NULL; + } + + if (igraph_vector_int_init(&membership, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_community_fluid_communities(&self->g, no_of_communities, &membership)) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&membership); + return NULL; + } + + result = igraphmodule_vector_int_t_to_PyList(&membership); + igraph_vector_int_destroy(&membership); + + return result; +} + /********************************************************************** * Random walks * **********************************************************************/ @@ -18394,6 +18426,28 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "\n" "@see: modularity()\n" }, + {"community_fluid_communities", + (PyCFunction) igraphmodule_Graph_community_fluid_communities, + METH_VARARGS | METH_KEYWORDS, + "community_fluid_communities(no_of_communities)\n--\n\n" + "Community detection based on fluids interacting on the graph.\n\n" + "The algorithm is based on the simple idea of several fluids interacting\n" + "in a non-homogeneous environment (the graph topology), expanding and\n" + "contracting based on their interaction and density. Weighted graphs are\n" + "not supported.\n\n" + "B{Reference}\n\n" + " - Parés F, Gasulla DG, et. al. (2018) Fluid Communities: A Competitive,\n" + " Scalable and Diverse Community Detection Algorithm. In: Complex Networks\n" + " & Their Applications VI: Proceedings of Complex Networks 2017 (The Sixth\n" + " International Conference on Complex Networks and Their Applications),\n" + " Springer, vol 689, p 229. https://doi.org/10.1007/978-3-319-72150-7_19\n\n" + "@param no_of_communities: The number of communities to be found. Must be\n" + " greater than 0 and fewer than number of vertices in the graph.\n" + "@return: a list with the community membership of each vertex.\n" + "@note: The graph must be simple and connected. Edge directions will be\n" + " ignored if the graph is directed.\n" + "@note: Time complexity: O(|E|)\n", + }, {"community_infomap", (PyCFunction) igraphmodule_Graph_community_infomap, METH_VARARGS | METH_KEYWORDS, @@ -18402,7 +18456,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "method of Martin Rosvall and Carl T. Bergstrom.\n\n" "See U{https://www.mapequation.org} for a visualization of the algorithm\n" "or one of the references provided below.\n" - "B{References}\n" + "B{Reference}: " " - M. Rosvall and C. T. Bergstrom: I{Maps of information flow reveal\n" " community structure in complex networks}. PNAS 105, 1118 (2008).\n" " U{https://arxiv.org/abs/0707.0609}\n" diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index b7f920cac..a1e87d13e 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -109,6 +109,7 @@ _community_multilevel, _community_optimal_modularity, _community_edge_betweenness, + _community_fluid_communities, _community_spinglass, _community_walktrap, _k_core, @@ -658,6 +659,7 @@ def es(self): community_multilevel = _community_multilevel community_optimal_modularity = _community_optimal_modularity community_edge_betweenness = _community_edge_betweenness + community_fluid_communities = _community_fluid_communities community_spinglass = _community_spinglass community_walktrap = _community_walktrap k_core = _k_core @@ -1100,6 +1102,7 @@ def write(graph, filename, *args, **kwds): _community_multilevel, _community_optimal_modularity, _community_edge_betweenness, + _community_fluid_communities, _community_spinglass, _community_walktrap, _k_core, diff --git a/src/igraph/community.py b/src/igraph/community.py index dfdcb3954..1150dcb57 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -468,6 +468,47 @@ def _community_leiden( ) +def _community_fluid_communities(graph, no_of_communities): + """Community detection based on fluids interacting on the graph. + + The algorithm is based on the simple idea of several fluids interacting + in a non-homogeneous environment (the graph topology), expanding and + contracting based on their interaction and density. Weighted graphs are + not supported. + + This function implements the community detection method described in: + Parés F, Gasulla DG, et. al. (2018) Fluid Communities: A Competitive, + Scalable and Diverse Community Detection Algorithm. + + @param no_of_communities: The number of communities to be found. Must be + greater than 0 and fewer than or equal to the number of vertices in the graph. + @return: an appropriate L{VertexClustering} object. + """ + # Validate input parameters + if no_of_communities <= 0: + raise ValueError("no_of_communities must be greater than 0") + + if no_of_communities > graph.vcount(): + raise ValueError("no_of_communities must be fewer than or equal to the number of vertices") + + # Check if graph is weighted (not supported) + if graph.is_weighted(): + raise ValueError("Weighted graphs are not supported by the fluid communities algorithm") + + # Handle directed graphs - the algorithm works on undirected graphs + # but can accept directed graphs (they are treated as undirected) + if graph.is_directed(): + import warnings + warnings.warn( + "Directed graphs are treated as undirected in the fluid communities algorithm", + UserWarning, + stacklevel=2 + ) + + membership = GraphBase.community_fluid_communities(graph, no_of_communities) + return VertexClustering(graph, membership) + + def _modularity(self, membership, weights=None, resolution=1, directed=True): """Calculates the modularity score of the graph with respect to a given clustering. diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index 336db7ded..5bcb0f7c3 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -276,6 +276,61 @@ def testEigenvector(self): cl = g.community_leading_eigenvector(2) self.assertMembershipsEqual(cl, [0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) self.assertAlmostEqual(cl.q, 0.4523, places=3) + + def testFluidCommunities(self): + # Test with a simple graph: two cliques connected by a single edge + g = Graph.Full(5) + Graph.Full(5) + g.add_edges([(0, 5)]) + + # Test basic functionality - should find 2 communities + cl = g.community_fluid_communities(2) + self.assertEqual(len(set(cl.membership)), 2) + self.assertMembershipsEqual(cl, [0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) + + # Test with 3 cliques + g = Graph.Full(4) + Graph.Full(4) + Graph.Full(4) + g += [(0, 4), (4, 8)] # Connect the cliques + cl = g.community_fluid_communities(3) + self.assertEqual(len(set(cl.membership)), 3) + self.assertMembershipsEqual(cl, [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2]) + + # Test error conditions + # Number of communities must be positive + with self.assertRaises(Exception): + g.community_fluid_communities(0) + + # Number of communities cannot exceed number of vertices + with self.assertRaises(Exception): + g.community_fluid_communities(g.vcount() + 1) + + # Test with disconnected graph (should raise error) + g_disconnected = Graph.Full(3) + Graph.Full(3) # No connecting edge + with self.assertRaises(Exception): + g_disconnected.community_fluid_communities(2) + + # Test with single vertex (edge case) + g_single = Graph(1) + cl = g_single.community_fluid_communities(1) + self.assertEqual(cl.membership, [0]) + + # Test with small connected graph + g_small = Graph([(0, 1), (1, 2), (2, 0)]) # Triangle + cl = g_small.community_fluid_communities(1) + self.assertEqual(len(set(cl.membership)), 1) + self.assertEqual(cl.membership, [0, 0, 0]) + + # Test deterministic behavior on simple structure + # Note: Fluid communities can be non-deterministic due to randomization, + # but on very simple structures it should be consistent + g_path = Graph([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]) + cl = g_path.community_fluid_communities(2) + self.assertEqual(len(set(cl.membership)), 2) + + # Test that it returns a VertexClustering object + g = Graph.Full(6) + cl = g.community_fluid_communities(2) + self.assertIsInstance(cl, VertexClustering) + self.assertEqual(len(cl.membership), g.vcount()) def testInfomap(self): g = Graph.Famous("zachary") From 915b55ce1bfa2a64532a7fa4854734a6b6236364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Fri, 27 Jun 2025 09:30:41 +0000 Subject: [PATCH 214/276] docs: fix invalid reference formatting for infomap --- src/_igraph/graphobject.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 09f9c7176..a2d982ad7 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -18455,8 +18455,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Finds the community structure of the network according to the Infomap\n" "method of Martin Rosvall and Carl T. Bergstrom.\n\n" "See U{https://www.mapequation.org} for a visualization of the algorithm\n" - "or one of the references provided below.\n" - "B{Reference}: " + "or one of the references provided below.\n\n" + "B{References}\n\n" " - M. Rosvall and C. T. Bergstrom: I{Maps of information flow reveal\n" " community structure in complex networks}. PNAS 105, 1118 (2008).\n" " U{https://arxiv.org/abs/0707.0609}\n" From cec82b4b5958123bcaa9fabefcf960fe5b4159e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Mon, 30 Jun 2025 22:06:15 +0000 Subject: [PATCH 215/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index beebfcdcd..e2a3b7c6e 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit beebfcdcd707f50a31cf8eb3568cf09f8b7baf54 +Subproject commit e2a3b7c6e26683e9bb5f6a07d4e380861bd40f93 From 62083868e6173120d2e3220591d23cbd5a022dce Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 3 Jul 2025 12:23:21 +0200 Subject: [PATCH 216/276] fix: add some missing files to source tarball, closes #830 --- MANIFEST.in | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 5c68b5e82..39e2ee9fe 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,6 +8,10 @@ include scripts/*.sh include scripts/*.py include tests/*.py +include CHANGELOG.md +include CONTRIBUTORS.md +include CITATION.cff + graft vendor/source/igraph graft doc From bd64579d49c7553eaafbbdff8ef0c8e0a6216fec Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 5 Jul 2025 01:44:02 +0200 Subject: [PATCH 217/276] fix: better punctuation of warnings emitted from igraph's C core, closes #838 --- src/_igraph/error.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/_igraph/error.c b/src/_igraph/error.c index 41b2b879e..b6940a27e 100644 --- a/src/_igraph/error.c +++ b/src/_igraph/error.c @@ -61,7 +61,20 @@ PyObject* igraphmodule_handle_igraph_error() { void igraphmodule_igraph_warning_hook(const char *reason, const char *file, int line) { char buf[4096]; - snprintf(buf, sizeof(buf), "%s at %s:%i", reason, file, line); + char end; + size_t len = strlen(reason); + const char* separator = " "; + + if (len == 0) { + separator = ""; + } else { + end = reason[len - 1]; + if (end != '.' && end != '?' && end != '!') { + separator = ". "; + } + } + + snprintf(buf, sizeof(buf), "%s%sLocation: %s:%i", reason, separator, file, line); PY_IGRAPH_WARN(buf); } From 4119c94ded9cd29ed770df5a169f90e2ecfa66d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bea=20M=C3=A1rton?= Date: Sat, 5 Jul 2025 02:58:17 +0300 Subject: [PATCH 218/276] feat: expose voronoi to python (#833) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Szabolcs Horvát Co-authored-by: MIBea13 --- src/_igraph/graphobject.c | 155 ++++++++++++++++++++++++++++++++++++ src/igraph/__init__.py | 3 + src/igraph/community.py | 62 +++++++++++++++ tests/test_decomposition.py | 49 ++++++++++++ 4 files changed, 269 insertions(+) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index a2d982ad7..0c1401f60 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13775,6 +13775,125 @@ PyObject *igraphmodule_Graph_community_fluid_communities(igraphmodule_GraphObjec return result; } +/** + * Voronoi clustering + */ +PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, + PyObject *args, PyObject *kwds) { + static char *kwlist[] = {"lengths", "weights", "mode", "radius", NULL}; + PyObject *lengths_o = Py_None, *weights_o = Py_None; + PyObject *mode_o = Py_None; + PyObject *radius_o = Py_None; + igraph_vector_t *lengths_v = NULL; + igraph_vector_t *weights_v = NULL; + igraph_vector_int_t membership_v, generators_v; + igraph_neimode_t mode = IGRAPH_OUT; + igraph_real_t radius = -1.0; /* negative means auto-optimize */ + igraph_real_t modularity = IGRAPH_NAN; + PyObject *membership_o, *generators_o, *result_o; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, + &lengths_o, &weights_o, &mode_o, &radius_o)) { + return NULL; + } + + /* Handle mode parameter */ + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) { + return NULL; + } + + /* Handle radius parameter */ + if (radius_o != Py_None) { + if (igraphmodule_PyObject_to_real_t(radius_o, &radius)) { + return NULL; + } + } + + /* Handle lengths parameter */ + if (igraphmodule_attrib_to_vector_t(lengths_o, self, &lengths_v, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } + + /* Handle weights parameter */ + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights_v, ATTRIBUTE_TYPE_EDGE)) { + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + return NULL; + } + + /* Initialize result vectors */ + if (igraph_vector_int_init(&membership_v, 0)) { + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != NULL) { + igraph_vector_destroy(weights_v); free(weights_v); + } + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_vector_int_init(&generators_v, 0)) { + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != NULL) { + igraph_vector_destroy(weights_v); free(weights_v); + } + igraph_vector_int_destroy(&membership_v); + igraphmodule_handle_igraph_error(); + return NULL; + } + + /* Call the C function - pass NULL for None parameters */ + if (igraph_community_voronoi(&self->g, &membership_v, &generators_v, + &modularity, + lengths_v, + weights_v, + mode, radius)) { + + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != NULL) { + igraph_vector_destroy(weights_v); free(weights_v); + } + igraph_vector_int_destroy(&membership_v); + igraph_vector_int_destroy(&generators_v); + igraphmodule_handle_igraph_error(); + return NULL; + } + + /* Clean up input vectors */ + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != NULL) { + igraph_vector_destroy(weights_v); free(weights_v); + } + + /* Convert results to Python objects */ + membership_o = igraphmodule_vector_int_t_to_PyList(&membership_v); + igraph_vector_int_destroy(&membership_v); + if (!membership_o) { + igraph_vector_int_destroy(&generators_v); + return NULL; + } + + generators_o = igraphmodule_vector_int_t_to_PyList(&generators_v); + igraph_vector_int_destroy(&generators_v); + if (!generators_o) { + Py_DECREF(membership_o); + return NULL; + } + + /* Return tuple with membership, generators, and modularity */ + result_o = Py_BuildValue("(NNd)", membership_o, generators_o, modularity); + + return result_o; +} + /********************************************************************** * Random walks * **********************************************************************/ @@ -18653,6 +18772,42 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " original implementation is used.\n" "@return: the community membership vector.\n" }, + {"community_voronoi", + (PyCFunction) igraphmodule_Graph_community_voronoi, + METH_VARARGS | METH_KEYWORDS, + "community_voronoi(lengths=None, weights=None, mode=\"out\", radius=None)\n--\n\n" + "Finds communities using Voronoi partitioning.\n\n" + "This function finds communities using a Voronoi partitioning of vertices based\n" + "on the given edge lengths divided by the edge clustering coefficient.\n" + "The generator vertices are chosen to be those with the largest local relative\n" + "density within a radius, with the local relative density of a vertex defined as\n" + "C{s * m / (m + k)}, where s is the strength of the vertex, m is the number of\n" + "edges within the vertex's first order neighborhood, while k is the number of\n" + "edges with only one endpoint within this neighborhood.\n\n" + "B{References}\n\n" + " - Deritei et al., Community detection by graph Voronoi diagrams,\n" + " New Journal of Physics 16, 063007 (2014)\n" + " U{https://doi.org/10.1088/1367-2630/16/6/063007}\n" + " - Molnár et al., Community Detection in Directed Weighted Networks\n" + " using Voronoi Partitioning, Scientific Reports 14, 8124 (2024)\n" + " U{https://doi.org/10.1038/s41598-024-58624-4}\n\n" + "@param lengths: edge lengths, or C{None} to consider all edges as having\n" + " unit length. Voronoi partitioning will use edge lengths equal to\n" + " lengths / ECC where ECC is the edge clustering coefficient.\n" + "@param weights: edge weights, or C{None} to consider all edges as having\n" + " unit weight. Weights are used when selecting generator points, as well\n" + " as for computing modularity.\n" + "@param mode: if C{\"out\"} (the default), distances from generator points to all other\n" + " nodes are considered. If C{\"in\"}, the reverse distances are used.\n" + " If C{\"all\"}, edge directions are ignored. This parameter is ignored\n" + " for undirected graphs.\n" + "@param radius: the radius/resolution to use when selecting generator points.\n" + " The larger this value, the fewer partitions there will be. Pass C{None}\n" + " to automatically select the radius that maximizes modularity.\n" + "@return: a tuple containing the membership vector, generator vertices, and\n" + " modularity score: (membership, generators, modularity).\n" + "@rtype: tuple\n" + }, {"community_leiden", (PyCFunction) igraphmodule_Graph_community_leiden, METH_VARARGS | METH_KEYWORDS, diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index a1e87d13e..78786b980 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -111,6 +111,7 @@ _community_edge_betweenness, _community_fluid_communities, _community_spinglass, + _community_voronoi, _community_walktrap, _k_core, _community_leiden, @@ -661,6 +662,7 @@ def es(self): community_edge_betweenness = _community_edge_betweenness community_fluid_communities = _community_fluid_communities community_spinglass = _community_spinglass + community_voronoi = _community_voronoi community_walktrap = _community_walktrap k_core = _k_core community_leiden = _community_leiden @@ -1104,6 +1106,7 @@ def write(graph, filename, *args, **kwds): _community_edge_betweenness, _community_fluid_communities, _community_spinglass, + _community_voronoi, _community_walktrap, _k_core, _community_leiden, diff --git a/src/igraph/community.py b/src/igraph/community.py index 1150dcb57..d08546bd7 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -327,6 +327,68 @@ def _community_spinglass(graph, *args, **kwds): return VertexClustering(graph, membership, modularity_params=modularity_params) +def _community_voronoi(graph, lengths=None, weights=None, mode="out", radius=None): + """Finds communities using Voronoi partitioning. + + This function finds communities using a Voronoi partitioning of vertices based + on the given edge lengths divided by the edge clustering coefficient. + The generator vertices are chosen to be those with the largest local relative + density within a radius, with the local relative density of a vertex defined + as C{s * m / (m + k)}, where C{s} is the strength of the vertex, C{m} is + the number of edges within the vertex's first order neighborhood, while C{k} + is the number of edges with only one endpoint within this neighborhood. + + B{References} + + - Deritei et al., Community detection by graph Voronoi diagrams, + I{New Journal of Physics} 16, 063007 (2014). + U{https://doi.org/10.1088/1367-2630/16/6/063007}. + - Molnár et al., Community Detection in Directed Weighted Networks using + Voronoi Partitioning, I{Scientific Reports} 14, 8124 (2024). + U{https://doi.org/10.1038/s41598-024-58624-4}. + + @param lengths: edge lengths, or C{None} to consider all edges as having + unit length. Voronoi partitioning will use edge lengths equal to + lengths / ECC where ECC is the edge clustering coefficient. + @param weights: edge weights, or C{None} to consider all edges as having + unit weight. Weights are used when selecting generator points, as well + as for computing modularity. + @param mode: specifies how to use the direction of edges when computing + distances from generator points. If C{"out"} (the default), distances + from generator points to all other nodes are considered following the + direction of edges. If C{"in"}, distances are computed in the reverse + direction (i.e., from all nodes to generator points). If C{"all"}, + edge directions are ignored and the graph is treated as undirected. + This parameter is ignored for undirected graphs. + @param radius: the radius/resolution to use when selecting generator points. + The larger this value, the fewer partitions there will be. Pass C{None} + to automatically select the radius that maximizes modularity. + @return: an appropriate L{VertexClustering} object with an extra attribute + called C{generators} (the generator vertices). + """ + # Convert mode string to proper enum value to avoid deprecation warning + if isinstance(mode, str): + mode_map = {"out": "out", "in": "in", "all": "all", "total": "all"} # alias + if mode.lower() in mode_map: + mode = mode_map[mode.lower()] + else: + raise ValueError(f"Invalid mode '{mode}'. Must be one of: out, in, all") + + membership, generators, modularity = GraphBase.community_voronoi(graph, lengths, weights, mode, radius) + + params = {"generators": generators} + modularity_params = {} + if weights is not None: + modularity_params["weights"] = weights + + clustering = VertexClustering( + graph, membership, modularity=modularity, params=params, modularity_params=modularity_params + ) + + clustering.generators = generators + return clustering + + def _community_walktrap(graph, weights=None, steps=4): """Community detection algorithm of Latapy & Pons, based on random walks. diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index 5bcb0f7c3..109cad0dd 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -534,6 +534,55 @@ def testSpinglass(self): ok = True break self.assertTrue(ok) + + def testVoronoi(self): + # Test 1: Two disconnected cliques - should find exactly 2 communities + g = Graph.Full(5) + Graph.Full(5) # Two separate complete graphs + cl = g.community_voronoi() + + # Should find exactly 2 communities + self.assertEqual(len(cl), 2) + + # Vertices 0-4 should be in one community, vertices 5-9 in another + communities = [set(), set()] + for vertex, community in enumerate(cl.membership): + communities[community].add(vertex) + + # One community should have vertices 0-4, the other should have 5-9 + expected_communities = [{0, 1, 2, 3, 4}, {5, 6, 7, 8, 9}] + self.assertEqual( + set(frozenset(c) for c in communities), + set(frozenset(c) for c in expected_communities) + ) + + # Test 2: Two cliques connected by a single bridge edge + g = Graph.Full(4) + Graph.Full(4) + g.add_edges([(0, 4)]) # Bridge connecting the two cliques + + cl = g.community_voronoi() + + # Should still find 2 communities (bridge is weak) + self.assertEqual(len(cl), 2) + + # Check that vertices within each clique are in the same community + # Vertices 0,1,2,3 should be together, and 4,5,6,7 should be together + comm_0123 = {cl.membership[i] for i in [0, 1, 2, 3]} + comm_4567 = {cl.membership[i] for i in [4, 5, 6, 7]} + + self.assertEqual(len(comm_0123), 1) # All in same community + self.assertEqual(len(comm_4567), 1) # All in same community + self.assertNotEqual(comm_0123, comm_4567) # Different communities + + # Test 3: Three disconnected triangles + g = Graph(9) + g.add_edges([(0, 1), (1, 2), (2, 0), # Triangle 1 + (3, 4), (4, 5), (5, 3), # Triangle 2 + (6, 7), (7, 8), (8, 6)]) # Triangle 3 + + cl = g.community_voronoi() + + # Should find exactly 3 communities + self.assertEqual(len(cl), 3) def testWalktrap(self): g = Graph.Full(5) + Graph.Full(5) + Graph.Full(5) From 593a263242c74391aa98bdeead647bc3b5f8cb98 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 5 Jul 2025 02:14:41 +0200 Subject: [PATCH 219/276] ci: do not run numpy and scipy related tests with a sanitizer either --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f5572e79..ddd14b2d8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -336,9 +336,9 @@ jobs: IGRAPH_USE_SANITIZERS: 1 run: | pip install --prefer-binary '.[test]' - # Uninstall Matplotlib because Matplotlib-related tests cause false positives in the + # Uninstall NumPy, SciPy and Matplotlib because related tests cause false positives in the # leak sanitizer checks - pip uninstall -y matplotlib + pip uninstall -y numpy scipy matplotlib # Only pytest, and nothing else should be run in this section due to the presence of LD_PRELOAD. # The ASan/UBSan library versions need to be updated when switching to a newer Ubuntu/GCC. From 29852053e719370635948b20756c27ef6228b995 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 5 Jul 2025 02:24:38 +0200 Subject: [PATCH 220/276] ci: do not run pandas and pillow related tests with a sanitizer either --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ddd14b2d8..22654b974 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -336,9 +336,9 @@ jobs: IGRAPH_USE_SANITIZERS: 1 run: | pip install --prefer-binary '.[test]' - # Uninstall NumPy, SciPy and Matplotlib because related tests cause false positives in the + # Uninstall some test dependencies because related tests cause false positives in the # leak sanitizer checks - pip uninstall -y numpy scipy matplotlib + pip uninstall -y numpy scipy matplotlib pandas pillow # Only pytest, and nothing else should be run in this section due to the presence of LD_PRELOAD. # The ASan/UBSan library versions need to be updated when switching to a newer Ubuntu/GCC. From 2f0558e73bd2c2e79fa89f910ca72b79432b002b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 5 Jul 2025 02:31:26 +0200 Subject: [PATCH 221/276] ci: another attempt to fix the sanitizer test case --- .github/workflows/build.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 22654b974..3bd62dea2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -335,10 +335,9 @@ jobs: env: IGRAPH_USE_SANITIZERS: 1 run: | - pip install --prefer-binary '.[test]' - # Uninstall some test dependencies because related tests cause false positives in the - # leak sanitizer checks - pip uninstall -y numpy scipy matplotlib pandas pillow + # We cannot install the test dependency group because many test dependencies cause + # false positives in the sanitizer + pip install --prefer-binary networkx pytest pytest-timeout # Only pytest, and nothing else should be run in this section due to the presence of LD_PRELOAD. # The ASan/UBSan library versions need to be updated when switching to a newer Ubuntu/GCC. From 4f6964a26ff1414d3b1f2a8a3367a6ad25b01ec8 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 5 Jul 2025 02:33:34 +0200 Subject: [PATCH 222/276] ci: of course also install igraph itself --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3bd62dea2..17de35c24 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -338,6 +338,7 @@ jobs: # We cannot install the test dependency group because many test dependencies cause # false positives in the sanitizer pip install --prefer-binary networkx pytest pytest-timeout + pip install -e . # Only pytest, and nothing else should be run in this section due to the presence of LD_PRELOAD. # The ASan/UBSan library versions need to be updated when switching to a newer Ubuntu/GCC. From 32f446a117db60617dc69233511d62e7667a03ef Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 5 Jul 2025 02:14:41 +0200 Subject: [PATCH 223/276] ci: do not run numpy and scipy related tests with a sanitizer either --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 75b88b40f..6687be989 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -336,9 +336,9 @@ jobs: IGRAPH_USE_SANITIZERS: 1 run: | pip install --prefer-binary '.[test]' - # Uninstall Matplotlib because Matplotlib-related tests cause false positives in the + # Uninstall NumPy, SciPy and Matplotlib because related tests cause false positives in the # leak sanitizer checks - pip uninstall -y matplotlib + pip uninstall -y numpy scipy matplotlib # Only pytest, and nothing else should be run in this section due to the presence of LD_PRELOAD. # The ASan/UBSan library versions need to be updated when switching to a newer Ubuntu/GCC. From 66d861f63d7cf587be20dde64609de1c1131534c Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 5 Jul 2025 02:24:38 +0200 Subject: [PATCH 224/276] ci: do not run pandas and pillow related tests with a sanitizer either --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6687be989..bb7d10463 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -336,9 +336,9 @@ jobs: IGRAPH_USE_SANITIZERS: 1 run: | pip install --prefer-binary '.[test]' - # Uninstall NumPy, SciPy and Matplotlib because related tests cause false positives in the + # Uninstall some test dependencies because related tests cause false positives in the # leak sanitizer checks - pip uninstall -y numpy scipy matplotlib + pip uninstall -y numpy scipy matplotlib pandas pillow # Only pytest, and nothing else should be run in this section due to the presence of LD_PRELOAD. # The ASan/UBSan library versions need to be updated when switching to a newer Ubuntu/GCC. From 99228ab7fae7e5744c80ed04d4d63222566c3eb6 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 5 Jul 2025 02:31:26 +0200 Subject: [PATCH 225/276] ci: another attempt to fix the sanitizer test case --- .github/workflows/build.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bb7d10463..155eee960 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -335,10 +335,9 @@ jobs: env: IGRAPH_USE_SANITIZERS: 1 run: | - pip install --prefer-binary '.[test]' - # Uninstall some test dependencies because related tests cause false positives in the - # leak sanitizer checks - pip uninstall -y numpy scipy matplotlib pandas pillow + # We cannot install the test dependency group because many test dependencies cause + # false positives in the sanitizer + pip install --prefer-binary networkx pytest pytest-timeout # Only pytest, and nothing else should be run in this section due to the presence of LD_PRELOAD. # The ASan/UBSan library versions need to be updated when switching to a newer Ubuntu/GCC. From 7e212f9bd090f8bbb8856dbb522af07b6603a620 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 5 Jul 2025 02:33:34 +0200 Subject: [PATCH 226/276] ci: of course also install igraph itself --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 155eee960..25567bef0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -338,6 +338,7 @@ jobs: # We cannot install the test dependency group because many test dependencies cause # false positives in the sanitizer pip install --prefer-binary networkx pytest pytest-timeout + pip install -e . # Only pytest, and nothing else should be run in this section due to the presence of LD_PRELOAD. # The ASan/UBSan library versions need to be updated when switching to a newer Ubuntu/GCC. From b50dc6fab91038f9d1f6d7a6fd05eb3c5381fd78 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 6 Jul 2025 17:24:43 +0200 Subject: [PATCH 227/276] doc: add @ivar fields for Vertex and Edge, refs #813 --- src/_igraph/edgeobject.c | 9 +++++++++ src/_igraph/vertexobject.c | 3 +++ 2 files changed, 12 insertions(+) diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index ca26c7009..26cefc72c 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -724,6 +724,15 @@ PyDoc_STRVAR( " >>> e[\"weight\"] = 2 #doctest: +SKIP\n" " >>> print(e[\"weight\"]) #doctest: +SKIP\n" " 2\n" + "\n" + "@ivar source: Source vertex index of this edge\n" + "@ivar source_vertex: Source vertex of this edge\n" + "@ivar target: Target vertex index of this edge\n" + "@ivar target_vertex: Target vertex of this edge\n" + "@ivar tuple: Source and target vertex index of this edge as a tuple\n" + "@ivar vertex_tuple: Source and target vertex of this edge as a tuple\n" + "@ivar index: Index of this edge\n" + "@ivar graph: The graph the edge belongs to\t" ); int igraphmodule_Edge_register_type() { diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index b15e67d90..baad6b3e9 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -875,6 +875,9 @@ PyDoc_STRVAR( " >>> v[\"color\"] = \"red\" #doctest: +SKIP\n" " >>> print(v[\"color\"]) #doctest: +SKIP\n" " red\n" + "\n" + "@ivar index: Index of the vertex\n" + "@ivar graph: The graph the vertex belongs to\t" ); int igraphmodule_Vertex_register_type() { From 2a2515709d699bbbf4918c276632a490fca8a8de Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 6 Jul 2025 18:02:00 +0200 Subject: [PATCH 228/276] chore: add some progress info to scripts/mkdoc.sh --- scripts/mkdoc.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index 32ea358d2..af4e2ee45 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -47,6 +47,7 @@ cd ${ROOT_FOLDER} # Create a virtual environment if [ ! -d ".venv" ]; then + echo "Creating virtualenv..." ${PYTHON:-python3} -m venv .venv # Install sphinx, matplotlib, pandas, scipy, wheel and pydoctor into the venv. @@ -54,10 +55,12 @@ if [ ! -d ".venv" ]; then .venv/bin/pip install -q -U pip wheel sphinx==7.4.7 matplotlib pandas scipy pydoctor sphinx-rtd-theme else # Upgrade pip in the virtualenv + echo "Upgrading pip in virtualenv..." .venv/bin/pip install -q -U pip wheel fi # Make sure that Sphinx, PyDoctor (and maybe doc2dash) are up-to-date in the virtualenv +echo "Making sure that all dependencies are up-to-date..." .venv/bin/pip install -q -U sphinx==7.4.7 pydoctor sphinx-gallery sphinxcontrib-jquery sphinx-rtd-theme if [ x$DOC2DASH = x1 ]; then .venv/bin/pip install -U doc2dash From a1b909951f9b6b63698f40329623b1131ba9463a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:28:46 +0200 Subject: [PATCH 229/276] build(deps): bump pypa/cibuildwheel from 3.0.0 to 3.0.1 (#839) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 17de35c24..0dd578565 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,14 +19,14 @@ jobs: fetch-depth: 0 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v3.0.0 + uses: pypa/cibuildwheel@v3.0.1 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_x86_64" CIBW_ENABLE: pypy - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v3.0.0 + uses: pypa/cibuildwheel@v3.0.1 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_x86_64" @@ -47,7 +47,7 @@ jobs: fetch-depth: 0 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v3.0.0 + uses: pypa/cibuildwheel@v3.0.1 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -69,7 +69,7 @@ jobs: fetch-depth: 0 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v3.0.0 + uses: pypa/cibuildwheel@v3.0.1 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -132,7 +132,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v3.0.0 + uses: pypa/cibuildwheel@v3.0.1 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" @@ -238,7 +238,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v3.0.0 + uses: pypa/cibuildwheel@v3.0.1 env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From 554d38ec0062efe5f88d7f627e9d11951eb78095 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 10 Jul 2025 17:25:29 +0200 Subject: [PATCH 230/276] chore: updated C core; some tests will fail now --- doc/examples_sphinx-gallery/spanning_trees.py | 2 +- src/_igraph/bfsiter.c | 2 +- src/_igraph/dfsiter.c | 2 +- src/_igraph/graphobject.c | 296 +++++++----------- src/_igraph/igraphmodule.c | 9 + src/_igraph/indexing.c | 2 +- src/_igraph/vertexobject.c | 7 +- src/igraph/__init__.py | 17 + src/igraph/adjacency.py | 30 +- tests/test_basic.py | 27 ++ tests/test_generators.py | 8 +- tests/test_isomorphism.py | 3 +- vendor/source/igraph | 2 +- 13 files changed, 199 insertions(+), 208 deletions(-) diff --git a/doc/examples_sphinx-gallery/spanning_trees.py b/doc/examples_sphinx-gallery/spanning_trees.py index 82f190a8a..ac131cbd3 100644 --- a/doc/examples_sphinx-gallery/spanning_trees.py +++ b/doc/examples_sphinx-gallery/spanning_trees.py @@ -29,7 +29,7 @@ g = g.permute_vertices(permutation) new_layout = g.layout("grid") for i in range(36): - new_layout[permutation[i]] = layout[i] + new_layout[i] = layout[permutation[i]] layout = new_layout # %% diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c index e0c976245..15ddf86e3 100644 --- a/src/_igraph/bfsiter.c +++ b/src/_igraph/bfsiter.c @@ -167,7 +167,7 @@ static PyObject* igraphmodule_BFSIter_iternext(igraphmodule_BFSIterObject* self) igraph_integer_t parent = igraph_dqueue_int_pop(&self->queue); igraph_integer_t i, n; - if (igraph_neighbors(self->graph, &self->neis, vid, self->mode)) { + if (igraph_neighbors(self->graph, &self->neis, vid, self->mode, /* loops = */ 0, /* multiple = */ 0)) { igraphmodule_handle_igraph_error(); return NULL; } diff --git a/src/_igraph/dfsiter.c b/src/_igraph/dfsiter.c index e8273173c..dbb203f8c 100644 --- a/src/_igraph/dfsiter.c +++ b/src/_igraph/dfsiter.c @@ -192,7 +192,7 @@ static PyObject* igraphmodule_DFSIter_iternext(igraphmodule_DFSIterObject* self) /* the values above are returned at at this stage. However, we must * prepare for the next iteration by putting the next unvisited * neighbor onto the stack */ - if (igraph_neighbors(self->graph, &self->neis, vid, self->mode)) { + if (igraph_neighbors(self->graph, &self->neis, vid, self->mode, /* loops = */ 0, /* multiple = */ 0)) { igraphmodule_handle_igraph_error(); return NULL; } diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 0c1401f60..d16d8d2e4 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -1317,20 +1317,28 @@ PyObject *igraphmodule_Graph_count_multiple(igraphmodule_GraphObject *self, PyObject *igraphmodule_Graph_neighbors(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - PyObject *list, *dmode_o = Py_None, *index_o; + PyObject *list, *dmode_o = Py_None, *index_o, *loops_o = Py_True, *multiple_o = Py_True; igraph_neimode_t dmode = IGRAPH_ALL; + igraph_loops_t loops = IGRAPH_LOOPS; + igraph_bool_t multiple = 1; igraph_integer_t idx; igraph_vector_int_t res; - static char *kwlist[] = { "vertex", "mode", NULL }; + static char *kwlist[] = { "vertex", "mode", "loops", "multiple", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &index_o, &dmode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, &index_o, &dmode_o, &loops_o, &multiple_o)) return NULL; if (igraphmodule_PyObject_to_neimode_t(dmode_o, &dmode)) { return NULL; } + if (igraphmodule_PyObject_to_loops_t(loops_o, &loops)) { + return NULL; + } + + multiple = PyObject_IsTrue(multiple_o); + if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) { return NULL; } @@ -1340,7 +1348,7 @@ PyObject *igraphmodule_Graph_neighbors(igraphmodule_GraphObject * self, return NULL; } - if (igraph_neighbors(&self->g, &res, idx, dmode)) { + if (igraph_neighbors(&self->g, &res, idx, dmode, loops, multiple)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&res); return NULL; @@ -1366,20 +1374,25 @@ PyObject *igraphmodule_Graph_neighbors(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_incident(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - PyObject *list, *dmode_o = Py_None, *index_o; + PyObject *list, *dmode_o = Py_None, *index_o, *loops_o = Py_True; igraph_neimode_t dmode = IGRAPH_OUT; + igraph_loops_t loops = IGRAPH_LOOPS; igraph_integer_t idx; igraph_vector_int_t res; - static char *kwlist[] = { "vertex", "mode", NULL }; + static char *kwlist[] = { "vertex", "mode", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &index_o, &dmode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, &index_o, &dmode_o, &loops_o)) return NULL; if (igraphmodule_PyObject_to_neimode_t(dmode_o, &dmode)) { return NULL; } + if (igraphmodule_PyObject_to_loops_t(loops_o, &loops)) { + return NULL; + } + if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) { return NULL; } @@ -1389,7 +1402,7 @@ PyObject *igraphmodule_Graph_incident(igraphmodule_GraphObject * self, return NULL; } - if (igraph_incident(&self->g, &res, idx, dmode)) { + if (igraph_incident(&self->g, &res, idx, dmode, loops)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&res); return NULL; @@ -1428,80 +1441,6 @@ PyObject *igraphmodule_Graph_reciprocity(igraphmodule_GraphObject * self, return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); } -/** \ingroup python_interface_graph - * \brief The successors of a given vertex in an \c igraph.Graph - * This method accepts a single vertex ID as a parameter, and returns the - * successors of the given vertex in the form of an integer list. It - * is equivalent to calling \c igraph.Graph.neighbors with \c type=OUT - * - * \return the successor list as a Python list object - * \sa igraph_neighbors - */ -PyObject *igraphmodule_Graph_successors(igraphmodule_GraphObject * self, - PyObject * args, PyObject * kwds) -{ - PyObject *list, *index_o; - igraph_integer_t idx; - igraph_vector_int_t res; - - static char *kwlist[] = { "vertex", NULL }; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &index_o)) - return NULL; - - if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) - return NULL; - - igraph_vector_int_init(&res, 0); - if (igraph_neighbors(&self->g, &res, idx, IGRAPH_OUT)) { - igraphmodule_handle_igraph_error(); - igraph_vector_int_destroy(&res); - return NULL; - } - - list = igraphmodule_vector_int_t_to_PyList(&res); - igraph_vector_int_destroy(&res); - - return list; -} - -/** \ingroup python_interface_graph - * \brief The predecessors of a given vertex in an \c igraph.Graph - * This method accepts a single vertex ID as a parameter, and returns the - * predecessors of the given vertex in the form of an integer list. It - * is equivalent to calling \c igraph.Graph.neighbors with \c type=IN - * - * \return the predecessor list as a Python list object - * \sa igraph_neighbors - */ -PyObject *igraphmodule_Graph_predecessors(igraphmodule_GraphObject * self, - PyObject * args, PyObject * kwds) -{ - PyObject *list, *index_o; - igraph_integer_t idx; - igraph_vector_int_t res; - - static char *kwlist[] = { "vertex", NULL }; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &index_o)) - return NULL; - - if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) - return NULL; - - igraph_vector_int_init(&res, 1); - if (igraph_neighbors(&self->g, &res, idx, IGRAPH_IN)) { - igraphmodule_handle_igraph_error(); - igraph_vector_int_destroy(&res); - return NULL; - } - - list = igraphmodule_vector_int_t_to_PyList(&res); - igraph_vector_int_destroy(&res); - - return list; -} - /** \ingroup python_interface_graph * \brief Decides whether a graph is connected. * \return Py_True if the graph is connected, Py_False otherwise @@ -1689,30 +1628,24 @@ PyObject *igraphmodule_Graph_diameter(igraphmodule_GraphObject * self, if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) return NULL; - if (weights) { - if (igraph_diameter_dijkstra(&self->g, weights, &diameter, - /* from, to, vertex_path, edge_path */ - 0, 0, 0, 0, - PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); + if ( + igraph_diameter(&self->g, weights, &diameter, + /* from, to, vertex_path, edge_path */ + 0, 0, 0, 0, + PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected)) + ) { + igraphmodule_handle_igraph_error(); + if (weights) { igraph_vector_destroy(weights); free(weights); - return NULL; - } - igraph_vector_destroy(weights); free(weights); - return igraphmodule_real_t_to_PyObject(diameter, IGRAPHMODULE_TYPE_FLOAT); - } else { - if (igraph_diameter(&self->g, &diameter, - /* from, to, vertex_path, edge_path */ - 0, 0, 0, 0, - PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); - return NULL; } + return NULL; + } - /* The diameter is integer in this case, except if igraph_diameter() - * returned NaN or infinity for some reason */ - return igraphmodule_real_t_to_PyObject(diameter, IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT); + if (weights) { + igraph_vector_destroy(weights); free(weights); } + + return igraphmodule_real_t_to_PyObject(diameter, IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT); } /** \ingroup python_interface_graph @@ -1737,30 +1670,33 @@ PyObject *igraphmodule_Graph_get_diameter(igraphmodule_GraphObject * self, if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) return NULL; - igraph_vector_int_init(&res, 0); - if (weights) { - if (igraph_diameter_dijkstra(&self->g, weights, 0, - /* from, to, vertex_path, edge_path */ - 0, 0, &res, 0, - PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); + if (igraph_vector_int_init(&res, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if ( + igraph_diameter(&self->g, weights, 0, + /* from, to, vertex_path, edge_path */ + 0, 0, &res, 0, + PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected) + ) + ) { + igraphmodule_handle_igraph_error(); + if (weights) { igraph_vector_destroy(weights); free(weights); - igraph_vector_int_destroy(&res); - return NULL; } + igraph_vector_int_destroy(&res); + return NULL; + } + + if (weights) { igraph_vector_destroy(weights); free(weights); - } else { - if (igraph_diameter(&self->g, 0, - /* from, to, vertex_path, edge_path */ - 0, 0, &res, 0, - PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); - return NULL; - } } result_o = igraphmodule_vector_int_t_to_PyList(&res); igraph_vector_int_destroy(&res); + return result_o; } @@ -1787,46 +1723,29 @@ PyObject *igraphmodule_Graph_farthest_points(igraphmodule_GraphObject * self, if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) return NULL; - if (weights) { - if (igraph_diameter_dijkstra(&self->g, weights, &len, - /* from, to, vertex_path, edge_path */ - &from, &to, 0, 0, - PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); + if ( + igraph_diameter( + &self->g, weights, &len, + /* from, to, vertex_path, edge_path */ + &from, &to, 0, 0, + PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected) + ) + ) { + igraphmodule_handle_igraph_error(); + if (weights) { igraph_vector_destroy(weights); free(weights); - return NULL; } + return NULL; + } + + if (weights) { igraph_vector_destroy(weights); free(weights); - if (from >= 0) { - return Py_BuildValue("nnd", (Py_ssize_t)from, (Py_ssize_t)to, (double)len); - } else { - return Py_BuildValue("OOd", Py_None, Py_None, (double)len); - } - } else { - if (igraph_diameter(&self->g, &len, - /* from, to, vertex_path, edge_path */ - &from, &to, 0, 0, - PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); - return NULL; - } + } - /* if len is finite and integer (which it typically is, unless it's - * infinite), then return a Python int as the third value; otherwise - * return a float */ - if (ceil(len) == len && isfinite(len)) { - if (from >= 0) { - return Py_BuildValue("nnn", (Py_ssize_t)from, (Py_ssize_t)to, (Py_ssize_t)len); - } else { - return Py_BuildValue("OOn", Py_None, Py_None, (Py_ssize_t)len); - } - } else { - if (from >= 0) { - return Py_BuildValue("nnd", (Py_ssize_t)from, (Py_ssize_t)to, (double)len); - } else { - return Py_BuildValue("OOd", Py_None, Py_None, (double)len); - } - } + if (from >= 0) { + return Py_BuildValue("nnd", (Py_ssize_t)from, (Py_ssize_t)to, (double)len); + } else { + return Py_BuildValue("OOd", Py_None, Py_None, (double)len); } } @@ -2019,7 +1938,7 @@ PyObject *igraphmodule_Graph_radius(igraphmodule_GraphObject * self, return NULL; } - if (igraph_radius_dijkstra(&self->g, weights, &radius, mode)) { + if (igraph_radius(&self->g, weights, &radius, mode)) { if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -3095,7 +3014,7 @@ PyObject *igraphmodule_Graph_LCF(PyTypeObject *type, if (igraphmodule_PyObject_to_vector_int_t(shifts_o, &shifts)) return NULL; - if (igraph_lcf_vector(&g, n, &shifts, repeats)) { + if (igraph_lcf(&g, n, &shifts, repeats)) { igraph_vector_int_destroy(&shifts); igraphmodule_handle_igraph_error(); return NULL; @@ -4271,19 +4190,16 @@ PyObject *igraphmodule_Graph_average_path_length(igraphmodule_GraphObject * if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) return NULL; - if (weights) { - if (igraph_average_path_length_dijkstra(&self->g, &res, 0, weights, PyObject_IsTrue(directed), PyObject_IsTrue(unconn))) { + if (igraph_average_path_length(&self->g, weights, &res, 0, PyObject_IsTrue(directed), PyObject_IsTrue(unconn))) { + if (weights) { igraph_vector_destroy(weights); free(weights); - igraphmodule_handle_igraph_error(); - return NULL; } + igraphmodule_handle_igraph_error(); + return NULL; + } + if (weights) { igraph_vector_destroy(weights); free(weights); - } else { - if (igraph_average_path_length(&self->g, &res, 0, PyObject_IsTrue(directed), PyObject_IsTrue(unconn))) { - igraphmodule_handle_igraph_error(); - return NULL; - } } return PyFloat_FromDouble(res); @@ -5226,7 +5142,7 @@ PyObject *igraphmodule_Graph_eccentricity(igraphmodule_GraphObject* self, return NULL; } - if (igraph_eccentricity_dijkstra(&self->g, weights, &res, vs, mode)) { + if (igraph_eccentricity(&self->g, weights, &res, vs, mode)) { if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -7622,7 +7538,7 @@ PyObject *igraphmodule_Graph_motifs_randesu(igraphmodule_GraphObject *self, PyObject *igraphmodule_Graph_motifs_randesu_no(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { igraph_vector_t cut_prob; - igraph_integer_t res; + igraph_real_t res; Py_ssize_t size = 3; PyObject* cut_prob_list=Py_None; static char* kwlist[] = {"size", "cut_prob", NULL}; @@ -7649,7 +7565,7 @@ PyObject *igraphmodule_Graph_motifs_randesu_no(igraphmodule_GraphObject *self, } igraph_vector_destroy(&cut_prob); - return igraphmodule_integer_t_to_PyObject(res); + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT); } /** \ingroup python_interface_graph @@ -7660,7 +7576,7 @@ PyObject *igraphmodule_Graph_motifs_randesu_no(igraphmodule_GraphObject *self, PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { igraph_vector_t cut_prob; - igraph_integer_t res; + igraph_real_t res; Py_ssize_t size = 3; PyObject* cut_prob_list=Py_None; PyObject *sample=Py_None; @@ -7718,7 +7634,7 @@ PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *s } igraph_vector_destroy(&cut_prob); - return igraphmodule_integer_t_to_PyObject(res); + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT); } /** \ingroup python_interface_graph @@ -14325,24 +14241,21 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_neighbors */ {"neighbors", (PyCFunction) igraphmodule_Graph_neighbors, METH_VARARGS | METH_KEYWORDS, - "neighbors(vertex, mode=\"all\")\n--\n\n" + "neighbors(vertex, mode=\"all\", loops=\"twice\", multiple=True)\n--\n\n" "Returns adjacent vertices to a given vertex.\n\n" "@param vertex: a vertex ID\n" "@param mode: whether to return only successors (C{\"out\"}),\n" " predecessors (C{\"in\"}) or both (C{\"all\"}). Ignored for undirected\n" - " graphs."}, - - {"successors", (PyCFunction) igraphmodule_Graph_successors, - METH_VARARGS | METH_KEYWORDS, - "successors(vertex)\n--\n\n" - "Returns the successors of a given vertex.\n\n" - "Equivalent to calling the L{neighbors()} method with type=C{\"out\"}."}, - - {"predecessors", (PyCFunction) igraphmodule_Graph_predecessors, - METH_VARARGS | METH_KEYWORDS, - "predecessors(vertex)\n--\n\n" - "Returns the predecessors of a given vertex.\n\n" - "Equivalent to calling the L{neighbors()} method with type=C{\"in\"}."}, + " graphs." + "@param loops: whether to return loops in I{undirected} graphs once\n" + " (C{\"once\"}), twice (C{\"twice\"}) or not at all (C{\"ignore\"}). C{False}\n" + " is accepted as an alias to C{\"ignore\"} and C{True} is accepted as an\n" + " alias to C{\"twice\"}. For directed graphs, C{\"twice\"} is equivalent\n" + " to C{\"once\"} (except when C{mode} is C{\"all\"} because the graph is\n" + " then treated as undirected).\n" + "@param multiple: whether to return endpoints of multiple edges as many\n" + " times as their multiplicities." + }, /* interface to igraph_get_eid */ {"get_eid", (PyCFunction) igraphmodule_Graph_get_eid, @@ -14386,7 +14299,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param vertex: a vertex ID\n" "@param mode: whether to return only successors (C{\"out\"}),\n" " predecessors (C{\"in\"}) or both (C{\"all\"}). Ignored for undirected\n" - " graphs."}, + " graphs." + "@param loops: whether to return loops in I{undirected} graphs once\n" + " (C{\"once\"}), twice (C{\"twice\"}) or not at all (C{\"ignore\"}). C{False}\n" + " is accepted as an alias to C{\"ignore\"} and C{True} is accepted as an\n" + " alias to C{\"twice\"}. For directed graphs, C{\"twice\"} is equivalent\n" + " to C{\"once\"} (except when C{mode} is C{\"all\"} because the graph is\n" + " then treated as undirected).\n" + }, ////////////////////// // GRAPH GENERATORS // @@ -16230,8 +16150,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "permute_vertices(permutation)\n--\n\n" "Permutes the vertices of the graph according to the given permutation\n" "and returns the new graph.\n\n" - "Vertex M{k} of the original graph will become vertex M{permutation[k]}\n" - "in the new graph. No validity checks are performed on the permutation\n" + "Vertex M{k} of the new graph will belong to vertex M{permutation[k]}\n" + "in the original graph. No validity checks are performed on the permutation\n" "vector.\n\n" "@return: the new graph\n" }, diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index f7ca0490a..db265aa6e 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -1031,6 +1031,7 @@ PyObject* PyInit__igraph(void) PyObject* m; static void *PyIGraph_API[PyIGraph_API_pointers]; PyObject *c_api_object; + igraph_error_t retval; /* Prevent linking 64-bit igraph to 32-bit Python */ PY_IGRAPH_ASSERT_AT_BUILD_TIME(sizeof(igraph_integer_t) >= sizeof(Py_ssize_t)); @@ -1043,6 +1044,14 @@ PyObject* PyInit__igraph(void) INITERROR; } + /* Initialize the igraph library */ + retval = igraph_setup(); + if (retval != IGRAPH_SUCCESS) { + PyErr_Format(PyExc_RuntimeError, "Failed to initialize the C core of " + "the igraph library, code: %d", retval); + INITERROR; + } + /* Run basic initialization of the pyhelpers.c module */ if (igraphmodule_helpers_init()) { INITERROR; diff --git a/src/_igraph/indexing.c b/src/_igraph/indexing.c index 5643cabba..f5f30884b 100644 --- a/src/_igraph/indexing.c +++ b/src/_igraph/indexing.c @@ -142,7 +142,7 @@ static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, /* Simple case: all edges */ IGRAPH_PYCHECK(igraph_vector_int_init(&eids, 0)); IGRAPH_FINALLY(igraph_vector_int_destroy, &eids); - IGRAPH_PYCHECK(igraph_incident(graph, &eids, from, neimode)); + IGRAPH_PYCHECK(igraph_incident(graph, &eids, from, neimode, IGRAPH_LOOPS)); n = igraph_vector_int_size(&eids); result = igraphmodule_PyList_Zeroes(igraph_vcount(graph)); diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index b15e67d90..3760bb81d 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -840,10 +840,10 @@ PyMethodDef igraphmodule_Vertex_methods[] = { GRAPH_PROXY_METHOD_SPEC(neighbors, "neighbors"), GRAPH_PROXY_METHOD_SPEC_3(outdegree, "outdegree"), GRAPH_PROXY_METHOD_SPEC_3(pagerank, "pagerank"), - GRAPH_PROXY_METHOD_SPEC(predecessors, "predecessors"), + GRAPH_PROXY_METHOD_SPEC_3(predecessors, "predecessors"), GRAPH_PROXY_METHOD_SPEC(personalized_pagerank, "personalized_pagerank"), GRAPH_PROXY_METHOD_SPEC(strength, "strength"), - GRAPH_PROXY_METHOD_SPEC(successors, "successors"), + GRAPH_PROXY_METHOD_SPEC_3(successors, "successors"), {NULL} }; @@ -875,6 +875,9 @@ PyDoc_STRVAR( " >>> v[\"color\"] = \"red\" #doctest: +SKIP\n" " >>> print(v[\"color\"]) #doctest: +SKIP\n" " red\n" + "\n" + "@ivar index: Index of the vertex\n" + "@ivar graph: The graph the vertex belongs to\n" ); int igraphmodule_Vertex_register_type() { diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 78786b980..16f41d6ed 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -582,6 +582,23 @@ def is_weighted(self): """ return "weight" in self.edge_attributes() + ############################################# + # Neighbors + + def predecessors(self, vertex, loops=True, multiple=True): + """Returns the predecessors of a given vertex. + + Equivalent to calling the L{Graph.neighbors()} method with mode=C{\"in\"}. + """ + return self.neighbors(vertex, mode="in", loops=loops, multiple=multiple) + + def successors(self, vertex, loops=True, multiple=True): + """Returns the successors of a given vertex. + + Equivalent to calling the L{Graph.neighbors()} method with mode=C{\"out\"}. + """ + return self.neighbors(vertex, mode="out", loops=loops, multiple=multiple) + ############################################# # Vertex and edge sequence @property diff --git a/src/igraph/adjacency.py b/src/igraph/adjacency.py index b38889c81..f941ec822 100644 --- a/src/igraph/adjacency.py +++ b/src/igraph/adjacency.py @@ -120,19 +120,27 @@ def _get_adjacency_sparse(self, attribute=None): return mtx -def _get_adjlist(self, mode="out"): +def _get_adjlist(self, mode="out", loops="twice", multiple=True): """Returns the adjacency list representation of the graph. The adjacency list representation is a list of lists. Each item of the outer list belongs to a single vertex of the graph. The inner list contains the neighbors of the given vertex. - @param mode: if C{\"out\"}, returns the successors of the vertex. If - C{\"in\"}, returns the predecessors of the vertex. If C{\"all\"}, both + @param mode: if C{"out"}, returns the successors of the vertex. If + C{"in"}, returns the predecessors of the vertex. If C{"all"}, both the predecessors and the successors will be returned. Ignored for undirected graphs. + @param loops: whether to return loops in I{undirected} graphs once + (C{"once"}), twice (C{"twice"}) or not at all (C{"ignore"}). C{False} + is accepted as an alias to C{"ignore"} and C{True} is accepted as an + alias to C{"twice"}. For directed graphs, C{"twice"} is equivalent + to C{"once"} (except when C{mode} is C{"all"} because the graph is + then treated as undirected). + @param multiple: whether to return endpoints of multiple edges as many + times as their multiplicities. """ - return [self.neighbors(idx, mode) for idx in range(self.vcount())] + return [self.neighbors(idx, mode, loops, multiple) for idx in range(self.vcount())] def _get_biadjacency(graph, types="type", *args, **kwds): @@ -153,7 +161,7 @@ def _get_biadjacency(graph, types="type", *args, **kwds): return super(Graph, graph).get_biadjacency(types, *args, **kwds) -def _get_inclist(graph, mode="out"): +def _get_inclist(graph, mode="out", loops="twice"): """Returns the incidence list representation of the graph. The incidence list representation is a list of lists. Each @@ -161,9 +169,15 @@ def _get_inclist(graph, mode="out"): The inner list contains the IDs of the incident edges of the given vertex. - @param mode: if C{\"out\"}, returns the successors of the vertex. If - C{\"in\"}, returns the predecessors of the vertex. If C{\"all\"}, both + @param mode: if C{"out"}, returns the successors of each vertex. If + C{"in"}, returns the predecessors of each vertex. If C{"all"}, both the predecessors and the successors will be returned. Ignored for undirected graphs. + @param loops: whether to return loops in I{undirected} graphs once + (C{"once"}), twice (C{"twice"}) or not at all (C{"ignore"}). C{False} + is accepted as an alias to C{"ignore"} and C{True} is accepted as an + alias to C{"twice"}. For directed graphs, C{"twice"} is equivalent + to C{"once"} (except when C{mode} is C{"all"} because the graph is + then treated as undirected). """ - return [graph.incident(idx, mode) for idx in range(graph.vcount())] + return [graph.incident(idx, mode, loops) for idx in range(graph.vcount())] diff --git a/tests/test_basic.py b/tests/test_basic.py index ddda4f2d2..35d511b1b 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -350,6 +350,20 @@ def testAdjacency(self): self.assertTrue(g.get_adjlist(IN) == [[2], [0], [1], [2]]) self.assertTrue(g.get_adjlist(ALL) == [[1, 2], [0, 2], [0, 1, 3], [2]]) + def testAdjacencyWithLoopsAndMultiEdges(self): + g = Graph(4, [(0, 0), (0, 1), (1, 2), (2, 0), (2, 3), (2, 3), (3, 3), (3, 3)], directed=True) + + self.assertTrue(g.neighbors(2) == [0, 1, 3, 3]) + self.assertTrue(g.predecessors(2) == [1]) + self.assertTrue(g.successors(2) == [0, 3, 3]) + + self.assertTrue(g.get_adjlist() == [[0, 1], [2], [0, 3, 3], [3, 3]]) + self.assertTrue(g.get_adjlist(IN) == [[0, 2], [0], [1], [2, 2, 3, 3]]) + self.assertTrue(g.get_adjlist(ALL) == [[0, 0, 1, 2], [0, 2], [0, 1, 3, 3], [2, 2, 3, 3, 3, 3]]) + + self.assertTrue(g.get_adjlist(ALL, loops="once") == [[0, 1, 2], [0, 2], [0, 1, 3, 3], [2, 2, 3, 3]]) + self.assertTrue(g.get_adjlist(ALL, loops="once", multiple=False) == [[0, 1, 2], [0, 2], [0, 1, 3], [2, 3]]) + def testEdgeIncidence(self): g = Graph(4, [(0, 1), (1, 2), (2, 0), (2, 3)], directed=True) self.assertTrue(g.incident(2) == [2, 3]) @@ -359,6 +373,19 @@ def testEdgeIncidence(self): self.assertTrue(g.get_inclist(IN) == [[2], [0], [1], [3]]) self.assertTrue(g.get_inclist(ALL) == [[0, 2], [0, 1], [2, 1, 3], [3]]) + def testEdgeIncidenceWithLoopsAndMultiEdges(self): + g = Graph(4, [(0, 1), (1, 2), (2, 0), (2, 3), (2, 3), (0, 0), (3, 3), (3, 3)], directed=True) + + self.assertTrue(g.incident(2) == [2, 4, 3]) + self.assertTrue(g.incident(2, IN) == [1]) + self.assertTrue(g.incident(2, ALL) == [2, 1, 4, 3]) + + self.assertTrue(g.get_inclist() == [[5, 0], [1], [2, 4, 3], [7, 6]]) + self.assertTrue(g.get_inclist(IN) == [[5, 2], [0], [1], [4, 3, 7, 6]]) + self.assertTrue(g.get_inclist(ALL) == [[5, 5, 0, 2], [0, 1], [2, 1, 4, 3], [4, 3, 7, 7, 6, 6]]) + + self.assertTrue(g.get_inclist(ALL, loops="once") == [[5, 0, 2], [0, 1], [2, 1, 4, 3], [4, 3, 7, 6]]) + def testMultiplesLoops(self): g = Graph.Tree(7, 2) diff --git a/tests/test_generators.py b/tests/test_generators.py index 3925eb7bd..feac15d50 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -537,14 +537,14 @@ def testAdjacencyNumPyLoopHandling(self): el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertListEqual(sorted(el), [(0, 1), (1, 3), (2, 2)]) + self.assertListEqual(sorted(el), [(0, 1), (1, 3), (2, 2), (2, 2)]) # ADJ UPPER g = Graph.Adjacency(mat, mode="upper", loops="twice") el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - self.assertListEqual(sorted(el), [(0, 1), (0, 2), (2, 2)]) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (2, 2), (2, 2)]) # ADJ_DIRECTED (default) g = Graph.Adjacency(mat, loops=False) @@ -705,7 +705,7 @@ def testWeightedAdjacency(self): g = Graph.Weighted_Adjacency(mat, attr="w0", loops="twice") el = g.get_edgelist() self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2), (2, 2)]) - self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 1.25]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 2.5]) @unittest.skipIf(np is None, "test case depends on NumPy") def testWeightedAdjacencyNumPy(self): @@ -731,7 +731,7 @@ def testWeightedAdjacencyNumPy(self): g = Graph.Weighted_Adjacency(mat, attr="w0", loops="twice") el = g.get_edgelist() self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2), (2, 2)]) - self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 1.25]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 2.5]) @unittest.skipIf( (sparse is None) or (np is None), "test case depends on NumPy/SciPy" diff --git a/tests/test_isomorphism.py b/tests/test_isomorphism.py index c1150da63..7984e9b34 100644 --- a/tests/test_isomorphism.py +++ b/tests/test_isomorphism.py @@ -359,6 +359,7 @@ def testCanonicalPermutation(self): perm = list(range(10)) shuffle(perm) g2 = g.permute_vertices(perm) + self.assertTrue(g.isomorphic(g2)) g3 = g.permute_vertices(g.canonical_permutation()) g4 = g2.permute_vertices(g2.canonical_permutation()) @@ -400,7 +401,7 @@ def testPermuteVertices(self): (7, 4), ], ) - _, _, mapping = g1.isomorphic_vf2(g2, return_mapping_21=True) + _, mapping, _ = g1.isomorphic_vf2(g2, return_mapping_12=True) g3 = g2.permute_vertices(mapping) self.assertTrue(g3.vcount() == g2.vcount() and g3.ecount() == g2.ecount()) self.assertTrue(set(g3.get_edgelist()) == set(g1.get_edgelist())) diff --git a/vendor/source/igraph b/vendor/source/igraph index e101e1597..4301f6e76 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit e101e15975b6b07eaae33390005a5637d23debce +Subproject commit 4301f6e769d7326da4fdea19a5f5c805990e395f From fde5ba83d437a420f1bdc3e7754370c9f7a34ed6 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 18 Jul 2025 16:34:06 +0200 Subject: [PATCH 231/276] ci: ninja is auto-installed in the macOS image --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 25567bef0..87cefcde0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,6 +3,7 @@ name: Build and test on: [push, pull_request] env: + CIBW_ENABLE: pypy CIBW_ENVIRONMENT_PASS_LINUX: PYTEST_TIMEOUT CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test]' && python -m pytest -v tests" CIBW_SKIP: "cp38-* pp38-*" @@ -118,7 +119,7 @@ jobs: - name: Install OS dependencies if: steps.cache-c-core.outputs.cache-hit != 'true' || steps.cache-c-deps.outputs.cache-hit != 'true' # Only needed when building the C core or libomp - run: brew install ninja autoconf automake libtool + run: brew install autoconf automake libtool - name: Install OpenMP library if: steps.cache-c-deps.outputs.cache-hit != 'true' From 4ff5c92d4349e6021dacb98be0b65720b8d687f5 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 18 Jul 2025 17:54:33 +0200 Subject: [PATCH 232/276] ci: use CIBW_PROJECT_REQUIRES_PYTHON --- .github/workflows/build.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87cefcde0..687ddf04b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,8 +5,8 @@ on: [push, pull_request] env: CIBW_ENABLE: pypy CIBW_ENVIRONMENT_PASS_LINUX: PYTEST_TIMEOUT + CIBW_PROJECT_REQUIRES_PYTHON: ">=3.9" CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test]' && python -m pytest -v tests" - CIBW_SKIP: "cp38-* pp38-*" PYTEST_TIMEOUT: 60 jobs: @@ -24,7 +24,6 @@ jobs: env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_x86_64" - CIBW_ENABLE: pypy - name: Build wheels (musllinux) uses: pypa/cibuildwheel@v3.0.0 @@ -53,7 +52,6 @@ jobs: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 CIBW_BUILD: "*-manylinux_aarch64" - CIBW_ENABLE: pypy - uses: actions/upload-artifact@v4 with: @@ -137,7 +135,6 @@ jobs: env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" - CIBW_ENABLE: pypy CIBW_ENVIRONMENT: "LDFLAGS=-L$HOME/local/lib" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_arch }} ${{ matrix.cmake_extra_args }} -DCMAKE_PREFIX_PATH=$HOME/local @@ -243,7 +240,6 @@ jobs: env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" - CIBW_ENABLE: pypy CIBW_TEST_COMMAND: 'cd /d {project} && pip install --prefer-binary ".[${{ matrix.test_extra }}]" && python -m pytest tests' # Skip tests for Python 3.10 onwards because SciPy does not have # 32-bit wheels for Windows any more From 104c0801fe67fdde4cb458ef82afea9b2257deb4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 19 Jul 2025 15:41:51 +0200 Subject: [PATCH 233/276] fix: fixed failing unit tests, now we are up-to-date again with the develop branch of the C core --- src/_igraph/graphobject.c | 2 +- src/igraph/operators/functions.py | 18 ++++++++++++++---- vendor/source/igraph | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index d16d8d2e4..6583a540f 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -10144,7 +10144,7 @@ PyObject *igraphmodule_Graph_canonical_permutation( if (igraphmodule_attrib_to_vector_int_t(color_o, self, &color, ATTRIBUTE_TYPE_VERTEX)) return NULL; - retval = igraph_canonical_permutation_bliss(&self->g, color, &labeling, sh, 0); + retval = igraph_canonical_permutation(&self->g, color, &labeling); if (color) { igraph_vector_int_destroy(color); free(color); } diff --git a/src/igraph/operators/functions.py b/src/igraph/operators/functions.py index b305f5cb8..4ba3ede43 100644 --- a/src/igraph/operators/functions.py +++ b/src/igraph/operators/functions.py @@ -158,7 +158,7 @@ def union(graphs, byname="auto"): raise RuntimeError( f"Some graphs are not named (got {n_named} named, {ngr-n_named} unnamed)" ) - # Now we know that byname is only used is all graphs are named + # Now we know that byname is only used if all graphs are named # Trivial cases if ngr == 0: @@ -184,7 +184,12 @@ def union(graphs, byname="auto"): # Reorder vertices to match uninames # vertex k -> p[k] permutation = [permutation_map[x] for x in ng.vs["name"]] - ng = ng.permute_vertices(permutation) + + # permute_vertices() needs the inverse permutation + inv_permutation = [0] * len(permutation) + for i, x in enumerate(permutation): + inv_permutation[x] = i + ng = ng.permute_vertices(inv_permutation) newgraphs.append(ng) else: @@ -353,7 +358,7 @@ def intersection(graphs, byname="auto", keep_all_vertices=True): raise RuntimeError( f"Some graphs are not named (got {n_named} named, {ngr-n_named} unnamed)" ) - # Now we know that byname is only used is all graphs are named + # Now we know that byname is only used if all graphs are named # Trivial cases if ngr == 0: @@ -389,7 +394,12 @@ def intersection(graphs, byname="auto", keep_all_vertices=True): # Reorder vertices to match uninames # vertex k -> p[k] permutation = [permutation_map[x] for x in ng.vs["name"]] - ng = ng.permute_vertices(permutation) + + # permute_vertices() needs the inverse permutation + inv_permutation = [0] * len(permutation) + for i, x in enumerate(permutation): + inv_permutation[x] = i + ng = ng.permute_vertices(inv_permutation) newgraphs.append(ng) else: diff --git a/vendor/source/igraph b/vendor/source/igraph index 4301f6e76..0f9ba000d 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 4301f6e769d7326da4fdea19a5f5c805990e395f +Subproject commit 0f9ba000d4cd6452a81feb593e93042235f3199f From 132c0c79c0add4d5e2905babce59f20f7a7708d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sun, 20 Jul 2025 13:34:41 +0800 Subject: [PATCH 234/276] minor cleanup / rewrite for conciseness --- .../stochastic_variability.py | 155 ++++++++++-------- 1 file changed, 85 insertions(+), 70 deletions(-) diff --git a/doc/examples_sphinx-gallery/stochastic_variability.py b/doc/examples_sphinx-gallery/stochastic_variability.py index 944f82118..ea126aecf 100644 --- a/doc/examples_sphinx-gallery/stochastic_variability.py +++ b/doc/examples_sphinx-gallery/stochastic_variability.py @@ -5,52 +5,61 @@ Stochastic Variability in Community Detection Algorithms ========================================================= -This example demonstrates the variability of stochastic community detection methods by analyzing the consistency of multiple partitions using similarity measures normalized mutual information (NMI), variation of information (VI), rand index (RI) on both random and structured graphs. +This example demonstrates the use of stochastic community detection methods to check whether a network possesses a strong community structure, and whether the partitionings we obtain are meaningul. Many community detection algorithms are randomized, and return somewhat different results after each run, depending on the random seed that was set. When there is a robust community structure, we expect these results to be similar to each other. When the community structure is weak or non-existent, the results may be noisy and highly variable. We will employ several partion similarity measures to analyse the consistency of the results, including the normalized mutual information (NMI), the variation of information (VI), and the Rand index (RI). """ # %% -# Import libraries import igraph as ig import matplotlib.pyplot as plt import itertools +import random # %% -# First, we generate a graph. -# Load the karate club network +# .. note:: +# We set a random seed to ensure that the results look exactly the same in +# the gallery. You don't need to do this when exploring randomness. +random.seed(42) + +# %% +# We will use Zachary's karate club dataset [1]_, a classic example of a network +# with a strong community structure: karate = ig.Graph.Famous("Zachary") # %% -#For the random graph, we use an Erdős-Rényi :math:`G(n, m)` model, where 'n' is the number of nodes -#and 'm' is the number of edges. We set 'm' to match the edge count of the empirical (Karate Club) -#network to ensure structural similarity in terms of connectivity, making comparisons meaningful. -n_nodes = karate.vcount() -n_edges = karate.ecount() -#Generate an Erdős-Rényi graph with the same number of nodes and edges -random_graph = ig.Graph.Erdos_Renyi(n=n_nodes, m=n_edges) +# We will compare it to an an Erdős-Rényi :math:`G(n, m)` random network having +# the same number of vertices and edges. The parameters 'n' and 'm' refer to the +# vertex and edge count, respectively. Since this is a random network, it should +# have no community structure. +random_graph = ig.Graph.Erdos_Renyi(n=karate.vcount(), m=karate.ecount()) # %% -# Now, lets plot the graph to visually understand them. +# First, let us plot the two networks for a visual comparison: # Create subplots -fig, axes = plt.subplots(1, 2, figsize=(12, 6)) +fig, axes = plt.subplots(1, 2, figsize=(12, 6), subplot_kw={'aspect': 'equal'}) -# Karate Club Graph -layout_karate = karate.layout("fr") +# Karate club network ig.plot( - karate, layout=layout_karate, target=axes[0], vertex_size=30, vertex_color="lightblue", edge_width=1, - vertex_label=[str(v.index) for v in karate.vs], vertex_label_size=10 + karate, target=axes[0], + vertex_color="lightblue", vertex_size=30, + vertex_label=range(karate.vcount()), vertex_label_size=10, + edge_width=1 ) -axes[0].set_title("Karate Club Network") +axes[0].set_title("Karate club network") -# Erdős-Rényi Graph -layout_random = random_graph.layout("fr") +# Random network ig.plot( - random_graph, layout=layout_random, target=axes[1], vertex_size=30, vertex_color="lightcoral", edge_width=1, - vertex_label=[str(v.index) for v in random_graph.vs], vertex_label_size=10 + random_graph, target=axes[1], + vertex_color="lightcoral", vertex_size=30, + vertex_label=range(random_graph.vcount()), vertex_label_size=10, + edge_width=1 ) -axes[1].set_title("Erdős-Rényi Random Graph") +axes[1].set_title("Erdős-Rényi random network") + +plt.show() + # %% -# Function to compute similarity between partitions +# Function to compute similarity between partitions using various methods: def compute_pairwise_similarity(partitions, method): similarities = [] @@ -61,74 +70,80 @@ def compute_pairwise_similarity(partitions, method): return similarities # %% -# We have used, stochastic community detection using the Louvain method, iteratively generating partitions and computing similarity metrics to assess stability. -# The Louvain method is a modularity maximization approach for community detection. -# Since exact modularity maximization is NP-hard, the algorithm employs a greedy heuristic that processes vertices in a random order. -# This randomness leads to variations in the detected communities across different runs, which is why results may differ each time the method is applied. -def run_experiment(graph, iterations=50): - partitions = [graph.community_multilevel().membership for _ in range(iterations)] +# The Leiden method, accessible through :meth:`igraph.Graph.community_leiden()`, +# is a modularity maximization approach for community detection. Since exact +# modularity maximization is NP-hard, the algorithm employs a greedy heuristic +# that processes vertices in a random order. This randomness leads to +# variation in the detected communities across different runs, which is why +# results may differ each time the method is applied. The following function +# runs the Leiden algorithm multiple times: +def run_experiment(graph, iterations=100): + partitions = [graph.community_leiden(objective_function='modularity').membership for _ in range(iterations)] nmi_scores = compute_pairwise_similarity(partitions, method="nmi") vi_scores = compute_pairwise_similarity(partitions, method="vi") ri_scores = compute_pairwise_similarity(partitions, method="rand") return nmi_scores, vi_scores, ri_scores # %% -# Run experiments +# Run the experiment on both networks: nmi_karate, vi_karate, ri_karate = run_experiment(karate) nmi_random, vi_random, ri_random = run_experiment(random_graph) # %% -# Lastly, lets plot probability density histograms to understand the result. -fig, axes = plt.subplots(3, 2, figsize=(12, 10)) +# Finally, let us plot histograms of the pairwise similarities of the obtained +# partitionings to understand the result: +fig, axes = plt.subplots(2, 3, figsize=(12, 6)) measures = [ - (nmi_karate, nmi_random, "NMI", 0, 1), # Normalized Mutual Information (0-1, higher = more similar) - (vi_karate, vi_random, "VI", 0, None), # Variation of Information (0+, lower = more similar) - (ri_karate, ri_random, "RI", 0, 1), # Rand Index (0-1, higher = more similar) + # Normalized Mutual Information (0-1, higher = more similar) + (nmi_karate, nmi_random, "NMI", 0, 1), + # Variation of Information (0+, lower = more similar) + (vi_karate, vi_random, "VI", 0, max(vi_karate + vi_random)), + # Rand Index (0-1, higher = more similar) + (ri_karate, ri_random, "RI", 0, 1), ] colors = ["red", "blue", "green"] for i, (karate_scores, random_scores, measure, lower, upper) in enumerate(measures): - # Karate Club histogram - axes[i][0].hist( - karate_scores, bins=20, alpha=0.7, color=colors[i], edgecolor="black", - density=True # Probability density + # Karate club histogram + axes[0][i].hist( + karate_scores, bins=20, range=(lower, upper), + density=True, # Probability density + alpha=0.7, color=colors[i], edgecolor="black" ) - axes[i][0].set_title(f"Probability Density of {measure} - Karate Club Network") - axes[i][0].set_xlabel(f"{measure} Score") - axes[i][0].set_ylabel("Density") - axes[i][0].set_xlim(lower, upper) # Set axis limits explicitly - - # Erdős-Rényi Graph histogram - axes[i][1].hist( - random_scores, bins=20, alpha=0.7, color=colors[i], edgecolor="black", - density=True + axes[0][i].set_title(f"{measure} - Karate club network") + axes[0][i].set_xlabel(f"{measure} score") + axes[0][i].set_ylabel("PDF") + + # Random network histogram + axes[1][i].hist( + random_scores, bins=20, range=(lower, upper), density=True, + alpha=0.7, color=colors[i], edgecolor="black" ) - axes[i][1].set_title(f"Probability Density of {measure} - Erdős-Rényi Graph") - axes[i][1].set_xlabel(f"{measure} Score") - axes[i][1].set_xlim(lower, upper) # Set axis limits explicitly + axes[1][i].set_title(f"{measure} - Random network") + axes[1][i].set_xlabel(f"{measure} score") + axes[0][i].set_ylabel("PDF") plt.tight_layout() plt.show() # %% -# We have compared the probability density of NMI, VI, and RI for the Karate Club network (structured) and an Erdős-Rényi random graph. +# We have compared the pairwise similarities using the NMI, VI, and RI measures +# between partitonings obtained for the karate club network (strong community +# structure) and a comparable random graph (which lacks communities). # -# **NMI (Normalized Mutual Information):** -# -# - Karate Club Network: The distribution is concentrated near 1, indicating high similarity across multiple runs, suggesting stable community detection. -# - Erdős-Rényi Graph: The values are more spread out, with lower NMI scores, showing inconsistent partitions due to the lack of clear community structures. +# The Normalized Mutual Information (NMI) and Rand Index (RI) both quantify +# similarity, and take values from :math:`[0,1]`. Higher values indicate more +# similar partitionings, with a value of 1 attained when the partitionings are +# identical. # -# **VI (Variation of Information):** +# The Variation of Information (VI) is a distance measure. It takes values from +# :math:`[0,\infty]`, with lower values indicating higher similarities. Identical +# partitionings have a distance of zero. # -# - Karate Club Network: The values are low and clustered, indicating stable partitioning with minor variations across runs. -# - Erdős-Rényi Graph: The distribution is broader and shifted toward higher VI values, meaning higher partition variability and less consistency. -# -# **RI (Rand Index):** -# -# - Karate Club Network: The RI values are high and concentrated near 1, suggesting consistent clustering results across multiple iterations. -# - Erdős-Rényi Graph: The distribution is more spread out, but with lower RI values, confirming unstable community detection. -# -# **Conclusion** -# -# The Karate Club Network exhibits strong, well-defined community structures, leading to consistent results across runs. -# The Erdős-Rényi Graph, being random, lacks clear communities, causing high variability in detected partitions. \ No newline at end of file +# For the karate club network, NMI and RI value are concentrated near 1, while +# VI is concentrated near 0, suggesting a robust community structure. In contrast +# the values obtained for the random network are much more spread out, showing +# inconsistent partitionings due to the lack of a clear community structure. + +# %% +# .. [1] W. Zachary: "An Information Flow Model for Conflict and Fission in Small Groups". Journal of Anthropological Research 33, no. 4 (1977): 452–73. https://www.jstor.org/stable/3629752 From ad0182a0dd1574c2f0965409639e2675f7b9e909 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 24 Jul 2025 12:51:13 +0200 Subject: [PATCH 235/276] doc: updated built-in help of scripts/mkdoc.sh --- scripts/mkdoc.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index af4e2ee45..1d94d3c32 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -15,7 +15,7 @@ DOC2DASH=0 LINKCHECK=0 CLEAN=0 -while getopts ":scjdl" OPTION; do +while getopts ":cdl" OPTION; do case $OPTION in c) CLEAN=1 @@ -27,7 +27,11 @@ while getopts ":scjdl" OPTION; do LINKCHECK=1 ;; \?) - echo "Usage: $0 [-sjd]" + echo "Usage: $0 [-cdl]" + echo "" + echo "-c: clean and force a full rebuild of the documentation" + echo "-d: generate Dash docset with doc2dash" + echo "-l: check the generated documentation for broken links" exit 1 ;; esac From bddaefb8d45efc0c22d97e0a21cd847d69ab1299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sat, 26 Jul 2025 16:29:49 +0800 Subject: [PATCH 236/276] chore: update C core --- src/_igraph/graphobject.c | 2 +- tests/test_decomposition.py | 2 +- vendor/source/igraph | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 6583a540f..bacd006d6 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -534,7 +534,7 @@ PyObject *igraphmodule_Graph_is_maximal_matching(igraphmodule_GraphObject* self, PyObject *igraphmodule_Graph_is_simple(igraphmodule_GraphObject* self, PyObject* Py_UNUSED(_null)) { igraph_bool_t res; - if (igraph_is_simple(&self->g, &res)) { + if (igraph_is_simple(&self->g, &res, IGRAPH_DIRECTED)) { igraphmodule_handle_igraph_error(); return NULL; } diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index 109cad0dd..e9cb73ddc 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -335,7 +335,7 @@ def testFluidCommunities(self): def testInfomap(self): g = Graph.Famous("zachary") cl = g.community_infomap() - self.assertAlmostEqual(cl.codelength, 4.60605, places=3) + self.assertAlmostEqual(cl.codelength, 4.31179, places=3) self.assertAlmostEqual(cl.q, 0.40203, places=3) self.assertMembershipsEqual( cl, diff --git a/vendor/source/igraph b/vendor/source/igraph index 0f9ba000d..0119d1f0b 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 0f9ba000d4cd6452a81feb593e93042235f3199f +Subproject commit 0119d1f0b2fca021eefac9e1aceba09f1b294808 From f1d7932ab3b80559a896ae25c20fd67e75eecf6e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 26 Jul 2025 22:42:21 +0200 Subject: [PATCH 237/276] fix: add missing type check for umap_compute_weights() --- src/_igraph/igraphmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 97864f61e..896e2e801 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -687,7 +687,7 @@ PyObject *igraphmodule_umap_compute_weights( PyObject *result_o; igraphmodule_GraphObject * graph; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", kwlist, &graph_o, &dist_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!O", kwlist, igraphmodule_GraphType, &graph_o, &dist_o)) return NULL; /* Initialize distances */ From 2643836370f6cc0d69d872219988816bbf4c33a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:28:46 +0200 Subject: [PATCH 238/276] build(deps): bump pypa/cibuildwheel from 3.0.0 to 3.0.1 (#839) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 687ddf04b..d80f0bed9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,13 +20,13 @@ jobs: fetch-depth: 0 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v3.0.0 + uses: pypa/cibuildwheel@v3.0.1 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_x86_64" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v3.0.0 + uses: pypa/cibuildwheel@v3.0.1 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_x86_64" @@ -47,7 +47,7 @@ jobs: fetch-depth: 0 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v3.0.0 + uses: pypa/cibuildwheel@v3.0.1 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -68,7 +68,7 @@ jobs: fetch-depth: 0 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v3.0.0 + uses: pypa/cibuildwheel@v3.0.1 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -131,7 +131,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v3.0.0 + uses: pypa/cibuildwheel@v3.0.1 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" @@ -236,7 +236,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v3.0.0 + uses: pypa/cibuildwheel@v3.0.1 env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From d452ad6dc5c6043532ecb7e83c9dcdae4ac8626a Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 24 Jul 2025 12:51:13 +0200 Subject: [PATCH 239/276] doc: updated built-in help of scripts/mkdoc.sh --- scripts/mkdoc.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index af4e2ee45..1d94d3c32 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -15,7 +15,7 @@ DOC2DASH=0 LINKCHECK=0 CLEAN=0 -while getopts ":scjdl" OPTION; do +while getopts ":cdl" OPTION; do case $OPTION in c) CLEAN=1 @@ -27,7 +27,11 @@ while getopts ":scjdl" OPTION; do LINKCHECK=1 ;; \?) - echo "Usage: $0 [-sjd]" + echo "Usage: $0 [-cdl]" + echo "" + echo "-c: clean and force a full rebuild of the documentation" + echo "-d: generate Dash docset with doc2dash" + echo "-l: check the generated documentation for broken links" exit 1 ;; esac From 7ce2f90f57dc1a365cb7d79ddabe1d7c38b504ae Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 26 Jul 2025 22:42:21 +0200 Subject: [PATCH 240/276] fix: add missing type check for umap_compute_weights() --- src/_igraph/igraphmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index db265aa6e..4bac0f081 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -683,7 +683,7 @@ PyObject *igraphmodule_umap_compute_weights( PyObject *result_o; igraphmodule_GraphObject * graph; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", kwlist, &graph_o, &dist_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!O", kwlist, igraphmodule_GraphType, &graph_o, &dist_o)) return NULL; /* Initialize distances */ From ad4a3f5a89736e4f544f06ff528f7aadf70a4b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sun, 27 Jul 2025 11:03:49 +0800 Subject: [PATCH 241/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 0119d1f0b..9ce1e1695 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 0119d1f0b2fca021eefac9e1aceba09f1b294808 +Subproject commit 9ce1e169536382e23e02d7310073adfe3bc2a9f5 From 81e789df81c37ee75017fb874081ede95242068e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sat, 26 Jul 2025 17:00:37 +0800 Subject: [PATCH 242/276] feat: align_layout() --- src/_igraph/igraphmodule.c | 36 ++++++++++++++++++++++++++++++++++++ src/igraph/__init__.py | 1 + src/igraph/layout.py | 22 ++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 4bac0f081..0af15fed4 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -227,6 +227,38 @@ PyObject* igraphmodule_set_status_handler(PyObject* self, PyObject* o) { Py_RETURN_NONE; } +PyObject* igraphmodule_align_layout(PyObject* self, PyObject* args, PyObject* kwds) { + static char* kwlist[] = {"graph", "layout", NULL}; + PyObject *graph_o, *layout_o; + PyObject *res; + igraph_t *graph; + igraph_matrix_t layout; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", kwlist, &graph_o, &layout_o)) { + return NULL; + } + + if (igraphmodule_PyObject_to_igraph_t(graph_o, &graph)) { + return NULL; + } + + if (igraphmodule_PyObject_to_matrix_t(layout_o, &layout, "layout")) { + return NULL; + } + + if (igraph_layout_align(graph, &layout)) { + igraphmodule_handle_igraph_error(); + igraph_matrix_destroy(&layout); + return NULL; + } + + res = igraphmodule_matrix_t_to_PyList(&layout, IGRAPHMODULE_TYPE_FLOAT); + + igraph_matrix_destroy(&layout); + + return res; +} + PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwds) { static char* kwlist[] = {"vs", "coords", NULL}; PyObject *vs, *o, *o1 = 0, *o2 = 0, *o1_float, *o2_float, *coords = Py_False; @@ -790,6 +822,10 @@ static PyMethodDef igraphmodule_methods[] = METH_VARARGS | METH_KEYWORDS, "_power_law_fit(data, xmin=-1, force_continuous=False, p_precision=0.01)\n--\n\n" }, + {"_align_layout", (PyCFunction)igraphmodule_align_layout, + METH_VARARGS | METH_KEYWORDS, + "_align_layout(graph, layout)\n--\n\n" + }, {"convex_hull", (PyCFunction)igraphmodule_convex_hull, METH_VARARGS | METH_KEYWORDS, "convex_hull(vs, coords=False)\n--\n\n" diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 16f41d6ed..65399e8ed 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -227,6 +227,7 @@ from igraph.io.images import _write_graph_to_svg from igraph.layout import ( Layout, + align_layout, _layout, _layout_auto, _layout_sugiyama, diff --git a/src/igraph/layout.py b/src/igraph/layout.py index f5b0a9181..712323f7a 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -16,6 +16,7 @@ __all__ = ( "Layout", + "align_layout", "_layout", "_layout_auto", "_layout_sugiyama", @@ -534,6 +535,27 @@ def _layout(graph, layout=None, *args, **kwds): return layout +def align_layout(graph, layout): + """Aligns a graph layout with the coordinate axes + + This function centers a vertex layout on the coordinate system origin and + rotates the layout to achieve a visually pleasing alignment with the coordinate + axes. Doing this is particularly useful with force-directed layouts such as + L{Graph.layout_fruchterman_reingold}. Layouts in arbitrary dimensional spaces + are supported. + + @param graph: the graph that the layout is associated with. + @param layout: the L{Layout} object containing the vertex coordinates + to align. + @return: a new aligned L{Layout} object. + """ + from igraph._igraph import _align_layout + + if not isinstance(layout, Layout): + layout = Layout(layout) + + return Layout(_align_layout(graph, layout.coords)) + def _layout_auto(graph, *args, **kwds): """Chooses and runs a suitable layout function based on simple topological properties of the graph. From e6516e9b9b5abc815e30884cb11f7e75fbe4183e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Mon, 28 Jul 2025 20:58:43 +0800 Subject: [PATCH 243/276] docs: do not save the config in gallery example because building the docs overwrites one's config and potentially causes issues, including unexpected tests outputs. --- doc/examples_sphinx-gallery/configuration.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/examples_sphinx-gallery/configuration.py b/doc/examples_sphinx-gallery/configuration.py index ab408886d..5c12a0044 100644 --- a/doc/examples_sphinx-gallery/configuration.py +++ b/doc/examples_sphinx-gallery/configuration.py @@ -18,16 +18,16 @@ ig.config["plotting.palette"] = "rainbow" # %% -# Then, we save them. By default, ``ig.config.save()`` will save files to -# ``~/.igraphrc`` on Linux and Max OS X systems, or in -# ``%USERPROFILE%\.igraphrc`` for Windows systems: -ig.config.save() +# The updated configuration affects only the current session. Optionally, it +# can be saved using ``ig.config.save()``. By default, this function writes the +# configuration to ``~/.igraphrc`` on Linux and Max OS X systems, and in +# ``%USERPROFILE%\.igraphrc`` on Windows systems. # %% -# The code above only needs to be run once (to store the new config options -# into the ``.igraphrc`` file). Whenever you use igraph and this file exists, -# igraph will read its content and use those options as defaults. For -# example, let's create and plot a new graph to demonstrate: +# The configuration only needs to be saved to `.igraphrc` once, and it will +# be automatically used in all future sessions. Whenever you use igraph and +# this file exists, igraph will read its content and use those options as +# defaults. For example, let's create and plot a new graph to demonstrate: random.seed(1) g = ig.Graph.Barabasi(n=100, m=1) From 1f48b47c0da9af7a95a166aa9049117ce932107b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Mon, 28 Jul 2025 21:24:21 +0800 Subject: [PATCH 244/276] feat: auto-align all organic layouts except DrL and bounded FR and KK --- src/_igraph/graphobject.c | 68 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index bacd006d6..6393de1c1 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -8188,14 +8188,15 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * return NULL; } - if (dim == 2) + if (dim == 2) { ret = igraph_layout_kamada_kawai (&self->g, &m, use_seed, maxiter, epsilon, kkconst, weights, /*bounds*/ minx, maxx, miny, maxy); - else + } else { ret = igraph_layout_kamada_kawai_3d (&self->g, &m, use_seed, maxiter, epsilon, kkconst, weights, /*bounds*/ minx, maxx, miny, maxy, minz, maxz); + } DESTROY_VECTORS; @@ -8207,6 +8208,19 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * return NULL; } + /* Align layout, but only if no bounding box was specified. */ + if (minx == NULL && maxx == NULL && + miny == NULL && maxy == NULL && + minz == NULL && maxz == NULL && + igraph_vcount(&self->g) <= 1000) { + ret = igraph_layout_align(&self->g, &m); + if (ret) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); return (PyObject *) result_o; @@ -8298,6 +8312,16 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel return NULL; } + /* Align layout */ + if (igraph_vcount(&self->g)) { + retval = igraph_layout_align(&self->g, &m); + if (retval) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); return (PyObject *) result_o; @@ -8520,6 +8544,19 @@ PyObject return NULL; } + /* Align layout, but only if no bounding box was specified. */ + if (minx == NULL && maxx == NULL && + miny == NULL && maxy == NULL && + minz == NULL && maxz == NULL && + igraph_vcount(&self->g) <= 1000) { + ret = igraph_layout_align(&self->g, &m); + if (ret) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + #undef DESTROY_VECTORS result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); @@ -8574,6 +8611,15 @@ PyObject *igraphmodule_Graph_layout_graphopt(igraphmodule_GraphObject *self, return NULL; } + /* Align layout */ + if (igraph_vcount(&self->g) <= 1000) { + if (igraph_layout_align(&self->g, &m)) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); return (PyObject *) result_o; @@ -8632,6 +8678,15 @@ PyObject *igraphmodule_Graph_layout_lgl(igraphmodule_GraphObject * self, return NULL; } + /* Align layout */ + if (igraph_vcount(&self->g) <= 1000) { + if (igraph_layout_align(&self->g, &m)) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); return (PyObject *) result_o; @@ -8697,6 +8752,15 @@ PyObject *igraphmodule_Graph_layout_mds(igraphmodule_GraphObject * self, igraph_matrix_destroy(dist); free(dist); } + /* Align layout */ + if (igraph_vcount(&self->g) <= 1000) { + if (igraph_layout_align(&self->g, &m)) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); return (PyObject *) result_o; From 271a52152e0bb592f9b828b1d2ad228595485aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Mon, 28 Jul 2025 21:44:27 +0800 Subject: [PATCH 245/276] tests: add test for align_layout() --- tests/test_layouts.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_layouts.py b/tests/test_layouts.py index ac32e0210..67dad6ca9 100644 --- a/tests/test_layouts.py +++ b/tests/test_layouts.py @@ -1,6 +1,6 @@ import unittest from math import hypot -from igraph import Graph, Layout, BoundingBox, InternalError +from igraph import Graph, Layout, BoundingBox, InternalError, align_layout from igraph import umap_compute_weights @@ -452,6 +452,13 @@ def testDRL(self): lo = g.layout("drl") self.assertTrue(isinstance(lo, Layout)) + def testAlign(self): + g = Graph.Ring(3, circular=False) + lo = Layout([[1,1], [2,2], [3,3]]) + lo = align_layout(g, lo) + self.assertTrue(isinstance(lo, Layout)) + self.assertTrue(all(abs(lo[i][1]) < 1e-10 for i in range(3))) + def suite(): layout_suite = unittest.defaultTestLoader.loadTestsFromTestCase(LayoutTests) From 183e6a4152bdd57660cfeec3389542d9af0513b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Mon, 28 Jul 2025 21:50:25 +0800 Subject: [PATCH 246/276] tests: update expected image outputs after adding layout alignment feature --- .../baseline_images/clustering_directed.png | Bin 31493 -> 32370 bytes .../cairo/baseline_images/graph_basic.png | Bin 16701 -> 17361 bytes .../cairo/baseline_images/graph_directed.png | Bin 17353 -> 18020 bytes .../graph_mark_groups_directed.png | Bin 17353 -> 18020 bytes .../graph_mark_groups_squares_directed.png | Bin 14924 -> 15899 bytes 5 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/drawing/cairo/baseline_images/clustering_directed.png b/tests/drawing/cairo/baseline_images/clustering_directed.png index 712f1b95f8fdc22b4eb1b2683143634a81146a14..282f3fb21b4e080798e753b3404ecb2e4e648bd3 100644 GIT binary patch literal 32370 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 G^Zx;CRKuSD literal 31493 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�VWkrGW0Js8;U=Oj7NR)9w@wr=C3*Dwb&t&H1 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 literal 16701 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 diff --git a/tests/drawing/cairo/baseline_images/graph_directed.png b/tests/drawing/cairo/baseline_images/graph_directed.png index 83d6e37647f1b6fe90f88f080974afea8573013d..b94e821698e145d8096544d1fd8cb4a63f5cde33 100644 GIT binary patch literal 18020 zcmaL92RzmN`#=7sjFMHy$cRo>WhEh-jFg?S3Wbne$DTzhvt(vFLP>;>9c5-5dyhm$ zX4&g^y>)-@@8|pd{vUt$<9^)dcFuW^*LA&~*YkQ_&$nlq>dKUdnGPceLaB0DK^sAc z_0WIGiQzZr4pIlfe@O3KRaQXu(0`K4GhQMHJEEc>r{fwwH{_-lxt6fE7w2VslZK8+ z%R!Dv&g>#nelzaq`HqddT<2smIu@KRSZOQhFco1QH~3PVDNa>+S+M%*Y;1b$h*`Sr za-3Q41(nA&*wRd2ibt6aypQr~#qqAtZZb>t1>L1 z!7Lm>+Rk_oh!BM48TKGz$)re*vJ}9i_74nfZEe+k`SLA6LKnw_5O;QWM@2>s6+70L38}>jaSEY7+Blx(52pFS zT{tHv*7^oF8G+VMMWVwkc2_jQ8iscRvaBlzQ(dz;(i>^Mp8c0-4Qd3uSM!tJ8Lll14QYg}o9ePJPG;! zq46M{I{FVRyJtuyB%w5I3eP{>YD($g3{bKKM$Wki#gwxNK zgimsEY(+%^Y%5o#3-g-}AAVcr@R{(!Fcw*1WMK)8xiD*-V|44{%t(px)%GxE+4h<&tqa@IvgXmw8qMsV(K|28vf_5rKOeE z4&J-UL=eNkz(7wQ6%oN0Qd{$cx;0MJKHiF`{*3$TuS?PVQs-)QyHwgcDk^jb`hWfU zW%i6D@l?chN5>`1ORIN$JSy!%Dkc4cgPoliMn>0#9zz3z!-o%7Y)r=GTeL2VGFAoL z{B(a}b7l5w-EQ^8aAATa@9ERef;<+fuIZ5~^5EaTef#CJ+WRGmAP- zBH0UYOSR{}jk!F0Cw3}=@WKeKIvJ^gd?nRtA z!Wk##_}wGP`^?H(+)}^w{QT9s-*R0!1{G-=d4z~GjYC2%UMW}k-8@1G}Fmo z?78%n!T87B<5fzEN=o!=Yt*q99(PEHZ3~`hT#jAjmXtn;Pf1BRevI2&VzrLDA}Wni zQkp39>YrbIWCFLCI<9>^8m6K5xMskiRAA{Lfg6M7OcW4Cxw4($?L)^cu)vi=W?@JM1fWb{RA9Z|3Jm^ecDY zSR}qUO~=tfJ|R+eXKrL*Ac<+^s<)Lz(J8Uw}8(D_!$mtO0OFX!jEPprL6>h0S}cvAPlc;^fjTg}vf=;`X{ zbhgUut}jSR?v6V$Y*&P*aSsi-uw7j4yr!Yg`6~S!)5mao$g-rpF}A2xzze6LHKMCr zDrdsed{+LH{hHm%YLyQ9zA7l&b>_?&qm6;egdxHULf`Sds`HPohZocg-CCGj9m`1K zHM z>`Y8X%UwpK@+?{t3JYI{hIY7n%&Dc*ixyRnH54{fcDK79C)l39dB#(O(wZ!?D^y`9M} z>ciXZ?d>mL6bxvpE*%e1QVFMGllGkJzd+Cz_Qw{yee{SpL7iKi1bNKFU!9nkDCMzw zgp%@ED4q_JvNHJT!O!nsg9rpdsvn}6SvsM6?b`39I<_E6JS)77p`+t7D5&r2)5p;u zhc9?4iTxVcof~OMk`WLLcGmRouDOY+s-NeR#}kUJd*0jSOV?4F%F`M&R8`4B!rZ(W zKgEtj%vQ`k>Un?5V{d0o@i75b|4L(HF8 z1QENwBMXAAl3T;zVz6`Won|IDE@KE;kf4~D>&EiLvuDq2IHIT( zEh$MzNDK>YXcLHi+bP4865`{rSS&sNO+6?-_9N1Sl+G~7611!FE%PT8t z8XCrVX3rqz4Q883dmBkvx;ZT|=Pu#+BgdIieyJyjzwVgUE1(?x0MUsOFqYm{@ppE1 zURqj`l(VQOI{@@p71_7Sr&JA1y4TFH9S1r)<#1|#|euvkZ(yy4G0J@EV6q> zm3kkOQg)bv!qn6hu1$fA$iyQ)KK{;~JApw#iey?B12v7lDG~*fb!eSUCO7Zw=>aVL znre&O4GIg(dH3$u?CddCg=QZ4UKFPfX1r}||cSL8W0 znTphgO=k}B$bX7aP5=J=4!!LGM(zZ4T|K?^Hm|{4@<2^~iUMMOjbsZ!N3DZBwcQ+fMIO->&=bVya%T%NY^5rsd(8`R_v@#R~9 zUDn*(OiN3P#y70KzJ79Y5|RT>{1jVI_UO^0;$ksnW$8bRzF)<}#cOM8>FMd=!19=s ziE|7j>}2V9AvnytjEs!*bS@E*8?XlVjxjK}On!@K*J7Z)8|TwDlp zNM=Y}+yG?N)vH%wnVAFIsv&`%Q+H@x)bx)M78XXcYkl1n{p3sajI;dwB1|E$l!p~9 zRiKz%%TQnW{rec}L0@sTSHe~^oo`_6Xjv7m^T=Pl!+%)TPF0yN6kEViZlpm={Jg9E z^;P{UTYXH**PA7{hT&mvxGeNW^JC9jh>MFGRd~WCTuCP93;4u?%$g7tnQQseU!o<> zrQ$zkK(AcdM%-ooCJt8s5h}onh^zugBqSujjS(qY z5`F#p^~H-9Xp}CzfBzm{W@seQvno)+H%BkJBi(hlPHmp*B_*dsS5xIKXih z77Xi`NiF~J>-ze~F&JUT?+UPLmO(i=_L`cS*RMxTv5SOZS#ltVfMkTzh$U@@R!}l| z-B&Xq$A>h&JmLxbGBP7(LeHq~4DeJtesUOll%M~=#3UKLNFG7Kw&v!!>cm9*@~`}^W!H^ii(Ox ziXAWEXqc5~U)itY+J+N-o*@1o^7CO2SrDzcE?ynl43V>mt$UcbbP8N^+c{&CLWo6~k;$n2EXK&uTnTp3>y>dl??89|TN-U(1CRI9q zT#ojYDO~@%cfpDS2nKudWXijD?qNSnXG#6KTX6EI;?-HR`R7X7jA(dP? zD<>x>FE1Qj8GdQ$iqzE9oE!~U89Ca}8~XZv@fRORN=Ye_5&6Dwb$1VV{5Uc?8W#UN zPc;E{R@DtbR@P&I4mwaT93371){8~gnzMHSP$3GGn6b=wDG%pr|3k1x>CkIw9U1Wf z@(%?580!}iO@Dkf+?DO^g{dhmf4n?wGCuKy`L)Wu@+nN=L#$^RfXnXgZePAd5EU_k zr=zF8E6f!4>J=QE2s1YR!IIgRFEA|Z=P-egOwJYt8`kj4B@AAHR=O6L{Hs^5u%}L; zSO4KzVBkIYv9<<)p602pYnxqxD-I0}Mc1$QFN)>)Zhe5aAmUZd=U7=O106WZ%gejE zx`G%4(1rX)%fJAD-!(MEC;l{5%m38r)29znQ(M6HW1g6wm-gA+&dU=b$YDaTEQer? z3qz-;Ott*O0Kjy#wX2~{h^S?B(9zKWc_7F|1YteXW@hgB`1r`k$iOD8@`M7qy0(_{ z<_&~Zs^sAuX#NYsm%n0nkFEuQKp2JtsNX4Az9Dx^8?t^^2n>8N#GF^I;=>0 z<-mahz#)LyK+^iIaBy)&MMXiv1W{p6DDuG0eB$rFzP^qoL-+XHoYaHKZ;g#t2y$QW zaPu&{e*$g4K^IC}VxD2~9H`N2{hp>xV_A#}Y%MIffoVPcOlO3pdY}n zvU*Lo1MR0G$oUiOXn|JLe)%FMA%Sjw?8S>00p}R#=$=twgPnt(lW`*^F|`?ZCcf&5 zii+jsWzMTn=)HgcT2~hx4SL54d@w*MD7UQ46WAA!KJ>m^jf{+7VNMGOFt92d386m; zSzH_)m4P?ltsYYxkQ5_AT&8g))z$L0{G;7>Xo1Fe_w>M~dxL&kdwpw5BSvrx&^pzR zJoziutda+jyqw>m#YcaVft+Ym4&bZv&9&5PT(y0NC7{ zH!3(9>yDeWR7q5Mmdwb!sf9&!OibqI=}e^{P26Eb6Puu((KD-k%fVpIXwdUd0z+wEl5yJtg>=lZ?9f9-)Q#@S|J7!-xpSk zLMZ`(xq$H{CE|aHnCAQ9;$kSM!^1`dxp&H!6kUN^x&5AWb9WDf#cP}x8#`w|T-M&M z{rBzqn;1mtlJ(iU1={|XE?t7k>`Q2y#zFol;o+=j30G(nFFv^27;)O$+Z$b?cC|!F zH~3$R)n`K~ znE(V4Qa?EyP2ZbJa*D%;zyFI}hIj1kb6$`PzT%Mw>TNi9Q1AUt|SW=r{yh~rd5oB2DteME6u4+;945aV|NFKX|6yc2 zW79*q3+M!_?GiJ;{L2Oo@uxR+4E-4djG#BAK79Bn^oihzU_lP<~0=f#wc?Q-m;X2pROoEj~ z^!aX;=$LnCeWXJ-s_c!KQq{~vtc{h`^wd<0gv)#MyP&i-&rVGhd+(G?vD4!dv64oFoORQ(&Lf@v5+mBSB(4`ne{|vHmhT+(;2TR|- z?jsrRB~C>>Jv~NN*12E5cmd}!lOT+M`1?)eha-^~6oYcPE~qJYY5D`kM@B|ojp9X@ zv;bwRWo2diY?VUu5-k9JOQO`kU$)wISU?~Ls1bl2ToBQ{zsc?OHa4{1la@-LKb@SY zsuAcpwENd{eFr9k76G;ZD*xeP1z$%-MtXYIpzlic3yyAPKre$vk+nk$RBjWlK+Q5} z`+(TCwP`}ll?|${u6B2K2Tr9xcAQ~=Umki^U)dSGD%%0Y%$`};n!d7UVnX{JXbXWT ze7eth=FAnyySDeRCKp6R3O{~?mO>iRyWFVKtl=mScl4;GAZbCG+jLMA+ZLmn_UN%= ziWvx!oxd`jnN5G&*x1;~=>jkUS%9r#kR|tV>zi5D|H3VUpMYB~^Mm^Y41K%)!5<`< zMMR4AV*{3ji3#fIBrqSIFVOu_P*8*@;iuRqa~rVl-n?1*jsBpC$*vME?6zzsBU1@7 zYarDfsyJRW#>v8k66j&fQjQ=zl!}&8l$4ZyetsuUo`iHoc5POF^#L+M!%fR-_RS`T z{D}scegP$Nx-quh#U~(P@ZT(kt%g+X0Oe{z5uI;P#2js zgrB!)0k8_95+W_s@#o~^{BQ7Dbg;9tqoC&%^V)=7)70duVm%oRvF~!InM!mUW?)e>DHCJOC~#DwH-ax}sNx!UV7#dRm%g4uD>VM{65p1IKdAa#iW3qSFf$V{~pRn-6WZLszKs`#Q_l& zy=%O=y*(QnTMpF9{df2GV~~xx_}f4Gpp~8d54HpS4+)X-+4bB9uCSCpSYCeq(BNR0 zyP%a>P=ueKAJh(5vs6FHfcBv0z$Sb7p$cy|`3LT!dwY93XddWECok_e+FI+xc8UzQ zfsScvwtaba4CS^lH9b9|Vq(Q8a6Ny)Q-YQ&-|TC5+qg4_v}Pl|fR#tSnuOpJ^?aLZMm&__bF82E3}u>N=vVYnZ3R?M!M6bzuAM^_6R zJ5xXo^8gqz{cKGVKv$`(x~67eV89+Fru8+a3&_!QK6VVy1|^ii(Ll$-863_Q>|f6- zo(-Xtqd|)xPr<$m4h=;^WO5wHaD~HI_1r)atR*=oJ;;A!08#M?3Fw{a1-yKD4H{oZ zM<|ulDPOQ?*_y%kPG^1(bO2VJn)(PTQL+$JqKS!#ot>Rjzd;R0MZFt0M4%~w7@;c{ z^hd%aDQt{OR>^k0LBQ? z6enW)DlAOH);4>J-A7Je5oIQwY7hUyl@w;|^7~t*-CbSKBXYs2$nwMfn4X`na9?=~ zpHENz2g}hqmKj3~>yf#?9`ro6;%_6cHum=RmX=Qg1JP2;o`YhRu<&4s(^cGa8SQ=E z0-dP%#fvDqKaYmhP*n}SPw#McpOg4H^V}1E`xFcMAIw_rz!@wR+8q9{yZN*=)j zh-~uHr%(S%f)9@?(BGDmlLG-7kH=RXL6C<4=s;KjV@dAP+}yvUaeocv$8@(fG&I!I z)NE|tWx~9d^J!3!#Dhsy7nh<;q&hG+EsaA`5_-g2Itb!>wD{8}=(;Kk3UqXk+xr(H zsQL@1qe=@4ks$3QE-h_T52M+@)Q)K=q5$U4@j<7np*mP~qyrT?bWt3U)dIOx*tWj_ zz=!-|Nz8uA7Drc2XU1SVXIDUymZzdbG}$55NHAlsuSSDX2eP5n&ZPrLgcU$5Xams# z#)Z2nB>VNb%J1mWqyDPOAhh~op8{6RVawcp-?g^3c6BX|45I*e1S$$s2*x9%XW4rC z74!^Q(kGzQR^iEGLk&KXgX{x3f2Hfv9qSVq z#N;gS(EGdF8!avB)+ajOD(c<5$wEsz21-G*ACTe4K|xm)6uwM%WMsQ(X3?iW^9H%Y z_;4iy-+dN{s2pq}L@6pJMo(K?NKDM~<0*q2a+vY?J$l5Y9uNNu#|Zsb4eScFEJvB3 z3A8bzAKpo_e$z3uJyXS=gEe*ZKagV{eFIW;wG zxJt+I0J>1~kbk)-!{Q4K1Ti$*@1^Y%NCxxTX$b%21-NlWejb=$@UyOTd>+HtUtJIs zzX@p?L{<%6%+Ac%ja2ZhJv0-j1`0kkWeG#rPt*qyO%R`>K|Bl(Kl+K9!(*SA#?*;- zXt@QU==E0_M9yE@5*!?9sj2ea`1*ZRM#e#K z|Ka^qzV;tl${PVS09cIqttviK+V1Xdpi86&57xVoBfcg7Eelp$s@FsJ1v1XPMr?;V z3M}s%b46s4=Ky~7^e6@7@;}jmknH z4B=JmZt~B=i~)EvGckdL_rvu8;@hq8uP7KQ8vU9ahPi-QKxJ9%sgO1%Ztjejn3&X5 z?8V`mQTv-Cmv@8S!LWjSYjg9=*|R8v$xQ}10SJCYg>+D5{zOpjyLUFg%z+cq`&b~p zccEweD+wBIC-52X-_!l)R^s;_Vucxx0D7|xKfNau3p@1K(WA9{g$Oce?fUgwTibK< zk#5Ird&1ygnNd-hspFQqlh#>P53kx!i-@m{b@ag4NJ+hx&Tph3v z`^|TRv?kivyAN7&pxY?bcXm$P2iCgQ)>cp*j-Dh!Wn57DtSv3|ptmC#=^(~yk&!*| z*!uuYL2mA1)x!f>H?q(0^0Mr09Y6$CGddE)of`8NtR7+uihS1N6ciPgXh@K%N5FW4 zl}xW)dkM!_w(ze3DOA*Mu)f_E@ihmL!Ekk0>r)`wghJ~(?(+by+I;B=kJ;(!%8j^6 zx0yE?wRT&ejcQDfwO1x>r3^2F{NZ>SsNIA8*P`YVHrlT*3)_qvn2--f-j zO1CQaHhWqne}A(Kr9`S?>IuqW0@cvJSR6gNJJQm^twg(IsN&w&@q70BcR;4F?{!27 zBVX+1)Hy{lF)_xA9?k~WI%LA^9-UEXuk0IGIE0!^#!{M!;kZRmbw?H78(&Z>lC+Io5h2W^I}i9pqQ=OwI7%M_yY zLkdsSHvG2$Sgzh#+*Jwi<_aA6@g zT<46+*x1-)1ry>c^YUYu(wS<2QFZQ&-=;D%`YJPy&Y^*UOj}psWJ&uc>+kC;FL8G& zEZx$hj-Q)c?Vss#cD_3}dH@<7vpIa7q^1w*-Ff(Mk3jf<>r|HFR1w%(?=&t4o$TTa z11#*(-*FsqXC1P)_iCrY^@cCqwk7Ao>ItD|Bi>yq*y( zC86)jI(^!72Cq9f>k@D^{XkK0H7uY<1H@820c>L6^Sqg{L!hGTf zmxZt!Im4wc_Mn&p&t-g0jC{LcY`m8Rb_r3@HJ`mTpCa1<5h)(aKVAj4YxBA{vfY23 zRNDGnmgKX$k(G60XS}Madw7^^LB9Nqip0%Zdrf^CKP$H<0_o0jsLV`F^}-Yzwhs_> z=nj-0kRwO{FiKCjDsJ`oV&}R3Mco@YMmyAdi`4DCTT>sD3_&V}Nz$DA)|{!#o?DFH zC3*XW;9YN(@b}^p3?-kssHt{GKlZc_dwYOT;^H%GCgio;z@6m1Ic;cPPRGhB)d+P; zxXDB?UfgLW&b|U5=hNCO0LQ0KpInrKP3Q10LQ=?|P*PEO|85qAv4*aIaTJG@(&fu$ zWnP4L2;z42~+cdfED27@dngSrcvy7x+#E|7cb!tf_g>NI>duK^Nl zRY-SByGNX3NI4F;9Lg#I(JHj*tIkeCd_7`S(^qDDU@S1NC6f!pY{B!V#4?HNw_AAn zv7bMqZJb&H)Uvxe%hwYISLJxCxg zKi{KyN0t)t-2`OkGc5d7A&{P)9=e;zkgxxdoKxj6Ik}kUdM*&F`}gl(y5tubMuli5 z!$%;5?yjyN*bnY}|J^FHXL6zOGm~hOWarX&-H69R`HvqzGLpOvE~Aw7$IqW^lCBP2 zZ?6kftbYf-4D)T67Nn~uY<48=c6$A4Qfy{_2WKk*qIdW1T^#Pus|(iPE>QT2Amq1& znZj5lR#$qAp+jf|YO@{$Dv*8oO85Lw>8VqvvX5Od1D8eT&`?5LoCj1KFtfpu9#SO+ z;mGgv^T^1F+1XN%akmoRmAWo{|J9eD9i`wvOSOf9H>*U{t5^3D90d{CcR&x|q|rw1 zRuhep)h^jQsc=niv$KaPygcsQ`3{DfbKS3jpc|eO7XCB#gv}dlR^W}em22{FN-R7r z9vi{&29Gygn;(MN7=+7ILlntuUHo|tjBZKWKe!t=r0nPFxRpl7$Ma3=LV-YZW$8vo zMm`HY0D~bAp3~CO+S=NNN?p!okZ>v{&<RGQ59rdt4X(~X8IUTn>H9u0!6{CKRH;I_gwa5DcJ}o2G`065|D!yX z!tOZ@FS&nCP6AswfBLkQf8-BnIbhbQapejOc%>aieT57)wSOHSyX9W)y|=ql!%U`N z0lo6)MbZxu5fR);p4zym7d+)~(tB1`4ULU33L7YP#6NlRWJ(P2t(%&j?yL0KgSG*D zoG@oosB=5=I8$upm{Jq2_?4;^*k7!;gB}iK1k)V;24{Ua+4Q z%6_*{`>fZ%y&K_xJYR~330H#4up7Lm`%dqqc#~zHHX8Td-k8|f<6tO!^=jt(cP!lZ zXc#Fe>9Eh<4lv!|;9&jyyU#+;lZps3mFt2ck#gw?DH++dYuA8Na>1u%!#Uo3{J7FA zIs$@cLsL_JHhqN6eDLD?F{vTo<%Tdkp5g^-E3}P(7Ki(iZAlIJLqo&s*RNB@5#Jj+*RO;6(hLra)XLF&is5}h_W9nlt!plFGimnj*ZyGh4ZUu!({-cgODW4*D$1k*9<^A{S}3F zp%hbjbWY9!NCIFlDh3Y}s^~!q-n#}ZIrITEG`TJdMqLHTY3b*yOXr8{w;IppJZ&LH z&ZK;Q(ygP1f^lKtU2}8PQ8Hc|0-P7UP)g?Eah{Qx`uT31mkyk?sPw)UouQ}+y z*V0w>OP%K+DHMQ77&^K5tv)AQuoKsJlvy*& z&>XvWukqVAaEqD4ZVJ{0WF=$s)YR4vfANC$=uxbAXj$h)2rRhcAh1BzV9?RmL5YNQ z!SMEWO#rkS(5!S7C8s@xo6+%XI)>;48sz{T_S%?;{`g?LJRlIlw7Izn&Ie%q5fPYG z+<~g{08z`mg}q(W@3XHZg|1j;!PP*}8ygzVF&=>p1+a_WH+Hbh?Eyp$mI3@*OUuhg zS?k^k>-x7#1ucu%_G6ft&q9`wr!P4*!yN546c<2a&(%H)W3QF#9v&+r!Uqv*p;*-< z&*cVy%(t&7`|q~dp_vUl060Fl`waI4`4GVxXkws8fvMMp-52j)NgrEYTI%~~t*fA* zaOKJsHMJ0kwTYCIlaqD#yV!H4wJ@G57DSRW-rs!lc|#1zKJFcF z17N8kx#T~^`(4Z5$4rnhFE0;Z!cpOlnWe}Pr%IM1VWW&9d! z_@?G&Xa+S@RBl65uHUnv{`*R4v%SxiRB) zVU=sc9+2A_GCKuRyp8g-(h9H? z8)Q^0qXw<@U|uM87?TGVIu0l7Q9{&9gD1HD>iqz|f^;Y=aiSVH7DYTcTTW@I1dtqX z1Ve=PrvYThi5e3@_-IX2lQj%M3#v2WTULpW5zp?T|hr+1z;! zZjRC1#DvpS+erflNwHG}zVJamGU~iiBHylDxdAX|H5I$B+=F$53E91SF#XDc+tW1d2J2l7b3x_q=B({N;Rm^a?>zGQ4jSi?tH-^yn_y8O1d?w! z$pST%P`dfSw9b2boJ|0fQOGfoH^s%SfVVztEWET&LyAl}DV$$YiJJ;}|<>E_`9wCOp0flv$iR~{Z7 zF^5rv8H-%@>AxHq9=`nJrAA;&sv;&u5L$a!IS}(y)zq9GJU|bDczJn&`4vh=bA3HI zI&4tJGlm>K2J_YR#oHh&fFmY1gSic*a!}M^NQ+kvYkx%SsKhgjGB~fbp&nzOolid# zk0p@!MsCl$R7$x2nWFYy^aF#s9F9I4PfnNe;lsJ}=g;%<@?N;m_4TVF+@cU@^{(Kt z1yB5WAt9(Oz(W{A&RqKcsJovE3JdLeb5mZw<`fVJqt_vCA&)e7)mQ@nPjdZ!h@g+? z>4DU4XE%RUpuFJz#FfH(JY)Mq{L|!1YxK*g8XxHb5MO?h<@ej)i}Z1kEX;G@P6*LCdBm`PduNUys zQqKH)uqFXC9{^qesNU34<^s|G0`(KmusS7BdglQfz(>Z%*9@*#SOk1~SJy|Eg<*IL zuq9tYXAG8L=tslL2lwy)kCHpO1TJjYaZ=`u5eLYq+8#C59ZzZI;pP1XU=8KT*lmIi z)(2xiBagX{|In^&ZmR(v zdBZNK>p-%Ym>Lu9WRu4z`+<=hKYkn?^Nm-r1S{EI|6lc&oY3CBJQ-&XbN0!J3Bseo zp!4!^b$s`HX$I@FnJ_6#km45R=A0ZHkcGL#c)~hCnGLPTz^C`!qh#ZXv~Leu^CGj1 zMyo&O=Xbje?IEFoDpMqrclmt{@Ez3K%RmOYGusE|?YWOV>(q3)n3oNIp!@UO zoX+@{80B&VYCc|HJYSW!{#4UxeCZG!_@}{6{h!s-*A~#sF#oQatZdbO@UKJgo54X0 zB7(8|^y|p8xl4Up>2;|RUc2GX9XXB`PwktcJnn2DNb=%-oI5- z@8i9$Ab|=VI9CX^GZRLpX6EMLGRjCxgP0kB*$|p1=j9&_0Mq+EFcxxI+Iza-FHl!k z2e&fR8jq3`HB!IZb)l@_EP_zn^((xq<^RgrMFTCc(Y*S=)=jQM<%Au3*H45;{__83 zNApRKHc@DWxnN8TBf*iKQIQ;^BDe#fCD5*?zkaP2FQ$cKkyU~o9lBr`ut5qr6n7Xc{MRX za|+M4w_b-jY*@*_A@|)IZUOcWw9)7-Xoz;E{N)Ic190TO9HFt=60-)gMWFk}U?$eq zZU@y&v5u&A7RnlEhO5iV;7y=TU%vd*OiKxbOfyNaB_3Y?EL9S&6iE# zkq`bc)0GAI23re63UD_A)iG^@Y>AErkN9yBOH;F8rE}rO&);PMLIjfd#fzU(Qsi(f zCIwJkOCY+i+xM}Y@wkOQBi-cH+>@#+=eRH@mC#Gsudo5(l=f{ z{Rhtmvv?@)Y9O$0F4j<2u73{#ctXubp$EX_TdiutA8<5R;KX>{_%Xqb#l(4VEBxE| z^1THY-IXhcDJh5YEvWg(^I?6KzIUkI_PUuBK%(WJi}-!cHLarpJ4bVzsK-76L^4-+ zZ2VhnxLP20&|l0-KS3%p^9;Em;oJz@p9_8EGM)BV44FvNgQ+;;Lj1K(s@h(TxMq< zT^y@{r~t(V=F6M<=8YAA53o6CSzeRec8sT$=dTN6^I02mf%3WXGtomGN8g(ZNx~|Q zd+H|f;}WgB64=xr4p7@5!Z2P32(3MG$iyxhNOMD57Zn^LV`A28ne7)H16(6uq5d|l{ovtLsC&qz>7SbjX$+># zwbfO-p%N!3GO#9&FjaSfCwhd1Bch^2ZdV^9I40BGJmtUQ>fy1_`{5L94@gBAOX65~ znifgta&vRxDWA4Sxm+#eCuL#Y>+CGzQF89rJn0;Ogu7NO)hI_tgWpvu#y>kvBca_O_aPSD(V8N1#Bm2mw8~_G|wir+9PUjy<^tU0jjp#(ju5JfZr<+RCaAOyWQe zpg;RDHg-~+$_$=I$@kgYg-JFj1~5T{bSr^5;@657|0Ot${<*{&t($-O(!h z^{p9bQ7_@?*FPN^8$lE{B#Pv5pPzJ{9$Dl8vExZtyg%jG(xKiX#>OLn)QSoUplx+_ zc0LXD+X9S#otAd+;6Z5mp3uYRf@umfa&rZt?fNHKO|>77Ix_U~2hxauG;`tXS!?jc zqeSP=9}KH<2WS~<3nM-d`TfPKra#B@SnZXNc_RcB;ssCn^4}`q5D#_!x4JIDT&sZH z24mr$Yg=_3d0>*YfC4r~CMG6EMu~X*H~KkPRbB6G1@b7<&K1kP<6m-rMV- zghv0%)eZg%YA|4P2jx)Ff{51->;Lx1!gT1fXDVnbU1WYdq!xB@f~4Dh&=2m(ove})cW5$N;W^%{oQiw) ztzNe$i%aD^nxgnXM}|_9mrR~=7b+>RWg(^znT-Ywn2hs?hlYR?u)d{bes&hFi+OJ~ z`Ofx*($b=MH#l@w-Gf=<@1qB^Iq$*(V3^^}vDG!cZ$Wo6IWh+-dXYl*xnKyxw zXr@dnfA$FJr5iV*K!t*tUSG!!4+xbYuJz;?gA*bN%q&x4z83_U9^Y8s*q9rsl!5N& z^yxR~b0aLAAQFFv8d+XeMs-5+GKIW^*eUGF5SxWM12I~a~}Co@V!E{1_S%6{tl@!xvklp%HcxW0q{RS zx+tWQKa`O<{~U;!(~J(m(ab`S5j4a3xj7&-^oblqJo2*P^L-uVAXK3qDn~ONEv**` z3FUXb9u~1YPy@$rMeUxj^MC;DOexFUcW|I#t*f55YxADXE`q^6Y}<%O*i_9UhY*C6 z2>rVN2Y0KX#==9Iu>ap$)lDuYSvxog^6{yu`S6=;11nmm0IY@p!|>U#+}+8|?bDQ4 zl-8|V37~xe!zZ;Jm5Zk(rp^epw(9@1dMG9Yd+hMN(=8OokO|=N1t^3s!^6=>GsRnaZ-U@5PikvG!$@S|Uo{TCE*3F|ssNGPBlh1wll?Ec ziNl-}JoC4O1=4ePq+5Fow1SOX)IJJ6_sMe+HHo^Oekf0e5f4#xtl z03OdS-=YKE2xJH)Ma2e}FMzZFAn2&!7ZpkEGi&(l1yNB33}$eP?qx#(&|Y}P@FJB9 zh*D4rtmI6##ZDcpeGO_fU@1Jh=ecOuDz!EkK*`MO`tILO*ZQ*yX~8!w98whkb1UHd zk_9B3Dw{Aq!inDk|G8o)?4!t+FJW;&@iW`u7DnjCgJ~F}p_rl#i%aF!E!aeUBP&Zw zZlDcttHvyj7J;q{sw8wyoGJj_SHJ}6>bi~()dmeZGLr7tG03CuQ}rXpL|0SEiJB}e zE!StVazfc;hP%6+7kZq;hbnhxD~EULU?x`PM>aB4Invn`X<{$)F+({#joHp_Gv%ZQ zby$~u#izSNC0ow1R110fkn}Lj%l>UaB>WhAkhj512pU^Y7$~5FGLQUk>1uHmzY`W< zMBg*3yR$P==j>uXHxuYR`f(z>V5kQFt!6yBa@PqS=HgI+Nec|o>}_nm)z`;>5AXUO zhwoiYe_vjhJ%Gwk34f4aWN#NJ)apXT*8Cg$%3E9g)^#PNy20z!dm7KssHi?hCc80&fT{s zW)ujdd49{@exWQm8Bba>B{6qi#o)Z#{h_X`cnaeC;}6|so!!W&Yo+b5?n#xAX)*Kj zr=CzXL>{V2g3?sakDKY-v$S$soR4`Cd8Hk^VKeV%mbV%j`Mu)bzmzGVG^64_D;nwdc7ZgHNyre>edNo-Qw+EEh>GT1%m@ zM)YVD$?IWi+45_4kxN2C+K%BlGyKLH9;oQqyKmkNoWJZ=_kwHJ~L9;b{XD6ubre4>ho+!7+ z**Cr*t15R*KC! z8gh1J&4;b4?^AY`_0oY6zQ{CQ>aa5!tzkaDnGI-inF48Y--44Imb<*vXRm5*;2HXd zNZdJ8O2#H{mfAg|!@NO<}G|JPQw_J{Zi9S9mXP^638o3c#Id literal 17353 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 z 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 mMB}CUMZX3{Qm%)pbLxu diff --git a/tests/drawing/cairo/baseline_images/graph_mark_groups_directed.png b/tests/drawing/cairo/baseline_images/graph_mark_groups_directed.png index 83d6e37647f1b6fe90f88f080974afea8573013d..b94e821698e145d8096544d1fd8cb4a63f5cde33 100644 GIT binary patch literal 18020 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 literal 17353 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 z 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 mMB}CUMZX3{Qm%)pbLxu diff --git a/tests/drawing/cairo/baseline_images/graph_mark_groups_squares_directed.png b/tests/drawing/cairo/baseline_images/graph_mark_groups_squares_directed.png index c5372745cb597e2bbd728fcaa117abd639f956cb..0e437f288eecd666b272c9f68bbc506f32e59dfc 100644 GIT binary patch literal 15899 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 literal 14924 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 From 511c2c67eb64e37d30c86396f0bc1cf72ca1ba5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Tue, 29 Jul 2025 00:02:32 +0800 Subject: [PATCH 247/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 9ce1e1695..f858f76c2 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 9ce1e169536382e23e02d7310073adfe3bc2a9f5 +Subproject commit f858f76c2f5d2483c446ed9e8cedbc47cf74f617 From a78b0256a6445fb985d0719c6fa59d0ce40a2278 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 2 Aug 2025 12:52:52 +0200 Subject: [PATCH 248/276] chore: updated contributors list --- .all-contributorsrc | 9 +++++++++ CONTRIBUTORS.md | 3 +++ 2 files changed, 12 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index af4e6a3d4..d175b8231 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -639,6 +639,15 @@ "contributions": [ "code" ] + }, + { + "login": "SKG24", + "name": "Sanat Kumar Gupta", + "avatar_url": "https://avatars.githubusercontent.com/u/123228827?v=4", + "profile": "https://github.com/SKG24", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5c5714306..d22d5bd6f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -97,6 +97,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Tim Bernhard
Tim Bernhard

💻 Bea Márton
Bea Márton

💻 + + Sanat Kumar Gupta
Sanat Kumar Gupta

💻 + From 5a29c796755f4a70bf6e1e6995c6cb94bab56159 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 2 Aug 2025 12:58:06 +0200 Subject: [PATCH 249/276] style: code formatting --- .../articulation_points.py | 7 ++- doc/examples_sphinx-gallery/betweenness.py | 10 +-- .../bipartite_matching.py | 3 +- .../bipartite_matching_maxflow.py | 5 +- doc/examples_sphinx-gallery/bridges.py | 9 +-- .../cluster_contraction.py | 11 +++- doc/examples_sphinx-gallery/complement.py | 9 +-- doc/examples_sphinx-gallery/configuration.py | 1 + .../connected_components.py | 3 +- .../delaunay-triangulation.py | 5 +- doc/examples_sphinx-gallery/erdos_renyi.py | 35 +++-------- doc/examples_sphinx-gallery/generate_dag.py | 1 + doc/examples_sphinx-gallery/isomorphism.py | 19 +++++- doc/examples_sphinx-gallery/maxflow.py | 9 +-- .../minimum_spanning_trees.py | 1 + .../online_user_actions.py | 49 ++++++++------- .../personalized_pagerank.py | 4 +- doc/examples_sphinx-gallery/quickstart.py | 25 +++++--- doc/examples_sphinx-gallery/ring_animation.py | 3 + .../shortest_path_visualisation.py | 22 +++---- doc/examples_sphinx-gallery/simplify.py | 63 ++++++++++++------- doc/examples_sphinx-gallery/spanning_trees.py | 9 +-- .../stochastic_variability.py | 56 ++++++++++++----- .../topological_sort.py | 13 ++-- doc/examples_sphinx-gallery/visual_style.py | 3 +- .../visualize_cliques.py | 22 ++++--- .../visualize_communities.py | 6 +- scripts/fix_pyodide_build.py | 4 +- scripts/patch_modularized_graph_methods.py | 3 +- setup.py | 4 +- src/igraph/__init__.py | 4 +- src/igraph/app/shell.py | 1 - src/igraph/community.py | 2 +- src/igraph/configuration.py | 2 +- src/igraph/drawing/__init__.py | 1 - src/igraph/drawing/cairo/edge.py | 19 +++--- src/igraph/drawing/cairo/plot.py | 3 +- src/igraph/drawing/plotly/edge.py | 19 +++--- src/igraph/drawing/shapes.py | 1 - src/igraph/io/bipartite.py | 2 +- src/igraph/io/images.py | 10 +-- src/igraph/layout.py | 1 - src/igraph/seq.py | 4 +- src/igraph/sparse_matrix.py | 22 +++---- tests/drawing/matplotlib/test_graph.py | 8 ++- tests/test_cliques.py | 18 +++++- tests/test_conversion.py | 7 +-- tests/test_generators.py | 15 ++++- tests/test_layouts.py | 8 ++- 49 files changed, 320 insertions(+), 241 deletions(-) diff --git a/doc/examples_sphinx-gallery/articulation_points.py b/doc/examples_sphinx-gallery/articulation_points.py index 0dadded36..6492f1f6f 100644 --- a/doc/examples_sphinx-gallery/articulation_points.py +++ b/doc/examples_sphinx-gallery/articulation_points.py @@ -8,6 +8,7 @@ This example shows how to compute and visualize the `articulation points `_ in a graph using :meth:`igraph.GraphBase.articulation_points`. For an example on bridges instead, see :ref:`tutorials-bridges`. """ + import igraph as ig import matplotlib.pyplot as plt @@ -30,9 +31,9 @@ vertex_size=30, vertex_color="lightblue", vertex_label=range(g.vcount()), - vertex_frame_color = ["red" if v in articulation_points else "black" for v in g.vs], - vertex_frame_width = [3 if v in articulation_points else 1 for v in g.vs], + vertex_frame_color=["red" if v in articulation_points else "black" for v in g.vs], + vertex_frame_width=[3 if v in articulation_points else 1 for v in g.vs], edge_width=0.8, - edge_color='gray' + edge_color="gray", ) plt.show() diff --git a/doc/examples_sphinx-gallery/betweenness.py b/doc/examples_sphinx-gallery/betweenness.py index 29ed9b3e3..7c52bdb08 100644 --- a/doc/examples_sphinx-gallery/betweenness.py +++ b/doc/examples_sphinx-gallery/betweenness.py @@ -24,14 +24,14 @@ # :meth:`igraph.utils.rescale` to rescale the betweennesses in the interval # ``[0, 1]``. def plot_betweenness(g, vertex_betweenness, edge_betweenness, ax, cax1, cax2): - '''Plot vertex/edge betweenness, with colorbars + """Plot vertex/edge betweenness, with colorbars Args: g: the graph to plot. ax: the Axes for the graph cax1: the Axes for the vertex betweenness colorbar cax2: the Axes for the edge betweenness colorbar - ''' + """ # Rescale betweenness to be between 0.0 and 1.0 scaled_vertex_betweenness = ig.rescale(vertex_betweenness, clamp=True) @@ -45,7 +45,7 @@ def plot_betweenness(g, vertex_betweenness, edge_betweenness, ax, cax1, cax2): # Plot graph g.vs["color"] = [cmap1(betweenness) for betweenness in scaled_vertex_betweenness] - g.vs["size"] = ig.rescale(vertex_betweenness, (10, 50)) + g.vs["size"] = ig.rescale(vertex_betweenness, (10, 50)) g.es["color"] = [cmap2(betweenness) for betweenness in scaled_edge_betweenness] g.es["width"] = ig.rescale(edge_betweenness, (0.5, 1.0)) ig.plot( @@ -58,8 +58,8 @@ def plot_betweenness(g, vertex_betweenness, edge_betweenness, ax, cax1, cax2): # Color bars norm1 = ScalarMappable(norm=Normalize(0, max(vertex_betweenness)), cmap=cmap1) norm2 = ScalarMappable(norm=Normalize(0, max(edge_betweenness)), cmap=cmap2) - plt.colorbar(norm1, cax=cax1, orientation="horizontal", label='Vertex Betweenness') - plt.colorbar(norm2, cax=cax2, orientation="horizontal", label='Edge Betweenness') + plt.colorbar(norm1, cax=cax1, orientation="horizontal", label="Vertex Betweenness") + plt.colorbar(norm2, cax=cax2, orientation="horizontal", label="Edge Betweenness") # %% diff --git a/doc/examples_sphinx-gallery/bipartite_matching.py b/doc/examples_sphinx-gallery/bipartite_matching.py index ad7538fa2..1046e1322 100644 --- a/doc/examples_sphinx-gallery/bipartite_matching.py +++ b/doc/examples_sphinx-gallery/bipartite_matching.py @@ -7,6 +7,7 @@ This example demonstrates an efficient way to find and visualise a maximum biparite matching using :meth:`igraph.Graph.maximum_bipartite_matching`. """ + import igraph as ig import matplotlib.pyplot as plt @@ -16,7 +17,7 @@ # - nodes 5-8 to the other side g = ig.Graph.Bipartite( [0, 0, 0, 0, 0, 1, 1, 1, 1], - [(0, 5), (1, 6), (1, 7), (2, 5), (2, 8), (3, 6), (4, 5), (4, 6)] + [(0, 5), (1, 6), (1, 7), (2, 5), (2, 8), (3, 6), (4, 5), (4, 6)], ) # %% diff --git a/doc/examples_sphinx-gallery/bipartite_matching_maxflow.py b/doc/examples_sphinx-gallery/bipartite_matching_maxflow.py index 28b06e0b7..eec39a81b 100644 --- a/doc/examples_sphinx-gallery/bipartite_matching_maxflow.py +++ b/doc/examples_sphinx-gallery/bipartite_matching_maxflow.py @@ -9,6 +9,7 @@ .. note:: :meth:`igraph.Graph.maximum_bipartite_matching` is usually a better way to find the maximum bipartite matching. For a demonstration on how to use that method instead, check out :ref:`tutorials-bipartite-matching`. """ + import igraph as ig import matplotlib.pyplot as plt @@ -17,7 +18,7 @@ g = ig.Graph( 9, [(0, 4), (0, 5), (1, 4), (1, 6), (1, 7), (2, 5), (2, 7), (2, 8), (3, 6), (3, 7)], - directed=True + directed=True, ) # %% @@ -62,6 +63,6 @@ vertex_size=30, vertex_label=range(g.vcount()), vertex_color=["lightblue" if i < 9 else "orange" for i in range(11)], - edge_width=[1.0 + flow.flow[i] for i in range(g.ecount())] + edge_width=[1.0 + flow.flow[i] for i in range(g.ecount())], ) plt.show() diff --git a/doc/examples_sphinx-gallery/bridges.py b/doc/examples_sphinx-gallery/bridges.py index 26b8dce6a..08dc47d48 100644 --- a/doc/examples_sphinx-gallery/bridges.py +++ b/doc/examples_sphinx-gallery/bridges.py @@ -7,6 +7,7 @@ This example shows how to compute and visualize the `bridges `_ in a graph using :meth:`igraph.GraphBase.bridges`. For an example on articulation points instead, see :ref:`tutorials-articulation-points`. """ + import igraph as ig import matplotlib.pyplot as plt @@ -37,7 +38,7 @@ target=ax, vertex_size=30, vertex_color="lightblue", - vertex_label=range(g.vcount()) + vertex_label=range(g.vcount()), ) plt.show() @@ -72,9 +73,9 @@ vertex_size=30, vertex_color="lightblue", vertex_label=range(g.vcount()), - edge_background="#FFF0", # transparent background color - edge_align_label=True, # make sure labels are aligned with the edge + edge_background="#FFF0", # transparent background color + edge_align_label=True, # make sure labels are aligned with the edge edge_label=g.es["label"], - edge_label_color="red" + edge_label_color="red", ) plt.show() diff --git a/doc/examples_sphinx-gallery/cluster_contraction.py b/doc/examples_sphinx-gallery/cluster_contraction.py index af997dc5d..fc02fc293 100644 --- a/doc/examples_sphinx-gallery/cluster_contraction.py +++ b/doc/examples_sphinx-gallery/cluster_contraction.py @@ -7,6 +7,7 @@ This example shows how to find the communities in a graph, then contract each community into a single node using :class:`igraph.clustering.VertexClustering`. For this tutorial, we'll use the *Donald Knuth's Les Miserables Network*, which shows the coapperances of characters in the novel *Les Miserables*. """ + import igraph as ig import matplotlib.pyplot as plt @@ -106,7 +107,10 @@ # Finally, we can assign colors to the clusters and plot the cluster graph, # including a legend to make things clear: palette2 = ig.GradientPalette("gainsboro", "black") -g.es["color"] = [palette2.get(int(i)) for i in ig.rescale(cluster_graph.es["size"], (0, 255), clamp=True)] +g.es["color"] = [ + palette2.get(int(i)) + for i in ig.rescale(cluster_graph.es["size"], (0, 255), clamp=True) +] fig2, ax2 = plt.subplots() ig.plot( @@ -123,7 +127,8 @@ legend_handles = [] for i in range(num_communities): handle = ax2.scatter( - [], [], + [], + [], s=100, facecolor=palette1.get(i), edgecolor="k", @@ -133,7 +138,7 @@ ax2.legend( handles=legend_handles, - title='Community:', + title="Community:", bbox_to_anchor=(0, 1.0), bbox_transform=ax2.transAxes, ) diff --git a/doc/examples_sphinx-gallery/complement.py b/doc/examples_sphinx-gallery/complement.py index 4436ae372..b4f2f17a0 100644 --- a/doc/examples_sphinx-gallery/complement.py +++ b/doc/examples_sphinx-gallery/complement.py @@ -7,6 +7,7 @@ This example shows how to generate the `complement graph `_ of a graph (sometimes known as the anti-graph) using :meth:`igraph.GraphBase.complementer`. """ + import igraph as ig import matplotlib.pyplot as plt import random @@ -49,14 +50,14 @@ layout="circle", vertex_color="black", ) -axs[0, 0].set_title('Original graph') +axs[0, 0].set_title("Original graph") ig.plot( g2, target=axs[0, 1], layout="circle", vertex_color="black", ) -axs[0, 1].set_title('Complement graph') +axs[0, 1].set_title("Complement graph") ig.plot( g_full, @@ -64,12 +65,12 @@ layout="circle", vertex_color="black", ) -axs[1, 0].set_title('Union graph') +axs[1, 0].set_title("Union graph") ig.plot( g_empty, target=axs[1, 1], layout="circle", vertex_color="black", ) -axs[1, 1].set_title('Complement of union graph') +axs[1, 1].set_title("Complement of union graph") plt.show() diff --git a/doc/examples_sphinx-gallery/configuration.py b/doc/examples_sphinx-gallery/configuration.py index 5c12a0044..4e9c077eb 100644 --- a/doc/examples_sphinx-gallery/configuration.py +++ b/doc/examples_sphinx-gallery/configuration.py @@ -7,6 +7,7 @@ This example shows how to use igraph's :class:`configuration instance ` to set default igraph settings. This is useful for setting global settings so that they don't need to be explicitly stated at the beginning of every igraph project you work on. """ + import igraph as ig import matplotlib.pyplot as plt import random diff --git a/doc/examples_sphinx-gallery/connected_components.py b/doc/examples_sphinx-gallery/connected_components.py index 008f66cec..33c341fee 100644 --- a/doc/examples_sphinx-gallery/connected_components.py +++ b/doc/examples_sphinx-gallery/connected_components.py @@ -7,6 +7,7 @@ This example demonstrates how to visualise the connected components in a graph using :meth:`igraph.GraphBase.connected_components`. """ + import igraph as ig import matplotlib.pyplot as plt import random @@ -21,7 +22,7 @@ # %% # Now we can cluster the graph into weakly connected components, i.e. subgraphs # that have no edges connecting them to one another: -components = g.connected_components(mode='weak') +components = g.connected_components(mode="weak") # %% # Finally, we can visualize the distinct connected components of the graph: diff --git a/doc/examples_sphinx-gallery/delaunay-triangulation.py b/doc/examples_sphinx-gallery/delaunay-triangulation.py index ba6ce5a85..3a25eebe1 100644 --- a/doc/examples_sphinx-gallery/delaunay-triangulation.py +++ b/doc/examples_sphinx-gallery/delaunay-triangulation.py @@ -8,6 +8,7 @@ This example demonstrates how to calculate the `Delaunay triangulation `_ of an input graph. We start by generating a set of points on a 2D grid using random ``numpy`` arrays and a graph with those vertex coordinates and no edges. """ + import numpy as np from scipy.spatial import Delaunay import igraph as ig @@ -20,8 +21,8 @@ np.random.seed(0) x, y = np.random.rand(2, 30) g = ig.Graph(30) -g.vs['x'] = x -g.vs['y'] = y +g.vs["x"] = x +g.vs["y"] = y # %% # Because we already set the `x` and `y` vertex attributes, we can use diff --git a/doc/examples_sphinx-gallery/erdos_renyi.py b/doc/examples_sphinx-gallery/erdos_renyi.py index c4f2879f7..aeb808992 100644 --- a/doc/examples_sphinx-gallery/erdos_renyi.py +++ b/doc/examples_sphinx-gallery/erdos_renyi.py @@ -12,6 +12,7 @@ We generate two graphs of each, so we can confirm that our graph generator is truly random. """ + import igraph as ig import matplotlib.pyplot as plt import random @@ -48,33 +49,11 @@ # differences: fig, axs = plt.subplots(2, 2) # Probability -ig.plot( - g1, - target=axs[0, 0], - layout="circle", - vertex_color="lightblue" -) -ig.plot( - g2, - target=axs[0, 1], - layout="circle", - vertex_color="lightblue" -) -axs[0, 0].set_ylabel('Probability') +ig.plot(g1, target=axs[0, 0], layout="circle", vertex_color="lightblue") +ig.plot(g2, target=axs[0, 1], layout="circle", vertex_color="lightblue") +axs[0, 0].set_ylabel("Probability") # N edges -ig.plot( - g3, - target=axs[1, 0], - layout="circle", - vertex_color="lightblue", - vertex_size=15 -) -ig.plot( - g4, - target=axs[1, 1], - layout="circle", - vertex_color="lightblue", - vertex_size=15 -) -axs[1, 0].set_ylabel('N. edges') +ig.plot(g3, target=axs[1, 0], layout="circle", vertex_color="lightblue", vertex_size=15) +ig.plot(g4, target=axs[1, 1], layout="circle", vertex_color="lightblue", vertex_size=15) +axs[1, 0].set_ylabel("N. edges") plt.show() diff --git a/doc/examples_sphinx-gallery/generate_dag.py b/doc/examples_sphinx-gallery/generate_dag.py index addbc661c..1565a59ed 100644 --- a/doc/examples_sphinx-gallery/generate_dag.py +++ b/doc/examples_sphinx-gallery/generate_dag.py @@ -8,6 +8,7 @@ This example demonstrates how to create a random directed acyclic graph (DAG), which is useful in a number of contexts including for Git commit history. """ + import igraph as ig import matplotlib.pyplot as plt import random diff --git a/doc/examples_sphinx-gallery/isomorphism.py b/doc/examples_sphinx-gallery/isomorphism.py index 7f98bc053..57c6034f4 100644 --- a/doc/examples_sphinx-gallery/isomorphism.py +++ b/doc/examples_sphinx-gallery/isomorphism.py @@ -7,6 +7,7 @@ This example shows how to check for `isomorphism `_ between small graphs using :meth:`igraph.GraphBase.isomorphic`. """ + import igraph as ig import matplotlib.pyplot as plt @@ -66,6 +67,20 @@ target=axs[2], **visual_style, ) -fig.text(0.38, 0.5, '$\\simeq$' if g1.isomorphic(g2) else '$\\neq$', fontsize=15, ha='center', va='center') -fig.text(0.65, 0.5, '$\\simeq$' if g2.isomorphic(g3) else '$\\neq$', fontsize=15, ha='center', va='center') +fig.text( + 0.38, + 0.5, + "$\\simeq$" if g1.isomorphic(g2) else "$\\neq$", + fontsize=15, + ha="center", + va="center", +) +fig.text( + 0.65, + 0.5, + "$\\simeq$" if g2.isomorphic(g3) else "$\\neq$", + fontsize=15, + ha="center", + va="center", +) plt.show() diff --git a/doc/examples_sphinx-gallery/maxflow.py b/doc/examples_sphinx-gallery/maxflow.py index 246d4c451..eb76684a4 100644 --- a/doc/examples_sphinx-gallery/maxflow.py +++ b/doc/examples_sphinx-gallery/maxflow.py @@ -8,16 +8,13 @@ This example shows how to construct a max flow on a directed graph with edge capacities using :meth:`igraph.Graph.maxflow`. """ + import igraph as ig import matplotlib.pyplot as plt # %% # First, we generate a graph and assign a "capacity" to each edge: -g = ig.Graph( - 6, - [(3, 2), (3, 4), (2, 1), (4,1), (4, 5), (1, 0), (5, 0)], - directed=True -) +g = ig.Graph(6, [(3, 2), (3, 4), (2, 1), (4, 1), (4, 5), (1, 0), (5, 0)], directed=True) g.es["capacity"] = [7, 8, 1, 2, 3, 4, 5] # %% @@ -39,6 +36,6 @@ target=ax, layout="circle", vertex_label=range(g.vcount()), - vertex_color="lightblue" + vertex_color="lightblue", ) plt.show() diff --git a/doc/examples_sphinx-gallery/minimum_spanning_trees.py b/doc/examples_sphinx-gallery/minimum_spanning_trees.py index 60c320bf6..9177e976a 100644 --- a/doc/examples_sphinx-gallery/minimum_spanning_trees.py +++ b/doc/examples_sphinx-gallery/minimum_spanning_trees.py @@ -8,6 +8,7 @@ This example shows how to generate a `minimum spanning tree `_ from an input graph using :meth:`igraph.Graph.spanning_tree`. If you only need a regular spanning tree, check out :ref:`tutorials-spanning-trees`. """ + import random import igraph as ig import matplotlib.pyplot as plt diff --git a/doc/examples_sphinx-gallery/online_user_actions.py b/doc/examples_sphinx-gallery/online_user_actions.py index 1da528570..47cc00ff5 100644 --- a/doc/examples_sphinx-gallery/online_user_actions.py +++ b/doc/examples_sphinx-gallery/online_user_actions.py @@ -18,23 +18,24 @@ # indicates a certain action taken by a user (e.g. click on a button within a # website). Actual user data usually come with time stamp, but that's not # essential for this example. -action_dataframe = pd.DataFrame([ - ['dsj3239asadsa3', 'createPage', 'greatProject'], - ['2r09ej221sk2k5', 'editPage', 'greatProject'], - ['dsj3239asadsa3', 'editPage', 'greatProject'], - ['789dsadafj32jj', 'editPage', 'greatProject'], - ['oi32ncwosap399', 'editPage', 'greatProject'], - ['4r4320dkqpdokk', 'createPage', 'miniProject'], - ['320eljl3lk3239', 'editPage', 'miniProject'], - ['dsj3239asadsa3', 'editPage', 'miniProject'], - ['3203ejew332323', 'createPage', 'private'], - ['3203ejew332323', 'editPage', 'private'], - ['40m11919332msa', 'createPage', 'private2'], - ['40m11919332msa', 'editPage', 'private2'], - ['dsj3239asadsa3', 'createPage', 'anotherGreatProject'], - ['2r09ej221sk2k5', 'editPage', 'anotherGreatProject'], +action_dataframe = pd.DataFrame( + [ + ["dsj3239asadsa3", "createPage", "greatProject"], + ["2r09ej221sk2k5", "editPage", "greatProject"], + ["dsj3239asadsa3", "editPage", "greatProject"], + ["789dsadafj32jj", "editPage", "greatProject"], + ["oi32ncwosap399", "editPage", "greatProject"], + ["4r4320dkqpdokk", "createPage", "miniProject"], + ["320eljl3lk3239", "editPage", "miniProject"], + ["dsj3239asadsa3", "editPage", "miniProject"], + ["3203ejew332323", "createPage", "private"], + ["3203ejew332323", "editPage", "private"], + ["40m11919332msa", "createPage", "private2"], + ["40m11919332msa", "editPage", "private2"], + ["dsj3239asadsa3", "createPage", "anotherGreatProject"], + ["2r09ej221sk2k5", "editPage", "anotherGreatProject"], ], - columns=['userid', 'action', 'project'], + columns=["userid", "action", "project"], ) # %% @@ -42,7 +43,7 @@ # We choose to use a weighted adjacency matrix for this, i.e. a table with rows # and columns indexes by the users that has nonzero entries whenever folks # collaborate. First, let's get the users and prepare an empty matrix: -users = action_dataframe['userid'].unique() +users = action_dataframe["userid"].unique() adjacency_matrix = pd.DataFrame( np.zeros((len(users), len(users)), np.int32), index=users, @@ -51,8 +52,8 @@ # %% # Then, let's iterate over all projects one by one, and add all collaborations: -for _project, project_data in action_dataframe.groupby('project'): - project_users = project_data['userid'].values +for _project, project_data in action_dataframe.groupby("project"): + project_users = project_data["userid"].values for i1, user1 in enumerate(project_users): for user2 in project_users[:i1]: adjacency_matrix.at[user1, user2] += 1 @@ -60,12 +61,12 @@ # %% # There are many ways to achieve the above matrix, so don't be surprised if you # came up with another algorithm ;-) Now it's time to make the graph: -g = ig.Graph.Weighted_Adjacency(adjacency_matrix, mode='plus') +g = ig.Graph.Weighted_Adjacency(adjacency_matrix, mode="plus") # %% # We can take a look at the graph via plotting functions. We can first make a # layout: -layout = g.layout('circle') +layout = g.layout("circle") # %% # Then we can prepare vertex sizes based on their closeness to other vertices @@ -79,7 +80,7 @@ g, target=ax, layout=layout, - vertex_label=g.vs['name'], + vertex_label=g.vs["name"], vertex_color="lightblue", vertex_size=vertex_size, edge_width=g.es["weight"], @@ -89,14 +90,14 @@ # %% # Loops indicate "self-collaborations", which are not very meaningful. To # filter out loops without losing the edge weights, we can use: -g = g.simplify(combine_edges='first') +g = g.simplify(combine_edges="first") fig, ax = plt.subplots() ig.plot( g, target=ax, layout=layout, - vertex_label=g.vs['name'], + vertex_label=g.vs["name"], vertex_color="lightblue", vertex_size=vertex_size, edge_width=g.es["weight"], diff --git a/doc/examples_sphinx-gallery/personalized_pagerank.py b/doc/examples_sphinx-gallery/personalized_pagerank.py index 2fd044612..f7e2f9e2b 100644 --- a/doc/examples_sphinx-gallery/personalized_pagerank.py +++ b/doc/examples_sphinx-gallery/personalized_pagerank.py @@ -55,7 +55,9 @@ def plot_pagerank(graph: ig.Graph, p_pagerank: list[float]): ig.plot(graph, target=ax, layout=graph.layout_grid()) # Add a colorbar - sm = cm.ScalarMappable(norm=plt.Normalize(min(p_pagerank), max(p_pagerank)), cmap=cmap) + sm = cm.ScalarMappable( + norm=plt.Normalize(min(p_pagerank), max(p_pagerank)), cmap=cmap + ) plt.colorbar(sm, ax=ax, label="Personalized PageRank") plt.title("Graph with Personalized PageRank") diff --git a/doc/examples_sphinx-gallery/quickstart.py b/doc/examples_sphinx-gallery/quickstart.py index ec6f7ddb7..0735768e5 100644 --- a/doc/examples_sphinx-gallery/quickstart.py +++ b/doc/examples_sphinx-gallery/quickstart.py @@ -15,6 +15,7 @@ To find out more features that igraph has to offer, check out the :ref:`gallery`! """ + import igraph as ig import matplotlib.pyplot as plt @@ -25,7 +26,13 @@ # Set attributes for the graph, nodes, and edges g["title"] = "Small Social Network" -g.vs["name"] = ["Daniel Morillas", "Kathy Archer", "Kyle Ding", "Joshua Walton", "Jana Hoyer"] +g.vs["name"] = [ + "Daniel Morillas", + "Kathy Archer", + "Kyle Ding", + "Joshua Walton", + "Jana Hoyer", +] g.vs["gender"] = ["M", "F", "F", "M", "F"] g.es["married"] = [False, False, False, False, False, False, False, True] @@ -35,27 +42,29 @@ # Plot in matplotlib # Note that attributes can be set globally (e.g. vertex_size), or set individually using arrays (e.g. vertex_color) -fig, ax = plt.subplots(figsize=(5,5)) +fig, ax = plt.subplots(figsize=(5, 5)) ig.plot( g, target=ax, - layout="circle", # print nodes in a circular layout + layout="circle", # print nodes in a circular layout vertex_size=30, - vertex_color=["steelblue" if gender == "M" else "salmon" for gender in g.vs["gender"]], + vertex_color=[ + "steelblue" if gender == "M" else "salmon" for gender in g.vs["gender"] + ], vertex_frame_width=4.0, vertex_frame_color="white", vertex_label=g.vs["name"], vertex_label_size=7.0, edge_width=[2 if married else 1 for married in g.es["married"]], - edge_color=["#7142cf" if married else "#AAA" for married in g.es["married"]] + edge_color=["#7142cf" if married else "#AAA" for married in g.es["married"]], ) plt.show() # Save the graph as an image file -fig.savefig('social_network.png') -fig.savefig('social_network.jpg') -fig.savefig('social_network.pdf') +fig.savefig("social_network.png") +fig.savefig("social_network.jpg") +fig.savefig("social_network.pdf") # Export and import a graph as a GML file. g.save("social_network.gml") diff --git a/doc/examples_sphinx-gallery/ring_animation.py b/doc/examples_sphinx-gallery/ring_animation.py index 25daa2918..33bd6a109 100644 --- a/doc/examples_sphinx-gallery/ring_animation.py +++ b/doc/examples_sphinx-gallery/ring_animation.py @@ -9,6 +9,7 @@ order to animate a ring graph sequentially being revealed. """ + import igraph as ig import matplotlib.pyplot as plt import matplotlib.animation as animation @@ -23,6 +24,7 @@ # Compute a 2D ring layout that looks like an actual ring layout = g.layout_circle() + # %% # Prepare an update function. This "callback" function will be run at every # frame and takes as a single argument the frame number. For simplicity, at @@ -71,6 +73,7 @@ def _update_graph(frame): handles = ax.get_children()[:nhandles] return handles + # %% # Run the animation fig, ax = plt.subplots() diff --git a/doc/examples_sphinx-gallery/shortest_path_visualisation.py b/doc/examples_sphinx-gallery/shortest_path_visualisation.py index 87fa01523..fedad160a 100644 --- a/doc/examples_sphinx-gallery/shortest_path_visualisation.py +++ b/doc/examples_sphinx-gallery/shortest_path_visualisation.py @@ -8,15 +8,13 @@ This example demonstrates how to find the shortest distance between two vertices of a weighted or an unweighted graph. """ + import igraph as ig import matplotlib.pyplot as plt # %% # To find the shortest path or distance between two nodes, we can use :meth:`igraph.GraphBase.get_shortest_paths`. If we're only interested in counting the unweighted distance, then we can do the following: -g = ig.Graph( - 6, - [(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)] -) +g = ig.Graph(6, [(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)]) results = g.get_shortest_paths(1, to=4, output="vpath") # results = [[1, 0, 2, 4]] @@ -25,7 +23,7 @@ # We can print the result of the computation: if len(results[0]) > 0: # The distance is the number of vertices in the shortest path minus one. - print("Shortest distance is: ", len(results[0])-1) + print("Shortest distance is: ", len(results[0]) - 1) else: print("End node could not be reached!") @@ -60,20 +58,20 @@ # %% # In case you are wondering how the visualization figure was done, here's the code: -g.es['width'] = 0.5 -g.es[results[0]]['width'] = 2.5 +g.es["width"] = 0.5 +g.es[results[0]]["width"] = 2.5 fig, ax = plt.subplots() ig.plot( g, target=ax, - layout='circle', - vertex_color='steelblue', + layout="circle", + vertex_color="steelblue", vertex_label=range(g.vcount()), - edge_width=g.es['width'], + edge_width=g.es["width"], edge_label=g.es["weight"], - edge_color='#666', + edge_color="#666", edge_align_label=True, - edge_background='white' + edge_background="white", ) plt.show() diff --git a/doc/examples_sphinx-gallery/simplify.py b/doc/examples_sphinx-gallery/simplify.py index 893b67fae..ed36b2da5 100644 --- a/doc/examples_sphinx-gallery/simplify.py +++ b/doc/examples_sphinx-gallery/simplify.py @@ -5,25 +5,28 @@ This example shows how to remove self loops and multiple edges using :meth:`igraph.GraphBase.simplify`. """ + import igraph as ig import matplotlib.pyplot as plt # %% # We start with a graph that includes loops and multiedges: -g1 = ig.Graph([ - (0, 1), - (1, 2), - (2, 3), - (3, 4), - (4, 0), - (0, 0), - (1, 4), - (1, 4), - (0, 2), - (2, 4), - (2, 4), - (2, 4), - (3, 3)], +g1 = ig.Graph( + [ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 0), + (0, 0), + (1, 4), + (1, 4), + (0, 2), + (2, 4), + (2, 4), + (2, 4), + (3, 3), + ], ) # %% @@ -59,13 +62,29 @@ target=axs[1], **visual_style, ) -axs[0].set_title('Multigraph...') -axs[1].set_title('...simplified') +axs[0].set_title("Multigraph...") +axs[1].set_title("...simplified") # Draw rectangles around axes -axs[0].add_patch(plt.Rectangle( - (0, 0), 1, 1, fc='none', ec='k', lw=4, transform=axs[0].transAxes, - )) -axs[1].add_patch(plt.Rectangle( - (0, 0), 1, 1, fc='none', ec='k', lw=4, transform=axs[1].transAxes, - )) +axs[0].add_patch( + plt.Rectangle( + (0, 0), + 1, + 1, + fc="none", + ec="k", + lw=4, + transform=axs[0].transAxes, + ) +) +axs[1].add_patch( + plt.Rectangle( + (0, 0), + 1, + 1, + fc="none", + ec="k", + lw=4, + transform=axs[1].transAxes, + ) +) plt.show() diff --git a/doc/examples_sphinx-gallery/spanning_trees.py b/doc/examples_sphinx-gallery/spanning_trees.py index 82f190a8a..cfca1135f 100644 --- a/doc/examples_sphinx-gallery/spanning_trees.py +++ b/doc/examples_sphinx-gallery/spanning_trees.py @@ -7,6 +7,7 @@ This example shows how to generate a spanning tree from an input graph using :meth:`igraph.Graph.spanning_tree`. For the related idea of finding a *minimum spanning tree*, see :ref:`tutorials-minimum-spanning-trees`. """ + import igraph as ig import matplotlib.pyplot as plt import random @@ -47,13 +48,7 @@ g.es[spanning_tree]["width"] = 3.0 fig, ax = plt.subplots() -ig.plot( - g, - target=ax, - layout=layout, - vertex_color="lightblue", - edge_width=g.es["width"] -) +ig.plot(g, target=ax, layout=layout, vertex_color="lightblue", edge_width=g.es["width"]) plt.show() # %% diff --git a/doc/examples_sphinx-gallery/stochastic_variability.py b/doc/examples_sphinx-gallery/stochastic_variability.py index ea126aecf..2afc01153 100644 --- a/doc/examples_sphinx-gallery/stochastic_variability.py +++ b/doc/examples_sphinx-gallery/stochastic_variability.py @@ -8,6 +8,7 @@ This example demonstrates the use of stochastic community detection methods to check whether a network possesses a strong community structure, and whether the partitionings we obtain are meaningul. Many community detection algorithms are randomized, and return somewhat different results after each run, depending on the random seed that was set. When there is a robust community structure, we expect these results to be similar to each other. When the community structure is weak or non-existent, the results may be noisy and highly variable. We will employ several partion similarity measures to analyse the consistency of the results, including the normalized mutual information (NMI), the variation of information (VI), and the Rand index (RI). """ + # %% import igraph as ig import matplotlib.pyplot as plt @@ -24,7 +25,7 @@ # We will use Zachary's karate club dataset [1]_, a classic example of a network # with a strong community structure: karate = ig.Graph.Famous("Zachary") - + # %% # We will compare it to an an Erdős-Rényi :math:`G(n, m)` random network having # the same number of vertices and edges. The parameters 'n' and 'm' refer to the @@ -36,39 +37,47 @@ # First, let us plot the two networks for a visual comparison: # Create subplots -fig, axes = plt.subplots(1, 2, figsize=(12, 6), subplot_kw={'aspect': 'equal'}) +fig, axes = plt.subplots(1, 2, figsize=(12, 6), subplot_kw={"aspect": "equal"}) # Karate club network ig.plot( - karate, target=axes[0], - vertex_color="lightblue", vertex_size=30, - vertex_label=range(karate.vcount()), vertex_label_size=10, - edge_width=1 + karate, + target=axes[0], + vertex_color="lightblue", + vertex_size=30, + vertex_label=range(karate.vcount()), + vertex_label_size=10, + edge_width=1, ) axes[0].set_title("Karate club network") # Random network ig.plot( - random_graph, target=axes[1], - vertex_color="lightcoral", vertex_size=30, - vertex_label=range(random_graph.vcount()), vertex_label_size=10, - edge_width=1 + random_graph, + target=axes[1], + vertex_color="lightcoral", + vertex_size=30, + vertex_label=range(random_graph.vcount()), + vertex_label_size=10, + edge_width=1, ) axes[1].set_title("Erdős-Rényi random network") plt.show() + # %% # Function to compute similarity between partitions using various methods: def compute_pairwise_similarity(partitions, method): similarities = [] - + for p1, p2 in itertools.combinations(partitions, 2): similarity = ig.compare_communities(p1, p2, method=method) similarities.append(similarity) - + return similarities + # %% # The Leiden method, accessible through :meth:`igraph.Graph.community_leiden()`, # is a modularity maximization approach for community detection. Since exact @@ -78,12 +87,16 @@ def compute_pairwise_similarity(partitions, method): # results may differ each time the method is applied. The following function # runs the Leiden algorithm multiple times: def run_experiment(graph, iterations=100): - partitions = [graph.community_leiden(objective_function='modularity').membership for _ in range(iterations)] + partitions = [ + graph.community_leiden(objective_function="modularity").membership + for _ in range(iterations) + ] nmi_scores = compute_pairwise_similarity(partitions, method="nmi") vi_scores = compute_pairwise_similarity(partitions, method="vi") ri_scores = compute_pairwise_similarity(partitions, method="rand") return nmi_scores, vi_scores, ri_scores + # %% # Run the experiment on both networks: nmi_karate, vi_karate, ri_karate = run_experiment(karate) @@ -106,9 +119,13 @@ def run_experiment(graph, iterations=100): for i, (karate_scores, random_scores, measure, lower, upper) in enumerate(measures): # Karate club histogram axes[0][i].hist( - karate_scores, bins=20, range=(lower, upper), + karate_scores, + bins=20, + range=(lower, upper), density=True, # Probability density - alpha=0.7, color=colors[i], edgecolor="black" + alpha=0.7, + color=colors[i], + edgecolor="black", ) axes[0][i].set_title(f"{measure} - Karate club network") axes[0][i].set_xlabel(f"{measure} score") @@ -116,8 +133,13 @@ def run_experiment(graph, iterations=100): # Random network histogram axes[1][i].hist( - random_scores, bins=20, range=(lower, upper), density=True, - alpha=0.7, color=colors[i], edgecolor="black" + random_scores, + bins=20, + range=(lower, upper), + density=True, + alpha=0.7, + color=colors[i], + edgecolor="black", ) axes[1][i].set_title(f"{measure} - Random network") axes[1][i].set_xlabel(f"{measure} score") diff --git a/doc/examples_sphinx-gallery/topological_sort.py b/doc/examples_sphinx-gallery/topological_sort.py index 7b0a05481..3c558f90b 100644 --- a/doc/examples_sphinx-gallery/topological_sort.py +++ b/doc/examples_sphinx-gallery/topological_sort.py @@ -7,6 +7,7 @@ This example demonstrates how to get a topological sorting on a directed acyclic graph (DAG). A topological sorting of a directed graph is a linear ordering based on the precedence implied by the directed edges. It exists iff the graph doesn't have any cycle. In ``igraph``, we can use :meth:`igraph.GraphBase.topological_sorting` to get a topological ordering of the vertices. """ + import igraph as ig import matplotlib.pyplot as plt @@ -26,21 +27,21 @@ # A topological sorting can be computed quite easily by calling # :meth:`igraph.GraphBase.topological_sorting`, which returns a list of vertex IDs. # If the given graph is not DAG, the error will occur. -results = g.topological_sorting(mode='out') -print('Topological sort of g (out):', *results) +results = g.topological_sorting(mode="out") +print("Topological sort of g (out):", *results) # %% # In fact, there are two modes of :meth:`igraph.GraphBase.topological_sorting`, # ``'out'`` ``'in'``. ``'out'`` is the default and starts from a node with # indegree equal to 0. Vice versa, ``'in'`` starts from a node with outdegree # equal to 0. To call the other mode, we can simply use: -results = g.topological_sorting(mode='in') -print('Topological sort of g (in):', *results) +results = g.topological_sorting(mode="in") +print("Topological sort of g (in):", *results) # %% # We can use :meth:`igraph.Vertex.indegree` to find the indegree of the node. for i in range(g.vcount()): - print('degree of {}: {}'.format(i, g.vs[i].indegree())) + print("degree of {}: {}".format(i, g.vs[i].indegree())) # % # Finally, we can plot the graph to make the situation a little clearer. @@ -51,7 +52,7 @@ ig.plot( g, target=ax, - layout='kk', + layout="kk", vertex_size=25, edge_width=4, vertex_label=range(g.vcount()), diff --git a/doc/examples_sphinx-gallery/visual_style.py b/doc/examples_sphinx-gallery/visual_style.py index 4ac7d05da..30c27d73b 100644 --- a/doc/examples_sphinx-gallery/visual_style.py +++ b/doc/examples_sphinx-gallery/visual_style.py @@ -6,6 +6,7 @@ This example shows how to change the visual style of network plots. """ + import igraph as ig import matplotlib.pyplot as plt import random @@ -17,7 +18,7 @@ "edge_width": 0.3, "vertex_size": 15, "palette": "heat", - "layout": "fruchterman_reingold" + "layout": "fruchterman_reingold", } # %% diff --git a/doc/examples_sphinx-gallery/visualize_cliques.py b/doc/examples_sphinx-gallery/visualize_cliques.py index 15f149360..c1ebd49d6 100644 --- a/doc/examples_sphinx-gallery/visualize_cliques.py +++ b/doc/examples_sphinx-gallery/visualize_cliques.py @@ -8,12 +8,13 @@ This example shows how to compute and visualize cliques of a graph using :meth:`igraph.GraphBase.cliques`. """ + import igraph as ig import matplotlib.pyplot as plt # %% # First, let's create a graph, for instance the famous karate club graph: -g = ig.Graph.Famous('Zachary') +g = ig.Graph.Famous("Zachary") # %% # Computing cliques can be done as follows: @@ -27,12 +28,13 @@ for clique, ax in zip(cliques, axs): ig.plot( ig.VertexCover(g, [clique]), - mark_groups=True, palette=ig.RainbowPalette(), + mark_groups=True, + palette=ig.RainbowPalette(), vertex_size=5, edge_width=0.5, target=ax, ) -plt.axis('off') +plt.axis("off") plt.show() @@ -45,16 +47,16 @@ axs = axs.ravel() for clique, ax in zip(cliques, axs): # Color vertices yellow/red based on whether they are in this clique - g.vs['color'] = 'yellow' - g.vs[clique]['color'] = 'red' + g.vs["color"] = "yellow" + g.vs[clique]["color"] = "red" # Color edges black/red based on whether they are in this clique clique_edges = g.es.select(_within=clique) - g.es['color'] = 'black' - clique_edges['color'] = 'red' + g.es["color"] = "black" + clique_edges["color"] = "red" # also increase thickness of clique edges - g.es['width'] = 0.3 - clique_edges['width'] = 1 + g.es["width"] = 0.3 + clique_edges["width"] = 1 ig.plot( ig.VertexCover(g, [clique]), @@ -63,5 +65,5 @@ vertex_size=5, target=ax, ) -plt.axis('off') +plt.axis("off") plt.show() diff --git a/doc/examples_sphinx-gallery/visualize_communities.py b/doc/examples_sphinx-gallery/visualize_communities.py index a13edbfa9..9ab696e8e 100644 --- a/doc/examples_sphinx-gallery/visualize_communities.py +++ b/doc/examples_sphinx-gallery/visualize_communities.py @@ -7,6 +7,7 @@ This example shows how to visualize communities or clusters of a graph. """ + import igraph as ig import matplotlib.pyplot as plt @@ -47,7 +48,8 @@ legend_handles = [] for i in range(num_communities): handle = ax.scatter( - [], [], + [], + [], s=100, facecolor=palette.get(i), edgecolor="k", @@ -56,7 +58,7 @@ legend_handles.append(handle) ax.legend( handles=legend_handles, - title='Community:', + title="Community:", bbox_to_anchor=(0, 1.0), bbox_transform=ax.transAxes, ) diff --git a/scripts/fix_pyodide_build.py b/scripts/fix_pyodide_build.py index 46d190518..9e239f973 100644 --- a/scripts/fix_pyodide_build.py +++ b/scripts/fix_pyodide_build.py @@ -4,7 +4,9 @@ from pathlib import Path from urllib.request import urlretrieve -target_dir = Path(pyodide_build.__file__).parent / "tools" / "cmake" / "Modules" / "Platform" +target_dir = ( + Path(pyodide_build.__file__).parent / "tools" / "cmake" / "Modules" / "Platform" +) target_dir.mkdir(exist_ok=True, parents=True) target_file = target_dir / "Emscripten.cmake" diff --git a/scripts/patch_modularized_graph_methods.py b/scripts/patch_modularized_graph_methods.py index 9e188b6b5..081ab73a5 100644 --- a/scripts/patch_modularized_graph_methods.py +++ b/scripts/patch_modularized_graph_methods.py @@ -13,7 +13,6 @@ def main(): - # Get instance and classmethods g = igraph.Graph() methods = inspect.getmembers(g, predicate=inspect.ismethod) @@ -43,7 +42,7 @@ def main(): newmodule = igraph.__file__ + ".new" with open(newmodule, "wt") as fout: # FIXME: whitelisting all cases is not great, try to improve - for (origin, value) in auxiliary_imports: + for origin, value in auxiliary_imports: fout.write(f"from {origin} import {value}\n") with open(igraph.__file__, "rt") as f: diff --git a/setup.py b/setup.py index a8d1c965a..c919d3507 100644 --- a/setup.py +++ b/setup.py @@ -1000,9 +1000,7 @@ def get_tag(self): # Dependencies needed for plotting with Cairo "cairo": ["cairocffi>=1.2.0"], # Dependencies needed for plotting with Matplotlib - "matplotlib": [ - "matplotlib>=3.6.0; platform_python_implementation != 'PyPy'" - ], + "matplotlib": ["matplotlib>=3.6.0; platform_python_implementation != 'PyPy'"], # Dependencies needed for plotting with Plotly "plotly": ["plotly>=5.3.0"], # Compatibility alias to 'cairo' for python-igraph <= 0.9.6 diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 0b1690849..27bb0ff59 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -2,7 +2,6 @@ igraph library. """ - __license__ = """ Copyright (C) 2006- The igraph development team @@ -951,8 +950,7 @@ def Incidence(cls, *args, **kwds): def are_connected(self, *args, **kwds): """Deprecated alias to L{Graph.are_adjacent()}.""" deprecated( - "Graph.are_connected() is deprecated; use Graph.are_adjacent() " - "instead" + "Graph.are_connected() is deprecated; use Graph.are_adjacent() " "instead" ) return self.are_adjacent(*args, **kwds) diff --git a/src/igraph/app/shell.py b/src/igraph/app/shell.py index 6a22eb097..d8318bcd0 100644 --- a/src/igraph/app/shell.py +++ b/src/igraph/app/shell.py @@ -19,7 +19,6 @@ Mac OS X users are likely to invoke igraph from the command line. """ - from abc import ABCMeta, abstractmethod import re import sys diff --git a/src/igraph/community.py b/src/igraph/community.py index 0fdcdf154..8baa337c7 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -392,7 +392,7 @@ def _community_leiden( initial_membership=None, n_iterations=2, node_weights=None, - **kwds + **kwds, ): """Finds the community structure of the graph using the Leiden algorithm of Traag, van Eck & Waltman. diff --git a/src/igraph/configuration.py b/src/igraph/configuration.py index d0202f541..9accb5325 100644 --- a/src/igraph/configuration.py +++ b/src/igraph/configuration.py @@ -326,7 +326,7 @@ def load(self, stream=None): if file_was_open: stream.close() - def save(self, stream: Optional[Union[str, IO[str]]]=None): + def save(self, stream: Optional[Union[str, IO[str]]] = None): """Saves the configuration. @param stream: name of a file or a file-like object. The configuration diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index 2725745f6..05743728a 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -26,7 +26,6 @@ (formerly known as Sketch, also free) or Adobe Illustrator. """ - from pathlib import Path from warnings import warn diff --git a/src/igraph/drawing/cairo/edge.py b/src/igraph/drawing/cairo/edge.py index 1b0c46731..7535004c9 100644 --- a/src/igraph/drawing/cairo/edge.py +++ b/src/igraph/drawing/cairo/edge.py @@ -168,14 +168,16 @@ def draw_directed_edge(self, edge, src_vertex, dest_vertex): ] # Midpoint of the base of the arrow triangle - x_arrow_mid, y_arrow_mid = (aux_points[0][0] + aux_points[1][0]) / 2.0, ( - aux_points[0][1] + aux_points[1][1] - ) / 2.0 + x_arrow_mid, y_arrow_mid = ( + (aux_points[0][0] + aux_points[1][0]) / 2.0, + (aux_points[0][1] + aux_points[1][1]) / 2.0, + ) # Vector representing the base of the arrow triangle x_arrow_base_vec, y_arrow_base_vec = ( - aux_points[0][0] - aux_points[1][0] - ), (aux_points[0][1] - aux_points[1][1]) + (aux_points[0][0] - aux_points[1][0]), + (aux_points[0][1] - aux_points[1][1]), + ) # Recalculate the curve such that it lands on the base of the arrow triangle aux1, aux2 = get_bezier_control_points_for_curved_edge( @@ -224,9 +226,10 @@ def draw_directed_edge(self, edge, src_vertex, dest_vertex): ] # Midpoint of the base of the arrow triangle - x_arrow_mid, y_arrow_mid = (aux_points[0][0] + aux_points[1][0]) / 2.0, ( - aux_points[0][1] + aux_points[1][1] - ) / 2.0 + x_arrow_mid, y_arrow_mid = ( + (aux_points[0][0] + aux_points[1][0]) / 2.0, + (aux_points[0][1] + aux_points[1][1]) / 2.0, + ) # Draw the line ctx.line_to(x_arrow_mid, y_arrow_mid) diff --git a/src/igraph/drawing/cairo/plot.py b/src/igraph/drawing/cairo/plot.py index f35db6ef1..5ac247b8f 100644 --- a/src/igraph/drawing/cairo/plot.py +++ b/src/igraph/drawing/cairo/plot.py @@ -22,7 +22,6 @@ (formerly known as Sketch, also free) or Adobe Illustrator. """ - import os from io import BytesIO @@ -286,7 +285,7 @@ def redraw(self, context=None): bbox=bbox, palette=palette, *args, # noqa: B026 - **kwds + **kwds, ) if opacity < 1.0: ctx.pop_group_to_source() diff --git a/src/igraph/drawing/plotly/edge.py b/src/igraph/drawing/plotly/edge.py index 15ea40a29..d9ee1e2cb 100644 --- a/src/igraph/drawing/plotly/edge.py +++ b/src/igraph/drawing/plotly/edge.py @@ -103,14 +103,16 @@ def draw_directed_edge(self, edge, src_vertex, dest_vertex): ] # Midpoint of the base of the arrow triangle - x_arrow_mid, y_arrow_mid = (aux_points[0][0] + aux_points[1][0]) / 2.0, ( - aux_points[0][1] + aux_points[1][1] - ) / 2.0 + x_arrow_mid, y_arrow_mid = ( + (aux_points[0][0] + aux_points[1][0]) / 2.0, + (aux_points[0][1] + aux_points[1][1]) / 2.0, + ) # Vector representing the base of the arrow triangle x_arrow_base_vec, y_arrow_base_vec = ( - aux_points[0][0] - aux_points[1][0] - ), (aux_points[0][1] - aux_points[1][1]) + (aux_points[0][0] - aux_points[1][0]), + (aux_points[0][1] - aux_points[1][1]), + ) # Recalculate the curve such that it lands on the base of the arrow triangle aux1, aux2 = get_bezier_control_points_for_curved_edge( @@ -161,9 +163,10 @@ def draw_directed_edge(self, edge, src_vertex, dest_vertex): ] # Midpoint of the base of the arrow triangle - x_arrow_mid, y_arrow_mid = (aux_points[0][0] + aux_points[1][0]) / 2.0, ( - aux_points[0][1] + aux_points[1][1] - ) / 2.0 + x_arrow_mid, y_arrow_mid = ( + (aux_points[0][0] + aux_points[1][0]) / 2.0, + (aux_points[0][1] + aux_points[1][1]) / 2.0, + ) # Draw the line path.append( format_path_step( diff --git a/src/igraph/drawing/shapes.py b/src/igraph/drawing/shapes.py index 4f0ea97b5..0c3ccd1c8 100644 --- a/src/igraph/drawing/shapes.py +++ b/src/igraph/drawing/shapes.py @@ -16,7 +16,6 @@ name in the C{shape} attribute of vertices. """ - __all__ = ("ShapeDrawerDirectory",) from abc import ABCMeta, abstractmethod diff --git a/src/igraph/io/bipartite.py b/src/igraph/io/bipartite.py index 70fac3750..24750d911 100644 --- a/src/igraph/io/bipartite.py +++ b/src/igraph/io/bipartite.py @@ -6,7 +6,7 @@ def _construct_bipartite_graph_from_adjacency( multiple=False, weighted=None, *args, - **kwds + **kwds, ): """Creates a bipartite graph from a bipartite adjacency matrix. diff --git a/src/igraph/io/images.py b/src/igraph/io/images.py index 202e39862..da1c8b2c9 100644 --- a/src/igraph/io/images.py +++ b/src/igraph/io/images.py @@ -17,7 +17,7 @@ def _write_graph_to_svg( edge_stroke_widths="width", font_size=16, *args, - **kwds + **kwds, ): """Saves the graph as an SVG (Scalable Vector Graphics) file @@ -289,16 +289,12 @@ def _write_graph_to_svg( vs = str(vertex_size) print( ' '.format( - vs, c[0] - ), + -{0},0" fill="{1}"/>'.format(vs, c[0]), file=f, ) print( ' '.format( - vs, c[1] - ), + -{0},0" fill="{1}"/>'.format(vs, c[1]), file=f, ) print( diff --git a/src/igraph/layout.py b/src/igraph/layout.py index f5b0a9181..17877e8f6 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -6,7 +6,6 @@ This package contains the implementation of the L{Layout} object. """ - from math import sin, cos, pi from igraph._igraph import GraphBase diff --git a/src/igraph/seq.py b/src/igraph/seq.py index 6107cfc9c..3790cf510 100644 --- a/src/igraph/seq.py +++ b/src/igraph/seq.py @@ -737,9 +737,7 @@ def decorated(*args, **kwds): restricted to this sequence, and returns the result. @see: Graph.%(name)s() for details. -""" % { - "name": name - } +""" % {"name": name} return decorated diff --git a/src/igraph/sparse_matrix.py b/src/igraph/sparse_matrix.py index fe9565a79..93c6fa7ae 100644 --- a/src/igraph/sparse_matrix.py +++ b/src/igraph/sparse_matrix.py @@ -24,19 +24,15 @@ def _convert_mode_argument(mode): # resolve mode constants, convert to lowercase - mode = ( - { - ADJ_DIRECTED: "directed", - ADJ_UNDIRECTED: "undirected", - ADJ_MAX: "max", - ADJ_MIN: "min", - ADJ_PLUS: "plus", - ADJ_UPPER: "upper", - ADJ_LOWER: "lower", - } - .get(mode, mode) - .lower() - ) + mode = { + ADJ_DIRECTED: "directed", + ADJ_UNDIRECTED: "undirected", + ADJ_MAX: "max", + ADJ_MIN: "min", + ADJ_PLUS: "plus", + ADJ_UPPER: "upper", + ADJ_LOWER: "lower", + }.get(mode, mode).lower() if mode not in _SUPPORTED_MODES: raise ValueError("mode should be one of " + (" ".join(_SUPPORTED_MODES))) diff --git a/tests/drawing/matplotlib/test_graph.py b/tests/drawing/matplotlib/test_graph.py index a044d323e..b77d42174 100644 --- a/tests/drawing/matplotlib/test_graph.py +++ b/tests/drawing/matplotlib/test_graph.py @@ -54,9 +54,11 @@ def test_labels(self): g = Graph.Ring(5) fig, ax = plt.subplots(figsize=(3, 3)) plot( - g, target=ax, layout=self.layout_small_ring, - vertex_label=['1', '2', '3', '4', '5'], - vertex_label_color='white', + g, + target=ax, + layout=self.layout_small_ring, + vertex_label=["1", "2", "3", "4", "5"], + vertex_label_color="white", vertex_label_size=16, ) diff --git a/tests/test_cliques.py b/tests/test_cliques.py index 3c1c215e9..b5801a5a1 100644 --- a/tests/test_cliques.py +++ b/tests/test_cliques.py @@ -138,7 +138,14 @@ def testLargestIndependentVertexSets(self): self.assertEqual( self.g1.largest_independent_vertex_sets(), [(0, 3, 4), (2, 3, 4)] ) - self.assertTrue(all(map(self.g1.is_independent_vertex_set, self.g1.largest_independent_vertex_sets()))) + self.assertTrue( + all( + map( + self.g1.is_independent_vertex_set, + self.g1.largest_independent_vertex_sets(), + ) + ) + ) def testMaximalIndependentVertexSets(self): self.assertEqual( @@ -155,7 +162,14 @@ def testMaximalIndependentVertexSets(self): (2, 4, 7, 8), ], ) - self.assertTrue(all(map(self.g2.is_independent_vertex_set, self.g2.maximal_independent_vertex_sets()))) + self.assertTrue( + all( + map( + self.g2.is_independent_vertex_set, + self.g2.maximal_independent_vertex_sets(), + ) + ) + ) def testIndependenceNumber(self): self.assertEqual(self.g2.independence_number(), 6) diff --git a/tests/test_conversion.py b/tests/test_conversion.py index bbd9574c5..6f3cdc22e 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -186,8 +186,7 @@ def testFromPrufer(self): self.assertEqual(6, g.vcount()) self.assertEqual(5, g.ecount()) self.assertEqual( - [(0, 3), (1, 3), (2, 3), (3, 4), (4, 5)], - sorted(g.get_edgelist()) + [(0, 3), (1, 3), (2, 3), (3, 4), (4, 5)], sorted(g.get_edgelist()) ) def testToPrufer(self): @@ -202,9 +201,7 @@ def suite(): representation_suite = unittest.defaultTestLoader.loadTestsFromTestCase( GraphRepresentationTests ) - prufer_suite = unittest.defaultTestLoader.loadTestsFromTestCase( - PruferTests - ) + prufer_suite = unittest.defaultTestLoader.loadTestsFromTestCase(PruferTests) return unittest.TestSuite([direction_suite, representation_suite, prufer_suite]) diff --git a/tests/test_generators.py b/tests/test_generators.py index 30cc58d23..4414e48ae 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -162,7 +162,20 @@ def testHexagonalLattice(self): self.assertEqual(sorted(g.get_edgelist()), sorted(el + [(y, x) for x, y in el])) def testHypercube(self): - el = [(0, 1), (0, 2), (0, 4), (1, 3), (1, 5), (2, 3), (2, 6), (3, 7), (4, 5), (4, 6), (5, 7), (6, 7)] + el = [ + (0, 1), + (0, 2), + (0, 4), + (1, 3), + (1, 5), + (2, 3), + (2, 6), + (3, 7), + (4, 5), + (4, 6), + (5, 7), + (6, 7), + ] g = Graph.Hypercube(3) self.assertEqual(g.get_edgelist(), el) diff --git a/tests/test_layouts.py b/tests/test_layouts.py index ac32e0210..63cf95cb6 100644 --- a/tests/test_layouts.py +++ b/tests/test_layouts.py @@ -242,8 +242,12 @@ def testKamadaKawai(self): self.assertTrue(bbox.right <= 6) lo = g.layout( - "kk", miny=[2] * 100, maxy=[3] * 100, minx=[4] * 100, maxx=[6] * 100, - weights=range(10, g.ecount() + 10) + "kk", + miny=[2] * 100, + maxy=[3] * 100, + minx=[4] * 100, + maxx=[6] * 100, + weights=range(10, g.ecount() + 10), ) self.assertTrue(isinstance(lo, Layout)) From c02d42db1a546304f6c9bb8ce8f128133d57ac27 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 5 Aug 2025 00:14:23 +0200 Subject: [PATCH 250/276] fix: remove usage of IGRAPH_NEGINFINITY --- src/_igraph/convert.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 248c35387..999163824 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -374,7 +374,7 @@ int igraphmodule_PyObject_to_eigen_which_t(PyObject *o, w->pos = IGRAPH_EIGEN_LM; w->howmany = 1; w->il = w->iu = -1; - w->vl = IGRAPH_NEGINFINITY; + w->vl = -IGRAPH_INFINITY; w->vu = IGRAPH_INFINITY; w->vestimate = 0; w->balance = IGRAPH_LAPACK_DGEEVX_BALANCE_NONE; From 8c10051c9ea21ab2e906750a35790ceef55ee6f0 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 5 Aug 2025 00:15:09 +0200 Subject: [PATCH 251/276] feat: added node_in_weights argument to Graph.community_leiden() --- src/_igraph/graphobject.c | 22 +++++++++++++++++----- src/igraph/community.py | 7 +++++++ vendor/source/igraph | 2 +- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 6393de1c1..f6d368c35 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13611,11 +13611,12 @@ PyObject *igraphmodule_Graph_community_walktrap(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"edge_weights", "node_weights", "resolution", + static char *kwlist[] = {"edge_weights", "node_weights", "node_in_weights", "resolution", "normalize_resolution", "beta", "initial_membership", "n_iterations", NULL}; PyObject *edge_weights_o = Py_None; PyObject *node_weights_o = Py_None; + PyObject *node_in_weights_o = Py_None; PyObject *initial_membership_o = Py_None; PyObject *normalize_resolution = Py_False; PyObject *res = Py_None; @@ -13624,14 +13625,14 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, Py_ssize_t n_iterations = 2; double resolution = 1.0; double beta = 0.01; - igraph_vector_t *edge_weights = NULL, *node_weights = NULL; + igraph_vector_t *edge_weights = NULL, *node_weights = NULL, *node_in_weights = NULL; igraph_vector_int_t *membership = NULL; igraph_bool_t start = true; igraph_integer_t nb_clusters = 0; igraph_real_t quality = 0.0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOdOdOn", kwlist, - &edge_weights_o, &node_weights_o, &resolution, &normalize_resolution, &beta, &initial_membership_o, &n_iterations)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOdOdOn", kwlist, + &edge_weights_o, &node_weights_o, &node_in_weights_o, &resolution, &normalize_resolution, &beta, &initial_membership_o, &n_iterations)) return NULL; if (n_iterations >= 0) { @@ -13654,6 +13655,13 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, error = -1; } + /* Get node in-weights (directed case) */ + if (!error && igraphmodule_attrib_to_vector_t(node_in_weights_o, self, &node_in_weights, + ATTRIBUTE_TYPE_VERTEX)) { + igraphmodule_handle_igraph_error(); + error = -1; + } + /* Get initial membership */ if (!error && igraphmodule_attrib_to_vector_int_t(initial_membership_o, self, &membership, ATTRIBUTE_TYPE_VERTEX)) { @@ -13696,7 +13704,7 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, /* Run actual Leiden algorithm for several iterations. */ if (!error) { error = igraph_community_leiden(&self->g, - edge_weights, node_weights, + edge_weights, node_weights, node_in_weights, resolution, beta, start, n_iterations, membership, &nb_clusters, &quality); @@ -13710,6 +13718,10 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, igraph_vector_destroy(node_weights); free(node_weights); } + if (node_in_weights != 0) { + igraph_vector_destroy(node_in_weights); + free(node_in_weights); + } if (!error && membership != 0) { res = igraphmodule_vector_int_t_to_PyList(membership); diff --git a/src/igraph/community.py b/src/igraph/community.py index 67648ad7f..a3c6ce50b 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -461,6 +461,7 @@ def _community_leiden( initial_membership=None, n_iterations=2, node_weights=None, + node_in_weights=None, **kwds, ): """Finds the community structure of the graph using the Leiden @@ -491,6 +492,11 @@ def _community_leiden( If this is not provided, it will be automatically determined on the basis of whether you want to use CPM or modularity. If you do provide this, please make sure that you understand what you are doing. + @param node_in_weights: the inbound node weights used in the directed + variant of the Leiden algorithm. If this is not provided, it will be + automatically determined on the basis of whether you want to use CPM or + modularity. If you do provide this, please make sure that you understand + what you are doing. @return: an appropriate L{VertexClustering} object with an extra attribute called C{quality} that stores the value of the internal quality function optimized by the algorithm. @@ -512,6 +518,7 @@ def _community_leiden( graph, edge_weights=weights, node_weights=node_weights, + node_in_weights=node_in_weights, resolution=resolution, normalize_resolution=(objective_function == "modularity"), beta=beta, diff --git a/vendor/source/igraph b/vendor/source/igraph index f858f76c2..ecc38dc1b 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit f858f76c2f5d2483c446ed9e8cedbc47cf74f617 +Subproject commit ecc38dc1b2eb1b9ec0c35f71bc60bbdcadc111b2 From d9b43f95d1d90d7a0cfe568740be2c1bb5e5c6a1 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 5 Aug 2025 02:14:16 +0200 Subject: [PATCH 252/276] fix: fix normalization in the directed Leiden algorithm --- src/_igraph/graphobject.c | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index f6d368c35..2bae33665 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13672,30 +13672,35 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, if (!error && membership == 0) { start = 0; membership = (igraph_vector_int_t*)calloc(1, sizeof(igraph_vector_int_t)); - if (membership==0) { + if (membership == 0) { PyErr_NoMemory(); error = -1; - } else { - igraph_vector_int_init(membership, 0); + } else if (igraph_vector_int_init(membership, 0)) { + igraphmodule_handle_igraph_error(); + error = -1; } } - if (PyObject_IsTrue(normalize_resolution)) + if (!error && PyObject_IsTrue(normalize_resolution)) { /* If we need to normalize the resolution parameter, * we will need to have node weights. */ if (node_weights == 0) { node_weights = (igraph_vector_t*)calloc(1, sizeof(igraph_vector_t)); - if (node_weights==0) { + if (node_weights == 0) { PyErr_NoMemory(); error = -1; - } else { - igraph_vector_init(node_weights, 0); - if (igraph_strength(&self->g, node_weights, igraph_vss_all(), IGRAPH_ALL, 0, edge_weights)) { - igraphmodule_handle_igraph_error(); - error = -1; - } + } else if (igraph_vector_init(node_weights, 0)) { + igraphmodule_handle_igraph_error(); + error = -1; + } else if (igraph_strength( + &self->g, node_weights, igraph_vss_all(), + igraph_is_directed(&self->g) ? IGRAPH_OUT : IGRAPH_ALL, + IGRAPH_NO_LOOPS, edge_weights + )) { + igraphmodule_handle_igraph_error(); + error = -1; } } resolution /= igraph_vector_sum(node_weights); From 42e7a53ab3c4a66b02ad528f0aa8220bf43e377c Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 5 Aug 2025 02:27:21 +0200 Subject: [PATCH 253/276] refactor: shortest path method auto-selection now delegated to the C core --- src/_igraph/graphobject.c | 36 ++++++++++---------- src/_igraph/utils.c | 70 --------------------------------------- src/_igraph/utils.h | 40 ---------------------- vendor/source/igraph | 2 +- 4 files changed, 18 insertions(+), 130 deletions(-) delete mode 100644 src/_igraph/utils.c delete mode 100644 src/_igraph/utils.h diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 2bae33665..028c2431c 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -33,7 +33,6 @@ #include "indexing.h" #include "memory.h" #include "pyhelpers.h" -#include "utils.h" #include "vertexseqobject.h" #include @@ -5571,14 +5570,14 @@ PyObject *igraphmodule_Graph_get_shortest_path( return NULL; } - if (algorithm == IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO) { - algorithm = igraphmodule_select_shortest_path_algorithm( - &self->g, weights, NULL, mode, /* allow_johnson = */ false - ); - } - /* Call the C function */ switch (algorithm) { + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO: + retval = igraph_get_shortest_path( + &self->g, weights, use_edges ? NULL : &vec, use_edges ? &vec : NULL, from, to, mode + ); + break; + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_DIJKSTRA: retval = igraph_get_shortest_path_dijkstra( &self->g, use_edges ? NULL : &vec, use_edges ? &vec : NULL, from, to, weights, mode @@ -5793,14 +5792,15 @@ PyObject *igraphmodule_Graph_get_shortest_paths(igraphmodule_GraphObject * return NULL; } - if (algorithm == IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO) { - algorithm = igraphmodule_select_shortest_path_algorithm( - &self->g, weights, NULL, mode, /* allow_johnson = */ false - ); - } - /* Call the C function */ switch (algorithm) { + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO: + retval = igraph_get_shortest_paths( + &self->g, weights, use_edges ? NULL : &veclist, use_edges ? &veclist : NULL, + from, to, mode, NULL, NULL + ); + break; + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_DIJKSTRA: retval = igraph_get_shortest_paths_dijkstra( &self->g, use_edges ? NULL : &veclist, use_edges ? &veclist : NULL, @@ -6619,14 +6619,12 @@ PyObject *igraphmodule_Graph_distances( return igraphmodule_handle_igraph_error(); } - if (algorithm == IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO) { - algorithm = igraphmodule_select_shortest_path_algorithm( - &self->g, weights, &from_vs, mode, /* allow_johnson = */ true - ); - } - /* Call the C function */ switch (algorithm) { + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO: + retval = igraph_distances(&self->g, weights, &res, from_vs, to_vs, mode); + break; + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_DIJKSTRA: retval = igraph_distances_dijkstra(&self->g, &res, from_vs, to_vs, weights, mode); break; diff --git a/src/_igraph/utils.c b/src/_igraph/utils.c deleted file mode 100644 index 2fd58420c..000000000 --- a/src/_igraph/utils.c +++ /dev/null @@ -1,70 +0,0 @@ -/* - IGraph library. - Copyright (C) 2006-2023 Tamas Nepusz - - 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 2 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, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - 02110-1301 USA - -*/ - -#include "utils.h" - -/************************ Miscellaneous functions *************************/ - -/** - * Helper function that automatically selects a shortest path algorithm based on - * a graph, its weight vector and the source vertex set (if any). - */ -igraphmodule_shortest_path_algorithm_t igraphmodule_select_shortest_path_algorithm( - const igraph_t* graph, const igraph_vector_t* weights, const igraph_vs_t* from_vs, - igraph_neimode_t mode, igraph_bool_t allow_johnson -) { - igraph_error_t retval; - igraph_integer_t vs_size; - - /* Select the most suitable algorithm */ - if (weights && igraph_vector_size(weights) > 0) { - if (igraph_vector_min(weights) >= 0) { - /* Only positive weights, use Dijkstra's algorithm */ - return IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_DIJKSTRA; - } else if (allow_johnson) { - /* There are negative weights. For a small number of sources, use Bellman-Ford. - * Otherwise, use Johnson's algorithm */ - if (from_vs) { - retval = igraph_vs_size(graph, from_vs, &vs_size); - } else { - retval = IGRAPH_SUCCESS; - vs_size = IGRAPH_INTEGER_MAX; - } - if (retval == IGRAPH_SUCCESS) { - if (vs_size <= 100 || mode != IGRAPH_OUT) { - return IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_BELLMAN_FORD; - } else { - return IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_JOHNSON; - } - } else { - /* Error while calling igraph_vs_size(). Use Bellman-Ford. */ - return IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_BELLMAN_FORD; - } - } else { - /* Johnson's algorithm is disallowed, use Bellman-Ford */ - return IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_BELLMAN_FORD; - } - } else { - /* No weights or empty weight vector, use Dijstra, which should fall back to - * an unweighted algorithm */ - return IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_DIJKSTRA; - } -} diff --git a/src/_igraph/utils.h b/src/_igraph/utils.h deleted file mode 100644 index bc39b84b9..000000000 --- a/src/_igraph/utils.h +++ /dev/null @@ -1,40 +0,0 @@ -/* -*- mode: C -*- */ -/* - IGraph library. - Copyright (C) 2006-2023 Tamas Nepusz - - 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 2 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, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - 02110-1301 USA - -*/ - -#ifndef IGRAPHMODULE_UTILS_H -#define IGRAPHMODULE_UTILS_H - -#include "preamble.h" - -#include -#include -#include "convert.h" -#include "graphobject.h" - -/************************ Miscellaneous functions *************************/ - -igraphmodule_shortest_path_algorithm_t igraphmodule_select_shortest_path_algorithm( - const igraph_t* graph, const igraph_vector_t* weights, const igraph_vs_t* from_vs, - igraph_neimode_t mode, igraph_bool_t allow_johnson -); - -#endif diff --git a/vendor/source/igraph b/vendor/source/igraph index ecc38dc1b..2b761fa3d 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit ecc38dc1b2eb1b9ec0c35f71bc60bbdcadc111b2 +Subproject commit 2b761fa3d5fbe4dc69920597531767ffd15822e1 From 701f8a1390a373d366dbf902245125f25761a50a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sun, 10 Aug 2025 18:07:40 +0000 Subject: [PATCH 254/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 2b761fa3d..be53d68e0 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 2b761fa3d5fbe4dc69920597531767ffd15822e1 +Subproject commit be53d68e08548f3d299cc7bd6bfcfd7b2ea532b4 From 2788bbb5f83ae035803b40abd02825da926b3bb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sun, 10 Aug 2025 18:08:32 +0000 Subject: [PATCH 255/276] fix: update convex_hull() for C API changes --- src/_igraph/igraphmodule.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 0af15fed4..e78b7f620 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -345,7 +345,7 @@ PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwd igraph_matrix_destroy(&mtrx); return NULL; } - if (igraph_convex_hull(&mtrx, &result, 0)) { + if (igraph_convex_hull_2d(&mtrx, &result, 0)) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&mtrx); igraph_vector_int_destroy(&result); @@ -359,7 +359,7 @@ PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwd igraph_matrix_destroy(&mtrx); return NULL; } - if (igraph_convex_hull(&mtrx, 0, &resmat)) { + if (igraph_convex_hull_2d(&mtrx, 0, &resmat)) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&mtrx); igraph_matrix_destroy(&resmat); From c185ea6093bd186d9a2df362e514cded4091ffca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sun, 10 Aug 2025 18:14:19 +0000 Subject: [PATCH 256/276] fix: update rewire() for C API changes --- src/_igraph/convert.c | 13 ------------- src/_igraph/convert.h | 1 - src/_igraph/graphobject.c | 14 +++++++------- src/_igraph/igraphmodule.c | 3 --- src/igraph/__init__.py | 2 -- tests/test_games.py | 6 +++--- 6 files changed, 10 insertions(+), 29 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 999163824..8a93fb8f6 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -766,19 +766,6 @@ int igraphmodule_PyObject_to_reciprocity_t(PyObject *o, igraph_reciprocity_t *re TRANSLATE_ENUM_WITH(reciprocity_tt); } -/** - * \brief Converts a Python object to an igraph \c igraph_rewiring_t - */ -int igraphmodule_PyObject_to_rewiring_t(PyObject *o, igraph_rewiring_t *result) { - static igraphmodule_enum_translation_table_entry_t rewiring_tt[] = { - {"simple", IGRAPH_REWIRING_SIMPLE}, - {"simple_loops", IGRAPH_REWIRING_SIMPLE_LOOPS}, - {"loops", IGRAPH_REWIRING_SIMPLE_LOOPS}, - {0,0} - }; - TRANSLATE_ENUM_WITH(rewiring_tt); -} - /** * \brief Converts a Python object to an igraph \c igraphmodule_shortest_path_algorithm_t */ diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 525afbb5e..a7ca02b16 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -86,7 +86,6 @@ int igraphmodule_PyObject_to_realize_degseq_t(PyObject *o, igraph_realize_degseq int igraphmodule_PyObject_to_random_tree_t(PyObject *o, igraph_random_tree_t *result); int igraphmodule_PyObject_to_random_walk_stuck_t(PyObject *o, igraph_random_walk_stuck_t *result); int igraphmodule_PyObject_to_reciprocity_t(PyObject *o, igraph_reciprocity_t *result); -int igraphmodule_PyObject_to_rewiring_t(PyObject *o, igraph_rewiring_t *result); int igraphmodule_PyObject_to_shortest_path_algorithm_t(PyObject *o, igraphmodule_shortest_path_algorithm_t *result); int igraphmodule_PyObject_to_spinglass_implementation_t(PyObject *o, igraph_spinglass_implementation_t *result); int igraphmodule_PyObject_to_spincomm_update_t(PyObject *o, igraph_spincomm_update_t *result); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 028c2431c..da1d3fbce 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -6512,12 +6512,12 @@ PyObject *igraphmodule_Graph_permute_vertices(igraphmodule_GraphObject *self, PyObject *igraphmodule_Graph_rewire(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "n", "mode", NULL }; - PyObject *n_o = Py_None, *mode_o = Py_None; + static char *kwlist[] = { "n", "allowed_edge_types", NULL }; + PyObject *n_o = Py_None, *allowed_edge_types_o = Py_None; igraph_integer_t n = 10 * igraph_ecount(&self->g); /* TODO overflow check */ - igraph_rewiring_t mode = IGRAPH_REWIRING_SIMPLE; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &n_o, &mode_o)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &n_o, &allowed_edge_types_o)) { return NULL; } @@ -6527,11 +6527,11 @@ PyObject *igraphmodule_Graph_rewire(igraphmodule_GraphObject * self, } } - if (igraphmodule_PyObject_to_rewiring_t(mode_o, &mode)) { + if (igraphmodule_PyObject_to_edge_type_sw_t(allowed_edge_types_o, &allowed_edge_types)) { return NULL; } - if (igraph_rewire(&self->g, n, mode)) { + if (igraph_rewire(&self->g, n, allowed_edge_types)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -16281,7 +16281,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_rewire */ {"rewire", (PyCFunction) igraphmodule_Graph_rewire, METH_VARARGS | METH_KEYWORDS, - "rewire(n=None, mode=\"simple\")\n--\n\n" + "rewire(n=None, allowed_edge_types=\"simple\")\n--\n\n" "Randomly rewires the graph while preserving the degree distribution.\n\n" "The rewiring is done \"in-place\", so the original graph will be modified.\n" "If you want to preserve the original graph, use the L{copy} method before\n" diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index e78b7f620..7c4a09191 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -1158,9 +1158,6 @@ PyObject* PyInit__igraph(void) PyModule_AddIntConstant(m, "GET_ADJACENCY_LOWER", IGRAPH_GET_ADJACENCY_LOWER); PyModule_AddIntConstant(m, "GET_ADJACENCY_BOTH", IGRAPH_GET_ADJACENCY_BOTH); - PyModule_AddIntConstant(m, "REWIRING_SIMPLE", IGRAPH_REWIRING_SIMPLE); - PyModule_AddIntConstant(m, "REWIRING_SIMPLE_LOOPS", IGRAPH_REWIRING_SIMPLE_LOOPS); - PyModule_AddIntConstant(m, "ADJ_DIRECTED", IGRAPH_ADJ_DIRECTED); PyModule_AddIntConstant(m, "ADJ_UNDIRECTED", IGRAPH_ADJ_UNDIRECTED); PyModule_AddIntConstant(m, "ADJ_MAX", IGRAPH_ADJ_MAX); diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index fb33f686c..7c2e50952 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -47,8 +47,6 @@ IN, InternalError, OUT, - REWIRING_SIMPLE, - REWIRING_SIMPLE_LOOPS, STAR_IN, STAR_MUTUAL, STAR_OUT, diff --git a/tests/test_games.py b/tests/test_games.py index 3e78637dc..cf36c75ce 100644 --- a/tests/test_games.py +++ b/tests/test_games.py @@ -163,7 +163,7 @@ def testRewire(self): self.assertTrue(g.is_simple()) # Rewiring with loops (1) - g.rewire(10000, mode="loops") + g.rewire(10000, allowed_edge_types="loops") self.assertEqual(degrees, g.degree()) self.assertFalse(any(g.is_multiple())) @@ -171,7 +171,7 @@ def testRewire(self): g = Graph.Full(4) g[1, 3] = 0 degrees = g.degree() - g.rewire(100, mode="loops") + g.rewire(100, allowed_edge_types="loops") self.assertEqual(degrees, g.degree()) self.assertFalse(any(g.is_multiple())) @@ -185,7 +185,7 @@ def testRewire(self): self.assertTrue(g.is_simple()) # Directed graph with loops - g.rewire(10000, mode="loops") + g.rewire(10000, allowed_edge_types="loops") self.assertEqual(indeg, g.indegree()) self.assertEqual(outdeg, g.outdegree()) self.assertFalse(any(g.is_multiple())) From 7926cb514d7480ae357aac3db913fb22e4d4b1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Mon, 11 Aug 2025 09:50:09 +0000 Subject: [PATCH 257/276] fix: perform mode -> allowed_edge_types parameter name renaming for review() in docs as well --- src/_igraph/graphobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index da1d3fbce..e5bef73b3 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -16288,7 +16288,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "rewiring.\n\n" "@param n: the number of rewiring trials. The default is 10 times the number\n" " of edges.\n" - "@param mode: the rewiring algorithm to use. It can either be C{\"simple\"} or\n" + "@param allowed_edge_types: the rewiring algorithm to use. It can either be C{\"simple\"} or\n" " C{\"loops\"}; the former does not create or destroy loop edges while the\n" " latter does.\n"}, From 38f391154e526744d0086aafb685394eecd0e814 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 20:12:46 +0000 Subject: [PATCH 258/276] build(deps): bump actions/checkout from 4 to 5 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0dd578565..8af4dece1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: name: Build wheels on Linux (x86_64) runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 @@ -41,7 +41,7 @@ jobs: name: Build wheels on Linux (aarch64/manylinux) runs-on: ubuntu-22.04-arm steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: name: Build wheels on Linux (aarch64/musllinux) runs-on: ubuntu-22.04-arm steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 @@ -97,7 +97,7 @@ jobs: wheel_arch: arm64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 @@ -150,7 +150,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 @@ -209,7 +209,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 @@ -262,7 +262,7 @@ jobs: name: Build sdist and test extra dependencies runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 @@ -308,7 +308,7 @@ jobs: env: IGRAPH_CMAKE_EXTRA_ARGS: -DFORCE_COLORED_OUTPUT=ON steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 From 0fce500dc706806541e4808a3e890f3dd8506b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Wed, 13 Aug 2025 08:29:43 +0000 Subject: [PATCH 259/276] chore: update C core --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index be53d68e0..be8bc7d25 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit be53d68e08548f3d299cc7bd6bfcfd7b2ea532b4 +Subproject commit be8bc7d25411da63414743ec16bf20022f4b7bab From a1a164b07b334952422860474b580dd72cb6f21e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Wed, 13 Aug 2025 08:33:49 +0000 Subject: [PATCH 260/276] refactor!: adapt Graph.SBM() interface to changes in C core --- src/_igraph/graphobject.c | 28 +++++++++++++--------------- tests/test_generators.py | 11 ++++------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index e5bef73b3..5887e751f 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -3563,24 +3563,22 @@ PyObject *igraphmodule_Graph_SBM(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - Py_ssize_t n; PyObject *block_sizes_o, *pref_matrix_o; PyObject *directed_o = Py_False; PyObject *loops_o = Py_False; + PyObject *multiple_o = Py_False; igraph_matrix_t pref_matrix; igraph_vector_int_t block_sizes; - static char *kwlist[] = { "n", "pref_matrix", "block_sizes", "directed", - "loops", NULL }; + static char *kwlist[] = { "pref_matrix", "block_sizes", "directed", + "loops", "multiple", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "nOO|OO", kwlist, - &n, &pref_matrix_o, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OOO", kwlist, + &pref_matrix_o, &block_sizes_o, - &directed_o, &loops_o)) + &directed_o, &loops_o, &multiple_o)) return NULL; - CHECK_SSIZE_T_RANGE(n, "vertex count"); - if (igraphmodule_PyObject_to_matrix_t(pref_matrix_o, &pref_matrix, "pref_matrix")) { return NULL; } @@ -3590,7 +3588,7 @@ PyObject *igraphmodule_Graph_SBM(PyTypeObject * type, return NULL; } - if (igraph_sbm_game(&g, n, &pref_matrix, &block_sizes, PyObject_IsTrue(directed_o), PyObject_IsTrue(loops_o))) { + if (igraph_sbm_game(&g, &pref_matrix, &block_sizes, PyObject_IsTrue(directed_o), PyObject_IsTrue(loops_o), PyObject_IsTrue(multiple_o))) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&pref_matrix); igraph_vector_int_destroy(&block_sizes); @@ -14754,19 +14752,19 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_CLASS | METH_KEYWORDS, "SBM(n, pref_matrix, block_sizes, directed=False, loops=False)\n--\n\n" "Generates a graph based on a stochastic block model.\n\n" - "A given number of vertices are generated. Every vertex is assigned to a\n" - "vertex type according to the given block sizes. Vertices of the same\n" + "Every vertex is assigned to a vertex type according to the given block\n" + "sizes, which also determine the total vertex count. Vertices of the same\n" "type will be assigned consecutive vertex IDs. Finally, every\n" "vertex pair is evaluated and an edge is created between them with a\n" "probability depending on the types of the vertices involved. The\n" "probabilities are taken from the preference matrix.\n\n" - "@param n: the number of vertices in the graph\n" - "@param pref_matrix: matrix giving the connection probabilities for\n" - " different vertex types.\n" + "@param pref_matrix: matrix giving the connection probabilities (or expected\n" + " edge multiplicities for multigraphs) between different vertex types.\n" "@param block_sizes: list giving the number of vertices in each block; must\n" " sum up to I{n}.\n" "@param directed: whether to generate a directed graph.\n" - "@param loops: whether loop edges are allowed.\n"}, + "@param loops: whether loop edges are allowed.\n" + "@param multiple: whether multi-edges are allowed.\n"}, // interface to igraph_star {"Star", (PyCFunction) igraphmodule_Graph_Star, diff --git a/tests/test_generators.py b/tests/test_generators.py index bfb1c4e23..675de880a 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -429,9 +429,8 @@ def testLattice(self): def testSBM(self): pref_matrix = [[0.5, 0, 0], [0, 0, 0.5], [0, 0.5, 0]] - n = 60 types = [20, 20, 20] - g = Graph.SBM(n, pref_matrix, types) + g = Graph.SBM(pref_matrix, types) # Simple smoke tests for the expected structure of the graph self.assertTrue(g.is_simple()) @@ -441,21 +440,19 @@ def testSBM(self): self.assertTrue(not any(e.source // 20 == e.target // 20 for e in g2.es)) # Check loops argument - g = Graph.SBM(n, pref_matrix, types, loops=True) + g = Graph.SBM(pref_matrix, types, loops=True) self.assertFalse(g.is_simple()) self.assertTrue(sum(g.is_loop()) > 0) # Check directedness - g = Graph.SBM(n, pref_matrix, types, directed=True) + g = Graph.SBM(pref_matrix, types, directed=True) self.assertTrue(g.is_directed()) self.assertTrue(sum(g.is_mutual()) < g.ecount()) self.assertTrue(sum(g.is_loop()) == 0) # Check error conditions - self.assertRaises(ValueError, Graph.SBM, -1, pref_matrix, types) - self.assertRaises(InternalError, Graph.SBM, 61, pref_matrix, types) pref_matrix[0][1] = 0.7 - self.assertRaises(InternalError, Graph.SBM, 60, pref_matrix, types) + self.assertRaises(InternalError, Graph.SBM, pref_matrix, types) def testTriangularLattice(self): g = Graph.Triangular_Lattice([2, 2]) From 24fee95e87fbf8bf59a544841e61eeefc7f9dfb2 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 13 Aug 2025 16:39:25 -0500 Subject: [PATCH 261/276] chore: updated igraph core, added compatibility alias for the allowed_edge_types argument of rewire() --- src/_igraph/graphobject.c | 39 ++++++++++++++++----------------------- src/igraph/__init__.py | 5 +++-- src/igraph/rewiring.py | 25 +++++++++++++++++++++++++ tests/test_generators.py | 11 ++++------- vendor/source/igraph | 2 +- 5 files changed, 49 insertions(+), 33 deletions(-) create mode 100644 src/igraph/rewiring.py diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index e5bef73b3..b878407d8 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -3563,24 +3563,22 @@ PyObject *igraphmodule_Graph_SBM(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - Py_ssize_t n; PyObject *block_sizes_o, *pref_matrix_o; PyObject *directed_o = Py_False; PyObject *loops_o = Py_False; + PyObject *multiple_o = Py_False; igraph_matrix_t pref_matrix; igraph_vector_int_t block_sizes; - static char *kwlist[] = { "n", "pref_matrix", "block_sizes", "directed", - "loops", NULL }; + static char *kwlist[] = { "pref_matrix", "block_sizes", "directed", + "loops", "multiple", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "nOO|OO", kwlist, - &n, &pref_matrix_o, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OOO", kwlist, + &pref_matrix_o, &block_sizes_o, - &directed_o, &loops_o)) + &directed_o, &loops_o, &multiple_o)) return NULL; - CHECK_SSIZE_T_RANGE(n, "vertex count"); - if (igraphmodule_PyObject_to_matrix_t(pref_matrix_o, &pref_matrix, "pref_matrix")) { return NULL; } @@ -3590,7 +3588,7 @@ PyObject *igraphmodule_Graph_SBM(PyTypeObject * type, return NULL; } - if (igraph_sbm_game(&g, n, &pref_matrix, &block_sizes, PyObject_IsTrue(directed_o), PyObject_IsTrue(loops_o))) { + if (igraph_sbm_game(&g, &pref_matrix, &block_sizes, PyObject_IsTrue(directed_o), PyObject_IsTrue(loops_o), PyObject_IsTrue(multiple_o))) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&pref_matrix); igraph_vector_int_destroy(&block_sizes); @@ -14760,13 +14758,15 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "vertex pair is evaluated and an edge is created between them with a\n" "probability depending on the types of the vertices involved. The\n" "probabilities are taken from the preference matrix.\n\n" - "@param n: the number of vertices in the graph\n" "@param pref_matrix: matrix giving the connection probabilities for\n" - " different vertex types.\n" + " different vertex types (when C{multiple} = C{False}) or the expected\n" + " number of edges between a vertex pair (when C{multiple} = C{True}).\n" "@param block_sizes: list giving the number of vertices in each block; must\n" " sum up to I{n}.\n" "@param directed: whether to generate a directed graph.\n" - "@param loops: whether loop edges are allowed.\n"}, + "@param loops: whether loop edges are allowed.\n" + "@param multiple: whether multiple edges are allowed.\n" + }, // interface to igraph_star {"Star", (PyCFunction) igraphmodule_Graph_Star, @@ -16279,18 +16279,11 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { }, /* interface to igraph_rewire */ - {"rewire", (PyCFunction) igraphmodule_Graph_rewire, + {"_rewire", (PyCFunction) igraphmodule_Graph_rewire, METH_VARARGS | METH_KEYWORDS, - "rewire(n=None, allowed_edge_types=\"simple\")\n--\n\n" - "Randomly rewires the graph while preserving the degree distribution.\n\n" - "The rewiring is done \"in-place\", so the original graph will be modified.\n" - "If you want to preserve the original graph, use the L{copy} method before\n" - "rewiring.\n\n" - "@param n: the number of rewiring trials. The default is 10 times the number\n" - " of edges.\n" - "@param allowed_edge_types: the rewiring algorithm to use. It can either be C{\"simple\"} or\n" - " C{\"loops\"}; the former does not create or destroy loop edges while the\n" - " latter does.\n"}, + "_rewire(n=None, allowed_edge_types=\"simple\")\n--\n\n" + "Internal function, undocumented.\n\n" + "@see: Graph.rewire()\n\n"}, /* interface to igraph_rewire_edges */ {"rewire_edges", (PyCFunction) igraphmodule_Graph_rewire_edges, diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 7c2e50952..70ac18c62 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -239,6 +239,7 @@ intersection, operator_method_registry as _operator_method_registry, ) +from igraph.rewiring import _rewire from igraph.seq import EdgeSeq, VertexSeq, _add_proxy_methods from igraph.statistics import ( FittedPowerLaw, @@ -632,6 +633,7 @@ def es(self): disjoint_union = _operator_method_registry["disjoint_union"] union = _operator_method_registry["union"] intersection = _operator_method_registry["intersection"] + rewire = _rewire ############################################# # Adjacency/incidence @@ -1152,6 +1154,7 @@ def write(graph, filename, *args, **kwds): _cohesive_blocks, _connected_components, _add_proxy_methods, + _rewire, ) # Re-export from _igraph for API docs @@ -1267,8 +1270,6 @@ def write(graph, filename, *args, **kwds): "GET_ADJACENCY_UPPER", "IN", "OUT", - "REWIRING_SIMPLE", - "REWIRING_SIMPLE_LOOPS", "STAR_IN", "STAR_MUTUAL", "STAR_OUT", diff --git a/src/igraph/rewiring.py b/src/igraph/rewiring.py new file mode 100644 index 000000000..b4881a6c8 --- /dev/null +++ b/src/igraph/rewiring.py @@ -0,0 +1,25 @@ +from igraph._igraph import GraphBase + +from .utils import deprecated + +__all__ = ("_rewire", ) + + +def _rewire(graph, n=None, allowed_edge_types="simple", *, mode=None): + """Randomly rewires the graph while preserving the degree distribution. + + The rewiring is done \"in-place\", so the original graph will be modified. + If you want to preserve the original graph, use the L{copy} method before + rewiring. + + @param n: the number of rewiring trials. The default is 10 times the number + of edges. + @param allowed_edge_types: the rewiring algorithm to use. It can either be + C{"simple"} or C{"loops"}; the former does not create or destroy + loop edges while the latter does. + """ + if mode is not None: + deprecated("The 'mode' keyword argument is deprecated, use 'allowed_edge_types' instead") + allowed_edge_types = mode + + return GraphBase._rewire(graph, n, allowed_edge_types) diff --git a/tests/test_generators.py b/tests/test_generators.py index bfb1c4e23..675de880a 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -429,9 +429,8 @@ def testLattice(self): def testSBM(self): pref_matrix = [[0.5, 0, 0], [0, 0, 0.5], [0, 0.5, 0]] - n = 60 types = [20, 20, 20] - g = Graph.SBM(n, pref_matrix, types) + g = Graph.SBM(pref_matrix, types) # Simple smoke tests for the expected structure of the graph self.assertTrue(g.is_simple()) @@ -441,21 +440,19 @@ def testSBM(self): self.assertTrue(not any(e.source // 20 == e.target // 20 for e in g2.es)) # Check loops argument - g = Graph.SBM(n, pref_matrix, types, loops=True) + g = Graph.SBM(pref_matrix, types, loops=True) self.assertFalse(g.is_simple()) self.assertTrue(sum(g.is_loop()) > 0) # Check directedness - g = Graph.SBM(n, pref_matrix, types, directed=True) + g = Graph.SBM(pref_matrix, types, directed=True) self.assertTrue(g.is_directed()) self.assertTrue(sum(g.is_mutual()) < g.ecount()) self.assertTrue(sum(g.is_loop()) == 0) # Check error conditions - self.assertRaises(ValueError, Graph.SBM, -1, pref_matrix, types) - self.assertRaises(InternalError, Graph.SBM, 61, pref_matrix, types) pref_matrix[0][1] = 0.7 - self.assertRaises(InternalError, Graph.SBM, 60, pref_matrix, types) + self.assertRaises(InternalError, Graph.SBM, pref_matrix, types) def testTriangularLattice(self): g = Graph.Triangular_Lattice([2, 2]) diff --git a/vendor/source/igraph b/vendor/source/igraph index be53d68e0..be8bc7d25 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit be53d68e08548f3d299cc7bd6bfcfd7b2ea532b4 +Subproject commit be8bc7d25411da63414743ec16bf20022f4b7bab From fb6f4072ae602db84187fa20193ba6f569a33950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Thu, 14 Aug 2025 21:29:09 +0000 Subject: [PATCH 262/276] feat: Graph.Nearest_Neighbor_Graph --- src/_igraph/convert.c | 15 +++++++++++ src/_igraph/convert.h | 1 + src/_igraph/graphobject.c | 56 +++++++++++++++++++++++++++++++++++++++ tests/test_generators.py | 21 +++++++++++++++ 4 files changed, 93 insertions(+) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 8a93fb8f6..7bd2f2ce0 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -4069,6 +4069,21 @@ int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, igraph_pagerank_algo_t TRANSLATE_ENUM_WITH(pagerank_algo_tt); } +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to an igraph \c igraph_metric_t + */ +int igraphmodule_PyObject_to_metric_t(PyObject *o, igraph_metric_t *result) { + static igraphmodule_enum_translation_table_entry_t metric_tt[] = { + {"euclidean", IGRAPH_METRIC_EUCLIDEAN}, + {"l2", IGRAPH_METRIC_L2}, /* alias to the previous */ + {"manhattan", IGRAPH_METRIC_MANHATTAN}, + {"l1", IGRAPH_METRIC_L1}, /* alias to the previous */ + {0,0} + }; + TRANSLATE_ENUM_WITH(metric_tt); +} + /** * \ingroup python_interface_conversion * \brief Converts a Python object to an igraph \c igraph_edge_type_sw_t diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index a7ca02b16..4b1a0731b 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -78,6 +78,7 @@ int igraphmodule_PyObject_to_laplacian_normalization_t(PyObject *o, igraph_lapla int igraphmodule_PyObject_to_layout_grid_t(PyObject *o, igraph_layout_grid_t *result); int igraphmodule_PyObject_to_lpa_variant_t(PyObject *o, igraph_lpa_variant_t *result); int igraphmodule_PyObject_to_loops_t(PyObject *o, igraph_loops_t *result); +int igraphmodule_PyObject_to_metric_t(PyObject *o, igraph_metric_t *result); int igraphmodule_PyObject_to_mst_algorithm_t(PyObject *o, igraph_mst_algorithm_t *result); int igraphmodule_PyObject_to_neimode_t(PyObject *o, igraph_neimode_t *result); int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, igraph_pagerank_algo_t *result); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 19bae50fc..bd88fed4d 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -14034,6 +14034,46 @@ PyObject *igraphmodule_Graph_random_walk(igraphmodule_GraphObject * self, } } +/********************************************************************** + * Spatial graphs * + **********************************************************************/ + +PyObject *igraphmodule_Graph_Nearest_Neighbor_Graph(PyTypeObject *type, + PyObject *args, PyObject *kwds) { + static char *kwlist[] = {"points", "k", "r", "metric", "directed", NULL}; + PyObject *points_o = Py_None, *metric_o = Py_None, *directed_o = Py_False; + double r = -1; + Py_ssize_t k = 1; + igraph_matrix_t points; + igraphmodule_GraphObject *self; + igraph_t graph; + igraph_metric_t metric = IGRAPH_METRIC_EUCLIDEAN; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|ndOO", kwlist, + &points_o, &k, &r, &metric_o, &directed_o)) { + return NULL; + } + + if (igraphmodule_PyObject_to_metric_t(metric_o, &metric)) { + return NULL; + } + + if (igraphmodule_PyObject_to_matrix_t(points_o, &points, "points")) { + return NULL; + } + + if (igraph_nearest_neighbor_graph(&graph, &points, metric, k, r, PyObject_IsTrue(directed_o))) { + igraph_matrix_destroy(&points); + return igraphmodule_handle_igraph_error(); + } + + igraph_matrix_destroy(&points); + + CREATE_GRAPH_FROM_TYPE(self, graph, type); + + return (PyObject *) self; +} + /********************************************************************** * Special internal methods that you won't need to mess around with * **********************************************************************/ @@ -18894,6 +18934,22 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " the given length (shorter if the random walk got stuck).\n" }, + /**********************/ + /* SPATIAL GRAPHS */ + /**********************/ + {"Nearest_Neighbor_Graph", (PyCFunction)igraphmodule_Graph_Nearest_Neighbor_Graph, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Nearest_Neighbor_Graph(points, k=1, r=-1, metric=\"euclidean\", directed=False)\n--\n\n" + "Constructs a k nearest neighbor graph of a give point set. Each point is\n" + "connected to at most k spatial neighbors within a radius of 1.\n\n" + "@param points: coordinates of the points to use, in an arbitrary number of dimensions\n" + "@param k: at most how many neighbors to connect to. Pass a negative value to ignore\n" + "@param r: only neighbors within this radius are considered. Pass a negative value to ignore\n" + "@param metric: the metric to use. C{\"euclidean\"} and C{\"manhattan\"} are supported.\n" + "@param directed: whethe to create directed edges.\n" + "@return: the nearest neighbor graph.\n" + }, + /**********************/ /* INTERNAL FUNCTIONS */ /**********************/ diff --git a/tests/test_generators.py b/tests/test_generators.py index 675de880a..c09b7c914 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -890,6 +890,27 @@ def testDataFrame(self): edges = pd.DataFrame(np.array([[0, 1], [1, np.nan], [1, 2]]), dtype="Int64") Graph.DataFrame(edges) + def testNearestNeighborGraph(self): + points = [[0,0], [1,2], [-3, -3]] + + g = Graph.Nearest_Neighbor_Graph(points) + # expecting 1 - 2, 3 - 1 + self.assertFalse(g.is_directed()) + self.assertEqual(g.vcount(), 3) + self.assertEqual(g.ecount(), 2) + + g = Graph.Nearest_Neighbor_Graph(points, directed=True) + # expecting 1 <-> 2, 3 -> 1 + self.assertTrue(g.is_directed()) + self.assertEqual(g.vcount(), 3) + self.assertEqual(g.ecount(), 3) + + # expecting a complete graph + g = Graph.Nearest_Neighbor_Graph(points, k=2) + self.assertFalse(g.is_directed()) + self.assertEqual(g.vcount(), 3) + self.assertTrue(g.is_complete()) + def suite(): generator_suite = unittest.defaultTestLoader.loadTestsFromTestCase(GeneratorTests) From c6cab4f17a73b50e2406b70198e8563cde9e9af6 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 1 Sep 2025 00:24:02 +0200 Subject: [PATCH 263/276] chore: updated to igraph 1.0.0-rc1 --- src/_igraph/convert.c | 34 ++++ src/_igraph/convert.h | 1 + src/_igraph/graphobject.c | 387 +++++++++++++++++++++++++------------- src/igraph/layout.py | 23 +-- tests/test_generators.py | 14 +- vendor/source/igraph | 2 +- 6 files changed, 308 insertions(+), 153 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 7bd2f2ce0..5cd062ac8 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -2436,6 +2436,40 @@ PyObject* igraphmodule_matrix_int_t_to_PyList(const igraph_matrix_int_t *m) { return list; } +/** + * \ingroup python_interface_conversion + * \brief Converts an igraph \c igraph_matrix_list_t to a Python list of lists of lists + * + * \param v the \c igraph_matrix_list_t containing the matrix list to be converted + * \return the Python list as a \c PyObject*, or \c NULL if an error occurred + */ +PyObject* igraphmodule_matrix_list_t_to_PyList(const igraph_matrix_list_t *m) { + PyObject *list, *item; + Py_ssize_t n, i; + + n = igraph_matrix_list_size(m); + if (n < 0) { + return igraphmodule_handle_igraph_error(); + } + + list = PyList_New(n); + if (!list) { + return NULL; + } + + for (i = 0; i < n; i++) { + item = igraphmodule_matrix_t_to_PyList(igraph_matrix_list_get_ptr(m, i), + IGRAPHMODULE_TYPE_FLOAT); + if (item == NULL) { + Py_DECREF(list); + return NULL; + } + PyList_SetItem(list, i, item); /* will not fail */ + } + + return list; +} + /** * \ingroup python_interface_conversion * \brief Converts an igraph \c igraph_vector_ptr_t to a Python list of lists diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 4b1a0731b..ce865de6a 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -195,4 +195,5 @@ PyObject* igraphmodule_vector_int_t_to_PyList(const igraph_vector_int_t *v); PyObject* igraphmodule_matrix_t_to_PyList(const igraph_matrix_t *m, igraphmodule_conv_t type); PyObject* igraphmodule_matrix_int_t_to_PyList(const igraph_matrix_int_t *m); +PyObject* igraphmodule_matrix_list_t_to_PyList(const igraph_matrix_list_t *m); #endif diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index bd88fed4d..f9b657d3a 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -1014,18 +1014,30 @@ PyObject *igraphmodule_Graph_strength(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_density(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - char *kwlist[] = { "loops", NULL }; + char *kwlist[] = { "loops", "weights", NULL }; igraph_real_t res; - PyObject *loops = Py_False; + PyObject *loops = Py_False, *weights_o = Py_None; + igraph_vector_t *weights = 0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &loops)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &loops, &weights_o)) return NULL; - if (igraph_density(&self->g, &res, PyObject_IsTrue(loops))) { + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } + + if (igraph_density(&self->g, weights, &res, PyObject_IsTrue(loops))) { igraphmodule_handle_igraph_error(); + if (weights) { + igraph_vector_destroy(weights); free(weights); + } return NULL; } + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); } @@ -3563,20 +3575,21 @@ PyObject *igraphmodule_Graph_SBM(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - PyObject *block_sizes_o, *pref_matrix_o; + PyObject *block_sizes_o, *pref_matrix_o, *edge_types_o = Py_None; PyObject *directed_o = Py_False; - PyObject *loops_o = Py_False; - PyObject *multiple_o = Py_False; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; igraph_matrix_t pref_matrix; igraph_vector_int_t block_sizes; - static char *kwlist[] = { "pref_matrix", "block_sizes", "directed", - "loops", "multiple", NULL }; + static char *kwlist[] = { "pref_matrix", "block_sizes", "directed", "allowed_edge_types", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OO", kwlist, &pref_matrix_o, &block_sizes_o, - &directed_o, &loops_o, &multiple_o)) + &directed_o, &edge_types_o)) + return NULL; + + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) return NULL; if (igraphmodule_PyObject_to_matrix_t(pref_matrix_o, &pref_matrix, "pref_matrix")) { @@ -3588,7 +3601,7 @@ PyObject *igraphmodule_Graph_SBM(PyTypeObject * type, return NULL; } - if (igraph_sbm_game(&g, &pref_matrix, &block_sizes, PyObject_IsTrue(directed_o), PyObject_IsTrue(loops_o), PyObject_IsTrue(multiple_o))) { + if (igraph_sbm_game(&g, &pref_matrix, &block_sizes, PyObject_IsTrue(directed_o), allowed_edge_types)) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&pref_matrix); igraph_vector_int_destroy(&block_sizes); @@ -3657,15 +3670,16 @@ PyObject *igraphmodule_Graph_Static_Fitness(PyTypeObject *type, Py_ssize_t m; PyObject *fitness_out_o = Py_None, *fitness_in_o = Py_None; PyObject *fitness_o = Py_None; - PyObject *multiple = Py_False, *loops = Py_False; + PyObject *edge_types_o = Py_None; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; igraph_vector_t fitness_out, fitness_in; static char *kwlist[] = { "m", "fitness_out", "fitness_in", - "loops", "multiple", "fitness", NULL }; + "allowed_edge_types", "fitness", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|OOOOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|OOOO", kwlist, &m, &fitness_out_o, &fitness_in_o, - &loops, &multiple, &fitness_o)) + &edge_types_o, &fitness_o)) return NULL; CHECK_SSIZE_T_RANGE(m, "edge count"); @@ -3681,6 +3695,9 @@ PyObject *igraphmodule_Graph_Static_Fitness(PyTypeObject *type, return NULL; } + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; + if (igraphmodule_PyObject_float_to_vector_t(fitness_out_o, &fitness_out)) return NULL; @@ -3693,7 +3710,7 @@ PyObject *igraphmodule_Graph_Static_Fitness(PyTypeObject *type, if (igraph_static_fitness_game(&g, m, &fitness_out, fitness_in_o == Py_None ? 0 : &fitness_in, - PyObject_IsTrue(loops), PyObject_IsTrue(multiple))) { + allowed_edge_types)) { igraph_vector_destroy(&fitness_out); if (fitness_in_o != Py_None) igraph_vector_destroy(&fitness_in); @@ -3722,21 +3739,25 @@ PyObject *igraphmodule_Graph_Static_Power_Law(PyTypeObject *type, igraph_t g; Py_ssize_t n, m; float exponent_out = -1.0f, exponent_in = -1.0f, exponent = -1.0f; - PyObject *multiple = Py_False, *loops = Py_False; + PyObject *edge_types_o = Py_None; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; PyObject *finite_size_correction = Py_True; static char *kwlist[] = { "n", "m", "exponent_out", "exponent_in", - "loops", "multiple", "finite_size_correction", "exponent", NULL }; + "allowed_edge_types", "finite_size_correction", "exponent", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|ffOOOf", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|ffOOf", kwlist, &n, &m, &exponent_out, &exponent_in, - &loops, &multiple, &finite_size_correction, + &edge_types_o, &finite_size_correction, &exponent)) return NULL; CHECK_SSIZE_T_RANGE(n, "vertex count"); CHECK_SSIZE_T_RANGE(m, "edge count"); + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; + /* This trickery allows us to use "exponent" or "exponent_out" as * keyword argument, with "exponent_out" taking precedence over * "exponent" */ @@ -3749,7 +3770,7 @@ PyObject *igraphmodule_Graph_Static_Power_Law(PyTypeObject *type, } if (igraph_static_power_law_game(&g, n, m, exponent_out, exponent_in, - PyObject_IsTrue(loops), PyObject_IsTrue(multiple), + allowed_edge_types, PyObject_IsTrue(finite_size_correction))) { igraphmodule_handle_igraph_error(); return NULL; @@ -3899,22 +3920,25 @@ PyObject *igraphmodule_Graph_Watts_Strogatz(PyTypeObject * type, { Py_ssize_t dim, size, nei; double p; - PyObject* loops = Py_False; - PyObject* multiple = Py_False; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; + PyObject* edge_types_o = Py_None; igraphmodule_GraphObject *self; igraph_t g; - static char *kwlist[] = { "dim", "size", "nei", "p", "loops", "multiple", NULL }; + static char *kwlist[] = { "dim", "size", "nei", "p", "allowed_edge_types", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "nnnd|OO", kwlist, - &dim, &size, &nei, &p, &loops, &multiple)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nnnd|O", kwlist, + &dim, &size, &nei, &p, &edge_types_o)) return NULL; CHECK_SSIZE_T_RANGE(dim, "dimensionality"); CHECK_SSIZE_T_RANGE(size, "size"); CHECK_SSIZE_T_RANGE(nei, "number of neighbors"); - if (igraph_watts_strogatz_game(&g, dim, size, nei, p, PyObject_IsTrue(loops), PyObject_IsTrue(multiple))) { + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; + + if (igraph_watts_strogatz_game(&g, dim, size, nei, p, allowed_edge_types)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -4027,25 +4051,35 @@ PyObject *igraphmodule_Graph_articulation_points(igraphmodule_GraphObject* self, */ PyObject *igraphmodule_Graph_assortativity_nominal(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = { "types", "directed", "normalized", NULL }; - PyObject *types_o = Py_None, *directed = Py_True, *normalized = Py_True; + static char *kwlist[] = { "types", "directed", "normalized", "weights", NULL }; + PyObject *types_o = Py_None, *weights_o = Py_None, *directed = Py_True, *normalized = Py_True; igraph_real_t res; igraph_error_t ret; igraph_vector_int_t *types = 0; + igraph_vector_t *weights = 0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, &types_o, &directed, &normalized)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, &types_o, &directed, &normalized, &weights_o)) return NULL; if (igraphmodule_attrib_to_vector_int_t(types_o, self, &types, ATTRIBUTE_TYPE_VERTEX)) return NULL; - ret = igraph_assortativity_nominal(&self->g, types, &res, PyObject_IsTrue(directed), + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + if (types) { igraph_vector_int_destroy(types); free(types); } + return NULL; + } + + ret = igraph_assortativity_nominal(&self->g, weights, types, &res, PyObject_IsTrue(directed), PyObject_IsTrue(normalized)); if (types) { igraph_vector_int_destroy(types); free(types); } + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + if (ret) { igraphmodule_handle_igraph_error(); return NULL; @@ -4060,26 +4094,36 @@ PyObject *igraphmodule_Graph_assortativity_nominal(igraphmodule_GraphObject *sel */ PyObject *igraphmodule_Graph_assortativity(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = { "types1", "types2", "directed", "normalized", NULL }; + static char *kwlist[] = { "types1", "types2", "directed", "normalized", "weights", NULL }; PyObject *types1_o = Py_None, *types2_o = Py_None, *directed = Py_True, *normalized = Py_True; + PyObject *weights_o = Py_None; igraph_real_t res; igraph_error_t ret; - igraph_vector_t *types1 = 0, *types2 = 0; + igraph_vector_t *types1 = 0, *types2 = 0, *weights = 0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, &types1_o, &types2_o, &directed, &normalized)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOOO", kwlist, &types1_o, &types2_o, &directed, &normalized, &weights_o)) return NULL; - if (igraphmodule_attrib_to_vector_t(types1_o, self, &types1, ATTRIBUTE_TYPE_VERTEX)) + if (igraphmodule_attrib_to_vector_t(types1_o, self, &types1, ATTRIBUTE_TYPE_VERTEX)) { return NULL; + } + if (igraphmodule_attrib_to_vector_t(types2_o, self, &types2, ATTRIBUTE_TYPE_VERTEX)) { if (types1) { igraph_vector_destroy(types1); free(types1); } return NULL; } - ret = igraph_assortativity(&self->g, types1, types2, &res, PyObject_IsTrue(directed), PyObject_IsTrue(normalized)); + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + if (types1) { igraph_vector_destroy(types1); free(types1); } + if (types2) { igraph_vector_destroy(types2); free(types2); } + return NULL; + } + + ret = igraph_assortativity(&self->g, weights, types1, types2, &res, PyObject_IsTrue(directed), PyObject_IsTrue(normalized)); if (types1) { igraph_vector_destroy(types1); free(types1); } if (types2) { igraph_vector_destroy(types2); free(types2); } + if (weights) { igraph_vector_destroy(weights); free(weights); } if (ret) { igraphmodule_handle_igraph_error(); @@ -4211,8 +4255,9 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "vertices", "directed", "cutoff", "weights", - "sources", "targets", NULL }; + "sources", "targets", "normalized", NULL }; PyObject *directed = Py_True; + PyObject *normalized = Py_False; PyObject *vobj = Py_None, *list; PyObject *cutoff = Py_None; PyObject *weights_o = Py_None; @@ -4224,9 +4269,9 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, igraph_vs_t vs, sources, targets; igraph_error_t retval; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOO", kwlist, &vobj, &directed, &cutoff, &weights_o, - &sources_o, &targets_o)) { + &sources_o, &targets_o, &normalized)) { return NULL; } @@ -4267,10 +4312,10 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, if (cutoff == Py_None) { if (is_subsetted) { retval = igraph_betweenness_subset( - &self->g, &res, vs, PyObject_IsTrue(directed), sources, targets, weights + &self->g, weights, &res, sources, targets, vs, PyObject_IsTrue(directed), PyObject_IsTrue(normalized) ); } else { - retval = igraph_betweenness(&self->g, &res, vs, PyObject_IsTrue(directed), weights); + retval = igraph_betweenness(&self->g, weights, &res, vs, PyObject_IsTrue(directed), PyObject_IsTrue(normalized)); } if (retval) { @@ -4304,8 +4349,8 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, return NULL; } - if (igraph_betweenness_cutoff(&self->g, &res, vs, PyObject_IsTrue(directed), - weights, PyFloat_AsDouble(cutoff_num))) { + if (igraph_betweenness_cutoff(&self->g, weights, &res, vs, PyObject_IsTrue(directed), + PyObject_IsTrue(normalized), PyFloat_AsDouble(cutoff_num))) { igraph_vs_destroy(&vs); igraph_vs_destroy(&targets); igraph_vs_destroy(&sources); @@ -5228,9 +5273,9 @@ PyObject *igraphmodule_Graph_edge_betweenness(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "directed", "cutoff", "weights", "sources", "targets", NULL }; + static char *kwlist[] = { "directed", "cutoff", "weights", "sources", "targets", "normalized", NULL }; igraph_vector_t res, *weights = 0; - PyObject *list, *directed = Py_True, *cutoff = Py_None; + PyObject *list, *directed = Py_True, *cutoff = Py_None, *normalized = Py_False; PyObject *weights_o = Py_None; PyObject *sources_o = Py_None; PyObject *targets_o = Py_None; @@ -5239,8 +5284,8 @@ PyObject *igraphmodule_Graph_edge_betweenness(igraphmodule_GraphObject * self, igraph_error_t retval; igraph_bool_t is_subsetted = false; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOO", kwlist, - &directed, &cutoff, &weights_o, &sources_o, &targets_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOO", kwlist, + &directed, &cutoff, &weights_o, &sources_o, &targets_o, &normalized)) return NULL; if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, @@ -5271,12 +5316,14 @@ PyObject *igraphmodule_Graph_edge_betweenness(igraphmodule_GraphObject * self, if (cutoff == Py_None) { if (is_subsetted) { retval = igraph_edge_betweenness_subset( - &self->g, &res, igraph_ess_all(IGRAPH_EDGEORDER_ID), - PyObject_IsTrue(directed), sources, targets, weights + &self->g, weights, &res, + sources, targets, igraph_ess_all(IGRAPH_EDGEORDER_ID), + PyObject_IsTrue(directed), PyObject_IsTrue(normalized) ); } else { retval = igraph_edge_betweenness( - &self->g, &res, PyObject_IsTrue(directed), weights + &self->g, weights, &res, igraph_ess_all(IGRAPH_EDGEORDER_ID), + PyObject_IsTrue(directed), PyObject_IsTrue(normalized) ); } if (retval) { @@ -5306,8 +5353,10 @@ PyObject *igraphmodule_Graph_edge_betweenness(igraphmodule_GraphObject * self, return NULL; } - if (igraph_edge_betweenness_cutoff(&self->g, &res, PyObject_IsTrue(directed), - weights, PyFloat_AsDouble(cutoff_num))) { + if (igraph_edge_betweenness_cutoff( + &self->g, weights, &res, igraph_ess_all(IGRAPH_EDGEORDER_ID), + PyObject_IsTrue(directed), PyObject_IsTrue(normalized), PyFloat_AsDouble(cutoff_num) + )) { igraph_vector_destroy(&res); igraph_vs_destroy(&targets); igraph_vs_destroy(&sources); @@ -6413,11 +6462,11 @@ PyObject *igraphmodule_Graph_personalized_pagerank(igraphmodule_GraphObject *sel } if (rvsobj != Py_None) - retval = igraph_personalized_pagerank_vs(&self->g, algo, &res, 0, vs, - PyObject_IsTrue(directed), damping, reset_vs, &weights, opts); + retval = igraph_personalized_pagerank_vs(&self->g, &weights, &res, 0, reset_vs, + damping, PyObject_IsTrue(directed), vs, algo, opts); else - retval = igraph_personalized_pagerank(&self->g, algo, &res, 0, vs, - PyObject_IsTrue(directed), damping, reset, &weights, opts); + retval = igraph_personalized_pagerank(&self->g, &weights, &res, 0, reset, + damping, PyObject_IsTrue(directed), vs, algo, opts); if (retval) { igraphmodule_handle_igraph_error(); @@ -6545,16 +6594,20 @@ PyObject *igraphmodule_Graph_rewire(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_rewire_edges(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "prob", "loops", "multiple", NULL }; + static char *kwlist[] = { "prob", "allowed_edge_types", NULL }; double prob; - PyObject *loops_o = Py_False, *multiple_o = Py_False; + PyObject *edge_types_o = Py_None; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "d|OO", kwlist, - &prob, &loops_o, &multiple_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "d|O", kwlist, + &prob, &edge_types_o)) return NULL; - if (igraph_rewire_edges(&self->g, prob, PyObject_IsTrue(loops_o), - PyObject_IsTrue(multiple_o))) { + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) { + return NULL; + } + + if (igraph_rewire_edges(&self->g, prob, allowed_edge_types)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -7696,13 +7749,15 @@ PyObject *igraphmodule_Graph_fundamental_cycles( ) { PyObject *cutoff_o = Py_None; PyObject *start_vid_o = Py_None; + PyObject *weights_o = Py_None; PyObject *result_o; igraph_integer_t cutoff = -1, start_vid = -1; igraph_vector_int_list_t result; + igraph_vector_t *weights = 0; - static char *kwlist[] = { "start_vid", "cutoff", NULL }; + static char *kwlist[] = { "start_vid", "cutoff", "weights", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &start_vid_o, &cutoff_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &start_vid_o, &cutoff_o, &weights_o)) return NULL; if (igraphmodule_PyObject_to_optional_vid(start_vid_o, &start_vid, &self->g)) @@ -7711,17 +7766,31 @@ PyObject *igraphmodule_Graph_fundamental_cycles( if (cutoff_o != Py_None && igraphmodule_PyObject_to_integer_t(cutoff_o, &cutoff)) return NULL; + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } + if (igraph_vector_int_list_init(&result, 0)) { + if (weights) { + igraph_vector_destroy(weights); free(weights); + } igraphmodule_handle_igraph_error(); return NULL; } - if (igraph_fundamental_cycles(&self->g, &result, start_vid, cutoff, /* weights = */ NULL)) { + if (igraph_fundamental_cycles(&self->g, weights, &result, start_vid, cutoff)) { igraph_vector_int_list_destroy(&result); + if (weights) { + igraph_vector_destroy(weights); free(weights); + } igraphmodule_handle_igraph_error(); return NULL; } + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + result_o = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&result); igraph_vector_int_list_destroy(&result); @@ -7734,32 +7803,48 @@ PyObject *igraphmodule_Graph_minimum_cycle_basis( PyObject *cutoff_o = Py_None; PyObject *complete_o = Py_True; PyObject *use_cycle_order_o = Py_True; + PyObject *weights_o = Py_None; PyObject *result_o; igraph_integer_t cutoff = -1; igraph_vector_int_list_t result; + igraph_vector_t *weights; - static char *kwlist[] = { "cutoff", "complete", "use_cycle_order", NULL }; + static char *kwlist[] = { "cutoff", "complete", "use_cycle_order", "weights", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &cutoff_o, &complete_o, &use_cycle_order_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, &cutoff_o, &complete_o, &use_cycle_order_o, &weights_o)) return NULL; if (cutoff_o != Py_None && igraphmodule_PyObject_to_integer_t(cutoff_o, &cutoff)) return NULL; + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } + if (igraph_vector_int_list_init(&result, 0)) { + if (weights) { + igraph_vector_destroy(weights); free(weights); + } igraphmodule_handle_igraph_error(); return NULL; } if (igraph_minimum_cycle_basis( - &self->g, &result, cutoff, PyObject_IsTrue(complete_o), - PyObject_IsTrue(use_cycle_order_o), /* weights = */ NULL + &self->g, weights, &result, cutoff, PyObject_IsTrue(complete_o), + PyObject_IsTrue(use_cycle_order_o) )) { igraph_vector_int_list_destroy(&result); + if (weights) { + igraph_vector_destroy(weights); free(weights); + } igraphmodule_handle_igraph_error(); return NULL; } + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + result_o = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&result); igraph_vector_int_list_destroy(&result); @@ -8264,7 +8349,7 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel } if (weight_edge_lengths < 0 || weight_edge_crossings < 0 || weight_node_edge_dist < 0) { - if (igraph_density(&self->g, &density, 0)) { + if (igraph_density(&self->g, 0, &density, 0)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -8890,40 +8975,36 @@ PyObject *igraphmodule_Graph_layout_reingold_tilford_circular( PyObject *igraphmodule_Graph_layout_sugiyama( igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "layers", "weights", "hgap", "vgap", "maxiter", - "return_extended_graph", NULL }; + static char *kwlist[] = { "layers", "weights", "hgap", "vgap", "maxiter", NULL }; igraph_matrix_t m; - igraph_t extd_graph; - igraph_vector_int_t extd_to_orig_eids; igraph_vector_t *weights = 0; igraph_vector_int_t *layers = 0; double hgap = 1, vgap = 1; Py_ssize_t maxiter = 100; - PyObject *layers_o = Py_None, *weights_o = Py_None, *extd_to_orig_eids_o = Py_None; - PyObject *return_extended_graph = Py_False; - PyObject *result_o; - igraphmodule_GraphObject *graph_o; + PyObject *layers_o = Py_None, *weights_o = Py_None; + PyObject *layout_o, *routing_o; + igraph_matrix_list_t routing; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOddnO", kwlist, - &layers_o, &weights_o, &hgap, &vgap, &maxiter, &return_extended_graph)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOddn", kwlist, + &layers_o, &weights_o, &hgap, &vgap, &maxiter)) return NULL; CHECK_SSIZE_T_RANGE_POSITIVE(maxiter, "maximum number of iterations"); - if (igraph_vector_int_init(&extd_to_orig_eids, 0)) { + if (igraph_matrix_list_init(&routing, 0)) { igraphmodule_handle_igraph_error(); return NULL; } if (igraph_matrix_init(&m, 1, 1)) { - igraph_vector_int_destroy(&extd_to_orig_eids); + igraph_matrix_list_destroy(&routing); igraphmodule_handle_igraph_error(); return NULL; } if (igraphmodule_attrib_to_vector_int_t(layers_o, self, &layers, ATTRIBUTE_TYPE_VERTEX)) { - igraph_vector_int_destroy(&extd_to_orig_eids); + igraph_matrix_list_destroy(&routing); igraph_matrix_destroy(&m); return NULL; } @@ -8931,46 +9012,39 @@ PyObject *igraphmodule_Graph_layout_sugiyama( if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { if (layers != 0) { igraph_vector_int_destroy(layers); free(layers); } - igraph_vector_int_destroy(&extd_to_orig_eids); + igraph_matrix_list_destroy(&routing); igraph_matrix_destroy(&m); return NULL; } - if (igraph_layout_sugiyama(&self->g, &m, - (PyObject_IsTrue(return_extended_graph) ? &extd_graph : 0), - (PyObject_IsTrue(return_extended_graph) ? &extd_to_orig_eids : 0), + if (igraph_layout_sugiyama(&self->g, &m, &routing, layers, hgap, vgap, maxiter, weights)) { if (layers != 0) { igraph_vector_int_destroy(layers); free(layers); } if (weights != 0) { igraph_vector_destroy(weights); free(weights); } - igraph_vector_int_destroy(&extd_to_orig_eids); + igraph_matrix_list_destroy(&routing); igraph_matrix_destroy(&m); igraphmodule_handle_igraph_error(); return NULL; } - if (layers != 0) { igraph_vector_int_destroy(layers); free(layers); } - if (weights != 0) { igraph_vector_destroy(weights); free(weights); } - - result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); - if (result_o == NULL) { - igraph_vector_int_destroy(&extd_to_orig_eids); + layout_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); + if (layout_o == NULL) { + igraph_matrix_list_destroy(&routing); igraph_matrix_destroy(&m); return NULL; } igraph_matrix_destroy(&m); - if (PyObject_IsTrue(return_extended_graph)) { - CREATE_GRAPH(graph_o, extd_graph); - if (graph_o == NULL) { - Py_DECREF(result_o); - } - extd_to_orig_eids_o = igraphmodule_vector_int_t_to_PyList(&extd_to_orig_eids); - result_o = Py_BuildValue("NNN", result_o, graph_o, extd_to_orig_eids_o); + routing_o = igraphmodule_matrix_list_t_to_PyList(&routing); + if (routing_o == NULL) { + igraph_matrix_list_destroy(&routing); + return NULL; } - igraph_vector_int_destroy(&extd_to_orig_eids); - return (PyObject *) result_o; + igraph_matrix_list_destroy(&routing); + + return Py_BuildValue("NN", layout_o, routing_o); } /** \ingroup python_interface_graph @@ -13261,10 +13335,12 @@ PyObject *igraphmodule_Graph_community_infomap(igraphmodule_GraphObject * self, return NULL; } - if (igraph_community_infomap(/*in */ &self->g, - /*e_weight=*/ e_ws, /*v_weight=*/ v_ws, - /*nb_trials=*/nb_trials, - /*out*/ &membership, &codelength)) { + if (igraph_community_infomap( + /*in */ &self->g, /*e_weight=*/ e_ws, /*v_weight=*/ v_ws, + /*nb_trials=*/nb_trials, /*is_regularized=*/0, + /*regularization_strength=*/ 0, + /*out*/ &membership, &codelength) + ) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&membership); if (e_ws) { @@ -14790,7 +14866,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_sbm_game */ {"SBM", (PyCFunction) igraphmodule_Graph_SBM, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "SBM(n, pref_matrix, block_sizes, directed=False, loops=False)\n--\n\n" + "SBM(pref_matrix, block_sizes, directed=False, allowed_edge_types=\"simple\")\n--\n\n" "Generates a graph based on a stochastic block model.\n\n" "Every vertex is assigned to a vertex type according to the given block\n" "sizes, which also determine the total vertex count. Vertices of the same\n" @@ -14803,8 +14879,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param block_sizes: list giving the number of vertices in each block; must\n" " sum up to I{n}.\n" "@param directed: whether to generate a directed graph.\n" - "@param loops: whether loop edges are allowed.\n" - "@param multiple: whether multi-edges are allowed.\n"}, + "@param allowed_edge_types: controls whether loops or multi-edges are allowed\n" + " during the generation process. Note that not all combinations are supported\n" + " for all types of graphs; an exception will be raised for unsupported\n" + " combinations. Possible values are:\n" + "\n" + " - C{\"simple\"}: simple graphs (no self-loops, no multi-edges)\n" + " - C{\"loops\"}: single self-loops allowed, but not multi-edges\n" + " - C{\"multi\"}: multi-edges allowed, but not self-loops\n" + " - C{\"all\"}: multi-edges and self-loops allowed\n" + "\n"}, // interface to igraph_star {"Star", (PyCFunction) igraphmodule_Graph_Star, @@ -14942,7 +15026,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_static_fitness_game */ {"Static_Fitness", (PyCFunction) igraphmodule_Graph_Static_Fitness, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Static_Fitness(m, fitness_out, fitness_in=None, loops=False, multiple=False)\n--\n\n" + "Static_Fitness(m, fitness_out, fitness_in=None, allowed_edge_types=\"simple\")\n--\n\n" "Generates a non-growing graph with edge probabilities proportional to node\n" "fitnesses.\n\n" "The algorithm randomly selects vertex pairs and connects them until the given\n" @@ -14957,8 +15041,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param fitness_in: a numeric vector with non-negative entries, one for each\n" " vertex. These values represent the in-fitness scores for directed graphs.\n" " For undirected graphs, this argument must be C{None}.\n" - "@param loops: whether loop edges are allowed.\n" - "@param multiple: whether multiple edges are allowed.\n" + "@param allowed_edge_types: controls whether loops or multi-edges are allowed\n" + " during the generation process. Note that not all combinations are supported\n" + " for all types of graphs; an exception will be raised for unsupported\n" + " combinations. Possible values are:\n" + "\n" + " - C{\"simple\"}: simple graphs (no self-loops, no multi-edges)\n" + " - C{\"loops\"}: single self-loops allowed, but not multi-edges\n" + " - C{\"multi\"}: multi-edges allowed, but not self-loops\n" + " - C{\"all\"}: multi-edges and self-loops allowed\n" + "\n" "@return: a directed or undirected graph with the prescribed power-law\n" " degree distributions.\n" }, @@ -14966,8 +15058,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_static_power_law_game */ {"Static_Power_Law", (PyCFunction) igraphmodule_Graph_Static_Power_Law, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Static_Power_Law(n, m, exponent_out, exponent_in=-1, loops=False, " - "multiple=False, finite_size_correction=True)\n--\n\n" + "Static_Power_Law(n, m, exponent_out, exponent_in=-1, allowed_edge_types=\"simple\", " + "finite_size_correction=True)\n--\n\n" "Generates a non-growing graph with prescribed power-law degree distributions.\n\n" "B{References}\n\n" " - Goh K-I, Kahng B, Kim D: Universal behaviour of load distribution\n" @@ -14985,8 +15077,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param exponent_in: the exponent of the in-degree distribution, which\n" " must be between 2 and infinity (inclusive) It can also be negative, in\n" " which case an undirected graph will be generated.\n" - "@param loops: whether loop edges are allowed.\n" - "@param multiple: whether multiple edges are allowed.\n" + "@param allowed_edge_types: controls whether loops or multi-edges are allowed\n" + " during the generation process. Note that not all combinations are supported\n" + " for all types of graphs; an exception will be raised for unsupported\n" + " combinations. Possible values are:\n" + "\n" + " - C{\"simple\"}: simple graphs (no self-loops, no multi-edges)\n" + " - C{\"loops\"}: single self-loops allowed, but not multi-edges\n" + " - C{\"multi\"}: multi-edges allowed, but not self-loops\n" + " - C{\"all\"}: multi-edges and self-loops allowed\n" + "\n" "@param finite_size_correction: whether to apply a finite-size correction\n" " to the generated fitness values for exponents less than 3. See the\n" " paper of Cho et al for more details.\n" @@ -15172,7 +15272,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_watts_strogatz_game */ {"Watts_Strogatz", (PyCFunction) igraphmodule_Graph_Watts_Strogatz, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Watts_Strogatz(dim, size, nei, p, loops=False, multiple=False)\n--\n\n" + "Watts_Strogatz(dim, size, nei, p, allowed_edge_types=\"simple\")\n--\n\n" "This function generates networks with the small-world property based on a\n" "variant of the Watts-Strogatz model. The network is obtained by first creating\n" "a periodic undirected lattice, then rewiring both endpoints of each edge with\n" @@ -15192,8 +15292,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param nei: value giving the distance (number of steps) within which\n" " two vertices will be connected.\n" "@param p: rewiring probability\n\n" - "@param loops: specifies whether loop edges are allowed\n" - "@param multiple: specifies whether multiple edges are allowed\n" + "@param allowed_edge_types: controls whether loops or multi-edges are allowed\n" + " during the generation process. Note that not all combinations are supported\n" + " for all types of graphs; an exception will be raised for unsupported\n" + " combinations. Possible values are:\n" + "\n" + " - C{\"simple\"}: simple graphs (no self-loops, no multi-edges)\n" + " - C{\"loops\"}: single self-loops allowed, but not multi-edges\n" + " - C{\"multi\"}: multi-edges allowed, but not self-loops\n" + " - C{\"all\"}: multi-edges and self-loops allowed\n" + "\n" "@see: L{Lattice()}, L{rewire()}, L{rewire_edges()} if more flexibility is\n" " needed\n" }, @@ -15251,7 +15359,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_assortativity */ {"assortativity", (PyCFunction)igraphmodule_Graph_assortativity, METH_VARARGS | METH_KEYWORDS, - "assortativity(types1, types2=None, directed=True, normalized=True)\n--\n\n" + "assortativity(types1, types2=None, directed=True, normalized=True, weights=None)\n--\n\n" "Returns the assortativity of the graph based on numeric properties\n" "of the vertices.\n\n" "This coefficient is basically the correlation between the actual\n" @@ -15276,6 +15384,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param normalized: whether to compute the normalized covariance, i.e.\n" " Pearson correlation. Supply True here to compute the standard\n" " assortativity.\n" + "@param weights: edge weights to be used. Can be a sequence or iterable or\n" + " even an edge attribute name.\n" "@return: the assortativity coefficient\n\n" "@see: L{assortativity_degree()} when the types are the vertex degrees\n" }, @@ -15296,7 +15406,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_assortativity_nominal */ {"assortativity_nominal", (PyCFunction)igraphmodule_Graph_assortativity_nominal, METH_VARARGS | METH_KEYWORDS, - "assortativity_nominal(types, directed=True, normalized=True)\n--\n\n" + "assortativity_nominal(types, directed=True, normalized=True, weights=None)\n--\n\n" "Returns the assortativity of the graph based on vertex categories.\n\n" "Assuming that the vertices belong to different categories, this\n" "function calculates the assortativity coefficient, which specifies\n" @@ -15315,6 +15425,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param normalized: whether to compute the (usual) normalized assortativity.\n" " The unnormalized version is identical to modularity. Supply True here to\n" " compute the standard assortativity.\n" + "@param weights: edge weights to be used. Can be a sequence or iterable or\n" + " even an edge attribute name.\n" "@return: the assortativity coefficient\n\n" }, @@ -15580,13 +15692,15 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_density */ {"density", (PyCFunction) igraphmodule_Graph_density, METH_VARARGS | METH_KEYWORDS, - "density(loops=False)\n--\n\n" + "density(loops=False, weights=None)\n--\n\n" "Calculates the density of the graph.\n\n" "@param loops: whether to take loops into consideration. If C{True},\n" " the algorithm assumes that there might be some loops in the graph\n" " and calculates the density accordingly. If C{False}, the algorithm\n" " assumes that there can't be any loops.\n" - "@return: the density of the graph."}, + "@param weights: weights associated to the edges. Can be an attribute name\n" + " as well. If C{None}, every edge will have the same weight.\n" + "@return: the (weighted or unweighted) density of the graph."}, /* interface to igraph_mean_degree */ {"mean_degree", (PyCFunction) igraphmodule_Graph_mean_degree, @@ -16326,7 +16440,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_rewire_edges */ {"rewire_edges", (PyCFunction) igraphmodule_Graph_rewire_edges, METH_VARARGS | METH_KEYWORDS, - "rewire_edges(prob, loops=False, multiple=False)\n--\n\n" + "rewire_edges(prob, allowed_edge_types=\"simple\")\n--\n\n" "Rewires the edges of a graph with constant probability.\n\n" "Each endpoint of each edge of the graph will be rewired with a constant\n" "probability, given in the first argument.\n\n" @@ -16334,9 +16448,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "graph will be modified. If you want to preserve the original graph,\n" "use the L{cop y} method before.\n\n" "@param prob: rewiring probability\n" - "@param loops: whether the algorithm is allowed to create loop edges\n" - "@param multiple: whether the algorithm is allowed to create multiple\n" - " edges.\n"}, + "@param allowed_edge_types: controls whether loops or multi-edges are allowed\n" + " during the rewiring process. Note that not all combinations are supported\n" + " for all types of graphs; an exception will be raised for unsupported\n" + " combinations. Possible values are:\n" + "\n" + " - C{\"simple\"}: simple graphs (no self-loops, no multi-edges)\n" + " - C{\"loops\"}: single self-loops allowed, but not multi-edges\n" + " - C{\"multi\"}: multi-edges allowed, but not self-loops\n" + " - C{\"all\"}: multi-edges and self-loops allowed\n" + "\n"}, /* interface to igraph_distances */ {"distances", (PyCFunction) igraphmodule_Graph_distances, @@ -16832,7 +16953,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"fundamental_cycles", (PyCFunction) igraphmodule_Graph_fundamental_cycles, METH_VARARGS | METH_KEYWORDS, - "fundamental_cycles(start_vid=None, cutoff=None)\n--\n\n" + "fundamental_cycles(start_vid=None, cutoff=None, weights=None)\n--\n\n" "Finds a single fundamental cycle basis of the graph\n\n" "@param start_vid: when C{None} or negative, a complete fundamental cycle basis is\n" " returned. When it is a vertex or a vertex ID, the fundamental cycles\n" @@ -16841,11 +16962,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param cutoff: when C{None} or negative, a complete cycle basis is returned. Otherwise\n" " the BFS is stopped after this many steps, so the result will effectively\n" " include cycles of length M{2 * cutoff + 1} or shorter only.\n" + "@param weights: edge weights to be used. Can be a sequence or iterable or\n" + " even an edge attribute name.\n" "@return: the cycle basis as a list of tuples containing edge IDs" }, {"minimum_cycle_basis", (PyCFunction) igraphmodule_Graph_minimum_cycle_basis, METH_VARARGS | METH_KEYWORDS, - "minimum_cycle_basis(cutoff=None, complete=True, use_cycle_order=True)\n--\n\n" + "minimum_cycle_basis(cutoff=None, complete=True, use_cycle_order=True, weights=None)\n--\n\n" "Computes a minimum cycle basis of the graph\n\n" "@param cutoff: when C{None} or negative, a complete minimum cycle basis is returned.\n" " Otherwise only those cycles in the result will be part of some minimum\n" @@ -16861,6 +16984,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param use_cycle_order: if C{True}, every cycle is returned in natural\n" " order: the edge IDs will appear ordered along the cycle. If C{False},\n" " no guarantees are given about the ordering of edge IDs within cycles.\n" + "@param weights: edge weights to be used. Can be a sequence or iterable or\n" + " even an edge attribute name.\n" "@return: the cycle basis as a list of tuples containing edge IDs" }, {"simple_cycles", (PyCFunction) igraphmodule_Graph_simple_cycles, diff --git a/src/igraph/layout.py b/src/igraph/layout.py index 206a6bb56..81afb0fc8 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -555,6 +555,7 @@ def align_layout(graph, layout): return Layout(_align_layout(graph, layout.coords)) + def _layout_auto(graph, *args, **kwds): """Chooses and runs a suitable layout function based on simple topological properties of the graph. @@ -630,7 +631,6 @@ def _layout_sugiyama( hgap=1, vgap=1, maxiter=100, - return_extended_graph=False, ): """Places the vertices using a layered Sugiyama layout. @@ -681,23 +681,14 @@ def _layout_sugiyama( The extended graph also contains an edge attribute called C{_original_eid} which specifies the ID of the edge in the original graph from which the edge of the extended graph was created. - @return: the calculated layout, which may (and usually will) have more rows - than the number of vertices; the remaining rows correspond to the dummy - nodes introduced in the layering step. When C{return_extended_graph} is - C{True}, it will also contain the extended graph. + @return: the calculated layout and an additional list of matrices where the + i-th matrix contains the control points of edge I{i} in the original graph + (or an empty matrix if no control points are needed on the edge) """ - if not return_extended_graph: - return Layout( - GraphBase._layout_sugiyama( - graph, layers, weights, hgap, vgap, maxiter, return_extended_graph - ) - ) - - layout, extd_graph, extd_to_orig_eids = GraphBase._layout_sugiyama( - graph, layers, weights, hgap, vgap, maxiter, return_extended_graph + layout, routing = GraphBase._layout_sugiyama( + graph, layers, weights, hgap, vgap, maxiter ) - extd_graph.es["_original_eid"] = extd_to_orig_eids - return Layout(layout), extd_graph + return Layout(layout), routing def _layout_method_wrapper(func): diff --git a/tests/test_generators.py b/tests/test_generators.py index c09b7c914..0e1227658 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -439,8 +439,8 @@ def testSBM(self): g2 = g.subgraph(list(range(20, 60))) self.assertTrue(not any(e.source // 20 == e.target // 20 for e in g2.es)) - # Check loops argument - g = Graph.SBM(pref_matrix, types, loops=True) + # Check allowed_edge_types argument + g = Graph.SBM(pref_matrix, types, allowed_edge_types="loops") self.assertFalse(g.is_simple()) self.assertTrue(sum(g.is_loop()) > 0) @@ -491,7 +491,9 @@ def testAdjacencyNumPy(self): # ADJ_DIRECTED (default) g = Graph.Adjacency(mat) el = g.get_edgelist() - self.assertListEqual(sorted(el), [(0, 1), (0, 2), (1, 0), (2, 2), (2, 2), (3, 1)]) + self.assertListEqual( + sorted(el), [(0, 1), (0, 2), (1, 0), (2, 2), (2, 2), (3, 1)] + ) # ADJ MIN g = Graph.Adjacency(mat, mode="min") @@ -528,7 +530,9 @@ def testAdjacencyNumPyLoopHandling(self): # ADJ_DIRECTED (default) g = Graph.Adjacency(mat) el = g.get_edgelist() - self.assertListEqual(sorted(el), [(0, 1), (0, 2), (1, 0), (2, 2), (2, 2), (3, 1)]) + self.assertListEqual( + sorted(el), [(0, 1), (0, 2), (1, 0), (2, 2), (2, 2), (3, 1)] + ) # ADJ MIN g = Graph.Adjacency(mat, mode="min", loops="twice") @@ -891,7 +895,7 @@ def testDataFrame(self): Graph.DataFrame(edges) def testNearestNeighborGraph(self): - points = [[0,0], [1,2], [-3, -3]] + points = [[0, 0], [1, 2], [-3, -3]] g = Graph.Nearest_Neighbor_Graph(points) # expecting 1 - 2, 3 - 1 diff --git a/vendor/source/igraph b/vendor/source/igraph index be8bc7d25..780c73d06 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit be8bc7d25411da63414743ec16bf20022f4b7bab +Subproject commit 780c73d06ebabb5c935a6eda6be52b7bc921135a From 902f4c42549272eeed038cdb6b8552c2deafae12 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 1 Sep 2025 13:01:15 +0200 Subject: [PATCH 264/276] fix: remove 'return_extended_graph' param from doc of Graph.layout_sugiyama() --- src/igraph/layout.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/igraph/layout.py b/src/igraph/layout.py index 81afb0fc8..5786ca320 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -671,16 +671,6 @@ def _layout_sugiyama( @param maxiter: maximum number of iterations to take in the crossing reduction step. Increase this if you feel that you are getting too many edge crossings. - @param return_extended_graph: specifies that the extended graph with the - added dummy vertices should also be returned. When this is C{True}, the - result will be a tuple containing the layout and the extended graph. The - first |V| nodes of the extended graph will correspond to the nodes of the - original graph, the remaining ones are dummy nodes. Plotting the extended - graph with the returned layout and hidden dummy nodes will produce a layout - that is similar to the original graph, but with the added edge bends. - The extended graph also contains an edge attribute called C{_original_eid} - which specifies the ID of the edge in the original graph from which the - edge of the extended graph was created. @return: the calculated layout and an additional list of matrices where the i-th matrix contains the control points of edge I{i} in the original graph (or an empty matrix if no control points are needed on the edge) From d801058097cbc5624084ccd12983b980020a65ac Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 2 Sep 2025 01:26:07 +0200 Subject: [PATCH 265/276] fix: store edge routing information on the Layout object in a property --- src/igraph/layout.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/igraph/layout.py b/src/igraph/layout.py index 5786ca320..ad5fffc09 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -56,6 +56,14 @@ class Layout: >>> layout[1] = coords >>> print(layout[1]) [0, 3] + + Optionally, a layout may have I{edge routing} information attached to + each edge in the layout in the L{edge_routing} property. + + @ivar edge_routing: C{None} if no edge routing information is available, + or a list of lists of control point coordinates, one for each edge. When + an edge has control points, it should be drawn in a way that the edge + passes through all the control points in the order they appear in the list. """ def __init__(self, coords=None, dim=None): @@ -69,6 +77,8 @@ def __init__(self, coords=None, dim=None): length of the coordinate list is zero, otherwise it should be left as is. """ + self.edge_routing = None + if coords is not None: self._coords = [list(coord) for coord in coords] else: @@ -675,10 +685,12 @@ def _layout_sugiyama( i-th matrix contains the control points of edge I{i} in the original graph (or an empty matrix if no control points are needed on the edge) """ - layout, routing = GraphBase._layout_sugiyama( + coords, routing = GraphBase._layout_sugiyama( graph, layers, weights, hgap, vgap, maxiter ) - return Layout(layout), routing + layout = Layout(coords) + layout.edge_routing = routing + return layout def _layout_method_wrapper(func): From 64877e3a1b84fe10671cef3df464cbfcb1707bdc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 13:12:44 +0000 Subject: [PATCH 266/276] build(deps): bump actions/setup-python from 5 to 6 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8af4dece1..4d9d6d4a3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -155,7 +155,7 @@ jobs: submodules: true fetch-depth: 0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 name: Install Python with: python-version: "3.12.1" @@ -279,7 +279,7 @@ jobs: if: steps.cache-c-core.outputs.cache-hit != 'true' # Only needed when building the C core run: sudo apt install ninja-build cmake flex bison - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 name: Install Python with: python-version: "3.9" @@ -326,7 +326,7 @@ jobs: if: steps.cache-c-core.outputs.cache-hit != 'true' # Only needed when building the C core run: sudo apt install ninja-build cmake flex bison - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 name: Install Python with: python-version: "3.12" From 9215b9db4f5006e681a2978bc9b4655417a398bf Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 19 Sep 2025 23:35:17 +0200 Subject: [PATCH 267/276] refactor: updated for igraph 1.0.0 --- src/_igraph/convert.c | 54 ++++++++++ src/_igraph/convert.h | 2 + src/_igraph/graphobject.c | 203 +++++++++++++++++++++++++++---------- src/igraph/__init__.py | 6 +- src/igraph/io/bipartite.py | 15 ++- tests/test_cliques.py | 18 ++++ vendor/source/igraph | 2 +- 7 files changed, 239 insertions(+), 61 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 5cd062ac8..dbeedaf78 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -1023,6 +1023,60 @@ int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v) { return 1; } +/** + * \brief Converts a Python object to an igraph \c igraph_integer_t when it is + * used as a limit on the number of results for some function. + * + * This is different from \ref igraphmodule_PyObject_to_integer_t such that it + * converts None and positive infinity to \c IGRAPH_UNLIMITED, and it does not + * accept negative values. + * + * Raises suitable Python exceptions when needed. + * + * \param object the Python object to be converted + * \param v the result is stored here + * \return 0 if everything was OK, 1 otherwise + */ +int igraphmodule_PyObject_to_max_results_t(PyObject *object, igraph_integer_t *v) { + int retval; + igraph_integer_t num; + + if (object != NULL) { + if (object == Py_None) { + *v = IGRAPH_UNLIMITED; + return 0; + } + + if (PyNumber_Check(object)) { + PyObject *flt = PyNumber_Float(object); + if (flt == NULL) { + return 1; + } + + if (PyFloat_AsDouble(flt) == IGRAPH_INFINITY) { + Py_DECREF(flt); + *v = IGRAPH_UNLIMITED; + return 0; + } + + Py_DECREF(flt); + } + } + + retval = igraphmodule_PyObject_to_integer_t(object, &num); + if (retval) { + return retval; + } + + if (num < 0) { + PyErr_SetString(PyExc_ValueError, "expected non-negative integer, None or infinity"); + return 1; + } + + *v = num; + return 0; +} + /** * \brief Converts a Python object to an igraph \c igraph_real_t * diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index ce865de6a..a8259a8db 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -104,6 +104,8 @@ int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v); int igraphmodule_PyObject_to_real_t(PyObject *object, igraph_real_t *v); int igraphmodule_PyObject_to_igraph_t(PyObject *o, igraph_t **result); +int igraphmodule_PyObject_to_max_results_t(PyObject *object, igraph_integer_t *v); + int igraphmodule_PyObject_to_vector_t(PyObject *list, igraph_vector_t *v, igraph_bool_t need_non_negative); int igraphmodule_PyObject_float_to_vector_t(PyObject *list, igraph_vector_t *v); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index f9b657d3a..b24c589d6 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2333,15 +2333,16 @@ PyObject *igraphmodule_Graph_Erdos_Renyi(PyTypeObject * type, igraph_t g; Py_ssize_t n, m = -1; double p = -1.0; - PyObject *loops = Py_False, *directed = Py_False; + PyObject *loops = Py_False, *directed = Py_False, *edge_labeled = Py_False; int retval; - static char *kwlist[] = { "n", "p", "m", "directed", "loops", NULL }; + static char *kwlist[] = { "n", "p", "m", "directed", "loops", "edge_labeled", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|dnOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|dnOOO", kwlist, &n, &p, &m, &directed, - &loops)) + &loops, + &edge_labeled)) return NULL; CHECK_SSIZE_T_RANGE(n, "vertex count"); @@ -2360,10 +2361,18 @@ PyObject *igraphmodule_Graph_Erdos_Renyi(PyTypeObject * type, if (m == -1) { /* GNP model */ - retval = igraph_erdos_renyi_game_gnp(&g, n, p, PyObject_IsTrue(directed), PyObject_IsTrue(loops)); + retval = igraph_erdos_renyi_game_gnp( + &g, n, p, PyObject_IsTrue(directed), + PyObject_IsTrue(loops) ? IGRAPH_LOOPS_SW : IGRAPH_SIMPLE_SW, + PyObject_IsTrue(edge_labeled) + ); } else { /* GNM model */ - retval = igraph_erdos_renyi_game_gnm(&g, n, m, PyObject_IsTrue(directed), PyObject_IsTrue(loops), /* multiple = */ 0); + retval = igraph_erdos_renyi_game_gnm( + &g, n, m, PyObject_IsTrue(directed), + PyObject_IsTrue(loops) ? IGRAPH_LOOPS_SW : IGRAPH_SIMPLE_SW, + PyObject_IsTrue(edge_labeled) + ); } if (retval) { @@ -3408,15 +3417,20 @@ PyObject *igraphmodule_Graph_Random_Bipartite(PyTypeObject * type, Py_ssize_t n1, n2, m = -1; double p = -1.0; igraph_neimode_t neimode = IGRAPH_ALL; - PyObject *directed_o = Py_False, *neimode_o = NULL; + PyObject *directed_o = Py_False, *neimode_o = NULL, *edge_labeled_o = Py_False; + PyObject *edge_types_o = Py_None; igraph_vector_bool_t vertex_types; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; PyObject *vertex_types_o; igraph_error_t retval; - static char *kwlist[] = { "n1", "n2", "p", "m", "directed", "neimode", NULL }; + static char *kwlist[] = { + "n1", "n2", "p", "m", "directed", "neimode", "allowed_edge_types", "edge_labeled", NULL + }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|dnOO", kwlist, - &n1, &n2, &p, &m, &directed_o, &neimode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|dnOOOO", kwlist, + &n1, &n2, &p, &m, &directed_o, &neimode_o, + &edge_types_o, &edge_labeled_o)) return NULL; CHECK_SSIZE_T_RANGE(n1, "number of vertices in first partition"); @@ -3436,6 +3450,9 @@ PyObject *igraphmodule_Graph_Random_Bipartite(PyTypeObject * type, if (igraphmodule_PyObject_to_neimode_t(neimode_o, &neimode)) return NULL; + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; + if (igraph_vector_bool_init(&vertex_types, n1+n2)) { igraphmodule_handle_igraph_error(); return NULL; @@ -3444,13 +3461,14 @@ PyObject *igraphmodule_Graph_Random_Bipartite(PyTypeObject * type, if (m == -1) { /* GNP model */ retval = igraph_bipartite_game_gnp( - &g, &vertex_types, n1, n2, p, PyObject_IsTrue(directed_o), neimode + &g, &vertex_types, n1, n2, p, PyObject_IsTrue(directed_o), neimode, + allowed_edge_types, PyObject_IsTrue(edge_labeled_o) ); } else { /* GNM model */ retval = igraph_bipartite_game_gnm( &g, &vertex_types, n1, n2, m, PyObject_IsTrue(directed_o), neimode, - /* multiple = */ 0 + allowed_edge_types, PyObject_IsTrue(edge_labeled_o) ); } @@ -6026,17 +6044,17 @@ PyObject *igraphmodule_Graph_get_all_simple_paths(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "v", "to", "minlen", "maxlen", "mode", NULL }; + static char *kwlist[] = { "v", "to", "minlen", "maxlen", "mode", "max_results", NULL }; igraph_vector_int_list_t res; igraph_neimode_t mode = IGRAPH_OUT; igraph_integer_t from; igraph_vs_t to; - igraph_integer_t minlen, maxlen; - PyObject *list, *from_o, *mode_o = Py_None, *to_o = Py_None; + igraph_integer_t minlen, maxlen, max_results = IGRAPH_UNLIMITED; + PyObject *list, *from_o, *mode_o = Py_None, *to_o = Py_None, *max_results_o = Py_None; PyObject *minlen_o = Py_None, *maxlen_o = Py_None; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOOO", kwlist, &from_o, - &to_o, &minlen_o, &maxlen_o, &mode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOOOO", kwlist, &from_o, + &to_o, &minlen_o, &maxlen_o, &mode_o, &max_results_o)) return NULL; if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) @@ -6054,13 +6072,16 @@ PyObject *igraphmodule_Graph_get_all_simple_paths(igraphmodule_GraphObject * if (igraphmodule_PyObject_to_vs_t(to_o, &to, &self->g, 0, 0)) return NULL; + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) + return NULL; + if (igraph_vector_int_list_init(&res, 0)) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&to); return NULL; } - if (igraph_get_all_simple_paths(&self->g, &res, from, to, minlen, maxlen, mode)) { + if (igraph_get_all_simple_paths(&self->g, &res, from, to, mode, minlen, maxlen, max_results)) { igraphmodule_handle_igraph_error(); igraph_vector_int_list_destroy(&res); igraph_vs_destroy(&to); @@ -6578,7 +6599,7 @@ PyObject *igraphmodule_Graph_rewire(igraphmodule_GraphObject * self, return NULL; } - if (igraph_rewire(&self->g, n, allowed_edge_types)) { + if (igraph_rewire(&self->g, n, allowed_edge_types, /* rewiring_stats = */ NULL)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -7484,7 +7505,7 @@ typedef struct { } igraphmodule_i_Graph_motifs_randesu_callback_data_t; igraph_error_t igraphmodule_i_Graph_motifs_randesu_callback(const igraph_t *graph, - igraph_vector_int_t *vids, igraph_integer_t isoclass, void* extra) { + const igraph_vector_int_t *vids, igraph_integer_t isoclass, void* extra) { igraphmodule_i_Graph_motifs_randesu_callback_data_t* data = (igraphmodule_i_Graph_motifs_randesu_callback_data_t*)extra; PyObject* vector; @@ -7859,16 +7880,23 @@ PyObject *igraphmodule_Graph_simple_cycles( PyObject *output_o = Py_None; PyObject *min_cycle_length_o = Py_None; PyObject *max_cycle_length_o = Py_None; + PyObject *max_results_o = Py_None; // argument defaults: no cycle limits igraph_integer_t mode = IGRAPH_OUT; igraph_integer_t min_cycle_length = -1; igraph_integer_t max_cycle_length = -1; + igraph_integer_t max_results = IGRAPH_UNLIMITED; igraph_bool_t use_edges = false; - static char *kwlist[] = { "mode", "min", "max", "output", NULL }; + static char *kwlist[] = { "mode", "min", "max", "output", "max_results", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, &mode_o, &min_cycle_length_o, &max_cycle_length_o, &output_o)) + if ( + !PyArg_ParseTupleAndKeywords( + args, kwds, "|OOOOO", kwlist, + &mode_o, &min_cycle_length_o, &max_cycle_length_o, &output_o, &max_results_o + ) + ) return NULL; if (mode_o != Py_None && igraphmodule_PyObject_to_integer_t(mode_o, &mode)) @@ -7880,6 +7908,9 @@ PyObject *igraphmodule_Graph_simple_cycles( if (max_cycle_length_o != Py_None && igraphmodule_PyObject_to_integer_t(max_cycle_length_o, &max_cycle_length)) return NULL; + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) + return NULL; + if (igraphmodule_PyObject_to_vpath_or_epath(output_o, &use_edges)) return NULL; @@ -7896,7 +7927,11 @@ PyObject *igraphmodule_Graph_simple_cycles( } if (igraph_simple_cycles( - &self->g, use_edges ? NULL : &vertices, use_edges ? &edges : NULL, mode, min_cycle_length, max_cycle_length + &self->g, + use_edges ? NULL : &vertices, + use_edges ? &edges : NULL, + mode, min_cycle_length, max_cycle_length, + max_results )) { igraph_vector_int_list_destroy(&vertices); igraph_vector_int_list_destroy(&edges); @@ -10355,16 +10390,16 @@ PyObject *igraphmodule_Graph_isoclass(igraphmodule_GraphObject * self, return NULL; if (vids) { - igraph_vector_int_t vidsvec; - if (igraphmodule_PyObject_to_vid_list(vids, &vidsvec, &self->g)) { + igraph_vs_t vs; + if (igraphmodule_PyObject_to_vs_t(vids, &vs, &self->g, NULL, NULL)) { return NULL; } - if (igraph_isoclass_subgraph(&self->g, &vidsvec, &isoclass)) { - igraph_vector_int_destroy(&vidsvec); + if (igraph_isoclass_subgraph(&self->g, vs, &isoclass)) { + igraph_vs_destroy(&vs); igraphmodule_handle_igraph_error(); return NULL; } - igraph_vector_int_destroy(&vidsvec); + igraph_vs_destroy(&vs); } else { if (igraph_isoclass(&self->g, &isoclass)) { igraphmodule_handle_igraph_error(); @@ -12687,13 +12722,15 @@ PyObject *igraphmodule_Graph_vertex_coloring_greedy( PyObject *igraphmodule_Graph_cliques(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "min", "max", NULL }; + static char *kwlist[] = { "min", "max", "max_results", NULL }; PyObject *list; + PyObject *max_results_o = Py_None; Py_ssize_t min_size = 0, max_size = 0; igraph_vector_int_list_t res; + igraph_int_t max_results; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nn", kwlist, - &min_size, &max_size)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnO", kwlist, + &min_size, &max_size, &max_results_o)) return NULL; if (min_size >= 0) { @@ -12708,12 +12745,16 @@ PyObject *igraphmodule_Graph_cliques(igraphmodule_GraphObject * self, max_size = -1; } + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) { + return NULL; + } + if (igraph_vector_int_list_init(&res, 0)) { igraphmodule_handle_igraph_error(); return NULL; } - if (igraph_cliques(&self->g, &res, min_size, max_size)) { + if (igraph_cliques(&self->g, &res, min_size, max_size, max_results)) { igraph_vector_int_list_destroy(&res); return igraphmodule_handle_igraph_error(); } @@ -12803,25 +12844,30 @@ PyObject *igraphmodule_Graph_maximum_bipartite_matching(igraphmodule_GraphObject */ PyObject *igraphmodule_Graph_maximal_cliques(igraphmodule_GraphObject * self, PyObject* args, PyObject* kwds) { - static char* kwlist[] = { "min", "max", "file", NULL }; - PyObject *list, *file = Py_None; + static char* kwlist[] = { "min", "max", "file", "max_results", NULL }; + PyObject *list, *file = Py_None, *max_results_o = Py_None; Py_ssize_t min = 0, max = 0; igraph_vector_int_list_t res; igraphmodule_filehandle_t filehandle; + igraph_int_t max_results = IGRAPH_UNLIMITED; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnO", kwlist, &min, &max, &file)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnOO", kwlist, &min, &max, &file, &max_results_o)) return NULL; CHECK_SSIZE_T_RANGE(min, "minimum size"); CHECK_SSIZE_T_RANGE(max, "maximum size"); + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) { + return NULL; + } + if (file == Py_None) { if (igraph_vector_int_list_init(&res, 0)) { PyErr_SetString(PyExc_MemoryError, ""); return NULL; } - if (igraph_maximal_cliques(&self->g, &res, min, max)) { + if (igraph_maximal_cliques(&self->g, &res, min, max, max_results)) { igraph_vector_int_list_destroy(&res); return igraphmodule_handle_igraph_error(); } @@ -12835,7 +12881,7 @@ PyObject *igraphmodule_Graph_maximal_cliques(igraphmodule_GraphObject * self, return igraphmodule_handle_igraph_error(); } if (igraph_maximal_cliques_file(&self->g, - igraphmodule_filehandle_get(&filehandle), min, max)) { + igraphmodule_filehandle_get(&filehandle), min, max, max_results)) { igraphmodule_filehandle_destroy(&filehandle); return igraphmodule_handle_igraph_error(); } @@ -12865,13 +12911,14 @@ PyObject *igraphmodule_Graph_independent_vertex_sets(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "min", "max", NULL }; - PyObject *list; + static char *kwlist[] = { "min", "max", "max_results", NULL }; + PyObject *list, *max_results_o = Py_None; Py_ssize_t min_size = 0, max_size = 0; igraph_vector_int_list_t res; + igraph_integer_t max_results = IGRAPH_UNLIMITED; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nn", kwlist, - &min_size, &max_size)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnO", kwlist, + &min_size, &max_size, &max_results_o)) return NULL; if (min_size >= 0) { @@ -12886,12 +12933,16 @@ PyObject *igraphmodule_Graph_independent_vertex_sets(igraphmodule_GraphObject max_size = -1; } + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) { + return NULL; + } + if (igraph_vector_int_list_init(&res, 0)) { PyErr_SetString(PyExc_MemoryError, ""); return NULL; } - if (igraph_independent_vertex_sets(&self->g, &res, min_size, max_size)) { + if (igraph_independent_vertex_sets(&self->g, &res, min_size, max_size, max_results)) { igraph_vector_int_list_destroy(&res); return igraphmodule_handle_igraph_error(); } @@ -12929,17 +12980,39 @@ PyObject *igraphmodule_Graph_largest_independent_vertex_sets( * \brief Find all maximal independent vertex sets in a graph */ PyObject *igraphmodule_Graph_maximal_independent_vertex_sets( - igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null) + igraphmodule_GraphObject *self, PyObject* args, PyObject *kwds ) { - PyObject *list; + static char *kwlist[] = { "min", "max", "max_results", NULL }; + PyObject *list, *max_results_o = Py_None; + Py_ssize_t min_size = 0, max_size = 0; igraph_vector_int_list_t res; + igraph_int_t max_results = IGRAPH_UNLIMITED; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnO", kwlist, &min_size, &max_size, &max_results_o)) + return NULL; + + if (min_size >= 0) { + CHECK_SSIZE_T_RANGE(min_size, "minimum size"); + } else { + min_size = -1; + } + + if (max_size >= 0) { + CHECK_SSIZE_T_RANGE(max_size, "maximum size"); + } else { + max_size = -1; + } + + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) { + return NULL; + } if (igraph_vector_int_list_init(&res, 0)) { PyErr_SetString(PyExc_MemoryError, ""); return NULL; } - if (igraph_maximal_independent_vertex_sets(&self->g, &res)) { + if (igraph_maximal_independent_vertex_sets(&self->g, &res, min_size, max_size, max_results)) { igraph_vector_int_list_destroy(&res); return igraphmodule_handle_igraph_error(); } @@ -14660,13 +14733,17 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_erdos_renyi_game {"Erdos_Renyi", (PyCFunction) igraphmodule_Graph_Erdos_Renyi, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Erdos_Renyi(n, p, m, directed=False, loops=False)\n--\n\n" + "Erdos_Renyi(n, p, m, directed=False, loops=False, edge_labeled=False)\n--\n\n" "Generates a graph based on the Erdős-Rényi model.\n\n" "@param n: the number of vertices.\n" "@param p: the probability of edges. If given, C{m} must be missing.\n" "@param m: the number of edges. If given, C{p} must be missing.\n" "@param directed: whether to generate a directed graph.\n" - "@param loops: whether self-loops are allowed.\n"}, + "@param loops: whether self-loops are allowed.\n" + "@param edge_labeled: whether to sample uniformly from the set of\n" + " I{ordered} edge lists. Use C{False} to recover the classic\n" + " Erdős-Rényi model.\n" + }, /* interface to igraph_famous */ {"Famous", (PyCFunction) igraphmodule_Graph_Famous, @@ -14838,7 +14915,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_bipartite_game */ {"_Random_Bipartite", (PyCFunction) igraphmodule_Graph_Random_Bipartite, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "_Random_Bipartite(n1, n2, p=None, m=None, directed=False, neimode=\"all\")\n--\n\n" + "_Random_Bipartite(n1, n2, p=None, m=None, directed=False, neimode=\"all\", allowed_edge_types=\"simple\", edge_labeled=False)\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.Random_Bipartite()\n\n"}, @@ -16064,7 +16141,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"_get_all_simple_paths", (PyCFunction) igraphmodule_Graph_get_all_simple_paths, METH_VARARGS | METH_KEYWORDS, - "_get_all_simple_paths(v, to=None, cutoff=-1, mode=\"out\")\n--\n\n" + "_get_all_simple_paths(v, to=None, minlen=0, maxlen=-1, mode=\"out\", max_results=None)\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.get_all_simple_paths()\n\n" }, @@ -18518,14 +18595,17 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /********************************/ {"cliques", (PyCFunction) igraphmodule_Graph_cliques, METH_VARARGS | METH_KEYWORDS, - "cliques(min=0, max=0)\n--\n\n" + "cliques(min=0, max=0, max_results=None)\n--\n\n" "Returns some or all cliques of the graph as a list of tuples.\n\n" "A clique is a complete subgraph -- a set of vertices where an edge\n" "is present between any two of them (excluding loops)\n\n" "@param min: the minimum size of cliques to be returned. If zero or\n" " negative, no lower bound will be used.\n" "@param max: the maximum size of cliques to be returned. If zero or\n" - " negative, no upper bound will be used."}, + " negative, no upper bound will be used.\n" + "@param max_results: the maximum number of results to return. C{None}\n" + " means no limit on the number of results.\n" + }, {"largest_cliques", (PyCFunction) igraphmodule_Graph_largest_cliques, METH_NOARGS, "largest_cliques()\n--\n\n" @@ -18537,7 +18617,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " L{maximal_cliques()} for the maximal cliques"}, {"maximal_cliques", (PyCFunction) igraphmodule_Graph_maximal_cliques, METH_VARARGS | METH_KEYWORDS, - "maximal_cliques(min=0, max=0, file=None)\n--\n\n" + "maximal_cliques(min=0, max=0, file=None, max_results=None)\n--\n\n" "Returns the maximal cliques of the graph as a list of tuples.\n\n" "A maximal clique is a clique which can't be extended by adding any other\n" "vertex to it. A maximal clique is not necessarily one of the largest\n" @@ -18551,6 +18631,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param file: a file object or the name of the file to write the results\n" " to. When this argument is C{None}, the maximal cliques will be returned\n" " as a list of lists.\n" + "@param max_results: the maximum number of results to return. C{None}\n" + " means no limit on the number of results.\n" "@return: the maximal cliques of the graph as a list of lists, or C{None}\n" " if the C{file} argument was given." "@see: L{largest_cliques()} for the largest cliques."}, @@ -18563,14 +18645,17 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"independent_vertex_sets", (PyCFunction) igraphmodule_Graph_independent_vertex_sets, METH_VARARGS | METH_KEYWORDS, - "independent_vertex_sets(min=0, max=0)\n--\n\n" + "independent_vertex_sets(min=0, max=0, max_results=None)\n--\n\n" "Returns some or all independent vertex sets of the graph as a list of tuples.\n\n" "Two vertices are independent if there is no edge between them. Members\n" "of an independent vertex set are mutually independent.\n\n" "@param min: the minimum size of sets to be returned. If zero or\n" " negative, no lower bound will be used.\n" "@param max: the maximum size of sets to be returned. If zero or\n" - " negative, no upper bound will be used."}, + " negative, no upper bound will be used.\n" + "@param max_results: the maximum number of results to return. C{None}\n" + " means no limit on the number of results.\n" + }, {"largest_independent_vertex_sets", (PyCFunction) igraphmodule_Graph_largest_independent_vertex_sets, METH_NOARGS, @@ -18585,8 +18670,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " (nonextendable) independent vertex sets"}, {"maximal_independent_vertex_sets", (PyCFunction) igraphmodule_Graph_maximal_independent_vertex_sets, - METH_NOARGS, - "maximal_independent_vertex_sets()\n--\n\n" + METH_VARARGS | METH_KEYWORDS, + "maximal_independent_vertex_sets(min=0, max=0, max_results=None)\n--\n\n" "Returns the maximal independent vertex sets of the graph as a list of tuples.\n\n" "A maximal independent vertex set is an independent vertex set\n" "which can't be extended by adding any other vertex to it. A maximal\n" @@ -18595,6 +18680,12 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "B{Reference}: S. Tsukiyama, M. Ide, H. Ariyoshi and I. Shirawaka: A new\n" "algorithm for generating all the maximal independent sets.\n" "I{SIAM J Computing}, 6:505-517, 1977.\n\n" + "@param min: the minimum size of sets to be returned. If zero or\n" + " negative, no lower bound will be used.\n" + "@param max: the maximum size of sets to be returned. If zero or\n" + " negative, no upper bound will be used.\n" + "@param max_results: the maximum number of results to return. C{None}\n" + " means no limit on the number of results.\n\n" "@see: L{largest_independent_vertex_sets()} for the largest independent\n" " vertex sets\n" }, diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 70ac18c62..6a4e189b9 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -708,7 +708,7 @@ def es(self): ########################### # Paths/traversals - def get_all_simple_paths(self, v, to=None, minlen=0, maxlen=-1, mode="out"): + def get_all_simple_paths(self, v, to=None, minlen=0, maxlen=-1, mode="out", max_results=None): """Calculates all the simple paths from a given node to some other nodes (or all of them) in a graph. @@ -730,11 +730,13 @@ def get_all_simple_paths(self, v, to=None, minlen=0, maxlen=-1, mode="out"): @param mode: the directionality of the paths. C{\"in\"} means to calculate incoming paths, C{\"out\"} means to calculate outgoing paths, C{\"all\"} means to calculate both ones. + @param max_results: the maximum number of results to return. C{None} means + no limit on the number of results. @return: all of the simple paths from the given node to every other reachable node in the graph in a list. Note that in case of mode=C{\"in\"}, the vertices in a path are returned in reversed order! """ - return self._get_all_simple_paths(v, to, minlen, maxlen, mode) + return self._get_all_simple_paths(v, to, minlen, maxlen, mode, max_results) def path_length_hist(self, directed=True): """Returns the path length histogram of the graph diff --git a/src/igraph/io/bipartite.py b/src/igraph/io/bipartite.py index 24750d911..1268dc0bf 100644 --- a/src/igraph/io/bipartite.py +++ b/src/igraph/io/bipartite.py @@ -117,7 +117,8 @@ def _construct_full_bipartite_graph( def _construct_random_bipartite_graph( - cls, n1, n2, p=None, m=None, directed=False, neimode="all", *args, **kwds + cls, n1, n2, p=None, m=None, directed=False, neimode="all", + allowed_edge_types="simple", edge_labeled=False, *args, **kwds ): """Generates a random bipartite graph with the given number of vertices and edges (if m is given), or with the given number of vertices and the given @@ -140,13 +141,23 @@ def _construct_random_bipartite_graph( edges will always point from type 1 to type 2. If it is C{"in"}, edges will always point from type 2 to type 1. This argument is ignored for undirected graphs. + @param allowed_edge_types: controls whether multi-edges are allowed + during the generation process. Possible values are: + + - C{"simple"}: simple graphs (no self-loops) + - C{"multi"}: multi-edges allowed + + @param edge_labeled: whether to sample uniformly from the set of + I{ordered} edge lists. Use C{False} to recover the classic random + bipartite model. """ if p is None: p = -1 if m is None: m = -1 result, types = cls._Random_Bipartite( - n1, n2, p, m, directed, neimode, *args, **kwds + n1, n2, p, m, directed, neimode, allowed_edge_types, edge_labeled, + *args, **kwds ) result.vs["type"] = types return result diff --git a/tests/test_cliques.py b/tests/test_cliques.py index b5801a5a1..40c0f9ea3 100644 --- a/tests/test_cliques.py +++ b/tests/test_cliques.py @@ -1,5 +1,7 @@ import unittest +from math import inf + from igraph import Graph from .utils import temporary_file @@ -59,9 +61,25 @@ def testCliques(self): [1, 2, 4, 5], ], } + for (lo, hi), exp in tests.items(): self.assertEqual(sorted(exp), sorted(map(sorted, self.g.cliques(lo, hi)))) + for (lo, hi), exp in tests.items(): + self.assertEqual(sorted(exp), sorted(map(sorted, self.g.cliques(lo, hi, max_results=inf)))) + + for (lo, hi), exp in tests.items(): + observed = [sorted(cl) for cl in self.g.cliques(lo, hi, max_results=10)] + for cl in observed: + self.assertTrue(cl in exp) + + for (lo, hi), _ in tests.items(): + self.assertEqual([], self.g.cliques(lo, hi, max_results=0)) + + for (lo, hi), _ in tests.items(): + with self.assertRaises(ValueError): + self.g.cliques(lo, hi, max_results=-2) + def testLargestCliques(self): self.assertEqual( sorted(map(sorted, self.g.largest_cliques())), [[1, 2, 3, 4], [1, 2, 4, 5]] diff --git a/vendor/source/igraph b/vendor/source/igraph index 780c73d06..fa546047d 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 780c73d06ebabb5c935a6eda6be52b7bc921135a +Subproject commit fa546047defd4d8f369fc7eb28a855ad5c430c63 From 64b3e20a776aa2dcb194ce0daecae9c043a94fa8 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 19 Sep 2025 23:40:08 +0200 Subject: [PATCH 268/276] chore: replace deprecated igraph_integer_t with igraph_int_t --- src/_igraph/arpackobject.c | 2 +- src/_igraph/attributes.c | 62 ++++++++--------- src/_igraph/attributes.h | 2 +- src/_igraph/bfsiter.c | 12 ++-- src/_igraph/convert.c | 64 ++++++++--------- src/_igraph/convert.h | 16 ++--- src/_igraph/dfsiter.c | 14 ++-- src/_igraph/edgeobject.c | 24 +++---- src/_igraph/edgeobject.h | 6 +- src/_igraph/edgeseqobject.c | 16 ++--- src/_igraph/graphobject.c | 126 +++++++++++++++++----------------- src/_igraph/igraphmodule.c | 6 +- src/_igraph/indexing.c | 18 ++--- src/_igraph/vertexobject.c | 16 ++--- src/_igraph/vertexobject.h | 6 +- src/_igraph/vertexseqobject.c | 20 +++--- src/igraph/utils.py | 2 +- vendor/source/igraph | 2 +- 18 files changed, 207 insertions(+), 207 deletions(-) diff --git a/src/_igraph/arpackobject.c b/src/_igraph/arpackobject.c index 86de32363..69d8841ab 100644 --- a/src/_igraph/arpackobject.c +++ b/src/_igraph/arpackobject.c @@ -115,7 +115,7 @@ static PyObject* igraphmodule_ARPACKOptions_getattr( static int igraphmodule_ARPACKOptions_setattr( igraphmodule_ARPACKOptionsObject* self, char* attrname, PyObject* value) { - igraph_integer_t igraph_int; + igraph_int_t igraph_int; if (value == 0) { PyErr_SetString(PyExc_TypeError, "attribute can not be deleted"); diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index cb543030f..1bc39a0eb 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -183,7 +183,7 @@ int igraphmodule_PyObject_matches_attribute_record(PyObject* object, igraph_attr return 0; } -int igraphmodule_get_vertex_id_by_name(igraph_t *graph, PyObject* o, igraph_integer_t* vid) { +int igraphmodule_get_vertex_id_by_name(igraph_t *graph, PyObject* o, igraph_int_t* vid) { igraphmodule_i_attribute_struct* attrs = ATTR_STRUCT(graph); PyObject* o_vid = NULL; @@ -351,7 +351,7 @@ static igraph_error_t igraphmodule_i_attribute_init( igraph_t *graph, const igraph_attribute_record_list_t *attr ) { igraphmodule_i_attribute_struct* attrs; - igraph_integer_t i, n; + igraph_int_t i, n; attrs = (igraphmodule_i_attribute_struct*)calloc(1, sizeof(igraphmodule_i_attribute_struct)); if (!attrs) { @@ -517,11 +517,11 @@ static igraph_error_t igraphmodule_i_attribute_copy(igraph_t *to, const igraph_t /* Adding vertices */ static igraph_error_t igraphmodule_i_attribute_add_vertices( - igraph_t *graph, igraph_integer_t nv, const igraph_attribute_record_list_t *attr + igraph_t *graph, igraph_int_t nv, const igraph_attribute_record_list_t *attr ) { /* Extend the end of every value in the vertex hash with nv pieces of None */ PyObject *key, *value, *dict; - igraph_integer_t i, j, k, num_attr_entries; + igraph_int_t i, j, k, num_attr_entries; igraph_attribute_record_t *attr_rec; igraph_vector_bool_t added_attrs; Py_ssize_t pos = 0; @@ -690,7 +690,7 @@ static igraph_error_t igraphmodule_i_attribute_add_vertices( /* Permuting vertices */ static igraph_error_t igraphmodule_i_attribute_permute_vertices(const igraph_t *graph, igraph_t *newgraph, const igraph_vector_int_t *idx) { - igraph_integer_t i, n; + igraph_int_t i, n; PyObject *key, *value, *dict, *newdict, *newlist, *o; Py_ssize_t pos = 0; @@ -748,7 +748,7 @@ static igraph_error_t igraphmodule_i_attribute_add_edges( /* Extend the end of every value in the edge hash with ne pieces of None */ PyObject *key, *value, *dict; Py_ssize_t pos = 0; - igraph_integer_t i, j, k, ne, num_attr_entries; + igraph_int_t i, j, k, ne, num_attr_entries; igraph_vector_bool_t added_attrs; igraph_attribute_record_t *attr_rec; @@ -904,7 +904,7 @@ static igraph_error_t igraphmodule_i_attribute_add_edges( /* Permuting edges */ static igraph_error_t igraphmodule_i_attribute_permute_edges(const igraph_t *graph, igraph_t *newgraph, const igraph_vector_int_t *idx) { - igraph_integer_t i, n; + igraph_int_t i, n; PyObject *key, *value, *dict, *newdict, *newlist, *o; Py_ssize_t pos=0; @@ -961,14 +961,14 @@ static igraph_error_t igraphmodule_i_attribute_permute_edges(const igraph_t *gra */ static PyObject* igraphmodule_i_ac_func(PyObject* values, const igraph_vector_int_list_t *merges, PyObject* func) { - igraph_integer_t i, len = igraph_vector_int_list_size(merges); + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *list, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); - igraph_integer_t j, n = igraph_vector_int_size(v); + igraph_int_t j, n = igraph_vector_int_size(v); list = PyList_New(n); for (j = 0; j < n; j++) { @@ -1044,14 +1044,14 @@ static PyObject* igraphmodule_i_ac_builtin_func(PyObject* values, */ static PyObject* igraphmodule_i_ac_sum(PyObject* values, const igraph_vector_int_list_t *merges) { - igraph_integer_t i, len = igraph_vector_int_list_size(merges); + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); igraph_real_t num = 0.0, sum = 0.0; - igraph_integer_t j, n = igraph_vector_int_size(v); + igraph_int_t j, n = igraph_vector_int_size(v); for (j = 0; j < n; j++) { item = PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[j]); @@ -1087,14 +1087,14 @@ static PyObject* igraphmodule_i_ac_sum(PyObject* values, */ static PyObject* igraphmodule_i_ac_prod(PyObject* values, const igraph_vector_int_list_t *merges) { - igraph_integer_t i, len = igraph_vector_int_list_size(merges); + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); igraph_real_t num = 1.0, prod = 1.0; - igraph_integer_t j, n = igraph_vector_int_size(v); + igraph_int_t j, n = igraph_vector_int_size(v); for (j = 0; j < n; j++) { item = PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[j]); @@ -1131,13 +1131,13 @@ static PyObject* igraphmodule_i_ac_prod(PyObject* values, */ static PyObject* igraphmodule_i_ac_first(PyObject* values, const igraph_vector_int_list_t *merges) { - igraph_integer_t i, len = igraph_vector_int_list_size(merges); + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); - igraph_integer_t n = igraph_vector_int_size(v); + igraph_int_t n = igraph_vector_int_size(v); item = n > 0 ? PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[0]) : Py_None; if (item == 0) { @@ -1164,7 +1164,7 @@ static PyObject* igraphmodule_i_ac_first(PyObject* values, */ static PyObject* igraphmodule_i_ac_random(PyObject* values, const igraph_vector_int_list_t *merges) { - igraph_integer_t i, len = igraph_vector_int_list_size(merges); + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *item, *num; PyObject *random_module = PyImport_ImportModule("random"); PyObject *random_func; @@ -1181,7 +1181,7 @@ static PyObject* igraphmodule_i_ac_random(PyObject* values, res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); - igraph_integer_t n = igraph_vector_int_size(v); + igraph_int_t n = igraph_vector_int_size(v); if (n > 0) { num = PyObject_CallObject(random_func, 0); @@ -1192,7 +1192,7 @@ static PyObject* igraphmodule_i_ac_random(PyObject* values, } item = PyList_GetItem( - values, VECTOR(*v)[(igraph_integer_t)(n * PyFloat_AsDouble(num))] + values, VECTOR(*v)[(igraph_int_t)(n * PyFloat_AsDouble(num))] ); if (item == 0) { Py_DECREF(random_func); @@ -1227,13 +1227,13 @@ static PyObject* igraphmodule_i_ac_random(PyObject* values, */ static PyObject* igraphmodule_i_ac_last(PyObject* values, const igraph_vector_int_list_t *merges) { - igraph_integer_t i, len = igraph_vector_int_list_size(merges); + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); - igraph_integer_t n = igraph_vector_int_size(v); + igraph_int_t n = igraph_vector_int_size(v); item = (n > 0) ? PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[n-1]) : Py_None; if (item == 0) { @@ -1261,14 +1261,14 @@ static PyObject* igraphmodule_i_ac_last(PyObject* values, */ static PyObject* igraphmodule_i_ac_mean(PyObject* values, const igraph_vector_int_list_t *merges) { - igraph_integer_t i, len = igraph_vector_int_list_size(merges); + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); igraph_real_t num = 0.0, mean = 0.0; - igraph_integer_t j, n = igraph_vector_int_size(v); + igraph_int_t j, n = igraph_vector_int_size(v); for (j = 0; j < n; ) { item = PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[j]); @@ -1307,13 +1307,13 @@ static PyObject* igraphmodule_i_ac_mean(PyObject* values, */ static PyObject* igraphmodule_i_ac_median(PyObject* values, const igraph_vector_int_list_t *merges) { - igraph_integer_t i, len = igraph_vector_int_list_size(merges); + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *list, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); - igraph_integer_t j, n = igraph_vector_int_size(v); + igraph_int_t j, n = igraph_vector_int_size(v); list = PyList_New(n); for (j = 0; j < n; j++) { item = PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[j]); @@ -1874,7 +1874,7 @@ igraph_error_t igraphmodule_i_get_numeric_vertex_attr(const igraph_t *graph, igraph_vector_destroy(&newvalue); } else { igraph_vit_t it; - igraph_integer_t i = igraph_vector_size(value); + igraph_int_t i = igraph_vector_size(value); IGRAPH_CHECK(igraph_vit_create(graph, vs, &it)); IGRAPH_FINALLY(igraph_vit_destroy, &it); IGRAPH_CHECK(igraph_vector_resize(value, i + IGRAPH_VIT_SIZE(it))); @@ -1921,12 +1921,12 @@ igraph_error_t igraphmodule_i_get_string_vertex_attr(const igraph_t *graph, igraph_strvector_destroy(&newvalue); } else { igraph_vit_t it; - igraph_integer_t i = igraph_strvector_size(value); + igraph_int_t i = igraph_strvector_size(value); IGRAPH_CHECK(igraph_vit_create(graph, vs, &it)); IGRAPH_FINALLY(igraph_vit_destroy, &it); IGRAPH_CHECK(igraph_strvector_resize(value, i + IGRAPH_VIT_SIZE(it))); while (!IGRAPH_VIT_END(it)) { - igraph_integer_t v = IGRAPH_VIT_GET(it); + igraph_int_t v = IGRAPH_VIT_GET(it); char* str; result = PyList_GetItem(list, v); @@ -1982,7 +1982,7 @@ igraph_error_t igraphmodule_i_get_boolean_vertex_attr(const igraph_t *graph, igraph_vector_bool_destroy(&newvalue); } else { igraph_vit_t it; - igraph_integer_t i = igraph_vector_bool_size(value); + igraph_int_t i = igraph_vector_bool_size(value); IGRAPH_CHECK(igraph_vit_create(graph, vs, &it)); IGRAPH_FINALLY(igraph_vit_destroy, &it); IGRAPH_CHECK(igraph_vector_bool_resize(value, i + IGRAPH_VIT_SIZE(it))); @@ -2023,7 +2023,7 @@ igraph_error_t igraphmodule_i_get_numeric_edge_attr(const igraph_t *graph, igraph_vector_destroy(&newvalue); } else { igraph_eit_t it; - igraph_integer_t i = igraph_vector_size(value); + igraph_int_t i = igraph_vector_size(value); IGRAPH_CHECK(igraph_eit_create(graph, es, &it)); IGRAPH_FINALLY(igraph_eit_destroy, &it); IGRAPH_CHECK(igraph_vector_resize(value, i + IGRAPH_EIT_SIZE(it))); @@ -2074,7 +2074,7 @@ igraph_error_t igraphmodule_i_get_string_edge_attr(const igraph_t *graph, igraph_strvector_destroy(&newvalue); } else { igraph_eit_t it; - igraph_integer_t i = igraph_strvector_size(value); + igraph_int_t i = igraph_strvector_size(value); IGRAPH_CHECK(igraph_eit_create(graph, es, &it)); IGRAPH_FINALLY(igraph_eit_destroy, &it); IGRAPH_CHECK(igraph_strvector_resize(value, IGRAPH_EIT_SIZE(it))); @@ -2133,7 +2133,7 @@ igraph_error_t igraphmodule_i_get_boolean_edge_attr(const igraph_t *graph, igraph_vector_bool_destroy(&newvalue); } else { igraph_eit_t it; - igraph_integer_t i = igraph_vector_bool_size(value); + igraph_int_t i = igraph_vector_bool_size(value); IGRAPH_CHECK(igraph_eit_create(graph, es, &it)); IGRAPH_FINALLY(igraph_eit_destroy, &it); IGRAPH_CHECK(igraph_vector_bool_resize(value, i + IGRAPH_EIT_SIZE(it))); diff --git a/src/_igraph/attributes.h b/src/_igraph/attributes.h index 6d3da9c90..d02d55239 100644 --- a/src/_igraph/attributes.h +++ b/src/_igraph/attributes.h @@ -83,7 +83,7 @@ int igraphmodule_attribute_name_check(PyObject* obj); void igraphmodule_initialize_attribute_handler(void); void igraphmodule_index_vertex_names(igraph_t *graph, igraph_bool_t force); void igraphmodule_invalidate_vertex_name_index(igraph_t *graph); -int igraphmodule_get_vertex_id_by_name(igraph_t *graph, PyObject* o, igraph_integer_t* id); +int igraphmodule_get_vertex_id_by_name(igraph_t *graph, PyObject* o, igraph_int_t* id); PyObject* igraphmodule_create_or_get_edge_attribute_values(const igraph_t* graph, const char* name); diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c index 15ddf86e3..c8b814f88 100644 --- a/src/_igraph/bfsiter.c +++ b/src/_igraph/bfsiter.c @@ -44,7 +44,7 @@ PyTypeObject* igraphmodule_BFSIterType; */ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraph_neimode_t mode, igraph_bool_t advanced) { igraphmodule_BFSIterObject* self; - igraph_integer_t no_of_nodes, r; + igraph_int_t no_of_nodes, r; self = (igraphmodule_BFSIterObject*) PyType_GenericNew(igraphmodule_BFSIterType, 0, 0); if (!self) { @@ -162,10 +162,10 @@ static PyObject* igraphmodule_BFSIter_iter(igraphmodule_BFSIterObject* self) { static PyObject* igraphmodule_BFSIter_iternext(igraphmodule_BFSIterObject* self) { if (!igraph_dqueue_int_empty(&self->queue)) { - igraph_integer_t vid = igraph_dqueue_int_pop(&self->queue); - igraph_integer_t dist = igraph_dqueue_int_pop(&self->queue); - igraph_integer_t parent = igraph_dqueue_int_pop(&self->queue); - igraph_integer_t i, n; + igraph_int_t vid = igraph_dqueue_int_pop(&self->queue); + igraph_int_t dist = igraph_dqueue_int_pop(&self->queue); + igraph_int_t parent = igraph_dqueue_int_pop(&self->queue); + igraph_int_t i, n; if (igraph_neighbors(self->graph, &self->neis, vid, self->mode, /* loops = */ 0, /* multiple = */ 0)) { igraphmodule_handle_igraph_error(); @@ -174,7 +174,7 @@ static PyObject* igraphmodule_BFSIter_iternext(igraphmodule_BFSIterObject* self) n = igraph_vector_int_size(&self->neis); for (i = 0; i < n; i++) { - igraph_integer_t neighbor = VECTOR(self->neis)[i]; + igraph_int_t neighbor = VECTOR(self->neis)[i]; if (self->visited[neighbor] == 0) { self->visited[neighbor] = 1; if (igraph_dqueue_int_push(&self->queue, neighbor) || diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index dbeedaf78..a36d0558d 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -951,7 +951,7 @@ int igraphmodule_PyObject_to_igraph_t(PyObject *o, igraph_t **result) { } /** - * \brief Converts a PyLong to an igraph \c igraph_integer_t + * \brief Converts a PyLong to an igraph \c igraph_int_t * * Raises suitable Python exceptions when needed. * @@ -962,7 +962,7 @@ int igraphmodule_PyObject_to_igraph_t(PyObject *o, igraph_t **result) { * \param v the result is stored here * \return 0 if everything was OK, 1 otherwise */ -int PyLong_to_integer_t(PyObject* obj, igraph_integer_t* v) { +int PyLong_to_integer_t(PyObject* obj, igraph_int_t* v) { if (IGRAPH_INTEGER_SIZE == 64) { /* here the assumption is that sizeof(long long) == 64 bits; anyhow, this * is the widest integer type that we can convert a PyLong to so we cannot @@ -987,7 +987,7 @@ int PyLong_to_integer_t(PyObject* obj, igraph_integer_t* v) { } /** - * \brief Converts a Python object to an igraph \c igraph_integer_t + * \brief Converts a Python object to an igraph \c igraph_int_t * * Raises suitable Python exceptions when needed. * @@ -995,9 +995,9 @@ int PyLong_to_integer_t(PyObject* obj, igraph_integer_t* v) { * \param v the result is stored here * \return 0 if everything was OK, 1 otherwise */ -int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v) { +int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_int_t *v) { int retval; - igraph_integer_t num; + igraph_int_t num; if (object == NULL) { } else if (PyLong_Check(object)) { @@ -1024,7 +1024,7 @@ int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v) { } /** - * \brief Converts a Python object to an igraph \c igraph_integer_t when it is + * \brief Converts a Python object to an igraph \c igraph_int_t when it is * used as a limit on the number of results for some function. * * This is different from \ref igraphmodule_PyObject_to_integer_t such that it @@ -1037,9 +1037,9 @@ int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v) { * \param v the result is stored here * \return 0 if everything was OK, 1 otherwise */ -int igraphmodule_PyObject_to_max_results_t(PyObject *object, igraph_integer_t *v) { +int igraphmodule_PyObject_to_max_results_t(PyObject *object, igraph_int_t *v) { int retval; - igraph_integer_t num; + igraph_int_t num; if (object != NULL) { if (object == Py_None) { @@ -1131,7 +1131,7 @@ int igraphmodule_PyObject_to_vector_t(PyObject *list, igraph_vector_t *v, igraph PyObject *item, *it; Py_ssize_t size_hint; int ok; - igraph_integer_t number; + igraph_int_t number; if (PyBaseString_Check(list)) { /* It is highly unlikely that a string (although it is a sequence) will @@ -1330,7 +1330,7 @@ int igraphmodule_PyObject_float_to_vector_t(PyObject *list, igraph_vector_t *v) */ int igraphmodule_PyObject_to_vector_int_t(PyObject *list, igraph_vector_int_t *v) { PyObject *it = 0, *item; - igraph_integer_t value = 0; + igraph_int_t value = 0; Py_ssize_t i, j, k; int ok; @@ -1492,13 +1492,13 @@ int igraphmodule_PyObject_to_vector_bool_t(PyObject *list, /** * \ingroup python_interface_conversion - * \brief Converts an igraph \c igraph_integer_t to a Python integer + * \brief Converts an igraph \c igraph_int_t to a Python integer * - * \param value the \c igraph_integer_t value to be converted + * \param value the \c igraph_int_t value to be converted * \return the Python integer as a \c PyObject*, or \c NULL if an * error occurred */ -PyObject* igraphmodule_integer_t_to_PyObject(igraph_integer_t value) { +PyObject* igraphmodule_integer_t_to_PyObject(igraph_int_t value) { #if IGRAPH_INTEGER_SIZE == 32 /* minimum size of a long is 32 bits so we are okay */ return PyLong_FromLong(value); @@ -1506,7 +1506,7 @@ PyObject* igraphmodule_integer_t_to_PyObject(igraph_integer_t value) { /* minimum size of a long long is 64 bits so we are okay */ return PyLong_FromLongLong(value); #else -# error "Unknown igraph_integer_t size" +# error "Unknown igraph_int_t size" #endif } @@ -1645,10 +1645,10 @@ PyObject* igraphmodule_vector_int_t_to_PyList(const igraph_vector_int_t *v) { * \param v the \c igraph_vector_int_t containing the vector to be converted * \return the Python integer list as a \c PyObject*, or \c NULL if an error occurred */ -PyObject* igraphmodule_vector_int_t_to_PyList_with_nan(const igraph_vector_int_t *v, const igraph_integer_t nanvalue) { +PyObject* igraphmodule_vector_int_t_to_PyList_with_nan(const igraph_vector_int_t *v, const igraph_int_t nanvalue) { PyObject *list, *item; Py_ssize_t n, i; - igraph_integer_t val; + igraph_int_t val; n = igraph_vector_int_size(v); if (n < 0) { @@ -1829,7 +1829,7 @@ int igraphmodule_PyObject_to_edgelist( ) { PyObject *item, *i1, *i2, *it, *expected; int ok; - igraph_integer_t idx1=0, idx2=0; + igraph_int_t idx1=0, idx2=0; if (PyBaseString_Check(list)) { /* It is highly unlikely that a string (although it is a sequence) will @@ -1844,13 +1844,13 @@ int igraphmodule_PyObject_to_edgelist( * detail that we don't want to commit ourselves to */ if (PyMemoryView_Check(list)) { item = PyObject_GetAttrString(list, "itemsize"); - expected = PyLong_FromSize_t(sizeof(igraph_integer_t)); + expected = PyLong_FromSize_t(sizeof(igraph_int_t)); ok = item && PyObject_RichCompareBool(item, expected, Py_EQ); Py_XDECREF(expected); Py_XDECREF(item); if (!ok) { PyErr_SetString( - PyExc_TypeError, "item size of buffer must match the size of igraph_integer_t" + PyExc_TypeError, "item size of buffer must match the size of igraph_int_t" ); return 1; } @@ -2004,7 +2004,7 @@ int igraphmodule_attrib_to_vector_t(PyObject *o, igraphmodule_GraphObject *self, /* Check whether the attribute exists and is numeric */ igraph_attribute_type_t at; igraph_attribute_elemtype_t et; - igraph_integer_t n; + igraph_int_t n; char *name = PyUnicode_CopyAsString(o); if (attr_type == ATTRIBUTE_TYPE_VERTEX) { @@ -2117,7 +2117,7 @@ int igraphmodule_attrib_to_vector_int_t(PyObject *o, igraphmodule_GraphObject *s if (PyUnicode_Check(o)) { igraph_vector_t* dummy = 0; - igraph_integer_t i, n; + igraph_int_t i, n; if (igraphmodule_attrib_to_vector_t(o, self, &dummy, attr_type)) { return 1; @@ -2145,7 +2145,7 @@ int igraphmodule_attrib_to_vector_int_t(PyObject *o, igraphmodule_GraphObject *s } for (i = 0; i < n; i++) { - VECTOR(*result)[i] = (igraph_integer_t) VECTOR(*dummy)[i]; + VECTOR(*result)[i] = (igraph_int_t) VECTOR(*dummy)[i]; } igraph_vector_destroy(dummy); free(dummy); @@ -2210,7 +2210,7 @@ int igraphmodule_attrib_to_vector_bool_t(PyObject *o, igraphmodule_GraphObject * return 0; if (PyUnicode_Check(o)) { - igraph_integer_t i, n; + igraph_int_t i, n; /* First, check if the attribute is a "real" boolean */ igraph_attribute_type_t at; @@ -2884,7 +2884,7 @@ int igraphmodule_PyObject_to_matrix_int_t_with_minimum_column_count( ) { Py_ssize_t nr, nc, n, i, j; PyObject *row, *item; - igraph_integer_t value; + igraph_int_t value; /* calculate the matrix dimensions */ if (!PySequence_Check(o) || PyUnicode_Check(o)) { @@ -3403,7 +3403,7 @@ int igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t_with_type(PyObject *it, * if we don't need name lookups. * \return 0 if everything was OK, 1 otherwise */ -int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *graph) { +int igraphmodule_PyObject_to_vid(PyObject *o, igraph_int_t *vid, igraph_t *graph) { if (o == 0) { PyErr_SetString(PyExc_TypeError, "only non-negative integers, strings or igraph.Vertex objects can be converted to vertex IDs"); return 1; @@ -3462,7 +3462,7 @@ int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *g * if we don't need name lookups. * \return 0 if everything was OK, 1 otherwise */ -int igraphmodule_PyObject_to_optional_vid(PyObject *o, igraph_integer_t *vid, igraph_t *graph) { +int igraphmodule_PyObject_to_optional_vid(PyObject *o, igraph_int_t *vid, igraph_t *graph) { if (o == 0 || o == Py_None) { return 0; } else { @@ -3484,7 +3484,7 @@ int igraphmodule_PyObject_to_optional_vid(PyObject *o, igraph_integer_t *vid, ig int igraphmodule_PyObject_to_vid_list(PyObject* o, igraph_vector_int_t* result, igraph_t* graph) { PyObject *iterator; PyObject *item; - igraph_integer_t vid; + igraph_int_t vid; if (PyBaseString_Check(o)) { /* exclude strings; they are iterable but cannot yield meaningful vertex IDs */ @@ -3548,8 +3548,8 @@ int igraphmodule_PyObject_to_vid_list(PyObject* o, igraph_vector_int_t* result, * \return 0 if everything was OK, 1 otherwise */ int igraphmodule_PyObject_to_vs_t(PyObject *o, igraph_vs_t *vs, - igraph_t *graph, igraph_bool_t *return_single, igraph_integer_t *single_vid) { - igraph_integer_t vid; + igraph_t *graph, igraph_bool_t *return_single, igraph_int_t *single_vid) { + igraph_int_t vid; igraph_vector_int_t vector; if (o == 0 || o == Py_None) { @@ -3666,9 +3666,9 @@ int igraphmodule_PyObject_to_vs_t(PyObject *o, igraph_vs_t *vs, * if we don't want to handle tuples. * \return 0 if everything was OK, 1 otherwise */ -int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *graph) { +int igraphmodule_PyObject_to_eid(PyObject *o, igraph_int_t *eid, igraph_t *graph) { int retval; - igraph_integer_t vid1, vid2; + igraph_int_t vid1, vid2; if (!o) { PyErr_SetString(PyExc_TypeError, @@ -3770,7 +3770,7 @@ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *g */ int igraphmodule_PyObject_to_es_t(PyObject *o, igraph_es_t *es, igraph_t *graph, igraph_bool_t *return_single) { - igraph_integer_t eid; + igraph_int_t eid; igraph_vector_int_t vector; if (o == 0 || o == Py_None) { diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index a8259a8db..39fbf5619 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -100,11 +100,11 @@ int igraphmodule_PyObject_to_vconn_nei_t(PyObject *o, igraph_vconn_nei_t *result /* Conversion from PyObject to igraph types */ -int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v); +int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_int_t *v); int igraphmodule_PyObject_to_real_t(PyObject *object, igraph_real_t *v); int igraphmodule_PyObject_to_igraph_t(PyObject *o, igraph_t **result); -int igraphmodule_PyObject_to_max_results_t(PyObject *object, igraph_integer_t *v); +int igraphmodule_PyObject_to_max_results_t(PyObject *object, igraph_int_t *v); int igraphmodule_PyObject_to_vector_t(PyObject *list, igraph_vector_t *v, igraph_bool_t need_non_negative); @@ -138,14 +138,14 @@ int igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t(PyObject *it, igraph_vector_ptr_t *v); int igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t_with_type(PyObject *it, igraph_vector_ptr_t *v, PyTypeObject **g_type); -int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *graph); -int igraphmodule_PyObject_to_optional_vid(PyObject *o, igraph_integer_t *vid, igraph_t *graph); +int igraphmodule_PyObject_to_vid(PyObject *o, igraph_int_t *vid, igraph_t *graph); +int igraphmodule_PyObject_to_optional_vid(PyObject *o, igraph_int_t *vid, igraph_t *graph); int igraphmodule_PyObject_to_vid_list(PyObject *o, igraph_vector_int_t *vids, igraph_t *graph); int igraphmodule_PyObject_to_vs_t( PyObject *o, igraph_vs_t *vs, igraph_t *graph, - igraph_bool_t *return_single, igraph_integer_t *single_vid + igraph_bool_t *return_single, igraph_int_t *single_vid ); -int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *graph); +int igraphmodule_PyObject_to_eid(PyObject *o, igraph_int_t *eid, igraph_t *graph); int igraphmodule_PyObject_to_es_t(PyObject *o, igraph_es_t *es, igraph_t *graph, igraph_bool_t *return_single); int igraphmodule_PyObject_to_attribute_values(PyObject *o, @@ -174,7 +174,7 @@ int igraphmodule_attrib_to_vector_bool_t(PyObject *o, igraphmodule_GraphObject * /* Conversion from igraph types to PyObjects */ -PyObject* igraphmodule_integer_t_to_PyObject(igraph_integer_t value); +PyObject* igraphmodule_integer_t_to_PyObject(igraph_int_t value); PyObject* igraphmodule_real_t_to_PyObject(igraph_real_t value, igraphmodule_conv_t type); PyObject* igraphmodule_vector_bool_t_to_PyList(const igraph_vector_bool_t *v); @@ -185,7 +185,7 @@ PyObject* igraphmodule_vector_int_t_pair_to_PyList(const igraph_vector_int_t *v1 const igraph_vector_int_t *v2); PyObject* igraphmodule_vector_int_t_to_PyList_of_fixed_length_tuples( const igraph_vector_int_t *v, Py_ssize_t tuple_len); -PyObject* igraphmodule_vector_int_t_to_PyList_with_nan(const igraph_vector_int_t *v, const igraph_integer_t nanvalue); +PyObject* igraphmodule_vector_int_t_to_PyList_with_nan(const igraph_vector_int_t *v, const igraph_int_t nanvalue); PyObject* igraphmodule_vector_ptr_t_to_PyList(const igraph_vector_ptr_t *v, igraphmodule_conv_t type); PyObject* igraphmodule_vector_int_ptr_t_to_PyList(const igraph_vector_ptr_t *v); diff --git a/src/_igraph/dfsiter.c b/src/_igraph/dfsiter.c index dbb203f8c..08a7fedea 100644 --- a/src/_igraph/dfsiter.c +++ b/src/_igraph/dfsiter.c @@ -44,7 +44,7 @@ PyTypeObject* igraphmodule_DFSIterType; */ PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraph_neimode_t mode, igraph_bool_t advanced) { igraphmodule_DFSIterObject* self; - igraph_integer_t no_of_nodes, r; + igraph_int_t no_of_nodes, r; self = (igraphmodule_DFSIterObject*) PyType_GenericNew(igraphmodule_DFSIterType, 0, 0); if (!self) { @@ -162,7 +162,7 @@ static PyObject* igraphmodule_DFSIter_iter(igraphmodule_DFSIterObject* self) { static PyObject* igraphmodule_DFSIter_iternext(igraphmodule_DFSIterObject* self) { /* the design is to return the top of the stack and then proceed until * we have found an unvisited neighbor and push that on top */ - igraph_integer_t parent_out, dist_out, vid_out; + igraph_int_t parent_out, dist_out, vid_out; igraph_bool_t any = false; /* nothing on the stack, end of iterator */ @@ -181,13 +181,13 @@ static PyObject* igraphmodule_DFSIter_iternext(igraphmodule_DFSIterObject* self) /* look for neighbors until we find one or until we have exhausted the graph */ while (!any && !igraph_stack_int_empty(&self->stack)) { - igraph_integer_t parent = igraph_stack_int_pop(&self->stack); - igraph_integer_t dist = igraph_stack_int_pop(&self->stack); - igraph_integer_t vid = igraph_stack_int_pop(&self->stack); + igraph_int_t parent = igraph_stack_int_pop(&self->stack); + igraph_int_t dist = igraph_stack_int_pop(&self->stack); + igraph_int_t vid = igraph_stack_int_pop(&self->stack); igraph_stack_int_push(&self->stack, vid); igraph_stack_int_push(&self->stack, dist); igraph_stack_int_push(&self->stack, parent); - igraph_integer_t i, n; + igraph_int_t i, n; /* the values above are returned at at this stage. However, we must * prepare for the next iteration by putting the next unvisited @@ -199,7 +199,7 @@ static PyObject* igraphmodule_DFSIter_iternext(igraphmodule_DFSIterObject* self) n = igraph_vector_int_size(&self->neis); for (i = 0; i < n; i++) { - igraph_integer_t neighbor = VECTOR(self->neis)[i]; + igraph_int_t neighbor = VECTOR(self->neis)[i]; /* new neighbor, push the next item onto the stack */ if (self->visited[neighbor] == 0) { any = 1; diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index 26cefc72c..0f5e54171 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -54,7 +54,7 @@ int igraphmodule_Edge_Check(PyObject* obj) { * exception and returns zero if the edge object is invalid. */ int igraphmodule_Edge_Validate(PyObject* obj) { - igraph_integer_t n; + igraph_int_t n; igraphmodule_EdgeObject *self; igraphmodule_GraphObject *graph; @@ -98,7 +98,7 @@ int igraphmodule_Edge_Validate(PyObject* obj) { * changes, your existing edge objects will point to elsewhere * (or they might even get invalidated). */ -PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_integer_t idx) { +PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_int_t idx) { return PyObject_CallFunction((PyObject*) igraphmodule_EdgeType, "On", gref, (Py_ssize_t) idx); } @@ -110,7 +110,7 @@ PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_integer_t static int igraphmodule_Edge_init(igraphmodule_EdgeObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "graph", "eid", NULL }; PyObject *g, *index_o = Py_None; - igraph_integer_t eid; + igraph_int_t eid; igraph_t *graph; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|O", kwlist, @@ -400,7 +400,7 @@ int igraphmodule_Edge_set_attribute(igraphmodule_EdgeObject* self, PyObject* k, /* result is NULL, check whether there was an error */ if (!PyErr_Occurred()) { /* no, there wasn't, so we must simply add the attribute */ - igraph_integer_t i, n = igraph_ecount(&o->g); + igraph_int_t i, n = igraph_ecount(&o->g); result = PyList_New(n); for (i = 0; i < n; i++) { if (i != self->idx) { @@ -437,7 +437,7 @@ int igraphmodule_Edge_set_attribute(igraphmodule_EdgeObject* self, PyObject* k, */ PyObject* igraphmodule_Edge_get_from(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; + igraph_int_t from, to; if (!igraphmodule_Edge_Validate((PyObject*)self)) { return NULL; @@ -456,7 +456,7 @@ PyObject* igraphmodule_Edge_get_from(igraphmodule_EdgeObject* self, void* closur */ PyObject* igraphmodule_Edge_get_source_vertex(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; + igraph_int_t from, to; if (!igraphmodule_Edge_Validate((PyObject*)self)) return NULL; @@ -474,7 +474,7 @@ PyObject* igraphmodule_Edge_get_source_vertex(igraphmodule_EdgeObject* self, voi */ PyObject* igraphmodule_Edge_get_to(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; + igraph_int_t from, to; if (!igraphmodule_Edge_Validate((PyObject*)self)) { return NULL; @@ -493,7 +493,7 @@ PyObject* igraphmodule_Edge_get_to(igraphmodule_EdgeObject* self, void* closure) */ PyObject* igraphmodule_Edge_get_target_vertex(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; + igraph_int_t from, to; if (!igraphmodule_Edge_Validate((PyObject*)self)) return NULL; @@ -515,9 +515,9 @@ PyObject* igraphmodule_Edge_get_index(igraphmodule_EdgeObject* self, void* closu /** * \ingroup python_interface_edge - * Returns the edge index as an igraph_integer_t + * Returns the edge index as an igraph_int_t */ -igraph_integer_t igraphmodule_Edge_get_index_as_igraph_integer(igraphmodule_EdgeObject* self) { +igraph_int_t igraphmodule_Edge_get_index_as_igraph_integer(igraphmodule_EdgeObject* self) { return self->idx; } @@ -527,7 +527,7 @@ igraph_integer_t igraphmodule_Edge_get_index_as_igraph_integer(igraphmodule_Edge */ PyObject* igraphmodule_Edge_get_tuple(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; + igraph_int_t from, to; PyObject *from_o, *to_o, *result; if (!igraphmodule_Edge_Validate((PyObject*)self)) { @@ -562,7 +562,7 @@ PyObject* igraphmodule_Edge_get_tuple(igraphmodule_EdgeObject* self, void* closu */ PyObject* igraphmodule_Edge_get_vertex_tuple(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; + igraph_int_t from, to; PyObject *from_o, *to_o; if (!igraphmodule_Edge_Validate((PyObject*)self)) diff --git a/src/_igraph/edgeobject.h b/src/_igraph/edgeobject.h index f42862fe3..039a6e20f 100644 --- a/src/_igraph/edgeobject.h +++ b/src/_igraph/edgeobject.h @@ -34,7 +34,7 @@ typedef struct { PyObject_HEAD igraphmodule_GraphObject* gref; - igraph_integer_t idx; + igraph_int_t idx; long hash; } igraphmodule_EdgeObject; @@ -43,7 +43,7 @@ extern PyTypeObject* igraphmodule_EdgeType; int igraphmodule_Edge_register_type(void); int igraphmodule_Edge_Check(PyObject* obj); -PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_integer_t idx); -igraph_integer_t igraphmodule_Edge_get_index_as_igraph_integer(igraphmodule_EdgeObject* self); +PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_int_t idx); +igraph_int_t igraphmodule_Edge_get_index_as_igraph_integer(igraphmodule_EdgeObject* self); #endif diff --git a/src/_igraph/edgeseqobject.c b/src/_igraph/edgeseqobject.c index 4e188ae2c..13f4364f9 100644 --- a/src/_igraph/edgeseqobject.c +++ b/src/_igraph/edgeseqobject.c @@ -109,7 +109,7 @@ int igraphmodule_EdgeSeq_init(igraphmodule_EdgeSeqObject *self, PyObject *args, igraph_es_all(&es, IGRAPH_EDGEORDER_ID); } else if (PyLong_Check(esobj)) { /* We selected a single edge */ - igraph_integer_t idx; + igraph_int_t idx; if (igraphmodule_PyObject_to_integer_t(esobj, &idx)) { return -1; @@ -124,7 +124,7 @@ int igraphmodule_EdgeSeq_init(igraphmodule_EdgeSeqObject *self, PyObject *args, } else { /* We selected multiple edges */ igraph_vector_int_t v; - igraph_integer_t n = igraph_ecount(&((igraphmodule_GraphObject*)g)->g); + igraph_int_t n = igraph_ecount(&((igraphmodule_GraphObject*)g)->g); if (igraphmodule_PyObject_to_vector_int_t(esobj, &v)) { return -1; } @@ -173,7 +173,7 @@ void igraphmodule_EdgeSeq_dealloc(igraphmodule_EdgeSeqObject* self) { */ Py_ssize_t igraphmodule_EdgeSeq_sq_length(igraphmodule_EdgeSeqObject* self) { igraph_t *g; - igraph_integer_t result; + igraph_int_t result; g = &GET_GRAPH(self); @@ -192,7 +192,7 @@ Py_ssize_t igraphmodule_EdgeSeq_sq_length(igraphmodule_EdgeSeqObject* self) { PyObject* igraphmodule_EdgeSeq_sq_item(igraphmodule_EdgeSeqObject* self, Py_ssize_t i) { igraph_t *g; - igraph_integer_t idx = -1; + igraph_int_t idx = -1; if (!self->gref) { return NULL; @@ -433,7 +433,7 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject igraphmodule_GraphObject *gr; igraph_vector_int_t es; Py_ssize_t i, j, n; - igraph_integer_t no_of_edges; + igraph_int_t no_of_edges; gr = self->gref; dict = ATTR_STRUCT_DICT(&gr->g)[ATTRHASH_IDX_EDGE]; @@ -570,7 +570,7 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject /* We don't have attributes with the given name yet. Create an entry * in the dict, create a new list, fill with None for vertices not in the * sequence and copy the rest */ - igraph_integer_t n2 = igraph_ecount(&gr->g); + igraph_int_t n2 = igraph_ecount(&gr->g); list = PyList_New(n2); if (list == 0) { igraph_vector_int_destroy(&es); @@ -682,7 +682,7 @@ PyObject* igraphmodule_EdgeSeq_find(igraphmodule_EdgeSeqObject *self, PyObject * PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject *args) { igraphmodule_EdgeSeqObject *result; igraphmodule_GraphObject *gr; - igraph_integer_t igraph_idx; + igraph_int_t igraph_idx; igraph_bool_t working_on_whole_graph = igraph_es_is_all(&self->es); igraph_vector_int_t v, v2; Py_ssize_t i, j, n, m; @@ -786,7 +786,7 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject for (; i < n; i++) { PyObject *item2 = PyTuple_GetItem(args, i); - igraph_integer_t idx; + igraph_int_t idx; if (item2 == 0) { Py_DECREF(result); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index b24c589d6..8be4bcbdb 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -196,7 +196,7 @@ int igraphmodule_Graph_init(igraphmodule_GraphObject * self, void* ptr = 0; Py_ssize_t n = 0; igraph_vector_int_t edges_vector; - igraph_integer_t vcount; + igraph_int_t vcount; igraph_bool_t edges_vector_owned = false; int retval = 0; @@ -876,7 +876,7 @@ PyObject *igraphmodule_Graph_diversity(igraphmodule_GraphObject * self, igraph_vector_t res, *weights = 0; igraph_vs_t vs; igraph_bool_t return_single = false; - igraph_integer_t no_of_nodes; + igraph_int_t no_of_nodes; static char *kwlist[] = { "vertices", "weights", NULL }; @@ -1076,7 +1076,7 @@ PyObject *igraphmodule_Graph_maxdegree(igraphmodule_GraphObject * self, igraph_neimode_t dmode = IGRAPH_ALL; PyObject *dmode_o = Py_None; PyObject *loops = Py_False; - igraph_integer_t res; + igraph_int_t res; igraph_vs_t vs; igraph_bool_t return_single = false; @@ -1332,7 +1332,7 @@ PyObject *igraphmodule_Graph_neighbors(igraphmodule_GraphObject * self, igraph_neimode_t dmode = IGRAPH_ALL; igraph_loops_t loops = IGRAPH_LOOPS; igraph_bool_t multiple = 1; - igraph_integer_t idx; + igraph_int_t idx; igraph_vector_int_t res; static char *kwlist[] = { "vertex", "mode", "loops", "multiple", NULL }; @@ -1388,7 +1388,7 @@ PyObject *igraphmodule_Graph_incident(igraphmodule_GraphObject * self, PyObject *list, *dmode_o = Py_None, *index_o, *loops_o = Py_True; igraph_neimode_t dmode = IGRAPH_OUT; igraph_loops_t loops = IGRAPH_LOOPS; - igraph_integer_t idx; + igraph_int_t idx; igraph_vector_int_t res; static char *kwlist[] = { "vertex", "mode", "loops", NULL }; @@ -1512,7 +1512,7 @@ PyObject *igraphmodule_Graph_are_adjacent(igraphmodule_GraphObject * self, { static char *kwlist[] = { "v1", "v2", NULL }; PyObject *v1, *v2; - igraph_integer_t idx1, idx2; + igraph_int_t idx1, idx2; igraph_bool_t res; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", kwlist, &v1, &v2)) @@ -1543,8 +1543,8 @@ PyObject *igraphmodule_Graph_get_eid(igraphmodule_GraphObject * self, PyObject *v1, *v2; PyObject *directed = Py_True; PyObject *error = Py_True; - igraph_integer_t idx1, idx2; - igraph_integer_t res; + igraph_int_t idx1, idx2; + igraph_int_t res; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OO", kwlist, &v1, &v2, &directed, &error)) @@ -1721,7 +1721,7 @@ PyObject *igraphmodule_Graph_farthest_points(igraphmodule_GraphObject * self, PyObject *dir = Py_True, *vcount_if_unconnected = Py_True; PyObject *weights_o = Py_None; igraph_vector_t *weights = 0; - igraph_integer_t from, to; + igraph_int_t from, to; igraph_real_t len; static char *kwlist[] = { "directed", "unconn", "weights", NULL }; @@ -2079,7 +2079,7 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, igraph_t g; Py_ssize_t n; float power = 1.0f, zero_appeal = 1.0f; - igraph_integer_t m = 1; + igraph_int_t m = 1; igraph_vector_int_t outseq; igraph_bool_t has_outseq = false; igraph_t *start_from = 0; @@ -3502,7 +3502,7 @@ PyObject *igraphmodule_Graph_Recent_Degree(PyTypeObject * type, igraph_t g; Py_ssize_t n, window = 0; float power = 0.0f, zero_appeal = 0.0f; - igraph_integer_t m = 0; + igraph_int_t m = 0; igraph_vector_int_t outseq; igraph_bool_t has_outseq = false; PyObject *m_obj, *outpref = Py_False, *directed = Py_False; @@ -4456,7 +4456,7 @@ PyObject *igraphmodule_Graph_biconnected_components(igraphmodule_GraphObject *se igraph_vector_int_list_t components; igraph_vector_int_t points; igraph_bool_t return_articulation_points; - igraph_integer_t no; + igraph_int_t no; PyObject *result_o, *aps=Py_False; static char* kwlist[] = {"return_articulation_points", NULL}; @@ -4635,7 +4635,7 @@ PyObject *igraphmodule_Graph_bipartite_projection_size(igraphmodule_GraphObject PyObject* args, PyObject* kwds) { PyObject *types_o = Py_None; igraph_vector_bool_t* types = 0; - igraph_integer_t vcount1, vcount2, ecount1, ecount2; + igraph_int_t vcount1, vcount2, ecount1, ecount2; static char* kwlist[] = {"types", NULL}; @@ -4932,7 +4932,7 @@ PyObject *igraphmodule_Graph_connected_components( static char *kwlist[] = { "mode", NULL }; igraph_connectedness_t mode = IGRAPH_STRONG; igraph_vector_int_t res1, res2; - igraph_integer_t no; + igraph_int_t no; PyObject *list, *mode_o = Py_None; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &mode_o)) @@ -5409,8 +5409,8 @@ PyObject *igraphmodule_Graph_edge_connectivity(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "source", "target", "checks", NULL }; PyObject *checks = Py_True, *source_o = Py_None, *target_o = Py_None; - igraph_integer_t source = -1, target = -1; - igraph_integer_t res; + igraph_int_t source = -1, target = -1; + igraph_int_t res; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &source_o, &target_o, &checks)) return NULL; @@ -5599,7 +5599,7 @@ PyObject *igraphmodule_Graph_get_shortest_path( static char *kwlist[] = { "v", "to", "weights", "mode", "output", "algorithm", NULL }; igraph_vector_t *weights=0; igraph_neimode_t mode = IGRAPH_OUT; - igraph_integer_t from, to; + igraph_int_t from, to; PyObject *list, *mode_o=Py_None, *weights_o=Py_None, *output_o=Py_None, *from_o = Py_None, *to_o=Py_None, *algorithm_o=Py_None; @@ -5683,7 +5683,7 @@ typedef struct { } igraphmodule_i_Graph_get_shortest_path_astar_callback_data_t; igraph_error_t igraphmodule_i_Graph_get_shortest_path_astar_callback( - igraph_real_t *result, igraph_integer_t from, igraph_integer_t to, + igraph_real_t *result, igraph_int_t from, igraph_int_t to, void *extra ) { igraphmodule_i_Graph_get_shortest_path_astar_callback_data_t* data = @@ -5735,7 +5735,7 @@ PyObject *igraphmodule_Graph_get_shortest_path_astar( static char *kwlist[] = { "v", "to", "heuristics", "weights", "mode", "output", NULL }; igraph_vector_t *weights=0; igraph_neimode_t mode = IGRAPH_OUT; - igraph_integer_t from, to; + igraph_int_t from, to; PyObject *list, *mode_o=Py_None, *weights_o=Py_None, *output_o=Py_None, *from_o = Py_None, *to_o=Py_None, *heuristics_o; @@ -5806,7 +5806,7 @@ PyObject *igraphmodule_Graph_get_shortest_paths(igraphmodule_GraphObject * igraph_vector_t *weights = NULL; igraph_neimode_t mode = IGRAPH_OUT; igraphmodule_shortest_path_algorithm_t algorithm = IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO; - igraph_integer_t from, no_of_target_nodes; + igraph_int_t from, no_of_target_nodes; igraph_vs_t to; PyObject *list, *mode_o=Py_None, *weights_o=Py_None, *output_o=Py_None, *from_o = Py_None, *to_o=Py_None, @@ -5916,7 +5916,7 @@ PyObject *igraphmodule_Graph_get_all_shortest_paths(igraphmodule_GraphObject * igraph_vector_int_list_t res; igraph_vector_t *weights = 0; igraph_neimode_t mode = IGRAPH_OUT; - igraph_integer_t from; + igraph_int_t from; igraph_vs_t to; PyObject *list, *from_o, *mode_o=Py_None, *to_o=Py_None, *weights_o=Py_None; @@ -5976,9 +5976,9 @@ PyObject *igraphmodule_Graph_get_k_shortest_paths( igraph_vector_int_list_t res; igraph_vector_t *weights = 0; igraph_neimode_t mode = IGRAPH_OUT; - igraph_integer_t from; - igraph_integer_t to; - igraph_integer_t k = 1; + igraph_int_t from; + igraph_int_t to; + igraph_int_t k = 1; PyObject *list, *from_o, *to_o; PyObject *output_o = Py_None, *mode_o = Py_None, *weights_o = Py_None, *k_o = NULL; igraph_bool_t use_edges = false; @@ -6047,9 +6047,9 @@ PyObject *igraphmodule_Graph_get_all_simple_paths(igraphmodule_GraphObject * static char *kwlist[] = { "v", "to", "minlen", "maxlen", "mode", "max_results", NULL }; igraph_vector_int_list_t res; igraph_neimode_t mode = IGRAPH_OUT; - igraph_integer_t from; + igraph_int_t from; igraph_vs_t to; - igraph_integer_t minlen, maxlen, max_results = IGRAPH_UNLIMITED; + igraph_int_t minlen, maxlen, max_results = IGRAPH_UNLIMITED; PyObject *list, *from_o, *mode_o = Py_None, *to_o = Py_None, *max_results_o = Py_None; PyObject *minlen_o = Py_None, *maxlen_o = Py_None; @@ -6582,7 +6582,7 @@ PyObject *igraphmodule_Graph_rewire(igraphmodule_GraphObject * self, { static char *kwlist[] = { "n", "allowed_edge_types", NULL }; PyObject *n_o = Py_None, *allowed_edge_types_o = Py_None; - igraph_integer_t n = 10 * igraph_ecount(&self->g); /* TODO overflow check */ + igraph_int_t n = 10 * igraph_ecount(&self->g); /* TODO overflow check */ igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &n_o, &allowed_edge_types_o)) { @@ -7063,7 +7063,7 @@ PyObject *igraphmodule_Graph_subcomponent(igraphmodule_GraphObject * self, static char *kwlist[] = { "v", "mode", NULL }; igraph_vector_int_t res; igraph_neimode_t mode = IGRAPH_ALL; - igraph_integer_t from; + igraph_int_t from; PyObject *list = NULL, *mode_o = Py_None, *from_o = Py_None; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &from_o, &mode_o)) @@ -7341,7 +7341,7 @@ PyObject *igraphmodule_Graph_vertex_connectivity(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "source", "target", "checks", "neighbors", NULL }; PyObject *checks = Py_True, *neis = Py_None, *source_o = Py_None, *target_o = Py_None; - igraph_integer_t source = -1, target = -1, res; + igraph_int_t source = -1, target = -1, res; igraph_vconn_nei_t neighbors = IGRAPH_VCONN_NEI_ERROR; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, @@ -7505,7 +7505,7 @@ typedef struct { } igraphmodule_i_Graph_motifs_randesu_callback_data_t; igraph_error_t igraphmodule_i_Graph_motifs_randesu_callback(const igraph_t *graph, - const igraph_vector_int_t *vids, igraph_integer_t isoclass, void* extra) { + const igraph_vector_int_t *vids, igraph_int_t isoclass, void* extra) { igraphmodule_i_Graph_motifs_randesu_callback_data_t* data = (igraphmodule_i_Graph_motifs_randesu_callback_data_t*)extra; PyObject* vector; @@ -7676,7 +7676,7 @@ PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *s if (PyLong_Check(sample)) { /* samples chosen randomly */ - igraph_integer_t ns; + igraph_int_t ns; if (igraphmodule_PyObject_to_integer_t(sample, &ns)) { igraph_vector_destroy(&cut_prob); return NULL; @@ -7772,7 +7772,7 @@ PyObject *igraphmodule_Graph_fundamental_cycles( PyObject *start_vid_o = Py_None; PyObject *weights_o = Py_None; PyObject *result_o; - igraph_integer_t cutoff = -1, start_vid = -1; + igraph_int_t cutoff = -1, start_vid = -1; igraph_vector_int_list_t result; igraph_vector_t *weights = 0; @@ -7826,7 +7826,7 @@ PyObject *igraphmodule_Graph_minimum_cycle_basis( PyObject *use_cycle_order_o = Py_True; PyObject *weights_o = Py_None; PyObject *result_o; - igraph_integer_t cutoff = -1; + igraph_int_t cutoff = -1; igraph_vector_int_list_t result; igraph_vector_t *weights; @@ -7883,10 +7883,10 @@ PyObject *igraphmodule_Graph_simple_cycles( PyObject *max_results_o = Py_None; // argument defaults: no cycle limits - igraph_integer_t mode = IGRAPH_OUT; - igraph_integer_t min_cycle_length = -1; - igraph_integer_t max_cycle_length = -1; - igraph_integer_t max_results = IGRAPH_UNLIMITED; + igraph_int_t mode = IGRAPH_OUT; + igraph_int_t min_cycle_length = -1; + igraph_int_t max_cycle_length = -1; + igraph_int_t max_results = IGRAPH_UNLIMITED; igraph_bool_t use_edges = false; static char *kwlist[] = { "mode", "min", "max", "output", "max_results", NULL }; @@ -8128,7 +8128,7 @@ PyObject *igraphmodule_Graph_layout_star(igraphmodule_GraphObject* self, igraph_matrix_t m; PyObject *result_o, *order_o = Py_None, *center_o = Py_None; - igraph_integer_t center = 0; + igraph_int_t center = 0; igraph_vector_int_t* order = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, @@ -8188,7 +8188,7 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * igraph_matrix_t m; igraph_bool_t use_seed = false; igraph_error_t ret; - igraph_integer_t maxiter; + igraph_int_t maxiter; Py_ssize_t dim = 2; igraph_real_t kkconst; double epsilon = 0.0; @@ -8755,7 +8755,7 @@ PyObject *igraphmodule_Graph_layout_lgl(igraphmodule_GraphObject * self, igraph_matrix_t m; PyObject *result_o, *root_o = Py_None; Py_ssize_t maxiter = 150; - igraph_integer_t proot = -1; + igraph_int_t proot = -1; double maxdelta, area, coolexp, repulserad, cellsize; maxdelta = igraph_vcount(&self->g); @@ -9169,7 +9169,7 @@ PyObject *igraphmodule_Graph_layout_umap( use_seed, dist, (igraph_real_t)min_dist, - (igraph_integer_t)epochs, + (igraph_int_t)epochs, distances_are_weights)) { if (dist) { igraph_vector_destroy(dist); free(dist); @@ -9183,7 +9183,7 @@ PyObject *igraphmodule_Graph_layout_umap( use_seed, dist, (igraph_real_t)min_dist, - (igraph_integer_t)epochs, + (igraph_int_t)epochs, distances_are_weights)) { if (dist) { igraph_vector_destroy(dist); free(dist); @@ -9544,7 +9544,7 @@ PyObject *igraphmodule_Graph_Read_DIMACS(PyTypeObject * type, { igraphmodule_GraphObject *self; igraphmodule_filehandle_t fobj; - igraph_integer_t source = 0, target = 0; + igraph_int_t source = 0, target = 0; igraph_vector_t capacity; igraph_t g; PyObject *fname = NULL, *directed = Py_False, *capacity_obj; @@ -9895,7 +9895,7 @@ PyObject *igraphmodule_Graph_write_dimacs(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { PyObject *capacity_obj = Py_None, *fname = NULL, *source_o, *target_o; - igraph_integer_t source, target; + igraph_int_t source, target; igraphmodule_filehandle_t fobj; igraph_vector_t* capacity = 0; @@ -10381,7 +10381,7 @@ PyObject *igraphmodule_Graph_count_automorphisms( PyObject *igraphmodule_Graph_isoclass(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - igraph_integer_t isoclass = 0; + igraph_int_t isoclass = 0; PyObject *vids = 0; char *kwlist[] = { "vertices", NULL }; @@ -10582,7 +10582,7 @@ igraph_error_t igraphmodule_i_Graph_isomorphic_vf2_callback_fn( igraph_bool_t igraphmodule_i_Graph_isomorphic_vf2_node_compat_fn( const igraph_t *graph1, const igraph_t *graph2, - const igraph_integer_t cand1, const igraph_integer_t cand2, + const igraph_int_t cand1, const igraph_int_t cand2, void* extra) { igraphmodule_i_Graph_isomorphic_vf2_callback_data_t* data = (igraphmodule_i_Graph_isomorphic_vf2_callback_data_t*)extra; @@ -10606,7 +10606,7 @@ igraph_bool_t igraphmodule_i_Graph_isomorphic_vf2_node_compat_fn( igraph_bool_t igraphmodule_i_Graph_isomorphic_vf2_edge_compat_fn( const igraph_t *graph1, const igraph_t *graph2, - const igraph_integer_t cand1, const igraph_integer_t cand2, + const igraph_int_t cand1, const igraph_int_t cand2, void* extra) { igraphmodule_i_Graph_isomorphic_vf2_callback_data_t* data = (igraphmodule_i_Graph_isomorphic_vf2_callback_data_t*)extra; @@ -10781,7 +10781,7 @@ PyObject *igraphmodule_Graph_isomorphic_vf2(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_count_isomorphisms_vf2(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - igraph_integer_t res = 0; + igraph_int_t res = 0; PyObject *o = Py_None; PyObject *color1_o=Py_None, *color2_o=Py_None; PyObject *edge_color1_o=Py_None, *edge_color2_o=Py_None; @@ -11119,7 +11119,7 @@ PyObject *igraphmodule_Graph_subisomorphic_vf2(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_count_subisomorphisms_vf2(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - igraph_integer_t res = 0; + igraph_int_t res = 0; PyObject *o = Py_None; PyObject *color1_o = Py_None, *color2_o = Py_None; PyObject *edge_color1_o=Py_None, *edge_color2_o=Py_None; @@ -11721,7 +11721,7 @@ PyObject *igraphmodule_Graph_bfs(igraphmodule_GraphObject * self, { static char *kwlist[] = { "vid", "mode", NULL }; PyObject *l1, *l2, *l3, *result_o, *mode_o = Py_None, *vid_o; - igraph_integer_t vid; + igraph_int_t vid; igraph_neimode_t mode = IGRAPH_OUT; igraph_vector_int_t vids; igraph_vector_int_t layers; @@ -11887,7 +11887,7 @@ PyObject *igraphmodule_Graph_dominator(igraphmodule_GraphObject * self, { static char *kwlist[] = { "vid", "mode", NULL }; PyObject *list = Py_None, *mode_o = Py_None, *root_o; - igraph_integer_t root; + igraph_int_t root; igraph_vector_int_t dom; igraph_neimode_t mode = IGRAPH_OUT; igraph_error_t res; @@ -11941,7 +11941,7 @@ PyObject *igraphmodule_Graph_maxflow_value(igraphmodule_GraphObject * self, PyObject *capacity_object = Py_None, *v1_o, *v2_o; igraph_vector_t capacity_vector; igraph_real_t res; - igraph_integer_t v1, v2; + igraph_int_t v1, v2; igraph_maxflow_stats_t stats; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|O", kwlist, @@ -11980,7 +11980,7 @@ PyObject *igraphmodule_Graph_maxflow(igraphmodule_GraphObject * self, PyObject *capacity_object = Py_None, *flow_o, *cut_o, *partition_o, *v1_o, *v2_o; igraph_vector_t capacity_vector; igraph_real_t res; - igraph_integer_t v1, v2; + igraph_int_t v1, v2; igraph_vector_t flow; igraph_vector_int_t cut, partition; igraph_maxflow_stats_t stats; @@ -12066,7 +12066,7 @@ PyObject *igraphmodule_Graph_all_st_cuts(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "source", "target", NULL }; - igraph_integer_t source, target; + igraph_int_t source, target; igraph_vector_int_list_t cuts, partition1s; PyObject *source_o, *target_o; PyObject *cuts_o, *partition1s_o; @@ -12117,7 +12117,7 @@ PyObject *igraphmodule_Graph_all_st_mincuts(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "source", "target", "capacity", NULL }; - igraph_integer_t source, target; + igraph_int_t source, target; igraph_real_t value; igraph_vector_int_list_t cuts, partition1s; igraph_vector_t capacity_vector; @@ -12184,7 +12184,7 @@ PyObject *igraphmodule_Graph_mincut_value(igraphmodule_GraphObject * self, PyObject *capacity_object = Py_None, *v1_o = Py_None, *v2_o = Py_None; igraph_vector_t capacity_vector; igraph_real_t res, mincut; - igraph_integer_t n, v1 = -1, v2 = -1; + igraph_int_t n, v1 = -1, v2 = -1; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &v1_o, &v2_o, &capacity_object)) @@ -12265,7 +12265,7 @@ PyObject *igraphmodule_Graph_mincut(igraphmodule_GraphObject * self, igraph_vector_t capacity_vector; igraph_real_t value; igraph_vector_int_t partition, partition2, cut; - igraph_integer_t source = -1, target = -1; + igraph_int_t source = -1, target = -1; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &source_o, &target_o, &capacity_object)) @@ -12406,7 +12406,7 @@ PyObject *igraphmodule_Graph_st_mincut(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "source", "target", "capacity", NULL }; - igraph_integer_t source, target; + igraph_int_t source, target; PyObject *cut_o, *part_o, *part2_o, *result_o; PyObject *source_o, *target_o, *capacity_o = Py_None; igraph_vector_t capacity_vector; @@ -12895,7 +12895,7 @@ PyObject *igraphmodule_Graph_maximal_cliques(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_clique_number(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { - igraph_integer_t i; + igraph_int_t i; if (igraph_clique_number(&self->g, &i)) { return igraphmodule_handle_igraph_error(); @@ -12915,7 +12915,7 @@ PyObject *igraphmodule_Graph_independent_vertex_sets(igraphmodule_GraphObject PyObject *list, *max_results_o = Py_None; Py_ssize_t min_size = 0, max_size = 0; igraph_vector_int_list_t res; - igraph_integer_t max_results = IGRAPH_UNLIMITED; + igraph_int_t max_results = IGRAPH_UNLIMITED; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnO", kwlist, &min_size, &max_size, &max_results_o)) @@ -13028,7 +13028,7 @@ PyObject *igraphmodule_Graph_maximal_independent_vertex_sets( PyObject *igraphmodule_Graph_independence_number( igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null) ) { - igraph_integer_t i; + igraph_int_t i; if (igraph_independence_number(&self->g, &i)) { return igraphmodule_handle_igraph_error(); @@ -13773,7 +13773,7 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, igraph_vector_t *edge_weights = NULL, *node_weights = NULL, *node_in_weights = NULL; igraph_vector_int_t *membership = NULL; igraph_bool_t start = true; - igraph_integer_t nb_clusters = 0; + igraph_int_t nb_clusters = 0; igraph_real_t quality = 0.0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOdOdOn", kwlist, @@ -14048,7 +14048,7 @@ PyObject *igraphmodule_Graph_random_walk(igraphmodule_GraphObject * self, static char *kwlist[] = { "start", "steps", "mode", "stuck", "weights", "return_type", NULL }; PyObject *start_o, *mode_o = Py_None, *stuck_o = Py_None, *weights_o = Py_None, *return_type_o = Py_None; - igraph_integer_t start; + igraph_int_t start; Py_ssize_t steps = 10; igraph_neimode_t mode = IGRAPH_OUT; igraph_random_walk_stuck_t stuck = IGRAPH_RANDOM_WALK_STUCK_RETURN; diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 7c4a09191..cd57da92a 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -669,7 +669,7 @@ PyObject* igraphmodule_split_join_distance(PyObject *self, static char* kwlist[] = { "comm1", "comm2", NULL }; PyObject *comm1_o, *comm2_o; igraph_vector_int_t comm1, comm2; - igraph_integer_t distance12, distance21; + igraph_int_t distance12, distance21; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", kwlist, &comm1_o, &comm2_o)) @@ -694,7 +694,7 @@ PyObject* igraphmodule_split_join_distance(PyObject *self, igraph_vector_int_destroy(&comm1); igraph_vector_int_destroy(&comm2); - /* sizeof(Py_ssize_t) is most likely the same as sizeof(igraph_integer_t), + /* sizeof(Py_ssize_t) is most likely the same as sizeof(igraph_int_t), * but even if it isn't, we cast explicitly so we are safe */ return Py_BuildValue("nn", (Py_ssize_t)distance12, (Py_ssize_t)distance21); } @@ -1070,7 +1070,7 @@ PyObject* PyInit__igraph(void) igraph_error_t retval; /* Prevent linking 64-bit igraph to 32-bit Python */ - PY_IGRAPH_ASSERT_AT_BUILD_TIME(sizeof(igraph_integer_t) >= sizeof(Py_ssize_t)); + PY_IGRAPH_ASSERT_AT_BUILD_TIME(sizeof(igraph_int_t) >= sizeof(Py_ssize_t)); /* Check if the module is already initialized (possibly in another Python * interpreter. If so, bail out as we don't support this. */ diff --git a/src/_igraph/indexing.c b/src/_igraph/indexing.c index f5f30884b..8da3f600e 100644 --- a/src/_igraph/indexing.c +++ b/src/_igraph/indexing.c @@ -31,8 +31,8 @@ /***************************************************************************/ static PyObject* igraphmodule_i_Graph_adjmatrix_indexing_get_value_for_vertex_pair( - igraph_t* graph, igraph_integer_t from, igraph_integer_t to, PyObject* values) { - igraph_integer_t eid; + igraph_t* graph, igraph_int_t from, igraph_int_t to, PyObject* values) { + igraph_int_t eid; PyObject* result; /* Retrieving a single edge */ @@ -53,14 +53,14 @@ static PyObject* igraphmodule_i_Graph_adjmatrix_indexing_get_value_for_vertex_pa } static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, - igraph_integer_t from, igraph_vs_t* to, igraph_neimode_t neimode, + igraph_int_t from, igraph_vs_t* to, igraph_neimode_t neimode, PyObject* values); PyObject* igraphmodule_Graph_adjmatrix_get_index(igraph_t* graph, PyObject* row_index, PyObject* column_index, PyObject* attr_name) { PyObject *result = 0, *values; igraph_vs_t vs1, vs2; - igraph_integer_t vid1 = -1, vid2 = -1; + igraph_int_t vid1 = -1, vid2 = -1; char* attr; if (igraphmodule_PyObject_to_vs_t(row_index, &vs1, graph, 0, &vid1)) @@ -131,10 +131,10 @@ PyObject* igraphmodule_Graph_adjmatrix_get_index(igraph_t* graph, } static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, - igraph_integer_t from, igraph_vs_t* to, igraph_neimode_t neimode, + igraph_int_t from, igraph_vs_t* to, igraph_neimode_t neimode, PyObject* values) { igraph_vector_int_t eids; - igraph_integer_t eid, i, n, v; + igraph_int_t eid, i, n, v; igraph_vit_t vit; PyObject *result = 0, *item; @@ -263,12 +263,12 @@ void igraphmodule_i_Graph_adjmatrix_set_index_data_destroy( } static int igraphmodule_i_Graph_adjmatrix_set_index_row(igraph_t* graph, - igraph_integer_t from, igraph_vs_t* to, igraph_neimode_t neimode, + igraph_int_t from, igraph_vs_t* to, igraph_neimode_t neimode, PyObject* values, PyObject* new_value, igraphmodule_i_Graph_adjmatrix_set_index_data_t* data) { PyObject *iter = 0, *item; igraph_vit_t vit; - igraph_integer_t v, v1, v2, eid; + igraph_int_t v, v1, v2, eid; igraph_bool_t deleting, ok = true; /* Check whether new_value is an iterable (and not a string). If not, @@ -423,7 +423,7 @@ int igraphmodule_Graph_adjmatrix_set_index(igraph_t* graph, PyObject *values; igraph_vs_t vs1, vs2; igraph_vit_t vit; - igraph_integer_t vid1 = -1, vid2 = -1, eid = -1; + igraph_int_t vid1 = -1, vid2 = -1, eid = -1; igraph_bool_t ok = true; igraphmodule_i_Graph_adjmatrix_set_index_data_t data; char* attr; diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index 5905a50a7..0c1ad31e9 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -54,7 +54,7 @@ int igraphmodule_Vertex_Check(PyObject* obj) { * exception and returns zero if the vertex object is invalid. */ int igraphmodule_Vertex_Validate(PyObject* obj) { - igraph_integer_t n; + igraph_int_t n; igraphmodule_VertexObject *self; igraphmodule_GraphObject *graph; @@ -98,7 +98,7 @@ int igraphmodule_Vertex_Validate(PyObject* obj) { * changes, your existing vertex objects will point to elsewhere * (or they might even get invalidated). */ -PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_integer_t idx) { +PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_int_t idx) { return PyObject_CallFunction((PyObject*) igraphmodule_VertexType, "On", gref, (Py_ssize_t) idx); } @@ -110,7 +110,7 @@ PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_integer static int igraphmodule_Vertex_init(igraphmodule_EdgeObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "graph", "vid", NULL }; PyObject *g, *index_o = Py_None; - igraph_integer_t vid; + igraph_int_t vid; igraph_t *graph; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|O", kwlist, @@ -531,7 +531,7 @@ int igraphmodule_Vertex_set_attribute(igraphmodule_VertexObject* self, PyObject* /* result is NULL, check whether there was an error */ if (!PyErr_Occurred()) { /* no, there wasn't, so we must simply add the attribute */ - igraph_integer_t i, n = igraph_vcount(&o->g); + igraph_int_t i, n = igraph_vcount(&o->g); result = PyList_New(n); for (i = 0; i < n; i++) { if (i != self->idx) { @@ -572,9 +572,9 @@ PyObject* igraphmodule_Vertex_get_index(igraphmodule_VertexObject* self, void* c /** * \ingroup python_interface_vertex - * Returns the vertex index as an igraph_integer_t + * Returns the vertex index as an igraph_int_t */ -igraph_integer_t igraphmodule_Vertex_get_index_igraph_integer(igraphmodule_VertexObject* self) { +igraph_int_t igraphmodule_Vertex_get_index_igraph_integer(igraphmodule_VertexObject* self) { return self->idx; } @@ -618,7 +618,7 @@ static PyObject* _convert_to_edge_list(igraphmodule_VertexObject* vertex, PyObje for (i = 0; i < n; i++) { PyObject* idx = PyList_GetItem(obj, i); PyObject* edge; - igraph_integer_t idx_int; + igraph_int_t idx_int; if (!idx) { return NULL; @@ -663,7 +663,7 @@ static PyObject* _convert_to_vertex_list(igraphmodule_VertexObject* vertex, PyOb for (i = 0; i < n; i++) { PyObject* idx = PyList_GetItem(obj, i); PyObject* v; - igraph_integer_t idx_int; + igraph_int_t idx_int; if (!idx) { return NULL; diff --git a/src/_igraph/vertexobject.h b/src/_igraph/vertexobject.h index ef8ce9e7b..21f1be9b7 100644 --- a/src/_igraph/vertexobject.h +++ b/src/_igraph/vertexobject.h @@ -35,7 +35,7 @@ typedef struct { PyObject_HEAD igraphmodule_GraphObject* gref; - igraph_integer_t idx; + igraph_int_t idx; long hash; } igraphmodule_VertexObject; @@ -44,8 +44,8 @@ extern PyTypeObject* igraphmodule_VertexType; int igraphmodule_Vertex_register_type(void); int igraphmodule_Vertex_Check(PyObject* obj); -PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_integer_t idx); -igraph_integer_t igraphmodule_Vertex_get_index_igraph_integer(igraphmodule_VertexObject* self); +PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_int_t idx); +igraph_int_t igraphmodule_Vertex_get_index_igraph_integer(igraphmodule_VertexObject* self); PyObject* igraphmodule_Vertex_update_attributes(PyObject* self, PyObject* args, PyObject* kwds); #endif diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index d511cd1d0..4e4f66e59 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -106,7 +106,7 @@ int igraphmodule_VertexSeq_init(igraphmodule_VertexSeqObject *self, igraph_vs_all(&vs); } else if (PyLong_Check(vsobj)) { /* We selected a single vertex */ - igraph_integer_t idx; + igraph_int_t idx; if (igraphmodule_PyObject_to_integer_t(vsobj, &idx)) { return -1; @@ -120,7 +120,7 @@ int igraphmodule_VertexSeq_init(igraphmodule_VertexSeqObject *self, igraph_vs_1(&vs, idx); } else { igraph_vector_int_t v; - igraph_integer_t n = igraph_vcount(&((igraphmodule_GraphObject*)g)->g); + igraph_int_t n = igraph_vcount(&((igraphmodule_GraphObject*)g)->g); if (igraphmodule_PyObject_to_vector_int_t(vsobj, &v)) return -1; if (!igraph_vector_int_isininterval(&v, 0, n-1)) { @@ -168,7 +168,7 @@ void igraphmodule_VertexSeq_dealloc(igraphmodule_VertexSeqObject* self) { */ Py_ssize_t igraphmodule_VertexSeq_sq_length(igraphmodule_VertexSeqObject* self) { igraph_t *g; - igraph_integer_t result; + igraph_int_t result; if (!self->gref) { return -1; @@ -190,7 +190,7 @@ Py_ssize_t igraphmodule_VertexSeq_sq_length(igraphmodule_VertexSeqObject* self) PyObject* igraphmodule_VertexSeq_sq_item(igraphmodule_VertexSeqObject* self, Py_ssize_t i) { igraph_t *g; - igraph_integer_t idx = -1; + igraph_int_t idx = -1; if (!self->gref) { return NULL; @@ -260,7 +260,7 @@ PyObject* igraphmodule_VertexSeq_attribute_names(igraphmodule_VertexSeqObject* s PyObject* igraphmodule_VertexSeq_get_attribute_values(igraphmodule_VertexSeqObject* self, PyObject* o) { igraphmodule_GraphObject *gr = self->gref; PyObject *result=0, *values, *item; - igraph_integer_t i, n; + igraph_int_t i, n; if (!igraphmodule_attribute_name_check(o)) return 0; @@ -409,7 +409,7 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb PyObject *dict, *list, *item; igraphmodule_GraphObject *gr; igraph_vector_int_t vs; - igraph_integer_t i, j, n, no_of_nodes; + igraph_int_t i, j, n, no_of_nodes; gr = self->gref; dict = ATTR_STRUCT_DICT(&gr->g)[ATTRHASH_IDX_VERTEX]; @@ -540,7 +540,7 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb /* We don't have attributes with the given name yet. Create an entry * in the dict, create a new list, fill with None for vertices not in the * sequence and copy the rest */ - igraph_integer_t n2 = igraph_vcount(&gr->g); + igraph_int_t n2 = igraph_vcount(&gr->g); list = PyList_New(n2); if (list == 0) { igraph_vector_int_destroy(&vs); @@ -605,7 +605,7 @@ PyObject* igraphmodule_VertexSeq_set_attribute_values(igraphmodule_VertexSeqObje */ PyObject* igraphmodule_VertexSeq_find(igraphmodule_VertexSeqObject *self, PyObject *args) { PyObject *item; - igraph_integer_t i; + igraph_int_t i; Py_ssize_t n; igraph_vit_t vit; @@ -683,7 +683,7 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, PyObject *args) { igraphmodule_VertexSeqObject *result; igraphmodule_GraphObject *gr; - igraph_integer_t igraph_idx, i, j, n, m; + igraph_int_t igraph_idx, i, j, n, m; igraph_bool_t working_on_whole_graph = igraph_vs_is_all(&self->vs); igraph_vector_int_t v, v2; @@ -787,7 +787,7 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, for (; i < n; i++) { PyObject *item2 = PyTuple_GetItem(args, i); - igraph_integer_t idx; + igraph_int_t idx; if (item2 == 0) { Py_DECREF(result); igraph_vector_int_destroy(&v); diff --git a/src/igraph/utils.py b/src/igraph/utils.py index 2116332ae..867d68870 100644 --- a/src/igraph/utils.py +++ b/src/igraph/utils.py @@ -75,7 +75,7 @@ def numpy_to_contiguous_memoryview(obj): dtype = int32 else: raise TypeError( - f"size of igraph_integer_t in the C layer ({INTEGER_SIZE} bits) is not supported" + f"size of igraph_int_t in the C layer ({INTEGER_SIZE} bits) is not supported" ) return memoryview(require(obj, dtype=dtype, requirements="AC")) diff --git a/vendor/source/igraph b/vendor/source/igraph index fa546047d..b9b573902 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit fa546047defd4d8f369fc7eb28a855ad5c430c63 +Subproject commit b9b573902ccbe393a78252ab5e94c7876ed92597 From f6e6bce784e478e4b105bf085a157f726a6ef5b5 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 19 Sep 2025 23:41:38 +0200 Subject: [PATCH 269/276] fix: fix signature of Graph.simple_cycles(), closes #856 --- src/_igraph/graphobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 8be4bcbdb..b6c14a340 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -17067,7 +17067,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { }, {"simple_cycles", (PyCFunction) igraphmodule_Graph_simple_cycles, METH_VARARGS | METH_KEYWORDS, - "simple_cycles(mode=None, min=-1, max=-1, output=\"epath\")\n--\n\n" + "simple_cycles(mode=None, min=-1, max=-1, output=\"vpath\")\n--\n\n" "Finds simple cycles in a graph\n\n" "@param mode: for directed graphs, specifies how the edge directions\n" " should be taken into account. C{\"all\"} means that the edge directions\n" From 6396ccbcecc3f745ae468a8dbd9994dfb50a126f Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 24 Sep 2025 12:03:58 +1000 Subject: [PATCH 270/276] Make gallery example with iplotx --- doc/examples_sphinx-gallery/plot_iplotx.py | 63 ++++++++++++++++++++++ doc/source/requirements.txt | 1 + 2 files changed, 64 insertions(+) create mode 100644 doc/examples_sphinx-gallery/plot_iplotx.py diff --git a/doc/examples_sphinx-gallery/plot_iplotx.py b/doc/examples_sphinx-gallery/plot_iplotx.py new file mode 100644 index 000000000..5c3c12c43 --- /dev/null +++ b/doc/examples_sphinx-gallery/plot_iplotx.py @@ -0,0 +1,63 @@ +""" +.. _tutorials-iplotx: + +============================== +Visualising graphs with iplotx +============================== +``iplotx`` (https://iplotx.readthedocs.io) is a library for visualisation of graphs/networks +with direct compatibility with both igraph and NetworkX. It uses ``matplotlib`` behind the +scenes so the results are compatible with the current igraph matplotlib backend and many +additional chart types (e.g. bar charts, annotations). + +Compared to the standard visualisations shipped with igraph, ``iplotx`` offers: + +- More styling options +- More consistent behaviour across DPI resolutions and backends +- More consistent matplotlib artists for plot editing and animation + +""" + +import igraph as ig +import iplotx as ipx + +# Construct a graph with 5 vertices +n_vertices = 5 +edges = [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (3, 4)] +g = ig.Graph(n_vertices, edges) + +# Set attributes for the graph, nodes, and edges +g["title"] = "Small Social Network" +g.vs["name"] = [ + "Daniel Morillas", + "Kathy Archer", + "Kyle Ding", + "Joshua Walton", + "Jana Hoyer", +] +g.vs["gender"] = ["M", "F", "F", "M", "F"] +g.es["married"] = [False, False, False, False, False, False, False, True] + +# Set individual attributes +g.vs[1]["name"] = "Kathy Morillas" +g.es[0]["married"] = True + +# Plot using iplotx +ipx.network( + g, + layout="circle", # print nodes in a circular layout + vertex_marker="s", + vertex_size=45, + vertex_linewidth=2, + vertex_facecolor=[ + "lightblue" if gender == "M" else "deeppink" for gender in g.vs["gender"] + ], + vertex_label_color=[ + "black" if gender == "M" else "white" for gender in g.vs["gender"] + ], + vertex_edgecolor="black", + vertex_labels=[name.replace(" ", "\n") for name in g.vs["name"]], + edge_linewidth=[2 if married else 1 for married in g.es["married"]], + edge_color=["#7142cf" if married else "#AAA" for married in g.es["married"]], + edge_padding=3, + aspect=1.0, +) diff --git a/doc/source/requirements.txt b/doc/source/requirements.txt index 9044d840b..ef825d09f 100644 --- a/doc/source/requirements.txt +++ b/doc/source/requirements.txt @@ -6,6 +6,7 @@ sphinx==7.4.7 sphinx-gallery>=0.14.0 sphinx-rtd-theme>=1.3.0 pydoctor>=23.4.0 +iplotx>=0.6.8 numpy scipy From 44b5dda9e942ba82bb63c3be86e11e1fa7d6d67c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:06:37 +0000 Subject: [PATCH 271/276] build(deps): bump pypa/cibuildwheel from 3.0.1 to 3.2.0 Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 3.0.1 to 3.2.0. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v3.0.1...v3.2.0) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-version: 3.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d9d6d4a3..b311e0df6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,14 +19,14 @@ jobs: fetch-depth: 0 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v3.0.1 + uses: pypa/cibuildwheel@v3.2.0 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_x86_64" CIBW_ENABLE: pypy - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v3.0.1 + uses: pypa/cibuildwheel@v3.2.0 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_x86_64" @@ -47,7 +47,7 @@ jobs: fetch-depth: 0 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v3.0.1 + uses: pypa/cibuildwheel@v3.2.0 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -69,7 +69,7 @@ jobs: fetch-depth: 0 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v3.0.1 + uses: pypa/cibuildwheel@v3.2.0 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -132,7 +132,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v3.0.1 + uses: pypa/cibuildwheel@v3.2.0 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" @@ -238,7 +238,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v3.0.1 + uses: pypa/cibuildwheel@v3.2.0 env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From b851c776b49a37e30e8733d54d9fba7424ab0c1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Tue, 30 Sep 2025 20:59:22 +0200 Subject: [PATCH 272/276] fix: exclude free-threaded Python 3.14 builds from CI --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b311e0df6..d5874bd3c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,8 @@ on: [push, pull_request] env: CIBW_ENVIRONMENT_PASS_LINUX: PYTEST_TIMEOUT CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test]' && python -m pytest -v tests" - CIBW_SKIP: "cp38-* pp38-*" + # Free-threaded builds excluded for Python 3.14 because they do not support the limited API + CIBW_SKIP: "cp38-* pp38-* cp314t-*" PYTEST_TIMEOUT: 60 jobs: From 2848d2a02aaef3ffcbd8672057982315fb3f3b95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Wed, 1 Oct 2025 14:07:45 +0200 Subject: [PATCH 273/276] ci: skip Win32 build on CPython 3.14 because neither SciPy nor pandas have wheels for that version --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d5874bd3c..968811ad1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -247,7 +247,7 @@ jobs: CIBW_TEST_COMMAND: 'cd /d {project} && pip install --prefer-binary ".[${{ matrix.test_extra }}]" && python -m pytest tests' # Skip tests for Python 3.10 onwards because SciPy does not have # 32-bit wheels for Windows any more - CIBW_TEST_SKIP: "cp310-win32 cp311-win32 cp312-win32 cp313-win32" + CIBW_TEST_SKIP: "cp310-win32 cp311-win32 cp312-win32 cp313-win32 cp314-win32" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVCPKG_TARGET_TRIPLET=${{ matrix.vcpkg_arch }}-windows-static-md -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake -A ${{ matrix.cmake_arch }} IGRAPH_EXTRA_LIBRARY_PATH: C:/vcpkg/installed/${{ matrix.vcpkg_arch }}-windows-static-md/lib/ IGRAPH_STATIC_EXTENSION: True From 65df5da9ef6d5282b3f6871b963d9361fe345ac8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 13:24:49 +0000 Subject: [PATCH 274/276] build(deps): bump pypa/cibuildwheel from 3.2.0 to 3.2.1 Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 3.2.0 to 3.2.1. - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v3.2.0...v3.2.1) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-version: 3.2.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 968811ad1..ddbfdca10 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,14 +20,14 @@ jobs: fetch-depth: 0 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v3.2.0 + uses: pypa/cibuildwheel@v3.2.1 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_x86_64" CIBW_ENABLE: pypy - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v3.2.0 + uses: pypa/cibuildwheel@v3.2.1 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_x86_64" @@ -48,7 +48,7 @@ jobs: fetch-depth: 0 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v3.2.0 + uses: pypa/cibuildwheel@v3.2.1 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -70,7 +70,7 @@ jobs: fetch-depth: 0 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v3.2.0 + uses: pypa/cibuildwheel@v3.2.1 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -133,7 +133,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v3.2.0 + uses: pypa/cibuildwheel@v3.2.1 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" @@ -239,7 +239,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v3.2.0 + uses: pypa/cibuildwheel@v3.2.1 env: CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From 8c55e236330c14249139843bc10eff95824b0400 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 23 Oct 2025 13:38:15 +0200 Subject: [PATCH 275/276] doc: install iplotx when building documentation --- scripts/mkdoc.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index 1d94d3c32..9309987f6 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -54,18 +54,18 @@ if [ ! -d ".venv" ]; then echo "Creating virtualenv..." ${PYTHON:-python3} -m venv .venv - # Install sphinx, matplotlib, pandas, scipy, wheel and pydoctor into the venv. + # Install documentation dependencies into the venv. # doc2dash is optional; it will be installed when -d is given - .venv/bin/pip install -q -U pip wheel sphinx==7.4.7 matplotlib pandas scipy pydoctor sphinx-rtd-theme + .venv/bin/pip install -q -U pip wheel sphinx==7.4.7 matplotlib pandas scipy pydoctor sphinx-rtd-theme iplotx else # Upgrade pip in the virtualenv echo "Upgrading pip in virtualenv..." .venv/bin/pip install -q -U pip wheel fi -# Make sure that Sphinx, PyDoctor (and maybe doc2dash) are up-to-date in the virtualenv +# Make sure that documentation dependencies are up-to-date in the virtualenv echo "Making sure that all dependencies are up-to-date..." -.venv/bin/pip install -q -U sphinx==7.4.7 pydoctor sphinx-gallery sphinxcontrib-jquery sphinx-rtd-theme +.venv/bin/pip install -q -U sphinx==7.4.7 pydoctor sphinx-gallery sphinxcontrib-jquery sphinx-rtd-theme iplotx if [ x$DOC2DASH = x1 ]; then .venv/bin/pip install -U doc2dash fi From b16f27618674dd1913007a52855b76075802cbf9 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 23 Oct 2025 13:38:34 +0200 Subject: [PATCH 276/276] chore: updated changelog, bumped version to 1.0.0 --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++-- src/igraph/version.py | 2 +- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 387957cd3..6b0c3a2b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,32 @@ # igraph Python interface changelog -## [main] +## [1.0.0] - 2025-10-23 + +### Added + +- Added `Graph.Nearest_Neighbor_Graph()`. + +- Added `node_in_weights` argument to `Graph.community_leiden()`. + +- Added `align_layout()` to align the principal axes of a layout nicely + with screen dimensions. + +- Added `Graph.commnity_voronoi()`. + +- Added `Graph.commnity_fluid_communities()`. + +### Changed + +- The C core of igraph was updated to version 1.0.0. + +- Most layouts are now auto-aligned using `align_layout()`. + +### Miscellaneous + +- Documentation improvements. + +- This is the last version that supports Python 3.9 as it will reach its + end of life at the end of October 2025. ## [0.11.9] - 2025-06-11 @@ -727,7 +753,7 @@ Please refer to the commit logs at for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. -[main]: https://github.com/igraph/python-igraph/compare/0.11.9...main +[1.0.0]: https://github.com/igraph/python-igraph/compare/0.11.9...1.0.0 [0.11.9]: https://github.com/igraph/python-igraph/compare/0.11.8...0.11.9 [0.11.8]: https://github.com/igraph/python-igraph/compare/0.11.7...0.11.8 [0.11.7]: https://github.com/igraph/python-igraph/compare/0.11.6...0.11.7 diff --git a/src/igraph/version.py b/src/igraph/version.py index fbd1c8748..b69224ddc 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 11, 9) +__version_info__ = (1, 0, 0) __version__ = ".".join("{0}".format(x) for x in __version_info__)