@@ -1031,10 +1031,7 @@ def _update_weights(self):
10311031 )
10321032 self .weights_ [:, signal ] = new_weight
10331033
1034- def _regularize_function (self , stretch = None ):
1035- if stretch is None :
1036- stretch = self .stretch_
1037-
1034+ def _stretch_residual_and_derivatives (self , stretch ):
10381035 stretched_components , d_stretch_comps , dd_stretch_comps = (
10391036 self ._compute_stretched_components (stretch = stretch )
10401037 )
@@ -1048,6 +1045,15 @@ def _regularize_function(self, stretch=None):
10481045 )
10491046 - self ._source_matrix
10501047 )
1048+ return residuals , d_stretch_comps , dd_stretch_comps
1049+
1050+ def _regularize_function (self , stretch = None ):
1051+ if stretch is None :
1052+ stretch = self .stretch_
1053+
1054+ residuals , d_stretch_comps , _ = self ._stretch_residual_and_derivatives (
1055+ stretch
1056+ )
10511057
10521058 fun = self ._get_objective_function (residuals , stretch )
10531059
@@ -1062,10 +1068,60 @@ def _regularize_function(self, stretch=None):
10621068 @ (self ._spline_smooth_operator .T @ self ._spline_smooth_operator )
10631069 )
10641070
1065- # Hessian would go here
1066-
10671071 return fun , gra
10681072
1073+ def _regularize_function_hessian (self , stretch ):
1074+ """Calculate the Hessian for the stretch optimization objective.
1075+
1076+ The Hessian combines the Gauss-Newton curvature from the stretched
1077+ component derivatives, the residual-weighted second derivatives of
1078+ those stretched components, and the quadratic smoothing penalty on
1079+ neighboring stretch factors.
1080+
1081+ Parameters
1082+ ----------
1083+ stretch : ndarray of shape (n_components, n_signals)
1084+ Stretching factors at which to evaluate the objective curvature.
1085+
1086+ Returns
1087+ -------
1088+ ndarray of shape (n_components * n_signals, n_components * n_signals)
1089+ Symmetric Hessian matrix for the flattened stretch variables.
1090+ """
1091+ residuals , d_stretch_comps , dd_stretch_comps = (
1092+ self ._stretch_residual_and_derivatives (stretch )
1093+ )
1094+ n_variables = self .n_components_ * self .n_signals_
1095+ hessian = np .zeros ((n_variables , n_variables ), dtype = float )
1096+
1097+ for signal in range (self .n_signals_ ):
1098+ variable_indices = (
1099+ np .arange (self .n_components_ ) * self .n_signals_ + signal
1100+ )
1101+ d_signal = d_stretch_comps [:, variable_indices ]
1102+ dd_signal = dd_stretch_comps [:, variable_indices ]
1103+ hessian [np .ix_ (variable_indices , variable_indices )] += (
1104+ d_signal .T @ d_signal
1105+ )
1106+ hessian [variable_indices , variable_indices ] += np .sum (
1107+ dd_signal * residuals [:, signal , None ],
1108+ axis = 0 ,
1109+ )
1110+
1111+ smooth_hessian = (
1112+ self ._spline_smooth_operator .T @ self ._spline_smooth_operator
1113+ ).toarray ()
1114+ for comp in range (self .n_components_ ):
1115+ component_slice = slice (
1116+ comp * self .n_signals_ ,
1117+ (comp + 1 ) * self .n_signals_ ,
1118+ )
1119+ hessian [component_slice , component_slice ] += (
1120+ self .rho * smooth_hessian
1121+ )
1122+
1123+ return 0.5 * (hessian + hessian .T )
1124+
10691125 def _update_stretch (self ):
10701126 """Updates stretching matrix using constrained optimization
10711127 (equivalent to fmincon in MATLAB)."""
@@ -1085,6 +1141,30 @@ def objective(stretch_vec):
10851141 gra = gra .flatten ()
10861142 return fun , gra
10871143
1144+ def hessian (stretch_vec ):
1145+ stretch_matrix = stretch_vec .reshape (self .stretch_ .shape )
1146+ return self ._regularize_function_hessian (stretch_matrix )
1147+
1148+ unconstrained_result = minimize (
1149+ fun = lambda stretch_vec : objective (stretch_vec )[0 ],
1150+ x0 = stretch_flat_initial ,
1151+ method = "trust-exact" ,
1152+ jac = lambda stretch_vec : objective (stretch_vec )[1 ],
1153+ hess = hessian ,
1154+ options = {"maxiter" : 300 },
1155+ )
1156+ unconstrained_stretch = unconstrained_result .x .reshape (
1157+ self .stretch_ .shape
1158+ )
1159+ if np .all (unconstrained_stretch >= 0.1 ):
1160+ current_objective = self ._regularize_function (self .stretch_ )[0 ]
1161+ candidate_objective = self ._regularize_function (
1162+ unconstrained_stretch
1163+ )[0 ]
1164+ if candidate_objective <= current_objective :
1165+ self .stretch_ = unconstrained_stretch
1166+ return
1167+
10881168 # Optimization constraints: lower bound 0.1, no upper bound
10891169 bounds = [
10901170 (0.1 , None )
@@ -1096,6 +1176,7 @@ def objective(stretch_vec):
10961176 x0 = stretch_flat_initial ,
10971177 method = "trust-constr" , # Substitute for 'trust-region-reflective'
10981178 jac = lambda stretch_vec : objective (stretch_vec )[1 ], # Gradient
1179+ hess = hessian ,
10991180 bounds = bounds ,
11001181 )
11011182
0 commit comments