Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions python/atoms/add.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ static PyObject *py_make_add(PyObject *self, PyObject *args)
PyErr_SetString(PyExc_RuntimeError, "failed to create add node");
return NULL;
}
expr_retain(node); /* Capsule owns a reference */
return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor);
}

Expand Down
1 change: 1 addition & 0 deletions python/atoms/constant.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ static PyObject *py_make_constant(PyObject *self, PyObject *args)
PyErr_SetString(PyExc_RuntimeError, "failed to create constant node");
return NULL;
}
expr_retain(node); /* Capsule owns a reference */
return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor);
}

Expand Down
1 change: 1 addition & 0 deletions python/atoms/exp.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ static PyObject *py_make_exp(PyObject *self, PyObject *args)
PyErr_SetString(PyExc_RuntimeError, "failed to create exp node");
return NULL;
}
expr_retain(node); /* Capsule owns a reference */
return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor);
}

Expand Down
1 change: 1 addition & 0 deletions python/atoms/linear.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ static PyObject *py_make_linear(PyObject *self, PyObject *args)
PyErr_SetString(PyExc_RuntimeError, "failed to create linear node");
return NULL;
}
expr_retain(node); /* Capsule owns a reference */
return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor);
}

Expand Down
1 change: 1 addition & 0 deletions python/atoms/log.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ static PyObject *py_make_log(PyObject *self, PyObject *args)
PyErr_SetString(PyExc_RuntimeError, "failed to create log node");
return NULL;
}
expr_retain(node); /* Capsule owns a reference */
return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor);
}

Expand Down
1 change: 1 addition & 0 deletions python/atoms/neg.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ static PyObject *py_make_neg(PyObject *self, PyObject *args)
PyErr_SetString(PyExc_RuntimeError, "failed to create neg node");
return NULL;
}
expr_retain(node); /* Capsule owns a reference */
return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor);
}

Expand Down
1 change: 1 addition & 0 deletions python/atoms/promote.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ static PyObject *py_make_promote(PyObject *self, PyObject *args)
PyErr_SetString(PyExc_RuntimeError, "failed to create promote node");
return NULL;
}
expr_retain(node); /* Capsule owns a reference */
return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor);
}

Expand Down
1 change: 1 addition & 0 deletions python/atoms/sum.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ static PyObject *py_make_sum(PyObject *self, PyObject *args)
PyErr_SetString(PyExc_RuntimeError, "failed to create sum node");
return NULL;
}
expr_retain(node); /* Capsule owns a reference */
return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor);
}

Expand Down
1 change: 1 addition & 0 deletions python/atoms/variable.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ static PyObject *py_make_variable(PyObject *self, PyObject *args)
PyErr_SetString(PyExc_RuntimeError, "failed to create variable node");
return NULL;
}
expr_retain(node); /* Capsule owns a reference */
return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor);
}

Expand Down
3 changes: 3 additions & 0 deletions python/bindings.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
/* Include problem bindings */
#include "problem/constraint_forward.h"
#include "problem/gradient.h"
#include "problem/hessian.h"
#include "problem/init_derivatives.h"
#include "problem/jacobian.h"
#include "problem/make_problem.h"
Expand Down Expand Up @@ -53,6 +54,8 @@ static PyMethodDef DNLPMethods[] = {
"Compute objective gradient"},
{"problem_jacobian", py_problem_jacobian, METH_VARARGS,
"Compute constraint jacobian"},
{"problem_hessian", py_problem_hessian, METH_VARARGS,
"Compute Lagrangian Hessian"},
{NULL, NULL, 0, NULL}};

static struct PyModuleDef dnlp_module = {PyModuleDef_HEAD_INIT, "DNLP_diff_engine",
Expand Down
19 changes: 19 additions & 0 deletions python/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,22 @@ def jacobian(self) -> sparse.csr_matrix:
"""Compute jacobian of constraints. Call constraint_forward first. Returns scipy CSR matrix."""
data, indices, indptr, shape = diffengine.problem_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)
76 changes: 76 additions & 0 deletions python/problem/hessian.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#ifndef PROBLEM_HESSIAN_H
#define PROBLEM_HESSIAN_H

#include "common.h"

