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
103 changes: 103 additions & 0 deletions src/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,48 @@


class Constraints:
"""
A class to define and manage constraints for an optimization problem.

This class allows users to define different types of constraints such as
budget, box, linear, and L1 constraints, and then store and manipulate them.

Attributes:
selection (str or list of str): A vector of selection criteria, typically representing
decision variables.
budget (dict): Contains budget-related constraints with keys 'Amat', 'sense', and 'rhs'.
box (dict): Contains box constraints, including 'box_type', 'lower', and 'upper'.
linear (dict): Contains linear constraints with keys 'Amat', 'sense', and 'rhs'.
l1 (dict): Contains L1 regularization constraints with additional user-defined parameters.

Methods:
__init__(self, selection="NA"):
Initializes the Constraints object.
__str__(self):
Provides a string representation of the current constraints.
add_budget(self, rhs=1, sense='='):
Adds a budget constraint to the object.
add_box(self, box_type="LongOnly", lower=None, upper=None):
Adds box constraints, including lower and upper bounds for the selection.
add_linear(self, Amat=None, a_values=None, sense='=', rhs=None, name=None):
Adds linear constraints with a matrix, sense, and right-hand side values.
add_l1(self, name, rhs=None, x0=None, *args, **kwargs):
Adds L1 regularization constraints with a name and user-defined parameters.
to_GhAb(self, lbub_to_G=False):
Converts the constraints to matrices suitable for optimization solvers.
"""

def __init__(self, selection="NA") -> None:
"""
Initializes a Constraints object with the provided selection criteria.

Args:
selection (str or list of str): A vector of decision variables.
If a list is provided, it must consist of string items.

Raises:
ValueError: If any item in the selection is not a string.
"""
if not all(isinstance(item, str) for item in selection):
raise ValueError("argument 'selection' has to be a character vector.")

Expand All @@ -34,9 +74,25 @@ def __init__(self, selection="NA") -> None:
return None

def __str__(self) -> str:
"""
Provides a string representation of the current constraints.

Returns:
str: A formatted string displaying the constraints defined for the object.
"""
return ' '.join(f'\n{key}:\n\n{vars(self)[key]}\n' for key in vars(self).keys())

def add_budget(self, rhs=1, sense='=') -> None:
"""
Adds a budget constraint to the object.

Args:
rhs (int or float): The right-hand side value of the budget constraint. Default is 1.
sense (str): The type of inequality ('=' or other). Default is '='.

Raises:
Warning: If the budget is being overwritten.
"""
if self.budget.get('rhs') is not None:
warnings.warn("Existing budget constraint is overwritten\n")

Expand All @@ -50,6 +106,17 @@ def add_box(self,
box_type="LongOnly",
lower=None,
upper=None) -> None:
"""
Adds box constraints, including lower and upper bounds for the selection.

Args:
box_type (str): The type of box constraint. Default is "LongOnly".
lower (float or pd.Series): The lower bound of the box constraint.
upper (float or pd.Series): The upper bound of the box constraint.

Raises:
ValueError: If any lower bound is higher than the corresponding upper bound.
"""
boxcon = box_constraint(box_type, lower, upper)

if np.isscalar(boxcon['lower']):
Expand All @@ -69,6 +136,19 @@ def add_linear(self,
sense: str = '=',
rhs=None,
name: str = None) -> None:
"""
Adds linear constraints with a matrix, sense, and right-hand side values.

Args:
Amat (pd.DataFrame, optional): The matrix of coefficients for the linear constraints.
a_values (pd.Series, optional): The coefficients as a series if Amat is not provided.
sense (str or pd.Series): The inequality type ('=', '<=', '>=').
rhs (int, float, or pd.Series): The right-hand side values for the constraints.
name (str, optional): The name of the constraint matrix.

Raises:
ValueError: If neither Amat nor a_values is provided.
"""
if Amat is None:
if a_values is None:
raise ValueError("Either 'Amat' or 'a_values' must be provided.")
Expand Down Expand Up @@ -99,6 +179,19 @@ def add_l1(self,
rhs=None,
x0=None,
*args, **kwargs) -> None:
"""
Adds L1 regularization constraints with a name and user-defined parameters.

Args:
name (str): The name of the L1 constraint.
rhs (int or float): The right-hand side value for the L1 constraint.
x0 (optional): The initial value for the constraint.
*args: Additional arguments to be passed.
**kwargs: Additional keyword arguments to be passed.

Raises:
TypeError: If rhs is not provided.
"""
if rhs is None:
raise TypeError("argument 'rhs' is required.")
con = {'rhs': rhs}
Expand All @@ -112,6 +205,16 @@ def add_l1(self,
return None

def to_GhAb(self, lbub_to_G: bool = False) -> Dict[str, pd.DataFrame]:
"""
Converts the constraints to matrices suitable for optimization solvers.

Args:
lbub_to_G (bool): If True, box constraints are converted to inequality constraints in G and h.
Default is False.

Returns:
dict: A dictionary containing matrices for inequality (G, h) and equality (A, b) constraints.
"""
A = None
b = None
G = None
Expand Down
81 changes: 75 additions & 6 deletions test/tests_quadratic_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,83 @@
import qpsolvers
from typing import Tuple
import time
import pytest
sys.path.insert(1, 'src')

from helper_functions import to_numpy
from data_loader import load_data_msci
from constraints import Constraints
from covariance import Covariance
from optimization import *
from optimization_data import OptimizationData
from src.helper_functions import to_numpy
from src.data_loader import load_data_msci
from src.constraints import Constraints
from src.covariance import Covariance
from src.optimization import *
from src.optimization_data import OptimizationData

"""
* unit Test class for Constraints class, reach a 100% coverage in this class
"""
class TestConstraintsMethods(unittest.TestCase):
def test_add_constaints_init(self):
"""
Test the successful initialization and basic functionality of the Constraints class.
"""
# Test with valid input
selection = ["A", "B", "C"]
constraints = Constraints(selection=selection)

# Test if selection is set correctly
assert constraints.selection == selection, "Selection should be correctly initialized"

# Test initial values for budget, box, linear, and l1
assert constraints.budget == {'Amat': None, 'sense': None, 'rhs': None}, "Initial budget should be empty"
assert constraints.box == {'box_type': 'NA', 'lower': None, 'upper': None}, "Initial box should be empty"
assert constraints.linear == {'Amat': None, 'sense': None, 'rhs': None}, "Initial linear constraints should be empty"
assert constraints.l1 == {}, "Initial l1 should be empty"


def test_add_constraints_init_error(self):
"""
Test for handling errors during the initialization of the Constraints class.
"""
# Test with invalid input for selection (non-string elements in selection)
with pytest.raises(ValueError):
Constraints(selection=[1, 2, 3]) # Should raise ValueError because selection is not all strings

# Test with empty selection
with pytest.raises(ValueError):
Constraints(selection=[]) # Should raise ValueError because empty selection is invalid


def test_add_budget(self):
pass # Placeholder for future tests

def test_add_budget_value_error(self):
pass

def test_add_box(self):
pass

def test_add_box_value_error(self):
pass

def test_add_linear(self):
pass

def test_add_linear_value_error(self):
pass

def test_add_l1(self):
pass

def test_add_l1_type_error(self):
pass

def to_GhAb(self):
pass

def to_GhAb_sense_G_not_None(self):
pass

def to_GhAb_idx__sum_negative(self):
pass



Expand Down