"""
Module for optimization Var.
"""
import logging
from typing import Optional, Union
from collections import OrderedDict
import numpy as np
from andes.core.common import Config
import cvxpy as cp
from ams.utils import pretty_long_message
from ams.shared import _prefix, _max_length
from ams.opt import OptzBase, ensure_symbols, ensure_mats_and_parsed
logger = logging.getLogger(__name__)
[docs]
class Var(OptzBase):
"""
Base class for variables used in a routine.
When `horizon` is provided, the variable will be expanded to a matrix,
where rows are indexed by the source variable index and columns are
indexed by the horizon index.
Parameters
----------
info : str, optional
Descriptive information
unit : str, optional
Unit
tex_name : str
LaTeX-formatted variable symbol. Defaults to the value of ``name``.
name : str, optional
Variable name. One should typically assigning the name directly because
it will be automatically assigned by the model. The value of ``name``
will be the symbol name to be used in expressions.
src : str, optional
Source variable name. Defaults to the value of ``name``.
model : str, optional
Name of the owner model or group.
horizon : ams.routines.RParam, optional
Horizon idx.
nonneg : bool, optional
Non-negative variable
nonpos : bool, optional
Non-positive variable
cplx : bool, optional
Complex variable
imag : bool, optional
Imaginary variable
symmetric : bool, optional
Symmetric variable
diag : bool, optional
Diagonal variable
psd : bool, optional
Positive semi-definite variable
nsd : bool, optional
Negative semi-definite variable
hermitian : bool, optional
Hermitian variable
boolean : bool, optional
Boolean variable
integer : bool, optional
Integer variable
pos : bool, optional
Positive variable
neg : bool, optional
Negative variable
Attributes
----------
a : np.ndarray
Variable address.
_v : np.ndarray
Local-storage of the variable value.
rtn : ams.routines.Routine
The owner routine instance.
"""
[docs]
def __init__(self,
name: Optional[str] = None,
tex_name: Optional[str] = None,
info: Optional[str] = None,
src: Optional[str] = None,
unit: Optional[str] = None,
model: Optional[str] = None,
shape: Optional[Union[tuple, int]] = None,
v0: Optional[str] = None,
horizon: Optional[str] = None,
nonneg: Optional[bool] = False,
nonpos: Optional[bool] = False,
cplx: Optional[bool] = False,
imag: Optional[bool] = False,
symmetric: Optional[bool] = False,
diag: Optional[bool] = False,
psd: Optional[bool] = False,
nsd: Optional[bool] = False,
hermitian: Optional[bool] = False,
boolean: Optional[bool] = False,
integer: Optional[bool] = False,
pos: Optional[bool] = False,
neg: Optional[bool] = False,
):
OptzBase.__init__(self, name=name, info=info, unit=unit, model=model)
self.tex_name = tex_name if tex_name else name
self.id = None
self.src = src
self.v0 = v0
self.horizon = horizon
self._shape = shape
self._v = None
self.a: np.ndarray = np.array([], dtype=int)
self.config = Config(name=self.class_name) # `config` that can be exported
self.config.add(OrderedDict((('nonneg', nonneg),
('nonpos', nonpos),
('complex', cplx),
('imag', imag),
('symmetric', symmetric),
('diag', diag),
('psd', psd),
('nsd', nsd),
('hermitian', hermitian),
('boolean', boolean),
('integer', integer),
('pos', pos),
('neg', neg),
)))
@property
def v(self):
"""
Return the CVXPY variable value.
"""
if self.optz is None:
return None
if self.optz.value is None:
try:
shape = self.optz.shape
return np.zeros(shape)
except AttributeError:
return None
else:
return self.optz.value
@v.setter
def v(self, value):
if self.optz is None:
logger.info(f"Variable <{self.name}> is not initialized yet.")
else:
self.optz.value = value
def _resolve_shape(self):
"""Resolve the variable shape from `owner`/`horizon`/`_shape`."""
if self.owner is not None:
nr = self.owner.n
if self.horizon:
return (nr, int(self.horizon.n))
return (nr,)
if isinstance(self._shape, int):
return (self._shape,)
if isinstance(self._shape, tuple):
return self._shape
raise ValueError(f"Invalid shape {self._shape}.")
@ensure_symbols
def parse(self):
"""
Parse the variable.
Var construction is fully AMS-controlled — there's no user-supplied
``e_str`` to rewrite — so parse is a no-op kept for OModel lifecycle
symmetry. Shape resolution and ``cp.Variable`` construction happen
in :meth:`evaluate`.
"""
return True
@ensure_mats_and_parsed
def evaluate(self):
"""
Evaluate the variable.
Returns
-------
bool
Returns True if the evaluation is successful, False otherwise.
"""
# NOTE: cvxpy Variable accepts attribute names with specific casing
config = {}
for k, v in self.config.as_dict().items():
if k == 'psd':
config['PSD'] = v
elif k == 'nsd':
config['NSD'] = v
else:
config[k] = v
shape = self._resolve_shape()
msg = f" - Var <{self.name}>: cp.Variable({shape}, **{config})"
logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length))
try:
self.optz = cp.Variable(shape, **config)
except Exception as e:
raise Exception(f"Error in evaluating Var <{self.name}>.\n{e}")
return True
def __repr__(self):
return f'{self.__class__.__name__}: {self.owner.__class__.__name__}.{self.name}'