Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
327
vendor/ruvector/crates/ruvector-mincut/examples/localkcut_demo.rs
vendored
Normal file
327
vendor/ruvector/crates/ruvector-mincut/examples/localkcut_demo.rs
vendored
Normal file
@@ -0,0 +1,327 @@
|
||||
//! LocalKCut Algorithm Demonstration
|
||||
//!
|
||||
//! This example demonstrates the deterministic LocalKCut algorithm from the
|
||||
//! December 2024 paper. It shows how to:
|
||||
//!
|
||||
//! 1. Find local minimum cuts near specific vertices
|
||||
//! 2. Use deterministic edge colorings for reproducibility
|
||||
//! 3. Apply forest packing for witness guarantees
|
||||
//! 4. Compare with global minimum cut algorithms
|
||||
|
||||
use ruvector_mincut::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn main() {
|
||||
println!("=== LocalKCut Algorithm Demonstration ===\n");
|
||||
|
||||
// Example 1: Simple graph with bridge
|
||||
println!("Example 1: Bridge Detection");
|
||||
demo_bridge_detection();
|
||||
println!();
|
||||
|
||||
// Example 2: Deterministic behavior
|
||||
println!("Example 2: Deterministic Coloring");
|
||||
demo_deterministic_coloring();
|
||||
println!();
|
||||
|
||||
// Example 3: Forest packing
|
||||
println!("Example 3: Forest Packing Witnesses");
|
||||
demo_forest_packing();
|
||||
println!();
|
||||
|
||||
// Example 4: Comparison with global mincut
|
||||
println!("Example 4: Local vs Global Minimum Cut");
|
||||
demo_local_vs_global();
|
||||
println!();
|
||||
|
||||
// Example 5: Complex graph
|
||||
println!("Example 5: Complex Graph Analysis");
|
||||
demo_complex_graph();
|
||||
}
|
||||
|
||||
/// Demonstrates finding a bridge using LocalKCut
|
||||
fn demo_bridge_detection() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create two components connected by a bridge
|
||||
// Component 1: triangle {1, 2, 3}
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(2, 3, 1.0).unwrap();
|
||||
graph.insert_edge(3, 1, 1.0).unwrap();
|
||||
|
||||
// Bridge: 3 -> 4 (the minimum cut!)
|
||||
graph.insert_edge(3, 4, 1.0).unwrap();
|
||||
|
||||
// Component 2: triangle {4, 5, 6}
|
||||
graph.insert_edge(4, 5, 1.0).unwrap();
|
||||
graph.insert_edge(5, 6, 1.0).unwrap();
|
||||
graph.insert_edge(6, 4, 1.0).unwrap();
|
||||
|
||||
println!("Graph: Two triangles connected by a bridge");
|
||||
println!(
|
||||
"Vertices: {}, Edges: {}",
|
||||
graph.num_vertices(),
|
||||
graph.num_edges()
|
||||
);
|
||||
|
||||
// Find local cut from vertex 1
|
||||
let local_kcut = LocalKCut::new(graph.clone(), 5);
|
||||
|
||||
println!("\nSearching for local cut from vertex 1 with k=5...");
|
||||
if let Some(result) = local_kcut.find_cut(1) {
|
||||
println!("✓ Found local cut!");
|
||||
println!(" Cut value: {}", result.cut_value);
|
||||
println!(" Cut set size: {}", result.cut_set.len());
|
||||
println!(" Cut edges: {:?}", result.cut_edges);
|
||||
println!(" Iterations: {}", result.iterations);
|
||||
|
||||
// The bridge should be found
|
||||
if result.cut_value == 1.0 {
|
||||
println!(" → Successfully detected the bridge!");
|
||||
}
|
||||
} else {
|
||||
println!("✗ No cut found within bound k=5");
|
||||
}
|
||||
}
|
||||
|
||||
/// Demonstrates deterministic coloring behavior
|
||||
fn demo_deterministic_coloring() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create a simple path graph: 1-2-3-4-5
|
||||
for i in 1..=4 {
|
||||
graph.insert_edge(i, i + 1, 1.0).unwrap();
|
||||
}
|
||||
|
||||
println!("Graph: Path of 5 vertices");
|
||||
|
||||
// Create two LocalKCut instances
|
||||
let lk1 = LocalKCut::new(graph.clone(), 3);
|
||||
let lk2 = LocalKCut::new(graph.clone(), 3);
|
||||
|
||||
println!("\nEdge colorings (deterministic):");
|
||||
for edge in graph.edges() {
|
||||
let color1 = lk1.edge_color(edge.id).unwrap();
|
||||
let color2 = lk2.edge_color(edge.id).unwrap();
|
||||
|
||||
println!(" Edge ({}, {}): {:?}", edge.source, edge.target, color1);
|
||||
|
||||
// Verify determinism
|
||||
assert_eq!(color1, color2, "Colors should be deterministic!");
|
||||
}
|
||||
|
||||
println!("\n✓ All edge colorings are deterministic!");
|
||||
|
||||
// Find cuts from different vertices
|
||||
println!("\nFinding cuts from different starting vertices:");
|
||||
for start_vertex in 1..=5 {
|
||||
if let Some(result) = lk1.find_cut(start_vertex) {
|
||||
println!(
|
||||
" Vertex {}: cut value = {}, set size = {}",
|
||||
start_vertex,
|
||||
result.cut_value,
|
||||
result.cut_set.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Demonstrates forest packing and witness properties
|
||||
fn demo_forest_packing() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create a more complex graph
|
||||
// 1 - 2
|
||||
// | |
|
||||
// 3 - 4 - 5
|
||||
// | |
|
||||
// 6 - 7
|
||||
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(1, 3, 1.0).unwrap();
|
||||
graph.insert_edge(2, 4, 1.0).unwrap();
|
||||
graph.insert_edge(3, 4, 1.0).unwrap();
|
||||
graph.insert_edge(4, 5, 1.0).unwrap();
|
||||
graph.insert_edge(4, 6, 1.0).unwrap();
|
||||
graph.insert_edge(5, 7, 1.0).unwrap();
|
||||
graph.insert_edge(6, 7, 1.0).unwrap();
|
||||
|
||||
println!("Graph: Complex grid-like structure");
|
||||
println!(
|
||||
"Vertices: {}, Edges: {}",
|
||||
graph.num_vertices(),
|
||||
graph.num_edges()
|
||||
);
|
||||
|
||||
// Create forest packing
|
||||
let lambda_max = 3; // Upper bound on min cut
|
||||
let epsilon = 0.1; // Approximation parameter
|
||||
|
||||
println!(
|
||||
"\nCreating forest packing with λ_max={}, ε={}...",
|
||||
lambda_max, epsilon
|
||||
);
|
||||
let packing = ForestPacking::greedy_packing(&*graph, lambda_max, epsilon);
|
||||
|
||||
println!("✓ Created {} forests", packing.num_forests());
|
||||
|
||||
// Show forest structures
|
||||
for i in 0..packing.num_forests().min(3) {
|
||||
if let Some(forest) = packing.forest(i) {
|
||||
println!(" Forest {}: {} edges", i, forest.len());
|
||||
}
|
||||
}
|
||||
|
||||
// Find a cut and check witness property
|
||||
let local_kcut = LocalKCut::new(graph.clone(), 5);
|
||||
if let Some(result) = local_kcut.find_cut(1) {
|
||||
println!("\nFound cut with value {}", result.cut_value);
|
||||
|
||||
let is_witnessed = packing.witnesses_cut(&result.cut_edges);
|
||||
println!(" Witnessed by all forests: {}", is_witnessed);
|
||||
|
||||
if is_witnessed {
|
||||
println!(" ✓ Cut satisfies witness property!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare local and global minimum cuts
|
||||
fn demo_local_vs_global() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create a graph where local and global cuts differ
|
||||
// 1 - 2 - 3
|
||||
// | | |
|
||||
// 4 - 5 - 6
|
||||
// | | |
|
||||
// 7 - 8 - 9
|
||||
|
||||
// Top row
|
||||
graph.insert_edge(1, 2, 2.0).unwrap();
|
||||
graph.insert_edge(2, 3, 2.0).unwrap();
|
||||
|
||||
// Middle row
|
||||
graph.insert_edge(4, 5, 2.0).unwrap();
|
||||
graph.insert_edge(5, 6, 2.0).unwrap();
|
||||
|
||||
// Bottom row
|
||||
graph.insert_edge(7, 8, 2.0).unwrap();
|
||||
graph.insert_edge(8, 9, 2.0).unwrap();
|
||||
|
||||
// Vertical connections (weaker)
|
||||
graph.insert_edge(1, 4, 1.0).unwrap();
|
||||
graph.insert_edge(2, 5, 1.0).unwrap();
|
||||
graph.insert_edge(3, 6, 1.0).unwrap();
|
||||
graph.insert_edge(4, 7, 1.0).unwrap();
|
||||
graph.insert_edge(5, 8, 1.0).unwrap();
|
||||
graph.insert_edge(6, 9, 1.0).unwrap();
|
||||
|
||||
println!("Graph: 3x3 grid with different edge weights");
|
||||
println!(
|
||||
"Vertices: {}, Edges: {}",
|
||||
graph.num_vertices(),
|
||||
graph.num_edges()
|
||||
);
|
||||
|
||||
// Find local cuts from different vertices
|
||||
let local_kcut = LocalKCut::new(graph.clone(), 10);
|
||||
|
||||
println!("\nLocal cuts from different vertices:");
|
||||
for vertex in &[1, 5, 9] {
|
||||
if let Some(result) = local_kcut.find_cut(*vertex) {
|
||||
println!(
|
||||
" Vertex {}: cut value = {}, iterations = {}",
|
||||
vertex, result.cut_value, result.iterations
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Build global minimum cut (using the algorithm)
|
||||
let mut mincut = MinCutBuilder::new().exact().build().unwrap();
|
||||
|
||||
// Add edges to global mincut
|
||||
for edge in graph.edges() {
|
||||
let _ = mincut.insert_edge(edge.source, edge.target, edge.weight);
|
||||
}
|
||||
|
||||
let global_value = mincut.min_cut_value();
|
||||
println!("\nGlobal minimum cut value: {}", global_value);
|
||||
|
||||
println!("\n✓ Local cuts provide fast approximations to global minimum cut");
|
||||
}
|
||||
|
||||
/// Analyze a complex graph with multiple cut candidates
|
||||
fn demo_complex_graph() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create a graph with multiple communities
|
||||
// Community 1: clique {1,2,3,4}
|
||||
for i in 1..=4 {
|
||||
for j in i + 1..=4 {
|
||||
graph.insert_edge(i, j, 2.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Community 2: clique {5,6,7,8}
|
||||
for i in 5..=8 {
|
||||
for j in i + 1..=8 {
|
||||
graph.insert_edge(i, j, 2.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Weak connections between communities
|
||||
graph.insert_edge(4, 5, 0.5).unwrap();
|
||||
graph.insert_edge(3, 6, 0.5).unwrap();
|
||||
|
||||
println!("Graph: Two dense communities with weak connections");
|
||||
println!(
|
||||
"Vertices: {}, Edges: {}",
|
||||
graph.num_vertices(),
|
||||
graph.num_edges()
|
||||
);
|
||||
|
||||
let stats = graph.stats();
|
||||
println!("Average degree: {:.2}", stats.avg_degree);
|
||||
println!("Total weight: {:.2}", stats.total_weight);
|
||||
|
||||
// Find local cuts
|
||||
let local_kcut = LocalKCut::new(graph.clone(), 5);
|
||||
|
||||
println!("\nSearching for cuts with k=5...");
|
||||
|
||||
// Try from community 1
|
||||
if let Some(result) = local_kcut.find_cut(1) {
|
||||
println!(" From community 1:");
|
||||
println!(" Cut value: {}", result.cut_value);
|
||||
println!(
|
||||
" Separates {} vertices from {}",
|
||||
result.cut_set.len(),
|
||||
graph.num_vertices() - result.cut_set.len()
|
||||
);
|
||||
}
|
||||
|
||||
// Try from community 2
|
||||
if let Some(result) = local_kcut.find_cut(5) {
|
||||
println!(" From community 2:");
|
||||
println!(" Cut value: {}", result.cut_value);
|
||||
println!(
|
||||
" Separates {} vertices from {}",
|
||||
result.cut_set.len(),
|
||||
graph.num_vertices() - result.cut_set.len()
|
||||
);
|
||||
}
|
||||
|
||||
// Enumerate paths to understand graph structure
|
||||
println!("\nPath enumeration from vertex 1:");
|
||||
let paths = local_kcut.enumerate_paths(1, 2);
|
||||
println!(" Found {} distinct reachable sets at depth 2", paths.len());
|
||||
|
||||
// Show diversity of reachable sets
|
||||
let mut sizes: Vec<_> = paths.iter().map(|p| p.len()).collect();
|
||||
sizes.sort_unstable();
|
||||
sizes.dedup();
|
||||
println!(" Reachable set sizes: {:?}", sizes);
|
||||
|
||||
println!("\n✓ LocalKCut successfully analyzes community structure");
|
||||
}
|
||||
97
vendor/ruvector/crates/ruvector-mincut/examples/sparsify_demo.rs
vendored
Normal file
97
vendor/ruvector/crates/ruvector-mincut/examples/sparsify_demo.rs
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
//! Demonstration of graph sparsification for approximate minimum cuts
|
||||
|
||||
use ruvector_mincut::graph::DynamicGraph;
|
||||
use ruvector_mincut::sparsify::{karger_sparsify, NagamochiIbaraki, SparseGraph, SparsifyConfig};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn main() {
|
||||
println!("=== Graph Sparsification Demo ===\n");
|
||||
|
||||
// Create a sample graph (complete graph on 10 vertices)
|
||||
println!("Creating complete graph with 10 vertices...");
|
||||
let graph = create_complete_graph(10);
|
||||
println!(
|
||||
"Original graph: {} vertices, {} edges\n",
|
||||
graph.num_vertices(),
|
||||
graph.num_edges()
|
||||
);
|
||||
|
||||
// Demo 1: Benczúr-Karger sparsification
|
||||
println!("--- Benczúr-Karger Sparsification ---");
|
||||
demo_benczur_karger(&graph);
|
||||
|
||||
// Demo 2: Karger sparsification (convenience function)
|
||||
println!("\n--- Karger Sparsification (convenience) ---");
|
||||
demo_karger(&graph);
|
||||
|
||||
// Demo 3: Nagamochi-Ibaraki deterministic sparsification
|
||||
println!("\n--- Nagamochi-Ibaraki Deterministic Sparsification ---");
|
||||
demo_nagamochi_ibaraki(&graph);
|
||||
|
||||
println!("\n=== Demo Complete ===");
|
||||
}
|
||||
|
||||
fn create_complete_graph(n: usize) -> DynamicGraph {
|
||||
let g = DynamicGraph::new();
|
||||
for i in 0..n {
|
||||
for j in (i + 1)..n {
|
||||
g.insert_edge(i as u64, j as u64, 1.0).unwrap();
|
||||
}
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
fn demo_benczur_karger(graph: &DynamicGraph) {
|
||||
let epsilons = vec![0.1, 0.2, 0.5];
|
||||
|
||||
for epsilon in epsilons {
|
||||
let config = SparsifyConfig::new(epsilon).unwrap().with_seed(42);
|
||||
|
||||
let sparse = SparseGraph::from_graph(graph, config).unwrap();
|
||||
|
||||
println!(
|
||||
" ε = {:.2}: {} edges ({:.1}% of original)",
|
||||
epsilon,
|
||||
sparse.num_edges(),
|
||||
sparse.sparsification_ratio() * 100.0
|
||||
);
|
||||
|
||||
let approx_cut = sparse.approximate_min_cut();
|
||||
println!(" Approximate min cut: {:.2}", approx_cut);
|
||||
}
|
||||
}
|
||||
|
||||
fn demo_karger(graph: &DynamicGraph) {
|
||||
let epsilon = 0.15;
|
||||
let sparse = karger_sparsify(graph, epsilon, Some(123)).unwrap();
|
||||
|
||||
println!(
|
||||
" ε = {:.2}: {} edges ({:.1}% of original)",
|
||||
epsilon,
|
||||
sparse.num_edges(),
|
||||
sparse.sparsification_ratio() * 100.0
|
||||
);
|
||||
}
|
||||
|
||||
fn demo_nagamochi_ibaraki(graph: &DynamicGraph) {
|
||||
let ni = NagamochiIbaraki::new(Arc::new(graph.clone()));
|
||||
|
||||
let k_values = vec![2, 3, 5];
|
||||
|
||||
for k in k_values {
|
||||
match ni.sparse_k_certificate(k) {
|
||||
Ok(sparse) => {
|
||||
let ratio = sparse.num_edges() as f64 / graph.num_edges() as f64;
|
||||
println!(
|
||||
" k = {}: {} edges ({:.1}% of original)",
|
||||
k,
|
||||
sparse.num_edges(),
|
||||
ratio * 100.0
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" k = {}: Error - {}", k, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
156
vendor/ruvector/crates/ruvector-mincut/examples/subpoly_bench.rs
vendored
Normal file
156
vendor/ruvector/crates/ruvector-mincut/examples/subpoly_bench.rs
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
//! Benchmark for SubpolynomialMinCut
|
||||
//!
|
||||
//! Demonstrates subpolynomial update performance.
|
||||
|
||||
use ruvector_mincut::subpolynomial::{SubpolyConfig, SubpolynomialMinCut};
|
||||
use std::time::Instant;
|
||||
|
||||
fn main() {
|
||||
println!("=== SubpolynomialMinCut Benchmark ===\n");
|
||||
|
||||
// Test different graph sizes
|
||||
for &n in &[100, 500, 1000, 5000] {
|
||||
benchmark_size(n);
|
||||
}
|
||||
|
||||
println!("\n=== Complexity Verification ===\n");
|
||||
verify_subpolynomial_complexity();
|
||||
}
|
||||
|
||||
fn benchmark_size(n: usize) {
|
||||
println!("Graph size: {} vertices", n);
|
||||
|
||||
let mut mincut = SubpolynomialMinCut::for_size(n);
|
||||
|
||||
// Build a random-ish graph
|
||||
let build_start = Instant::now();
|
||||
|
||||
// Path + cross edges for connectivity
|
||||
for i in 0..(n as u64 - 1) {
|
||||
mincut.insert_edge(i, i + 1, 1.0).unwrap();
|
||||
}
|
||||
|
||||
// Add cross edges
|
||||
for i in (0..n as u64).step_by(10) {
|
||||
let j = (i + n as u64 / 2) % n as u64;
|
||||
if i != j {
|
||||
let _ = mincut.insert_edge(i, j, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
println!(" Build graph: {:?}", build_start.elapsed());
|
||||
|
||||
// Build hierarchy
|
||||
let hier_start = Instant::now();
|
||||
mincut.build();
|
||||
println!(" Build hierarchy: {:?}", hier_start.elapsed());
|
||||
|
||||
let stats = mincut.hierarchy_stats();
|
||||
println!(
|
||||
" Levels: {}, Expanders: {}",
|
||||
stats.num_levels, stats.total_expanders
|
||||
);
|
||||
|
||||
// Benchmark updates
|
||||
let num_updates = 100;
|
||||
let update_start = Instant::now();
|
||||
|
||||
for i in 0..num_updates {
|
||||
let u = (i * 7) as u64 % n as u64;
|
||||
let v = (i * 13 + 5) as u64 % n as u64;
|
||||
if u != v {
|
||||
let _ = mincut.insert_edge(u, v, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
let update_time = update_start.elapsed();
|
||||
let avg_update_us = update_time.as_micros() as f64 / num_updates as f64;
|
||||
|
||||
println!(
|
||||
" {} updates: {:?} ({:.2} μs/update)",
|
||||
num_updates, update_time, avg_update_us
|
||||
);
|
||||
println!(" Min cut: {:.1}", mincut.min_cut_value());
|
||||
|
||||
let recourse = mincut.recourse_stats();
|
||||
println!(
|
||||
" Avg recourse: {:.2}, Is subpolynomial: {}",
|
||||
recourse.amortized_recourse(),
|
||||
recourse.is_subpolynomial(n)
|
||||
);
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
fn verify_subpolynomial_complexity() {
|
||||
// Compare update time scaling
|
||||
let sizes = [100, 200, 400, 800, 1600];
|
||||
let mut results = Vec::new();
|
||||
|
||||
for &n in &sizes {
|
||||
let mut mincut = SubpolynomialMinCut::for_size(n);
|
||||
|
||||
// Build graph
|
||||
for i in 0..(n as u64 - 1) {
|
||||
mincut.insert_edge(i, i + 1, 1.0).unwrap();
|
||||
}
|
||||
for i in (0..n as u64).step_by(5) {
|
||||
let j = (i + n as u64 / 3) % n as u64;
|
||||
if i != j {
|
||||
let _ = mincut.insert_edge(i, j, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
mincut.build();
|
||||
|
||||
// Measure updates
|
||||
let num_updates = 50;
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..num_updates {
|
||||
let u = (i * 11) as u64 % n as u64;
|
||||
let v = (i * 17 + 3) as u64 % n as u64;
|
||||
if u != v {
|
||||
let _ = mincut.insert_edge(u, v, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
let avg_us = start.elapsed().as_micros() as f64 / num_updates as f64;
|
||||
results.push((n, avg_us));
|
||||
}
|
||||
|
||||
println!("Size\tAvg Update (μs)\tScaling");
|
||||
println!("----\t---------------\t-------");
|
||||
|
||||
for i in 0..results.len() {
|
||||
let (n, time) = results[i];
|
||||
let scaling = if i > 0 {
|
||||
let (prev_n, prev_time) = results[i - 1];
|
||||
let n_ratio = n as f64 / prev_n as f64;
|
||||
let time_ratio = time / prev_time;
|
||||
let exponent = time_ratio.log2() / n_ratio.log2();
|
||||
format!("n^{:.2}", exponent)
|
||||
} else {
|
||||
"-".to_string()
|
||||
};
|
||||
|
||||
println!("{}\t{:.2}\t\t{}", n, time, scaling);
|
||||
}
|
||||
|
||||
// For subpolynomial: exponent should approach 0 as n grows
|
||||
let last_ratio = results.last().unwrap().1 / results.first().unwrap().1;
|
||||
let size_ratio = sizes.last().unwrap() / sizes.first().unwrap();
|
||||
let overall_exponent = last_ratio.log2() / (size_ratio as f64).log2();
|
||||
|
||||
println!("\nOverall scaling: n^{:.2}", overall_exponent);
|
||||
println!("For subpolynomial, expect exponent → 0 as n → ∞");
|
||||
println!(
|
||||
"Current exponent ({:.2}) is {} polynomial",
|
||||
overall_exponent,
|
||||
if overall_exponent < 0.5 {
|
||||
"sub"
|
||||
} else {
|
||||
"super"
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user