ciphercore_base/applications/
minimum.rs

1//! Minimum of an integer array
2use crate::custom_ops::CustomOperation;
3use crate::data_types::{array_type, ScalarType, BIT};
4use crate::errors::Result;
5use crate::graphs::{Context, Graph, SliceElement};
6use crate::ops::min_max::Min;
7
8/// Creates a graph that finds the minimum of an array.
9///
10/// # Arguments
11///
12/// * `context` - context where a minimum graph should be created
13/// * `n` - number of elements of an array (i.e., 2<sup>n</sup>)
14/// * `st` - scalar type of array elements
15///
16/// # Returns
17///
18/// Graph that finds the minimum of an array
19pub fn create_minimum_graph(context: Context, n: u64, st: ScalarType) -> Result<Graph> {
20    // Get sign of the input scalar type that indicates whether signed comparisons should be computed
21    let signed_comparison = st.is_signed();
22
23    // Create a graph in a given context that will be used for finding the minimum
24    let g = context.create_graph()?;
25
26    // Create the type of the input array with `2^n` elements.
27    let input_type = array_type(vec![1 << n], st);
28
29    // Add an input node to the empty graph g created above.
30    // This input node requires the input array type generated previously.
31    let input_array = g.input(input_type)?;
32
33    // To find the minimum of an array, we resort to the custom operation Min (see ops.rs) that accepts only binary input.
34    // Thus, we need to convert the input in the binary form (if necessary).
35    let mut binary_array = if st != BIT {
36        input_array.a2b()?
37    } else {
38        input_array
39    };
40
41    // We find the minimum using the tournament method. This allows to reduce the graph size to O(n) from O(2<sup>n</sup>) nodes.
42    // Namely, we split the input array into pairs, find a minimum within each pair and create a new array from these minima.
43    // Then, we repeat this procedure for the new array.
44    // For example, let [2,7,0,3,11,5,0,4] be an input array.
45    // The 1st iteration yields [min(2,11), min(7,5), min(0,0), min(3,4)] = [2,5,0,3]
46    // The 2nd iteration results in [min(2,0), min(5,3)] = [0,3]
47    // The 3rd iteration returns [min(0,3)] = [0]
48    for level in (0..n).rev() {
49        // Extract the first half of the array using the [Graph::get_slice] operation.
50        // Our slicing conventions follow [the NumPy rules](https://numpy.org/doc/stable/user/basics.indexing.html).
51        let half1 =
52            binary_array.get_slice(vec![SliceElement::SubArray(None, Some(1 << level), None)])?;
53        // Extract the first half of the array using the [Graph::get_slice] operation.
54        let half2 =
55            binary_array.get_slice(vec![SliceElement::SubArray(Some(1 << level), None, None)])?;
56        // Compare the first half with the second half elementwise to find minimums.
57        // This is done via the custom operation Min (see ops.rs).
58        binary_array = g.custom_op(
59            CustomOperation::new(Min { signed_comparison }),
60            vec![half1, half2],
61        )?;
62    }
63    // Convert output from the binary form to the arithmetic form
64    let output = if st != BIT {
65        binary_array.b2a(st)?
66    } else {
67        binary_array
68    };
69    // Before computation every graph should be finalized, which means that it should have a designated output node.
70    // This can be done by calling `g.set_output_node(output)?` or as below.
71    output.set_as_output()?;
72    // Finalization checks that the output node of the graph g is set. After finalization the graph can't be changed.
73    g.finalize()?;
74
75    Ok(g)
76}
77
78#[cfg(test)]
79mod tests {
80    use crate::custom_ops::run_instantiation_pass;
81    use crate::data_types::{INT32, UINT32};
82    use crate::data_values::Value;
83    use crate::evaluators::random_evaluate;
84    use crate::graphs::create_context;
85    use std::ops::Not;
86
87    use super::*;
88
89    fn test_minimum_helper<T: TryInto<u128> + Not<Output = T> + TryInto<u8> + Copy>(
90        input_value: &[T],
91        n: u64,
92        st: ScalarType,
93    ) -> Value {
94        || -> Result<Value> {
95            let c = create_context()?;
96            let g = create_minimum_graph(c.clone(), n, st)?;
97            g.set_as_main()?;
98            c.finalize()?;
99            let mapped_c = run_instantiation_pass(c)?.get_context();
100            let mapped_g = mapped_c.get_main_graph()?;
101
102            let input_type = array_type(vec![n], st);
103            let val = Value::from_flattened_array(input_value, input_type.get_scalar_type())?;
104            random_evaluate(mapped_g, vec![val])
105        }()
106        .unwrap()
107    }
108
109    #[test]
110    fn test_minimum() {
111        || -> Result<()> {
112            assert!(
113                test_minimum_helper(
114                    &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
115                    4,
116                    UINT32
117                ) == Value::from_flattened_array(&[1], UINT32)?
118            );
119            Ok(())
120        }()
121        .unwrap();
122        || -> Result<()> {
123            assert!(
124                test_minimum_helper(
125                    &[-1, 2, -3, 4, -5, 6, -7, 8, -9, 10, -11, 12, -13, 14, -15, 16],
126                    4,
127                    INT32
128                ) == Value::from_flattened_array(&[-15], INT32)?
129            );
130            Ok(())
131        }()
132        .unwrap();
133        || -> Result<()> {
134            assert!(
135                test_minimum_helper(&[0, 1, 1, 0, 1, 1, 0, 0], 3, BIT)
136                    == Value::from_flattened_array(&[0], BIT)?
137            );
138            Ok(())
139        }()
140        .unwrap();
141    }
142}