Skip to content

Commit

Permalink
example: Projected / Repeated columns usage (#6)
Browse files Browse the repository at this point in the history
* example: Add example of Projected column usage

* example: Add example of Repeated column usage

* example: Add example of ZeroPadded column usage
  • Loading branch information
storojs72 authored Feb 6, 2025
1 parent 07aa5f4 commit 891f844
Show file tree
Hide file tree
Showing 4 changed files with 429 additions and 0 deletions.
12 changes: 12 additions & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ path = "acc-shifted.rs"
name = "acc-packed"
path = "acc-packed.rs"

[[example]]
name = "acc-projected"
path = "acc-projected.rs"

[[example]]
name = "acc-repeated"
path = "acc-repeated.rs"

[[example]]
name = "acc-zeropadded"
path = "acc-zeropadded.rs"

[lints.clippy]
needless_range_loop = "allow"

Expand Down
248 changes: 248 additions & 0 deletions examples/acc-projected.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
use binius_circuits::{builder::ConstraintSystemBuilder, unconstrained::unconstrained};
use binius_core::{constraint_system::validate::validate_witness, oracle::ProjectionVariant};
use binius_field::{arch::OptimalUnderlier, BinaryField128b, BinaryField8b};

type U = OptimalUnderlier;
type F128 = BinaryField128b;
type F8 = BinaryField8b;

#[derive(Clone)]
struct U8U128ProjectionInfo {
log_size: usize,
decimal: usize,
binary: Vec<F128>,
variant: ProjectionVariant,
}

// The idea behind projection is that data from a column of some given field (F8)
// can be interpreted as a data of some or greater field (F128) and written to another column with equal or smaller length,
// which depends on LOG_SIZE and values of projection. Also two possible variants of projections are available, which
// has significant impact on input data processing.
// In the following example we have input column with bytes (u8) projected to the output column with u128 values.
fn projection(
builder: &mut ConstraintSystemBuilder<U, F128>,
projection_info: U8U128ProjectionInfo,
namespace: &str,
) {
builder.push_namespace(format!("projection {}", namespace));

let input =
unconstrained::<U, F128, F8>(builder, "in", projection_info.clone().log_size).unwrap();

let projected = builder
.add_projected(
"projected",
input,
projection_info.clone().binary,
projection_info.clone().variant,
)
.unwrap();

if let Some(witness) = builder.witness() {
let input_values = witness.get::<F8>(input).unwrap().as_slice::<u8>();
let mut projected_witness = witness.new_column::<F128>(projected);
let projected_values = projected_witness.as_mut_slice::<F128>();

assert_eq!(projected_values.len(), projection_info.expected_projection_len());

match projection_info.variant {
ProjectionVariant::FirstVars => {
// Quite elaborated regularity, on my opinion
for idx in 0..projected_values.len() {
projected_values[idx] = F128::new(
input_values[(idx
* 2usize.pow(projection_info.clone().binary.len() as u32))
+ projection_info.clone().decimal] as u128,
);
}
}
ProjectionVariant::LastVars => {
// decimal representation of the binary values is used as a simple offset
for idx in 0..projected_values.len() {
projected_values[idx] =
F128::new(input_values[projection_info.clone().decimal + idx] as u128);
}
}
};
}
builder.pop_namespace();
}

impl U8U128ProjectionInfo {
fn new(
log_size: usize,
decimal: usize,
binary: Vec<F128>,
variant: ProjectionVariant,
) -> U8U128ProjectionInfo {
assert!(log_size >= binary.len());

if variant == ProjectionVariant::LastVars {
// Pad with zeroes to LOG_SIZE len iterator.
// In this case we interpret binary values in a reverse order, meaning that the very first
// element is elder byte, so zeroes must be explicitly appended
let mut binary_clone = binary.clone();
let mut zeroes = vec![F128::new(0u128); log_size - binary.len()];
binary_clone.append(&mut zeroes);

let coefficients = (0..binary_clone.len())
.map(|degree| F128::new(2usize.pow(degree as u32) as u128))
.collect::<Vec<F128>>();

let value = binary_clone
.iter()
.zip(coefficients.iter().rev())
.fold(F128::new(0u128), |acc, (byte, coefficient)| acc + (*byte) * (*coefficient));

assert_eq!(decimal as u128, value.val());
}

U8U128ProjectionInfo {
log_size,
decimal,
binary,
variant,
}
}

fn expected_projection_len(&self) -> usize {
2usize.pow((self.log_size - self.binary.len()) as u32)
}
}

fn main() {
let allocator = bumpalo::Bump::new();
let mut builder = ConstraintSystemBuilder::<U, F128>::new_with_witness(&allocator);

let projection_data = U8U128ProjectionInfo::new(
4usize,
9usize,
vec![
F128::from(1u128),
F128::from(0u128),
F128::from(0u128),
F128::from(1u128),
],
ProjectionVariant::FirstVars,
);
projection(&mut builder, projection_data, "test_1");

let projection_data = U8U128ProjectionInfo::new(
16usize,
34816usize,
vec![
F128::from(1u128),
F128::from(0u128),
F128::from(0u128),
F128::from(0u128),
F128::from(1u128),
],
ProjectionVariant::LastVars,
);
projection(&mut builder, projection_data, "test_2");

let projection_data = U8U128ProjectionInfo::new(
4usize,
15usize,
vec![
F128::from(1u128),
F128::from(1u128),
F128::from(1u128),
F128::from(1u128),
],
ProjectionVariant::LastVars,
);
projection(&mut builder, projection_data, "test_3");

let projection_data = U8U128ProjectionInfo::new(
6usize,
60usize,
vec![
F128::from(1u128),
F128::from(1u128),
F128::from(1u128),
F128::from(1u128),
],
ProjectionVariant::LastVars,
);
/*
With projection_data defined above we have 2^LOG_SIZE = 2^6 bytes in the input,
the size of projection is computed as follows: 2.pow(LOG_SIZE - binary.len()) = 2.pow(6 - 4) = 4.
the index of the input byte to use as projection is computed as follows (according to
a LastVars projection variant regularity):
idx + decimal, e.g.:
0 + 60
1 + 60
2 + 60
3 + 60
where idx is [0..4].
Memory layout:
input: [a5, a2, b1, 60, 91, ed, 5e, fb, ae, 1c, b2, 14, 92, 73, 92, c8, 56, 6d, fa, de, a8, 46, 77, 48, e1, cc, 90, 75, 78, d5, 19, be, 0c, 86, 39, 28, 0c, cc, e9, 4e, 46, d9, 84, 65, 4a, a2, b4, 64, eb, 59, 7b, fd, 3f, 0e, 2d, ea, 06, 42, a9, ea, (19), (8f), (19), (52)], len: 64
output: [
BinaryField128b(0x00000000000000000000000000000019),
BinaryField128b(0x0000000000000000000000000000008f),
BinaryField128b(0x00000000000000000000000000000019),
BinaryField128b(0x00000000000000000000000000000052)
]
*/
projection(&mut builder, projection_data, "test_4");

let projection_data = U8U128ProjectionInfo::new(
4usize,
15usize,
vec![
F128::from(1u128),
F128::from(1u128),
F128::from(1u128),
F128::from(1u128),
],
ProjectionVariant::FirstVars,
);
projection(&mut builder, projection_data, "test_5");

let projection_data = U8U128ProjectionInfo::new(
6usize,
13usize,
vec![
F128::from(1u128),
F128::from(0u128),
F128::from(1u128),
F128::from(1u128),
],
ProjectionVariant::FirstVars,
);
/*
With projection_data defined above we have 2^LOG_SIZE = 2^6 bytes in the input,
the size of projection is computed as follows: 2.pow(LOG_SIZE - binary.len()) = 2.pow(6 - 4) = 4.
the index of the input byte to use as projection is computed as follows:
idx * 2usize.pow(binary.len()) + decimal, e.g.:
0 * 2.pow(4) + 13 = 13, so input[13]
1 * 2.pow(4) + 13 = 29, so input[29]
2 * 2.pow(4) + 13 = 45, so input[45]
3 * 2.pow(4) + 13 = 61, so input[61]
where idx is [0..4] according to a FirstVars projection variant regularity.
Memory layout:
input: [18, d8, 58, d3, 24, f1, 8b, ec, 74, 1c, ab, 78, 13, (3e), 57, d7, 36, 15, 54, 50, 9a, cb, 98, 90, 58, cb, 79, 05, 83, (72), ea, 4d, f6, 3d, f3, 2f, af, e3, 32, 11, c9, 97, fb, ba, 24, (36), e9, 38, 7e, c7, a9, 68, bf, 31, 51, cf, 7b, 12, 20, 53, d8, (df), d7, cc], len: 64
output: BinaryField128b(0x0000000000000000000000000000003e)
output: BinaryField128b(0x00000000000000000000000000000072)
output: BinaryField128b(0x00000000000000000000000000000036)
output: BinaryField128b(0x000000000000000000000000000000df)
*/
projection(&mut builder, projection_data, "test_6");

let witness = builder.take_witness().unwrap();
let cs = builder.build().unwrap();

validate_witness(&cs, &[], &witness).unwrap();
}
124 changes: 124 additions & 0 deletions examples/acc-repeated.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use binius_circuits::{builder::ConstraintSystemBuilder, unconstrained::unconstrained};
use binius_core::constraint_system::validate::validate_witness;
use binius_field::{
arch::OptimalUnderlier, packed::set_packed_slice, BinaryField128b, BinaryField1b,
BinaryField8b, PackedBinaryField128x1b,
};

type U = OptimalUnderlier;
type F128 = BinaryField128b;
type F8 = BinaryField8b;
type F1 = BinaryField1b;

// FIXME: Following gadgets are unconstrained. Only for demonstrative purpose, don't use in production

const LOG_SIZE: usize = 8;

// The idea of 'Repeated' column is that one can just copy data from initial column multiple times,
// so new column is X times bigger than original one. The following gadget operates over bytes, e.g.
// it creates column with some input bytes written and then creates one more 'Repeated' column
// where the same bytes are copied multiple times.
fn bytes_repeat_gadget(builder: &mut ConstraintSystemBuilder<U, F128>) {
builder.push_namespace("bytes_repeat_gadget");

let bytes = unconstrained::<U, F128, F8>(builder, "input", LOG_SIZE).unwrap();

let repeat_times_log = 4usize;
let repeating = builder
.add_repeating("repeating", bytes, repeat_times_log)
.unwrap();

if let Some(witness) = builder.witness() {
let input_values = witness.get::<F8>(bytes).unwrap().as_slice::<u8>();

let mut repeating_witness = witness.new_column::<F8>(repeating);
let repeating_values = repeating_witness.as_mut_slice::<u8>();

let repeat_times = 2usize.pow(repeat_times_log as u32);
assert_eq!(2usize.pow(LOG_SIZE as u32), input_values.len());
assert_eq!(input_values.len() * repeat_times, repeating_values.len());

for idx in 0..repeat_times {
let start = idx * input_values.len();
let end = start + input_values.len();
repeating_values[start..end].copy_from_slice(input_values);
}
}

builder.pop_namespace();
}

// Bit-oriented repeating is more elaborated due to a specifics of memory layout in Binius.
// In the following example, we use LOG_SIZE=8, which gives 2.pow(8) = 32 bytes written in the memory
// layout. This gives 32 * 8 = 256 bits of input information. Having that Repeated' column
// is instantiated with 'repeat_times_log = 2', this means that we have to repeat our bytes
// 2.pow(repeat_times_log) = 4 times ultimately. For setting bit values we use PackedBinaryField128x1b,
// so for 32 bytes (256 bits) of input data we use 2 PackedBinaryField128x1b elements. Considering 4
// repetitions Binius creates column with 8 PackedBinaryField128x1b elements totally.
// Proper writing bits requires separate iterating over PackedBinaryField128x1b elements and input bytes
// with extracting particular bit values from the input and setting appropriate bit in a given PackedBinaryField128x1b.
fn bits_repeat_gadget(builder: &mut ConstraintSystemBuilder<U, F128>) {
builder.push_namespace("bits_repeat_gadget");

let bits = unconstrained::<U, F128, F1>(builder, "input", LOG_SIZE).unwrap();
let repeat_times_log = 2usize;

// Binius will create column with appropriate height for us
let repeating = builder
.add_repeating("repeating", bits, repeat_times_log)
.unwrap();

if let Some(witness) = builder.witness() {
let input_values = witness.get::<F1>(bits).unwrap().as_slice::<u8>();
let mut repeating_witness = witness.new_column::<F1>(repeating);
let output_values = repeating_witness.packed();

// this performs writing input bits exactly 1 time. Depending on number of repetitions we
// need to call this multiple times, providing offset for output values (PackedBinaryField128x1b elements)
fn write_input(
input_values: &[u8],
output_values: &mut [PackedBinaryField128x1b],
output_packed_offset: usize,
) {
let mut output_index = output_packed_offset;
for (input_index, _) in (0..input_values.len()).enumerate() {
let byte = input_values[input_index];

set_packed_slice(output_values, output_index, F1::from(byte));
set_packed_slice(output_values, output_index + 1, F1::from((byte >> 1) & 0x01));
set_packed_slice(output_values, output_index + 2, F1::from((byte >> 2) & 0x01));
set_packed_slice(output_values, output_index + 3, F1::from((byte >> 3) & 0x01));
set_packed_slice(output_values, output_index + 4, F1::from((byte >> 4) & 0x01));
set_packed_slice(output_values, output_index + 5, F1::from((byte >> 5) & 0x01));
set_packed_slice(output_values, output_index + 6, F1::from((byte >> 6) & 0x01));
set_packed_slice(output_values, output_index + 7, F1::from((byte >> 7) & 0x01));

output_index += 8;
}
}

let repeat_times = 2u32.pow(repeat_times_log as u32);

let mut offset = 0;
for _ in 0..repeat_times {
write_input(input_values, output_values, offset);
offset += input_values.len() * 8;
}
}

builder.pop_namespace();
}

fn main() {
let allocator = bumpalo::Bump::new();

let mut builder = ConstraintSystemBuilder::<U, F128>::new_with_witness(&allocator);

bytes_repeat_gadget(&mut builder);
bits_repeat_gadget(&mut builder);

let witness = builder.take_witness().unwrap();
let cs = builder.build().unwrap();

validate_witness(&cs, &[], &witness).unwrap();
}
Loading

0 comments on commit 891f844

Please sign in to comment.