Skip to content
Open
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
70 changes: 44 additions & 26 deletions cvxpy/problems/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""
from __future__ import annotations

import contextlib
import time
import warnings
from collections import namedtuple
Expand Down Expand Up @@ -56,6 +57,7 @@
from cvxpy.utilities import debug_tools
from cvxpy.utilities.citations import CITATION_DICT
from cvxpy.utilities.deterministic import unique_list
from cvxpy.utilities.scopes import ignore_nan_scope

SolveResult = namedtuple(
'SolveResult',
Expand Down Expand Up @@ -794,6 +796,12 @@ def get_problem_data(
for reduction in solving_chain.reductions:
reduction.update_parameters(self)

# Set ignore_nan flag on param_prog if specified in solver_opts
if solver_opts and solver_opts.get('ignore_nan', False):
self._cache.param_prog.ignore_nan = True
else:
self._cache.param_prog.ignore_nan = False

data, solver_inverse_data = solving_chain.solver.apply(
self._cache.param_prog)
inverse_data = self._cache.inverse_data + [solver_inverse_data]
Expand Down Expand Up @@ -828,6 +836,9 @@ def get_problem_data(
'(Subsequent compilations of this problem, using the '
'same arguments, should ' 'take less time.)')
self._cache.param_prog = data[s.PARAM_PROB]
# Set ignore_nan flag on param_prog if specified in solver_opts
if solver_opts and solver_opts.get('ignore_nan', False):
self._cache.param_prog.ignore_nan = True
Comment on lines +839 to +841
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a redundancy in how ignore_nan is handled. The code sets self._cache.param_prog.ignore_nan both in lines 800-803 and 839-841. Additionally, the ignore_nan_scope context manager is being used alongside these flag settings, creating two overlapping mechanisms. This dual approach is confusing - either use the context manager consistently or use the flag consistently, but mixing both adds unnecessary complexity.

Suggested change
# Set ignore_nan flag on param_prog if specified in solver_opts
if solver_opts and solver_opts.get('ignore_nan', False):
self._cache.param_prog.ignore_nan = True

Copilot uses AI. Check for mistakes.
# the last datum in inverse_data corresponds to the solver,
# so we shouldn't cache it
self._cache.inverse_data = inverse_data[:-1]
Expand Down Expand Up @@ -1198,35 +1209,42 @@ def _solve(self,
self.unpack(chain.retrieve(soln))
return self.value

data, solving_chain, inverse_data = self.get_problem_data(
solver, gp, enforce_dpp, ignore_dpp, verbose, canon_backend, kwargs
# Use ignore_nan_scope if ignore_nan is specified in solver options
nan_scope = (
ignore_nan_scope() if kwargs.get('ignore_nan', False)
else contextlib.nullcontext()
)

if verbose:
print(_NUM_SOLVER_STR)
s.LOGGER.info(
'Invoking solver %s to obtain a solution.',
solving_chain.reductions[-1].name())
start = time.time()
solver_verbose = kwargs.pop('solver_verbose', verbose)
if solver_verbose and (not verbose):
print(_NUM_SOLVER_STR)
if verbose and bibtex:
print(_CITATION_STR)

# Cite CVXPY papers.
print(CITATION_DICT["CVXPY"])
with nan_scope:
data, solving_chain, inverse_data = self.get_problem_data(
solver, gp, enforce_dpp, ignore_dpp, verbose, canon_backend, kwargs
)

# Cite problem grammar.
if self.is_dcp():
print(CITATION_DICT["DCP"])
if gp:
print(CITATION_DICT["DGP"])

# Cite solver.
print(solving_chain.reductions[-1].cite(data))
solution = solving_chain.solve_via_data(
self, data, warm_start, solver_verbose, kwargs)
if verbose:
print(_NUM_SOLVER_STR)
s.LOGGER.info(
'Invoking solver %s to obtain a solution.',
solving_chain.reductions[-1].name())
start = time.time()
solver_verbose = kwargs.pop('solver_verbose', verbose)
if solver_verbose and (not verbose):
print(_NUM_SOLVER_STR)
if verbose and bibtex:
print(_CITATION_STR)

# Cite CVXPY papers.
print(CITATION_DICT["CVXPY"])

# Cite problem grammar.
if self.is_dcp():
print(CITATION_DICT["DCP"])
if gp:
print(CITATION_DICT["DGP"])

# Cite solver.
print(solving_chain.reductions[-1].cite(data))
solution = solving_chain.solve_via_data(
self, data, warm_start, solver_verbose, kwargs)
Comment on lines +1223 to +1247
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The solve_via_data call should be outside the ignore_nan_scope context manager. Currently, the scope exits at line 1248, which means the actual solving happens inside the scope. However, the validation that the scope is meant to bypass only happens during get_problem_data (specifically in apply_parameters). Keeping the solver call inside the scope unnecessarily extends its lifetime and could mask validation issues in other parts of the solving process.

Suggested change
if verbose:
print(_NUM_SOLVER_STR)
s.LOGGER.info(
'Invoking solver %s to obtain a solution.',
solving_chain.reductions[-1].name())
start = time.time()
solver_verbose = kwargs.pop('solver_verbose', verbose)
if solver_verbose and (not verbose):
print(_NUM_SOLVER_STR)
if verbose and bibtex:
print(_CITATION_STR)
# Cite CVXPY papers.
print(CITATION_DICT["CVXPY"])
# Cite problem grammar.
if self.is_dcp():
print(CITATION_DICT["DCP"])
if gp:
print(CITATION_DICT["DGP"])
# Cite solver.
print(solving_chain.reductions[-1].cite(data))
solution = solving_chain.solve_via_data(
self, data, warm_start, solver_verbose, kwargs)
if verbose:
print(_NUM_SOLVER_STR)
s.LOGGER.info(
'Invoking solver %s to obtain a solution.',
solving_chain.reductions[-1].name())
start = time.time()
solver_verbose = kwargs.pop('solver_verbose', verbose)
if solver_verbose and (not verbose):
print(_NUM_SOLVER_STR)
if verbose and bibtex:
print(_CITATION_STR)
# Cite CVXPY papers.
print(CITATION_DICT["CVXPY"])
# Cite problem grammar.
if self.is_dcp():
print(CITATION_DICT["DCP"])
if gp:
print(CITATION_DICT["DGP"])
# Cite solver.
print(solving_chain.reductions[-1].cite(data))
solution = solving_chain.solve_via_data(
self, data, warm_start, solver_verbose, kwargs)

Copilot uses AI. Check for mistakes.
end = time.time()
self._solve_time = end - start
self.unpack_results(solution, solving_chain, inverse_data)
Expand Down
16 changes: 10 additions & 6 deletions cvxpy/reductions/dcp2cone/cone_matrix_stuffing.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
nonpos2nonneg,
)
from cvxpy.utilities.coeff_extractor import CoeffExtractor
from cvxpy.utilities.scopes import ignore_nan_scope_active


class ConeDims:
Expand Down Expand Up @@ -192,14 +193,16 @@ def __init__(self, q, x, A,
# whether this param cone prog has been formatted for a solver
self.formatted = formatted

# Flag to skip NaN/Inf validation (set via solver_opts['ignore_nan'])
self.ignore_nan = False

def is_mixed_integer(self) -> bool:
"""Is the problem mixed-integer?"""
return self.x.attributes['boolean'] or \
self.x.attributes['integer']

def apply_parameters(self, id_to_param_value=None, zero_offset: bool = False,
keep_zeros: bool = False, quad_obj: bool = False,
ignore_unknown_params: bool = False):
keep_zeros: bool = False, quad_obj: bool = False):
"""Returns A, b after applying parameters (and reshaping).

Args:
Expand All @@ -209,7 +212,6 @@ def apply_parameters(self, id_to_param_value=None, zero_offset: bool = False,
keep_zeros: (optional) if True, store explicit zeros in A where
parameters are affected.
quad_obj: (optional) if True, include quadratic objective term.
ignore_unknown_params: (optional) if True, skip NaN/Inf validation.
"""
self.reduced_A.cache(keep_zeros)

Expand All @@ -230,10 +232,12 @@ def param_value(idx):
b = np.atleast_1d(b)

# Validate for NaN/Inf unless:
# 1. explicitly ignored via ignore_unknown_params, or
# 2. any parameter values are None (DPP compilation without values)
# 1. self.ignore_nan is True (set via solver_opts), or
# 2. ignore_nan_scope is active (context manager), or
# 3. any parameter values are None (DPP compilation without values)
should_validate = (
not ignore_unknown_params
not self.ignore_nan
and not ignore_nan_scope_active()
and all(p.value is not None for p in self.parameters)
)
if should_validate:
Expand Down
107 changes: 107 additions & 0 deletions cvxpy/tests/test_solver_data_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,110 @@ def test_qp_solver_with_inf(self):
if cp.OSQP in cp.installed_solvers():
with pytest.raises(ValueError, match="contains NaN or Inf"):
prob.solve(solver=cp.OSQP)

def test_ignore_nan_option(self):
"""Test that ignore_nan=True skips validation."""
x = cp.Variable()
# Create a problem with inf in constraint
prob = cp.Problem(cp.Minimize(x), [x >= cp.Constant(np.inf)])

if cp.SCS in cp.installed_solvers():
# With ignore_nan=True, no ValueError should be raised during
# apply_parameters. The solver may still fail or return
# a non-optimal status, but that's expected behavior.
try:
prob.solve(solver=cp.SCS, ignore_nan=True)
# The solver may return inf, nan, or fail gracefully
# We just verify no ValueError was raised by our validation
except ValueError as e:
if "contains NaN or Inf" in str(e):
pytest.fail("ignore_nan=True should skip validation")
# Other ValueErrors from the solver itself are acceptable
except Exception:
# Solver-level exceptions are acceptable when data contains inf
pass

def test_neg_inf_in_constraint(self):
"""Negative infinity in constraint should raise ValueError."""
x = cp.Variable()
prob = cp.Problem(cp.Minimize(x), [x <= cp.Constant(-np.inf)])

if cp.SCS in cp.installed_solvers():
with pytest.raises(ValueError, match="contains NaN or Inf"):
prob.solve(solver=cp.SCS)

def test_nan_in_matrix_constraint(self):
"""NaN in matrix constraint data should raise ValueError."""
X = cp.Variable((2, 2))
A = np.array([[1, np.nan], [0, 1]])
prob = cp.Problem(cp.Minimize(cp.sum(X)), [X == A])

if cp.SCS in cp.installed_solvers():
with pytest.raises(ValueError, match="contains NaN or Inf"):
prob.solve(solver=cp.SCS)

def test_inf_in_quadratic_objective(self):
"""Inf coefficient in quadratic objective should raise ValueError."""
x = cp.Variable()
# Create quadratic form with inf coefficient
prob = cp.Problem(cp.Minimize(cp.Constant(np.inf) * x**2), [x >= 0])

if cp.OSQP in cp.installed_solvers():
with pytest.raises(ValueError, match="contains NaN or Inf"):
prob.solve(solver=cp.OSQP)

def test_inf_from_expression_overflow(self):
"""Inf resulting from expression overflow should raise ValueError."""
x = cp.Variable()
# Very large values that will overflow to inf when combined
large_val = 1e308
prob = cp.Problem(cp.Minimize(x), [x >= large_val * 2])

if cp.SCS in cp.installed_solvers():
with pytest.raises(ValueError, match="contains NaN or Inf"):
prob.solve(solver=cp.SCS)

def test_nan_in_socp_constraint(self):
"""NaN in second-order cone constraint should raise ValueError."""
x = cp.Variable(2)
t = cp.Variable()
A = np.array([[1, np.nan], [0, 1]])
prob = cp.Problem(cp.Minimize(t), [cp.norm(A @ x) <= t])

if cp.SCS in cp.installed_solvers():
with pytest.raises(ValueError, match="contains NaN or Inf"):
prob.solve(solver=cp.SCS)

def test_inf_in_equality_constraint(self):
"""Inf in equality constraint should raise ValueError."""
x = cp.Variable()
prob = cp.Problem(cp.Minimize(x**2), [x == cp.Constant(np.inf)])

if cp.SCS in cp.installed_solvers():
with pytest.raises(ValueError, match="contains NaN or Inf"):
prob.solve(solver=cp.SCS)

def test_multiple_solves_with_and_without_ignore_nan(self):
"""Test that ignore_nan doesn't persist across solves."""
x = cp.Variable()
prob = cp.Problem(cp.Minimize(x), [x >= cp.Constant(np.inf)])

if cp.SCS in cp.installed_solvers():
# First solve with ignore_nan=True - should not raise our error
try:
prob.solve(solver=cp.SCS, ignore_nan=True)
except Exception:
pass # Solver errors are expected

# Second solve without ignore_nan - SHOULD raise our error
with pytest.raises(ValueError, match="contains NaN or Inf"):
prob.solve(solver=cp.SCS)

def test_clarabel_with_inf(self):
"""Test that Clarabel also catches Inf in problem data."""
x = cp.Variable()
prob = cp.Problem(cp.Minimize(x), [x >= cp.Constant(np.inf)])

if cp.CLARABEL in cp.installed_solvers():
with pytest.raises(ValueError, match="contains NaN or Inf"):
prob.solve(solver=cp.CLARABEL)
28 changes: 28 additions & 0 deletions cvxpy/utilities/scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from typing import Generator

_dpp_scope_active = False
_ignore_nan_scope_active = False


@contextlib.contextmanager
Expand Down Expand Up @@ -47,3 +48,30 @@ def dpp_scope() -> Generator[None, None, None]:
def dpp_scope_active() -> bool:
"""Returns True if a `dpp_scope` is active. """
return _dpp_scope_active


@contextlib.contextmanager
def ignore_nan_scope() -> Generator[None, None, None]:
"""Context manager to disable NaN/Inf validation during problem solving.

When this scope is active, `ParamConeProg.apply_parameters()` will not
raise an error if problem data contains NaN or Inf values. This is useful
when solving problems where NaN/Inf values are intentional (e.g., for
testing solver robustness).

Example:
with ignore_nan_scope():
problem.solve() # No error even if data contains NaN/Inf
"""
global _ignore_nan_scope_active
prev_state = _ignore_nan_scope_active
_ignore_nan_scope_active = True
try:
yield
finally:
_ignore_nan_scope_active = prev_state
Comment on lines +69 to +72
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dpp_scope context manager does not use a try/finally block to restore the previous state, but ignore_nan_scope does. For consistency and robustness, both should use the same pattern. The try/finally pattern in ignore_nan_scope is better as it ensures the state is restored even if an exception occurs.

Copilot uses AI. Check for mistakes.


def ignore_nan_scope_active() -> bool:
"""Returns True if an `ignore_nan_scope` is active."""
return _ignore_nan_scope_active