Coverage for gpkit/nomials/map.py: 100%

122 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-07 22:56 -0500

1"Implements the NomialMap class" 

2from collections import defaultdict 

3import numpy as np 

4from .. import units 

5from ..exceptions import DimensionalityError 

6from ..small_classes import HashVector, Strings, EMPTY_HV 

7from ..units import qty 

8from .substitution import parse_subs 

9 

10DIMLESS_QUANTITY = qty("dimensionless") 

11 

12 

13class NomialMap(HashVector): 

14 """Class for efficent algebraic represention of a nomial 

15 

16 A NomialMap is a mapping between hashvectors representing exponents 

17 and their coefficients in a posynomial. 

18 

19 For example, {{x : 1}: 2.0, {y: 1}: 3.0} represents 2*x + 3*y, where 

20 x and y are VarKey objects. 

21 """ 

22 units = None 

23 expmap = None # used for monomial-mapping postsubstitution; see .mmap() 

24 csmap = None # used for monomial-mapping postsubstitution; see .mmap() 

25 

26 def copy(self): 

27 "Return a copy of this" 

28 return self.__class__(self) 

29 

30 def units_of_product(self, thing, thing2=None): 

31 "Sets units to those of `thing*thing2`. Ugly optimized code." 

32 if thing is None and thing2 is None: 

33 self.units = None 

34 elif hasattr(thing, "units"): 

35 if hasattr(thing2, "units"): 

36 self.units, dimless_convert = units.of_product(thing, thing2) 

37 if dimless_convert: 

38 for key in self: 

39 self[key] *= dimless_convert 

40 else: 

41 self.units = qty(thing.units) 

42 elif hasattr(thing2, "units"): 

43 self.units = qty(thing2.units) 

44 elif thing2 is None and isinstance(thing, Strings): 

45 self.units = qty(thing) 

46 else: 

47 self.units = None 

48 

49 def to(self, to_units): 

50 "Returns a new NomialMap of the given units" 

51 sunits = self.units or DIMLESS_QUANTITY 

52 nm = self * sunits.to(to_units).magnitude # note that * creates a copy 

53 nm.units_of_product(to_units) # pylint: disable=no-member 

54 return nm 

55 

56 def __add__(self, other): 

57 "Adds NomialMaps together" 

58 if self.units != other.units: 

59 try: 

60 other *= float(other.units/self.units) 

61 except (TypeError, AttributeError) as exc: # one of those is None 

62 raise DimensionalityError(self.units, other.units) from exc 

63 hmap = HashVector.__add__(self, other) 

64 hmap.units = self.units 

65 return hmap 

66 

67 def diff(self, varkey): 

68 "Differentiates a NomialMap with respect to a varkey" 

69 out = NomialMap() 

70 out.units_of_product(self.units, 

71 1/varkey.units if varkey.units else None) 

72 for exp in self: 

73 if varkey in exp: 

74 x = exp[varkey] 

75 c = self[exp] * x 

76 exp = exp.copy() 

77 if x == 1: 

78 exp.hashvalue ^= hash((varkey, 1)) 

79 del exp[varkey] 

80 else: 

81 exp.hashvalue ^= hash((varkey, x)) ^ hash((varkey, x-1)) 

82 exp[varkey] = x-1 

83 out[exp] = c 

84 return out 

85 

86 def sub(self, substitutions, varkeys, parsedsubs=False): 

87 """Applies substitutions to a NomialMap 

88 

89 Parameters 

90 ---------- 

91 substitutions : (dict-like) 

92 list of substitutions to perform 

93 

94 varkeys : (set-like) 

95 varkeys that are present in self 

96 (required argument so as to require efficient code) 

97 

98 parsedsubs : bool 

99 flag if the substitutions have already been parsed 

100 to contain only keys in varkeys 

101 

102 """ 

103 # pylint: disable=too-many-locals, too-many-branches 

104 if parsedsubs or not substitutions: 

