Current section
Files
Jump to
Current section
Files
src/yog@centrality.erl
-module(yog@centrality).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/yog/centrality.gleam").
-export([degree/2, closeness/5, harmonic_centrality/5, betweenness/5, pagerank/2, eigenvector/3, katz/5, alpha_centrality/5, degree_total/1, closeness_int/1, closeness_float/1, harmonic_centrality_int/1, harmonic_centrality_float/1, betweenness_int/1, betweenness_float/1, default_pagerank_options/0]).
-export_type([degree_mode/0, page_rank_options/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Centrality measures for identifying important nodes in graphs.\n"
"\n"
" Provides degree, closeness, harmonic, betweenness, and PageRank centrality.\n"
" All functions return a Dict(NodeId, Float) mapping nodes to their scores.\n"
"\n"
" ## Overview\n"
"\n"
" | Measure | Function | Best For |\n"
" |---------|----------|----------|\n"
" | Degree | `degree/2` | Local connectivity |\n"
" | Closeness | `closeness/5` | Distance to all others |\n"
" | Harmonic | `harmonic_centrality/5` | Disconnected graphs |\n"
" | Betweenness | `betweenness/5` | Bridge/gatekeeper detection |\n"
" | PageRank | `pagerank/2` | Link-quality importance |\n"
).
-type degree_mode() :: in_degree | out_degree | total_degree.
-type page_rank_options() :: {page_rank_options, float(), integer(), float()}.
-file("src/yog/centrality.gleam", 45).
?DOC(
" Calculates the Degree Centrality for all nodes in the graph.\n"
" \n"
" For directed graphs, use `mode` to specify which edges to count.\n"
" For undirected graphs, the `mode` is ignored.\n"
).
-spec degree(yog@model:graph(any(), any()), degree_mode()) -> gleam@dict:dict(integer(), float()).
degree(Graph, Mode) ->
N = yog@model:order(Graph),
Nodes = yog@model:all_nodes(Graph),
Factor = case N > 1 of
true ->
erlang:float(N - 1);
false ->
1.0
end,
gleam@list:fold(
Nodes,
maps:new(),
fun(Acc, Id) ->
Count = case erlang:element(2, Graph) of
undirected ->
erlang:length(yog@model:neighbors(Graph, Id));
directed ->
case Mode of
in_degree ->
erlang:length(yog@model:predecessors(Graph, Id));
out_degree ->
erlang:length(yog@model:successors(Graph, Id));
total_degree ->
erlang:length(yog@model:successors(Graph, Id)) + erlang:length(
yog@model:predecessors(Graph, Id)
)
end
end,
gleam@dict:insert(Acc, Id, case Factor of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> erlang:float(Count) / Gleam@denominator
end)
end
).
-file("src/yog/centrality.gleam", 103).
?DOC(
" Calculates Closeness Centrality for all nodes.\n"
"\n"
" Closeness centrality measures how close a node is to all other nodes\n"
" in the graph. It is calculated as the reciprocal of the sum of the\n"
" shortest path distances from the node to all other nodes.\n"
"\n"
" Formula: C(v) = (n - 1) / Σ d(v, u) for all u ≠ v\n"
"\n"
" Note: In disconnected graphs, nodes that cannot reach all other nodes\n"
" will have a centrality of 0.0. Consider harmonic_centrality for \n"
" disconnected graphs.\n"
"\n"
" **Time Complexity:** O(V * (V + E) log V) using Dijkstra from each node\n"
"\n"
" ## Parameters\n"
"\n"
" - `zero`: The identity element for distances (e.g., 0 for integers)\n"
" - `add`: Function to add two distances\n"
" - `compare`: Function to compare two distances\n"
" - `to_float`: Function to convert distance type to Float for final score\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" centrality.closeness(\n"
" graph,\n"
" zero: 0,\n"
" add: int.add,\n"
" compare: int.compare,\n"
" to_float: int.to_float,\n"
" )\n"
" // => dict.from_list([#(1, 0.666), #(2, 1.0), #(3, 0.666)])\n"
" ```\n"
).
-spec closeness(
yog@model:graph(any(), KDG),
KDG,
fun((KDG, KDG) -> KDG),
fun((KDG, KDG) -> gleam@order:order()),
fun((KDG) -> float())
) -> gleam@dict:dict(integer(), float()).
closeness(Graph, Zero, Add, Compare, To_float) ->
Nodes = yog@model:all_nodes(Graph),
N = erlang:length(Nodes),
case N =< 1 of
true ->
gleam@list:fold(
Nodes,
maps:new(),
fun(Acc, Id) -> gleam@dict:insert(Acc, Id, +0.0) end
);
false ->
gleam@list:fold(
Nodes,
maps:new(),
fun(Acc@1, Source) ->
Distances = yog@pathfinding@dijkstra:single_source_distances(
Graph,
Source,
Zero,
Add,
Compare
),
case maps:size(Distances) =:= N of
false ->
gleam@dict:insert(Acc@1, Source, +0.0);
true ->
Total_distance = gleam@dict:fold(
Distances,
Zero,
fun(Sum, _, Dist) -> Add(Sum, Dist) end
),
Centrality_score = case To_float(Total_distance) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> erlang:float(N - 1) / Gleam@denominator
end,
gleam@dict:insert(Acc@1, Source, Centrality_score)
end
end
)
end.
-file("src/yog/centrality.gleam", 152).
?DOC(
" Calculates Harmonic Centrality for all nodes.\n"
"\n"
" Harmonic centrality is a variation of closeness centrality that handles\n"
" disconnected graphs gracefully. It sums the reciprocals of the shortest \n"
" path distances from a node to all other reachable nodes.\n"
"\n"
" Formula: H(v) = Σ (1 / d(v, u)) / (n - 1) for all u ≠ v\n"
"\n"
" **Time Complexity:** O(V * (V + E) log V)\n"
).
-spec harmonic_centrality(
yog@model:graph(any(), KDK),
KDK,
fun((KDK, KDK) -> KDK),
fun((KDK, KDK) -> gleam@order:order()),
fun((KDK) -> float())
) -> gleam@dict:dict(integer(), float()).
harmonic_centrality(Graph, Zero, Add, Compare, To_float) ->
Nodes = yog@model:all_nodes(Graph),
N = erlang:length(Nodes),
case N =< 1 of
true ->
gleam@list:fold(
Nodes,
maps:new(),
fun(Acc, Id) -> gleam@dict:insert(Acc, Id, +0.0) end
);
false ->
Denominator = erlang:float(N - 1),
gleam@list:fold(
Nodes,
maps:new(),
fun(Acc@1, Source) ->
Distances = yog@pathfinding@dijkstra:single_source_distances(
Graph,
Source,
Zero,
Add,
Compare
),
Sum_of_reciprocals = gleam@dict:fold(
Distances,
+0.0,
fun(Sum, Node, Dist) -> case Node =:= Source of
true ->
Sum;
false ->
D = To_float(Dist),
case D > +0.0 of
true ->
Sum + (case D of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> 1.0 / Gleam@denominator
end);
false ->
Sum
end
end end
),
gleam@dict:insert(Acc@1, Source, case Denominator of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@1 -> Sum_of_reciprocals / Gleam@denominator@1
end)
end
)
end.
-file("src/yog/centrality.gleam", 329).
-spec get_sigma(gleam@dict:dict(integer(), integer()), integer()) -> integer().
get_sigma(Sigmas, Id) ->
_pipe = gleam_stdlib:map_get(Sigmas, Id),
gleam@result:unwrap(_pipe, 0).
-file("src/yog/centrality.gleam", 243).
-spec do_brandes_dijkstra(
yog@model:graph(any(), KDW),
gleamy@pairing_heap:heap({KDW, integer()}),
gleam@dict:dict(integer(), KDW),
gleam@dict:dict(integer(), integer()),
gleam@dict:dict(integer(), list(integer())),
list(integer()),
fun((KDW, KDW) -> KDW),
fun((KDW, KDW) -> gleam@order:order())
) -> {list(integer()),
gleam@dict:dict(integer(), list(integer())),
gleam@dict:dict(integer(), integer())}.
do_brandes_dijkstra(Graph, Queue, Dists, Sigmas, Preds, Stack, Add, Compare) ->
case gleamy@priority_queue:pop(Queue) of
{error, nil} ->
{Stack, Preds, Sigmas};
{ok, {{D_v, V}, Rest_q}} ->
Current_best = begin
_pipe = gleam_stdlib:map_get(Dists, V),
gleam@result:unwrap(_pipe, D_v)
end,
case Compare(D_v, Current_best) of
gt ->
do_brandes_dijkstra(
Graph,
Rest_q,
Dists,
Sigmas,
Preds,
Stack,
Add,
Compare
);
_ ->
New_stack = [V | Stack],
{Next_q, Next_dists, Next_sigmas, Next_preds} = begin
_pipe@1 = yog@model:successors(Graph, V),
gleam@list:fold(
_pipe@1,
{Rest_q, Dists, Sigmas, Preds},
fun(State, Edge) ->
{Q, Ds, Ss, Ps} = State,
{W, Weight} = Edge,
New_dist = Add(D_v, Weight),
case gleam_stdlib:map_get(Ds, W) of
{error, nil} ->
Q2 = gleamy@priority_queue:push(
Q,
{New_dist, W}
),
Ds2 = gleam@dict:insert(Ds, W, New_dist),
Ss2 = gleam@dict:insert(
Ss,
W,
get_sigma(Ss, V)
),
Ps2 = gleam@dict:insert(Ps, W, [V]),
{Q2, Ds2, Ss2, Ps2};
{ok, Old_dist} ->
case Compare(New_dist, Old_dist) of
lt ->
Q2@1 = gleamy@priority_queue:push(
Q,
{New_dist, W}
),
Ds2@1 = gleam@dict:insert(
Ds,
W,
New_dist
),
Ss2@1 = gleam@dict:insert(
Ss,
W,
get_sigma(Ss, V)
),
Ps2@1 = gleam@dict:insert(
Ps,
W,
[V]
),
{Q2@1, Ds2@1, Ss2@1, Ps2@1};
eq ->
Ss2@2 = gleam@dict:upsert(
Ss,
W,
fun(Curr) ->
gleam@option:unwrap(
Curr,
0
)
+ get_sigma(Ss, V)
end
),
Ps2@2 = gleam@dict:upsert(
Ps,
W,
fun(Curr@1) ->
[V |
gleam@option:unwrap(
Curr@1,
[]
)]
end
),
{Q, Ds, Ss2@2, Ps2@2};
gt ->
State
end
end
end
)
end,
do_brandes_dijkstra(
Graph,
Next_q,
Next_dists,
Next_sigmas,
Next_preds,
New_stack,
Add,
Compare
)
end
end.
-file("src/yog/centrality.gleam", 224).
-spec run_discovery(
yog@model:graph(any(), KDS),
integer(),
KDS,
fun((KDS, KDS) -> KDS),
fun((KDS, KDS) -> gleam@order:order())
) -> {list(integer()),
gleam@dict:dict(integer(), list(integer())),
gleam@dict:dict(integer(), integer())}.
run_discovery(Graph, Source, Zero, Add, Compare) ->
Queue = begin
_pipe = gleamy@priority_queue:new(
fun(A, B) -> Compare(erlang:element(1, A), erlang:element(1, B)) end
),
gleamy@priority_queue:push(_pipe, {Zero, Source})
end,
Dists = maps:from_list([{Source, Zero}]),
Sigmas = maps:from_list([{Source, 1}]),
Preds = maps:new(),
Stack = [],
do_brandes_dijkstra(Graph, Queue, Dists, Sigmas, Preds, Stack, Add, Compare).
-file("src/yog/centrality.gleam", 338).
-spec accumulate_dependencies(
list(integer()),
gleam@dict:dict(integer(), list(integer())),
gleam@dict:dict(integer(), integer())
) -> gleam@dict:dict(integer(), float()).
accumulate_dependencies(Stack, Preds, Sigmas) ->
Initial_deltas = maps:new(),
gleam@list:fold(
Stack,
Initial_deltas,
fun(Deltas, V) ->
Sigma_v = erlang:float(get_sigma(Sigmas, V)),
Delta_v = begin
_pipe = gleam_stdlib:map_get(Deltas, V),
gleam@result:unwrap(_pipe, +0.0)
end,
V_preds = begin
_pipe@1 = gleam_stdlib:map_get(Preds, V),
gleam@result:unwrap(_pipe@1, [])
end,
gleam@list:fold(
V_preds,
Deltas,
fun(Acc_deltas, U) ->
Sigma_u = erlang:float(get_sigma(Sigmas, U)),
Fraction = (case Sigma_v of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> Sigma_u / Gleam@denominator
end) * (1.0 + Delta_v),
gleam@dict:upsert(
Acc_deltas,
U,
fun(Curr) ->
gleam@option:unwrap(Curr, +0.0) + Fraction
end
)
end
)
end
).
-file("src/yog/centrality.gleam", 333).
-spec run_accumulation(
{list(integer()),
gleam@dict:dict(integer(), list(integer())),
gleam@dict:dict(integer(), integer())}
) -> gleam@dict:dict(integer(), float()).
run_accumulation(Discovery) ->
{Stack, Preds, Sigmas} = Discovery,
accumulate_dependencies(Stack, Preds, Sigmas).
-file("src/yog/centrality.gleam", 361).
-spec merge_scores(
gleam@dict:dict(integer(), float()),
gleam@dict:dict(integer(), float()),
integer()
) -> gleam@dict:dict(integer(), float()).
merge_scores(Acc, Dependencies, Source) ->
gleam@dict:fold(
Dependencies,
Acc,
fun(Acc2, Node, Delta) -> case Node =:= Source of
true ->
Acc2;
false ->
Current = begin
_pipe = gleam_stdlib:map_get(Acc2, Node),
gleam@result:unwrap(_pipe, +0.0)
end,
gleam@dict:insert(Acc2, Node, Current + Delta)
end end
).
-file("src/yog/centrality.gleam", 377).
-spec scale_all(gleam@dict:dict(integer(), float()), float()) -> gleam@dict:dict(integer(), float()).
scale_all(Scores, Factor) ->
gleam@dict:map_values(Scores, fun(_, Score) -> Score * Factor end).
-file("src/yog/centrality.gleam", 381).
-spec apply_undirected_scaling(
gleam@dict:dict(integer(), float()),
yog@model:graph_type()
) -> gleam@dict:dict(integer(), float()).
apply_undirected_scaling(Scores, Kind) ->
case Kind of
undirected ->
scale_all(Scores, 0.5);
directed ->
Scores
end.
-file("src/yog/centrality.gleam", 199).
?DOC(
" Calculates Betweenness Centrality for all nodes.\n"
" \n"
" Betweenness centrality of a node v is the sum of the fraction of \n"
" all-pairs shortest paths that pass through v.\n"
"\n"
" **Time Complexity:** O(VE) for unweighted, O(VE + V²logV) for weighted.\n"
).
-spec betweenness(
yog@model:graph(any(), KDO),
KDO,
fun((KDO, KDO) -> KDO),
fun((KDO, KDO) -> gleam@order:order()),
fun((KDO) -> float())
) -> gleam@dict:dict(integer(), float()).
betweenness(Graph, Zero, Add, Compare, _) ->
Nodes = yog@model:all_nodes(Graph),
Initial = gleam@list:fold(
Nodes,
maps:new(),
fun(Acc, Id) -> gleam@dict:insert(Acc, Id, +0.0) end
),
Scores = gleam@list:fold(
Nodes,
Initial,
fun(Acc@1, S) ->
Discovery = run_discovery(Graph, S, Zero, Add, Compare),
Dependencies = run_accumulation(Discovery),
merge_scores(Acc@1, Dependencies, S)
end
),
apply_undirected_scaling(Scores, erlang:element(2, Graph)).
-file("src/yog/centrality.gleam", 473).
-spec get_in_neighbors(yog@model:graph(any(), any()), integer()) -> list(integer()).
get_in_neighbors(Graph, Node) ->
case erlang:element(2, Graph) of
undirected ->
_pipe = yog@model:successors(Graph, Node),
gleam@list:map(_pipe, fun(Edge) -> erlang:element(1, Edge) end);
directed ->
_pipe@1 = yog@model:predecessors(Graph, Node),
gleam@list:map(
_pipe@1,
fun(Edge@1) -> erlang:element(1, Edge@1) end
)
end.
-file("src/yog/centrality.gleam", 486).
-spec get_out_degree(yog@model:graph(any(), any()), integer()) -> integer().
get_out_degree(Graph, Node) ->
case erlang:element(2, Graph) of
undirected ->
_pipe = yog@model:neighbors(Graph, Node),
erlang:length(_pipe);
directed ->
_pipe@1 = yog@model:successors(Graph, Node),
erlang:length(_pipe@1)
end.
-file("src/yog/centrality.gleam", 455).
-spec calculate_sink_sum(
yog@model:graph(any(), any()),
gleam@dict:dict(integer(), float()),
list(integer()),
float()
) -> float().
calculate_sink_sum(Graph, Ranks, Nodes, N_float) ->
gleam@list:fold(
Nodes,
+0.0,
fun(Sum, Node) ->
Out_degree = get_out_degree(Graph, Node),
case Out_degree =:= 0 of
true ->
Node_rank = begin
_pipe = gleam_stdlib:map_get(Ranks, Node),
gleam@result:unwrap(_pipe, +0.0)
end,
Sum + (case N_float of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> Node_rank / Gleam@denominator
end);
false ->
Sum
end
end
).
-file("src/yog/centrality.gleam", 499).
-spec calculate_l1_norm(
gleam@dict:dict(integer(), float()),
gleam@dict:dict(integer(), float()),
list(integer())
) -> float().
calculate_l1_norm(Old_ranks, New_ranks, Nodes) ->
gleam@list:fold(
Nodes,
+0.0,
fun(Sum, Node) ->
Old_val = begin
_pipe = gleam_stdlib:map_get(Old_ranks, Node),
gleam@result:unwrap(_pipe, +0.0)
end,
New_val = begin
_pipe@1 = gleam_stdlib:map_get(New_ranks, Node),
gleam@result:unwrap(_pipe@1, +0.0)
end,
Diff = case New_val > Old_val of
true ->
New_val - Old_val;
false ->
Old_val - New_val
end,
Sum + Diff
end
).
-file("src/yog/centrality.gleam", 408).
-spec iterate_pagerank(
yog@model:graph(any(), any()),
gleam@dict:dict(integer(), float()),
list(integer()),
integer(),
page_rank_options(),
integer()
) -> gleam@dict:dict(integer(), float()).
iterate_pagerank(Graph, Ranks, Nodes, N, Options, Iteration) ->
case Iteration >= erlang:element(3, Options) of
true ->
Ranks;
false ->
Damping = erlang:element(2, Options),
N_float = erlang:float(N),
Sink_sum = calculate_sink_sum(Graph, Ranks, Nodes, N_float),
New_ranks = gleam@list:fold(
Nodes,
maps:new(),
fun(Acc, Node) ->
In_neighbors = get_in_neighbors(Graph, Node),
Rank_sum = gleam@list:fold(
In_neighbors,
+0.0,
fun(Sum, Neighbor) ->
Neighbor_rank = begin
_pipe = gleam_stdlib:map_get(Ranks, Neighbor),
gleam@result:unwrap(_pipe, +0.0)
end,
Out_degree = get_out_degree(Graph, Neighbor),
case Out_degree > 0 of
true ->
Sum + (case erlang:float(Out_degree) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> Neighbor_rank / Gleam@denominator
end);
false ->
Sum
end
end
),
New_rank = (case N_float of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@1 -> (1.0 - Damping) / Gleam@denominator@1
end) + (Damping * (Sink_sum + Rank_sum)),
gleam@dict:insert(Acc, Node, New_rank)
end
),
L1_norm = calculate_l1_norm(Ranks, New_ranks, Nodes),
case L1_norm < erlang:element(4, Options) of
true ->
New_ranks;
false ->
iterate_pagerank(
Graph,
New_ranks,
Nodes,
N,
Options,
Iteration + 1
)
end
end.
-file("src/yog/centrality.gleam", 395).
-spec pagerank(yog@model:graph(any(), any()), page_rank_options()) -> gleam@dict:dict(integer(), float()).
pagerank(Graph, Options) ->
Nodes = yog@model:all_nodes(Graph),
N = erlang:length(Nodes),
Initial_rank = case erlang:float(N) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> 1.0 / Gleam@denominator
end,
Ranks = gleam@list:fold(
Nodes,
maps:new(),
fun(Acc, Id) -> gleam@dict:insert(Acc, Id, Initial_rank) end
),
iterate_pagerank(Graph, Ranks, Nodes, N, Options, 0).
-file("src/yog/centrality.gleam", 615).
-spec calculate_l2_norm(gleam@dict:dict(integer(), float()), list(integer())) -> float().
calculate_l2_norm(Scores, Nodes) ->
Sum_squares = gleam@list:fold(
Nodes,
+0.0,
fun(Sum, Node) ->
Score = begin
_pipe = gleam_stdlib:map_get(Scores, Node),
gleam@result:unwrap(_pipe, +0.0)
end,
Sum + (Score * Score)
end
),
case Sum_squares > +0.0 of
true ->
_pipe@1 = Sum_squares,
_pipe@2 = gleam@float:square_root(_pipe@1),
gleam@result:unwrap(_pipe@2, +0.0);
false ->
+0.0
end.
-file("src/yog/centrality.gleam", 627).
-spec calculate_l2_difference(
gleam@dict:dict(integer(), float()),
gleam@dict:dict(integer(), float()),
list(integer())
) -> float().
calculate_l2_difference(Old_scores, New_scores, Nodes) ->
Sum_squares = gleam@list:fold(
Nodes,
+0.0,
fun(Sum, Node) ->
Old_val = begin
_pipe = gleam_stdlib:map_get(Old_scores, Node),
gleam@result:unwrap(_pipe, +0.0)
end,
New_val = begin
_pipe@1 = gleam_stdlib:map_get(New_scores, Node),
gleam@result:unwrap(_pipe@1, +0.0)
end,
Diff = New_val - Old_val,
Sum + (Diff * Diff)
end
),
case Sum_squares > +0.0 of
true ->
_pipe@2 = Sum_squares,
_pipe@3 = gleam@float:square_root(_pipe@2),
gleam@result:unwrap(_pipe@3, +0.0);
false ->
+0.0
end.
-file("src/yog/centrality.gleam", 567).
-spec iterate_eigenvector(
yog@model:graph(any(), any()),
list(integer()),
gleam@dict:dict(integer(), float()),
integer(),
float(),
integer()
) -> gleam@dict:dict(integer(), float()).
iterate_eigenvector(Graph, Nodes, Scores, Max_iterations, Tolerance, Iteration) ->
case Iteration >= Max_iterations of
true ->
Scores;
false ->
New_scores = gleam@list:fold(
Nodes,
maps:new(),
fun(Acc, Node) ->
Neighbor_sum = begin
_pipe = get_in_neighbors(Graph, Node),
gleam@list:fold(
_pipe,
+0.0,
fun(Sum, Neighbor) ->
Neighbor_score = begin
_pipe@1 = gleam_stdlib:map_get(
Scores,
Neighbor
),
gleam@result:unwrap(_pipe@1, +0.0)
end,
Sum + Neighbor_score
end
)
end,
gleam@dict:insert(Acc, Node, Neighbor_sum)
end
),
L2_norm = calculate_l2_norm(New_scores, Nodes),
Normalized = case L2_norm > +0.0 of
true ->
gleam@dict:map_values(
New_scores,
fun(_, Score) -> case L2_norm of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> Score / Gleam@denominator
end end
);
false ->
New_scores
end,
L2_diff = calculate_l2_difference(Scores, Normalized, Nodes),
case L2_diff < Tolerance of
true ->
Normalized;
false ->
iterate_eigenvector(
Graph,
Nodes,
Normalized,
Max_iterations,
Tolerance,
Iteration + 1
)
end
end.
-file("src/yog/centrality.gleam", 538).
?DOC(
" Calculates Eigenvector Centrality for all nodes.\n"
"\n"
" Eigenvector centrality measures a node's influence based on the centrality\n"
" of its neighbors. A node is important if it is connected to other important\n"
" nodes. Uses power iteration to converge on the principal eigenvector.\n"
"\n"
" **Time Complexity:** O(max_iterations * (V + E))\n"
"\n"
" ## Parameters\n"
"\n"
" - `max_iterations`: Maximum number of power iterations\n"
" - `tolerance`: Convergence threshold for L2 norm\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" centrality.eigenvector(graph, max_iterations: 100, tolerance: 0.0001)\n"
" // => dict.from_list([#(1, 0.707), #(2, 1.0), #(3, 0.707)])\n"
" ```\n"
).
-spec eigenvector(yog@model:graph(any(), any()), integer(), float()) -> gleam@dict:dict(integer(), float()).
eigenvector(Graph, Max_iterations, Tolerance) ->
Nodes = yog@model:all_nodes(Graph),
N = erlang:length(Nodes),
case N =< 1 of
true ->
gleam@list:fold(
Nodes,
maps:new(),
fun(Acc, Id) -> gleam@dict:insert(Acc, Id, 1.0) end
);
false ->
Initial_scores = gleam@list:fold(
Nodes,
maps:new(),
fun(Acc@1, Id@1) ->
gleam@dict:insert(Acc@1, Id@1, case erlang:float(N) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> 1.0 / Gleam@denominator
end)
end
),
iterate_eigenvector(
Graph,
Nodes,
Initial_scores,
Max_iterations,
Tolerance,
0
)
end.
-file("src/yog/centrality.gleam", 749).
-spec calculate_l1_norm_diff(
gleam@dict:dict(integer(), float()),
gleam@dict:dict(integer(), float()),
list(integer())
) -> float().
calculate_l1_norm_diff(Old_scores, New_scores, Nodes) ->
gleam@list:fold(
Nodes,
+0.0,
fun(Sum, Node) ->
Old_val = begin
_pipe = gleam_stdlib:map_get(Old_scores, Node),
gleam@result:unwrap(_pipe, +0.0)
end,
New_val = begin
_pipe@1 = gleam_stdlib:map_get(New_scores, Node),
gleam@result:unwrap(_pipe@1, +0.0)
end,
Diff = case New_val > Old_val of
true ->
New_val - Old_val;
false ->
Old_val - New_val
end,
Sum + Diff
end
).
-file("src/yog/centrality.gleam", 703).
-spec iterate_katz(
yog@model:graph(any(), any()),
list(integer()),
gleam@dict:dict(integer(), float()),
float(),
float(),
integer(),
float(),
integer()
) -> gleam@dict:dict(integer(), float()).
iterate_katz(
Graph,
Nodes,
Scores,
Alpha,
Beta,
Max_iterations,
Tolerance,
Iteration
) ->
case Iteration >= Max_iterations of
true ->
Scores;
false ->
New_scores = gleam@list:fold(
Nodes,
maps:new(),
fun(Acc, Node) ->
Neighbor_sum = begin
_pipe = get_in_neighbors(Graph, Node),
gleam@list:fold(
_pipe,
+0.0,
fun(Sum, Neighbor) ->
Neighbor_score = begin
_pipe@1 = gleam_stdlib:map_get(
Scores,
Neighbor
),
gleam@result:unwrap(_pipe@1, +0.0)
end,
Sum + Neighbor_score
end
)
end,
New_score = (Alpha * Neighbor_sum) + Beta,
gleam@dict:insert(Acc, Node, New_score)
end
),
L1_diff = calculate_l1_norm_diff(Scores, New_scores, Nodes),
case L1_diff < Tolerance of
true ->
New_scores;
false ->
iterate_katz(
Graph,
Nodes,
New_scores,
Alpha,
Beta,
Max_iterations,
Tolerance,
Iteration + 1
)
end
end.
-file("src/yog/centrality.gleam", 673).
?DOC(
" Calculates Katz Centrality for all nodes.\n"
"\n"
" Katz centrality is a variant of eigenvector centrality that adds an\n"
" attenuation factor (alpha) to prevent the infinite accumulation of\n"
" centrality in cycles. It also includes a constant term (beta) to give\n"
" every node some base centrality.\n"
"\n"
" Formula: C(v) = α * Σ C(u) + β for all neighbors u\n"
"\n"
" **Time Complexity:** O(max_iterations * (V + E))\n"
"\n"
" ## Parameters\n"
"\n"
" - `alpha`: Attenuation factor (must be < 1/largest_eigenvalue, typically 0.1-0.3)\n"
" - `beta`: Base centrality (typically 1.0)\n"
" - `max_iterations`: Maximum number of iterations\n"
" - `tolerance`: Convergence threshold\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" centrality.katz(graph, alpha: 0.1, beta: 1.0, max_iterations: 100, tolerance: 0.0001)\n"
" // => dict.from_list([#(1, 2.5), #(2, 3.0), #(3, 2.5)])\n"
" ```\n"
).
-spec katz(yog@model:graph(any(), any()), float(), float(), integer(), float()) -> gleam@dict:dict(integer(), float()).
katz(Graph, Alpha, Beta, Max_iterations, Tolerance) ->
Nodes = yog@model:all_nodes(Graph),
N = erlang:length(Nodes),
case N =< 0 of
true ->
maps:new();
false ->
Initial_scores = gleam@list:fold(
Nodes,
maps:new(),
fun(Acc, Id) -> gleam@dict:insert(Acc, Id, Beta) end
),
iterate_katz(
Graph,
Nodes,
Initial_scores,
Alpha,
Beta,
Max_iterations,
Tolerance,
0
)
end.
-file("src/yog/centrality.gleam", 827).
-spec iterate_alpha(
yog@model:graph(any(), any()),
list(integer()),
gleam@dict:dict(integer(), float()),
float(),
integer(),
float(),
integer()
) -> gleam@dict:dict(integer(), float()).
iterate_alpha(Graph, Nodes, Scores, Alpha, Max_iterations, Tolerance, Iteration) ->
case Iteration >= Max_iterations of
true ->
Scores;
false ->
New_scores = gleam@list:fold(
Nodes,
maps:new(),
fun(Acc, Node) ->
Neighbor_sum = begin
_pipe = get_in_neighbors(Graph, Node),
gleam@list:fold(
_pipe,
+0.0,
fun(Sum, Neighbor) ->
Neighbor_score = begin
_pipe@1 = gleam_stdlib:map_get(
Scores,
Neighbor
),
gleam@result:unwrap(_pipe@1, +0.0)
end,
Sum + Neighbor_score
end
)
end,
New_score = Alpha * Neighbor_sum,
gleam@dict:insert(Acc, Node, New_score)
end
),
L1_diff = calculate_l1_norm_diff(Scores, New_scores, Nodes),
case L1_diff < Tolerance of
true ->
New_scores;
false ->
iterate_alpha(
Graph,
Nodes,
New_scores,
Alpha,
Max_iterations,
Tolerance,
Iteration + 1
)
end
end.
-file("src/yog/centrality.gleam", 795).
?DOC(
" Calculates Alpha Centrality for all nodes.\n"
"\n"
" Alpha centrality is a generalization of Katz centrality for directed\n"
" graphs. It measures the total number of paths from a node, weighted\n"
" by path length with attenuation factor alpha.\n"
"\n"
" Unlike Katz, alpha centrality does not include a constant beta term\n"
" and is particularly useful for analyzing influence in directed networks.\n"
"\n"
" Formula: C(v) = α * Σ C(u) for all predecessors u (or neighbors for undirected)\n"
"\n"
" **Time Complexity:** O(max_iterations * (V + E))\n"
"\n"
" ## Parameters\n"
"\n"
" - `alpha`: Attenuation factor (typically 0.1-0.5)\n"
" - `initial`: Initial centrality value for all nodes\n"
" - `max_iterations`: Maximum number of iterations\n"
" - `tolerance`: Convergence threshold\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" centrality.alpha(graph, alpha: 0.3, initial: 1.0, max_iterations: 100, tolerance: 0.0001)\n"
" // => dict.from_list([#(1, 2.0), #(2, 3.0), #(3, 2.0)])\n"
" ```\n"
).
-spec alpha_centrality(
yog@model:graph(any(), any()),
float(),
float(),
integer(),
float()
) -> gleam@dict:dict(integer(), float()).
alpha_centrality(Graph, Alpha, Initial, Max_iterations, Tolerance) ->
Nodes = yog@model:all_nodes(Graph),
N = erlang:length(Nodes),
case N =< 0 of
true ->
maps:new();
false ->
Initial_scores = gleam@list:fold(
Nodes,
maps:new(),
fun(Acc, Id) -> gleam@dict:insert(Acc, Id, Initial) end
),
iterate_alpha(
Graph,
Nodes,
Initial_scores,
Alpha,
Max_iterations,
Tolerance,
0
)
end.
-file("src/yog/centrality.gleam", 877).
?DOC(
" Degree centrality with default options for undirected graphs.\n"
" Uses TotalDegree mode.\n"
).
-spec degree_total(yog@model:graph(any(), any())) -> gleam@dict:dict(integer(), float()).
degree_total(Graph) ->
degree(Graph, total_degree).
-file("src/yog/centrality.gleam", 883).
?DOC(
" Closeness centrality with **Int** weights (e.g., unweighted graphs).\n"
" Uses 0 as zero, int.add, int.compare, and int.to_float.\n"
).
-spec closeness_int(yog@model:graph(any(), integer())) -> gleam@dict:dict(integer(), float()).
closeness_int(Graph) ->
closeness(
Graph,
0,
fun gleam@int:add/2,
fun gleam@int:compare/2,
fun erlang:float/1
).
-file("src/yog/centrality.gleam", 889).
?DOC(
" Closeness centrality with **Float** weights.\n"
" Uses 0.0 as zero, float.add, float.compare, and identity.\n"
).
-spec closeness_float(yog@model:graph(any(), float())) -> gleam@dict:dict(integer(), float()).
closeness_float(Graph) ->
closeness(
Graph,
+0.0,
fun gleam@float:add/2,
fun gleam@float:compare/2,
fun(X) -> X end
).
-file("src/yog/centrality.gleam", 894).
?DOC(" Harmonic centrality with **Int** weights.\n").
-spec harmonic_centrality_int(yog@model:graph(any(), integer())) -> gleam@dict:dict(integer(), float()).
harmonic_centrality_int(Graph) ->
harmonic_centrality(
Graph,
0,
fun gleam@int:add/2,
fun gleam@int:compare/2,
fun erlang:float/1
).
-file("src/yog/centrality.gleam", 899).
?DOC(" Harmonic centrality with **Float** weights.\n").
-spec harmonic_centrality_float(yog@model:graph(any(), float())) -> gleam@dict:dict(integer(), float()).
harmonic_centrality_float(Graph) ->
harmonic_centrality(
Graph,
+0.0,
fun gleam@float:add/2,
fun gleam@float:compare/2,
fun(X) -> X end
).
-file("src/yog/centrality.gleam", 904).
?DOC(" Betweenness centrality with **Int** weights.\n").
-spec betweenness_int(yog@model:graph(any(), integer())) -> gleam@dict:dict(integer(), float()).
betweenness_int(Graph) ->
betweenness(
Graph,
0,
fun gleam@int:add/2,
fun gleam@int:compare/2,
fun erlang:float/1
).
-file("src/yog/centrality.gleam", 909).
?DOC(" Betweenness centrality with **Float** weights.\n").
-spec betweenness_float(yog@model:graph(any(), float())) -> gleam@dict:dict(integer(), float()).
betweenness_float(Graph) ->
betweenness(
Graph,
+0.0,
fun gleam@float:add/2,
fun gleam@float:compare/2,
fun(X) -> X end
).
-file("src/yog/centrality.gleam", 914).
?DOC(" Default PageRank options (damping=0.85, max_iterations=100, tolerance=0.0001).\n").
-spec default_pagerank_options() -> page_rank_options().
default_pagerank_options() ->
{page_rank_options, 0.85, 100, 0.0001}.