diff --git a/src/constraints.py b/src/constraints.py index dc2b21c..a12a256 100644 --- a/src/constraints.py +++ b/src/constraints.py @@ -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.") @@ -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") @@ -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']): @@ -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.") @@ -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} @@ -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 diff --git a/test/tests_quadratic_program.py b/test/tests_quadratic_program.py index fecd08f..b966b67 100644 --- a/test/tests_quadratic_program.py +++ b/test/tests_quadratic_program.py @@ -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