/*
* py_problem_hessian: Compute Lagrangian Hessian
*
* Args:
* prob_capsule: PyCapsule containing problem pointer
* obj_factor: Scaling factor for objective Hessian (double)
* lagrange: Array of Lagrange multipliers (numpy array, length = total_constraint_size)
*
* Returns:
* Tuple of (data, indices, indptr, (m, n)) for scipy.sparse.csr_matrix
*/
static PyObject *py_problem_hessian(PyObject *self, PyObject *args)
{
PyObject *prob_capsule;
double obj_factor;
PyObject *lagrange_obj;

if (!PyArg_ParseTuple(args, "OdO", &prob_capsule, &obj_factor, &lagrange_obj))
{
return NULL;
}

problem *prob =
(problem *) PyCapsule_GetPointer(prob_capsule, PROBLEM_CAPSULE_NAME);
if (!prob)
{
PyErr_SetString(PyExc_ValueError, "invalid problem capsule");
return NULL;
}

/* Convert lagrange to contiguous C array */
PyArrayObject *lagrange_arr = (PyArrayObject *) PyArray_FROM_OTF(
lagrange_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY);
if (!lagrange_arr)
{
return NULL;
}

double *lagrange = (double *) PyArray_DATA(lagrange_arr);

/* Compute Hessian */
problem_hessian(prob, obj_factor, lagrange);

Py_DECREF(lagrange_arr);

/* Extract CSR components and return as tuple */
CSR_Matrix *H = prob->lagrange_hessian;
npy_intp nnz = H->nnz;
npy_intp n_plus_1 = H->n + 1;

PyObject *data = PyArray_SimpleNew(1, &nnz, NPY_DOUBLE);
PyObject *indices = PyArray_SimpleNew(1, &nnz, NPY_INT32);
PyObject *indptr = PyArray_SimpleNew(1, &n_plus_1, NPY_INT32);

if (!data || !indices || !indptr)
{
Py_XDECREF(data);
Py_XDECREF(indices);
Py_XDECREF(indptr);
return NULL;
}

/* Copy CSR data using memcpy for efficiency */
memcpy(PyArray_DATA((PyArrayObject *) data), H->x, nnz * sizeof(double));
memcpy(PyArray_DATA((PyArrayObject *) indices), H->i, nnz * sizeof(int));
memcpy(PyArray_DATA((PyArrayObject *) indptr), H->p, n_plus_1 * sizeof(int));

return Py_BuildValue("(OOO(ii))", data, indices, indptr, H->m, H->n);
}

#endif /* PROBLEM_HESSIAN_H */
154 changes: 154 additions & 0 deletions python/tests/test_constrained.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,147 @@ def test_repeated_evaluations():
assert np.allclose(jac2.toarray(), np.diag(-np.exp(u2)))


def test_hessian_objective_only():
"""Test Hessian of sum(log(x)) with no constraints.

For f(x) = sum(log(x)), Hessian is diagonal: d²f/dx_i² = -1/x_i²
"""
x = cp.Variable(3)
obj = cp.sum(cp.log(x))
problem = cp.Problem(cp.Minimize(obj))
prob = C_problem(problem)

u = np.array([1.0, 2.0, 4.0])
prob.init_derivatives()

prob.objective_forward(u)
# No constraints, so pass empty lagrange array
H = prob.hessian(obj_factor=1.0, lagrange=np.array([]))

# Expected: diag([-1/1², -1/2², -1/4²]) = diag([-1, -0.25, -0.0625])
expected_H = np.diag([-1.0, -0.25, -0.0625])
assert np.allclose(H.toarray(), expected_H)


def test_hessian_with_constraint():
"""Test Lagrangian Hessian: sum(log(x)) + lagrange * (-exp(x)).

Objective Hessian: diag([-1/x²])
Constraint (-exp(x)) Hessian: diag([-exp(x)])

Lagrangian = obj + lagrange * constraint
"""
x = cp.Variable(2)
obj = cp.sum(cp.log(x))
constraints = [cp.exp(x) >= 0] # becomes -exp(x) <= 0

problem = cp.Problem(cp.Minimize(obj), constraints)
prob = C_problem(problem)

u = np.array([1.0, 2.0])
prob.init_derivatives()

prob.objective_forward(u)
prob.constraint_forward(u)

# Lagrange multipliers for the constraint (size 2)
lagrange = np.array([0.5, 0.5])
H = prob.hessian(obj_factor=1.0, lagrange=lagrange)

# Constraint expr is -exp(x), so its Hessian is -exp(x)
# H[0,0] = -1/1² + 0.5*(-exp(1))
# H[1,1] = -1/4 + 0.5*(-exp(2))
expected_00 = -1.0 + 0.5 * (-np.exp(1.0))
expected_11 = -0.25 + 0.5 * (-np.exp(2.0))
expected_H = np.diag([expected_00, expected_11])

assert np.allclose(H.toarray(), expected_H)


def test_hessian_obj_factor_zero():
"""Test Hessian with obj_factor=0 (constraint Hessian only)."""
x = cp.Variable(2)
obj = cp.sum(cp.log(x))
constraints = [cp.exp(x) >= 0] # becomes -exp(x) <= 0

problem = cp.Problem(cp.Minimize(obj), constraints)
prob = C_problem(problem)

