Coverage for src / cvx / core / bounds.py: 100%
31 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-13 06:46 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-13 06:46 +0000
1# Copyright 2023 Stanford University Convex Optimization Group
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Box constraints for optimization variables.
16This module provides the :class:`Bounds` class, which tracks lower and upper
17bound constraints for a named group of variables. It works for any bounded
18quantity — portfolio weights, factor exposures, sector allocations, etc.
20Example:
21 >>> import numpy as np
22 >>> from cvx.core.bounds import Bounds
23 >>> bounds = Bounds(m=3, name="assets")
24 >>> bounds.update(
25 ... lower_assets=np.array([0.0, 0.1, 0.0]),
26 ... upper_assets=np.array([0.5, 0.4, 0.3])
27 ... )
28 >>> lb, ub = bounds.get_bounds()
29 >>> lb
30 array([0. , 0.1, 0. ])
31 >>> ub
32 array([0.5, 0.4, 0.3])
34"""
36from __future__ import annotations
38from dataclasses import dataclass
39from typing import Any
41import numpy as np
43from cvx.core.model import Model
44from cvx.core.parameter import Parameter
47@dataclass
48class Bounds(Model):
49 """Box constraints for a named group of optimization variables.
51 Stores lower and upper bounds as :class:`~cvx.core.parameter.Parameter`
52 objects so they can be updated between solves without rebuilding the
53 problem structure. The ``name`` attribute identifies the variable group
54 (e.g. ``"assets"``, ``"factors"``); bound keys are derived as
55 ``lower_{name}`` / ``upper_{name}``.
57 Attributes:
58 m: Capacity — maximum number of variables in the group.
59 name: Label for the variable group, used to form parameter key names.
61 Example:
62 >>> import numpy as np
63 >>> from cvx.core.bounds import Bounds
64 >>> bounds = Bounds(m=5, name="assets")
65 >>> bounds.update(
66 ... lower_assets=np.array([0.0, 0.0, 0.1]),
67 ... upper_assets=np.array([0.5, 0.5, 0.4])
68 ... )
69 >>> bounds.parameter["lower_assets"].value[:3]
70 array([0. , 0. , 0.1])
71 >>> bounds.parameter["upper_assets"].value[:3]
72 array([0.5, 0.5, 0.4])
74 Any variable group name works:
76 >>> factor_bounds = Bounds(m=3, name="factors")
77 >>> factor_bounds.update(
78 ... lower_factors=np.array([-0.1, -0.2, -0.15]),
79 ... upper_factors=np.array([0.1, 0.2, 0.15])
80 ... )
81 >>> lb, ub = factor_bounds.get_bounds()
82 >>> lb
83 array([-0.1 , -0.2 , -0.15])
84 >>> ub
85 array([0.1 , 0.2 , 0.15])
87 """
89 m: int = 0
90 """Capacity — maximum number of variables."""
92 name: str = ""
93 """Label for the variable group."""
95 def estimate(self, weights: np.ndarray, **kwargs: Any) -> float:
96 """Not implemented — ``Bounds`` only provides constraint data.
98 Args:
99 weights: Ignored.
100 **kwargs: Ignored.
102 Raises:
103 NotImplementedError: Always.
105 Example:
106 >>> import numpy as np
107 >>> from cvx.core.bounds import Bounds
108 >>> bounds = Bounds(m=3, name="assets")
109 >>> try:
110 ... bounds.estimate(np.zeros(3))
111 ... except NotImplementedError:
112 ... print("estimate not implemented for Bounds")
113 estimate not implemented for Bounds
115 """
116 raise NotImplementedError("Bounds does not implement estimate")
118 def _f(self, str_prefix: str) -> str:
119 """Return the parameter key ``{str_prefix}_{name}``.
121 Example:
122 >>> from cvx.core.bounds import Bounds
123 >>> bounds = Bounds(m=3, name="assets")
124 >>> bounds._f("lower")
125 'lower_assets'
126 >>> bounds._f("upper")
127 'upper_assets'
129 """
130 return f"{str_prefix}_{self.name}"
132 def __post_init__(self) -> None:
133 """Create lower (zeros) and upper (ones) bound parameters.
135 Example:
136 >>> from cvx.core.bounds import Bounds
137 >>> bounds = Bounds(m=3, name="assets")
138 >>> bounds.parameter["lower_assets"].shape
139 3
140 >>> bounds.parameter["upper_assets"].shape
141 3
143 """
144 self.parameter[self._f("lower")] = Parameter(
145 shape=self.m,
146 name="lower bound",
147 )
148 self.parameter[self._f("upper")] = Parameter(
149 shape=self.m,
150 name="upper bound",
151 )
152 self.parameter[self._f("upper")].value = np.ones(self.m)
154 def update(self, **kwargs: Any) -> None:
155 """Update bound parameters from keyword arguments.
157 Input arrays shorter than ``m`` are zero-padded on the right.
159 Args:
160 **kwargs: Must contain ``lower_{name}`` and ``upper_{name}`` keys
161 with numpy arrays of length ≤ ``m``.
163 Example:
164 >>> import numpy as np
165 >>> from cvx.core.bounds import Bounds
166 >>> bounds = Bounds(m=5, name="assets")
167 >>> bounds.update(
168 ... lower_assets=np.array([0.0, 0.1, 0.2]),
169 ... upper_assets=np.array([0.5, 0.4, 0.3])
170 ... )
171 >>> bounds.parameter["lower_assets"].value[:3]
172 array([0. , 0.1, 0.2])
174 """
175 lower = kwargs[self._f("lower")]
176 lower_arr = np.zeros(self.m)
177 lower_arr[: len(lower)] = lower
178 self.parameter[self._f("lower")].value = lower_arr
180 upper = kwargs[self._f("upper")]
181 upper_arr = np.zeros(self.m)
182 upper_arr[: len(upper)] = upper
183 self.parameter[self._f("upper")].value = upper_arr
185 def get_bounds(self) -> tuple[np.ndarray, np.ndarray]:
186 """Return ``(lower, upper)`` bound arrays of length ``m``.
188 Example:
189 >>> import numpy as np
190 >>> from cvx.core.bounds import Bounds
191 >>> bounds = Bounds(m=3, name="assets")
192 >>> bounds.update(
193 ... lower_assets=np.array([0.1, 0.2, 0.0]),
194 ... upper_assets=np.array([0.6, 0.7, 0.5])
195 ... )
196 >>> lb, ub = bounds.get_bounds()
197 >>> lb
198 array([0.1, 0.2, 0. ])
199 >>> ub
200 array([0.6, 0.7, 0.5])
202 """
203 return (
204 self.parameter[self._f("lower")].value.copy(),
205 self.parameter[self._f("upper")].value.copy(),
206 )