Skip to main content

tnc/contractionpath/contraction_tree/
balancing.rs

1//! Functionality for greedy balancing of tensor network partitions.
2
3use core::f64;
4use std::rc::Rc;
5
6use itertools::Itertools;
7use log::debug;
8use rand::seq::IndexedRandom;
9use rand::{rngs::StdRng, Rng};
10use rustc_hash::FxHashMap;
11
12use crate::contractionpath::communication_schemes::CommunicationScheme;
13use crate::contractionpath::contraction_cost::communication_path_op_costs;
14use crate::contractionpath::contraction_tree::{
15    utils::{characterize_partition, subtree_contraction_path},
16    ContractionTree,
17};
18use crate::contractionpath::paths::validate_path;
19use crate::contractionpath::{ContractionPath, SimplePath};
20use crate::tensornetwork::tensor::{CompositeTensor, LeafTensor, TensorIndex, TensorType};
21
22mod balancing_schemes;
23
24pub use balancing_schemes::BalancingScheme;
25
26#[derive(Debug, Clone, Copy)]
27pub struct BalanceSettings<R>
28where
29    R: Rng,
30{
31    /// If not None, randomly chooses from top `usize` options. Random choice is
32    /// weighted by objective outcome.
33    random_balance: Option<(usize, R)>,
34    rebalance_depth: usize,
35    iterations: usize,
36    objective_function: fn(&LeafTensor, &LeafTensor) -> f64,
37    communication_scheme: CommunicationScheme,
38    balancing_scheme: BalancingScheme,
39    memory_limit: Option<f64>,
40}
41
42impl BalanceSettings<StdRng> {
43    pub fn new(
44        rebalance_depth: usize,
45        iterations: usize,
46        objective_function: fn(&LeafTensor, &LeafTensor) -> f64,
47        communication_scheme: CommunicationScheme,
48        balancing_scheme: BalancingScheme,
49        memory_limit: Option<f64>,
50    ) -> Self {
51        BalanceSettings::<StdRng> {
52            random_balance: None,
53            rebalance_depth,
54            iterations,
55            objective_function,
56            communication_scheme,
57            balancing_scheme,
58            memory_limit,
59        }
60    }
61}
62
63impl<R> BalanceSettings<R>
64where
65    R: Rng,
66{
67    pub fn new_random(
68        random_balance: Option<(usize, R)>,
69        rebalance_depth: usize,
70        iterations: usize,
71        objective_function: fn(&LeafTensor, &LeafTensor) -> f64,
72        communication_scheme: CommunicationScheme,
73        balancing_scheme: BalancingScheme,
74        memory_limit: Option<f64>,
75    ) -> Self {
76        BalanceSettings::<R> {
77            random_balance,
78            rebalance_depth,
79            iterations,
80            objective_function,
81            communication_scheme,
82            balancing_scheme,
83            memory_limit,
84        }
85    }
86}
87
88#[derive(Debug, Clone)]
89pub(crate) struct PartitionData {
90    pub id: usize,
91    pub flop_cost: f64,
92    pub mem_cost: f64,
93    pub contraction: SimplePath,
94    pub local_tensor: LeafTensor,
95}
96
97/// Balances a partitioned tensor network to greedily optimize for a given objective.
98pub fn balance_partitions_iter<R>(
99    tensor_network: &CompositeTensor,
100    path: &ContractionPath,
101    mut balance_settings: BalanceSettings<R>,
102    rng: &mut R,
103) -> (usize, CompositeTensor, ContractionPath, Vec<(f64, f64)>)
104where
105    R: Rng,
106{
107    let mut contraction_tree = ContractionTree::from_contraction_path(tensor_network, path);
108
109    let BalanceSettings {
110        rebalance_depth,
111        iterations,
112        balancing_scheme,
113        communication_scheme,
114        memory_limit,
115        ..
116    } = balance_settings;
117    let mut partition_data =
118        characterize_partition(&contraction_tree, rebalance_depth, tensor_network);
119
120    assert!(partition_data.len() > 1);
121
122    let partition_number = partition_data.len();
123
124    let (partition_tensors, partition_costs): (Vec<_>, Vec<_>) = partition_data
125        .iter()
126        .map(
127            |PartitionData {
128                 local_tensor,
129                 flop_cost,
130                 ..
131             }| (local_tensor.clone(), *flop_cost),
132        )
133        .collect();
134
135    let ((mut best_cost, sum_cost), _) = communication_path_op_costs(
136        &partition_tensors,
137        &path.toplevel,
138        true,
139        Some(&partition_costs),
140    );
141
142    let mut max_costs = Vec::with_capacity(iterations + 1);
143    max_costs.push((best_cost, sum_cost));
144
145    let mut best_iteration = 0;
146    let mut best_contraction_path = path.to_owned();
147    let mut best_tn = tensor_network.clone();
148
149    for iteration in 1..=iterations {
150        debug!("Balancing iteration {iteration} with balancing scheme {balancing_scheme:?}, communication scheme {communication_scheme:?}");
151
152        // Balances and updates partitions
153        let (nested_paths, new_tensor_network) = balance_partitions(
154            &mut partition_data,
155            &mut contraction_tree,
156            tensor_network,
157            &mut balance_settings,
158            iteration,
159        );
160
161        assert_eq!(nested_paths.len(), partition_number, "Tensors lost!");
162
163        // Ensures that children tensors are mapped to their respective partition costs
164        // Communication costs include intermediate costs
165        let communication_path = communicate_partitions(
166            &partition_data,
167            &mut contraction_tree,
168            &new_tensor_network,
169            &balance_settings,
170            Some(rng),
171        );
172
173        let (partition_tensors, partition_costs): (Vec<_>, Vec<_>) = partition_data
174            .iter()
175            .map(
176                |PartitionData {
177                     local_tensor,
178                     flop_cost,
179                     ..
180                 }| (local_tensor.clone(), *flop_cost),
181            )
182            .collect();
183
184        let ((flop_cost, sum_cost), mem_cost) = communication_path_op_costs(
185            &partition_tensors,
186            &communication_path,
187            true,
188            Some(&partition_costs),
189        );
190
191        let new_path = ContractionPath {
192            nested: nested_paths,
193            toplevel: path.toplevel.clone(),
194        };
195        validate_path(&new_path);
196
197        max_costs.push((flop_cost, sum_cost));
198        if memory_limit.is_some_and(|limit| mem_cost > limit) {
199            break;
200        }
201        if flop_cost < best_cost {
202            best_cost = flop_cost;
203            best_iteration = iteration;
204            best_tn = new_tensor_network;
205            best_contraction_path = new_path;
206        }
207    }
208
209    (best_iteration, best_tn, best_contraction_path, max_costs)
210}
211
212fn communicate_partitions<R>(
213    partition_data: &[PartitionData],
214    contraction_tree: &mut ContractionTree,
215    tensor_network: &CompositeTensor,
216    balance_settings: &BalanceSettings<R>,
217    rng: Option<&mut R>,
218) -> SimplePath
219where
220    R: Rng,
221{
222    let communication_scheme = balance_settings.communication_scheme;
223    let children_tensors = tensor_network
224        .tensors()
225        .iter()
226        .map(|t| match t.kind() {
227            TensorType::Composite => t.as_composite().unwrap().external_tensor(),
228            TensorType::Leaf => t.as_leaf().unwrap().clone(),
229        })
230        .collect_vec();
231    let latency_map = partition_data
232        .iter()
233        .enumerate()
234        .map(|(i, partition)| (i, partition.flop_cost))
235        .collect::<FxHashMap<_, _>>();
236
237    let partition_ids = partition_data
238        .iter()
239        .map(|partition| partition.id)
240        .collect_vec();
241    let communication_path =
242        communication_scheme.communication_path(&children_tensors, &latency_map, rng);
243
244    contraction_tree.replace_communication_path(partition_ids, &communication_path);
245
246    communication_path
247}
248
249fn balance_partitions<R>(
250    partition_data: &mut [PartitionData],
251    contraction_tree: &mut ContractionTree,
252    tensor_network: &CompositeTensor,
253    balance_settings: &mut BalanceSettings<R>,
254    iteration: usize,
255) -> (FxHashMap<TensorIndex, ContractionPath>, CompositeTensor)
256where
257    R: Rng,
258{
259    let BalanceSettings {
260        ref mut random_balance,
261        rebalance_depth,
262        objective_function,
263        balancing_scheme,
264        ..
265    } = balance_settings;
266    // If there are less than 3 tensors in the tn, rebalancing will not make sense.
267    if tensor_network.total_num_tensors() < 3 {
268        // TODO: should not panic, but handle gracefully
269        panic!("No rebalancing undertaken, as tn is too small (< 3 tensors)");
270    }
271    // Will cause strange errors (picking of same partition multiple times if this is not true.Better to panic here.)
272    assert!(partition_data.len() > 1);
273
274    partition_data.sort_unstable_by(|a, b| a.flop_cost.total_cmp(&b.flop_cost));
275
276    let shifted_nodes = match balancing_scheme {
277        BalancingScheme::BestWorst => balancing_schemes::best_worst(
278            partition_data,
279            contraction_tree,
280            random_balance,
281            *objective_function,
282            tensor_network,
283        ),
284        BalancingScheme::Tensor => balancing_schemes::best_tensor(
285            partition_data,
286            contraction_tree,
287            random_balance,
288            *objective_function,
289            tensor_network,
290        ),
291        BalancingScheme::Tensors => balancing_schemes::best_tensors(
292            partition_data,
293            contraction_tree,
294            random_balance,
295            *objective_function,
296            tensor_network,
297        ),
298        BalancingScheme::AlternatingTensors => {
299            if iteration % 2 == 1 {
300                balancing_schemes::tensors_odd(
301                    partition_data,
302                    contraction_tree,
303                    random_balance,
304                    *objective_function,
305                    tensor_network,
306                )
307            } else {
308                balancing_schemes::tensors_even(
309                    partition_data,
310                    contraction_tree,
311                    random_balance,
312                    *objective_function,
313                    tensor_network,
314                )
315            }
316        }
317        BalancingScheme::IntermediateTensors { height_limit } => {
318            balancing_schemes::best_intermediate_tensors(
319                partition_data,
320                contraction_tree,
321                random_balance,
322                *objective_function,
323                tensor_network,
324                *height_limit,
325            )
326        }
327        BalancingScheme::AlternatingIntermediateTensors { height_limit } => {
328            if iteration % 2 == 1 {
329                balancing_schemes::intermediate_tensors_odd(
330                    partition_data,
331                    contraction_tree,
332                    random_balance,
333                    *objective_function,
334                    tensor_network,
335                    *height_limit,
336                )
337            } else {
338                balancing_schemes::intermediate_tensors_even(
339                    partition_data,
340                    contraction_tree,
341                    random_balance,
342                    *objective_function,
343                    tensor_network,
344                    *height_limit,
345                )
346            }
347        }
348        BalancingScheme::AlternatingTreeTensors { height_limit } => {
349            if iteration % 2 == 1 {
350                balancing_schemes::tree_tensors_odd(
351                    partition_data,
352                    contraction_tree,
353                    *objective_function,
354                    tensor_network,
355                    *height_limit,
356                )
357            } else {
358                balancing_schemes::tree_tensors_even(
359                    partition_data,
360                    contraction_tree,
361                    *objective_function,
362                    tensor_network,
363                    *height_limit,
364                )
365            }
366        }
367    };
368    let mut shifted_indices = FxHashMap::default();
369    for shift in shifted_nodes {
370        let shifted_from_id = *shifted_indices
371            .get(&shift.from_subtree_id)
372            .unwrap_or(&shift.from_subtree_id);
373
374        let shifted_to_id = *shifted_indices
375            .get(&shift.to_subtree_id)
376            .unwrap_or(&shift.to_subtree_id);
377
378        let (
379            larger_id,
380            larger_contraction,
381            larger_subtree_flop_cost,
382            larger_subtree_mem_cost,
383            smaller_id,
384            smaller_contraction,
385            smaller_subtree_flop_cost,
386            smaller_subtree_mem_cost,
387        ) = shift_node_between_subtrees(
388            contraction_tree,
389            *rebalance_depth,
390            shifted_from_id,
391            shifted_to_id,
392            shift.moved_leaf_ids,
393            tensor_network,
394        );
395        shifted_indices.insert(shift.from_subtree_id, larger_id);
396        shifted_indices.insert(shift.to_subtree_id, smaller_id);
397
398        let larger_tensor = contraction_tree.tensor(larger_id, tensor_network);
399        let smaller_tensor = contraction_tree.tensor(smaller_id, tensor_network);
400
401        // Update partition data based on shift
402        for PartitionData {
403            id,
404            flop_cost,
405            mem_cost,
406            contraction: subtree_contraction,
407            local_tensor,
408        } in partition_data.iter_mut()
409        {
410            if *id == shifted_from_id {
411                *id = larger_id;
412                subtree_contraction.clone_from(&larger_contraction);
413                *flop_cost = larger_subtree_flop_cost;
414                *mem_cost = larger_subtree_mem_cost;
415                *local_tensor = larger_tensor.clone();
416            } else if *id == shifted_to_id {
417                *id = smaller_id;
418                subtree_contraction.clone_from(&smaller_contraction);
419                *flop_cost = smaller_subtree_flop_cost;
420                *mem_cost = smaller_subtree_mem_cost;
421                *local_tensor = smaller_tensor.clone();
422            }
423        }
424    }
425
426    partition_data.sort_unstable_by(
427        |PartitionData {
428             flop_cost: cost_a, ..
429         },
430         PartitionData {
431             flop_cost: cost_b, ..
432         }| { cost_a.total_cmp(cost_b) },
433    );
434
435    let mut rebalanced_paths = FxHashMap::default();
436    let (partition_tensors, partition_ids): (Vec<_>, Vec<_>) = partition_data
437        .iter()
438        .enumerate()
439        .map(
440            |(
441                i,
442                PartitionData {
443                    id,
444                    contraction: subtree_contraction,
445                    ..
446                },
447            )| {
448                rebalanced_paths.insert(i, ContractionPath::simple(subtree_contraction.clone()));
449                let leaf_ids = contraction_tree.leaf_ids(*id);
450                let leaf_tensors = leaf_ids
451                    .iter()
452                    .map(|node_id| {
453                        let nested_indices = contraction_tree
454                            .node(*node_id)
455                            .tensor_index()
456                            .cloned()
457                            .unwrap();
458                        tensor_network.nested_tensor(&nested_indices).clone()
459                    })
460                    .collect_vec();
461                let child_tensor = CompositeTensor::new(leaf_tensors);
462                (child_tensor, *id)
463            },
464        )
465        .collect();
466
467    contraction_tree
468        .partitions
469        .insert(*rebalance_depth, partition_ids);
470
471    let updated_tn = CompositeTensor::new(partition_tensors);
472    (rebalanced_paths, updated_tn)
473}
474
475/// Takes two hashmaps that contain node information. Identifies which pair of nodes from larger and smaller hashmaps maximizes the greedy cost function and returns the node from the `larger_subtree_nodes`.
476///
477/// # Arguments
478/// * `random_balance` - Allows for random selection of balanced node. If not None, identifies the best `usize` options and randomly selects one by weighted choice.
479/// * `larger_subtree_nodes` - A set of nodes used in comparison. Only the id from the larger subtree is returned.
480/// * `smaller_subtree_nodes` - A set of nodes used in comparison.
481/// * `objective_function` - Cost function that takes in two tensors and returns an f64 cost.
482fn find_rebalance_node<R>(
483    random_balance: &mut Option<(usize, R)>,
484    larger_subtree_nodes: &FxHashMap<usize, LeafTensor>,
485    smaller_subtree_nodes: &FxHashMap<usize, LeafTensor>,
486    objective_function: fn(&LeafTensor, &LeafTensor) -> f64,
487) -> (usize, f64)
488where
489    R: Rng,
490{
491    let node_comparison = larger_subtree_nodes
492        .iter()
493        .cartesian_product(smaller_subtree_nodes.iter())
494        .map(|((larger_node_id, larger_tensor), (_, smaller_tensor))| {
495            (
496                *larger_node_id,
497                objective_function(larger_tensor, smaller_tensor),
498            )
499        });
500    if let Some((options_considered, ref mut rng)) = random_balance {
501        let node_options = node_comparison
502            .sorted_unstable_by(|a, b| b.1.total_cmp(&a.1))
503            .take(*options_considered)
504            .collect_vec();
505        let max = node_options.first().unwrap().1;
506        // Initial division done here as sum of weights can cause overflow before normalization.
507        *node_options
508            .choose_weighted(rng, |node_option| node_option.1 / max)
509            .unwrap()
510    } else {
511        node_comparison.max_by(|a, b| a.1.total_cmp(&b.1)).unwrap()
512    }
513}
514
515/// Shifts `rebalance_node` from the larger subtree to the smaller subtree
516/// Updates partition tensor ids after subtrees are updated and a new contraction order is found.
517fn shift_node_between_subtrees(
518    contraction_tree: &mut ContractionTree,
519    rebalance_depth: usize,
520    larger_subtree_id: usize,
521    smaller_subtree_id: usize,
522    rebalanced_nodes: Vec<usize>,
523    tensor_network: &CompositeTensor,
524) -> (usize, SimplePath, f64, f64, usize, SimplePath, f64, f64) {
525    // Obtain parents of the two subtrees that are being updated.
526    let larger_subtree_parent_id = contraction_tree
527        .node(larger_subtree_id)
528        .parent_id()
529        .unwrap();
530    let smaller_subtree_parent_id = contraction_tree
531        .node(smaller_subtree_id)
532        .parent_id()
533        .unwrap();
534
535    let mut larger_subtree_leaf_nodes = contraction_tree.leaf_ids(larger_subtree_id);
536    let mut smaller_subtree_leaf_nodes = contraction_tree.leaf_ids(smaller_subtree_id);
537
538    // Always check that a node can be moved over.
539    assert!(rebalanced_nodes
540        .iter()
541        .all(|node| !smaller_subtree_leaf_nodes.contains(node)));
542    assert!(rebalanced_nodes
543        .iter()
544        .all(|node| larger_subtree_leaf_nodes.contains(node)));
545
546    // Remove selected tensors from bigger subtree. Add it to the smaller subtree
547    larger_subtree_leaf_nodes.retain(|leaf| !rebalanced_nodes.contains(leaf));
548    smaller_subtree_leaf_nodes.extend(rebalanced_nodes);
549
550    // Run Greedy on the two updated subtrees
551    let (updated_larger_path, local_larger_path, larger_flop_cost, larger_mem_cost) =
552        subtree_contraction_path(&larger_subtree_leaf_nodes, contraction_tree, tensor_network);
553
554    let (updated_smaller_path, local_smaller_path, smaller_flop_cost, smaller_mem_cost) =
555        subtree_contraction_path(
556            &smaller_subtree_leaf_nodes,
557            contraction_tree,
558            tensor_network,
559        );
560
561    // Remove larger subtree and add new subtree, keep track of updated root id
562    contraction_tree.remove_subtree(larger_subtree_id);
563
564    let new_larger_subtree_id = if updated_larger_path.is_empty() {
565        // In this case, there is only one node left.
566        contraction_tree.nodes[&larger_subtree_leaf_nodes[0]]
567            .borrow_mut()
568            .set_parent(Rc::downgrade(
569                &contraction_tree.nodes[&larger_subtree_parent_id],
570            ));
571        contraction_tree.nodes[&larger_subtree_parent_id]
572            .borrow_mut()
573            .add_child(Rc::downgrade(
574                &contraction_tree.nodes[&larger_subtree_leaf_nodes[0]],
575            ));
576        larger_subtree_leaf_nodes[0]
577    } else {
578        contraction_tree.add_path_as_subtree(
579            &ContractionPath::simple(updated_larger_path),
580            larger_subtree_parent_id,
581            &larger_subtree_leaf_nodes,
582        )
583    };
584
585    // Remove smaller subtree
586    contraction_tree.remove_subtree(smaller_subtree_id);
587    // Add new subtree, keep track of updated root id
588    let new_smaller_subtree_id = contraction_tree.add_path_as_subtree(
589        &ContractionPath::simple(updated_smaller_path),
590        smaller_subtree_parent_id,
591        &smaller_subtree_leaf_nodes,
592    );
593
594    // Remove the old partition ids from the `ContractionTree` partitions member as the intermediate tensor id will be updated and then add the updated partition numbers.`
595    let partition = contraction_tree
596        .partitions
597        .get_mut(&rebalance_depth)
598        .unwrap();
599    partition.retain(|&e| e != smaller_subtree_id && e != larger_subtree_id);
600    partition.push(new_larger_subtree_id);
601    partition.push(new_smaller_subtree_id);
602
603    (
604        new_larger_subtree_id,
605        local_larger_path,
606        larger_flop_cost,
607        larger_mem_cost,
608        new_smaller_subtree_id,
609        local_smaller_path,
610        smaller_flop_cost,
611        smaller_mem_cost,
612    )
613}
614
615#[cfg(test)]
616mod tests {
617    use super::*;
618
619    use std::rc::Rc;
620
621    use rand::{rngs::StdRng, SeedableRng};
622    use rustc_hash::FxHashMap;
623
624    use crate::contractionpath::contraction_tree::{
625        balancing::find_rebalance_node,
626        node::{child_node, parent_node},
627        ContractionTree,
628    };
629    use crate::path;
630
631    fn setup_complex() -> (ContractionTree, CompositeTensor) {
632        let bond_dims = FxHashMap::from_iter([
633            (0, 27),
634            (1, 18),
635            (2, 12),
636            (3, 15),
637            (4, 5),
638            (5, 3),
639            (6, 18),
640            (7, 22),
641            (8, 45),
642            (9, 65),
643            (10, 5),
644        ]);
645        let (tensor, contraction_path) = (
646            CompositeTensor::new(vec![
647                LeafTensor::new_from_map(vec![4, 3, 2], &bond_dims),
648                LeafTensor::new_from_map(vec![0, 1, 3, 2], &bond_dims),
649                LeafTensor::new_from_map(vec![4, 5, 6], &bond_dims),
650                LeafTensor::new_from_map(vec![6, 8, 9], &bond_dims),
651                LeafTensor::new_from_map(vec![10, 8, 9], &bond_dims),
652                LeafTensor::new_from_map(vec![5, 1, 0], &bond_dims),
653            ]),
654            path![(1, 5), (0, 1), (3, 4), (2, 3), (0, 2)],
655        );
656        (
657            ContractionTree::from_contraction_path(&tensor, &contraction_path),
658            tensor,
659        )
660    }
661
662    #[test]
663    fn test_shift_leaf_node_between_subtrees() {
664        let (mut tree, tensor) = setup_complex();
665        tree.partitions.entry(1).or_insert_with(|| vec![9, 7]);
666        shift_node_between_subtrees(&mut tree, 1, 9, 7, vec![3], &tensor);
667
668        let ContractionTree { nodes, root, .. } = tree;
669
670        let node0 = child_node(0, vec![0]);
671        let node1 = child_node(1, vec![1]);
672        let node2 = child_node(2, vec![2]);
673        let node3 = child_node(3, vec![3]);
674        let node4 = child_node(4, vec![4]);
675        let node5 = child_node(5, vec![5]);
676
677        let node6 = parent_node(6, &node1, &node5);
678        let node7 = parent_node(7, &node6, &node0);
679        let node8 = parent_node(8, &node2, &node4);
680        let node9 = parent_node(9, &node7, &node3);
681        let node10 = parent_node(10, &node9, &node8);
682
683        let ref_root = Rc::clone(&node10);
684        let ref_nodes = [
685            node0, node1, node2, node3, node4, node5, node6, node7, node8, node9, node10,
686        ];
687
688        for (key, ref_node) in ref_nodes.iter().enumerate() {
689            let node = &nodes[&key];
690            assert_eq!(node, ref_node);
691        }
692        assert_eq!(root.upgrade().unwrap(), ref_root);
693    }
694
695    #[test]
696    fn test_shift_subtree_between_subtrees() {
697        let (mut tree, tensor) = setup_complex();
698        tree.partitions.entry(1).or_insert_with(|| vec![9, 7]);
699        shift_node_between_subtrees(&mut tree, 1, 9, 7, vec![2, 3], &tensor);
700
701        let ContractionTree { nodes, root, .. } = tree;
702
703        let node0 = child_node(0, vec![0]);
704        let node1 = child_node(1, vec![1]);
705        let node2 = child_node(2, vec![2]);
706        let node3 = child_node(3, vec![3]);
707        let node4 = child_node(4, vec![4]);
708        let node5 = child_node(5, vec![5]);
709
710        let node6 = parent_node(6, &node1, &node5);
711        let node7 = parent_node(7, &node2, &node3);
712        let node8 = parent_node(8, &node6, &node0);
713        let node9 = parent_node(9, &node8, &node7);
714        let node10 = parent_node(10, &node9, &node4);
715
716        let ref_root = Rc::clone(&node10);
717        let ref_nodes = [
718            node0, node1, node2, node3, node4, node5, node6, node7, node8, node9, node10,
719        ];
720
721        for (key, ref_node) in ref_nodes.iter().enumerate() {
722            let node = &nodes[&key];
723            assert_eq!(node, ref_node);
724        }
725
726        assert_eq!(root.upgrade().unwrap(), ref_root);
727    }
728
729    fn custom_weight_function(a: &LeafTensor, b: &LeafTensor) -> f64 {
730        (a & b).legs().len() as f64
731    }
732
733    #[test]
734    fn test_find_rebalance_node() {
735        let bond_dims =
736            FxHashMap::from_iter([(0, 2), (1, 1), (2, 3), (3, 5), (4, 3), (5, 8), (6, 7)]);
737        let larger_hash = FxHashMap::from_iter([
738            (0, LeafTensor::new_from_map(vec![0, 1, 2], &bond_dims)),
739            (1, LeafTensor::new_from_map(vec![1, 2, 3], &bond_dims)),
740            (2, LeafTensor::new_from_map(vec![3, 4, 5], &bond_dims)),
741        ]);
742
743        let smaller_hash =
744            FxHashMap::from_iter([(3, LeafTensor::new_from_map(vec![4, 5, 6], &bond_dims))]);
745
746        let ref_balanced_node = 2;
747        let (node_id, cost) = find_rebalance_node::<StdRng>(
748            &mut None,
749            &larger_hash,
750            &smaller_hash,
751            custom_weight_function,
752        );
753        assert_eq!(cost, 2.);
754        assert_eq!(node_id, ref_balanced_node);
755    }
756
757    #[test]
758    fn test_find_random_rebalance_node() {
759        let bond_dims =
760            FxHashMap::from_iter([(0, 2), (1, 1), (2, 3), (3, 5), (4, 3), (5, 8), (6, 7)]);
761        let larger_hash = FxHashMap::from_iter([
762            (0, LeafTensor::new_from_map(vec![0, 1, 2], &bond_dims)),
763            (1, LeafTensor::new_from_map(vec![1, 2, 6], &bond_dims)),
764            (2, LeafTensor::new_from_map(vec![3, 4, 5], &bond_dims)),
765        ]);
766
767        let smaller_hash =
768            FxHashMap::from_iter([(3, LeafTensor::new_from_map(vec![4, 5, 6], &bond_dims))]);
769
770        let ref_balanced_node = 1;
771        let (node_id, cost) = find_rebalance_node(
772            &mut Some((2, StdRng::seed_from_u64(1))),
773            &larger_hash,
774            &smaller_hash,
775            custom_weight_function,
776        );
777        assert_eq!(cost, 1.);
778        assert_eq!(node_id, ref_balanced_node);
779    }
780}