Encoding messages

Concrete enables operating homomorphically on real-values by encoding them into fixed-precision representation called plaintexts. Encoders have:

Defining the right encoders is important to ensure your homomorphic program runs accurately and efficiently. More precision typically means more internal operations, and thus, a more computationally expensive homomorphic program, while less precision can lead to imprecise results.

Always chose the smallest possible precision that yields the desired output. This will ensure your homomorphic program runs faster!

Creating an encoder

Concrete simplifies managing precision by providing an Encoder struct that encodes real messages into plaintexts. An encoder takes three parameters:

  • the number of bits of precision needed to represent your data

  • the number of bits of padding to carry on leveled operations (more on that later)

let min = -10.;
let max = 10.;
let precision = 8; // bits
let padding = 0;   // bits

// create an Encoder instance
let encoder = Encoder::new(min, max, precision, padding)?;

Instead of using the min and max of the interval, an encoder can also be defined using the center value of the interval and a radius:

let center = 0.;
let radius = 10.;
let precision = 8;
let padding = 0;

// this is equivalent to the previous encoder
let encoder = Encoder::new_centered(center, radius, precision, padding)?;

Concrete only requires that you specify the encoding for your input messages. Once you start operating on the ciphertexts, the encoding will evolve dynamically to represent the range of possible values. When it is not possible to infer the encoding, an output encoder will need to be specified.

The last parameter, the number of padding bits is required to ensure the correctness of future computations. In a nutshell, they allow to keep the precision and granularity defined in the encoder while taking in account the potential carries. The processes related to the padding are details in the Leveled Operations section.

Encoding a message into a plaintext

Under the hood, a Plaintext instance stores a vector of encoded messages, each with their own encoder. This enables Concrete to better manage performances. Thus, a plaintext in Concrete can be either a single encoded message, or a vector of encoded messages.

Encoding a message into a plaintext is rather simple:

// create a message in the interval
let message = -6.276;

// create a new Plaintext using the encoder's function
let p1: Plaintext = encoder.encode_single(message)?;

The encode function is versatile, meaning you can pass it a vector of messages instead of a single message. Internally, both are represented in the same way, with single-message plaintexts simply representing the values as a vector of size 1.

// create a list of messages in our interval
let messages = vec![-6.276, 4.3, 0.12, -1.1, 7.78];

// create a new Plaintext using the encoder's function
let p2: Plaintext = encoder.encode(&messages)?;

Decoding a plaintext into a message

You can decode a plaintext back into a raw message using the decode method:

// decode the plaintext into a vector of messages
let output: Vec<f64> = p2.decode()?;

The decode method always returns a vector of messages, since this is how the plaintext stores values internally. If you only encoded one value, it will be stored in the decoded vector's first element.

Since encoding reduces the precision of the original message, the decoded message will not always match exactly the original message, but rather the closest value that the encoder can represent.

Putting everything together

Here is an example program that specifies and uses an encoder, for a single message and a message vector:

/// file: main.rs
use concrete::*;

fn main() -> Result<(), CryptoAPIError> {
    // the encoder's parameters
    let min = -10.;
    let max = 10.;
    let precision = 8;
    let padding = 0;

    // create an Encoder instance
    let encoder = Encoder::new(min, max, precision, padding)?;

    // encode a single message
    let m1 = -6.276;
    let p1: Plaintext = encoder.encode_single(m1)?;

    // encode a vector of messages
    let m2 = vec![-6.276, 4.3, 0.12, -1.1, 7.78];
    let p2: Plaintext = encoder.encode(&m2)?;

    // decode the plaintext
    let o1: Vec<f64> = p1.decode()?;
    let o2: Vec<f64> = p2.decode()?;

    println!("{}", p1);
    println!("{}", p2);
    println!("m1 = {}, o1 = {:?}", m1, o1[0]);
    println!("m2 = {:?}, o2 = {:?}", m2, o2);
    Ok(())
}

Last updated