mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-08-16 21:50:36 +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
185f1a6266
commit
c381dbdfe6
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 \
|
voltage_mode_controller.v \
|
||||||
scale_space_bram.v \
|
scale_space_bram.v \
|
||||||
highs_pivot_accelerator.v \
|
highs_pivot_accelerator.v \
|
||||||
|
fractal_box_counter.v \
|
||||||
|
fractal_fd_selector.v \
|
||||||
Blitter6502OISC_small.v \
|
Blitter6502OISC_small.v \
|
||||||
research_stack_top.v;
|
research_stack_top.v;
|
||||||
synth_gowin -top ${TOP} -json ${JSON};
|
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)
|
// Unified FPGA design for Tang Nano 9K (GW1NR-9C)
|
||||||
// Combines: Blitter6502OISC + Q16 LUT + Memory Map + Voltage Controller
|
// Combines: Blitter6502OISC + Q16 LUT + Memory Map + Voltage Controller
|
||||||
// + Scale Space BRAM + HiGHS Pivot Accelerator
|
// + Scale Space BRAM + HiGHS Pivot Accelerator
|
||||||
|
// + Fractal Box Counter + FD Selector
|
||||||
|
|
||||||
`timescale 1ns / 1ps
|
`timescale 1ns / 1ps
|
||||||
|
|
||||||
|
|
@ -101,6 +102,12 @@ module research_stack_top (
|
||||||
wire [5:0] highs_write_idx;
|
wire [5:0] highs_write_idx;
|
||||||
wire [31:0] highs_write_data;
|
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 ────────────────────
|
// ── Q16 LUT: use lower 16 bits of operands ────────────────────
|
||||||
wire [15:0] q16_a_16 = map_q16_a[15:0];
|
wire [15:0] q16_a_16 = map_q16_a[15:0];
|
||||||
wire [15:0] q16_b_16 = map_q16_b[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)
|
.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 ─────────────────────────────────────────────────
|
// ── LED Output ─────────────────────────────────────────────────
|
||||||
// When CPU is busy: show running pattern (blinking)
|
// When CPU is busy: show running pattern (blinking)
|
||||||
// When CPU is halted: show cpu_led (register values from Blitter)
|
// 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