use super::scoring::TopSSelector;
use crate::frames::GrassmannFrame;
use ndarray::{Array1, Array2, Array3, ArrayView1, ArrayView2, Axis};
use rayon::prelude::*;
#[derive(Clone, Copy, Debug)]
pub struct BlockSparseConfig {
pub n_blocks: usize,
pub block_size: usize,
pub block_topk: usize,
pub max_epochs: usize,
pub minibatch: usize,
pub block_tile: usize,
pub frame_ridge: f64,
pub aux_k: usize,
pub tolerance: f64,
}
impl BlockSparseConfig {
pub fn new(n_blocks: usize, block_size: usize) -> Self {
Self {
n_blocks,
block_size,
..Self::default()
}
}
pub fn n_atoms(&self) -> usize {
self.n_blocks * self.block_size
}
}
impl Default for BlockSparseConfig {
fn default() -> Self {
Self {
n_blocks: 1,
block_size: 2,
block_topk: 1,
max_epochs: 30,
minibatch: 512,
block_tile: 1024,
frame_ridge: 1.0e-9,
aux_k: 0,
tolerance: 1.0e-6,
}
}
}
#[derive(Clone, Debug)]
pub struct BlockSparseFit {
pub decoder: Array2<f32>,
pub blocks: Array2<u32>,
pub gates: Array2<f32>,
pub codes: Array3<f32>,
pub gamma: f32,
pub block_utilization: Vec<f32>,
pub block_stable_rank: Vec<f32>,
pub explained_variance: f64,
pub epochs: usize,
pub converged: bool,
pub block_topk: usize,
pub block_size: usize,
}
impl BlockSparseFit {
pub fn reconstruct(&self) -> Array2<f32> {
let n = self.blocks.nrows();
let p = self.decoder.ncols();
let b = self.block_size;
let mut out = Array2::<f32>::zeros((n, p));
for i in 0..n {
for j in 0..self.block_topk {
let g = self.blocks[[i, j]] as usize;
for r in 0..b {
let code = self.codes[[i, j, r]];
if code == 0.0 {
continue;
}
let row = self.decoder.row(g * b + r);
for c in 0..p {
out[[i, c]] += code * row[c];
}
}
}
}
out
}
}
pub fn block_projections_row(
row: ArrayView1<'_, f32>,
decoder: ArrayView2<'_, f32>,
n_blocks: usize,
b: usize,
) -> Array2<f32> {
let mut w = Array2::<f32>::zeros((n_blocks, b));
for g in 0..n_blocks {
for r in 0..b {
let atom = decoder.row(g * b + r);
let mut acc = 0.0f32;
for (xr, ar) in row.iter().zip(atom.iter()) {
acc += *xr * *ar;
}
w[[g, r]] = acc;
}
}
w
}
pub fn block_gates(w: ArrayView2<'_, f32>) -> Vec<f32> {
w.outer_iter()
.map(|wg| wg.iter().map(|v| v * v).sum::<f32>().sqrt())
.collect()
}
pub fn route_row_blocks(gates: &[f32], k: usize) -> Vec<(u32, f32)> {
let mut sel = TopSSelector::new(k.max(1));
for (g, &gate) in gates.iter().enumerate() {
sel.offer(g as u32, gate);
}
sel.finish()
}
pub fn reconstruct_row(
row: ArrayView1<'_, f32>,
decoder: ArrayView2<'_, f32>,
selected: &[u32],
gamma: f32,
b: usize,
) -> Array1<f32> {
let p = row.len();
let mut out = Array1::<f32>::zeros(p);
for &g in selected {
let g = g as usize;
for r in 0..b {
let atom = decoder.row(g * b + r);
let mut wr = 0.0f32;
for (xr, ar) in row.iter().zip(atom.iter()) {
wr += *xr * *ar;
}
let coef = gamma * wr;
if coef == 0.0 {
continue;
}
for c in 0..p {
out[c] += coef * atom[c];
}
}
}
out
}
pub fn row_loss(
row: ArrayView1<'_, f32>,
decoder: ArrayView2<'_, f32>,
selected: &[u32],
gamma: f32,
b: usize,
) -> f64 {
let recon = reconstruct_row(row, decoder, selected, gamma, b);
row.iter()
.zip(recon.iter())
.map(|(&x, &r)| {
let d = x as f64 - r as f64;
d * d
})
.sum()
}
pub(super) fn orthonormalize_block(block: &mut Array2<f32>) {
let (b, p) = block.dim();
assert!(b <= p, "block size b must not exceed output dim p");
let mut cm = Array2::<f64>::zeros((p, b));
for r in 0..b {
for c in 0..p {
cm[[c, r]] = block[[r, c]] as f64;
}
}
if let Ok(frame) = GrassmannFrame::polar_update(cm.view()) {
let u = frame.frame(); let sv = frame.gauge_singular_values();
let full_rank = sv.len() == b && sv.iter().all(|&s| s > 1.0e-9);
if full_rank && u.ncols() == b {
for r in 0..b {
for c in 0..p {
block[[r, c]] = u[[c, r]] as f32;
}
}
return;
}
}
gram_schmidt_rows(block);
}
pub(super) fn gram_schmidt_rows(block: &mut Array2<f32>) {
let (b, p) = block.dim();
let mut basis: Vec<Vec<f64>> = Vec::with_capacity(b);
for r in 0..b {
let mut v: Vec<f64> = (0..p).map(|c| block[[r, c]] as f64).collect();
for u in basis.iter() {
let dot: f64 = v.iter().zip(u).map(|(a, b)| a * b).sum();
for (vc, uc) in v.iter_mut().zip(u) {
*vc -= dot * uc;
}
}
let mut norm = v.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm <= 1.0e-9 {
let mut installed = false;
for axis in 0..p {
let mut e = vec![0.0f64; p];
e[axis] = 1.0;
for u in basis.iter() {
let dot = u[axis];
for (ec, uc) in e.iter_mut().zip(u) {
*ec -= dot * uc;
}
}
let en = e.iter().map(|x| x * x).sum::<f64>().sqrt();
if en > 1.0e-9 {
for ec in e.iter_mut() {
*ec /= en;
}
v = e;
norm = 1.0;
installed = true;
break;
}
}
if !installed {
for c in 0..p {
v[c] = if c == r % p { 1.0 } else { 0.0 };
}
norm = 1.0;
}
}
for vc in v.iter_mut() {
*vc /= norm;
}
for c in 0..p {
block[[r, c]] = v[c] as f32;
}
basis.push(v);
}
}
pub(super) struct RowBlockCode {
pub(super) blocks: Vec<u32>,
pub(super) gates: Vec<f32>,
pub(super) codes: Vec<f32>,
}
fn route_block_minibatch(
block_rows: ArrayView2<'_, f32>,
decoder: ArrayView2<'_, f32>,
n_blocks: usize,
b: usize,
k: usize,
block_tile: usize,
) -> Vec<Vec<(u32, f32)>> {
let nb = block_rows.nrows();
let mut selectors: Vec<TopSSelector> = (0..nb).map(|_| TopSSelector::new(k)).collect();
let tile = block_tile.max(1);
let mut g0 = 0usize;
while g0 < n_blocks {
let g1 = (g0 + tile).min(n_blocks);
let atom_lo = g0 * b;
let atom_hi = g1 * b;
let slab = decoder.slice(ndarray::s![atom_lo..atom_hi, ..]);
let scores = block_rows.dot(&slab.t()); for (row_idx, srow) in scores.axis_iter(Axis(0)).enumerate() {
for (local_g, g) in (g0..g1).enumerate() {
let base = local_g * b;
let mut e = 0.0f32;
for r in 0..b {
let v = srow[base + r];
e += v * v;
}
selectors[row_idx].offer(g as u32, e.sqrt());
}
}
g0 = g1;
}
selectors.into_iter().map(TopSSelector::finish).collect()
}
pub(super) fn route_and_code_all(
x: ArrayView2<'_, f32>,
decoder: ArrayView2<'_, f32>,
gamma: f32,
n_blocks: usize,
b: usize,
k: usize,
minibatch: usize,
block_tile: usize,
) -> Vec<RowBlockCode> {
let n = x.nrows();
let batch = minibatch.max(1);
let mut out: Vec<RowBlockCode> = Vec::with_capacity(n);
let mut start = 0usize;
while start < n {
let end = (start + batch).min(n);
let mb = x.slice(ndarray::s![start..end, ..]);
let routed = route_block_minibatch(mb, decoder, n_blocks, b, k, block_tile);
let mut coded: Vec<RowBlockCode> = mb
.axis_iter(Axis(0))
.into_par_iter()
.zip(routed.into_par_iter())
.map(|(row, shortlist)| code_row(row, decoder, gamma, b, k, &shortlist))
.collect();
out.append(&mut coded);
start = end;
}
out
}
fn code_row(
row: ArrayView1<'_, f32>,
decoder: ArrayView2<'_, f32>,
gamma: f32,
b: usize,
k: usize,
shortlist: &[(u32, f32)],
) -> RowBlockCode {
let mut blocks = Vec::with_capacity(k);
let mut gates = Vec::with_capacity(k);
let mut codes = Vec::with_capacity(k * b);
for &(g, gate) in shortlist.iter().take(k) {
blocks.push(g);
gates.push(gate);
let gg = g as usize;
for r in 0..b {
let atom = decoder.row(gg * b + r);
let mut wr = 0.0f32;
for (xr, ar) in row.iter().zip(atom.iter()) {
wr += *xr * *ar;
}
codes.push(gamma * wr);
}
}
while blocks.len() < k {
blocks.push(0);
gates.push(0.0);
for _ in 0..b {
codes.push(0.0);
}
}
RowBlockCode {
blocks,
gates,
codes,
}
}
fn projection_sum_row(
row: ArrayView1<'_, f32>,
decoder: ArrayView2<'_, f32>,
blocks: &[u32],
gates: &[f32],
b: usize,
) -> Array1<f32> {
let p = row.len();
let mut out = Array1::<f32>::zeros(p);
for (j, &g) in blocks.iter().enumerate() {
if gates[j] == 0.0 {
continue; }
let gg = g as usize;
for r in 0..b {
let atom = decoder.row(gg * b + r);
let mut wr = 0.0f32;
for (xr, ar) in row.iter().zip(atom.iter()) {
wr += *xr * *ar;
}
if wr == 0.0 {
continue;
}
for c in 0..p {
out[c] += wr * atom[c];
}
}
}
out
}
fn refresh_gamma(
x: ArrayView2<'_, f32>,
codes: &[RowBlockCode],
decoder: ArrayView2<'_, f32>,
b: usize,
prev_gamma: f32,
) -> f32 {
let mut num = 0.0f64;
let mut den = 0.0f64;
for (i, code) in codes.iter().enumerate() {
let xi = x.row(i);
let p_i = projection_sum_row(xi, decoder, &code.blocks, &code.gates, b);
for c in 0..xi.len() {
num += xi[c] as f64 * p_i[c] as f64;
den += p_i[c] as f64 * p_i[c] as f64;
}
}
if den <= 1.0e-24 {
prev_gamma
} else {
(num / den) as f32
}
}
fn refresh_frames(
x: ArrayView2<'_, f32>,
codes: &[RowBlockCode],
decoder: &mut Array2<f32>,
gamma: f32,
n_blocks: usize,
b: usize,
ridge: f64,
) {
let p = x.ncols();
let mut cm: Vec<Array2<f64>> = (0..n_blocks)
.map(|_| Array2::<f64>::zeros((p, b)))
.collect();
let mut touched = vec![false; n_blocks];
for (i, code) in codes.iter().enumerate() {
let xi = x.row(i);
let selected: Vec<u32> = code
.blocks
.iter()
.zip(code.gates.iter())
.filter(|&(_, &gate)| gate != 0.0)
.map(|(&g, _)| g)
.collect();
if selected.is_empty() {
continue;
}
let recon = reconstruct_row(xi, decoder.view(), &selected, gamma, b);
for (j, &g) in code.blocks.iter().enumerate() {
if code.gates[j] == 0.0 {
continue;
}
let gg = g as usize;
let z = &code.codes[j * b..j * b + b];
let mg = &mut cm[gg];
for c in 0..p {
let mut decode_g_c = 0.0f32;
for r in 0..b {
decode_g_c += z[r] * decoder[[gg * b + r, c]];
}
let resid_c = (xi[c] - recon[c] + decode_g_c) as f64;
for r in 0..b {
mg[[c, r]] += resid_c * z[r] as f64;
}
}
touched[gg] = true;
}
}
for g in 0..n_blocks {
if !touched[g] {
continue;
}
if ridge > 0.0 {
for r in 0..b {
for c in 0..p {
cm[g][[c, r]] += ridge * decoder[[g * b + r, c]] as f64;
}
}
}
if let Ok(frame) = GrassmannFrame::polar_update(cm[g].view()) {
let u = frame.frame(); let sv = frame.gauge_singular_values();
let full_rank = sv.len() == b && sv.iter().all(|&s| s > 1.0e-9);
if full_rank && u.ncols() == b {
for r in 0..b {
for c in 0..p {
decoder[[g * b + r, c]] = u[[c, r]] as f32;
}
}
}
}
}
}
fn revive_dead_blocks(
x: ArrayView2<'_, f32>,
codes: &[RowBlockCode],
decoder: &mut Array2<f32>,
gamma: f32,
n_blocks: usize,
b: usize,
aux_k: usize,
) -> usize {
if aux_k == 0 {
return 0;
}
let n = x.nrows();
let p = x.ncols();
let mut usage = vec![0usize; n_blocks];
for code in codes.iter() {
for (j, &g) in code.blocks.iter().enumerate() {
if code.gates[j] != 0.0 {
usage[g as usize] += 1;
}
}
}
let mut order: Vec<usize> = (0..n_blocks).collect();
order.sort_by(|&a, &c| usage[a].cmp(&usage[c]).then(a.cmp(&c)));
let candidates: Vec<usize> = order
.into_iter()
.take(aux_k)
.filter(|&g| usage[g] == 0) .collect();
if candidates.is_empty() {
return 0;
}
let mut resid = Array2::<f32>::zeros((n, p));
let mut resid_norm2 = vec![0.0f64; n];
for i in 0..n {
let xi = x.row(i);
let code = &codes[i];
let selected: Vec<u32> = code
.blocks
.iter()
.zip(code.gates.iter())
.filter(|&(_, &gate)| gate != 0.0)
.map(|(&g, _)| g)
.collect();
let recon = reconstruct_row(xi, decoder.view(), &selected, gamma, b);
let mut acc = 0.0f64;
for c in 0..p {
let rc = xi[c] - recon[c];
resid[[i, c]] = rc;
acc += rc as f64 * rc as f64;
}
resid_norm2[i] = acc;
}
let mut row_order: Vec<usize> = (0..n).collect();
row_order.sort_by(|&a, &c| {
resid_norm2[c]
.partial_cmp(&resid_norm2[a])
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.cmp(&c))
});
let mut revived = 0usize;
let mut cursor = 0usize;
for &g in candidates.iter() {
if cursor >= n || resid_norm2[row_order[cursor]] <= 1.0e-12 {
break; }
let mut seed = Array2::<f32>::zeros((b, p));
for r in 0..b {
let row = if cursor < n {
row_order[cursor]
} else {
row_order[n - 1]
};
cursor += 1;
for c in 0..p {
seed[[r, c]] = resid[[row, c]];
}
}
gram_schmidt_rows(&mut seed);
for r in 0..b {
for c in 0..p {
decoder[[g * b + r, c]] = seed[[r, c]];
}
}
revived += 1;
}
revived
}
fn explained_variance(
x: ArrayView2<'_, f32>,
codes: &[RowBlockCode],
decoder: ArrayView2<'_, f32>,
gamma: f32,
b: usize,
) -> f64 {
let n = x.nrows();
let p = x.ncols();
let mut means = vec![0.0f64; p];
for i in 0..n {
let xi = x.row(i);
for c in 0..p {
means[c] += xi[c] as f64;
}
}
for c in 0..p {
means[c] /= n as f64;
}
let mut rss = 0.0f64;
let mut tss = 0.0f64;
for i in 0..n {
let xi = x.row(i);
let code = &codes[i];
let selected: Vec<u32> = code
.blocks
.iter()
.zip(code.gates.iter())
.filter(|&(_, &gate)| gate != 0.0)
.map(|(&g, _)| g)
.collect();
let recon = reconstruct_row(xi, decoder, &selected, gamma, b);
for c in 0..p {
let r = xi[c] as f64 - recon[c] as f64;
rss += r * r;
let t = xi[c] as f64 - means[c];
tss += t * t;
}
}
if tss <= 1.0e-24 {
if rss <= 1.0e-24 { 1.0 } else { 0.0 }
} else {
1.0 - rss / tss
}
}
fn block_reports(
codes: &[RowBlockCode],
n_blocks: usize,
b: usize,
n_rows: usize,
) -> (Vec<f32>, Vec<f32>) {
let mut usage = vec![0usize; n_blocks];
let mut second: Vec<Array2<f64>> = (0..n_blocks)
.map(|_| Array2::<f64>::zeros((b, b)))
.collect();
for code in codes.iter() {
for (j, &g) in code.blocks.iter().enumerate() {
if code.gates[j] == 0.0 {
continue;
}
let gg = g as usize;
usage[gg] += 1;
let z = &code.codes[j * b..j * b + b];
let cg = &mut second[gg];
for r1 in 0..b {
for r2 in 0..b {
cg[[r1, r2]] += z[r1] as f64 * z[r2] as f64;
}
}
}
}
let util: Vec<f32> = usage
.iter()
.map(|&u| u as f32 / n_rows.max(1) as f32)
.collect();
let stable: Vec<f32> = second
.iter()
.map(|cg| stable_rank_symmetric(cg.view()))
.collect();
(util, stable)
}
pub(super) fn stable_rank_symmetric(c: ArrayView2<'_, f64>) -> f32 {
use gam_linalg::faer_ndarray::FaerEigh;
let trace: f64 = (0..c.nrows()).map(|i| c[[i, i]]).sum();
if trace <= 1.0e-24 {
return 0.0;
}
let owned = c.to_owned();
let lambda_max = match owned.eigh(faer::Side::Lower) {
Ok((evals, _)) => evals.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
Err(_) => trace, };
if lambda_max <= 1.0e-24 {
return 0.0;
}
(trace / lambda_max) as f32
}
pub(super) fn seed_frames(x: ArrayView2<'_, f32>, n_blocks: usize, b: usize) -> Array2<f32> {
let k = n_blocks * b;
let mut decoder = super::update::seed_decoder(x, k);
for g in 0..n_blocks {
let mut block = decoder.slice(ndarray::s![g * b..g * b + b, ..]).to_owned();
orthonormalize_block(&mut block);
for r in 0..b {
for c in 0..decoder.ncols() {
decoder[[g * b + r, c]] = block[[r, c]];
}
}
}
decoder
}
fn validate(x: ArrayView2<'_, f32>, config: &BlockSparseConfig) -> Result<(), String> {
if x.nrows() == 0 || x.ncols() == 0 {
return Err("fit_block_sparse_dictionary requires a non-empty N×P matrix".to_string());
}
if !x.iter().all(|v| v.is_finite()) {
return Err("fit_block_sparse_dictionary input must be finite".to_string());
}
if config.n_blocks == 0 {
return Err("fit_block_sparse_dictionary requires n_blocks >= 1".to_string());
}
if config.block_size == 0 {
return Err("fit_block_sparse_dictionary requires block_size >= 1".to_string());
}
if config.block_size > x.ncols() {
return Err(format!(
"fit_block_sparse_dictionary block_size b={} cannot exceed output dim P={} \
(a block's b orthonormal rows must fit in ℝ^P)",
config.block_size,
x.ncols()
));
}
if config.block_topk == 0 {
return Err("fit_block_sparse_dictionary requires block_topk >= 1".to_string());
}
if config.max_epochs == 0 {
return Err("fit_block_sparse_dictionary requires max_epochs >= 1".to_string());
}
if !(config.frame_ridge.is_finite() && config.frame_ridge >= 0.0) {
return Err("fit_block_sparse_dictionary frame_ridge must be finite and >= 0".to_string());
}
if !config.tolerance.is_finite() {
return Err("fit_block_sparse_dictionary tolerance must be finite".to_string());
}
Ok(())
}
pub fn fit_block_sparse_dictionary(
x: ArrayView2<'_, f32>,
config: &BlockSparseConfig,
) -> Result<BlockSparseFit, String> {
validate(x, config)?;
let n = x.nrows();
let g = config.n_blocks;
let b = config.block_size;
let k = config.block_topk.min(g).max(1);
let mut decoder = seed_frames(x, g, b);
let mut gamma = 1.0f32;
let mut codes = route_and_code_all(
x,
decoder.view(),
gamma,
g,
b,
k,
config.minibatch,
config.block_tile,
);
let mut prev_ev = f64::NEG_INFINITY;
let mut converged = false;
let mut epochs_run = 0usize;
for epoch in 0..config.max_epochs {
epochs_run = epoch + 1;
gamma = refresh_gamma(x, &codes, decoder.view(), b, gamma);
refresh_frames(x, &codes, &mut decoder, gamma, g, b, config.frame_ridge);
let revived = revive_dead_blocks(x, &codes, &mut decoder, gamma, g, b, config.aux_k);
codes = route_and_code_all(
x,
decoder.view(),
gamma,
g,
b,
k,
config.minibatch,
config.block_tile,
);
let ev = explained_variance(x, &codes, decoder.view(), gamma, b);
let improve = ev - prev_ev;
if revived == 0 && improve.abs() <= config.tolerance && epoch > 0 {
converged = true;
break;
}
prev_ev = ev;
}
gamma = refresh_gamma(x, &codes, decoder.view(), b, gamma);
let final_ev = explained_variance(x, &codes, decoder.view(), gamma, b);
let (block_utilization, block_stable_rank) = block_reports(&codes, g, b, n);
let mut blocks = Array2::<u32>::zeros((n, k));
let mut gates = Array2::<f32>::zeros((n, k));
let mut code_arr = Array3::<f32>::zeros((n, k, b));
for (i, code) in codes.iter().enumerate() {
for j in 0..k {
blocks[[i, j]] = code.blocks[j];
for r in 0..b {
code_arr[[i, j, r]] = code.codes[j * b + r];
}
}
}
recompute_gates(x, decoder.view(), &blocks, gamma, b, &mut gates);
Ok(BlockSparseFit {
decoder,
blocks,
gates,
codes: code_arr,
gamma,
block_utilization,
block_stable_rank,
explained_variance: final_ev,
epochs: epochs_run,
converged,
block_topk: k,
block_size: b,
})
}
fn recompute_gates(
x: ArrayView2<'_, f32>,
decoder: ArrayView2<'_, f32>,
blocks: &Array2<u32>,
gamma: f32,
b: usize,
gates: &mut Array2<f32>,
) {
let (n, k) = blocks.dim();
for i in 0..n {
let xi = x.row(i);
for j in 0..k {
let g = blocks[[i, j]] as usize;
let mut e = 0.0f32;
for r in 0..b {
let atom = decoder.row(g * b + r);
let mut wr = 0.0f32;
for (xr, ar) in xi.iter().zip(atom.iter()) {
wr += *xr * *ar;
}
e += wr * wr;
}
gates[[i, j]] = gamma.abs() * e.sqrt();
}
}
}
pub fn block_sparse_dictionary_transform(
x: ArrayView2<'_, f32>,
decoder: ArrayView2<'_, f32>,
gamma: f32,
block_size: usize,
block_topk: usize,
block_tile: usize,
) -> Result<(Array2<u32>, Array2<f32>, Array3<f32>), String> {
let b = block_size;
if b == 0 {
return Err("block_sparse_dictionary_transform: block_size must be >= 1".to_string());
}
let krows = decoder.nrows();
if krows == 0 || krows % b != 0 {
return Err(format!(
"block_sparse_dictionary_transform: decoder has K={krows} rows, not a multiple of \
block_size b={b}"
));
}
if x.ncols() != decoder.ncols() {
return Err(format!(
"block_sparse_dictionary_transform: X has P={} columns but the frames have P={}",
x.ncols(),
decoder.ncols()
));
}
let g = krows / b;
let k = block_topk.min(g).max(1);
let minibatch = 4096usize;
let codes = route_and_code_all(x, decoder, gamma, g, b, k, minibatch, block_tile.max(1));
let m = x.nrows();
let mut blocks = Array2::<u32>::zeros((m, k));
let mut gates = Array2::<f32>::zeros((m, k));
let mut code_arr = Array3::<f32>::zeros((m, k, b));
for (i, code) in codes.iter().enumerate() {
for j in 0..k {
blocks[[i, j]] = code.blocks[j];
gates[[i, j]] = gamma.abs() * code.gates[j];
for r in 0..b {
code_arr[[i, j, r]] = code.codes[j * b + r];
}
}
}
Ok((blocks, gates, code_arr))
}
#[cfg(test)]
#[path = "block_tests.rs"]
mod block_tests;