query_planning/plan/graph_algorithm/layout.rs
1//! Graph layout requirements and algorithm parameter contracts.
2use std::fmt;
3
4/// Required cuGraph graph-layout state for an algorithm.
5///
6/// Mirrors the `Graph<Forward>` / `Graph<Transposed>` type-state in the local `cugraph` crate.
7/// [`Any`](Self::Any) indicates that the algorithm runs under either layout;
8/// the executor still picks one concrete layout at runtime.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum GraphLayoutRequirement {
11 /// Algorithm requires a forward graph layout.
12 Forward,
13 /// Algorithm requires a transposed graph layout.
14 Transposed,
15 /// Algorithm can run on either layout.
16 Any,
17}
18
19impl GraphLayoutRequirement {
20 /// Stable lowercase layout code.
21 pub const fn as_str(self) -> &'static str {
22 match self {
23 Self::Forward => "forward",
24 Self::Transposed => "transposed",
25 Self::Any => "any",
26 }
27 }
28}
29
30impl fmt::Display for GraphLayoutRequirement {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 f.write_str(self.as_str())
33 }
34}
35
36/// Parameters for ForceAtlas2 graph layout.
37///
38/// Mirrors `cugraph::layout::ForceAtlas2Params` without the `cugraph` crate's
39/// new type wrappers and adds the RNG seed used to create the cuGraph
40/// `RngState` for each execution. The seed fixes the initial positions only;
41/// it does not make layouts reproducible across runs.
42#[derive(Debug, Clone, PartialEq)]
43pub struct GraphForceAtlas2Params {
44 /// Maximum number of iterations. Must be in `[1, i32::MAX]`.
45 pub max_iter: u32,
46 /// Distribute attraction along outbound edges.
47 pub outbound_attraction_distribution: bool,
48 /// Use the lin-log attraction model.
49 pub lin_log_mode: bool,
50 /// Prevent nodes from overlapping.
51 ///
52 /// Plan validation rejects `true` unless `vertex_attributes` binds a radius
53 /// column, because ForceAtlas2 overlap prevention requires per-vertex radius
54 /// side input.
55 pub prevent_overlapping: bool,
56 /// Repulsion scaling for overlapping nodes. Must be > 0.
57 pub overlap_scaling_ratio: f64,
58 /// Influence of edge weights. Must be >= 0.
59 pub edge_weight_influence: f64,
60 /// Swinging tolerance. Must be >= 0.
61 pub jitter_tolerance: f64,
62 /// Use the Barnes-Hut approximation.
63 pub barnes_hut_optimize: bool,
64 /// Barnes-Hut speed/accuracy trade-off. Must be in [0, 1].
65 pub barnes_hut_theta: f64,
66 /// Repulsion scaling ratio. Must be > 0.
67 pub scaling_ratio: f64,
68 /// Use stronger gravity for distant nodes.
69 pub strong_gravity_mode: bool,
70 /// Gravity strength. Must be >= 0.
71 pub gravity: f64,
72 /// Whether cuGraph should print convergence info.
73 pub verbose: bool,
74 /// RNG seed for the random initial positions.
75 ///
76 /// The seed does not make runs reproducible: the parallel force
77 /// iteration accumulates in scheduling-dependent order, so reruns may
78 /// place individual vertices far apart. Compare layouts structurally,
79 /// not coordinate-by-coordinate.
80 pub seed: u64,
81 /// Optional per-vertex radius, mobility, and mass relation.
82 pub vertex_attributes: Option<GraphForceAtlas2VertexAttributesInput>,
83 /// Optional complete warm-start position relation.
84 pub initial_positions: Option<GraphForceAtlas2InitialPositionsInput>,
85}
86
87impl GraphForceAtlas2Params {
88 /// Visits every side-input ordinal in stable role order.
89 pub(crate) fn side_input_ordinals(&self) -> impl Iterator<Item = usize> + '_ {
90 self.vertex_attributes
91 .iter()
92 .map(|input| input.input)
93 .chain(self.initial_positions.iter().map(|input| input.input))
94 }
95}
96
97/// Optional per-vertex ForceAtlas2 attributes produced by another plan node.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct GraphForceAtlas2VertexAttributesInput {
100 /// Plan node producing the attribute relation.
101 pub input: usize,
102 /// Vertex identifier column.
103 pub vertex: String,
104 /// Optional non-null Float32 radius column.
105 pub radius: Option<String>,
106 /// Optional non-null Float32 mobility column.
107 pub mobility: Option<String>,
108 /// Optional non-null Float32 mass column.
109 pub mass: Option<String>,
110}
111
112impl GraphForceAtlas2VertexAttributesInput {
113 /// Creates a vertex-attribute binding.
114 pub fn new(
115 input: usize,
116 vertex: impl Into<String>,
117 radius: Option<impl Into<String>>,
118 mobility: Option<impl Into<String>>,
119 mass: Option<impl Into<String>>,
120 ) -> Self {
121 Self {
122 input,
123 vertex: vertex.into(),
124 radius: radius.map(Into::into),
125 mobility: mobility.map(Into::into),
126 mass: mass.map(Into::into),
127 }
128 }
129}
130
131/// Complete per-vertex ForceAtlas2 warm-start positions.
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct GraphForceAtlas2InitialPositionsInput {
134 /// Plan node producing the position relation.
135 pub input: usize,
136 /// Vertex identifier column.
137 pub vertex: String,
138 /// Non-null Float32 X-coordinate column.
139 pub x: String,
140 /// Non-null Float32 Y-coordinate column.
141 pub y: String,
142}
143
144impl GraphForceAtlas2InitialPositionsInput {
145 /// Creates an initial-position binding.
146 pub fn new(input: usize, vertex: impl Into<String>, x: impl Into<String>, y: impl Into<String>) -> Self {
147 Self { input, vertex: vertex.into(), x: x.into(), y: y.into() }
148 }
149}
150
151impl Default for GraphForceAtlas2Params {
152 fn default() -> Self {
153 // Matches cugraph::layout::ForceAtlas2Params::default(), plus a
154 // fixed seed for the required cuGraph RNG state.
155 Self {
156 max_iter: 500,
157 outbound_attraction_distribution: false,
158 lin_log_mode: false,
159 prevent_overlapping: false,
160 overlap_scaling_ratio: 2.0,
161 edge_weight_influence: 1.0,
162 jitter_tolerance: 1.0,
163 barnes_hut_optimize: false,
164 barnes_hut_theta: 0.5,
165 scaling_ratio: 2.0,
166 strong_gravity_mode: false,
167 gravity: 1.0,
168 verbose: false,
169 seed: 0,
170 vertex_attributes: None,
171 initial_positions: None,
172 }
173 }
174}