105 fixed = substitutions 

106 else: 

107 fixed, _, _ = parse_subs(varkeys, substitutions) 

108 

109 if not fixed: 

110 if not self.expmap: 

111 self.expmap, self.csmap = {exp: exp for exp in self}, {} 

112 return self 

113 

114 cp = NomialMap() 

115 cp.units = self.units 

116 # csmap is modified during substitution, but keeps the same exps 

117 cp.expmap, cp.csmap = {}, self.copy() 

118 varlocs = defaultdict(set) 

119 for exp, c in self.items(): 

120 new_exp = exp.copy() 

121 cp.expmap[exp] = new_exp # cp modifies exps, so it needs new ones 

122 cp[new_exp] = c 

123 for vk in new_exp: 

124 if vk in fixed: 

125 varlocs[vk].add((exp, new_exp)) 

126 

127 squished = set() 

128 for vk in varlocs: 

129 exps, cval = varlocs[vk], fixed[vk] 

130 if hasattr(cval, "hmap"): 

131 if cval.hmap is None or any(cval.hmap.keys()): 

132 raise ValueError("Monomial substitutions are not" 

133 " supported.") 

134 cval, = cval.hmap.to(vk.units or DIMLESS_QUANTITY).values() 

135 elif hasattr(cval, "to"): 

136 cval = cval.to(vk.units or DIMLESS_QUANTITY).magnitude 

137 for o_exp, exp in exps: 

138 subinplace(cp, exp, o_exp, vk, cval, squished) 

139 return cp 

140 

141 def mmap(self, orig): 

142 """Maps substituted monomials back to the original nomial 

143 

144 self.expmap is the map from pre- to post-substitution exponents, and 

145 takes the form {original_exp: new_exp} 

146 

147 self.csmap is the map from pre-substitution exponents to coefficients. 

148 

149 m_from_ms is of the form {new_exp: [old_exps, ]} 

150 

151 pmap is of the form [{orig_idx1: fraction1, orig_idx2: fraction2, }, ] 

152 where at the index corresponding to each new_exp is a dictionary 

153 mapping the indices corresponding to the old exps to their 

154 fraction of the post-substitution coefficient 

155 """ 

156 pmap = [{} for _ in self] 

157 origexps = list(orig.keys()) 

158 selfexps = list(self.keys()) 

159 for orig_exp, self_exp in self.expmap.items(): 

160 if self_exp not in self: # can occur in tautological constraints 

161 continue # after substitution 

162 fraction = self.csmap.get(orig_exp, orig[orig_exp])/self[self_exp] 

163 orig_idx = origexps.index(orig_exp) 

164 pmap[selfexps.index(self_exp)][orig_idx] = fraction 

165 return pmap 

166 

167 

168# pylint: disable=invalid-name, too-many-arguments 

169def subinplace(cp, exp, o_exp, vk, cval, squished): 

170 "Modifies cp by substituing cval/expval for vk in exp" 

171 x = exp[vk] 

172 powval = float(cval)**x if cval != 0 or x >= 0 else np.sign(cval)*np.inf 

173 cp.csmap[o_exp] *= powval 

174 if exp in cp: 

175 c = cp.pop(exp) 

176 exp.hashvalue ^= hash((vk, x)) # remove (key, value) from hashvalue 

177 del exp[vk] 

178 value = powval * c 

179 if exp in cp: 

180 squished.add(exp.copy()) 

181 currentvalue = cp[exp] 

182 if value != -currentvalue: 

183 cp[exp] += value 

184 else: 

185 del cp[exp] # remove zeros created during substitution 

186 elif value: 

187 cp[exp] = value 

188 if not cp: # make sure it's never an empty hmap 

189 cp[EMPTY_HV] = 0.0 

190 elif exp in squished: 

191 exp.hashvalue ^= hash((vk, x)) # remove (key, value) from hashvalue 

192 del exp[vk]