Coverage for picos/expressions/mutable.py: 91.11%

90 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-26 07:46 +0000

1# ------------------------------------------------------------------------------ 

2# Copyright (C) 2019-2020 Maximilian Stahlberg 

3# Based on the original picos.expressions module by Guillaume Sagnol. 

4# 

5# This file is part of PICOS. 

6# 

7# PICOS is free software: you can redistribute it and/or modify it under the 

8# terms of the GNU General Public License as published by the Free Software 

9# Foundation, either version 3 of the License, or (at your option) any later 

10# version. 

11# 

12# PICOS is distributed in the hope that it will be useful, but WITHOUT ANY 

13# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 

14# A PARTICULAR PURPOSE. See the GNU General Public License for more details. 

15# 

16# You should have received a copy of the GNU General Public License along with 

17# this program. If not, see <http://www.gnu.org/licenses/>. 

18# ------------------------------------------------------------------------------ 

19 

20"""Implements the :class:`Mutable` base class for variables and parameters.""" 

21 

22import random 

23import threading 

24from abc import ABC, abstractmethod 

25from copy import copy as pycopy 

26 

27from .. import glyphs 

28from ..apidoc import api_end, api_start 

29from ..caching import cached_property 

30from .data import load_data 

31from .expression import Expression, NotValued 

32 

33_API_START = api_start(globals()) 

34# ------------------------------- 

35 

36 

37# The mutable IDs start at a random value to prevent a clash if mutables are 

38# pickled and loaded in another python session. The choice of 31 bits is to 

39# produce up to 2**31-1 short hexadecimal IDs for use in private mutable names. 

40_NEXT_MUTABLE_ID = int(random.getrandbits(31)) 

41 

42# A lock for _NEXT_MUTABLE_ID, if the user uses threads to create mutables. 

43_MUTABLE_ID_LOCK = threading.Lock() 

44 

45 

46def _make_mutable_id(dim): 

47 """Create a unique (starting) ID for a new mutable.""" 

48 with _MUTABLE_ID_LOCK: 

49 global _NEXT_MUTABLE_ID 

50 id = _NEXT_MUTABLE_ID 

51 _NEXT_MUTABLE_ID += dim 

52 return id 

53 

54 

55# A map used when converting IDs to strings. 

56_ID_HEX2STR_MAP = {ord(a): ord(b) for a, b in zip( 

57 "0123456789abcdef", "ABCDEFGHIJKLMNOP")} 

58 

59 

60def _id_to_str(id): 

61 """Convert a mutable id to a short string identifier.""" 

62 return "{:08x}".format(id).translate(_ID_HEX2STR_MAP) 

63 

64 

65class Mutable(ABC): 

66 """Primary base class for all variable and parameter types. 

67 

68 Mutables need to inherit this class with priority (first class listed) and 

69 the affine expression type that they represent without priority. 

70 """ 

71 

72 def __init__(self, name, vectorization): 

73 """Perform basic initialization for :class:`Mutable` instances. 

74 

75 :param str name: 

76 Name of the mutable. A leading `"__"` denotes a private mutable 

77 and is replaced by a sequence containing the mutable's unique ID. 

78 

79 :param vectorization: 

80 Vectorization format used to store the value. 

81 :type vectorization: 

82 ~picos.expressions.vectorizations.BaseVectorization 

83 """ 

84 if not isinstance(self, Expression): 

85 raise TypeError("{} may not be initialized directly.".format( 

86 type(self).__name__)) 

87 

88 self._id = _make_mutable_id(vectorization.dim) 

89 self._vec = vectorization 

90 self._value = None 

91 

92 if not name or name.startswith("__"): 

93 id_str = "__{}_".format(_id_to_str(self._id)) 

94 name = name.replace("__", id_str, 1) if name else id_str + "_" 

95 

96 self._name = name 

97 

98 @abstractmethod 

99 def copy(self, new_name=None): 

100 """Return an independent copy of the mutable. 

101 

102 Note that unlike constraints which keep their ID on copy, mutables are 

103 supposed to receive a new id. 

104 """ 

105 pass 

106 

107 @property 

108 def id(self): 

109 """The unique (starting) ID of the mutable, assigned at creation.""" 

110 return self._id 

111 

112 def id_at(self, index): 

113 """Return the unique ID of a scalar entry, assigned at creation.""" 

114 if index < 0 or index >= self._vec.dim: 

115 raise IndexError("Bad index {} for the {} dimensional mutable {}." 

116 .format(index, self._vec.dim, self.name)) 

117 

118 return self._id + index 

119 

120 @property 

121 def dim(self): 

122 """The mutable's dimension on the real field. 

123 

124 This corresponds to the length of its vectorized value. 

125 """ 

126 return self._vec.dim 

127 

128 @property 

129 def name(self): 

130 """The name of the mutable.""" 

131 return self._name 

132 

133 @cached_property 

134 def long_string(self): 

135 """A string used to represent the mutable in a problem string.""" 

136 return "{} {} {}".format(glyphs.shape(self.shape), 

137 self._get_type_string_base().lower(), self._name) 

138 

139 def _load_vectorized(self, value): 

140 """Support :meth:`__init__` and ``_set_value``.""" 

141 return self._vec.vectorize( 

142 load_data(value, self._vec.shape, self._typecode)[0]) 

143 

144 def _check_internal_value(self, value): 

145 """Support :meth:`_set_value` and :meth:`_set_internal_value`.""" 

146 pass 

147 

148 # NOTE: This needs to be inherited with priority. 

149 def _get_value(self): 

150 return self._vec.devectorize(self._get_internal_value()) 

151 

152 # NOTE: This needs to be inherited with priority. 

153 def _set_value(self, value): 

154 if value is None: 

155 self._value = None 

156 return 

157 

158 try: 

159 value = self._load_vectorized(value) 

160 self._check_internal_value(value) 

161 except Exception as error: 

162 raise type(error)("Failed to assign a value to mutable {}: {}" 

163 .format(self.string, error)) from None 

164 else: 

165 self._value = value 

166 

167 def _get_internal_value(self): 

168 if self._value is None: 

169 raise NotValued("Mutable {} is not valued.".format(self.string)) 

170 else: 

171 return pycopy(self._value) 

172 

173 def _set_internal_value(self, value): 

174 if value is None: 

175 self._value = None 

176 return 

177 

178 try: 

179 value = load_data(value, (self._vec.dim, 1), "d")[0] 

180 self._check_internal_value(value) 

181 except Exception as error: 

182 raise type(error)( 

183 "Failed to assign an internal value to mutable {}: {}" 

184 .format(self.string, error)) from None 

185 else: 

186 self._value = value 

187 

188 internal_value = property( 

189 lambda self: self._get_internal_value(), 

190 lambda self, value: self._set_internal_value(value), 

191 lambda self: self._set_internal_value(None), 

192 """The internal (special vectorized) value of the mutable.""") 

193 

194 def _get_clstype(self): 

195 # Mimic an ordinary (complex) affine expression. 

196 return self._basetype 

197 

198 def _get_subtype(self): 

199 # Mimic an ordinary (complex) affine expression. 

200 return self._basetype._get_subtype(self) 

201 

202 @classmethod 

203 def _predict(cls, subtype, relation, other): 

204 # Mimic an ordinary (complex) affine expression. 

205 return cls._get_basetype()._predict(subtype, relation, other) 

206 

207 

208# -------------------------------------- 

209__all__ = api_end(_API_START, globals())