Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
763e0e4
Integrate DNLP diff engine into NLP solver Oracles class
Transurgeon Jan 11, 2026
d5ff25e
Update TODO tracking for diff engine atom status
Transurgeon Jan 11, 2026
402eefd
added converter to multiply
dance858 Jan 12, 2026
f39baad
Optimize Oracles class: remove redundant forward calls and simplify v…
Transurgeon Jan 12, 2026
66735ce
Update TODO tracking for diff engine atom status
Transurgeon Jan 13, 2026
235fc21
Update TODO tracking: 14/15 test_nlp_solvers tests now passing
Transurgeon Jan 14, 2026
8dd8292
Update TODO with full NLP test suite results
Transurgeon Jan 14, 2026
a778cf6
Clarify MulExpression: matrix @ matrix where both depend on variables
Transurgeon Jan 14, 2026
7f551fa
Update TODO: Prod atom now implemented (13/14 tests pass)
Transurgeon Jan 14, 2026
1d6c446
Add diff_engine integration layer for CVXPY-to-C expression conversion
Transurgeon Jan 15, 2026
3096405
Fix E402 lint errors: combine docstrings
Transurgeon Jan 15, 2026
3bfbdd0
debugging
dance858 Jan 15, 2026
7612021
super nasty buggit statusgit status
dance858 Jan 15, 2026
826468f
equivalent treatment of (n, ) as numpy and cvxpy. Very subtle
dance858 Jan 15, 2026
8bf2d6f
multiply
dance858 Jan 16, 2026
fcdd0a5
converters
dance858 Jan 16, 2026
3748c44
relative entropy converter
dance858 Jan 16, 2026
3849d53
clean up oracle jacobian
dance858 Jan 16, 2026
77824c7
cleaned up jacobian oracle a bit
dance858 Jan 16, 2026
4cdcc66
minor
dance858 Jan 16, 2026
c73e621
cleaned up oracle class
dance858 Jan 16, 2026
554460c
best of fix and added derivative checker class
dance858 Jan 16, 2026
2b39e67
removed hacky logic with many reshapes to handle numpy's weird broadc…
dance858 Jan 17, 2026
4e5ce8b
removed skipping of tests (fixed with new matmul convention)
dance858 Jan 17, 2026
9fad57f
prod converter
dance858 Jan 18, 2026
6c6d88c
prod with axis one
dance858 Jan 18, 2026
ffbb581
matmul
dance858 Jan 19, 2026
698a217
stress_tests_diff_engine/
dance858 Jan 19, 2026
579883e
random initial points
dance858 Jan 19, 2026
fa6be5c
test for sum
dance858 Jan 19, 2026
4abed21
added power flow as a test and started on transpose converter
dance858 Jan 19, 2026
f8ce13b
cleaned up convert_matmul so we take advantage of sparsity
dance858 Jan 20, 2026
c6d2ba1
added test for sparse matrix vector
dance858 Jan 20, 2026
bb15a07
small edit to test
dance858 Jan 20, 2026
95fbb05
cleaned up converter of multiply
dance858 Jan 20, 2026
5753097
clean up quad form converter
dance858 Jan 20, 2026
189eba4
changed name of test
dance858 Jan 20, 2026
d42f9d6
added derivative checker to all tests
dance858 Jan 21, 2026
42e9c02
added hstack in converter and as test
dance858 Jan 21, 2026
f9edf06
trace converter
dance858 Jan 21, 2026
c404c28
transpose converter
dance858 Jan 22, 2026
f8f2dd2
test_affine_matrix_atoms.py
dance858 Jan 22, 2026
7e96256
diag_vec converter and tests
Transurgeon Jan 22, 2026
1d81fb4
Add diff_engine_core as git submodule
Transurgeon Jan 24, 2026
a2fe740
Add diff_engine build infrastructure and CI submodule checkout
Transurgeon Jan 30, 2026
eb26d21
Remove submodules: recursive from workflows that don't need NLP
Transurgeon Jan 30, 2026
ee4d329
Fix PowerApprox support after upstream Power/PowerApprox split
Transurgeon Jan 30, 2026
8181889
Fix Linux build: define _POSIX_C_SOURCE for clock_gettime
Transurgeon Jan 30, 2026
b91920a
Fix p_rational -> p_used and add Approx atom support
Transurgeon Jan 30, 2026
7c84f7a
Add DIFF_ENGINE_VERSION macro for pip builds
Transurgeon Jan 30, 2026
e9841c0
add debug output test-nlp-solvers
dance858 Jan 30, 2026
ee2717b
Thread verbose parameter to C diff engine and remove debug prints
Transurgeon Jan 30, 2026
7300311
Fix import formatting and update diff_engine_core submodule
Transurgeon Jan 30, 2026
dcd1d47
Remove UNO installation from CI to fix Ubuntu stall
Transurgeon Jan 31, 2026
b6d518e
Remove debug artifacts from NLP tests
Transurgeon Jan 31, 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
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ jobs:

steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Set Additional Envs
run: |
echo "PYTHON_SUBVERSION=$(echo $PYTHON_VERSION | cut -c 3-)" >> $GITHUB_ENV
Expand Down Expand Up @@ -114,6 +116,8 @@ jobs:

steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/test_backends.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jobs:
with:
python-version: "3.12"
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Install cvxpy dependencies
run: |
pip install -e .
Expand Down
7 changes: 3 additions & 4 deletions .github/workflows/test_nlp_solvers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ jobs:
with:
python-version: "3.12"
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Set up Conda
uses: conda-incubator/setup-miniconda@v3
with:
Expand All @@ -35,13 +37,10 @@ jobs:
- name: Install cyipopt via conda
run: |
conda install -y -c conda-forge cyipopt
- name: Install uno via pip
run: |
pip install unopy
- name: Install test dependencies
run: |
pip install -e .
pip install pytest hypothesis
- name: Run nlp tests
run: |
pytest cvxpy/tests/nlp_tests/.
pytest -s -v cvxpy/tests/nlp_tests/.
1 change: 0 additions & 1 deletion .github/workflows/test_optional_solvers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ jobs:
with:
python-version: 3.12
enable-cache: true
- uses: actions/checkout@v5
- name: Setup Julia
uses: julia-actions/setup-julia@v2
with:
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "diff_engine_core"]
path = diff_engine_core
url = https://github.com/dance858/DNLP-diff-engine
12 changes: 6 additions & 6 deletions cvxpy/atoms/elementwise/power.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,16 +386,16 @@ def _grad(self, values):

def _verify_hess_vec_args(self):
# we can't compute the hessian if p is not constant and specified
if (self.p_rational is None and self.p.value is None) or \
if (self.p_used is None and self.p.value is None) or \
not isinstance(self.args[0], Variable):
return False

return True

def _hess_vec(self, vec):
""" See the docstring of the hess_vec method of the atom class. """
if self.p_rational is not None:
p = self.p_rational
if self.p_used is not None:
p = self.p_used
elif self.p.value is not None:
p = self.p.value

Expand All @@ -409,7 +409,7 @@ def _hess_vec(self, vec):
return {(x, x): (idxs, idxs, vals)}

def _verify_jacobian_args(self):
if (self.p_rational is None and self.p.value is None):
if (self.p_used is None and self.p.value is None):
return False

