@@ -131,93 +131,177 @@ getVb <- function(v,Zt,root.phi,scale,Xf,Xfp,Sp,B,python_cholmod=FALSE,woodbury=
131131# # and Vb = B Vbp B' in orginal para.
132132
133133 if (python_cholmod ) {
134- reticulate :: py_require(packages = " scikit-sparse>=0.4.16" )
135- cholmod <- reticulate :: import(" sksparse.cholmod" )
134+ reticulate :: py_require(packages = " scikit-sparse~=0.5.0" )
135+ cholmod <- reticulate :: import(" sksparse.cholmod" , convert = FALSE )
136+ scipy_sparse <- reticulate :: import(" scipy.sparse" , convert = FALSE )
136137 }
137138
139+ # Ensure sparse matrices
138140 Xf <- as(Xf ," dgCMatrix" )
139141 Xfp <- as(Xfp ," dgCMatrix" )
140- if (nrow(Zt )) { # # drop 0 variance components before proceeding
142+
143+ # drop 0 variance components before proceeding
144+ if (nrow(Zt )) {
141145 phi <- crossprod(root.phi )
142146 ind <- which(diag(phi )< .Machine $ double.eps ^ .9 * norm(phi ))
147+
143148 if (length(ind )) { # # drop zero variance terms
144149 phi <- phi [- ind ,- ind ]
145150 root.phi <- root.phi [,- ind ]
146151 Zt <- Zt [- ind ,] # # better to be using Z?
147152 }
148- if (woodbury && nrow(Zt )) { # # should really use Cholesky and chol2inv
149- phi.inv <- try(solve(phi ),silent = TRUE ) # # could fail if phi semi-def
150- if (inherits(phi.inv ," try-error" )) woodbury <- FALSE # # in which case fall back on direct
153+
154+ if (woodbury && nrow(Zt )) {
155+ if (python_cholmod ) {
156+ phi_py = reticulate :: r_to_py(phi )$ copy()
157+ cho_phi_py <- try(cholmod $ cho_factor(phi_py , supernodal_mode = " auto" , lower = TRUE ), silent = TRUE )
158+ if (inherits(cho_phi_py , " try-error" )) {
159+ woodbury <- FALSE
160+
161+ } else {
162+ perm <- as.integer(reticulate :: py_to_r(cho_phi_py $ get_perm()) + 1 )
163+ inv_perm <- perm
164+ inv_perm [perm ] <- 1 : length(perm )
165+ phi.rphi <- reticulate :: py_to_r(scipy_sparse $ csc_matrix(cho_phi_py $ R ))[,inv_perm ]
166+ }
167+ } else {
168+ phi.inv <- try(chol2inv(chol(phi )), silent = TRUE )
169+ if (inherits(phi.inv ," try-error" )) woodbury <- FALSE
170+ }
151171 }
152- }
153- if (woodbury ) { # # Woodbury formula version of XVX computations
172+ }
173+
174+ if (woodbury ) {
175+ # # Woodbury formula version of XVX computations
154176 # # if V=diag(v) and s scale and phi = crossprod(root.phi) then...
155177 # # (V+ZphiZ's)^-1 = V^{-1} - V^{-1}Z(phi^{-1}/s+Z'V^{-1}Z)^{-1} Z'V^{-1}
178+
156179 vi <- 1 / v
157180 if (nrow(Zt )> 0 ) {
158- V <- phi.inv / scale + Zt %*% Diagonal(n = length(vi ),x = vi )%*% t(Zt )
159181 if (python_cholmod ) {
160- R <- cholmod $ cholesky(V )
161- X1 <- as.matrix(R $ solve_Lt(Zt %*% (Xf * vi ),use_LDLt_decomposition = FALSE ))
162- XVX <- t(Xf )%*% (vi * Xf - vi * t(Zt )%*% as.matrix(R $ solve_L(X1 ,use_LDLt_decomposition = FALSE )))
163- X1 <- as.matrix(R $ solve_Lt(Zt %*% (Xfp * vi ),use_LDLt_decomposition = FALSE ))
164- XVXS <- t(Xfp )%*% (vi * Xfp - vi * t(Zt )%*% as.matrix(R $ solve_L(X1 ,use_LDLt_decomposition = FALSE ))) + Sp ^ 2 / scale
182+ Ztilde_t <- phi.rphi %*% Zt
183+ V <- Diagonal(n = nrow(Ztilde_t ))/ scale + Ztilde_t %*% Diagonal(n = length(vi ),x = vi )%*% t(Ztilde_t )
184+ V_py <- reticulate :: r_to_py(V )$ copy()
185+ R <- cholmod $ cho_factor(V_py ,supernodal_mode = " auto" , lower = TRUE )
186+
187+ # Prepare permuation and inverse permutation arrays
188+ perm <- as.integer(reticulate :: py_to_r(R $ get_perm()) + 1 )
189+ inv_perm <- perm
190+ inv_perm [perm ] <- 1 : length(perm )
191+
192+ # XVX
193+ Z1 <- (Ztilde_t %*% (Xf * vi ))[perm ,]
194+ Z1_py = reticulate :: r_to_py(Z1 )$ copy()
195+ X1 <- R $ solve(Z1_py , system = " L" )
196+ XVX <- t(Xf )%*% (vi * Xf - vi * t(Ztilde_t )%*% (reticulate :: py_to_r(scipy_sparse $ csc_matrix(R $ solve(X1 , system = " Lt" )))[inv_perm ,]))
197+
198+ # XVXS
199+ ZS1 <- (Ztilde_t %*% (Xfp * vi ))[perm ,]
200+ ZS1_py <- reticulate :: r_to_py(ZS1 )$ copy()
201+ XS1 <- R $ solve(ZS1_py ,system = " L" )
202+ XVXS <- t(Xfp )%*% (vi * Xfp - vi * t(Ztilde_t )%*% (reticulate :: py_to_r(scipy_sparse $ csc_matrix(R $ solve(XS1 ,system = " Lt" )))[inv_perm ,])) + Sp ^ 2 / scale
203+
165204 } else {
205+ V <- phi.inv / scale + Zt %*% Diagonal(n = length(vi ),x = vi )%*% t(Zt )
166206 R <- mgcv :: mchol(V )
167207 ipiv <- piv <- attr(R ," pivot" ); ipiv [piv ] <- 1 : length(piv )
168208 XVX <- t(Xf )%*% (vi * Xf - vi * t(Zt )%*% solve(R ,solve(t(R ),(Zt %*% (Xf * vi ))[piv ,]))[ipiv ,])
169- # XVX0 <- t(Xf)%*%solve(Diagonal(n=length(v),x=v)+scale*crossprod(root.phi%*%Zt),Xf) ## equivalent direct
170209 XVXS <- t(Xfp )%*% (vi * Xfp - vi * t(Zt )%*% solve(R ,solve(t(R ),(Zt %*% (Xfp * vi ))[piv ,]))[ipiv ,]) + Sp ^ 2 / scale
171210 }
172211 } else {
173212 XVX <- t(Xf )%*% (vi * Xf ); XVXS <- t(Xfp )%*% (vi * Xfp ) + Sp ^ 2 / scale
174213 }
175- } else { # # Direct Xf'(diag(v) + crossprod(root.phi%*%Zt))^{-1} Xf
176- V <- Diagonal(n = length(v ),x = v )
177- if (nrow(Zt )> 0 ) V <- V + crossprod(root.phi %*% Zt )* scale # # data or pseudodata cov matrix, treating smooths as fixed now
214+ } else {
215+ # # Direct Xf'(diag(v) + crossprod(root.phi%*%Zt))^{-1} Xf
216+ V <- Diagonal(n = length(v ),x = v )
217+
218+ if (nrow(Zt )> 0 ) {
219+ # # data or pseudodata cov matrix, treating smooths as fixed now
220+ V <- V + crossprod(root.phi %*% Zt )* scale
221+ }
178222
179223 if (python_cholmod ) {
180- R <- cholmod $ cholesky(V )
181- WX <- as.matrix(R $ solve_Lt(Xfp ,use_LDLt_decomposition = FALSE ))
182- XVX <- as.matrix(R $ solve_Lt(Xf ,use_LDLt_decomposition = FALSE ))
224+ # Python Bridge
225+ V_py = reticulate :: r_to_py(V )$ copy()
226+ R <- cholmod $ cho_factor(V_py , supernodal_mode = " auto" , lower = TRUE )
227+
228+ XFP_py <- reticulate :: r_to_py(Xfp )$ copy()
229+ XF_py <- reticulate :: r_to_py(Xf )$ copy()
230+
231+ # X'V^{-1}X
232+ XVXS <- t(Xfp ) %*% reticulate :: py_to_r(scipy_sparse $ csc_matrix(R $ solve(XFP_py , system = " A" ))) + Sp ^ 2 / scale
233+ XVX <- t(Xf ) %*% reticulate :: py_to_r(scipy_sparse $ csc_matrix(R $ solve(XF_py , system = " A" )))
234+
183235 } else {
236+ # Native R
184237 R <- mgcv :: mchol(V );piv <- attr(R ," pivot" )
185- if (is.null( piv )) { # # not sure this can happen any more
186- WX <- as(solve(t(R ),Xfp )," matrix" ) # # V^{-.5}Xp -- fit parameterization
187- XVX <- as(solve(t(R ),Xf )," matrix" ) # # same in original parameterization
188- } else {
189- WX <- as(solve(t( R ), Xfp [ piv ,]), " matrix " ) # # V^{-.5}Xp -- fit parameterization
190- XVX <- as(solve(t( R ), Xf [ piv ,]), " matrix " ) # # same in original parameterization
191- }
238+
239+ WX <- as(solve(t(R ),Xfp [ piv ,] )," matrix" ) # # V^{-.5}Xp -- fit parameterization
240+ XVX <- as(solve(t(R ),Xf [ piv ,] )," matrix" ) # # same in original parameterization
241+
242+ # there is no need to undo the permutations because crossproduct below would cancel it
243+ XVX <- crossprod( XVX ) # # X'V^{-1}X original parameterization
244+ XVXS <- crossprod( WX ) + Sp ^ 2 / scale # # X'V^{-1}X + S fit para
192245 }
193- XVX <- crossprod(XVX ) # # X'V^{-1}X original parameterization
194- XVXS <- crossprod(WX )+ Sp ^ 2 / scale # # X'V^{-1}X + S fit para
195- }
196- if (TRUE ) { # # Cholesky based XVX and R
246+
247+
248+ }
249+
250+ cholmod_fallback <- FALSE
251+ if (python_cholmod ) {
252+ XVX_py <- reticulate :: r_to_py(XVX )$ copy()
253+ R_chol <- try(cholmod $ cho_factor(XVX_py , supernodal_mode = " auto" , lower = TRUE ), silent = TRUE )
254+
255+ if (inherits(R_chol ," try-error" )) {
256+ cholmod_fallback <- TRUE
257+ } else {
258+
259+ # Prepare permutation and inverse permutation arrays
260+ perm <- as.integer(reticulate :: py_to_r(R_chol $ get_perm()) + 1 )
261+ inv_perm <- perm
262+ inv_perm [perm ] <- 1 : length(perm )
263+
264+ R <- reticulate :: py_to_r(scipy_sparse $ csc_matrix(R_chol $ R ))[,inv_perm ]
265+ }
266+ }
267+
268+ if (! python_cholmod || cholmod_fallback ) {
197269 R <- try(mgcv :: mchol(XVX ),silent = TRUE ) # # can be semi-def so only dense and pivot works
198270 if (inherits(R ," try-error" )|| all.equal(attr(R ," rank" ),- 1 )== TRUE ) R <- mgcv :: mchol(as.matrix(XVX ))
199- R [,attr(R ," pivot" )] <- R ; attr(R ," pivot" ) <- NULL
200- } else { # # QR based XVX and R ## DEBUG ONLY requires XVX pre-crossprod
201- qrz <- qr(XVX ,LAPACK = TRUE )
202- R <- qr.R(qrz ); R [,qrz $ pivot ] <- R
203- XVX <- crossprod(R ) # # X'V^{-1}X original parameterization
271+ R [,attr(R ," pivot" )] <- R ; attr(R ," pivot" ) <- NULL # inverse permutation
204272 }
205- # # Alternative cov matrix calculation. Basic
206- # # idea is that cov matrix is computed stably in
207- # # fitting parameterization, and then transformed to
208- # # original parameterization.
209-
210- if (TRUE ) { # # Cholesky version...
211- Rf <- mgcv :: mchol(XVXS ) # # Rf'Rf = X'V^{-1}X + S in fit para
273+
274+ cholmod_fallback <- FALSE
275+
276+ if (python_cholmod ) {
277+ XVXS_py <- reticulate :: r_to_py(XVXS )$ copy()
278+ Rf <- try(cholmod $ cho_factor(XVXS_py , supernodal_mode = " auto" , lower = TRUE ), silent = TRUE ) # # Rf'Rf = X'V^{-1}X + S in fit para
279+
280+ if (inherits(Rf ," try-error" )) {
281+ cholmod_fallback <- TRUE
282+ } else {
283+ # Get inverse decomposition in the permuted space
284+ id_py <- reticulate :: r_to_py(diag(ncol(XVXS )))$ copy()
285+ Ri <- reticulate :: py_to_r(scipy_sparse $ csc_matrix(Rf $ solve(id_py ,system = " Lt" )))
286+
287+ # Prepare permutation and inverse permutation arrays
288+ perm <- as.integer(reticulate :: py_to_r(Rf $ get_perm()) + 1 )
289+ inv_perm <- perm
290+ inv_perm [perm ] <- 1 : length(perm )
291+
292+ # Apply inverse permutation to the inverted cholesky factor to project into original space
293+ Ri <- Ri [inv_perm ,]
294+ }
295+ }
296+
297+ if (! python_cholmod || cholmod_fallback ) {
298+ Rf <- try(mgcv :: mchol(XVXS ),silent = TRUE ) # # Rf'Rf = X'V^{-1}X + S in fit para
299+ if (inherits(Rf ," try-error" )|| all.equal(attr(Rf ," rank" ),- 1 )== TRUE ) Rf <- mgcv :: mchol(as.matrix(XVXS ))
212300 Ri <- backsolve(Rf ,diag(ncol(Rf ))); ind <- attr(Rf ," pivot" )
213301 ind [ind ] <- 1 : length(ind )
214- Ri <- Ri [ind ,] # # unpivoted square root of cov matrix in fitting parameterization Ri Ri' = cov
215- } else { # # QR version...
216- qrx <- qr(rbind(WX ,Sp / sqrt(scale )),LAPACK = TRUE )
217- Ri <- backsolve(qr.R(qrx ),diag(ncol(WX ))) # # R'R =X'V^{-1}X + S in fit para
218- ind <- qrx $ pivot ;ind [ind ] <- 1 : length(ind )# # qrx$pivot
219- Ri <- Ri [ind ,] # # unpivoted square root of cov matrix in fitting parameterization Ri Ri' = cov
302+ Ri <- Ri [ind ,]
220303 }
304+
221305 Vb <- B %*% Ri ; Vb <- Vb %*% t(Vb )
222306 list (Vb = Vb ,XVX = XVX ,R = R )
223307} # # getVb
@@ -230,6 +314,13 @@ gamm4 <- function(formula,random=NULL,family=gaussian(),data=list(),weights=NULL
230314# parts of the smooth terms are treated as random effects. The onesided formula random defines additional
231315# random terms.
232316
317+ if (isTRUE(python_cholmod ) && ! requireNamespace(" reticulate" , quietly = TRUE )) {
318+ stop(
319+ " `python_cholmod = TRUE` requires the `reticulate` package to be installed." ,
320+ call. = FALSE
321+ )
322+ }
323+
233324
234325 if (! is.null(random )) {
235326 if (! inherits(random ," formula" )) stop(" gamm4 requires `random' to be a formula" )
@@ -496,7 +587,7 @@ gamm4 <- function(formula,random=NULL,family=gaussian(),data=list(),weights=NULL
496587
497588 if (length(ind )) { # # extract columns corresponding to non-smooth r.e.s
498589 Zt <- getME(ret $ mer ," Zt" )[ind ,] # # extracting random effects model matrix
499- root.phi <- getME(ret $ mer ," Lambdat" )[,ind ] # # and corresponding sqrt of cov matrix (phi) (was ind,ind - seems wrong )
590+ root.phi <- getME(ret $ mer ," Lambdat" )[ind ,ind ] # # and corresponding sqrt of cov matrix (phi)
500591 }
501592
502593 object $ sp <- sp
@@ -519,10 +610,8 @@ gamm4 <- function(formula,random=NULL,family=gaussian(),data=list(),weights=NULL
519610 object $ prior.weights <- G $ w
520611 object $ weights <- if (linear ) object $ prior.weights else ret $ mer @ resp $ sqrtWrkWt()^ 2
521612 v <- scale / object $ weights
522- V <- Diagonal(n = length(v ),x = v )
523613
524614 a <- getVb(v ,Zt ,root.phi ,scale ,G $ Xf ,Xfp ,Sp ,B ,python_cholmod ,ncol(Zt )> nrow(Zt ))
525-
526615 Vb <- a $ Vb ; XVX <- a $ XVX ; object $ R <- a $ R
527616
528617 object $ edf <- rowSums(Vb * t(XVX ))
0 commit comments