The assignment can be completed in one step by simply transforming the index of the lower triangle:
def fill_diagonal(n):
assert n > 0
m = int((2 * n - 1.75) ** 0.5 + 0.5)
'''n >= ((1 + (m - 1)) * (m - 1)) / 2 + 1
=> 2n - 2 >= m ** 2 - m
=> 2n - 7 / 4 >= (m - 1 / 2) ** 2
=> (2n - 7 / 4) ** (1 / 2) + 1 / 2 >= m for n > 0
=> m = floor((2n - 7 / 4) ** (1 / 2) + 1 / 2)
or n <= ((1 + m) * m) / 2
=> (2n + 1 / 4) ** (1 / 2) - 1 / 2 <= m for n > 0
=> m = ceil((2n + 1 / 4) ** (1 / 2) - 1 / 2)
'''
i, j = np.tril_indices(m)
i -= j
ret = np.zeros((m, m), int)
ret[i[:n], j[:n]] = np.arange(1, n + 1)
return ret
Test:
>>> for i in range(1, 16):
... print(repr(fill_diagonal(i)), end='\n\n')
...
array([[1]])
array([[1, 0],
[2, 0]])
array([[1, 3],
[2, 0]])
array([[1, 3, 0],
[2, 0, 0],
[4, 0, 0]])
array([[1, 3, 0],
[2, 5, 0],
[4, 0, 0]])
array([[1, 3, 6],
[2, 5, 0],
[4, 0, 0]])
array([[1, 3, 6, 0],
[2, 5, 0, 0],
[4, 0, 0, 0],
[7, 0, 0, 0]])
array([[1, 3, 6, 0],
[2, 5, 0, 0],
[4, 8, 0, 0],
[7, 0, 0, 0]])
array([[1, 3, 6, 0],
[2, 5, 9, 0],
[4, 8, 0, 0],
[7, 0, 0, 0]])
array([[ 1, 3, 6, 10],
[ 2, 5, 9, 0],
[ 4, 8, 0, 0],
[ 7, 0, 0, 0]])
array([[ 1, 3, 6, 10, 0],
[ 2, 5, 9, 0, 0],
[ 4, 8, 0, 0, 0],
[ 7, 0, 0, 0, 0],
[11, 0, 0, 0, 0]])
array([[ 1, 3, 6, 10, 0],
[ 2, 5, 9, 0, 0],
[ 4, 8, 0, 0, 0],
[ 7, 12, 0, 0, 0],
[11, 0, 0, 0, 0]])
array([[ 1, 3, 6, 10, 0],
[ 2, 5, 9, 0, 0],
[ 4, 8, 13, 0, 0],
[ 7, 12, 0, 0, 0],
[11, 0, 0, 0, 0]])
array([[ 1, 3, 6, 10, 0],
[ 2, 5, 9, 14, 0],
[ 4, 8, 13, 0, 0],
[ 7, 12, 0, 0, 0],
[11, 0, 0, 0, 0]])
array([[ 1, 3, 6, 10, 15],
[ 2, 5, 9, 14, 0],
[ 4, 8, 13, 0, 0],
[ 7, 12, 0, 0, 0],
[11, 0, 0, 0, 0]])
For the case of large n, the performance is about 10 to 20 times that of the loop solution:
%timeit fill_diagonal(10 ** 5)
1.63 ms ± 94.5 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
%timeit fill_diagonal_loop(10 ** 5) # OP's solution
25.1 ms ± 218 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)