Skip to main content

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}