Thanks to Reinderien's comment, I was able to figure this out - I had no idea what a Kronecker product was until now. sp.kron
does exactly what I want, with the added benefit of being able to multiply each block by a coefficient.
For the contrived example, the code to specify the pattern would be:
import scipy.sparse as sp
import numpy as np
# Setup subarray and big array parameters
a, b, c, d = 1, 2, 3, 4
sub = sp.coo_array([[a, b], [c, d]])
N = 8
# Setup block locations for our arbitrary pattern
row_idx = np.hstack((np.arange(N/sub.shape[0], dtype=int), np.arange(N/sub.shape[0]-1, dtype=int)))
col_idx = np.hstack((np.arange(N/sub.shape[1], dtype=int), np.arange(N/sub.shape[0]-1, dtype=int)+1))
coeff = np.ones_like(row_idx) # Multiply blocks by coefficients here
locs = sp.csc_array((coeff, (row_idx, col_idx))) # Array of coefficients at specified locations
# Not necessary, but shows what's going on.
print(f'Placing block top left corners at rows{row_idx*sub.shape[0]}, cols {col_idx*sub.shape[1]}')
Actually creating the sparse array is a one-liner once the locations and subarray are specified:
arr = sp.kron(locs, sub)
print(arr.toarray())
yields:
[[1 2 1 2 0 0 0 0]
[3 4 3 4 0 0 0 0]
[0 0 1 2 1 2 0 0]
[0 0 3 4 3 4 0 0]
[0 0 0 0 1 2 1 2]
[0 0 0 0 3 4 3 4]
[0 0 0 0 0 0 1 2]
[0 0 0 0 0 0 3 4]]
This implementation...
None