-module(graffeo_traverse). -moduledoc """ Traversal and degree-metric algorithms over the read-half behaviour. BFS supports direction (`out`/`in`/`both`) and an optional edge-type filter predicate, yielding vertices with distances. Degree metrics cover in/out/total degree, normalised degree centrality, and top-k. """. -export([ bfs/4, in_degree/3, out_degree/3, degree/3, degree_centrality/3, top_k_by_degree/4 ]). -doc "BFS traversal direction: outgoing, incoming, or both.". -type direction() :: out | in | both. -type filter() :: fun((graffeo:vertex(), graffeo:vertex()) -> boolean()) | fun((graffeo:vertex(), graffeo:vertex(), graffeo:edge_meta()) -> boolean()). -doc "BFS result: vertices paired with their distance from the source.". -type bfs_result() :: [{graffeo:vertex(), non_neg_integer()}]. -export_type([direction/0, bfs_result/0]). -doc """ Breadth-first search from `Source` in the given direction, with an optional filter. Returns `[{Vertex, Distance}]`. Options: - `direction`: `out` (default), `in`, or `both`. - `filter`: `fun(From, To) -> boolean()` or `fun(From, To, Meta) -> boolean()` — only traverse edges where the filter returns `true`. The arity-3 form receives the edge metadata from `edge_meta/3`. """. -spec bfs(module(), term(), graffeo:vertex(), map()) -> bfs_result(). bfs(Backend, Ref, Source, Opts) -> Dir = maps:get(direction, Opts, out), RawFilter = maps:get(filter, Opts, fun(_From, _To) -> true end), Filter = normalise_filter(RawFilter, Backend, Ref), bfs_loop( Backend, Ref, Dir, Filter, [{Source, 0}], sets:from_list([Source], [{version, 2}]), [] ). %%% --- Degree metrics --- -doc "In-degree of vertex `V`.". -spec in_degree(module(), term(), graffeo:vertex()) -> non_neg_integer(). in_degree(Backend, Ref, V) -> Backend:in_degree(Ref, V). -doc "Out-degree of vertex `V`.". -spec out_degree(module(), term(), graffeo:vertex()) -> non_neg_integer(). out_degree(Backend, Ref, V) -> Backend:out_degree(Ref, V). -doc "Total degree (in + out) of vertex `V`.". -spec degree(module(), term(), graffeo:vertex()) -> non_neg_integer(). degree(Backend, Ref, V) -> Backend:in_degree(Ref, V) + Backend:out_degree(Ref, V). -doc """ Normalised degree centrality for vertex `V`: `degree(V) / (2 * (N - 1))` where N is the vertex count. Returns `0.0` for a single-vertex graph. """. -spec degree_centrality(module(), term(), graffeo:vertex()) -> float(). degree_centrality(Backend, Ref, V) -> N = Backend:no_vertices(Ref), case N of N when N =< 1 -> 0.0; _ -> degree(Backend, Ref, V) / (2 * (N - 1)) end. -doc """ Top-k vertices by total degree, descending. Returns `[{Vertex, Degree}]`. """. -spec top_k_by_degree(module(), term(), [graffeo:vertex()], pos_integer()) -> [{graffeo:vertex(), non_neg_integer()}]. top_k_by_degree(Backend, Ref, Vertices, K) -> Scored = [{V, degree(Backend, Ref, V)} || V <- Vertices], Sorted = lists:sort(fun({_, D1}, {_, D2}) -> D1 > D2 end, Scored), lists:sublist(Sorted, K). %%% --- Filter normalisation --- -spec normalise_filter(filter(), module(), term()) -> fun((graffeo:vertex(), graffeo:vertex()) -> boolean()). normalise_filter(F, Backend, Ref) -> case erlang:fun_info(F, arity) of {arity, 2} -> F; {arity, 3} -> fun(From, To) -> case Backend:edge_meta(Ref, From, To) of {ok, Meta} -> F(From, To, Meta); error -> false end end end. %%% --- Internal BFS --- -spec bfs_loop( module(), term(), direction(), filter(), [{graffeo:vertex(), non_neg_integer()}], sets:set(), bfs_result() ) -> bfs_result(). bfs_loop(_Backend, _Ref, _Dir, _Filter, [], _Visited, Acc) -> lists:reverse(Acc); bfs_loop(Backend, Ref, Dir, Filter, [{V, Dist} | Rest], Visited, Acc) -> Neighbours = directed_neighbours(Backend, Ref, Dir, V), Filtered = [N || N <- Neighbours, Filter(V, N), not sets:is_element(N, Visited)], NewVisited = lists:foldl(fun sets:add_element/2, Visited, Filtered), NewQueue = Rest ++ [{N, Dist + 1} || N <- Filtered], bfs_loop(Backend, Ref, Dir, Filter, NewQueue, NewVisited, [{V, Dist} | Acc]). -spec directed_neighbours(module(), term(), direction(), graffeo:vertex()) -> [graffeo:vertex()]. directed_neighbours(Backend, Ref, out, V) -> Backend:out_neighbours(Ref, V); directed_neighbours(Backend, Ref, in, V) -> Backend:in_neighbours(Ref, V); directed_neighbours(Backend, Ref, both, V) -> lists:usort(Backend:out_neighbours(Ref, V) ++ Backend:in_neighbours(Ref, V)).