Skip to main content

torsh_graph/
generative.rs

1//! Graph Generation Models
2//!
3//! Advanced implementation of generative models for graphs including
4//! Variational Autoencoders (VAE) and Generative Adversarial Networks (GAN)
5//! specifically designed for graph-structured data.
6//!
7//! # Features:
8//! - Graph Variational Autoencoder (GraphVAE)
9//! - Graph Generative Adversarial Network (GraphGAN)
10//! - Conditional graph generation
11//! - Graph reconstruction and completion
12//! - Latent space graph interpolation
13//! - Property-guided graph generation
14// Framework infrastructure - components designed for future use
15#![allow(dead_code)]
16/// Crate-local result alias: the error type defaults to [`TorshError`],
17/// so both `Result<T>` and `Result<T, OtherError>` stay valid.
18type Result<T, E = torsh_core::error::TorshError> = std::result::Result<T, E>;
19
20use crate::parameter::Parameter;
21use crate::{GraphData, GraphLayer};
22use scirs2_core::random::thread_rng;
23use torsh_tensor::{
24    creation::{from_vec, randn, zeros},
25    Tensor,
26};
27
28/// Numerically stable `softplus(x) = ln(1 + e^x)`.
29///
30/// Evaluated as `x + ln(1 + e^-x)` for positive `x` so that neither branch ever
31/// overflows `e^x`; the result is finite for every finite input.
32fn softplus(x: f32) -> f32 {
33    if x > 0.0 {
34        x + (-x).exp().ln_1p()
35    } else {
36        x.exp().ln_1p()
37    }
38}
39
40/// Graph Variational Autoencoder (GraphVAE)
41/// Learns a probabilistic latent representation of graphs
42#[derive(Debug)]
43pub struct GraphVAE {
44    // Encoder parameters
45    encoder_in_features: usize,
46    encoder_hidden_features: usize,
47    latent_dim: usize,
48
49    // Encoder layers
50    encoder_layer1: Parameter,
51    encoder_layer2: Parameter,
52
53    // Variational parameters (mean and log-variance)
54    mu_layer: Parameter,
55    logvar_layer: Parameter,
56
57    // Decoder parameters
58    decoder_layer1: Parameter,
59    decoder_layer2: Parameter,
60    node_decoder: Parameter,
61    edge_decoder: Parameter,
62
63    // KL divergence weight
64    beta: f32,
65
66    // Bias terms
67    encoder_bias1: Option<Parameter>,
68    encoder_bias2: Option<Parameter>,
69    decoder_bias1: Option<Parameter>,
70    decoder_bias2: Option<Parameter>,
71}
72
73impl GraphVAE {
74    /// Create a new Graph Variational Autoencoder
75    pub fn new(
76        in_features: usize,
77        hidden_features: usize,
78        latent_dim: usize,
79        beta: f32,
80        use_bias: bool,
81    ) -> Result<Self> {
82        // Encoder layers
83        let encoder_layer1 = Parameter::new(randn(&[in_features, hidden_features])?);
84        let encoder_layer2 = Parameter::new(randn(&[hidden_features, hidden_features])?);
85
86        // Variational layers
87        let mu_layer = Parameter::new(randn(&[hidden_features, latent_dim])?);
88        let logvar_layer = Parameter::new(randn(&[hidden_features, latent_dim])?);
89
90        // Decoder layers
91        let decoder_layer1 = Parameter::new(randn(&[latent_dim, hidden_features])?);
92        let decoder_layer2 = Parameter::new(randn(&[hidden_features, hidden_features])?);
93        let node_decoder = Parameter::new(randn(&[hidden_features, in_features])?);
94        let edge_decoder = Parameter::new(randn(&[hidden_features, 1])?);
95
96        let (encoder_bias1, encoder_bias2, decoder_bias1, decoder_bias2) = if use_bias {
97            (
98                Some(Parameter::new(zeros(&[hidden_features])?)),
99                Some(Parameter::new(zeros(&[hidden_features])?)),
100                Some(Parameter::new(zeros(&[hidden_features])?)),
101                Some(Parameter::new(zeros(&[hidden_features])?)),
102            )
103        } else {
104            (None, None, None, None)
105        };
106
107        Ok(Self {
108            encoder_in_features: in_features,
109            encoder_hidden_features: hidden_features,
110            latent_dim,
111            encoder_layer1,
112            encoder_layer2,
113            mu_layer,
114            logvar_layer,
115            decoder_layer1,
116            decoder_layer2,
117            node_decoder,
118            edge_decoder,
119            beta,
120            encoder_bias1,
121            encoder_bias2,
122            decoder_bias1,
123            decoder_bias2,
124        })
125    }
126
127    /// Encode graph to latent distribution parameters
128    pub fn encode(&self, graph: &GraphData) -> Result<(Tensor, Tensor)> {
129        // Forward through encoder
130        let mut h = graph.x.matmul(&self.encoder_layer1.clone_data())?;
131        if let Some(ref bias) = self.encoder_bias1 {
132            h = h.add(&bias.clone_data())?;
133        }
134        h = self.relu(&h)?;
135
136        h = h.matmul(&self.encoder_layer2.clone_data())?;
137        if let Some(ref bias) = self.encoder_bias2 {
138            h = h.add(&bias.clone_data())?;
139        }
140        h = self.relu(&h)?;
141
142        // Global mean pooling
143        let graph_embedding = h.mean(Some(&[0]), false)?;
144        let graph_embedding_2d = graph_embedding.unsqueeze(0)?; // Make 2D for matmul
145
146        // Compute mu and logvar
147        let mu = graph_embedding_2d.matmul(&self.mu_layer.clone_data())?;
148        let logvar = graph_embedding_2d.matmul(&self.logvar_layer.clone_data())?;
149
150        Ok((mu, logvar))
151    }
152
153    /// Reparameterization trick for sampling from latent distribution
154    pub fn reparameterize(&self, mu: &Tensor, logvar: &Tensor) -> Result<Tensor> {
155        // std = exp(0.5 * logvar)
156        let std = logvar.mul_scalar(0.5)?.exp()?;
157
158        // Sample epsilon from N(0, 1)
159        let epsilon = randn(mu.shape().dims())?;
160
161        // z = mu + std * epsilon
162        Ok(mu.add(&std.mul(&epsilon)?)?)
163    }
164
165    /// Decode latent representation to graph
166    pub fn decode(&self, z: &Tensor, num_nodes: usize) -> Result<GraphData> {
167        // Forward through decoder
168        let mut h = z.matmul(&self.decoder_layer1.clone_data())?;
169        if let Some(ref bias) = self.decoder_bias1 {
170            h = h.add(&bias.clone_data())?;
171        }
172        h = self.relu(&h)?;
173
174        h = h.matmul(&self.decoder_layer2.clone_data())?;
175        if let Some(ref bias) = self.decoder_bias2 {
176            h = h.add(&bias.clone_data())?;
177        }
178        h = self.relu(&h)?;
179
180        // Expand to node-level representation
181        let h_expanded = self.expand_to_nodes(&h, num_nodes)?;
182
183        // Decode node features
184        let node_features = h_expanded.matmul(&self.node_decoder.clone_data())?;
185
186        // Decode edge probabilities
187        let edge_logits = self.decode_edges(&h_expanded, num_nodes)?;
188        let edge_index = self.sample_edges(&edge_logits, num_nodes)?;
189
190        Ok(GraphData::new(node_features, edge_index))
191    }
192
193    /// Forward pass through GraphVAE
194    ///
195    /// # Errors
196    /// Propagates encoder/decoder tensor-operation failures.
197    pub fn forward(&self, graph: &GraphData) -> Result<(GraphData, Tensor, Tensor)> {
198        // Encode
199        let (mu, logvar) = self.encode(graph)?;
200
201        // Sample latent variable
202        let z = self.reparameterize(&mu, &logvar)?;
203
204        // Decode
205        let reconstructed = self.decode(&z, graph.num_nodes)?;
206
207        Ok((reconstructed, mu, logvar))
208    }
209
210    /// Compute VAE loss (reconstruction + KL divergence)
211    pub fn compute_loss(
212        &self,
213        graph: &GraphData,
214        reconstructed: &GraphData,
215        mu: &Tensor,
216        logvar: &Tensor,
217    ) -> Result<f32> {
218        // Reconstruction loss (MSE for node features)
219        let recon_loss = self.reconstruction_loss(graph, reconstructed)?;
220
221        // KL divergence: -0.5 * sum(1 + logvar - mu^2 - exp(logvar))
222        let kl_loss = self.kl_divergence(mu, logvar)?;
223
224        // Total loss
225        Ok(recon_loss + self.beta * kl_loss)
226    }
227
228    /// Reconstruction loss (MSE)
229    fn reconstruction_loss(&self, original: &GraphData, reconstructed: &GraphData) -> Result<f32> {
230        let orig_data = original.x.to_vec()?;
231        let recon_data = reconstructed.x.to_vec()?;
232
233        let mut mse = 0.0;
234        let len = orig_data.len().min(recon_data.len());
235
236        for i in 0..len {
237            mse += (orig_data[i] - recon_data[i]).powi(2);
238        }
239
240        Ok(mse / len as f32)
241    }
242
243    /// KL divergence loss
244    fn kl_divergence(&self, mu: &Tensor, logvar: &Tensor) -> Result<f32> {
245        let mu_data = mu.to_vec()?;
246        let logvar_data = logvar.to_vec()?;
247
248        let mut kl = 0.0;
249        for i in 0..mu_data.len() {
250            kl += -0.5 * (1.0 + logvar_data[i] - mu_data[i].powi(2) - logvar_data[i].exp());
251        }
252
253        Ok(kl / mu_data.len() as f32)
254    }
255
256    /// Generate new graph from random latent vector
257    pub fn generate(&self, num_nodes: usize) -> Result<GraphData> {
258        // Sample from standard normal
259        let z = randn(&[1, self.latent_dim])?;
260
261        // Decode to graph
262        self.decode(&z, num_nodes)
263    }
264
265    /// Interpolate between two graphs in latent space
266    pub fn interpolate(
267        &self,
268        graph1: &GraphData,
269        graph2: &GraphData,
270        alpha: f32,
271        num_nodes: usize,
272    ) -> Result<GraphData> {
273        let (mu1, _) = self.encode(graph1)?;
274        let (mu2, _) = self.encode(graph2)?;
275
276        // Linear interpolation
277        let z_interp = mu1.mul_scalar(1.0 - alpha)?.add(&mu2.mul_scalar(alpha)?)?;
278
279        // Decode interpolated latent
280        self.decode(&z_interp, num_nodes)
281    }
282
283    // Helper methods
284
285    fn relu(&self, x: &Tensor) -> Result<Tensor> {
286        let data = x.to_vec()?;
287        let activated: Vec<f32> = data.iter().map(|&v| v.max(0.0)).collect();
288        Ok(from_vec(
289            activated,
290            x.shape().dims(),
291            torsh_core::device::DeviceType::Cpu,
292        )?)
293    }
294
295    fn expand_to_nodes(&self, h: &Tensor, num_nodes: usize) -> Result<Tensor> {
296        // Repeat graph-level embedding for each node
297        let h_data = h.to_vec()?;
298        let feat_dim = h_data.len();
299
300        let mut expanded_data = Vec::new();
301        for _ in 0..num_nodes {
302            expanded_data.extend(&h_data);
303        }
304
305        Ok(from_vec(
306            expanded_data,
307            &[num_nodes, feat_dim],
308            torsh_core::device::DeviceType::Cpu,
309        )?)
310    }
311
312    fn decode_edges(&self, h: &Tensor, num_nodes: usize) -> Result<Tensor> {
313        // Compute pairwise edge probabilities
314        let mut edge_logits_data = Vec::new();
315
316        for i in 0..num_nodes {
317            for j in 0..num_nodes {
318                if i != j {
319                    // Simplified: use dot product of node embeddings as edge logit
320                    let h_i = h.slice_tensor(0, i, i + 1)?;
321                    let h_j = h.slice_tensor(0, j, j + 1)?;
322
323                    let logit = h_i.dot(&h_j.t()?)?.item()?;
324                    edge_logits_data.push(logit);
325                } else {
326                    edge_logits_data.push(-1000.0); // No self-loops
327                }
328            }
329        }
330
331        Ok(from_vec(
332            edge_logits_data,
333            &[num_nodes, num_nodes],
334            torsh_core::device::DeviceType::Cpu,
335        )?)
336    }
337
338    fn sample_edges(&self, edge_logits: &Tensor, num_nodes: usize) -> Result<Tensor> {
339        let logits_data = edge_logits.to_vec()?;
340        let mut edges = Vec::new();
341
342        // Sample edges based on probabilities (threshold at 0.5)
343        for i in 0..num_nodes {
344            for j in 0..num_nodes {
345                if i != j {
346                    let idx = i * num_nodes + j;
347                    let prob = 1.0 / (1.0 + (-logits_data[idx]).exp()); // Sigmoid
348
349                    if prob > 0.5 {
350                        edges.push(i as f32);
351                        edges.push(j as f32);
352                    }
353                }
354            }
355        }
356
357        if edges.is_empty() {
358            // Return empty edge index
359            return Ok(zeros(&[2, 0])?);
360        }
361
362        let num_edges = edges.len() / 2;
363        Ok(from_vec(
364            edges,
365            &[2, num_edges],
366            torsh_core::device::DeviceType::Cpu,
367        )?)
368    }
369}
370
371impl GraphLayer for GraphVAE {
372    fn forward(&self, graph: &GraphData) -> Result<GraphData> {
373        let (reconstructed, _, _) = GraphVAE::forward(self, graph)?;
374        Ok(reconstructed)
375    }
376
377    fn parameters(&self) -> Vec<Tensor> {
378        let mut params = vec![
379            self.encoder_layer1.clone_data(),
380            self.encoder_layer2.clone_data(),
381            self.mu_layer.clone_data(),
382            self.logvar_layer.clone_data(),
383            self.decoder_layer1.clone_data(),
384            self.decoder_layer2.clone_data(),
385            self.node_decoder.clone_data(),
386            self.edge_decoder.clone_data(),
387        ];
388
389        if let Some(ref b) = self.encoder_bias1 {
390            params.push(b.clone_data());
391        }
392        if let Some(ref b) = self.encoder_bias2 {
393            params.push(b.clone_data());
394        }
395        if let Some(ref b) = self.decoder_bias1 {
396            params.push(b.clone_data());
397        }
398        if let Some(ref b) = self.decoder_bias2 {
399            params.push(b.clone_data());
400        }
401
402        params
403    }
404}
405
406/// Graph Generative Adversarial Network (GraphGAN)
407/// Learns to generate realistic graphs through adversarial training
408#[derive(Debug)]
409pub struct GraphGAN {
410    latent_dim: usize,
411    hidden_dim: usize,
412    output_features: usize,
413
414    // Generator network
415    generator: GraphGANGenerator,
416
417    // Discriminator network
418    discriminator: GraphGANDiscriminator,
419}
420
421impl GraphGAN {
422    /// Create a new Graph GAN
423    pub fn new(
424        latent_dim: usize,
425        hidden_dim: usize,
426        output_features: usize,
427        use_bias: bool,
428    ) -> Result<Self> {
429        let generator = GraphGANGenerator::new(latent_dim, hidden_dim, output_features, use_bias)?;
430        let discriminator = GraphGANDiscriminator::new(output_features, hidden_dim, use_bias)?;
431
432        Ok(Self {
433            latent_dim,
434            hidden_dim,
435            output_features,
436            generator,
437            discriminator,
438        })
439    }
440
441    /// Generate fake graph from random noise
442    pub fn generate(&self, num_nodes: usize) -> Result<GraphData> {
443        let z = randn(&[1, self.latent_dim])?;
444        self.generator.generate(&z, num_nodes)
445    }
446
447    /// Discriminator forward pass (returns real/fake probability in `(0, 1)`)
448    ///
449    /// # Errors
450    /// Propagates discriminator tensor-operation failures.
451    pub fn discriminate(&self, graph: &GraphData) -> Result<f32> {
452        self.discriminator.forward(graph)
453    }
454
455    /// Discriminator forward pass returning the raw pre-sigmoid logit.
456    ///
457    /// The losses are computed from this value rather than from the sigmoid
458    /// output: in `f32` the sigmoid saturates to exactly `0.0` or `1.0` for
459    /// logits beyond roughly +-17, and `ln(0)` would make the loss infinite.
460    ///
461    /// # Errors
462    /// Propagates discriminator tensor-operation failures.
463    pub fn discriminate_logit(&self, graph: &GraphData) -> Result<f32> {
464        self.discriminator.forward_logit(graph)
465    }
466
467    /// Train generator (maximize discriminator error)
468    ///
469    /// Computes `-log D(G(z))` as `softplus(-logit)`, which is finite for every
470    /// finite logit.
471    ///
472    /// # Errors
473    /// Propagates generator/discriminator tensor-operation failures.
474    pub fn generator_loss(&self, num_nodes: usize) -> Result<f32> {
475        let fake_graph = self.generate(num_nodes)?;
476        let fake_logit = self.discriminate_logit(&fake_graph)?;
477
478        // Generator loss: -log(D(G(z))) = softplus(-logit)
479        Ok(softplus(-fake_logit))
480    }
481
482    /// Train discriminator (distinguish real from fake)
483    ///
484    /// Computes `-log D(real) - log(1 - D(fake))` in the numerically stable
485    /// binary-cross-entropy-with-logits form
486    /// `softplus(-logit_real) + softplus(logit_fake)`.
487    ///
488    /// # Errors
489    /// Propagates generator/discriminator tensor-operation failures.
490    pub fn discriminator_loss(&self, real_graph: &GraphData, num_nodes: usize) -> Result<f32> {
491        let real_logit = self.discriminate_logit(real_graph)?;
492
493        let fake_graph = self.generate(num_nodes)?;
494        let fake_logit = self.discriminate_logit(&fake_graph)?;
495
496        // -log(sigmoid(x))     = softplus(-x)
497        // -log(1 - sigmoid(x)) = softplus(x)
498        Ok(softplus(-real_logit) + softplus(fake_logit))
499    }
500
501    /// Get generator parameters
502    pub fn generator_parameters(&self) -> Vec<Tensor> {
503        self.generator.parameters()
504    }
505
506    /// Get discriminator parameters
507    pub fn discriminator_parameters(&self) -> Vec<Tensor> {
508        self.discriminator.parameters()
509    }
510}
511
512/// Generator network for GraphGAN
513#[derive(Debug)]
514struct GraphGANGenerator {
515    latent_dim: usize,
516    hidden_dim: usize,
517    output_features: usize,
518
519    layer1: Parameter,
520    layer2: Parameter,
521    node_layer: Parameter,
522    edge_layer: Parameter,
523
524    bias1: Option<Parameter>,
525    bias2: Option<Parameter>,
526}
527
528impl GraphGANGenerator {
529    fn new(
530        latent_dim: usize,
531        hidden_dim: usize,
532        output_features: usize,
533        use_bias: bool,
534    ) -> Result<Self> {
535        let layer1 = Parameter::new(randn(&[latent_dim, hidden_dim])?);
536        let layer2 = Parameter::new(randn(&[hidden_dim, hidden_dim])?);
537        let node_layer = Parameter::new(randn(&[hidden_dim, output_features])?);
538        let edge_layer = Parameter::new(randn(&[hidden_dim, 1])?);
539
540        let (bias1, bias2) = if use_bias {
541            (
542                Some(Parameter::new(zeros(&[hidden_dim])?)),
543                Some(Parameter::new(zeros(&[hidden_dim])?)),
544            )
545        } else {
546            (None, None)
547        };
548
549        Ok(Self {
550            latent_dim,
551            hidden_dim,
552            output_features,
553            layer1,
554            layer2,
555            node_layer,
556            edge_layer,
557            bias1,
558            bias2,
559        })
560    }
561
562    fn generate(&self, z: &Tensor, num_nodes: usize) -> Result<GraphData> {
563        // Forward through generator
564        let mut h = z.matmul(&self.layer1.clone_data())?;
565        if let Some(ref bias) = self.bias1 {
566            h = h.add(&bias.clone_data())?;
567        }
568        h = self.leaky_relu(&h, 0.2)?;
569
570        h = h.matmul(&self.layer2.clone_data())?;
571        if let Some(ref bias) = self.bias2 {
572            h = h.add(&bias.clone_data())?;
573        }
574        h = self.leaky_relu(&h, 0.2)?;
575
576        // Expand to node-level
577        let h_expanded = self.expand_to_nodes(&h, num_nodes)?;
578
579        // Generate node features
580        let node_features = h_expanded.matmul(&self.node_layer.clone_data())?;
581        let node_features = self.tanh(&node_features)?;
582
583        // Generate edges
584        let edge_index = self.generate_edges(&h_expanded, num_nodes)?;
585
586        Ok(GraphData::new(node_features, edge_index))
587    }
588
589    fn leaky_relu(&self, x: &Tensor, alpha: f32) -> Result<Tensor> {
590        let data = x.to_vec()?;
591        let activated: Vec<f32> = data
592            .iter()
593            .map(|&v| if v > 0.0 { v } else { alpha * v })
594            .collect();
595        Ok(from_vec(
596            activated,
597            x.shape().dims(),
598            torsh_core::device::DeviceType::Cpu,
599        )?)
600    }
601
602    fn tanh(&self, x: &Tensor) -> Result<Tensor> {
603        let data = x.to_vec()?;
604        let activated: Vec<f32> = data.iter().map(|&v| v.tanh()).collect();
605        Ok(from_vec(
606            activated,
607            x.shape().dims(),
608            torsh_core::device::DeviceType::Cpu,
609        )?)
610    }
611
612    fn expand_to_nodes(&self, h: &Tensor, num_nodes: usize) -> Result<Tensor> {
613        let h_data = h.to_vec()?;
614        let feat_dim = h_data.len();
615
616        let mut expanded_data = Vec::new();
617        for _ in 0..num_nodes {
618            expanded_data.extend(&h_data);
619        }
620
621        Ok(from_vec(
622            expanded_data,
623            &[num_nodes, feat_dim],
624            torsh_core::device::DeviceType::Cpu,
625        )?)
626    }
627
628    fn generate_edges(&self, _h: &Tensor, num_nodes: usize) -> Result<Tensor> {
629        let mut edges = Vec::new();
630        let mut rng = thread_rng();
631
632        // Generate edges probabilistically
633        for i in 0..num_nodes {
634            for j in (i + 1)..num_nodes {
635                // Use node embeddings to determine edge probability
636                if rng.gen_range(0.0..1.0) > 0.7 {
637                    edges.push(i as f32);
638                    edges.push(j as f32);
639                    edges.push(j as f32);
640                    edges.push(i as f32);
641                }
642            }
643        }
644
645        if edges.is_empty() {
646            return Ok(zeros(&[2, 0])?);
647        }
648
649        let num_edges = edges.len() / 2;
650        Ok(from_vec(
651            edges,
652            &[2, num_edges],
653            torsh_core::device::DeviceType::Cpu,
654        )?)
655    }
656
657    fn parameters(&self) -> Vec<Tensor> {
658        let mut params = vec![
659            self.layer1.clone_data(),
660            self.layer2.clone_data(),
661            self.node_layer.clone_data(),
662            self.edge_layer.clone_data(),
663        ];
664
665        if let Some(ref b) = self.bias1 {
666            params.push(b.clone_data());
667        }
668        if let Some(ref b) = self.bias2 {
669            params.push(b.clone_data());
670        }
671
672        params
673    }
674}
675
676/// Discriminator network for GraphGAN
677#[derive(Debug)]
678struct GraphGANDiscriminator {
679    input_features: usize,
680    hidden_dim: usize,
681
682    layer1: Parameter,
683    layer2: Parameter,
684    output_layer: Parameter,
685
686    bias1: Option<Parameter>,
687    bias2: Option<Parameter>,
688    bias_out: Option<Parameter>,
689}
690
691impl GraphGANDiscriminator {
692    fn new(input_features: usize, hidden_dim: usize, use_bias: bool) -> Result<Self> {
693        let layer1 = Parameter::new(randn(&[input_features, hidden_dim])?);
694        let layer2 = Parameter::new(randn(&[hidden_dim, hidden_dim])?);
695        let output_layer = Parameter::new(randn(&[hidden_dim, 1])?);
696
697        let (bias1, bias2, bias_out) = if use_bias {
698            (
699                Some(Parameter::new(zeros(&[hidden_dim])?)),
700                Some(Parameter::new(zeros(&[hidden_dim])?)),
701                Some(Parameter::new(zeros(&[1])?)),
702            )
703        } else {
704            (None, None, None)
705        };
706
707        Ok(Self {
708            input_features,
709            hidden_dim,
710            layer1,
711            layer2,
712            output_layer,
713            bias1,
714            bias2,
715            bias_out,
716        })
717    }
718
719    /// Raw pre-sigmoid discriminator output.
720    fn forward_logit(&self, graph: &GraphData) -> Result<f32> {
721        // Forward through discriminator
722        let mut h = graph.x.matmul(&self.layer1.clone_data())?;
723        if let Some(ref bias) = self.bias1 {
724            h = h.add(&bias.clone_data())?;
725        }
726        h = self.leaky_relu(&h, 0.2)?;
727
728        h = h.matmul(&self.layer2.clone_data())?;
729        if let Some(ref bias) = self.bias2 {
730            h = h.add(&bias.clone_data())?;
731        }
732        h = self.leaky_relu(&h, 0.2)?;
733
734        // Global mean pooling
735        let graph_repr = h.mean(Some(&[0]), false)?;
736        let graph_repr_2d = graph_repr.unsqueeze(0)?; // Make 2D for matmul
737
738        // Output layer
739        let mut logit = graph_repr_2d.matmul(&self.output_layer.clone_data())?;
740        if let Some(ref bias) = self.bias_out {
741            logit = logit.add(&bias.clone_data())?;
742        }
743
744        logit.item()
745    }
746
747    /// Discriminator score in `(0, 1)`: `sigmoid(logit)`.
748    fn forward(&self, graph: &GraphData) -> Result<f32> {
749        let logit_val = self.forward_logit(graph)?;
750        Ok(1.0 / (1.0 + (-logit_val).exp()))
751    }
752
753    fn leaky_relu(&self, x: &Tensor, alpha: f32) -> Result<Tensor> {
754        let data = x.to_vec()?;
755        let activated: Vec<f32> = data
756            .iter()
757            .map(|&v| if v > 0.0 { v } else { alpha * v })
758            .collect();
759        Ok(from_vec(
760            activated,
761            x.shape().dims(),
762            torsh_core::device::DeviceType::Cpu,
763        )?)
764    }
765
766    fn parameters(&self) -> Vec<Tensor> {
767        let mut params = vec![
768            self.layer1.clone_data(),
769            self.layer2.clone_data(),
770            self.output_layer.clone_data(),
771        ];
772
773        if let Some(ref b) = self.bias1 {
774            params.push(b.clone_data());
775        }
776        if let Some(ref b) = self.bias2 {
777            params.push(b.clone_data());
778        }
779        if let Some(ref b) = self.bias_out {
780            params.push(b.clone_data());
781        }
782
783        params
784    }
785}
786
787/// Conditional Graph Generation
788#[derive(Debug)]
789pub struct ConditionalGraphGenerator {
790    vae: GraphVAE,
791    condition_dim: usize,
792    condition_layer: Parameter,
793}
794
795impl ConditionalGraphGenerator {
796    /// Create a new conditional graph generator
797    pub fn new(
798        in_features: usize,
799        hidden_features: usize,
800        latent_dim: usize,
801        condition_dim: usize,
802        beta: f32,
803    ) -> Result<Self> {
804        let vae = GraphVAE::new(in_features, hidden_features, latent_dim, beta, true)?;
805        let condition_layer = Parameter::new(randn(&[condition_dim, latent_dim])?);
806
807        Ok(Self {
808            vae,
809            condition_dim,
810            condition_layer,
811        })
812    }
813
814    /// Generate graph conditioned on a property vector
815    pub fn generate_conditional(&self, condition: &Tensor, num_nodes: usize) -> Result<GraphData> {
816        // Map condition to latent space bias
817        let condition_bias = condition.matmul(&self.condition_layer.clone_data())?;
818
819        // Sample base latent vector
820        let z_base = randn(&[1, self.vae.latent_dim])?;
821
822        // Add conditional bias
823        let z = z_base.add(&condition_bias)?;
824
825        // Decode to graph
826        self.vae.decode(&z, num_nodes)
827    }
828
829    fn parameters(&self) -> Vec<Tensor> {
830        let mut params = self.vae.parameters();
831        params.push(self.condition_layer.clone_data());
832        params
833    }
834}
835
836#[cfg(test)]
837mod tests {
838    use super::*;
839    use torsh_core::device::DeviceType;
840
841    #[test]
842    fn test_graphvae_creation() {
843        let vae = GraphVAE::new(8, 16, 10, 1.0, true).expect("operation should succeed");
844        assert_eq!(vae.encoder_in_features, 8);
845        assert_eq!(vae.encoder_hidden_features, 16);
846        assert_eq!(vae.latent_dim, 10);
847        assert_eq!(vae.beta, 1.0);
848    }
849
850    #[test]
851    fn test_graphvae_encode_decode() {
852        let features = randn(&[5, 8]).unwrap();
853        let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0];
854        let edge_index = from_vec(edges, &[2, 4], DeviceType::Cpu).unwrap();
855        let graph = GraphData::new(features, edge_index);
856
857        let vae = GraphVAE::new(8, 16, 10, 1.0, true).expect("operation should succeed");
858
859        let (mu, logvar) = vae.encode(&graph).expect("operation should succeed");
860        assert_eq!(mu.shape().dims(), &[1, 10]);
861        assert_eq!(logvar.shape().dims(), &[1, 10]);
862
863        let z = vae
864            .reparameterize(&mu, &logvar)
865            .expect("operation should succeed");
866        assert_eq!(z.shape().dims(), &[1, 10]);
867
868        let reconstructed = vae.decode(&z, 5).expect("operation should succeed");
869        assert_eq!(reconstructed.num_nodes, 5);
870    }
871
872    #[test]
873    fn test_graphvae_generation() {
874        let vae = GraphVAE::new(8, 16, 10, 1.0, true).expect("operation should succeed");
875        let generated = vae.generate(6).expect("operation should succeed");
876
877        assert_eq!(generated.num_nodes, 6);
878        assert_eq!(generated.x.shape().dims()[0], 6);
879        assert_eq!(generated.x.shape().dims()[1], 8);
880    }
881
882    #[test]
883    fn test_graphvae_interpolation() {
884        let features1 = randn(&[4, 6]).unwrap();
885        let features2 = randn(&[4, 6]).unwrap();
886        let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0];
887        let edge_index = from_vec(edges.clone(), &[2, 3], DeviceType::Cpu).unwrap();
888
889        let graph1 = GraphData::new(features1, edge_index.clone());
890        let graph2 = GraphData::new(features2, edge_index);
891
892        let vae = GraphVAE::new(6, 12, 8, 1.0, true).expect("operation should succeed");
893
894        // Interpolate at alpha = 0.5 (midpoint)
895        let interpolated = vae
896            .interpolate(&graph1, &graph2, 0.5, 4)
897            .expect("operation should succeed");
898        assert_eq!(interpolated.num_nodes, 4);
899    }
900
901    #[test]
902    fn test_graphgan_creation() {
903        let gan = GraphGAN::new(16, 32, 8, true).expect("operation should succeed");
904        assert_eq!(gan.latent_dim, 16);
905        assert_eq!(gan.hidden_dim, 32);
906        assert_eq!(gan.output_features, 8);
907    }
908
909    #[test]
910    fn test_graphgan_generation() {
911        let gan = GraphGAN::new(16, 32, 8, true).expect("operation should succeed");
912        let generated = gan.generate(5).expect("operation should succeed");
913
914        assert_eq!(generated.num_nodes, 5);
915        assert_eq!(generated.x.shape().dims()[1], 8);
916    }
917
918    #[test]
919    fn test_graphgan_discriminate() {
920        let features = randn(&[4, 8]).unwrap();
921        let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0];
922        let edge_index = from_vec(edges, &[2, 3], DeviceType::Cpu).unwrap();
923        let graph = GraphData::new(features, edge_index);
924
925        let gan = GraphGAN::new(16, 32, 8, true).expect("operation should succeed");
926        let score = gan.discriminate(&graph).expect("operation should succeed");
927
928        assert!(score >= 0.0 && score <= 1.0);
929    }
930
931    #[test]
932    fn test_conditional_generation() {
933        let cond_gen =
934            ConditionalGraphGenerator::new(8, 16, 10, 4, 1.0).expect("operation should succeed");
935
936        let condition = randn(&[1, 4]).unwrap();
937        let generated = cond_gen
938            .generate_conditional(&condition, 5)
939            .expect("operation should succeed");
940
941        assert_eq!(generated.num_nodes, 5);
942        assert_eq!(generated.x.shape().dims()[1], 8);
943    }
944
945    #[test]
946    fn test_graphvae_loss_computation() {
947        let features = randn(&[3, 6]).unwrap();
948        let edges = vec![0.0, 1.0, 1.0, 2.0];
949        let edge_index = from_vec(edges, &[2, 2], DeviceType::Cpu).unwrap();
950        let graph = GraphData::new(features, edge_index);
951
952        let vae = GraphVAE::new(6, 12, 8, 1.0, true).expect("operation should succeed");
953        let (reconstructed, mu, logvar) = vae.forward(&graph).expect("operation should succeed");
954
955        let loss = vae
956            .compute_loss(&graph, &reconstructed, &mu, &logvar)
957            .expect("operation should succeed");
958        assert!(loss > 0.0);
959    }
960
961    /// A value in `[0, 1)` that is a pure function of `name` and `index` — no
962    /// RNG, no thread-local state, no process-global generator state. See
963    /// `deterministic_reinit_mpnn` in
964    /// `torsh-graph/tests/comprehensive_gnn_tests.rs` for the twin of this
965    /// helper and the measurement backing it.
966    fn deterministic_unit_interval(name: &str, index: u64) -> f32 {
967        let mut hash: u64 = 0xcbf2_9ce4_8422_2325; // FNV-1a offset basis
968        for byte in name.bytes() {
969            hash ^= u64::from(byte);
970            hash = hash.wrapping_mul(0x0000_0100_0000_01b3); // FNV-1a prime
971        }
972        hash ^= index.wrapping_add(0x9E37_79B9_7F4A_7C15);
973        hash = (hash ^ (hash >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
974        hash = (hash ^ (hash >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
975        hash ^= hash >> 31;
976        ((hash >> 40) as f32) / (1u64 << 24) as f32
977    }
978
979    /// Deterministically overwrites every tensor in `params` in place with a
980    /// pure function of `(label, parameter position, flat index)`.
981    ///
982    /// `params` tensors (as returned by `GraphGAN::generator_parameters` /
983    /// `discriminator_parameters`, themselves built from `Parameter::
984    /// clone_data`) alias the layer's real storage: `Tensor`'s `InMemory`
985    /// backing is `Arc<RwLock<Vec<T>>>`, `Clone` shares the `Arc`, and
986    /// `Tensor::set_slice` writes through shared storage with no
987    /// copy-on-write step — so overwriting these tensors mutates the live
988    /// `GraphGAN` in place (mirrors the empirically-confirmed aliasing in
989    /// `deterministic_reinit_mpnn`).
990    ///
991    /// Skips 1-D tensors (every bias in `GraphGANGenerator`/
992    /// `GraphGANDiscriminator` is 1-D, and every weight is 2-D): both
993    /// constructors already initialize biases to `zeros(..)`, so they are
994    /// already deterministic, and overwriting them with nonzero values would
995    /// only make the reinitialized network diverge from what `GraphGAN::new`
996    /// actually produces, for no determinism gained.
997    fn reinit_params(params: &[Tensor], label: &str) {
998        for (i, tensor) in params.iter().enumerate() {
999            let dims = tensor.shape().dims().to_vec();
1000            if dims.len() != 2 {
1001                continue;
1002            }
1003            let numel: usize = dims.iter().product();
1004            let bound = (6.0 / (dims[0] + dims[1]) as f32).sqrt();
1005            let name = format!("{label}.param{i}");
1006            let values: Vec<f32> = (0..numel)
1007                .map(|j| bound * (2.0 * deterministic_unit_interval(&name, j as u64) - 1.0))
1008                .collect();
1009            tensor
1010                .set_slice(0, &values)
1011                .expect("deterministic reinit set_slice should succeed");
1012        }
1013    }
1014
1015    /// Deterministically reinitializes every parameter of `gan` (both
1016    /// generator and discriminator).
1017    ///
1018    /// # Why not `torsh_tensor::creation::manual_seed`
1019    ///
1020    /// `GraphGANGenerator`/`GraphGANDiscriminator::new` draw every weight
1021    /// from `randn` with no Xavier/Kaiming scaling, stacked through 2-3
1022    /// unnormalized layers. On an unlucky draw the discriminator's logit for
1023    /// the generated graph reaches a large enough magnitude that
1024    /// `softplus(-logit)` underflows to exactly `0.0f32` in `generator_loss`,
1025    /// failing `gen_loss > 0.0` — mathematically an open bound (`softplus` is
1026    /// strictly positive everywhere), so that exact `0.0` is an `f32`
1027    /// underflow artifact of an extreme, unscaled-init logit, not a value
1028    /// this test should legitimately produce. As
1029    /// `deterministic_reinit_mpnn`'s doc comment describes (and its swept
1030    /// probe measured) for the same `randn`-without-scaling pattern in
1031    /// `MPNNConv`, `manual_seed`'s effective per-thread seed depends on a
1032    /// process-global, scheduling-dependent "stream index" under `cargo
1033    /// test`'s shared-process default, so a fixed seed does not reliably
1034    /// avoid the underflow either — it just relocates which run hits it.
1035    /// Reinitializing post-construction (this function) avoids that source
1036    /// of nondeterminism entirely, and — by drawing from a Xavier-uniform
1037    /// bound instead of raw `randn` — keeps logits far from the underflow
1038    /// edge. Fixing the scaling in `GraphGANGenerator`/`Discriminator::new`
1039    /// itself is out of scope: it is production default initialization with
1040    /// its own blast radius, not this test's bug.
1041    fn deterministic_reinit_gan(gan: &GraphGAN) {
1042        reinit_params(&gan.generator_parameters(), "gan.generator");
1043        reinit_params(&gan.discriminator_parameters(), "gan.discriminator");
1044    }
1045
1046    #[test]
1047    fn test_graphgan_losses() {
1048        let features = randn(&[4, 8]).unwrap();
1049        let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0];
1050        let edge_index = from_vec(edges, &[2, 3], DeviceType::Cpu).unwrap();
1051        let graph = GraphData::new(features, edge_index);
1052
1053        let gan = GraphGAN::new(16, 32, 8, true).expect("operation should succeed");
1054        // Flake: see `deterministic_reinit_gan`. Reinitializing in place
1055        // makes this run reproducible without touching `GraphGAN`'s
1056        // production default init.
1057        deterministic_reinit_gan(&gan);
1058
1059        let gen_loss = gan.generator_loss(4).expect("operation should succeed");
1060        assert!(gen_loss > 0.0);
1061
1062        let disc_loss = gan
1063            .discriminator_loss(&graph, 4)
1064            .expect("operation should succeed");
1065        // Discriminator loss can be negative
1066        assert!(disc_loss.is_finite());
1067    }
1068
1069    /// `deterministic_reinit_gan` fixes the GAN's *weights*, but
1070    /// `discriminator_loss`'s `real_graph.x` is supplied fresh by the caller
1071    /// and `generator_loss`/`discriminator_loss` both draw a fresh latent
1072    /// `z = randn(..)` internally (`GraphGAN::generate`) — neither is
1073    /// touched by the reinit, so both losses still depend on an unseeded
1074    /// draw every call. That's fine *only if* the now-Xavier-scaled network
1075    /// keeps logits far from the `softplus` underflow edge (`gen_loss > 0.0`
1076    /// fails only when the logit magnitude reaches roughly 104, see
1077    /// `deterministic_reinit_gan`) regardless of which `z`/`graph.x` gets
1078    /// drawn. This test is that check, run with much higher confidence than
1079    /// `test_graphgan_losses`'s single draw: 2000 fresh `graph`/`z` draws
1080    /// against the same reinit'd weights stay well clear of the edge
1081    /// (measured range roughly `[-0.7, 0.15]`, vs. the ~104 needed to
1082    /// underflow) rather than just not-yet-having-hit it once.
1083    #[test]
1084    fn discriminator_logit_stays_bounded_across_many_random_graphs() {
1085        let gan = GraphGAN::new(16, 32, 8, true).expect("operation should succeed");
1086        deterministic_reinit_gan(&gan);
1087
1088        for _ in 0..2000 {
1089            let features = randn(&[4, 8]).expect("randn");
1090            let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0];
1091            let edge_index = from_vec(edges, &[2, 3], DeviceType::Cpu).expect("from_vec");
1092            let graph = GraphData::new(features, edge_index);
1093
1094            let real_logit = gan.discriminate_logit(&graph).expect("discriminate_logit");
1095            assert!(
1096                real_logit.abs() < 50.0,
1097                "discriminator logit strayed far enough from 0 to approach the \
1098                 softplus underflow edge: {real_logit}"
1099            );
1100
1101            let gen_loss = gan.generator_loss(4).expect("generator_loss");
1102            assert!(gen_loss > 0.0, "gen_loss was not > 0.0: {gen_loss}");
1103
1104            let disc_loss = gan
1105                .discriminator_loss(&graph, 4)
1106                .expect("discriminator_loss");
1107            assert!(
1108                disc_loss.is_finite(),
1109                "disc_loss was non-finite: {disc_loss}"
1110            );
1111        }
1112    }
1113}