u = np.array([1.0, 2.0])
prob.init_derivatives()

prob.objective_forward(u)
prob.constraint_forward(u)

# With obj_factor=0, only constraint Hessian contributes
# Constraint is -exp(x), so Hessian is -exp(x)
lagrange = np.array([1.0, 1.0])
H = prob.hessian(obj_factor=0.0, lagrange=lagrange)

expected_H = np.diag([-np.exp(1.0), -np.exp(2.0)])
assert np.allclose(H.toarray(), expected_H)


def test_hessian_two_constraints():
"""Test Hessian with two constraints."""
x = cp.Variable(2)
obj = cp.sum(cp.log(x))
constraints = [
cp.log(x) >= 0, # becomes -log(x) <= 0, Hessian: 1/x²
cp.exp(x) >= 0, # becomes -exp(x) <= 0, Hessian: -exp(x)
]

problem = cp.Problem(cp.Minimize(obj), constraints)
prob = C_problem(problem)

u = np.array([2.0, 3.0])
prob.init_derivatives()

prob.objective_forward(u)
prob.constraint_forward(u)

# Lagrange: [lambda1_1, lambda1_2, lambda2_1, lambda2_2]
# First constraint (-log): 2 elements, second constraint (-exp): 2 elements
lagrange = np.array([1.0, 1.0, 0.5, 0.5])
H = prob.hessian(obj_factor=1.0, lagrange=lagrange)

# Objective Hessian: diag([-1/x²])
# Constraint 1 (-log) Hessian: diag([1/x²]) (second derivative of -log is 1/x²)
# Constraint 2 (-exp) Hessian: diag([-exp(x)])
# Total: (-1/x²) + 1.0*(1/x²) + 0.5*(-exp(x)) = -0.5*exp(x)
expected_00 = -1.0/(2.0**2) + 1.0*(1.0/(2.0**2)) + 0.5*(-np.exp(2.0))
expected_11 = -1.0/(3.0**2) + 1.0*(1.0/(3.0**2)) + 0.5*(-np.exp(3.0))
expected_H = np.diag([expected_00, expected_11])

assert np.allclose(H.toarray(), expected_H)


def test_hessian_repeated_evaluations():
"""Test Hessian with repeated evaluations at different points."""
x = cp.Variable(2)
obj = cp.sum(cp.log(x))
problem = cp.Problem(cp.Minimize(obj))
prob = C_problem(problem)

prob.init_derivatives()

# First evaluation
u1 = np.array([1.0, 2.0])
prob.objective_forward(u1)
H1 = prob.hessian(obj_factor=1.0, lagrange=np.array([]))

# Second evaluation at different point
u2 = np.array([2.0, 4.0])
prob.objective_forward(u2)
H2 = prob.hessian(obj_factor=1.0, lagrange=np.array([]))

expected_H1 = np.diag([-1.0, -0.25])
expected_H2 = np.diag([-0.25, -0.0625])

assert np.allclose(H1.toarray(), expected_H1)
assert np.allclose(H2.toarray(), expected_H2)


if __name__ == "__main__":
test_single_constraint()
print("test_single_constraint passed!")
Expand All @@ -199,4 +340,17 @@ def test_repeated_evaluations():
print("test_larger_scale passed!")
test_repeated_evaluations()
print("test_repeated_evaluations passed!")

# Hessian tests
test_hessian_objective_only()
print("test_hessian_objective_only passed!")
test_hessian_with_constraint()
print("test_hessian_with_constraint passed!")
test_hessian_obj_factor_zero()
print("test_hessian_obj_factor_zero passed!")
test_hessian_two_constraints()
print("test_hessian_two_constraints passed!")
test_hessian_repeated_evaluations()
print("test_hessian_repeated_evaluations passed!")

print("\nAll constrained tests passed!")
15 changes: 15 additions & 0 deletions src/affine/constant.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ static void eval_jacobian(expr *node)
(void) node;
}

static void wsum_hess_init(expr *node)
{
/* Constant Hessian is all zeros: n_vars x n_vars with 0 nonzeros. */
node->wsum_hess = new_csr_matrix(node->n_vars, node->n_vars, 0);
}

static void eval_wsum_hess(expr *node, const double *w)
{
/* Constant Hessian is always zero - nothing to compute */
(void) node;
(void) w;
}

static bool is_affine(const expr *node)
{
(void) node;
Expand All @@ -35,6 +48,8 @@ expr *new_constant(int d1, int d2, int n_vars, const double *values)
node->is_affine = is_affine;
node->jacobian_init = jacobian_init;
node->eval_jacobian = eval_jacobian;
node->wsum_hess_init = wsum_hess_init;
node->eval_wsum_hess = eval_wsum_hess;

return node;
}
Loading