@@ -255,241 +255,3 @@ def intermediate(self, alg_mod, iter_count, obj_value, inf_pr, inf_du, mu,
255255 self .iterations = iter_count
256256 self .objective_forward_passed = False
257257 self .constraints_forward_passed = False
258-
259-
260- # TODO: maybe add a cchecker like this to the diff-engine? Or rather do a checker that
261- # uses cvxpy expressions to evaluate values. It will be slower, but will better test
262- # consistency with cvxpy.
263- class DerivativeChecker :
264- """
265- A utility class to verify derivative computations by comparing
266- C-based diff engine results against Python-based evaluations.
267- """
268-
269- def __init__ (self , problem ):
270- """
271- Initialize the derivative checker with a CVXPY problem.
272-
273- Parameters
274- ----------
275- problem : cvxpy.Problem
276- The CVXPY problem to check derivatives for.
277- """
278- from cvxpy .reductions .dnlp2smooth .dnlp2smooth import Dnlp2Smooth
279- from cvxpy .reductions .solvers .nlp_solvers .diff_engine import C_problem
280-
281- self .original_problem = problem
282-
283- # Apply Dnlp2Smooth to get canonicalized problem
284- canon = Dnlp2Smooth ().apply (problem )
285- self .canonicalized_problem = canon [0 ]
286-
287- # Construct the C version
288- print ("Constructing C diff engine problem for derivative checking..." )
289- self .c_problem = C_problem (self .canonicalized_problem )
290- print ("Done constructing C diff engine problem." )
291-
292- # Construct initial point using Bounds functionality
293- self .bounds = Bounds (self .canonicalized_problem )
294- self .x0 = self .bounds .x0
295-
296- # Initialize constraint bounds for checking
297- self .cl = self .bounds .cl
298- self .cu = self .bounds .cu
299-
300- def check_constraint_values (self , x = None ):
301- if x is None :
302- x = self .x0
303-
304- # Evaluate constraints using C implementation
305- c_values = self .c_problem .constraint_forward (x )
306-
307- # Evaluate constraints using Python implementation
308- # First, set variable values
309- x_offset = 0
310- for var in self .canonicalized_problem .variables ():
311- var_size = var .size
312- var .value = x [x_offset :x_offset + var_size ].reshape (var .shape , order = 'F' )
313- x_offset += var_size
314-
315- # Now evaluate each constraint
316- python_values = []
317- for constr in self .canonicalized_problem .constraints :
318- constr_val = constr .expr .value .flatten (order = 'F' )
319- python_values .append (constr_val )
320-
321- python_values = np .hstack (python_values ) if python_values else np .array ([])
322-
323- match = np .allclose (c_values , python_values , rtol = 1e-10 , atol = 1e-10 )
324- return match
325-
326- def check_jacobian (self , x = None , epsilon = 1e-8 ):
327- if x is None :
328- x = self .x0
329-
330- # Get Jacobian from C implementation
331- self .c_problem .init_jacobian ()
332- self .c_problem .init_hessian ()
333- self .c_problem .constraint_forward (x )
334- c_jac_csr = self .c_problem .jacobian ()
335- c_jac_dense = c_jac_csr .toarray ()
336-
337- # Compute numerical Jacobian using central differences
338- n_vars = len (x )
339- n_constraints = len (self .cl )
340- numerical_jac = np .zeros ((n_constraints , n_vars ))
341-
342- # Define constraint function for finite differences
343- def constraint_func (x_eval ):
344- return self .c_problem .constraint_forward (x_eval )
345-
346- # Compute each column using central differences
347- for j in range (n_vars ):
348- x_plus = x .copy ()
349- x_minus = x .copy ()
350- x_plus [j ] += epsilon
351- x_minus [j ] -= epsilon
352-
353- c_plus = constraint_func (x_plus )
354- c_minus = constraint_func (x_minus )
355-
356- numerical_jac [:, j ] = (c_plus - c_minus ) / (2 * epsilon )
357-
358- match = np .allclose (c_jac_dense , numerical_jac , rtol = 1e-4 , atol = 1e-5 )
359- return match
360-
361- def check_hessian (self , x = None , duals = None , obj_factor = 1.0 , epsilon = 1e-8 ):
362- if x is None :
363- x = self .x0
364-
365- if duals is None :
366- duals = np .random .rand (len (self .cl ))
367-
368- # Get Hessian from C implementation
369- self .c_problem .objective_forward (x )
370- self .c_problem .constraint_forward (x )
371- #jac = self.c_problem.jacobian()
372-
373- # must run gradient because for logistic it fills some values
374- self .c_problem .gradient ()
375- c_hess_csr = self .c_problem .hessian (obj_factor , duals )
376-
377- # Convert to full dense matrix (C returns lower triangular)
378- c_hess_coo = c_hess_csr .tocoo ()
379- n_vars = len (x )
380- c_hess_dense = np .zeros ((n_vars , n_vars ))
381-
382- # Fill in the full symmetric matrix from lower triangular
383- for i , j , v in zip (c_hess_coo .row , c_hess_coo .col , c_hess_coo .data ):
384- c_hess_dense [i , j ] = v
385- if i != j :
386- c_hess_dense [j , i ] = v
387-
388- # Compute numerical Hessian using finite differences of the Lagrangian gradient
389- # Lagrangian gradient: ∇L = obj_factor * ∇f + J^T * duals
390- def lagrangian_gradient (x_eval ):
391- self .c_problem .objective_forward (x_eval )
392- grad_f = self .c_problem .gradient ()
393-
394- self .c_problem .constraint_forward (x_eval )
395- jac = self .c_problem .jacobian ()
396-
397- # Lagrangian gradient = obj_factor * grad_f + J^T * duals
398- return obj_factor * grad_f + jac .T @ duals
399-
400- # Compute Hessian via central differences of gradient
401- numerical_hess = np .zeros ((n_vars , n_vars ))
402- for j in range (n_vars ):
403- x_plus = x .copy ()
404- x_minus = x .copy ()
405- x_plus [j ] += epsilon
406- x_minus [j ] -= epsilon
407-
408- grad_plus = lagrangian_gradient (x_plus )
409- grad_minus = lagrangian_gradient (x_minus )
410-
411- numerical_hess [:, j ] = (grad_plus - grad_minus ) / (2 * epsilon )
412-
413- # Symmetrize the numerical Hessian (average with transpose to reduce numerical errors)
414- numerical_hess = (numerical_hess + numerical_hess .T ) / 2
415-
416- match = np .allclose (c_hess_dense , numerical_hess , rtol = 1e-4 , atol = 1e-6 )
417- return match
418-
419- def check_objective_value (self , x = None ):
420- """ Compare objective value from C implementation with Python implementation. """
421- if x is None :
422- x = self .x0
423-
424- # Evaluate objective using C implementation
425- c_obj_value = self .c_problem .objective_forward (x )
426-
427- # Evaluate objective using Python implementation
428- x_offset = 0
429- for var in self .canonicalized_problem .variables ():
430- var_size = var .size
431- var .value = x [x_offset :x_offset + var_size ].reshape (var .shape , order = 'F' )
432- x_offset += var_size
433-
434- python_obj_value = self .canonicalized_problem .objective .expr .value
435-
436- # Compare results
437- match = np .allclose (c_obj_value , python_obj_value , rtol = 1e-10 , atol = 1e-10 )
438-
439- return match
440-
441- def check_gradient (self , x = None , epsilon = 1e-8 ):
442- """ Compare C-based gradient with numerical approximation using finite differences. """
443- if x is None :
444- x = self .x0
445- # Get gradient from C implementation
446- self .c_problem .objective_forward (x )
447- c_grad = self .c_problem .gradient ()
448-
449- # Compute numerical gradient using central differences
450- n_vars = len (x )
451- numerical_grad = np .zeros (n_vars )
452-
453- def objective_func (x_eval ):
454- return self .c_problem .objective_forward (x_eval )
455-
456- # Compute each component using central differences
457- for j in range (n_vars ):
458- x_plus = x .copy ()
459- x_minus = x .copy ()
460- x_plus [j ] += epsilon
461- x_minus [j ] -= epsilon
462-
463- f_plus = objective_func (x_plus )
464- f_minus = objective_func (x_minus )
465-
466- numerical_grad [j ] = (f_plus - f_minus ) / (2 * epsilon )
467-
468- match = np .allclose (c_grad , numerical_grad , rtol = 5 * 1e-3 , atol = 1e-5 )
469- assert (match )
470- return match
471-
472- def run (self , x = None ):
473- """ Run all derivative checks (constraints, Jacobian, and Hessian). """
474-
475- self .c_problem .init_jacobian ()
476- self .c_problem .init_hessian ()
477- objective_result = self .check_objective_value (x )
478- gradient_result = self .check_gradient (x )
479- constraints_result = self .check_constraint_values ()
480- jacobian_result = self .check_jacobian (x )
481- hessian_result = self .check_hessian (x )
482-
483- result = {'objective' : objective_result ,
484- 'gradient' : gradient_result ,
485- 'constraints' : constraints_result ,
486- 'jacobian' : jacobian_result ,
487- 'hessian' : hessian_result }
488-
489- return result
490-
491- def run_and_assert (self , x = None ):
492- """ Run all derivative checks and assert correctness. """
493- results = self .run (x )
494- for key , passed in results .items ():
495- assert passed , f"Derivative check failed for { key } ."
0 commit comments