1use std::collections::HashMap;
17
18use crate::Error;
19use crate::numeric::{
20 EXACT_GRID_STEPS, Range, evaluate_boolean, fround, is_power_of_two_grid, largest_grid, lowest_power_of_two, relu32,
21 smallest_grid,
22};
23use crate::wire::{Lin, Term, Wire};
24
25#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
26pub enum OpKind {
27 Affine,
28 Reglu,
29 Boolean,
30 Index,
31}
32
33#[derive(Copy, Clone, Debug, PartialEq, Eq)]
36pub enum Macro {
37 Multiply { left: Wire, right: Wire },
38 Select { gate: Wire, off: Wire, on: Wire },
39}
40
41#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
43pub enum OutOfRange {
44 Clamp,
46 Zero,
48}
49
50#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
53pub enum RoundingPointKind {
54 Boolean,
55 IntegerBit,
56 SignCascade,
57 Marked,
58}
59
60#[derive(Clone, Debug)]
61pub struct IndexRead {
62 pub table: usize,
63 pub below: OutOfRange,
64 pub above: OutOfRange,
65 pub index_range: Range,
67}
68
69#[derive(Clone, Debug, PartialEq)]
73pub struct OneHotGroup {
74 pub members: Vec<Wire>,
75 pub allow_all_zero: bool,
76}
77
78#[derive(Clone, Debug)]
79pub struct CandidateTable {
80 pub candidates: Vec<Wire>,
81 pub maximum_depth: u32,
82}
83
84#[derive(Clone, Debug)]
85pub struct Operation {
86 pub kind: OpKind,
87 pub left_terms: Vec<Term>,
88 pub left_bias: f64,
89 pub gate_terms: Vec<Term>,
91 pub gate_bias: f64,
92 pub depth: u32,
93 pub preserve_rounding: bool,
94 pub macro_op: Option<Macro>,
95 pub index_read: Option<IndexRead>,
96 pub rounding_point: Option<RoundingPointKind>,
97 pub comparison_bit: bool,
99 pub comparison: Option<Comparison>,
101}
102
103#[derive(Clone, Debug, PartialEq)]
105pub struct Comparison {
106 pub terms: Vec<Term>,
107 pub bias: f64,
108 pub equal: bool,
109}
110
111#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
113pub enum BooleanGradient {
114 #[default]
115 Reject,
116 StraightThrough,
117}
118
119#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
121pub enum IndexGradient {
122 #[default]
123 Reject,
124 Zero,
125 BackwardDifference,
126}
127
128#[derive(Copy, Clone, Debug, PartialEq, Default)]
130pub enum ComparisonGradient {
131 #[default]
134 Exact,
135 Surrogate { width: f64 },
139 RelativeSurrogate { fraction: f64 },
143}
144
145#[derive(Copy, Clone, Debug, Default)]
146pub struct BackpropOptions {
147 pub boolean_gradient: BooleanGradient,
148 pub index_gradient: IndexGradient,
149 pub comparison_gradient: ComparisonGradient,
150}
151
152const AFFINE_SHARE_THRESHOLD: usize = 4;
155
156const INDEX_MAGNITUDE_LIMIT: f64 = 16_777_216.0;
159
160#[derive(Clone, Debug)]
161pub struct Circuit {
162 source_size: usize,
163 pub(crate) operations: Vec<Operation>,
164 depths: Vec<u32>,
165 constants: Vec<Option<f64>>,
166 pub(crate) expression_wires: HashMap<Vec<u64>, Wire>,
167 pub(crate) ranges: Vec<Range>,
168 grids: Vec<Option<f64>>,
169 exact_bits: Vec<bool>,
170 has_readers: Vec<bool>,
171 pub(crate) tables: Vec<CandidateTable>,
172 table_by_content: HashMap<Vec<Wire>, usize>,
173 pub(crate) one_hot_groups: Vec<OneHotGroup>,
174}
175
176impl Circuit {
177 pub fn new(source_size: usize) -> Circuit {
178 Circuit {
179 source_size,
180 operations: Vec::new(),
181 depths: vec![0; source_size],
182 constants: vec![None; source_size],
183 expression_wires: HashMap::new(),
184 ranges: vec![Range::UNKNOWN; source_size],
185 grids: vec![None; source_size],
186 exact_bits: vec![false; source_size],
187 has_readers: vec![false; source_size],
188 tables: Vec::new(),
189 table_by_content: HashMap::new(),
190 one_hot_groups: Vec::new(),
191 }
192 }
193
194 pub fn source_size(&self) -> usize {
195 self.source_size
196 }
197
198 pub fn width(&self) -> usize {
200 self.source_size + self.operations.len()
201 }
202
203 pub fn operations(&self) -> &[Operation] {
204 &self.operations
205 }
206
207 pub fn operation(&self, wire: Wire) -> Option<&Operation> {
208 wire.index().checked_sub(self.source_size).and_then(|index| self.operations.get(index))
209 }
210
211 pub fn range(&self, wire: Wire) -> Range {
212 self.ranges[wire.index()]
213 }
214
215 pub fn grid(&self, wire: Wire) -> Option<f64> {
216 self.grids[wire.index()]
217 }
218
219 pub fn is_exact_bit(&self, wire: Wire) -> bool {
220 self.exact_bits[wire.index()]
221 }
222
223 pub fn constant_value(&self, wire: Wire) -> Option<f64> {
225 self.constants[wire.index()]
226 }
227
228 pub fn depth(&self, wire: Wire) -> u32 {
230 self.depths[wire.index()]
231 }
232
233 pub fn is_non_negative(&self, wire: Wire) -> bool {
234 self.ranges[wire.index()].min >= 0.0
235 }
236
237 pub(crate) fn assert_wire(&self, wire: Wire) {
238 assert!(wire.index() < self.width(), "wire {} is not in the circuit", wire.index());
239 }
240
241 pub fn assume_range(&mut self, wire: Wire, min: f64, max: f64) {
247 self.declare_range(wire, min, max, false);
248 }
249
250 pub fn assume_range_allowing_readers(&mut self, wire: Wire, min: f64, max: f64) {
253 self.declare_range(wire, min, max, true);
254 }
255
256 fn declare_range(&mut self, wire: Wire, min: f64, max: f64, allow_existing_readers: bool) {
257 self.assert_wire(wire);
258 assert!(min <= max, "numeric range is inverted");
259 let current = self.ranges[wire.index()];
260 let narrowed = Range::new(current.min.max(min), current.max.min(max));
261 assert!(narrowed.min <= narrowed.max, "numeric range contradicts known bounds");
262 let narrows = narrowed.min > current.min || narrowed.max < current.max;
263 if narrows && self.has_readers[wire.index()] && !allow_existing_readers {
264 panic!(
265 "operations that already read wire {} were built with its range {}..{}, and declaring {}..{} now \
266 doesn't change them. Declare the range before the first operation that reads the wire",
267 wire.index(),
268 current.min,
269 current.max,
270 min,
271 max
272 );
273 }
274 self.ranges[wire.index()] = narrowed;
275 }
276
277 pub fn assume_grid(&mut self, wire: Wire, step: f64) {
279 self.declare_grid(wire, step, false);
280 }
281
282 pub fn assume_grid_allowing_readers(&mut self, wire: Wire, step: f64) {
283 self.declare_grid(wire, step, true);
284 }
285
286 fn declare_grid(&mut self, wire: Wire, step: f64, allow_existing_readers: bool) {
287 self.assert_wire(wire);
288 assert!(is_power_of_two_grid(step), "a grid step must be a power of two from 2^-149 to 2^127, got {step}");
289 let current = self.grids[wire.index()];
290 let grid = current.unwrap_or(step).max(step);
291 if Some(grid) != current && self.has_readers[wire.index()] && !allow_existing_readers {
292 panic!(
293 "operations that already read wire {} were built with grid {:?}, and declaring the grid {} now \
294 doesn't change them. Declare the grid before the first operation that reads the wire",
295 wire.index(),
296 current,
297 step
298 );
299 }
300 self.grids[wire.index()] = Some(grid);
301 }
302
303 pub fn assume_bit(&mut self, wire: Wire) {
305 self.assume_range(wire, 0.0, 1.0);
306 self.exact_bits[wire.index()] = true;
307 }
308
309 pub fn assume_one_hot(&mut self, members: &[Wire], allow_all_zero: bool) {
314 self.declare_one_hot(members, allow_all_zero, false);
315 }
316
317 pub fn assume_one_hot_allowing_readers(&mut self, members: &[Wire], allow_all_zero: bool) {
319 self.declare_one_hot(members, allow_all_zero, true);
320 }
321
322 fn declare_one_hot(&mut self, members: &[Wire], allow_all_zero: bool, allow_existing_readers: bool) {
323 assert!(!members.is_empty(), "a one-hot group needs at least one member");
324 for (index, &member) in members.iter().enumerate() {
325 self.assert_wire(member);
326 assert!(!members[..index].contains(&member), "one-hot group members must be distinct wires");
327 self.declare_range(member, 0.0, 1.0, allow_existing_readers);
328 }
329 self.one_hot_groups.push(OneHotGroup { members: members.to_vec(), allow_all_zero });
330 }
331
332 pub fn one_hot_groups(&self) -> &[OneHotGroup] {
333 &self.one_hot_groups
334 }
335
336 pub fn validate_one_hot_groups(&self, values: &[f32]) -> Result<(), String> {
339 for (group_index, group) in self.one_hot_groups.iter().enumerate() {
340 let mut ones = 0;
341 for &member in &group.members {
342 match values[member.index()] {
343 0.0 => {}
344 1.0 => ones += 1,
345 value => {
346 return Err(format!("one-hot group {group_index}: member {} holds {value}", member.index()));
347 }
348 }
349 }
350 if ones > 1 || (ones == 0 && !group.allow_all_zero) {
351 return Err(format!("one-hot group {group_index} has {ones} members set"));
352 }
353 }
354 Ok(())
355 }
356
357 pub(crate) fn mark_exact_bit(&mut self, wire: Wire) {
359 self.exact_bits[wire.index()] = true;
360 }
361
362 pub fn mark_rounding_point(&mut self, wire: Wire) {
364 self.record_rounding_point(wire, RoundingPointKind::Marked);
365 }
366
367 pub(crate) fn record_rounding_point(&mut self, wire: Wire, kind: RoundingPointKind) {
369 self.assert_wire(wire);
370 let index = wire.index().checked_sub(self.source_size).expect("a rounding point must be an operation");
371 self.operations[index].rounding_point.get_or_insert(kind);
372 }
373
374 pub fn rounding_points(&self) -> Vec<(Wire, RoundingPointKind)> {
376 self.operations
377 .iter()
378 .enumerate()
379 .filter_map(|(index, operation)| {
380 operation.rounding_point.map(|kind| (Wire::from_index(self.source_size + index), kind))
381 })
382 .collect()
383 }
384
385 pub fn source(&self, index: usize) -> Wire {
388 assert!(index < self.source_size, "source cell {index} is outside the source table");
389 Wire::from_index(index)
390 }
391
392 pub fn constant(&mut self, value: f64) -> Wire {
394 self.affine(value)
395 }
396
397 pub fn affine(&mut self, form: impl Into<Lin>) -> Wire {
400 self.append(OpKind::Affine, form.into(), Lin::default(), false, None)
401 }
402
403 pub fn round_affine(&mut self, form: impl Into<Lin>) -> Wire {
406 self.append(OpKind::Affine, form.into(), Lin::default(), true, None)
407 }
408
409 pub fn reglu(&mut self, left: impl Into<Lin>, gate: impl Into<Lin>) -> Wire {
411 self.append(OpKind::Reglu, left.into(), gate.into(), false, None)
412 }
413
414 pub fn relu(&mut self, value: impl Into<Lin>) -> Wire {
416 self.reglu(1.0, value)
417 }
418
419 pub fn boolean(&mut self, score: Wire) -> Wire {
421 let range = self.ranges[score.index()];
422 assert!(range.min >= 0.0 && range.max <= 1.0, "boolean input must be within zero and one");
423 self.append(OpKind::Boolean, Lin::from(score), Lin::default(), false, None)
424 }
425
426 pub fn multiply(&mut self, left: Wire, right: Wire) -> Wire {
428 let macro_op = Some(Macro::Multiply { left, right });
429 if self.is_non_negative(right) {
430 return self.append(OpKind::Reglu, Lin::from(left), Lin::from(right), true, macro_op);
431 }
432 if self.is_non_negative(left) {
433 return self.append(OpKind::Reglu, Lin::from(right), Lin::from(left), true, macro_op);
434 }
435 let positive = self.append(OpKind::Reglu, Lin::from(left), Lin::from(right), true, None);
436 let negative = self.append(OpKind::Reglu, Lin::from(left), -right, true, None);
437 self.append(OpKind::Affine, positive - negative, Lin::default(), true, macro_op)
438 }
439
440 pub fn select(&mut self, gate: Wire, off: Wire, on: Wire) -> Wire {
442 let complement = self.round_affine(1.0 - gate);
443 let selected_on = self.multiply(gate, on);
444 let selected_off = self.multiply(complement, off);
445 self.append(
446 OpKind::Affine,
447 selected_on + selected_off,
448 Lin::default(),
449 true,
450 Some(Macro::Select { gate, off, on }),
451 )
452 }
453
454 pub fn index_read(&mut self, index: Wire, candidates: &[Wire], out_of_range: OutOfRange) -> Wire {
457 self.assert_wire(index);
458 let declared = self.ranges[index.index()];
459 self.index_read_affine(Lin::from(index), candidates, out_of_range, out_of_range, Some(declared))
460 }
461
462 pub fn index_read_affine(
464 &mut self,
465 index: Lin,
466 candidates: &[Wire],
467 below: OutOfRange,
468 above: OutOfRange,
469 declared_index_range: Option<Range>,
470 ) -> Wire {
471 for &(wire, coefficient) in &index.terms {
472 self.assert_wire(wire);
473 assert!(coefficient.is_finite(), "index coefficient must be finite");
474 }
475 assert!(index.bias.is_finite(), "index bias must be finite");
476 let table = self.register_candidate_table(candidates);
477 let (terms, bias) = self.fold_constant_terms(normalize_terms(&index.terms), index.bias);
478 let read_terms = terms.clone();
479 let (terms, bias) = self.compose_affine_terms(&terms, bias);
480 if terms.is_empty() {
481 let constant_index = fround(bias);
482 assert!(!constant_index.is_nan(), "a constant index must not be NaN");
483 let count = self.tables[table].candidates.len();
484 let position = resolve_index_position(constant_index, count, below, above);
485 return if position >= 0 { self.tables[table].candidates[position as usize] } else { self.affine(0.0) };
486 }
487 let mut index_range = evaluate_affine_range(&terms, bias, &self.ranges);
488 #[allow(clippy::neg_cmp_op_on_partial_ord)]
490 if let Some(declared) = declared_index_range {
491 index_range = Range::new(index_range.min.max(declared.min), index_range.max.min(declared.max));
492 assert!(
493 !(index_range.min > index_range.max),
494 "the declared index range contradicts the range inferred from the index form"
495 );
496 }
497 assert!(
498 index_range.min > -INDEX_MAGNITUDE_LIMIT && index_range.max < INDEX_MAGNITUDE_LIMIT,
499 "index read needs an index range declared strictly inside ±2^24, got {}..{}",
500 index_range.min,
501 index_range.max
502 );
503 let mut key = expression_key(OpKind::Index, &[], 0.0, &terms, bias);
504 key.extend([u64::MAX, below as u64, above as u64, table as u64]);
505 if let Some(&existing) = self.expression_wires.get(&key) {
506 return existing;
507 }
508 let mut depth = self.tables[table].maximum_depth;
509 for &(dependency, _) in &terms {
510 depth = depth.max(self.depths[dependency.index()]);
511 }
512 let wire = Wire::from_index(self.width());
513 let range = index_read_range(&self.tables[table], index_range, below, above, &self.ranges);
514 self.operations.push(Operation {
515 kind: OpKind::Index,
516 left_terms: Vec::new(),
517 left_bias: 0.0,
518 gate_terms: terms,
519 gate_bias: bias,
520 depth: depth + 1,
521 preserve_rounding: false,
522 macro_op: None,
523 index_read: Some(IndexRead { table, below, above, index_range }),
524 rounding_point: None,
525 comparison_bit: false,
526 comparison: None,
527 });
528 self.push_wire_metadata(depth + 1, None, None, range);
529 for &(read, _) in &read_terms {
530 self.has_readers[read.index()] = true;
531 }
532 self.expression_wires.insert(key, wire);
533 wire
534 }
535
536 fn register_candidate_table(&mut self, candidates: &[Wire]) -> usize {
537 assert!(!candidates.is_empty(), "an index read needs at least one candidate");
538 if let Some(&table) = self.table_by_content.get(candidates) {
539 return table;
540 }
541 let mut maximum_depth = 0;
542 for &candidate in candidates {
543 self.assert_wire(candidate);
544 maximum_depth = maximum_depth.max(self.depths[candidate.index()]);
545 self.has_readers[candidate.index()] = true;
546 }
547 let table = self.tables.len();
548 self.tables.push(CandidateTable { candidates: candidates.to_vec(), maximum_depth });
549 self.table_by_content.insert(candidates.to_vec(), table);
550 table
551 }
552
553 pub(crate) fn append(
558 &mut self,
559 mut kind: OpKind,
560 left: Lin,
561 gate: Lin,
562 preserve_rounding: bool,
563 macro_op: Option<Macro>,
564 ) -> Wire {
565 debug_assert!(kind != OpKind::Index);
566 for &(wire, _) in left.terms.iter().chain(&gate.terms) {
567 self.assert_wire(wire);
568 }
569 let (mut left_terms, mut left_bias) = (left.terms, left.bias);
570 let (mut gate_terms, mut gate_bias) = (gate.terms, gate.bias);
571 let read_left_terms;
572 let read_gate_terms;
573 if !preserve_rounding {
574 (left_terms, left_bias) = self.fold_constant_terms(normalize_terms(&left_terms), left_bias);
575 (gate_terms, gate_bias) = self.fold_constant_terms(normalize_terms(&gate_terms), gate_bias);
576 read_left_terms = left_terms.clone();
577 read_gate_terms = gate_terms.clone();
578 if kind == OpKind::Affine
579 && let Some(alias) = identity_wire(&left_terms, left_bias)
580 {
581 return alias;
582 }
583 (left_terms, left_bias) = self.compose_affine_terms(&left_terms, left_bias);
584 (gate_terms, gate_bias) = self.compose_affine_terms(&gate_terms, gate_bias);
585 if kind == OpKind::Reglu && gate_terms.is_empty() {
586 let gate_value = if gate_bias.is_nan() { f64::NAN } else { gate_bias.max(0.0) };
587 kind = OpKind::Affine;
588 if gate_value == 0.0 {
589 left_terms.clear();
590 } else {
591 for term in &mut left_terms {
592 term.1 *= gate_value;
593 }
594 }
595 left_bias *= gate_value;
596 gate_bias = 0.0;
597 } else if kind == OpKind::Reglu && left_terms.is_empty() && left_bias == 0.0 {
598 kind = OpKind::Affine;
599 gate_terms.clear();
600 gate_bias = 0.0;
601 }
602 if kind == OpKind::Affine
603 && let Some(alias) = identity_wire(&left_terms, left_bias)
604 {
605 return alias;
606 }
607 } else {
608 read_left_terms = left_terms.clone();
609 read_gate_terms = gate_terms.clone();
610 }
611 let mut key = expression_key(kind, &left_terms, left_bias, &gate_terms, gate_bias);
612 if preserve_rounding {
613 key.push(u64::MAX - 1);
614 }
615 match macro_op {
616 Some(Macro::Multiply { left, right }) => key.extend([u64::MAX - 2, left.0 as u64, right.0 as u64]),
617 Some(Macro::Select { gate, off, on }) => {
618 key.extend([u64::MAX - 3, gate.0 as u64, off.0 as u64, on.0 as u64])
619 }
620 None => {}
621 }
622 if let Some(&existing) = self.expression_wires.get(&key) {
623 return existing;
624 }
625 let wire = Wire::from_index(self.width());
626 let gate_dependencies: &[Term] = if kind == OpKind::Reglu { &gate_terms } else { &[] };
627 let depth = left_terms
628 .iter()
629 .chain(gate_dependencies)
630 .map(|&(dependency, _)| self.depths[dependency.index()])
631 .max()
632 .unwrap_or(0);
633 let constant =
634 if !preserve_rounding && kind == OpKind::Affine && left_terms.is_empty() { Some(left_bias) } else { None };
635 let grid =
636 infer_operation_grid(kind, &left_terms, left_bias, &gate_terms, gate_bias, &self.grids, &self.ranges);
637 let left_range = evaluate_affine_range(&left_terms, left_bias, &self.ranges);
638 let range = match kind {
639 OpKind::Affine => left_range,
640 OpKind::Reglu => {
641 multiply_by_relu_range(left_range, evaluate_affine_range(&gate_terms, gate_bias, &self.ranges))
642 }
643 _ => Range::new(0.0, 1.0),
644 };
645 if !left_terms.is_empty() {
646 for &(read, _) in &read_left_terms {
647 self.has_readers[read.index()] = true;
648 }
649 }
650 if !gate_terms.is_empty() {
651 for &(read, _) in &read_gate_terms {
652 self.has_readers[read.index()] = true;
653 }
654 }
655 self.operations.push(Operation {
656 kind,
657 left_terms,
658 left_bias,
659 gate_terms,
660 gate_bias,
661 depth: depth + 1,
662 preserve_rounding,
663 macro_op,
664 index_read: None,
665 rounding_point: if kind == OpKind::Boolean { Some(RoundingPointKind::Boolean) } else { None },
666 comparison_bit: false,
667 comparison: None,
668 });
669 self.push_wire_metadata(depth + 1, constant, grid, range);
670 self.expression_wires.insert(key, wire);
671 wire
672 }
673
674 fn push_wire_metadata(&mut self, depth: u32, constant: Option<f64>, grid: Option<f64>, range: Range) {
675 self.depths.push(depth);
676 self.constants.push(constant);
677 self.grids.push(grid);
678 self.exact_bits.push(false);
679 self.has_readers.push(false);
680 self.ranges.push(range);
681 }
682
683 fn fold_constant_terms(&self, terms: Vec<Term>, mut bias: f64) -> (Vec<Term>, f64) {
684 let mut dynamic_terms = Vec::with_capacity(terms.len());
685 for (wire, coefficient) in terms {
686 match self.constants[wire.index()] {
687 Some(constant) => bias += coefficient * constant,
688 None => dynamic_terms.push((wire, coefficient)),
689 }
690 }
691 (dynamic_terms, bias)
692 }
693
694 fn compose_affine_terms(&self, terms: &[Term], mut bias: f64) -> (Vec<Term>, f64) {
695 let mut composed = Vec::with_capacity(terms.len());
696 for &(wire, coefficient) in terms {
697 let producer = self.operation(wire);
698 match producer {
699 Some(operation)
700 if operation.kind == OpKind::Affine
701 && !operation.preserve_rounding
702 && operation.left_terms.len() < AFFINE_SHARE_THRESHOLD =>
703 {
704 bias += coefficient * operation.left_bias;
705 for &(dependency, dependency_coefficient) in &operation.left_terms {
706 composed.push((dependency, coefficient * dependency_coefficient));
707 }
708 }
709 _ => composed.push((wire, coefficient)),
710 }
711 }
712 (normalize_terms(&composed), bias)
713 }
714
715 pub fn validate_source_ranges(&self, source: &[f32]) -> Result<(), Error> {
719 if source.len() != self.source_size {
720 return Err(Error::SourceLength { expected: self.source_size, actual: source.len() });
721 }
722 for (index, &value) in source.iter().enumerate() {
723 let range = self.ranges[index];
724 let value = value as f64;
725 if !(value >= range.min && value <= range.max) {
726 return Err(Error::SourceOutOfRange { wire: Wire::from_index(index), value, range });
727 }
728 }
729 Ok(())
730 }
731
732 pub fn evaluate_reference(&self, source: &[f32]) -> Result<Vec<f32>, Error> {
735 self.validate_source_ranges(source)?;
736 Ok(self.evaluate_unchecked(source))
737 }
738
739 pub(crate) fn evaluate_unchecked(&self, source: &[f32]) -> Vec<f32> {
740 let mut values = Vec::with_capacity(self.width());
741 values.extend_from_slice(source);
742 for operation in &self.operations {
743 let left = evaluate_terms(&values, &operation.left_terms, operation.left_bias);
744 let value = match operation.kind {
745 OpKind::Affine => left,
746 OpKind::Reglu => left * relu32(evaluate_terms(&values, &operation.gate_terms, operation.gate_bias)),
747 OpKind::Boolean => evaluate_boolean(left),
748 OpKind::Index => {
749 let read = operation.index_read.as_ref().unwrap();
750 let index = evaluate_terms(&values, &operation.gate_terms, operation.gate_bias);
751 read_indexed_value(&values, &self.tables[read.table].candidates, read, index as f64)
752 }
753 };
754 values.push(value);
755 }
756 values
757 }
758
759 pub fn backpropagate_reference(
763 &self,
764 values: &[f32],
765 output_gradients: &[f32],
766 options: BackpropOptions,
767 ) -> Result<Vec<f32>, Error> {
768 assert_eq!(values.len(), self.width(), "forward values have the wrong length");
769 assert_eq!(output_gradients.len(), self.width(), "output gradients have the wrong length");
770 let mut gradients = output_gradients.to_vec();
771 for (operation_index, operation) in self.operations.iter().enumerate().rev() {
772 let output_wire = self.source_size + operation_index;
773 let output_gradient = gradients[output_wire];
774 if output_gradient == 0.0 {
775 continue;
776 }
777 let surrogate_width = match (options.comparison_gradient, &operation.comparison) {
778 (ComparisonGradient::Surrogate { width }, Some(_)) => {
779 assert!(width > 0.0 && width.is_finite(), "a surrogate width must be positive and finite");
780 Some(width)
781 }
782 (ComparisonGradient::RelativeSurrogate { fraction }, Some(comparison)) => {
783 assert!(fraction > 0.0 && fraction.is_finite(), "a surrogate fraction must be positive and finite");
784 let range = evaluate_affine_range(&comparison.terms, comparison.bias, &self.ranges);
785 let span = range.max - range.min;
786 if !(span > 0.0 && span.is_finite()) {
788 continue;
789 }
790 Some(fraction * span)
791 }
792 _ => None,
793 };
794 if let (Some(width), Some(comparison)) = (surrogate_width, &operation.comparison) {
795 let difference = comparison.bias
796 + comparison
797 .terms
798 .iter()
799 .map(|&(wire, coefficient)| coefficient * values[wire.index()] as f64)
800 .sum::<f64>();
801 let sigmoid = 1.0 / (1.0 + (-difference / width).exp());
802 let slope = if comparison.equal {
803 4.0 * sigmoid * (1.0 - sigmoid) * (1.0 - 2.0 * sigmoid) / width
804 } else {
805 sigmoid * (1.0 - sigmoid) / width
806 };
807 let scaled: Vec<Term> =
808 comparison.terms.iter().map(|&(wire, coefficient)| (wire, coefficient * slope)).collect();
809 accumulate(&mut gradients, &scaled, output_gradient);
810 continue;
811 }
812 match operation.macro_op {
813 Some(Macro::Multiply { left, right }) => {
814 accumulate(&mut gradients, &[(left, values[right.index()] as f64)], output_gradient);
815 accumulate(&mut gradients, &[(right, values[left.index()] as f64)], output_gradient);
816 continue;
817 }
818 Some(Macro::Select { gate, off, on }) => {
819 let gate_value = values[gate.index()];
820 accumulate(
821 &mut gradients,
822 &[(off, (1.0 - gate_value) as f64), (on, gate_value as f64)],
823 output_gradient,
824 );
825 let swing = values[on.index()] - values[off.index()];
826 accumulate(&mut gradients, &[(gate, swing as f64)], output_gradient);
827 continue;
828 }
829 None => {}
830 }
831 match operation.kind {
832 OpKind::Index => {
833 let read = operation.index_read.as_ref().unwrap();
834 if options.index_gradient == IndexGradient::Reject {
835 return Err(Error::IndexGradient { wire: Wire::from_index(output_wire) });
836 }
837 let candidates = &self.tables[read.table].candidates;
838 let index_value = evaluate_terms(values, &operation.gate_terms, operation.gate_bias) as f64;
839 let position = resolve_index_position(index_value, candidates.len(), read.below, read.above);
840 if position >= 0 {
841 accumulate(&mut gradients, &[(candidates[position as usize], 1.0)], output_gradient);
842 }
843 if options.index_gradient == IndexGradient::BackwardDifference && index_value.is_finite() {
844 let floor_index = index_value.floor();
845 let difference = read_indexed_value(values, candidates, read, floor_index) as f64
846 - read_indexed_value(values, candidates, read, floor_index - 1.0) as f64;
847 let scaled = (output_gradient as f64 * difference) as f32;
848 accumulate(&mut gradients, &operation.gate_terms, scaled);
849 }
850 }
851 OpKind::Boolean => {
852 if options.boolean_gradient != BooleanGradient::StraightThrough {
853 return Err(Error::BooleanGradient { wire: Wire::from_index(output_wire) });
854 }
855 accumulate(&mut gradients, &operation.left_terms, output_gradient);
856 }
857 OpKind::Affine => accumulate(&mut gradients, &operation.left_terms, output_gradient),
858 OpKind::Reglu => {
859 let left = evaluate_terms(values, &operation.left_terms, operation.left_bias);
860 let gate = evaluate_terms(values, &operation.gate_terms, operation.gate_bias);
861 accumulate(&mut gradients, &operation.left_terms, output_gradient * relu32(gate));
862 if gate > 0.0 {
863 accumulate(&mut gradients, &operation.gate_terms, output_gradient * left);
864 }
865 }
866 }
867 }
868 Ok(gradients)
869 }
870}
871
872fn key_number(value: f64) -> u64 {
875 (value + 0.0).to_bits()
877}
878
879pub(crate) fn expression_key(
880 kind: OpKind,
881 left_terms: &[Term],
882 left_bias: f64,
883 gate_terms: &[Term],
884 gate_bias: f64,
885) -> Vec<u64> {
886 let mut key = Vec::with_capacity(4 + 2 * (left_terms.len() + gate_terms.len()));
887 key.push(kind as u64);
888 key.push(key_number(left_bias));
889 for &(wire, coefficient) in left_terms {
890 key.extend([wire.0 as u64, key_number(coefficient)]);
891 }
892 key.push(u64::MAX - 4);
893 key.push(key_number(gate_bias));
894 for &(wire, coefficient) in gate_terms {
895 key.extend([wire.0 as u64, key_number(coefficient)]);
896 }
897 key
898}
899
900fn identity_wire(terms: &[Term], bias: f64) -> Option<Wire> {
901 match terms {
902 [(wire, coefficient)] if bias == 0.0 && *coefficient == 1.0 => Some(*wire),
903 _ => None,
904 }
905}
906
907pub(crate) fn normalize_terms(terms: &[Term]) -> Vec<Term> {
909 let mut sorted = terms.to_vec();
910 sorted.sort_by_key(|&(wire, _)| wire);
911 let mut normalized: Vec<Term> = Vec::with_capacity(sorted.len());
912 for (wire, coefficient) in sorted {
913 match normalized.last_mut() {
914 Some(previous) if previous.0 == wire => previous.1 += coefficient,
915 _ => normalized.push((wire, coefficient)),
916 }
917 }
918 normalized.retain(|&(_, coefficient)| coefficient != 0.0);
919 normalized
920}
921
922pub(crate) fn evaluate_terms(values: &[f32], terms: &[Term], bias: f64) -> f32 {
924 let mut value = bias as f32;
925 for &(wire, coefficient) in terms {
926 value += coefficient as f32 * values[wire.index()];
927 }
928 value
929}
930
931fn accumulate(gradients: &mut [f32], terms: &[Term], output_gradient: f32) {
932 for &(wire, coefficient) in terms {
933 gradients[wire.index()] += output_gradient * coefficient as f32;
934 }
935}
936
937pub(crate) fn resolve_index_position(index: f64, count: usize, below: OutOfRange, above: OutOfRange) -> i64 {
940 if index.is_nan() {
941 return -2;
942 }
943 let position = index.floor();
944 if position < 0.0 {
945 return if below == OutOfRange::Zero { -1 } else { 0 };
946 }
947 if position > (count - 1) as f64 {
948 return if above == OutOfRange::Zero { -1 } else { count as i64 - 1 };
949 }
950 position as i64
951}
952
953fn read_indexed_value(values: &[f32], candidates: &[Wire], read: &IndexRead, index: f64) -> f32 {
954 match resolve_index_position(index, candidates.len(), read.below, read.above) {
955 -2 => f32::NAN,
956 -1 => 0.0,
957 position => values[candidates[position as usize].index()],
958 }
959}
960
961pub(crate) fn evaluate_affine_range(terms: &[Term], bias: f64, ranges: &[Range]) -> Range {
962 let mut min = fround(bias);
963 let mut max = fround(bias);
964 for &(wire, coefficient) in terms {
965 let range = ranges[wire.index()];
966 let rounded = fround(coefficient);
967 if rounded > 0.0 {
968 min = fround(min + fround(rounded * range.min));
969 max = fround(max + fround(rounded * range.max));
970 } else {
971 min = fround(min + fround(rounded * range.max));
972 max = fround(max + fround(rounded * range.min));
973 }
974 if min.is_nan() || max.is_nan() {
975 return Range::UNKNOWN;
976 }
977 }
978 if min.is_nan() || max.is_nan() {
979 return Range::UNKNOWN;
980 }
981 Range::new(min, max)
982}
983
984fn multiply_by_relu_range(left: Range, gate: Range) -> Range {
985 let gate_min = gate.min.max(0.0);
986 let gate_max = gate.max.max(0.0);
987 let products = [
988 fround(left.min * gate_min),
989 fround(left.min * gate_max),
990 fround(left.max * gate_min),
991 fround(left.max * gate_max),
992 ];
993 if products.iter().any(|product| product.is_nan()) {
995 return Range::UNKNOWN;
996 }
997 Range::new(
998 products.iter().copied().fold(f64::INFINITY, f64::min),
999 products.iter().copied().fold(f64::NEG_INFINITY, f64::max),
1000 )
1001}
1002
1003fn index_read_range(
1004 table: &CandidateTable,
1005 index_range: Range,
1006 below: OutOfRange,
1007 above: OutOfRange,
1008 ranges: &[Range],
1009) -> Range {
1010 let count = table.candidates.len() as f64;
1011 let lowest = index_range.min.floor().min(count).max(-1.0) as i64;
1012 let highest = index_range.max.floor().min(count).max(-1.0) as i64;
1013 let count = table.candidates.len() as i64;
1014 let mut min = f64::INFINITY;
1015 let mut max = f64::NEG_INFINITY;
1016 let mut include = |range: Range| {
1017 min = min.min(range.min);
1018 max = max.max(range.max);
1019 };
1020 for position in lowest.max(0)..=highest.min(count - 1) {
1021 include(ranges[table.candidates[position as usize].index()]);
1022 }
1023 let zero = Range::new(0.0, 0.0);
1024 if lowest < 0 {
1025 include(if below == OutOfRange::Zero { zero } else { ranges[table.candidates[0].index()] });
1026 }
1027 if highest > count - 1 {
1028 include(if above == OutOfRange::Zero { zero } else { ranges[table.candidates[(count - 1) as usize].index()] });
1029 }
1030 Range::new(min, max)
1031}
1032
1033struct GridForm {
1034 grid: f64,
1035 magnitude: f64,
1036}
1037
1038fn infer_form_grid(terms: &[Term], bias: f64, grids: &[Option<f64>], ranges: &[Range]) -> Option<GridForm> {
1042 let rounded_bias = fround(bias);
1043 let mut grid = if rounded_bias == 0.0 { f64::INFINITY } else { lowest_power_of_two(rounded_bias) };
1044 let mut magnitude = rounded_bias.abs();
1045 for &(wire, coefficient) in terms {
1046 let wire_grid = grids[wire.index()]?;
1047 let range = ranges[wire.index()];
1048 let largest_operand = range.min.abs().max(range.max.abs());
1049 if !largest_operand.is_finite() {
1050 return None;
1051 }
1052 let rounded = fround(coefficient);
1053 if rounded == 0.0 {
1054 continue;
1055 }
1056 grid = grid.min(wire_grid * lowest_power_of_two(rounded));
1057 magnitude += rounded.abs() * largest_operand;
1058 }
1059 if !(grid.is_finite() && grid >= smallest_grid() && magnitude <= EXACT_GRID_STEPS * grid) {
1060 return None;
1061 }
1062 Some(GridForm { grid: grid.min(largest_grid()), magnitude })
1063}
1064
1065fn infer_operation_grid(
1066 kind: OpKind,
1067 left_terms: &[Term],
1068 left_bias: f64,
1069 gate_terms: &[Term],
1070 gate_bias: f64,
1071 grids: &[Option<f64>],
1072 ranges: &[Range],
1073) -> Option<f64> {
1074 if kind == OpKind::Boolean {
1075 return Some(1.0);
1076 }
1077 let left = infer_form_grid(left_terms, left_bias, grids, ranges)?;
1078 if kind == OpKind::Affine {
1079 return Some(left.grid);
1080 }
1081 let gate = infer_form_grid(gate_terms, gate_bias, grids, ranges)?;
1082 let grid = left.grid * gate.grid;
1083 if !(grid >= smallest_grid() && left.magnitude * gate.magnitude <= EXACT_GRID_STEPS * grid) {
1084 return None;
1085 }
1086 Some(grid.min(largest_grid()))
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091 use super::*;
1092
1093 #[test]
1094 fn small_affine_sums_compose_and_constants_fold() {
1095 let mut circuit = Circuit::new(2);
1096 let (a, b) = (circuit.source(0), circuit.source(1));
1097 let three = circuit.constant(3.0);
1098 let sum = circuit.affine(a + b * 2.0);
1099 let shifted = circuit.affine(sum - three);
1100 let operation = circuit.operation(shifted).unwrap();
1101 assert_eq!(operation.left_terms, vec![(a, 1.0), (b, 2.0)]);
1102 assert_eq!(operation.left_bias, -3.0);
1103 assert_eq!(circuit.affine(a + b * 2.0), sum);
1105 assert_eq!(circuit.affine(a), a);
1107 }
1108
1109 #[test]
1110 fn ranges_and_grids_are_inferred() {
1111 let mut circuit = Circuit::new(1);
1112 let x = circuit.source(0);
1113 circuit.assume_range(x, 0.0, 16_384.0);
1114 circuit.assume_grid(x, 1.0 / 64.0);
1115 let half = circuit.affine(x * 0.5 - 26.0);
1116 assert_eq!(circuit.range(half), Range::new(-26.0, 8_166.0));
1117 assert_eq!(circuit.grid(half), Some(1.0 / 128.0));
1118 }
1119
1120 #[test]
1121 #[should_panic(expected = "already read wire 0")]
1122 fn late_range_declaration_is_refused() {
1123 let mut circuit = Circuit::new(1);
1124 let x = circuit.source(0);
1125 circuit.relu(x);
1126 circuit.assume_range(x, 0.0, 1.0);
1127 }
1128
1129 #[test]
1130 fn index_read_floors_and_applies_side_rules() {
1131 let mut circuit = Circuit::new(1);
1132 let index = circuit.source(0);
1133 circuit.assume_range(index, -2.0, 10.0);
1134 let table: Vec<Wire> = [28.0, 44.0, 28.0, 60.0].iter().map(|&value| circuit.constant(value)).collect();
1135 let clamped = circuit.index_read(index, &table, OutOfRange::Clamp);
1136 let zeroed = circuit.index_read(index, &table, OutOfRange::Zero);
1137 assert_eq!(circuit.range(zeroed), Range::new(0.0, 60.0));
1138 for (input, expected_clamped, expected_zero) in
1139 [(2.5, 28.0, 28.0), (1.0, 44.0, 44.0), (-1.0, 28.0, 0.0), (7.0, 60.0, 0.0)]
1140 {
1141 let values = circuit.evaluate_reference(&[input]).unwrap();
1142 assert_eq!(values[clamped.index()], expected_clamped);
1143 assert_eq!(values[zeroed.index()], expected_zero);
1144 }
1145 let constant_index =
1147 circuit.index_read_affine(Lin::constant(3.0), &table, OutOfRange::Clamp, OutOfRange::Clamp, None);
1148 assert_eq!(constant_index, table[3]);
1149 }
1150
1151 #[test]
1152 fn source_ranges_are_checked() {
1153 let mut circuit = Circuit::new(1);
1154 let x = circuit.source(0);
1155 circuit.assume_range(x, 0.0, 1.0);
1156 assert!(matches!(circuit.evaluate_reference(&[2.0]), Err(Error::SourceOutOfRange { .. })));
1157 assert!(matches!(circuit.evaluate_reference(&[f32::NAN]), Err(Error::SourceOutOfRange { .. })));
1158 }
1159}