1use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub enum MultipartiteMode {
65 All,
69 Out,
72 In,
75}
76
77#[derive(Debug, Clone)]
83pub struct FullMultipartite {
84 pub graph: Graph,
86 pub types: Vec<u32>,
88}
89
90pub fn full_multipartite(
121 partitions: &[u32],
122 directed: bool,
123 mode: MultipartiteMode,
124) -> IgraphResult<FullMultipartite> {
125 let no_of_types = partitions.len();
126
127 if no_of_types == 0 {
128 let graph = Graph::new(0, directed)?;
129 return Ok(FullMultipartite {
130 graph,
131 types: Vec::new(),
132 });
133 }
134
135 let mut n_acc: Vec<u64> = Vec::with_capacity(no_of_types + 1);
139 n_acc.push(0);
140 for &n_i in partitions {
141 let next = n_acc[n_acc.len() - 1]
142 .checked_add(u64::from(n_i))
143 .ok_or_else(|| {
144 IgraphError::InvalidArgument(
145 "full_multipartite: cumulative partition size overflows u64".to_string(),
146 )
147 })?;
148 n_acc.push(next);
149 }
150 let total_v_u64 = n_acc[no_of_types];
151 let total_v = u32::try_from(total_v_u64).map_err(|_| {
152 IgraphError::InvalidArgument(format!(
153 "full_multipartite: total vertex count {total_v_u64} cannot fit u32"
154 ))
155 })?;
156
157 let total_v_usize = usize::try_from(total_v).map_err(|_| {
163 IgraphError::InvalidArgument(format!(
164 "full_multipartite: total vertex count {total_v} cannot fit usize"
165 ))
166 })?;
167 let mut sum_2e: usize = 0;
168 for &n_i in partitions {
169 let n_i_us = n_i as usize;
170 let partial = total_v_usize
172 .checked_sub(n_i_us)
173 .and_then(|d| d.checked_mul(n_i_us))
174 .ok_or_else(|| {
175 IgraphError::InvalidArgument(
176 "full_multipartite: partition product overflows usize".to_string(),
177 )
178 })?;
179 sum_2e = sum_2e.checked_add(partial).ok_or_else(|| {
180 IgraphError::InvalidArgument("full_multipartite: edge sum overflows usize".to_string())
181 })?;
182 }
183 let e_undirected = sum_2e / 2;
187 let edge_count = if directed && mode == MultipartiteMode::All {
188 sum_2e
189 } else {
190 e_undirected
191 };
192
193 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(edge_count);
194
195 if no_of_types >= 2 {
207 for from_type in 0..(no_of_types - 1) {
208 #[allow(clippy::cast_possible_truncation)]
211 let from_base = n_acc[from_type] as VertexId;
212 for i in 0..partitions[from_type] {
213 #[allow(clippy::cast_possible_truncation)]
214 let edge_from = from_base + i as VertexId;
215 for to_type in (from_type + 1)..no_of_types {
216 #[allow(clippy::cast_possible_truncation)]
217 let to_base = n_acc[to_type] as VertexId;
218 for j in 0..partitions[to_type] {
219 #[allow(clippy::cast_possible_truncation)]
220 let edge_to = to_base + j as VertexId;
221 if !directed || mode == MultipartiteMode::Out {
222 edges.push((edge_from, edge_to));
223 } else if mode == MultipartiteMode::In {
224 edges.push((edge_to, edge_from));
225 } else {
226 edges.push((edge_from, edge_to));
228 edges.push((edge_to, edge_from));
229 }
230 }
231 }
232 }
233 }
234 }
235 debug_assert_eq!(edges.len(), edge_count);
236
237 let mut types: Vec<u32> = Vec::with_capacity(total_v_usize);
240 if total_v_usize > 0 {
241 let mut v: usize = 1;
242 for i in 0..total_v_usize {
243 while v < no_of_types && (i as u64) == n_acc[v] {
246 v += 1;
247 }
248 let part_idx = u32::try_from(v - 1).map_err(|_| {
250 IgraphError::InvalidArgument(
251 "full_multipartite: partition index overflows u32".to_string(),
252 )
253 })?;
254 types.push(part_idx);
255 }
256 }
257 debug_assert_eq!(types.len(), total_v_usize);
258
259 let mut graph = Graph::new(total_v, directed)?;
260 graph.add_edges(edges)?;
261 Ok(FullMultipartite { graph, types })
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use std::collections::BTreeSet;
268
269 fn dump_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
270 let ec = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
271 (0..ec)
272 .map(|e| g.edge(e).expect("edge id in range"))
273 .collect()
274 }
275
276 fn canonical_undirected(g: &Graph) -> BTreeSet<(VertexId, VertexId)> {
277 dump_edges(g)
278 .into_iter()
279 .map(|(u, v)| if u <= v { (u, v) } else { (v, u) })
280 .collect()
281 }
282
283 fn partition_of(types: &[u32], v: VertexId) -> u32 {
284 types[v as usize]
285 }
286
287 #[test]
290 fn empty_partitions_directed_all() {
291 let r = full_multipartite(&[], true, MultipartiteMode::All).expect("ok");
292 assert_eq!(r.graph.vcount(), 0);
293 assert_eq!(r.graph.ecount(), 0);
294 assert!(r.graph.is_directed());
295 assert!(r.types.is_empty());
296 }
297
298 #[test]
299 fn empty_partitions_undirected_all() {
300 let r = full_multipartite(&[], false, MultipartiteMode::All).expect("ok");
301 assert_eq!(r.graph.vcount(), 0);
302 assert_eq!(r.graph.ecount(), 0);
303 assert!(!r.graph.is_directed());
304 assert!(r.types.is_empty());
305 }
306
307 #[test]
308 fn single_partition_n4_directed_all() {
309 let r = full_multipartite(&[4], true, MultipartiteMode::All).expect("ok");
311 assert_eq!(r.graph.vcount(), 4);
312 assert_eq!(r.graph.ecount(), 0);
313 assert!(r.graph.is_directed());
314 assert_eq!(r.types, vec![0, 0, 0, 0]);
315 }
316
317 #[test]
318 fn all_zero_partitions_directed_all() {
319 let r = full_multipartite(&[0, 0, 0], true, MultipartiteMode::All).expect("ok");
321 assert_eq!(r.graph.vcount(), 0);
322 assert_eq!(r.graph.ecount(), 0);
323 assert!(r.types.is_empty());
324 }
325
326 #[test]
329 fn directed_three_partitions_all_2_3_3() {
330 let r = full_multipartite(&[2, 3, 3], true, MultipartiteMode::All).expect("ok");
332 assert_eq!(r.graph.vcount(), 8);
333 assert_eq!(r.graph.ecount(), 42);
334 assert_eq!(r.types, vec![0, 0, 1, 1, 1, 2, 2, 2]);
335 let expected: Vec<(u32, u32)> = vec![
337 (0, 2),
338 (2, 0),
339 (0, 3),
340 (3, 0),
341 (0, 4),
342 (4, 0),
343 (0, 5),
344 (5, 0),
345 (0, 6),
346 (6, 0),
347 (0, 7),
348 (7, 0),
349 (1, 2),
350 (2, 1),
351 (1, 3),
352 (3, 1),
353 (1, 4),
354 (4, 1),
355 (1, 5),
356 (5, 1),
357 (1, 6),
358 (6, 1),
359 (1, 7),
360 (7, 1),
361 (2, 5),
362 (5, 2),
363 (2, 6),
364 (6, 2),
365 (2, 7),
366 (7, 2),
367 (3, 5),
368 (5, 3),
369 (3, 6),
370 (6, 3),
371 (3, 7),
372 (7, 3),
373 (4, 5),
374 (5, 4),
375 (4, 6),
376 (6, 4),
377 (4, 7),
378 (7, 4),
379 ];
380 assert_eq!(dump_edges(&r.graph), expected);
381 }
382
383 #[test]
384 fn directed_four_partitions_in_2_3_4_2() {
385 let r = full_multipartite(&[2, 3, 4, 2], true, MultipartiteMode::In).expect("ok");
387 assert_eq!(r.graph.vcount(), 11);
388 assert_eq!(r.graph.ecount(), 44);
389 assert_eq!(r.types, vec![0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 3]);
390 let expected: Vec<(u32, u32)> = vec![
391 (2, 0),
392 (3, 0),
393 (4, 0),
394 (5, 0),
395 (6, 0),
396 (7, 0),
397 (8, 0),
398 (9, 0),
399 (10, 0),
400 (2, 1),
401 (3, 1),
402 (4, 1),
403 (5, 1),
404 (6, 1),
405 (7, 1),
406 (8, 1),
407 (9, 1),
408 (10, 1),
409 (5, 2),
410 (6, 2),
411 (7, 2),
412 (8, 2),
413 (9, 2),
414 (10, 2),
415 (5, 3),
416 (6, 3),
417 (7, 3),
418 (8, 3),
419 (9, 3),
420 (10, 3),
421 (5, 4),
422 (6, 4),
423 (7, 4),
424 (8, 4),
425 (9, 4),
426 (10, 4),
427 (9, 5),
428 (10, 5),
429 (9, 6),
430 (10, 6),
431 (9, 7),
432 (10, 7),
433 (9, 8),
434 (10, 8),
435 ];
436 assert_eq!(dump_edges(&r.graph), expected);
437 }
438
439 #[test]
440 fn undirected_four_partitions_all_2_3_4_2() {
441 let r = full_multipartite(&[2, 3, 4, 2], false, MultipartiteMode::All).expect("ok");
443 assert_eq!(r.graph.vcount(), 11);
444 assert_eq!(r.graph.ecount(), 44);
445 assert!(!r.graph.is_directed());
446 assert_eq!(r.types, vec![0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 3]);
447 }
448
449 #[test]
450 fn directed_one_empty_partition_2_0_3() {
451 let r = full_multipartite(&[2, 0, 3], true, MultipartiteMode::All).expect("ok");
453 assert_eq!(r.graph.vcount(), 5);
454 assert_eq!(r.graph.ecount(), 12);
455 assert_eq!(r.types, vec![0, 0, 2, 2, 2]);
458 }
459
460 #[test]
463 fn directed_out_count_matches_undirected_for_same_partitions() {
464 let parts = [3u32, 4, 2];
465 let undir = full_multipartite(&parts, false, MultipartiteMode::All).expect("ok");
466 let out = full_multipartite(&parts, true, MultipartiteMode::Out).expect("ok");
467 let in_ = full_multipartite(&parts, true, MultipartiteMode::In).expect("ok");
468 let all = full_multipartite(&parts, true, MultipartiteMode::All).expect("ok");
469 assert_eq!(undir.graph.ecount(), out.graph.ecount());
470 assert_eq!(undir.graph.ecount(), in_.graph.ecount());
471 assert_eq!(all.graph.ecount(), 2 * undir.graph.ecount());
472 }
473
474 #[test]
475 fn out_and_in_have_reversed_arcs() {
476 let parts = [2u32, 3, 1];
477 let out = full_multipartite(&parts, true, MultipartiteMode::Out).expect("ok");
478 let in_ = full_multipartite(&parts, true, MultipartiteMode::In).expect("ok");
479 let out_edges: BTreeSet<(VertexId, VertexId)> =
480 dump_edges(&out.graph).into_iter().collect();
481 let in_reversed: BTreeSet<(VertexId, VertexId)> = dump_edges(&in_.graph)
482 .into_iter()
483 .map(|(a, b)| (b, a))
484 .collect();
485 assert_eq!(out_edges, in_reversed);
486 }
487
488 #[test]
489 fn no_intra_partition_edges() {
490 let parts = [2u32, 3, 4, 2];
491 let r = full_multipartite(&parts, true, MultipartiteMode::All).expect("ok");
492 for (u, v) in dump_edges(&r.graph) {
493 assert_ne!(
494 partition_of(&r.types, u),
495 partition_of(&r.types, v),
496 "edge ({u}, {v}) connects same-partition vertices"
497 );
498 }
499 }
500
501 #[test]
502 fn types_are_monotone_non_decreasing() {
503 let r = full_multipartite(&[3, 0, 2, 1, 4], false, MultipartiteMode::All).expect("ok");
505 for w in r.types.windows(2) {
506 assert!(w[0] <= w[1], "types {:?} must be monotone", r.types);
507 }
508 }
509
510 #[test]
511 fn vcount_equals_partition_sum() {
512 let parts = [5u32, 7, 0, 3];
513 let r = full_multipartite(&parts, true, MultipartiteMode::All).expect("ok");
514 let want: u64 = parts.iter().map(|&x| u64::from(x)).sum();
515 assert_eq!(u64::from(r.graph.vcount()), want);
516 }
517
518 #[test]
519 fn directed_out_undirected_share_canonical_multiset() {
520 let parts = [3u32, 4, 2];
521 let undir = full_multipartite(&parts, false, MultipartiteMode::All).expect("ok");
522 let out = full_multipartite(&parts, true, MultipartiteMode::Out).expect("ok");
523 assert_eq!(
524 canonical_undirected(&undir.graph),
525 canonical_undirected(&out.graph),
526 );
527 }
528}
529
530#[cfg(all(test, feature = "proptest-harness"))]
531mod proptests {
532 use super::*;
533 use proptest::prelude::*;
534
535 fn arb_partitions() -> impl Strategy<Value = Vec<u32>> {
536 prop::collection::vec(0u32..=8, 0..=6)
538 }
539
540 proptest! {
541 #[test]
542 fn pp_vcount_equals_partition_sum(parts in arb_partitions()) {
543 let r = full_multipartite(&parts, true, MultipartiteMode::All).expect("ok");
544 let want: u64 = parts.iter().map(|&x| u64::from(x)).sum();
545 prop_assert_eq!(r.graph.vcount() as u64, want);
546 prop_assert_eq!(r.types.len() as u64, want);
547 }
548
549 #[test]
550 fn pp_no_intra_partition_edges(parts in arb_partitions()) {
551 let r = full_multipartite(&parts, true, MultipartiteMode::All).expect("ok");
552 let ec = u32::try_from(r.graph.ecount()).unwrap();
553 for e in 0..ec {
554 let (u, v) = r.graph.edge(e).expect("edge in range");
555 prop_assert_ne!(r.types[u as usize], r.types[v as usize]);
556 }
557 }
558
559 #[test]
560 fn pp_all_doubles_out_ecount(parts in arb_partitions()) {
561 let out = full_multipartite(&parts, true, MultipartiteMode::Out).expect("ok");
562 let all = full_multipartite(&parts, true, MultipartiteMode::All).expect("ok");
563 prop_assert_eq!(all.graph.ecount(), 2 * out.graph.ecount());
564 }
565
566 #[test]
567 fn pp_out_undirected_same_canonical_multiset(parts in arb_partitions()) {
568 let und = full_multipartite(&parts, false, MultipartiteMode::All).expect("ok");
569 let out = full_multipartite(&parts, true, MultipartiteMode::Out).expect("ok");
570 let canon = |g: &Graph| -> std::collections::BTreeSet<(VertexId, VertexId)> {
571 let ec = u32::try_from(g.ecount()).unwrap();
572 (0..ec)
573 .map(|e| g.edge(e).expect("ok"))
574 .map(|(a, b)| if a <= b { (a, b) } else { (b, a) })
575 .collect()
576 };
577 prop_assert_eq!(canon(&und.graph), canon(&out.graph));
578 }
579
580 #[test]
581 fn pp_types_monotone_non_decreasing(parts in arb_partitions()) {
582 let r = full_multipartite(&parts, false, MultipartiteMode::All).expect("ok");
583 for w in r.types.windows(2) {
584 prop_assert!(w[0] <= w[1]);
585 }
586 }
587
588 #[test]
589 fn pp_in_reverses_out(parts in arb_partitions()) {
590 let out = full_multipartite(&parts, true, MultipartiteMode::Out).expect("ok");
591 let in_ = full_multipartite(&parts, true, MultipartiteMode::In).expect("ok");
592 prop_assert_eq!(out.graph.ecount(), in_.graph.ecount());
593 let out_set: std::collections::BTreeSet<(VertexId, VertexId)> = {
594 let ec = u32::try_from(out.graph.ecount()).unwrap();
595 (0..ec).map(|e| out.graph.edge(e).expect("ok")).collect()
596 };
597 let in_reversed: std::collections::BTreeSet<(VertexId, VertexId)> = {
598 let ec = u32::try_from(in_.graph.ecount()).unwrap();
599 (0..ec)
600 .map(|e| in_.graph.edge(e).expect("ok"))
601 .map(|(a, b)| (b, a))
602 .collect()
603 };
604 prop_assert_eq!(out_set, in_reversed);
605 }
606 }
607}