-
Notifications
You must be signed in to change notification settings - Fork 48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: generic circuit struct #123
Changes from 8 commits
12eb964
242a1c9
6b76ffc
df4b074
599f66c
8947554
db13e8e
e1cea18
cf4f3a5
3ac77dd
8ed2ac2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
[package] | ||
name = "mpz-circuit-generic" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[lib] | ||
name = "mpz_circuit_generic" | ||
|
||
[lints] | ||
workspace = true | ||
|
||
[dependencies] | ||
thiserror = "1.0.59" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
//! Circuit Module | ||
//! | ||
//! Main circuit module. | ||
|
||
use crate::model::Component; | ||
use std::{ | ||
collections::{HashMap, VecDeque}, | ||
mem::take, | ||
}; | ||
use thiserror::Error; | ||
|
||
/// The Circuit Builder assembles a collection of gates into a circuit. | ||
/// | ||
/// The built output is ensured to be a directed acyclic graph (DAG). | ||
/// | ||
/// The gates are topologically sorted. | ||
#[derive(Debug)] | ||
pub struct CircuitBuilder<T> | ||
where | ||
T: Component, | ||
{ | ||
/// Circuit gates | ||
gates: Vec<T>, | ||
/// For each input, map the gate index that provides it. | ||
input_map: HashMap<usize, usize>, | ||
} | ||
|
||
impl<T> CircuitBuilder<T> | ||
where | ||
T: Component, | ||
{ | ||
/// Creates a new circuit builder. | ||
pub fn new() -> Self { | ||
Self { | ||
gates: Vec::new(), | ||
input_map: HashMap::new(), | ||
} | ||
} | ||
|
||
/// Adds a gate to the builder. | ||
pub fn add_gate(&mut self, gate: T) -> &mut Self { | ||
for &input in gate.get_inputs().iter() { | ||
self.input_map.insert(input, self.gates.len()); | ||
} | ||
|
||
self.gates.push(gate); | ||
self | ||
} | ||
|
||
/// Builds the circuit. | ||
pub fn build(&mut self) -> Result<Circuit<T>, CircuitError> { | ||
self.sort_gates()?; | ||
|
||
Ok(Circuit::new(take(&mut self.gates))) | ||
} | ||
|
||
/// Performs a topological sort of the gates. | ||
/// | ||
/// This ensures that the gates are linearly ordered such that the | ||
/// dependencies (input gates) of each gate are processed before the gate itself. | ||
/// | ||
/// This requires that the gates form a directed acyclic graph (DAG). | ||
/// | ||
/// The sorting is done using Kahn's Algorithm. | ||
fn sort_gates(&mut self) -> Result<(), CircuitError> { | ||
// In-degree: the number of gates that provide input to each gate | ||
// This represents how many other gates need to be processed before this gate | ||
let mut in_degree = vec![0; self.gates.len()]; | ||
// Adjacency list: for each gate, list the gates that directly depend on its output | ||
// This is used to keep track of which gates need to be updated after processing a gate | ||
let mut adjacency_list = vec![vec![]; self.gates.len()]; | ||
|
||
// Populate lists | ||
for (i, gate) in self.gates.iter().enumerate() { | ||
for &output in gate.get_outputs().iter() { | ||
let output = self.input_map.get(&output); | ||
|
||
if let Some(&gate_index) = output { | ||
adjacency_list[i].push(gate_index); | ||
in_degree[gate_index] += 1; | ||
} | ||
} | ||
} | ||
|
||
let mut queue = VecDeque::new(); | ||
let mut sorted_indices = Vec::with_capacity(self.gates.len()); | ||
|
||
// Push ready-to-process nodes (no dependencies) to the queue | ||
for (i, °ree) in in_degree.iter().enumerate() { | ||
if degree == 0 { | ||
queue.push_back(i); | ||
} | ||
} | ||
|
||
// Process nodes | ||
while let Some(node) = queue.pop_front() { | ||
sorted_indices.push(node); | ||
|
||
// Reduce in-degree of dependent nodes | ||
for &neighbor in &adjacency_list[node] { | ||
in_degree[neighbor] -= 1; | ||
|
||
// If the dependent node is now ready to be processed, add it to the queue | ||
if in_degree[neighbor] == 0 { | ||
queue.push_back(neighbor); | ||
} | ||
} | ||
} | ||
|
||
// If some node is left unprocessed, there is a cycle | ||
if sorted_indices.len() != self.gates.len() { | ||
return Err(CircuitError::CycleDetected); | ||
} | ||
|
||
// Sort the gates | ||
// To preserve the order of the gates we create this temporary vector of optionals | ||
let mut temp_gates: Vec<Option<T>> = self.gates.drain(..).map(Some).collect(); | ||
let mut sorted_gates = Vec::with_capacity(temp_gates.len()); | ||
for &i in &sorted_indices { | ||
// Whenever we take a gate from the vector we replace it with None | ||
// This way we avoid shifting items | ||
if let Some(gate) = temp_gates[i].take() { | ||
sorted_gates.push(gate); | ||
} | ||
} | ||
|
||
self.gates = sorted_gates; | ||
Ok(()) | ||
} | ||
} | ||
|
||
/// A circuit constructed from a collection of gates. | ||
/// | ||
/// - Each node in the circuit is an indexed point within an external array. | ||
/// - Each gate acts as a unit of logic that connects these nodes. | ||
#[derive(Debug)] | ||
pub struct Circuit<T> { | ||
gates: Vec<T>, | ||
} | ||
|
||
impl<T> Circuit<T> { | ||
/// Creates a new circuit. | ||
fn new(gates: Vec<T>) -> Self { | ||
Self { gates } | ||
} | ||
|
||
/// Returns the gates. | ||
pub fn gates(&self) -> &[T] { | ||
&self.gates | ||
} | ||
} | ||
|
||
/// Circuit errors. | ||
#[derive(Debug, Error, PartialEq, Eq)] | ||
pub enum CircuitError { | ||
#[error("Cycle detected")] | ||
CycleDetected, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
pub mod circuit; | ||
pub mod model; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's make these private and export needed types |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
But both are good suggestions, I could be missing something! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It's all about the public API: What do you want to commit to? Using
Yes it will be used to index into linear memory, but a |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
//! Model module. | ||
//! | ||
//! This module contains the main traits and structures used to represent the circuits. | ||
|
||
/// A `Component` trait that represents a block with inputs and outputs. | ||
pub trait Component { | ||
/// Returns the input node indices. | ||
fn get_inputs(&self) -> Vec<usize>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should return |
||
|
||
/// Returns the output node indices. | ||
fn get_outputs(&self) -> Vec<usize>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
//! Sorting tests. | ||
//! | ||
//! This module aims to test the topological sorting of the gates in the circuit. | ||
|
||
use mpz_circuit_generic::model::Component; | ||
|
||
/// Binary gate value. | ||
#[derive(Debug, Clone)] | ||
pub enum BinaryValue { | ||
/// Binary zero. | ||
Zero, | ||
/// Binary one. | ||
One, | ||
} | ||
|
||
#[derive(Debug, Clone, PartialEq)] | ||
/// Binary gates. | ||
pub enum BinaryOperation { | ||
/// AND Operation. | ||
AND, | ||
/// NOT Operation. | ||
NOT, | ||
/// XOR Operation. | ||
XOR, | ||
} | ||
|
||
/// Binary gate. | ||
#[derive(Debug, Clone)] | ||
pub struct BinaryGate { | ||
/// Gate inputs. Each input is a usize that represents the index of the input nodes. | ||
inputs: Vec<usize>, | ||
/// Gate output. A usize that represents the index of the output node. | ||
output: usize, | ||
} | ||
|
||
impl Component for BinaryGate { | ||
fn get_inputs(&self) -> Vec<usize> { | ||
self.inputs.clone() | ||
} | ||
|
||
fn get_outputs(&self) -> Vec<usize> { | ||
vec![self.output] | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a couple test cases?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added the disconnected gate test, but I'm confused about testing the node ids, in which way they should be contiguous or ordered? The order is based on that the gates should be ready to be processed, so node ids are not always contiguous or ordered There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Exactly, so the node id should correspond to the gate position. For example: assert!(gates.windows(2).map(|window| window[1].out.0 == window[0].out.0 + 1).all()); This asserts the nodes are ordered and contiguous. |
||
use super::*; | ||
use mpz_circuit_generic::circuit::{CircuitBuilder, CircuitError}; | ||
|
||
#[test] | ||
fn test_reorder() { | ||
// Setup circuit builder | ||
let mut circuit_builder = CircuitBuilder::<BinaryGate>::new(); | ||
|
||
// Define gates in the correct order | ||
let gate1 = BinaryGate { | ||
inputs: vec![0, 1], | ||
output: 2, | ||
}; | ||
let gate2 = BinaryGate { | ||
inputs: vec![3, 4], | ||
output: 5, | ||
}; | ||
let gate3 = BinaryGate { | ||
inputs: vec![2, 5], | ||
output: 6, | ||
}; | ||
let gate4 = BinaryGate { | ||
inputs: vec![2, 6], | ||
output: 7, | ||
}; | ||
let gate5 = BinaryGate { | ||
inputs: vec![8, 9], | ||
output: 10, | ||
}; | ||
let gate6 = BinaryGate { | ||
inputs: vec![10, 7], | ||
output: 11, | ||
}; | ||
|
||
// Add gates in a random order | ||
circuit_builder | ||
.add_gate(gate6) | ||
.add_gate(gate3) | ||
.add_gate(gate1) | ||
.add_gate(gate5) | ||
.add_gate(gate4) | ||
.add_gate(gate2); | ||
|
||
// Build circuit | ||
let circuit = circuit_builder.build(); | ||
assert!( | ||
circuit.is_ok(), | ||
"Failed to build circuit: {:?}", | ||
circuit.err() | ||
); | ||
let circuit = circuit.unwrap(); | ||
let gates = circuit.gates(); | ||
|
||
// Verify topological order | ||
assert_eq!( | ||
gates[0].get_outputs(), | ||
vec![2], | ||
"First gate outputs mismatch" // Gate 1 | ||
); | ||
assert_eq!( | ||
gates[1].get_outputs(), | ||
vec![10], | ||
"Second gate outputs mismatch" // Gate 5 | ||
); | ||
assert_eq!( | ||
gates[2].get_outputs(), | ||
vec![5], | ||
"Third gate outputs mismatch" // Gate 2 | ||
); | ||
assert_eq!( | ||
gates[3].get_outputs(), | ||
vec![6], | ||
"Fourth gate outputs mismatch" // Gate 3 | ||
); | ||
assert_eq!( | ||
gates[4].get_outputs(), | ||
vec![7], | ||
"Fifth gate outputs mismatch" // Gate 4 | ||
); | ||
assert_eq!( | ||
gates[5].get_outputs(), | ||
vec![11], | ||
"Sixth gate outputs mismatch" // Gate 6 | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_reorder_with_cycle() { | ||
// Setup circuit builder | ||
let mut circuit_builder = CircuitBuilder::<BinaryGate>::new(); | ||
|
||
// Define gates in the correct order | ||
let gate1 = BinaryGate { | ||
inputs: vec![0, 1], | ||
output: 2, | ||
}; | ||
let gate2 = BinaryGate { | ||
inputs: vec![3, 4], | ||
output: 5, | ||
}; | ||
let gate3 = BinaryGate { | ||
inputs: vec![2, 5], | ||
output: 6, | ||
}; | ||
let gate4 = BinaryGate { | ||
inputs: vec![2, 6], | ||
output: 7, | ||
}; | ||
let gate5 = BinaryGate { | ||
inputs: vec![8, 9], | ||
output: 10, | ||
}; | ||
let gate6 = BinaryGate { | ||
inputs: vec![10, 7], | ||
output: 11, | ||
}; | ||
let cycle_gate = BinaryGate { | ||
inputs: vec![11], | ||
output: 0, | ||
}; | ||
|
||
// Add gates in the wrong order | ||
circuit_builder | ||
.add_gate(gate6) | ||
.add_gate(gate3) | ||
.add_gate(gate1) | ||
.add_gate(gate5) | ||
.add_gate(gate4) | ||
.add_gate(gate2) | ||
.add_gate(cycle_gate); | ||
|
||
// Build circuit should fail due to cycle | ||
let circuit = circuit_builder.build(); | ||
assert!(circuit.is_err(), "Expected cycle detection error"); | ||
assert_eq!( | ||
circuit.unwrap_err(), | ||
CircuitError::CycleDetected, | ||
"Unexpected error type" | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.