11import numpy as np
22
3- from autoarray .util .cholesky_funcs import cholinsertlast , choldeleteindexes
3+ from autoarray .util .cholesky_funcs import (
4+ _cho_solve_buffer ,
5+ cholinsertlast_inplace ,
6+ choldeleteindexes_inplace ,
7+ )
48
59from autoarray import exc
610
@@ -50,6 +54,18 @@ def fnnls_cholesky(
5054 w = ZTx - (ZTZ ) @ d
5155 s_chol = np .zeros (n )
5256
57+ # The Cholesky factor of ZTZ[passive][:, passive] lives in the top-left
58+ # k_active x k_active corner of a single preallocated buffer, updated in
59+ # place by cholinsertlast_inplace / choldeleteindexes_inplace as the
60+ # active set changes. The buffer is allocated once (first factorisation)
61+ # instead of the factor being rebuilt with np.insert/np.delete every
62+ # iteration — the dominant cost of this solver at n ~ 1000. Zeroed, not
63+ # empty: the update/solve kernels only ever read the upper triangle, but
64+ # keeping the rest exactly zero costs one memset and keeps every k x k
65+ # view a valid dense factor for inspection and tests.
66+ U_buffer = np .zeros ((n , n ))
67+ k_active = 0
68+
5369 if P_initial .shape [0 ] != 0 :
5470 P_number = np .arange (len (P ), dtype = "int" )
5571 P_inorder = P_number [P_initial ]
@@ -76,22 +92,27 @@ def fnnls_cholesky(
7692 if loop_count == 0 :
7793 # We need to initialize the Cholesky factorisation, U, for the first loop.
7894 U = slg .cholesky (ZTZ [P_inorder ][:, P_inorder ])
95+ k_active = U .shape [0 ]
96+ U_buffer [:k_active , :k_active ] = U
7997 else :
80- U = cholinsertlast (U , ZTZ [idmax ][P_inorder ])
98+ k_active = cholinsertlast_inplace (
99+ U_buffer , k_active , ZTZ [idmax ][P_inorder ]
100+ )
81101
82- # solve the lstsq problem by cho_solve
102+ # solve the lstsq problem via the copy-free buffer cho_solve
83103
84- s_chol [P_inorder ] = slg . cho_solve (( U , False ) , ZTx [P_inorder ])
104+ s_chol [P_inorder ] = _cho_solve_buffer ( U_buffer , k_active , ZTx [P_inorder ])
85105
86106 P [idmax ] = True
87107 while np .any (P ) and np .min (s_chol [P ]) <= tolerance :
88- s_chol , d , P , P_inorder , U = fix_constraint_cholesky (
108+ s_chol , d , P , P_inorder , k_active = fix_constraint_cholesky (
89109 ZTx = ZTx ,
90110 s_chol = s_chol ,
91111 d = d ,
92112 P = P ,
93113 P_inorder = P_inorder ,
94- U = U ,
114+ U_buffer = U_buffer ,
115+ k_active = k_active ,
95116 tolerance = tolerance ,
96117 )
97118
@@ -132,18 +153,18 @@ def fnnls_cholesky(
132153 return d
133154
134155
135- def fix_constraint_cholesky (ZTx , s_chol , d , P , P_inorder , U , tolerance ):
156+ def fix_constraint_cholesky (ZTx , s_chol , d , P , P_inorder , U_buffer , k_active , tolerance ):
136157 """
137158 Similar to fix_constraint, but solve the lstsq by Cholesky factorisation.
138159 If this function is called, it means some solutions in the current passive sets needed to be
139160 taken out and put into the active set.
140161 So, this function involves 3 procedure:
141162 1. Identifying what solutions should be taken out of the current passive set.
142- 2. Updating the P, P_inorder and the Cholesky factorisation U.
143- 3. Solving the lstsq by using the new Cholesky factorisation U.
163+ 2. Updating the P, P_inorder and the Cholesky factorisation (the active
164+ k_active x k_active corner of U_buffer, updated in place).
165+ 3. Solving the lstsq by using the new Cholesky factorisation.
144166 As some solutions are taken out from the passive set, the Cholesky factorisation needs to be
145- updated by choldeleteindexes. To realize that, we call the `choldeleteindexes` from
146- cholesky_funcs.
167+ updated in place by `choldeleteindexes_inplace` from cholesky_funcs.
147168 """
148169 q = P * (s_chol <= tolerance )
149170 alpha = np .min (d [q ] / (d [q ] - s_chol [q ]))
@@ -153,20 +174,20 @@ def fix_constraint_cholesky(ZTx, s_chol, d, P, P_inorder, U, tolerance):
153174
154175 id_delete = np .where (d [P_inorder ] <= tolerance )[0 ]
155176
156- U = choldeleteindexes (U , id_delete ) # update the Cholesky factorisation
177+ # update the Cholesky factorisation
178+
179+ k_active = choldeleteindexes_inplace (U_buffer , k_active , id_delete )
157180
158181 P_inorder = np .delete (P_inorder , id_delete ) # update the P_inorder
159182
160183 P [d <= tolerance ] = False # update the P
161184
162- # solve the lstsq problem by cho_solve
185+ # solve the lstsq problem via the copy-free buffer cho_solve
163186
164187 if len (P_inorder ):
165- from scipy import linalg as slg
166-
167188 # there could be a case where P_inorder is empty.
168- s_chol [P_inorder ] = slg . cho_solve (( U , False ) , ZTx [P_inorder ])
189+ s_chol [P_inorder ] = _cho_solve_buffer ( U_buffer , k_active , ZTx [P_inorder ])
169190
170191 s_chol [~ P ] = 0.0 # set solutions taken out of the passive set to be 0
171192
172- return s_chol , d , P , P_inorder , U
193+ return s_chol , d , P , P_inorder , k_active
0 commit comments