diff --git a/arduino/strix_capture/strix_capture.ino b/arduino/strix_capture/strix_capture.ino index 173b579..5e115d8 100644 --- a/arduino/strix_capture/strix_capture.ino +++ b/arduino/strix_capture/strix_capture.ino @@ -8,6 +8,7 @@ // cap start a capture with the current settings and send it as // "DATA \n" followed by n raw bytes // stats print the latest statistics trial snapshot as hex +// amp print the latest bias amplifier trial snapshot as hex #include #include @@ -25,6 +26,7 @@ enum : uint8_t { REG_STATUS = 0x0C, REG_DATA = 0x10, REG_STATS = 0x40, + REG_AMP = 0x42, }; const uint8_t READ = 0x80; @@ -32,7 +34,8 @@ const uint8_t CONTROL_START_CAPTURE = 0x01; const uint8_t STATUS_CAPTURE_DONE = 0x02; const size_t CAPTURE_BYTES = 32768; const size_t CHUNK_BYTES = 512; -const size_t SNAPSHOT_BYTES = 72; +const size_t STATS_SNAPSHOT_BYTES = 72; +const size_t AMP_SNAPSHOT_BYTES = 44; VidorUtils fpga; @@ -92,9 +95,9 @@ void capture() { } } -void printSnapshot() { - beginTransaction(READ | REG_STATS); - for (size_t i = 0; i < SNAPSHOT_BYTES; i++) { +void printSnapshot(uint8_t reg, size_t length) { + beginTransaction(READ | reg); + for (size_t i = 0; i < length; i++) { uint8_t value = strixSpi.transfer(0); if (value < 0x10) Serial.print('0'); Serial.print(value, HEX); @@ -131,7 +134,9 @@ void handleCommand(String line) { } else if (command == "cap") { capture(); } else if (command == "stats") { - printSnapshot(); + printSnapshot(REG_STATS, STATS_SNAPSHOT_BYTES); + } else if (command == "amp") { + printSnapshot(REG_AMP, AMP_SNAPSHOT_BYTES); } else if (line.length() > 0) { Serial.println("ERR unknown or malformed command"); } diff --git a/devbox.json b/devbox.json index 8309783..874e6ed 100644 --- a/devbox.json +++ b/devbox.json @@ -25,8 +25,10 @@ ], "sim": [ "mkdir -p /tmp/strix-sim", - "iverilog -g2005 -DSIM -o /tmp/strix-sim/tb_strix_core fpga/sim/tb_strix_core.v fpga/rtl/strix_core.v fpga/rtl/entropy_generator.v fpga/rtl/health_monitor.v fpga/rtl/stats_block.v fpga/rtl/freq_counter.v fpga/rtl/ring_osc.v fpga/rtl/raw_capture.v fpga/rtl/spi_slave.v", - "vvp -n /tmp/strix-sim/tb_strix_core" + "iverilog -g2005 -DSIM -o /tmp/strix-sim/tb_strix_core fpga/sim/tb_strix_core.v fpga/rtl/strix_core.v fpga/rtl/entropy_generator.v fpga/rtl/health_monitor.v fpga/rtl/stats_block.v fpga/rtl/bias_amplifier.v fpga/rtl/amp_path.v fpga/rtl/amp_stage.v fpga/rtl/freq_counter.v fpga/rtl/ring_osc.v fpga/rtl/raw_capture.v fpga/rtl/spi_slave.v", + "vvp -n /tmp/strix-sim/tb_strix_core", + "iverilog -g2005 -o /tmp/strix-sim/tb_amp_stage fpga/sim/tb_amp_stage.v fpga/rtl/amp_stage.v", + "vvp -n /tmp/strix-sim/tb_amp_stage" ], "fpga": [ "cd fpga/src && quartus_sh --flow compile strix" diff --git a/fpga/rtl/amp_path.v b/fpga/rtl/amp_path.v new file mode 100644 index 0000000..517e36c --- /dev/null +++ b/fpga/rtl/amp_path.v @@ -0,0 +1,109 @@ +// One amplifier path: the generator streams are merged into one stream, which then passes +// three stages in series. Merging before amplifying is equivalent to amplifying each stream +// with the same first stage and merging after (the random walk is distributive), and needs a +// third of the stages. Amplified bits are tallied per sub-trial and per trial. +module amp_path #( + parameter GENERATORS = 4 +) ( + input wire clk, + input wire restart, + input wire [GENERATORS-1:0] in_valid, + input wire [GENERATORS-1:0] in_bit, + input wire [2:0] stage_mode, + input wire [47:0] stage_param, // stage s at [16*s +: 16] + input wire sub_end, // pulse: close the current sub-trial + input wire [3:0] sub_index, // sub-trial being closed + input wire trial_end, // pulse: start a new trial (after the last sub_end) + output reg [31:0] trial_bits, // amplified bits in the current trial + output reg [31:0] trial_ones, + output reg [15:0] votes, // bit i: sub-trial i voted 1 + output reg [15:0] voted, // bit i: sub-trial i had a majority + output reg overflow // a merge input arrived before the last was taken +); + + // Merge: each generator's latest bit waits in its own slot; one slot is drained per clock, + // lowest index first. Generators deliver at most one bit per `temporal` (>= 1, default 5) + // clocks in lockstep, so with up to 4 generators the slots never overflow at temporal >= 4 + // (flagged if they do). + + reg [GENERATORS-1:0] pending, pending_bit, take; + reg merged_valid, merged_bit; + integer i; + + always @(*) begin + take = {GENERATORS{1'b0}}; + for (i = 0; i < GENERATORS; i = i + 1) + if (pending[i] && take == {GENERATORS{1'b0}}) take[i] = 1'b1; + end + + always @(posedge clk) begin + merged_valid <= |take && !restart; + for (i = 0; i < GENERATORS; i = i + 1) + if (take[i]) merged_bit <= pending_bit[i]; + + if (restart) begin + pending <= {GENERATORS{1'b0}}; + overflow <= 1'b0; + end else begin + for (i = 0; i < GENERATORS; i = i + 1) begin + if (in_valid[i]) begin + if (pending[i] && !take[i]) overflow <= 1'b1; + pending[i] <= 1'b1; + pending_bit[i] <= in_bit[i]; + end else if (take[i]) begin + pending[i] <= 1'b0; + end + end + end + end + + // Stages + + wire first_valid, first_bit, second_valid, second_bit, out_valid, out_bit; + + amp_stage first ( + .clk(clk), .restart(restart), .mode(stage_mode[0]), .param(stage_param[15:0]), + .in_valid(merged_valid), .in_bit(merged_bit), .out_valid(first_valid), .out_bit(first_bit) + ); + + amp_stage second ( + .clk(clk), .restart(restart), .mode(stage_mode[1]), .param(stage_param[31:16]), + .in_valid(first_valid), .in_bit(first_bit), .out_valid(second_valid), .out_bit(second_bit) + ); + + amp_stage third ( + .clk(clk), .restart(restart), .mode(stage_mode[2]), .param(stage_param[47:32]), + .in_valid(second_valid), .in_bit(second_bit), .out_valid(out_valid), .out_bit(out_bit) + ); + + // Tallies. A bit arriving in a sub_end or trial_end cycle counts towards the next one, so + // the trial count and the sum of its sub-trial counts can differ by one bit. + + reg [23:0] sub_bits, sub_ones; + + always @(posedge clk) begin + if (restart || trial_end) begin + trial_bits <= out_valid ? 32'd1 : 32'd0; + trial_ones <= (out_valid && out_bit) ? 32'd1 : 32'd0; + votes <= 16'd0; + voted <= 16'd0; + end else if (out_valid) begin + trial_bits <= trial_bits + 32'd1; + trial_ones <= trial_ones + out_bit; + end + + if (restart || sub_end) begin + sub_bits <= out_valid ? 24'd1 : 24'd0; + sub_ones <= (out_valid && out_bit) ? 24'd1 : 24'd0; + end else if (out_valid) begin + sub_bits <= sub_bits + 24'd1; + sub_ones <= sub_ones + out_bit; + end + + if (!restart && sub_end && sub_bits != 0 && {sub_ones, 1'b0} != sub_bits) begin + voted[sub_index] <= 1'b1; + votes[sub_index] <= {sub_ones, 1'b0} > sub_bits; + end + end + +endmodule diff --git a/fpga/rtl/amp_stage.v b/fpga/rtl/amp_stage.v new file mode 100644 index 0000000..75bf152 --- /dev/null +++ b/fpga/rtl/amp_stage.v @@ -0,0 +1,48 @@ +// One bias amplifier stage. Both modes track the walk position d = ones - zeros. +// +// Random walk (mode 0): reaching d = +param outputs a 1, d = -param outputs a 0, and the walk +// restarts at 0. Stages compose: bounds n1 then n2 amplify like a single bound n1*n2. +// +// Majority vote (mode 1): every param input bits produce one output bit, 1 if d > 0 (use odd +// windows to avoid ties, which output 0). +// +// param = 1 passes bits through unchanged in both modes. +module amp_stage ( + input wire clk, + input wire restart, + input wire mode, // 0 random walk, 1 majority vote + input wire [15:0] param, // bound or window, at least 1 + input wire in_valid, + input wire in_bit, + output reg out_valid, + output reg out_bit +); + + reg signed [16:0] position; + reg [15:0] seen; + + wire signed [16:0] next_position = position + (in_bit ? 17'sd1 : -17'sd1); + wire signed [16:0] bound = {1'b0, param}; + wire walk_done = next_position == bound || next_position == -bound; + wire vote_done = seen == param - 16'd1; + wire done = mode ? vote_done : walk_done; + + always @(posedge clk) begin + out_valid <= 1'b0; + if (restart) begin + position <= 17'sd0; + seen <= 16'd0; + end else if (in_valid) begin + if (done) begin + out_valid <= 1'b1; + out_bit <= next_position > 0; + position <= 17'sd0; + seen <= 16'd0; + end else begin + position <= next_position; + seen <= seen + 16'd1; + end + end + end + +endmodule diff --git a/fpga/rtl/bias_amplifier.v b/fpga/rtl/bias_amplifier.v new file mode 100644 index 0000000..baef0f5 --- /dev/null +++ b/fpga/rtl/bias_amplifier.v @@ -0,0 +1,186 @@ +// Bias amplifier with two paths fed by the selected healthy generators: +// path 0 (bias): the generator bits themselves; +// path 1 (autocorrelation): each bit XNOR the previous bit of the same generator, so +// positive lag-1 autocorrelation becomes an excess of ones. +// An optional von Neumann extractor per generator runs before both paths. +// +// Trials consist of `subtrials` sub-trials of `sub_ms` milliseconds. Each sub-trial votes with +// the majority of its amplified bits (no vote on a tie or without bits); the trial result is +// the majority of the votes. +// +// Snapshot layout (big-endian), latched at each trial end: +// 0-3 trial sequence number +// 4 bits 3:0 generators used during the whole trial, bit 4 von Neumann during the +// whole trial, bit 5 merge overflow since restart (should never be set) +// 5 sub-trials per trial +// 6 sub-trial length in ms +// 7 0 +// 8+16p path p: amplified bits (4), ones (4), votes (2), voted (2), vote count (1), +// result (1: bit 0 trial bit, bit 1 trial valid), 0 (2) +// 40-43 trial sequence number again; a mismatch with bytes 0-3 means a torn read +module bias_amplifier #( + parameter GENERATORS = 4, + parameter TICK_CYCLES = 120000 // clk cycles per millisecond +) ( + input wire clk, + input wire reset, + input wire [GENERATORS-1:0] gen_bit, + input wire gen_valid, // all generators deliver in lockstep + input wire [GENERATORS-1:0] sources, // selected and healthy generators + input wire von_neumann, + input wire [2:0] stage_mode, + input wire [47:0] stage_param, + input wire [7:0] sub_ms, + input wire [3:0] subtrials, + input wire restart, + input wire [7:0] read_index, + output reg [7:0] read_byte, // one cycle after read_index + output reg trial_end +); + + localparam SNAPSHOT_BYTES = 44; + + wire start_over = reset || restart; + + // Von Neumann (01 -> 0, 10 -> 1) and autocorrelation conversion per generator + + reg [GENERATORS-1:0] pair_first, have_first, previous_bit, has_previous; + wire [GENERATORS-1:0] vn_valid = von_neumann ? (gen_valid ? have_first & (gen_bit ^ pair_first) : {GENERATORS{1'b0}}) + : {GENERATORS{gen_valid}}; + wire [GENERATORS-1:0] vn_bit = von_neumann ? pair_first : gen_bit; + wire [GENERATORS-1:0] bias_valid = vn_valid & sources; + wire [GENERATORS-1:0] ac_valid = vn_valid & has_previous & sources; + wire [GENERATORS-1:0] ac_bit = ~(vn_bit ^ previous_bit); + + always @(posedge clk) begin + if (start_over || !von_neumann) begin + have_first <= {GENERATORS{1'b0}}; + end else if (gen_valid) begin + have_first <= ~have_first; + pair_first <= gen_bit; + end + if (start_over) begin + has_previous <= {GENERATORS{1'b0}}; + end else begin + has_previous <= has_previous | vn_valid; + previous_bit <= (vn_valid & vn_bit) | (~vn_valid & previous_bit); + end + end + + // Sub-trial and trial timing + + reg [16:0] tick_count; + reg [7:0] ms_count; + reg [3:0] sub_index; + reg sub_end; + wire tick = (tick_count == TICK_CYCLES - 1); + + always @(posedge clk) begin + sub_end <= 1'b0; + trial_end <= 1'b0; + if (start_over) begin + tick_count <= 17'd0; + ms_count <= 8'd0; + sub_index <= 4'd0; + end else begin + tick_count <= tick ? 17'd0 : tick_count + 17'd1; + if (tick) begin + if (ms_count + 8'd1 >= sub_ms) begin + ms_count <= 8'd0; + sub_end <= 1'b1; + end else begin + ms_count <= ms_count + 8'd1; + end + end + // trial_end follows the last sub_end by one cycle, once its vote is registered. + if (sub_end) begin + if (sub_index + 4'd1 >= subtrials) begin + sub_index <= 4'd0; + trial_end <= 1'b1; + end else begin + sub_index <= sub_index + 4'd1; + end + end + end + end + + // Paths + + wire [31:0] trial_bits [0:1]; + wire [31:0] trial_ones [0:1]; + wire [15:0] votes [0:1]; + wire [15:0] voted [0:1]; + wire [1:0] overflow; + + amp_path #(.GENERATORS(GENERATORS)) bias_path ( + .clk(clk), .restart(start_over), .in_valid(bias_valid), .in_bit(vn_bit), + .stage_mode(stage_mode), .stage_param(stage_param), + .sub_end(sub_end), .sub_index(sub_index), .trial_end(trial_end), + .trial_bits(trial_bits[0]), .trial_ones(trial_ones[0]), + .votes(votes[0]), .voted(voted[0]), .overflow(overflow[0]) + ); + + amp_path #(.GENERATORS(GENERATORS)) autocorrelation_path ( + .clk(clk), .restart(start_over), .in_valid(ac_valid), .in_bit(ac_bit), + .stage_mode(stage_mode), .stage_param(stage_param), + .sub_end(sub_end), .sub_index(sub_index), .trial_end(trial_end), + .trial_bits(trial_bits[1]), .trial_ones(trial_ones[1]), + .votes(votes[1]), .voted(voted[1]), .overflow(overflow[1]) + ); + + // Snapshot + + function [4:0] count_ones(input [15:0] value); + integer b; + begin + count_ones = 5'd0; + for (b = 0; b < 16; b = b + 1) count_ones = count_ones + value[b]; + end + endfunction + + function [127:0] path_bytes(input [31:0] bits, input [31:0] ones, input [15:0] vote, input [15:0] cast); + reg [4:0] cast_count, one_votes; + reg valid, result; + begin + cast_count = count_ones(cast); + one_votes = count_ones(vote & cast); + valid = cast_count != 0 && {one_votes, 1'b0} != {1'b0, cast_count}; + result = {one_votes, 1'b0} > {1'b0, cast_count}; + path_bytes = {bits, ones, vote, cast, 3'd0, cast_count, 6'd0, valid, result, 16'd0}; + end + endfunction + + reg [31:0] sequence_number; + reg [GENERATORS-1:0] sources_during; + reg von_neumann_during; + reg [7:0] snapshot [0:SNAPSHOT_BYTES-1]; // byte 0 first, indexed directly + + wire [8*SNAPSHOT_BYTES-1:0] snapshot_next = { + sequence_number, + 2'd0, |overflow, von_neumann_during & von_neumann, sources_during & sources, + 4'd0, subtrials, sub_ms, 8'd0, + path_bytes(trial_bits[0], trial_ones[0], votes[0], voted[0]), + path_bytes(trial_bits[1], trial_ones[1], votes[1], voted[1]), + sequence_number + }; + + integer k; + always @(posedge clk) begin + if (start_over) begin + sequence_number <= 32'd0; + sources_during <= sources; + von_neumann_during <= von_neumann; + end else if (trial_end) begin + for (k = 0; k < SNAPSHOT_BYTES; k = k + 1) + snapshot[k] <= snapshot_next[8*(SNAPSHOT_BYTES-1-k) +: 8]; + sequence_number <= sequence_number + 32'd1; + sources_during <= sources; + von_neumann_during <= von_neumann; + end else begin + sources_during <= sources_during & sources; + von_neumann_during <= von_neumann_during & von_neumann; + end + read_byte <= (read_index < SNAPSHOT_BYTES) ? snapshot[read_index] : 8'h00; + end + +endmodule diff --git a/fpga/rtl/stats_block.v b/fpga/rtl/stats_block.v index 605d5a3..909be2c 100644 --- a/fpga/rtl/stats_block.v +++ b/fpga/rtl/stats_block.v @@ -86,9 +86,10 @@ module stats_block #( reg [3:0] healthy_during; reg von_neumann_during; - // The snapshot is latched as one flat byte string (byte 0 in the top bits), so reading it - // is a plain byte multiplexer. - reg [8*SNAPSHOT_BYTES-1:0] snapshot, snapshot_next; + // The snapshot is assembled as one flat byte string (byte 0 in the top bits) and latched + // into a byte array, so reading it is a plain byte multiplexer on the index. + reg [8*SNAPSHOT_BYTES-1:0] snapshot_next; + reg [7:0] snapshot [0:SNAPSHOT_BYTES-1]; // byte 0 first, indexed directly integer k; always @(*) begin @@ -100,7 +101,7 @@ module stats_block #( snapshot_next[31:0] = sequence_number; end - integer c; + integer c, b; always @(posedge clk) begin if (reset || restart) begin sequence_number <= 32'd0; @@ -129,7 +130,8 @@ module stats_block #( end end if (trial_end) begin - snapshot <= snapshot_next; + for (b = 0; b < SNAPSHOT_BYTES; b = b + 1) + snapshot[b] <= snapshot_next[8*(SNAPSHOT_BYTES-1-b) +: 8]; sequence_number <= sequence_number + 32'd1; healthy_during <= healthy; von_neumann_during <= von_neumann; @@ -143,6 +145,6 @@ module stats_block #( // Snapshot read-out, registered: the byte for read_index appears one cycle later. always @(posedge clk) - read_byte <= (read_index < SNAPSHOT_BYTES) ? snapshot[8*(SNAPSHOT_BYTES-1-read_index) +: 8] : 8'h00; + read_byte <= (read_index < SNAPSHOT_BYTES) ? snapshot[read_index] : 8'h00; endmodule diff --git a/fpga/rtl/strix_core.v b/fpga/rtl/strix_core.v index 3cedc92..d89f5b0 100644 --- a/fpga/rtl/strix_core.v +++ b/fpga/rtl/strix_core.v @@ -1,5 +1,6 @@ // Board-independent STRIX logic: entropy generators with health monitors, live statistics, -// source selection, raw capture, frequency counter and the SPI register interface to the SAMD21. +// bias amplifier, source selection, raw capture, frequency counter and the SPI register +// interface to the SAMD21. // // SPI protocol (mode 0, one transaction per ss_n low period): the first byte is a command, // bit 7 = read (1) or write (0), bits 6:0 = register address. A write stores the following @@ -18,7 +19,8 @@ // // Statistics channels 0-3 are the generators' processed outputs, channel 4 is the XOR of the // healthy generators' outputs. Reading STATS returns the latest trial snapshot (layout in -// stats_block.v) byte by byte, starting at byte 0 in every transaction. +// stats_block.v) byte by byte, starting at byte 0 in every transaction. AMP does the same for +// the bias amplifier's trial snapshot (layout in bias_amplifier.v). module strix_core #( parameter CAPTURE_ADDR_WIDTH = 15, parameter FREQ_GATE_BITS = 24, @@ -35,7 +37,7 @@ module strix_core #( output wire capture_done ); - localparam [7:0] VERSION = 8'h05; + localparam [7:0] VERSION = 8'h06; localparam GENERATORS = 4; localparam [6:0] REG_ID0 = 7'h00, // "STRX" @@ -74,7 +76,15 @@ module strix_core #( REG_TRIAL_LOW = 7'h27, REG_STATS_CTRL = 7'h28, // bit 0 von Neumann on the statistics path; // write bit 1: restart trials, sequence 0 - REG_STATS = 7'h40; // burst read of the trial snapshot + REG_STATS = 7'h40, // burst read of the statistics trial snapshot + REG_AMP = 7'h42, // burst read of the amplifier trial snapshot + REG_AMP_CTRL = 7'h50, // bit 0 von Neumann on the amplifier path; + // write bit 1: restart the amplifier, sequence 0 + REG_AMP_SOURCES = 7'h51, // generators feeding the amplifier (healthy only) + REG_AMP_SUB_MS = 7'h52, // sub-trial length in ms + REG_AMP_SUBS = 7'h53, // sub-trials per trial, 1-15 + REG_AMP_STAGE = 7'h54; // stage s at 0x54 + 3s: mode (0 random walk, + // 1 majority vote), parameter high, parameter low // Per-generator health registers at 0x30 + 4g + index localparam [1:0] GEN_FLAGS = 2'd0, // bit 0 RCT failed, 1 APT failed, 2 too many dead rings, @@ -146,6 +156,26 @@ module strix_core #( stats_valid <= out_valid[0]; end + // Bias amplifier, fed from the same registered generator outputs + + reg [GENERATORS-1:0] amp_sources; + reg amp_von_neumann, amp_restart; + reg [7:0] amp_sub_ms; + reg [3:0] amp_subtrials; + reg [2:0] amp_stage_mode; + reg [47:0] amp_stage_param; + reg [7:0] amp_index; + wire [7:0] amp_byte; + wire amp_trial_end; + + bias_amplifier #(.GENERATORS(GENERATORS), .TICK_CYCLES(TICK_CYCLES)) amplifier ( + .clk(clk), .reset(reset), .gen_bit(stats_bits[GENERATORS-1:0]), .gen_valid(stats_valid), + .sources(amp_sources & stats_healthy), .von_neumann(amp_von_neumann), + .stage_mode(amp_stage_mode), .stage_param(amp_stage_param), + .sub_ms(amp_sub_ms), .subtrials(amp_subtrials), .restart(amp_restart), + .read_index(amp_index), .read_byte(amp_byte), .trial_end(amp_trial_end) + ); + stats_block #(.CHANNELS(GENERATORS + 1), .TICK_CYCLES(TICK_CYCLES)) stats ( .clk(clk), .reset(reset), .bits(stats_bits), .valid(stats_valid), .von_neumann(stats_von_neumann), .trial_ms(trial_ms), .restart(stats_restart), @@ -196,7 +226,7 @@ module strix_core #( wire selected, rx_valid, rx_is_first, tx_loaded; wire [7:0] rx_byte; - reg [7:0] tx_byte; + reg [7:0] tx_byte, tx_byte_next; spi_slave spi ( .clk(clk), .reset(reset), @@ -233,47 +263,67 @@ module strix_core #( always @(*) begin case (reg_addr) - REG_ID0: tx_byte = "S"; - REG_ID1: tx_byte = "T"; - REG_ID2: tx_byte = "R"; - REG_ID3: tx_byte = "X"; - REG_VERSION: tx_byte = VERSION; - REG_FREQ_GATE: tx_byte = FREQ_GATE_BITS; - REG_GENERATORS: tx_byte = GENERATORS; - REG_RINGS: tx_byte = RINGS_PER_GEN; - REG_SOURCE_A: tx_byte = {4'd0, source_a}; - REG_DECIM_HIGH: tx_byte = decimation[15:8]; - REG_DECIM_LOW: tx_byte = decimation[7:0]; - REG_STATUS: tx_byte = {4'd0, freq_valid, freq_busy, capture_done, capture_busy}; - REG_SOURCE_B: tx_byte = {4'd0, source_b}; - REG_MODE: tx_byte = {7'd0, pair_mode}; - REG_DATA: tx_byte = capture_byte; - REG_RING_SELECT: tx_byte = ring_select; - REG_FREQ_COUNT3: tx_byte = freq_count[31:24]; - REG_FREQ_COUNT2: tx_byte = freq_count[23:16]; - REG_FREQ_COUNT1: tx_byte = freq_count[15:8]; - REG_FREQ_COUNT0: tx_byte = freq_count[7:0]; - REG_GEN_ENABLE: tx_byte = {4'd0, gen_enable}; - REG_TEMPORAL: tx_byte = {4'd0, temporal}; - REG_FREQ_GEN: tx_byte = {6'd0, freq_gen}; - REG_HEALTHY: tx_byte = {4'd0, healthy}; - REG_RCT_CUTOFF: tx_byte = rct_cutoff; - REG_APT_HIGH: tx_byte = {5'd0, apt_cutoff[10:8]}; - REG_APT_LOW: tx_byte = apt_cutoff[7:0]; - REG_MAX_DEAD: tx_byte = max_dead; - REG_TRIAL_HIGH: tx_byte = trial_ms[15:8]; - REG_TRIAL_LOW: tx_byte = trial_ms[7:0]; - REG_STATS_CTRL: tx_byte = {7'd0, stats_von_neumann}; - REG_STATS: tx_byte = stats_byte; - default: tx_byte = gen_register(reg_addr); + REG_ID0: tx_byte_next = "S"; + REG_ID1: tx_byte_next = "T"; + REG_ID2: tx_byte_next = "R"; + REG_ID3: tx_byte_next = "X"; + REG_VERSION: tx_byte_next = VERSION; + REG_FREQ_GATE: tx_byte_next = FREQ_GATE_BITS; + REG_GENERATORS: tx_byte_next = GENERATORS; + REG_RINGS: tx_byte_next = RINGS_PER_GEN; + REG_SOURCE_A: tx_byte_next = {4'd0, source_a}; + REG_DECIM_HIGH: tx_byte_next = decimation[15:8]; + REG_DECIM_LOW: tx_byte_next = decimation[7:0]; + REG_STATUS: tx_byte_next = {4'd0, freq_valid, freq_busy, capture_done, capture_busy}; + REG_SOURCE_B: tx_byte_next = {4'd0, source_b}; + REG_MODE: tx_byte_next = {7'd0, pair_mode}; + REG_DATA: tx_byte_next = capture_byte; + REG_RING_SELECT: tx_byte_next = ring_select; + REG_FREQ_COUNT3: tx_byte_next = freq_count[31:24]; + REG_FREQ_COUNT2: tx_byte_next = freq_count[23:16]; + REG_FREQ_COUNT1: tx_byte_next = freq_count[15:8]; + REG_FREQ_COUNT0: tx_byte_next = freq_count[7:0]; + REG_GEN_ENABLE: tx_byte_next = {4'd0, gen_enable}; + REG_TEMPORAL: tx_byte_next = {4'd0, temporal}; + REG_FREQ_GEN: tx_byte_next = {6'd0, freq_gen}; + REG_HEALTHY: tx_byte_next = {4'd0, healthy}; + REG_RCT_CUTOFF: tx_byte_next = rct_cutoff; + REG_APT_HIGH: tx_byte_next = {5'd0, apt_cutoff[10:8]}; + REG_APT_LOW: tx_byte_next = apt_cutoff[7:0]; + REG_MAX_DEAD: tx_byte_next = max_dead; + REG_TRIAL_HIGH: tx_byte_next = trial_ms[15:8]; + REG_TRIAL_LOW: tx_byte_next = trial_ms[7:0]; + REG_STATS_CTRL: tx_byte_next = {7'd0, stats_von_neumann}; + REG_STATS: tx_byte_next = stats_byte; + REG_AMP: tx_byte_next = amp_byte; + REG_AMP_CTRL: tx_byte_next = {7'd0, amp_von_neumann}; + REG_AMP_SOURCES: tx_byte_next = {4'd0, amp_sources}; + REG_AMP_SUB_MS: tx_byte_next = amp_sub_ms; + REG_AMP_SUBS: tx_byte_next = {4'd0, amp_subtrials}; + 7'h54: tx_byte_next = {7'd0, amp_stage_mode[0]}; + 7'h55: tx_byte_next = amp_stage_param[15:8]; + 7'h56: tx_byte_next = amp_stage_param[7:0]; + 7'h57: tx_byte_next = {7'd0, amp_stage_mode[1]}; + 7'h58: tx_byte_next = amp_stage_param[31:24]; + 7'h59: tx_byte_next = amp_stage_param[23:16]; + 7'h5A: tx_byte_next = {7'd0, amp_stage_mode[2]}; + 7'h5B: tx_byte_next = amp_stage_param[47:40]; + 7'h5C: tx_byte_next = amp_stage_param[39:32]; + default: tx_byte_next = gen_register(reg_addr); endcase end + // Registered read-back: the next byte is needed only after half an SCK period, long after + // the command byte has set reg_addr, so a cycle of latency keeps this wide multiplexer off + // the SPI timing path. + always @(posedge clk) tx_byte <= tx_byte_next; + always @(posedge clk) begin capture_start <= 1'b0; freq_start <= 1'b0; health_clear <= 1'b0; stats_restart <= 1'b0; + amp_restart <= 1'b0; if (reset) begin gen_enable <= {GENERATORS{1'b1}}; temporal <= 4'd5; @@ -290,11 +340,23 @@ module strix_core #( trial_ms <= 16'd200; stats_von_neumann <= 1'b0; stats_index <= 8'd0; + amp_sources <= {GENERATORS{1'b1}}; + amp_von_neumann <= 1'b0; + amp_sub_ms <= 8'd40; + amp_subtrials <= 4'd5; + amp_stage_mode <= 3'b000; + amp_stage_param <= {16'd4, 16'd8, 16'd8}; // random walk 8 x 8 x 4 = bound 256 + amp_index <= 8'd0; is_read <= 1'b0; reg_addr <= 7'd0; bytes_read <= {CAPTURE_ADDR_WIDTH{1'b0}}; prefetch_addr <= {CAPTURE_ADDR_WIDTH{1'b0}}; end else begin + if (!selected) + amp_index <= 8'd0; + else if (tx_loaded && is_read && reg_addr == REG_AMP) + amp_index <= amp_index + 8'd1; + if (!selected) stats_index <= 8'd0; else if (tx_loaded && is_read && reg_addr == REG_STATS) @@ -329,6 +391,22 @@ module strix_core #( REG_MAX_DEAD: max_dead <= rx_byte; REG_TRIAL_HIGH: trial_ms[15:8] <= rx_byte; REG_TRIAL_LOW: trial_ms[7:0] <= rx_byte; + REG_AMP_CTRL: begin + amp_von_neumann <= rx_byte[0]; + amp_restart <= rx_byte[1]; + end + REG_AMP_SOURCES: amp_sources <= rx_byte[GENERATORS-1:0]; + REG_AMP_SUB_MS: if (rx_byte != 8'd0) amp_sub_ms <= rx_byte; + REG_AMP_SUBS: if (rx_byte >= 8'd1 && rx_byte <= 8'd15) amp_subtrials <= rx_byte[3:0]; + 7'h54: amp_stage_mode[0] <= rx_byte[0]; + 7'h55: amp_stage_param[15:8] <= rx_byte; + 7'h56: amp_stage_param[7:0] <= rx_byte; + 7'h57: amp_stage_mode[1] <= rx_byte[0]; + 7'h58: amp_stage_param[31:24] <= rx_byte; + 7'h59: amp_stage_param[23:16] <= rx_byte; + 7'h5A: amp_stage_mode[2] <= rx_byte[0]; + 7'h5B: amp_stage_param[47:40] <= rx_byte; + 7'h5C: amp_stage_param[39:32] <= rx_byte; REG_STATS_CTRL: begin stats_von_neumann <= rx_byte[0]; stats_restart <= rx_byte[1]; diff --git a/fpga/sim/tb_amp_stage.v b/fpga/sim/tb_amp_stage.v new file mode 100644 index 0000000..b4d492d --- /dev/null +++ b/fpga/sim/tb_amp_stage.v @@ -0,0 +1,97 @@ +// Testbench for amp_stage: random input bits against a reference model of both modes, plus a +// statistical check that a biased input comes out more biased. +`timescale 1ns / 1ps + +module tb_amp_stage; + + reg clk = 1'b0; + always #5 clk = ~clk; + + reg restart = 1'b1, mode = 1'b0, in_valid = 1'b0, in_bit = 1'b0; + reg [15:0] param = 16'd1; + wire out_valid, out_bit; + + amp_stage dut ( + .clk(clk), .restart(restart), .mode(mode), .param(param), + .in_valid(in_valid), .in_bit(in_bit), .out_valid(out_valid), .out_bit(out_bit) + ); + + integer errors = 0; + + // Reference model, advanced on the same edges as the stage + integer model_position, model_seen, model_ones, model_bound, dut_outputs, ones_out; + reg model_valid, model_bit; + + always @(posedge clk) begin + model_valid = 1'b0; + if (restart) begin + model_position = 0; model_seen = 0; model_ones = 0; + end else if (in_valid) begin + if (mode == 1'b0) begin + model_bound = param; // signed copy: -param would wrap as unsigned + model_position = model_position + (in_bit ? 1 : -1); + if (model_position >= model_bound || model_position <= -model_bound) begin + model_valid = 1'b1; model_bit = model_position > 0; model_position = 0; + end + end else begin + model_seen = model_seen + 1; model_ones = model_ones + in_bit; + if (model_seen >= param) begin + model_valid = 1'b1; model_bit = 2 * model_ones > model_seen; + model_seen = 0; model_ones = 0; + end + end + end + end + + // The model and the stage both update on the rising edge; compare in between. + always @(negedge clk) if (!restart) begin + if (out_valid !== model_valid || (out_valid && out_bit !== model_bit)) begin + if (errors < 5) $display("FAIL at %0t: out %b/%b, model %b/%b", $time, out_valid, out_bit, + model_valid, model_bit); + errors = errors + 1; + end + if (out_valid) begin + dut_outputs = dut_outputs + 1; + ones_out = ones_out + out_bit; + end + end + + task run(input m, input [15:0] p, input integer cycles, input integer probability_permille); + integer i; + begin + @(negedge clk); + mode = m; param = p; restart = 1'b1; + @(negedge clk); + restart = 1'b0; dut_outputs = 0; ones_out = 0; + for (i = 0; i < cycles; i = i + 1) begin + in_valid = ({$random} % 3) != 0; + in_bit = ({$random} % 1000) < probability_permille; + @(negedge clk); + end + in_valid = 1'b0; + @(negedge clk); + end + endtask + + initial begin + run(1'b0, 16'd1, 2000, 500); + run(1'b0, 16'd5, 20000, 500); + run(1'b0, 16'd16, 50000, 500); + run(1'b1, 16'd1, 2000, 500); + run(1'b1, 16'd7, 20000, 500); + run(1'b1, 16'd8, 20000, 500); // even window: ties output 0 + + // Amplification: 55% ones in, random walk bound 10 -> about 88% ones out + // (P = 1 / (1 + ((1-p)/p)^n) = 1 / (1 + (0.45/0.55)^10) = 0.88) + run(1'b0, 16'd10, 400000, 550); + if (dut_outputs < 500 || ones_out * 100 < dut_outputs * 83 || ones_out * 100 > dut_outputs * 93) begin + $display("FAIL amplification: %0d of %0d outputs are 1", ones_out, dut_outputs); + errors = errors + 1; + end + + if (errors == 0) $display("PASS (amplified %0d/%0d ones)", ones_out, dut_outputs); + else $display("%0d error(s)", errors); + $finish; + end + +endmodule diff --git a/fpga/sim/tb_strix_core.v b/fpga/sim/tb_strix_core.v index 3d7691b..e5e548a 100644 --- a/fpga/sim/tb_strix_core.v +++ b/fpga/sim/tb_strix_core.v @@ -1,7 +1,7 @@ // Testbench for strix_core: register access, generator XOR logic checked against the sampled // ring outputs, captures read back across transactions, generator enable, the frequency -// counter, the health monitors with injected faults, and the statistics snapshots against a -// reference model. Rings are replaced by the SIM model in ring_osc.v. +// counter, the health monitors with injected faults, the statistics snapshots against a +// reference model, and the bias amplifier's bit counts in several configurations. Rings are replaced by the SIM model in ring_osc.v. `timescale 1ns / 1ps module tb_strix_core; @@ -17,10 +17,16 @@ module tb_strix_core; REG_DATA = 7'h10, REG_RING_SELECT = 7'h11, REG_FREQ_COUNT0 = 7'h15, REG_GEN_ENABLE = 7'h16, REG_TEMPORAL = 7'h17, REG_FREQ_GEN = 7'h18, REG_HEALTHY = 7'h20, REG_HEALTH_CTRL = 7'h21, REG_TRIAL_LOW = 7'h27, - REG_STATS_CTRL = 7'h28, REG_STATS = 7'h40; + REG_STATS_CTRL = 7'h28, REG_STATS = 7'h40, REG_AMP = 7'h42, + REG_AMP_CTRL = 7'h50, REG_AMP_SOURCES = 7'h51, REG_AMP_SUB_MS = 7'h52, + REG_AMP_SUBS = 7'h53, REG_AMP_STAGE = 7'h54; + localparam AMP_SNAPSHOT_BYTES = 44; localparam SNAPSHOT_BYTES = 72; localparam TICK_CYCLES = 1500; // 1 "ms" = 12.5 us: a 10 "ms" trial outlasts a 72-byte read localparam TRIAL_BITS = 10 * TICK_CYCLES / 5; // 10 "ms" trials at temporal 5 + // Amplifier trials in the tests: 3 sub-trials of 2 "ms" -> 9000 clk cycles -> 1800 output + // bits per generator at temporal 5 + localparam AMP_GEN_BITS = 3 * 2 * TICK_CYCLES / 5; // Per-generator health registers function [6:0] gen_reg(input integer generator, input integer index); @@ -219,6 +225,43 @@ module tb_strix_core; end endtask + reg [7:0] amp [0:AMP_SNAPSHOT_BYTES-1]; + + function [31:0] amp_word(input integer index); + amp_word = {amp[index], amp[index+1], amp[index+2], amp[index+3]}; + endfunction + + // Configures the amplifier (stage bounds or windows, random walk), restarts it, waits for its + // second trial and reads that trial's snapshot. + task amp_trial(input [3:0] sources, input von_neumann, input [15:0] p0, input [15:0] p1, input [15:0] p2); + reg [7:0] dummy; + integer i; + begin + write_reg(REG_AMP_SOURCES, sources); + write_reg(REG_AMP_STAGE + 1, p0 >> 8); write_reg(REG_AMP_STAGE + 2, p0); + write_reg(REG_AMP_STAGE + 4, p1 >> 8); write_reg(REG_AMP_STAGE + 5, p1); + write_reg(REG_AMP_STAGE + 7, p2 >> 8); write_reg(REG_AMP_STAGE + 8, p2); + write_reg(REG_AMP_CTRL, {6'd0, 1'b1, von_neumann}); // restart + @(posedge dut.amp_trial_end); @(posedge dut.amp_trial_end); #200; + ss_n = 1'b0; #SCK_HALF; + spi_byte({1'b1, REG_AMP}, dummy); + for (i = 0; i < AMP_SNAPSHOT_BYTES; i = i + 1) spi_byte(8'h00, amp[i]); + #SCK_HALF ss_n = 1'b1; #(4 * SCK_HALF); + if (amp_word(0) !== amp_word(AMP_SNAPSHOT_BYTES - 4) || amp_word(0) !== 32'd1) begin + $display("FAIL amplifier snapshot sequence %0d/%0d", amp_word(0), amp_word(AMP_SNAPSHOT_BYTES - 4)); + errors = errors + 1; + end + if (amp[4][5]) begin $display("FAIL amplifier merge overflow"); errors = errors + 1; end + end + endtask + + task expect_range(input integer got, input integer low, input integer high, input [8*24-1:0] what); + if (got < low || got > high) begin + $display("FAIL %0s: %0d not in %0d..%0d", what, got, low, high); + errors = errors + 1; + end + endtask + task wait_trial_end; begin @(posedge dut.trial_end); @@ -237,7 +280,7 @@ module tb_strix_core; // Identification read_reg(REG_ID0, v); expect(v, "S", "ID0"); read_reg(REG_ID3, v); expect(v, "X", "ID3"); - read_reg(REG_VERSION, v); expect(v, 8'h05, "VERSION"); + read_reg(REG_VERSION, v); expect(v, 8'h06, "VERSION"); read_reg(REG_GENERATORS, v); expect(v, 8'd4, "GENERATORS"); read_reg(REG_RINGS, v); expect(v, RINGS, "RINGS"); read_reg(REG_TEMPORAL, v); expect(v, 8'd5, "TEMPORAL"); @@ -358,6 +401,34 @@ module tb_strix_core; end write_reg(REG_STATS_CTRL, 8'h00); + // Bias amplifier + write_reg(REG_AMP_SUB_MS, 8'd2); + write_reg(REG_AMP_SUBS, 8'd3); + + // Passthrough (all bounds 1): every generator bit appears once on the bias path + amp_trial(4'hF, 1'b0, 16'd1, 16'd1, 16'd1); + expect(amp[4] & 8'h0F, 8'h0F, "amp sources"); + expect(amp[5], 8'd3, "amp sub-trials"); + expect_range(amp_word(8), 4 * AMP_GEN_BITS - 8, 4 * AMP_GEN_BITS + 8, "amp passthrough bits"); + expect_range(amp_word(24), 4 * AMP_GEN_BITS - 8, 4 * AMP_GEN_BITS + 8, "amp autocorr bits"); + expect_range(amp_word(12), 4 * AMP_GEN_BITS / 3, 4 * AMP_GEN_BITS * 2 / 3, "amp passthrough ones"); + if (amp[20] == 0) begin $display("FAIL amplifier cast no votes"); errors = errors + 1; end + + // One source only + amp_trial(4'h1, 1'b0, 16'd1, 16'd1, 16'd1); + expect(amp[4] & 8'h0F, 8'h01, "amp single source"); + expect_range(amp_word(8), AMP_GEN_BITS - 2, AMP_GEN_BITS + 2, "amp single-source bits"); + + // Random walk bound 2 in stage 1: about a quarter of the bits (4 steps per output) + amp_trial(4'hF, 1'b0, 16'd2, 16'd1, 16'd1); + expect_range(amp_word(8), 4 * AMP_GEN_BITS / 6, 4 * AMP_GEN_BITS / 3, "amp bound-2 bits"); + + // Von Neumann keeps about a quarter of the bits + amp_trial(4'hF, 1'b1, 16'd1, 16'd1, 16'd1); + expect(amp[4] & 8'h10, 8'h10, "amp von Neumann flag"); + expect_range(amp_word(8), 4 * AMP_GEN_BITS / 6, 4 * AMP_GEN_BITS / 3, "amp von Neumann bits"); + write_reg(REG_AMP_CTRL, 8'h00); + if (xor_checks < 100) begin $display("FAIL only %0d temporal XOR checks ran", xor_checks); errors = errors + 1; diff --git a/fpga/src/strix.qsf b/fpga/src/strix.qsf index de0a037..5732530 100644 --- a/fpga/src/strix.qsf +++ b/fpga/src/strix.qsf @@ -63,6 +63,9 @@ set_global_assignment -name VERILOG_FILE ../rtl/strix_core.v set_global_assignment -name VERILOG_FILE ../rtl/entropy_generator.v set_global_assignment -name VERILOG_FILE ../rtl/health_monitor.v set_global_assignment -name VERILOG_FILE ../rtl/stats_block.v +set_global_assignment -name VERILOG_FILE ../rtl/bias_amplifier.v +set_global_assignment -name VERILOG_FILE ../rtl/amp_path.v +set_global_assignment -name VERILOG_FILE ../rtl/amp_stage.v set_global_assignment -name VERILOG_FILE ../rtl/freq_counter.v set_global_assignment -name VERILOG_FILE ../rtl/ring_osc.v set_global_assignment -name VERILOG_FILE ../rtl/raw_capture.v diff --git a/tools/jitter.py b/tools/jitter.py index aeddeba..be15963 100644 --- a/tools/jitter.py +++ b/tools/jitter.py @@ -8,6 +8,7 @@ Subcommands: output run the onod3000 test suite on every generator's processed output (256 KB each) health show the health monitors (NIST repetition count / adaptive proportion, dead rings) stats follow the live statistics trial by trial, with rolling and cumulative windows + amp configure the bias amplifier and follow its trials (baseline without intention) pairs capture the same ring index in two generators simultaneously and test for correlation Entropy per bit is estimated from conditional block entropies and converted to a @@ -46,6 +47,8 @@ REG_HEALTHY, REG_HEALTH_CTRL, REG_RCT_CUTOFF, REG_APT_HIGH, REG_APT_LOW, REG_MAX REG_GEN_HEALTH = 0x30 # + 4g: flags, RCT events, APT events, dead rings REG_TRIAL_HIGH, REG_TRIAL_LOW, REG_STATS_CTRL = 0x26, 0x27, 0x28 CHANNEL_NAMES = ["g0", "g1", "g2", "g3", "combined"] +REG_AMP_CTRL, REG_AMP_SOURCES, REG_AMP_SUB_MS, REG_AMP_SUBS, REG_AMP_STAGE = 0x50, 0x51, 0x52, 0x53, 0x54 +PATH_NAMES = ["bias", "autocorr"] CONTROL_START_FREQ = 0x04 STATUS_FREQ_VALID = 0x08 @@ -79,8 +82,8 @@ class Board: while self.serial.readline(): pass self.serial.timeout = 200 - if not identity.startswith("STRX v5"): - sys.exit(f"unexpected board identity: {identity!r} (needs the v5 FPGA image)") + if not identity.startswith("STRX v6"): + sys.exit(f"unexpected board identity: {identity!r} (needs the v6 FPGA image)") self.generators = self.read(REG_GENERATORS) self.rings = self.read(REG_RINGS) print(f"board: {identity}, {self.generators} generators x {self.rings} rings") @@ -137,6 +140,22 @@ def read_snapshot(board: "Board") -> dict | None: "trial_ms": raw[6] << 8 | raw[7], "channels": channels} +def read_amp_snapshot(board: "Board") -> dict | None: + """Latest amplifier trial snapshot (layout in fpga/rtl/bias_amplifier.v), or None if torn.""" + raw = bytes.fromhex(board.command("amp")) + word = lambda i: int.from_bytes(raw[i:i + 4], "big") + if word(0) != word(len(raw) - 4): + return None + paths = [] + for p in range(2): + base = 8 + 16 * p + paths.append({"bits": word(base), "ones": word(base + 4), + "votes": raw[base + 8] << 8 | raw[base + 9], "voted": raw[base + 10] << 8 | raw[base + 11], + "result": raw[base + 13] & 1, "valid": bool(raw[base + 13] & 2)}) + return {"sequence": word(0), "sources": raw[4] & 0x0F, "von_neumann": bool(raw[4] & 0x10), + "overflow": bool(raw[4] & 0x20), "subtrials": raw[5], "sub_ms": raw[6], "paths": paths} + + def cached_capture(board: Board | None, path: Path, *args) -> bytes | None: if board: path.write_bytes(board.capture(*args)) @@ -400,6 +419,60 @@ def run_stats(board: Board | None, args) -> None: f" {int(np.sum(np.abs(z) > 3)):>5d} {ac.mean():>15.3f} {ac.std():>5.2f}") +def run_amp(board: Board | None, args) -> None: + if board is None: + sys.exit("amp needs the board") + if len(args.stages) != 3: + sys.exit("--stages needs three values") + mode = 1 if args.method == "vote" else 0 + for s, param in enumerate(args.stages): + board.write(REG_AMP_STAGE + 3 * s, mode) + board.write(REG_AMP_STAGE + 3 * s + 1, param >> 8) + board.write(REG_AMP_STAGE + 3 * s + 2, param & 0xFF) + board.write(REG_AMP_SOURCES, args.sources) + board.write(REG_AMP_SUB_MS, args.sub_ms) + board.write(REG_AMP_SUBS, args.subtrials) + board.write(REG_AMP_CTRL, (1 if args.von_neumann else 0) | 2) # bit 1 restarts + while (snapshot := read_amp_snapshot(board)) is None or snapshot["sequence"] > 1: + time.sleep(args.sub_ms * args.subtrials / 4000) + + total = math.prod(args.stages) + print(f"{args.method}, stages {' x '.join(map(str, args.stages))} (total {total}), sources {args.sources:#x}, " + f"{args.subtrials} x {args.sub_ms} ms sub-trials, von Neumann {'on' if args.von_neumann else 'off'}") + print(f"{'trial':>5} {'src':>3} " + " ".join(f"{n + ' bits':>13} {'z':>6} {'votes':>{args.subtrials}} {'res':>3}" + for n in PATH_NAMES)) + results = [[] for _ in PATH_NAMES] + bit_z = [[] for _ in PATH_NAMES] + last_sequence = -1 + while len(results[0]) < args.trials: + snapshot = read_amp_snapshot(board) + if snapshot is None or snapshot["sequence"] == last_sequence: + time.sleep(args.sub_ms * args.subtrials / 4000) + continue + last_sequence = snapshot["sequence"] + if snapshot["overflow"]: + print("warning: merge overflow reported") + cells = [] + for p, path in enumerate(snapshot["paths"]): + z, _ = z_scores(path["bits"], path["ones"], 0) + votes = "".join(("1" if path["votes"] >> i & 1 else "0") if path["voted"] >> i & 1 else "." + for i in range(snapshot["subtrials"])) + result = str(path["result"]) if path["valid"] else "-" + if path["valid"]: + results[p].append(path["result"]) + bit_z[p].append(z) + cells.append(f"{path['bits']:>13d} {z:>6.2f} {votes:>{args.subtrials}} {result:>3}") + print(f"{snapshot['sequence']:>5} {snapshot['sources']:>3x} " + " ".join(cells)) + + print("\nbaseline: amplified-bit z per trial should have sd about 1; trial results about 50% ones\n") + print(f"{'path':<9} {'trials':>6} {'ones':>5} {'rate':>6} {'z':>6} {'bit z mean':>10} {'sd':>5}") + for p, name in enumerate(PATH_NAMES): + n, ones = len(results[p]), sum(results[p]) + z = (2 * ones - n) / math.sqrt(n) if n else float("nan") + bz = np.array(bit_z[p]) + print(f"{name:<9} {n:>6} {ones:>5} {ones / max(n, 1):>6.3f} {z:>6.2f} {np.nanmean(bz):>10.3f} {np.nanstd(bz):>5.2f}") + + def run_pairs(board: Board | None, args) -> None: generators = board.generators if board else 4 rng = np.random.default_rng(0) @@ -454,6 +527,15 @@ def main() -> None: stats.add_argument("--trial-ms", type=int, default=200) stats.add_argument("--rolling", type=int, default=10, help="trials in the rolling window") stats.add_argument("--von-neumann", action="store_true") + amp = sub.add_parser("amp") + amp.add_argument("--trials", type=int, default=50) + amp.add_argument("--method", choices=["walk", "vote"], default="walk") + amp.add_argument("--stages", type=int, nargs="+", default=[8, 8, 4], + help="bound (walk) or window (vote) of the three stages") + amp.add_argument("--sources", type=lambda s: int(s, 0), default=0xF, help="generator mask") + amp.add_argument("--sub-ms", type=int, default=40) + amp.add_argument("--subtrials", type=int, default=5) + amp.add_argument("--von-neumann", action="store_true") health = sub.add_parser("health") health.add_argument("--clear", action="store_true", help="clear failures and event counters first") pairs = sub.add_parser("pairs") @@ -464,7 +546,7 @@ def main() -> None: args.out.mkdir(parents=True, exist_ok=True) board = None if args.analyze_only else Board(args.port) handlers = {"freq": run_freq, "rings": run_rings, "sweep": run_sweep, - "output": run_output, "health": run_health, "stats": run_stats, "pairs": run_pairs} + "output": run_output, "health": run_health, "stats": run_stats, "amp": run_amp, "pairs": run_pairs} handlers[args.command](board, args)