Coverage for src/cvx/core/model.py: 100%
24 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"""Abstract parametric model with named numpy-array parameters.
7This module provides the :class:`Model` abstract base class for any
8parametric optimization model whose data can be stored as named
9:class:`~cvx.core.parameter.Parameter` objects and updated independently
10of the problem structure.
12Example:
13 Concrete subclasses implement ``estimate`` and ``update``:
15 >>> import numpy as np
16 >>> from cvx.risk.sample import SampleCovariance
17 >>> model = SampleCovariance(num=3)
18 >>> model.update(
19 ... cov=np.eye(3),
20 ... lower_assets=np.zeros(3),
21 ... upper_assets=np.ones(3)
22 ... )
23 >>> isinstance(model.estimate(np.ones(3) / 3), float)
24 True
26"""
28from __future__ import annotations
30from abc import ABC, abstractmethod
31from collections.abc import Callable
32from dataclasses import dataclass, field
33from typing import TYPE_CHECKING, Any
35import numpy as np
37from cvx.core.parameter import Parameter
39if TYPE_CHECKING:
40 from cvx.core.conic import ConeProgramBuilder
41 from cvx.core.variable import Variable
44@dataclass
45class Model(ABC):
46 """Abstract base class for parametric optimization models.
48 A ``Model`` holds a dictionary of named :class:`~cvx.core.parameter.Parameter`
49 objects (numpy arrays) that can be updated between solver calls without
50 reconstructing the optimization problem structure. Subclasses implement
51 :meth:`estimate` to evaluate the model output and :meth:`update` to refresh
52 the parameter values.
54 Attributes:
55 parameter: Dictionary of named :class:`~cvx.core.parameter.Parameter`
56 objects. Parameters can be updated independently of the problem
57 structure, making it cheap to solve a sequence of related problems.
59 Example:
60 >>> import numpy as np
61 >>> from cvx.risk.sample import SampleCovariance
62 >>> model = SampleCovariance(num=2)
63 >>> model.update(
64 ... cov=np.array([[1.0, 0.5], [0.5, 2.0]]),
65 ... lower_assets=np.zeros(2),
66 ... upper_assets=np.ones(2)
67 ... )
68 >>> 'chol' in model.parameter
69 True
71 Parameters are :class:`~cvx.core.parameter.Parameter` instances:
73 >>> from cvx.core.parameter import Parameter
74 >>> isinstance(model.parameter['chol'], Parameter)
75 True
77 """
79 parameter: dict[str, Parameter] = field(default_factory=dict)
80 """Dictionary of named parameters."""
82 @abstractmethod
83 def estimate(self, weights: np.ndarray, **kwargs: Any) -> float:
84 """Evaluate the model for the given input vector.
86 Args:
87 weights: Input vector (e.g. portfolio weights or factor exposures).
88 **kwargs: Additional keyword arguments for subclass-specific logic.
90 Returns:
91 Scalar float result (e.g. risk, cost, or objective contribution).
93 Example:
94 >>> import numpy as np
95 >>> from cvx.risk.sample import SampleCovariance
96 >>> model = SampleCovariance(num=2)
97 >>> model.update(
98 ... cov=np.array([[1.0, 0.0], [0.0, 1.0]]),
99 ... lower_assets=np.zeros(2),
100 ... upper_assets=np.ones(2)
101 ... )
102 >>> isinstance(model.estimate(np.array([0.5, 0.5])), float)
103 True
105 """
107 @abstractmethod
108 def update(self, **kwargs: Any) -> None:
109 """Update the parameter values from keyword arguments.
111 Updating parameters allows the same problem structure to be re-solved
112 with new data without any symbolic re-compilation.
114 Args:
115 **kwargs: New parameter values. The expected keys depend on the
116 concrete subclass.
118 Example:
119 >>> import numpy as np
120 >>> from cvx.risk.sample import SampleCovariance
121 >>> model = SampleCovariance(num=3)
122 >>> model.update(
123 ... cov=np.eye(3),
124 ... lower_assets=np.zeros(3),
125 ... upper_assets=np.ones(3)
126 ... )
128 """
130 def _solve_and_unpack(
131 self,
132 builder: ConeProgramBuilder,
133 q: np.ndarray,
134 weights: Variable,
135 w_cols: slice,
136 result: Callable[[Any], tuple[float, float]],
137 *,
138 y_var: Variable | None = None,
139 y_cols: slice | None = None,
140 ) -> tuple[float | None, float | None, str]:
141 """Solve an assembled cone program and unpack the primal solution.
143 This is the shared tail of every concrete :meth:`solve_minrisk`: it runs
144 the Clarabel solver on the linear objective ``q`` and, on a solved
145 status, copies the optimal asset weights (and, when both ``y_var`` and
146 ``y_cols`` are supplied, the factor exposures) back into the caller's
147 variables. The reported ``(objective, risk)`` pair is derived from the
148 solution by the model-specific ``result`` callback. If the solver does
149 not converge, returns ``(None, None, status)`` and leaves the variables
150 untouched.
152 Args:
153 builder: The cone program with all constraints already added.
154 q: Linear objective coefficients.
155 weights: Variable that receives the optimal asset weights.
156 w_cols: Columns of the solution vector holding the asset weights.
157 result: Maps the solved solution to the ``(objective, risk)`` pair.
158 y_var: Optional variable that receives the optimal factor exposures.
159 y_cols: Columns of the solution vector holding the factor exposures.
161 Returns:
162 ``(objective, risk, status)`` on a solved status, otherwise
163 ``(None, None, status)``.
165 """
166 sol, status = builder.solve(q)
167 if "Solved" not in status:
168 return None, None, status
169 weights.value = np.array(sol.x[w_cols])
170 if y_var is not None and y_cols is not None:
171 y_var.value = np.array(sol.x[y_cols])
172 objective, risk = result(sol)
173 return objective, risk, status