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

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. 

15 

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. 

19 

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]) 

33 

34""" 

35 

36from __future__ import annotations 

37 

38from dataclasses import dataclass 

39from typing import Any 

40 

41import numpy as np 

42 

43from cvx.core.model import Model 

44from cvx.core.parameter import Parameter 

45 

46 

47@dataclass 

48class Bounds(Model): 

49 """Box constraints for a named group of optimization variables. 

50 

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}``. 

56 

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. 

60 

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]) 

73 

74 Any variable group name works: 

75 

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]) 

86 

87 """ 

88 

89 m: int = 0 

90 """Capacity — maximum number of variables.""" 

91 

92 name: str = "" 

93 """Label for the variable group.""" 

94 

95 def estimate(self, weights: np.ndarray, **kwargs: Any) -> float: 

96 """Not implemented — ``Bounds`` only provides constraint data. 

97 

98 Args: 

99 weights: Ignored. 

100 **kwargs: Ignored. 

101 

102 Raises: 

103 NotImplementedError: Always. 

104 

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 

114 

115 """ 

116 raise NotImplementedError("Bounds does not implement estimate") 

117 

118 def _f(self, str_prefix: str) -> str: 

119 """Return the parameter key ``{str_prefix}_{name}``. 

120 

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' 

128 

129 """ 

130 return f"{str_prefix}_{self.name}" 

131 

132 def __post_init__(self) -> None: 

133 """Create lower (zeros) and upper (ones) bound parameters. 

134 

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 

142 

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) 

153 

154 def update(self, **kwargs: Any) -> None: 

155 """Update bound parameters from keyword arguments. 

156 

157 Input arrays shorter than ``m`` are zero-padded on the right. 

158 

159 Args: 

160 **kwargs: Must contain ``lower_{name}`` and ``upper_{name}`` keys 

161 with numpy arrays of length ≤ ``m``. 

162 

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]) 

173 

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 

179 

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 

184 

185 def get_bounds(self) -> tuple[np.ndarray, np.ndarray]: 

186 """Return ``(lower, upper)`` bound arrays of length ``m``. 

187 

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]) 

201 

202 """ 

203 return ( 

204 self.parameter[self._f("lower")].value.copy(), 

205 self.parameter[self._f("upper")].value.copy(), 

206 )