Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
411a294
Integrate DNLP diff engine into NLP solver Oracles class
Transurgeon Jan 11, 2026
fdc797e
Update TODO tracking for diff engine atom status
Transurgeon Jan 11, 2026
e8f2f91
added converter to multiply
dance858 Jan 12, 2026
7bc6b76
Optimize Oracles class: remove redundant forward calls and simplify v…
Transurgeon Jan 12, 2026
0382a55
Update TODO tracking for diff engine atom status
Transurgeon Jan 13, 2026
780a1b4
Update TODO tracking: 14/15 test_nlp_solvers tests now passing
Transurgeon Jan 14, 2026
7f88d83
Update TODO with full NLP test suite results
Transurgeon Jan 14, 2026
5fd79b2
Clarify MulExpression: matrix @ matrix where both depend on variables
Transurgeon Jan 14, 2026
bd696aa
Update TODO: Prod atom now implemented (13/14 tests pass)
Transurgeon Jan 14, 2026
530bb79
Add diff_engine integration layer for CVXPY-to-C expression conversion
Transurgeon Jan 15, 2026
0da23b5
Fix E402 lint errors: combine docstrings
Transurgeon Jan 15, 2026
bac71da
debugging
dance858 Jan 15, 2026
78c9e44
super nasty buggit statusgit status
dance858 Jan 15, 2026
e129758
equivalent treatment of (n, ) as numpy and cvxpy. Very subtle
dance858 Jan 15, 2026
7310fd0
multiply
dance858 Jan 16, 2026
bc7abc5
converters
dance858 Jan 16, 2026
d43570f
relative entropy converter
dance858 Jan 16, 2026
5b3c073
clean up oracle jacobian
dance858 Jan 16, 2026
452c44e
cleaned up jacobian oracle a bit
dance858 Jan 16, 2026
3bd8b4c
minor
dance858 Jan 16, 2026
fb292d3
cleaned up oracle class
dance858 Jan 16, 2026
6d98f03
best of fix and added derivative checker class
dance858 Jan 16, 2026
e3a87d5
removed hacky logic with many reshapes to handle numpy's weird broadc…
dance858 Jan 17, 2026
932466d
removed skipping of tests (fixed with new matmul convention)
dance858 Jan 17, 2026
b1196b7
prod converter
dance858 Jan 18, 2026
c445413
prod with axis one
dance858 Jan 18, 2026
0649032
matmul
dance858 Jan 19, 2026
f1e20ca
stress_tests_diff_engine/
dance858 Jan 19, 2026
a5939d9
random initial points
dance858 Jan 19, 2026
8f657cd
test for sum
dance858 Jan 19, 2026
d19a9ec
added power flow as a test and started on transpose converter
dance858 Jan 19, 2026
698b4f4
cleaned up convert_matmul so we take advantage of sparsity
dance858 Jan 20, 2026
1478159
added test for sparse matrix vector
dance858 Jan 20, 2026
e3d3d76
small edit to test
dance858 Jan 20, 2026
a831303
cleaned up converter of multiply
dance858 Jan 20, 2026
a42b587
clean up quad form converter
dance858 Jan 20, 2026
5b96f19
changed name of test
dance858 Jan 20, 2026
87a678a
added derivative checker to all tests
dance858 Jan 21, 2026
2ed0cd0
added hstack in converter and as test
dance858 Jan 21, 2026
9773c05
trace converter
dance858 Jan 21, 2026
18b71fc
transpose converter
dance858 Jan 22, 2026
cc1ea55
test_affine_matrix_atoms.py
dance858 Jan 22, 2026
88aa75c
diag_vec converter and tests
Transurgeon Jan 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion cvxpy/problems/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -1268,10 +1268,21 @@ def _solve(self,
canon_problem, inverse_data = nlp_chain.apply(problem=self)
solution = nlp_chain.solver.solve_via_data(canon_problem, warm_start,
verbose, solver_opts=kwargs)

# This gives the objective value of the C problem
# which can be slightly different from the original NLP
# so we use the below approach with unpacking. Preferably
# we would have a way to do this without unpacking.
#obj_value = canon_problem['objective'](solution['x'])

# set cvxpy variable
self.unpack_results(solution, nlp_chain, inverse_data)
obj_value = self.objective.value

all_objs[run] = obj_value
if obj_value < best_obj:
best_obj = obj_value
print("best_obj: ", best_obj)
best_solution = solution

# unpack best solution
Expand Down Expand Up @@ -1593,7 +1604,7 @@ def unpack(self, solution) -> None:
def unpack_results(self, solution, chain: SolvingChain, inverse_data) -> None:
"""Updates the problem state given the solver results.

Updates problem.status, problem.value and value of
Updates problem.status, problem.value and value ofro
primal and dual variables.

Arguments
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def log_sum_exp_canon(expr, args):

if x.value is not None:
t.value = expr.numeric(x.value)
v.value = x.value - t.value
v.value = np.minimum(x.value - t.value, -1)
else:
t.value = np.ones(expr.shape)
v.value = -np.ones(x.shape)
Expand Down
38 changes: 38 additions & 0 deletions cvxpy/reductions/solvers/nlp_solvers/diff_engine/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
Copyright 2025, the CVXPY developers

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

"""
CVXPY integration layer for the DNLP diff engine.

This module converts CVXPY expressions to C expression trees
for automatic differentiation.
"""

from cvxpy.reductions.solvers.nlp_solvers.diff_engine.c_problem import C_problem
from cvxpy.reductions.solvers.nlp_solvers.diff_engine.converters import (
ATOM_CONVERTERS,
build_variable_dict,
convert_expr,
convert_expressions,
)

__all__ = [
"C_problem",
"ATOM_CONVERTERS",
"build_variable_dict",
"convert_expr",
"convert_expressions",
]
97 changes: 97 additions & 0 deletions cvxpy/reductions/solvers/nlp_solvers/diff_engine/c_problem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Wrapper around C problem struct for CVXPY problems.

Copyright 2025, the CVXPY developers

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

import numpy as np
from scipy import sparse

import cvxpy as cp

# Import the low-level C bindings
try:
import dnlp_diff_engine as _diffengine
except ImportError as e:
raise ImportError(
"dnlp-diff-engine is required for NLP solving. "
"Install with: pip install dnlp-diff-engine"
) from e

from cvxpy.reductions.solvers.nlp_solvers.diff_engine.converters import (
build_variable_dict,
convert_expr,
)


class C_problem:
"""Wrapper around C problem struct for CVXPY problems."""

def __init__(self, cvxpy_problem: cp.Problem):
var_dict, n_vars = build_variable_dict(cvxpy_problem.variables())
c_obj = convert_expr(cvxpy_problem.objective.expr, var_dict, n_vars)
c_constraints = [convert_expr(c.expr, var_dict, n_vars) for c in cvxpy_problem.constraints]
self._capsule = _diffengine.make_problem(c_obj, c_constraints)
self._allocated = False

def init_derivatives(self):
"""Initialize derivative structures. Must be called before forward/gradient/jacobian."""
_diffengine.problem_init_derivatives(self._capsule)
self._allocated = True

def objective_forward(self, u: np.ndarray) -> float:
"""Evaluate objective. Returns obj_value float."""
return _diffengine.problem_objective_forward(self._capsule, u)

def constraint_forward(self, u: np.ndarray) -> np.ndarray:
"""Evaluate constraints only. Returns constraint_values array."""
return _diffengine.problem_constraint_forward(self._capsule, u)

def gradient(self) -> np.ndarray:
"""Compute gradient of objective. Call objective_forward first. Returns gradient array."""
return _diffengine.problem_gradient(self._capsule)

def jacobian(self) -> sparse.csr_matrix:
"""Compute constraint Jacobian. Call constraint_forward first."""
data, indices, indptr, shape = _diffengine.problem_jacobian(self._capsule)
return sparse.csr_matrix((data, indices, indptr), shape=shape)

def get_jacobian(self) -> sparse.csr_matrix:
"""Get constraint Jacobian. This function does not evaluate the jacobian. """
data, indices, indptr, shape = _diffengine.get_jacobian(self._capsule)
return sparse.csr_matrix((data, indices, indptr), shape=shape)

def hessian(self, obj_factor: float, lagrange: np.ndarray) -> sparse.csr_matrix:
"""Compute Lagrangian Hessian.

Computes: obj_factor * H_obj + sum(lagrange_i * H_constraint_i)

Call objective_forward and constraint_forward before this.

Args:
obj_factor: Weight for objective Hessian
lagrange: Array of Lagrange multipliers (length = total_constraint_size)

Returns:
scipy CSR matrix of shape (n_vars, n_vars)
"""
data, indices, indptr, shape = _diffengine.problem_hessian(
self._capsule, obj_factor, lagrange
)
return sparse.csr_matrix((data, indices, indptr), shape=shape)

def get_hessian(self) -> sparse.csr_matrix:
"""Get Lagrangian Hessian. This function does not evaluate the hessian."""
data, indices, indptr, shape = _diffengine.get_hessian(self._capsule)
return sparse.csr_matrix((data, indices, indptr), shape=shape)
Loading
Loading