Coverage for src/cvx/core/bounds.py: 100%
34 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-07-22 10:34 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-07-22 10:34 +0000
1# Copyright (c) 2025 Jebel Quant Research
2#
3# Licensed under the MIT License. See the LICENSE file in the project root
4# for the full license text.
5"""Box constraints for optimization variables.
7This module provides the :class:`Bounds` class, which tracks lower and upper
8bound constraints for a named group of variables. It works for any bounded
9quantity — portfolio weights, factor exposures, sector allocations, etc.
11Example:
12 >>> import numpy as np
13 >>> from cvx.core.bounds import Bounds
14 >>> bounds = Bounds(m=3, name="assets")
15 >>> bounds.update(
16 ... lower_assets=np.array([0.0, 0.1, 0.0]),
17 ... upper_assets=np.array([0.5, 0.4, 0.3])
18 ... )
19 >>> lb, ub = bounds.get_bounds()
20 >>> lb
21 array([0. , 0.1, 0. ])
22 >>> ub
23 array([0.5, 0.4, 0.3])
25"""
27from __future__ import annotations
29from dataclasses import dataclass
30from typing import Any
32import numpy as np
34from cvx.core.model import Model
35from cvx.core.parameter import Parameter
38@dataclass
39class Bounds(Model):
40 """Box constraints for a named group of optimization variables.
42 Stores lower and upper bounds as :class:`~cvx.core.parameter.Parameter`
43 objects so they can be updated between solves without rebuilding the
44 problem structure. The ``name`` attribute identifies the variable group
45 (e.g. ``"assets"``, ``"factors"``); bound keys are derived as
46 ``lower_{name}`` / ``upper_{name}``.
48 Attributes:
49 m: Capacity — maximum number of variables in the group.
50 name: Label for the variable group, used to form parameter key names.
52 Example:
53 >>> import numpy as np
54 >>> from cvx.core.bounds import Bounds
55 >>> bounds = Bounds(m=5, name="assets")
56 >>> bounds.update(
57 ... lower_assets=np.array([0.0, 0.0, 0.1]),
58 ... upper_assets=np.array([0.5, 0.5, 0.4])
59 ... )
60 >>> bounds.parameter["lower_assets"].value[:3]
61 array([0. , 0. , 0.1])
62 >>> bounds.parameter["upper_assets"].value[:3]
63 array([0.5, 0.5, 0.4])
65 Any variable group name works:
67 >>> factor_bounds = Bounds(m=3, name="factors")
68 >>> factor_bounds.update(
69 ... lower_factors=np.array([-0.1, -0.2, -0.15]),
70 ... upper_factors=np.array([0.1, 0.2, 0.15])
71 ... )
72 >>> lb, ub = factor_bounds.get_bounds()
73 >>> lb
74 array([-0.1 , -0.2 , -0.15])
75 >>> ub
76 array([0.1 , 0.2 , 0.15])
78 """
80 m: int = 0
81 """Capacity — maximum number of variables."""
83 name: str = ""
84 """Label for the variable group."""
86 def estimate(self, weights: np.ndarray, **kwargs: Any) -> float:
87 """Not implemented — ``Bounds`` only provides constraint data.
89 Args:
90 weights: Ignored.
91 **kwargs: Ignored.
93 Raises:
94 NotImplementedError: Always.
96 Example:
97 >>> import numpy as np
98 >>> from cvx.core.bounds import Bounds
99 >>> bounds = Bounds(m=3, name="assets")
100 >>> try:
101 ... bounds.estimate(np.zeros(3))
102 ... except NotImplementedError:
103 ... print("estimate not implemented for Bounds")
104 estimate not implemented for Bounds
106 """
107 raise NotImplementedError("Bounds does not implement estimate")
109 def _f(self, str_prefix: str) -> str:
110 """Return the parameter key ``{str_prefix}_{name}``.
112 Example:
113 >>> from cvx.core.bounds import Bounds
114 >>> bounds = Bounds(m=3, name="assets")
115 >>> bounds._f("lower")
116 'lower_assets'
117 >>> bounds._f("upper")
118 'upper_assets'
120 """
121 return f"{str_prefix}_{self.name}"
123 def __post_init__(self) -> None:
124 """Create lower (zeros) and upper (ones) bound parameters.
126 Example:
127 >>> from cvx.core.bounds import Bounds
128 >>> bounds = Bounds(m=3, name="assets")
129 >>> bounds.parameter["lower_assets"].shape
130 3
131 >>> bounds.parameter["upper_assets"].shape
132 3
134 """
135 self.parameter[self._f("lower")] = Parameter(
136 shape=self.m,
137 name="lower bound",
138 )
139 self.parameter[self._f("upper")] = Parameter(
140 shape=self.m,
141 name="upper bound",
142 )
143 self.parameter[self._f("upper")].value = np.ones(self.m)
145 def update(self, **kwargs: Any) -> None:
146 """Update bound parameters from keyword arguments.
148 Input arrays shorter than ``m`` are zero-padded on the right.
150 Args:
151 **kwargs: Must contain ``lower_{name}`` and ``upper_{name}`` keys
152 with numpy arrays of length ≤ ``m``.
154 Raises:
155 ValueError: If a required key is missing or an array is longer
156 than ``m``.
158 Example:
159 >>> import numpy as np
160 >>> from cvx.core.bounds import Bounds
161 >>> bounds = Bounds(m=5, name="assets")
162 >>> bounds.update(
163 ... lower_assets=np.array([0.0, 0.1, 0.2]),
164 ... upper_assets=np.array([0.5, 0.4, 0.3])
165 ... )
166 >>> bounds.parameter["lower_assets"].value[:3]
167 array([0. , 0.1, 0.2])
169 """
170 for key in (self._f("lower"), self._f("upper")):
171 if key not in kwargs:
172 msg = f"update() requires a '{key}' argument"
173 raise ValueError(msg)
174 values = kwargs[key]
175 if len(values) > self.m:
176 msg = f"'{key}' has length {len(values)} but the maximum is {self.m}"
177 raise ValueError(msg)
178 arr = np.zeros(self.m)
179 arr[: len(values)] = values
180 self.parameter[key].value = arr
182 def get_bounds(self) -> tuple[np.ndarray, np.ndarray]:
183 """Return ``(lower, upper)`` bound arrays of length ``m``.
185 Example:
186 >>> import numpy as np
187 >>> from cvx.core.bounds import Bounds
188 >>> bounds = Bounds(m=3, name="assets")
189 >>> bounds.update(
190 ... lower_assets=np.array([0.1, 0.2, 0.0]),
191 ... upper_assets=np.array([0.6, 0.7, 0.5])
192 ... )
193 >>> lb, ub = bounds.get_bounds()
194 >>> lb
195 array([0.1, 0.2, 0. ])
196 >>> ub
197 array([0.6, 0.7, 0.5])
199 """
200 return (
201 self.parameter[self._f("lower")].value.copy(),
202 self.parameter[self._f("upper")].value.copy(),
203 )