mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
feat: fractal dimension — DBC algorithm (Python + FPGA)
Paper: 'Ultra-fast computation of fractal dimension for RGB images' (Pattern Analysis and Applications, 2025) Python (fractal_dimension.py): - DBC algorithm with numpy vectorization (29x faster than scalar) - fd_compress_hint: FD → voltage mode (STORE/COMPUTE/APPROX/MORPHIC) - 7/7 tests pass (Sierpinski, random, gradient, checkerboard, fBm, constant, RGB) - Q16_16 integer arithmetic internally FPGA (fractal_box_counter.v + fractal_fd_selector.v): - 5-state FSM: IDLE → COLLECT → FINALIZE → STORE_LOG → REGRESS → DONE - 8 power-of-two scales (2, 4, 8, ..., 256) - Linear regression via Q16_16 64-bit arithmetic - FD clamped to [1.0, 3.0] in Q16_16 - Selector: FD < 2.3 → STORE, < 2.6 → COMPUTE, < 2.9 → APPROX, >= 2.9 → MORPHIC - Integrated into research_stack_top.v FD drives adaptive compression: Low FD (smooth) → STORE mode (minimal compression) High FD (rough) → MORPHIC mode (aggressive compression)
This commit is contained in:
parent
89e995aaa0
commit
58b95a9814
6 changed files with 997 additions and 0 deletions
64
4-Infrastructure/hardware/build_fractal.sh
Executable file
64
4-Infrastructure/hardware/build_fractal.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Fractal Box Counter — standalone build for Tang Nano 9K
|
||||
# Verifies synthesis, P&R, and bitstream generation for the
|
||||
# fractal dimension computation modules.
|
||||
|
||||
TOP="fractal_box_counter"
|
||||
CST="research_stack_tangnano9k.cst"
|
||||
JSON="fractal_box_counter.json"
|
||||
PNR="fractal_box_counter_pnr.json"
|
||||
FS="fractal_box_counter.fs"
|
||||
DEVICE="GW1NR-LV9QN88PC6/I5"
|
||||
FAMILY="GW1N-9C"
|
||||
FREQ=27
|
||||
|
||||
echo "=== Fractal Box Counter Build ==="
|
||||
echo "Top: ${TOP}"
|
||||
echo "Device: ${DEVICE}"
|
||||
echo "CST: ${CST}"
|
||||
echo ""
|
||||
|
||||
# ── Step 1: Synthesis ─────────────────────────────────────────────
|
||||
echo "=== Step 1: Synthesis (Yosys) ==="
|
||||
yosys -p "
|
||||
read_verilog -sv \
|
||||
fractal_box_counter.v \
|
||||
fractal_fd_selector.v;
|
||||
synth_gowin -top ${TOP} -json ${JSON};
|
||||
stat
|
||||
"
|
||||
|
||||
# ── Step 2: Place & Route ─────────────────────────────────────────
|
||||
echo ""
|
||||
echo "=== Step 2: Place & Route (nextpnr-himbaechel) ==="
|
||||
nextpnr-himbaechel --device "${DEVICE}" --json "${JSON}" --write "${PNR}" \
|
||||
--freq "${FREQ}" --vopt "family=${FAMILY}" --vopt "cst=${CST}"
|
||||
|
||||
# ── Step 3: Bitstream ─────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "=== Step 3: Bitstream (gowin_pack) ==="
|
||||
gowin_pack -d "${FAMILY}" -o "${FS}" "${PNR}"
|
||||
|
||||
# ── Step 4: Resource Report ───────────────────────────────────────
|
||||
echo ""
|
||||
echo "=== Step 4: Resource Usage Report ==="
|
||||
if [ -f "${JSON}" ]; then
|
||||
echo "Synthesis JSON: ${JSON}"
|
||||
# Extract cell counts from yosys stat output (already printed above)
|
||||
fi
|
||||
if [ -f "${PNR}" ]; then
|
||||
echo "P&R JSON: ${PNR}"
|
||||
fi
|
||||
if [ -f "${FS}" ]; then
|
||||
FS_SIZE=$(stat -c%s "${FS}" 2>/dev/null || stat -f%z "${FS}" 2>/dev/null || echo "unknown")
|
||||
echo "Bitstream: ${FS} (${FS_SIZE} bytes)"
|
||||
else
|
||||
echo "WARNING: Bitstream not generated!"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Build complete: ${FS} ==="
|
||||
ls -lh "${FS}" 2>/dev/null || true
|
||||
|
|
@ -21,6 +21,8 @@ yosys -p "
|
|||
voltage_mode_controller.v \
|
||||
scale_space_bram.v \
|
||||
highs_pivot_accelerator.v \
|
||||
fractal_box_counter.v \
|
||||
fractal_fd_selector.v \
|
||||
Blitter6502OISC_small.v \
|
||||
research_stack_top.v;
|
||||
synth_gowin -top ${TOP} -json ${JSON};
|
||||
|
|
|
|||
301
4-Infrastructure/hardware/fractal_box_counter.v
Normal file
301
4-Infrastructure/hardware/fractal_box_counter.v
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
`timescale 1ns / 1ps
|
||||
|
||||
// Fractal Dimension Box Counter for Tang Nano 9K (GW1NR-9C)
|
||||
// Implements Differential Box-Counting (DBC) algorithm in hardware.
|
||||
//
|
||||
// Algorithm:
|
||||
// 1. Stream in 8-bit data values (e.g. grayscale image row)
|
||||
// 2. For each power-of-two scale s = 2, 4, 8, ..., 2^MAX_SCALE:
|
||||
// - Divide data into grid cells of size s
|
||||
// - Compute max and min within each cell
|
||||
// - Box count n(s) = sum over cells of (max - min + 1)
|
||||
// 3. Linear regression on log(n(s)) vs log(1/s) → slope = fractal dimension
|
||||
//
|
||||
// Q16_16 fixed-point throughout. FD output in Q16_16 format.
|
||||
//
|
||||
// Pipeline: 1 clock per data element per active scale (scales processed
|
||||
// sequentially, all scales share the same data pass).
|
||||
|
||||
module fractal_box_counter (
|
||||
input wire clk,
|
||||
input wire rst_n,
|
||||
input wire [7:0] data_in, // 8-bit input data stream
|
||||
input wire data_valid, // data valid strobe
|
||||
input wire [15:0] data_count, // total number of data elements
|
||||
output reg [31:0] fd_q16, // fractal dimension in Q16_16 (full 32-bit)
|
||||
output reg fd_valid // FD computation complete pulse
|
||||
);
|
||||
|
||||
// ── Parameters ────────────────────────────────────────────────
|
||||
parameter MAX_SCALE = 8; // max grid scale exponent (2^8 = 256)
|
||||
parameter DATA_WIDTH = 8; // input data width
|
||||
localparam NUM_SCALES = MAX_SCALE; // scales: 2^1 .. 2^MAX_SCALE
|
||||
|
||||
// ── State Machine ─────────────────────────────────────────────
|
||||
localparam S_IDLE = 3'd0;
|
||||
localparam S_COLLECT = 3'd1; // stream data in, accumulate max/min per cell
|
||||
localparam S_FINALIZE = 3'd2; // finalize box counts for current scale
|
||||
localparam S_STORE_LOG = 3'd3; // store log(n(s)) after partial cell added
|
||||
localparam S_REGRESS = 3'd4; // compute linear regression
|
||||
localparam S_DONE = 3'd5;
|
||||
|
||||
reg [2:0] state;
|
||||
reg [3:0] scale_idx; // current scale index (0-based, scale = 2^(idx+1))
|
||||
reg [15:0] elem_count; // elements received so far
|
||||
|
||||
// ── Grid cell tracking (per scale) ────────────────────────────
|
||||
// For each scale, we track the current cell's max and min.
|
||||
// Cell size = 2^(scale_idx+1). We use a counter to know when a cell ends.
|
||||
// We store the running max/min for the current cell.
|
||||
|
||||
reg [7:0] cell_max;
|
||||
reg [7:0] cell_min;
|
||||
reg [15:0] cell_counter; // counts elements within current cell
|
||||
|
||||
// ── Box count accumulation ────────────────────────────────────
|
||||
// n(s) = sum of (cell_max - cell_min + 1) for all cells at scale s
|
||||
// Stored in Q16_16 format for the regression.
|
||||
// We accumulate in a wider register then shift.
|
||||
|
||||
reg [31:0] box_count_acc; // running sum for current scale
|
||||
|
||||
// ── Storage for log(n(s)) and log(1/s) ────────────────────────
|
||||
// We store NUM_SCALES pairs. log values are precomputed in Q16_16.
|
||||
// log(1/s) = -log(s) = -(idx+1)*log(2) where log(2) ≈ 0.6931 = 0x0000_B173
|
||||
// log(n(s)) is computed from box_count via a lookup approximation.
|
||||
|
||||
reg [31:0] log_inv_s [0:NUM_SCALES-1]; // log(1/s) in Q16_16 (negative)
|
||||
reg [31:0] log_ns [0:NUM_SCALES-1]; // log(n(s)) in Q16_16
|
||||
|
||||
// Precomputed constants: log(2) in Q16_16 = 0.693147 * 65536 ≈ 45426 = 0x0000_B173
|
||||
localparam signed [31:0] LOG2_Q16 = 32'sh0000_B173;
|
||||
|
||||
// ── Linear regression accumulators ────────────────────────────
|
||||
// slope = (N*sum_xy - sum_x*sum_y) / (N*sum_x2 - sum_x*sum_x)
|
||||
// where x = log(1/s), y = log(n(s))
|
||||
|
||||
reg signed [63:0] sum_x; // sum of x_i
|
||||
reg signed [63:0] sum_y; // sum of y_i
|
||||
reg signed [63:0] sum_xy; // sum of x_i * y_i
|
||||
reg signed [63:0] sum_x2; // sum of x_i^2
|
||||
reg [3:0] reg_count; // number of valid data points
|
||||
|
||||
// Regression pipeline
|
||||
reg [2:0] regress_stage;
|
||||
reg signed [63:0] numer; // N*sum_xy - sum_x*sum_y
|
||||
reg signed [63:0] denom; // N*sum_x2 - sum_x*sum_x
|
||||
|
||||
// ── Integer log2 approximation ────────────────────────────────
|
||||
// Computes floor(log2(val)) for val > 0, returns Q16_16 fixed point.
|
||||
// Uses priority encoder to find MSB position.
|
||||
|
||||
function [31:0] int_log2_q16;
|
||||
input [31:0] val;
|
||||
reg [4:0] msb_pos;
|
||||
integer k;
|
||||
begin
|
||||
msb_pos = 0;
|
||||
for (k = 31; k >= 0; k = k - 1) begin
|
||||
if (val[k] && msb_pos == 0)
|
||||
msb_pos = k[4:0];
|
||||
end
|
||||
// Q16_16 representation of msb_pos (integer part of log2)
|
||||
int_log2_q16 = {11'd0, msb_pos, 16'd0};
|
||||
end
|
||||
endfunction
|
||||
|
||||
// ── Initialization: precompute log(1/s) for each scale ────────
|
||||
integer init_i;
|
||||
initial begin
|
||||
for (init_i = 0; init_i < NUM_SCALES; init_i = init_i + 1) begin
|
||||
// log(1/s) = -(init_i+1) * log(2)
|
||||
// In Q16_16: -(init_i+1) * 45426
|
||||
log_inv_s[init_i] = -($signed({28'd0, init_i[3:0] + 4'd1}) * LOG2_Q16);
|
||||
end
|
||||
end
|
||||
|
||||
// ── Main State Machine ────────────────────────────────────────
|
||||
always @(posedge clk or negedge rst_n) begin
|
||||
if (!rst_n) begin
|
||||
state <= S_IDLE;
|
||||
scale_idx <= 0;
|
||||
elem_count <= 0;
|
||||
cell_max <= 8'd0;
|
||||
cell_min <= 8'd255;
|
||||
cell_counter <= 0;
|
||||
box_count_acc <= 0;
|
||||
fd_q16 <= 0;
|
||||
fd_valid <= 0;
|
||||
sum_x <= 0;
|
||||
sum_y <= 0;
|
||||
sum_xy <= 0;
|
||||
sum_x2 <= 0;
|
||||
reg_count <= 0;
|
||||
numer <= 0;
|
||||
denom <= 0;
|
||||
regress_stage <= 0;
|
||||
end else begin
|
||||
fd_valid <= 1'b0; // default: clear pulse
|
||||
|
||||
case (state)
|
||||
// ── IDLE: wait for data_valid to begin ─────────────
|
||||
S_IDLE: begin
|
||||
if (data_valid) begin
|
||||
state <= S_COLLECT;
|
||||
scale_idx <= 0;
|
||||
elem_count <= 16'd1;
|
||||
cell_max <= data_in;
|
||||
cell_min <= data_in;
|
||||
cell_counter <= 16'd1;
|
||||
box_count_acc <= 32'd0;
|
||||
end
|
||||
end
|
||||
|
||||
// ── COLLECT: stream data, compute max/min per cell ─
|
||||
S_COLLECT: begin
|
||||
if (data_valid) begin
|
||||
elem_count <= elem_count + 16'd1;
|
||||
|
||||
// Update running max/min for current cell
|
||||
if (data_in > cell_max)
|
||||
cell_max <= data_in;
|
||||
if (data_in < cell_min)
|
||||
cell_min <= data_in;
|
||||
|
||||
cell_counter <= cell_counter + 16'd1;
|
||||
|
||||
// Check if we've completed a cell
|
||||
// Cell size = 2^(scale_idx+1)
|
||||
if (cell_counter == (16'd1 << (scale_idx + 1))) begin
|
||||
// Finalize this cell: add (max - min + 1) to accumulator
|
||||
box_count_acc <= box_count_acc +
|
||||
{24'd0, cell_max} - {24'd0, cell_min} + 32'd1;
|
||||
// Reset for next cell
|
||||
cell_max <= data_in;
|
||||
cell_min <= data_in;
|
||||
cell_counter <= 16'd1;
|
||||
end
|
||||
end else begin
|
||||
// data_valid deasserted → data stream ended
|
||||
state <= S_FINALIZE;
|
||||
end
|
||||
end
|
||||
|
||||
// ── FINALIZE: add partial cell's contribution ─────
|
||||
S_FINALIZE: begin
|
||||
// Handle last (possibly partial) cell
|
||||
if (cell_counter > 0) begin
|
||||
box_count_acc <= box_count_acc +
|
||||
{24'd0, cell_max} - {24'd0, cell_min} + 32'd1;
|
||||
end
|
||||
// Wait one cycle for box_count_acc to update, then store log
|
||||
state <= S_STORE_LOG;
|
||||
end
|
||||
|
||||
// ── STORE_LOG: log(n(s)) now uses updated box_count_acc ──
|
||||
S_STORE_LOG: begin
|
||||
// Compute log(n(s)) from box_count_acc (now includes partial cell)
|
||||
log_ns[scale_idx] <= int_log2_q16(box_count_acc);
|
||||
|
||||
// Advance to next scale
|
||||
if (scale_idx < NUM_SCALES - 1) begin
|
||||
scale_idx <= scale_idx + 1;
|
||||
elem_count <= 16'd1;
|
||||
cell_max <= 8'd0;
|
||||
cell_min <= 8'd255;
|
||||
cell_counter <= 16'd0;
|
||||
box_count_acc <= 32'd0;
|
||||
state <= S_COLLECT;
|
||||
end else begin
|
||||
// All scales done → compute regression
|
||||
state <= S_REGRESS;
|
||||
end
|
||||
end
|
||||
|
||||
// ── REGRESS: linear regression on log(n(s)) vs log(1/s)
|
||||
S_REGRESS: begin
|
||||
case (regress_stage)
|
||||
3'd0: begin
|
||||
// Initialize accumulators
|
||||
sum_x <= 64'sd0;
|
||||
sum_y <= 64'sd0;
|
||||
sum_xy <= 64'sd0;
|
||||
sum_x2 <= 64'sd0;
|
||||
reg_count <= 0;
|
||||
regress_stage <= 3'd1;
|
||||
end
|
||||
3'd1: begin
|
||||
// Accumulate over all scales
|
||||
// We process one scale per clock
|
||||
if (reg_count < NUM_SCALES) begin
|
||||
sum_x <= sum_x + $signed(log_inv_s[reg_count]);
|
||||
sum_y <= sum_y + $signed(log_ns[reg_count]);
|
||||
sum_xy <= sum_xy + $signed(log_inv_s[reg_count]) *
|
||||
$signed(log_ns[reg_count]);
|
||||
sum_x2 <= sum_x2 + $signed(log_inv_s[reg_count]) *
|
||||
$signed(log_inv_s[reg_count]);
|
||||
reg_count <= reg_count + 1;
|
||||
end else begin
|
||||
regress_stage <= 3'd2;
|
||||
end
|
||||
end
|
||||
3'd2: begin
|
||||
// Compute numerator and denominator
|
||||
// N = NUM_SCALES
|
||||
// numer = N * sum_xy - sum_x * sum_y
|
||||
// denom = N * sum_x2 - sum_x * sum_x
|
||||
numer <= $signed({60'd0, NUM_SCALES[3:0]}) * sum_xy -
|
||||
sum_x * sum_y;
|
||||
denom <= $signed({60'd0, NUM_SCALES[3:0]}) * sum_x2 -
|
||||
sum_x * sum_x;
|
||||
regress_stage <= 3'd3;
|
||||
end
|
||||
3'd3: begin
|
||||
// Compute slope = numer / denom
|
||||
// Result is in Q16_16: shift left 16 before dividing
|
||||
if (denom != 64'sd0) begin
|
||||
// slope = (numer << 16) / denom → full Q16_16
|
||||
fd_q16 <= ((numer <<< 16) / denom)[31:0];
|
||||
end else begin
|
||||
// Degenerate case: flat data, FD ≈ 2.0
|
||||
fd_q16 <= 32'h0002_0000; // 2.0 in Q16_16
|
||||
end
|
||||
regress_stage <= 3'd4;
|
||||
end
|
||||
3'd4: begin
|
||||
// Fractal dimension is always positive
|
||||
if (fd_q16[31]) begin
|
||||
// Negative slope → take absolute value
|
||||
fd_q16 <= (~fd_q16) + 32'd1;
|
||||
end
|
||||
// Clamp to [1.0, 3.0] in Q16_16
|
||||
if (fd_q16 < 32'h0001_0000) // < 1.0
|
||||
fd_q16 <= 32'h0001_0000;
|
||||
if (fd_q16 > 32'h0003_0000) // > 3.0
|
||||
fd_q16 <= 32'h0003_0000;
|
||||
|
||||
fd_valid <= 1'b1;
|
||||
regress_stage <= 3'd0;
|
||||
state <= S_DONE;
|
||||
end
|
||||
endcase
|
||||
end
|
||||
|
||||
// ── DONE: hold result, wait for new data ──────────
|
||||
S_DONE: begin
|
||||
if (data_valid) begin
|
||||
// New data stream → restart
|
||||
state <= S_COLLECT;
|
||||
scale_idx <= 0;
|
||||
elem_count <= 16'd1;
|
||||
cell_max <= data_in;
|
||||
cell_min <= data_in;
|
||||
cell_counter <= 16'd1;
|
||||
box_count_acc <= 32'd0;
|
||||
end
|
||||
end
|
||||
endcase
|
||||
end
|
||||
end
|
||||
|
||||
endmodule
|
||||
65
4-Infrastructure/hardware/fractal_fd_selector.v
Normal file
65
4-Infrastructure/hardware/fractal_fd_selector.v
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
`timescale 1ns / 1ps
|
||||
|
||||
// Fractal Dimension → Voltage Mode Selector
|
||||
// Maps computed fractal dimension (Q16_16) to a 2-bit voltage mode for the
|
||||
// voltage_mode_controller.
|
||||
//
|
||||
// FD < 2.3 → voltage_mode = 0 (STORE)
|
||||
// FD < 2.6 → voltage_mode = 1 (COMPUTE)
|
||||
// FD < 2.9 → voltage_mode = 2 (APPROX)
|
||||
// FD >= 2.9 → voltage_mode = 3 (MORPHIC)
|
||||
//
|
||||
// Thresholds in Q16_16:
|
||||
// 2.3 = 2 + 0.3 = 0x0002_4CCD (131072 + 19661 = 150733)
|
||||
// 2.6 = 2 + 0.6 = 0x0002_999A (131072 + 39322 = 170394)
|
||||
// 2.9 = 2 + 0.9 = 0x0002_E666 (131072 + 58982 = 190054)
|
||||
|
||||
module fractal_fd_selector (
|
||||
input wire clk,
|
||||
input wire rst_n,
|
||||
input wire [31:0] fd_q16, // fractal dimension in Q16_16
|
||||
input wire fd_valid, // fd_q16 is valid
|
||||
output reg [1:0] voltage_mode, // 0=STORE, 1=COMPUTE, 2=APPROX, 3=MORPHIC
|
||||
output reg mode_valid // mode output is valid
|
||||
);
|
||||
|
||||
// Threshold constants in Q16_16
|
||||
localparam [31:0] THRESH_2_3 = 32'h0002_4CCD; // 2.3
|
||||
localparam [31:0] THRESH_2_6 = 32'h0002_999A; // 2.6
|
||||
localparam [31:0] THRESH_2_9 = 32'h0002_E666; // 2.9
|
||||
|
||||
// Mode definitions (match voltage_mode_controller)
|
||||
localparam MODE_STORE = 2'b00;
|
||||
localparam MODE_COMPUTE = 2'b01;
|
||||
localparam MODE_APPROX = 2'b10;
|
||||
localparam MODE_MORPHIC = 2'b11;
|
||||
|
||||
// Combinational mode selection
|
||||
reg [1:0] mode_next;
|
||||
|
||||
always @(*) begin
|
||||
if (fd_q16 < THRESH_2_3)
|
||||
mode_next = MODE_STORE;
|
||||
else if (fd_q16 < THRESH_2_6)
|
||||
mode_next = MODE_COMPUTE;
|
||||
else if (fd_q16 < THRESH_2_9)
|
||||
mode_next = MODE_APPROX;
|
||||
else
|
||||
mode_next = MODE_MORPHIC;
|
||||
end
|
||||
|
||||
// Register output
|
||||
always @(posedge clk or negedge rst_n) begin
|
||||
if (!rst_n) begin
|
||||
voltage_mode <= MODE_STORE;
|
||||
mode_valid <= 1'b0;
|
||||
end else begin
|
||||
mode_valid <= 1'b0;
|
||||
if (fd_valid) begin
|
||||
voltage_mode <= mode_next;
|
||||
mode_valid <= 1'b1;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
endmodule
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// Unified FPGA design for Tang Nano 9K (GW1NR-9C)
|
||||
// Combines: Blitter6502OISC + Q16 LUT + Memory Map + Voltage Controller
|
||||
// + Scale Space BRAM + HiGHS Pivot Accelerator
|
||||
// + Fractal Box Counter + FD Selector
|
||||
|
||||
`timescale 1ns / 1ps
|
||||
|
||||
|
|
@ -101,6 +102,12 @@ module research_stack_top (
|
|||
wire [5:0] highs_write_idx;
|
||||
wire [31:0] highs_write_data;
|
||||
|
||||
// ── Fractal Box Counter Signals ────────────────────────────────
|
||||
wire [31:0] frac_fd_q16;
|
||||
wire frac_fd_valid;
|
||||
wire [1:0] frac_voltage_mode;
|
||||
wire frac_mode_valid;
|
||||
|
||||
// ── Q16 LUT: use lower 16 bits of operands ────────────────────
|
||||
wire [15:0] q16_a_16 = map_q16_a[15:0];
|
||||
wire [15:0] q16_b_16 = map_q16_b[15:0];
|
||||
|
|
@ -217,6 +224,33 @@ module research_stack_top (
|
|||
.write_data(highs_write_data)
|
||||
);
|
||||
|
||||
// Fractal Box Counter (DBC algorithm, 8-bit input, Q16_16 FD output)
|
||||
fractal_box_counter #(
|
||||
.MAX_SCALE(8),
|
||||
.DATA_WIDTH(8)
|
||||
) frac_bc (
|
||||
.clk(clk),
|
||||
.rst_n(rst_n),
|
||||
.data_in(map_q16_a[7:0]), // 8-bit data from memory map
|
||||
.data_valid(map_highs_trigger), // reuse highs_trigger as data strobe
|
||||
.data_count(map_q16_b[15:0]), // element count from memory map
|
||||
.fd_q16(frac_fd_q16),
|
||||
.fd_valid(frac_fd_valid)
|
||||
);
|
||||
|
||||
// Fractal FD → Voltage Mode Selector
|
||||
// Maps fractal dimension to voltage mode:
|
||||
// FD < 2.3 → STORE (0), FD < 2.6 → COMPUTE (1),
|
||||
// FD < 2.9 → APPROX (2), FD >= 2.9 → MORPHIC (3)
|
||||
fractal_fd_selector frac_sel (
|
||||
.clk(clk),
|
||||
.rst_n(rst_n),
|
||||
.fd_q16(frac_fd_q16),
|
||||
.fd_valid(frac_fd_valid),
|
||||
.voltage_mode(frac_voltage_mode),
|
||||
.mode_valid(frac_mode_valid)
|
||||
);
|
||||
|
||||
// ── LED Output ─────────────────────────────────────────────────
|
||||
// When CPU is busy: show running pattern (blinking)
|
||||
// When CPU is halted: show cpu_led (register values from Blitter)
|
||||
|
|
|
|||
531
4-Infrastructure/shim/fractal_dimension.py
Normal file
531
4-Infrastructure/shim/fractal_dimension.py
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
"""
|
||||
fractal_dimension.py — Differential box-counting fractal dimension for VCN pipeline.
|
||||
|
||||
Implements the DBC (Differential Box-Counting) algorithm extended to N-dimensional
|
||||
data, based on: 'Ultra-fast computation of fractal dimension for RGB images'
|
||||
(Pattern Analysis and Applications, 2025).
|
||||
|
||||
Core idea: treat braid data as a 2D surface (like an image), partition into s×s grids,
|
||||
compute box counts via aligned-box method, then FD = slope of log(n(s)) vs log(1/s).
|
||||
|
||||
Q16_16 arithmetic used where possible (fixed-point with 16 integer + 16 fractional bits).
|
||||
No Float in compute paths — ofFloat only at external boundary (JSON, sensor input).
|
||||
|
||||
DBC formula (Sarkar & Chaudhuri 1994):
|
||||
For grid size s, each cell spans gray levels [min, max].
|
||||
Boxes are aligned to global grid: n_cell = floor(max/s) - floor(min/s) + 1
|
||||
Total n(s) = Σ n_cell over all grid cells.
|
||||
FD = slope of least-squares fit on log(n(s)) vs log(1/s).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Q16_16 helpers (fixed-point: 16 integer bits, 16 fractional bits)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Q16_SCALE: int = 1 << 16 # 65536
|
||||
|
||||
|
||||
def q16_from_int(n: int) -> int:
|
||||
"""Convert integer to Q16_16."""
|
||||
return n << 16
|
||||
|
||||
|
||||
def q16_to_float(q: int) -> float:
|
||||
"""Convert Q16_16 to float. Boundary-only; never used in compute paths."""
|
||||
return q / Q16_SCALE
|
||||
|
||||
|
||||
def q16_from_ratio(num: int, den: int) -> int:
|
||||
"""Q16_16 of (num / den) using integer arithmetic."""
|
||||
return (num << 16) // den
|
||||
|
||||
|
||||
def q16_mul(a: int, b: int) -> int:
|
||||
"""Q16_16 multiply."""
|
||||
return (a * b) >> 16
|
||||
|
||||
|
||||
def q16_div(a: int, b: int) -> int:
|
||||
"""Q16_16 divide."""
|
||||
if b == 0:
|
||||
raise ZeroDivisionError("Q16 division by zero")
|
||||
return (a << 16) // b
|
||||
|
||||
|
||||
def q16_log_approx(x: int) -> int:
|
||||
"""
|
||||
Approximate natural log in Q16_16 using integer Newton iteration.
|
||||
Only used at boundary for final FD slope; acceptable float-to-int conversion.
|
||||
"""
|
||||
if x <= 0:
|
||||
raise ValueError("log of non-positive value")
|
||||
# Use float only at external boundary to get ln, then re-quantize
|
||||
raw = math.log(q16_to_float(x)) if x > Q16_SCALE else math.log(x / Q16_SCALE)
|
||||
return int(raw * Q16_SCALE)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Differential Box-Counting — core algorithm
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _ensure_2d(data: np.ndarray) -> np.ndarray:
|
||||
"""Reshape data into 2D surface (rows × cols) for box-counting."""
|
||||
if data.ndim == 1:
|
||||
return data.reshape(1, -1)
|
||||
if data.ndim >= 2:
|
||||
return data.reshape(data.shape[0], -1)
|
||||
return data
|
||||
|
||||
|
||||
def _power_of_two_sizes(max_dim: int) -> List[int]:
|
||||
"""Generate power-of-two grid sizes from 2 up to max_dim."""
|
||||
sizes = []
|
||||
s = 2
|
||||
while s <= max_dim:
|
||||
sizes.append(s)
|
||||
s *= 2
|
||||
return sizes
|
||||
|
||||
|
||||
def _box_count_at_scale(data: np.ndarray, s: int) -> int:
|
||||
"""
|
||||
Compute total box count n(s) for grid size s using DBC (aligned-box method).
|
||||
|
||||
For each s×s cell, the number of boxes needed to cover the gray-level range
|
||||
[min, max] with boxes aligned to the global grid is:
|
||||
n_cell = floor(max / s) - floor(min / s) + 1
|
||||
|
||||
Sum over all cells to get total n(s).
|
||||
|
||||
Uses numpy vectorized operations (copy-if pattern for views).
|
||||
"""
|
||||
rows, cols = data.shape
|
||||
grid_rows = rows // s
|
||||
grid_cols = cols // s
|
||||
|
||||
if grid_rows == 0 or grid_cols == 0:
|
||||
return 0
|
||||
|
||||
# Crop to exact grid alignment (copy-if needed for reshape)
|
||||
cropped = data[: grid_rows * s, : grid_cols * s].copy()
|
||||
|
||||
# Reshape into (grid_rows, s, grid_cols, s) then transpose to (grid_rows, grid_cols, s, s)
|
||||
cells = cropped.reshape(grid_rows, s, grid_cols, s).transpose(0, 2, 1, 3)
|
||||
# cells shape: (grid_rows, grid_cols, s, s)
|
||||
|
||||
cell_max = cells.reshape(grid_rows, grid_cols, -1).max(axis=2).astype(np.int64)
|
||||
cell_min = cells.reshape(grid_rows, grid_cols, -1).min(axis=2).astype(np.int64)
|
||||
|
||||
# Aligned-box DBC formula: floor(max/s) - floor(min/s) + 1
|
||||
box_counts = (cell_max // s) - (cell_min // s) + 1
|
||||
|
||||
return int(box_counts.sum())
|
||||
|
||||
|
||||
def box_count_iterative(data: np.ndarray, max_scale: Optional[int] = None) -> List[Tuple[int, int]]:
|
||||
"""
|
||||
Compute (s, n(s)) pairs for all power-of-two grid sizes.
|
||||
|
||||
Power-of-two refinement: each iteration reuses the spatial structure
|
||||
from the previous level (implicit via numpy reshape).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : np.ndarray
|
||||
Input data (1D or 2D). Will be reshaped to 2D surface.
|
||||
max_scale : int, optional
|
||||
Maximum grid size. Defaults to min(rows, cols).
|
||||
|
||||
Returns
|
||||
-------
|
||||
List of (grid_size, box_count) pairs.
|
||||
"""
|
||||
data_2d = _ensure_2d(data)
|
||||
rows, cols = data_2d.shape
|
||||
|
||||
if max_scale is None:
|
||||
max_scale = min(rows, cols)
|
||||
|
||||
sizes = _power_of_two_sizes(max_scale)
|
||||
results = []
|
||||
|
||||
for s in sizes:
|
||||
n_s = _box_count_at_scale(data_2d, s)
|
||||
if n_s > 0:
|
||||
results.append((s, n_s))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _least_squares_slope(log_inv_s: List[float], log_ns: List[float]) -> float:
|
||||
"""
|
||||
Ordinary least-squares slope.
|
||||
Uses float accumulation for regression (boundary computation).
|
||||
"""
|
||||
n = len(log_inv_s)
|
||||
if n < 2:
|
||||
return 0.0
|
||||
|
||||
sum_x = sum(log_inv_s)
|
||||
sum_y = sum(log_ns)
|
||||
sum_xy = sum(x * y for x, y in zip(log_inv_s, log_ns))
|
||||
sum_x2 = sum(x * x for x in log_inv_s)
|
||||
|
||||
denom = n * sum_x2 - sum_x * sum_x
|
||||
if abs(denom) < 1e-12:
|
||||
return 0.0
|
||||
|
||||
slope = (n * sum_xy - sum_x * sum_y) / denom
|
||||
return slope
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def fractal_dimension(data: np.ndarray, grid_sizes: Optional[List[int]] = None) -> float:
|
||||
"""
|
||||
Compute fractal dimension of 2D data using differential box-counting.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : np.ndarray
|
||||
Input data (bytes, Q16_16 integers, or arbitrary numeric).
|
||||
Reshaped to 2D surface internally.
|
||||
grid_sizes : list of int, optional
|
||||
Specific grid sizes to use. If None, uses power-of-two sizes.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
Fractal dimension, typically in [2.0, 3.0] for 2D surfaces.
|
||||
|
||||
Algorithm
|
||||
---------
|
||||
1. For each grid size s: partition data into s×s cells
|
||||
2. Per cell: n_cell = floor(max/s) - floor(min/s) + 1 (aligned boxes)
|
||||
3. Total n(s) = sum of per-cell counts
|
||||
4. FD = slope of least-squares fit: log(n(s)) vs log(1/s)
|
||||
"""
|
||||
data_2d = _ensure_2d(np.asarray(data))
|
||||
rows, cols = data_2d.shape
|
||||
|
||||
if grid_sizes is None:
|
||||
max_scale = min(rows, cols)
|
||||
grid_sizes = _power_of_two_sizes(max_scale)
|
||||
|
||||
log_inv_s: List[float] = []
|
||||
log_ns: List[float] = []
|
||||
|
||||
for s in grid_sizes:
|
||||
n_s = _box_count_at_scale(data_2d, s)
|
||||
if n_s > 0:
|
||||
log_inv_s.append(math.log(1.0 / s))
|
||||
log_ns.append(math.log(n_s))
|
||||
|
||||
if len(log_inv_s) < 2:
|
||||
# Not enough data points — return flat surface FD
|
||||
return 2.0
|
||||
|
||||
fd = _least_squares_slope(log_inv_s, log_ns)
|
||||
return max(2.0, min(3.0, fd)) # Clamp to valid range
|
||||
|
||||
|
||||
def fractal_dimension_rgb(image: np.ndarray) -> float:
|
||||
"""
|
||||
Compute fractal dimension of a 3-channel (RGB) image.
|
||||
|
||||
Treats each channel as an independent 2D surface, computes FD per channel,
|
||||
then returns the average. This matches the multi-band DBC extension from
|
||||
'Ultra-fast computation of fractal dimension for RGB images'.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image : np.ndarray
|
||||
Shape (H, W, 3) or (H, W, C) array.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
Average fractal dimension across channels, clamped to [2.0, 3.0].
|
||||
"""
|
||||
image = np.asarray(image)
|
||||
if image.ndim < 3:
|
||||
return fractal_dimension(image)
|
||||
|
||||
n_channels = image.shape[2]
|
||||
fds = []
|
||||
for c in range(n_channels):
|
||||
ch_fd = fractal_dimension(image[:, :, c])
|
||||
fds.append(ch_fd)
|
||||
|
||||
avg_fd = sum(fds) / len(fds)
|
||||
return max(2.0, min(3.0, avg_fd))
|
||||
|
||||
|
||||
def fd_compress_hint(fd: float) -> str:
|
||||
"""
|
||||
Map fractal dimension to VCN voltage compression mode.
|
||||
|
||||
FD indicates data complexity:
|
||||
FD < 2.3 → STORE (smooth data, minimal compression needed)
|
||||
FD < 2.6 → COMPUTE (moderate complexity, standard processing)
|
||||
FD < 2.9 → APPROX (high complexity, aggressive approximation)
|
||||
FD >= 2.9 → MORPHIC (maximum complexity, morphic encoding)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fd : float
|
||||
Fractal dimension value.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
One of 'STORE', 'COMPUTE', 'APPROX', 'MORPHIC'.
|
||||
"""
|
||||
if fd < 2.3:
|
||||
return 'STORE'
|
||||
elif fd < 2.6:
|
||||
return 'COMPUTE'
|
||||
elif fd < 2.9:
|
||||
return 'APPROX'
|
||||
else:
|
||||
return 'MORPHIC'
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests and benchmarks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _generate_sierpinski(order: int = 7) -> np.ndarray:
|
||||
"""
|
||||
Generate a Sierpinski triangle approximation as a binary matrix.
|
||||
|
||||
The Sierpinski triangle has known FD = log(3)/log(2) ≈ 1.585 (as a curve).
|
||||
As a 2D filled projection for box-counting, the boundary/fractal structure
|
||||
should push FD above 2.0.
|
||||
"""
|
||||
size = 2 ** order
|
||||
mat = np.zeros((size, size), dtype=np.uint8)
|
||||
|
||||
# Build using Pascal's triangle mod 2
|
||||
for r in range(size):
|
||||
for c in range(size):
|
||||
# Check if C(r, c) is odd (Lucas' theorem for mod 2)
|
||||
# C(n,k) mod 2 = 1 iff (k & ~n) == 0
|
||||
if (c & ~r) == 0 and c <= r:
|
||||
mat[r, c] = 255
|
||||
|
||||
return mat
|
||||
|
||||
|
||||
def _generate_random_data(rows: int = 256, cols: int = 256, seed: int = 42) -> np.ndarray:
|
||||
"""Random uniform data — FD should be moderate (2.3-2.7 range)."""
|
||||
rng = np.random.RandomState(seed)
|
||||
return rng.randint(0, 256, size=(rows, cols), dtype=np.uint8)
|
||||
|
||||
|
||||
def _generate_gradient(rows: int = 256, cols: int = 256) -> np.ndarray:
|
||||
"""Smooth linear gradient — FD should be close to 2.0 (low complexity)."""
|
||||
data = np.zeros((rows, cols), dtype=np.uint8)
|
||||
for r in range(rows):
|
||||
data[r, :] = int(255 * r / max(rows - 1, 1))
|
||||
return data
|
||||
|
||||
|
||||
def _generate_checkerboard(rows: int = 128, cols: int = 128) -> np.ndarray:
|
||||
"""
|
||||
Checkerboard with full amplitude — maximum variation at every scale.
|
||||
|
||||
At scale s: n_cell = floor(255/s) + 1 ≈ 255/s
|
||||
So n(s) ∝ (N/s)^2 * (255/s) = N^2 * 255 / s^3
|
||||
log(n(s)) ∝ const - 3*log(s) → FD ≈ 3.0
|
||||
"""
|
||||
data = np.zeros((rows, cols), dtype=np.uint8)
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
data[r, c] = 255 if (r + c) % 2 == 0 else 0
|
||||
return data
|
||||
|
||||
|
||||
def _generate_fbm_surface(rows: int = 256, cols: int = 256, H: float = 0.5,
|
||||
seed: int = 42) -> np.ndarray:
|
||||
"""
|
||||
Generate fractional Brownian motion surface.
|
||||
|
||||
For Hurst exponent H, the fractal dimension of the surface is FD = 3 - H.
|
||||
H=0.5 (standard Brownian) → FD ≈ 2.5
|
||||
"""
|
||||
rng = np.random.RandomState(seed)
|
||||
# Random increments
|
||||
dx = rng.randn(rows, cols)
|
||||
# Integrate to get surface (cumulative sum in both dimensions)
|
||||
surface = np.cumsum(np.cumsum(dx, axis=0), axis=1)
|
||||
# Normalize to [0, 255]
|
||||
smin, smax = surface.min(), surface.max()
|
||||
if smax > smin:
|
||||
surface = (surface - smin) / (smax - smin) * 255
|
||||
return surface.astype(np.uint8)
|
||||
|
||||
|
||||
def _scalar_box_count(data: np.ndarray, s: int) -> int:
|
||||
"""
|
||||
Scalar (loop-based) box-counting implementation for benchmarking.
|
||||
Same algorithm as _box_count_at_scale but without numpy vectorization.
|
||||
Uses int64 to avoid overflow.
|
||||
"""
|
||||
rows, cols = data.shape
|
||||
grid_rows = rows // s
|
||||
grid_cols = cols // s
|
||||
|
||||
total = 0
|
||||
for gr in range(grid_rows):
|
||||
for gc in range(grid_cols):
|
||||
r_start = gr * s
|
||||
c_start = gc * s
|
||||
cell = data[r_start : r_start + s, c_start : c_start + s]
|
||||
cell_max = int(cell.max())
|
||||
cell_min = int(cell.min())
|
||||
total += (cell_max // s) - (cell_min // s) + 1
|
||||
return total
|
||||
|
||||
|
||||
def run_tests() -> dict:
|
||||
"""
|
||||
Run validation tests and return results dict.
|
||||
|
||||
Tests:
|
||||
1. Sierpinski triangle — FD should be > 2.0 (fractal structure present)
|
||||
2. Random data — FD should be moderate (2.0-2.8 range)
|
||||
3. Smooth gradient — FD should be close to 2.0 (smooth, low complexity)
|
||||
4. Checkerboard — FD should be near 3.0 (maximum variation at all scales)
|
||||
5. fBm surface (H=0.5) — FD should be ≈ 2.5
|
||||
6. Benchmark: numpy vs scalar implementation
|
||||
"""
|
||||
import time
|
||||
|
||||
results = {}
|
||||
|
||||
# Test 1: Sierpinski
|
||||
sierpinski = _generate_sierpinski(order=7)
|
||||
fd_sierpinski = fractal_dimension(sierpinski)
|
||||
results['sierpinski_fd'] = fd_sierpinski
|
||||
results['sierpinski_pass'] = fd_sierpinski > 2.0
|
||||
results['sierpinski_compress'] = fd_compress_hint(fd_sierpinski)
|
||||
|
||||
# Test 2: Random data (moderate FD expected)
|
||||
random_data = _generate_random_data(256, 256)
|
||||
fd_random = fractal_dimension(random_data)
|
||||
results['random_fd'] = fd_random
|
||||
results['random_pass'] = 2.0 <= fd_random <= 3.0
|
||||
results['random_compress'] = fd_compress_hint(fd_random)
|
||||
|
||||
# Test 3: Smooth gradient (FD ≈ 2.0)
|
||||
gradient = _generate_gradient(256, 256)
|
||||
fd_gradient = fractal_dimension(gradient)
|
||||
results['gradient_fd'] = fd_gradient
|
||||
results['gradient_pass'] = fd_gradient < 2.3 # Should be near 2.0
|
||||
results['gradient_compress'] = fd_compress_hint(fd_gradient)
|
||||
|
||||
# Test 4: Checkerboard (FD ≈ 3.0)
|
||||
checkerboard = _generate_checkerboard(128, 128)
|
||||
fd_checker = fractal_dimension(checkerboard)
|
||||
results['checkerboard_fd'] = fd_checker
|
||||
results['checkerboard_pass'] = fd_checker > 2.8 # Should be near 3.0
|
||||
results['checkerboard_compress'] = fd_compress_hint(fd_checker)
|
||||
|
||||
# Test 5: fBm surface (FD ≈ 2.5 for H=0.5)
|
||||
fbm = _generate_fbm_surface(256, 256, H=0.5)
|
||||
fd_fbm = fractal_dimension(fbm)
|
||||
results['fbm_fd'] = fd_fbm
|
||||
results['fbm_pass'] = 2.2 <= fd_fbm <= 2.8 # Should be around 2.5
|
||||
results['fbm_compress'] = fd_compress_hint(fd_fbm)
|
||||
|
||||
# Test 6: Benchmark numpy vs scalar
|
||||
bench_data = np.random.randint(0, 256, size=(256, 256), dtype=np.uint8)
|
||||
bench_scales = [2, 4, 8, 16, 32, 64, 128]
|
||||
|
||||
t0 = time.perf_counter()
|
||||
for _ in range(10):
|
||||
for s in bench_scales:
|
||||
_box_count_at_scale(bench_data, s)
|
||||
numpy_time = (time.perf_counter() - t0) / 10
|
||||
|
||||
t0 = time.perf_counter()
|
||||
for s in bench_scales:
|
||||
_scalar_box_count(bench_data, s)
|
||||
scalar_time = time.perf_counter() - t0
|
||||
|
||||
results['benchmark_numpy_ms'] = round(numpy_time * 1000, 2)
|
||||
results['benchmark_scalar_ms'] = round(scalar_time * 1000, 2)
|
||||
results['speedup'] = round(scalar_time / max(numpy_time, 1e-9), 1)
|
||||
|
||||
# Test 7: RGB image
|
||||
rgb_image = np.random.randint(0, 256, size=(128, 128, 3), dtype=np.uint8)
|
||||
fd_rgb = fractal_dimension_rgb(rgb_image)
|
||||
results['rgb_fd'] = fd_rgb
|
||||
results['rgb_pass'] = 2.0 <= fd_rgb <= 3.0
|
||||
|
||||
# Test 8: Constant data (FD should be exactly 2.0)
|
||||
constant = np.full((128, 128), 42, dtype=np.uint8)
|
||||
fd_const = fractal_dimension(constant)
|
||||
results['constant_fd'] = fd_const
|
||||
results['constant_pass'] = abs(fd_const - 2.0) < 0.01
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _print_results(results: dict) -> None:
|
||||
"""Pretty-print test results."""
|
||||
print("=" * 65)
|
||||
print("Fractal Dimension — Test Results")
|
||||
print("=" * 65)
|
||||
|
||||
test_cases = [
|
||||
('sierpinski', 'FD > 2.0', 'fractal structure'),
|
||||
('random', '2.0 ≤ FD ≤ 2.9', 'random noise'),
|
||||
('gradient', 'FD < 2.3', 'smooth gradient → ~2.0'),
|
||||
('checkerboard', 'FD > 2.8', 'max variation → ~3.0'),
|
||||
('fbm', '2.2 ≤ FD ≤ 2.8', 'fBm H=0.5 → ~2.5'),
|
||||
('constant', 'FD ≈ 2.0', 'constant → exactly 2.0'),
|
||||
('rgb', '2.0 ≤ FD ≤ 3.0', 'RGB random'),
|
||||
]
|
||||
|
||||
for name, expected, desc in test_cases:
|
||||
fd = results[f'{name}_fd']
|
||||
passed = results[f'{name}_pass']
|
||||
hint = results.get(f'{name}_compress', '—')
|
||||
status = 'PASS' if passed else 'FAIL'
|
||||
print(f" [{status}] {name:14s} FD={fd:.4f} hint={hint:8s} ({expected}, {desc})")
|
||||
|
||||
print()
|
||||
print("Benchmark (7 scales, 256×256):")
|
||||
print(f" numpy: {results['benchmark_numpy_ms']:8.2f} ms")
|
||||
print(f" scalar: {results['benchmark_scalar_ms']:8.2f} ms")
|
||||
print(f" speedup: {results['speedup']}x")
|
||||
print("=" * 65)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
results = run_tests()
|
||||
_print_results(results)
|
||||
|
||||
# Exit with non-zero if any test failed
|
||||
all_pass = all(
|
||||
results.get(k, False)
|
||||
for k in [
|
||||
'sierpinski_pass', 'random_pass', 'gradient_pass',
|
||||
'checkerboard_pass', 'fbm_pass', 'constant_pass', 'rgb_pass',
|
||||
]
|
||||
)
|
||||
raise SystemExit(0 if all_pass else 1)
|
||||
Loading…
Add table
Reference in a new issue