if not isinstance(self.args[0], Variable):
Expand All @@ -423,8 +423,8 @@ def _jacobian(self):
entries p * x_i^(p-1). We vectorize matrix expressions, so we flatten the
values in column-major (Fortran) order.
"""
if self.p_rational is not None:
p = self.p_rational
if self.p_used is not None:
p = self.p_used
elif self.p.value is not None:
p = self.p.value

Expand Down
13 changes: 12 additions & 1 deletion cvxpy/problems/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -1285,10 +1285,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 @@ -1610,7 +1621,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
15 changes: 9 additions & 6 deletions cvxpy/reductions/dnlp2smooth/canonicalizers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
"""
from cvxpy.atoms.geo_mean import geo_mean
from cvxpy.atoms.geo_mean import GeoMean, GeoMeanApprox
from cvxpy.atoms.prod import Prod
from cvxpy.atoms.quad_over_lin import quad_over_lin
from cvxpy.atoms.elementwise.exp import exp
Expand All @@ -24,13 +24,13 @@
from cvxpy.atoms.elementwise.kl_div import kl_div
from cvxpy.atoms.elementwise.minimum import minimum
from cvxpy.atoms.elementwise.maximum import maximum
from cvxpy.atoms.elementwise.power import power
from cvxpy.atoms.elementwise.power import Power, PowerApprox
from cvxpy.atoms.elementwise.trig import cos, sin, tan
from cvxpy.atoms.elementwise.hyperbolic import sinh, asinh, tanh, atanh
from cvxpy.atoms.elementwise.huber import huber
from cvxpy.atoms.norm1 import norm1
from cvxpy.atoms.norm_inf import norm_inf
from cvxpy.atoms.pnorm import Pnorm
from cvxpy.atoms.pnorm import Pnorm, PnormApprox
from cvxpy.atoms.sum_largest import sum_largest
from cvxpy.atoms.elementwise.abs import abs
from cvxpy.atoms.max import max
Expand Down Expand Up @@ -77,15 +77,18 @@
tanh: tanh_canon,
atanh: atanh_canon,
quad_over_lin: quad_over_lin_canon,
power: power_canon,
Pnorm : pnorm_canon,
Power: power_canon,
PowerApprox: power_canon,
Pnorm: pnorm_canon,
PnormApprox: pnorm_canon,
DivExpression: div_canon,
entr: entr_canon,
rel_entr: rel_entr_canon,
kl_div: kl_div_canon,
multiply: multiply_canon,
MulExpression: matmul_canon,
geo_mean: geo_mean_canon,
GeoMean: geo_mean_canon,
GeoMeanApprox: geo_mean_canon,
log_sum_exp: log_sum_exp_canon,
Prod: prod_canon,

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
2 changes: 1 addition & 1 deletion cvxpy/reductions/dnlp2smooth/canonicalizers/power_canon.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

def power_canon(expr, args):
x = args[0]
p = expr.p_rational
p = expr.p_used
shape = expr.shape
ones = Constant(np.ones(shape))
if p == 0:
Expand Down
9 changes: 6 additions & 3 deletions cvxpy/reductions/solvers/nlp_solvers/copt_nlpif.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ def solve_via_data(self, data, warm_start: bool, verbose: bool, solver_opts, sol
"""
import coptpy as copt

from cvxpy.reductions.solvers.nlp_solvers.nlp_solver import Oracles

# Create oracles object (deferred from apply() so we have access to verbose)
bounds = data["_bounds"]
oracles = Oracles(bounds.new_problem, bounds.x0, len(bounds.cl), verbose=verbose)

class COPTNlpCallbackCVXPY(copt.NlpCallbackBase):
def __init__(self, oracles, m):
super().__init__()
Expand Down Expand Up @@ -184,9 +190,6 @@ def EvalHess(self, xdata, sigma, lambdata, outdata):
# Pass through verbosity
model.setParam(copt.COPT.Param.Logging, verbose)

# Get oracles for function evaluation
oracles = data['oracles']

# Get the NLP problem data
x0 = data['x0']
lb, ub = data['lb'].copy(), data['ub'].copy()
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",
]
96 changes: 96 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,96 @@
"""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 _diffengine
except ImportError as e:
raise ImportError(
"NLP support requires diff-engine. Rebuild with: pip install -e ."
) 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, verbose: bool = True):
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, verbose)
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